【python】【ポケモンSV】レイドバトル画像から情報を抽出する

# 0 はじめに
poke-controllerを使ったレイド自動化プログラムを作成中なので、レイド画面から情報を抽出する部分を備忘録として記しておきます。初心者ですので間違っている部分があるかと思います。気になる点があれば教えてください。

:::note info
poke-controller自体にも画像認識機能は備わっていますが、そちらを使用したプログラムの紹介ではありません。内容は主にpythonで画像を編集するものになっています。
:::

# 1 やりたいこと
先日発売されたポケモンSVにはレイドバトルというものがあり、このレイドバトルに勝利すると様々な報酬がもらえます。レイドバトルには主に以下の3つの情報があり、それに応じて戦法を変えることで、自動化・通常プレイに関わらず勝率を高めることができます。レイド情報がある画面はこんな感じになっており、主に①難易度・②テラスタイプ・③敵ポケモンの3つの情報が記載されています。
![テラレイドバトル.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2514045/d00b9bf1-4e6b-a768-8e66-f1fbdc97ec91.png)
やりたいことは、できるだけ安定し・環境に依存しない方法でにこれらの情報を読み取ることです。このような画像は機材・設定等の環境によって色が変わってしまうため、できるだけそれらの影響を受けない・または後から調整がし易いようなプログラムを心がけてみます。
他環境での検証はしていないので、あくまで心がけ程度ですが…

# 2.レイド情報読み取り（難易度）
とりあえず情報のある部分をクリップしてみます。黄色い星の数が難易度で、これはスカーレットver（以下S）の難易度3です。テラレイドバトルには現時点で難易度1~7までありますが、通常レイドの1~5は基本的にこの背景です。クリア後は難易度1と2は出ないらしいので、今回の対象は難易度3~7となります。
![S_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2514045/88dfb993-febf-880e-fdc2-267c7035096a.png)
厄介なのは、この背景はバージョンが違うと変わります。バイオレットver(以下V)では、このようになります。背景が紫色になっており、画像処理をする上では面倒です。
![V_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2514045/26006607-4464-dcee-b415-72fa904c820a.png)
星の色は同じかと思いきや、必ずしもそうではありません。難易度6のテラレイドバトルでは、背景の色とともに星の色も変わります。（多分これはver違いないですよね…？）
![S_6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2514045/4cb453eb-e701-c906-2074-119f5138f509.png)
更に期間限定のイベント中は、難易度7のテラレイドバトルの他、難易度3~5でもイベント仕様の画面になることがあります。
![S_7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2514045/d9de7d33-e829-c0e3-562e-eeae0fb8119e.png)
これらの画像から背景を消去し、難易度を判定することを考えていきます。

グレースケールからの二値化ではうまくいきませんでしたので、今回は画像の淵の色の類似色を消していくという方法で背景を消します。プログラムは以下のようになります。

