画像検索

Date:

Share post:

昨今の投稿で触れてきたエンベディング及びベクトル検索を使用して、本来の目的である画像検索を実施してみます。

PostgreSQLの下準備

ベクトル検索に際してはPythonのプログラムからPostgreSQLにアクセスできるようにする必要がありますが、今までの工程ではPostgreSQLへのアクセスパスワードが設定されていないため、改めて設定しておきます。

postgres=# ALTER USER postgres WITH PASSWORD 'postgres';

また、先の投稿では登録されるベクトルの次元を暫定的に3次元としてあったので、ここもエンベディングの結果に合わせて2048次元に変更します。
加えて、当該画像のファイル名を合わせて登録するようにカラムを追加して、テーブルを作り直します。

testdb=# CREATE TABLE items (
    id bigserial PRIMARY KEY,
    name text,
    embedding vector(2048)
);
CREATE TABLE

Pythonの下準備

Pythonに関してはPostgreSQLにアクセスするための機能「psycopg2」を追加する必要があります。
なお、パッケージとしては「psycopg2」が存在しますが、これはインストール後にビルドが必要になるようで、私の環境ではビルド時にエラーが発生してしまいました。
エラーの原因を追求しても良いのですが、手っ取り早くビルド済のものをインストールする方法もあるようなので、今回はこちらで対処します。

# pip install psycopg2-binary

また、先の投稿で作成したエンベディング関数ですが、使い回しがしやすいように「embedding.py」として独立させておきます。

import numpy as np
import tensorflow as tf

model = tf.keras.applications.ResNet50(include_top=False, pooling="avg", weights='imagenet')

def embedding(image_path):
    image_bytes = tf.io.read_file(image_path)
    decoded_image = tf.image.decode_jpeg(image_bytes, channels=3)
    resized_image = tf.image.resize(decoded_image, [224, 224])
    vector = model.predict(np.array([resized_image.numpy()]))[0]
    return vector

画像データの登録処理

比較対象となる画像群をエンベディングして、PostgreSQLに登録する処理を以下のように実装しました。

画像群はディレクトリ「images」の配下にまとめて格納しておき、プログラム内でそれらの名称を全て取得しつつ、各名称を指定して前述のembedding関数を実行します。
結果は画像ファイル名と合わせてテーブル「items」に登録します。

import psycopg2
from embedding import embedding
import os

conn = psycopg2.connect(
	host="127.0.0.1",
	database="testdb",
	user="postgres",
	password="postgres"
)
cur = conn.cursor()

images = os.listdir("./images")
for image in images:
    vector = embedding("./images/" + image)
    cur.execute("INSERT INTO items (name, embedding) VALUES (%s, %s)", (image, vector.tolist()))

conn.commit()
cur.close()
conn.close()

embedding関数が返すvectorの型はNumPy配列(numpy.ndarray)であり、そのままではINSERTの入力値として使えないため、リスト型に変換しています。

画像検索処理

画像検索に関しては、指定した画像をエンベディングし、その結果(ベクトル)との近さを条件とする検索を行います。
ただ、今回はベクトル検索の傾向を色々と確認してみたいので、先に登録対象とした画像群のそれぞれの画像と他の画像の距離を全て抽出しつつ、距離の近い順で並べた結果を取得してみます。

具体的な内容は以下の通り。

import psycopg2
from embedding import embedding
import os

conn = psycopg2.connect(
	host="127.0.0.1",
	database="testdb",
	user="postgres",
	password="postgres"
)
cur = conn.cursor()

images = os.listdir("./images")
for image in images:
	vector = embedding("./images/" + image)
	cur.execute("""
		SELECT
			name,
			embedding <=> %s::vector AS distance
		FROM items
		ORDER BY distance
	""", (vector.tolist(),))
	rows = cur.fetchall()
	print("----------")
	print(image)
	for row in rows:
		print(row)

cur.close()
conn.close()

ベクトル検索において重要な「距離」に関しては、今回は「コサイン距離」を採用してみました。

実験データ

今回使用する画像データは以下の11点です。

