社会人研究者が色々頑張るブログ

pythonで画像処理やパターン認識をやっていきます

Python+OpenCVで文書画像からテキストラインの自動検出

はじめに

tesseractでOCRする際、文書画像をそのまま入力するより、行単位で入力した方が読み取り精度が高い事を確認しました。
nsr-9.hatenablog.jp

前回の実験では手動で行を切り出していたので、今回はそれを自動で行えるようにします。

Haar-Like(Like)を用いた行の検出

テキスト行の検出は、白線検出で使ったHaar-Like(Like)特徴量を用います。
nsr-9.hatenablog.jp

上下で白と黒に別れたマスクパターンを文書画像の縦方向に走査することで、行とブランクを検出することができます。
f:id:nsr_9:20210817044643p:plain

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)
f:id:nsr_9:20210817045514j:plain f:id:nsr_9:20210817045529p:plain

応答値は横軸が画像のy座標値に相当するので、右に90°回転させて入力画像に重ねてみました。
f:id:nsr_9:20210817045643p:plain

いい感じに行とブランクを検出できそうですね。
応答値が正の値かつ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

以下に実行例を示します。

入力画像 行検出結果
f:id:nsr_9:20210817045514j:plain f:id:nsr_9:20210817052030p:plain

行の始まりを青線、行の終わりを緑線で可視化しました。
うまく動いてそうに見えますね。

Tesseractの認識精度の改善効果確認

文章一括読み取り、手動による行単位読み取り、自動行単位読み取り(今回)の比較を行いました。
比較方法は前回と同じでレーベンシュタイン距離を用いました。

文書単位の読み取り 行単位の読み取り(手動) 行単位の読み取り(自動)
38 10 15

手動切り出しに届いていませんが、文書単位での読み取りに比べて大幅に性能を改善する事ができました。
自動的に行を切り出すことができるので、お手軽に認識精度を向上させることができます。

また、以下にそれぞれの認識結果を添付します。

正解文章

f:id:nsr_9:20210815183924p:plain

文書単位の読み取り結果

f:id:nsr_9:20210815184027p:plain

行単位の読み取り結果(手動)

f:id:nsr_9:20210815183945p:plain

行単位の読み取り結果(自動)

f:id:nsr_9:20210817055737p:plain

自動の切り出しの結果を観察すると、各行の冒頭に読み取りミスが頻発しています。
これは、書籍の綴じ目の部分の影を文字と誤認識しているのだと思います。
文字の開始位置も自動で検出することができればさらなる認識精度の改善が狙える可能性がありますね。

まとめ

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)