Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
172
Help us understand the problem. What is going on with this article?
@Cartelet

「画像認識ウマ娘イベント選択肢チェッカー」をPythonで作るチュートリアル

はじめに

ウマ娘、流行ってますね (ひと波超えた感はありますが)
特に流行りがピークだった4月頃にこんなツールを見かけて、「自分もいつかこんなツールが作れるようになりたい…!」と思った方も多いのではないでしょうか。

このツールはC++で書かれていますが、根幹部分である、画像認識、OCR、イベント選択肢の表示の機能は、実はPythonを使えば結構簡単に実現することができます。

というわけで今回は、上記の3機能に焦点を当てて、順を追って作っていきたいと思います。

今回のゴール

yq4iv-gm1jx.gif

1. 機能の整理

プログラムを書く前にまずは、

・どんな機能を付けるか
 - 最低限実現したい機能は何か

・それはどうすれば実装可能か
 - 使えそうなライブラリはあるか
 - 情報は自分で入力するか、どこかから拾ってきて整形するか

などを整理します。

今回であれば、

・どんな機能を付けるか
 - GUIは省略、コマンドラインに直出力
 - ウィンドウ位置を特定する
 - イベント名をOCRで抽出する
 - イベント選択肢の効果を出力する

・それはどうすれば実装可能か
 - PyAutoGUIの画像認識(locateOnScreen)を使えばウィンドウの位置が判りそう!
 - PyOCR+Tesseractで画像から日本語を含む文字列を抽出できそう!
 - GameWithのイベント選択肢チェッカー(逆引き検索ツール)の情報が使えそう!

といった具合ですね。

2. アタリを付けた構想から具体的な機能を作ってみる

せっかくPythonを使っているなら、こういった開発を行う場合はJupyter Notebook(Lab)の使用を強くおすすめします。

・ウィンドウ位置の特定

PyAutoGUIlocateOnScreenは、画面内から入力した画像に一致する部分を探し出し、一致する部分があればその場所を返してくれる非常に便利な関数です。

ウマ娘のアプリウィンドウで画面変遷に依る変化のない部分はあるでしょうか?
umaratio.jpg
この部分は変わらなそうですね。
umaraatio.jpg
スクリーンショットからこの部分を切り出してtitlebar.pngとして実行ディレクトリに保存してみます。
すると画面からタイトルバーを見つけ出す関数は以下のようになります。

import pyautogui

titlebar_pos = pyautogui.locateOnScreen('titlebar.png', confidence=0.8) # confidenceは正確さ(小さくすると多少の違いを吸収できる)
print(titlebar_pos)

驚異的な簡単さですね。
このような高度な機能を、2, 3行で享受できるのがPythonの最大の強みだと個人的には思っています。

さて、これでウィンドウ位置が特定できました。と言いたいところですが、ウマ娘のアプリはウィンドウサイズを変更することができます。
このままでウィンドウサイズの変更に対応できるでしょうか。
試しにウィンドウを小さくしてみましょう。
image.png
先程と比較するとタイトルバーの空白部分が詰まっていますね。
locateOnScreen関数はこのような変化を吸収できないので、もう一捻り工夫が必要そうです。

ウィンドウサイズの変更の前後でよく観察してみると、タイトル部分と「X」などのアイコンサイズは変化がなさそうです。
ここで、locateOnScreen関数の戻り値を見てみると、

out
Box(left=1730, top=398, width=172, height=45)

(左上の座標(左), 左上の座標(上), 幅, 高さ)となっていることが分かります。
つまり、一致した部分の上下左右のカドの座標は足し算で求められるので、
左上のこの部分の左上の座標と、
umatitle.png
右上のこの部分の右上の座標を
x.png
知ることで、「ウィンドウの位置」と「ウィンドウサイズ」の両方を得ることができそうです。
上の2画像をそれぞれumatitle.png, x.pngと名付けたとすると、

import pyautogui

pos = pyautogui.locateOnScreen('umatitle.png', confidence=0.8)
x_pos = pyautogui.locateOnScreen('x.png', confidence=0.8)
print(pos[0]+pos[2]) # タイトルバーの下のアプリ描画部分の左上の座標
print(x_pos[0]+x_pos[2]-pos[0]) # ウィンドウ幅