1.jpg2.jpg3.jpg4.jpg
5.jpg6.jpg7.jpg8.jpg
9.jpg10.jpg11.jpg

「1.jpg」を基本形とし、他の画像を作成してみました。

「2.jpg」から「7.jpg」までは「1.jpg」と同じマウスを被写体とし、撮影方法を変えています。

  • 「2.jpg」は「1.jpg」と比較して対象物が小さく写るように撮影
  • 「3.jpg」は「2.jpg」と同定度のサイズですが、位置が左寄りにずらして撮影
  • 「4.jpg」は「1.jpg」と同程度のサイズとして写るようにしつつ、向きを90度ずらして撮影
  • 「5.jpg」は「1.jpg」と同程度のサイズ、同じ方向で、背景を白に変えて撮影
  • 「6.jpg」は「1.jpg」の状態に対して、真上からではなく、やや手前から斜めに撮影
  • 「7.jpg」は「4.jpg」の状態に対して、真上からではなく、やや手前から斜めに撮影

「8.jpg」は「1.jpg」とは色の異なるマウスを、「1.jpg」と同程度のサイズに写るよう撮影したものです。
「9.jpg」は「1.jpg」と色合いの酷似したマウスを、「1.jpg」と同程度のサイズに写るよう撮影したものです。

「10.jpg」「11.jpg」は上記までの画像との比較用として、全く異なる対象物を撮影したものです。
「10.jpg」に関しては配置や撮影の角度を「1.jpg」に近い状態にしています。
「11.jpg」に関しては、他の画像と比較して、ほとんど共通点がないようなものとして用意しました。

実験結果

先のデータに対する実験結果を以下に整理します。
全ての比較結果を記載すると冗長になるので、特徴的なもののみ抜粋します。

なお、自身との比較結果が最も上位にあり、かつ距離が0になることは自明なので、下記比較結果では除外してあります。
数値はコサイン距離を示しており、0〜2の範囲になります。

1.jpgとの比較

今回の主目的である「1.jpg」との比較です。

15.JPG0.11841988708423767
22.JPG0.1407688128183303
33.JPG0.20300479642237867
49.JPG0.21921023962016217
57.JPG0.22920902768692297
66.JPG0.24923588062509827
74.JPG0.33580066865584346
88.JPG0.43069954975501756
910.JPG0.5030188558405837
1011.JPG0.6265555390769518

1〜3位は納得の結果かと思います。

「7.jpg」「6.jpg」「4.jpg」(同じマウスの撮影角度を変えたもの)よりも「9.jpg」(別のマウスだが色と構図が似ている)が上位に来ているのが印象的です。
撮影されている対象物が何であるかという点に関してはあまり考慮されず、あくまで絵的な類似性が重視されているようです。
ただ、その場合は「2.jpg」「3.jpg」よりも「9.jpg」の方が似ていると判断されても良いようにも思いますが。
また、「7.jpg」「6.jpg」「4.jpg」を比較した場合、私の印象としては「6.jpg」「4.jpg」「7.jpg」もしくは「4.jpg」「6.jpg」「7.jpg」の順になりそうに思うのですが、なぜか「7.jpg」が「4.jpg」「6.jpg」よりも似ていると判断された点は謎です。

「8.jpg」に関しては色の違いが大きかったようです。構図は「7.jpg」「6.jpg」「4.jpg」よりも似ているのですが、この順位に甘んじています。

9,10位も納得の結果ですね。

2.jpgとの比較

「1.jpg」と比較して対象物が小さく写った画像が対象です。

13.JPG0.09840151369905992
21.JPG0.1407688128183303
35.JPG0.2180842174679397
49.JPG0.22571964609296968
57.JPG0.2740340911302601
66.JPG0.277223437651009
74.JPG0.34104425067126587
88.JPG0.38560333128253665
910.JPG0.5066518234441573
1011.JPG0.6036497635369562

個人的興味は「1.jpg」(対象物の見た目のサイズの違い)と「3.jpg」(対象物の位置の違い)のどちらが影響するかという点でした。
結果は「3.jpg」の勝利で、距離的にも「1.jpg」と「5.jpg」(背景色の違い)よりも近いと判断されています。この結果だけ見れば多少の位置の違いは影響を与えないと期待できそうです。
「1.jpg」と比較して「5.jpg」(さらに背景色が違う)がこの順位となるのは順当かと。

