2. 签收证明图像预处理:灰度化、二值化、降噪、倾斜校正与透视变换

签收证明(POD)图像通常由手机或扫描仪在非受控环境下采集,存在光照不均、背景杂乱、纸张褶皱、倾斜透视等问题。直接送入 OCR 引擎会导致识别率骤降。本章将系统讲解图像预处理的五大核心步骤,并使用 OpenCV 实现工程级代码。

2.1 预处理流程总览

完整的 POD 图像预处理管线按以下顺序执行,每一步的输出作为下一步的输入:

  1. 图像灰度化 — 将三通道彩色图转为单通道灰度图,减少计算量。
  2. 图像二值化 — 将灰度图转为纯黑/白二值图,突出文字与印章。
  3. 降噪 — 去除扫描噪点、墨渍、背景纹理。
  4. 倾斜校正 — 检测并旋转图像,使文字行水平。
  5. 透视变换 — 矫正纸张因拍摄角度产生的梯形畸变。
工程要点: 预处理质量直接决定 OCR 的字符识别准确率。在真实项目中,建议将预处理参数(如二值化阈值、滤波核大小)做成可配置项,方便针对不同扫描仪/手机来源做调优。
签收证明图像预处理管线 输入图像 灰度化 二值化 降噪 倾斜校正 透视变换 输出 预处理管线:每一步的输出作为下一步的输入,顺序不可颠倒

2.2 图像灰度化

灰度化将 RGB 三通道图像转换为单通道灰度图像,常用加权平均法:

gray = 0.299 * R + 0.587 * G + 0.114 * B

OpenCV 实现:

import cv2