これで、ウィンドウ位置、ウィンドウサイズを特定することができました。

・イベント名をOCRで抽出する

OCRをかける前段階として、
・文字部分の切り取り
・2値化など、認識精度を上げるための前処理
を行う必要があります。

まずは、文字部分の切り取りから行っていきましょう。

- 文字部分の切り取り

どのように目的部分を切り取るか戦略を立てるべく、今回の目的であるイベント選択画面を観察してみましょう。
名称未設定 1.jpg
プレイしている方ならすぐに気付くと思いますが、イベントタイトルは決まった位置に表示されています。
先程までで、ウィンドウの位置、サイズは取得できていますので相対座標で切り取れそうですね。
ウィンドウサイズ(幅)を乗算すれば常に狙った位置を切り取れるように、ウィンドウ幅を基準に規格化しておきましょう。
図1.png

PyAutoGUIには指定した長方形領域のスクリーンショットを撮る関数も、そのままscreenshotという名前で用意されていて、引数としてregion=(左上の座標(左), 左上の座標(上), 幅, 高さ)を渡すことで領域を指定できます。

pos = pyautogui.locateOnScreen('umatitle.png', confidence=0.8)
x_pos = pyautogui.locateOnScreen('x.png', confidence=0.8)
width = x_pos[0] + x_pos[2] - pos[0]
text_pos = (np.array([0.15, 0.33, 0.6, 0.065]) * width).astype(int)
pyautogui.screenshot(region=(pos[0] + text_pos[0], pos[1] + pos[3] + text_pos[1], *text_pos[2:]))


image.png
目的の部分は切り出せましたので、次は認識精度を上げるための前処理を行っていきます。

- OCRの認識精度を上げるための前処理

今回の場合、文字の背景は軽いグラデーションが掛かっている程度ですので、一番簡単な「閾値による2値化」を採用したいと思います。
「閾値による2値化」を行うというのはものすごく単純な話で、画像をモノクロにしたとき、ある値より暗ければ真っ黒、明るければ真っ白にするという処理のことです。

今回はcv2というライブラリを使って2値化を行ってみたいと思います。
先程のスクリーンショットの結果がpicに格納されているとして、

import cv2
import numpy as np

gray = cv2.cvtColor(np.array(pic), cv2.COLOR_BGR2GRAY)
_, th = cv2.threshold(gray, 210, 255, cv2.THRESH_BINARY)
th


image.png
さらに、白黒を反転して、

255 - th


image.png

きれいに加工することができましたので、いよいよOCRをかけてみたいと思います。

- OCRによる文字列抽出

今回はPyOCR+Tesseractを採用して文字を抽出してみます。
それぞれ下記のページなどを参考にインストールを行ってください。使い方に関しても参考になると思います。
Tesseract+PyOCRで簡易OCRを試してみる
PythonとTesseract OCRで文字認識

import pyocr

tool = pyocr.get_available_tools()[0]
text = tool.image_to_string(Image.fromarray(255 - th),
                            lang='jpn',
                            builder=pyocr.builders.TextBuilder())
print(text)
トレーナー心得「指導は常に改良せよ』

ほぼ完璧に抽出できました。

ここまで来ればほとんど終わったようなもので、あとはイベント名と効果のセットを用意して出力すれば完了です。
ということで、次はデータの収集を行います。

イベント選択肢の効果を出力する

出力の前に、イベント名と効果がセットにっているデータがないと始まりませんので、データの収集方法を模索します。

- データ収集

今の時代ゲームの攻略サイトは多いので、何かしらの方法でイベント効果の情報を集めて、{イベント名: 選択肢・効果}の形に整形し、保存しておきます。

- 表示するイベントのピックアップ

OCRでは高い確率で文字列を完璧には抽出できないので、何かしらの方法で近い名前のイベントをピックアップする必要があります。
ここでは標準ライブラリのdifflibからSequenceMatcherを使って文字列同士の類似度を計算します。
Pythonで文字列部分一致度合いを調べる
最も近い文字列のスコアが、ある一定の値より大きくなった場合にイベントと判断し、出力するという方針で書いてみたいと思います。

