らんらん技術日記

日々の学習メモに

ゲーム中のセリフをOCRで取得したい

オクトパストラベラー(Steam版)絶賛プレイ中です!
以前の記事を挙げてから一ヶ月が経過、8人中5人の2章をクリアしました。

yukirunrun.hatenablog.com

進捗が遅いように見えますが、それは英語でプレイしているせいです。

人に話しかける
→ちょっと何言ってるかわからない
→グーグルで単語を調べる
→グーグルで熟語を調べる
→グーグルで全文翻訳をかける

と繰り返しているので、めちゃくちゃ時間がかかります。数日プレイして、一度も街の外に出ないことはざらです。ゲームとしてのテンポは最悪ですが、それでもオクトパストラベラーは面白いです!
というのも、ただの村人にさえバックグラウンドが用意されていて、

  • 多額の借金を背負って、本当の自分を偽りながらお金を稼ぐ踊り子
  • 貧民あがりで、貴族出身の同僚に対抗意識を抱く兵士
  • 悪そうな人を見つけては、雪玉を投げつける女の子

などなど、色々な人がいます。
単に話しかけるだけでは「ここは始まりの町です」レベルのことしか言わない人でも、深く探ると「宿屋の経営に行き詰っているため、夫にリフォームするように訴えている女性」ということが判明します。
村人にもそれぞれの人生があるんやなぁ、と干渉にひたりながらプレイしています。

OCRで翻訳作業を効率化

※ここからの話はSteamでゲームをしていることが前提になります。

さて、上記のプレイスタイルにあって、面倒に感じていることがあります。
英文の意味を調べるには、ブラウザ上で英文を打つ必要があるということです。

  • 現状の作業
    ゲーム画面を見ながら、英文を打ち、ブラウザに張り付ける

これを、

  • 変更後の作業
    キーを押したら、英文がブラウザに張り付けられる

という風に変更できたら、多少は楽になります。
そこで、以下のようなシステムを考えてみました。

f:id:yukirunrun:20200620233129p:plain

青色の箱はプロセスを、橙色の箱はファイルを表します。
システムの説明をデータの流れる順に行います。

  • SteamでF12キーを押すと、スクリーンショットを保存します(標準機能)。
  • Pythonは、スクリーンショットの保存を確認すると、Tesseractを呼び出します。
  • Tesseractは、OCRを行うためのツールで、スクリーンショット内に含まれる文字情報を取り出します。
  • Pythonは、Tesseractから文字情報を受け取り、整形した後、json形式で保存します。
  • Webサーバはjsonファイルを読み取り、Webページを動的生成します。

・・・といっても、どんなシステムかわかりにくいですよね。
システムの入力と出力だけでも具体的に紹介します。
入力はSteam画面のスクリーンショットです。

f:id:yukirunrun:20200620234828p:plain
©2018 SQUARE ENIX CO., LTD. All Rights Reserved.

出力はブラウザ上の文字になります。

f:id:yukirunrun:20200620235323p:plain

やりたいことは、Steam上のセリフをブラウザ上で表示するだけです。
後はコピペすれば、Google翻訳にかけられるでしょ?

このシステムで肝になるのがOCRと呼ばれる技術です。
OCRとは、Optical character recognitionの略で、日本語では光学文字認識と訳されます。活字の文書をイメージスキャナで画像化し、パソコン上で画像から文字情報を抽出する、といった使い方が起源のようです。スクリーンショットの場合、もはや光学要素はないのですが、OCRを応用することは 可能です。
OCRを実現するためのツールとして、僕はTesseractを採用しました。Googleが開発元で、オープンソースソフトウエア(Apache License 2.0)です。
正直、OCRについても、Tesseractについても、僕は全く詳しくないですw
今回はOCRを試すことができればよいので、今後必要になれば勉強することにします。

プロトタイプ1の開発

プロトタイプ1と銘打って、とりあえずシステム全体として動くものを作ります。

Steamの設定

スクリーンショットの保存方法を変更します。
保存先をプログラムで扱いやすい場所(僕は D:\steam_ss にしました)、保存形式を非圧縮にします。非圧縮にする理由は、圧縮すると画像内の文字が滲んで、OCRの精度に悪影響がありそうだからです(※あくまで素人意見です)。
具体的な手順は、以下ページをご参考に。

appli-world.jp

Tesseractのセットアップ

インストール&Pythonから呼び出せるようにします。
僕の環境はWindowsなので、以下ページを参考にインストールしました。

Tesseract OCR をWindowsにインストールする方法 | ガンマソフト株式会社

僕はWindows 64bit、v5.0.0α版のインストーラにしました。インストール完了後、システム環境変数を編集し、tesseract.exeにパスを通します。

Pythonコードの作成

