3. OpenCV基础(二):图像几何变换、滤波与边缘检测
各位好,欢迎来到第三章。上一章我们把OpenCV的基本IO和像素操作捋了一遍,算是把工具箱打开了。这一章咱们要往里塞几个真正干活用的家伙——几何变换、图像滤波、边缘检测。这三板斧在工业项目里几乎天天见,我自己的经验是,搞懂它们,你就能处理掉80%的图像预处理需求。
本章核心脉络:几何变换解决“图像怎么摆”的问题,滤波解决“图像干不干净”的问题,边缘检测解决“图像里有什么轮廓”的问题。三者层层递进,是后续YOLO训练数据增强和预处理的理论基础。
3.1 图像几何变换:让图像动起来
几何变换说白了就是改变图像里像素的位置。你想想看,拍照时角度歪了、物体太小了、或者想做个数据增强,都得靠它。OpenCV里最常用的就四种:缩放、旋转、仿射、透视。
3.1.1 缩放与旋转
缩放用 cv2.resize(),这个最简单。但我提醒一句:插值方法选不对,效果天差地别。
| 场景 | 推荐插值方法 | 原因 |
|---|---|---|
| 缩小图像 | cv2.INTER_AREA |
避免摩尔纹,保留区域特征 |
| 放大图像 | cv2.INTER_LINEAR 或 cv2.INTER_CUBIC |
线性/三次插值,边缘更平滑 |
| YOLO训练数据增强 | cv2.INTER_LINEAR |
速度与质量平衡,我项目里一直用这个 |
import cv2
import numpy as np
img = cv2.imread('example.jpg')
# 缩放:指定宽高
resized = cv2.resize(img, (416, 416), interpolation=cv2.INTER_LINEAR)
# 缩放:按比例
h, w = img.shape[:2]
scale_resized = cv2.resize(img, None, fx=0.5, fy=0.5, interpolation=cv2.INTER_AREA)
# 旋转:先获取旋转矩阵
center = (w // 2, h // 2)
M = cv2.getRotationMatrix2D(center, 45, 1.0) # 旋转45度,不缩放
rotated = cv2.warpAffine(img, M, (w, h))
我的经验:做YOLO训练时,我习惯把图像统一缩放到416x416或640x640,用INTER_LINEAR。别用INTER_NEAREST,那个锯齿感太强,模型学到的特征会带噪声。
3.1.2 仿射变换与透视变换
这两个容易搞混,我简单说一下区别:
- 仿射变换:保持平行线平行。平移、旋转、缩放、剪切都属于仿射。需要3个点对来求解变换矩阵。
- 透视变换:平行线可能不再平行。模拟人眼视角变化,比如把倾斜的文档拍正。需要4个点对。
我曾经在做一个车牌识别项目时,摄像头安装角度不对,拍出来的车牌是梯形的。用仿射变换怎么调都歪,后来换成透视变换,一下就正了。嗯,这里要注意:透视变换的4个点必须按顺序(左上、右上、左下、右下)传入,否则图像会拧成麻花。
# 仿射变换:3个点
pts1 = np.float32([[50,50], [200,50], [50,200]])
pts2 = np.float32([[10,100], [200,50], [100,250]])
M_affine = cv2.getAffineTransform(pts1, pts2)
affine_img = cv2.warpAffine(img, M_affine, (w, h))
# 透视变换: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)
perspective_img = cv2.warpPerspective(img, M_perspective, (300, 300))
避坑指南:我曾经在透视变换时没注意坐标顺序,结果图像被翻转了180度。调试了半天才发现是点顺序错了。建议用np.float32确保数据类型,否则OpenCV会报类型错误。
3.2 图像滤波:去噪与保边
滤波的本质是“用邻域像素的某种统计值替换当前像素”。说白了就是让图像变模糊,或者去掉噪点。但不同滤波器的脾气不一样,选错了要么糊成一片,要么噪点纹丝不动。
3.2.1 四种滤波器对比
| 滤波器 | 原理 | 适用场景 | 缺点 |
|---|---|---|---|
| 均值滤波 | 邻域像素取平均 | 简单去噪,要求不高时 | 边缘模糊严重 |
| 高斯滤波 | 按高斯权重加权平均 | 通用去噪,保留一定边缘 | 对椒盐噪声效果一般 |
| 中值滤波 | 取邻域像素中位数 | 椒盐噪声、脉冲噪声 | 计算慢,对大噪声效果有限 |
| 双边滤波 | 空间距离+像素值差异双重权重 | 保边去噪,美颜、预处理 | 参数敏感,速度慢 |
# 四种滤波一行代码
img_blur = cv2.blur(img, (5, 5)) # 均值
img_gaussian = cv2.GaussianBlur(img, (5, 5), 1.5) # 高斯
img_median = cv2.medianBlur(img, 5) # 中值
img_bilateral = cv2.bilateralFilter(img, 9, 75, 75) # 双边
核心建议:做边缘检测前,我通常先用高斯滤波去噪。双边滤波虽然保边效果好,但参数调起来很费时间。如果你只是做YOLO训练前的预处理,高斯滤波+中值滤波的组合就够用了。
3.2.2 参数怎么调?
我个人的习惯是:
- 核大小:必须是奇数。3x3、5x5、7x7最常见。核越大,模糊越强,但细节丢失也越多。
- 高斯滤波的sigma:一般设为0,OpenCV会自动根据核大小计算。手动设的话,sigma越大越模糊。
- 双边滤波的sigmaColor和sigmaSpace:sigmaColor控制像素值差异的敏感度,sigmaSpace控制空间距离的影响。我一般从sigmaColor=50、sigmaSpace=50开始调。
你想想看,如果sigmaColor设得太大,双边滤波就跟高斯滤波没区别了,保边效果就没了。反过来设得太小,噪点根本去不掉。这个平衡点需要根据实际图像试。
3.3 边缘检测:Canny算子
边缘检测是计算机视觉的基石。YOLO虽然不直接依赖边缘检测,但做数据预处理、背景分割、ROI提取时,Canny几乎是标配。
3.3.1 Canny的四个步骤
Canny不是简单的算子,而是一个流程。我当年面试时被问过好几次,其实记住四个步骤就行:
- 高斯滤波去噪——先让图像干净点
- 计算梯度幅值和方向——用Sobel算子算水平和垂直梯度
- 非极大值抑制——把不是边缘的像素去掉,只保留局部梯度最大的点
- 双阈值检测与边缘连接——高阈值确定强边缘,低阈值保留弱边缘,但只有与强边缘相连的弱边缘才保留
# Canny边缘检测:一行代码搞定
edges = cv2.Canny(img, 100, 200) # 低阈值100,高阈值200
# 如果想先滤波再检测,可以这样
blurred = cv2.GaussianBlur(img, (5, 5), 1.5)
edges = cv2.Canny(blurred, 50, 150)
3.3.2 双阈值怎么设?
这是Canny里最让人头疼的地方。我分享一个经验:
- 高阈值:设为图像最大梯度值的70%-80%。可以用
np.max()先看看梯度图的范围。 - 低阈值:设为高阈值的1/2到1/3。比如高阈值200,低阈值就设100或66。
- 经验值:对于光照均匀的图片,我常用(50, 150)或(100, 200)。对于光照变化大的场景,我会用(30, 100)。
调试技巧:我曾经在调试Canny参数时,写了个滑动条回调函数,实时调整阈值看效果。你可以用cv2.createTrackbar()快速搭建一个调参工具,比硬编码改值高效得多。
3.3.3 Canny vs Sobel vs Laplacian
简单说两句:Sobel和Laplacian是基础算子,直接算梯度。Canny是它们的“升级版”,加了非极大值抑制和双阈值,所以边缘更细、更干净。我个人的建议是:除非你有特殊需求,否则直接用Canny。Sobel和Laplacian更适合做梯度特征提取,而不是最终边缘输出。
# Sobel和Laplacian作为补充
sobel_x = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3)
sobel_y = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3)
laplacian = cv2.Laplacian(img, cv2.CV_64F)
注意:Sobel和Laplacian输出的是浮点数,直接显示会全黑或全白。记得用cv2.convertScaleAbs()转成8位无符号整数再显示。我刚开始学时就踩过这个坑,显示出来一片黑,还以为代码写错了。
3.4 实战组合:滤波+边缘检测+几何变换
最后给一个实际项目中的组合用法。比如你要从一张照片里提取某个物体的轮廓,然后把它摆正:
# 1. 读取图像
img = cv2.imread('object.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 2. 高斯滤波去噪
blurred = cv2.GaussianBlur(gray, (5, 5), 1.5)
# 3. Canny边缘检测
edges = cv2.Canny(blurred, 50, 150)
# 4. 找轮廓(下一章会细讲)
contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 5. 假设找到了一个矩形轮廓,做透视变换摆正
# 这里省略轮廓筛选和点提取的细节
# 最终用 cv2.warpPerspective() 把物体摆正
这个流程我在做工业零件检测时用过很多次。先滤波去噪,再边缘检测找轮廓,最后用透视变换把零件摆正,方便后续测量或识别。每一步的参数都需要根据实际图像微调,但整体框架是通用的。