输入图片
扫描得到如下的结果
用OpenCV构建文档扫描仪只需三个简单步骤:
1.边缘检测
2.使用图像中的边缘来找到代表被扫描纸张的轮廓。
3.应用透视变换来获得文档的自顶向下视图。
导入必要的包
from skimage.filters import threshold_local import numpy as np import argparse import cv2 import imutils
初始化一个坐标列表,该列表中的第一个元素是左上,第二个元素是右上,第三个元素是右下,第四个元素是左下
该坐标排序方法有缺陷,具体可参考 【python】OpenCV—Coordinates Sorted Clockwise
def order_points(pts): rect = np.zeros((4, 2), dtype = "float32") # 左上角点的和最小,然而右下角的点的和最大 s = pts.sum(axis = 1) rect[0] = pts[np.argmin(s)] rect[2] = pts[np.argmax(s)] # 现在,计算点之间的差值,右上角的差值最小,而左下角的差值最大 diff = np.diff(pts, axis = 1) rect[1] = pts[np.argmin(diff)] rect[3] = pts[np.argmax(diff)] # 返回有序坐标 return rect def four_point_transform(image, pts): # 获得点的一致顺序,并将它们分别拆封 rect = order_points(pts) (tl, tr, br, bl) = rect # 计算新图像的宽度,这将是右下角和左下角x坐标或右上角和左上角x坐标之间的最大距离 widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2)) widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2)) maxWidth = max(int(widthA), int(widthB)) # 计算新图像的高度,这将是右上角和右下角y坐标或左上角和左下角y坐标之间的最大距离 heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2)) heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2)) maxHeight = max(int(heightA), int(heightB)) # 现在我们有了新图像的维数,构建目标点集以获得图像的“鸟瞰视图”(即自顶向下视图),再次指定左上、右上、右下和左下顺序中的点 dst = np.array([ [0, 0], [maxWidth - 1, 0], [maxWidth - 1, maxHeight - 1], [0, maxHeight - 1]], dtype = "float32") # 计算透视变换矩阵,然后应用它 M = cv2.getPerspectiveTransform(rect, dst) warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight)) # 返回变换后的图像 return warped
构造参数解析器并解析参数
ap = argparse.ArgumentParser() ap.add_argument("-i", "--image", required = True, default="1.jpg", help = "Path to the image to be scanned") args = vars(ap.parse_args()) # 加载图像并计算旧高度与新高度的比率,克隆它,并调整它的大小 image = cv2.imread(args["image"]) ratio = image.shape[0] / 500.0 orig = image.copy() image = imutils.resize(image, height=500) # ratio = 1.0 # orig = image.copy() # 将图像转换为灰度,模糊它,并在图像中找到边缘 gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) cv2.imwrite("gray.jpg", gray)
gray = cv2.GaussianBlur(gray, (7, 7), 0) cv2.imwrite("GaussianBlur.jpg", gray)
这里 kernel size 需要设置大一些,不然很容易检测到发票上的黑色字体为轮廓了
edged = cv2.Canny(gray, 75, 200) cv2.imwrite("Canny.jpg", edged)
显示原始图像和边缘检测图像
print("STEP 1: Edge Detection") cv2.imshow("Image", image) cv2.imshow("Edged", edged) cv2.waitKey(0)
找到边缘图像中的轮廓,只保留最大的轮廓,并初始化屏幕轮廓
cnts = cv2.findContours(edged.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) cnts = imutils.grab_contours(cnts) cnts = sorted(cnts, key = cv2.contourArea, reverse = True)[:5] # 循环迭代所有轮廓 for c in cnts: # 近似轮廓 peri = cv2.arcLength(c, True) approx = cv2.approxPolyDP(c, 0.02 * peri, True) # cv2.drawContours(image, c, -1, (0, 0, 255), 3) # 如果我们的近似轮廓有4个点,那么我们可以假设我们已经找到了我们的屏幕 if len(approx) == 4: screenCnt = approx break # 画出这张票的轮廓 print("STEP 2: Find contours of paper") cv2.drawContours(image, [screenCnt], -1, (0, 255, 0), 2) cv2.imshow("Outline", image) cv2.waitKey(0)
# 应用四点变换获得原始图像自上而下的视图 warped = four_point_transform(orig, screenCnt.reshape(4, 2) * ratio) cv2.imwrite("warped.jpg", warped)
# 将变换后的图像转换为灰度,然后使用阈值给它“黑白”纸的效果 warped = cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY) cv2.imwrite("warped_gray.jpg", warped)
T = threshold_local(warped, 11, offset = 10, method = "gaussian") warped = (warped > T).astype("uint8") * 255 cv2.imwrite("warped_threshold_local.jpg", warped)
# 显示原始和扫描图像 print("STEP 3: Apply perspective transform") cv2.imshow("Original", imutils.resize(orig, height = 650)) cv2.imshow("Scanned", imutils.resize(warped, height = 650)) cv2.waitKey(0)
再来个例子
输入图片
输出结果
cv2.arcLength 是 OpenCV(Open Source Computer Vision Library)库中的一个函数,用于计算多边形的周长或轮廓的弧长。这个函数在处理图像中的形状分析、轮廓检测等任务时非常有用。它接受一个轮廓(轮廓是一系列的点,通常通过边缘检测或轮廓查找算法获得)作为输入,并返回该轮廓的周长。
length = cv2.arcLength(contour, True)
contour:输入轮廓,应该是一个点集(通常是numpy.ndarray类型),这些点定义了轮廓的形状。
第二个参数是True或False,指定轮廓是否应该被近似为闭合的(通过连接轮廓的第一个点和最后一个点)。如果轮廓已经是闭合的,或者你不关心轮廓是否闭合,可以传递True。如果轮廓不是闭合的,但你不希望它被视为闭合的,应该传递False。注意,这个参数在一些版本的OpenCV中可能不是必需的,或者默认值为True。
返回值
cv2.approxPolyDP 是 OpenCV 库中的一个函数,用于对轮廓或曲线进行多边形逼近。该函数使用 Douglas-Peucker 算法来减少表示轮廓或曲线所需的点数,同时尽可能保持其形状特征。这个功能在图像处理、计算机视觉和机器学习等领域中非常有用,特别是在处理轮廓检测、形状分析等方面。
approx = cv2.approxPolyDP(curve, epsilon, closed)
返回值
需要注意的是,epsilon 的值是一个权衡参数,需要根据具体应用进行调整。较小的 epsilon 值会产生更精确的逼近结果,但可能会增加计算复杂性和所需的存储空间。较大的 epsilon 值则会产生更简单的逼近结果,但可能会损失一些形状细节。因此,在实际应用中,需要根据具体需求来选择合适的 epsilon 值。
skimage.filters.threshold_local 是 scikit-image 库中的一个函数,用于对图像进行局部阈值处理。与全局阈值处理(如使用固定的阈值来分割图像)不同,局部阈值处理会考虑图像中的每个像素及其邻域,从而根据局部区域的统计特性(如亮度或对比度)动态地确定阈值。这种方法在处理光照不均或具有不同亮度水平的图像时特别有用。
skimage.filters.threshold_local(image, block_size, method='gaussian', offset=0, mode='reflect', param=None, cval=0, **kwargs)
参数说明
返回值
imutils.grab_contours 是 imutils 库中的一个函数,该库是一个为OpenCV提供便利函数的Python库,旨在简化图像处理任务。在OpenCV中,特别是在使用cv2.findContours函数时,返回的轮廓信息可能会根据OpenCV的版本(主要是3.x和4.x版本之间)而有所不同。在OpenCV 3.x中,cv2.findContours返回三个值:图像、轮廓、和轮廓的层次结构。而在OpenCV 4.x中,它只返回两个值:轮廓和轮廓的层次结构。
imutils.grab_contours函数就是为了解决这种版本差异而设计的。它接受cv2.findContours的输出,并始终返回轮廓列表,无论OpenCV的版本如何。这样,开发者就可以编写不依赖于特定OpenCV版本的代码。
假设你正在使用OpenCV来检测图像中的轮廓,你可能会写出如下代码:
import cv2 import imutils # 读取图像 image = cv2.imread('path_to_image.jpg') gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) blurred = cv2.GaussianBlur(gray, (5, 5), 0) edged = cv2.Canny(blurred, 30, 150) # 在OpenCV 4.x中,cv2.findContours返回两个值 contours, hierarchy = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 使用imutils.grab_contours来获取轮廓 contours = imutils.grab_contours(contours) # 现在,无论你的OpenCV版本是什么,contours都是一个轮廓列表 # 接下来,你可以使用这些轮廓进行进一步的处理,比如绘制轮廓等 cv2.drawContours(image, contours, -1, (0, 255, 0), 2) # 显示图像 cv2.imshow("Image", image) cv2.waitKey(0) cv2.destroyAllWindows()
请注意,如果你正在使用OpenCV 3.x,cv2.findContours实际上会返回三个值,但imutils.grab_contours函数会忽略第一个返回值(通常是原始图像,但在这个上下文中并不重要),并只返回轮廓列表。这使得你的代码更加健壮,因为它不依赖于OpenCV的特定版本。
# 导入必要的包 from skimage.filters import threshold_local import numpy as np import argparse import cv2 import imutils def order_points(pts): # 初始化一个坐标列表,该列表中的第一个元素是左上,第二个元素是右上,第三个元素是右下,第四个元素是左下 rect = np.zeros((4, 2), dtype = "float32") # 左上角点的和最小,然而右下角的点的和最大 s = pts.sum(axis = 1) rect[0] = pts[np.argmin(s)] rect[2] = pts[np.argmax(s)] # 现在,计算点之间的差值,右上角的差值最小,而左下角的差值最大 diff = np.diff(pts, axis = 1) rect[1] = pts[np.argmin(diff)] rect[3] = pts[np.argmax(diff)] # 返回有序坐标 return rect def four_point_transform(image, pts): # 获得点的一致顺序,并将它们分别拆封 rect = order_points(pts) (tl, tr, br, bl) = rect # 计算新图像的宽度,这将是右下角和左下角x坐标或右上角和左上角x坐标之间的最大距离 widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2)) widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2)) maxWidth = max(int(widthA), int(widthB)) # 计算新图像的高度,这将是右上角和右下角y坐标或左上角和左下角y坐标之间的最大距离 heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2)) heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2)) maxHeight = max(int(heightA), int(heightB)) # 现在我们有了新图像的维数,构建目标点集以获得图像的“鸟瞰视图”(即自顶向下视图),再次指定左上、右上、右下和左下顺序中的点 dst = np.array([ [0, 0], [maxWidth - 1, 0], [maxWidth - 1, maxHeight - 1], [0, maxHeight - 1]], dtype = "float32") # 计算透视变换矩阵,然后应用它 M = cv2.getPerspectiveTransform(rect, dst) warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight)) # 返回变换后的图像 return warped # 构造参数解析器并解析参数 ap = argparse.ArgumentParser() ap.add_argument("-i", "--image", required = True, default="1.jpg", help = "Path to the image to be scanned") args = vars(ap.parse_args()) # 加载图像并计算旧高度与新高度的比率,克隆它,并调整它的大小 image = cv2.imread(args["image"]) ratio = image.shape[0] / 500.0 orig = image.copy() image = imutils.resize(image, height=500) # ratio = 1.0 # orig = image.copy() # 将图像转换为灰度,模糊它,并在图像中找到边缘 gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) cv2.imwrite("gray.jpg", gray) gray = cv2.GaussianBlur(gray, (7, 7), 0) cv2.imwrite("GaussianBlur.jpg", gray) edged = cv2.Canny(gray, 75, 200) cv2.imwrite("Canny.jpg", edged) # 显示原始图像和边缘检测图像 print("STEP 1: Edge Detection") cv2.imshow("Image", image) cv2.imshow("Edged", edged) cv2.waitKey(0) # 找到边缘图像中的轮廓,只保留最大的轮廓,并初始化屏幕轮廓 cnts = cv2.findContours(edged.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) cnts = imutils.grab_contours(cnts) cnts = sorted(cnts, key = cv2.contourArea, reverse = True)[:5] # 循环迭代所有轮廓 for c in cnts: # 近似轮廓 peri = cv2.arcLength(c, True) approx = cv2.approxPolyDP(c, 0.02 * peri, True) # cv2.drawContours(image, c, -1, (0, 0, 255), 3) # 如果我们的近似轮廓有4个点,那么我们可以假设我们已经找到了我们的屏幕 if len(approx) == 4: screenCnt = approx break # 画出这张票的轮廓 print("STEP 2: Find contours of paper") cv2.drawContours(image, [screenCnt], -1, (0, 255, 0), 2) cv2.imshow("Outline", image) cv2.waitKey(0) # 应用四点变换获得原始图像自上而下的视图 warped = four_point_transform(orig, screenCnt.reshape(4, 2) * ratio) cv2.imwrite("warped.jpg", warped) # 将变换后的图像转换为灰度,然后使用阈值给它“黑白”纸的效果 warped = cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY) cv2.imwrite("warped_gray.jpg", warped) T = threshold_local(warped, 11, offset = 10, method = "gaussian") warped = (warped > T).astype("uint8") * 255 cv2.imwrite("warped_threshold_local.jpg", warped) # 显示原始和扫描图像 print("STEP 3: Apply perspective transform") cv2.imshow("Original", imutils.resize(orig, height = 650)) cv2.imshow("Scanned", imutils.resize(warped, height = 650)) cv2.waitKey(0)
参考学习来自 imutils基础(4)构建一个文档扫描仪