少し前置きが長いので必要な方は下記リンクで飛んでください
背景
先に公開しているゲーム操作自動化プログラムに付随する画像のようなツールを作っていました。
このツールでは画像を元にGUI操作をさせるものです。
※ロックを自動で解除する事が目的のツールです。
この画像内の表部分をTesseractを利用して取得し、取得した文字については一切の加工をしていない状態です。
Tesseractを扱ったことがない方はこれぐらいこのように読めて当然。
と思う方もいらっしゃると思いますが、画像をそのまま読ませるだけでは期待する精度が出ないこともままあります。
以前同様のプログラムを作成した事が有り、その際は期待から外れるパターンをテストで見つけて正規表現で置換を繰り返すという方法でアプローチしました。
%の記号が特に鬼門で、96と認識されたりすることが多かったのですが、前後の表現と組み合わせると正規表現での処理は困難で最終的に実用レベルには至りませんでした。
その際に読み取り精度向上のためにできそうなことは考えてはいたのですが、本記事はその実践内容を紹介します。
正直、綺麗なやり方か?と言われるとそうではなく、高度なことでもなく当たり前だと思われる方もいらっしゃることと思います。
しかし、検索する限りでは自分のケースに対応できるような手法が紹介されている記事をうまく見つけられませんでした。
ですのでこれが誰かの役に立てば幸いです。
目次
対象・前提条件
要点
読み取らせる範囲を絞り、1行単位に分割する
- LineBoxBuilderを利用して行を取得
誤認識1されやすい箇所をマスクする
- テンプレートマッチングで座標を取得
画像は二値化する
サンプルで使用する画像
読み取らせる範囲を絞り、1行単位に分割する。
これはやらなくても良い結果が得られるなら不要です。
どちらかと言うと全体的な処理の都合でそうしたところがあります。2
LineBoxBuilder
を利用して行を識別
import pyocr import cv2 from PIL import Image tools = pyocr.get_available_tools() tool = tools[0] # 文字と認識された座標の取得 res = tool.image_to_string ( Image.open(target), lang = 'jpn', builder = pyocr.builders.LineBoxBuilder(tesseract_layout=6) ) # 描画とプレビューをする場合は以下 out = cv2.imread(Image_to_write_rectangle) for d in res: cv2.rectangle(out, d.position[0], d.position[1], (0, 0, 255), 1) ) cv2.imshow('Rectangle Drew', out) cv2.waitKey(0) cv2.destroyAllWindows()
結果
※事前に白黒で取得した座標を元にカラー画像上に描画しています。
※コード中の対象画像はこちらの画像です。
検出した座標で画像を切り抜いて保存
# LineBoxBuilderで検出された座標を取得 # ( (左上頂点x ,y), (右下頂点 x, y) ) で格納されます。 coords = res[0].position # 切り取る範囲を設定します。 # 検出結果によっては文字を削ってしまうので必要に応じて margin を与えて画像サイズを調整します。 margin = 3 box = ( coords[0][0] - margin, coords[0][1] - margin, coords[1][0] + margin, coords[1][1] + margin, ) # 画像を保存します。 # baseimage から box で指定された領域を切り取ります。 baseimage = Image.open(baseimage_path) baseimage.crop(box).save(filepath, quality=100)
誤認識されやすい箇所をマスクする
OpenCVのテンプレートマッチングを利用し、マスクしたい座標を取得後塗りつぶします。
import cv2 # テンプレートを検出したい画像とテンプレートを読み込む origin = cv2.imread(origin_image_path) template = cv2.imread(template_image_path) # cv2.minMaxLocを利用して、類似率の高い座標を取得する。 retvals = cv2.minMaxLoc( cv2.matchTemplate(origin, template, cv2.TM_CCOEFF_NORMED) ) # origin から塗りつぶしたい領域(area)を設定する。 area = ( retvals[3][0], retvals[3][1], retvals[3][0] + template.shape[1], retvals[3][1] + template.shape[0] ) # origin から area を塗りつぶす cv2.rectangle( origin, pt1=( area[0], area[1] ), pt2=( area[2], area[3] ), thickness= -1, color=(0, 0, 0) ) # プレビューする場合は以下 cv2.imshow('Filled', origin) cv2.waitKey(0) cv2.destroyAllWindows() # 類似率に基づいてTrue / Falseを判別する。 # 最も高い類似率が0.9を超えればTrueそうでない時はFalse if retvals[1] > 0.9: print('True') else: print('False')
結果
,
使用画像(テンプレートを検出したい画像)
,
使用画像(テンプレート(一部)3)
,
マスクするだけだと読み取る値も空白などになってしまうので、マッチング時に類似率が一定以上ならばTrueとして値を格納、出力の際はTrueかどうかを見て値を付与するようにしました。
今回の例では%画像での類似率が高い時は、出力時に%を付与するような流れです。
なお、テンプレートは検出したい画像よりもサイズが小さい必要があります。
そのため、テンプレートが大きい場合は可能な限り前の工程でマスクすることも検討してください。
類似率についてはcv2.minMaxLocの[1]の値が最も高い類似率を示します。 www.tutorialexample.com
画像は二値化する
よくある方法ですが、画像は二値化することで認識率が向上します。
二値化すること、というよりは、二値化することで曖昧な境界を調整するというイメージ。
今回はOpenCVの機能でやりましたが、numpyを使って多次元配列を直接いじる方法もあります。
import cv2 #画像の各ピクセルを見て binaryThreshold の値を元に type の基準で二値化する binaryThreshold = 190 target = cv2.imread(target_image_path, 0) ret, binaried = cv2.threshold(target, binaryThreshold, 255, cv2.THRESH_BINARY_INV)
結果
元
後
もっと削ってもいいぐらいかなと個人的には思います。
cv2.imshow()では拡大するとピクセルの値が見れるので大雑把なあたりを付けて微調整しました。
builders.TextBuilder(tesseract_layout)の値
3つの事。と言いつつ4つ目なのと根拠はオプションでそう言われている…というレベルです。
今回は1つの画像に1行という前提のために7でやりました。
元々の操作自動化アプリについて starsand.hateblo.jp