Edited at

Tesseract 4.1にLSTMを使って手書き文字を再学習させる


背景

TesseractはオープンソースのOCRエンジンです。バージョン4.0から深層学習を採用したことで認識精度が大きく上がりました。このTesseractを実務で使ってみて、苦手分野があることが分かりました。


  • 全角英数字

  • 半角カタカナ

  • 手書き文字

今回は手書き文字の認識精度を上げるため、Tesseractの英語モデルを再学習させてみます。

なお、他の2つの再学習についてはTesseract 4.1にLSTMを使って日本語を再学習させるにまとめています。


学習方法の選択

Tesseract 4.1にLSTMを使って日本語を再学習させると同じ方法を採用します。


環境設定

Tesseract 4.1にLSTMを使って日本語を再学習させると同じ手順を実行します。

今回は画像データを使うので、フォントはインストール不要です。


チューニング方法と評価方法の計画

今回は手書き英数字(アルファベットは大文字のみ)の認識精度を上げることを目標とします。


チューニング方法

通常は以下の3ステップが必要です。

(1) tesstrain.shで学習データを生成

(2) (1)で作った学習データへlstmtrainingを実行して既存モデルを再学習

(3) 再学習したモデルをlstmtrainingでcheckpoint形式からtraineddata形式に書き換え、環境変数TESSDATA_PREFIXに設定したフォルダへ出力

今回はフォントを使わず、画像とテキストのペアから学習データファイルを生成します。

ソースデータとして、筆者の手書き文字とこれをテキストファイルに打ち込んだものを用意します。

また、手製データのみだと十分なボリュームがないので、ノイズや回転を加えてデータを水増しします。

それでもデータ量が十分でないので、MNISTから各100文字のデータを追加します。

このためにステップ(1) を差し替えることとします。