img = cv2.imread('pod_sample.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
cv2.imwrite('gray.jpg', gray)
为什么不用简单平均? 人眼对绿色最敏感、对蓝色最不敏感,OpenCV 默认的加权系数符合人眼视觉特性,能保留更多文字细节。

2.3 图像二值化

二值化将灰度图转为纯黑(0)与纯白(255)图像。针对 POD 图像光照不均的特点,推荐使用 自适应阈值 或 Otsu 大津法。

方法 适用场景 OpenCV 函数 关键参数
全局阈值 光照均匀、背景单一 cv2.threshold() thresh=127, maxval=255
Otsu 大津法 双峰直方图、自动计算阈值 cv2.threshold(…, cv2.THRESH_OTSU) 自动
自适应阈值 光照不均、阴影、渐变背景 cv2.adaptiveThreshold() blockSize=11, C=2

推荐代码(自适应阈值):

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
binary = cv2.adaptiveThreshold(
    gray, 255,
    cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
    cv2.THRESH_BINARY,
    blockSize=11,
    C=2
)
cv2.imwrite('binary.jpg', binary)
避坑: blockSize 必须为奇数,且值越大,对局部光照变化的适应性越弱。C 值越大,二值化后保留的细节越少。建议在验证集上做网格搜索调参。

2.4 降噪

二值化后的图像仍可能包含孤立噪点、断线或墨渍。常用降噪方法:

  • 中值滤波 — 对椒盐噪声效果最好,保留边缘。
  • 高斯滤波 — 平滑高斯噪声,但会模糊文字边缘。
  • 形态学操作 — 开运算(先腐蚀后膨胀)去除小白点;闭运算(先膨胀后腐蚀)填充文字断裂。

工程推荐组合:

# 中值滤波去除椒盐噪声
denoised = cv2.medianBlur(binary, 3)

# 形态学开运算去除小噪点
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
opened = cv2.morphologyEx(denoised, cv2.MORPH_OPEN, kernel)

# 形态学闭运算连接断裂笔画
closed = cv2.morphologyEx(opened, cv2.MORPH_CLOSE, kernel)

2.5 倾斜校正

POD 图像常存在小角度倾斜(±15°以内)。使用 霍夫变换检测直线 或 最小面积矩形 计算旋转角。

基于文本行的倾斜校正步骤:

  1. 对二值图做 膨胀,将文字行连成块。
  2. 使用 cv2.HoughLinesP() 检测直线。
  3. 计算所有直线的平均角度。
  4. 使用仿射变换旋转图像。
import numpy as np

def correct_skew(image):
    # 1. 膨胀使文字行连成块
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
    dilated = cv2.dilate(image, kernel, iterations=2)

    # 2. 检测直线
    lines = cv2.HoughLinesP(dilated, 1, np.pi/180, 100,
                            minLineLength=100, maxLineGap=10)

    # 3. 计算平均角度
    angles = []
    for line in lines:
        x1, y1, x2, y2 = line[0]
        angle = np.arctan2(y2 - y1, x2 - x1) * 180 / np.pi
        angles.append(angle)
    median_angle = np.median(angles)

    # 4. 旋转校正
    h, w = image.shape
    center = (w // 2, h // 2)
    M = cv2.getRotationMatrix2D(center, median_angle, 1.0)
    rotated = cv2.warpAffine(image, M, (w, h),
                             flags=cv2.INTER_CUBIC,
                             borderMode=cv2.BORDER_REPLICATE)
    return rotated

2.6 透视变换

当拍摄角度过大时,纸张呈现梯形畸变,仿射变换无法矫正。透视变换通过 4 个源点 与 4 个目标点 计算 3×3 变换矩阵。

自动检测纸张四角的常用方法:

  1. 对二值图做 Canny 边缘检测。
  2. 使用 cv2.findContours() 找到最大轮廓(纸张)。
  3. 使用 cv2.approxPolyDP() 逼近为四边形。
  4. 按左上、右上、右下、左下排序四个顶点。
  5. 计算目标矩形(如 A4 比例)并执行透视变换。
def four_point_transform(image, pts):
    # 排序四个顶点
    rect = order_points(pts)
    (tl, tr, br, bl) = rect

    # 计算目标矩形宽高
    widthA = np.linalg.norm(br - bl)
    widthB = np.linalg.norm(tr - tl)
    maxWidth = max(int(widthA), int(widthB))

    heightA = np.linalg.norm(tr - br)
    heightB = np.linalg.norm(tl - bl)
    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
工程经验: 透视变换前务必先做倾斜校正。如果纸张四角被阴影或背景干扰,可先用 cv2.GaussianBlur() 平滑后再做边缘检测。对于严重畸变的图像,可考虑使用深度学习关键点检测(如 CornerNet)替代传统轮廓逼近。
透视变换原理:从梯形到矩形 畸变图像(梯形) TL TR BR BL 透视变换 校正后图像(矩形) TL' TR' BR' BL' 透视变换需要 4 个源点 → 4 个目标点,计算 3×3 单应性矩阵

2.7 完整预处理管线代码

将上述步骤封装为可复用的函数:

def preprocess_pod(image_path):
    # 1. 读取图像
    img = cv2.imread(image_path)
    if img is None:
        raise ValueError(f"无法读取图像: {image_path}")

    # 2. 灰度化
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    # 3. 自适应二值化
    binary = cv2.adaptiveThreshold(
        gray, 255,
        cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
        cv2.THRESH_BINARY,
        blockSize=11, C=2
    )

    # 4. 降噪(中值滤波 + 形态学开闭运算)
    denoised = cv2.medianBlur(binary, 3)
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
    cleaned = cv2.morphologyEx(denoised, cv2.MORPH_CLOSE, kernel)

    # 5. 倾斜校正
    rotated = correct_skew(cleaned)

    # 6. 透视变换(假设 detect_document 返回四角坐标)
    corners = detect_document(rotated)
    if corners is not None:
        warped = four_point_transform(rotated, corners)
    else:
        warped = rotated  # 未检测到纸张则跳过透视变换

    return warped
性能优化: 在批量处理时,可将灰度图缩小 50% 做倾斜校正和透视变换的参数计算,再将变换矩阵应用到原图,可提速 3-4 倍。

2.8 预处理效果评估指标

在工程部署中,建议使用以下指标量化预处理效果:

指标 计算方式 目标值
文字连通域数量 cv2.connectedComponents() 接近真实文字数
倾斜角度标准差 霍夫直线角度统计 < 0.5°
OCR 字符准确率 与人工标注对比 > 95%
处理耗时 time.perf_counter() < 200ms/张