カメラのExifデータから撮影傾向を分析する~データ分析編

※当サイトはアフィリエイト広告を利用しています

データ分析

フォルダに格納されている複数の画像ファイル(RAWデータ、JPEGデータ)からExifデータを収集/分析します。撮影データのExif情報は個々の画像で確認するのは簡単ですが、複数の画像データから傾向を見るにはPythonなどツールの利用が必要です。
今回は取得したExifデータの加工、可視化を進めていきます。

Exifデータの取得は下記記事を参照してください。

データの加工

import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
import numpy as np

#exifデータのロード
exif_df = pd.read_csv('exif_data.csv')

#必要なデータの取捨選択
DATA = exif_df[['FileName','DateTimeOriginal','FileType','Model','LensModel','ExposureTime','FNumber','ISO',
                'FocalLength','FocusDistanceUpper','FocusDistanceLower','CanonExposureMode','FocusMode','AFAreaMode',
                'ContinuousDrive','SelfTimer','Flash','Orientation','CameraTemperature','ShutterMode','ImageStabilization']]
DATA = DATA[DATA['FileType']=='CR3']

前回取得したデータをロードし、データの取捨選択をします。

今回細かな機材のデータを利用するため、RAWデータの「.CR3」ファイルのみを対象にしています。

分析/可視化

カメラ機種別の最大シャッター

DATA.groupby('Model')['ShutterCount'].max().reset_index()

Output

EOS KISS Mにはシャッターカウントのデータがありませんでしたので、R6のみデータが表示されます。20,133回とのことでした。

年間撮影枚数

年間の撮影枚数の推移を確認できます。今回はカメラの機材ごとにカウントし色分けしてみました。

# フォントの設定
plt.rcParams['font.family'] = 'Meiryo'  # 日本語フォントを設定

# DateTimeOriginalのフォーマット修正
DATA['DateTimeOriginal'] = pd.to_datetime(DATA['DateTimeOriginal'], format='%Y:%m:%d %H:%M:%S', errors='coerce')

# 年を抽出
DATA['Year'] = DATA['DateTimeOriginal'].dt.year

# 年とモデルごとの撮影枚数を集計
yearly_model_data = DATA.groupby(['Year', 'Model']).size().unstack(fill_value=0)

# カラーマップの設定
colors = plt.cm.Paired.colors

# グラフを作成
fig, ax = plt.subplots(figsize=(12, 6.75))
yearly_model_data.plot(kind='bar', stacked=True, ax=ax, color=colors)

# 各棒の合計値を計算して表示(コンマ区切りの数値表示)
totals = yearly_model_data.sum(axis=1)
for i, total in enumerate(totals):
    ax.text(i, total + 5, f'{total:,}', ha='center', fontsize=10, weight='bold') 

# 棒グラフの各セグメントに数値を表示(コンマ区切りの数値表示)
for container in ax.containers:
    ax.bar_label(container, label_type='center', fmt='{:,.0f}')

# タイトルやラベルに日本語を使用
plt.title('年間撮影枚数', fontsize=16)
plt.xticks(rotation=45)
plt.legend(title='機材', bbox_to_anchor=(1.05, 1), loc='upper left')
plt.tight_layout()
plt.show()

Output

毎年4,000~5,000枚ほど撮影しているようです。写真を上達するには年間1万枚ほど撮影すべし、という何の根拠もない話もありますが、もう少し撮影したいところです。

F値/SS/ISO

撮影の基本となるF値/SS/ISOの傾向を分析します。

import matplotlib.pyplot as plt
import pandas as pd
import fractions

# グラフ描画とパーセント表示を行う関数
def plot_bar(ax, data_percent, labels, title, ylabel, color, threshold=5, rotation=0):
    ax.bar(labels, data_percent.values, color=color, edgecolor='black')
    ax.set_title(title, fontsize=10)
    ax.set_ylabel(ylabel, fontsize=8)
    ax.tick_params(axis='x', rotation=rotation, labelsize=8)

    # 指定のしきい値を超えるパーセンテージを表示
    for i, value in enumerate(data_percent.values):
        if value > threshold:
            ax.text(i, value + 0.5, f'{value:.1f}%', ha='center', fontsize=8)

# 各設定値に対する頻度を計算し、パーセントに変換
fig, axs = plt.subplots(3, 1, figsize=(12, 6.75))

# F値の処理
fnumber_counts = DATA['FNumber'].value_counts().sort_index()
fnumber_percent = (fnumber_counts / fnumber_counts.sum()) * 100
plot_bar(axs[0], fnumber_percent, fnumber_percent.index.astype(str), 'F-Number', 'Percentage (%)', 'lightcoral')

