日本語画像キャプショニングと sentence-transformers を利用した映像検索システムの仕組み

## はじめに

以前、AWS の Amazon Rekognition と elasticsearch を使って映像検索システムを作ったことをご報告させていただきました。

AWS のビデオ分析と Elasticsearch の全文検索を使って映像検索システムを作りました。
https://qiita.com/toshiouchi/items/9b97c7be366dc33b9d28

NHK技研の人が、映像検索システムを作るために画像キャプショニングを研究しているというテレビ放送を見て、また作ってみようと思いました。

今回は、すべて無料のサービスで実現できたので、ご報告させていただきます。
映像は、NHK の映像ライブラリーを使わせていただきました。加えて、 PIXTA だったと思いますが、こちらの映像ライブラリーも使わせていただきました。映像は、3～4年前に集めたものです。

前回、Amazon Rekognition の物体検出は、英語での検出でした。これを、exite の翻訳で日本語に直して使っていました。今回は、映像を OpenCV を使って画像に変換し、画像キャプショニングは日本語で学習させたものを使いましたので、映像のキャプションも日本語です。

## 検索システムの使い方

映像検索の使い方についてご説明させていただきます。テキストフィールドに「シマウマ」と入力して検索します。

![fig1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2958180/9ffd3070-fcf3-fc93-d1af-f326f836449c.png)

すると1秒以内に

![fig2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2958180/3f1eb607-11a5-dd10-52c6-cea142048e03.png)

のような検索結果が表示されます。ここで、表の上から3番目の「シマウマが地面に生えた草を食べている」の右側の「頭出し再生」をクリックすると

![fig3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2958180/58cd0b3e-a3b0-4023-96f1-98b0c60544a1.png)

のように、シマウマが草を食べている映像が再生されるというものです。

　python で、機械学習のアプリケーションを作ったことのある人は分かると思いますが、推論時には model とデータの読み込みに時間がかかります。検索リクエストが来てから、model とデータを読み込んでいるとレスポンスがとても悪いです。なので、機械学習の話ではありませんが、

 index.html →　client.php ⇔（TCP/IP のソケット通信でサーバークライアント型） server.py

 としています。server.py は常駐させて、モデルと検索対象の文章を読み込んだあとに、while loop で検索リクエストを待っています。

# システム概略

最初に、映像を準備します。映像を OpenCV で1秒置きの画像に変換します。画像ファイルの名前には、映像内の時間（秒数）が含まれます。

https://note.nkmk.me/python-opencv-video-to-still-image/

# システム後付けの機能（追記：2024年6月3日）

1秒置きの画像にするときに、前の画像と現在の画像が似ている時は、保存しないことにして、画像枚数を減らしました。これは、画像キャプショニングなど、これ以降の処理を素早く行うために有効のようです。

実際には、imgsim

https://qiita.com/kagami_t/items/a1cae07c9565ce501ced

を用いて、画像の類似度を測ります。

```
import imgsim
from scipy.spatial import distance

vtr = imgsim.Vectorizer()
・・・
vector = vtr.vectorize(frame)
dist = distance.cdist( np.expand_dims( vector, 0 ), np.expand_dims( last_vector, 0 ), metric="cosine")
// コサイン距離、1 - cosine_similarity。 0が似ている2に近いほど似ていない。
if dist[0] > 0.1:
    cv2.imwrite(
        '{}_{}_{:.2f}.{}'.format(
        base_path, str(n).zfill(digit), n * fps_inv, ext
        ),
        frame
    )
else:
    pass
・・・
last_vector = vector
```
こんな感じです。

# 追記からもとに戻る。

画像に

「Python で学ぶ画像認識」

という本の第六章「画像キャプショニング」の第五節 「Transformer による画像キャプショニングを実装してみよう」で紹介されているプログラムでキャプション付けしました。プログラムを学習させる学習データは Coco train2014 です。教師データとしてのキャプションに

https://www.ogis-ri.co.jp/otc/hiroba/technical/similar-document-search/part9.html

のページで紹介されている

https://stair-lab-cit.github.io/STAIR-captions-web/

である日本語画像キャプションを使わせていただきました。日本語 tokenizer には、janome の tokenizer を使わせていただきました。このデータで学習したプログラムで、画像に日本語キャプションを付けました。キャプションと画像を、Hugging Face japanese-clip


https://qiita.com/Mamoru12/items/f540959013e2b59a3f24

https://huggingface.co/stabilityai/japanese-stable-clip-vit-l-16#usage

で評価しました。
```
cos_sim = torch.nn.CosineSimilarity( dim = 1 )
・・・
# 画像とテキストの特徴量を取得し、類似度を算出
with torch.no_grad():
    image_features = model.get_image_features(**image)
    text_features = model.get_text_features(**text)
    cos_sim1 = cos_sim( image_features, text_features )

print("cos_sim1:", cos_sim1) 
```

画像ベクトル(上のimage_features)とキャプションベクトル(上のtext_features)のコサイン類似度が 0.05 以上のものだけにしました。

これらから

映像ファイル名&lt;sep&gt;映像内の秒数&lt;sep&gt;画像キャプション
映像ファイル名&lt;sep&gt;映像内の秒数&lt;sep&gt;画像キャプション
映像ファイル名&lt;sep&gt;映像内の秒数&lt;sep&gt;画像キャプション
映像ファイル名&lt;sep&gt;映像内の秒数&lt;sep&gt;画像キャプション
・・・

というテキストファイルのデータを作り、Hugging Face の sentence-transformers に登録しました。

https://future-architect.github.io/articles/20230906a/

このウェブページですと、検索サーバーに登録するのは、文章（この場合キャプション）のみですが、他の情報も必要だったため、&lt;sep&gt; セパレーターを用いて、映像ファイル名と映像内の秒数の情報もサーバーに登録して、検索結果に含めました。検索結果には文章以外も含まれますが、検索につかう vector の生成は、読み込んだテキストファイルの一行を sentence として

```
def feed(self, sentences):
    for sentence in sentences:
        split_line = sentence.split( "<sep>" )
        vector = self.encoder.encode( split_line[2] )
        self.index.add_one(sentence, vector)
    self.index.build()
```

のように、文章だけから vector を生成しました。

データを登録するときに、前のキャプションと現在のキャプションの距離を計算し、似ている時は登録しない（コサイン類似度が0.5以上は登録しない）という工夫をしました。これによって、上記の検索システムができました。

```
    def feed(self, sentences):
        for i, sentence in enumerate( sentences ):
            split_line = sentence.split('<sep>')
            if len( split_line) <= 1:
                break
            vector = self.encoder.encode(split_line[2])
            if  i > 1:
                dist = self.metric_func(last_vector, vector)
                if dist < 0.5:
                    self.index.add_one(sentence, vector)
            else:
                self.index.add_one(sentence, vector)
            
            last_vector = vector

        self.index.build()
```
## 参考のため英語版のプログラムを github にアップしました。

https://github.com/toshiouchi/movie_search/blob/main/README.md