ネットワークに向かって呟いている言葉の履歴

2025年1月1日〜

古いのはこちら −前− −後− 最近の私 top_page

'26.6.25 画像振り分けツール

顔認識画像振り分けツール

インターネットに画像があふれ出してからはや30年、くらい?たぶん?? あまたの画像がHDDに入っているわけで、私は掃除しない子なので当然にHDDのなかの画像ファイルもただただフォルダにぶち込まれているだけでした。最近になって、画像を見て「この人の画像が欲しいな」とおもっても、このあまたのファイルの中から見つけるのは大変。全然整理されていませんからなぁ。

前から思っていたのですが、人物別にフォルダに仕分け分けられたら便利だな、と。そのようなツールがあればいいのですが、写真関係のアプリケーションには備わっていたりするのですが、わりと有料だったりです。長年諦めていたのですが、昨今のAIですよ、AIなら出来るんじゃね?とGeminiに相談したら作れるよーって。

そこで仕様を言って、作ってもらったのが以下のツール。このツールの完成までには紆余曲折有りました。完璧では無いですが、それなりに使えるツールと思います。

画像中の顔を見つけて512次元で顔を識別して、振り分けします。ただし、ツールが顔を見つけられなかったらnofaceフォルダに仕分けます。私の持っているファイル群だとそれなりの確率でnofaceフォルダに入ってしまいました。顔が見つけられたら同一人物判定をして、同じ人物のファイルが同じフォルダに入るという寸法です。正直言うと、そこそこの割合で複数の人物が一つのフォルダに入ってしまったり、逆に同じ人物が複数のフォルダにばらけたりします。512次元で顔を識別すると言っても、世界中の幅広い年齢層の男女を識別するわけで、有る一定の年齢の女性だけに限定したら、それほどの識別能力は無いのかもしれませんね。さらに日本人の若い女性で綺麗めの顔となったら見分けるのが難しいのかも知れぬ。海外製の認識ライブラリだからか洋物じゃなかった西洋の女性の認識はわりとうまくゆくのに日本人となるとそこそこの打率になるんですよね。中国製のアジア人を見分けるのが得意なライブラリもあるらしいのですが導入が手間ということなので見送りました。それはともかく、そういう性能なので仕分けの助けになるくらいの認識で利用ください。

ツールそのものはPyhtonで記述されたプログラムです。そのためPythonと顔識別ライブラリをインストールする必要があります。その辺りのツールが使えるまでのやり方ををGeminiに書いてもらったので、以下のドキュメントを参照して、ツールを使ってください。やり方がわからない場合は、GeminiなどのAIにこのページを入力または参照させて「やり方教えて」と頼めば親切に教えてくれると思います。



# DeepFace+MTCNNによる超厳格・爆速画像仕分けシステム構築記

大量の画像から特定の人物(LoRA学習素材など)をクリーンに仕分けるための、Pythonスクリプト構築ドキュメントです。

顔認識ライブラリのブラックボックスな挙動(お節介な自動フォールバック)による「服のシワの誤認識」や「速度低下」の罠を、エンジニアリング(ダイレクト検出&物理サイズ防壁)で手懐け、純度100%の仕分け精度と爆速化を両立させた軌跡をまとめました。

---

## 0. 背景と直面した課題(ディープラーニングの罠)

当初、顔認識ライブラリ「DeepFace(モデル: Facenet512)」の標準APIを用いて仕分けを行っていましたが、本番データ(約900枚)を流した際に以下の2つの致命的な問題に直面しました。

1. **フェイク顔(衣服のシワ・背景ノイズ)の混入と別人フォルダの乱立**

下請けの顔検出器(MTCNN)が、服のチェック柄や陰影を「完璧な顔だ」と100%の自信で誤認識(シミュラクラ現象)して切り抜いてしまう。

2. **「画像全体」を巻き込んだ誤仕分けの連鎖(3値論理の崩壊)**

