OpenCVを使って問題集をトリミングと結合してAnkiに取り込む

2021年08月09日 00時08分

目次

Ankiとは

単語カードツール。 忘れそうなタイミングで問題を出してくれて、徐々に記憶を長期記憶に持っていくツール。

Ankiとは、分散学習(Spaced Repetition)ができるフラッシュカードです。 このような原則に基づくと効果的な結果が得られるという学習理論に基づいて作られています。 初めてのAnki

カードはHTML形式で記載でき、問(表面)と解答(裏面)をCSV形式でインポートできる。

なので取り込んだデータをいい感じに分割できれば、サクッと暗記カードを作ることができる。

暗記カード形式になってないものも、問と解答をいい感じに分割して取り込むことができれば、このシステムに全部投げることができる。

このやり方で単語を覚えるとめっちゃ覚えれた成功体験があるので執着しているところもある。

画像取り込み

画像は見開きで取り込むのに、iOSアプリのFineReaderを使った。

取り込み後一旦アップロードして補正をかけてくれる点、本モードにすると見開きで取り込んだ後に各ページで分割を行ってくれる。

グレースケールでも保存できる。

画像分割

画像認識のライブラリOpenCVを使用した。

少し古いが日本語のチュートリアルを見て使える程度に抑えた。

問題集の問題、解答に連番が振ってあるので"-No"の形式と一致するテンプレートマッチングと問題集の1問ごとに直線が引いてあるのでそこを検出する直線検出のやり方で実施してみる。

結果的に画像が綺麗に取り込めているものについてはテンプレートマッチングで十分に分割ができる。

複数テンプレートを用意して配列に詰めていけばもうちょっと傾いたり画像が乱れても大丈夫かも。

テンプレートマッチ

テンプレートイメージ

テンプレートイメージ

取り込む際にy軸が1ずれて2回検出することがあるのでy軸から一定距離離れていないとトリミングしないことにした。

テンプレートマッチ後

テンプレートマッチ後イメージ

テンプレートマッチする座標が取れるのでそこから上で画像をカットして保存することにした。

問題や解答が次ページに跨っているページについてはサイズを合わせて結合する必要がある。(後述)

templateMatch.py
import cv2
import numpy as np
import glob

image_no = 0
def main():
    files = glob.glob("./images/*.JPG")
    for f in files:
        divide_image(f)


def divide_image(file):
    global image_no
    # 読み込み
    img = cv2.imread(file, 0)
    template = cv2.imread('./template.jpg', 0)
    ymax, xmax = img.shape
    # 検出
    res = cv2.matchTemplate(img, template, cv2.TM_CCOEFF_NORMED)
    # 適合確認
    threshold = 0.6
    loc = np.where(res >= threshold)
    count = 0
    x_top, y_top, = [0, 0]
    # 各マッチした座標に対して処理
    for pt in zip(*loc[::-1]):
        if 'y_previous' not in locals():
            y_previous = 0
        x1, y1 = pt
        # 前のy座標と間隔が100以下なら分割しない
        if y1 - y_previous > 100:
            img1 = img[y_top: y1, 0: xmax]
            cv2.imwrite("./divided/answer" + str(image_no).zfill(4) + ".jpg", img1)
            x_top, y_top, = [0, y1]
            image_no = image_no + 1
        y_previous = y1
    img1 = img[y_top: ymax, 0: xmax]
    cv2.imwrite("./divided/answer" + str(image_no).zfill(4) + ".jpg", img1)
    image_no += 1


if __name__ == '__main__':
    main()

直線検出

直線検出は試してみたが、ページが歪んだ際にはうまく検出ができなかったり、ある程度の直線が無いと切り取りたいところ以外で切り取りが発生して結合が大変だった。

テンプレートマッチ同様に問題や解答が次ページに跨っているページについてはサイズを合わせて結合する必要がある。

直接検出後イメージ

findLine.py
import cv2
import numpy as np
import glob