```python
# 切り取り範囲
top = 400
btm = 450
lft = 660
rgt = 1110
ss_crop = ss[top:btm, lft:rgt].copy()  # ss:取得したゲーム画面

# 外周の色を抽出し背景を削除
atol = 15  # 色の誤差を許す範囲(要調整)
bkg_cls = np.concatenate([ss_crop[0, :, :], ss_crop[-1, :, :], ss_crop[:, 0, :], ss_crop[:, -1, :]], axis = 0)  # 外周の色
bkg_cls = np.unique(bkg_cls, axis = 0)  # ユニークな色のみ残す
for bkg_cl in bkg_cls:
    ss_crop[np.where(np.isclose(ss_crop, bkg_cl, atol = atol).all(axis = 2))] = [0, 0, 0]  # 背景部色を黒にする
```
![編集前後.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2514045/493eabb8-8665-285d-cd1d-a0f4392ca3dc.png)
多少星の淵に背景が残ってはいますが、背景を削除することができました。他ver、イベントレイドでも同様に処理できます。
![編集前後まとめ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2514045/d844bb83-d0c5-b06d-adf9-a8954b290bcd.png)
二値化した難易度3~7の画像をテンプレート画像として保存しておき、比較することで難易度を判定する関数は以下になります。今のところこれで安定しています。
```python
# tpls:予め用意したテンプレート画像
# 先程の画像を二値化・グレースケール化し、背景部分を0，星の部分を255としたもの
# tplsの中には[文字列(難易度)，画像(0or255のndarray)]のリストが難易度3~7まで入っている
def check_star(ss, tpls, atol):
    # 切り取り範囲
    top = 400
    btm = 450
    lft = 660
    rgt = 1110
    ss_crop = ss[top:btm, lft:rgt].copy()
    # 背景色削除
    bkg_cls = np.concatenate([ss_crop[0, :, :], ss_crop[-1, :, :], ss_crop[:, 0, :], ss_crop[:, -1, :]], axis = 0)  # 外周の色
    bkg_cls = np.unique(bkg_cls, axis = 0)  # ユニークな色のみ残す
    for bkg_cl in bkg_cls:
        ss_crop[np.where(np.isclose(ss_crop, bkg_cl, atol = atol).all(axis = 2))] = [0, 0, 0]
    ss_bl = np.all(ss_crop != [0, 0, 0], axis = 2)  # True/Falseで二値化
    
    # 予め保存した画像と最も一致するものを選択
    rr = 0
    for star, tpl in tpls:
        tpl_bl = tpl != 0  # True/Falseで二値化
        r = np.sum(ss_bl & tpl_bl) / np.sum(ss_bl | tpl_bl)  # 一致度を0~1で計算
        if r > rr:
            rr = r
            res = star
    return rr, res  # 一致度、難易度を返す

rr, star = check_star(ss, tpls_star, 15)
```
# 3.レイド情報読み取り（テラスタイプ）
そもそもテラスタイプというのは、SVから登場した要素で、ポケモンのタイプを戦闘中に1度だけ変えることができるというものです。テラレイドバトルで出現する敵ポケモンはそのポケモン自身のタイプではなくランダムなテラスタイプ単タイプとなっています。現在ポケモンには18種類のタイプがあります。
![タイプ一覧.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2514045/0e024cfe-a6ed-335e-71f2-71315ef3c65d.png)
難易度6やイベントでも同様に処理できるか検証するために、適当に違う背景も混ぜています。この場合は注目したい部分の色が同じなので、グレースケール化し閾値を用いて二値化していきます。閾値を適当に調整して、二値化します。
```python
threshold = 45  # 閾値（要調整）
ss_gray = cv2.cvtColor(ss, cv2.COLOR_RGB2GRAY)  # グレースケール化
_, ss_gray = cv2.threshold(ss_gray, threshold, 255, cv2.THRESH_BINARY)  # 二値化
```
![タイプ一覧_gray.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2514045/470fbb5f-e11a-0084-3a1b-a85187a62b2c.png)
背景が異なるものもありましたが、かなりうまく二値化できています。後は難易度の時と同じで、予め用意したテンプレート画像と比較して最も一致するものを決めます。ただし、今回は注目したい部分が黒色なのでTrue/Falseの取り方を逆にしています。
```python
# tpls:予め用意したテンプレート画像
# 先程の画像を二値化・グレースケール化し、タイプ部分を0，星の部分を255としたもの
# tplsの中には[文字列(テラスタイプ)，画像(0or255のndarray)]のリストが18タイプ入っている
def check_ttype(ss, tpls, threshold):
    # 画像の切り取り
    top = 100
    btm = 200
    lft = 1005
    rgt = 1105
    ss_crop = ss[top:btm, lft:rgt].copy()
    # 二値化
    ss_crop = cv2.cvtColor(ss_crop, cv2.COLOR_RGB2GRAY)
    _, ss_crop = cv2.threshold(ss_crop, threshold, 255, cv2.THRESH_BINARY)
    ss_bl = ss_crop == 0  # True/Falseで二値化
    # 最も一致するものを選択
    cr_max = 0
    rr = 0
    for ttype, tpl in tpls:
        tpl_bl = tpl == 0  # True/Falseで二値化
        r = np.sum(ss_bl & tpl_bl) / np.sum(ss_bl | tpl_bl)  # 一致度を0~1で計算
        print(ttype, r)
        if r > rr:
            rr = r
            res = ttype
    return rr, res  # 一致度、難易度を返す
```

# 4.レイド情報読み取り（敵ポケモン）
最後は敵ポケモンですが、これについてはタイプの時と全く同じ手法で、範囲だけ変えればできます。
![ヤドキング.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2514045/3bf50b39-2808-da2a-eea8-eebd7a7e1ad3.png)
ただし問題があり、レイドで出現する可能性があるポケモンの種類が多く、テンプレート画像を揃えるのが難しいです。ここについてはいい方法はあるのでしょうか…？現状はポケモンシルエットクイズを解きまくって、ゴリ押しである程度集めたものを使っています。例外があればその都度収集という感じでとりあえず進めています。

# 5.おわりに
レイド画面から情報を抽出する方法は以上となります。どこかで誰かの参考になれば幸いです。最後に、poke-contorollerをはじめとした様々な自動化ツールの開発・発展に貢献してくださった先人達には感謝しかありません。本当にありがとうございます。