Pythonで画像ファイルの仕分け

## まえがき

自分は古い人なので基本「無いものは作る」派。

## お題

今回はタイトル通り画像の仕分けということで。
恐らく2000年前後からデジカメやスマホで撮った写真、ダウンロードしてきた壁紙、スクリーンショットなど何ファイルあるかわからない上にある時点のスナップショット的なバックアップが複数のHDDに入ってるので大変カオスな状態。まぁ分散＆冗長性を持たせて保存するのはある意味リスクヘッジ的とも言えますが。

- HDD1：～2000枚まで
- HDD2：～2500枚まで
- HDD3：～3000枚まで

といった感じで重複が激しいのと以前少しだけ仕分けをやりかけた残骸がfilename(1).jpgとかで残っているので重複を省いて新しいHDDに収めるのとAmazonPhotosへアップロードしておこうかなと。

1. 仕分け先フォルダはdst\yyyymmとする(取り敢えず年月で分類)
1. yyyymmはExifのDateTimeOriginalから決定する
1. ExifはあってもDateTimeOriginalが無い、またはExifが無い画像はファイルのタイムスタンプ(最終更新日時)から決定する
1. 同名ファイルが仕分け先フォルダに既に存在する場合はファイル名の後ろに連番「(1)」を付与して一旦重複しない状態で仕分けする
1. 後で重複ファイルを削除したいのでyyyymmフォルダごとにファイル名・CRC32・ファイルサイズを一行に出力したcsvファイルを生成する
1. 対象形式は.jpgと.pngのみとする(.gifや.bmpはほぼ無いはず＆.q4や.magはもう見捨てる)

## 開発環境

- OS:Windows10Home(1903)
- Editor:Visual Studio Code
- Python:3.8.3

## コード

crc32を取得するところだけ後で使いまわすかもしれないので別モジュールに。

```Python:mycrc32.py
import binascii
import glob
import os
import sys

# ファイルのCRC32を取得する
def get_crc32(file):
    with open(file, "rb") as f:
        barray = f.read()
    return binascii.crc32(barray, 0)

# {ファイル名、CRC32、ファイルサイズ}を出力する
def output_info(srcFolder):
    files = glob.glob(os.path.join(srcFolder, "*.*"), recursive=True)
    infofile = os.path.join(srcFolder, "info.csv")

    if os.path.isfile(infofile):
        os.remove(infofile)

    for file in files:
        with open(infofile, "a") as f:
            efull = os.path.join(srcFolder, file)
            crc32 = get_crc32(efull)
            f.write(f"{os.path.basename(efull)},{hex(crc32)},{os.path.getsize(file)}\n")
```

こっちが本体。

```Python:PictureSorter.py
import datetime
import os
import glob
from PIL import Image
from PIL.ExifTags import TAGS
import shutil
import sys
import mycrc32

# Exif情報を取得
def get_exif_of_image(file):
    img = Image.open(file)

    try:
        exif = img._getexif()
    except AttributeError:
        return {}
    exifTable = {}

    if exif is not None:
        for key in exif.keys():
            tag = TAGS.get(key, key)
            exifTable[tag] = exif[key]
    return exifTable

# ファイルの最終更新日時を取得
def get_last_write_time(file):
    t = os.path.getmtime(file)
    return datetime.datetime.fromtimestamp(t)

# 移動先サブフォルダ名を取得
def get_destination_folder(file):
    exifTable = get_exif_of_image(file)

    if exifTable is not None:
        t = exifTable.get('DateTimeOriginal')

        if t is not None:
            t = datetime.datetime.strptime(t, '%Y:%m:%d %H:%M:%S')
        else:
            t = get_last_write_time(file)
    else:
        t = get_last_write_time(file)

    return t.strftime("%Y%m")

# ファイル名が重複する際に重複しないファイル名を決める
def ensure_filename(dfull):
    path = os.path.dirname(dfull)
    pureName = os.path.splitext(os.path.basename(dfull))[0]
    ext = os.path.splitext(os.path.basename(dfull))[1]

    newName = f"{os.path.join(path, pureName)}{ext}"

    i = 0
    while os.path.isfile(newName):
        i += 1
        newName = f"{os.path.join(path, pureName)}({i}){ext}"

    return newName

if __name__ == "__main__":
    _SOURCE_FOLDER = sys.argv[1]
    _DESTINATION_FOLDER = sys.argv[2]

    def get_ext(file):
        return os.path.splitext(os.path.basename(file))[1].lower()

    if (len(sys.argv) != 3):
        print("PictureSorter.py srcFolder dstFolder")
        x = input()
        exit
    else:
        print(f"srcFolder={_SOURCE_FOLDER}")
        print(f"dstFolder={_DESTINATION_FOLDER}")
        print("any key to go!")
        x = input()

    files = glob.glob(os.path.join(_SOURCE_FOLDER, "*.*"), recursive=True)

    for file in filter(lambda file: get_ext(file) in [ ".jpg", ".png" ], files):
        dstfol = get_destination_folder(file)
        dfol = os.path.join(_DESTINATION_FOLDER, dstfol)

        if not os.path.exists(dfol):
            os.makedirs(dfol, exist_ok=True)

        # ファイル移動(といいつつロストが恐ろしいので一旦コピー＆あとで消す)
        dfull = os.path.join(dfol, os.path.basename(file))
        efull = ensure_filename(dfull)
        print(f"{file} -> {efull}")
        shutil.copy2(file, efull)       # 移動で良ければcopy2の代わりにmoveで

    # ファイル名・CRC32・ファイルサイズ出力
    mycrc32.output_info(dfol)
```

## 使ってみた

これはこれで要求仕様通り動作はするが、現実はどうだ。スマホで撮った日常写真(所謂家族写真的な)とゲームのスクリーンショットが日付が同じというだけで同じフォルダに入っていいわけがない。更に上位にカテゴリごとのフォルダが必要。カテゴリが決まればその配下フォルダへの移動は今回のコードで要件は満たす。

となれば大変面倒ではありますがエクスプローラーで縮小画像を見ながらGUIフォームで行先カテゴリを選択した上で移動させたい画像ファイルをゴソっとD&Dするのが妥当なところかと。それこそ機械学習で行先カテゴリを判別させるのがイケてるとは思いますが今の知識ではそこまでは(汗

そんなわけで少し調べてみましたがWindowsAPI使う方法からwxPython使う実装だったり3.9.0で搭載されるとか色々あってとても休み中に終わらなさそうなので(実は夏季休暇中にやってます)今回はGUI部分だけC#で書いてお茶を濁す方向でいきます(C#側は大した内容ではないので省きます)

## あとがき

多分もっとPythonぽいコードはあるのでしょうが、にわかなのでこのあたりで。最近メンブレンキーボードの感触が気持ち悪くなってきたので何年かぶりにメカニカルなキーボード(茶軸)に買い換えました。キーを打つのが楽しい！