3、图像几何变换:resize、warpAffine、getRotationMatrix2D、仿射变换与透视变换

图像几何变换,说白了就是让图像「动起来」——放大缩小、旋转、拉伸、甚至换个视角看。这是图像预处理里最基础也最实用的操作之一。我个人习惯把几何变换分成两类:一类是改变图像尺寸的,比如缩放;另一类是改变图像形状的,比如仿射和透视。咱们一个一个来聊。

3.1 图像缩放:resize

resize 可能是你用得最多的函数了。不管是把大图缩小加速处理,还是把小图放大做输入,都离不开它。

import cv2

img = cv2.imread('example.jpg')
# 指定目标尺寸
resized = cv2.resize(img, (300, 200))
# 按比例缩放
scale_percent = 0.5
width = int(img.shape[1] * scale_percent)
height = int(img.shape[0] * scale_percent)
resized = cv2.resize(img, (width, height))

这里有个坑,我踩过好几次——resize 的尺寸参数是 (宽度, 高度),而 numpy 数组的 shape 是 (高度, 宽度, 通道数)。顺序反了,图像就变形了。嗯,写代码时多看一眼。

插值方法的选择:

  • 缩小图像:用 cv2.INTER_AREA,效果最平滑,不容易出现锯齿
  • 放大图像:用 cv2.INTER_LINEAR(默认),速度快,效果也不错
  • 追求极致质量:用 cv2.INTER_CUBIC,但速度慢,一般不用

我的经验:做深度学习模型输入时,我习惯用 INTER_LINEAR 放大,用 INTER_AREA 缩小。别小看这个选择,处理人脸图像时,插值方法选不对,关键点检测的精度能掉好几个点。

3.2 仿射变换:warpAffine 与 getRotationMatrix2D

仿射变换,说白了就是「平移 + 旋转 + 缩放 + 错切」的组合。它有个特点:平行线变换后还是平行线。你想想看,矩形经过仿射变换后还是平行四边形,不会变成梯形。

实现仿射变换需要两步:先算变换矩阵,再用 warpAffine 执行变换。

3.2.1 平移变换

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]))

平移矩阵很简单,就是单位矩阵加个偏移量。但要注意,平移后图像会超出边界,超出部分默认填黑色。你可以通过 borderMode 参数控制填充方式。

3.2.2 旋转与缩放

旋转就稍微复杂一点了。OpenCV 提供了 getRotationMatrix2D 这个函数,帮你自动生成旋转矩阵。

# 获取旋转矩阵
# 参数:旋转中心,旋转角度(逆时针为正),缩放比例
center = (img.shape[1] // 2, img.shape[0] // 2)
M = cv2.getRotationMatrix2D(center, 45, 1.0)  # 绕中心旋转45度,不缩放

# 执行旋转
rotated = cv2.warpAffine(img, M, (img.shape[1], img.shape[0]))

注意:旋转后图像会丢失四个角的信息。如果你不想丢失内容,可以调整输出尺寸。我曾经在做文档扫描时,因为没处理好这个,旋转后的文档边缘全被裁掉了,白忙活半天。

为什么会丢失角?因为旋转后的图像尺寸没变,但内容旋转出去了。解决办法是计算旋转后的外接矩形大小:

h, w = img.shape[:2]
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.2.3 自定义仿射变换

如果你知道变换前后三组对应点的坐标,可以用 getAffineTransform 直接算矩阵。

# 原图中的三个点
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]))

三个点就够了,因为仿射变换有6个自由度(2x3矩阵),每组对应点提供两个方程,三组正好解出来。

3.3 透视变换:getPerspectiveTransform

透视变换比仿射变换更「自由」。它允许平行线变成相交线——说白了,就是模拟人眼从不同角度观察的效果。你想想看,一个矩形从侧面看过去,就变成梯形了,这就是透视变换。

透视变换需要四组对应点,因为变换矩阵是3x3的,有8个自由度。

# 原图中的四个角点(按顺序:左上、右上、右下、左下)
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))

经典应用场景:文档矫正

拍身份证、拍文档时,很难保证手机正对着拍。透视变换可以把倾斜的文档「拉正」。我在做身份证识别项目时,就靠这个把准确率从85%提到了97%。

3.4 实战对比:仿射 vs 透视

特性 仿射变换 透视变换
变换矩阵 2x3 3x3
自由度 6 8
对应点数量 3组 4组
平行线关系 保持平行 可能不平行
典型应用 旋转、缩放、平移 视角矫正、文档拉正

我的建议:如果你只是想让图像旋转或缩放,用仿射变换就够了。如果你需要「改变视角」,比如把斜着拍的文档变成正面的,那就用透视变换。别杀鸡用牛刀,也别牛刀杀鸡。

3.5 避坑指南

做几何变换时,有几个坑我反复踩过,分享给你:

  • 坐标顺序搞反:OpenCV 里坐标是 (x, y),x 是列,y 是行。写代码时多检查一遍。
  • 数据类型不对:变换矩阵必须是 np.float32 类型,用 float64 会报错。
  • 边界处理:变换后图像边缘可能出现黑色区域,用 borderMode=cv2.BORDER_REPLICATE 可以复制边缘像素填充。
  • 逆时针 vs 顺时针:getRotationMatrix2D 的角度是逆时针为正,别搞反了。

我曾经在一个项目里,因为旋转角度符号搞反了,调试了整整一个下午。后来发现就是少了个负号。嗯,这种低级错误,谁还没犯过呢?

好了,几何变换就聊到这儿。下一章咱们聊聊图像滤波,那可是去噪的利器。