3、OpenCV核心基础(二):图像几何变换、滤波与边缘检测
各位同学,欢迎来到第三章。上一章我们聊了图像的基本操作,比如读取、显示、像素操作这些。这一章,我们要动真格的了——让图像真正“变形”和“净化”。
说白了,图像几何变换就是让图像换个姿势,滤波就是给它“磨皮”或者“锐化”,而边缘检测,嗯,就是帮我们找到图像里那些“轮廓分明”的地方。这三个技能,在后续做目标检测时几乎每步都会用到。
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)
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.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.3 边缘检测:找到图像的“骨架”
边缘是图像中像素值发生剧烈变化的地方。说白了,就是物体的轮廓。Canny 边缘检测是业界标准,几乎所有的目标检测流程都会用到它。
3.3.1 Canny 算法原理
Canny 算法分四步走:
- 高斯滤波:先降噪,避免噪声被误判为边缘
- 计算梯度:用 Sobel 算子算水平和垂直方向的梯度
- 非极大值抑制:只保留梯度方向上的局部最大值,让边缘变细
- 双阈值检测:高阈值确定强边缘,低阈值连接弱边缘
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)
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
嗯,这里要注意:滤波和边缘检测的顺序不能颠倒。先滤波再检测,否则噪声会被当成边缘。我曾经犯过这个错误,结果检测出来的边缘密密麻麻全是噪点,根本没法用。