图像几何变换:缩放、旋转、裁剪与仿射变换
图像几何变换,说白了就是改变图像的形状、大小和位置。我刚开始接触 OpenCV 时,觉得这些操作不就是调几个函数嘛,没什么技术含量。直到有一次做车牌识别项目,发现拍到的车牌总是歪的,才意识到——嗯,几何变换其实是很多高级视觉任务的基石。
这一节,我会带你逐个击破 OpenCV 里最常用的几何变换操作。每个操作我都会给出代码示例,顺便聊聊我在实际项目中踩过的坑。
3.1 图像缩放
缩放是最基础的操作。OpenCV 用 cv2.resize() 搞定。
import cv2
img = cv2.imread('input.jpg')
# 指定目标尺寸 (宽, 高)
resized = cv2.resize(img, (640, 480))
# 按比例缩放
scale_percent = 0.5
width = int(img.shape[1] * scale_percent)
height = int(img.shape[0] * scale_percent)
resized = cv2.resize(img, (width, height))
插值方法的选择
缩小图像用 cv2.INTER_AREA,放大用 cv2.INTER_LINEAR 或 cv2.INTER_CUBIC。我习惯缩小用 AREA,放大用 LINEAR,速度和质量比较均衡。
尺寸顺序别搞反
cv2.resize() 的尺寸参数是 (宽度, 高度),而 img.shape 返回的是 (高度, 宽度, 通道数)。我刚开始写代码时经常搞混,结果图像变形了还找半天 bug。
3.2 图像旋转
旋转需要两步:先构建旋转矩阵,再应用仿射变换。
import cv2
import numpy as np
h, w = img.shape[:2]
center = (w // 2, h // 2)
angle = 45
scale = 1.0
M = cv2.getRotationMatrix2D(center, angle, scale)
rotated = cv2.warpAffine(img, M, (w, h))
这里有个细节——旋转后图像边缘会被裁剪。我曾经做过一个全景拼接项目,旋转后丢失了大量边缘信息,导致拼接失败。解决办法是手动计算旋转后的边界框大小。
cos = abs(M[0, 0])
sin = 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]
rotated = cv2.warpAffine(img, M, (new_w, new_h))
3.3 图像裁剪
裁剪其实不是函数调用,而是 NumPy 的切片操作。简单到让人怀疑人生。
cropped = img[y_start:y_end, x_start:x_end]
比如我要裁剪左上角 200x200 的区域:
crop = img[0:200, 0:200]
裁剪的坑
切片范围不能超出图像边界,否则会报错。我建议先检查边界:
h, w = img.shape[:2]
x1, y1, x2, y2 = 100, 100, 500, 500
x1 = max(0, x1)
y1 = max(0, y1)
x2 = min(w, x2)
y2 = min(h, y2)
crop = img[y1:y2, x1:x2]
3.4 仿射变换
仿射变换是旋转、缩放、平移、错切的组合。核心思想是:用三个点确定一个变换。
# 原图中的三个点
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, (w, h))
为什么是三个点?因为仿射变换有 6 个自由度(2x3 矩阵),三个点正好提供 6 个方程。我在做文档扫描矫正时,就是通过检测文档的四个角点,然后取其中三个做仿射变换。
选点技巧
三个点不要选在一条直线上,否则矩阵会奇异。我一般选三角形的三个顶点,尽量分散开。
3.5 透视变换
透视变换比仿射变换更强大。它用四个点确定变换,可以模拟视角变化。
# 原图四个角点
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))
透视变换有 8 个自由度(3x3 矩阵),四个点正好。我做过一个名片识别系统,用透视变换把倾斜的名片矫正成正面视角,识别率从 60% 直接跳到 95%。
点的顺序必须一致
四个点的顺序要一一对应。我习惯按左上、右上、左下、右下的顺序排列。顺序乱了,变换结果会完全扭曲。
3.6 四种变换对比
| 变换类型 | 所需点数 | 自由度 | 变换矩阵 | 典型应用 |
|---|---|---|---|---|
| 缩放 | 无 | 2 | 对角矩阵 | 图像缩略图 |
| 旋转 | 1(中心点) | 3 | 2x3 旋转矩阵 | 图像校正 |
| 仿射变换 | 3 | 6 | 2x3 矩阵 | 文档矫正 |
| 透视变换 | 4 | 8 | 3x3 矩阵 | 视角矫正 |
3.7 知识体系总览
下面这张图把本章的核心知识点串起来了。你可以看到,从最简单的缩放裁剪,到最复杂的透视变换,它们之间是层层递进的关系。
3.8 实战经验总结
最后分享几个我自己的经验:
- 缩放优先用 INTER_AREA 缩小:效果比 LINEAR 好,锯齿少。
- 旋转记得扩展画布:否则边缘信息会丢失,我吃过这个亏。
- 裁剪前做边界检查:用
max()和min()限制范围,避免越界。 - 仿射变换选点要分散:三个点别在一条线上,否则矩阵奇异。
- 透视变换点序要一致:我习惯左上、右上、左下、右下,顺序乱了结果就废了。