Pythonを使用した写真の地理位置データの視覚化

May 08 2022
最新のGPS対応カメラや携帯電話のほとんどは、写真が撮影されたときに地理位置情報を記録し、この情報は他のすべてのメタデータと一緒に保存されます。このメタデータを使用してさまざまな視覚化を作成できる、通常はWebベースのアプリケーションもたくさんあります。

最新のGPS対応カメラや携帯電話のほとんどは、写真が撮影されたときに地理位置情報を記録し、この情報は他のすべてのメタデータと一緒に保存されます。

このメタデータを使用してさまざまな視覚化を作成できる、通常はWebベースのアプリケーションもたくさんあります。しかし、あなたが私のようで、データセキュリティのレベルが不明なサードパーティのサービスにあなたの写真を信頼していない場合はどうなりますか?幸い、これらの視覚化のほとんどは、Pythonを使用して複製するのが比較的簡単です。このガイドでは、写真のGPSデータを使用して旅行地図を作成する方法を示します。

このプロジェクトでは、Python環境にいくつかのパッケージをインストールする必要があります。2つの主要なパッケージを使用します。exifを使用すると写真からメタデータを抽出でき、Foliumを使用て地図を作成してロケーションマーカーを追加します。最後に、 matplotlibのカラーマップ、およびデータ操作にpandasnumpyも使用します。 必要なすべてのパッケージをインストールするには、ターミナルで次のコマンドを実行するだけです。

pip install exif folium pandas numpy matplotlib

import json
from pathlib import Path
import folium
import matplotlib
import numpy as np
import pandas as pd
from exif import Image

exifを使用したメタデータの読み取りは非常に簡単です。ファイルをバイナリ形式で開き、そこからImageオブジェクトを作成するだけです。このための便利な関数を定義しましょう:

def read_exif_data(file_path: Path) -> Image:
    """Read metadata from photo."""
    with open(file_path, 'rb') as f:
        return Image(f)

BASE_LOC = Path("/media/pav/Storage/Photo/Phone")
file = Path("IMAG0036.jpg")
img = read_exif_data(base_loc / file)
print('\n'.join([i for i in img.list_all()
                 if i.startswith('gps_')]))

gps_latitude_ref 
gps_latitude 
gps_longitude_ref 
gps_longitude 
gps_altitude_ref 
gps_altitude 
gps_timestamp 
gps_processing_method 
gps_datestamp

_ref で終わるカテゴリ には、データの解釈方法に関する参照情報が含まれています。高度の場合、値が海抜高度であることを通知するだけですが、緯度と経度の場合は参照半球の情報、経度の場合は「E」または「W」、「N」または「S」が含まれます。緯度について。標準表記ではグリニッジの西と赤道の南の座標が負であるため、これらは生データを10進形式に変換するときに重要です。

foliumは度/分/秒の座標を理解しないため、緯度と経度を10進表現に変換する必要があります。また、何度も変換するため、次の関数を定義することで、作業を楽にし、コードをクリーンにすることができます。座標のタプルと参照値を取り、そのバイナリ表現を返します。このために、分と秒をそれぞれ60と3600で割って、分数に変換します。次に、参照データに基づいて符号が決定されます。

def convert_coords_to_decimal(
    coords: tuple[float,...], 
    ref: str
) -> float:
    if ref.upper() in ['W', 'S']:
        mul = -1
    elif ref.upper() in ['E', 'N']:
        mul = 1
    else:
        print("Incorrect hemisphere reference. "
              "Expecting one of 'N', 'S', 'E' or 'W', "
              f"got {ref} instead.")
    return mul * (coords[0] + coords[1] / 60 + coords[2] / 3600)

def get_decimal_coord_from_exif(exif_data: Image
                                ) -> tuple[float, ...]:
    try:
        lat = convert_coords_to_decimal(
            exif_data['gps_latitude'],
            exif_data['gps_latitude_ref']
            )
        lon = convert_coords_to_decimal(
            exif_data['gps_longitude'],
            exif_data['gps_longitude_ref']
            )
        alt = exif_data['gps_altitude']
        return (lat, lon, alt)
    except (AttributeError, KeyError):
        print('Image does not contain spatial data '
              'or data is invalid.')
        raise

