3、图像几何变换:图像缩放、旋转、平移、仿射变换与透视变换实战
图像几何变换,说白了就是让图像「动起来」。
缩放、旋转、平移,这些是基础操作。仿射变换和透视变换,则是更高级的玩法。
我刚开始做图像处理时,觉得几何变换不就是调个函数嘛。后来在项目中踩过坑才发现,这里面的门道还真不少。今天咱们就把它彻底讲透。
3.1 图像缩放:不只是变大变小
图像缩放,最直观的操作。但你真的了解它吗?
缩放的核心是插值。你想想看,一张 100x100 的图要变成 200x200,多出来的像素从哪来?
OpenCV 提供了几种插值方法:
- 最近邻插值:速度快,但锯齿明显。适合实时预览。
- 双线性插值:平滑效果好,最常用。
- 双三次插值:质量最高,但计算量大。
我的经验:做物体检测时,我习惯用双线性插值。既保证了速度,质量也够用。但如果是做超分辨率,我会用双三次插值。
import cv2
import numpy as np
# 读取图像
img = cv2.imread('input.jpg')
# 方法1:指定目标尺寸
resized = cv2.resize(img, (800, 600), interpolation=cv2.INTER_LINEAR)
# 方法2:按比例缩放
scale_percent = 0.5 # 缩小到50%
width = int(img.shape[1] * scale_percent)
height = int(img.shape[0] * scale_percent)
resized = cv2.resize(img, (width, height), interpolation=cv2.INTER_AREA)
# 方法3:使用fx, fy参数
resized = cv2.resize(img, None, fx=2.0, fy=2.0, interpolation=cv2.INTER_CUBIC)
注意:缩小图像时,用 INTER_AREA 效果最好。放大时,用 INTER_LINEAR 或 INTER_CUBIC。
我曾经在项目中用 INTER_LINEAR 缩小图像,结果出现了摩尔纹。换成 INTER_AREA 后问题就解决了。
3.2 图像旋转:绕哪个点转?
旋转操作,很多人以为就是调个角度。其实没那么简单。
旋转需要三个要素:旋转中心、旋转角度、缩放比例。
OpenCV 提供了 cv2.getRotationMatrix2D 来生成旋转矩阵。
# 获取图像尺寸
h, w = img.shape[:2]
# 旋转中心:图像中心
center = (w // 2, h // 2)
# 旋转角度:45度,逆时针为正
angle = 45
# 缩放比例:1.0 表示不缩放
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 图像平移:最简单的几何变换
平移,说白了就是把图像挪个位置。
平移矩阵长这样:
M = [[1, 0, tx],
[0, 1, ty]]
其中 tx 是水平偏移,ty 是垂直偏移。
# 向右平移100像素,向下平移50像素
tx, ty = 100, 50
M = np.float32([[1, 0, tx], [0, 1, ty]])
translated = cv2.warpAffine(img, M, (w, h))
我的习惯:做数据增强时,我经常用随机平移来增加样本多样性。平移范围一般控制在图像尺寸的10%-20%。
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)
# 执行仿射变换
affined = cv2.warpAffine(img, M, (w, h))
实战经验:做文档矫正时,我通常选取文档的四个角中的三个点。但要注意,三个点不能共线,否则矩阵会奇异。
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)
# 执行透视变换
warped = cv2.warpPerspective(img, M, (300, 400))
经典应用:文档扫描、车牌矫正、广告牌替换。我在做车牌识别时,就是用透视变换把倾斜的车牌矫正成正视图,识别率提升了30%。
3.6 实战:综合应用
咱们来做一个完整的例子:从图像中提取一个区域,然后进行透视矫正。
def perspective_correction(image, points):
"""
透视矫正函数
points: 原始图像中的四个点,顺序为左上、右上、左下、右下
"""
# 计算目标矩形的尺寸
width = max(
np.linalg.norm(points[1] - points[0]),
np.linalg.norm(points[2] - points[3])
)
height = max(
np.linalg.norm(points[3] - points[0]),
np.linalg.norm(points[2] - points[1])
)
# 目标点
dst_pts = np.float32([
[0, 0],
[width - 1, 0],
[0, height - 1],
[width - 1, height - 1]
])
# 计算透视变换矩阵
M = cv2.getPerspectiveTransform(points, dst_pts)
# 执行变换
warped = cv2.warpPerspective(image, M, (int(width), int(height)))
return warped
# 使用示例
# 假设我们检测到了文档的四个角点
corners = np.float32([[100, 150], [400, 140], [80, 500], [420, 490]])
result = perspective_correction(img, corners)
重要提醒:点的顺序很重要。如果顺序错了,矫正结果会完全变形。我习惯用左上、右上、左下、右下的顺序。
3.7 性能优化建议
| 操作 | 耗时 | 优化建议 |
|---|---|---|
| 缩放 | 低 | 缩小用INTER_AREA,放大用INTER_LINEAR |
| 旋转 | 中 | 预计算旋转矩阵,避免重复计算 |
| 仿射变换 | 中 | 使用单精度浮点数,减少内存占用 |
| 透视变换 | 高 | 缩小图像后再做透视变换,最后再放大 |
我的优化技巧:做视频处理时,如果变换矩阵不变,我会缓存矩阵。这样每帧只需要做 warpAffine,省去了计算矩阵的时间。
嗯,几何变换的内容就这些。说白了,核心就是理解变换矩阵的含义。你想想看,掌握了这些,你就能让图像「随心所欲」地变形了。
下一章咱们聊聊图像滤波,这可是去噪的利器。到时候见!