基于 OpenCV 的票据图像矫正与透视变换实战

在日常的图像处理场景中,我们经常需要对倾斜、变形的票据 / 文档图像进行矫正,使其变成规整的正射视角,方便后续的文字识别、信息提取等操作。本文将基于 Python+OpenCV 实现完整的票据图像矫正流程,核心包含轮廓检测、四点透视变换、形态学处理等关键技术。

一、核心原理介绍

1.1 整体流程

整个票据矫正流程可以总结为以下步骤:

  1. 图像预处理(缩放、灰度化、二值化)
  2. 轮廓检测与筛选(找到票据的四个角点)
  3. 四点透视变换(将不规则四边形转换为矩形)
  4. 后处理(二值化、形态学操作、旋转)

1.2 关键技术点

  • 轮廓检测:通过寻找图像中面积最大的轮廓,定位票据的物理边界
  • 四点排序:对检测到的四个角点进行有序排列(左上、右上、右下、左下)
  • 透视变换 :利用cv2.getPerspectiveTransformcv2.warpPerspective实现图像的视角矫正
python 复制代码
import numpy as np
import cv2

def order_points(pts):
    """
    对检测到的4个角点进行排序
    :param pts: 原始4个角点坐标,形状为(4,2)
    :return: 排序后的坐标,顺序为[左上, 右上, 右下, 左下]
    """
    rect = np.zeros((4, 2), dtype="float32")
    
    # 利用x+y的和排序:和最小的是左上,和最大的是右下
    s = pts.sum(axis=1)
    rect[0] = pts[np.argmin(s)]
    rect[2] = pts[np.argmax(s)]
    
    # 利用y-x的差排序:差最小的是右上,差最大的是左下
    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):
    """
    四点透视变换,将不规则四边形转换为矩形
    :param image: 原始图像
    :param pts: 四边形的4个角点
    :return: 矫正后的图像
    """
    # 获取排序后的坐标
    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(image, M, (maxWidth, maxHeight))
    
    return warped

def resize(image, width=None, height=None, inter=cv2.INTER_AREA):
    """
    保持比例的图像缩放
    :param image: 输入图像
    :param width: 目标宽度
    :param height: 目标高度
    :param inter: 插值方法
    :return: 缩放后的图像
    """
    dim = None
    (h, w) = image.shape[:2]
    
    # 如果宽高都未指定,返回原图
    if width is None and height is None:
        return image
    
    # 按高度缩放
    if width is None:
        r = height / float(h)
        dim = (int(w * r), height)
    # 按宽度缩放
    else:
        r = width / float(w)
        dim = (width, int(h * r))
    
    # 执行缩放
    resized = cv2.resize(image, dim, interpolation=inter)
    return resized

