[Docker] MongoDBとFaissを使って類似画像を検索する

    はじめに

    プロジェクトごとにMongoDBを使い分けられる環境をDockerで作成してあります。
    この記事では、Docker上に構築されたデータベースから、Faissを使って類似画像を検索していこうと思います。

    新しいコンテナを作成する

    docker run --name new-mongodb-3 -p 27019:27017 -d mongo:latest

    コンテナの確認

    docker ps
    CONTAINER ID   IMAGE          COMMAND                   CREATED          STATUS          PORTS                                           NAMES
    bc80d5f77ba2   mongo:latest   "docker-entrypoint.s…"   17 seconds ago   Up 16 seconds   0.0.0.0:27019->27017/tcp, :::27019->27017/tcp   new-mongodb-3

    追加解説:ポートマッピング

    -pオプションで指定する値27019:27017は、「ホストマシンの27019ポートをコンテナ内部の27017ポートにマッピングする」という意味です。

    • 27019: ホストマシン(あなたのPCやサーバー)のポート
    • 27017: コンテナ内部でMongoDBが動作するポート
    0.0.0.0:27019->27017/tcp, :::27019->27017/tcpとは?
    • 0.0.0.0:27019->27017/tcp: IPv4アドレスでのアクセスを意味します。外部からはホストマシンの27019ポートにアクセスすると、コンテナ内の27017ポートに転送されます。
    • :::27019->27017/tcp: IPv6アドレスでのアクセスを意味します。基本的にはIPv4と同様の動作をします。

    MongoDBに接続するPythonコード

    MongoDBに接続する際には、上記のポートマッピング設定に基づいてPythonコードを書きます。

    from pymongo import MongoClient
    
    # MongoDBが動いているDockerコンテナに接続
    client = MongoClient('localhost', 27019)
    
    # 既存のデータベース名をリストで取得
    database_names = client.list_database_names()
    
    # データベース名を出力
    print("Existing databases:")
    for db_name in database_names:
        print(db_name)

    このコードでは、MongoClient('localhost', 27019)として、ホストマシンの27019ポートに接続しています。この27019ポートは、Dockerコンテナ内の27017ポートにマッピングされているため、実際にはコンテナ内のMongoDBに接続することになります。

    出力結果

    Existing databases:
    admin
    config
    local

    これらはMongoDBにデフォルトで存在するシステムデータベースです。

    • admin: 管理用のデータベースで、ユーザー認証やロールの情報が格納
    • config: シャーディング(データの分散配置)に関する設定情報が格納
    • local: 各MongoDBインスタンス固有のデータが格納

    この状態であれば、ユーザーが作成したデータベースは存在していません。
    新しいデータベースを作成する場合は、Pythonのpymongoライブラリを使って簡単に作成できます。

    データベースを設計する

    コレクションの設計

    今回のプロジェクトでは、npKnown.npzから取得したファイル名と512次元ベクトルデータを格納するコレクションを作成します。このコレクションをknown_vectorsと名付けます。

    フィールドの設計

    known_vectorsコレクションには以下のフィールドを持たせます。

    • file_name: ファイル名を格納するフィールド(String型)
    • vector: 512次元ベクトルデータを格納するフィールド(Array型)

    Pythonでコレクションを作成するテンプレート

    Pythonのpymongoライブラリを使って、新しいコレクションとフィールドを作成します。
    大まかな流れは以下のコードの通りです。(このコードは実行しません)

    from pymongo import MongoClient
    
    # MongoDBに接続
    client = MongoClient('localhost', 27019)
    
    # 新しいデータベースとコレクションを作成(データベース名:my_database, コレクション名:known_vectors)
    db = client['my_database']
    collection = db['known_vectors']
    
    # サンプルデータを挿入(実際にはnpKnown.npzからデータを読み込む)
    sample_data = {
        "file_name": "sample_file",
        "vector": [0.1, 0.2, 0.3, ..., 0.512]  # 512次元ベクトル
    }
    collection.insert_one(sample_data)
    
    # 挿入したデータを確認
    for doc in collection.find({}):
        print(doc)

    このようにして、MongoDBに新しいデータベースとコレクションを作成し、データを格納できます。

    MongoDBにデータを格納する

    """
    このスクリプトは、指定されたディレクトリとそのサブディレクトリ内に存在するnpKnown.npzファイルを探し、
    その内容をMongoDBに保存します。
    
    npKnown.npzファイルの構造:
    - name.npy: ファイル名の配列が格納されています。
    - efficientnetv2_arcface.npy: 512次元ベクトルの配列が格納されています。
      name.npyとefficientnetv2_arcface.npyの各要素は順番に対応しています。
    
    MongoDBのドキュメント構造:
    - file_name: npKnown.npz内のname.npyから取得したファイル名
    - vector: npKnown.npz内のefficientnetv2_arcface.npyから取得した512次元ベクトル
    - directory: npKnown.npzファイルが存在するディレクトリのパス
    
    処理の流れ:
    1. MongoDBサーバーに接続します。
    2. 指定したディレクトリを走査して、npKnown.npzファイルを見つけます。
    3. npKnown.npzファイルの内容を読み込み、MongoDBに保存します。
    """
    
    import os
    
    import numpy as np
    from pymongo import MongoClient
    
    def save_npz_to_mongodb(npz_data, collection):
        """
        npzファイルから読み込んだデータをMongoDBに保存する。
    
        Parameters:
        - npz_data: np.lib.npyio.NpzFile, npzファイルから読み込んだデータ
        - collection: pymongo.collection.Collection, MongoDBのコレクション
        """
        file_names = npz_data['name']
        vectors = npz_data['efficientnetv2_arcface']
    
        for file_name, vector in zip(file_names, vectors):
            file_name_str = str(file_name)  # NumPyの文字列型をPythonの文字列型に変換
            vector_list = vector.tolist()  # NumPyのndarray型をPythonのリスト型に変換
            face_data = {
                "file_name": file_name_str,
                "vector": vector_list
            }
            result = collection.insert_one(face_data)  # 挿入操作の結果を取得
            # debug
            # if result.inserted_id is not None:  # 挿入が成功したか確認
            #     print(f"Successfully inserted with ID: {result.inserted_id}")
            #     print(f"Saving: {file_name_str}, {vector_list}")
            # else:
            #     print("Insert failed.")
    
    def load_npz_from_directory(directory_path, collection):
        """
        指定されたディレクトリとそのサブディレクトリからnpKnown.npzを探し、
        見つかった場合はMongoDBに保存する。
    
        Parameters:
        - directory_path: str, 走査するディレクトリのパス
        - collection: pymongo.collection.Collection, MongoDBのコレクション
        """
        for root, _, files in os.walk(directory_path):
            for file in files:
                if file == "npKnown.npz":
                    file_path = os.path.join(root, file)
                    npz_data = np.load(file_path)
                    save_npz_to_mongodb(npz_data, collection)
    
    # MongoDBに接続
    client = MongoClient('localhost', 27019)
    
    # データベースを選択
    db = client['face_recognition_db']
    
    # コレクションを選択
    collection = db['known_faces']
    
    # 起点となるディレクトリのパス
    directory_path = "/media/terms/2TB_Movie/face_data_backup/data"
    
    # npKnown.npzファイルを読み込み、MongoDBに保存
    load_npz_from_directory(directory_path, collection)

    格納されたデータの確認

    from pymongo import MongoClient
    
    # MongoDBに接続
    client = MongoClient('localhost', 27019)
    
    # データベースとコレクションを選択
    db = client['face_recognition_db']
    collection = db['known_faces']
    
    # コレクションを消去
    # collection.drop()
    
    # コレクション内のドキュメント数をカウント
    count = collection.count_documents({})
    print(f"Number of documents in 'known_faces' collection: {count}")
    
    # コレクションから2件のドキュメントを取得して表示
    for doc in collection.find().limit(2):
        print(doc)

    出力結果

    Number of documents in 'known_faces' collection: 59422
    
    {'_id': ObjectId('6517a1c2eb9f7c5d01bf3688'), 'file_name': '風間杜夫_0uGH.jpg.png_default.png_0.png_0_align_resize.png', 'vector': [[-2.002249002456665, 1.1896134614944458, -2.685077667236328, -0.35192739963531494, -4.17877721786499, 2.763749599456787, 0.7297924160957336, -1.7906469106674194, 0.9215985536575317, -0.9142802953720093, -1.0857841968536377,
    (中略)
    0.6890649795532227, -2.702629327774048, 1.0395612716674805, -1.9293512105941772, 1.8116620779037476,  -0.49356165528297424, -2.5102062225341797, -2.5021908283233643, 1.2771133184432983, 0.052276045083999634, 2.0962891578674316, 1.2408920526504517, -0.6889671683311462]]}
    
    {'_id': ObjectId('6517a1c2eb9f7c5d01bf3689'), 'file_name': '風間杜夫_BNcF.jpg.png_default.png_0.png_0_align_resize.png', 'vector': [[-2.548264980316162, 2.27040433883667, 0.3865867555141449, -0.43443238735198975, -0.9933996796607971, 1.4334064722061157, -0.7523462772369385, 1.9946444034576416, 1.1201945543289185, 1.3821269273757935, 1.2295866012573242,
    (中略)
    0.8671872615814209, 0.6558080911636353, -0.8653426766395569, 1.4265012741088867, 3.138498306274414, 0.05671160668134689, -0.868281364440918, -1.763211965560913, -1.2924718856811523, 0.6372048854827881, -1.6095494031906128, 0.24598270654678345, 0.018475055694580078, 0.3127145767211914]]}

    約6万件のデータがMongoDBに格納されました。

    faissを使って検索する

    検索対象顔画像

    生成AIで作成された顔画像を検索対象とします。

    実装

    import os
    import sys
    import time
    
    import faiss
    import numpy as np
    from pymongo import MongoClient
    
    # FACE01ライブラリのインポート
    sys.path.insert(1, '/home/terms/bin/FACE01_IOT_dev')
    from face01lib.api import Dlib_api
    
    api = Dlib_api()
    
    # 処理開始時刻を記録
    start_time = time.time()
    
    # 顔写真をロード
    # face_image = api.load_image_file("/home/terms/ドキュメント/find_similar_faces/assets/woman2.png")
    face_image = api.load_image_file("/home/terms/ドキュメント/find_similar_faces/assets/woman.png")
    face_location = api.face_locations(face_image, mode="cnn")
    face_encoding = api.face_encodings(
        deep_learning_model=1,
        resized_frame=face_image,
        face_location_list=face_location,
    )
    face_encoding = np.array(face_encoding[0][0]).reshape(1, 512)
    
    # MongoDBに接続
    client = MongoClient('localhost', 27019)
    db = client['face_recognition_db']
    collection = db['known_faces']
    
    # MongoDBから全てのドキュメントを取得
    documents = collection.find({})
    
    # MongoDBから取得した特徴量を格納するリスト
    db_features = []
    # 各特徴量に対応するドキュメントIDを格納するリスト
    db_ids = []
    
    for doc in documents:
        feature = np.array(doc['vector']).reshape(1, 512)
        db_features.append(feature)
        db_ids.append(doc['_id'])
    
    # NumPy配列に変換し、データ型をfloat32に変換
    db_features = np.vstack(db_features).astype('float32')
    
    # 特徴量ベクトルをL2正規化
    db_features = db_features / np.linalg.norm(db_features, axis=1)[:, np.newaxis]
    face_encoding = face_encoding / np.linalg.norm(face_encoding, axis=1)[:, np.newaxis]  # 追加; face_encodingも正規化
    
    # FAISSインデックスを作成(内積を使用)
    index = faiss.IndexFlatIP(512)
    index.add(db_features)
    
    # 類似度検索(コサイン類似度)
    D, I = index.search(face_encoding, 5)  # 5つの最も類似した特徴量を検索
    
    # 類似した特徴量のドキュメントIDと類似度(コサイン類似度)を表示
    for i, d in zip(I[0], D[0]):
        similar_doc_id = db_ids[i]
        print(f"Similar document ID: {similar_doc_id}")
    
        # 類似度(コサイン類似度)を表示
        print(f"Similarity Score (Cosine Similarity): {d}")
    
        # IDに基づいてMongoDBからドキュメントを取得
        similar_doc = collection.find_one({"_id": similar_doc_id})
    
        # ドキュメントからfile_nameを取得して表示
        if similar_doc and 'file_name' in similar_doc:
            print(f"Similar file name: {similar_doc['file_name']}")
    
    # 処理時間を計算して出力
    end_time = time.time()
    elapsed_time = end_time - start_time
    minutes, seconds = divmod(elapsed_time, 60)
    print(f"処理時間: {int(minutes)}分 {seconds:.2f}秒")

    出力結果

    Similar document ID: 6517a1fbeb9f7c5d01c01952
    Similarity Score (Cosine Similarity): 0.4709591567516327
    Similar file name: 阿川泰子_vn5Z..png.png.png_0_align_resize_default.png
    Similar document ID: 6517a1f3eb9f7c5d01bff6ee
    Similarity Score (Cosine Similarity): 0.42845162749290466
    Similar file name: 大谷直子_vixw.jpg.png_align_resize_default.png
    Similar document ID: 6517a1d0eb9f7c5d01bf6eb5
    Similarity Score (Cosine Similarity): 0.41680583357810974
    Similar file name: 後藤晴菜_kCOc.webp.png_default..png.png_0.png_0_align_resize.png
    Similar document ID: 6517a1fbeb9f7c5d01c01957
    Similarity Score (Cosine Similarity): 0.40054333209991455
    Similar file name: 阿川泰子_tDgX..png_0_align_resize_default.png
    Similar document ID: 6517a1ebeb9f7c5d01bfd61b
    Similarity Score (Cosine Similarity): 0.38524919748306274
    Similar file name: 岸本加世子_QIp1.jpg_default.png.png_0.png_0_align_resize.png
    処理時間: 0分 6.07秒

    まとめ

    わずか6秒で、約6万件のデータから類似画像を検索できました。

    検索結果は、最大の類似度が0.4709だったこともあり、やはりリアルの人物にはそっくりな方はおられませんでした。
    しかし、どこかで見たお顔なんですよね。誰だろう。

    今回の記事では、Docker上に構築されたデータベースから、Faissを使って類似画像を検索する方法を紹介しました。

    以上です。
    ありがとうございました。