# 露出時間の処理
DATA['ExposureTime'] = pd.to_numeric(DATA['ExposureTime'].apply(lambda x: eval(x) if '/' in str(x) else x), errors='coerce')
DATA['ExposureTime'] = DATA['ExposureTime'].apply(lambda x: int(x) if x >= 1 else x)

exposure_counts = DATA['ExposureTime'].value_counts().sort_index()
exposure_percent = (exposure_counts / exposure_counts.sum()) * 100

# 露出時間を分数に変換して表示
exposure_labels = [str(fractions.Fraction(x).limit_denominator()) if x < 1 else str(int(x)) for x in exposure_percent.index]
plot_bar(axs[1], exposure_percent, exposure_labels, 'Exposure Time', 'Percentage (%)', 'lightgreen', rotation=90)

# ISOの処理
DATA['ISO'] = DATA['ISO'].astype(int)
iso_counts = DATA['ISO'].value_counts().sort_index()
iso_percent = (iso_counts / iso_counts.sum()) * 100
plot_bar(axs[2], iso_percent, iso_percent.index.astype(str), 'ISO', 'Percentage (%)', 'skyblue', rotation=45)

# グラフ間の余白を調整
plt.subplots_adjust(hspace=0.35)
plt.show()

Output

F値については、普段利用しているレンズがRF24-105 F4LなのでF4.0にピークがきています。また風景写真を撮ることが多いのでF8.0も多く撮影しているようです。

SSについては標準域で手振れの影響が小さくなる1/80~1/60を多用しているようです。一方夜景の長秒露光のため1秒以上のものも存在しています。

ISOは風景写真がメインのためISO100が4割ほどを占めています。
夜景や風景をきれいに撮りたいので高感度耐性が重要と考えていましたが、意外と高感度での撮影は少なく、ISO1600(もしくは6400)程度まででノイズが少なければ良さそうです。
今後メイン機をフルサイズからAPS-Cに戻してもよいかもしれません。

焦点距離

# 焦点距離を数値に変換
DATA['FocalLength_35mm'] = pd.to_numeric(DATA['FocalLength'].astype(str).str.replace(' mm', '', regex=True), errors='coerce')

# APS-C機の焦点距離を1.6倍に修正
DATA.loc[DATA['Model'] == 'Canon EOS KISS M', 'FocalLength_35mm'] = DATA['FocalLength_35mm'] * 1.6

# 焦点距離を四捨五入して整数に変換
DATA['FocalLength_35mm'] = DATA['FocalLength_35mm'].round().astype('Int64')

# ヒストグラムのデータとbinを取得
n, bins = np.histogram(DATA['FocalLength_35mm'].dropna(), bins=range(int(DATA['FocalLength_35mm'].min()), int(DATA['FocalLength_35mm'].max()) + 2))

# 縦軸をパーセンテージに変換
n_percent = (n / n.sum()) * 100

# ヒストグラムをプロット(パーセンテージ表示)
plt.figure(figsize=(12, 6.75))
plt.bar(bins[:-1], n_percent, width=bins[1]-bins[0], color='lightcoral', edgecolor='black')

plt.title('Focal Length', fontsize=16)
plt.xlabel('Focal Length (mm)', fontsize=12)
plt.ylabel('Percentage (%)', fontsize=12)

# ラベル表示(存在比率が2%以上のものに対して)
for bin_left, bin_right, percent in zip(bins[:-1], bins[1:], n_percent):
    if percent > 2:  # 存在比率が2%以上のものにラベルを表示
        plt.text((bin_left + bin_right) / 2, percent + 0.5, f'{int((bin_left + bin_right) / 2)} mm', ha='center', weight='bold')

plt.tight_layout()
plt.show()

Output

RF24-105 F4Lを常用しているので24mm,105mmにピークが来ています。
35mmはRF35 F1.8、EF-M 22mm F2での短焦点レンズの影響と思われます。
51mm,480mmはAPS-C機(EOS kissM)でEF-M 32mm F1.4、EF70-300mm F4-5.6 IS II USMを利用していたのが影響しています。

Canon 単焦点広角レンズ RF35mm F1.8 マクロ IS STM EOSR対応 RF3518MISSTM
キヤノン
¥73,000(2024/10/12 00:20時点)
開放F1.8の明るさと9枚羽根の円形絞りで、美しいボケ味を実現

撮影距離

まずは’FocusDistanceUpper’と’FocusDistanceLower’の加工を行います。

撮影距離として、’FocusDistanceUpper’が「inf(無限遠)」の場合は99、そうでない場合は’FocusDistanceLower’の数値部分のみを出力するようにしました

