OpenCV:如何将收据检测边缘连接为标准矩形以完整捕获收据
收据检测:生成标准矩形捕获区域
问题背景
我正在用OpenCV做收据检测,已完成图像轮廓检测,但之前尝试用凸包生成矩形时,凸包会直接连接断点,无法生成沿收据90度边框的标准矩形。希望实现用4个点组成的矩形完整捕获收据区域,效果为用矩形框住倾斜的收据,四个角对齐收据边缘。
现有代码如下:
import numpy as np import cv2 import matplotlib.pyplot as plt from skimage.filters import threshold_local from PIL import Image import sys file_name = "test.jpg" image = cv2.imread(file_name) # Downscale image as finding receipt contour is more efficient on a small image resize_ratio = 500 / image.shape[0] original = image.copy() image = opencv_resize(image, resize_ratio) # Convert to grayscale for further processing gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # Get rid of noise with Gaussian Blur filter blurred = cv2.GaussianBlur(gray, (5, 5), 0) # Detect white regions rectKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9, 9)) dilated = cv2.dilate(blurred, rectKernel) # Detect edge edged = cv2.Canny(dilated, 40, 200, apertureSize=3) # Detect all contours in Canny-edged image contours, hierarchy = cv2.findContours(edged, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) image_with_contours = cv2.drawContours(image.copy(), contours, -1, (0,255,0), 3) # Create hull to have good shape rectangle # Find the convex hull object for each contour hull_list = [] for i in range(len(contours)): hull = cv2.convexHull(contours[i]) hull_list.append(hull) image_with_hull = cv2.drawContours(image.copy(), hull_list, -1, (0,255,0), 3) # Get 10 largest contours #largest_contours = sorted(contours, key = cv2.contourArea, reverse = True)[:10] largest_contours = sorted(hull_list, key = cv2.contourArea, reverse = True)[:10] image_with_largest_contours = cv2.drawContours(image.copy(), largest_contours, -1, (0,255,0), 3)
解决方案
核心是通过多边形逼近提取四边形轮廓,再经透视变换生成标准矩形。以下是修改后的完整代码及关键说明:
import numpy as np import cv2 import matplotlib.pyplot as plt from skimage.filters import threshold_local from PIL import Image import sys def opencv_resize(image, ratio): width = int(image.shape[1] * ratio) height = int(image.shape[0] * ratio) dim = (width, height) return cv2.resize(image, dim, interpolation = cv2.INTER_AREA) file_name = "test.jpg" image = cv2.imread(file_name) # Downscale image as finding receipt contour is more efficient on a small image resize_ratio = 500 / image.shape[0] original = image.copy() image = opencv_resize(image, resize_ratio) # Convert to grayscale for further processing gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # Get rid of noise with Gaussian Blur filter blurred = cv2.GaussianBlur(gray, (5, 5), 0) # Detect white regions rectKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9, 9)) dilated = cv2.dilate(blurred, rectKernel) # Detect edge edged = cv2.Canny(dilated, 40, 200, apertureSize=3) # Detect all contours in Canny-edged image contours, hierarchy = cv2.findContours(edged, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) image_with_contours = cv2.drawContours(image.copy(), contours, -1, (0,255,0), 3) # 替换凸包处理:筛选最大轮廓并提取四边形 largest_contours = sorted(contours, key=cv2.contourArea, reverse=True)[:10] receipt_contour = None for contour in largest_contours: # 多边形逼近,epsilon为周长比例,控制精度 perimeter = cv2.arcLength(contour, True) approx = cv2.approxPolyDP(contour, 0.02 * perimeter, True) # 筛选顶点数为4的轮廓,同时验证面积合理性 if len(approx) == 4: area = cv2.contourArea(approx) if area > image.shape[0] * image.shape[1] * 0.1: receipt_contour = approx break if receipt_contour is not None: # 绘制检测到的收据轮廓 image_with_receipt = cv2.drawContours(image.copy(), [receipt_contour], -1, (255,0,0), 3) # 排序顶点:左上、右上、右下、左下,确保透视变换正确 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 # 转换坐标并排序 pts = receipt_contour.reshape(4, 2) rect = order_points(pts) (tl, tr, br, bl) = rect # 计算目标矩形的宽高 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)) 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(original, M * resize_ratio, (maxWidth, maxHeight)) # 显示结果 plt.figure(figsize=(12,6)) plt.subplot(121), plt.imshow(cv2.cvtColor(image_with_receipt, cv2.COLOR_BGR2RGB)), plt.title("检测到的收据轮廓") plt.subplot(122), plt.imshow(cv2.cvtColor(warped, cv2.COLOR_BGR2RGB)), plt.title("矫正后的收据") plt.show() else: print("未检测到收据轮廓")
关键说明
- 多边形逼近:
cv2.approxPolyDP通过轮廓周长的比例(0.02)控制逼近精度,可根据图像清晰度调整该值。 - 顶点排序:
order_points函数确保四个顶点按固定顺序排列,避免透视变换后图像颠倒或变形。 - 透视变换比例还原:因为前期对图像做了缩放,所以变换矩阵要乘以
resize_ratio,才能对应原始图像的坐标。
内容的提问来源于stack exchange,提问作者Waqar Ahmed
相关产品推荐
相关产品推荐