顔がハッキリ見えない画像(ポーズや手で輪郭が隠れている顔判定がグレイな結果の画像)の際、ライブラリの親切心(フォールバック仕様)により「画像全体」が脳(Facenet512)に渡されてしまう。結果、AIは顔ではなく「面積の大部分を占める衣服の色や構図」を測定してしまい、同じ服を着た別人が同一フォルダにでたらめに混ざる地獄絵図が発生。

3. **処理速度の激重化(1枚1秒)**

ライブラリの内部バグにより、1枚の画像に対して重いMTCNNの検出処理が「2回」走ってしまい、メモリ展開しているにもかかわらずCPU処理がボトルネック化。

### ➔ 導き出したエンジニアリング的解決策

* **画像全体の評価の完全廃止:** 顔が綺麗に切り出せないものは一律除外する。

* **DeepFaceのブラックボックスをバイパス:** 内部の下請けである `MTCNN` を直接叩き、顔部分の生座標を取得して切り抜き。

* **物理サイズ防壁の導入:** 切り出されたエリアの縦横が「160px未満」の極小ノイズは、服のシワとみなして識別しない。

これにより、純度100%の仕分けと、処理速度の劇的な改善(約2倍速化)を達成しました。


---

## 1. 環境構築(Apple Silicon Mac mini 8GB 向け完全ガイド)

工場出荷状態(素の状態)のMac mini環境から、本システムを動かすための最新Python環境、および難所となる `dlib` を含む各種ライブラリのインストールを迷わず進めるための全手順です。

---

### STEP 1: 開発者向け基礎ツールの導入 (Xcode Command Line Tools)

PythonやdlibなどのライブラリをMacでビルド・実行するために必要な、Apple公式のコンパイラツール一式を導入します。

ターミナル(Macの「アプリケーション」>「ユーティリティ」>「ターミナル.app」)を開き、以下のコマンドを実行します。

bash
xcode-select --install

※ポップアップ画面が表示されるので「インストール」をクリックし、完了するまで待ちます。

STEP 2: パッケージ管理ツール (Homebrew) のインストール

Macの各種ツール(最新Pythonやdlibの依存ライブラリ)を簡単に管理・導入するために「Homebrew」をインストールします。

ターミナルに以下を貼り付けて実行します。

/bin/bash -c "$(curl -fsSL [https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh](https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh))"

※画面の指示に従ってパスワードを入力します。インストール完了画面の最後に 「Next steps:」 として表示される2〜3行のコマンド(echo ... や eval ...)を、ターミナルにコピー&ペーストして実行し、パス(Path)を通してください。

STEP 3: 最新の Python 3 およびビルドツールのインストール

Mac標準のPythonではなく、パッケージ管理が安定する最新のPython 3、および dlib のビルドに必要なツール類一式をHomebrew経由で一発導入します。

brew install python cmake jpeg webp

• python: 最新の安定版 Python 3 がインストールされます。

• cmake: dlib をC++ソースからMac用にビルドする際に絶対必要なコンパイル支援ツールです。

インストール完了後、正しく導入されたか確認します。

python3 --version
pip3 --version

STEP 4: 最難関「dlib」のインストール

DeepFaceの内部や一部の顔認識バックエンドで要求される dlib は、Apple Silicon(M1/M2/M3など)環境において普通に pip で入れると高確率でコンパイルエラーを起こします。

STEP 1〜3でビルド環境(Xcode Tools / CMake / jpegライブラリ)が整っているため、以下のコマンドで安全にコンパイル・インストールが可能です。

pip3 install dlib --no-cache-dir

※数万行のC++コードのビルドが裏で走るため、Mac mini(メモリ8GB)環境では完了まで数分(体感で少し長く)かかります。ターミナルが静止しますが、じっと待ってください。

STEP 5: コア・ライブラリの一括インストール

仕分けシステムの脳と目となる、DeepFaceおよびTensorFlow、OpenCVなどの主要ライブラリをまとめてインストールします。