# FocusDistanceの加工
def calculate_focus_distance(row):
    if row['FocusDistanceUpper'] == 'inf':
        return 99
    else:
        lower_value = row['FocusDistanceLower']
        if isinstance(lower_value, str):
            return float(lower_value.split()[0])
        else:
            return lower_value

DATA['FocusDistance'] = DATA.apply(calculate_focus_distance, axis=1)

次に撮影距離がどのように分布しているのかを表示させました

plt.figure(figsize=(12, 6.3))
focus_distance_counts = DATA['FocusDistance'].value_counts(normalize=True) * 100

plt.scatter(focus_distance_counts.index, focus_distance_counts.values, s=3, color='blue', alpha=0.6)

for i, (x, y) in enumerate(zip(focus_distance_counts.index, focus_distance_counts.values)):
    if y >= 2:  # Only annotate if the percentage is 2 or more
        plt.text(x, y, f'{y:.1f}%', ha='center', va='bottom',fontsize=8)

plt.xlabel('FocusDistance')
plt.ylabel('Percentage (%)')
plt.title('Scatter Plot of FocusDistance Distribution (in %)')

plt.xticks(rotation=45)
plt.tight_layout()
plt.show()

Output

無限遠のものが27%、そのほかはほぼ10m以内に収まっています。

レンズ別撮影距離

# レンズモデル名の抽出
lens_models = DATA['LensModel'].unique()

lens_models =['RF35mm F1.8 MACRO IS STM','RF50mm F1.8 STM',  'RF85mm F2 MACRO IS STM',
        'RF14-35mm F4 L IS USM','RF24-105mm F4 L IS USM','RF100-400mm F5.6-8 IS USM']

plt.figure(figsize=(12, 6.3))

# レンズ別の累積グラフの作成
for lens in lens_models:
    lens_data = DATA[DATA['LensModel'] == lens]['FocusDistance']
    lens_data_sorted = lens_data.sort_values()
    cumulative_percentage = lens_data_sorted.rank(pct=True) * 100
    plt.plot(lens_data_sorted, cumulative_percentage, marker='o', linestyle='-', alpha=0.7, label=lens, markersize=2)


    # Label the shortest focus distance for each lens
    shortest_focus_distance = lens_data_sorted.min()
    shortest_percentage = cumulative_percentage.loc[lens_data_sorted.idxmin()]
    plt.text(shortest_focus_distance, shortest_percentage + 4, f'{shortest_focus_distance}m', 
             ha='center', va='bottom', fontsize=6)

# グラフの書式設定
plt.xlabel('FocusDistance')
plt.ylabel('Cumulative Percentage (%)')
plt.title('Cumulative Distribution of FocusDistance by LensModel')

plt.legend()
plt.xlim(0, 3)
plt.ylim(0, 100)

plt.tight_layout()
plt.show()

Output

RF35mm、RF14mmなど広角レンズは寄りで撮っているようです。RF35mmは1割弱が0.5m以下で撮影しているようです。おそらくストリートフォトなどスナップ的に写真を撮ることが多いからと思われます。

一方、紫色のRF24-105mmはグラフの立ち上がりが急になっています。これは、レンズの仕様として最短撮影距離 0.45mとなっており、もっと寄りで撮りたかったけど撮れなかったと想定されます。
それまでのグラフの傾きからすると、最短撮影距離が30cm程度あればストレスなく撮影できていたと思われます。

その他設定内容

data_columns = [ 'CanonExposureMode', 'Flash', 'AFAreaMode',  'SelfTimer']
fig_counts_list = [DATA[column].value_counts() for column in data_columns]

# グラフを2行2列に配置
fig, axes = plt.subplots(2, 2, figsize=(12, 6.75))

# 4つのグラフをサブプロットとして描画
for i, ax in enumerate(axes.flat):
    fig_counts = fig_counts_list[i]
    
    # ドーナツグラフを描画
    wedges, texts, autotexts = ax.pie(
        fig_counts,
        labels=fig_counts.index,
        autopct='%1.1f%%',
        startangle=0,  # 開始角度を0度に調整
        colors=plt.cm.Paired.colors,
        wedgeprops={'width': 0.2}  # ドーナツグラフの幅を指定
    )

    # フォントサイズやスタイルの設定
    for autotext in autotexts:
        autotext.set_fontsize(8)

    for text in texts:
        text.set_fontsize(8)
        text.set_weight('bold')

    # 中央にタイトルを追加
    ax.text(0, 0, data_columns[i], horizontalalignment='center', verticalalignment='center', fontsize=12, weight='bold')
    ax.axis('equal')  # 円グラフを円として描画

