AI画像の選別が面倒すぎたので、Python 200行で生産性を3倍にした話
AI画像生成で大量に出力される画像の振り分け作業を、キーボード1つで生産性3倍にするPythonツールを作った話。tkinter製、依存最小、Undo付き。
最終更新:
AI画像の選別が面倒すぎたので、Python 200行で生産性を3倍にした話
100枚の画像。そのうち使えるのは20枚。
AI画像生成を使ったことがある人なら、この「選別地獄」に覚えがあるはずです。私はComfyUIでバッチ生成した画像を毎回エクスプローラで1枚ずつ開いて、右クリックして、フォルダに移動して……を繰り返していました。
キーボード1つ叩くだけで、画像が一瞬で振り分けられたら?
そんなツールをPython 200行で作りました。
🎯 この記事で得られること
この記事を読むと、AI画像の振り分け作業を3倍速にするツールが手に入ります。
- ✅ キーボードショートカットで画像を瞬時に仕分けする方法
- ✅ 「間違えた!」を即座に取り消せるUndo機能の実装
- ✅ 生成中でもリアルタイムに新画像を取り込むホットリロード
- ✅ Python標準ライブラリ+Pillowだけで動く軽量設計
😰 あなたもこんな悩みを抱えていませんか?
- 「バッチ生成したけど、使える画像を探すのが面倒すぎる…」
- 「エクスプローラの右クリック→移動、を100回繰り返すのは地獄…」
- 「間違って良い画像を削除してしまった…もう戻せない…」
- 「生成しながら同時に振り分けたいけど、ファイルが増え続けて追いつかない…」
私も以前は、まったく同じ状況でした。
📖 私のストーリー:選別地獄からワンキー振り分けへ
Before:エクスプローラとの終わりなき戦い
正直に告白します。最初は「画像の振り分けくらい、手作業でいいでしょ」と思っていました。
しかし、AI画像生成にハマると状況は一変します。
- 1回のバッチ生成で 50〜100枚 が出力される
- そのうち「使える」のは 2割 程度
- 残りは構図が崩れていたり、手指が異常だったり
- エクスプローラで1枚ずつプレビューし、ドラッグ&ドロップで仕分け
- 100枚の振り分けに毎回10分以上
最悪だったのは、「良い画像」を間違って削除してしまった時です。
ゴミ箱に移動したつもりが完全削除していて、同じシード値で再生成しても微妙にパラメータが違って再現できない。あの画像は二度と戻ってきませんでした。
「もっと効率的にやれないか……」
そう思い始めたのが、ツール開発のきっかけでした。
転機:「画像ビューアに仕分け機能があればいい」
あるとき、写真管理ソフトのライトテーブル機能を見ていて気づきました。
プロのカメラマンは撮影後に「セレクト作業」をします。大量の写真をサムネイルで眺めて、「使う」「使わない」をキー1つで仕分けていく。Adobe Lightroomの旗フラグ機能やスター評価がまさにそれです。
「同じことを、AI画像でもやりたい」
しかしLightroomは重いし、AI画像生成のワークフローに組み込むには大げさすぎます。
必要なのは、Lightroomの「セレクト機能」だけを抜き出した超軽量ツールでした。
「Python + tkinterなら、依存を最小限にしつつ作れるはず」
その瞬間、頭の中で設計がまとまりました。
After:生産性3倍の世界
結果として、以下のツールが完成しました:
| 項目 | Before | After |
|---|---|---|
| 100枚の振り分け | 10分 | 3分 |
| 操作方法 | マウスで右クリック→移動 | キーボード1つ |
| 間違い時の復旧 | 不可能(完全削除リスク) | Zキーで即Undo |
| 新規画像の取り込み | フォルダを開き直す | Rキーでホットリロード |
たった200行のPythonで、振り分け作業が3倍速になりました。
正直、もっと早く作ればよかったです。
💭 なぜ「キーボード駆動」にこだわったのか
実は、最初から今の設計を選んだわけではありません。
最初に考えた方法とその限界
まず考えたのは、GUIのボタンでの操作でした。「SAVEボタン」「NGボタン」のように画面に配置して、クリックで仕分けるパターンです。
しかし、すぐに問題に気づきました。
- マウスを使う時点で遅い: 画像を見る → マウスを移動 → ボタンをクリック → 次の画像を見る。この視線の往復が無駄
- 判断は一瞬なのに操作が追いつかない: 人間が「この画像は使える/使えない」を判断するのは0.5秒。しかしマウス操作に2〜3秒かかる
- 誤クリックのリスク: 隣のボタンを押し間違えた場合の取り消しが面倒
「画面を見る目線を一切動かさず、指だけで完結させたい」
この要件を満たすのは、キーボードショートカットしかありませんでした。
発想の転換:ゲームのUIから学ぶ
行き詰まった時、ふとゲームのUIを思い出しました。
アクションゲームでは、プレイヤーは画面中央に集中しながら、指だけで複雑な操作をこなします。目と手が完全に分離している。
画像振り分けも同じだ。目は画像に、手はキーボードに。
この考え方で設計し直すと、すべてがシンプルになりました。
決め手になった3つの設計判断
- ホームポジションから動かないキー配置:
S(SAVE)、N(保留)、D(削除)——左手だけで完結 - 即時フィードバック: キーを押した瞬間にステータスバーの色が変わり、次の画像が表示される。操作の結果を目で確認する必要がない
- Undoは必須機能: 高速操作では必ず間違えます。
Zキーで直前の操作を元に戻せる安心感が、さらに振り分け速度を上げてくれます
「速度を上げるために必要なのは、高速な処理ではなく、迷わないUI」 ——これが今回の最大の学びでした。
🔧 具体的な実装方法
全体アーキテクチャ
入力ディレクトリ(ComfyUI出力先など)
├── image_001.png ← 未振り分け画像
├── image_002.png
├── ...
├── save/ ← Sキーで移動(採用)
├── hold/ ← Nキーで移動(保留)
└── trash/ ← Dキーで移動(削除)
設計はシンプルです。指定ディレクトリ内の画像を1枚ずつ全画面表示し、キー入力に応じてサブフォルダに shutil.move するだけ。出力フォルダは入力ディレクトリ内に自動作成されます。
Step 1: 基本構造
class ImageSorter:
def __init__(self, input_dir: Path, extensions: set):
self.input_dir = input_dir
self.extensions = extensions
# 出力フォルダを入力パス内に作成
self.save_dir = input_dir / "save"
self.hold_dir = input_dir / "hold"
self.trash_dir = input_dir / "trash"
for d in (self.save_dir, self.hold_dir, self.trash_dir):
d.mkdir(exist_ok=True)
self.images: list[Path] = []
self.index = 0
self._undo_stack: list[tuple] = [] # Undo用の操作履歴
ポイントは _undo_stack です。移動操作を記録するスタックで、ZキーでPOPして元に戻します。
Step 2: キーボード駆動の画像表示
# キーバインド
self.root.bind("s", lambda e: self._move_to("save"))
self.root.bind("n", lambda e: self._move_to("hold"))
self.root.bind("d", lambda e: self._move_to("trash"))
self.root.bind("z", lambda e: self._undo())
self.root.bind("<Right>", lambda e: self._next())
self.root.bind("<Left>", lambda e: self._prev())
self.root.bind("r", lambda e: self._refresh())
tkinterの bind でキーイベントをハンドリングするだけ。大文字・小文字の両方をバインドして、CapsLock状態でも動作するようにしています。
Step 3: Undo機能の実装
これが最も重要な機能です。
def _move_to(self, dest: str):
src = self.images[self.index]
dest_dir = {"save": self.save_dir, "hold": self.hold_dir, "trash": self.trash_dir}[dest]
dst = dest_dir / src.name
shutil.move(str(src), str(dst))
# 操作履歴をスタックに記録
self._undo_stack.append((src, dst, dest, self.index))
self.images.pop(self.index)
self._show_current()
def _undo(self):
if not self._undo_stack:
return
original_path, moved_path, dest_key, original_index = self._undo_stack.pop()
shutil.move(str(moved_path), str(original_path))
# 元の位置にリストへ再挿入
self.images.insert(original_index, original_path)
self.index = original_index
self._show_current()
スタック方式にした理由: Ctrl+Zのように、連続して複数回Undoできることが重要です。「3枚前に仕分けた画像が気になる」というケースは頻繁に起きます。スタックなら Z を3回押すだけで戻れます。
操作履歴には「元のパス」「移動先パス」「カテゴリ」「リスト内の位置」を記録しているため、ファイルの物理移動もリスト上の位置も完全に復元できます。
Step 4: ホットリロード
def _refresh(self):
"""入力ディレクトリから新しい画像を検出して追加"""
existing_set = set(self.images)
new_images = []
for f in sorted(self.input_dir.iterdir()):
if (f.is_file()
and f.suffix.lower() in self.extensions
and f not in existing_set
and f.parent not in self._exclude_dirs):
new_images.append(f)
self.images.extend(new_images)
return len(new_images)
AI画像生成では「バッチ生成しながら並行して振り分ける」ワークフローが一般的です。Rキーで新しく生成された画像をその場で取り込めるので、生成完了を待つ必要がありません。
_exclude_dirs で save/、hold/、trash/ を除外しているのがポイントです。振り分け済みの画像を再度読み込んでしまうのを防ぎます。
💡 実践Tips:AI画像振り分けツールの実装ポイント
完成コードの使い方
# インストール(Pillow のみ)
pip install Pillow
# 実行
python image_sorter.py /path/to/images
# 拡張子を限定
python image_sorter.py /path/to/images --extensions png webp
キー操作一覧
| キー | 動作 |
|---|---|
S | SAVE(採用)に移動 |
N | HOLD(保留)に移動 |
D | ゴミ箱(trash)に移動 |
Z | 直前の操作をUndo |
→ / Space | スキップ |
← | 前の画像に戻る |
R | 新規画像を取り込む |
Q / Esc | 終了 |
Tips: 同名ファイルの衝突回避
ComfyUIなど一部のツールでは、異なるバッチで同じファイル名が生成されることがあります。
# 同名ファイル対策
if dst.exists():
stem = src.stem
suffix = src.suffix
i = 1
while dst.exists():
dst = dest_dir / f"{stem}_{i}{suffix}"
i += 1
Tips: tkinterで画像をキャンバスにフィットさせる
ratio = min(canvas_width / img.width, canvas_height / img.height)
new_w = int(img.width * ratio)
new_h = int(img.height * ratio)
img = img.resize((new_w, new_h), Image.LANCZOS)
アスペクト比を維持しつつ、キャンバスの幅・高さの小さい方に合わせてリサイズします。LANCZOSフィルタで縮小時のジャギーを防止。
Tips: PhotoImageのGC防止
tkinterのよくあるハマりポイントです。
# NG: ローカル変数のPhotoImageはGCされて画像が表示されない
photo = ImageTk.PhotoImage(img)
canvas.create_image(0, 0, image=photo)
# OK: インスタンス変数に保持してGCを防ぐ
self._photo_ref = ImageTk.PhotoImage(img)
canvas.create_image(0, 0, image=self._photo_ref)
カテゴリのカスタマイズ
コード中の save / hold / trash はあくまでデフォルトの命名です。用途に応じて自由に変更できます。
# 例: 写真のコンテスト応募用
self.save_dir = input_dir / "finalist" # S → 最終候補
self.hold_dir = input_dir / "maybe" # N → 保留
self.trash_dir = input_dir / "reject" # D → 不採用
🧱 壁にぶつかった瞬間と乗り越え方
順調に進んでいた開発が、意外なところで止まりました。
「あっ、間違えた」の恐怖
最初のバージョンにはUndo機能がありませんでした。
テスト中、高速にキーを叩いていると、うっかり良い画像を削除フォルダに送ってしまうことが何度かありました。元に戻すには、trashフォルダを開いて手動でファイルを探して移動し直す——これでは本末転倒です。
高速化したからこそ、ミスも高速に起きる。
この矛盾に気づいた時、Undo機能は「あったら便利」ではなく「なければ使い物にならない」機能だと理解しました。
スタック方式に辿り着くまで
最初は「直前の1回だけ戻せればいい」と考えていました。
しかし実際に使ってみると、「3枚前に振り分けた画像も気になる」「5枚まとめて間違えた方向に送ってしまった」というケースが頻発します。
固定長のバッファではダメだ。スタックでないと。
そう確信してからは実装は簡単でした。移動のたびに (元パス, 移動先, カテゴリ, リスト位置) のタプルをスタックに積む。Undoでポップして逆操作を実行する。これだけです。
この失敗から学んだこと
「ツールの速度を上げるなら、リカバリの速度も同時に上げなければいけない」 ——これはUI設計の鉄則ですが、自分で体験するまで腹落ちしていませんでした。
Undo機能がないエディタ、Ctrl+Zが効かないフォームは、使う気になりません。振り分けツールも同じでした。
🎓 この経験から得た3つの教訓
教訓1:「退屈な作業」は道具のせいかもしれない
画像の振り分けが苦痛だったのは、作業自体が面倒なのではなく、道具が作業に最適化されていなかっただけでした。
エクスプローラは汎用ファイルマネージャであって、「大量の画像を高速に仕分ける」用途には設計されていません。目的に特化した道具を作ると、同じ作業でも体験がまったく変わります。
教訓2:「最小限」は「機能が少ない」ではない
このツールの全コードは200行。Pillow以外の外部依存はゼロ。
しかし、高速キー操作・Undo・ホットリロード・同名衝突回避・ステータス表示と、実用に必要な機能は全て揃っています。
「最小限」とは、不要なものを削ぎ落とした状態であり、必要なものが欠けた状態ではない。 この違いは、実際に自分で使い込まないと判断できません。
教訓3:自分が欲しいものを作るのが最速の道
世の中には画像管理ツールが山ほどあります。しかし、「AI画像のバッチ生成出力を、生成しながらリアルタイムに振り分ける」という具体的なニーズにピッタリのものは見つかりませんでした。
「探す時間」が「作る時間」を超えたら、作った方が早い。 200行なら1時間で書けます。既存ツールを探して試して合わないと分かる時間より短い。
🚀 応用編:さらに上を目指すために
応用1: カテゴリの追加
現在は3カテゴリ(S/N/D)ですが、数字キーで5段階評価にすることも可能です。
# 1〜5の数字キーでスター評価
for i in range(1, 6):
self.root.bind(str(i), lambda e, star=i: self._move_to(f"star{star}"))
応用2: ファイル監視による完全自動化
Rキーの手動リロードの代わりに、watchdogライブラリでディレクトリを監視すれば、新しい画像が生成された瞬間に自動で取り込めます。
応用3: メタデータ連携
ComfyUIは画像のEXIFにワークフロー情報を埋め込みます。振り分け結果とEXIFメタデータを組み合わせれば、「どのプロンプト・モデル・シード値の組み合わせが良い画像を生むか」の分析にも使えます。
❓ よくある質問(FAQ)
Q1: Windows以外でも動きますか?
A: Python + tkinter + Pillowが動く環境なら、macOS・Linuxでも動作します。tkinterはPythonに標準で含まれています。
Q2: 大きな画像(4K以上)でも重くなりませんか?
A: 表示前にキャンバスサイズに合わせてリサイズしているため、元画像のサイズに関わらず軽快に動作します。
Q3: 画像以外のファイル(動画など)がフォルダにあっても大丈夫ですか?
A: 拡張子フィルタ(デフォルト: png, jpg, jpeg, webp, bmp)に一致するファイルのみ読み込むため、他のファイルは無視されます。--extensions で対象を変更可能です。
📝 まとめ:今日からできるアクションプラン
この記事で解説した内容をまとめます:
- 道具を作る: AI画像の振り分けは汎用ツールではなく、専用ツールで3倍速になる
- キーボード駆動: 視線を動かさず指だけで完結するUIが最速
- Undoは必須: 速度を上げるなら、リカバリも同じ速度で
今日からできる具体的なアクション:
📌 Pillowをインストールして、普段使っているAI画像出力フォルダでツールを起動してみてください。
pip install Pillow python image_sorter.py /path/to/your/ai-images所要時間は約1分。次のバッチ生成から世界が変わります。
🙏 おわりに:退屈な作業を楽しくする小さな工具
最後まで読んでいただき、ありがとうございました。
この記事を書いたのは、「AI画像生成の楽しさが、振り分け作業の退屈さで台無しになるのはもったいない」と思ったからです。
画像を生成する瞬間はワクワクするのに、その後の仕分け作業で疲れてしまう。私がまさにそうでした。
200行のスクリプトが、その退屈を吹き飛ばしてくれました。
プログラミングの良いところは、自分の「面倒くさい」を自分で解決できることです。この記事が、あなたのワークフローを少しでも快適にするきっかけになれば幸いです。
あなたの「面倒くさい」は、きっと200行で解決できます。
質問や改善アイデアがあれば、ぜひコメントやSNSでお知らせください。
📚 参考リンク
この記事が役に立ったら、ぜひシェアをお願いします!
あなたのシェアが、同じ悩みを持つ誰かの助けになります。
secure-auto-lab
情報処理安全確保支援士とPMの資格を使ってITコンサルタントとして働く傍ら、自宅で自動化とセキュリティを研究しているエンジニア