ネット上に落ちているコードを集めて、適当に組み合わせます。
なにせPythonも詳しくはないので、とりあえず動けばOKで作ります。
簡単にコードの紹介をすると、

  • watchdogクラスを使って、フォルダの監視を行います。つまり、スクリーンショットが保存されたことを検出し、OCRを行うハンドラを呼び出します。
  • subprocessクラスを使って、Tesseractを実行します。Tesseractの文字情報の出力先はstdoutとし、さらにstdoutをsubprocessの返り値にリダイレクトしています。この仕組みにより、Python内でTesseractによる結果をバイナリデータ列として扱えます。
  • collectionsクラスおよびjsonクラスを使って、文字情報をjson形式で保存します。
  • jsonの保存先は、python実行場所と同一階層にあるocrというフォルダにしています。

という感じでしょうか。コード全体は以下のようになります。

import sys                                                                                                            
import os                                                                                                             
import time                                                                                                           
import subprocess
import json
import collections 
from watchdog.observers import Observer                                                                               
from watchdog.events import PatternMatchingEventHandler                                                               

class screenshotEventHandler(PatternMatchingEventHandler):
    text_index = 0
    def __init__(self, patterns=['*.png'], ignore_patterns=[],
                ignore_directories=True, case_sensitive=False):
        super().__init__(patterns, ignore_patterns, ignore_directories, case_sensitive)

    def on_any_event(self, event):
        if (event.event_type == "modified"):
            print("Chaptured a screenshot No." + str(self.text_index))

            # Read lines from Steam's screenshot using OCR
            mycmd = 'tesseract.exe ' + event.src_path + ' stdout --dpi 96'
            tesseract_out = subprocess.check_output(mycmd, shell=True).decode()
            person = ''
            msg = ''
            for i in tesseract_out.splitlines():
                if len(i) > 3:
                    if (len(person) == 0):
                        person = i 
                    else:
                        msg = msg + i + " "

            # Write lines in JSON format
            lines = collections.OrderedDict()
            lines["person"] = person
            lines["msg"] = msg
            fn = './ocr/lines' + str(self.text_index) + '.json'
            self.text_index += 1
            fw = open(fn, 'w')
            json.dump(lines, fw, indent=4)

if __name__ == "__main__":
    evh = screenshotEventHandler()
    obs = Observer()
    obs.schedule(evh, 'D:/steam_ss', recursive=True)
    print("Start OCR")
    obs.start()
    try:
        while True:
           time.sleep(1)
    except KeyboardInterrupt:
        obs.unschedule_all()
        obs.stop() 
    obs.join() 
    print("Finish OCR")
出力するjsonは以下のようになります。
{
    "person": "Coachman",
    "msg": "If you're planning a journey ..."
}

Webサーバ側のコード

なんのへんてつもないhtml + phpで作ります。
これも素人なのでクオリティはお察し、動けばいいやで作ります。
ちなみにWebサーバはApatche2を使っています。

まずはhtmlから。

<!DOCTYPE html>
<html>
<head>
    <meta charset='utf-8'>
    <title>OCR Test</title>
</head>
<body>
    <h2>Octopath Traveller's dialogue</h2>
    <?php include('./proto1.php'); ?>
    </body>
</html>

ついでphp

<?php
$dir_ocr = "./ocr";
if (is_dir($dir_ocr)) {
    if ($dh = opendir($dir_ocr)) {
        while (($file = readdir($dh)) !== false) {
            $dot_pos = strpos($file, '.json');
            if(strlen($file) >  2) {
                $filepath = $dir_ocr."/".$file;
                $json = file_get_contents($filepath);
                $arr = json_decode($json,true);
                echo "<h3>$arr[person]</h3>\n";
                echo "<p>$arr[msg]</p>\n";
                echo "<br/>\n";
            }
        }
        closedir($dh);
    }
}
?>

プロトタイプ1、始動

一通り仕組みが完成したところで、動かしてみます!
わくわく。

まず、ターミナルソフトでpythonを実行します。
無限ループするので、そのまま放置しておきます(画面上でpython_sl.exeとなっているのは、python.exeを実行すると、Microsoft純正のpythonのダウンロードを勧められるため。僕はAnacondaのpythonシンボリックリンクを張って使っています)。
f:id:yukirunrun:20200621014520p:plain

ゲームをプレイします。
人々との会話を楽しむさなか、英語の意味がわからないタイミングで、F12を押します。

f:id:yukirunrun:20200621014945p:plain
©2018 SQUARE ENIX CO., LTD. All Rights Reserved.

スクリーンショットの保存を検出すると、自動的にOCRを実行し、jsonファイルを生成します。
ターミナル上では、その旨を簡潔に表示します。

f:id:yukirunrun:20200621015217p:plain

適当に4人ほどに試した後、先ほど作成したWebページを開きます。

f:id:yukirunrun:20200621015544p:plain

人々のセリフを正しく取得できてい・・・ないですね。
やっぱ適当に作るとこうなるよなーーーー

今回はとりあえず動いたということで、改善は次回以降にします!
以上。