4、图像预处理(上):图像缩放、图像裁剪、图像旋转与仿射变换、图像翻转
图像预处理,说白了就是给原始图像「洗个澡、理个发」,让它变成算法喜欢的样子。我刚开始做计算机视觉项目时,总觉得预处理是小事,结果模型死活不收敛,最后发现是输入尺寸没对齐。嗯,从那以后我再也不敢小看这一步了。
这一章我们先聊四个最基础的操作:缩放、裁剪、旋转与仿射变换、翻转。它们看似简单,但坑不少,我一个个说。
4.1 图像缩放:别让像素「打架」
图像缩放,就是把图片变大或变小。你想想看,一张 1920×1080 的图直接塞进 224×224 的模型,不缩放怎么行?
缩放的核心是插值算法。我简单列一下常用的几种:
| 插值方法 | 特点 | 我常用的场景 |
|---|---|---|
| 最近邻插值 | 速度快,但锯齿明显 | 像素风格、实时预览 |
| 双线性插值 | 平滑,速度适中 | 日常缩放,大部分情况够用 |
| 双三次插值 | 更平滑,计算量大 | 高质量输出,比如打印 |
| Lanczos 插值 | 细节保留好,边缘锐利 | 遥感图像、医学图像 |
OpenCV 里缩放用 cv2.resize(),举个例子:
import cv2
img = cv2.imread('cat.jpg')
# 指定目标尺寸
resized = cv2.resize(img, (224, 224), interpolation=cv2.INTER_LINEAR)
# 按比例缩放
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 是 (高度, 宽度, 通道)。我刚开始就搞反过,调试了半天才发现图是歪的。
4.2 图像裁剪:只取精华部分
裁剪就是切图。为什么要切?因为原始图像里可能有很多无关区域。比如做车牌识别,你总得先把车牌从整车照片里切出来吧?
裁剪在 OpenCV 里其实就是数组切片,简单到不行:
# 裁剪 [y1:y2, x1:x2]
cropped = img[100:400, 200:500]
但这里有个坑——边界检查。我在项目中遇到过,训练数据里有一张图特别小,裁剪坐标超出了图像范围,程序直接崩了。所以一定要加判断:
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)
cropped = img[y1:y2, x1:x2]
4.3 图像旋转与仿射变换:不只是转个角度
旋转看起来简单,但实际做起来有门道。直接旋转会怎样?图像会超出边界,或者出现黑边。
OpenCV 里旋转需要两步:先算旋转矩阵,再应用仿射变换。
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 - w) / 2
M[1, 2] += (new_h - h) / 2
rotated = cv2.warpAffine(img, M, (new_w, new_h))
仿射变换不止能旋转,还能做平移、缩放、错切。说白了,任何保持「平行线依旧平行」的变换,都是仿射变换。通用公式是:
# 自定义仿射变换矩阵
# 通过三个点映射来定义
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.4 图像翻转:镜像世界的秘密
翻转就是镜像。水平翻转、垂直翻转、同时翻转。在 OpenCV 里用 cv2.flip():
# 水平翻转(左右镜像)
flip_h = cv2.flip(img, 1)
# 垂直翻转(上下颠倒)
flip_v = cv2.flip(img, 0)
# 同时翻转
flip_hv = cv2.flip(img, -1)
翻转在数据增强里特别常用。比如做目标检测,水平翻转后,标注框的坐标也要跟着变。我习惯这样处理:
def horizontal_flip(img, bboxes):
h, w = img.shape[:2]
flipped_img = cv2.flip(img, 1)
flipped_bboxes = []
for bbox in bboxes:
x1, y1, x2, y2 = bbox
# 水平翻转后,x 坐标变成 w - x
new_x1 = w - x2
new_x2 = w - x1
flipped_bboxes.append([new_x1, y1, new_x2, y2])
return flipped_img, flipped_bboxes
4.5 组合使用:预处理流水线
实际项目中,这些操作很少单独用。我一般会搭一个预处理流水线:
- 先裁剪:去掉无关背景,减少计算量
- 再缩放:统一到模型输入尺寸
- 然后翻转/旋转:数据增强,随机应用
- 最后归一化:把像素值缩放到 [0,1] 或 [-1,1]
举个例子,一个简单的训练预处理函数:
def preprocess(img, target_size=(224, 224), augment=False):
# 裁剪中心区域
h, w = img.shape[:2]
crop_size = min(h, w)
start_x = (w - crop_size) // 2
start_y = (h - crop_size) // 2
img = img[start_y:start_y+crop_size, start_x:start_x+crop_size]
# 缩放
img = cv2.resize(img, target_size)
# 数据增强
if augment:
if np.random.rand() > 0.5:
img = cv2.flip(img, 1) # 随机水平翻转
angle = np.random.randint(-10, 10)
M = cv2.getRotationMatrix2D((112, 112), angle, 1.0)
img = cv2.warpAffine(img, M, target_size)
# 归一化
img = img.astype(np.float32) / 255.0
return img
嗯,这一章就到这里。图像预处理看似基础,但做得好不好,直接决定模型的上限。下一章我们继续聊滤波、直方图均衡化和形态学操作,那些才是真正让图像「脱胎换骨」的功夫。