# 変数名が適当で申し訳ない
from difflib import SequenceMatcher

eventData = {イベント名: 選択肢効果} # イベントの情報が入った変数
t = ''
r = 0
for i in eventData:
    rr = SequenceMatcher(None, i, text).ratio()
    if r < rr:
        r = rr
        t = i
if r >= .625:
    print(f'【{t}】')
    for i in eventData[t]:
        print(i['n'], '    ' + '\n    '.join(i['t'].split('[br]')), sep='\n')
out
【トレーナー心得『指導は常に改良せよ』】
あまり無理はなさらずに
    体力+10~14
    スキルPt+15~18
    桐生院トレーナーの絆ゲージ+5
    ※サポート効果により数値が変動
『トレーナー白書』見てみたいです
    スピード+5~6
    賢さ+5~6
    桐生院トレーナーの絆ゲージ+5

これで、ウィンドウ認識からイベント選択肢の効果の出力までを実装できました。

最後に、一つのクラス・プログラムにまとめたものを掲載したいと思います。

3. 最終的に1つのプログラムとしてまとめる

umamusume_options_checker.py
import pyautogui
import numpy as np
import time
import pyocr
from PIL import Image
from requests import get
from difflib import SequenceMatcher
import cv2


class uma_option_checker:
    def __init__(self, title_ref, x_ref):
        self.pos_ref = title_ref
        self.x_ref = x_ref
        self.eventData = {} # イベント情報をまとめた辞書
        self.get_win_pos()
        self.tool = pyocr.get_available_tools()[0]
        self.scene = ""

    def get_win_pos(self):
        pos = pyautogui.locateOnScreen(self.pos_ref, confidence=0.9)
        x_pos = pyautogui.locateOnScreen(self.x_ref, confidence=0.9)
        if pos is None or x_pos is None:
            self.pos = 0
            return False
        else:
            self.pos = pos
            self.ratio = x_pos[0] + x_pos[2] - pos[0]
            self.text_pos = (np.array([0.15, 0.33, 0.6, 0.065]) * self.ratio).astype(
                int
            )
            return True

    def get_scene(self):
        self.p = pyautogui.screenshot(
            region=(
                self.pos[0] + self.text_pos[0],
                self.pos[1] + self.pos[3] + self.text_pos[1],
                *self.text_pos[2:],
            )
        )
        gray = cv2.cvtColor(np.asarray(self.p), cv2.COLOR_BGR2GRAY)
        _, th = cv2.threshold(gray, 210, 255, cv2.THRESH_BINARY)
        text = self.tool.image_to_string(
            Image.fromarray(255 - th), lang="jpn", builder=pyocr.builders.TextBuilder()
        )
        self.text = text
        if text:
            return True
        else:
            return False

    def get_likely(self):
        t = ""
        r = 0
        for i in self.eventData:
            rr = SequenceMatcher(None, i, self.text).ratio()
            if r < rr:
                r = rr
                t = i
        return t if r >= 0.625 else self.scene

    def show(self):
        print(f"【{self.scene}】")
        for i in self.eventData[self.scene]:
            print(i["n"], "    " + "\n    ".join(i["t"].split("[br]")), sep="\n")
        print()

    def update(self):
        if self.get_win_pos():
            if self.ratio > 0:
                if self.get_scene():
                    t = self.get_likely()
                    if t != self.scene:
                        self.scene = t
                        self.show()


if __name__ == "__main__":
    uop = uma_option_checker("umatitle.png", "x.png")

    while 1:
        time.sleep(0.5)
        uop.update()

改善点の候補

・GUIの開発
・現状では育成ウマ娘の情報を考慮していない為、イベント名が共通のイベントは異なるウマ娘の選択肢が表示される点の解決
・OCR精度の向上
興味のある方は是非挑戦してみてください。

さいごに

Pythonでは豊富なライブラリのおかげで、一見するととても高レベルな内容も短いコードで簡単に実現することができます。
もっと色んな人に気軽に使えるツールだよって伝えたい…

172
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Cartelet
趣味でやったことや勉強したことをメモ感覚でまとめたり紹介したり

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
172
Help us understand the problem. What is going on with this article?