GPS の位置情報と精度を「本気で」可視化

accuracy が取れることがわかったのですぐに試してみたくなり。

四の五の言わずに先に結果の動画:

これだけでもいい人はこれだけでも良かろう。じっくり観察するとかなり役に立つんじゃないかと思う。

基本的に現実の時間の10倍早回しで、時間が止まって見える箇所は以下のいずれかが理由:

  • 実際に動いてない(駅で飯食ってたりとか)
  • GPS センサがロケーション変化を検出出来てない (例: 横須賀線の東京品川間は地下なので全然ダメ)
  • 稀: 後述の可視化に使ったプログラムが少し遅延している (タイルのキャッシュの関係)
  • さらに稀: 取れたはずのデータを記録出来なかった (後述)

一応この可視化に使ったプログラムはこんな:

 1 # -*- coding: utf-8 -*-
 2 import sys
 3 # please edit next line for your environment.
 4 sys.path.append("/sdcard/kivy-my-site-packages")
 5 
 6 from datetime import datetime
 7 from kivy.lang import Builder
 8 from kivy.app import App
 9 from kivy.properties import StringProperty
10 from kivy.uix.boxlayout import BoxLayout
11 from kivy.clock import Clock
12 from kivy.garden.mapview import MapView, MapMarker, MapSource
13 
14 from geographiclib.geodesic import Geodesic
15 _WGS84 = Geodesic.WGS84
16 
17 Builder.load_string('''
18 <GardenMapviewDemo>:
19     orientation: 'vertical'
20 
21     detail: detail
22     map: map
23 
24     Label:
25         id: detail
26         text: root.detail_text
27         size_hint: (1, None)
28         font_size: 40
29         height: 150
30 
31     MapView:
32         id: map
33         zoom: 17
34 ''')
35 
36 class GardenMapviewDemo(BoxLayout):
37     detail_text = StringProperty()
38 
39     def __init__(self):
40         super(GardenMapviewDemo, self).__init__()
41         self.map.map_source = MapSource(
42             url='http://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png',
43             attribution='http://maps.gsi.go.jp/development/ichiran.html')
44 
45         self._data_all = []
46         self._index = 0
47         self._added_markers = []
48         with open("location_all.csv") as fi:
49             fi.readline()  # skip header
50             for i, line in enumerate(fi.readlines()):
51                 self._data_all.append(
52                     tuple(map(float, line.split(",")[:4])))
53 
54         self.update_view()
55 
56     def update_view(self, *arg, **kwarg):
57         if self._added_markers:
58             for m in self._added_markers:
59                 self.map.remove_marker(m)
60             self._added_markers = []
61 
62         cur = self._data_all[self._index]
63 
64         self.map.center_on(cur[2], cur[1])
65         m = MapMarker(lon=cur[1], lat=cur[2], source="marker18.png")
66         self.map.add_marker(m)
67         self._added_markers.append(m)
68         if cur[3] < 50:
69             s = "mymarkerS.png"
70         elif cur[3] < 100:
71             s = "mymarkerM.png"
72         else:
73             s = "mymarkerL.png"
74         for az in range(0, 360, 15):
75             dr = _WGS84.Direct(cur[2], cur[1], az, cur[3])
76             m = MapMarker(lon=dr['lon2'], lat=dr['lat2'], source=s)
77             self.map.add_marker(m)
78             self._added_markers.append(m)
79 
80         self.detail_text = "[{}] accuracy = {:5.1f} [m]".format(
81             datetime.fromtimestamp(cur[0]).strftime("%H:%M:%S"),
82             cur[3])
83 
84         if self._index < len(self._data_all) - 1:
85             elapse = self._data_all[self._index + 1][0] - self._data_all[self._index][0]
86             self._index += 1
87             Clock.schedule_once(self.update_view, elapse / 10)
88 
89 class GardenMapviewDemoApp(App):
90     def build(self):
91         return GardenMapviewDemo()
92 
93 
94 if __name__ == '__main__':
95     GardenMapviewDemoApp().run()

であるが、前提としてこれへの措置であるとか、そもそも導入が一癖ある、というだけでなく、これまで説明してない問題もあって、騙しながら使う必要がある。(高速にアクセスし過ぎるとタイルのキャッシュが取れなくて死ぬ。のでキャッシュが揃うまで何度も動かした。)

コード中の「location_all.csv」が今日記録した、GPS の位置情報変化イベントの「ほぼ全て」を記録したデータで、こんな感じ:

