3. 图像处理基础(下):图像滤波与去噪、边缘检测(Canny/Sobel)、形态学操作
好,咱们接着往下聊。上一节我们把图像的基本操作和色彩空间过了一遍,这一节要进入真正“干活”的部分了。
你想想看,一张从摄像头拍回来的交通标志照片,能直接用吗?大概率不行。路上有灰尘、光线不均匀、车牌反光、运动模糊……这些噪声不处理掉,后面的识别准确率会惨不忍睹。所以,滤波、去噪、边缘检测、形态学操作,这些就是咱们的“预处理三板斧”。
3.1 图像滤波与去噪:把脏东西洗掉
滤波,说白了就是用一个小窗口(核)在图像上滑动,把每个像素的值替换成它周围像素的某种“平均值”。不同的“平均”方式,就产生了不同的滤波效果。
3.1.1 均值滤波
最简单粗暴的方法。把核覆盖的所有像素加起来,除以像素个数。嗯,就像你拿一块抹布在图像上抹一下,模糊了,但噪声也淡了。
import cv2
import numpy as np
img = cv2.imread('traffic_sign.jpg')
# 5x5的核,核越大越模糊
blurred = cv2.blur(img, (5, 5))
cv2.imshow('Mean Blur', blurred)
3.1.2 高斯滤波
高斯滤波比均值滤波聪明一点。它不再是一视同仁地平均,而是根据像素离中心点的距离,给不同的权重。离得近的权重大,离得远的权重小。这个权重分布,就是高斯分布。
我在项目中遇到过这样的情况:用均值滤波去噪,结果标志的白色边框和红色底色糊在一起了。换成高斯滤波,边缘保留得好很多。
# 高斯滤波,核大小5x5,sigmaX=0表示自动计算
gaussian = cv2.GaussianBlur(img, (5, 5), 0)
cv2.imshow('Gaussian Blur', gaussian)
3.1.3 中值滤波
这个我要重点说一下。中值滤波不是算平均,而是把核覆盖的所有像素值排序,取中间那个值。它对椒盐噪声(就是那种黑白点点的噪声)有奇效。
我曾经处理过一张在强光下拍摄的限速标志,上面全是白点。均值滤波搞不定,高斯滤波也一般,换成中值滤波,一下就干净了。
# 中值滤波,核大小必须是奇数
median = cv2.medianBlur(img, 5)
cv2.imshow('Median Blur', median)
| 滤波方法 | 去噪效果 | 边缘保留 | 适用场景 |
|---|---|---|---|
| 均值滤波 | 一般 | 差 | 简单去噪,要求不高 |
| 高斯滤波 | 较好 | 中等 | 通用去噪,保留一定边缘 |
| 中值滤波 | 优秀(椒盐噪声) | 好 | 椒盐噪声、脉冲噪声 |
3.2 边缘检测:找到物体的“骨架”
去完噪,下一步就是找边缘。边缘是图像中像素值发生剧烈变化的地方。说白了,就是物体的轮廓。交通标志的圆形、三角形、矩形,全靠边缘来提取。
3.2.1 Sobel算子
Sobel算子是一种基于一阶导数的边缘检测方法。它用两个3x3的核,一个检测水平方向的变化,一个检测垂直方向的变化。
# 先转成灰度图
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Sobel在x方向
sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3)
# Sobel在y方向
sobely = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3)
# 取绝对值并转成uint8
sobelx = np.uint8(np.absolute(sobelx))
sobely = np.uint8(np.absolute(sobely))
# 合并
sobel_combined = cv2.bitwise_or(sobelx, sobely)
cv2.imshow('Sobel', sobel_combined)
3.2.2 Canny边缘检测
Canny是边缘检测的“扛把子”。它比Sobel复杂,但效果也更好。它做了四件事:去噪(高斯滤波)、计算梯度、非极大值抑制、双阈值检测。
非极大值抑制是啥意思?说白了,就是只保留梯度方向上的局部最大值,把那些“不够尖”的边缘去掉。双阈值检测呢?设定一个高阈值和一个低阈值,高于高阈值的肯定是边缘,低于低阈值的肯定不是,介于中间的,如果跟确定的边缘相连,就算它是边缘。
我记得第一次用Canny的时候,调阈值调了半天。后来发现,对于交通标志,低阈值设50,高阈值设150,效果通常不错。
# Canny边缘检测
# 参数:图像,低阈值,高阈值
edges = cv2.Canny(gray, 50, 150)
cv2.imshow('Canny Edges', edges)
3.3 形态学操作:修修补补的艺术
边缘检测完了,得到的二值图像往往不完美。边缘有断裂、有空洞、有多余的小点。这时候,形态学操作就派上用场了。它就像给图像做“整形手术”。
3.3.1 腐蚀与膨胀
腐蚀:让白色区域“瘦”一圈。可以去掉小的白点噪声。
膨胀:让白色区域“胖”一圈。可以连接断裂的边缘。
# 定义结构元素(核)
kernel = np.ones((5,5), np.uint8)
# 腐蚀
erosion = cv2.erode(edges, kernel, iterations=1)
# 膨胀
dilation = cv2.dilate(edges, kernel, iterations=1)
cv2.imshow('Erosion', erosion)
cv2.imshow('Dilation', dilation)
3.3.2 开运算与闭运算
开运算 = 先腐蚀后膨胀。用来去除小白点噪声,同时保持大物体形状不变。
闭运算 = 先膨胀后腐蚀。用来填充物体内部的小空洞,连接断裂的轮廓。
我曾经在处理一个“禁止停车”标志时,Canny检测出来的圆形边缘断成了好几段。用闭运算一搞,立马连成完整的圆。嗯,那种感觉,很爽。
# 开运算
opening = cv2.morphologyEx(edges, cv2.MORPH_OPEN, kernel)
# 闭运算
closing = cv2.morphologyEx(edges, cv2.MORPH_CLOSE, kernel)
cv2.imshow('Opening', opening)
cv2.imshow('Closing', closing)
3.4 实战小流程:从原图到干净边缘
好了,把今天学的串起来。一个典型的交通标志预处理流程是这样的:
- 读取图像,转成灰度图
- 高斯滤波,去除高斯噪声
- Canny边缘检测,得到初步边缘
- 闭运算,连接断裂的轮廓
- 开运算,去除小的噪声点
def preprocess_for_edge_detection(img_path):
img = cv2.imread(img_path)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 1. 高斯滤波
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
# 2. Canny边缘检测
edges = cv2.Canny(blurred, 50, 150)
# 3. 形态学操作
kernel = np.ones((3,3), np.uint8)
closed = cv2.morphologyEx(edges, cv2.MORPH_CLOSE, kernel, iterations=1)
cleaned = cv2.morphologyEx(closed, cv2.MORPH_OPEN, kernel, iterations=1)
return cleaned
# 调用
result = preprocess_for_edge_detection('stop_sign.jpg')
cv2.imshow('Final Result', result)
这个流程,我用了好几年,在多个交通标志数据集上验证过,效果稳定。当然,具体参数要根据你的图像质量微调。但大方向,就是这个路子。
下一节,我们会基于这些干净的边缘,去提取轮廓、计算形状特征,然后真正开始识别交通标志。准备好了吗?