Pythonでデジカメ写真のExif情報（レンズ名を含む）を一覧化する

# 目的
デジカメで撮った写真が大量にあるので、どんなレンズやカメラでどれくらい撮っているのか集計したいと考えた。Pythonを使ってExif情報を一覧化してExcelファイルに出力するため、いくつかのライブラリ、ツールを試してみた。

# まとめ
以下の4種の方法で、レンズ名を含む撮影情報の一覧化にトライした。
- (1) Pythonライブラリ PIL
- (2) Pythonライブラリ Pyexiv2
- (3) ExifTool.exe
- (4) Pythonライブラリ PyExifTool

結論としては、
- 撮影日時やカメラ名、撮影時のレンズの焦点距離、F値、ISO、シャッタースピードなどはどれでも取得できる。（Nikon, Canon, Sony, Fujifilm, Pentaxのレンズ交換式デジカメ、Ricoh, Sigmaのコンデジで検証）
- レンズ名を取得するのが厄介で以下の通り。
    |-|(1)PIL|(2)Pyexiv2|(3)ExifTool|(4)PyExifTool|
    |:--|:--:|:--:|:--:|:--:|
    |Nikon|×|？|〇|〇|
    |Fujifilm (X-Series)|〇|〇|〇|〇|
    |Pentax|×|〇|〇|〇|

    - CanonとSonyは電子接点付きレンズを持っていないので不明。
    - Pyexiv2はNikonではトライしていない。

 - 処理速度はざっくりこれくらい違う。
    |-|(1)PIL|(2)Pyexiv2|(3)ExifTool|(4)PyExifTool|
    |:--|:--:|:--:|:--:|:--:|
    |枚/秒|135|29|1.6|13|

# 環境
Windows 10 64bit
Anaconda (Python 3.6.10, 64bit)
- PIL 7.2.0
- Pandas 1.1.5
- Pyexiv2 2.7.1
- PyExifTool 0.1

ExifTool.exe 12.41


# Pythonコード
## (1) PILを用いる場合
```python
import re
from glob import glob
from tqdm import tqdm
import pandas as pd
from PIL import Image

# jpgが入っているフォルダ
photo_dir = 'test_data'
# 結果を書き出すExcelファイル
output_xl = 'output_pil.xlsx'
# 出力したい情報の対応辞書
odic = {
    'Date'      : 36867,
    'SS'        : 33434,
    'F'         : 33437,
    'ISO'       : 34855,
    'Exposure'  : 37380,
    'FL'        : 37386,
    'FL35'      : 41989,
    'Make'      : 271,
    'Model'     : 272,
    'LensModel' : 42036,
}

def illegal_char_remover(data):
    ILLEGAL_CHARACTERS_RE = re.compile(
        r'[\000-\010]|[\013-\014]|[\016-\037]|[\x00-\x1f\x7f-\x9f]|[\uffff]')
    """Remove ILLEGAL CHARACTER."""
    if isinstance(data, str):
        return ILLEGAL_CHARACTERS_RE.sub("", data)
    else:
        return data

glob_dir = glob('%s/**/*.jpg'%photo_dir, recursive=True)
df = pd.DataFrame(columns=list(odic.keys()))

for fname in tqdm(glob_dir):
    try:
        im = Image.open(fname)
        exif = im._getexif()
        edic = dict()
        for id, value in exif.items():
            edic[id] = value
    except:
        # exifを取得できなかった場合
        print('error ', fname)
        continue
    
    # edicから欲しい情報だけをadicに転記
    adic = dict()
    for out_key in odic.keys():
        if odic[out_key] in edic.keys():
            adic[out_key] = edic[odic[out_key]]
        else:
            # exif情報がない場合
            adic[out_key] = '-'

    # dfにデータを格納
    for key in adic.keys():
        df.at[fname, key] = adic[key]
# df書き出し(PILの場合、時々pandasで書き出せない文字が混じるので除去する)
df = df.applymap(illegal_char_remover)
df.to_excel(output_xl)
```
出力イメージ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2679894/e9b6205b-140b-a289-f51d-afb376b7d765.png)
Fujifilm Xシリーズはこれだけでレンズ名まで取れる。

