导读
本文主要介绍如何使用 OpenCV GrabCut实现一个文档自动扫描仪。(公众号:OpenCV与AI深度学习)
背景介绍 文档扫描是将物理文档转换为数字形式的过程。可以通过扫描仪或手机摄像头拍摄图像来完成。我们将在本文中讨论如何使用计算机视觉和图像处理技术有效地实现这一目标。 有许多软件解决方案和应用程序可以做到这一点。借助计算机视觉的力量,从物理文档到扫描文档的过程与将相机对准文档并单击图片没有太大区别。速度和易用性是此类解决方案的主要优势,它们可用于计算机和移动设备。 让我们看看如何使用经典的计算机视觉技术创建一个简单的 OpenCV 文档扫描仪,其中输入将是我们要扫描的文档的图像,而预期的输出将是正确对齐的文档扫描图像。
实现目标 如下图所示,给定一张包含文档的图片,通过代码自动将文档提取并矫正。
实现步骤 测试原图如下:
实现步骤: 【1】通过形态学处理,得到一个空白页。这里直接用闭运算即可,闭运算是膨胀,然后是腐蚀。不断重复这些关闭操作,直到你得到一个空白页。
kernel = np.ones((5,5),np.uint8) img = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel, iterations= 3)
为什么我们想要一个空白文档呢?因为后面会进行边缘检测,并且我们不希望被页面的文字内容干扰该。 【2】用GrabCut去掉背景。
- 它只需要在前景中的对象周围设置一个边界框,边界框之外的所有内容都被视为背景。
- GrabCut 会自动消除所有背景,即使在边界框内也是如此。现在剩下的就是前景对象。
我们将角落 20 像素作为背景,GrabCut 会自动确定前景和背景,只留下文档。 mask = np.zeros(img.shape[:2],np.uint8) bgdModel = np.zeros((1,65),np.float64) fgdModel = np.zeros((1,65),np.float64) rect = (20,20,img.shape[1]-20,img.shape[0]-20) cv2.grabCut(img,mask,rect,bgdModel,fgdModel,5,cv2.GC_INIT_WITH_RECT) mask2 = np.where((mask==2)|(mask==0),0,1).astype('uint8') img = img*mask2[:,:,np.newaxis]
【3】Canny边缘检测 轮廓提取。
代码语言:javascript复制gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
gray = cv2.GaussianBlur(gray, (11, 11), 0)
# Edge Detection.
canny = cv2.Canny(gray, 0, 200)
canny = cv2.dilate(canny, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)))
- 首先将空白页的图像转换为灰度,因为canny只对灰度图像起作用。
- 然后执行高斯模糊以去除图像中的噪声。
- 最后,对图像进行精确边缘检测。
- 此外,放大图像以获得文档的细轮廓。
# Blank canvas.
con = np.zeros_like(img)
# Finding contours for the detected edges.
contours, hierarchy = cv2.findContours(canny, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)
# Keeping only the largest detected contour.
page = sorted(contours, key=cv2.contourArea, reverse=True)[:5]
con = cv2.drawContours(con, page, -1, (0, 255, 255), 3)
- 根据大小对检测到的轮廓进行排序
- 只保留检测到的最大轮廓
- 然后在空白画布上绘制这个检测到的最大轮廓
【4】角点检测 排序。
代码语言:javascript复制# Blank canvas.
con = np.zeros_like(img)
# Loop over the contours.
for c in page:
# Approximate the contour.
epsilon = 0.02 * cv2.arcLength(c, True)
corners = cv2.approxPolyDP(c, epsilon, True)
# If our approximated contour has four points
if len(corners) == 4:
break
cv2.drawContours(con, c, -1, (0, 255, 255), 3)
cv2.drawContours(con, corners, -1, (0, 255, 0), 10)
# Sorting the corners and converting them to desired shape.
corners = sorted(np.concatenate(corners).tolist())
# Displaying the corners.
for index, c in enumerate(corners):
character = chr(65 index)
cv2.putText(con, character, tuple(c), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 0, 0), 1, cv2.LINE_AA)
角点排序:
代码语言:javascript复制def order_points(pts):
'''Rearrange coordinates to order:
top-left, top-right, bottom-right, bottom-left'''
rect = np.zeros((4, 2), dtype='float32')
pts = np.array(pts)
s = pts.sum(axis=1)
# Top-left point will have the smallest sum.
rect[0] = pts[np.argmin(s)]
# Bottom-right point will have the largest sum.
rect[2] = pts[np.argmax(s)]
diff = np.diff(pts, axis=1)
# Top-right point will have the smallest difference.
rect[1] = pts[np.argmin(diff)]
# Bottom-left will have the largest difference.
rect[3] = pts[np.argmax(diff)]
# Return the ordered coordinates.
return rect.astype('int').tolist()
确定目标坐标:一旦获得文档的角点,接下来只需要目标坐标来执行透视变换和对齐文档。
【5】透视变换对齐文档。
代码语言:javascript复制# Getting the homography.
M = cv2.getPerspectiveTransform(np.float32(corners), np.float32(destination_corners))
# Perspective transform using homography.
final = cv2.warpPerspective(orig_img, M, (destination_corners[2][0], destination_corners[2][1]), flags=cv2.INTER_LINEAR)
【6】扩展测试。
我们在 23 种不同的背景和不同的方向上进行了测试,自动文档扫描仪几乎在所有情况下都运行良好。
失败情况:
当文档的一部分在图像之外时,可能会丢失一个角落,GrabCut 无法扫描。这是使用 GrabCut 的唯一限制。在大多数其他情况下,我们的文档扫描仪运行良好。
这种方法的另一个限制是边缘和轮廓检测。如果背景中存在大量噪声,则会检测到许多不需要的边缘,并且在某些情况下,轮廓检测步骤可能会将这些边缘误认为是文档。此外,如果文档边缘与背景无法区分,则轮廓检测可能无法完全正常工作。
但 GrabCut 和轮廓检测并不是唯一经过验证的文档扫描方法。对于消费级文档扫描解决方案,首选角点检测和分割等深度学习技术,因为它们更强大。