def read_spatial_data_from_folder(
    folder: Path,
    image_extension: str = '*.jpg'
    ) -> dict[str, dict]:
    coord_dict = dict()
    source_files = [f for f in folder.rglob(image_extension)]
    exif = [read_exif_data(f) for f in source_files]
    
    for f, data in zip(source_files, exif):
    try:
        coord = get_decimal_coord_from_exif(data)
    except (AttributeError, KeyError):
        continue
    else:
        coord_dict[str(f)] = dict()
        coord_dict[str(f)]['latitude'] = coord[0]
        coord_dict[str(f)]['longitude'] = coord[1]
        coord_dict[str(f)]['altitude'] = coord[2]
  
    # Also read date when photo was taken (if available)
    try:
        coord_dict[str(f)]['timestamp'] = data.datetime
    except (AttributeError, KeyError):
        print(f"Photo {f.name} does not contain datetime information.")
        coord_dict[str(f)]['timestamp'] = None
    return coord_dict

res = read_spatial_data_from_folder(BASE_LOC)

print(json.dumps(res, indent=4))
Out:
{     
    "/media/pav/Storage/Photo/Phone/IMAG0036.jpg": {
        "latitude": -37.79912566666666,         
        "longitude": 144.9850463611111,         
        "altitude": 0.0,         
        "timestamp": "2014:04:12 18:14:59"},  
    "/media/pav/Storage/Photo/Phone/IMAG0037.jpg": { 
        "latitude": -37.79912566666666,         
        "longitude": 144.9850463611111,         
        "altitude": 0.0,         
        "timestamp": "2014:04:12 18:15:36"},
    ...
}

将来の操作を簡素化するために、結果の辞書をpandasDataFrameに変換できます。次に、タイムスタンプ列を日時形式に変換し、値を日付の昇順で並べ替えます。

データポイントを色分けするために、matplotlibパッケージの一部として提供されるカラーマップの1つを取得できます。これに加えて、Normalizeオブジェクトも作成する必要があります。このオブジェクトは、0から1までのカラーマップの個々の色に数値を割り当て、その数値によって色空間の任意の色を呼び出すことができます。このために、 numpy.linspace()を使用してデータフレームに列を追加します。

小さな問題の1つは、色がRGBA形式になることですが、葉では機能しないため、色を16進表現に変換する必要があります。これは簡単に実行できます。

def rgba_to_hex(rgba: tuple[float, ...]):
    return ('#{:02X}{:02X}{:02X}').format(*rgba[:3])

foliumパッケージの主なオブジェクトはMap()であり、folium.Map()を介して呼び出すだけで、全世界のマップが作成されます。

デフォルトのフォリウムマップ。

これは問題ありませんが、すべてのマーカーが表示されるレベルにマップを自動的にズームすることをお勧めします。これは、Mapクラスのfit_bounds()メソッドを使用し、マップ範囲の南西および北東のコーナーの座標を提供することで実現できます。

データから最大緯度と最小経度を取得することで、南西の角の座標を計算できます。同様に、北東の角の座標は最小緯度と最大経度です。以下のコードでは、データがウィンドウ内にきちんと収まるように、バウンディングボックスを各方向に3度調整しました。

sw = (df.latitude.max() + 3, df.longitude.min() - 3)
ne = (df.latitude.min() - 3, df.longitude.max() + 3)

m = folium.Map(location=[df.latitude.mean(), df.longitude.mean()])
# Add markers
for lat, lon, col, date in zip(df.latitude.values,
                               df.longitude.values, 
                               df.color_mapping.values, 
                               df.timestamp.values):
folium.CircleMarker(
    [lat, lon],
    color=rgba_to_hex(cmap(col, bytes=True)),
    fill_color=rgba_to_hex(cmap(col, bytes=True)),
    radius=6,
    tooltip = np.datetime_as_string(date, unit='m')
    ).add_to(m)
m.fit_bounds([sw, ne])
m

結論

ここでは、写真のメタデータが提供するすべての機会のごく一部にのみ触れました。このデータを使用して、写真の独自のデータベースを作成し、写真を簡単に分類および管理できます。また、ジオロケーションデータの使用を拡張して、画像フォルダ内の場所で写真を検索できるようにすることもできます。他にも多くの機能が利用できます。

このプロジェクトの完全なコードは、次の場所にある私のGitHubからダウンロードできます。https://github.com/pavelcherepan/photo_location。

© Copyright 2021 - 2022 | hachiwiki.com | All Rights Reserved