## (2) Pyexiv2を用いる場合
Anacondaの仮想環境にpyexiv2をインストール

    pip install pyexiv2

pyexiv2を用いると、Pentaxの場合はMakerNote内に記録されたレンズID（数字の羅列）を取得できるので、IDとレンズ名の対応表があればレンズ名が得られる。Pentax以外はトライしていない。
ここで使ったレンズIDとレンズ名の対応表（pentax_lens_db.xlsx）の中身は末尾に掲載している。

```python
from glob import glob
from tqdm import tqdm
import pandas as pd
import pyexiv2

# jpgが入っているフォルダ
photo_dir = 'test_data'
# 結果を書き出すExcelファイル
output_xl = 'output_pyexiv2.xlsx'
# 出力したい情報の対応辞書
odic = {
    'Date'      : 'Exif.Image.DateTime',
    'SS'        : 'Exif.Photo.ExposureTime',
    'F'         : 'Exif.Photo.FNumber',
    'ISO'       : 'Exif.Photo.ISOSpeedRatings',
    'Exposure'  : 'Exif.Photo.ExposureBiasValue',
    'FL'        : 'Exif.Photo.FocalLength',
    'FL35'      : 'Exif.Photo.FocalLengthIn35mmFilm',
    'Make'      : 'Exif.Image.Make',
    'Model'     : 'Exif.Image.Model',
    'LensModel' : 'Exif.Photo.LensModel',
}
# レンズIDからレンズ名に変換するための辞書
pentax_lens_db = 'pentax_lens_db.xlsx'
lens_db = pd.read_excel(pentax_lens_db, index_col=0)

glob_dir = glob('%s/**/*.jpg'%photo_dir, recursive=True)
df = pd.DataFrame(columns=list(odic.keys()))

for fname in tqdm(glob_dir):
    try:
        im = pyexiv2.Image(fname, encoding='cp932')
        edic = im.read_exif()
    except:
        # exifを取得できなかった場合
        print('error ', fname)
        continue
    
    # edicから欲しい情報だけをadicに転記
    adic = dict()
    for out_key in odic.keys():
        if odic[out_key] in edic.keys():
            adic[out_key] = edic[odic[out_key]]
        else:
            # exif情報がない場合
            adic[out_key] = '-'

    # PentaxのMakernoteからレンズIDを取得できる
    if adic['LensModel'] == '-':
        if 'Exif.Pentax.LensType' in edic.keys():
            lids = edic['Exif.Pentax.LensType'].split(' ')
            if len(lids) > 1:
                # レンズIDの最初2つの数字でレンズを判断
                lid = lids[0] + ' ' + lids[1]
                if lid in lens_db.index:
                    # lens_dbに情報がある場合、レンズ名に変換
                    lens_name = lens_db.loc[lid, 'Lens_Name']
                    adic['LensModel'] = lens_name
                else:
                    # lens_dbになければレンズID（数字）を書き出す
                    adic['LensModel'] = lid

    # dfにデータを格納
    for key in adic.keys():
        df.at[fname, key] = adic[key]
# df書き出し
df.to_excel(output_xl)
```
出力イメージ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2679894/10d5457d-34f5-5ce8-b114-f3425319f798.png)
Pentaxのレンズ名が取得できた。
（が、たまに失敗するケースがあったので、最終的には使わなかった。）

## (3) ExifTool.exeを使う場合
exiftool.exeはCUIプログラムで、画像ファイル名を渡すと、Exif情報をテキストとして標準出力に書き出す。
なので、Pythonからはsubprocessを用いてキックして、標準出力に出されたテキストをPIPEで取得している。1ファイルごとに外部プログラムをキックしているから、前述の2ケースとは比べ物にならないくらい遅い。具体的にはPILの100倍くらい遅い。

