万歩計アプリで歩行ルートに Kalman + RTS smoother を入れた話
| 開発記録 | 万歩計
タグ: #GPS #信号処理 #Flutter
1 秒ごとに取った GPS を地図に描いたら、まっすぐ歩いた道がジグザグでした。自分こんなに千鳥足だったかな、と。リアルタイムは Kalman フィルタ、後から見る軌跡は RTS smoother。地味な信号処理で自然な歩行ルートに整えるまでの話です。
生の GPS は思った以上にノイジー
位置情報を 1 秒ごとに取得して地図に描きました。まっすぐ歩いたはずの場所が、ジグザグの軌跡になります。都市部や室内だと特にひどいです。GPS の精度誤差は数 m〜数十 m あるので、これは避けられません。
そのままユーザーに見せると、こういう体験になります。「自分こんなに千鳥足で歩いてた…?」
Kalman フィルタを採用
位置推定の鉄板である Kalman フィルタを採用しました。各時点の観測値と、その精度(GPS の accuracy)を使います。そこから実際の位置と速度を更新していくアルゴリズムです。
Flutter から呼ぶ部分は Dart で実装しました。状態行列と観測行列をフレームごとに更新しています。
スカラー版の Kalman 更新だけ取り出すと、本質は「予測」と「更新」の 2 行に集約されます。一次元の擬似コードは下記のとおりです。
class Kalman1D {
Kalman1D({required this.processNoise, required this.measurementNoise})
: _x = 0,
_p = 1;
final double processNoise; // Q
final double measurementNoise; // R
double _x; // 推定状態
double _p; // 推定誤差共分散
double update(double measurement) {
// Predict (今回はモデルが恒等なので x はそのまま)
_p = _p + processNoise;
// Update
final k = _p / (_p + measurementNoise); // カルマンゲイン
_x = _x + k * (measurement - _x);
_p = (1 - k) * _p;
return _x;
}
}
RTS smoother で後処理
Kalman フィルタは「過去から現在」までの情報で推定します。だからリアルタイム表示には向きます。一方、「記録された軌跡を後から見る」用途は事情が違います。未来の情報も使ったほうが滑らかになります。
RTS(Rauch-Tung-Striebel)smoother は、Kalman フィルタの結果を時間方向に再パスする手法です。過去と未来の両方の情報で軌跡を整えます。これで地図上の軌跡が劇的に滑らかになりました。ここは素直にうれしかったです。
stationary → vehicle 遷移問題
停止状態(stationary)から移動状態(vehicle / walking)への遷移検出も直しました。停止中に GPS のノイズで「動いた」と誤検知される。すると移動モードに切り替わって、変な軌跡が記録されます(座ったままの僕が、地図の上では勝手に歩き回っていました)。
軌跡の整合性チェックを入れて、ノイズ起因の誤検知をフィルタするようにしました。
バッテリー改善
位置情報を 1 秒ごとに取り続けると、バッテリーがどんどん減ります。対処は次の三つです。
- iOS では OS 標準の省電力な位置イベントを併用し、必要な時だけ高精度取得
- distanceFilter を動的に変化させ、停止中は粗く、移動中は細かく
- DB への書き込み回数を削減
地図のデバッグ点表示
開発中は、生の GPS 点と Kalman 推定値を地図に重ねて表示していました。デバッグ用の機能です。設定で表示の段階を切り替えられます。精度(accuracy)の円も可視化しました。
この設定を永続化しておくと、デバッグセッションの間も保持されます。地味ですが、かなり捗ります。
教訓
- 生の GPS は使わない。必ず何らかの平滑化を入れる
- リアルタイムは Kalman、後処理は RTS smoother
- バッテリーは「必要な時に細かく、不要な時は粗く」の原則
位置情報アプリは、地味な信号処理の積み重ねで体験が大きく変わります。数式を足すたびに軌跡がきれいになるのは、気持ちのいい作業でした。