python/OpenCVでレシートみたいな四角いものを見つけてトリミングする
pythonでOpenCV使ってレシートとかメモとかその他諸々の四角い領域を発見、そいつを切り抜く。そんな感じのプログラムを書きました。 書きましたって言ってもだいぶ前に書いたやつなのですが、リクエストがあったので公開します。
とりあえずソースコード。大雑把な解説は下に。
#!/usr/bin/python3 import cv2 import numpy def transform_by4(img, points): """ 4点を指定してトリミングする。 """ points = sorted(points, key=lambda x:x[1]) # yが小さいもの順に並び替え。 top = sorted(points[:2], key=lambda x:x[0]) # 前半二つは四角形の上。xで並び替えると左右も分かる。 bottom = sorted(points[2:], key=lambda x:x[0], reverse=True) # 後半二つは四角形の下。同じくxで並び替え。 points = numpy.array(top + bottom, dtype='float32') # 分離した二つを再結合。 width = max(numpy.sqrt(((points[0][0]-points[2][0])**2)*2), numpy.sqrt(((points[1][0]-points[3][0])**2)*2)) height = max(numpy.sqrt(((points[0][1]-points[2][1])**2)*2), numpy.sqrt(((points[1][1]-points[3][1])**2)*2)) dst = numpy.array([ numpy.array([0, 0]), numpy.array([width-1, 0]), numpy.array([width-1, height-1]), numpy.array([0, height-1]), ], numpy.float32) trans = cv2.getPerspectiveTransform(points, dst) # 変換前の座標と変換後の座標の対応を渡すと、透視変換行列を作ってくれる。 return cv2.warpPerspective(img, trans, (int(width), int(height))) # 透視変換行列を使って切り抜く。 if __name__ == '__main__': cam = cv2.VideoCapture(0) while cv2.waitKey(10) == -1: orig = cam.read()[1] lines = orig.copy() # 輪郭を抽出する canny = cv2.cvtColor(orig, cv2.COLOR_BGR2GRAY) canny = cv2.GaussianBlur(canny, (5, 5), 0) canny = cv2.Canny(canny, 50, 100) cv2.imshow('canny', canny) cnts = cv2.findContours(canny, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)[0] # 抽出した輪郭に近似する直線(?)を探す。 cnts.sort(key=cv2.contourArea, reverse=True) # 面積が大きい順に並べ替える。 warp = None for i, c in enumerate(cnts): arclen = cv2.arcLength(c, True) approx = cv2.approxPolyDP(c, 0.02*arclen, True) level = 1 - float(i)/len(cnts) # 面積順に色を付けたかったのでこんなことをしている。 if len(approx) == 4: cv2.drawContours(lines, [approx], -1, (0, 0, 255*level), 2) if warp is None: warp = approx.copy() # 一番面積の大きな四角形をwarpに保存。 else: cv2.drawContours(lines, [approx], -1, (0, 255*level, 0), 2) for pos in approx: cv2.circle(lines, tuple(pos[0]), 4, (255*level, 0, 0)) cv2.imshow('edge', lines) if warp is not None: warped = transform_by4(orig, warp[:,0,:]) # warpが存在した場合、そこだけくり抜いたものを作る。 cv2.imshow('warp', warped) cam.release() cv2.destroyAllWindows()
長いけど、やってることは割と単純。
- カメラから画像を読み取って、
cv2.Canny
で輪郭を抽出。 - 抽出した輪郭画像から
cv2.findContours
で輪郭を座標に直す。 - 見つけた輪郭線を画像に描画しつつ、一番面積の大きな四角形を見つける。
- 四角形があれば、そのエリアをトリミングする。
オリジナルの画像がこんな感じだとすると
こんな感じで輪郭を見つけて
一番大きな四角形(赤で表示されていた輪郭)をくり抜く。
あんまり精度は高くないですが、割と楽しい、かも。 改良の余地はかなりありそうな気がします。
2020-06-19 追記
最新の環境(Python 3.8 / OpenCV 4.2)でも動くようにプログラムを更新しました。