# グラフを表示
plt.subplots_adjust(wspace=0.2, hspace=0.1)  # 横と縦の余白を調整
plt.show()

Output

左上から撮影モード、AFエリア設定、フラッシュ有無、セルフタイマー設定となっています。

撮影モードは圧倒的にAモード(絞り優先)が多く67%となっています。夜景撮影が多いからか意外とマニュアルモードも22%と多く存在しました。

AFエリア設定は領域拡大AFの利用率が高いようです。3%程マニュアルフォーカスも利用しているようです。

フラッシュ/セルフタイマーはそれぞれ5%弱、4%弱利用しているようです。

カメラ温度

# カメラ温度列のデータ型を確認し、数値変換の前処理を修正
DATA['CameraTemperature'] = DATA['CameraTemperature'].astype(str)
DATA['CameraTemperature'] = pd.to_numeric(DATA['CameraTemperature'].str.replace(' C', '', regex=False), errors='coerce')

# カメラ温度の頻度を計算し、パーセントに変換
temperature_counts = DATA['CameraTemperature'].value_counts().sort_index()
temperature_percent = (temperature_counts / temperature_counts.sum()) * 100

# カメラ温度のヒストグラム作成(存在比率で表示)
plt.figure(figsize=(12, 6.75))
plt.bar(temperature_percent.index, temperature_percent.values, color='lightblue', edgecolor='black')
plt.title('Camera Temperature (Percentage)', fontsize=16)
plt.xlabel('Camera Temperature (°C)', fontsize=12)
plt.ylabel('Percentage (%)', fontsize=12)

# 存在比率の値を各棒に表示
for i in range(len(temperature_percent)):
    if temperature_percent.values[i] > 5:  # 存在比率が1%以上のものを表示
        plt.text(temperature_percent.index[i], temperature_percent.values[i] + 0.1,
                 f'{temperature_percent.values[i]:.1f}%', ha='center')

plt.tight_layout()
plt.show()

Output

カメラの本体温度が32℃程度がピークとなっています。冬の夜景を撮影していたこともあるので10℃以下の数値も存在しました。

最大/最小のレコードは下記のコードで抽出できます。

# 最低値を抽出
DATA[DATA['CameraTemperature'] == DATA['CameraTemperature'].min()]

# 最高値を抽出
DATA[DATA['CameraTemperature'] == DATA['CameraTemperature'].max()]

真冬の深夜に、雪の嵐山を撮影した写真が最低値で3℃でした。

沖縄のビーチで動画撮影後に撮影した写真が最高値で65℃でした。

レンズ別撮影枚数と1撮影当たりのコスト算出

レンズ別に自身の取得コストのマスタをエクセルで作成しました。(LensPrice.csvとして保存)

# レンズ価格データの読み込み
lens_price_data = pd.read_csv('LensPrice.csv')

# 撮影枚数をLensModelごとにカウント
lens_model_counts = DATA['LensModel'].value_counts().reset_index()
lens_model_counts.columns = ['LensModel', 'ShootingCount']

# レンズ価格と撮影枚数を結合
lens_data = pd.merge(lens_model_counts, lens_price_data, on='LensModel', how='left')

# 撮影1枚当たりのレンズ価格を計算
lens_data['PricePerShot'] = lens_data['Price'] / lens_data['ShootingCount']
lens_data['PricePerShot'] = lens_data['PricePerShot'].round(1)
lens_data = lens_data.dropna()

lens_data['ShootingCount'] = lens_data['ShootingCount'].apply(lambda x: f'{int(x):,}')
lens_data['Price'] = lens_data['Price'].apply(lambda x: f'{int(x):,}')
lens_data['PricePerShot'] = lens_data['PricePerShot'].apply(lambda x: f'{x:,}')

# 表を表示
lens_data

Output

RF24-105を常用しているので飛びぬけて撮影枚数が多いですね。1枚当たりのコストも16.6円と最も安くなっています。

一方RF24-240mmはまだ活用しきれておらず40枚しか撮影しておらず、1枚当たりコストが3,341円と高額になっています。

RF14-35 F4L、RF85 F2、RF100-400 F5.6-8、RF24-240 F4-6.3は、コストが数十円レベルになるよう活用の機会を増やすか、思い切って売却するか検討した方が良さそうです。

さいごに

Exifデータの分析により、自身の撮影スタイルを見直す良い機会になりました。
ぜひ皆さんもご自身の撮影データを分析してみてはいかがでしょうか?

この記事を読んだ人がよく見ています

タイトルとURLをコピーしました