天下一反省会!

電子工作、プログラミング、読書など

\今月のイチオシ記事/

おススメ記事1
おススメ記事2
おススメ記事3
おススメ記事4

【物体検出】python+OpenCVで麻雀ゲームの画面から手牌を分析するツールを作ってみよう

研究でいつか使うかもしれないpyhtonの練習がてら、麻雀ゲーム雀魂のスクリーンショットから自身の手牌を検出し、テキストとして出力するプログラムを作成してみましたた。

python初心者ながら納得のいくレベルのツールを作ることができたので、作成手順について紹介しようと思います。

 
 

・低スペックPCしかもっていないが、物体検出をやりたい。
・AIや複雑な計算は分からないが物体検出をやりたい
・検出したい図形が決まった形、あるいはマークである(麻雀牌やトランプの絵柄)
・pythonを始めてみたいけれど、作りたいものが特にない

・とりあえずコードをコピペして実行してみたい

 

AIを使わなくても、物体検出はできる!

”物体検出”と聞くと、AIに大量の画像を学習させなければならない、というイメージを持たれている方も多いのではないでしょうか。


しかし、実際にはAIを用いなくても物体検出は行えます!(もちろんAIを用いたほうが、より高精度かつ汎用性の高い物体検出ツールが作れます。)

今回は、AIを用いずに行う物体検出の手法の一つである、"テンプレートマッチング"を活用して、物体検出を行ってみます。

 

テンプレートマッチングとは

 

テンプレートマッチングは調べたい画像の中に、見つけたい要素、すなわちテンプレート画像が含まれているかどうかを検出することができる。

 

詳細は割愛するが、簡単に言えば調べたい画像の上にテンプレート画像を重ね、テンプレート画像の位置を少しずつずらしながら、両者の類似度が高い点を検出するという手法である。

 

図で説明するとこんな感じ

 

f:id:potala123:20220414123845p:plain

テンプレートマッチングのイメージ

 

今回は、雀魂のプレイ画面のスクリーンショットを元画像、牌の画像をテンプレート画像としてテンプレートマッチングを行った。

 

 

 

画像処理に強いライブラリ「OpenCV」を使ってみる

 

テンプレートマッチングを行うための機能を実装するには、複雑なコードを書かなければならない。

 

しかしながら、OpenCVと呼ばれるライブラリ(便利機能の詰め合わせセットみたいなもの)を用いれば、あらかじめプロが書いたコードを引っ張ってくるだけで簡単にテンプレートマッチングを行うことができる

 

OpenCVはテンプレートマッチングだけでなく、画像のサイズ変更やトリミング、色調の操作など、画像に関するあらゆる機能が搭載されているため、画像処理を行いたい人は最優先でOpenCVの使用を検討すべきだろう。

 

OpenCVの導入は非常に簡単だった。OpenCVの導入は非常に簡単だった。導入方法については「OpenCV 導入方法」で検索すれば色々出てくると思う。

 

 

まずはおおまかな設計を考えてみる

さて、先ほどもちらっと書いたように、今回は雀魂プレイ画面のスクリーンショットに牌の画像を重ねることでテンプレートマッチングを行おうと考えた。

 

そこでまずは、全種類の牌の画像を180×120のサイズで作成し、これらをテンプレート画像とした。

 

手始めに、テンプレート画像1枚を用いてテンプレートマッチングを行ってみる。


今回は4pの180×120の画像をテンプレート画像とし、雀魂のプレイ画面のスクリーンショットに対して、テンプレートマッチングを行ってみた。

 

この際に参考にした動画を紹介しておく。

 

youtu.be

 

 

コードを作成し実行してみると、次のような結果となった。

f:id:potala123:20220414135221p:plain

 

上図、2枚ある4pのうち、右側のものが四角く囲まれているのが確認できる。

 

実行時のターミナルの表示を見ると、

min_val: 0.0,max_val: 0.9998940229415894,min_loc: (2616, 376),max_loc: (1000, 1547)

 

