AWS のビデオ分析と Elasticsearch の全文検索を使って映像検索システムを作りました。

AWS のビデオ分析と Elasticsearch の全文検索を使って、映像検索システムを作りました。記録を残させていただきます。

## 報告する内容
<ul>
<li>検索サービス概略</li>
<li>Amazon Rekognition というビデオ分析サービスから得られる情報</li>
<li>得られる情報が英語なので、翻訳サイト Excite で翻訳して使うことにしました。</li>
<li>Elasticsearch のデータスキーマ</li>
<li>検索サービスのアルゴリズム</li>
<li>映像頭出し再生の HTML と Javascript </li>
<li>ほかに考えられる映像検索サービス</li>
</ul>

システムを構築したのが2年前くらいなので、Elasticsearch のバージョンが6くらいです。Java でプログラミングしてあり、今も動きますが、Java のバージョンも11です。なので、プログラムは掲載しません。エッセンスのみご報告させていただきます。

## 検索サービス概略

検索サービスを受け付けるページは、図のようなページです。

![Fig1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2958180/e134e20f-2ee9-6a56-9768-b6bfb3e927cc.png)


この図で赤枠で囲まれたテキストボックスに「クリスマスツリー」と入力されていますが、「クリスマスツリー」が映っている映像の「クリスマスツリー」の箇所を頭出ししてください。という指示が、赤で囲まれている「検索」ボタンです。この検索ボタンをクリックすると、図

![FIg2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2958180/5581571d-f117-4582-80b2-d78b4eb7b794.png)

のようになります。赤枠で示しました通り、クリスマスツリーの映った映像が準備され、右側にある「頭出し再生」をクリックすると、緑で囲まれた映像が、「クリスマスツリー」が映っている箇所から再生されます。表示されている 100.0 は、後で述べるように、Amazon Rekognition での confidence です。

## Amazon Rekognition というビデオ分析サービスから得られる情報。

映像を AWS クラウドの S3 にアップし、ビデオ分析サービス（12か月お試しの後有料）を利用すると、各映像について次のような分析結果（jsonファイル)が得られます。

