見出し画像

バトオペ2 個人戦績画面のスクリーンショットをeasyOCRでTSV化→図示する

「機動戦士ガンダム バトルオペレーション2」はPlayStation4, 5で配信されているネットゲームです。本ゲームには特定の機体を「愛機」として登録し、戦績を保存する機能があります。ここでは戦績画面のスクリーンショットをOCRを通して数値データ化し、長期に渡る戦績の推移を記録可能にするpythonスクリプトの作成を試みました。

OCRに与える戦績画面は次のようなものです。戦績評価に必要な情報は3箇所: モビルスーツの名称、レベル、戦績テーブルです。モビルスーツは種類とレベルによって武装、機体ステータス、スキル等に違いがあるため、戦績データはモビルスーツ名とレベルに紐づけられている必要があります。なお、スクリーンショットはPS5でシェアボタンを押して撮影するとiPhoneのPS appに同期されるので、それをMacの写真アプリで保存しています。

まずはcondaで環境構築を行います。ここでは事前調査で日本語・英語・数値ともに正確に認識できて、かつ取り扱いの簡単だった easyOCR を利用します。

conda create -n bo2analysis
conda activate bo2analysis
mamba install -c conda-forge easyocr opencv
mamba install -c anaconda pandas

次にpythonスクリプトを作成します。ここではライブラリの読み込み、コマンドライン引数で読み込む画像ファイルの名前を与える記述、OCRの立ち上げ、全体像から手動で各セルの座標を決めてクロップします。ここは認識精度にも関わるため、手動の調整を繰り返しています。

import cv2
import easyocr
import numpy as np
import pandas as pd
import re
import sys

args = sys.argv
input = args[1]
print("Input: " + input, file=sys.stderr)

reader = easyocr.Reader(['ja'], gpu = False)
whole = cv2.blur(cv2.imread(input), (3,3))

