カルマンフィルタの以下の本に例題として matlab によるカルマンフィルタの実装がなされていました。自分の理解を深めるためにも、c++ で実装してみようと思い、本記事を書いています。
カルマンフィルタの基本的な数式
システムの離散時間状態方程式は以下のように与えられているものとします。
上記システムで、変数はそれぞれ以下を表しています。
システムの状態 | |
システムの入力 | |
システムの出力 | |
システム雑音 | |
観測雑音 |
ここで、システム雑音
カルマンフィルタは、以下の2ステップで状態を逐次的に推定していきます。
予測ステップ
フィルタリングステップ
詳細は、「カルマンフィルタの基礎 6.5節 システム制御のためのカルマンフィルタ」をご参照ください。
※多変数を取り扱う形式に一般化したかったため、変数名が参考書籍と若干異なることにご注意ください。「Point 6.7 多変数時系列データに対するカルマンフィルタ」も参考にしています。
例題
モデル

Fig 1: Spring-Mass-Damper system
バネ・マス・ダンパ系を考えます。qiita の記事を参考にしました。カートがバネで壁から繋がれているモデルです。簡単のため、地面と車輪の摩擦は考慮しません。
バネの自然長からの変位を
カートの質量 | |
バネ定数 | |
減衰係数 |
バネ・マス・ダンパ系の運動方程式は、以下のようになります。
状態方程式
状態
ただし、システム行列
今回は簡単のため、
離散化
最後にシステムの離散化表現を考えます。状態方程式の離散化に関しては、この動画を参考にしました。
(1) を離散化した状態方程式は、以下のようになります。
ここで、
今回は、
さらに、システム雑音
ソースコード
ソースコードは github に載せているので、詳しくはこちらをご確認ください。ドキュメントも doxygen により生成しています。
準備
Eigen library
行列演算のため、Eigen ライブラリを PC にダウンロードする必要があります。
Linux / Mac の場合は、ライブラリをコチラからダウンロードしてきて Eigen ディレクトリを /usr/local/include/ に置けば大丈夫です。
On Linux or Mac OS X, another option is to symlink or copy the Eigen folder into
Eigen: Getting started/usr/local/include/
.
gnuplot
グラフを出力するために gnuplot が必要です。ubuntu であれば以下のコマンドで簡単にインストールできます。
sudo apt-get install gnuplot
カルマンフィルタの実装
フィルタリングのプロセスのソースコードを貼り付けておきます。他の部分は github をご参照ください。
Eigen::VectorXf KalmanFilter::filter(const Eigen::VectorXf& pre_x_hat,
const Eigen::VectorXf& pre_u,
const Eigen::VectorXf& cur_y) {
if (!check_filter_matrix_vector_size(pre_x_hat, pre_u, cur_y))
return pre_x_hat;
/// prediction step
Eigen::VectorXf x_hat_ps = A_ * pre_x_hat + B_ * pre_u;
Eigen::MatrixXf P_ps = A_ * P_ * A_.transpose() + Bv_ * Q_ * Bv_.transpose();
/// filtering step
Eigen::MatrixXf G =
P_ps * C_.transpose() * (C_ * P_ps * C_.transpose() + R_).inverse();
Eigen::VectorXf new_x_hat = x_hat_ps + G * (cur_y - C_ * x_hat_ps);
Eigen::MatrixXf I = Eigen::MatrixXf::Identity(A_.cols(), A_.cols());
P_ = (I - G * C_) * P_ps;
return new_x_hat;
}
バネ・マス・ダンパ系の例題
こちらも乱数の生成やグラフプロット用のファイル保存に関するソースコードは省きます。大まかな流れは、以下のようになります。
- 物理パラメータ・システム行列の定義:
initialize()
- 観測値・真値を計算:
simulate_dynamics()
- カルマンフィルタで状態推定:
estimate_states()
initialize()
void SpringMassDamper::initialize() {
/// physical parameter
float m = 1.0;
float k = 1.0;
float c = 1.0;
/// system matrices
Eigen::MatrixXf A(2, 2);
A << 0.0, 1.0, -k / m, -c / m;
Eigen::MatrixXf Ad(2, 2);
Ad = Eigen::MatrixXf::Identity(2, 2) + A * delta_t_;
Eigen::MatrixXf B(2, 1);
B << 0, 1 / m;
Eigen::MatrixXf Bd(2, 1);
Bd = B * delta_t_;
Eigen::MatrixXf Cd(1, 2);
Cd << 1.0, 0.0;
Eigen::MatrixXf Dd(1, 1);
Dd << 0.0;
/// covariance matrices
float gamma = 0.0;
Eigen::Matrix2f P0 = gamma * Eigen::Matrix2f::Identity();
float sigma_v = 1e-2;
Eigen::MatrixXf Q(1, 1); // sigma_v^2
Q << sigma_v * sigma_v;
float sigma_w = 5.0 * 1e-3;
// float sigma_w = 5.0 * 1e-6;
Eigen::MatrixXf R(1, 1); // sigma_w^2
R << sigma_w * sigma_w;
kf_ = std::make_shared<KalmanFilter>(P0, Ad, Bd, Bd, Cd, Dd, Q, R);
simulate_dynamics(Ad, Bd, Cd, Dd);
}
simulate_dynamics()
void SpringMassDamper::simulate_dynamics(const Eigen::MatrixXf& Ad,
const Eigen::MatrixXf& Bd,
const Eigen::MatrixXf& Cd,
const Eigen::MatrixXf& Dd) {
/// parameter for noise and input
float sigma_v = 1e-2;
float sigma_w = 5.0 * 1e-3;
float u_rand_max = 0.5;
/// generate random numbers for v and w
int rand_size = sim_times_ + 1;
std::vector<float> rand_nd_v = generate_rand_nd(rand_size);
std::vector<float> rand_nd_w = generate_rand_nd(rand_size);
/// initialize setting for random number u
std::srand(2);
// define 1-dimentional vector as follows
Eigen::Matrix<float, 1, 1> v(sigma_v * rand_nd_v[0]);
Eigen::Matrix<float, 1, 1> w(sigma_w * rand_nd_w[0]);
Eigen::Vector2f x(0, 0);
Eigen::Matrix<float, 1, 1> u(u_rand_max * get_rand_m1p1());
Eigen::Matrix<float, 1, 1> y = Cd * x + Dd * u + w;
x_v_.push_back(x);
u_v_.push_back(u);
y_v_.push_back(y);
for (int i = 0; i < sim_times_; i++) {
/// generate noise
v(0, 0) = sigma_v * rand_nd_v[i + 1];
w(0, 0) = sigma_w * rand_nd_w[i + 1];
/// generate input
u(0, 0) = u_rand_max * get_rand_m1p1();
/// calculate states
x = Ad * x + Bd * u + Bd * v;
y = Cd * x + Dd * u + w;
x_v_.push_back(x);
y_v_.push_back(y);
u_v_.push_back(u);
}
}
estimate_states()
void SpringMassDamper::estimate_states() {
Eigen::Vector2f x_hat(0, 0);
x_hat_v_.push_back(x_hat);
/// calculate filtered value
for (int i = 1; i < sim_times_; i++) {
x_hat = kf_->filter(x_hat, u_v_[i - 1], y_v_[i]);
x_hat_v_.push_back(x_hat);
}
}
参考
- doxygen
- Eigen ライブラリ
- 1次元の Vector を定義するための方法(参考)
- c++ で正規分布に従った乱数の生成(参考)
- c++ と gnuplot でデータをプロットする(参考)
- ユニットテスト
- テストコードで、private メンバにアクセスする(参考)
シミュレーション
設定
初期状態や物理パラメータを以下のように定めて、シミュレーションを行います。
初期状態 | |||
初期推定値 | |||
共分散行列の初期値 | |||
質量 | |||
バネ定数 | |||
減衰係数 | |||
システム雑音の標準偏差 | |||
観測雑音の標準偏差 | |||
制御入力乱数の最大値 | |||
シミュレーション間隔 | |||
シミュレーション時間 |
カルマンフィルタの共分散行列の初期値
今回は
システム雑音
※カルマンフィルタは正規性白色雑音で駆動されるシステムを仮定しますが、このシミュレーション例だと白色性は保証できていないです。
制御入力 rand()
関数で生成されるランダムな値とします。
結果
共分散行列 が実際の雑音の分散と同じ場合
カルマンフィルタの予測ステップ・フィルタリングステップで使われる
これは、カルマンフィルタを適用する段階で予め雑音の分散が既知であることを意味します。
観測雑音が入ったデータの観測値、システム雑音のみが入った真値、カルマンフィルタによる推定値、ランダムな制御入力をそれぞれ図示します。
位置
速度
入力
観測値にはかなりノイズが乗っているものの、位置・速度の推定値ともに真値とかなり近い値になっていることがわかります。
共分散行列 が実際の雑音の分散と異なる場合
以下のように、カルマンフィルタで使用する共分散行列と実際の雑音の分散に差異がある場合を考えます。
このとき、カルマンフィルタ側では観測値の誤差の標準偏差が
グラフからもわかる通り、真値と推定値は大幅にずれてしまっていることがわかります。特に、速度はほぼ推定できていないです。
位置
速度
定量的な評価
以下のようなプログラムで、位置の推定値と真値の誤差の積算値を算出してみました。
float total_error = 0.0;
for (int i=0; i<smd.sim_times_; i++) {
total_error += std::fabs(smd.x_hat_v_[i](0) - smd.x_v_[i](0));
}
「共分散行列
定量的な評価からも、カルマンフィルタに設定する
まとめ
今回は、c++ でカルマンフィルタの実装をして、バネ・マス・ダンパ系の例を通してカルマンフィルタの効果を検証してみました。観測誤差があっても共分散行列が正しく設定されていれば、正確に状態推定できることを確認できました。
コメント