かくすけのいろいろ作るブログ

かくすけの開発者ブログです。開発の他いろいろなモノづくりについて書きます。

【C#】【Tesseract OCR】画像処理を行ってOCRの精度を向上させる

こんにちは。かくすけです
前回はTesseract OCRを開発中の「画面翻訳ソフト」に導入し、デフォルト状態での精度を他のOCRエンジンと比較してみました。

kakusuke98.hatenablog.com

「画面翻訳ソフト」開発の発端と仕組みの説明記事はこちら

kakusuke98.hatenablog.com

その結果、使えそうだと判断したので今回はTesseract OCRの精度向上を目指して色々やってみます。
Tesseract OCR以外のOCRでも画像の変換処理は有効だと思いますが、一番結果がはっきりでそうだったのでTesseract OCRで確認しています。

そして、今回は前回の精度確認でTesseract OCRの認識精度が低かった以下のような
吹き出し無しでゲーム画面に白文字が表示されているパターン」
をうまく認識させられるようにすることを目的とします。
f:id:kakusuke98:20191113235320p:plain

こういうゲームは結構多いですからね。そしてこの場合大抵白文字なので白文字特化で対応させます!!

方法

精度向上の方法は

  • Tesseract OCRの学習
  • Tesseract OCRに渡す画像の加工

といったものがあると思います。

Tesseract OCRの学習

Tesseractには学習させるためのツールが準備されており、こちらの公式GitHubページではその方法が細かく紹介されています(英語)

github.com

しかし学習というのは基本的に"特定の特徴をもつ画像内の文字を認識する精度を上げる"ためのものであると考えています。
つまり、いろんなタイプの画像に含まれる文字を認識させたい、つまり特徴がバラバラな画像を認識させる場合はあまり精度向上が見込めないか、むしろ精度が落ちる可能性が高いと考えました。
私のシステムではいろんなゲーム画面を読み込ませるつもりなので、このパターンに当たります。ですのでひとまず学習は無視します。
(いつかその人のプレイしているゲームにあわせて学習するようにできたら最高ですね。遠い遠いお話だけど)

ただ、学習とは違って使えそうな機能があるとどこかの記事で目にしました。
それは認識する文字の種類(アルファベットだけ認識するとか、どの記号を使うとか)を設定できるというもの。
それはぜひ使いたいと思っていたのですが、どうやらバージョン4では使えないようです(2019/12/05現在)
Tesseract OCR v4 を使うつもりでいたのですが、この理由でやはりv3にするかもしれません!が、それはまた今度のお話。今回はv4で検証していきます。

画像の加工手法

学習をさせないということで、私のシステムでは画像の加工を行うことで精度向上を図ります。
効果のある画像加工の手法も公式のGitHubページで紹介してあります。

github.com

これはありがたい!!
紹介されている画像加工の手法は以下の7つ。

  • 色反転
  • サイズ変更
  • 二値化
  • ノイズ除去
  • 傾き補正
  • ボーダー除去
  • アルファチャネル除去

色反転

Tesseractの公式のページには「バージョン3系までは暗い背景に明るい文字も認識するけど、バージョン4系は明るい背景に暗い文字しか認識しないよ」と書いてあります。
その割には前回↓の画像とかいい感じに認識してたけどね。

f:id:kakusuke98:20191205134541p:plain

できなくはないけど、得意ではないよということなんでしょうかね。
そんな難しいものでもないので対応します!

サイズ変更

公式の以下FAQページによると、Tesseractでは小文字のxが8ピクセル以上で解像度が300dpi以上の画像であれば文字認識し、8px以下の文字はノイズとして扱われるようです。

github.com

私のアプリでは切り抜き画像の高さが低い場合は拡大することににします。

二値化

二値化というのは画像を"真っ白"と"真っ黒"の2つの色だけで表現されることです。
ぼんやりとした画像は認識しづらいから白黒はっきりさせた画像にしな!ということですね。
とはいえこれは諸刃の剣。下手したら文字がつぶれちゃってまったく認識できないなんてことになりかねません。
私のアプリでは二値化ではなく中間のグレーも含むように変換します。

ノイズ除去

これはそのまま。ノイズは削除せいという話。
ゲーム画像はそりゃあノイズ多いのですが…私の力量では厳しいところがあります。とりあえず今回は見送り。

