图像几何变换:缩放、旋转、裁剪与仿射变换

图像几何变换,说白了就是改变图像的形状、大小和位置。我刚开始接触 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点对应 / 6自由度 4点对应 / 8自由度 缩略图 / ROI提取 图像校正 文档矫正 视角矫正 / 名片识别 核心原则 变换矩阵 → 映射关系 → 插值采样 自由度越高,矫正能力越强,计算开销也越大

3.8 实战经验总结

最后分享几个我自己的经验:

  • 缩放优先用 INTER_AREA 缩小:效果比 LINEAR 好,锯齿少。
  • 旋转记得扩展画布:否则边缘信息会丢失,我吃过这个亏。
  • 裁剪前做边界检查:用 max() 和 min() 限制范围,避免越界。
  • 仿射变换选点要分散:三个点别在一条线上,否则矩阵奇异。
  • 透视变换点序要一致:我习惯左上、右上、左下、右下,顺序乱了结果就废了。