4、车道线检测技术:基于视觉的车道线检测(边缘检测、霍夫变换)、基于激光雷达的车道线检测

车道线检测,说白了就是给自动驾驶汽车画“跑道”。没有它,车辆连自己在哪条道上都搞不清。我做了这么多年高精地图,最深的体会就是:车道线检测做不好,后面的规划控制全是空中楼阁。

今天咱们聊聊两种主流方案:视觉方案和激光雷达方案。这两种我都踩过坑,也积累了不少经验,分享给你。

4.1 基于视觉的车道线检测

视觉方案成本低、信息丰富,是量产车的首选。但它的短板也很明显——怕光照、怕雨雪、怕遮挡。嗯,这里要注意,视觉方案的核心流程其实就三步:预处理、特征提取、模型拟合。

4.1.1 边缘检测:找到车道线的“边界”

边缘检测是视觉车道线检测的第一步。为什么要做边缘检测?因为车道线在图像中就是一条亮线,和路面有显著的灰度差异。找到这个差异,就能定位车道线。

我个人习惯用Canny边缘检测。它比Sobel、Laplacian这些老方法稳定得多。Canny的核心思想是:先平滑去噪,再计算梯度,最后做非极大值抑制和双阈值处理。

关键参数:

  • 低阈值:控制边缘连接的灵敏度。设得太低,噪声会被当成边缘;设得太高,真正的边缘会断裂。
  • 高阈值:控制强边缘的保留。一般设为低阈值的2-3倍。

我在项目中遇到过一个问题:城市道路的沥青路面纹理复杂,Canny会检测出大量伪边缘。后来我加了一个预处理步骤——先做高斯滤波,再用形态学闭运算填充路面纹理。效果立竿见影。

import cv2
import numpy as np

def detect_edges(image):
    # 1. 高斯滤波去噪
    blurred = cv2.GaussianBlur(image, (5, 5), 1.5)
    
    # 2. Canny边缘检测
    low_threshold = 50
    high_threshold = 150
    edges = cv2.Canny(blurred, low_threshold, high_threshold)
    
    return edges

避坑指南:我曾经在隧道场景中吃过亏。隧道内光照均匀,但车道线磨损严重,Canny检测出的边缘断断续续。我的解决办法是:先做直方图均衡化增强对比度,再降低Canny的低阈值到30。这样虽然会引入一些噪声,但车道线的完整性保住了。

4.1.2 霍夫变换:从“点”到“线”的魔法

边缘检测得到的是像素点,但我们需要的是直线方程。霍夫变换就是干这个的——把图像空间中的点映射到参数空间,通过投票找到最可能的直线。

霍夫变换的原理其实不复杂。你想想看,一条直线可以用极坐标表示为:ρ = x·cosθ + y·sinθ。每个边缘点都会在(ρ, θ)空间画出一条正弦曲线。多条曲线的交点,就是一条直线。

实际应用中,我常用概率霍夫变换(Probabilistic Hough Transform)。它比标准霍夫变换快得多,而且能直接输出线段的端点坐标。

def detect_lane_lines(edges):
    # 概率霍夫变换
    lines = cv2.HoughLinesP(
        edges,
        rho=1,              # 距离分辨率(像素)
        theta=np.pi/180,    # 角度分辨率(弧度)
        threshold=50,       # 投票阈值
        minLineLength=40,   # 最小线段长度
        maxLineGap=100      # 最大线段间隙
    )
    return lines

注意:霍夫变换的参数调优是个体力活。我建议你先用一张典型场景的图片,手动调整threshold和minLineLength,直到检测结果满意。然后把这个参数作为基准,再针对特殊场景做微调。

4.1.3 车道线拟合与跟踪

霍夫变换检测出的线段往往是零散的。我们需要把它们合并成完整的车道线。常用的方法有两种:

  • 滑动窗口法:从图像底部开始,用固定宽度的窗口向上滑动,统计窗口内的像素点,拟合二次曲线。
  • RANSAC拟合:随机采样点,拟合直线,计算内点比例,迭代找到最优模型。

我个人更推荐滑动窗口法。为什么?因为车道线在图像中通常是曲线(尤其是弯道),二次曲线比直线更贴合实际。RANSAC更适合直线检测。