pip3 install deepface mtcnn opencv-python tensorflow numpy

Mac環境における警告について(動作に支障なし)

スクリプト実行時、ターミナルに以下の警告が出る場合があります。

NotOpenSSLWarning: urllib3 v2 only supports OpenSSL 1.1.1+, currently the 'ssl' module is compiled with 'LibreSSL 2.8.3'.

これはMac標準のPython環境が組み込んでいる暗号化ライブラリ(LibreSSL)と、最新のPythonパッケージが求める仕様の差異によるものです。今回のローカル画像仕分けバッチの処理(MTCNNやFacenet512、行列計算)の動作、および安全性には一切影響ありませんので、そのまま無視して問題ありません。


※ Mac環境において、urllib3 のバージョンにより NotOpenSSLWarning が出力される場合がありますが、ローカルでの画像処理動作には一切影響ありません。

2. ツールのコード (sort_images_final_fixed.py)

デスクトップに 画像整理 というフォルダを作成し、その中に 未整理 フォルダを作って仕分けたい画像を投入してください。その後、同階層に以下のコードを「sort_images_final_fixed.py」という名前で保存します。

以下、ソースコード(UTF-8で記述されてます)


import os
import sys
import shutil
import cv2
import numpy as np
from pathlib import Path

# システムログを完全に黙らせる
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"
import logging
logging.getLogger("tensorflow").setLevel(logging.ERROR)

from deepface import DeepFace
# MTCNNの純正エンジンを直接インポート(DeepFaceのフォールバック仕様をバイパスするため)
from mtcnn import MTCNN


# ─── 設定 ───
DESKTOP_PATH = Path.home() / "Desktop" / "画像整理"
INPUT_DIR = DESKTOP_PATH / "未整理"
OUTPUT_DIR = DESKTOP_PATH / "仕分け済み"
NOFACE_DIR = OUTPUT_DIR / "noface"

# 【512次元・純度最優先モード専用しきい値】
# 0.20はかなり厳格(クリーン)な設定です。フォルダの分裂を抑えたい場合は0.22〜0.23に調整します。
DISTANCE_TOLERANCE = 0.20

# 衣服のシワや片目だけのノイズを叩き落とす最小の顔サイズ(ピクセル数)
# 縦横どちらかがこれ未満の極小の切り抜きは、問答無用でノイズ(偽物の顔)として却下します。
MIN_FACE_SIZE = 160

OUTPUT_DIR.mkdir(exist_ok=True)
NOFACE_DIR.mkdir(exist_ok=True)

# 進捗バーなどの不要な標準出力を完全に遮断するコンテキストマネージャ
class SuppressStdout:
    def __enter__(self):
        self._original_stdout = sys.stdout
        sys.stdout = open(os.devnull, 'w')
    def __exit__(self, exc_type, exc_val, exc_tb):
        sys.stdout.close()
        sys.stdout = self._original_stdout

