3. 图像基础与预处理(下)

好,咱们接着聊。上一节我们把图像的基本概念和简单预处理讲完了,这一节要动真格的了——滤波、边缘检测、形态学操作。这三个东西,说白了就是自动驾驶感知系统的“眼睛清洗剂”和“轮廓笔”。

我刚开始做车道线检测的时候,觉得原始图像直接扔进算法就能出结果。结果呢?噪声大得离谱,车道线边缘跟狗啃的一样。后来才明白,预处理这一步没做好,后面全是白费功夫。

3.1 图像滤波:给图像“去噪”

滤波是什么?你可以把它想象成用一块抹布在图像上擦一遍。抹布擦过的地方,像素值被重新计算,噪声就被抹平了。

3.1.1 高斯滤波

高斯滤波是我个人最常用的滤波器。它的核心思想很简单:离中心点越近的像素,权重越大;越远的,权重越小。权重分布服从高斯函数,也就是正态分布。

为什么用高斯?因为自然界的噪声大多服从高斯分布,用高斯滤波去匹配它,效果最好。我在项目中遇到过一种情况:车道线被路面上的小石子反光干扰,用高斯滤波轻轻一过,那些小亮点就消失了,而车道线本身还保留得很好。

代码实现也很直接:

import cv2
import numpy as np

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

# 高斯滤波,核大小5x5,标准差1.5
gaussian_blur = cv2.GaussianBlur(img, (5, 5), 1.5)

# 显示结果
cv2.imshow('Gaussian Blur', gaussian_blur)
cv2.waitKey(0)
我的小技巧: 核大小选奇数,3、5、7都行。标准差一般取1.0到2.0之间。太大图像会糊,太小去噪效果差。我习惯先设1.5,看效果再微调。

3.1.2 中值滤波

中值滤波跟高斯滤波的思路完全不同。它不搞加权平均那一套,而是把窗口内的像素值排序,取中间那个值作为新像素值。

你想想看,如果图像里有椒盐噪声(就是那种黑白点),高斯滤波只能把它变淡,但中值滤波能直接把它干掉。为什么?因为椒盐噪声的像素值要么极大要么极小,排序后肯定在两端,中间值根本轮不到它。

我曾经在一个雨夜场景里测试车道线检测,图像上全是雨水反光造成的亮点。高斯滤波搞不定,中值滤波一上,效果立竿见影。

# 中值滤波,核大小5
median_blur = cv2.medianBlur(img, 5)
注意: 中值滤波的核大小也必须奇数。而且它比较耗时,核越大越慢。实时系统里慎用大核。

3.2 边缘检测:找到图像的“轮廓”

滤波做完了,图像干净了。接下来要干嘛?找边缘。车道线本质上就是图像中的边缘——路面和车道线之间的灰度突变。

3.2.1 Sobel算子

Sobel算子是最基础的边缘检测方法。它用两个3x3的卷积核,一个检测水平方向的变化,一个检测垂直方向的变化。

说白了,Sobel就是在算梯度。图像中灰度变化剧烈的地方,梯度值就大,那就是边缘。

# Sobel边缘检测
sobel_x = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3)  # 水平方向
sobel_y = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3)  # 垂直方向

# 合并两个方向
sobel_combined = cv2.magnitude(sobel_x, sobel_y)

嗯,这里要注意:Sobel对噪声比较敏感。所以我的习惯是——先高斯滤波,再做Sobel。顺序不能反。

3.2.2 Canny边缘检测

Canny是边缘检测里的“扛把子”。它比Sobel复杂,但效果也更好。Canny的流程大致是:

  1. 高斯滤波去噪
  2. 计算梯度幅值和方向
  3. 非极大值抑制(只保留梯度方向上的最大值)
  4. 双阈值检测(高阈值确定强边缘,低阈值连接弱边缘)

为什么Canny效果好?因为它做了“非极大值抑制”,把那些模糊的、不清晰的边缘都去掉了,只留下最锐利的线条。我在做车道线检测时,Canny几乎是标配。

# Canny边缘检测
# 参数:低阈值50,高阈值150
edges = cv2.Canny(img, 50, 150)
核心经验: Canny的阈值怎么调?我一般先设低阈值50,高阈值150。如果边缘太多(噪声多),就提高低阈值;如果边缘太少(漏检),就降低高阈值。高低阈值比例保持在1:2到1:3之间。

3.3 形态学操作:修整边缘

边缘检测完了,你以为就结束了?不,车道线边缘可能断断续续的,或者有毛刺。这时候就需要形态学操作来“修一修”。

3.3.1 膨胀与腐蚀

膨胀和腐蚀是形态学操作的基础。它们都用一个结构元素(比如3x3的矩形)在图像上滑动。

  • 腐蚀: 结构元素覆盖的区域中,只要有一个像素是0,中心点就变成0。说白了就是“瘦身”,把边缘往里缩。
  • 膨胀: 结构元素覆盖的区域中,只要有一个像素是1,中心点就变成1。说白了就是“增肥”,把边缘往外扩。

我记得有一次,车道线因为路面磨损变得很细,Canny检测出来断断续续的。我用膨胀操作把线条加粗,断开的点就连上了。

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

# 腐蚀
eroded = cv2.erode(edges, kernel, iterations=1)

# 膨胀
dilated = cv2.dilate(edges, kernel, iterations=1)

3.3.2 开运算与闭运算

开运算 = 先腐蚀后膨胀。闭运算 = 先膨胀后腐蚀。

开运算用来去除小噪点。比如图像上有些孤立的白点,先腐蚀把它们消掉,再膨胀恢复原来的大小。闭运算用来填充小空洞。比如车道线中间有个小缺口,先膨胀填上,再腐蚀恢复原状。

我建议你记住这个口诀:开运算去噪点,闭运算填空洞。

# 开运算
opening = cv2.morphologyEx(edges, cv2.MORPH_OPEN, kernel)

# 闭运算
closing = cv2.morphologyEx(edges, cv2.MORPH_CLOSE, kernel)
避坑指南: 我曾经在一条有阴影的道路上做检测,阴影导致车道线中间出现大量空洞。用闭运算一次就搞定了。但要注意迭代次数别太多,否则会把不该连的东西连在一起。

3.4 本章知识体系

下面这张图把本章的核心逻辑串起来了。你可以把它当作一个处理流水线:

车道线检测预处理流水线 原始图像 图像滤波 高斯/中值滤波 边缘检测 Sobel / Canny 形态学操作 膨胀/腐蚀/开闭 各步骤的作用与选择 • 高斯滤波:去除高斯噪声,保留边缘信息 • 中值滤波:去除椒盐噪声,适合雨夜场景 • Sobel:计算梯度,速度快,对噪声敏感 • Canny:双阈值检测,边缘更清晰,推荐使用 • 开/闭运算:开运算去噪点,闭运算填空洞

你看,整个流程是串行的:原始图像 → 滤波去噪 → 边缘检测 → 形态学修整。每一步都依赖上一步的结果。我在实际项目中,经常在这个流水线上来回调试参数,直到车道线边缘清晰、连续、没有噪声为止。

好了,这一节的内容就到这儿。记住一句话:预处理做得好,后面算法跑得顺。别嫌麻烦,多调几次参数,你会找到感觉的。