You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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("未检测到收据轮廓")

关键说明

  1. 多边形逼近:cv2.approxPolyDP通过轮廓周长的比例(0.02)控制逼近精度,可根据图像清晰度调整该值。
  2. 顶点排序:order_points函数确保四个顶点按固定顺序排列,避免透视变换后图像颠倒或变形。
  3. 透视变换比例还原:因为前期对图像做了缩放,所以变换矩阵要乘以resize_ratio,才能对应原始图像的坐标。

内容的提问来源于stack exchange,提问作者Waqar Ahmed

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 03:10:42