ruby と OpenCV, ffmpeg を使用した画像/動画の認識とパフォーマンスについて ヽ(ﾟｰﾟ*ヽ)(ﾉ*ﾟｰﾟ)ﾉわぁい

こんにちはでス :smile: 先日「複数の動画を画面n分割で同期した動画をつくるのに ffmpeg, opencv, ruby でやってみた ヽ(ﾟｰﾟ*ヽ)(ﾉ*ﾟｰﾟ)ﾉわぁい http://qiita.com/scleen_x_x/items/ef5e49c8dd46c74df9b1 」という記事を書いたものです。今回も OpenCV とか使った話を書いていきます。より詳しい内容です。

![5x5.png](https://qiita-image-store.s3.amazonaws.com/0/56465/a9cea51c-99c8-49ae-5877-505276a55955.png)

参考動画：https://www.youtube.com/watch?v=DyKwJD-T-bc

こんな感じのものを仕上げるイメージです。

画像処理 :camera: 本当に面白いでございます、でも、[少し前に話題になった量子コンピューターの記事](http://qiita.com/kenokabe/items/92189d658011da808442)を読むと苦笑いしちゃいます。画像処理なんてものは「数字にして計算しなおしている」だけ、なんですよね。見りゃわかるでしょって :cry: 思う様なものを。

この記事は OpenCV の画像認識（テンプレートマッチング）を使った時の話を書いていきます。対象は例によって、モンストのガチャです。

![gacha_100.png](https://qiita-image-store.s3.amazonaws.com/0/56465/ad5f7e3c-2ac8-a2be-9c64-54b4324743ff.png)

これね。みんな、モンストやってる？ :game_die: : 少しだけ突っ込んだ話もします。できるだけ、わかりやすく書きます。たとえば、↓下のような数式も出ます。げげげ。

![2a0fd0272aa40817a15f92fe295e07a4d997f3a4.png](https://qiita-image-store.s3.amazonaws.com/0/56465/1796955b-ae2b-95d8-c702-e671f8ad2dae.png)

この数式見てもブラウザ閉じずに :scream: 少しだけ我慢するかストック（笑）

切り出した後のガチャ動画（いくつか）

 - https://www.youtube.com/watch?v=exGZI8k1SLM
 - https://www.youtube.com/watch?v=4sTNg5MoOIA
 - https://www.youtube.com/watch?v=uTJcGYg5aE0
 - https://www.youtube.com/watch?v=oREvlNH6Ako

シンクロさせた動画

2x2の場合 => https://www.youtube.com/watch?v=HYNE7XVBrus
3x3の場合 => https://www.youtube.com/watch?v=dw0k9JQioBo
4x4の場合 => https://www.youtube.com/watch?v=NuKRcvH3Hfg
5x5の場合 => https://www.youtube.com/watch?v=DyKwJD-T-bc

# この記事のテーマ

『コンピューターで使う電気も、コンピューターの性能も限界があるし、使えるお金も無いので節約＝パフォーマンスをあげる必要があるよね』というのが今回のテーマです。

もし、この記事の内容を気に入っていただけたら、わたしが使っているVPSの紹介リンク↓から利用登録してもらえると、お互いに$10のクレジットがもらえるので、とてもうれしいです。ご協力をお願いいたします。

DigitalOcean ($5/月でSSDのVPSを提供しているサービスです)
https://www.digitalocean.com/?refcode=2eab928d2c73

## パフォーマンスについて考えること

テンプレートマッチングのパフォーマンスについて考えるとき、たくさんの動画を処理するならかんがえることはいろいろあったりします。

 - 計算量を減らせないか
 - 検出精度を上げられないか
 - いちどに処理できないか
 - その他：DiskIO、メモリ使用量とか

**この記事では計算量を減らせないかを主に取り上げていきます。** 複数の動画を処理するので、Hadoop使って一度に並列分散して処理する方法についても、こんど書けたらなと思ってます。

# OpenCV のテンプレートマッチングについて

モンストのガチャの認識にOpenCVというソフトウェアを使います。画像処理や認識をするのにとても便利なオープンソースのプロジェクトです。OpenCVの機能の一つとして提供されている、テンプレートマッチングの機能を使います。こちら↓にOpenCVのテンプレートマッチングのチュートリアルがあります。英語ですがどうぞ
http://docs.opencv.org/doc/tutorials/imgproc/histograms/template_matching/template_matching.html

OpenCVにはテンプレートマッチングに使ういくつかの数式が用意されています。それぞれ一長一短あるので、使用するケースにあった数式（メソッド）を使うのがパフォーマンスや、検出率を高める上で大事です。

この記事ではデフォルトでも採用されている CV_TM_SQDIFF を使います。数式はこれ↓です。

![2a0fd0272aa40817a15f92fe295e07a4d997f3a4.png](https://qiita-image-store.s3.amazonaws.com/0/56465/1796955b-ae2b-95d8-c702-e671f8ad2dae.png)

数式アレルギーでもブラウザバックしないで :scream: 

# YouTube のガチャ動画からモンストのガチャ動画のシーンを検出する

![gacha_100.png](https://qiita-image-store.s3.amazonaws.com/0/56465/ad5f7e3c-2ac8-a2be-9c64-54b4324743ff.png)

モンストのガチャは↑のようなガチャが画面に出てきます。（※傾いているのはタイミングを検出するためです）この画像があるかどうかを調べることで、モンストのガチャ動画か？欲しいタイミングか？を検出します。

## 動画開始から132秒後にガチャが始まる、といったことを知りたい

と言う感じで、やっていきましょう。

## どうやってやるか？だいたいのアイデア

例えば5分の動画があったとします。その動画がモンストのガチャ動画かどうかは、人間も実際に観るまでわかりません。これは、コンピューターも同じなので、動画内のシーンを切り出して静止画にして扱います。たとえば、1秒目、2秒目、3秒目、10秒目には何がうつっているのか？を順番に分析させることで、コンピューターに認識させます。

## 対象の動画を絞り込む

毎日山ほど動画が投稿されるので、「モンスト ガチャ」「モンスト 獣神祭」といったテキストで絞り込んだ動画のみを対象にします。このテキスト検索で得られた動画が実際にモンストのガチャの動画かどうかはわかりません。スパムっぽいのも入っている可能性もあります。でも、観るまでわからないので、ここは我慢します。

## 動画から画像を切り出す

1秒目、2秒目には何が映っているのか？を分析させるために、動画から画像を切り出します。ffmpegというツールを使います。ffmpegもオープンソースのプロジェクトで動画や画像の処理に使える大変便利なものです。ffmpeg さえインストールされていれば一発で動画から画像を切り出すことができます。

### このコマンドで一発

```
ffmpeg -i video.mp4 -r 30 thumbnail-%04d.png
```

で、video.mp4というファイルから30枚/秒で画像を切り出せ :scissors: ます。切り出された画像は 

thumbnail-0001.png
thumbnail-0002.png
thumbnail-0003.png
thumbnail-0004.png

という感じで自動的に連番のファイル名が付けられます。もし、10枚/秒にしたい場合は

```
ffmpeg -i video.mp4 -r 10 thumbnail-%04d.png
```

1枚/秒なら

```
ffmpeg -i video.mp4 -r 1 thumbnail-%04d.png
```

です。

### 何枚切り出すべきか？

「何枚切り出すべきか？」を考えるのはとても大事です。1000枚切り出した方が、100枚切り出すより、精度の高い分析ができます。でもその分、つまり10倍ぐらい時間はかかることになります。

### 切り出し枚数が多ければ良いのか？

精度はできるだけ高い方が良いのですが、動画自体が持っている情報にそもそも限度があります。動画は fps (1秒あたりの情報量：フレーム枚数）が決まっていて、それ以上の枚数を切り出そうとしても意味がありません。

例えば 30 fps の動画なら、1秒に対して30枚より多く切り出しても意味が無いし時間の無駄です。

### 最適な枚数はどうやって見つけるか？

これは実験します。10枚/秒、20枚/秒、等で検出した精度から必要な量を決めます。少ない方が計算量が減り望ましいですが、精度が十分かどうかはわかりません。ケースバイケースです。

このケースでは10枚/秒が十分な精度が出ませんでした。30枚/秒で処理しています。

![gacha_100.png](https://qiita-image-store.s3.amazonaws.com/0/56465/ad5f7e3c-2ac8-a2be-9c64-54b4324743ff.png)

↑のガチャがモンスターの顔が回転している、つまり動いているため、10枚/秒では不十分でした。

## 切り出した画像をOpenCVのテンプレートマッチングにかける

![2a0fd0272aa40817a15f92fe295e07a4d997f3a4.png](https://qiita-image-store.s3.amazonaws.com/0/56465/1796955b-ae2b-95d8-c702-e671f8ad2dae.png)

↑を使います。

### ruby のサンプルコード

ruby-opencv テンプレートマッチのサンプル https://gist.github.com/ser1zw/2429501

↑が参考になります。

こんなコードでループを回しています

```opencv.rb
            template = OpenCV::IplImage.load("gacha.png")
            Dir.glob("*.png") do |file|
              image = OpenCV::IplImage.load(file)
              result = image.match_template(template)
              min_score, max_score, min_point, max_point = result.min_max_loc
              # min_score がR
            end
```

### なぜこの式を使うのか？

OpenCVにはこの式を含めて6種類ぐらい使えるメソッドがあります。この方法はこの中で最もシンプルです。数式を眺めているとわかりますが、やっていることは

**比較する画像をずらしながら画像を1ピクセルずつ比べる**

です。人間にはとても無理な作業をコンピューターは文句も言わずにこなしてくれます。あら、素敵 :heart: 

この式の方法はシンプルゆえ弱点があり、なんにでもつかえるというわけではありません。数式を眺めているとわかりますが、次のような条件があてはまる場合にとても強力です。

 - 対象の画像のサイズがほぼ一定
 - 対象の画像の色味（明るさ等）がほぼ一定
 - 対象が回転していない

例えば、工場のラインでカメラの位置が固定されていて、撮影環境（明るさ等）もコントロールできる場合、流れてきた製品を画像でチェックするような時に有効です。

YouTubeに投稿されているガチャ動画の場合、面白いことにこの条件をほぼ満たします。

### 他の式だとどうなるのか？

わかりません。うまくいくかもしれないし、うまくいかないかもしれません。数式の特徴からある程度の長所・短所は眺めているとわかりますが、やってみるのが一番です。先ほど書いた条件に合致すれば

![2a0fd0272aa40817a15f92fe295e07a4d997f3a4.png](https://qiita-image-store.s3.amazonaws.com/0/56465/1796955b-ae2b-95d8-c702-e671f8ad2dae.png)

は、強力ですが、合致しないケース、例えば、「車に取り付けたカメラで何らかの安全を確保する仕組みをつくる」には不向きなのは何となくわかってもらえると思います。

今回、ガチャの回転角度をマッチングしようとしてますが、もっと曖昧で良いケースもあります。たとえば、「肌色を検出して云々」といった場合は他の方法を採用する方が良いでしょう。

### いくつかの方法と特徴をおさえることで正しそうな選択ができる

画像認識全体に言えることですが、いくつかの方法とその特徴をおさえれば、取り組みたい認識対象によってより良い方法を選択できる可能性が高くなります。画像認識はまだまだ発展中の分野ですので、最先端の研究内容を把握しておきたいものです。

最近、全脳コンピューティングとかそんなものが流行っているらしいですが、あれは、もっと曖昧な感じです。人間もものごとの認識はかなりあいまいにやっているのでそれで十分ですね。

話がだいぶそれました・・・ :sweat_drops: 

### より高速なテンプレートマッチングをするには？

![2a0fd0272aa40817a15f92fe295e07a4d997f3a4.png](https://qiita-image-store.s3.amazonaws.com/0/56465/1796955b-ae2b-95d8-c702-e671f8ad2dae.png)

この式を眺めていると、1ピクセルずつ比べていることがわかります。というのは先ほど書いた通りです。このテンプレートマッチングには意外と時間がかかります。

サイズ、1280x720の動画の画像に対して、400x400ぐらいのパターンをマッチングしたとき、手元の環境では200msec以上かかりました。10000枚あったらそれだけで、2000秒で30分以上かかってしまいます。

| 画像サイズ組み合わせ | 正解率 | 処理時間 (msec) | 最良スコアR | ファイルサイズ (KB)|
|:-----------|:-:|------------:|-----:|-----:|
| T: 200x197, I: 640x360    | 50%  |        45.45 | 76.9*10^7| 302|
| T: 400x394, I: 1280x720   | 50%   |        275.87 | 312.5*10^7| 924|


このマッチングを高速にするにはどうしたら良いか？この式を眺めながら考えてみます。さぁ、もう一度よく眺めてください。

![2a0fd0272aa40817a15f92fe295e07a4d997f3a4.png](https://qiita-image-store.s3.amazonaws.com/0/56465/1796955b-ae2b-95d8-c702-e671f8ad2dae.png)

どうですか？

眺めていると、「Σしてるから、その部分を減らせばいいんじゃない？」と気づくはずです。気づいたあなたは天才です。そう、画像を小さくすればいいんです。「１ピクセル飛ばしてとか数ピクセル飛ばして比較したらいいんじゃない？」と思ったあなたも、天才です。画像を縮小することで同じ効果が得られます。おおおぉ。

 - 動画から抜き出した画像を小さくする
 - パターン（テンプレート）を小さくする

することで、計算量が減ることが式からわかります。というわけで小さくしてみましょう。小さくするときは、がつんと小さくするのがコツです。１辺を半分とは言わず一気に十分の一とかにしちゃいます。1280x720 => 128x72 みたいな感じで。

試してだめなら、その中間ぐらいにしてもう一度やってみるを繰り返しましょう。

動画から画像を切り出す :scissors: ときのコマンドにサイズを指定する場合は -s オプションを使ってこんな風にやります。

128x72 で出したい場合

```
ffmpeg -i video.mp4 -r 30 -s 128x72 thumbnail-%04d.png
```

640x360　で出したい場合

```
ffmpeg -i video.mp4 -r 30 -s 640x360 thumbnail-%04d.png
```


### サイズを小さくすると実は・・・

サイズを小さくすると↓この式の計算量が減ってハッピーですが、実は、、なんと、検出精度が向上することがあります

![2a0fd0272aa40817a15f92fe295e07a4d997f3a4.png](https://qiita-image-store.s3.amazonaws.com/0/56465/1796955b-ae2b-95d8-c702-e671f8ad2dae.png)

高速になって、精度もあがって、なんて、いいことづくしです :ramen: なんで精度があがるんでしょうか？

### なぜ精度が向上するのか？

続きはあとで書く

## 結果が出たら、該当箇所を切り出す

ffmpeg を使います。

続きはまた今度書きます。


## 5x5に画面を分割した動画をffmpegでつくる

### ffmpeg コマンドの例

ガチャ部分を切り出した動画25個を5x5のモザイク状に貼り付けた動画を ffmpeg の1コマンドでつくります。ffmpeg は -filter_complex というオプションで複雑な画像処理をすることができます。

```
ffmpeg  -i mosaic_background.png -i gacha_23.mp4 -i gacha_24.mp4 -i gacha_25.mp4 -i gacha_26.mp4 -i gacha_27.mp4 -i gacha_28.mp4 -i gacha_29.mp4 -i gacha_30.mp4 -i gacha_31.mp4 -i gacha_32.mp4 -i gacha_33.mp4 -i gacha_34.mp4 -i gacha_35.mp4 -i gacha_36.mp4 -i gacha_37.mp4 -i gacha_38.mp4 -i gacha_39.mp4 -i gacha_40.mp4 -i gacha_41.mp4 -i gacha_42.mp4 -i gacha_43.mp4 -i gacha_44.mp4 -i gacha_45.mp4 -i gacha_46.mp4 -i gacha_47.mp4  -filter_complex "[1:v]scale=256:144,setpts=PTS[out1];[2:v]scale=256:144,setpts=PTS[out2];[3:v]scale=256:144,setpts=PTS[out3];[4:v]scale=256:144,setpts=PTS[out4];[5:v]scale=256:144,setpts=PTS[out5];[6:v]scale=256:144,setpts=PTS[out6];[7:v]scale=256:144,setpts=PTS[out7];[8:v]scale=256:144,setpts=PTS[out8];[9:v]scale=256:144,setpts=PTS[out9];[10:v]scale=256:144,setpts=PTS[out10];[11:v]scale=256:144,setpts=PTS[out11];[12:v]scale=256:144,setpts=PTS[out12];[13:v]scale=256:144,setpts=PTS[out13];[14:v]scale=256:144,setpts=PTS[out14];[15:v]scale=256:144,setpts=PTS[out15];[16:v]scale=256:144,setpts=PTS[out16];[17:v]scale=256:144,setpts=PTS[out17];[18:v]scale=256:144,setpts=PTS[out18];[19:v]scale=256:144,setpts=PTS[out19];[20:v]scale=256:144,setpts=PTS[out20];[21:v]scale=256:144,setpts=PTS[out21];[22:v]scale=256:144,setpts=PTS[out22];[23:v]scale=256:144,setpts=PTS[out23];[24:v]scale=256:144,setpts=PTS[out24];[25:v]scale=256:144,setpts=PTS[out25];[0:v][out1] overlay=x=0:y=0 [oo1];[oo1][out2] overlay=x=256:y=0 [oo2];[oo2][out3] overlay=x=512:y=0 [oo3];[oo3][out4] overlay=x=768:y=0 [oo4];[oo4][out5] overlay=x=1024:y=0 [oo5];[oo5][out6] overlay=x=0:y=144 [oo6];[oo6][out7] overlay=x=256:y=144 [oo7];[oo7][out8] overlay=x=512:y=144 [oo8];[oo8][out9] overlay=x=768:y=144 [oo9];[oo9][out10] overlay=x=1024:y=144 [oo10];[oo10][out11] overlay=x=0:y=288 [oo11];[oo11][out12] overlay=x=256:y=288 [oo12];[oo12][out13] overlay=x=512:y=288 [oo13];[oo13][out14] overlay=x=768:y=288 [oo14];[oo14][out15] overlay=x=1024:y=288 [oo15];[oo15][out16] overlay=x=0:y=432 [oo16];[oo16][out17] overlay=x=256:y=432 [oo17];[oo17][out18] overlay=x=512:y=432 [oo18];[oo18][out19] overlay=x=768:y=432 [oo19];[oo19][out20] overlay=x=1024:y=432 [oo20];[oo20][out21] overlay=x=0:y=576 [oo21];[oo21][out22] overlay=x=256:y=576 [oo22];[oo22][out23] overlay=x=512:y=576 [oo23];[oo23][out24] overlay=x=768:y=576 [oo24];[oo24][out25] overlay=x=1024:y=576:shortest=1" -s 1280x720 -y mosaic.mp4
```