Tesseract v4に特定のフォントを学習させる ~ 画像のデノイジング処理を添えて ~
メディア研究開発センターの倉井です。
最近Tesseractを使ってOCRをしたいことがあったのですが、新たにフォントを学習させたい場合など、いくつか初見では難しい設定をする必要があったので、備忘録的に残したいと思います。
合わせて元の画像のノイズをopenCVを使って取り除き、綺麗にする方法も共有したいと思います。
なお、検証に当たって大いに参考にさせていただいたWebページのリンクを最後にまとめています。
Tesseractをインストールする
Tesseractは記事執筆時点(2022.05.31)で、v5.1.0まで公開されています。
5系ではモデルのfinetuneをしたい場合、文字の写っている画像とその文字のアノテーションデータが必要となり(筆者調べ)、その準備が大変です。
一方で4系では学習させたい文字列とフォントさえ準備すれば、学習用のデータを自動で生成してくれるので、追加学習が容易です。
このため今回は最新版ではないものの、4系を使って学習を行なっていきます。
今回はUbuntu 18.xx環境におけるインストールを想定しています。
こちらにあるように、別環境であってもビルド可能のようです。
まずはTesseractをインストールする所から説明していきます。
以下のコマンドを実行し、Tesseractをインストールします。
https以下がコメントアウトされてしまっていますが、noteの仕様でそうなってしまっているだけなので、コメントと見なさずに実行して下さい。
# Tesseract本体や学習用のツールをインストール
apt-get install -y tesseract-ocr
apt-get install -y libtesseract-dev
# Tesseractの学習用スクリプトや設定ファイルをクローン
cd /home # ご自身の都合の良いディレクトリを指定してください
git clone -b 4.1 --depth 1 https://github.com/tesseract-ocr/tesseract.git
cd tesseract
git clone --depth 1 https://github.com/tesseract-ocr/langdata.git
# 公式が用意している日本語モデルのうち最も精度の良いものをダウンロード
cd /usr/share/tesseract-ocr/4.00/tessdata/
wget https://github.com/tesseract-ocr/tessdata_best/raw/main/jpn.traineddata
wget https://github.com/tesseract-ocr/tessdata_best/raw/main/jpn_vert.traineddata
tesseract --list-langs
# 出力にjpnとjpn_vertが表示されていれば日本語の横書き、縦書きがOCR可能になっています
ここまでくれば、日本語画像のOCRが可能になっているはずです。
試しに画像を用意してOCRにかけてみましょう。
OCR自体は以下のコマンドで実行可能です。
tesseract hogehoge.png stdout -l jpn # 縦書きであればjpn_vertを指定してください
標準出力にOCR結果が表示されているはずです。
学習させたいフォントの設定を整える
次に学習させたいフォントを準備する必要があります。
今回は`AsaSM-Book.ttf`というttfファイルがあると想定します。
もし手元にない配布されているフォントを学習させたい場合は、`wget`などを使って手元に用意してください。
# まずはttfファイルを適切な場所に配置します
mkdir /usr/share/fonts/truetype/asahi
mv AsaAM-Book.ttf /usr/share/fonts/truetype/asahi
# フォントのキャッシュを更新します
fc-cache -fv
fc-list | grep AsaAM # ここを用意したフォント名に置き換えてください
text2image --list_available_fonts --fonts_dir /usr/share/fonts
最後のコマンド2つの実行結果として、用意したフォント名がそれぞれ出力されればOKです。
これでシステムのTesseract両方からフォントが認識できていることが確認できました。
次にTesseractのフォント周りの設定を整えていきます。
cd /usr/share/tesseract-ocr/
# バックアップを念のため準備
cp language-specific.sh language-specific.sh.bk
# language-specific.shを編集
vi language-specific.sh
340行あたりにある`JPN_FONTS`と850行あたりの`VERTICAL_FONTS`を編集します。
今回はAsaSMだけを使えれば良いので、それぞれこのようになりました。
JPN_FONTS=( \
"AsaSM" \
)
VERTICAL_FONTS=( \
"AsaSM" \ # for jpn
"AR PL UKai Patched" \ # for chi_tra
"AR PL UMing Patched Light" \ # for chi_tra
"Baekmuk Batang Patched" \ # for kor
)
もし追加したいフォント以外に通常のフォントも対応されるようにしたい場合は、それらも上記のフローで登録することをお勧めします。
学習データの生成の際に、未登録フォントは指定できないので注意が必要です。
この後設定の変更がうまく行ったか検証するために、次のコマンドを実行します。
最後に`Completed ~~`と表示されればここまでの作業に問題はありません。
また`--langdata_dir`と`--tessdata_dir`は各自の環境に合わせてください。
bash tesstrain.sh --overwrite --lang jpn --langdata_dir /home/tesseract/langdata --tessdata_dir /usr/share/tesseract-ocr/4.00/tessdata/
次に、今回登録したフォントのフォント属性をプロパティファイルに定義します。
`/home/tesseract/langdata/font_properties` に、追加したファイルのプロパティを記述します(下記を参考)。
AsaSM 0 0 0 0 0
それぞれの数字は
<italic> <bold> <fixed> <serif> <fraktur>
を示しており、それぞれのプロパティの対象フォントである場合は1を、そうでない場合は0を設定します。
今回のフォントは全て該当しないので、0とします。
ここまででフォントの設定は完了です。
学習データの準備&学習を回す
次に用意したフォントによる学習データの準備に入ります。
まず学習用の文章データを準備します。
文章データは単語ごとに半角スペースで区切られ、また適度な長さで改行されていることが想定されています。
具体的には下記のようなフォーマットになります(一部)。
筆者は朝日新聞社の過去記事群から、上のようなデータを生成しました。
今回の記事では、単語ごとにスペースを挿入し適当な長さで改行する処理方法の解説はしませんが、もし反響や要望が多ければ共有するかもしれません。
こちらの記事では疑似的な文章データを生成していらっしゃいますので、手軽に試してみたい方は参考にしてみてください。
ということで、`sentence.txt`という学習用文章が準備できたとしましょう。
そのファイルは`/home/tesseract/train`というディレクトリに置いたとします。
早速、次のコマンドで学習データを生成します。
bash /usr/share/tesseract-ocr/tesstrain.sh --fonts_dir /usr/share/fonts --lang jpn --linedata_only --noextract_font_properties --langdata_dir /home/tesseract/langdata/ --tessdata_dir /usr/share/tesseract-ocr/4.00/tessdata/ --output_dir /home/tesseract/train --training_text /home/tesseract/train/sentence.txt --fontlist "AsaSM"
処理完了後、`/home/tesseract/train`に幾つかのファイルが配置されていることが確認できると思います。
それらを使って早速学習に入りたいところですが、今回は既存のモデルにfinetuneを行うので、モデルからチェックポイントを生成する必要があります。
今回は縦書き用の学習を行おうと思うので、下のコマンドを実行します。
combine_tessdata -e /usr/share/tesseract-ocr/4.00/tessdata/jpn_vert.traineddata /home/tesseract/train/jpn_vert_best.lstm
実行後`/home/tesseract/train/jpn_vert_best.lstm`が生成されているはずです。
これにfinetuneを行なっていきます。
具体的には以下のコマンドを実行します。
nohup lstmtraining --model_output /home/tesseract/train/ -continue_from /home/tesseract/train/jpn_vert_best.lstm --old_traineddata /usr/share/tesseract-ocr/4.00/tessdata/jpn_vert.traineddata --traineddata /home/tesseract/train/jpn/jpn.traineddata --train_listfile /home/tesseract/train/jpn.training_files.txt --max_iterations 1000 >> /home/tesseract/train/train.log 2>&1 &
するとバックグラウンドで学習が開始されます。
`tail -f /home/tesseract/train/train.log`で学習状況を確認することができます。
今回は検証のために1000回しか回しませんでしたが、最低でも1万、できれば5~10万イテレーション回したいところです。
学習が終わると`/home/tesseract/train/1000_checkpoint` が出力されていることがわかります。
学習結果を検証してみる
学習が終わったら`checkpoint`データをモデルの形に変換する必要があります。
今回の例では、次のコマンドでモデルへの変換が可能です。
lstmtraining --stop_training --continue_from /home/tesseract/train/1000_checkpoint --traineddata /home/tesseract/train/jpn/jpn.traineddata --model_output /usr/share/tesseract-ocr/4.00/tessdata/jpn_vert_train_1000.traineddata
うまく行っていれば`tesseract --list-langs`を実行するとjpn_vert_train_1000が表示されるはずです。
ここまできたら、jpn_vert_train_1000モデルで実際にOCRを実行してみましょう。
tesseract hogehoge.png stdout -l jpn_vert_train_1000
精度が良くなっているか、はたまた悪くなってしまっているか確認してみてください!
Tesseractの日本語OCR精度を向上させるためのテクニック
Tesseractではさまざまなパラメータを指定することが可能で、それによって認識精度の向上が見込める場合があります。
全てを検証し切れてはいませんが、効果が実際にあったもの、ありそうだなと思っているものを紹介したいと思います。
tesseractコマンドに渡すことのできるパラメータ(特にpsm)
下のリンクで紹介されているように、tesseractコマンドではさまざまなパラメータを指定して実行することが可能です。
その中でも`--psm`をいじることで認識結果が変わることがそれなりにあります。
psmは`page segmentation mode`の略で、文章のレイアウトを解析する時の処理の挙動を指定することができます。
具体的なオプションについては上のリンクを参照ください。
筆者は縦書き画像を対象にすることが多かったため、`--psm 5`を指定すると結果が変化する(良くなることもあれば悪くなることも)場合がありました。
また1行の文字列に対しては`--psm 7`も有効なようです。
画像処理によるノイズ除去
本家の精度向上Tipsにもあるように画像のノイズ除去は精度向上のための非常に有用な前処理です。
ちなみにここには画像処理以外にも、上で紹介したpsm含めいくつかのアイデアがあるので、確認してみてください。
画像処理に関しては、筆者の試した中で精度が向上した処理があるので、共有したいと思います。
処理にはpythonを使っています。
またpythonRLSAというモジュールを使うのでpip等でインストールしてください。
あくまで検証時にgoogle colabで試した走り書きなので、整頓されていない&colab特有の処理がありますが、ご容赦ください。
import cv2
import json
import numpy as np
from pythonRLSA import rlsa
from matplotlib import pyplot as plt
# カーネルの定義
kernel = np.ones((2, 1), np.uint8)
itr = 1
# 画像のパス
tmp_file_name = '/content/drive/MyDrive/hogehoge.png'
img1 = cv2.imread(tmp_file_name, cv2.IMREAD_GRAYSCALE)
# ノイズに対する処理ここから
dst = cv2.fastNlMeansDenoising(img1, h=50)
erode = cv2.erode(dst, kernel, iterations=itr)
output = cv2.copyMakeBorder(erode, 50, 50, 50, 50, cv2.BORDER_CONSTANT, value=[255,255,255])
blur = cv2.GaussianBlur(output,(3,3),0)
# ノイズに対する処理ここまで
# ここからは複数行の文字列を縦一列(or 横一列)に並べる処理
(thresh, image_binary) = cv2.threshold(blur, 150, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)
copy = image_binary.copy()
image_rlsa_vertical = rlsa.rlsa(image_binary, False, True, 30)
image_rlsa_horizontal = rlsa.rlsa(copy, True, False, 30)
cnts_vertical = cv2.findContours(image_rlsa_vertical, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
cnts_vertical = cnts_vertical[0] if len(cnts_vertical) == 2 else cnts_vertical[1]
cnts_horizontal = cv2.findContours(image_rlsa_horizontal, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
cnts_horizontal = cnts_horizontal[0] if len(cnts_horizontal) == 2 else cnts_horizontal[1]
test = []
if (len(cnts_vertical) < len(cnts_horizontal)):
image_rlsa = rlsa.rlsa(image_binary, False, True, [0, 100])
cnts_vertical = cv2.findContours(image_rlsa, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts_vertical[0] if len(cnts_vertical) == 2 else cnts_vertical[1]
for c in cnts:
x,y,w,h = cv2.boundingRect(c)
if (not(x == 0 & y == 0) and w > 25 and h > 20):
test.append([x-5,y-1,35,h+1])
test.sort(reverse=True, key=lambda x:x[0])
else:
image_rlsa = rlsa.rlsa(copy, True, False, [100, 0])
cnts_horizontal = cv2.findContours(image_rlsa, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts_horizontal[0] if len(cnts_horizontal) == 2 else cnts_horizontal[1]
for c in cnts:
x,y,w,h = cv2.boundingRect(c)
if (not(x == 0 & y == 0) and w > 25 and h > 20):
test.append([x-1, y-5, w+1, 35])
test.sort(reverse=False, key=lambda x:x[1])
new_image = []
count = 0
for a in test:
if(count == 0):
new_image = blur[a[1] : a[1]+a[3], a[0] : a[0]+a[2]]
else:
if (len(cnts_vertical) < len(cnts_horizontal)):
new_image = np.concatenate((new_image, blur[a[1] : a[1]+a[3], a[0] : a[0]+a[2]]), axis = 0)
else:
new_image = np.concatenate((new_image, blur[a[1] : a[1]+a[3], a[0] : a[0]+a[2]]), axis = 1)
count += 1
# ここまでが複数行の文字列を縦一列(or 横一列)に並べる処理
# 文字列の周りに余白を追加(精度向上のため)
output = cv2.copyMakeBorder(new_image, 50, 50, 50, 50, cv2.BORDER_CONSTANT, value=[255,255,255])
cv2.imwrite("/content/drive/MyDrive/test/output.jpg", output)
configファイルの設定
Tesseractではコマンドライン引数以外にもたくさんのパラメータを指定することができます。
特に日本語のOCRを行う上で効果的なパラメータが下に纏まっているので、参考にしてみてください。
また、configファイルの場所やそのほか参考になりそうな情報がこちらに纏まっておりますので、本気で精度向上に取り組む場合には、ぜひ確認してみてください。
最後に
自分が試したことは以上になります。
具体的なデータの共有はできませんが、GoogleのOCR(有料)と精度を比較してみると大きく溝を開けられてしまっています。
また最近NDLからLINEのOCRを元にしたモデルが公開されました。
公開されたままのモデルでも、Tesseractで学習を行なったモデルより良い精度を叩き出していました。
画像とそのアノテーションデータを用意すればfinetuneが可能なので、Tesseract5系と併せてチューニングしてみようかなと思っています。
どちらも一定の条件に従えば商用利用可能なので、お金をかけずにOCRがしたい場合は有力な選択肢になると思います。
今回の記事はここまでとなります。
Tesseractで日本語OCRを試そうと思っている方の助けになれたなら幸いです。
(メディア研究開発センター・倉井敬史)