image_no = 0
conf = {"min_line_length": 5500, "max_line_gap": 300, "y_previous": 270, "allCut": "true"}


def main():
    global conf
    files = glob.glob("./images/*.JPG")
    for f in files:
        divide_image(f, conf)


def divide_image(file, conf):
    global image_no
    img = cv2.imread(file)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    gray2 = cv2.bitwise_not(gray)
    min_line_length = conf.get("min_line_length")
    max_line_gap = conf.get("max_line_gap") 
    y_previous_limit = conf.get("y_previous") 
    # 開始地点を設定
    ymax, xmax, layer = img.shape
    # 直線検出条件
    lines = cv2.HoughLinesP(gray2, 1, np.pi / 360, 80, min_line_length, max_line_gap)
    # y座標順ソート
    lines = sorted(lines, key=lambda x: x[0][1])
    x_top, y_top, = [0, 0]
    for line in lines:
        # 前のy座標と一定間隔空いてないと分割しない
        if 'y_previous' not in locals():
            # ページを跨って記述が続いている場合、
            # ページの最初の検出でもトリミングする必要がある
            if conf.get("allCut") == "false":
                y_previous = line[0][1]
            else:
                y_previous = 0
        x1, y1, x2, y2 = line[0]
        if y1 - y_previous > y_previous_limit:
            img1 = img[y_top: y1, 0: xmax]
            cv2.imwrite("./divided/answer" + str(image_no).zfill(4) + ".jpg", img1)
            x_top, y_top, = [0, y1]
            image_no = image_no + 1
        y_previous = y1
    img1 = img[y_top: ymax, 0: xmax]
    cv2.imwrite("./divided/answer" + str(image_no).zfill(4) + ".jpg", img1)
    image_no += 1


if __name__ == '__main__':
    main()

画像結合

次ページに問題や解答が跨った問題について、テンプレートマッチを利用して次のテンプレートマッチが発生するまでは画像を結合させ続ける処理にした。

画像結合時、縦に結合する場合には横幅を揃える必要があり、その際のリサイズ処理は下記から引用させていただきました。Python, OpenCVで画像を縦・横に連結

combine.py
import cv2
import numpy as np
import glob

image_no = 0
image_list = []


def main():
    files = glob.glob("./divided/*.JPG")
    for f in files:
        combine_img(f)
    if image_list:
        combined_img = v_concat_resize_min(image_list)
        cv2.imwrite("./combined/answer" + str(image_no).zfill(4) + ".jpg", combined_img)

def combine_img(file):
    global image_no
    global image_list
    img = cv2.imread(file, 0)
    template = cv2.imread('./template.jpg', 0)
    # print(img.shape)
    res = cv2.matchTemplate(img, template, cv2.TM_CCOEFF_NORMED)
    # テンプレートマッチ条件 分割より少し小さくても良い
    threshold = 0.5
    loc = np.where(res >= threshold)
    if not any(loc[0]):
        image_list.append(img)
    else:
        if image_list:
            combined_img = v_concat_resize_min(image_list) # 横幅揃えて結合する
            cv2.imwrite("./combined/answer" + str(image_no).zfill(4) + ".jpg", combined_img)
            # 配列追加して終わり
        image_list = [img]
        image_no += 1


def v_concat_resize_min(im_list, interpolation=cv2.INTER_CUBIC):
# [横幅リサイズ](https://note.nkmk.me/python-opencv-hconcat-vconcat-np-tile/)

if __name__ == '__main__':
    main()

カスケード分類器の作成について

今回は画像を綺麗に取り込むことや最後は人の手を使うことでOpenCVの標準機能で対応することができた。

1-2問は画像の取り込みが甘く、分割や結合できないものがあったが、人の手が一番早かったのでそれで…

問題数が500問超えるようなら画像も乱れる可能性がちょっとずつ増えていくと思うのでこれらの記事を読んで分類器を作成し、楽するとよさそう。(どちらにしても画像を綺麗に取り込むこと大切!