Pythonでマスク付きのテンプレートマッチングをする(OpenCV)

# 1.概要
みんな大好き大乱闘スマッシュブラザーズ(以下スマブラ)！！
私事ながら，スマブラ配信をしています．[チャンネル登録お願いします！](https://www.youtube.com/channel/UCKys_FIMcKfIn5L4HOyy0vQ)
スマブラ×機械学習でなんかできないかなー，と思っていたところこんな記事を発見しました．
[画像認識でスマブラの戦績を自動で作成するツールを作ろう](https://note.com/kelp168/n/nf717de3a2d22)
最終的に何を作るかは置いといて，面白そうだしとりあえずやってみよう！

前回，[Pythonで画像のエッジ検出と直線検出をする](https://qiita.com/kenshin-627/items/098703383374e1e7b65b)ことができました．
<font color="Red">今回の目標は，画面上部の直線から対戦開始タイミングを，テンプレートマッチングで対戦終了タイミングを検知することです．</font>
画像の上部にこんな角度の直線が検出されれば，この画像は対戦開始タイミング！
![before_130-400_straight.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3073654/eb792aa6-f46c-8618-4b9e-c3a513749773.png)
画像内に GAME SET が含まれていれば，この画像は対戦終了タイミング！
![after.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3073654/9b45a80c-bd54-8a15-b567-2271f1ca5e02.png)

# 2.対戦開始タイミングの検知
対戦開始タイミングは，画面上部に一定の角度の直線があります．これを検知することで，対戦開始タイミングかどうかを判断します．
[前回](https://qiita.com/kenshin-627/items/098703383374e1e7b65b)，直線の検出まで出来ました．今回はその直線が画面上部に一定の角度で2本存在するかどうかを確認します．

```python
import numpy as np

# 対戦開始のタイミングならTrue
def is_start(im_lines, error=10**-2):
    # 直線が検出されたときだけ，このフレームが対戦開始タイミングかどうかを判定する
    if im_lines is not None:
        # いい感じの直線の数
        line_count = 0
        # 画面の上部かどうかを判断する閾値
        threshold = 576*0.2
        for line in im_lines:
            line = line[0]
            # 直線が画面の上部かどうか(上部でないなら次の線)
            if line[0] > threshold:
                continue
            # いい感じの角度の直線ならカウントする
            if np.abs(line[1] - 1.5358897) < error:
                line_count += 1
                # 2本あれば対戦開始のタイミング
                if line_count == 2:
                    return True
    return False
```
今回は，引数で与えた直線が対戦開始タイミングのものならTrueを返してくれるis_start関数を作りました．
画面の上部かどうかを判断する閾値は576×0.2としました．これは，今回使用する画像の縦のサイズが576で，上から20%ぐらいまでを画面の上部とするイメージです．(直線が水平線ではないので，厳密には上部20%とは少し異なります．)
角度については対戦開始時，画面上部にある直線の法線の角度が1.5358897ラジアンなので，検出された直線の法線の角度$\pm10^{-2}$に1.5358897がおさまればオッケーとしました．

試しに6試合分の対戦動画から定期的にスクリーンショットを撮り，is_start関数で判定したところ，正しいタイミングで6回検知されました．
特に問題なく対戦開始タイミングを検知できてそうです．

# 3.対戦終了タイミングの検知
対戦終了タイミングは画面中央に**GAME SET**の文字が表示されます．これをテンプレートマッチングで検知します．
テンプレートマッチングをすると，スクリーンショット(入力画像)の局所領域と**GAME SET**の画像(テンプレート画像)の類似度がわかります．
テンプレートマッチングの全般的な内容は[こちら](http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_imgproc/py_template_matching/py_template_matching.html)，マスクについては[この記事](https://zenn.dev/kazaki/articles/893b37b1d1c78c)を参考にしました．

## 3-1.テンプレート画像とマスク画像の作成
テンプレート画像のうち，マッチングに使用したくない箇所を黒，使用したい箇所を白としたマスク画像を用いることによって，**GAME SET**の文字のみをマッチングに使用できます．

まず，テンプレート画像とマスク画像の元となる画像を用意します．これは適当に画像編集ソフトを使って作成します．
今回はこれをテンプレート画像の元とします．
![gameset_all.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3073654/08ff00f2-261a-f137-1332-871530043c2a.png)
マスク画像の元はこんな感じ
![gameset_mask.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3073654/219109d5-b6f5-afc1-00a3-bf12e4be052d.png)

テンプレート画像はグレースケール，マスク画像は白黒に二値化し，サイズを調整します．
```python
import cv2

# テンプレート画像とマスク画像を作る
def make_template(template_path, mask_path):
    # 引数で与えられたパスからグレースケールで画像を読み込んでリサイズ
    template_gray = cv2.resize(cv2.imread(template_path, cv2.IMREAD_GRAYSCALE), dsize=(1024, 576))
    template_mask = cv2.resize(cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE), dsize=(1024, 576))
    # 128を閾値としてマスク画像を二値化
    _, template_mask = cv2.threshold(template_mask, 128, 255, cv2.THRESH_BINARY)
    # 画像の周り1/10省いてちょっとずれてもマッチングできるようにする
    template_gray = template_gray[int(576/20):int(-576/20), int(1024/20):int(-1024/20)]
    template_mask = template_mask[int(settings.DSIZE_H/20):int(-settings.DSIZE_H/20), int(settings.DSIZE_W/20):int(-settings.DSIZE_W/20)]
    return template_gray, template_mask

gameset_gray, gameset_mask = make_template("Image/gameset_color.png", "Image/gameset_mask.png")
```

テンプレート画像とマスク画像の元となる画像が保存されているパスを指定することで，テンプレート画像，マスク画像を作成してくれる関数を作りました．
まず，元画像をグレースケールで読み込み，テンプレートマッチングする画像と同じサイズにリサイズします．今回はテンプレートマッチングに使用するスクリーンショットが1024×576なのでそれに合わせます．
次に，マスク画像は二値化する必要があるのでOpenCVのthreshold関数で二値化します．第四引数にcv2.THRESH_BINARYを指定することで，第二引数を閾値として二値化できます．戻り値は二つ返ってきますが，一つ目は二値化に使用した閾値なので今回は使いません．
threshold関数については[この記事](https://qiita.com/tokkuri/items/ad5e858cbff8159829e9)を参考にしました．
最後に，画像の上下左右をそれぞれ1/20ずつ省いて少し小さくします．テンプレート画像とスクリーンショットのサイズが全く同じだと，完全に同じ位置に**GAME SET**の文字がなければ上手く検知できません．そのため，テンプレート画像を少し小さくすることで，**GAME_SET**の位置が少しずれていても検知できるようにしています．

## 3-2.テンプレート画像とマスク画像を使ってパターンマッチング
3-1.で作成した画像を使用して，いよいよパターンマッチングを行います．
パターンマッチングはOpenCVに用意されているmatchTemplate関数で簡単に実装できます．
```python
import cv2

# 対戦終了のタイミングならTrue
def is_finish(im_gray, gameset_gray, gameset_mask):
    res = cv2.matchTemplate(im_gray, gameset_gray, cv2.TM_CCOEFF_NORMED, gameset_mask)
    _, max_val, _, _ = cv2.minMaxLoc(res)
    is_fin = True if max_val > 0.37 else False
    return is_fin
```
引数で与えた画像(im_gray)に，**GAME SET**が含まれていればTrueを返してくれるis_finish関数を作りました．
まず，matchTemplate関数でテンプレートマッチングを行います．戻り値として，im_grayの各局所領域と**GAME SET**画像の類似度が返ってきます．
次に，minMaxLoc関数で類似度の最大値を調べます．戻り値は四つで，それぞれ類似度の最小値，最大値，最小値の領域，最大値の領域となっていますが，今回は**GAME SET**が入力画像に含まれているかどうかだけを知りたいので，類似度の最大値のみを使用します．
今回は，類似度の最大値が0.37を超えていれば入力画像に**GAME SET**が含まれている(Trueを返す)，超えていなければ含まれていない(Falseを返す)と判断することにします．

こちらも同様に，6試合分の対戦動画から定期的にスクリーンショットを撮り，is_finish関数で判定したところ，正しいタイミングで6回検知されました．
特に問題なく対戦終了タイミングを検知できてそうです．

# 3.まとめ
- [前回](https://qiita.com/kenshin-627/items/098703383374e1e7b65b)やったこと
    - 画像をグレースケールに変換
    - 画像のリサイズ
    - Canny法でエッジ検出
    - Hough変換で直線検出
- 今回やったこと
    - 検出した直線から対戦開始のタイミングを検知
    - グレースケール画像からテンプレートマッチングで対戦終了のタイミングを検知
- 次回やること(予定)
    - 対戦開始タイミングで文字を認識してキャラクター名の検出
    - 対戦終了タイミングで数字を認識して勝敗の検出

# 4.終わりに
年末年始，結構だらけちゃいました．気付いたら作り始めてから二週間以上経ってるし，一ヶ月かけても完成しなさそうで震えてます．今日から本気出す，はず．．
次回，初めてのOCR！！サービス，サービスゥー

# 参考文献
[画像認識でスマブラの戦績を自動で作成するツールを作ろう](https://note.com/kelp168/n/nf717de3a2d22)
[テンプレートマッチング](http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_imgproc/py_template_matching/py_template_matching.html)
[マスク付きテンプレートマッチングでスプラトゥーン3のイカランプを認識させる](https://zenn.dev/kazaki/articles/893b37b1d1c78c)
[OpenCV 画像の二値化](https://qiita.com/tokkuri/items/ad5e858cbff8159829e9)