4コマ漫画のコマを切り抜く

esuji5.hateblo.jp

上記の記事に触発され自分も書いてみました。

4コマを切り出す

srcフォルダ以下にある画像を再帰的に処理してdstフォルダ以下に切り抜き画像を出力するようにしてあります。

まず

OpenCVにfindContoursという輪郭を抽出するメソッドがあるのでそれでコマの輪郭を抽出することを目指します。画像サンプルは手持ちで上からとった悪条件のものを。

二値化
def apply_adaptive_threshold(image, radius=15, C=5):
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    return cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 2 * radius + 1, C)

輪郭を抽出する前にまず二値化する必要があります。グレーに変換した後、Adaptive Thresholdを行います。Adaptiveの場合、画像全体で一意の閾値を適用するのではなく周囲のピクセルに応じて閾値を上下させるので影があっても綺麗に処理してくれます。
条件が悪い場合はブロックサイズを大きくしたり等パラメータを調整する必要がありますが、スキャナできちんとスキャンしたものを対象にする場合はそこまで気を使う必要はないと思います。

輪郭抽出
def find_external_contours(thresh):
    _, contours, hierarchy = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    external_num = hierarchy.shape[1] if hierarchy is not None else 0
    return contours[0:external_num]

findContoursで輪郭を抽出します。今回は一番外部の輪郭(コマの内部の輪郭は必要ない)のみが必要なので2つめのパラメータにcv2.RETR_EXTERNALを渡します。するとhierarchyに一番外部の輪郭のみのリストが返ってくるので、個数を数えてその分だけの輪郭データだけを返すようにしてます。
OpenCV3系では戻り値が変わって最初に画像を返すようになったので、OpenCV2系の場合は注意が必要です。

コマの輪郭だけを得、その外接矩形を取得する
def extract_rects_from_controus(contours, min_perimeter, max_perimeter):
    frames = []
    for contour in contours:
        frame = cv2.minAreaRect(contour)
        center, size, angle = frame
        # 縦・横が逆になっている場合、90度回転させる
        if angle < -45:
            size = tuple(reversed(size))
            angle = angle + 90
        w, h = size
        perimeter = 2 * (w + h)
        if min_perimeter < perimeter < max_perimeter and abs(angle) < 3.0 and 0.1 <= min(w, h) / max(w, h) <= 1.0:
            frames.append((center, (w + 2, h + 2), angle))  # パディングを加える
    return frames

輪郭の外接矩形を取得し、その矩形の、周長、角度、縦横比でフィルタを掛け、コマの輪郭だけを抽出を試みてます。

コマをソートする
def cmp_frame(tolerance):
    def _cmp(lhs, rhs):
        return (lhs > rhs) - (lhs < rhs)

    def _cmp_frame(lhs, rhs):
        if lhs[0] == rhs[0]:
            return 0
        x1, y1 = lhs[0]
        x2, y2 = rhs[0]
        if abs(x1 - x2) < tolerance:
            return _cmp(y1, y2)
        else:
            return _cmp(x2, x1)

    return _cmp_frame

コマを外接矩形の中心位置に基づいて並び替えます。傾いている場合も考慮して、x方向は許容範囲内であれば同じx位置と見て、y方向のみで比較するようにしてます。

rects = sorted(rects, key=cmp_to_key(cmp_frame(tolerance)))

python3系ではsortedにcmpを渡せなくなったのでfunctools.cmp_to_keyを用いてます。python2系の場合は直接cmpに渡せば大丈夫かと思います。

コマの部分を切り抜く
def cut_frame(image, rect):
    center, size, angle = rect
    size = int(np.round(size[0])), int(np.round(size[1]))
    box = cv2.boxPoints(rect)
    M = cv2.getAffineTransform(np.float32(box[1:4]),  np.float32([[0, 0], [size[0], 0], [size[0], size[1]]]))
    return cv2.warpAffine(image, M, size)

アフィン変換で傾きを直しつつ切り抜いてます。

かわいい。