以下のサイトからWindows用実行ファイルをダウンロードして利用した。

    https://exiftool.org/

```python
import subprocess
import re
from glob import glob
from tqdm import tqdm
import pandas as pd

# jpgが入っているフォルダ
photo_dir = 'test_data'
# 結果を書き出すExcelファイル
output_xl = 'output_exiftool.xlsx'
# 出力したい情報の対応辞書
odic = {
    'Date'      : 'Date/Time Original',
    'SS'        : 'Exposure Time',
    'F'         : 'F Number',
    'ISO'       : 'ISO',
    'Exposure'  : 'Exposure Compensation',
    'FL'        : 'Focal Length',
    'FL35'      : 'Focal Length In 35mm Format',
    'Make'      : 'Make',
    'Model'     : 'Camera Model Name',
    'LensModel' : 'Lens ID',
}

glob_dir = glob('%s/**/*.jpg'%photo_dir, recursive=True)
df = pd.DataFrame(columns=list(odic.keys()))

def get_exif(img_path):
    ''' exiftool.exeを用いてExif辞書を作成 '''
    # 外部プログラムexiftool.exeを起動
    prog = r"C:\exiftool\exiftool.exe"
    result = subprocess.run('%s "%s"'%(prog, img_path), shell=True, check=True,
                            stdout=subprocess.PIPE, stderr=subprocess.PIPE, 
                            universal_newlines=True)
    # stdoutに出力されたテキストを取得して、:で区切って、空白を除去して、辞書に格納
    edic = dict()
    for line in result.stdout.splitlines():
        sp = line.split(':', maxsplit=1)
        key = re.sub(r'\s+$', '', sp[0])
        val = re.sub(r'^\s+', '', sp[1])
        if key in edic.keys():
            # keyが重複する場合があるため暫定対策
            key += '_2'
        edic[key] = val
    return edic

for fname in tqdm(glob_dir):
    try:
        edic = get_exif(fname)
    except:
        # exifを取得できなかった場合
        print('error ', fname)
        continue
    
    # edicから欲しい情報だけをadicに転記
    adic = dict()
    for out_key in odic.keys():
        if odic[out_key] in edic.keys():
            adic[out_key] = edic[odic[out_key]]
        else:
            # exif情報がない場合
            adic[out_key] = '-'

    # lens ('Lens ID'になければ'Lens Model'から取ってくる)
    if adic['LensModel'] == '-':
        if 'Lens Model' in edic.keys():
            adic['LensModel'] = edic['Lens Model']

    # dfにデータを格納
    for key in adic.keys():
        df.at[fname, key] = adic[key]
# df書き出し
df.to_excel(output_xl)
```
出力イメージ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2679894/5b7b63ed-0d93-42b3-9783-d79d0dc2b93c.png)
Fujifilm, Nikon, Pentaxのレンズ情報が取得できた。CanonとSonyは電子接点つきのレンズではないのでUnknownとなっているが、電子接点があればもしかしたらレンズ情報が出るかもしれない。（調べていない）

## (4) PyExifToolを用いる場合
exiftool.exeのラッパーを利用する。(3)とは違い、exeを開きっぱなしにして、次々ファイルを渡していくので、得られる情報はそのままに(3)よりも圧倒的に速い。具体的には8倍くらい速い。

    https://github.com/smarnach/pyexiftool

↑をコピーしてきて、"exiftool.py"の172行目（下記コードの4行目）の"-n"（レンズID→レンズ名の変換を無効化するオプション）を削除。

```python
        with open(os.devnull, "w") as devnull:
            self._process = subprocess.Popen(
                [self.executable, "-stay_open", "True",  "-@", "-",
                 "-common_args", "-G", "-n"],
                stdin=subprocess.PIPE, stdout=subprocess.PIPE,
                stderr=devnull)
```