```json
{
    "JobStatus": "SUCCEEDED",
    "VideoMetadata": {
        "Codec": "h264",
        "DurationMillis": 58859,
        "Format": "QuickTime / MOV",
        "FrameRate": 29.970029830932617,
        "FrameHeight": 1080,
        "FrameWidth": 1920
    },
    "NextToken": "yQeP2mrPuKOdc6sbTk/x4khURO/5y0wN2tw583U3DhNPFPksQeJln0lXRAP+CkRRwl8jIghb4cMS",
    "Labels": [
        {
            "Timestamp": 0,
            "Label": {
                "Name": "Apparel",
                "Confidence": 91.41659545898438,
                "Instances": [],
                "Parents": []
            }
        },
        {
            "Timestamp": 0,
            "Label": {
                "Name": "Automobile",
                "Confidence": 62.82073211669922,
                "Instances": [],
                "Parents": [
                    {
                        "Name": "Vehicle"
                    },
                    {
                        "Name": "Transportation"
                    }
                ]
            }
        },
        {
            "Timestamp": 0,
            "Label": {
                "Name": "Bathtub",
                "Confidence": 94.39823150634766,
                "Instances": [
                    {
                        "BoundingBox": {
                            "Width": 0.3972117006778717,
                            "Height": 0.5696443319320679,
                            "Left": 0.09280557930469513,
                            "Top": 0.392807275056839
                        },
                        "Confidence": 96.05530548095703
                    }
                ],
                "Parents": [
                    {
                        "Name": "Tub"
                    }
                ]
            }
        },

・・・

        {
            "Timestamp": 467,
            "Label": {
                "Name": "Apparel",
                "Confidence": 88.64424896240234,
                "Instances": [],
                "Parents": []
            }
        },
        {
            "Timestamp": 467,
            "Label": {
                "Name": "Automobile",
                "Confidence": 54.07227325439453,
                "Instances": [],
                "Parents": [
                    {
                        "Name": "Vehicle"
                    },
                    {
                        "Name": "Transportation"
                    }
                ]
            }
        },
        {
            "Timestamp": 467,
            "Label": {
                "Name": "Bathtub",
                "Confidence": 91.77007293701172,
                "Instances": [
                    {
                        "BoundingBox": {
                            "Width": 0.3931315839290619,
                            "Height": 0.5672365427017212,
                            "Left": 0.09496594965457916,
                            "Top": 0.3969167172908783
                        },
                        "Confidence": 89.29952239990234
                    }
                ],
                "Parents": [
                    {
                        "Name": "Tub"
                    }
                ]
            }
        },
        {
            "Timestamp": 467,
            "Label": {
                "Name": "Car",
                "Confidence": 54.07227325439453,
                "Instances": [],
                "Parents": [
                    {
                        "Name": "Vehicle"
                    },
                    {
                        "Name": "Transportation"
                    }
                ]
            }
        },
        {
            "Timestamp": 467,
            "Label": {
                "Name": "Clinic",
                "Confidence": 55.420047760009766,
                "Instances": [],
                "Parents": []
            }
        },
        {
            "Timestamp": 467,
            "Label": {
                "Name": "Clothing",
                "Confidence": 88.64424896240234,
                "Instances": [],
                "Parents": []
            }
        },
        {
            "Timestamp": 467,
            "Label": {
                "Name": "Coat",
                "Confidence": 72.20368957519531,
                "Instances": [],
                "Parents": [
                    {
                        "Name": "Clothing"
                    }
                ]
            }
        },
        {
            "Timestamp": 467,
            "Label": {
                "Name": "Desk",
                "Confidence": 67.85700225830078,
                "Instances": [],
                "Parents": [
                    {
                        "Name": "Table"
                    },
                    {
                        "Name": "Furniture"
                    }
                ]
            }
        },
        {
            "Timestamp": 467,
            "Label": {
                "Name": "Furniture",
                "Confidence": 96.25518798828125,
                "Instances": [],
                "Parents": []
            }
        },
・・・
```

わたくしは、Timestamp、 Label:Name と Label:Confidence （確からしさ）に注目しました。Timestamp は、この映像の何ミリ秒のところに、解析結果 Label:Name が確からしさ、Label:Confidence で映っている。ということと考えています。
要は、Furniture という検索があった時に、この映像ファイルの 467 ミリ秒が頭出しされて検索結果のリストに掲載されれば良いわけです。

## 得られる情報が英語なので、翻訳サイト Excite で翻訳して使うようにしました。

