見出し画像

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

前回の続きです。

今回の記事までの処理で、例えば月ごとにどの焦点距離を頻繁に使っているかなど集計できるようになります。

月ごとの焦点距離ごとの撮影枚数

前回までで、一応データフレームができましたが、まだ扱いづらい形になっています。具体的に以下の問題あり、それを今回解決します。

  • 型が適切ではなく集計や可視化できない列がある
    ⇒適切な型に変換

  • 露出プログラム等、値が番号表記で、何を示しているかわからない
    ⇒その番号が示す露出プログラムの名前に変換

最後おまけで、「広角」、「望遠」など焦点距離に応じたカテゴリを設定します。


型の変換

データフレーム内のデータの型を確認してみます。 

# いろいろな方の要素が混ざっているとdtypeはobjectになる
# object型の列にどんな型の要素が入っている確認する
type_list_dict = {}
for col in exif_df.columns:
    type_list_dict[col] = exif_df[col].map(lambda x:type(x)).unique()
type_list_dict
{'path': array([<class 'pathlib.WindowsPath'>], dtype=object),
 'DateTimeOriginal': array([<class 'str'>], dtype=object),
 'DateTimeDigitized': array([<class 'str'>], dtype=object),
 'FocalLength': array([<class 'PIL.TiffImagePlugin.IFDRational'>], dtype=object),
 'SceneCaptureType': array([<class 'int'>], dtype=object),
 'ExposureTime': array([<class 'PIL.TiffImagePlugin.IFDRational'>], dtype=object),
 'FNumber': array([<class 'PIL.TiffImagePlugin.IFDRational'>], dtype=object),
 'ExposureProgram': array([<class 'int'>], dtype=object),
 'ISOSpeedRatings': array([<class 'int'>], dtype=object),
 'LensModel': array([<class 'str'>], dtype=object),
 'FocalLengthIn35mmFilm': array([<class 'int'>], dtype=object),
 'GPSVersionID': array([<class 'bytes'>], dtype=object),
 'GPSStatus': array([<class 'str'>], dtype=object),
 'GPSMapDatum': array([<class 'str'>], dtype=object),
 'GPSDifferential': array([<class 'int'>], dtype=object),
 'GPSLatitudeRef': array([<class 'float'>, <class 'str'>], dtype=object),
 'GPSLatitude': array([<class 'float'>, <class 'tuple'>], dtype=object),
 'GPSLongitudeRef': array([<class 'float'>, <class 'str'>], dtype=object),
 'GPSLongitude': array([<class 'float'>, <class 'tuple'>], dtype=object),
 'GPSTimeStamp': array([<class 'float'>, <class 'tuple'>], dtype=object),
 'GPSMeasureMode': array([<class 'float'>, <class 'str'>], dtype=object),
 'GPSDateStamp': array([<class 'float'>, <class 'str'>], dtype=object)}

文字列や独自の型はpandasで数値的な集計や可視化ができないため、pandasで集計可能な形へ変換します

  • DateTime系:文字列
    ⇒datetime型に変換

  • FlocalLength, ExposureTime, FNumber:PIL独自の型
    ⇒int型、float型に変換

  • GPS情報:タプル
    ⇒float型に変換

DateTime系

文字列として入っている

DateTime系のカラムはただの文字列になっているのでdatetime型に変換します。ついでに、Subsec(ミリ秒)情報も追加します。

# 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')
    elif time in exif_df.columns: # なければそのままdatetime化
        exif_df[time] = pd.to_datetime(exif_df[time],format='%Y:%m:%d %H:%M:%S')
datetime型へ変換後

焦点距離・露光時間・F値


数値が表示されるが中身はPIL独自の型

FocalLength, ExposureTime, FNumberはPIL独自の型になっています。中でFractionクラスが使われており、分数として扱うことができるようです。Fractionについては以下のサイトが参考になります。

普通にfloat型に変換できますし、
.realをつけてstr()で囲むと分数表記の文字列にも変換できます。

分数表記の文字列として出力