def sliding_window_fit(binary_warped):
    # 直方图统计,找到车道线起始位置
    histogram = np.sum(binary_warped[binary_warped.shape[0]//2:, :], axis=0)
    
    # 找到左右车道线的起始x坐标
    midpoint = histogram.shape[0] // 2
    leftx_base = np.argmax(histogram[:midpoint])
    rightx_base = np.argmax(histogram[midpoint:]) + midpoint
    
    # 滑动窗口参数
    nwindows = 9
    window_height = binary_warped.shape[0] // nwindows
    margin = 100
    
    # ... 窗口滑动和像素点收集 ...
    # 用二次多项式拟合
    left_fit = np.polyfit(lefty, leftx, 2)
    right_fit = np.polyfit(righty, rightx, 2)
    
    return left_fit, right_fit

经验之谈:我在高速公路上做过测试,滑动窗口法在直道和缓弯上表现很好。但遇到急弯(比如匝道),窗口宽度要适当增大,否则车道线会跑出窗口。我一般把margin设为150像素,能覆盖大多数弯道场景。

4.2 基于激光雷达的车道线检测

激光雷达方案不受光照影响,夜间、逆光都能稳定工作。但它的缺点也很明显:点云稀疏,车道线这种“薄”物体很难直接检测。

说白了,激光雷达看到的车道线,就是路面上一条反射强度不同的“线”。我们需要利用反射强度信息来提取它。

4.2.1 反射强度阈值分割

车道线通常用高反光材料(如热熔涂料)绘制,反射强度远高于普通路面。我们可以设定一个强度阈值,把高反射点提取出来。

def extract_lane_points_from_lidar(point_cloud):
    # point_cloud: N x 4 矩阵 (x, y, z, intensity)
    intensity_threshold = 0.3  # 根据传感器标定
    
    # 提取高反射点
    lane_points = point_cloud[point_cloud[:, 3] > intensity_threshold]
    
    return lane_points

注意:反射强度阈值不是固定的。不同厂家的激光雷达(如Velodyne、Hesai、RoboSense)的强度值范围不同。我建议你在采集数据时,先做一次强度标定——找一段标准车道线,统计其强度分布,再设定阈值。

4.2.2 基于高度滤波的路面分割

提取高反射点之前,必须先剔除地面以上的物体(如路沿、护栏、车辆)。否则这些物体的高反射点会干扰车道线检测。

我常用的方法是:基于高度进行路面分割。假设车辆在平坦路面上行驶,激光雷达安装高度已知,那么路面点的高度应该在一个小范围内波动。

def ground_segmentation(point_cloud, lidar_height=1.8):
    # 假设路面高度在 -0.2m 到 0.2m 之间
    ground_mask = (point_cloud[:, 2] > -0.2) & (point_cloud[:, 2] < 0.2)
    ground_points = point_cloud[ground_mask]
    
    return ground_points

避坑指南:我曾经在坡道路段翻过车。车辆上坡时,激光雷达的俯仰角变化,导致路面高度偏移。我的解决办法是:用IMU数据补偿激光雷达的姿态,或者用RANSAC平面拟合动态估计路面高度。

4.2.3 点云聚类与车道线拟合

提取出高反射路面点后,还需要把它们聚类成独立的车道线。常用的聚类算法有:

  • DBSCAN:基于密度的聚类,能自动识别噪声点。适合车道线这种形状规则的簇。
  • 欧式聚类:基于距离的聚类,简单快速。但需要手动设定聚类半径。

我个人习惯用DBSCAN。它不需要预先指定簇的数量,而且能很好地处理车道线断裂的情况。

from sklearn.cluster import DBSCAN

def cluster_lane_points(lane_points):
    # DBSCAN聚类
    clustering = DBSCAN(eps=0.3, min_samples=10).fit(lane_points[:, :2])
    labels = clustering.labels_
    
    # 提取每个簇的点
    clusters = {}
    for i, label in enumerate(labels):
        if label not in clusters:
            clusters[label] = []
        clusters[label].append(lane_points[i])
    
    return clusters

聚类完成后,对每个簇做直线或曲线拟合。由于激光雷达点云在远距离处很稀疏,我一般只拟合直线,或者用低阶多项式(一次或二次)。

4.3 视觉与激光雷达的融合策略

两种方案各有优劣。在实际工程中,我建议做融合。怎么融合?我分享一个我常用的策略:

场景 主传感器 辅助传感器 说明
白天、光照良好 视觉 激光雷达 视觉检测为主,激光雷达做校验
夜间、逆光 激光雷达 视觉 激光雷达为主,视觉辅助
雨雪、雾天 激光雷达 视觉(关闭) 视觉基本失效,全靠激光雷达
车道线磨损 视觉 + 激光雷达 — 两者独立检测,结果做加权融合

核心思路:不要试图用一个传感器解决所有问题。视觉和激光雷达是互补的。视觉提供纹理信息,激光雷达提供几何信息。两者结合,才能在各种场景下稳定输出车道线。

4.4 本章知识体系

下面这张图总结了车道线检测的核心逻辑。你可以把它当作一个快速参考。

车道线检测技术体系 基于视觉的车道线检测 基于激光雷达的车道线检测 图像预处理 边缘检测 (Canny) 霍夫变换 滑动窗口拟合 地面分割 强度阈值分割 DBSCAN聚类 直线/曲线拟合 融合策略:场景自适应切换 车道线参数输出

这张图把视觉和激光雷达两条技术路线并列展示。你可以看到,两者最终都汇聚到“车道线参数输出”。在实际工程中,我建议你根据场景动态选择主传感器,而不是固定用一种方案。

最后说一句:车道线检测没有银弹。视觉方案便宜但脆弱,激光雷达稳定但昂贵。作为工程师,我们的任务不是争论哪个更好,而是根据项目需求、成本预算、运行场景,做出最合理的技术选型。嗯,这就是我这些年最大的体会。

可可精品资料库