ライブラリをAnacondaの仮想環境にインストール。

    python setup.py install

ちなみに「pip install pyexiftool」のようにPyPIから自動的にインストールしようとすると、同名の別のライブラリがインストールされたので注意。

```python
from glob import glob
from tqdm import tqdm
import pandas as pd
from exiftool import ExifTool

# jpgが入っているフォルダ
photo_dir = 'test_data'
# 結果を書き出すExcelファイル
output_xl = 'output_pyexiftool.xlsx'
# 出力したい情報の対応辞書
odic = {
    'Date'      : 'EXIF:DateTimeOriginal',
    'SS'        : 'EXIF:ExposureTime',
    'F'         : 'EXIF:FNumber',
    'ISO'       : 'EXIF:ISO',
    'Exposure'  : 'EXIF:ExposureCompensation',
    'FL'        : 'EXIF:FocalLength',
    'FL35'      : 'EXIF:FocalLengthIn35mmFormat',
    'Make'      : 'EXIF:Make',
    'Model'     : 'EXIF:Model',
    'LensModel' : 'Composite:LensID',
}

prog = r"C:\exiftool\exiftool.exe"

glob_dir = glob('%s/**/*.jpg'%photo_dir, recursive=True)
df = pd.DataFrame(columns=list(odic.keys()))

with ExifTool(prog) as et:
    for fname in tqdm(glob_dir):
        try:
            edic = et.get_metadata(fname)
        except:
            # exifを取得できなかった場合
            print('error ', fname)
            continue
        
        # edicから欲しい情報だけをadicに転記
        adic = dict()
        for out_key in odic.keys():
            if odic[out_key] in edic.keys():
                adic[out_key] = edic[odic[out_key]]
            else:
                # exif情報がない場合
                adic[out_key] = '-'

        # dfにデータを格納
        for key in adic.keys():
            df.at[fname, key] = adic[key]
# df書き出し
df.to_excel(output_xl)
```

出力イメージ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2679894/f9dce67f-2df8-f99f-dff8-692f729150e0.png)
ライブラリのインストールがちょっと面倒だが、コードも少し短く、処理速度もまあまあ速く、取れる情報も多い。


# ほかのExif情報も出力したい場合
ホワイトバランスなど、ほかのExif情報も出したい場合は、上記コードのedicの中身をprintしてkeyを調べて、odicに追加すればOK。

# PentaxのレンズIDとレンズ名の対応表（一部）
Pyexiv2のケースで使用した"pentax_lens_db.xlsx"の中身。

|ID|Lens_Name|
|:--|:--|
|4 3|smc PENTAX-FA 43mmF1.9 Limited|
|4 24|smc PENTAX-FA 77mmF1.8 Limited|
|4 28|smc PENTAX-FA 35mmF2AL|
|4 39|smc PENTAX-FA 31mmF1.8AL Limited|
|4 244|smc PENTAX-DA 21mmF3.2AL Limited|
|4 247|smc PENTAX-DA FISH-EYE 10-17mmF3.5-4.5ED[IF]|
|4 251|smc PENTAX-DA 40mmF2.8 Limited|
|4 254|smc PENTAX-DA 16-45mmF4ED AL|
|5 4|smc PENTAX-FA 50mmF1.4|
|7 244|smc PENTAX-DA 21mmF3.2AL Limited|
|8 63|HD PENTAX-D FA 15-30mmF2.8ED SDM WR|
|8 197|HD PENTAX-DA 55-300mmF4.5-6.3ED PLM WR RE|
|8 234|smc PENTAX-DA★300mmF4ED[IF] SDM|

PentaxのレンズIDの調べ方。
```python
import pyexiv2
im = pyexiv2.Image('hoge.jpg', encoding='cp932')
edic = im.read_exif()
print(edic['Exif.Pentax.LensType'])
```