def main():
    image_extensions = {".jpg", ".jpeg", ".png", ".webp"}
    images = [f for f in INPUT_DIR.iterdir() if f.suffix.lower() in image_extensions]
    
    if not images:
        print("「未整理」フォルダに画像が見つかりません。")
        return

    print(f"合計 {len(images)} 枚の処理を開始します(ダイレクトMTCNN・爆速クリーンモード)...")
    print(f"※モデル: Facenet512 / 最小顔サイズ: {MIN_FACE_SIZE}px / しきい値: {DISTANCE_TOLERANCE}\n")

    # MTCNN検出器のインスタンスをメモリ上に1つだけ生成(使い回すことで爆速化)
    detector = MTCNN()

    # メモリ上のチェックポイントDBの復元
    checkpoint_db = {}
    for folder in OUTPUT_DIR.iterdir():
        if folder.is_dir() and folder.name != "noface":
            npy_file = folder / "face_checkpoint.npy"
            if npy_file.is_file():
                checkpoint_db[folder.name] = np.load(npy_file)

    print(f"➔ 記憶の復元: 既存の人物 {len(checkpoint_db)} 名のチェックポイントをロードしました。\n")

    for idx, img_path in enumerate(images, 1):
        filename = img_path.name
        
        img = cv2.imread(str(img_path))
        if img is None:
            print(f"[{idx}/{len(images)}] [FILE_CORRUPTED] {filename} ➔ nofaceへ隔離")
            shutil.copy(img_path, NOFACE_DIR / f"[CORRUPTED]_{filename}")
            continue
            
        img_height, img_width = img.shape[:2]
        img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

        face_crop = np.array([])
        valid_face_found = False

        # 1. MTCNNを直接叩いて画像内の顔の「生座標」を取得
        try:
            with SuppressStdout():
                # ここでMTCNNが画像全体をスキャン(1回目・かつ唯一の検出処理)
                detections = detector.detect_faces(img_rgb)
        except Exception as e:
            print(f"[{idx}/{len(images)}] [DETECTION_ERROR] {filename} ➔ {e}")
            shutil.copy(img_path, NOFACE_DIR / f"[DET_ERR]_{filename}")
            continue

        # 2. 検出結果(生データ)の厳格な検証
        if detections:
            best_detect = detections[0]
            confidence = best_detect.get("confidence", 0.0)
            x, y, w, h = best_detect.get("box", [0, 0, 0, 0])
            
            # 信頼度が一定以上、かつ物理サイズが閾値(160px)以上の場合のみ「本物の顔」と認定
            if confidence > 0.50 and w >= MIN_FACE_SIZE and h >= MIN_FACE_SIZE:
                y_min, y_max = max(0, y), min(img_height, y + h)
                x_min, x_max = max(0, x), min(img_width, x + w)
                
                # 顔部分だけをメモリ上でクリーンに切り出す(元画像ママになるのを物理的に防ぐ)
                face_crop = img[y_min:y_max, x_min:x_max]
                valid_face_found = True

        # 3. 本物の顔がない(または小さすぎる)場合は計算させずに即隔離
        if not valid_face_found or face_crop.size == 0:
            print(f"[{idx}/{len(images)}] [REJECT: NO_FACE_OR_SMALL] {filename} ➔ nofaceへ隔離")
            shutil.copy(img_path, NOFACE_DIR / f"[REJECT_NS]_{filename}")
            continue

        # 4. 切り出した「顔画像だけ」を脳(Facenet512)に渡して一瞬でベクトル化
        try:
            face_crop_rgb = cv2.cvtColor(face_crop, cv2.COLOR_BGR2RGB)
            
            with SuppressStdout():
                embedding_objs = DeepFace.represent(
                    img_path=face_crop_rgb,
                    model_name="Facenet512",
                    enforce_detection=False,
                    detector_backend="skip" # 切り出し済みなので検出を100%スキップ(爆速化)
                )
            current_face_encoding = np.array(embedding_objs[0]["embedding"])
        except Exception as e:
            print(f"[{idx}/{len(images)}] [EMBEDDING_ERROR] {filename} ➔ {e}")
            shutil.copy(img_path, NOFACE_DIR / f"[EMB_ERR]_{filename}")
            continue

        # 5. メモリ上のチェックポイントDBと超高速行列計算
        matched_folder_name = None
        min_distance = float('inf')
        
        for folder_name, saved_checkpoint in checkpoint_db.items():
            norm_a = np.linalg.norm(saved_checkpoint)
            norm_b = np.linalg.norm(current_face_encoding)
            if norm_a == 0 or norm_b == 0:
                continue
                
            dot_product = np.dot(saved_checkpoint, current_face_encoding)
            cosine_distance = 1.0 - (dot_product / (norm_a * norm_b))
            
            if cosine_distance < DISTANCE_TOLERANCE and cosine_distance < min_distance:
                min_distance = cosine_distance
                matched_folder_name = folder_name

        # 6. 仕分けと保存
        if matched_folder_name:
            print(f"[{idx}/{len(images)}] [MATCH] {filename} ➔ {matched_folder_name} (Dist: {min_distance:.4f})")
            dest_folder = OUTPUT_DIR / matched_folder_name
            shutil.copy(img_path, dest_folder / filename)
            
            face_filename = f"face_{matched_folder_name}_{img_path.stem}.jpg"
            cv2.imwrite(str(dest_folder / face_filename), face_crop)
        else:
            new_id = len(checkpoint_db) + 1
            new_folder_name = f"Person_{new_id:03d}"
            dest_folder = OUTPUT_DIR / new_folder_name
            dest_folder.mkdir(exist_ok=True)
            
            print(f"[{idx}/{len(images)}] [NEW_PERSON] {filename} ➔ {new_folder_name} (Checkpoint)")
            shutil.copy(img_path, dest_folder / filename)
            
            face_filename = f"face_{new_folder_name}_{img_path.stem}.jpg"
            cv2.imwrite(str(dest_folder / face_filename), face_crop)
            
            np.save(dest_folder / "face_checkpoint.npy", current_face_encoding)
            checkpoint_db[new_folder_name] = current_face_encoding

    print("\n" + "="*50)
    print("すべての厳密・爆速仕分け処理が完了しました!")
    print("="*50)

