![見出し画像](https://assets.st-note.com/production/uploads/images/99030555/rectangle_large_type_2_4d01e1a41a6c944718ae8c30404b7b80.jpeg?width=1200)
バトオペ2 個人戦績画面のスクリーンショットをeasyOCRでTSV化→図示する
「機動戦士ガンダム バトルオペレーション2」はPlayStation4, 5で配信されているネットゲームです。本ゲームには特定の機体を「愛機」として登録し、戦績を保存する機能があります。ここでは戦績画面のスクリーンショットをOCRを通して数値データ化し、長期に渡る戦績の推移を記録可能にするpythonスクリプトの作成を試みました。
OCRに与える戦績画面は次のようなものです。戦績評価に必要な情報は3箇所: モビルスーツの名称、レベル、戦績テーブルです。モビルスーツは種類とレベルによって武装、機体ステータス、スキル等に違いがあるため、戦績データはモビルスーツ名とレベルに紐づけられている必要があります。なお、スクリーンショットはPS5でシェアボタンを押して撮影するとiPhoneのPS appに同期されるので、それをMacの写真アプリで保存しています。
![](https://assets.st-note.com/img/1677381980083-slP5CEBleq.png?width=1200)
まずは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であることを教えないと文字化けしました。
![](https://assets.st-note.com/img/1677382531859-OZVEDVExSX.png?width=1200)
Rのggplotで適当に図示してみると、いい感じになりました。よかったですね。
![](https://assets.st-note.com/img/1677491192199-v94RTwhAIr.png?width=1200)
以下、発生した問題と解決方法
いくつかのデータが抜ける → 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 の指定により大幅に改善
本スクリプトの作成にあたっては、以下のウェブページを参考にさせて頂きました。ありがとうございます。
この記事が気に入ったらサポートをしてみませんか?