上でみたように、ビデオ分析で得られる情報は英語です。この( 時間, 物, 確からしさ ）のデータを、Elasticsearch にオブジェクトとしてデータ登録して、全文検索するのですが、物が英語なので、これを、Excite の翻訳を使って日本語に変換してデータ登録しました。参考にした ページは

https://yukun.info/java-excite-translation/

です。加えて、同じ物のデータが時間的に連続して現れる場合、最初の一回だけ登録するなど工夫しました。

##   Elasticsearch のデータスキーマ

次に、Elasticsearch のデータスキーマを掲載します。

```json
      {
        "_index" : "aws7",
        "_type" : "_doc",
        "_id" : "8",
        "_score" : 1.0,
        "_source" : {
          "id" : 8,
          "videoURL" : "Christmas - 100317.mp4",
          "title" : "Pixabayクリスマス",
          "thumb" : "white.jpg",
          "content" : [
            "大かがり火",
            "クリスマスツリー",
            "火",
            "暖炉",
            "炎",
            "炉床",
            "人",
            "屋内",
            "飾り",
            "人",
            "プラント",
            "木",
            "タワー",
            "火",
            "人",
            "大かがり火",
            "火",
            "炎",
            "大かがり火",
            "火",
            "炎",
            "人",
            "人",
            "火",
            "炎",
            "大かがり火",
            "アーキテクチャ",
            "大かがり火",
            "時計台",
            "火",
            "炎",
            "タワー",
            "人",
            "人",
            "火",
            "火",
            "炎",
            "ビルディング",
            "火",
            "炎",
            "人",
            "人",
            "火",
            "人",
            "人",
            "炎",
            "大かがり火",
            "火",
            "炎",
            "大かがり火",
            "火",
            "炎",
            "アーキテクチャ"
          ],
          "stime" : [
            "0",
            "0",
            "0",
            "0",
            "0",
            "0",
            "0",
            "0",
            "0",
            "0",
            "0",
            "0",
            "967",
            "2469",
            "3470",
            "3970",
            "3970",
            "3970",
            "6973",
            "8975",
            "8975",
            "9476",
            "9476",
            "10977",
            "10977",
            "11478",
            "12979",
            "12979",
            "12979",
            "12979",
            "12979",
            "12979",
            "13480",
            "13480",
            "15482",
            "16483",
            "16483",
            "17484",
            "19486",
            "19486",
            "21488",
            "21488",
            "23990",
            "24491",
            "24491",
            "24991",
            "26993",
            "26993",
            "26993",
            "29496",
            "29496",
            "29496",
            "32999"
          ],
          "confidence" : [
            "99.98523",
            "71.76269",
            "99.98523",
            "99.98523",
            "70.716125",
            "92.66866",
            "99.9752",
            "75.593506",
            "75.593506",
            "99.800415",
            "70.90156",
            "99.89908",
            "99.85937",
            "90.57477",
            "99.980354",
            "99.86668",
            "99.98296",
            "99.9839",
            "99.98202",
            "99.90119",
            "99.98202",
            "92.39357",
            "99.979996",
            "99.978264",
            "99.977104",
            "99.978226",
            "99.85288",
            "99.98084",
            "99.98103",
            "99.98103",
            "71.01289",
            "71.01289",
            "99.92051",
            "92.43491",
            "99.97917",
            "94.832855",
            "99.98056",
            "99.80443",
            "99.871124",
            "99.982956",
            "99.98352",
            "99.87072",
            "91.7783",
            "99.984055",
            "99.98507",
            "99.98507",
            "99.98507",
            "74.64062",
            "99.92558",
            "93.75878",
            "99.97713",
            "77.258606",
            "99.906624"
          ],
          "time" : "2022/04/04"
        }
      },

```

上記が、一つの映像についてのデータです。id はシーケンシャルな映像ファイルの番号、videoURL が映像ファイル名。title がタイトル、thumb がサムネイルファイル名、content、stime と confidence が( 物、時間、確からしさ ）に対応しています。confidence は、Amazon Rekognition で解析された、物の確からしさです。content, stime, confidence は、データの中身が配列になっています。配列の若い方から、content[0], content[1], content[2],　・・・, stime[0], stime[1], stime[2], ・・・, confidence[0], confidence[1], confidence[2], ・・・, とすると、（content[1], stime[1], confidence[1] )は関連性がありますので、（物、時間、確からしさ）という表現をしています。プロパティは次のようです。


```json
{
  "aws2" : {
    "aliases" : { },
    "mappings" : {
      "properties" : {
        "confidence" : {
          "type" : "text",
          "index" : false,
          "analyzer" : "default",
          "search_analyzer" : "default_search"
        },
        "content" : {
          "type" : "text",
          "analyzer" : "default",
          "search_analyzer" : "default_search"
        },
        "id" : {
          "type" : "long"
        },
        "stime" : {
          "type" : "text",
          "index" : false,
          "analyzer" : "default",
          "search_analyzer" : "default_search"
        },
        "thumb" : {
          "type" : "keyword"
        },
        "time" : {
          "type" : "date",
          "format" : "yyyy/MM/dd"
        },
        "title" : {
          "type" : "keyword"
        },
        "videoURL" : {
          "type" : "keyword"
        }
      }
    },
    "settings" : {
      "index" : {
        "refresh_interval" : "3ms",
        "number_of_shards" : "1",
        "blocks" : {
          "read_only_allow_delete" : "true"
        },
        "provided_name" : "aws2",
        "creation_date" : "1638750716324",
        "analysis" : {
          "filter" : {
            "ja_customized_filter" : {
              "type" : "kuromoji_part_of_speech",
              "stoptags" : [
                "その他-間投",
                "その他",
                "フィラー",
                "記号-一般",
                "記号-括弧開",
                "記号-括弧閉",
                "記号-句点",
                "記号-空白",
                "記号-読点",
                "記号",
                "助詞-格助詞-一般",
                "助詞-格助詞-引用",
                "助詞-格助詞-連語",
                "助詞-格助詞",
                "助詞-間投助詞",
                "助詞-係助詞",
                "助詞-終助詞",
                "助詞-接続助詞",
                "助詞-特殊",
                "助詞-副詞化",
                "助詞-副助詞",
                "助詞-副助詞／並立助詞／終助詞",
                "助詞-並立助詞",
                "助詞-連体化",
                "助詞",
                "助動詞",
                "接続詞",
                "接頭詞-形容詞接続",
                "接頭詞-数接続",
                "接頭詞-動詞接続",
                "接頭詞-名詞接続",
                "接頭詞",
                "非言語音",
                "副詞-一般",
                "副詞-助詞類接続",
                "副詞",
                "連体詞"
              ]
            },
            "my_katakana_stemmer" : {
              "type" : "kuromoji_stemmer",
              "minimum_length" : "4"
            }
          },
          "char_filter" : {
            "normalize" : {
              "mode" : "compose",
              "name" : "nfkc",
              "type" : "icu_normalizer"
            }
          },
          "analyzer" : {
            "default_search" : {
              "filter" : [
                "lowercase",
                "cjk_width",
                "kuromoji_baseform",
                "ja_customized_filter",
                "my_katakana_stemmer",
                "ja_stop"
              ],
              "char_filter" : [
                "html_strip",
                "icu_normalizer"
              ],
              "tokenizer" : "ja_kuromoji_search_tokenizer"
            },
            "default" : {
              "filter" : [
                "lowercase",
                "cjk_width",
                "kuromoji_baseform",
                "ja_customized_filter",
                "my_katakana_stemmer",
                "ja_stop"
              ],
              "char_filter" : [
                "html_strip",
                "icu_normalizer"
              ],
              "tokenizer" : "ja_kuromoji_normal_tokenizer"
            }
          },
          "tokenizer" : {
            "ja_kuromoji_search_tokenizer" : {
              "mode" : "search",
              "type" : "kuromoji_tokenizer",
              "user_dictionary" : "/var/lib/elasticsearch/my_jisho.dic"
            },
            "ja_kuromoji_normal_tokenizer" : {
              "mode" : "normal",
              "type" : "kuromoji_tokenizer",
              "user_dictionary" : "/var/lib/elasticsearch/my_jisho.dic"
            }
          }
        },
        "number_of_replicas" : "1",
        "uuid" : "4R-iHoi0TCOyCVb2BYyfOQ",
        "version" : {
          "created" : "7080099"
        }
      }
    }
  }
}
```
## Elasticsearch で配列の登録

Elasticsearch で、配列構造のあるデータを登録する方法について簡単に述べる。

```java
class Aws {
	public long id;
	public String videoURL;
	public String title;
	public String thumb;
	public List<String> content;
	public List<String> stime;
	public List<String> confidence;
	public String time;
}
```
のようなクラスを作り、
```java
Aws aws = new Aws();

//略 変数 aws にデータを入れる。

ObjectMapper mapper = new ObjectMapper();
byte[] bytes = mapper.writeValueAsBytes( aws );
IndexRequest request = new IndexRequest( awsindex );// awsindex はインデックス名 aws
request.id( String.valueOf( aws.id ) );
request.source( bytes, XContentType.JSON);
IndexResponse indexResponse = client.index(request, RequestOptions.DEFAULT);

```
変数 aws をクラス Aws で宣言して、aws にデータを入れ、上記のプログラムで index にデータ登録していました。

## 検索サービスのアルゴリズム

Elasticsearch の検索のアルゴリズムについて基本的なところを書きます。インデックス名を aws として、検索対象が content だとすると、

curl -XGET -H "Content-type application/json" http://localhost:9200/aws/_search?pretty -d '{ "query": { "match": { "content" : "検索語" }}} 

です。この結果から、videoURLを取得します。また、content、stimeと confidence を配列として取得して、検索語が content 配列の何番目の何文字目にあるか探して、この何番目かを便りに、stime と confidence の同じ何番目を取得します。これらから Javascript で、頭出しされた映像を再生できるようにプログラムすれば良いです。

## 映像頭出し再生の HTML と Javascript

検索サービス自体は、Java で書いていますが、映像を頭出し再生するところは、Javascript です。ここのプログラミングに苦労した覚えがあるので、プログラミングのエッセンスを掲載させていただきます。Java で苦労しなかったという分けではありません。

最初に video の スタイルシートです。このスタイルシートは、検索結果の映像ファイルの数だけ、index.jsp の中でループして、番号を付けた要素となります。私は、スタイルシートを html や jsp ファイルの中に書いておく癖があります。

```jsp
<%
for( int i = 0; i < player_max; i++ ){
%>
#ivideo<% out.print( i ); %> {
	width: 290px;
	height: 160px;
	margin: 0 auto;
	text-align: center;
}
<%
}
%>
```

次に、<code>&lt;video&gt;</code>タグです。source src には、検索で得られたビデオファイル名 parse[i].video をセットしています。

```javascript
//略
html += "<video controls id='ivideo" + String(i) + "' >" +
		"<source src='movie/" + parse[i].video + "' type='video/mp4' />" +
		"</video>"
//略
```

次に頭出し再生する関数です。添え字 i が何番目の映像ファイルかを表し、parse[i].S[j].stime が i 番目のファイルの j 番目の再生位置の開始時間です。このほかに、視聴ログをとっているために、title と keyword を関数に渡しています。


```javascript

//略
result_html += '<a href="javascript:void(0);" onClick="CueVideo(' + i + ", "+ parse[i].S[j].stime + ", \'" + title + "\', \'" + keyword + '\')">頭出し再生</a>';
//略
```

```javascript
function CueVideo( i, seconds, title, keyword ) {

	//略	（視聴ログの書き込み ajax や、参照する video タグ要素へのカーソルスクロールなど）

	//ビデオパラメータのセット
	var video = document.getElementById("video" + i );
	//新しいビデオファイルのロード
	video.load();
    //ビデオが load されたら、頭出しして再生。
	video.addEventListener("loadeddata", function() {
		video.currentTime = seconds / 1000;
		video.play();
	}, false);
}
```

## ほかに考えられる映像検索サービス

この頃は、県議会の質疑応答のビデオがネット上で視聴できます。このビデオは、音声が入っており、AI の音声認識で字幕ファイルを作ることにより、（時間、言葉）の配列を得ることができます。言葉の方は、自治体が提供している会議録からとってきても良いです。会議録の言葉を使い、音声認識の時間と照合することで、（時間、言葉）の配列を Elasticsearch に登録できます。これにより全文検索して有意義な検索システムを構築することが可能です。これは、わたくしが作ってみたシステムで使ってみて良いシステムだと感じました。しかし、まだ、音声認識のレベルがあまりよくなかったので、実用化できませんでした。しかし、現在、OpenAI の Whisper という音声認識ソフトがあり、かなり良い精度で認識可能になっています。