写真の座標情報を可視化する

昨年、ある大学の人文科学系研究でフィールドワークの撮影ポイントを可視化するという案件を受託開発しました。内容が面白かったのと、学生で同様の課題に取り組まれている方が多いようなのでメモがてら一部の実装テクニックを紹介したいと思います。

Exifとは

JPEG形式の画像にはExifと呼ばれる画像に関する領域があります。PillowでこのExif情報を取得することができます。(Pillowについてはこちらで解説しています。)以下のサンプルは、読み込んだjpegのExif情報のコード、名称、値を一覧表示しています。スマホで撮影した写真は大抵Exif情報が設定されていますので試してみてください。

from PIL import Image
from PIL.ExifTags import TAGS

image = Image.open("mypicture.jpg")
exif = image._getexif()
for id, value in exif.items():
    print(id, TAGS.get(id), value)

上のコードの通り、Pillowのimageオブジェクトから_getexifで取得することができます。また、Exifのコードと名称の辞書にPIL.ExifTags.TAGSが使えます。

36864 ExifVersion b'0231'
37121 ComponentsConfiguration b'\x01\x02\x03\x00'
37377 ShutterSpeedValue 11.173345356176736
36867 DateTimeOriginal 2020:04:15 11:44:48
36868 DateTimeDigitized 2020:04:15 11:44:48
:
:

たくさんの情報が出力されましたが、座標に相当する情報が34853のGPSInfoです。さっそく座標情報を取り出してみましょう。

cood = exif[34853]
print(cood)

以下のように座標情報の辞書が取得できます。

{1: 'N', 2: (35.0, 41.0, 52.66), 3: 'E', 4: (139.0, 46.0, 13.39), 5: b'\x00', 6: 19.115526665681784, 12: 'K', 13: 1.4998638622285754, 16: 'T', 17: 300.0668789808917, 23: 'T', 24: 300.0668789808917, 31: 57.14691317266425}

1が北緯、2が緯度、3が東経、4が経度を表しています。経度緯度については3数のタプルになっていますが、これは度分秒となっていますので、度に変換します。

lon = int(geoinfo[4][0]) + float(geoinfo[4][1]) / 60 + float(geoinfo[4][2]) / 3600
lat = int(geoinfo[2][0]) + float(geoinfo[2][1]) / 60 + float(geoinfo[2][2]) / 3600
print(lon, lat) # 139.77038611111112 35.697961111111105

座標情報の可視化

次に、座標情報を可視化してみましょう。matplotlibの拡張、Basemapを使用します。(実際の案件は近年可視化についてWeb化のためJavaScriptを使用することが多いです。)残念ながら、PyPIには登録されていないため、インストールは少し手間です。

Ubuntu18.04の場合は以下でインストールすることができました。

sudo apt-get install libgeos-dev
pip install git+https://github.com/matplotlib/basemap.git

Windows系はバイナリが提供されています。詳しくは以下の公式を参照してください。

https://matplotlib.org/basemap/users/installing.html#installation

基本的な使い方はBasemapオブジェクトを生成しplotするだけです。

import matplotlib.pyplot as plt
from mpl_toolkits.basemap import Basemap

lon = 139.77038611111112
lat = 35.697961111111105

basemap = Basemap(projection='merc',  # メルカトル
                  resolution='i',  # 解像度=i
                  llcrnrlon=138.5, llcrnrlat=35, urcrnrlon=141, urcrnrlat=37)  # 描画する矩形

basemap.drawmapboundary(fill_color='#F0F0F0')  # 背景
basemap.drawcoastlines(color='#303030')  # 海岸線
basemap.fillcontinents(color='#FFFFFF')  # 陸地

x, y = basemap(lon, lat)
basemap.plot(x, y, 'o', markersize=4)

plt.show()

実行すると以下のようになります。

上のサンプルでは図法、解像度、対象矩形、背景・海岸線・陸地の色などを設定していますが、詳しいオプションなどは以下公式を参照してください。
https://basemaptutorial.readthedocs.io/en/latest/index.html

写真の座標情報を可視化する

では前半のExifの取得と組み合わせてみましょう。imgフォルダ配下の画像データから座標を取得して散布してみます。

from PIL import Image
import glob
import matplotlib.pyplot as plt
from mpl_toolkits.basemap import Basemap

def map_lot(points):
    basemap = Basemap(projection='merc',  # メルカトル
                      resolution='i',  # 解像度=i
                      llcrnrlon=138.5, llcrnrlat=35, urcrnrlon=141, urcrnrlat=37)  # 描画する矩形

    basemap.drawmapboundary(fill_color='#F0F0F0')  # 背景
    basemap.drawcoastlines(color='#303030')  # 海岸線
    basemap.fillcontinents(color='#FFFFFF')  # 陸地

    for lon, lat in points:
        x, y = basemap(lon, lat)
        basemap.plot(x, y, 'o', markersize=5)

    plt.show()

def main():
    points = []
    files = glob.glob("./img/*.*")

    for file in files:
        image = Image.open(file)
        exif = image._getexif() or {}
        geoinfo = exif.get(34853)
        if geoinfo:
            lon = int(geoinfo[4][0]) + float(geoinfo[4][1]) / 60 + float(geoinfo[4][2]) / 3600
            lat = int(geoinfo[2][0]) + float(geoinfo[2][1]) / 60 + float(geoinfo[2][2]) / 3600
            points.append((lon, lat,))
    map_lot(points)

main()

ある一週間の私のスマホの撮影箇所を可視化したところ、以下のような出力となりました。仕事で移動した都内周辺とに遊びに行った千葉県の養老渓谷がプロットされています。

Exifには日付が入っているため、撮影順序に経路を生成することも可能です。調査分野では色々応用が効くと思いますので是非活用してみてください。