Android開発者のためのOCR入門（tess-two編）

# はじめに
AndroidアプリでOCRを実装する際、TessaractのAndroid用ラッパーライブラリtess-twoを利用するか、またはML Kit for Firebaseを利用する方法があります。
今回は、前者のtess-twoを使ってギャラリーから画像を選択してその画像にある文字を読み取るアプリを作ってみたいと思います。

サンプルコードは[ここ](https://github.com/tarumzu/OCRSampleAndroid)に置いてあります。

# インストール
まずはtess-twoを入れます。また、画像をギャラリーから読み込む際にExifを読み取って画像の向きを正しい方向に直したいのでexifinterfaceも入れておきます。

```kotlin:app/build.gradle.kts
// 略
dependencies {
    implementation("com.rmtheis:tess-two:9.0.0")
    implementation("androidx.exifinterface:exifinterface:1.0.0")
}
```

続いてappディレクトリ以下に/assets/tessdata/ディレクトリを作成して言語データを配置します。
今回はこんな感じにしました。

```
app - assets - tessdata
                  |-- jpn.traineddata
                  `-- eng.traineddata
```
言語データは下記からダウンロードしてください。tess-twoはtesseract3.0.5以下で作成された言語データが必要です。
https://github.com/tesseract-ocr/tessdata/tree/3.04.00


# 実装
tess-twoの使い方自体は非常に簡単です。わずか５行程度です。

```kotlin:OCRUtil.kt
        val baseApi = TessBaseAPI()
        // initで言語データを読み込む
        baseApi.init(context.getFilesDir().toString(), "jpn")
        // ギャラリーから読み込んだ画像をFile or Bitmap or byte[] or Pix形式に変換して渡してあげる
        baseApi.setImage(bitmap)
        // これだけで読み取ったテキストを取得できる
        val recognizedText = baseApi.utF8Text
        baseApi.end()
```

ですが注意点が２つあります。

１つ目はbaseApi.initの中では、言語データをFileでopenしているためFileで読み込める場所に言語データをコピーしてあげる必要があります。

```kotlin:OCRUtil.kt
    private val TESS_DATA_DIR = "tessdata" + File.separator
    private val TESS_TRAINED_DATA = arrayListOf("eng.traineddata", "jpn.traineddata")
    private fun copyFiles(context: Context) {
        try {
            TESS_TRAINED_DATA.forEach {
                val filePath = context.filesDir.toString() + File.separator + TESS_DATA_DIR + it

                // assets以下をinputStreamでopenしてbaseApi.initで読み込める領域にコピー
                context.assets.open(TESS_DATA_DIR + it).use {inputStream ->
                    FileOutputStream(filePath).use {outStream ->
                        val buffer = ByteArray(1024)
                        var read = inputStream.read(buffer)
                        while (read != -1) {
                            outStream.write(buffer, 0, read)
                            read = inputStream.read(buffer)
                        }
                        outStream.flush()
                    }
                }

                val file = File(filePath)
                if (!file.exists()) throw FileNotFoundException()
            }
        } catch (e: FileNotFoundException) {
            e.printStackTrace()
        } catch (e: IOException) {
            e.printStackTrace()
        }
    }
```

２つ目はtess-twoで画像を読み込む場合、画像の向きが正しくないと当然読み込めません。ギャラリーから読み込む場合、画像が回転してしまっている場合があるのでExifを見て正しい方向に直す必要があります。

```kotlin:MainActivity.kt
    // 画像のuri
    uri?.let {
        contentResolver.openFileDescriptor(it, "r").use {parcelFileDescriptorNullable ->
            parcelFileDescriptorNullable?.let {parcelFileDescriptor ->
                val fileDescriptor = parcelFileDescriptor.fileDescriptor
                bitmapOrigin = BitmapFactory.decodeFileDescriptor(fileDescriptor)
                contentResolver.openInputStream(it).use {
                    it?.let {
                        // ExifInterfaceを初期化
                        val exifInterface = ExifInterface(it)
                        val orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED)
                        val degrees = when (orientation) {
                            // 正しい方向の場合は回転させない
                            1 -> { 0f }
                            // 逆向きなので180度回転させる
                            3-> { 180f }
                            // 左向きの画像になってるので90度回転させる
                            6 -> { 90f }
                            // 右向きの画像になってるので270度回転させる
                            8 -> { 270f }
                            else -> { 0f }
                        }
                        val matrix = Matrix()

                        val imageWidth = bitmapOrigin?.getWidth() ?: 0
                        val imageHeight = bitmapOrigin?.getHeight() ?: 0
                        matrix.setRotate(degrees, imageWidth.toFloat() / 2, imageHeight.toFloat() / 2)
                        bitmap = Bitmap.createBitmap(bitmapOrigin!!, 0, 0, imageWidth, imageHeight, matrix, true)
                    }
                }
            }
        }
    }
```

ここまで実装したら早速動かしてみましょう。今回はこちらの画像から文字を読み取ってみます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106116/c18bae7e-6172-eb32-c237-77a588b77bca.png" width=50%>

早速アプリを起動して画像を読み込んでみると…

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106116/1da9a774-e6ac-2790-2d51-c42b4025d48f.png" width=50%>

無事、文字を読み取ることが出来るようになりました！が、、精度がいまいちです…

# 日本語言語データをカスタマイズ

先程の画像を見てみると数字が全然読み取れていないことに気がつくと思います。今回はこの数字の精度を改善してみたいと思います。

数字の誤変換の原因ですが、[こちら](https://a244.hateblo.jp/entry/2015/08/24/001916)のサイトを見てみると認識結果の変換マッピングが原因のようです。なのでjpn.unicharambigsを修正してみます。

今回、tesseract-ocrの環境をDockerで用意しました。こちらの環境を使ってカスタマイズします。
https://github.com/tarumzu/OCRSampleAndroid/tree/master/containers

まずは下記の手順でDockerコンテナにログイン

```bash
cd containers
docker-compose build
docker-compose up

docker-compose exec tesseract bash
```

ログインしたら、jpn.traineddataをダウンロード、次にcombine_tessdataコマンドのeオプションでjpn.traineddataからjpn.unicharambigsを取り出します。

```bash
wget https://github.com/tesseract-ocr/tessdata/raw/3.04.00/jpn.traineddata
combine_tessdata -e jpn.traineddata jpn.unicharambigs
```

取り出したらjpn.unicharambigsをvimで開いて下記の行を削除します。これは左側のキャラクタを右側のものに変換するというルールなのですが正直必要ない変換だと思います。

```
1	l	1	ー	1
1	｜	1	ー	1
1	I	1	ー	1
1	1	1	ー	1
1	|	1	ー	1
1	O	1	。	1
1	°	1	。	1
```

ここまでできたらjpn.traineddataを再作成します。

```bash
combine_tessdata -o jpn.traineddata jpn.unicharambigs
```

では、先程作成した言語データはcontainersに保存されます。この言語データを使って再度画像を読み込んでみましょう。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106116/a68030b2-ea71-d62d-e630-06ae100c780a.png" width=50%>

たったこれだけで数字の精度が劇的に改善できました！

# 終わりに
簡単にOCRが実装できましたが言語データのカスタマイズは必須ですね…
次はML Kit for FirebaseでOCRを実装してみて違いなどを記事にしてみたいと思います。


# 参考
- [Android OCR (Optical Character Recognition)](https://tyatran.hatenablog.com/entry/2017/10/09/142015)
- [tesseract-ocr の言語データ(jpn.traineddata)について（その1）](https://a244.hateblo.jp/entry/2015/08/24/001916)