4. 图像预处理:图像滤波、边缘检测与形态学操作

各位同学,欢迎来到第四讲。上一章我们聊了图像的基本操作,今天咱们进入一个更核心的话题——图像预处理

说实话,在真实的交通标志检测项目里,你拿到的原始图像往往「脏」得不行。光照不均、镜头污渍、运动模糊……这些噪声如果不处理,后面的模型再牛也白搭。我自己的经验是,预处理花的时间,有时候比调模型还多。

今天我们就来搞定三件事:图像滤波(去噪)、边缘检测(找轮廓)、形态学操作(修形状)。


4.1 图像滤波:给图像「洗把脸」

滤波说白了就是去噪。你想想看,摄像头拍到的交通标志,上面可能沾了灰尘,或者因为光线太强出现了椒盐噪声(就是那种黑白小点)。这时候就需要滤波来「平滑」一下。

4.1.1 高斯滤波

高斯滤波是我最常用的滤波器。它的原理很简单:用一个高斯核(就是一个权重矩阵)去扫描整张图,每个像素的新值由它周围像素的加权平均决定。离中心越近,权重越大。

为什么用高斯?因为自然界很多噪声是服从高斯分布的。说白了,高斯滤波对高斯噪声的抑制效果最好。

核心参数: 核大小 (ksize) 和标准差 (sigma)。核越大,图像越模糊;sigma 控制权重分布的集中程度。
import cv2
import numpy as np

# 读取图像
img = cv2.imread('traffic_sign.jpg')

# 高斯滤波
# (5,5) 是核大小,必须为奇数;0 表示自动计算 sigma
gaussian_blur = cv2.GaussianBlur(img, (5, 5), 0)

# 显示结果
cv2.imshow('Original', img)
cv2.imshow('Gaussian Blur', gaussian_blur)
cv2.waitKey(0)
cv2.destroyAllWindows()
我的小经验: 核大小一般取 3、5、7 就够了。我做过一个项目,用 9x9 的核去处理小尺寸标志,结果标志边缘全糊了,检测率直接掉了一半。记住:核越大,细节丢失越严重。

4.1.2 中值滤波

中值滤波跟高斯滤波的思路完全不同。它不是算平均,而是取邻域内所有像素的中位数作为新值。

这招对付椒盐噪声特别管用。为什么?因为椒盐噪声是极端值(要么特别亮要么特别暗),取中位数就能完美避开这些异常点。

# 中值滤波
# 参数是核大小,这里用 5
median_blur = cv2.medianBlur(img, 5)
注意: 中值滤波的核大小也必须是奇数。我曾经犯过傻,传了个偶数进去,程序直接报错。嗯,这个坑我替你们踩过了。

对比一下两种滤波:高斯滤波适合处理高斯噪声(比如传感器热噪声),中值滤波适合处理椒盐噪声。实际项目中,我经常两个都试一下,看哪个效果更好。


4.2 边缘检测:Canny 算法

边缘检测是计算机视觉的「老本行」。你想识别交通标志,首先得知道标志的轮廓在哪。Canny 算法是业界公认的「黄金标准」。

Canny 算法其实是一套组合拳,包含四个步骤:

  1. 去噪:先用高斯滤波平滑图像
  2. 计算梯度:用 Sobel 算子算出每个像素的梯度强度和方向
  3. 非极大值抑制:只保留梯度方向上的局部最大值,把「胖」边缘变「瘦」
  4. 双阈值检测:用高低两个阈值,确定哪些是强边缘、哪些是弱边缘

这里最关键的就是双阈值。高阈值决定哪些边缘是「铁定算数」的,低阈值决定哪些边缘「可能算数」。介于两者之间的,如果跟强边缘相连,就保留;否则丢弃。

# Canny 边缘检测
# 参数:低阈值 50,高阈值 150
edges = cv2.Canny(img, 50, 150)

# 显示结果
cv2.imshow('Canny Edges', edges)
阈值怎么调? 我个人习惯用 1:2 或 1:3 的比例。比如低阈值 50,高阈值 100 或 150。如果图像噪声大,可以适当提高低阈值。

我记得有一次做高速公路限速牌检测,因为图像里有大量树影,Canny 检测出了无数条「假边缘」。后来我把低阈值从 30 提到 80,效果立竿见影。所以,阈值不是死的,要根据实际场景调。


4.3 形态学操作:膨胀与腐蚀

形态学操作,说白了就是「修形状」。边缘检测完,你得到的可能是一堆断断续续的线条,或者有一些小噪点。这时候就需要膨胀和腐蚀来「整形」。

4.3.1 腐蚀

腐蚀就像「削铅笔」。它会消除物体边界上的像素,让物体「瘦」一圈。对于去除小白点噪声特别有效。

# 定义结构元素(核)
kernel = np.ones((5,5), np.uint8)

# 腐蚀
erosion = cv2.erode(img, kernel, iterations=1)

4.3.2 膨胀

膨胀跟腐蚀相反,它会「增肥」。把物体边界向外扩张,填补空洞、连接断裂的线条。

# 膨胀
dilation = cv2.dilate(img, kernel, iterations=1)
实用技巧: 我经常把腐蚀和膨胀组合使用。先腐蚀去掉小噪点,再膨胀恢复物体大小。这个操作叫「开运算」。反过来,先膨胀再腐蚀叫「闭运算」,用来填补物体内部的小空洞。
# 开运算:先腐蚀后膨胀
opening = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)

# 闭运算:先膨胀后腐蚀
closing = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)

这里有个小表格,方便大家对比:

操作 作用 典型应用场景
腐蚀 消除边界点,使物体缩小 去除小白点噪声
膨胀 扩大边界,使物体增大 填补空洞,连接断裂
开运算 先腐蚀后膨胀 去噪点,保持物体大小
闭运算 先膨胀后腐蚀 填补内部空洞

4.4 实战小案例:预处理流水线

最后,我给大家搭一个完整的预处理流水线。假设我们要检测一个限速 60 的标志:

import cv2
import numpy as np

# 1. 读取图像
img = cv2.imread('speed_limit_60.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 2. 高斯滤波去噪
blurred = cv2.GaussianBlur(gray, (5, 5), 0)

# 3. Canny 边缘检测
edges = cv2.Canny(blurred, 50, 150)

# 4. 形态学闭运算,连接断裂边缘
kernel = np.ones((3,3), np.uint8)
closed = cv2.morphologyEx(edges, cv2.MORPH_CLOSE, kernel)

# 5. 显示结果
cv2.imshow('Pipeline Result', closed)
cv2.waitKey(0)
cv2.destroyAllWindows()

这个流水线我用了很多次,效果稳定。当然,具体参数要根据你的图像质量微调。比如光线暗的时候,Canny 的阈值要降低;噪声大的时候,滤波核要加大。

避坑指南: 我曾经在预处理阶段「过度滤波」,把标志上的数字都给模糊掉了。结果模型死活识别不出「60」这个数字。所以,预处理不是越强越好,要适可而止。

好了,这一章的内容就到这里。下一章我们会讲图像分割与轮廓提取,到时候这些预处理的结果就能派上大用场了。大家先动手试试今天的代码,有问题随时交流。