# -------------------------- 主程序 --------------------------
if __name__ == "__main__":
    # 1. 读取并显示原始图像
    image = cv2.imread('fapiao.jpg')
    cv2.imshow('原始图像', image)
    
    # 2. 图像缩放(避免原图过大影响处理速度)
    ratio = image.shape[0] / 500.0  # 计算缩放比例
    orig = image.copy()
    image = resize(orig, height=500)
    cv2.imshow('缩放后图像', image)
    
    # 3. 轮廓检测
    print("STEP 1: 轮廓检测")
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)  # 灰度化
    # 自动阈值二值化(OTSU算法)
    edged = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
    # 查找轮廓
    cnts = cv2.findContours(edged.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)[-2]
    # 绘制所有轮廓
    image_contours = cv2.drawContours(image.copy(), cnts, -1, (0, 0, 255), 1)
    cv2.imshow('所有轮廓', image_contours)
    
    # 4. 获取票据的最大轮廓
    print("STEP 2: 获取最大轮廓")
    # 按轮廓面积排序,取最大的轮廓
    screenCnt = sorted(cnts, key=cv2.contourArea, reverse=True)[0]
    # 轮廓近似(减少轮廓点数量)
    peri = cv2.arcLength(screenCnt, True)  # 计算轮廓周长
    screenCnt = cv2.approxPolyDP(screenCnt, 0.05 * peri, True)  # 轮廓近似
    print(f"轮廓近似后的角点数量: {screenCnt.shape}")
    # 绘制票据轮廓
    image_contour = cv2.drawContours(image.copy(), [screenCnt], -1, (0, 255, 0), 2)
    cv2.imshow("票据轮廓", image_contour)
    
    # 5. 透视变换矫正
    # 还原轮廓坐标到原始图像尺寸
    warped = four_point_transform(orig, screenCnt.reshape(4, 2) * ratio)
    cv2.imwrite('invoice_new.jpg', warped)
    cv2.namedWindow('透视变换后', cv2.WINDOW_NORMAL)
    cv2.imshow("透视变换后", warped)
    
    # 6. 后处理(二值化+形态学操作+旋转)
    warped_gray = cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY)
    # 二值化
    ref = cv2.threshold(warped_gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
    cv2.imshow('二值化结果', ref)
    
    # 形态学闭运算(先膨胀后腐蚀),消除小的孔洞
    kernel = np.ones((2, 2), np.uint8)
    ref_new = cv2.morphologyEx(ref, cv2.MORPH_CLOSE, kernel)
    # 缩放便于查看
    ref_new = resize(ref_new.copy(), width=500)
    cv2.imshow('形态学处理后', ref_new)
    
    # 旋转90度(根据实际需求调整)
    rotated_image = cv2.rotate(ref_new, cv2.ROTATE_90_COUNTERCLOCKWISE)
    cv2.imshow("最终矫正结果", rotated_image)
    
    # 等待按键退出
    cv2.waitKey(0)
    cv2.destroyAllWindows()

三、代码详细解析

3.1 核心函数说明

1. order_points(pts)

该函数的核心作用是对检测到的 4 个角点进行有序排列,确保顺序为左上、右上、右下、左下

  • 利用x+y的和排序:和最小的是左上,和最大的是右下
  • 利用y-x的差排序:差最小的是右上,差最大的是左下
2. four_point_transform(image, pts)

实现透视变换的核心函数:

  • 计算变换后图像的宽度和高度(取两组对边的最大值,保证完整性)
  • 定义目标坐标(规整的矩形)
  • 通过cv2.getPerspectiveTransform计算透视变换矩阵
  • 通过cv2.warpPerspective应用变换,得到矫正后的图像
3. resize(image, width=None, height=None)

保持图像比例的缩放函数,避免缩放后图像变形。

3.2 主流程解析

  1. 图像预处理

    • 读取图像并缩放(解决大图处理慢的问题)
    • 灰度化 + OTSU 自动阈值二值化,突出轮廓
  2. 轮廓检测

    • 使用cv2.findContours查找所有轮廓
    • 按面积排序,筛选出票据的最大轮廓
    • cv2.approxPolyDP进行轮廓近似,得到 4 个角点
  3. 透视变换

    • 将检测到的角点还原到原始图像尺寸
    • 应用四点透视变换,得到正射视角的票据图像
  4. 后处理

    • 二值化增强对比度
    • 形态学闭运算消除小噪声
    • 旋转调整方向(根据实际需求)
相关推荐
marteker2 小时前
研究发现,电商零售商计划在代理电商领域进行大规模投资
人工智能
人工智能AI技术2 小时前
Qwen3.5-Plus登顶|C#集成通义千问,高并发服务实战优化
人工智能·c#
阿_旭2 小时前
基于YOLO26深度学习的蓝莓成熟度检测与分割系统【python源码+Pyqt5界面+数据集+训练代码】图像分割、人工智能
人工智能·python·深度学习·毕业设计·蓝莓成熟度检测
lxmyzzs2 小时前
使用Python分析COCO数据集标注信息:一个简单脚本实现统计与可视化
python·深度学习·目标检测·计算机视觉
智算菩萨2 小时前
【How Far Are We From AGI】4 AGI的“生理系统“——从算法架构到算力基座的工程革命
论文阅读·人工智能·深度学习·算法·ai·架构·agi
White-Legend2 小时前
GPT5.4每日200刀
人工智能·gpt
小程故事多_802 小时前
重构 RAG 质量标准,一套可落地、可量化的全维度评估框架
人工智能·重构·aigc·ai编程·rag
IT_陈寒2 小时前
JavaScript开发者必看:3个让代码效率翻倍的隐藏技巧
前端·人工智能·后端
jkyy20142 小时前
健康有益健康监测座舱:以科技之力,定义出行健康新标杆
大数据·人工智能·物联网·健康医疗