winloseoriginal = [ whole[685:730 , 475:480],whole[770:820,475:480],whole[850:900,475:480],whole[940:990,475:480],whole[1030:1080,475:480],whole[1110:1160,475:480],whole[1190:1240,475:480],whole[1270:1320,475:480],whole[1360:1410,475:480],whole[1440:1490,475:480],whole[1530:1580,475:480],whole[1610:1660,475:480],whole[1700:1750,475:480],whole[1780:1830,475:480],whole[1870:1920,475:480]]
rivaloriginal = [ whole[685:730 , 700:720],whole[770:820,700:720],whole[865:900,700:720],whole[940:970,700:720],whole[1030:1060,700:720],whole[1110:1140,700:720],whole[1200:1230,700:720],whole[1280:1310,700:720],whole[1360:1390,700:720],whole[1450:1480,700:720],whole[1530:1560,700:720],whole[1610:1640,700:720],whole[1700:1730,700:720],whole[1780:1810,700:720],whole[1870:1900,700:720]]
ruleoriginal = [whole[690:740,320:400],whole[770:820,320:400],whole[850:900,320:400],whole[940:990,320:400],whole[1030:1080,320:400],whole[1110:1160,320:400],whole[1190:1240,320:400],whole[1280:1330,320:400],whole[1360:1410,320:400],whole[1450:1500,320:400],whole[1530:1580,320:400],whole[1610:1660,320:400],whole[1700:1750,320:400],whole[1780:1830,320:400],whole[1870:1920,320:400]]
zscoreoriginal = [whole[690:740 , 860:1060],whole[770:820,860:1060],whole[850:900,860:1060],whole[940:990,860:1060],whole[1030:1080,860:1060],whole[1110:1160,860:1060],whole[1190:1240,860:1060],whole[1280:1330,860:1060],whole[1360:1410,860:1060],whole[1450:1500,860:1060],whole[1530:1580,860:1060],whole[1610:1660,860:1060],whole[1700:1750,860:1060],whole[1780:1830,860:1060],whole[1870:1920,860:1060]]
personalscoreoriginal = [ whole[690:740 , 1130:1330],whole[770:820,1130:1330],whole[850:900,1130:1330],whole[940:990,1130:1330],whole[1030:1080,1130:1330],whole[1110:1160,1130:1330],whole[1190:1240,1130:1330],whole[1280:1330,1130:1330],whole[1360:1410,1130:1330],whole[1450:1500,1130:1330],whole[1530:1580,1130:1330],whole[1610:1660,1130:1330],whole[1700:1750,1130:1330],whole[1780:1830,1130:1330],whole[1870:1920,1130:1330]]
assistscoreoriginal = [whole[690:740 , 1430:1540], whole[770:820,1430:1540], whole[850:900,1430:1540], whole[940:990,1430:1540], whole[1030:1080,1430:1540], whole[1110:1160,1430:1540], whole[1190:1240,1430:1540], whole[1280:1330,1430:1540], whole[1360:1410,1430:1540], whole[1450:1500,1430:1540], whole[1530:1580,1430:1540], whole[1610:1660,1430:1540], whole[1700:1750,1430:1540], whole[1780:1830,1430:1540], whole[1870:1920,1430:1540]]
damageoriginal = [whole[690:740 , 1650:1830],whole[770:820,1650:1830],whole[850:900,1650:1830],whole[940:990,1650:1830],whole[1030:1080,1650:1830],whole[1110:1160,1650:1830],whole[1190:1240,1650:1830],whole[1280:1330,1650:1830],whole[1360:1410,1650:1830],whole[1450:1500,1650:1830],whole[1530:1580,1650:1830],whole[1610:1660,1650:1830],whole[1700:1750,1650:1830],whole[1780:1830,1650:1830],whole[1870:1920,1650:1830]]
mskilloriginal = [whole[690:740 , 1920:1980],whole[770:820,1920:1980],whole[850:900,1920:1980],whole[940:990,1920:1980],whole[1030:1080,1920:1980],whole[1110:1160,1920:1980],whole[1190:1240,1920:1980],whole[1280:1330,1920:1980],whole[1360:1410,1920:1980],whole[1450:1500,1920:1980],whole[1530:1580,1920:1980],whole[1610:1660,1920:1980],whole[1700:1750,1920:1980],whole[1780:1830,1920:1980],whole[1870:1920,1920:1980]]
msdeathoriginal = [ whole[690:740 , 2095:2155],whole[770:820,2095:2155],whole[850:900,2095:2155],whole[940:990,2095:2155],whole[1030:1080,2095:2155],whole[1110:1160,2095:2155],whole[1190:1240,2095:2155],whole[1280:1330,2095:2155],whole[1360:1410,2095:2155],whole[1450:1500,2095:2155],whole[1530:1580,2095:2155],whole[1610:1660,2095:2155],whole[1700:1750,2095:2155],whole[1780:1830,2095:2155],whole[1870:1920,2095:2155]]
dateoriginal = [whole[690:740 , 2210:2470],whole[770:820,2210:2470],whole[850:900,2210:2470],whole[940:990,2210:2470],whole[1030:1080,2210:2470],whole[1110:1160,2210:2470],whole[1190:1240,2210:2470],whole[1280:1330,2210:2470],whole[1360:1410,2210:2470],whole[1450:1500,2210:2470],whole[1530:1580,2210:2470],whole[1610:1660,2210:2470],whole[1700:1750,2210:2470],whole[1780:1830,2210:2470],whole[1870:1920,2210:2470]] 

勝敗とライバルのカラムに関しては、背景に埋もれた文字や記号はeasyOCRでは全く認識できないので、他の手法で対応します。画像類似度の比較と色の数値での判定を検討し、前もって比較対象ファイルの準備が不要な後者で進めます。勝利時と敗北時で背景色が異なることを利用します。

まず、勝敗時のセルの背景色を取得します。注意としては、RGBではなくBGRの順です。

print(winlose[0].T[0].flatten().mean()) #win
print(winlose[0].T[1].flatten().mean())
print(winlose[0].T[2].flatten().mean())
print(winlose[2].T[0].flatten().mean()) #lose
print(winlose[2].T[1].flatten().mean())
print(winlose[2].T[2].flatten().mean())

