Mihiraki_split

Posted on

背景

物理本を裁断器で裁断し、スキャナで取り込むことを自炊と呼ぶが、場合によっては裁断したくない物理本なども存在する。このとき、スキャナに本を押し当てて見開きの画像を得るのが一般的である。

動機

しかし、見開き画像のままでは、OCR でテキストを自動認識させる際に、あるページの次ページの文章もひとつづきの文章であると認識されてしまい、都合が悪い。

目的

そこで、見開き画像を単ページに自動的に分割しようと考えた。

手法

具体的には、ページの境目が浮いている(= 境目は黒くスキャンされる) ことを利用し、機械的に境目を判定する。

作業ログ

import os
import cv2
import matplotlib.pyplot as plt
%matplotlib inline 
import matplotlib
import numpy as np
import shutil

mihiraki_split()

この関数に画像ファイルを与えると、分割された画像のリストが返ってくる。

大まかな働き

  1. まず与えられた画像ファイルをグレースケールで2値化する。この際、thresh 以下の値は 0、それ以外は 255 (?) に変換される。
  2. 次に与えられた画像における、境目を探索する。アイデアとしては、x を固定したときの、y における 黒いピクセルを合計していきその合計値が最も高いところが境目であると仮定する。ただし、そのままでは本の外側の領域で境目判定が行われてしまう可能性があるため、画像の真ん中周辺のみを探索するようにした。また、引数 inverse は、上下の見開きの画像の場合にも対応するためにあとから追加した。
  3. 境目 x が分かれば、あとは元の画像のうち、0<=x と x<width を取得すればよい (もちろん y はどちらも全取得する)。inverse が True の場合は、x と y が逆になる。
def mihiraki_split(img, thresh=130, around_px=300, inverse=False):
    img_binary = get_grayscale(img, thresh)
    h, w = img.shape[:2]
    if not inverse:
        y0 = h
        x0 = find_separate_x(img_binary, around_px, inverse)
        c = [img[:y0, x0*x:x0*(x+1)] for x in range(2)]
    else:
        y0 = find_separate_x(img_binary, around_px, inverse)
        x0 = w
        c = [img[y0*y:y0*(y+1), :x0] for y in range(2)]
    return c


def get_grayscale(img, thresh=130):
    img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    _, img_binary = cv2.threshold(img_gray, thresh, 255, cv2.THRESH_BINARY)

    img_binary = cv2.bitwise_not(img_binary)
    return img_binary


def find_separate_x(img_binary, around_px=300, inverse=False):
    """
    inverse: True のとき、x軸方向で分割 (1 ページに縦に2枚スキャンされている場合に使用)
    """
    h,w = img_binary.shape
    if inverse:
        h,w = w,h
    x_mid = w // 2
    y_mid = h // 2
    
    x_black_sum = {}  # x を固定したときの、y軸の black の値の合計値
    for y in range(h):
        for i in range(-around_px, around_px):
            if x_mid+i not in x_black_sum:
                x_black_sum[x_mid+i] = 0
            if not inverse:
                x_black_sum[x_mid+i] += 1 if 0 < img_binary[y][x_mid+i] else 0
            else:
                x_black_sum[x_mid+i] += 1 if 0 < img_binary[x_mid+i][y] else 0
    max_black_x = max(x_black_sum, key=x_black_sum.get) if not inverse else min(
        x_black_sum, key=x_black_sum.get)
    return max_black_x

example

img = cv2.imread("./scanned_book.png")
c = mihiraki_split(img)
for i, splitted in enumerate(c, start=1):
    cv2.imwrite(f"separeted_book_{i}.png", splitted)

result

scanned_book.png

separeted_book_1.png

separeted_book_2.png

まとめ

見開きスキャンした本を、その境目を自動的に判断し、単ページにすることができた。