見出し画像

撮影情報(Exif)をPythonで集計する(3)

前回の続きですが、最後にコピペすれば動くコードをまとめて掲載したので、この記事だけ読むでも大丈夫です。
また、今までのコードに一部不足があったため修正しています。

最後のまとめコードを実行すると以下のようなデータフレームを得ることができます。

Exif情報のデータフレーム

Exifの構造について

前回までの実装だと、ModelやSoftwareなんかもExif IFDタグにあると勘違いした実装になっていて、これらの情報が取れていなかったのですが、
正しくは以下のような構造になっています。

Exifの構造

以下の二つの記事が参考になります。

この構成を念頭においてコードを修正しました。

実装

使用するタグの辞書の作成

Exifの取得方法の修正の前に、PILのバージョンを上げてタグの参照の仕方を少し変えました。PIL 9.4.0からタグ番号をもつ列挙型クラスが追加されたので、これを使うようにしています。タグ番号を直に書くよりこちらの方が可読性が高いと思います。

IFDごとに使用するタグの辞書を作っておきます。

from PIL.ExifTags import Base,GPS,IFD,TAGS,GPSTAGS 

use_tag_ids = {
    "0th IFD":[
        Base.DateTime,
        Base.Model,
        Base.Software,
        Base.Orientation
    ],
    "Exif IFD":[
        Base.DateTimeOriginal,
        Base.DateTimeDigitized,
        Base.SubsecTime,
        Base.SubsecTimeOriginal,
        Base.SubsecTimeDigitized,
        Base.FNumber,
        Base.ExposureTime,
        Base.ISOSpeedRatings,
        Base.FocalLength,
        Base.FocalLengthIn35mmFilm,
        Base.ExposureProgram,
        Base.SceneCaptureType,
        Base.LensModel
    ],
    "GPS Info IFD":[
        GPS.GPSLatitude,
        GPS.GPSLatitudeRef,
        GPS.GPSLongitude,
        GPS.GPSLongitudeRef
    ]
}

Exif情報の読み込み

0th IFD、EXif IFD, GPSInfo IFDそれぞれの辞書から必要な情報に限定して抽出しています。また、タグ番号はタグ名称に置き換えるようにしています。

def load_exif(path):
    with Image.open(path) as im:
        exif = im.getexif()
    # 各IFDの情報を必要なタグだけ取得
    zeroth_ifd = {TAGS[tag_id]: value for tag_id, value in exif.items() 
                        if tag_id in use_tag_ids["0th IFD"]} 
    exif_ifd = {TAGS[tag_id]: value for tag_id, value in exif.get_ifd(IFD.Exif).items() 
                        if tag_id in use_tag_ids["Exif IFD"]} 
    gps_ifd = {GPSTAGS[tag_id]: value for tag_id, value in exif.get_ifd(IFD.GPSInfo).items()
                        if tag_id in use_tag_ids["GPS Info IFD"]} 
    exif_dict = dict(**zeroth_ifd,**exif_ifd,**gps_ifd) # 辞書の連結
    exif_dict["path"] = path
    
    return exif_dict

コードまとめ

これまでのコードをまとめて掲載します。これをコピペして、写真フォルダのパスを指定すればExif情報のデータフレームが得られます。
Pillowのバージョンは10.0.0で検証しましたが、おそらく9.4.0以上で動くはずです。

import pandas as pd
import numpy as np
from pathlib import Path
from PIL import Image
from PIL.ExifTags import Base,GPS,IFD,TAGS,GPSTAGS 
from tqdm import tqdm

