初めに
こんにちは。あつし(@Anaakikutsushit)です。
普段からSplatoon2の非公式大会を運営しています。
これまでに運営した大会はこちら
⇒ https://ofuse.me/#users/3093
運営しているうちに、大会を自動化できないか考えるようになり、今回のようなbotを作成した次第です。
本記事は、ある程度プログラミングが出来て、大会運営などに興味があり、そもそも自動化を考えたことがなかった/自動化に興味があるという狭い範囲の人を対象にしています。
プログラミング初学者向けに、このような手順を踏めばプログラミングできるようになるよという内容はありません。
この記事を読んでわかること
- どういう方法で開発したのか?(記事タイトル通り、Python3.7)
- 実際に開発しているリポジトリ
- 大会本番での様子(これ無しでは無理なレベルでした)
- 今回から学んだ反省点(多少人力を介する&API制限をちゃんと考える 必要がある)
- 参考にした記事(記事の最後にまとめて)
この記事を読んでもわからないこと
- プログラミングしたことない人が環境構築する方法(参考になるページへのリンクはあり)
- 技術的な実装手順とか細かいこと
- 実際のコードの全容(リポジトリから参照はできる)
- 記事内容以外の方法との比較や、もっといい方法
大会とbotの概要
今回の大会は下記のようなものでした。
選手(チーム)それぞれが記録を出して運営に報告。
運営側はその記録をランキングとして張り出すという形式の大会です。
ウデマエの関係ないスプラトゥーン2大会を開きます!
— あつし☀人生クリア率10.1% (@Anaakikutsushit) 2018年12月24日
ルールは簡単。スペシャル発動回数の多いチームが勝ち!
同じチームのメンバーだけでプラベを開いて記録を出し、他チームの記録と競い合うシステムです!
詳しいルールや応募方法はdiscordサーバーに参加して確認!
⇒ https://t.co/GugKUuYfmn pic.twitter.com/5RXAJr4bLf
①選手が結果画像をdiscordに送信する
結果画像のサンプル↓
②黄色枠部分の数を読み、スプレッドシートに記録する
実際のスプレッドシート
③スプレッドシート内で、選手が現在何位かを表示する
このうち②の手順を自動化しました。
discordに送信された画像から数字を読み取って、スプレッドシートに書き込むという一連の処理を自動化したわけですね。
どういう方法で開発したのか?
前述の処理内容は、大まかに3つに分割することができます。
- Discordの投稿内容を処理する
- 画像から数字などを読み取る
- 情報をスプレッドシートに書き込む
これらの3つの処理にはそれぞれ、次のライブラリを使わせて頂きました。
- discord.py ⇒discordサーバー上で動くbotを作るために使いました
- opencv ⇒画像解析し、ゲーム内どのステージの記録なのかを特定するのに使いました。
- tesseract ⇒画像解析するのはopencvと同じですが、文字認識に特化しています。
- gspread ⇒グーグルスプレッドシートを読み書きするために使いました。
開発言語はPythonです。バージョンは3.7。
もともとPythonでの開発経験があったことに加えて、別の画像認識アプリでOpenCVとPythonを利用していたんですね。
今回のbotを作るにあたり、discordもスプレッドシートもどちらもPythonで操作できるとわかったので、そのままPythonで開発しました。
Pythonを導入するには、現在はこういうスタートアップページが有志の手で用意されています。
Python 環境構築ガイド
これ以降は実際の開発の様子を書いていきます。
開発しながら書いてたTwitterのスレッドには、もっと詳しい情報が載っています。知りたい方はどうぞ。
discord botを作る
参考にした記事はこちら。
Pythonで実用Discord bot(discord.py解説)
この記事の内容は非常にやさしいです。私でもすぐにbotを作れました。
一方、本当に初歩的な内容しか載っていないということでもあります。
botでもっといろんな処理をさせたい場合は、discord.pyのドキュメントを読みましょう。
discord.py 1.0.0a ドキュメント
このドキュメント内のAPIリファレンスが、botに出来ることの全てと考えてオッケーです。
また、discordの設定をDeveloperモードにするのも忘れずに。
discordをDeveloperモードにすることで、プログラミングに必須な各種情報をdiscordから簡単にゲットできます。まずは特定のチャンネルにメッセージを送りたくなりました。
— あつし🌘人生クリア率10.1% (@Anaakikutsushit) 2018年12月25日
メッセージを送るためには、送信先のチャンネルIDを確認する必要があります。
チャンネルIDは通常は表示されていません。Developerモードにすると見つけられますhttps://t.co/lvVrzIMJng
補足
2018年現在では、discord botを作成・運用する上でPython3.6以下かdiscord.pyの開発中バージョンが必要になります。
https://t.co/eMhAXNgIBfの正式版がpython3.7をサポートしていないのが問題とのこと。次のどちらかの対策が必要
— あつし🌘人生クリア率10.1% (@Anaakikutsushit) 2018年12月25日
・python3.6にダウングレードする
・https://t.co/eMhAXNgIBfの開発中バージョンにアップグレードする(3.7をサポート)https://t.co/EtbKNwj8Sn
上述した記事を読みつつコードを書いていけば、discordで動くbotを作れます。
実際に私が作ったコードはリポジトリから確認してみてください。
※botのトークンを直書きしてしまっていますが、現在再発行しています
画像から数字を読み取る
botを作り、選手がdiscordに投稿した記録画像を取得できるようになりました。
今度はその画像をプログラムに解析させる段階です。
画像解析でやることは、数字を読み取る以外にもいくつかありました。
discordに投げられてきたこの画像で処理しなきゃならないことをおさらい。数字は優先度。
— あつし🌘人生クリア率10.1% (@Anaakikutsushit) 2018年12月26日
1.全プレイヤーのスペシャル発動数の合計を求める
2.部屋に参加しているプレイヤー数を求める
3.ルールとステージが大会の指定に沿っているか判定する
4.タイムスタンプが大会の進行に沿っているか判定する pic.twitter.com/ueLkC1VRN2
最初に見積もったタスクは最終的に少し変化し、次のようになっています。
- 画像の解像度を一定にそろえる
- ゲーム内のどのステージの記録なのか認識する
- プレイヤーの人数を認識する
- プレイヤー一人ひとりのスペシャル発動数を認識する
画像から情報を読み取るにあたっての基本的な考えは「あらかじめ座標を調べておいた領域を解析する」ということです。
一枚の画像の中で、読み取るべき数字は決まった座標にしか存在しません。
従って画像内の領域を決め打ちして、その領域から取り出すべき情報を認識すれば、目的の作業を完了することができます。
この考えはスペシャル数以外の「ステージ名」「プレイヤー人数」にも共通しています。
ヒストグラム比較は、黄色く塗った2つの領域で2回行います。ABの領域とも標本のヒストグラムと同じであれば、同じステージとして判定していいだろうという試みですhttps://t.co/CI0WAm4B3N
— あつし🌘人生クリア率10.1% (@Anaakikutsushit) 2019年1月1日
プレイヤーがいるかどうかは、「真っ白いピクセルがあるかどうか」という条件でよさそう。
— あつし🌘人生クリア率10.1% (@Anaakikutsushit) 2018年12月29日
最初は「黒っぽいピクセル」で判定しようと思ったけど、割と色が変わるし、白なら色変わらないしってことでhttps://t.co/wXEcjrTsV8
画像から数字を認識するのは少し面倒でした。
ステージは8パターンだけ認識すればよかったので、標本を用意するのも可能だったんですが……。
数字となると0~99まで可能性があるので、とても標本を用意する気にはなれません。
そこで文字を認識するライブラリtesseractの出番。
ただし、このライブラリをそのまま動かしただけではうまくいきませんでした。
ちなみに他の数字もよろしくありません。
— あつし🌘人生クリア率10.1% (@Anaakikutsushit) 2018年12月28日
9⇒空文字
7⇒1
さてどうしたものか。スプラトゥーンフォントを学習させるか
- 文字以外のノイズが入っているため、うまく認識できない
- スプラトゥーンのフォントが特殊なため、うまく認識できない
文字以外のノイズが入っているため、うまく認識できない
1番目の問題は、画像を反転・2値化することで白黒の画像に変換して解決しました。
このような色の変換処理はopencvに備わっている機能で可能です。
うん、閾値処理によってかなり精度が上がるようです。
— あつし🌘人生クリア率10.1% (@Anaakikutsushit) 2018年12月29日
ただpytesseractからは全然脱却できてません。なんかエラー出るんすよね……https://t.co/WFVhxV8Kty
さらに、関係のない画素をギリギリまで排除すればするほど認識制度を上げることができます。
思い切って右端5pxをさらにカットしました。これならかなりうまくいっています
— あつし🌘人生クリア率10.1% (@Anaakikutsushit) 2018年12月29日
明らかに数字と認識されないようなノイズだとしても、tesseractは何かしらの文字を認識しようと頑張ってくれるんですね。
私の場合、解析対象の右下の隅に三角形が入り込んでしまうというノイズを抱えていました。
tesseractは、そのノイズも「3」として返してきました。
このノイズは横幅にしてたったの5px。
ですが、この5pxぶんだけ解析範囲を狭めてあげることで認識制度は格段に向上しました。
文字認識させる場合は可能な限り解析対象にノイズが入り込まないように調整しましょう。
スプラトゥーンのフォントが特殊なため、うまく認識できない
2番目の問題は、tesseract(文字認識ライブラリ)にスプラトゥーンで使われているフォントを学習させることで解決しました。
当該ライブラリに任意のフォントを学習させる手順はこちらの記事がわかりやすいです。
甲骨文字で書かれた文章をOCRで読み取れるようしてみる
ただし当然ですが、フォントを学習させるためにはフォントが必要になります。
しかし私はスプラトゥーンに使われているフォントは持っていません(聞いた話によると特注だとか任天堂の自作だとか らしいです)。
そこでフォントを自作して利用することにしました。
私も今回初めて知ったのですが、フォントは誰でも簡単に自作することができるんですね。
caligraphrというサービスが簡単便利で必要十分な感じ。おススメです。
フォントを自作するとなると大量の作業が必要になると思いますよね。
しかし今回の場合はそこまで大変な作業量にはなりません。
今回は0~9の10種類の文字だけ学習させればよいからです。
- ゲーム画面のスクリーンショットを集めて、0~9の数字画像を作る。
- opencvで2値化処理を施し、白黒の画像にする。
- 拡大し、ペイントソフトでノイズを消して整える
スペシャル回数のサンプル画像から、学習に使うフォントサンプルを作成しましたhttps://t.co/rZ4BBBaEh6
— あつし🌘人生クリア率10.1% (@Anaakikutsushit) 2018年12月28日
こんな感じ。
— あつし🌘人生クリア率10.1% (@Anaakikutsushit) 2018年12月29日
この画像ファイルをもう一度calligraphrに送ればフォント化されるはずですhttps://t.co/fjH6T48XNE
作成した実際のフォントはこちらです。
フォントはotfもttfも両方作れます。と言っても私はこの二つの違いがよくわかりませんけどね。
— あつし🌘人生クリア率10.1% (@Anaakikutsushit) 2018年12月29日
実際に表示するとなんかイイ感じだhttps://t.co/7HENr9uUup
あとは甲骨文字を認識させたという上記の記事の通りに学習させればOKです。
しかし私の環境では、いくら学習量を増やしても誤認識をゼロにすることはできませんでした。
1と7を相互に誤認識するというパターンだけが残った形です。
作業量さらに5倍ドンでも精度上がりません!うわあああ!!
— あつし🌘人生クリア率10.1% (@Anaakikutsushit) 2018年12月30日
テスト用画像を眺めてたんですが、このスペシャル回数を1とか7とか正確に認識しろっていうのは無理があったかもしれんな……人間の目で見てもジェッパの回数7回かな?って一瞬思ったもんhttps://t.co/wRlGSRRVHF
— あつし🌘人生クリア率10.1% (@Anaakikutsushit) 2018年12月30日
もともとのフォントが似ている以上、ここの誤認識は避けられないと考えるべきです。
実際の大会運営中にも10回ほど誤認識が発生しました。
そのときは目で見て手動で正しい結果に書き換えるという対応で切り抜けています。
画像からステージを読み取る
ステージを認識するのは簡単です。文字を認識するのに比べれば。
原理的には、領域内の「色の出現頻度」=ヒストグラムを比較することで実現しています。
大会で使用されるステージは8種類。
あらかじめ「ステージ1~8のヒストグラム」を解析・保持しておきます。
discordに画像が投稿されたらそのヒストグラムを解析。
そのあと、保持しておいたヒストグラムの中のどれと似ているかな?と判断するといった具合です。
ヒストグラム比較はこの記事が参考になります。
— あつし🌘人生クリア率10.1% (@Anaakikutsushit) 2019年1月1日
ヒストグラムの計算はグレースケール画像に対して行う!という内容の記事が多いのですが、こちらは三原色とも考慮した比較について記載していますhttps://t.co/raVS3xRbpf
グレースケール化した画像に対してヒストグラムを計算しても一定の効果はあります。
ただ今回は更に精度を上げたかったため、三原色全てに対してヒストグラムを計算しました。
なお、グレースケールの場合と比べてどの程度精度が上がるのかという対照実験はしていません。
感覚的に、輝度(白黒)だけで判定するよりも精度は上がるとみてよいでしょう。
保持しておいたヒストグラムとどのくらい似ていればいいのか?という閾値は思ったよりも低い値に設定する必要がありました。
最終的に、80%程度の類似が認められればOKという設定に着地しています。
閾値を下げたら正しく認識されました。90%でいいと思ってたのですが、80%くらいにしないとダメなのかも
— あつし🌘人生クリア率10.1% (@Anaakikutsushit) 2019年1月2日
以上の内容を実装し、画像から読み取るべきことは全て読み取れるようになりました。
- ステージ名
- プレイヤー数
- プレイヤー1のスペシャル数
- プレイヤー2のスペシャル数
- ……
- プレイヤー8のスペシャル数
欲を言えば以下の情報も読み取りたかったです。
- どのルール(ナワバリバトルやガチエリアなど)の記録か
- 画像のタイムスタンプは大会期間中のものか
しかしこの辺りは時間的余裕がなかったため飛ばしました。
実装するとなると多言語対応が必要になるため、かなり大変そうです。
将来的には画像認識でルールも特定できるようにしたいんですけど、各国語対応しなきゃいけないのかな……と思ってつらくなりました。
— あつし🌘人生クリア率10.1% (@Anaakikutsushit) 2019年1月1日
日本語/英語だけならまだやる気大丈夫かな。画像集めるの面倒だけど。https://t.co/evP7f7lYsE
以上の内容を実装して完成です。
実際のコードはリポジトリから確認してみてください。
情報をスプレッドシートに書き込む
discordに投稿された画像を処理し、情報を読み取ることができるようになりました。
最後は、読み取った情報をスプレッドシートに書き込む段階です。
Pythonのプログラムからスプレッドシートを扱うにはgspreadというモジュールを利用します。
しかしその前に、スプレッドシートのAPIを利用するための初期設定が必要です。
初期設定にはこの記事を参考にしました。
【もう迷わない】Pythonでスプレッドシートに読み書きする初期設定まとめ
初期設定さえ完了してしまえば、あとは自由にスプレッドシートにアクセス可能になります。
自由と言っても、API制限には気を付けましょう。
私は大会が終わってから気づいたのですが、gspreadには時間あたりのAPI利用制限があります。
Usage Limits
要約すると次のようになります。
- 500秒あたり500回以内(プロジェクト単位)
- かつ、 100秒あたり100回以内(ユーザー単位)
後述しますが、私はこの制限に引っかかってしまいました。
その結果、大会中にbotが止まるという事故に。そのときだけ手動で処理しました。
API制限を上手く乗りこなすには、n秒の間隔を空けて処理するコードを実装しましょう。
これはgspread側ではなく、discord.py側で制御が必要です。
リポジトリのサンプルを見ながら実装するのがいいでしょうね。
スプレッドシートを各種操作するコードはこの記事を参考にしました。
gspreadライブラリの使い方まとめ!Pythonでスプレッドシートを操作する
上記内容を読んでいけば、大抵のことはできるようになります。
公式のドキュメントもあります。英語ですが一次情報を確認したい方はこちら。
実際の大会における処理の様子を下記に示しておきます。
前提としてスプレッドシートの内容を軽く説明しておきましょう。
大会で使ったスプレッドシートは次のような構成をしています。
①大会で使うルールとステージを書き込んだシートがあります。
②参加チームの一覧と、現在勝ち進んでいるか/いないかがわかるシートがあります。
ラウンド1というのは、ラウンド1で敗退したという意味です。
oは勝ち進んでいるという意味です。
③各参加者の画像の解析結果を記載するシートがあります。ラウンドごとに用意しました。
グレーの部分がbotから書き込むエリアです。
白いエリアはプログラミングには関係ありません。書き込まれた内容を計算・整理して表示するだけのエリアです。
現在進行しているラウンドのシートを、必ず右端に置くようにしています。
右端に固定することで、どのシートに書き込めばよいのかを楽に取得できるようになっています。
1行目(ヘッダ)に書いてある情報を補足します。
pnはプレイヤー数、p1~p8はプレイヤーのスペシャル回数を書きこむ列となっています。
ラウンドごとにステージが3つずつあるので、s1~s3の列を右に作ってあります。
このスプレッドシートの構成を前提として、次のような手順で処理していました。
- discordに画像を送信したユーザーのIDを書き込む列を特定(実際には列の位置は固定なので特定する必要がなかった)
- 3つあるうちのどのステージの記録かを特定し、s1~s3のどの列にするかを決定
- Discord IDがシート上に既存の場合は値を上書きする
- Discord IDがシート上に存在しない場合は新規行に書き込む
- チームが勝ち上がっているか確認する
- 勝ち上がっていない場合は順位を数字ではなく「参考」と入力する
こうしてスプレッドシートに値が書き込まれたあと、人間の目でチェックして修正します。
修正が必要になるのは、数字の1と7を間違えて認識・書き込んでしまった場合だけでした。
実際のコードはリポジトリから確認してみてください。
大会本番での様子
以上までで開発は終了し、大会本番を迎えました。
discord上に残っている実際の動作ログはこんな感じです。
黄色い名前のユーザーが大会参加者です。
自分たちのチームの記録を画像で送信し、botがそれを処理しています。
正直言って大会運営はめちゃくちゃ快適でした。
人力だとbotの比ではないくらいに打ち間違いがあったり時間がかかったりしてしまいます。
botに完全に任せきりには出来ないものの、明らかな省力化になったことは間違いないですね。
特にbotの効果が出ているのが最後の画像提出からラウンド締め処理までの時間です。
ラウンド1は大量の画像が送信されたためにAPI制限を引き起こし、botが停止しました。
その結果、途中から人力での入力を余儀なくされたわけです。ラウンド1の手動処理が全部終わったのは最後の画像提出から25分後。
パンクが起こった正確な時刻はわかりませんが、本来なら23時30分でラウンド1は終了するはずでした。
しかし手動処理が途中から始まった(つまり全部の報告を手動で処理したわけではない)にも関わらず、手動処理が終わったのは23時55分となってしまいました。
一方、参加者の皆さんの協力を得て終始botで運営した場合は非常にスムーズ。
ラウンド3の最後の画像提出は1時20分で締切。
全部botで処理すればわずか2分、1時22分にはラウンド3終了のアナウンスをすることができました。
この3分間は、人力で勝ち上がったチームとそうでないチームの記録を付けている時間でした。
同じ形式の大会を全て人力でこなしたら、膨大な時間がかかったことが容易に想像できます。
ほか、形式が異なる画像を自動で却下してくれるなんて働きもありました。
これは画像から情報を読み取るにあたってガチガチに座標などを指定したことのオマケですね。
今回から学んだ反省点
なんと言ってもbotが止まってしまうのは絶対に避けるべき事態でした。
まさか大量の報告を処理しきれずに止まるなんてことは夢にも思わずテストもしていなかったのです。
現在では対策も見つかっていますし、実装もできそうです。
これは今回勉強できたことで最も大きかったことでしょう。
ほか、人力が必要になるポイントでは専用の通知を出すべきでしたね。
そして、人力で切り抜けるにしてもその人力を最小限にする工夫をしておくべきでしょう。
人力が必要になると予見できていたポイントはいくつもあります。
- 処理結果に1や7が含まれているとき(誤認識の確率が相対的に非常に高い)
- スプレッドシート内で記入中の位置をハイライトする機能。リアルタイムで誤認識を修正しやすくなる。
- 報告の締め切り時刻でbotを停止すること
- ランキングが更新されるたびに入れ替えること(大会中はラウンド終了後の1回だけ入れ替えてました)
- 新しいラウンドを始めるときの処理。ラウンドごとのシートを追加したりアナウンスしたり。
ほか、こういう機能も付いていると参加者が楽しくプレーできたでしょう。
- 順位を抜かされた時に通知する機能(特に勝ち上がり圏内から外れたとき)
- 報告した画像のスコアが結局いくつになったのかを表示する機能
- ランキングで前後のチームとのスコアの差を表示する機能
やはり頭で考えているだけだったり、本番にさらされないと見えてこないものがたくさんありました。
同様の大会は今後も継続して主催していきます!
興味が湧いた方は是非選手側としても運営側としても関わって頂けると嬉しいです。ご連絡お待ちしています。
あと、私自身は2018年中で仕事が無くなってしまいました。
C#の業務経験2年と、上記botを開発できる程度のPythonの技術力あります!
在宅ワークしかするつもりありませんが、良いお話あればTwitter(@Anaakikutsushit)でお待ちしております!
開発中に参考にした記事
Discord botを作る・動かす
Pythonで簡単なDiscord Botの作り方
create_task = asyncio.async: SyntaxError: invalid syntax
Pythonで実用Discord bot(discord.py解説)
discord.py 1.0.0a ドキュメント
ユーザー/サーバー/メッセージIDはどこで見つけられる?
PythonでWeb上のファイルをダウンロードする
Python3でwebスクレイピングしたいのですが存在するURLが開けません。
urllib.request — URL を開くための拡張可能なライブラリ
【Python】Webスクレイピング urllib.request / urlopen() でダウンロード
画像解析で情報を読み取る
Pythonで画像内の数字認識
Windows で Tesseract 3.0.5 を使ってみる
Tesseract Not Found Error
Tesseract-OCRの学習
甲骨文字で書かれた文章をOCRで読み取れるようしてみる
【文字フォント】Calligraphrで自作の手書きフォントを作る
'numpy.ndarray' object has no attribute 'mode'
OpenCVを使ってヒストグラムの相関で画像同士の近さを計算してみた話
スプレッドシートに書き込む
【もう迷わない】Pythonでスプレッドシートに読み書きする初期設定まとめ
burnash/gspread - Google Spreadsheets Python API
その他Pythonの基本的なこと
unittest — ユニットテストフレームワーク
Getting Started with unittest in Python
テストコードの構成
Pythonで文字列を連結/結合する方法まとめ
最後までお読みいただきありがとうございました。