はじめに、課題感
私は2~3か月に1度、マイクラでサバイバルモードをイチからやり直して、エリトラを取るくらいまでやったらそのワールドは(飽きて)やめる、ということを繰り返してます。
その中の作業の1つで、司書ガチャがあります。ほしいエンチャント本が出るまで、とある作業を繰り返します。
具体的には、下のような画面を見て
"取引"の下にある2段の、1段目が「本棚」(箱のような絵)と、2段目が「エンチャントの本、ダメージ増加 V」であることを認識します。(ダメージ増加 Vは、割といいやつ)
これを繰り返すんですが、結構速いループです。
1回数秒のことを繰り返すのですが、ほしいエンチャント本が全然出なくて、確率どうなってんだ?と思って、記録して、検証したのが下記のエントリーでした。
記録するには、司書ガチャの1回ごとに手を止めて記録していくという地味な作業。1回数秒の作業が数十秒になるので、とてもめんどくさいです。(でも1つずつ積み上げる作業は好きだったり、いろいろプラマイでちょっとプラスなので記録してました。)
ということで本題。でもやっぱ、記録は自動化したい!
マイクラの画面を監視して、上のような司書の取引画面の必要な個所を読み取ればできるよねという話。
できたもの
最終的には、マイクラ画面・取引内容を認識して、標準出力でこんな情報を出力し、DBへ登録しています。タイムリーに。
登録されたDBの内容(3~4秒に1回の頻度で繰り返しているようです)
ソースはこちら
先に応用の話
技術的な話はあとでするとして、先にこの技術は応用範囲が広いと思ってます。抽象化すると、「画面に何かが表示され、それを認識して、状況に応じて何かする」という話。
例えば、農作物の温度計のシステムを監視して、温度が上がったら窓を開けるとか。システムが連携することは想定してなくて、ただ画面として表示するというものは山ほどあるので。その結果、人が画面を見ないといけない。
あとは今回の司書ガチャのように、結構なスピードで、結構な量の単純判断をしないといけないこととか。これはちょっと具体例が思いつかないけど、人が張り付いて画面をじっと見ているような作業ですかね。想像するだけで悲しいお仕事です。
そういう必要性があったら是非ご相談ください。
画面の読み取りでなく、プログラムの読むことができる口(API)があるときもあると思いますが、それにしてもエンジニアは必要です。
技術的な話
概要
下記の機能を作りました。
- ① 司書の取引ウィンドウが開かれているか判断する
- ② 取引ウィンドウの2つのアイテムを判別する(紙 or 本棚 or エンチャントの本、の3択)
- ③ エンチャントの本の場合、本のタイトルとレベルを解読する
- ④ エンチャントの本の場合、価格を解読する
- ⑤ 読み取った内容をDBへ登録する
- ⑥ これらの機能を使って、連続的に読み取る全体の仕組み
長い記事になりそうです。技術のあらすじを書いて、コーディングまで踏み込むのはやめます。ソースをご覧ください。
① 司書の取引ウィンドウが開かれているか判断する
課題
随時キャプチャされる画像には、司書ウィンドウが表示されているときもあれば、表示されていないこともあります。(大体は表示されていない)
まずは表示されているかどうかを判定します。
対応方法
司書ウィンドウは、同じ位置に同じ色で表示されるので、以前出た画面と比較します。ただし、ウィンドウの周囲とか取引の内容、ついでに表示される自分の持ち物は違うのでその部分は比較対象外とします。
キャプチャ画像を、このお手本画像の黒い部分は同じように黒く塗りつぶし、その状態で比較して、一致していれば開かれていると判断します。
ひと工夫
ひと工夫している点としては、下記のあたりに、マウスホバーのツールチップのような形でメッセージが出ることがあるので、そこもマスクしています。
② 取引ウィンドウの2つのアイテムを判別する(紙 or 本棚 or エンチャントの本、の3択)
課題
司書さんは2種類のアイテムを交換してくれるために、2段になっています。
1段ごとに、紙か、本棚か、エンチャントの本のいずれかなので、まずそこを判別します。
対応方法
3択の紙、本棚、エンチャントの本は次のような絵になっています。
まず左端のアイコンが紙のアイコンだったら紙。
つぎに、真ん中に本のアイコンがあったらエンチャントの本。
そうでなければ、本棚。
というロジックにしました。
基本的には①の「ウィンドウが開かれているか」と同様です。
1段目、2段目の位置は毎回きっちり同じなので、座標指定で切り抜き、①と同様に紙のお手本、本のお手本と比較しました。
ひと工夫
工夫1: マウスカーソルオンで若干明るい
1段目か2段目の、どちらを選択している状態かをはっきりするために、マウスカーソルONの状態で、白い枠が出ます。上の絵でいうと、本棚に白い枠が出てます。それ以外の2つはない状態。この内側だけを見ればいいかと思ったら、灰色のエリアも5%ほど明るくなっていました。目で判別できないくらいの仕様。
なので、矩形で単純に比較しようと思ったら打ち取られました。まぁでも、①と同様に、マスクをドット単位で繊細に作って対応しました。
工夫2: 叩いたときに金額が変わる
これは偶然気づいたのですが、司書さんを誤って叩くと、怒って金額を釣り上げてきます。
つまり紙があるかどうかの判断で、24という数字が含まれていると、24のときに、紙ではないという判断になってしまいます。
その対応は、取り消し線より上で判定してもよかったけど、きっちり取り消し線の部分もマスクし、比較対象外として判定することで回避しました。
これは、④のエンチャント本の価格でも同様のことが起き、エンチャント本は訂正前の金額を知りたいので、④でもひと工夫します。
③ エンチャントの本の場合、本のタイトルとレベルを解読する
ここが一番の難所で、ここだけで1つのエントリーにしたいくらいですが、それはそれでめんどくさいので、一気に書きます。
課題
1段目か2段目にエンチャントの本がある前提で、エンチャントの本の内容(タイトル)とレベルを読みます。
下記の場合は、「忠誠 II」から、「忠誠」というタイトルと、2というレベルを得たい。
なお、エンチャントの本の内容は、カーソルを本に乗せた時にしか現れないので、下記のような可能性があります。
また、カーソルのツールチップのように表示されるので、位置は不確定です。
対応方法
位置は不確定ですが、枠の色は紫なので、以前ビリヤード台の写真から台を認識するのに使った技術を使い、矩形を認識して切り出します。
切り出した矩形の下半分を相手にして、字本体の薄い灰色か、そうでないかの2値に分けます。
そうして集めたいくつかのタイトルから、レベル1~5の部分を切り出してお手本にします。
そしたらまた①と同じ手法で、今回得た「忠誠 II」という画像の右端と、「I」「II」「III」「IV」「V」の画像を比較し、一致しているか確認することで、まずレベルを得ます。レベルはないこともあるので、それも考慮して。
司書ガチャによって得られるすべての本の種類のタイトルの画像を全39種類を得て、39種類のお手本を作りました。そしてそれを、タイトルの画像とタイトルの文字を組み合わせました。そして①と同じように、39画像と比較・・・すると、1つの画像に付き画像比較を最大39回しないといけないので、非常によくないです。まぁ正直、最大39回ならいけるかもしれませんが、気に食わないのでひと工夫で後述します。ここでは概要のみ。
そしてめでたく、マウスカーソルが本に乗ったときに出る枠から、文字を抜き出して、「忠誠」と「II=2」を得ることができました。
ひと工夫
工夫1: 39種類の画像の一致判定
単純にループすればいいんですが、比較が、平均19回、最大38回になるのでやめ、結果的に二分探索できるようにしました。平均5.?回、最大6回に。
まず状況の整理ですが、x方向(200px)y方向(15px)の2次元の配列に、0(黒)か255(白)が入っています。その2値しかないです。そしてそれが、39種類あり、必ずどれかに一致します。
対応方法は、2次元を一直線にならべて1次元にし、255を1とすることで、0 or 1 の3,000要素の1次元配列としました。
次に39種類を並べて、また2次元配列にします。
下記はイメージ。1つの画像が横方向に3,000要素まであり、縦方向に39段。
縦方向に白(1)を足して20くらいになる、横方向のindexを探します。実際には、index=1,007でした。この点を見れば、19種類と20種類に分けられる。次に分けられた20種類を同様に10くらいになる点を探すと、index=220で、10種類と9種類い分けられる。
そうやって再帰的に探して、みごと6回で分岐できました。
分岐点を探すこともプログラミングで行ったのですが、ここはプログラミングらしくて面白かった。
でもさらに問題が生じて、この6階層の入れ子・数十個のif文をどう書くか。条件はすべてわかっていて正しく動きそうだけど、いい実装方法が思いつかなかったので、思い切って全部if文を書いてます😮 下手な変数化とかやっちゃうと、高速化の工夫が台無しになりそうだったので。
まぁ、標準出力でif文をコーディングして、ソースへコピペするという荒業で、手で逐一書くことは一応避けましたが。
(ここが最も厳しい難所でした。あとは気楽にどうぞ。)
工夫2: 色はほぼ確定=半透明
ツールチップの紫が、濃いめの半透明なので、若干、色の揺れがあります。なので紫の定義を前後に少し広げてあります。でも今のところ問題は出てないです。たぶん大丈夫。
余談:文字の判定方法
最終的に採用しなかった方法なので、技術説明でもなく、工夫でもない"余談"として書いておきます。
エンチャント本のタイトルの判定で、"工夫1"に書いた独特な方法を採用する前に、OCRと機械学習を試したのでそれを書いておきます。
不採用の判定方法1:OCR: Tesseract
TesseractというOCRのexeがあり、それを利用できたら~と思って試行しました。
結果的には、文字検出の精度が低く使用をやめました。おそらくマイクラの文字が苦手。特にカタカナが苦手で、「ノ」を「フ」とか「メ」と読んだり、「ノックバック」を「フックタバパック」とかになりました。文字数が明らかに多い🤣 また、日本語とアルファベットが混ざった「ノックバック II」とかなので、さらに難しかったのかも。
「ノ」の間違いから、ゴシックの等幅フォントで学習しているのでは、と思いました。書き始めの溜めを、横線と判断している模様🤔
あと全体を右にずらしてみてなんかする処理が入っているかも?「フックタバパック」って、「ク」と「バ」が2回ずつ出てるから。
日本語って難しい。
それとpythonからのインタフェースとなる、pytesseractというライブラリはあるものの、結局内部的には、windowsにインストールしたexeをキックしているだけで、オプションもそのexeのオプションのインタフェースがあったりなかったり。pytesseract.image_to_string(img_line_size, lang="jpn", config="--psm 7")
こんな感じで試行しましたが、psmのオプションも、pytesseractのソースを見て、最終的にはローカルにインストールしたexeのドキュメントを見て、やってました。そういうところも、ちょっと微妙かなぁと思いました。
それでも、下記のような置換を並べまくってなんとかして、辟易しかけたころに、文字そのものを認識しない(文字がないことになる)というケースにぶちあたり、使用をあきらめました。ないと言われたらもうどうしようもない。
replaced_text = re.sub(r"[ノメフブ][ッツ][クタ]{1,2}[バパ]{1,2}ッ[クタ]{1,2}", "ノックバック", replaced_text)
replaced_text = re.sub(r"[還練壇棘]の[鏡鐘鎧]", "棘の鎧", replaced_text)
不採用の判定方法2:機械学習
上のTesseractと格闘しながら、最終的にとった方法(工夫1の案)を頭に思い浮かべつつもダサそうだし、それって機械学習のニューラルネットワークだなぁと思っていました。
なので、最終的にとった方法(工夫1の案)の前にごく軽めに機械学習を試してみました。
結果的には、これもうまくいきませんでした。
理由はたぶん、まず学習データが少なかったこと。あとは過学習するまでぶん回すというところまで調整しなかったこと、とにかく軽くやりたかったのでsk-learnでやってみたこと(超久しぶりに触った)、このあたりだと思います。ちゃんとやれば、画像の前処理も不要でできると思います。sk-learnでだってできそう。
一応githubに残しておいたので貼っておきます。
④ エンチャントの本の場合、価格を解読する
(さて、やはり長くなってきました)
課題
エンチャントの本の価格を読みます。
誤って司書を叩いて値上げすることがあります。でも13
がほしい。(線が引かれると3
と8
が酷似しますが、左端の上から4ドット目が白なら8
と、判別可能です)
対応方法
0~9の数字を5x7のドット絵として見て判別する方法にしました。人(私)が見比べて、「この点を見れば判断できる」と法則を見つけて、if文を書いたという、とても泥臭い方法です。
値上げしたときに横線が引かれていることを想定して、その部分は判別で使わず、ないものと思って判別する必要があります。それは、プログラミングでやるとかえって複雑かも。
ちなみに、二分探索せず、最大9回比較しています😊 1ドットの比較だから許して。
ひと工夫の取り消し線の話は、さらっと書いてしまいました。取り消されていると誤判断してしまう可能性があるので、取り消し線が来る位置のドットは使わないというだけ。
ほかのひと工夫は、特にないです。紙に重なった数字を読む必要があるとしたら、白地なので工夫が必要そうですね。今回はエメラルドに乗った数字なので考慮不要でした。
⑤ 読み取った内容をDBへ登録する
課題
毛色が変わり、DBの話。
これまで読み取った文字を、DBへ登録します。
まず紙か本棚かエンチャントの本。エンチャントの本の場合は、タイトルとレベルと金額です。
対応方法
ローカルで、しかも1か所からしかアクセスされない、DBの仕組みのうちの「記録」の役割しか担わないようなときは、SQLiteが便利です。Pythonの標準ライブラリだし。
ここはinsertするだけで、取り出しもしないし、問題はないでしょう。
一応テーブル定義は下記です。紙と本棚のときは、level、priceはnullというルールで。
create table if not exists lib_log
(
name text not null,
level integer,
price integer,
created_at text not null
);
DBファイルがないときは勝手に作り、テーブルがないときはCREATE TABLE文のif not exists
で作ります。あるときは空振り。
⑥ これらの機能を使って、連続的に読み取る全体の仕組み
課題
連続的にマイクラ画面をキャプチャして、司書の取引をDBへ記録する。
対応方法
0.1秒か0.2秒くらい待ち、ループして、そのループ内で①~⑥の機能を実行すればよいです。監視しながら判定し、得られたらDBへ登録します。
マイクラ画面をキャプチャする仕組みは、以前調べた方法をほぼそのまま使っています。当時Qiitaの記事を書いたので、説明はそちらをご覧ください。
おわりに
キャッチーなタイトルのわりに、長くて重い内容ですみませんでした。ここまで読んでくださった方、本当にありがとうございます。私も疲れましたが、読まれた方もさぞお疲れになったことでしょう。
今回の内容は、過去にやったことの積み上げがすごく多かった印象です。この記事としても、過去の引用がとても多かった。ピンポイントの技術でピンポイントの課題を解決するのでなく、総合力でそれなりの粒度の課題を解消できたのはうれしかったです。時間的にも3日くらいでササっとできたのもよかった。(この記事書くのに1日かかった)
最初の方に書いた話をもう一度書いておくと、「画面に何かが表示され、それを認識して、状況に応じて何かする」という今回の話は、たくさんの応用が利くと思います。私もきっと、今後使う。「こんなことできないかな~」という課題がありましたら、是非ご相談ください。
また、初めてYoutubeの動画を作ってアップしてみましたが、案外簡単で面白かった。ほかのこともやってみたいなと思いました。マイクラ動画か。マイクラの何かはレッドオーシャンすぎるけど、開発者目線はたぶんレアそう。
ではよきいろいろ自動化ライフを