use_tag_ids = {
    "0th IFD":[
        Base.DateTime,
        Base.Model,
        Base.Software,
        Base.Orientation
    ],
    "Exif IFD":[
        Base.DateTimeOriginal,
        Base.DateTimeDigitized,
        Base.SubsecTime,
        Base.SubsecTimeOriginal,
        Base.SubsecTimeDigitized,
        Base.FNumber,
        Base.ExposureTime,
        Base.ISOSpeedRatings,
        Base.FocalLength,
        Base.FocalLengthIn35mmFilm,
        Base.ExposureProgram,
        Base.SceneCaptureType,
        Base.LensModel
    ],
    "GPS Info IFD":[
        GPS.GPSLatitude,
        GPS.GPSLatitudeRef,
        GPS.GPSLongitude,
        GPS.GPSLongitudeRef
    ]
}

MODE_DICT = {
    "ExposureProgram":{
        0:"未定義",
        1:"マニュアル",
        2:"ノーマルプログラム",
        3:"絞り優先",
        4:"シャッター優先",
        5:"creativeプログラム",  # 被写界深度方向にバイアス
        6:"actionプログラム",  # シャッタースピード高速側にバイアス
        7:"ポートレイトモード",  # クローズアップ撮影、背景フォーカス外す
        8:"ランドスケープモード",  # landscape撮影、背景はフォーカス合う
    },
    "SceneCaptureType":{
        0:"標準",
        1:"風景",
        2:"人物",
        3:"夜景"
    }
}

def load_exif(path):
    with Image.open(path) as im:
        exif = im.getexif()
    # 各IFDの情報を必要なタグだけ取得
    zeroth_ifd = {TAGS[tag_id]: value for tag_id, value in exif.items() 
                        if tag_id in use_tag_ids["0th IFD"]} 
    exif_ifd = {TAGS[tag_id]: value for tag_id, value in exif.get_ifd(IFD.Exif).items() 
                        if tag_id in use_tag_ids["Exif IFD"]} 
    gps_ifd = {GPSTAGS[tag_id]: value for tag_id, value in exif.get_ifd(IFD.GPSInfo).items()
                        if tag_id in use_tag_ids["GPS Info IFD"]} 
    exif_dict = dict(**zeroth_ifd,**exif_ifd,**gps_ifd) # 辞書の連結
    exif_dict["path"] = path
    
    return exif_dict

def convert_exif_cols(exif_df):
    # datetime型への変換
    time_subsec = [("DateTime",         "SubsecTime"),  # 日付系のカラム名と対応するSubsec(ミリ秒情報)のカラム名
                    ("DateTimeOriginal", "SubsecTimeOriginal"),
                    ("DateTimeDigitized","SubsecTimeDigitized")]
    for time,subsec in time_subsec:
        if subsec in exif_df.columns: # ミリ秒情報があれば日付情報にマージしdatetime化
            exif_df[time] = exif_df[time].astype(str).replace("nan","")+"."\
                            +exif_df[subsec].astype(str).replace("nan","0")# 日付と小数点以下を"."で連結
            exif_df[time] = exif_df[time].replace(".0",np.nan) # 日付自体が欠損の場合↑の処理によって".0"だけになるので欠損にする
            exif_df[time] = pd.to_datetime(exif_df[time],format='%Y:%m:%d %H:%M:%S.%f',errors='coerce')
        elif time in exif_df.columns: # なければそのままdatetime化
            exif_df[time] = pd.to_datetime(exif_df[time],format='%Y:%m:%d %H:%M:%S')

    exif_df["FocalLength"] = exif_df["FocalLength"].astype(float)
    exif_df["FNumber"] = exif_df["FNumber"].astype(float)
    exif_df["ShutterSpeed"] = exif_df["ExposureTime"].map(lambda x:str(x.real)) # 分数表記
    exif_df["ExposureTime"] = exif_df["ExposureTime"].astype(float) # 数値

    # カテゴリ情報をカラムに関して、番号をカテゴリ名に変換
    for key in MODE_DICT.keys():
        if(key in exif_df.columns):
            exif_df[key] = exif_df[key].map(MODE_DICT[key])

    # GPS情報の変換(度分秒のタプル→度)
    exif_df["GPSLatitude"]  = exif_df["GPSLatitude"].map(dms2deg).astype(float)
    exif_df["GPSLongitude"] = exif_df["GPSLongitude"].map(dms2deg).astype(float)
    exif_df["GPSLatitude"]  =  exif_df["GPSLatitude"]*exif_df["GPSLatitudeRef"].replace("N",1).replace("S",-1)
    exif_df["GPSLongitude"] =  exif_df["GPSLongitude"]*exif_df["GPSLongitudeRef"].replace("E",1).replace("W",-1)

    # 欠損の場合0が入るみたいなので改めて欠損に置換
    int_cols = ["FocalLength","FocalLengthIn35mmFilm"]
    exif_df[int_cols] = exif_df[int_cols].replace(0,pd.NA)
    float_cols = ["FNumber","ExposureTime","GPSLatitude","GPSLongitude"]
    exif_df[float_cols] = exif_df[float_cols].replace(0,np.nan)

    return exif_df

