RunKeeperやNike+並みのパフォーマンスを実現する高精度位置情報フィルターの作り方 — 位置情報を正確にトラッキングする技術 in Android (第3回)

前回までのサンプルアプリを実機にインストールして色々なところを移動してみると高いビルに囲まれた路地裏や、木が茂っている公園の中、または厚い雲に覆われた天気で自分の位置とずれたところにパスが描かれることがあるのがわかります。

実はこのように普通にLocationManagerを使っただけでは綺麗な位置情報の軌跡をとることはできないことがあります。

Uberのように一時的に車が少し道路よりずれた場所に表示されてもいいアプリのケースもありますがもしNike+のようにユーザーの行動の軌跡を美しくマップ上に描画し、なおかつ走った距離も正確でなければいけないアプリは高い精度の位置情報だけを取り続ける必要があります。

この回では高い精度の位置情報だけを取り続けるためのフィルターの作り方について説明します。

まずonLocationChangedで今までは、locationListに取得した位置情報をそのまま登録していた箇所をfilterAndAddLocation()という関数で置き換えます。

このfilterAndAddLocation()というメソッドの中で位置情報をフィルタし、フィルタを通過したものだけを登録するようにします。

下がfilterAndAddLocation()のコードです。

ここでは3つのフィルタリングをやっているので順番に説明します。

位置情報を取得した時刻でフィルタする

LocationオブジェクトにはgetElapsedRealtimeNanosという関数があり、GPSチップが位置情報を取得したのがSystemブート時から何ナノ秒経過した時点だったかを教えてくれます。

これを現時点でのSystemClock.elapsedRealtimeNanos、つまりシステム起動時からの経過時間と比べることで、何ナノ秒にこの位置情報が取得されたかをみることができます。

このように位置情報の”古さ”を図る関数getLocationAgeを下のように作り古さを判定します。

GSPの取得は非同期処理です。以下の条件によって位置情報の取得できるタイミングが変わります。

  • GPS衛星の位置 — 位置情報を取得するには複数のGPSとの通信が必要なので宇宙を飛んでいる複数の衛星と自分の位置はその都度変わる
  • 衛星とAndroid機との間の障害物(ビル、樹木、雲など)の状況
  • 携帯電話基地局の位置 —スマートフォンに搭載されているGPSチップはA-GPS(Assisted GPS)と呼ばれるもので基地局との通信によって位置情報の精度を補正するチップなので、基地局との通信が必要
  • 携帯電話基地局とAndroid機との間の障害物の状況

上の条件が悪いと一定時間位置情報が取れません。マンションなどの集合住宅の中に入った時にGoogleマップがしばらく更新されないような時です。

この場合、LocationManagerはキャッシュしてある位置情報を渡してきます。キャッシュしてある位置情報とは上の諸条件が良くて位置情報が取得できていた時に取得した最後の位置情報です。

キャッシュした古い情報を取得してユーザーに見せてしまうと、何分も前にいた位置に自分の位置が表示されることになり位置トラッキングの信頼性を極端に落としてしまいます。

このため、getLocationAgeで位置情報の新しさ(古さ)を測り古い位置情報を取得しないようにします。

サンプルでは10秒以上古い位置情報であればその位置情報を捨てるようにしています。

10秒は走っている時でも40メートルくらいなので、40メートル以上前の位置にユーザーの現在地を表示してしまうというようなことを避けることができます。

水平方向のAccuracyでフィルタする

次にLocationオブジェクトのgetAccuracyという関数を使って位置情報の水平方向の制度を測定します。この値は前回マップ表示のところでも現在の位置情報の制度を示す円を描くために使いました。

まずこのaccuracy情報が取得できないことがあります。この場合値がゼロかマイナスになっているので、この場合は位置情報を取得しないようにします。

次にaccuracy情報 がゼロより大きいばい、その精度によってフィルターをかけます。

このサンプルでは、10メートルよりもhorizontalAccurayが大きかたら位置情報を捨てるようにしています。

これは”水平方向の誤差が10メートル以下の位置情報だけを使う”という意味です。

この値はアプリごとの目的によって変わってくると思います。Uberであれば車が車線から多少ずれていてもこちらに向かっているのがわかるので100メートルぐらいでいいと思いますし、Pokemonのようなアプリだと10メートル以上の精度は欲しいかもしれませんが、位置情報が取れなくてキャラクターがずっと同じところに止まっているよりは、とにかく動いていてくれた方がいいから200メートルなどに設定した方がいいという判断になるかもしれません。もちろんさらにこの値を狭めて精度の高い位置情報だけを扱うようにすることもできます。

黄色い丸の部分が精度10m以下で弾かれた位置情報

Kalman Filterによる除去

ここまで説明したフィルターを施せばほとんどのケースで精度の高い位置情報を所得して移動経路を地図上に描画することができます。
ただ、稀にこれらのフィルターを通過したにも関わらず自分の移動した場所とはかけ離れた場所に軌跡が描かれることがあります。
”取得時刻も古くなく、水平方向の精度情報も良いのに実際の位置が間違っているLocationオブジェクト”というのが取得されることがあります。

Android4.xの機種の時は数キロメール走って入れば1、2個はこのような位置情報が取得されてしまっていました。