焦点距離はint、F値と露光時間はfloat型に変換。分数文字列で示した露光時間はShutterSpeedという列を作って格納することにしました。

exif_df["FocalLength"] = exif_df["FocalLength"].astype(int)
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) # 数値
型変換後

GPS系

度分秒のタプル

度分秒のタプルが入っています。
これを小数ありの度表記に変換します。
こちらのサイトが参考になります。

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

さらに、GPSLatitudeRefとGPSLongitudeRefにそれぞれ、北緯か南緯か、東経か西経かを示す情報が入っているので、それも用いて正の値にするか負の値にするか決めます。

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)
floatの度形式に変換後

露出プログラム等

番号表記で内容がわからない

露出プログラム、撮影シーンタイプは、番号表記のためぱっと見何が何かわかりません。番号と対応する名前の辞書を作成し、番号から名前に変換します。

MODE_DICT = {
    "ExposureProgram":{
        0:"未定義",
        1:"マニュアル",
        2:"ノーマルプログラム",
        3:"絞り優先",
        4:"シャッター優先",
        5:"creativeプログラム",  # 被写界深度方向にバイアス
        6:"actionプログラム",  # シャッタースピード高速側にバイアス
        7:"ポートレイトモード",  # クローズアップ撮影、背景フォーカス外す
        8:"ランドスケープモード",  # landscape撮影、背景はフォーカス合う
    },
    "SceneCaptureType":{
        0:"標準",
        1:"風景",
        2:"人物",
        3:"夜景"
    }
}
# カテゴリ情報をカラムに関して、番号をカテゴリ名に変換
for key in MODE_DICT.keys():
    if(key in exif_df.columns):
        exif_df[key] = exif_df[key].map(MODE_DICT[key])


番号に対応した名称に変更後

焦点距離等をカテゴリ分け

焦点距離等は値の範囲ごとに「広角」や「望遠」といったカテゴリ名がついていた方がわかりやすいかと思い、数値をカテゴリ名に変換する処理を追加しています。数値範囲は何となくで決めています。

def categorize_focal_length(focal_length):
    if(focal_length<24):
        return "超広角(~23mm)"
    elif(focal_length<35):
        return "広角(24~34mm)"
    elif(focal_length<100):
        return "標準(35~99mm)"
    elif(focal_length<300):
        return "望遠(100~299mm)"
    else:
        return "超望遠(300~mm)"
    
def categorize_exposure_time(x):
    if(pd.isna(x)):
        return np.nan
    if(x<=1/1000):
        return "~1/1000sec"
    elif(x<1):
        return "1/800~1/10sec"
    else:
        return "1/8~sec"
    
def categorize_f_number(x):
    if(pd.isna(x)):
        return np.nan
    if(x<4):
        return "~F3.5"
    elif(x<8):
        return "F4~F7.1"
    elif(x<13):
        return "F8~F11"
    else:
        return "F13~"

exif_df["FocalLengthCategory"] = exif_df["FocalLength"].map(categorize_focal_length)
exif_df["ExposureTimeCategory"] = exif_df["ExposureTime"].map(categorize_exposure_time)
exif_df["FNumberCategory"] = exif_df["FNumber"].map(categorize_f_number)

可視化

最後に、今回変換したデータで可視化してみます。

import plotly.express as px
flength_cnt = exif_df.groupby([pd.Grouper(key='DateTimeOriginal',freq='MS'),"FocalLengthCategory"]).size().rename("count").reset_index()
cat_order = ['超広角(~23mm)','広角(24~34mm)','標準(35~99mm)', '望遠(100~299mm)','超望遠(300~mm)']
px.bar(flength_cnt,x='DateTimeOriginal',y='count',color='FocalLengthCategory',category_orders={"FocalLengthCategory":cat_order},width=1000)
月ごとの焦点距離ごとの撮影枚数

自分的には広角多めかと思っていましたが、意外と標準域をよく使っているという気づきが得られました。
4月は野鳥を撮りに行ったので望遠が多めです。

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

カメラのたのしみ方

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