1 time, lon, lat, accuracy, bearing, speed
2 1471325896.17,139.708407,35.558779,13.000,68.30,1.08
3 1471325896.19,139.708407,35.558779,13.000,68.30,1.08
4 1471325896.29,139.708290,35.558816,19.130,0.00,0.00
5 1471325896.29,139.708290,35.558816,19.130,0.00,0.00
6 1471325897.12,139.708404,35.558801,11.000,74.80,1.41
7 1471325897.13,139.708404,35.558801,11.000,74.80,1.41
8 1471325899.11,139.708447,35.558741,7.000,99.60,1.33

一応…こんなんで記録したわけね:

android 的に「サービス」の kivy プログラムだよ
 1 # -*- coding: utf-8 -*-
 2 # ...snip...
 3 from plyer import gps
 4 # ...snip...
 5 
 6 class Observer(object):
 7     # ...snip...
 8     def __init__(self):
 9         # ...snip...
10         gps.configure(on_location=self.on_location,
11                       on_status=self.on_status)
12         gps.start()
13         #
14         with open("location_all.csv", "w") as fo:
15             fo.write("time, lon, lat, accuracy, bearing, speed\n")
16         # ...snip...
17 
18     def on_location(self, **kwargs):
19         #
20         with open("location_all.csv", "a") as fo:
21             fo.write("""\
22 {time:.2f},{lon:.6f},{lat:.6f},{accuracy:.3f},{bearing:.2f},{speed:.2f}
23 """.format(time=(datetime.utcnow() - _ERA).total_seconds(),
24            lon=kwargs['lon'],
25            lat=kwargs['lat'],
26            accuracy=kwargs['accuracy'],
27            bearing=kwargs['bearing'],
28            speed=kwargs['speed']))
29         # ...snip...

ただし sqlite3 と同じく I/O エラーがたまに起こって少し取りこぼした:

1 Traceback (most recent call last):
2   File "jnius/jnius_proxy.pxi", line 47, in jnius.jnius.PythonJavaClass.invoke (jnius/jnius.c:29223)
3   File "jnius/jnius_proxy.pxi", line 73, in jnius.jnius.PythonJavaClass._invoke (jnius/jnius.c:29924)
4   File "/home/tito/code/python-for-android-upstream/build/python-install/lib/python2.7/site-packages/plyer/platforms/android/gps.py", line 30, in onLocationChanged
5   File "/storage/emulated/0/kivy/simple_altimeter/service/observer.py", line 76, in on_location
6     with open("location_all.csv", "a") as fo:
7 IOError: [Errno 4] Interrupted system call: 'location_all.csv'

10057レコードが記録されていたが、この I/O エラーは 11回だけなので、GPS 精度の評価をするのに致命的なものではないと思う。

というわけで、実際に可視化してみて色々わかった:

  1. accuracy は滑らかに大きくなったり小さくなったりを繰り返しているのね(生き物みたいで面白い)
  2. たとえば accuracy=1800m なんてのは確かに捨ててもいいかもしれない
  3. けれども、accuracy が小さな値でも実際は大きく外していることもある(錦糸町なんか通ってない、実際は)

あんまし単純に信頼するわけにもいかなくて、「accuracy を信じて処理してもなおゴミ掃除が必要」てことがはっきりわかったってこと。でもまぁどう考えてもゴミ、てのが一応デカすぎる accuracy をみれば結構わかるのはありがたいとは思う。






01:15追記:
書くつもりで忘れてた。

一応 accuracy の分布的なものも。シンプルにヒストグラムで:

 1 import numpy as np
 2 import matplotlib.pyplot as plt
 3 import matplotlib.mlab as mlab
 4 
 5 _data_all = []
 6 with open("location_all.csv") as fi:
 7     fi.readline()  # skip header
 8     for i, line in enumerate(fi.readlines()):
 9         acc = float(line.split(",")[3])
10         _data_all.append(acc)
11 
12 fig, ax = plt.subplots()
13 plt.yscale("log")  # 右肩が小さすぎて読み取れないので対数で。
14 ax.grid(True)
15 n, bins, patches = ax.hist(
16     _data_all,
17     100,
18     facecolor='blue',
19     alpha=0.9)
20 
21 plt.savefig("hist_accuracies.png", bbox_inches="tight")
22 #plt.show()

なかなかにヘンチクリンな分布である。計算上の作為が入ってるんじゃないだろうか。(何かの閾値を超えたら何かしら諦める、のような特殊処理でも入ってんじゃないのかな。)