(1'-A) オリジナルデータを用意して水増し (Data Augmentation)

(1'-B) MNISTから(TIFF)画像ファイルと(TXT)テキストファイルを生成

(1'-C) (TIFF)と(TXT)から(BOX)文字座標ファイルと(LSTMF)学習データファイルを生成

調べてみたところOCR-Dを利用すれば(1'-C)から(3)のtraineddata形式への書き換えまでを一気に処理できることがわかりました。ベルリンのKay-Michael Würznerさんありがとうございます!


評価方法

手書き文字の評価用画像を用意します。

aki_tegaki.png

この画像に対し、既存モデルで精度を見てみましょう。


グレースケール画像

$ tesseract -l eng_best '画像へのパス' stdout

C1 Liege 282
ABCPEFGHT J kL m NO
PQRSTUVW X YZ

ノイズのせいでしょうか、英語用モデルなのに数字の読み取りが悲惨な結果になりました。

画像を2階調化して読み取りなおしてみると...


モノクロ画像

$ tesseract -l eng_best '画像へのパス' stdout

01 234 5 6 7%7
ABC PEF GHT J ELM® NO
PQRSTUVW X TZ

数字の認識性能が改善しました。

今回はこのeng_best.traineddataを元にチューニングを行いましょう。


LSTMによるチューニング


(1'-A)オリジナルデータを用意して水増し (Data Augmentation)

すべてPhotoshopとテキストエディタを使った手作業ですので、概略のみ記載します。

特に(1)と(2)が地道で時間がかかりますが、この作業量が精度に大きく影響します。

(1) ランダムな英数字を紙に書いてスキャナーからPhotoshopへ読み込み、1行ずつ切り抜いてTIFF形式で保存します。

(2) 切り抜いた画像と同じファイル名で拡張子が.txtのテキストファイルを生成し、画像に対応する文字を打ち込みます。

(3) 画像ファイルとテキストファイルを水増しする分だけコピーします。

(4) コピーした画像ファイルへまとめて回転または(および)ノイズフィルターを適用します。


(1'-B) MNISTから(TIFF)画像ファイルと(TXT)テキストファイルを生成

今回はMATLAB用のemnist-byclass.matから生成します。


generate_training_data.py

import sys

import scipy.io as sio
from PIL import Image
import cv2
import numpy as np

args = sys.argv # MATLAB形式のEMNISTデータへのパスを第1変数として渡します

MAX_TRAIN_NUM = 100 # 学習データのバランスをとるため、各文字が100個を超えないようにします
mat_contents = sio.loadmat(args[1])
training_img = mat_contents['dataset'][0][0][0][0][0][0]
training_chr = mat_contents['dataset'][0][0][0][0][0][1]
chr_cnt = {}

def cnv_chr(code):
if code <= 9:
res = chr(code + 48) # 0-9
elif code <= 35:
res = chr(code + 55) # A-Z
else:
res = 'del' # a-z (今回は小文字は対象外とします)
return res

def save_txt(txt, n):
with open('eng.' + '{:0=4}'.format(n) + '.gt.txt', 'w', encoding='utf-8') as box:
box.write(txt)
box.write('\n')

def pick_image(mat_image, n):
return mat_image[n].reshape((28, 28), order='F')

def save_image(img_array, n):
img_array = np.ones(img_array.shape) * 255 - img_array # 白黒反転
saving_img = Image.fromarray(img_array).convert('L') # グレースケール画像化
saving_img.save('eng.' + '{:0=4}'.format(n) + '.tif') # 保存

text = ''
img = np.zeros((28, 1))
for i in range(len(training_img)):
character = cnv_chr(training_chr[i][0])
if character is not 'del':
if character in chr_cnt.keys():
chr_cnt[character] += 1
else:
chr_cnt[character] = 1
if chr_cnt[character] <= MAX_TRAIN_NUM:
text += character
img = np.hstack((img, pick_image(training_img, i)))
if len(text) >= 40: # 1行40文字を限度とします
save_txt(text, i)
save_image(img, i)
text = ''
img = np.zeros((28, 1))


データをダウンロードして実行してみましょう。


以下、rootでコマンドを実行しているものとします

# EMNISTをダウンロードして解凍します (約709MBあります)

$ wget http://www.itl.nist.gov/iaui/vip/cs_links/EMNIST/matlab.zip
$ apt install unzip
$ unzip matlab.zip
# 必要なライブラリをインストールします
$ apt install -y python python-pip python3-pip imagemagick libsm6 libxext6
$ pip install pillow
$ pip3 install pillow opencv-python scipy
# 作業用のフォルダを作成して移動します
$ mkdir emnist && cd emnist
# 画像データとテキストデータを生成します
$ python3 generate_training_data.py ../matlab/emnist-byclass.mat


(1'-C) 文字座標ファイルと学習データファイルを生成 ~ (3) traineddata出力

# OCR-Dをダウンロードします

$ cd ../ && $ git clone --depth 1 https://github.com/OCR-D/ocrd-train.git
# eng_best.traineddataをOCR-D用にコピーします
$ mkdir -p ocrd-train/usr/share/tessdata/
$ cp $TESSDATA_PREFIX/eng_best.traineddata ocrd-train/usr/share/tessdata/

# 生成したMNISTデータをocrd-train/data/ground-truthディレクトリに移動します

$ mv emnist/*.tif ocrd-train/data/ground-truth/
$ mv emnist/*.txt ocrd-train/data/ground-truth/

オリジナルデータも同じocrd-train/data/ground-truthディレクトリに保存します。

この際、名前の重複がないようにしてください。

また、イタレーション回数は10,000回、学習レートは0.002となっているので、必要に応じて設定を修正します。

ocrd-train/Makefileの141行目 --max_iterations 10000 と138行目 --learning_rate 20e-4 を書き換えます。

# OCR-Dを実行します

$ cd ocrd-train
$ nohup time -f "Run time = %E\n" make training MODEL_NAME=tegaki >> train.log 2>&1 &
# 実行状況をモニタリングします
$ tail -f train.log

イタレーションが途中で終了してしまいました。設定した回数まで行わずに十分な精度に達したようです。

# traineddataをTESSDATAディレクトリへコピーします

$ cp data/tegaki.traineddata $TESSDATA_PREFIX/tegaki.traineddata


評価

テスト画像で学習結果を評価してみます。

$ tesseract -l tegaki '画像へのパス' stdout

0123456789
ABCDEFGHIJKLMNO
PQRSTUVWXYZ

バッチリ!...なんですが、テスト終了が早すぎたので どうも過学習しているようです。

一般的に通用するモデルを作るには、もっと学習データのバラエティーを増やさなきゃですね。

例えば、手書き風フォントを使ってみるのもいいかもしれません。

(1) Tesseract 4.1にLSTMを使って日本語を再学習させるの手順を参考に、手書き風フォントと数字とアルファベット大文字のテキストから学習データを生成

(2) 一緒に生成されるlstmfファイルリスト(イタレーション時に--train_listfileで指定するeng.training_files.txt)へocrd-train/data/all-lstmfのリストを追記

(3) LSTMによるチューニングを実行

引き続きいろいろと試してみようと思います。

もし何かいいアイディアがあれば、お気軽にコメントください!