イタリア旅行の地図アルバムをPythonで作って共有してみた結果


しばらく前にイタリアを旅行しました。ミラノ・ヴェネツィア・フィレンツェ・ピサ・ローマ・ポンペイを1週間で回るツアーで、気づいたら1000枚くらい写真を撮っていました〜。
そこそこ自由時間もあって毎日1万歩くらいは散策したのでルート込みで記録に残しておきたい、ということで地図アルバムを作ってみました……!

可視化

全体像はこんな感じ。OpenStreetMapの地図を使ってます。

ここから、たとえばヴェネツィアをズームアップすると、こんな風に散策ルートが見れます。日にちによって色分けをしていて、ヴェネツィアは一泊したので二色ルートがありますね。
そしてマーカーが写真を撮ったポイントで、クリックすると写真がポップアップします。

こちらが元写真です。ゴンドラ遊覧中に撮ったので、マーカーも水路上にあります。
ヴェネツィアは『ARIA』の聖地ということで楽しみにしていたのですが、期待より遥かに幻想的な水の都でした。全力でオススメします。

こちらは不朽の名作『ローマの休日』の聖地であるところのスペイン広場です。行きの飛行機で観ました(ぉぃ

こちらは噴火で滅んだポンペイを見守る猫です。ここにねこが出没します。ねこでした。よろしくおねがいします

反応

家族や友人たちに共有してみたのですが、旅路を追いながら写真を見せつつ思い出を話せたので、だいぶウケ良かったです。この屋台通りを抜けて、このお店でふらっと夕食取って、この道走って集合時間ギリギリだったんだよー、みたいな。
ただJupyterLabのノートブックをエクスポートしたHTMLファイルをそのまま共有したので、エンジニア系の人たちはPythonのコードを読みはじめてしまい、こちらの話をなかなか聞いてくれないという難点はありましたw

手順

JupyterLab上で50行程度のPythonコードを書いて、次のような処理を行いました。
Foliumは業務でも使いはじめているのですが、とてもお手軽ですね。

  • Pillowで画像ファイルを読みこむ。
  • 画像のExifから緯度・経度の情報を抜く。
  • 画像のExifから回転・反転の情報を抜いて適用する。
  • Foliumに緯度・経度の列を食わせてルートを描く。
  • Foliumに緯度・経度とBase64にエンコードした画像を食わせてマーカーを打つ。
  • JupterLabでHTML出力する。

Foliumのマーカーにはimageタグを渡せるのですが、どうもローカルファイルは参照できないようなので、Base64にエンコードするという荒技を使っています。
おかげで独立した単一のHTMLファイルを出力できるので共有するのは楽なのですが、縮小しているとはいえ1000枚ほどの画像をぶち込んでいるので、100MBほどのHTMLファイルになりました……w

コード

import base64
import folium
import glob
import pandas as pd
from io import BytesIO
from matplotlib import pyplot as plt
from PIL import ExifTags, Image, ImageOps
def to_deg(v, ref, pos):
    d = float(v[0][0]) / float(v[0][1])
    m = float(v[1][0]) / float(v[1][1])
    s = float(v[2][0]) / float(v[2][1])
    return (d + (m / 60.0) + (s / 3600.0)) * (1 if ref == pos else -1)
to_trans_methods = {
    1: [],
    2: [Image.FLIP_LEFT_RIGHT],
    3: [Image.ROTATE_180],
    4: [Image.FLIP_TOP_BOTTOM],
    5: [Image.FLIP_LEFT_RIGHT, Image.ROTATE_90],
    6: [Image.ROTATE_270],
    7: [Image.FLIP_LEFT_RIGHT, Image.ROTATE_270],
    8: [Image.ROTATE_90]
}
files = glob.glob('/path/to/*.jpg')
rows = []
for file in files:
    with Image.open(file) as im:
        exif = {ExifTags.TAGS[k]: v for k, v in im.getexif().items() if k in ExifTags.TAGS}
        if 'GPSInfo' in exif:
            gps = {ExifTags.GPSTAGS[k]: v for k, v in exif['GPSInfo'].items() if k in ExifTags.GPSTAGS}
            lat = to_deg(gps['GPSLatitude'], gps['GPSLatitudeRef'], 'N')
            lon = to_deg(gps['GPSLongitude'], gps['GPSLongitudeRef'], 'E')
            im.thumbnail((192, 192))
            for method in to_trans_methods[exif.get('Orientation', 1)]:
                im = im.transpose(method)
            buf = BytesIO()
            im.save(buf, format="png")
            rows.append([lat, lon, exif['DateTimeOriginal'], base64.b64encode(buf.getvalue()).decode()])

df = pd.DataFrame(rows, columns=['lat', 'lon', 'dt', 'base64'])
df['dt'] = pd.to_datetime(df['dt'], format='%Y:%m:%d %H:%M:%S')
df = df.sort_values('dt')
fmap = folium.Map(location=[df['lat'].mean(), df['lon'].mean()], zoom_start=6)
hsv=[plt.get_cmap('hsv', 12)(i) for i in range(12)]
fmap.add_child(folium.ColorLine(zip(df['lat'], df['lon']), colors=df['dt'].dt.day, colormap=hsv, weight=4))
for _, row in df.iterrows():
    fmap.add_child(folium.Marker([row['lat'], row['lon']], popup=f'<img src="data:image/png;base64,{row["base64"]}">'))
fmap

実行環境

$ python --version
Python 3.7.4

$ pip list | grep -e folium -e jupyter -e matplotlib -e pandas -e Pillow
folium               0.10.1    
jupyter-client       5.3.3     
jupyter-core         4.5.0     
jupyterlab           1.1.4     
jupyterlab-server    1.0.6     
matplotlib           3.1.2     
pandas               1.0.1     
Pillow               7.0.0     

参考リンク

Pythonで写真に埋め込まれているGPS情報から撮影場所を調べよう | マイナビニュース
PILでEXIF Orientationタグを考慮して処理 | Qiita
View image on popup | python-visualization/folium