得られた数値を元に勝敗を判定します。理想的には適当な範囲を設けたり、異常値を弾くような工夫をした方が安全とは思います。

winlose = ["win" if all([box.T[0].flatten().mean() < 53, 
                        box.T[1].flatten().mean() < 52,
                        box.T[2].flatten().mean() < 33]) else "lose" for box in winloseoriginal]
rival =   ["win" if all([box.T[0].flatten().mean() < 53, 
                        box.T[1].flatten().mean() < 52,
                        box.T[2].flatten().mean() < 33]) else "lose" for box in rivaloriginal]

OCRの結果も取り出して、TSVで出力します。

filename      = [input for i in range(15)]
msname        = [reader.readtext(whole[510:570,240:880],  detail = 1                           )[0][1] for i in range(15)]
mslevel       = [reader.readtext(whole[510:570,890:1030], detail = 1, allowlist = 'LV123456789')[0][1] for i in range(15)]
rule          = [value[0][1] if (value := reader.readtext(box, detail = 1, allowlist = 'BASIC'      )) else "failed" for box in ruleoriginal]
zscore        = [value[0][1] if (value := reader.readtext(box, detail = 1, allowlist = '0123456789.')) else "failed" for box in zscoreoriginal]
personalscore = [value[0][1] if (value := reader.readtext(box, detail = 1, allowlist = '0123456789' )) else "failed" for box in personalscoreoriginal]
assistscore   = [value[0][1] if (value := reader.readtext(box, detail = 1, allowlist = '0123456789' )) else "failed" for box in assistscoreoriginal]
damage        = [value[0][1] if (value := reader.readtext(box, detail = 1, allowlist = '0123456789' )) else "failed" for box in damageoriginal]
mskill        = [value[0][1] if (value := reader.readtext(box, detail = 1, allowlist = '0123456789', mag_ratio=2, text_threshold = 0.5)) else "failed" for box in mskilloriginal]
msdeath       = [value[0][1] if (value := reader.readtext(box, detail = 1, allowlist = '0123456789', mag_ratio=2, text_threshold = 0.5)) else "failed" for box in msdeathoriginal]
date          = [value[0][1] if (value := reader.readtext(box, detail = 1, allowlist = '0123456789/')) else "failed" for box in dateoriginal]

resulttable = pd.DataFrame({"filename"     : input,
                            "msname"       : msname,
                            "mslevel"      : mslevel,
                            "rule"         : rule,
                            "winlose"      : winlose,
                            "rival"        : rival,
                            "zscore"       : zscore,
                            "personalscore": personalscore,
                            "assistscore"  : assistscore,
                            "damage"       : damage,
                            "mskill"       : mskill,
                            "msdeath"      : msdeath,
                            "date"         : date })

resulttabletsv = re.sub(" +", "\t", resulttable.to_string(header=False, index=False))
print(resulttabletsv)
ls *.JPG | xargs -I% python badgechallenge.py % | sort | uniq > cat.tsv

出力ファイルをテキストエディタで開いてみると、きちんとTSVになっていることがわかりました。余談ですが、Excelで開く際は明示的にUTF-8であることを教えないと文字化けしました。

Rのggplotで適当に図示してみると、いい感じになりました。よかったですね。

以下、発生した問題と解決方法

  • いくつかのデータが抜ける → text_threshold = 0.5 (defaultは0.7) に変更

  • 900が9ooになる → allowlist = '0123456789' の設定で解決

  • 桁数の少ないカラムでデータが抜けたり信頼度が0.5を切るくらい悪化する → 余白を100px以上与えないよう横幅を厳し目にクロップすると改善

  • 撃墜数・被撃墜数のカラムで数値が1のとき、認識に失敗する→クロップ範囲を上端は数字のぎりぎりまで余白を詰めて、下端は5px程度余裕をもたせるとともに、cv2.blurの利用と mag_ratio=2, text_threshold = 0.5 の指定により大幅に改善

本スクリプトの作成にあたっては、以下のウェブページを参考にさせて頂きました。ありがとうございます。


この記事が気に入ったらサポートをしてみませんか?