4. 图像预处理(上):图像缩放、裁剪、旋转、仿射变换、透视变换

各位同学,欢迎来到第四章。

上一章我们聊了图像的基本读写和颜色空间。说实话,那些只是开胃菜。真正进入工程实战,你会发现——你拿到的原始图像,几乎从来不会直接符合模型或算法的输入要求。

为什么?

因为现实世界太「乱」了。摄像头角度歪了、物体太小了、背景太杂了、图片分辨率太高了……这些问题,统统需要图像预处理来解决。

今天这一章,我带你搞定图像预处理的上半场:几何变换。说白了,就是怎么把图像「掰」成你想要的样子。

4.1 图像缩放:别让像素打架

图像缩放,听起来最简单对吧?就是把图变大或变小。但这里有个坑——插值算法

我记得刚入行时,有个项目需要把一张 4K 图缩成 224x224 喂给分类网络。我直接用最近邻插值,结果模型精度掉了 3 个点。排查了半天,才发现是缩放方式不对。

OpenCV 里常用的缩放函数是 cv2.resize()。它有几个关键参数:

import cv2

img = cv2.imread('input.jpg')

# 指定目标尺寸
resized = cv2.resize(img, (224, 224))

# 按比例缩放(宽高各缩小一半)
scale_percent = 50
width = int(img.shape[1] * scale_percent / 100)
height = int(img.shape[0] * scale_percent / 100)
resized = cv2.resize(img, (width, height))

# 推荐:使用插值方式
resized = cv2.resize(img, (224, 224), interpolation=cv2.INTER_LINEAR)

这里我重点说一下插值方式的选择:

插值方法 适用场景 特点
INTER_NEAREST 像素艺术、标签图 最快,但锯齿严重
INTER_LINEAR 日常缩放(默认) 速度与质量均衡
INTER_CUBIC 放大高质量图像 慢但平滑
INTER_AREA 缩小图像 避免摩尔纹
我的习惯:缩小用 INTER_AREA,放大用 INTER_CUBIC。如果追求速度,统一用 INTER_LINEAR 也够用。

4.2 图像裁剪:切掉没用的部分

裁剪比缩放简单,就是取图像的一个矩形区域。但这里有个细节——坐标顺序

OpenCV 里图像是 numpy 数组,行是 y,列是 x。所以裁剪时,先写高度范围,再写宽度范围。

# 裁剪:从 (x, y) 开始,宽 w,高 h
x, y, w, h = 100, 50, 200, 300
cropped = img[y:y+h, x:x+w]

我曾经在项目里犯过一个低级错误:把坐标写反了,结果裁出来的图全是黑的。排查了半小时才发现是 y 和 x 搞混了。嗯,这种错误犯过一次就再也不会忘了。

注意:裁剪不会改变像素值,只是「切」出一块。如果你需要固定尺寸,裁剪后通常还要接一个缩放。

4.3 图像旋转:绕哪个点转?

旋转看起来简单,但实际做起来容易出问题。直接旋转的话,图像边缘会被裁掉。

OpenCV 提供了 cv2.rotate() 做 90 度、180 度、270 度的旋转。但更通用的做法是用仿射变换矩阵:

# 获取旋转矩阵(中心点、角度、缩放)
(h, w) = img.shape[:2]
center = (w // 2, h // 2)
M = cv2.getRotationMatrix2D(center, 45, 1.0)

# 执行旋转
rotated = cv2.warpAffine(img, M, (w, h))

这样旋转 45 度,四个角会被裁掉。如果你不想丢失信息,可以调整输出尺寸:

# 计算旋转后的新尺寸
import numpy as np
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]

rotated = cv2.warpAffine(img, M, (new_w, new_h))
核心要点:旋转时一定要想清楚——你是要「裁掉边缘」还是「保留全部」?这在做数据增强时特别重要。

4.4 仿射变换:平移、旋转、缩放的组合拳

仿射变换,说白了就是「线性变换 + 平移」。它能保持直线还是直线,平行线还是平行线。

你想想看,前面讲的缩放、旋转、平移,其实都是仿射变换的特例。

仿射变换需要 3 个点对来确定变换矩阵:

# 原图中的三个点
pts1 = np.float32([[50, 50], [200, 50], [50, 200]])
# 目标图中的三个点
pts2 = np.float32([[10, 100], [200, 50], [100, 250]])

# 计算仿射变换矩阵
M = cv2.getAffineTransform(pts1, pts2)

# 应用变换
transformed = cv2.warpAffine(img, M, (w, h))

我在做车牌识别时,经常用仿射变换来矫正倾斜的车牌。只要找到车牌的四个角点,就能把它「掰正」。

避坑指南:我曾经因为选的点太集中,导致变换矩阵不稳定。记住——三个点要尽量分散,最好覆盖整个图像区域。

4.5 透视变换:从「歪」到「正」

仿射变换搞不定的情况,就得请出透视变换了。

透视变换可以改变视角——比如把一张从侧面拍的照片,变成正面视角。它需要 4 个点对,而且这 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)

# 应用变换
warped = cv2.warpPerspective(img, M, (300, 400))

这个功能在 OCR、文档扫描、二维码识别中太常用了。说白了,就是把「拍歪了」的东西「摆正」。

重要提醒:透视变换的计算量比仿射变换大。如果你只是做平移旋转,别用透视变换,杀鸡用牛刀了。

4.6 实战经验总结

好了,这一章的内容不少。我帮你理一下思路:

  1. 缩放:注意插值方式,缩小用 INTER_AREA,放大用 INTER_CUBIC
  2. 裁剪:记住坐标顺序是 [y:y+h, x:x+w]
  3. 旋转:想清楚要不要保留全部内容
  4. 仿射变换:3 个点,保持平行性,适合矫正倾斜
  5. 透视变换:4 个点,改变视角,适合文档矫正

我个人建议,刚开始学的时候,把每个函数都跑一遍,看看不同参数的效果。图像处理这东西,光看理论没用,得动手试。

下一章,我们会继续聊图像预处理的下半场——滤波、边缘检测、形态学操作。那些才是真正让图像「变干净」的手段。

今天就到这里。有问题随时交流。

一句话总结:几何变换就是「把图像掰成你想要的样子」,但每种掰法都有自己的脾气,选对了事半功倍。