[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を使って類似画像を検索する方法を紹介しました。
以上です。
ありがとうございました。