スマホで撮った写真のExif情報から経緯度を取得して、歩いてきた軌道を描いてみよう（piexifとmatplotlibで）

# はじめに
最近カメラを持つのをやめて、完全にスマホだけで写真を撮ることになりました。スマホで写真を撮ったらGPSの情報（緯度や経度など）も写真のファイルの中に入るので、後でこの写真がどこで撮ったのか調べることができて便利です（ただしGPS機能が機動しているか確認しておくように）。全部撮った写真の位置を取ったらあの時歩いてきた軌道がわかります。


この記事ではスマホから得た写真ファイルの中の位置情報を取得して軌道（グラフ）を描く方法について書きます。

普段カメラやスマホで撮った写真のファイルには、画像データ自体の他にExif（[Exchangeable image file format](https://ja.wikipedia.org/wiki/Exchangeable_image_file_format)）情報というメタデータが含まれています。GPS情報もExif情報の中に含まれています。

Exif情報を取得するためにpythonではpiexifというモジュールがあります。
https://github.com/hMatoba/Piexif

そして緯度と経度と時間の情報を得たら次は[matplotlib](https://matplotlib.org/)で軌道を描きます。


# piexifでExif情報を取得する方法

まずExif情報とpiexifの使い方について簡単に説明します。

Exif情報はスマホの中でもパソコンの中でも色んな方法で調べることができますね。例えば私はいつもAdobe Bridgeを使っています。

例として福島県伊達市梁川駅で撮ったこの写真を挙げます。Adobe Bridgeではこのように情報が見えます。
![
](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/b0590cd0-883d-278d-dd13-fe73dc9ce418.jpeg)
GPS情報を含め、色んな情報が記載されていますね。

その情報をpythonで調べたい場合、色んなモジュールが使えるようですが、私が使っているのはpiexifです。その他に例えばこの記事で紹介されたもの https://qiita.com/EH-PX/items/0a5e06d0459586ca48d7

piexifのインストールは簡単にpipでできます。
```sh
pip install piexif
```

使う時は`piexif`を`import`して`piexif.load`関数を使えばすぐ全部の情報が出てきます。
```python
import piexif
exif_dict = piexif.load('写真ファイル')
print(exif_dict)
```

上述の写真ファイルに使ったらこんな結果となります。
```python
{'0th': {256: 3468,
  257: 4624,
  271: b'OPPO',
  272: b'OPPO Reno5 5G',
  274: 1,
  282: (72, 1),
  283: (72, 1),
  296: 2,
  306: b'2023:04:01 09:53:35',
  531: 1,
  34665: 214,
  34853: 900},
 'Exif': {33434: (1, 1920),
  33437: (170, 100),
  34850: 2,
  34855: 100,
  36864: b'0220',
  36867: b'2023:04:01 09:53:35',
  36868: b'2023:04:01 09:53:35',
  36881: b'+09:00',
  37121: b'\x01\x02\x03\x00',
  37377: (10906, 1000),
  37378: (153, 100),
  37379: (628, 100),
  37380: (0, 6),
  37381: (153, 100),
  37383: 2,
  37385: 16,
  37386: (4730, 1000),
  37500: b'{"PiFlag":"0","nightFlag":"0","nightMode": "0","asdOut": ["0"],"iso": "100","expTime": "520920","fType":"3","bkMode":"0","aideblur":"0","aisState":"0"}',
  37510: b'oplus_32',
  37520: b'250',
  37521: b'250',
  37522: b'250',
  40960: b'0100',
  40961: 1,
  40962: 0,
  40963: 0,
  41495: 1,
  41729: 1,
  41986: 0,
  41987: 0,
  41989: 27,
  41990: 0},
 'GPS': {1: b'N',
  2: ((37, 1), (50, 1), (4477, 100)),
  3: b'E',
  4: ((140, 1), (36, 1), (195, 100)),
  5: 0,
  6: (0, 1000),
  7: ((0, 1), (53, 1), (8, 1)),
  29: b'2023:04:01'},
 'Interop': {},
 '1st': {},
 'thumbnail': None}
```

出てきた情報は辞書オブジェクトという形になっています。しかしキーは番号で表示されているから、このままではどれが何の情報なのかわかりませんね。

フィールド名と番号とその意味について詳しくはここに書いてあります。https://www.cipa.jp/std/documents/j/DC-008-2012_J.pdf

この記事も参考に https://qiita.com/XPT60/items/54073c61bcd5f437021b

それでもいちいち調べるのは面倒でしょう。

便利なことに、piexifではそれぞれの番号がどのフィールド名なのかは`piexif.TAGS`オブジェクトに書いてあります。例えばGPS情報は`piexif.TAGS['GPS']`にあります。

このように使うことができます。

```python
for k in ['0th','Exif','GPS']:
    print('\n=='+k+'==')
    for n in exif_dict[k]:
        print('%s: %s'%(piexif.TAGS[k][n]['name'],exif_dict[k][n]))
```

こうやって情報はわかりやすく表示されます。
```python
==0th==
ImageWidth: 3468
ImageLength: 4624
Make: b'OPPO'
Model: b'OPPO Reno5 5G'
Orientation: 1
XResolution: (72, 1)
YResolution: (72, 1)
ResolutionUnit: 2
DateTime: b'2023:04:01 09:53:35'
YCbCrPositioning: 1
ExifTag: 214
GPSTag: 900

==Exif==
ExposureTime: (1, 1920)
FNumber: (170, 100)
ExposureProgram: 2
ISOSpeedRatings: 100
ExifVersion: b'0220'
DateTimeOriginal: b'2023:04:01 09:53:35'
DateTimeDigitized: b'2023:04:01 09:53:35'
OffsetTimeOriginal: b'+09:00'
ComponentsConfiguration: b'\x01\x02\x03\x00'
ShutterSpeedValue: (10906, 1000)
ApertureValue: (153, 100)
BrightnessValue: (628, 100)
ExposureBiasValue: (0, 6)
MaxApertureValue: (153, 100)
MeteringMode: 2
Flash: 16
FocalLength: (4730, 1000)
MakerNote: b'{"PiFlag":"0","nightFlag":"0","nightMode": "0","asdOut": ["0"],"iso": "100","expTime": "520920","fType":"3","bkMode":"0","aideblur":"0","aisState":"0"}'
UserComment: b'oplus_32'
SubSecTime: b'250'
SubSecTimeOriginal: b'250'
SubSecTimeDigitized: b'250'
FlashpixVersion: b'0100'
ColorSpace: 1
PixelXDimension: 0
PixelYDimension: 0
SensingMethod: 1
SceneType: 1
ExposureMode: 0
WhiteBalance: 0
FocalLengthIn35mmFilm: 27
SceneCaptureType: 0

==GPS==
GPSLatitudeRef: b'N'
GPSLatitude: ((37, 1), (50, 1), (4477, 100))
GPSLongitudeRef: b'E'
GPSLongitude: ((140, 1), (36, 1), (195, 100))
GPSAltitudeRef: 0
GPSAltitude: (0, 1000)
GPSTimeStamp: ((0, 1), (53, 1), (8, 1))
GPSDateStamp: b'2023:04:01'
```

ちなみに`piexif.TAGS`とは逆に、`piexif.GPSIFD`や`piexif.ExifIFD`オブジェクトはフィールド名から番号を知りたい場合に使います。これをexif_dictの辞書のキーとして使えばいいです。

例えば`ExposureTime`（露出時間）を調べたい場合、こう書きます。
```python
print(exif_dict['Exif'][piexif.ExifIFD.ExposureTime])
```

結果
```python
 (1, 1920)
```

Exif情報は`(分子, 分母)`という形で書かれるものが多いです。例えばこの写真の露出時間は「1/1920秒」となります。

## 緯度と経度の情報

緯度と経度はGPSLatitudeとGPSLongitudeというフィールドに書いてあります。

でもややこしいことに、値は6つの数字で書いています。それは`((degree, 分母), (arcmin, 分母), (arcsec, 分母))`です。

degree（度）単位の経緯度が欲しい場合こうやって計算します。

```python
elat = exif_dict['GPS'][piexif.GPSIFD.GPSLatitude]
elon = exif_dict['GPS'][piexif.GPSIFD.GPSLongitude]
lat = elat[0][0]/elat[0][1] + elat[1][0]/elat[1][1]/60 + elat[2][0]/elat[2][1]/3600
lon = elon[0][0]/elon[0][1] + elon[1][0]/elon[1][1]/60 + elon[2][0]/elon[2][1]/3600
print('(%.5f°N, %.5f°E)'%(lat,lon))
```

こうやってdegree単位の座標が表示されます。
```
(37.845769°N, 140.600542°E)
```

これが正しいのか確認しておきたいですね。google mapを調べたらこれはほぼ梁川駅と一致しているとはわかります。とりあえずこの写真は梁川駅の辺りで撮ったものに違いないと確認できるでしょう。
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/48f88f13-cc79-cb56-b3c8-e613025b5119.jpeg)

ちなみに標高の情報は`GPSAltitude`というフィールドに書かれるはずですが、ここでは0になっています。つまり情報が入ってないようです。調べてみたら`GPSAltitude`が0以外の数字になっている写真もありますが、あまり正しくないようです。やはり写真のGPSデータから標高を調べない方がいいでしょう。


## 時間の情報

位置の他に、時間の情報もGPSに入っています。`GPSTimeStamp`（原子時計の時間）というフィールドにあります。経緯度と同様に6つの数字で記入されています。

hour単位の時間が欲しい場合こうします。
```python
et = exif_dict['GPS'][piexif.GPSIFD.GPSTimeStamp]
t = et[0][0]/et[0][1] + et[1][0]/et[1][1]/60 + et[2][0]/et[2][1]/3600
```

ただ、これは地方の時間ではなく、UTC標準時間です。日本の時間の場合は+9が必要です。

それに実はこの時間はあまり正確ではないのです。本当の時間とは多少ズレているので、あまり使い物にならないかもしれません。

その代りに、基本Exif情報にある`DateTimeOriginal`（原画像データの生成日時）の方が信用できます。これを使えばいいです。

ただし`DateTimeOriginal`は数字ではなくbytesオブジェクトになっています。少し面倒ですが、扱い方は大体文字列と似ていて、intなどの数字に変換することができます。

hour単位の時間はこうやって得られます。

```python
hms = exif_dict['Exif'][piexif.ExifIFD.DateTimeOriginal].split(b' ')[1]
t = int(hms[0:2]) + int(hms[3:5])/60 + int(hms[6:8])/3600
```


# 実装

さて、Exifを取得するための必要な基礎に関しては以上です。次は取得した情報を使って歩いてきた軌道を描いてみます。

今回使うデータは、私が[仙台駅](https://ja.wikipedia.org/wiki/仙台駅)から[福島駅](https://ja.wikipedia.org/wiki/福島駅)まで[阿武隈急行](https://ja.wikipedia.org/wiki/阿武隈急行)の電車に乗って旅行した時の写真です。ただし途中で[やながわ希望の森公園前駅](https://ja.wikipedia.org/wiki/やながわ希望の森公園前駅)に降りて[梁川駅](https://ja.wikipedia.org/wiki/梁川駅)まで歩いて、その後再び電車に乗って福島駅まで行ったのです。

途中でしばしば写真を撮っていて、全部129枚あります。一枚目は仙台駅で、最後は福島駅です。これらの写真のGPS情報を調べたら阿武隈急行の軌道が作れるはずです。

それでは実装します。

```python
from glob import glob
import piexif
import matplotlib.pyplot as plt

lis_lat = []
lis_lon = []
lis_t = []
for img in sorted(glob(r'写真フォルダ\*.jpg')):
    exif_dict = piexif.load(img)
    if(piexif.GPSIFD.GPSLatitude not in exif_dict['GPS']):
        continue # GPS情報が正しく入っていないファイルも含まれるのでこの場合は無視
    
    elat = exif_dict['GPS'][piexif.GPSIFD.GPSLatitude]
    elon = exif_dict['GPS'][piexif.GPSIFD.GPSLongitude]
    hms = exif_dict['Exif'][piexif.ExifIFD.DateTimeOriginal].split(b' ')[1]
    lat = elat[0][0]/elat[0][1] + elat[1][0]/elat[1][1]/60 + elat[2][0]/elat[2][1]/3600
    lon = elon[0][0]/elon[0][1] + elon[1][0]/elon[1][1]/60 + elon[2][0]/elon[2][1]/3600
    t = int(hms[0:2]) + int(hms[3:5])/60 + int(hms[6:8])/3600
    
    lis_lat.append(lat)
    lis_lon.append(lon)
    lis_t.append(t)

plt.figure(figsize=[7.5,7],dpi=100)
plt.axes(aspect=1)
plt.xlabel('経度（°Ｅ）',fontname='MS Gothic',size=13)
plt.ylabel('緯度（°Ｎ）',fontname='MS Gothic',size=13)
plt.plot(lis_lon,lis_lat,':',color='#AAAAAA',lw=0.8)
plt.scatter(lis_lon,lis_lat,s=10,c=lis_t,marker='*',cmap='jet')
cb = plt.colorbar(pad=0.01,aspect=50,format=lambda t,_: '%02d:%02d'%(t,(t%1*60)))
cb.set_label(label='時間（時）',fontname='MS Gothic',size=13)
plt.tight_layout()
plt.savefig(r'aruitekita.png')
plt.close()
```

実行したら、このように見事に電車の軌道ができました。ちなみにここで色は時間を示します。
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/be323ed8-25d3-45b2-23b9-8bf1fbce5b32.png)
ちょっと変な形になっている部分があります。それはおそらくGPSの調子が悪い時に撮った写真です。色んな原因によって正確度が落ちることが多いようなので、残念ながらどうしようもないことです。

特にやながわ希望の森公園前駅から梁川駅までの間です。ここは徒歩なので、写真数が多くて、129枚の中の69枚はこの辺りの写真です。その中に大きく間違っているものが含まれます。

この部分だけの写真を取って同じような軌道を書くとこうなります。
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/294b10d6-bd62-e219-55bb-daa790c77197.png)
足で歩くという狭い範囲で見るとズレが目立つようになりました。大体の軌道は現実と噛み合っているものの、こんなくねくねは不自然ですね。普通に道を歩いているはずだったのに。しかも大きく西の方にズレているものも含まれています。

このような町の中でもあまり信用すぎてはならないGPS情報ですが、実は山の中とかでは状況は更に酷いらしいです。

例えば福島県二本松市の[安達太良山](https://ja.wikipedia.org/wiki/安達太良山)の写真です。あの時登山に行ってきました。撮った写真は304枚。その写真のGPS情報を調べて軌道を描いてみたらこうなります。
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/ace34b8b-43f7-b37a-f43d-ea1cf6642f5a.png)
すごく滅茶苦茶で全然当てにならないらしいです。一応、安達太良山の山頂の緯度と経度は(37.621, 140.288)なので大体合っているということはわかりますが、これその辺りを中心にランダムするみたいな感じです。

# 終わりに

以上写真のExif情報の扱い方からその情報を活かすことまで説明しました。GPS情報を使えば写真を撮った場所をわかって、旅行などの時にとても便利です。

ただしGPSの正確度に問題があって、思い通りにならない場合が多いこともわかりますよね。

スマホに記載された情報のクォリティに関しては自分ではどうしようもないことですが、ここで説明したExif情報の扱い方に関しては間違いはないはずなので、誰かの参考になれたら幸いです。