【Splatoon2】"曖昧かつ厳密"な画像認識で8836回のガチャ結果から排出率を計算する

自動でガチャを回したい方はこちらを参照：
[ギアのかけらやチケットを無限に入手！Switch操作自動化のススメ【Splatoon2】](https://qiita.com/Anaakikutsushit/items/b50b092c5ec43d83808a)

# 排出率
## 前提
### NAMACOポイントガチャとは
NAMACOポイントガチャとは、深海メトロ中央駅（A00駅）にて15000ポイントと引き換えにアイテムをゲットできる自販機のことです。
Splatoon2の有料追加コンテンツ「オクト・エキスパンション」の本編クリア後に利用可能になります。
![20191017114618.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/336249/a418b534-7fae-e50e-95c1-9f9e51a1192f.jpeg)

### 今回算出した排出率
今回は、ガチャを1度回したとき、それぞれのアイテムがどの程度の確率で手に入るかを計算しました。

## 手に入るアイテムと計算結果
![rsu_qsj_rsu.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/336249/dad2b87b-dc65-dc39-0ee2-14cc4794840f.jpeg)

小数点以下第3位まで計算しました。有効数字とかよく考えていません。
標本数は8836、うち自動検出できた7948のガチャ結果を計算に用いています。

|手に入るアイテム|確率(%)|
|:--------------|-----:|
|**おカネ**|**45.370**|
|4000|25.214|
|8000|16.734|
|20000|2.177|
|40000|1.246|
|**フードチケット**|**22.622**|
|おカネ1.5倍|9.273|
|同、2.0倍|1.032|
|同、2.5倍|1.195|
|経験値1.5倍|8.946|
|同、2.0倍|1.095|
|同、2.5倍|1.082|
|**ドリンクチケット**|**21.917**|
|いずれか1枚|19.716|
|いずれか3枚|1.988|
|14種類を1枚ずつ|0.214|
|**ギアパワーのかけら**|**10.091**|
|いずれか1つ|0.582×14種=8.148|
|同一の種類のかけらを3つ|0.093×14種=1.302|
|同一の種類のかけらを5つ|0.022×14種=0.308|
|同一の種類のかけらを10つ|0.024×14種=0.336|

太字の結果は同一のカテゴリの小計です。
小計値は切り捨てた分も考慮しているため、表上の数値の合計とは異なります。
ギアパワーごとに14種ずつ存在するドリンクチケットおよびギアパワーのかけらについては、ギアパワーごとに確率にバラツキがありました。
生データが見たい方は[こちらのスプレッドシート](https://docs.google.com/spreadsheets/d/1RD-aQfWY2cDWe0cwKDi4ns9u6h7TB2PFiRU34MJ0zVc/edit?usp=sharing)をご参照ください。

# 計算方法
## 標本を用意する
確率を計算するにあたって、それなりの数の標本数が必要になります。
ただし私は統計学に明るくないため、どの程度の数の標本があれば妥当なのかを判断することは出来ません。
**とりあえず大量にあればあるだけ良いだろう**の精神でやっていきます。

標本を用意するとはつまるところ、ガチャを回して回して回しまくるということ。
ガチャを回し、その結果を記録する工程に関しては自動化して行いました。
詳しくは下記の記事をご参照ください。
[ギアのかけらやチケットを無限に入手！Switch操作自動化のススメ【Splatoon2】](https://qiita.com/Anaakikutsushit/items/b50b092c5ec43d83808a)

![2d7e06f757ee0067632b8daa2e11f481.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/336249/0130568d-19e8-1185-0c5c-f4b181761041.png)
空いた時間にガチャを回しまくり、microSDカード経由でスクショをPCに移動します。
そうして8836枚のガチャ結果のスクショを用意することが出来ました。

## 画像認識でガチャ結果を仕分ける
専用のプログラムを作成し、そこに画像を突っ込んだら自動でガチャ結果を判別して返してくれるようにします。
完成したものがこちらになります↓
https://github.com/anaakikutushita/Analyzing-NAMACO-Gacha

### 基本的な考え方
先にお手本となる画像を用意しておきます。
そこに自動判別したい画像が入ってきたら、お手本の画像と比較して、どの画像と似ているかを判別します。
![57ed862d8dbc680bfb68cf48b330027f.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/336249/36854f9f-a25b-5f1f-9837-e280314c1710.png)
![IMG_20191018_071926-700x1137.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/336249/f0dd5317-71a6-7edd-5c12-064a1d4ba631.jpeg)
![2019051301435800-C616B031331154665D639EF16DA76BC0.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/336249/f8d5f5e9-cd05-db5a-58cf-e3efeb0d8e30.jpeg)

### 画像比較のコアロジック
画像を比較する方法は、**ピクセルごとの二乗平均平方根を計算する方法**を採用しました。
ただ、二乗平均平方根を計算する方法の意味はちゃんと分かっていません。
計算は以下のように行うみたいです。

1. 各ピクセルの色の差を計算して二乗する
2. 二乗したものを全てのピクセルに渡って足し上げる
3. 平均を取る

この方法だと（なぜか）後述する問題点をパスしてうまく画像を判別することが出来ました。
ただし、計算する前に①グレースケール化して、②適度に画像をボカすというステップを踏んでおります。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Splatoonの各種アイコン類は視認性が高くなるように考え抜いてデザインされているそうです。私のNAMACOガチャ排出率解析機では画像をグレースケールにした上ボカして比較材料に使っているのですが、こんな加工した画像でもしっかり判定できるほどの視認性の高さは流石ですね <a href="https://t.co/uXsUFs6lia">https://t.co/uXsUFs6lia</a></p>&mdash; あつし🎮デジゲー博B-04a (@Anaakikutsushit) <a href="https://twitter.com/Anaakikutsushit/status/1184373081738895361?ref_src=twsrc%5Etfw">October 16, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

ソースコードでは以下のように実装されています。
引数の`cropped_image`, `model`はいずれも[Pillow](https://pillow.readthedocs.io/)の`Image`オブジェクトです。
`threshold_difference`は何となく8に設定したら上手く行った感じでした。
本当なら計算の結果から妥当な値を設定すべきなんでしょうけど、力尽きました。

```.py
def is_same_image(cropped_image, model, threshold_difference):
    # グレースケール化してから画像をガウスぼかしして、ピクセルごとの二乗平均平方根を使う
    grayed_cropped = cropped_image.convert('L')
    grayed_model = model.convert('L')

    rad = 2
    gaussed_cropped = grayed_cropped.filter(filter=ImageFilter.GaussianBlur(radius=rad))
    gaussed_model = grayed_model.filter(filter=ImageFilter.GaussianBlur(radius=rad))

    hist = ImageChops.difference(gaussed_cropped, gaussed_model).histogram()
    # 計算式はググって出てきたものをコピペしただけで、意味はよくわからない
    difference = math.sqrt(reduce(operator.add,
                                  map(lambda hist,
                                      i: hist*(i**2),
                                      hist,
                                      range(256))) / (float(gaussed_model.size[0]) * gaussed_cropped.size[1]))

    is_same_image = difference < threshold_difference
    return is_same_image
```

途中でヒストグラムを計算していますが、コメントでも書いている通り、何でヒストグラムが必要なのかは私にもよくわかりません。
途中の`map`式も謎です。優しく解説してくれる方がいらっしゃると嬉しいです。

ただし、この方法でもあらゆるスクショを正しく判別することは出来ませんでした。
おおよそ10%の割合で、ガチャ結果が何なのかを判別できなかったスクショが出てきます。
各スクショの判別に失敗した理由は調査していません。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">今度は検出失敗したのは888枚になりました。検出成功率は90%弱まで向上。これなら文句はないでしょう。</p>&mdash; あつし🎮デジゲー博B-04a (@Anaakikutsushit) <a href="https://twitter.com/Anaakikutsushit/status/1184383453548072961?ref_src=twsrc%5Etfw">October 16, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

下記には、なぜこの二乗平均平方根を計算する方法に行き着いたのかを付記しておきます。

#### ボツ1：画像の完全一致
単純に「画像が完全に一致するかどうか」を判定する方法は使うことが出来ません。
なぜなら、入力されるスクショのピクセルが背景によって微妙に異なってしまうからです。
![2019051316491900-C616B031331154665D639EF16DA76BC0.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/336249/1601544b-d3ae-0e26-81ac-6b899cd5713f.jpeg)
上記画像では、スクショを撮影するときの画角によって、背景部分のピクセルが緑っぽかったり赤っぽかったりしてしまう様子を確認できます。
もちろん、ガチャ自体は自動で回しています。
しかしコントローラ入力を行うマイコンの挙動に揺らぎが発生するため、生成されるスクショは毎回異なってしまうのです。
従って、入力画像とお手本画像が完全に一致しているかどうかという方法は使えませんでした。

#### ボツ2：ヒストグラム比較
過去記事でも登場した、私のお得意の比較方法です。
その時は、スプラトゥーンのステージ画像がどこのものか？を判別するために利用しました。
参考：[画像認識でスプラトゥーンの大会運営を自動化するbotを作りました](https://qiita.com/Anaakikutsushit/items/07fe4a194a5279be1fa3#%E7%94%BB%E5%83%8F%E3%81%8B%E3%82%89%E3%82%B9%E3%83%86%E3%83%BC%E3%82%B8%E3%82%92%E8%AA%AD%E3%81%BF%E5%8F%96%E3%82%8B)
（いま考えると、これは画像の完全一致の方法でも良かったかも）

これは特定の一色の出現頻度だけに注目すればよいという方法です。複数色に拡張することも出来ます。
そのため、上記のような別の色が混ざり込んでいる場合でも比較的うまく判別できることが期待されます。
実際、ある程度まで上手く行っていたのですが、数字の認識でコケてしまいました。
![0a4337410356ed558hg23d05e6c89eaca.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/336249/8cf43ce8-20ed-20ce-d995-cba5e19d3047.png)
数字の画像はどれも「背景が緑っぽくて、白いピクセルが多めに登場している」という共通の特徴を持っています。
この特徴がまさしくヒストグラム比較をする際に壁となりました。
どの画像も「白ピクセルの登場頻度は同じくらいだよ」という結果が出てしまうんですね。
従ってヒストグラム比較の方法もボツとなりました。

## 結果を出力する
判別結果はCSVにして出力し、その後の計算は手作業で行うことにしました。
この部分も自動化できたらカッコよかったんですが、何をどう計算したら良い感じの結果が出るのかをイメージできずに手作業となりました。
結果は前出の通りですが、[こちらのスプレッドシート](https://docs.google.com/spreadsheets/d/1RD-aQfWY2cDWe0cwKDi4ns9u6h7TB2PFiRU34MJ0zVc/edit?usp=sharing)で確認できます。
1行がガチャ1回の結果に相当します。
最下部で結果を集計しています。
しているのですが、メチャメチャ分かりづらいですね。スミマセン……。

将来的には、下記の工程まで含めて全て自動化したいと考えています。

- 撮影したスクショをTwitterに投稿して、本体からスクショを削除してガチャ操作に戻る
- Twitterに投稿されたスクショを拾って結果を判別し、結果一覧に新しく加える
- 新規の結果も含めて確率を再計算する
- 再計算の結果を適当なWebページに掲載して更新する

Webと連携する周りの知見が全く無いので、どなたか技術のある人にご協力を頂けると助かります。

### 期待値を計算してみよう
前出のスプレッドシートの計算結果を用いて、実際に期待値を計算してみます。

#### ヒト速のかけら
私はヒト速が大好きですので、ヒト速のかけらの入手期待値を計算してみましょう。
J列「chunks_run_speed_up」に注目します。

- かけら1個が出る確率：0.541%
- かけら3個が出る確率：0.063%
- かけら5個が出る確率：0.038%
- かけら10個が出る確率：0.025%

以上の結果を確認できます。従って
0.00541*1 + 0.00063*3 + 0.00038*5 + 0.00025*10 = 0.0117個
これがガチャ1回あたりで手に入るヒト速のかけらの期待値となります。
逆に言えば、ヒト速のかけらを1個手に入れるためには
1 / 0.0117 = 85.47……
おおよそ86回程度ガチャを回せばよいということがわかります。
**どう考えてもガチャを回すよりバトルした方が効率がいいです。**

#### ヒト速チケット
ガチャからかけらを直接手に入れるのは効率が悪いとわかりました。
それならば妥協案としてヒト速チケットの期待値を計算してみましょう。
X列「drinks_run_speed_up」に注目します。

- チケット1枚が単独で出る確率：2.051%
- チケット2枚が被りで出る確率：0.025%
- チケット3枚が被りで出る確率：0.000%

またこれに加えて、14種類が一度に1種ずつ手に入るときにもヒト速チケットを入手できます。
![drink_14.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/336249/8ad5db49-0357-c13e-2852-ff1580d50d1e.jpeg)
これはAM列「fourteen_tickets」に注目することで

- 14枚が一度に出る確率：0.214%

と確認できます。
以上の結果から期待値を計算すると、
0.00214*1 + 0.02051*1 + 0.00025*2 + 0.00000*3 = 0.02315枚
これがガチャ1回あたりで手に入るヒト速チケットの期待値となります。
同様に、ヒト速チケット1枚を手に入れるために必要になりそうなガチャ回数の期待値は
1 / 0.02315 = 43.19……
となります。
おおよそ44回ガチャを回せばヒト速チケットが1枚手に入ると考えて良さそうです。

#### つまるところ
現在の自動化プログラムでは、ガチャを1回引くためには17分程度の時間がかかります。
ヒト速チケットを引くためにはだいたい17 * 44 / 60 = 12.46……
13時間ぐらい自動ガチャを回せば良さそうですね。
1日のうち、7時間の睡眠中 + 8時間の出勤中としてざっくり考えると、**平均して毎日1枚のチケットを手に入れることが出来る**ことがわかります。
毎日1枚チケット消費してヒト速を付けると考えると現実的な感じがしますね👌
この結果が得られてよかったです。

# あなたも計算してみよう
私が作ったこのプログラムですが、あなた自身が利用して各種確率を追検証することもできます。
Pythonのインストールが済んでいることを前提にします

1. 過去記事→を参考に自動化環境を構築する。[ギアのかけらやチケットを無限に入手！Switch操作自動化のススメ【Splatoon2】](https://qiita.com/Anaakikutsushit/items/b50b092c5ec43d83808a)
2. 自動でガチャを回してスクショを大量に用意する。
3. 解析プログラム→をダウンロードする。https://github.com/anaakikutushita/Analyzing-NAMACO-Gacha
4. 解析プログラムの中の`input_screenshots`にスクショを全部入れる。
5. `main.py`を実行する。
6. `result_*日時*.csv`というファイルに結果を出力する

私と同じような結果になるか、ぜんぜん違う結果になるか、ぜひ追検証してみてください。