Python+OpenCVで文書画像からテキストラインの自動検出
はじめに
tesseractでOCRする際、文書画像をそのまま入力するより、行単位で入力した方が読み取り精度が高い事を確認しました。
nsr-9.hatenablog.jp
前回の実験では手動で行を切り出していたので、今回はそれを自動で行えるようにします。
Haar-Like(Like)を用いた行の検出
テキスト行の検出は、白線検出で使ったHaar-Like(Like)特徴量を用います。
nsr-9.hatenablog.jp
上下で白と黒に別れたマスクパターンを文書画像の縦方向に走査することで、行とブランクを検出することができます。
import numpy as np import sys import cv2 def calc_haarlike(crop_img, rect_h): pattern_h = rect_h // 2 height = crop_img.shape[0] out = np.zeros([height-rect_h]) for index in range(height-rect_h): a1 = np.mean(crop_img[index: index+pattern_h, :]) a2 = np.mean(crop_img[index+pattern_h: index+rect_h,:]) out[index] = a1-a2 return out
以下に入力画像に対するHaar-Like(Like)の応答値を示します。
入力画像 | Haar-Like(Like) |
---|---|
応答値は横軸が画像のy座標値に相当するので、右に90°回転させて入力画像に重ねてみました。
いい感じに行とブランクを検出できそうですね。
応答値が正の値かつpeakの位置が行の始まりで、負の値かつpeakの位置が行の終わりになっているように思えます。
信号のpeakはscipy.signalにあるargrelmax関数を用いる事で簡単に求める事ができます。
from scipy.signal import argrelmax def peak_detection(data, th): peak1 = argrelmax(data)[0] # 行の始まりの検出 peak2 = argrelmax(-data)[0] # 行の終わりの検出 peak1 = peak1[np.where(data[peak1] > th)] # 一定以下の応答値のピークを除去する peak2 = peak2[np.where(np.abs(data[peak2]) > th)] # 一定以下の応答値のピークを除去する return peak1, peak2
以下に実行例を示します。
入力画像 | 行検出結果 |
---|---|
行の始まりを青線、行の終わりを緑線で可視化しました。
うまく動いてそうに見えますね。
Tesseractの認識精度の改善効果確認
文章一括読み取り、手動による行単位読み取り、自動行単位読み取り(今回)の比較を行いました。
比較方法は前回と同じでレーベンシュタイン距離を用いました。
文書単位の読み取り | 行単位の読み取り(手動) | 行単位の読み取り(自動) |
---|---|---|
38 | 10 | 15 |
手動切り出しに届いていませんが、文書単位での読み取りに比べて大幅に性能を改善する事ができました。
自動的に行を切り出すことができるので、お手軽に認識精度を向上させることができます。
また、以下にそれぞれの認識結果を添付します。
正解文章
文書単位の読み取り結果
行単位の読み取り結果(手動)
行単位の読み取り結果(自動)
自動の切り出しの結果を観察すると、各行の冒頭に読み取りミスが頻発しています。
これは、書籍の綴じ目の部分の影を文字と誤認識しているのだと思います。
文字の開始位置も自動で検出することができればさらなる認識精度の改善が狙える可能性がありますね。
まとめ
Haar-Like(Like)特徴量を用いて、文書画像から自動的に行の検出が可能になりました。
また、行検出を行うことで、Tesseractの認識精度を向上できることを確認できました。
最後にコード全体を記載します。
import numpy as np import sys import cv2 from scipy.signal import argrelmax import pyocr from PIL import Image from PIL import ImageDraw from PIL import ImageFont tools = pyocr.get_available_tools() tool = tools[0] def pred_img(cv_img, layout): #img = Image.open(path) gray = cv2.cvtColor(cv_img, cv2.COLOR_BGR2GRAY) gray = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2) gray = Image.fromarray(gray) img = Image.fromarray(cv_img) builder = pyocr.builders.TextBuilder(tesseract_layout=layout) text = tool.image_to_string(img, lang="jpn", builder=builder) return text def calc_haarlike(crop_img, rect_h): pattern_h = rect_h // 2 height = crop_img.shape[0] out = np.zeros([height-rect_h]) for index in range(height-rect_h): a1 = np.mean(crop_img[index: index+pattern_h, :]) a2 = np.mean(crop_img[index+pattern_h: index+rect_h,:]) out[index] = a1-a2 return out def peak_detection(data, th): peak1 = argrelmax(data)[0] # 行の始まりの検出 peak2 = argrelmax(-data)[0] # 行の終わりの検出 peak1 = peak1[np.where(data[peak1] > th)] # 一定以下の応答値のピークを除去する peak2 = peak2[np.where(np.abs(data[peak2]) > th)] # 一定以下の応答値のピークを除去する return peak1, peak2 def line_detection(peak1, peak2, rect_h, pad=5): lines = list() for p in peak1: bottom = np.min(peak2[peak2 > p]) +rect_h//2 + pad top = p + rect_h//2 - pad lines.append([p, bottom]) return lines if __name__ == "__main__": rect_h = 20 pad = 5 img = cv2.imread(sys.argv[1]) out = calc_haarlike(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY), rect_h) peak1, peak2 = peak_detection(out, 15) lines = line_detection(peak1, peak2, rect_h) for l in lines: out = img[l[0]: l[1]] text = pred_img(out, 7) print(text) cv2.imshow("", out) cv2.waitKey(0)