4位以降は「1.jpgとの比較」と同じ結果です。

8.jpgとの比較

複数のマウス画像の中で、マウスの色が異なっている点で特徴的な画像との比較です。

13.JPG0.3537774949060002
22.JPG0.38560333128253665
37.JPG0.41801104143869827
41.JPG0.43069954975501756
56.JPG0.4661933272359252
65.JPG0.48786951947700863
79.JPG0.4883972665533808
810.JPG0.5239584780856646
94.JPG0.5850812517863409
1011.JPG0.6915559256321544

順位がどうこう言う以前に、距離が最小でも0.3を超えています。
他の比較を見ても、「同じ物を写した」と言って良いボーダーは0.2辺りにありそうなので、0.3を超えていたら考慮しなくて良いのではないかと思っています。

なお、特徴的なのは8,9位で、対象物が同種のもの(マウス)であることよりも、構図的な類似性(同じ背景の真ん中に、上下いっぱいになる程度のサイズで何か縦長なものが写っている)ということの方が重視されたのでしょうかね?
どちらにしても距離が0.5を超えているので似てない判定で良いのですが。

9.jpgとの比較

「1.jpg」とは色合いや構図は酷似しているものの、別のマウスを写した画像との比較です。
本比較のポイントは、他の画像との距離がどの程度遠い(似ていない)と判断されるかです。

11.JPG0.21921023962016217
22.JPG0.22571964609296968
35.JPG0.26658100433218623
43.JPG0.2877597163239666
57.JPG0.30588486304108753
66.JPG0.32612541913122584
74.JPG0.3535695958455044
88.JPG0.4883972665533808
910.JPG0.5183403064345349
1011.JPG0.6216443223784578

最も似ている(近い)とされた画像は「1.jpg」で、距離は約0.22です。
ただ、この距離以下でなければ似ていないと判断すると、最初に示した「1.jpg」と他の画像の比較のように、同じ対象物を写した画像の多くが除外されてしまう可能性があります。

疑わしきは除外して確実に同じと言える場合のみを採用するか、多少怪しくても近いと判断されたものを採用するかが課題になりそうです。

11.jpgとの比較

ほとんどオマケのような比較ですが、見た目似ていないにも関わらず距離が近いと判断されるケースがないかどうかの確認です。

110.JPG0.5104636059923182
22.JPG0.6036497635369562
33.JPG0.6100208333336778
49.JPG0.6216443223784578
51.JPG0.6265555390769518
66.JPG0.643298562452018
77.JPG0.6552266245988223
85.JPG0.6758433107380102
94.JPG0.6807029975827883
108.JPG0.6915559256321544

最も近いもので0.5を超えていますので論外と言うことで良いかと。

まとめ

一部の順位に疑問はあるものの、距離が0.2前半程度で近いと判断されたものは、人間の目で見ても似ている画像と言えるかと思います。
ただ、途中で何度が触れましたが、重要なのは絵的な類似性であり、色合いや構図などが似ていることが重要なようです。
言い換えれば、縦のものを横にしただけで類似度はかなり下がるので、ゴリゴリのAIのようにリンゴの写真に関して「被写体はリンゴである」というような認知能力を持った上での検索を行なっている訳ではないという点は認識しておくべきでしょう。

今回はエンベディング時の深層学習モデルとして「ResNet50」、ベクトル検索の距離判定に「コサイン距離」を採用しましたが、この辺を変更することで違った結果が得られるのかもしれません。
その辺は次回以降の課題と言うことで。

Related articles

画像データのエンベディング

昨今、ベクトル検索に絡む投稿を続けて...

PostgreSQLでベクトル検索

先にPostgreSQL関連の記事を...

Ubuntu22.04にPostgreSQL17を...

長年LAMP使いとして生きてきました...

ローカルSMTPメールサーバ(Mailpit)をE...

ローカル環境でのメール送受信テストにつ...