2. 环境感知基础

各位同学,今天我们来聊聊自动驾驶里最核心的一环——环境感知。说白了,就是让车「看懂」周围的世界。我做了这么多年决策算法,最大的感触就是:感知做不好,决策就是空中楼阁。你想想看,连路都看不清,怎么敢让车自己开?

这一章我们聚焦三个关键点:传感器融合、目标检测与跟踪、可行驶区域分割。这三块是决策算法的「眼睛」和「耳朵」。嗯,咱们一个一个来拆解。

传感器融合:取长补短的艺术

先问大家一个问题:为什么不用一种传感器搞定所有事?

答案很简单——每种传感器都有短板。摄像头看得远、能识别颜色和纹理,但怕强光、怕黑夜。激光雷达精度高、能直接测距,但怕雨雪雾,而且贵得离谱。毫米波雷达穿透力强、不受天气影响,但分辨率低,分不清是人是树。

所以,我个人的习惯是:让它们「打配合」。就像团队作战,有人冲锋、有人掩护、有人断后。

核心思路:利用不同传感器的互补特性,通过算法将数据融合到统一的空间坐标系中,得到比任何单一传感器都更可靠、更完整的环境模型。

我在项目中遇到过最典型的场景:大雾天,摄像头基本废了,激光雷达也打折扣,但毫米波雷达依然坚挺。这时候如果只依赖摄像头,决策系统就会「瞎掉」。融合之后,毫米波雷达提供距离和速度,激光雷达补充轮廓,摄像头在能见度稍好的时候提供类别信息——这样系统才能稳定工作。

传感器融合一般分三个层级:

  • 数据级融合:直接把原始数据拼在一起。比如把激光雷达的点云和摄像头的像素对齐。计算量大,但对齐精度高。
  • 特征级融合:先各自提取特征(比如障碍物位置、速度),再融合。我比较推荐这种方式,效率高,而且容错性好。
  • 决策级融合:每个传感器独立做判断,最后投票。简单粗暴,但容易丢失细节信息。

下面这张图是我自己总结的传感器融合流程,你可以看看整体逻辑:

传感器融合流程框架 摄像头 图像数据 激光雷达 点云数据 毫米波雷达 雷达回波 图像预处理 点云滤波/降采样 信号处理/CFAR 时空对齐 + 数据融合 坐标变换 | 时间戳同步 | 卡尔曼滤波 统一环境模型

你看,从三个传感器各自的数据,经过预处理、时空对齐,最后融合成一个统一的环境模型。这个模型里包含了障碍物的位置、速度、类别,还有可行驶区域的信息。决策模块就拿这个模型来做规划。

目标检测与跟踪:让车「看见」并「记住」

传感器融合之后,下一步就是目标检测。说白了,就是告诉系统:前面那个东西是什么?它在哪?它要往哪去?

目标检测这块,现在主流的方法还是深度学习。我常用的框架是 YOLO 系列和 CenterNet。不过要注意,在自动驾驶场景里,检测的实时性要求极高——你想想看,车速 60km/h,每秒钟跑 16 米,晚 100 毫秒检测出来,车已经跑出去 1.6 米了。所以模型轻量化很重要。

这里给一段简单的检测后处理代码,帮你理解一下:

def filter_detections(detections, confidence_threshold=0.5, iou_threshold=0.4):
    """
    过滤低置信度检测,并做NMS去重
    detections: list of [x1, y1, x2, y2, score, class_id]
    """
    # 第一步:按置信度过滤
    valid = [d for d in detections if d[4] > confidence_threshold]
    
    # 第二步:按类别分组做NMS
    final = []
    for class_id in set(d[5] for d in valid):
        class_dets = [d for d in valid if d[5] == class_id]
        # 按置信度降序排列
        class_dets.sort(key=lambda x: x[4], reverse=True)
        
        while class_dets:
            best = class_dets.pop(0)
            final.append(best)
            # 移除与best重叠过大的检测框
            class_dets = [d for d in class_dets 
                         if compute_iou(best[:4], d[:4]) < iou_threshold]
    
    return final

检测出来之后,还得做跟踪。为什么?因为单帧检测会有漏检、误检,而且决策需要知道目标的运动趋势。我习惯用 SORT(Simple Online and Realtime Tracking)算法,配合卡尔曼滤波做状态估计。

我的经验:跟踪时一定要处理好 ID 切换问题。有一次在高速上,两辆车并排行驶,跟踪算法频繁交换 ID,导致决策模块以为有车在「瞬移」,差点触发急刹车。后来我加了外观特征匹配(ReID),才稳住。

跟踪的核心逻辑其实不复杂:

  • 用卡尔曼滤波预测每个目标在当前帧的位置
  • 用匈牙利算法做检测框和预测框的匹配
  • 匹配上的更新状态,没匹配上的新建或删除轨迹

可行驶区域分割:告诉车「哪里能走」

目标检测解决的是「障碍物在哪」,可行驶区域分割解决的是「路在哪」。这两个信息缺一不可。

可行驶区域分割,本质上是一个语义分割任务。把图像中的每个像素分类为「可行驶」或「不可行驶」。但实际场景比这复杂——车道线内能走,路肩上能不能走?对面车道能不能借道?

我建议的做法是分层分割:

  1. 路面分割:识别出所有路面区域,不管能不能走
  2. 车道线检测:识别车道线,划分车道边界
  3. 可行驶区域推理:结合交通规则和当前场景,判断哪些路面可以走

举个例子,在施工路段,路面可能被锥桶隔开。单纯的路面分割会认为整个路面都能走,但结合锥桶检测后,系统就知道只有左侧车道能通行。

注意:可行驶区域分割最怕的是「过分割」和「欠分割」。过分割会把路面切得支离破碎,导致路径规划找不到连续路径。欠分割则会忽略小障碍物,比如路面上的一块石头。我曾在测试中遇到过欠分割导致车辆压到路肩的情况,幸好速度慢,不然后果严重。

这里给一个简单的分割后处理思路:

def postprocess_drivable_area(mask, min_area=100):
    """
    对分割结果做后处理,去除小区域和噪声
    mask: 二值图,1表示可行驶,0表示不可行驶
    """
    # 形态学操作:先腐蚀再膨胀,去除噪点
    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
    mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
    
    # 连通域分析,去除面积过小的区域
    num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(mask)
    for i in range(1, num_labels):
        if stats[i, cv2.CC_STAT_AREA] < min_area:
            mask[labels == i] = 0
    
    return mask

嗯,说到这,我想强调一点:可行驶区域分割不是终点,它要和目标检测的结果做交叉验证。比如,检测到障碍物在可行驶区域内,那这个区域就要被标记为「暂时不可通行」。反过来,如果可行驶区域突然消失,那可能是传感器出了问题,需要触发降级策略。

感知模块 输入 输出 典型算法
传感器融合 图像、点云、雷达数据 统一坐标系下的环境数据 卡尔曼滤波、ICP、BEV融合
目标检测 图像/点云 障碍物位置、类别、尺寸 YOLO、PointPillars、CenterNet
目标跟踪 检测结果序列 目标轨迹、速度、ID SORT、DeepSORT、卡尔曼滤波
可行驶区域分割 图像 可行驶区域二值图 UNet、DeepLab、LaneNet

最后说一句,环境感知这块,没有银弹。每个场景都有它的特殊性。我做了这么多年,最大的体会就是:永远要为 corner case 留一手。传感器会坏、模型会误检、天气会突变——你的系统必须能优雅地降级,而不是直接崩溃。

好了,这一章的内容就到这里。记住,感知是决策的基础,基础不牢,地动山摇。

可可精品资料库