if __name__ == "__main__":
    main()

3. 使い方と運用(職人のチューニング)

1) ディレクトリ構造の準備

デスクトップ等に以下のようにフォルダを配置します。

画像整理/
  ├── 未整理/                      ← ここに仕分けたい大量の画像を投入
  └── sort_images_final_fixed.py   ← 上記のスクリプト

2) スクリプトの実行

ターミナルからスクリプトを実行します。

python3 sort_images_final_fixed.py

3) 実行結果の仕様

•	仕分け済み/Person_xxx/

同一人物と判定された**「元画像」が格納されます。同時に、そのフォルダ内にはMTCNNが切り抜いた純粋な「顔画像(face_...jpg)」と、その人物の特徴を記憶した「チェックポイント(face_checkpoint.npy)」**が自動生成されます。

•	仕分け済み/noface/

顔が検出されなかった画像、および服のシワ等のノイズ(160px未満)と判定され、仕分け崩壊を防ぐために拒否された画像が [REJECT_NS]_元のファイル名.jpg として安全に隔離されます。

4. 精度をさらに追い込む運用のTips(バーターの調整)

本システムは、ノイズを100%混入させない**「適合率(純度)最優先」**の設計に尖らせています。そのため、同一人物であっても光の加減や表情の変化で別フォルダ(Person_001とPerson_002など)に分裂することがあります。これは仕分けの純度とトレードオフの関係(バーター)にあります。

実用領域からさらに100点満点に近づけるための運用方法です。

しきい値(DISTANCE_TOLERANCE)の微調整

• 同じ人が別フォルダに分裂しすぎる場合:

コード内の DISTANCE_TOLERANCE = 0.20 を 0.22 や 0.23 に少しだけ引き上げ、判定の門戸を広げます。

• フォルダに別人が混ざり始めた場合:

逆に 0.20 方向に数値を下げて厳格化します。

手動移動による「人間とAIのハイブリッド追加学習」

人間の目で見て「Person_005」と「Person_012」が同一人物だと分かった場合、012の中身の画像をすべて005のフォルダに手動で移動させ、空になった012のフォルダを削除してください。

次にスクリプトを実行した際、システムは「手動で統合され、バリエーションが豊かになった005の顔画像」を自動的に再読込(チェックポイントの更新)します。これにより、次回以降はその人物の認識打率が劇的に向上する、簡易的な追加学習の仕組みとして機能します。



以上です。ではー。


先頭に戻る top_page
これはすが制作しました。著作権を放棄してません http://suji.la.coocan.jp/
suji@mba.nifty.ne.jp