3、车道线检测基础(视觉篇)

各位同学,欢迎来到视觉检测的第一课。说实话,车道线检测是整个自动驾驶感知里最“接地气”的活儿。你想想看,车要跑,首先得知道路在哪。而视觉方案,说白了就是用摄像头模拟人眼,从图像里把车道线给“抠”出来。

这一章我会带你走完一套经典的视觉检测流水线:从原始图像到最终的车道线参数。我个人习惯把这套流程叫做“图像净化三部曲”加上“几何变换两板斧”。咱们一步步来。

3.1 图像预处理:把“脏活”干漂亮

摄像头采集的原始图像,直接拿来用是不行的。光照、噪声、路面纹理,全是干扰。预处理的目的就一个:去芜存菁

3.1.1 灰度化

为什么要把彩色图变成灰度图?因为颜色信息在车道线检测里,大部分时候是冗余的。你关心的只是亮度变化,而不是颜色本身。彩色图三个通道,灰度图一个通道,计算量直接降到三分之一。

我在项目中遇到过一个问题:黄色车道线在蓝色天空背景下,用RGB三个通道分别处理,反而容易把天空误检成车道线。灰度化之后,亮度对比度反而更清晰了。

常用的灰度化公式是加权平均法:

Gray = 0.299 * R + 0.587 * G + 0.114 * B

嗯,这个权重是模拟人眼对亮度的敏感度。绿色权重最高,因为人眼对绿光最敏感。

小技巧: 如果你用的是OpenCV,直接用 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边缘检测是业界标准。它分四步:

  1. 高斯滤波(我们已经做了)
  2. 计算梯度幅值和方向(用Sobel算子)
  3. 非极大值抑制(只保留梯度方向上的局部最大值)
  4. 双阈值检测(高阈值确定强边缘,低阈值连接弱边缘)

双阈值是关键。我见过很多新手把阈值设得太高,结果车道线断成一段一段的。或者设得太低,路面裂缝全被当成车道线。

low_threshold = 50
high_threshold = 150
edges = cv2.Canny(blurred, low_threshold, high_threshold)

我个人习惯用 low:high = 1:3 的比例。比如低阈值50,高阈值150。然后根据实际效果微调。记住,高阈值决定了你能看到多少“强”边缘

避坑指南: 我曾经在隧道入口处翻过车。隧道内外光照差异巨大,固定阈值完全失效。后来我改用自适应阈值,或者根据图像直方图动态调整Canny阈值,才解决了这个问题。

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

我在项目中遇到过一个问题:当车道线不连续时,滑动窗口会跟丢。后来我加了一个“记忆机制”,用上一帧的拟合结果作为当前帧的搜索起点,效果好了很多。

经验之谈: 滑动窗口的宽度和高度需要根据图像分辨率调整。一般来说,窗口高度设为图像高度的1/10,宽度设为100像素左右。如果车道线很细,可以适当减小宽度。

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)

源点的选取很关键。我建议你选一个直道场景,在图像中画一个梯形,梯形上边对应远处的车道线,下边对应近处的车道线。目标点是一个矩形,代表鸟瞰图中的车道线位置。

注意: IPM对地面平坦度非常敏感。如果路面有坡度,或者车辆颠簸,IPM的结果会变形。我曾经在过桥的时候,因为桥面有坡度,IPM出来的车道线直接歪了。后来我加入了IMU数据,动态调整俯仰角,才解决了这个问题。

好了,这一章的内容就到这里。从灰度化到Canny边缘检测,从霍夫变换到滑动窗口,再到逆透视变换,你已经掌握了视觉车道线检测的完整流程。下一章我们会把这些技术整合起来,实现一个完整的车道线检测系统。

记住,理论是基础,但真正的坑都在实践中。多动手,多调试,你也能成为车道线检测的老司机。