# GPSデータの処理に使用
def dms2deg(x):
    # 緯度経度の度分秒フォーマットを度に変換
    return x[0]+x[1]/60+x[2]/3600 if type(x)==tuple else np.nan

def categorize_focal_length(x):
    # 参考
    # https://ptl.imagegateway.net/contents/original/glossary/標準レンズ、広角レンズ、望遠レンズ.html
    # https://av.jpn.support.panasonic.com/support/dsc/knowhow/knowhow21.html
    # https://goopass.jp/magazine/300mmsupertelephotoens10/
    if(pd.isna(x)):
        return np.nan
    elif(x<24):
        return "超広角(~23mm)"
    elif(x<35):
        return "広角(24~34mm)"
    elif(x<100):
        return "標準(35~99mm)"
    elif(x<300):
        return "望遠(100~299mm)"
    elif(x>=300):
        return "超望遠(300~mm)"
    
def categorize_exposure_time(x):
    if(pd.isna(x)):
        return np.nan
    elif(x<=1/1000):
        return "~1/1000sec"
    elif(x<1):
        return "1/800~1/10sec"
    elif(x>=1):
        return "1/8~sec"
    
def categorize_f_number(x):
    # 参考
    # https://photobook.ikuji-park.com/f-number.html
    if(pd.isna(x)):
        return np.nan
    elif(x<4):
        return "~F3.5"
    elif(x<8):
        return "F4~F7.1"
    elif(x<13):
        return "F8~F11"
    elif(x>=13):
        return "F13~"

def add_extra_cols(exif_df):
    exif_df["FocalLengthCategory"] = exif_df["FocalLengthIn35mmFilm"].map(categorize_focal_length)
    exif_df["ExposureTimeCategory"] = exif_df["ExposureTime"].map(categorize_exposure_time)
    exif_df["FNumberCategory"] = exif_df["FNumber"].map(categorize_f_number)
    return exif_df

def get_exif_df(path_list):
    exif_dict_list = [load_exif(path) for path in tqdm(path_list)]
    exif_df = pd.DataFrame(exif_dict_list)
    exif_df = convert_exif_cols(exif_df) # 型変換
    exif_df = add_extra_cols(exif_df) # カテゴリカラム追加
    return exif_df

# 既存のexif_dfがあれば、path_listからはまだ存在しないpathだけ読み込んで追加する
def get_exif_df_add(path_list,existing_exif_df=None):
    additional_path_list = sorted(set(path_list)-set(existing_exif_df["path"]))
    additional_exif_df = get_exif_df(additional_path_list)
    exif_df = pd.concat([existing_exif_df,additional_exif_df],ignore_index=True)
    return exif_df

使用例

root_path = Path(r"C:\hoge\PhotoFolder") # 写真フォルダを指定
path_list = list(root_path.glob("**/*.JPG")) # jpegファイル一覧を取得
exif_df = get_exif_df(path_list) # Exifのデータフレームを取得



この記事が参加している募集

#カメラのたのしみ方

55,011件

この記事が気に入ったらサポートをしてみませんか?