AndroidのAPI DocumentでもgetAccuracy()の信頼性は68%と書かれており、32%の確率でgetAccuracyの返す水平方向の位置情報と実際の精度は違うということになります。

私の経験上最新の機種(例えば私の持っているNexus 6p + Android OS7.0)ではこのようなケースが起きることは稀ですが、古い機種をサポートしたり、日本以外の国のユーザーをサポートする場合(日本は携帯電話基地局のカバレッジがよくAssited GPSの精度が上がるが海外ではこれほど精度は良くないため)は、これらのLocationオブジェクトを弾くフィルターが必要になります。

この問題のためにKalman Filterを使います。Kalman Filterによって今までの経路情報から次の経路情報を予測し、その予測値と実際のLocationオブジェクトの位置があまりにも違う場合はその位置情報を弾くようにします。

Kalman Filter自体の実装の詳細は省きますが、サンプルのKalmanLatLongクラスの実装を見てみてください。

使い方は下13 行目のようにKalmanLatLongオブジェクトに緯度、経度、位置精度、現在までのランニング時間(秒)、現在のペース(メートル/秒)を入力すると予測値が返ってきます。この値が緯度経度とかけ離れていた場合はログしないようにします。

このサンプルでは、60メートル以上離れていたらログしないようにしています。

Kalmanフィルターの予測位置をベースにして半径60メートルを緑色の円で表示してみると下のようになります。この円に入っていればOKと判断します。

”所得時刻も問題なく(キャッシュされた位置情報ではない)、水平方向の位置精度も悪くないのになぜか変な位置情報がくる”というケースに対応したい場合このフィルタが有効になります。

RunKeeper、Nike+とのパフォーマンス比較

ここまでのサンプルコードを使って実際に人気のランニングアプリと性能を比較してみました。

まずはRunKeeper

左がRun Keeper、右がサンプルアプリ

次にNike+

左がRun Keeper、右がサンプルアプリ

Nike+やRunKeeperと同じ制度のトラッキングができていることがわかります。

さらにすごい位置情報トラッキングアルゴリズムを作る指針

ここまで説明したトラッキングエンジンでは大雨の日に、森の中を、電話をカバンの中に入れた状態で走る、などの非常にGPS取得環境が悪い状態で使われるとパフォーマスが落ちます。

トレイルランのためのアプリのようにユーザーが森の中を走ることが多い場合や、Uberのように車の中に電話があって雨の日も走行することが多いアプリではこのような悪条件でも位置情報を取るようなトラッキングエンジンが必要でしょう。

このような環境で重要なのは耐久性を高めることです。
好条件に最適化するために上で説明したフィルターの閾値を設定して行く(例えば水平方向の精度を5m以下にする)と閾値が厳し目になりがちです
この状態で雨の日に森を走るとほとんどの位置情報がこのクライテリアをクリアできず、ユーザーのマップ上には何も表示されないということがあります。

精度は高いけど、何も取れないことがある

というトラッキングエンジンになります。

精度は悪いけど、とり続けてくれる

ということもアプリの目的によっては必要になるかもしれません。

このよう”タフ”な位置情報トラッキングエンジンを作る方針に少し触れたいと思います。

環境を理解する

タフなトラッキングエンジンを作る鍵は今GPSデバイスがどういう状況に置かれているのかということをアプリが理解することです。

例として

  1. 取得される位置情報の精度
  2. 所得される位置情報の時間間隔

などがあります。

①は、水平方向の位置情報精度を蓄積しておいて、その平均値や変化量を見て、環境の良し悪しを判断します。

水平方向の位置情報がずっと悪いようであればフィルターの閾値を徐々に緩くして多少悪くてもログするようにします。逆に改善していけば動的に閾値を厳しくしていきます。

②はどれくらいの感覚で位置情報が取得できているかということです。もし長い時間位置情報が取得できていなければ雨などでGPSが取得しづらくなっていると予測することができます。時間間隔はTimerを別に走らせてモニターをします。
ある一定時間以上位置情報が取れないようであればフィルターの閾値を変えるのではなく、LocationManagerに設定するCriteriaのaccuracyやhorizontalAccuracyの設定を緩くして精度の低い位置情報も受け付けるようにすることで位置情報が再び取得できるようになったりします。requestLocationUpdateに渡すminDistanceの値を小さくしても位置情報を取得できやすい方に状況を持っていくことができます。

そのほかにもユーザーの環境を理解する方法は色々とあると思います

  • 天候情報をWebから取ってくる
  • 気圧センサーの変化を見て天候を推測する
  • 地図情報と照らし合わせてビルの多い場所か判断する
  • ユーザーが多いアプリでは位置情報の精度の悪い場所を過去のユーザーデータから判定する

このように環境に合わせてLocationManagerのCriteria、minimumDistance, minimumTime、フィルターの閾値を動的に調整するアルゴリズムを作ると自分のアプリのユースケースにあわせてトラッキングエンジンを進化させることができるので挑戦して見てください。

サンプルアプリのリンクはこちら↓

https://github.com/mizutori/AndroidLocationStarterKit


位置情報トラッキング機能開発(iOS, Android)に関する質問や、開発のご相談などはこちらにご連絡ください。
@mizutory
mizutori@goldrushcomputing.com