らんらん技術日記

日々の学習メモに

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

前回のあらすじ

オクトパストラベラーを英語でプレイするさなか、ふとある思いを抱く。
「ゲーム画面を見ながら、Google翻訳かけるのって面倒だな・・・」
ゲームなのに面倒、これは深刻な問題である。
ゲームに娯楽の要素が薄くなれば、プレイの中止も検討せざるをえない。
そんな現状を打破すべく、OCR(Tesseract)による自動文字情報取得システムの開発に着手、
苦労の末、プロトタイプ1が完成した!
僕の期待を背負ってプロトタイプ1は始動するも・・・
無残にも、まともに文字情報を得ることはできなかった。

前回の問題点

前回、OCRはそんなに簡単じゃないことが判明しました。
仕方がないので精度向上について考えてみます。
Tesseractのホームページをみていると、役に立ちそうな記事を発見しました。

tesseract-ocr.github.io

記事の主旨を一言でまとめると、
「Tesseractに画像を入力する前に、前処理をやってくれ」
という感じになるでしょうか。
前処理とは、以下のような処理を指すようです。

  • 画像の2値化
  • 白背景に黒文字にする
  • 不要なものを除去する(ページの境界線とか)

確かにそう言われるとなー、前回はスクショをそのままTesseractに放り投げていました。
そりゃあうまく行かない訳だw

ということで、スクショに前処理をかけてみます!!

前処理をかける

素人なりに試行錯誤したところ、精度が安定する方法を見つけることができました。
具体例を紹介します。

〇前処理をかける前

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

〇前処理をかけた後

f:id:yukirunrun:20200628213029p:plain


もはや全然違う画像ですねw
ここまでやれば認識精度は格段に上がります!
しかしここまでやる必要はあるのか?
素人なので、ホントのところはわかりません。
ただし根拠はあります。
Tesseractはアプリケーションとして、古書のデジタル化を想定しているようです。
つまり一般的な本に対して、精度が良くなるように設計していると推測できます。
ということで、本と同じような状態に近づけると、精度向上につながるはずです!

実はこの状態でも、幾つかの文字は誤認識します。
おそらく、オクトパストラベラーのフォントが日常生活で使われていないからでしょうね。
その問題を解決するにはTesseractの再学習をやる必要がありそうなので、今回はやめておきます。

では、具体的なフローに入っていきます。

領域を特定する

抽出したい文字情報がある場所を、どうにかして抽出します。
どうにかしてというのが、素人が素人たる所以です。
理想的には、体系的かつ流用性のある方法を探るべきですが、そんなことは気にしません。
オクトパストラベラーに特化した方法でやります!

今回の例でいえば、上のスクショにおいて、表示されているメッセージには以下の特徴があります。

  • メッセージの大きさは変化する
  • メッセージの位置は変化する
  • ほぼ一様なベージュ色
  • 四角形のような形をしている(少なくとも丸や三角ではない)

大きさと位置が固定であれば、座標の直打ちで切り抜いていたところです。
仕方がないので、色や形の特徴を使うことにします。
何かいい方法はないかなーとググることしばらく、良いページを見つけました。

axa.biopapyrus.jp

このページの「オブジェクト検出例(バウンディングボックス)」を使うことにします。
この方法をベースに、幾つかコード修正を加えると・・・

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

うまいことメッセージ部を特定することができました!
特定した領域をクロッピングし、その他の部分は破棄します。
参考までに、該当箇所のソースコードはこんな感じ。

    def get_dialogue(self, img_S):
        # img_S is S(Saturation) element of a screenshot in HSV space
        img_bin = cv2.inRange(img_S, 33, 34)
        contours, hierarchy = cv2.findContours(img_bin, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)
        for i in range(0, len(contours)):
            if (i == 0):
                max_contour = contours[i]
            else:
                if cv2.contourArea(max_contour) < cv2.contourArea(contours[i]):
                    max_contour = contours[i]
        return max_contour

2値化、ノイズ除去

先ほどと同様に、どうにかしてやります
OCRをかけるにあたってのポイントは、

  • 文字はくっきりと
  • 余分なノイズはなくす

となります。このバランスが結構難しいんですよ。
文字がぼやけると、文字の誤認識になります。
ノイズがあると、ノイズを文字と認識します。

ノイズが残ってしまった例を紹介します。

f:id:yukirunrun:20200628225808p:plain

画像上の方に、謎の横線がいくつか残っていますよね。取り切れなかったノイズです。
これくらいいいじゃんって思うかもしまれせん。
僕はそう思っていました。
しかしTesseractは平気で誤認識してきます!
うまく回避する方法はあるのかもしれませんが、いずれにせよノイズが無いに越したことはありません。

参考までに、僕の実装例を紹介します。
ノイズ除去のため、2値化の範囲をできるだけ絞り、一部モルフォロジー変換を使っています。
また、白背景に黒字になるように色を反転しています。

        #Get the position of a dialogue in the screenshot
        dialogue= self.get_dialogue(img_S)

        #Preprocessing to the dialogue
        x, y, w, h = cv2.boundingRect(dialogue)
        img_crop = img_V[y:y+h, x+3:x+w-50]
        img_bin = cv2.inRange(img_crop, 60, 95)
        img_bin[0:3] = 1
        img_bin[0:40, 400:] = 1
        img_inv = cv2.bitwise_not(img_bin)
        kernel = np.ones((2,0),np.uint8)
        img_inv[0:6] = cv2.morphologyEx(img_inv[0:6], cv2.MORPH_CLOSE, kernel)

プロトタイプ2、始動

そんなこんなで、前処理の仕組みを取り入れたプロタイプ2が完成しました。
メッセージの他に、「尋ねる」「精査する」コマンドにも対応させています。
簡単に結果画面だけ紹介します。

f:id:yukirunrun:20200628232044p:plain

だいぶ実用的なシステムになってきました!
しかし、ところどころ文字の誤認識が見られます(I「アイ」を|「罫線」とか)。
このままだと少し不便なので、Tesseractのチューニング(再学習)が必要かな。
そのためには学習のためのデータが必要なので、とりあえずはプロトタイプ2を運用しつつ、データを集めたいと思います。

以上。