3、OpenCV核心基础(二):图像几何变换、滤波与边缘检测

各位同学,欢迎来到第三章。上一章我们聊了图像的基本操作,比如读取、显示、像素操作这些。这一章,我们要动真格的了——让图像真正“变形”和“净化”。

说白了,图像几何变换就是让图像换个姿势,滤波就是给它“磨皮”或者“锐化”,而边缘检测,嗯,就是帮我们找到图像里那些“轮廓分明”的地方。这三个技能,在后续做目标检测时几乎每步都会用到。

本章核心脉络: 几何变换(缩放/旋转/仿射)→ 图像滤波(模糊/去噪)→ 边缘检测(Canny算法)。这三步是图像预处理的标准流水线。
第3章:图像几何变换 · 滤波 · 边缘检测 几何变换 图像滤波 边缘检测 缩放 旋转 均值滤波 高斯滤波 Canny Sobel 预处理流水线:先几何校正 → 再滤波去噪 → 最后提取边缘 每一步的输出都是下一步的输入,顺序不要搞反

3.1 图像几何变换:让图像换个姿势

几何变换,说白了就是改变图像中像素的位置。你想想看,摄像头拍到的画面角度不对怎么办?目标在图像里太小怎么办?这时候就需要几何变换出手了。

3.1.1 图像缩放

缩放是最常用的操作。OpenCV 里用 cv2.resize() 搞定。我记得刚入行时,有个项目需要把 4K 图像缩放到 640×480 再送进模型,结果直接用了默认参数,图像锯齿严重,检测率掉了 15 个点。后来才发现是插值方法没选对。

插值方法适用场景特点
cv2.INTER_NEAREST像素艺术、最邻近插值速度快,有锯齿
cv2.INTER_LINEAR一般缩放(默认)速度与质量均衡
cv2.INTER_CUBIC放大图像质量高,计算慢
cv2.INTER_AREA缩小图像避免摩尔纹,推荐
import cv2

img = cv2.imread('input.jpg')
# 缩小:用 INTER_AREA 避免锯齿
small = cv2.resize(img, (640, 480), interpolation=cv2.INTER_AREA)
# 放大:用 INTER_CUBIC 保持细节
big = cv2.resize(img, None, fx=2.0, fy=2.0, interpolation=cv2.INTER_CUBIC)
我的习惯: 缩小用 INTER_AREA,放大用 INTER_CUBIC。别偷懒用默认的 LINEAR,尤其是做目标检测预处理时,插值方式直接影响小目标的检测效果。

3.1.2 图像旋转

旋转需要两步走:先算旋转矩阵,再做仿射变换。OpenCV 提供了 cv2.getRotationMatrix2D() 和 cv2.warpAffine()。

import cv2
import numpy as np

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

这里有个坑——旋转后图像边缘会被裁剪。我曾经在做一个车牌识别项目时,旋转校正后车牌字符被切掉了一半,模型死活识别不出来。解决方案是手动计算旋转后的新画布尺寸,把 warpAffine 的第三个参数设大一点。

3.1.3 仿射变换

仿射变换是旋转、平移、缩放的“全家桶”。它用 2×3 的矩阵表示,能保持图像的“平行性”——直线还是直线,平行线还是平行线。

# 原图三个点
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 个点对就能确定矩阵。如果图像畸变严重(比如鱼眼镜头),仿射变换就不够用了,得用透视变换(需要 4 个点对)。这个我们后面章节会讲。

3.2 图像滤波:给图像“磨皮”

滤波的本质是“卷积”——用一个小的核在图像上滑动,每个像素的值被它周围像素的加权平均替代。为什么要做这个?因为摄像头采集的图像通常有噪声,噪声会干扰后续的边缘检测和目标识别。

3.2.1 均值滤波

最简单的滤波,核内所有像素取平均。速度快,但会模糊边缘。

blurred = cv2.blur(img, (5, 5))  # 5x5 核

3.2.2 高斯滤波

高斯滤波给中心像素更高的权重,边缘保留比均值滤波好一些。我建议你把它作为默认的滤波方式。

gaussian = cv2.GaussianBlur(img, (5, 5), 1.5)  # sigma=1.5

3.2.3 中值滤波

中值滤波对椒盐噪声(黑白点噪声)有奇效。它取核内所有像素的中位数,而不是平均值。我在做工业质检项目时,产线摄像头经常因为灰尘产生椒盐噪声,中值滤波一上,效果立竿见影。

median = cv2.medianBlur(img, 5)  # 核大小必须是奇数
滤波方式噪声类型边缘保留速度
均值滤波高斯噪声差快
高斯滤波高斯噪声中中
中值滤波椒盐噪声好慢
避坑指南: 滤波核大小必须是奇数(3、5、7...),否则 OpenCV 会报错。核越大,模糊越强,但计算量也越大。对于 640×480 的图像,5×5 核通常够用。

3.3 边缘检测:找到图像的“骨架”

边缘是图像中像素值发生剧烈变化的地方。说白了,就是物体的轮廓。Canny 边缘检测是业界标准,几乎所有的目标检测流程都会用到它。

3.3.1 Canny 算法原理

Canny 算法分四步走:

  1. 高斯滤波:先降噪,避免噪声被误判为边缘
  2. 计算梯度:用 Sobel 算子算水平和垂直方向的梯度
  3. 非极大值抑制:只保留梯度方向上的局部最大值,让边缘变细
  4. 双阈值检测:高阈值确定强边缘,低阈值连接弱边缘
edges = cv2.Canny(img, 100, 200)  # 低阈值100,高阈值200

双阈值怎么调?我有个经验法则:高阈值设为低阈值的 2~3 倍。低阈值太高会丢失边缘,太低会引入太多噪声。我在做车道线检测时,低阈值从 50 试到 150,最后发现 100/200 的组合最适合高速公路场景。

3.3.2 Sobel 算子

Sobel 算子是 Canny 的“前身”,直接计算一阶导数。它分别计算 x 方向和 y 方向的梯度,然后合成。

sobelx = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3)
sobely = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3)
sobel = cv2.magnitude(sobelx, sobely)
我的建议: 如果你只需要边缘做可视化,直接用 Canny。如果你需要边缘的方向信息(比如做特征匹配),用 Sobel 保留梯度方向。两个函数各有所长,看场景选。

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

把今天学的串起来,就是一个标准的预处理流水线。我在做 YOLO 训练数据预处理时,经常用这个流程:

def preprocess_pipeline(img, target_size=(640, 640)):
    # 1. 缩放
    img = cv2.resize(img, target_size, interpolation=cv2.INTER_AREA)
    # 2. 高斯滤波去噪
    img = cv2.GaussianBlur(img, (5, 5), 1.5)
    # 3. Canny 边缘检测
    edges = cv2.Canny(img, 100, 200)
    return edges

嗯,这里要注意:滤波和边缘检测的顺序不能颠倒。先滤波再检测,否则噪声会被当成边缘。我曾经犯过这个错误,结果检测出来的边缘密密麻麻全是噪点,根本没法用。

本章小结: 几何变换让图像“摆正姿势”,滤波让图像“干净清爽”,边缘检测让图像“轮廓分明”。这三板斧是 OpenCV 图像处理的基石,也是后续 YOLO 目标检测预处理的核心环节。多动手写代码,调参数,你会找到最适合自己场景的那组配置。