3、图像预处理(上):图像缩放、旋转、平移、仿射变换与透视变换
各位同学,欢迎来到图像预处理的上半场。
说实话,在文档扫描这个领域,预处理做得好不好,直接决定了后面OCR识别的成败。我见过太多项目,算法选得再牛,结果因为图像歪歪扭扭、分辨率不对,识别率直接掉到30%以下。所以,这一章我们得把基本功打扎实。
3.1 图像缩放:别让像素白忙活
图像缩放,说白了就是改变图片的尺寸。你想想看,一张A4纸扫描进来,可能分辨率高达4000x6000,直接扔给OCR模型?那速度慢得你想哭。
OpenCV里最常用的两个函数:
cv2.resize()— 核心函数,没有之一cv2.INTER_*— 插值方法的选择
我个人习惯,在文档预处理阶段,一般把长边缩放到1000-1500像素。为什么呢?因为大部分OCR模型在这个尺度下,既能保证识别精度,又能跑得飞快。
核心知识点:插值方法的选择
- 缩小图像:用
cv2.INTER_AREA。这是最保真的方式,不会出现锯齿。 - 放大图像:用
cv2.INTER_CUBIC或cv2.INTER_LINEAR。CUBIC更平滑,但慢一点。
import cv2
img = cv2.imread('document.jpg')
# 缩小到宽度800像素,高度按比例自动计算
width = 800
ratio = width / img.shape[1]
height = int(img.shape[0] * ratio)
resized = cv2.resize(img, (width, height), interpolation=cv2.INTER_AREA)
cv2.imshow('Resized', resized)
cv2.waitKey(0)
我的小技巧: 如果你不确定用哪种插值,先试试INTER_LINEAR。它在速度和效果之间取得了很好的平衡。我在项目中90%的情况都用它。
3.2 图像旋转与平移:把歪的扶正
文档扫描最头疼的问题是什么?拍歪了!
你想想看,一张身份证照片,角度偏了5度,OCR识别率可能就从95%掉到60%。为什么会这样?因为OCR模型默认文字是水平的,你给它一个倾斜的文本行,它当然认不准。
3.2.1 平移:简单但有用
平移就是让图像在x轴或y轴上移动。OpenCV用仿射变换矩阵来实现。
import numpy as np
# 定义平移矩阵:向右平移100像素,向下平移50像素
M = np.float32([[1, 0, 100],
[0, 1, 50]])
shifted = cv2.warpAffine(img, M, (img.shape[1], img.shape[0]))
嗯,这里要注意:平移后超出边界的部分会被裁剪掉。如果你不想丢失内容,可以适当扩大画布尺寸。
3.2.2 旋转:扶正文档的关键
旋转是文档预处理的重头戏。OpenCV提供了cv2.getRotationMatrix2D()来生成旋转矩阵。
# 以图像中心为旋转点,逆时针旋转15度,缩放比例1.0
(h, w) = img.shape[:2]
center = (w // 2, h // 2)
M = cv2.getRotationMatrix2D(center, 15, 1.0)
rotated = cv2.warpAffine(img, M, (w, h))
我曾经踩过的坑: 旋转后图像四个角会被裁剪掉!如果你要保留完整内容,需要重新计算旋转后的画布尺寸。我当年第一次做文档矫正时,旋转完发现文字缺了一角,排查了半天才发现是这个问题。
解决方案:计算旋转后图像的外接矩形,动态调整输出尺寸。
def rotate_bound(image, angle):
(h, w) = image.shape[:2]
center = (w // 2, h // 2)
M = cv2.getRotationMatrix2D(center, angle, 1.0)
# 计算新边界
cos = np.abs(M[0, 0])
sin = np.abs(M[0, 1])
new_w = int((h * sin) + (w * cos))
new_h = int((h * cos) + (w * sin))
# 调整平移量
M[0, 2] += (new_w / 2) - center[0]
M[1, 2] += (new_h / 2) - center[1]
return cv2.warpAffine(image, M, (new_w, new_h))
3.3 仿射变换:更自由的变形
仿射变换,说白了就是「平移+旋转+缩放+剪切」的组合。它有一个重要特性:平行线变换后仍然是平行线。
在文档处理中,仿射变换常用于矫正轻微的透视变形。比如你从侧面拍了一张文档,虽然有点歪,但整体还是平行四边形。
# 原图中的三个点
pts1 = np.float32([[50, 50], [200, 50], [50, 200]])
# 目标图中的三个点
pts2 = np.float32([[10, 100], [200, 50], [100, 250]])
M = cv2.getAffineTransform(pts1, pts2)
result = cv2.warpAffine(img, M, (img.shape[1], img.shape[0]))
关键理解: 仿射变换只需要3个点对就能确定。为什么?因为3个点可以唯一确定一个平行四边形。你想想看,3个点就能锁定一个平面,这就是仿射变换的数学基础。
3.4 透视变换:文档矫正的终极武器
终于到了重头戏。透视变换,也叫「四点变换」,是文档扫描中最核心的技术之一。
和仿射变换不同,透视变换允许平行线变成不平行。什么意思呢?就是你从斜上方拍一张名片,原本是矩形的名片在照片里变成了梯形。透视变换可以把梯形「拉」回矩形。
# 原图中的四个角点(比如文档的四个角)
pts1 = np.float32([[56, 65], [368, 52], [28, 387], [389, 390]])
# 目标图中的四个角点(拉直后的矩形)
pts2 = np.float32([[0, 0], [300, 0], [0, 400], [300, 400]])
M = cv2.getPerspectiveTransform(pts1, pts2)
result = cv2.warpPerspective(img, M, (300, 400))
我的实战经验: 在文档扫描项目中,最难的不是透视变换本身,而是如何自动找到文档的四个角点。我通常的做法是:先做边缘检测(Canny),然后找轮廓,再用多边形逼近找到四个顶点。这个过程我会在后面的章节详细讲。
3.5 四种变换的对比总结
| 变换类型 | 所需点数 | 保留特性 | 典型应用 |
|---|---|---|---|
| 平移 | 1个向量 | 形状、大小不变 | 图像对齐 |
| 旋转 | 1个角度+中心 | 形状不变 | 文档扶正 |
| 仿射变换 | 3个点对 | 平行线保持平行 | 轻微变形矫正 |
| 透视变换 | 4个点对 | 直线保持直线 | 文档矫正、名片扫描 |
我个人建议,初学者先把仿射变换和透视变换搞明白。这两个是文档预处理的核心。旋转和平移虽然简单,但它们是基础中的基础,千万别忽视。
注意: 透视变换要求输入的4个点不能共线,且最好按顺时针或逆时针顺序排列。我曾经因为点的顺序搞反了,结果矫正出来的图像是镜像的,调试了半天才发现。
好了,这一章的内容就到这里。下一章我们会继续讲图像预处理的下半部分——滤波、二值化和形态学操作。这些技术会帮你把文档图像处理得更干净,为OCR识别铺平道路。
记住一句话:预处理做得好,OCR识别没烦恼。
公众号:蓝海资料掘金营,微信deep3321