と書いてあった。ここでmax_valは類似度が最も高かった点の類似度(最大で1)、max_locは類似度が最大となった点の座標をそれぞれ示している。

 

以上より、テンプレート画像と類似している点(4pの位置)をしっかりと判定することができたことがわかる。

 

補足:テンプレートマッチングを行う際に類似度を導出する方法は6種類ある。個人的なおすすめはcv2.TM_CCOEFF_NORMEDという手法である。おすすめの理由としては

・誤検出する回数が少なかった

・類似度が最大で1となるため、扱いやすい(「類似度0.9以上の点を検出」などとすれば類似度の高い複数の点を同時に扱える)

 

以降は、TM_CCOEFF_NORMEDを用いるという前提で話を進めていく。

 

類似度の高い点を何か所か特定してみる

 

今行った方法では、類似度が最も高い一か所しか検出することができない。

 

そこで、類似度がある一定の値よりも高い点の座標をすべて出力するようにプログラムを書き換えて実行してみた。

 

※以下の記事を参考にしたので、示しておく。

 

pystyle.info

 

とりあえず、4pのテンプレート画像との類似度が0.98以上になる点の座標をすべて出力するようにプログラムを修正し、実行してみた。

 

実行結果は次のようになった。

 

ターミナル出力: 座標:(865, 1547) 座標:(1000, 1547)

 

得られた画像

 

f:id:potala123:20220414152459p:plain

 

ターミナルの表示から、類似度が0.98より大きい点は2か所検出されたことが分かる。

 

また、得られた画像より、それらの2点は4pの位置であり、きちんと物体検出が行えていることが分かった。

 

とりあえず、手牌の中から特定の種類の牌をすべて探し出すことは可能なようだ。

 

手牌をすべて検出してみる

ここからはいよいよ、手牌のすべての牌の検出を目指していく。

 

プログラムを書く前に、どのような手順で処理を行うか簡単に考えてみる。

 

処理の流れとしては、

 

牌の種類を1つ指定

その牌をテンプレート画像とし、元画像に対しテンプレートマッチングを実行

この際、検出された点の種類と個数をカウントしておく

この作業をすべて種類の牌について行う

検出された牌の枚数と、自分の手牌をテキストで出力

 

という感じでやってみることにする。

 

プログラムを組んで実行してみると次のようになった。

 

ターミナルの表示:

1mの枚数:0枚
2mの枚数:0枚
3mの枚数:3枚
4mの枚数:0枚
5mの枚数:0枚
a5mの枚数:1枚
6mの枚数:1枚
7mの枚数:1枚
8mの枚数:0枚
9mの枚数:0枚
1pの枚数:0枚
2pの枚数:0枚
3pの枚数:0枚
4pの枚数:2枚
5pの枚数:0枚
a5pの枚数:1枚
6pの枚数:0枚
7pの枚数:0枚
8pの枚数:0枚
9pの枚数:0枚
1sの枚数:1枚
2sの枚数:0枚
3sの枚数:0枚
4sの枚数:3枚
5sの枚数:2枚
a5sの枚数:0枚
6sの枚数:0枚
7sの枚数:0枚
8sの枚数:0枚
9sの枚数:0枚
tonの枚数:0枚
nanの枚数:0枚
shaの枚数:0枚
peの枚数:0枚
hakuの枚数:0枚
hatsuの枚数:0枚
chunの枚数:0枚

 

得られた画像

f:id:potala123:20220415114606p:plain

 

 

ターミナルの表示を見ると、手牌には1枚しかない3mが3枚も検出されてしまっていることが分かる。

 

誤検出された位置を確かめるべく、とりあえず検出された枚数だけでなく、検出した位置(座標)も出力するようにプログラムを修正して再度実行してみた。

 

ターミナルの表示

座標1(325, 1546)
座標2(326, 1546)
座標3(327, 1546)
3mの枚数:3枚

 

 

