Unityでtesseract-4.1.0を使う

ネット上にいくつかtesseractをUnityに導入する記事はあるものの、想像以上に手こずってしまったので備忘録も兼ねて記事にします。
今回使用した環境はWindowsなので、その他の環境では同じ方法でも動かない可能性があります、ご了承ください。

# そもそもtesseractって？
tesseractとはOCR(Optical Character Recognition、光学文字認識)と呼ばれる画像から文字を認識するためのライブラリの一つで、Apache License 2.0 の下で配布されています。

基本的に内部の言語はC++で書かれているため、Unity上で利用するためにはこれをC#から呼び出せるようにしなければなりません。そこで今回利用したのがDllImport属性です。
DllImport属性を用いるとC#から.dllファイル内の関数を呼び出すことができます。つまり、tesseractを.dllファイルとしてビルドしておけばUnityからその関数を呼び出すことができるということです。

# tesseractをビルドする
ここから先は実際にtesseractをdllファイルとしてビルドしていきます。
## tesseractのダウンロード
[GitHub](https://github.com/tesseract-ocr/tesseract/releases)からtesseract-4.1.0のソースコードをダウンロードしてきます。今回は.zip型式でダウンロードしました。
ダウンロード後は通常通り解凍しておきます。これでダウンロード自体は完了です。
## ビルド環境の構築
次にtesseractをビルドするために必要な環境を構築します。tesseractの[wiki](https://github.com/tesseract-ocr/tesseract/wiki/Compiling#develop-tesseract)にコンパイルの方法が詳しく載っているのでこちらを参考にします。今回はダウンロードしたソースコードからビルドを行うのでGitHubからcloneする必要はありません。また、CPPANやCMakeの環境構築は別の記事が詳しいので本記事では割愛させていただきます。どちらも基本的には公式からダウンロードしてインストール、PATHを通せば完了です。

CPPANおよびCMakeの環境構築が完了した後は、まず先ほど解凍したtesseractフォルダ内のcppan.ymlファイルを変更します。変更するのは5行目です。

```yml:cppan.yml(変更前)
local_settings:
    #use_shared_libs: true
    #short_local_names: true
    #use_cache: false
    #generator: Visual Studio 14 2015 Win64
```

5行目のコメントアウトを解除して使うVisual Studioの環境に書き換えます。
(今回は64bitのdllとしてビルドしますが、もし32bitのdllとしてビルドする場合は5行目と15行目のWin64をWin32に書き換えてください。)

```yml:cppan.yml(変更済)
local_settings:
    #use_shared_libs: true
    #short_local_names: true
    #use_cache: false
    generator: Visual Studio 15 2017 Win64
```
上記の変更を加えればビルド環境の構築は完了です。
## ビルドする
先ほどのwiki通りにビルドをすれば完了・・・と言いたいところですが、64bitの場合はCMakeコマンドにオプションでWin64であることを指定する必要があります。なので以下のコマンドでビルドすることになります。

```
cppan
mkdir build && cd build
cmake .. -G "Visual Studio 15 2017 Win64"
```

cmakeでのビルド完了後はVisual Studioでビルドを行います。先ほど作成したbuildフォルダ内のtesseract.slnファイルを開きます。ソリューションのプロパティから構成がRelease、プラットフォームがx64になっていることを確認(またはそれに変更)してからソリューションのビルドを行います。

特に失敗がなければこれでtesseractのビルドは完了です。作成したdllファイルはbuild/bin/Release内にあるはずです。(既定の設定のままの場合)

# Unityでtesseractを使う
先ほどビルドしたtesseractのdllファイルをUnityから使用します。(今回はUnity 2018.4.10f1を使います)
## プロジェクトの作成
適当な名前をつけてUnityプロジェクトを作成します。今回はTesseractTestとしました。
プロジェクトが立ち上がったら、まずはAssetsフォルダ内にPluginsフォルダを作成し、先ほど作成した.dllファイルを全てPluginsフォルダ内にいれます。(自環境では33個のdllファイルがありました)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302026/71fdf834-460f-b261-2809-02297a6f90b7.png)
階層の様子は上記のようになるはずです。
次にtesseract-4.1.0フォルダ内にあるtessdataフォルダをまとめてAssetsフォルダにいれます。この時エラーが出ますが無視して大丈夫です。今のままでは学習用データがなく、Unityで実行すると必ずクラッシュしてしまうので今度はtesseractのGitHubの[tessdata](https://github.com/tesseract-ocr/tessdata)からとりあえずeng.trainneddataをダウンロードしてこちらもtessdata内にいれます。今回はアルファベットの認識のみとするので、他の言語を使用したい場合は対応した言語のtrainneddataをダウンロードすればいいかと思います。(この辺りはUnity等関係なくtesseract周りの記事が詳しいと思います)

## OCR用の画像の準備
画像を準備します。今回は以下のような画像で文字認識を行いたいと思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302026/646b23b5-39f6-ced1-f488-e08258fa88cd.png)
※tesseractを使う上での注意点として、文字色が黒で背景が白、または文字色が白で背景が黒でない場合精度がガクッと落ちるそうです。そのためもし色の着いた背景の物を使いたい場合などはその画像を二値変換してからtesseractに渡すといいかと思います。

上記の画像をAssetsフォルダにいれ、Inspector内のRead/Write Enableにチェックを入れApplyします。

## ゲームオブジェクトの設置
画像を映すためのRawImage、認識した文字を映すためのTextを設置します。RawImageのInspector内のTextureに先ほどAssetsフォルダにいれた画像をD&Dします。このTextureに設定した画像を対象にtesseractで文字認識を行うという形になります。また、動作確認のためCanvasはScreen Space - CameraでMain Cameraの前に表示されるように設定し、適当な位置になるようにRawImageとTextを移動します。
次に空のオブジェクトを設置します。(わかりやすさのためにtesseractという名前にしておきます)

## スクリプトの作成
適当な位置に空のスクリプトを作成します。今回はOcrFromTexture.csとしました。tesseractのAPIを見つつDllImport属性を用いて関数を呼び出せるようにします。

```cs:OcrFromTexture.cs
using UnityEngine;
using UnityEngine.UI;
using System;
using System.Runtime.InteropServices;

public class OcrFromTexture : MonoBehaviour
{
    [SerializeField]
    RawImage source;
    Texture2D texture;
    [SerializeField]
    GameObject output;

    IntPtr ocrHandle = IntPtr.Zero;

	[DllImport ("tesseract41")]
	public static extern IntPtr TessBaseAPICreate ();
	[DllImport ("tesseract41")]
	public static extern void TessBaseAPIDelete (IntPtr ocrHandle);
	[DllImport ("tesseract41")]
	public static extern int TessBaseAPIInit3 (IntPtr ocrHandle, string dataPath, string language);
	[DllImport ("tesseract41")]
	public static extern void  TessBaseAPISetImage (IntPtr ocrHandle, byte[] imagedata, int width, int height,
	                                                 int bytes_per_pixel, int bytes_per_line);
	[DllImport ("tesseract41")]
	public static extern void  TessBaseAPISetImage2 (IntPtr ocrHandle, IntPtr pix);
	[DllImport ("tesseract41")]
	public static extern IntPtr  TessBaseAPIGetInputImage (IntPtr ocrHandle);
	[DllImport ("tesseract41")]
	public static extern IntPtr  TessBaseAPIGetThresholdedImage (IntPtr ocrHandle);
	[DllImport ("tesseract41")]
	public static extern int TessBaseAPIRecognize (IntPtr ocrHandle, IntPtr monitor);
	[DllImport ("tesseract41")]
	public static extern string  TessBaseAPIGetUTF8Text (IntPtr ocrHandle);
	[DllImport ("tesseract41")]
	public static extern void  TessDeleteText (IntPtr text);
	[DllImport ("tesseract41")]
	public static extern void  TessBaseAPIEnd (IntPtr ocrHandle);
	[DllImport ("tesseract41")]
	public static extern void  TessBaseAPIClear (IntPtr ocrHandle);

    // Start is called before the first frame update
    void Start()
    {
        /* OCRハンドラの作成と初期化 */
        ocrHandle = IntPtr.Zero;
        ocrHandle = TessBaseAPICreate();
        if(ocrHandle.Equals(IntPtr.Zero)) Debug.Log("Error:Cannot Create");
        string dataPath = Application.dataPath + "/tessdata";
        if(TessBaseAPIInit3(ocrHandle, dataPath, "eng") != 0) Debug.Log("Error:Cannot Init");
        /* テクスチャから文字を認識する */
        string recognition = RecognizeFromTexture();
        output.GetComponent<Text>().text = recognition;
    }
    public string RecognizeFromTexture()
    {
        /* 画像をTexture2D型として読み込む */
        texture = source.texture as Texture2D;
        Color32[] colors = texture.GetPixels32();

        /* 画像データをbyte[]型にする */
        int width = texture.width;
        int height = texture.height;
        int area = width * height;
        byte[] bytes = new byte[area * 4];
        int point = 0;
        int index = area - 1;
        for(int y = height - 1; y >= 0; --y){
            for(int x = 0; x < width; ++x){
                index = x + y * width;
                bytes[point] = colors[index].r;
                bytes[point+1] = colors[index].g;
                bytes[point+2] = colors[index].b;
                bytes[point+3] = colors[index].a;
                point += 4;
            }
        }

        /* byte[]をtesseractに渡して文字認識をする */
        TessBaseAPISetImage(ocrHandle, bytes, width, height, 4, width * 4);
        if(TessBaseAPIRecognize(ocrHandle, IntPtr.Zero) != 0) return null;
        string result = TessBaseAPIGetUTF8Text(ocrHandle);
        Debug.Log(result);
        return result;
    }
}
```
上記のスクリプトを先ほどのtesseractオブジェクトにアタッチし、InspectorからSourceにRawImageオブジェクトを、OutputにTextオブジェクトをD&Dすれば準備は完了です。

## Unityでプレイする
上記が終わったら実際にUnityのプレイボタンを押してみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302026/e2d6ae0c-5d25-ba12-acb5-12d5ec9caaff.png)
実際に文字を認識できていることがわかります。(上側がText、下側がRawImageです)
ただし空白部分は左に詰められてしまうようです。

# まとめ
tessdataフォルダをUnityにいれていなくてクラッシュして、入れたのにtraieddataが入っていないからクラッシュして・・・と一週間ほど格闘してようやくUnity上でもtesseractを使えるようになりました。
きちんと白黒のはっきりした画像であればそれなりに良い精度で認識できていると感じます。


Unity上では動いていたのにUWPとしてビルドしたところ動かなくなってしまったので、(解決策もわからなかったので)tesseractを使うことはなかったとか・・・