傾き補正

これも難しいですね!
文字そのものが傾いてたらちゃんと横並びになるように画像を回転させてね。というものです。
自動変換は難しいし、傾いてる文字自体ゲームの重要な文章には少ないと思うのでスルー。ゲームによってはそういう演出もあると思うけどね。
多分これはスキャン画像の認識などを想定した内容なんじゃないかな。

ボーダー除去

無駄な線は消してくださいという内容です。
これも多分スキャン画像で折り目の線が入っちゃったりしてる場合にはそれを取り除いた方が良いという話だと思います。
しかし同じ項目に気になる一文が。
"If you OCR just text area without any border, tesseract could have problems with it."
境界線の無いテキスト部分のみの画像を読み込ませたらエラーが発生するかもという内容だと思います。
ボーダーは消さないといけないけど境界線は必要・・・?
画像処理の最後に画像全体を細い白線で囲った方がいいのかも。やってみます。

アルファチャネル除去

アルファチャネルとは透明部分を表す情報。
今回はもともと透明色なんてないのでスルーします!

全項目について書いていきましたが、今回私がすることをまとめると

  • 色反転
  • 画像拡大
  • 4値化か5値化あたり
  • 境界線追加

となります!
ちなみに後ほど紹介しますが"4値化か5値化あたり"の処理ではちょっと特殊な変換をします。
細かいところは秘密ですが。

変換処理コード紹介

いよいよ実践です!
今回は実況動画をとっていた中で認識できなかった、ゲーム"Skylar & Plux Adventure On Clover Island"のこの画像を使用します。
f:id:kakusuke98:20191205151231p:plain

色反転

色反転は次のページのソースコードをほぼそのまま使わせていただきました。

参考にしたページ dobon.net

ColorMatrixを使って赤、青、緑の値を画像すべてのピクセルで反転(-1)させてありますね。
画像を色反転させた結果はこちらです!
f:id:kakusuke98:20191205151531p:plain ちゃんと反転されていますね!ちょっとホラーチックに。

しかしこれだけではOCR結果は相変わらず空のままでした。

画像拡大

今回の画像は文字大きいから拡大の必要なんてないんだろうけども。
できる仕組みだけ作っておきます。

こちらのページを参考にさせていただきます!
dobon.net

拡大にも色々な手法があるようですが、今回は高品質補間を使ってみました。
小さい画像のみ拡大する予定なのでそこまで処理に時間はかからないはず。
でもそれぞれの違いによる認識の結果を比較していないので、このあたりの検証もしないといけませんね!

ほぼ参考にさせていただいたページそのままですが次のようなメソッドを作成しました。

        //--------------------------------------
        // 画像をリサイズする
        // 引数: snap リサイズしたいBitmap
        //       width リサイズ後の幅
        //       height リサイズ後の高さ
        // 戻り値: リサイズ結果Bitmap
        //--------------------------------------
        private Bitmap ResizeBitmap(Bitmap snap, int width, int height)
        {
            // 描画先とするImageオブジェクトを作成する
            Bitmap canvas = new Bitmap(width, height);
            // ImageオブジェクトのGraphicsオブジェクトを作成する
            Graphics g = Graphics.FromImage(canvas);

            // 補間方法として高品質補間を指定する
            g.InterpolationMode =
                System.Drawing.Drawing2D.InterpolationMode.High;
            // 画像をリサイズする
            g.DrawImage(snap, 0, 0, width, height);

            // 不要なオブジェクトを破棄
            snap.Dispose();
            g.Dispose();

            return canvas;
        }

そして結果はこちら!
f:id:kakusuke98:20191205155516p:plain

ブログ側の処理で自動リサイズされてたらほぼ同じ画像を載せてるだけになりますねw
もともと文字のサイズが原因で読み取れない画像ではないため当然のごとく認識結果は空のままです。
今回のこれはただの仕組みづくりですもん!

4値化か5値化あたり

まずは単純にグレースケール画像にしてみます。
こちらのソースコードをそのまままるっと使わせていただきました!

dobon.net

結果はこちら。ちゃんとモノクロ画像になってますね!
f:id:kakusuke98:20191205160205p:plain

認識結果は相変わらず。
ここで、自分でいままで使ってみて「Tesseractは原色に近い色と白文字の組み合わせに弱い」と感じていたので独自の画像変換値を準備することにしました。

