3、OpenCV基础回顾(二):图像缩放、裁剪、旋转、仿射变换实战

好,咱们接着聊。上一节我们把图像的读取、显示、保存这些基本功过了一遍。这一节,咱们要动真格的了——图像几何变换。

说白了,就是怎么把一张图“折腾”成你想要的样子。缩放、裁剪、旋转,还有那个听起来有点吓人的仿射变换。别怕,我带你一个个拆解。

3.1 图像缩放:resize() 的学问

图像缩放,最常用的就是 cv2.resize()。这个函数看着简单,但坑不少。我刚开始用的时候,就吃过它的亏。

import cv2

img = cv2.imread('cat.jpg')
# 指定目标尺寸 (宽, 高)
resized = cv2.resize(img, (300, 200))
# 或者按比例缩放
scale_percent = 60  # 缩放60%
width = int(img.shape[1] * scale_percent / 100)
height = int(img.shape[0] * scale_percent / 100)
resized_ratio = cv2.resize(img, (width, height))

注意: cv2.resize() 的第二个参数是 (宽度, 高度),而 img.shape 返回的是 (高度, 宽度, 通道数)。顺序别搞反了!我曾经因为这个 bug 调了一下午。

还有一个关键参数——插值方法。默认是双线性插值,够用。但如果你要缩小图片,我建议用 cv2.INTER_AREA;放大图片,用 cv2.INTER_CUBIC 效果更好。

我的经验: 实际项目中,我习惯先按比例缩放,再裁剪到目标尺寸。这样能保证图像不变形。比如做目标检测时,输入尺寸是 640x640,我会先保持宽高比缩放,然后填充或裁剪。

3.2 图像裁剪:NumPy 切片大法

裁剪其实不用 OpenCV 的特殊函数。NumPy 的切片操作就够了。简单粗暴,但好用。

# 裁剪 ROI (Region of Interest)
# 语法: img[y_start:y_end, x_start:x_end]
roi = img[100:300, 200:400]  # 裁剪出 (200,100) 到 (400,300) 的区域
cv2.imshow('ROI', roi)

你想想看,为什么这么简单?因为 OpenCV 的图像在 Python 里就是一个 NumPy 数组。数组怎么切,图像就怎么切。

避坑指南: 切片时注意坐标范围不要越界。我曾经在写一个批量处理脚本时,没检查边界,结果程序跑着跑着就崩了。后来我加了个判断:

h, w = img.shape[:2]
x1, y1, x2, y2 = 100, 50, 400, 300
x1, y1 = max(0, x1), max(0, y1)
x2, y2 = min(w, x2), min(h, y2)
roi = img[y1:y2, x1:x2]

3.3 图像旋转:cv2.getRotationMatrix2D + warpAffine

旋转稍微复杂一点。OpenCV 没有直接的 cv2.rotate() 函数(其实有,但只能做 90、180、270 度旋转)。要做任意角度旋转,得用仿射变换。

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

# 应用仿射变换
rotated = cv2.warpAffine(img, rotation_matrix, (img.shape[1], img.shape[0]))

这里有个问题:旋转后,图像四个角可能会被裁剪掉。为什么?因为旋转后的图像尺寸没变,但内容已经转出去了。

我的解决方案: 计算旋转后的新边界,调整输出尺寸。这样就能看到完整的旋转图像了。

# 计算旋转后的新尺寸
cos = abs(rotation_matrix[0, 0])
sin = abs(rotation_matrix[0, 1])
new_w = int(img.shape[0] * sin + img.shape[1] * cos)
new_h = int(img.shape[0] * cos + img.shape[1] * sin)

# 调整旋转矩阵的平移部分
rotation_matrix[0, 2] += new_w / 2 - center[0]
rotation_matrix[1, 2] += new_h / 2 - center[1]

rotated_full = cv2.warpAffine(img, rotation_matrix, (new_w, new_h))

3.4 仿射变换:从理论到实战

仿射变换,说白了就是“线性变换 + 平移”。它能实现缩放、旋转、平移、错切(shear)等操作。核心是找到一个 2x3 的变换矩阵。

OpenCV 提供了 cv2.getAffineTransform(),你只需要提供三组对应点,它就能算出变换矩阵。

# 原图中的三个点
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, (img.shape[1], img.shape[0]))

实战场景: 我在做车牌识别时,经常遇到车牌倾斜的情况。这时候我会手动标出车牌的四个角点,然后用仿射变换把它“拉正”。效果立竿见影。

还有一个更灵活的——cv2.getPerspectiveTransform(),它需要四组对应点,能做透视变换。比如把一张歪着的文档照片矫正成正面视角。

# 透视变换需要4个点
pts1 = np.float32([[56, 65], [368, 52], [28, 387], [389, 390]])
pts2 = np.float32([[0, 0], [300, 0], [0, 300], [300, 300]])

M_perspective = cv2.getPerspectiveTransform(pts1, pts2)
warped = cv2.warpPerspective(img, M_perspective, (300, 300))

3.5 实战:图像预处理流水线

好了,知识点讲完了。咱们来串一下。假设你要做一个图像分类任务,输入尺寸是 224x224。你的预处理流水线可能是这样的:

def preprocess_image(image_path, target_size=(224, 224)):
    img = cv2.imread(image_path)
    if img is None:
        return None
    
    # 1. 保持宽高比缩放
    h, w = img.shape[:2]
    scale = min(target_size[0] / w, target_size[1] / h)
    new_w, new_h = int(w * scale), int(h * scale)
    resized = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_AREA)
    
    # 2. 填充到目标尺寸
    delta_w = target_size[0] - new_w
    delta_h = target_size[1] - new_h
    top, bottom = delta_h // 2, delta_h - delta_h // 2
    left, right = delta_w // 2, delta_w - delta_w // 2
    padded = cv2.copyMakeBorder(resized, top, bottom, left, right, 
                                 cv2.BORDER_CONSTANT, value=[0, 0, 0])
    
    # 3. 数据增强(可选):随机旋转
    if np.random.random() > 0.5:
        angle = np.random.uniform(-10, 10)
        center = (padded.shape[1] // 2, padded.shape[0] // 2)
        M = cv2.getRotationMatrix2D(center, angle, 1.0)
        padded = cv2.warpAffine(padded, M, (target_size[0], target_size[1]))
    
    return padded

我的习惯: 预处理时,我一般会把缩放和填充分开做。这样能保留更多的图像信息,不会因为直接拉伸导致物体变形。尤其是在做 YOLO 训练时,这个细节会影响模型的定位精度。

3.6 总结与避坑清单

嗯,这一节内容不少。我帮你整理一下容易踩的坑:

操作 常见错误 正确做法
resize 宽高顺序搞反 resize(宽, 高),shape是(高, 宽)
裁剪 坐标越界 加边界检查
旋转 图像被裁剪 计算新尺寸,调整平移量
仿射变换 点对应关系错误 仔细核对 pts1 和 pts2 的对应顺序

最后说一句:这些操作看起来基础,但它们是整个计算机视觉 pipeline 的基石。我在做模型部署时,经常发现预处理和后处理占了推理时间的 30% 以上。所以,别小看这些“简单”的操作,优化好了,整个系统的性能能提升一大截。

下一节,咱们要进入图像滤波和边缘检测了。那是真正开始“看”图像特征的时候。做好准备!