表示された結果を見ると、検出された3点のx座標は1刻みであった。つまり、1枚の牌が3枚分カウントされてしまっていたということである

 

この問題を解決するべく、前に検出した点とx座標が100ピクセル以上離れている場合のみ、検出された点として数えるようにプログラムを修正した。このようにすることで、1枚の牌をスキャンする間に3回も検出してしまうという事態は防げるはずである。それとついでに、検出された牌の合計枚数も出力するようにしておいた。

 

修正後のプログラムを実行した結果はこんな感じ。

 

ターミナルの表示

1mの枚数:0枚
2mの枚数:0枚
座標1(325, 1546)
3mの枚数:1枚
4mの枚数:0枚
5mの枚数:0枚
座標1(461, 1548)
a5mの枚数:1枚
座標1(594, 1547)
6mの枚数:1枚
座標1(729, 1547)
7mの枚数:1枚
8mの枚数:0枚
9mの枚数:0枚
1pの枚数:0枚
2pの枚数:0枚
3pの枚数:0枚
座標1(865, 1547)
座標2(1000, 1547)
4pの枚数:2枚
5pの枚数:0枚
座標1(1136, 1547)
a5pの枚数:1枚
6pの枚数:0枚
7pの枚数:0枚
8pの枚数:0枚
9pの枚数:0枚
座標1(1270, 1548)
1sの枚数:1枚
2sの枚数:0枚
3sの枚数:0枚
座標1(1406, 1549)
座標2(1541, 1549)
座標3(1676, 1549)
4sの枚数:3枚
座標1(1812, 1546)
座標2(1947, 1546)
5sの枚数:2枚
a5sの枚数:0枚
6sの枚数:0枚
7sの枚数:0枚
8sの枚数:0枚
9sの枚数:0枚
tonの枚数:0枚
nanの枚数:0枚
shaの枚数:0枚
peの枚数:0枚
hakuの枚数:0枚
hatsuの枚数:0枚
chunの枚数:0枚

合計13枚

 

問題点が解決されていることが確認できた。念のため、元画像を変えて数回検証したが誤検出などはなく、正確に牌の種類と枚数をカウントできていた。

 

手牌をテキストで出力してみる

とりあえず、元画像から誤検出なくすべての手牌を検出することはできるようになった。そこで次は、手牌の内容をテキストで表示(1m,3m,4m,5p,7p..みたいな感じ)できるようにプログラムを修正した。

 

実行結果は次のようになった。

手牌['3m', 'a5m', '6m', '7m', '4p', '4p', 'a5p', '1s', '4s', '4s', '4s', '5s', '5s']

処理にかかる時間をなるべく短くしたい

何回か分析をしていて感じたのは、「思っていたよりも一回一回の分析に時間がかかる」ということ。

 

原因はおそらく、元画像のサイズが大きい(画面全体のスクリーンショット)から。

 

元画像の大半の部分は、テンプレートマッチングをする必要が無い部分なので、テンプレートマッチングを実行する前に元画像から必要な部分(手牌周辺)のみを切り出し、切り出した画像に対してテンプレートマッチングを実行することにした。

 

切り取った画像はこんな感じ(ちょっと小さい...)

 

f:id:potala123:20220415124715p:plain

 

テンプレートマッチングを行う前にトリミングを行った場合と行わなかった場合にかかった時間をそれぞれ測定すると次のようになった。

 

トリミングしない場合:経過時間7.378021955490112秒

トリミングした場合: 経過時間1.8098366260528564秒

 

 

なんとトリミングをした場合は、しなかった場合の4倍近く速いという結果になった。

テンプレートマッチングをする際は、なるべく画像サイズを絞ってから行うと、大幅な時間短縮につながることが実感できた。

 

まとめと今後の展望

初めて画像解析にチャレンジしたが、ライブラリを使えば思ったより簡単に実装できることが分かった。

 

次は、今回実装した機能(手牌をテキスト化)を使った別のツール、例えば手牌を分析して、次の打牌を提案するようなツールが作れたらいいなと思っている。