参考にさせていただいたのはこちらの2値化のソースコードです。
dobon.net

ページ内こちらのコードでピクセル1つ1つの明るさをとって、白にするか黒にするかを決定しているようです。

    //新しい画像のピクセルデータを作成する
    byte[] pixels = new byte[bmpDate.Stride * bmpDate.Height];
    for (int y = 0; y < bmpDate.Height; y++)
    {
        for (int x = 0; x < bmpDate.Width; x++)
        {
            //明るさが0.5以上の時は白くする
            if (0.5f <= img.GetPixel(x, y).GetBrightness())
            {
                //ピクセルデータの位置
                int pos = (x >> 3) + bmpDate.Stride * y;
                //白くする
                pixels[pos] |= (byte)(0x80 >> (x & 0x7));
            }
        }
    }

私はここで明るさ「img.GetPixel(x, y).GetBrightness()」だけでなく色相「img.GetPixel(x, y).GetHue()」も使って計算するようにしました。
その結果はこちら!

f:id:kakusuke98:20191205161734p:plain

そして認識結果がついに空以外になりました!!
「v Wm“ ( , I // , \ /Mhat? w' 'e‘ll that’mat’... \」 まあボロボロではありますが…空よりはマシ!

境界線追加

ここまでやって私は感じ始めました。
「これって空で返ってきてるのは実は認識できていないわけではなく、それ以前に噂の"境界線がないエラー"が発生しているのではないか…」と

やってみればわかりますね!
もとの画像にただ白い枠線を入れただけの画像を生成して認識させてみます。
下のコードは雑なのであまりマネしないでくださいね。

        // Bitmapに枠線を追加する
        private Bitmap AddBorder(Bitmap snap)
        {
            // 描画先とするImageオブジェクトを作成する
            Bitmap canvas = new Bitmap(snap.Width + (1 * 2), snap.Height + (1 * 2));
            // ImageオブジェクトのGraphicsオブジェクトを作成する
            Graphics g = Graphics.FromImage(canvas);

            // 枠付き画像の生成
            g.DrawImage(snap, 1, 1, snap.Width + 1, snap.Height + 1);
            g.DrawRectangle(new Pen(Color.White, 1), new Rectangle(0, 0, canvas.Width - 1, canvas.Height - 1));

            // 不要なオブジェクトを破棄
            snap.Dispose();
            g.Dispose();

            return canvas;
        }

生成された画像がこちら!
f:id:kakusuke98:20191205170351p:plain

そして認識結果は空!!
良かった…いままでの努力は無駄じゃなかった…!

全部合わせた処理結果

前の項目で使用する画像処理を紹介しました。
ひとつひとつの効果は薄かったですが、組み合わせると・・・?

  1. 画像拡大
  2. 4値化か5値化あたり
  3. 色反転
  4. 境界線追加

の順番で変換処理をかけた結果はこちらです

これが f:id:kakusuke98:20191205151231p:plain

こう f:id:kakusuke98:20191205171427p:plain

ずいぶん変わりました!

そして、OCRの結果は・・・
「\ / \IIV // /\V / Plux: What? Well that’s just great... \」

まだまだゴミは残っていますが、ちゃんと文字が取得できるようになりました!!
無駄な部分はテキストを取得した後に取り除いたり、Tesseract OCR v3の認識する文字の種類設定機能を使うことで除去できる気がします!!

問題点

上の結果で良さそうに感じる結果が出たのですが、逆に悪いこともあります。
もともと認識できていたこの画像が
f:id:kakusuke98:20191205173136p:plain

こうなってしまい
f:id:kakusuke98:20191205173156p:plain

全然認識できなくなってしまいました。
今回の目的は吹き出し無しの白文字を取得できるようにすること(別モードとして切り替えられるようにする)だったから良いっちゃ良いんだけど…
それにしても文字がつぶれすぎじゃありません!?

きっと4値化か5値化あたりの変換時に白が正しく取得できていないのが原因ですね。
今後も解像度いじったり、閾値を変更したり、ピクセル単位の動作を確認しつつ修正したりする必要がありそうです。

とはいえいままで認識できなかった文字が認識できたんです!
大きな一歩ということで、今回はここまで!
読んでくださってありがとうございましたー!