3、车道线检测基础(视觉篇)
各位同学,欢迎来到视觉检测的第一课。说实话,车道线检测是整个自动驾驶感知里最“接地气”的活儿。你想想看,车要跑,首先得知道路在哪。而视觉方案,说白了就是用摄像头模拟人眼,从图像里把车道线给“抠”出来。
这一章我会带你走完一套经典的视觉检测流水线:从原始图像到最终的车道线参数。我个人习惯把这套流程叫做“图像净化三部曲”加上“几何变换两板斧”。咱们一步步来。
3.1 图像预处理:把“脏活”干漂亮
摄像头采集的原始图像,直接拿来用是不行的。光照、噪声、路面纹理,全是干扰。预处理的目的就一个:去芜存菁。
3.1.1 灰度化
为什么要把彩色图变成灰度图?因为颜色信息在车道线检测里,大部分时候是冗余的。你关心的只是亮度变化,而不是颜色本身。彩色图三个通道,灰度图一个通道,计算量直接降到三分之一。
我在项目中遇到过一个问题:黄色车道线在蓝色天空背景下,用RGB三个通道分别处理,反而容易把天空误检成车道线。灰度化之后,亮度对比度反而更清晰了。
常用的灰度化公式是加权平均法:
Gray = 0.299 * R + 0.587 * G + 0.114 * B
嗯,这个权重是模拟人眼对亮度的敏感度。绿色权重最高,因为人眼对绿光最敏感。
cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) 就行。注意OpenCV默认是BGR顺序,别搞反了。
3.1.2 高斯滤波
灰度化之后,图像里还有噪声。比如摄像头传感器的高频噪声、路面上的小石子。这些噪声在后续的边缘检测里会被放大,造成大量假边缘。
高斯滤波就是用来平滑图像的。它用一个高斯核去卷积图像,每个像素的值被周围像素的加权平均替代。权重由高斯分布决定,离中心越近,权重越大。
kernel_size = (5, 5)
sigma = 1.0
blurred = cv2.GaussianBlur(gray, kernel_size, sigma)
核大小和sigma怎么选?我建议你从(5,5)开始试。核太小,去噪效果差;核太大,边缘也会被模糊掉。我曾经在一条磨损严重的车道线上吃过亏,用了(9,9)的核,结果车道线边缘直接糊了,Canny检测出来断断续续的。
3.1.3 Canny边缘检测
好了,图像干净了。现在我们要找边缘。边缘就是图像中亮度变化剧烈的地方。车道线相对于路面,正好是这种变化。
Canny边缘检测是业界标准。它分四步:
- 高斯滤波(我们已经做了)
- 计算梯度幅值和方向(用Sobel算子)
- 非极大值抑制(只保留梯度方向上的局部最大值)
- 双阈值检测(高阈值确定强边缘,低阈值连接弱边缘)
双阈值是关键。我见过很多新手把阈值设得太高,结果车道线断成一段一段的。或者设得太低,路面裂缝全被当成车道线。
low_threshold = 50
high_threshold = 150
edges = cv2.Canny(blurred, low_threshold, high_threshold)
我个人习惯用 low:high = 1:3 的比例。比如低阈值50,高阈值150。然后根据实际效果微调。记住,高阈值决定了你能看到多少“强”边缘。
3.2 霍夫变换与滑动窗口
边缘检测之后,我们得到的是二值图像,里面全是白色像素点。但这些点哪些属于车道线?我们需要把它们“连”成线。
3.2.1 霍夫变换
霍夫变换的原理很简单:把图像空间中的点,映射到参数空间中的曲线。在图像空间中,一条直线由斜率和截距决定。但在参数空间中,我们用极坐标表示:ρ = x*cosθ + y*sinθ。
每个边缘点对应参数空间中的一条正弦曲线。多条曲线相交的点,就对应图像空间中的一条直线。
lines = cv2.HoughLinesP(edges, rho=1, theta=np.pi/180, threshold=50, minLineLength=100, maxLineGap=50)
参数说明:
| 参数 | 含义 | 建议值 |
|---|---|---|
| rho | 距离分辨率(像素) | 1 |
| theta | 角度分辨率(弧度) | π/180 |
| threshold | 投票阈值 | 50-100 |
| minLineLength | 最小线段长度 | 100 |
| maxLineGap | 同一条线上点的最大间隔 | 50 |
霍夫变换的缺点也很明显:它只能检测直线。对于弯道,你需要分段处理,或者用滑动窗口。
3.2.2 滑动窗口
滑动窗口是处理弯道的利器。它的思路是:从图像底部开始,用一个固定宽度的窗口,统计窗口内白色像素点的数量。如果数量超过阈值,就认为窗口中心是车道线的一部分。然后窗口向上滑动,继续搜索。
说白了,就是“跟着像素走”。
def sliding_window(binary_warped):
# 统计图像下半部分的像素分布
histogram = np.sum(binary_warped[binary_warped.shape[0]//2:,:], axis=0)
# 找到左右车道线的起始点
midpoint = histogram.shape[0] // 2
leftx_base = np.argmax(histogram[:midpoint])
rightx_base = np.argmax(histogram[midpoint:]) + midpoint
# ... 后续窗口迭代
return left_fit, right_fit
我在项目中遇到过一个问题:当车道线不连续时,滑动窗口会跟丢。后来我加了一个“记忆机制”,用上一帧的拟合结果作为当前帧的搜索起点,效果好了很多。
3.3 逆透视变换(IPM)
终于到了最关键的一步。摄像头拍到的图像是透视投影,近大远小。车道线在图像中会汇聚到消失点。这种透视效果对后续的曲率计算和车辆控制非常不友好。
逆透视变换(IPM)就是要把图像“拉直”,变成鸟瞰图。这样车道线就变成平行的了,曲率计算也简单了。
IPM的核心是找到透视变换矩阵。你需要知道摄像头的内参(焦距、主点)和外参(俯仰角、偏航角、高度)。如果没有这些参数,也可以手动选取四个点来估算。
# 手动选取源点和目标点
src = np.float32([[200, 720], [1100, 720], [595, 450], [685, 450]])
dst = np.float32([[300, 720], [980, 720], [300, 0], [980, 0]])
# 计算变换矩阵
M = cv2.getPerspectiveTransform(src, dst)
# 执行变换
warped = cv2.warpPerspective(img, M, (1280, 720), flags=cv2.INTER_LINEAR)
源点的选取很关键。我建议你选一个直道场景,在图像中画一个梯形,梯形上边对应远处的车道线,下边对应近处的车道线。目标点是一个矩形,代表鸟瞰图中的车道线位置。
好了,这一章的内容就到这里。从灰度化到Canny边缘检测,从霍夫变换到滑动窗口,再到逆透视变换,你已经掌握了视觉车道线检测的完整流程。下一章我们会把这些技术整合起来,实现一个完整的车道线检测系统。
记住,理论是基础,但真正的坑都在实践中。多动手,多调试,你也能成为车道线检测的老司机。