2. 环境感知基础
各位同学,今天我们来聊聊自动驾驶里最核心的一环——环境感知。说白了,就是让车「看懂」周围的世界。我做了这么多年决策算法,最大的感触就是:感知做不好,决策就是空中楼阁。你想想看,连路都看不清,怎么敢让车自己开?
这一章我们聚焦三个关键点:传感器融合、目标检测与跟踪、可行驶区域分割。这三块是决策算法的「眼睛」和「耳朵」。嗯,咱们一个一个来拆解。
传感器融合:取长补短的艺术
先问大家一个问题:为什么不用一种传感器搞定所有事?
答案很简单——每种传感器都有短板。摄像头看得远、能识别颜色和纹理,但怕强光、怕黑夜。激光雷达精度高、能直接测距,但怕雨雪雾,而且贵得离谱。毫米波雷达穿透力强、不受天气影响,但分辨率低,分不清是人是树。
所以,我个人的习惯是:让它们「打配合」。就像团队作战,有人冲锋、有人掩护、有人断后。
核心思路:利用不同传感器的互补特性,通过算法将数据融合到统一的空间坐标系中,得到比任何单一传感器都更可靠、更完整的环境模型。
我在项目中遇到过最典型的场景:大雾天,摄像头基本废了,激光雷达也打折扣,但毫米波雷达依然坚挺。这时候如果只依赖摄像头,决策系统就会「瞎掉」。融合之后,毫米波雷达提供距离和速度,激光雷达补充轮廓,摄像头在能见度稍好的时候提供类别信息——这样系统才能稳定工作。
传感器融合一般分三个层级:
- 数据级融合:直接把原始数据拼在一起。比如把激光雷达的点云和摄像头的像素对齐。计算量大,但对齐精度高。
- 特征级融合:先各自提取特征(比如障碍物位置、速度),再融合。我比较推荐这种方式,效率高,而且容错性好。
- 决策级融合:每个传感器独立做判断,最后投票。简单粗暴,但容易丢失细节信息。
下面这张图是我自己总结的传感器融合流程,你可以看看整体逻辑:
你看,从三个传感器各自的数据,经过预处理、时空对齐,最后融合成一个统一的环境模型。这个模型里包含了障碍物的位置、速度、类别,还有可行驶区域的信息。决策模块就拿这个模型来做规划。
目标检测与跟踪:让车「看见」并「记住」
传感器融合之后,下一步就是目标检测。说白了,就是告诉系统:前面那个东西是什么?它在哪?它要往哪去?
目标检测这块,现在主流的方法还是深度学习。我常用的框架是 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),才稳住。
跟踪的核心逻辑其实不复杂:
- 用卡尔曼滤波预测每个目标在当前帧的位置
- 用匈牙利算法做检测框和预测框的匹配
- 匹配上的更新状态,没匹配上的新建或删除轨迹
可行驶区域分割:告诉车「哪里能走」
目标检测解决的是「障碍物在哪」,可行驶区域分割解决的是「路在哪」。这两个信息缺一不可。
可行驶区域分割,本质上是一个语义分割任务。把图像中的每个像素分类为「可行驶」或「不可行驶」。但实际场景比这复杂——车道线内能走,路肩上能不能走?对面车道能不能借道?
我建议的做法是分层分割:
- 路面分割:识别出所有路面区域,不管能不能走
- 车道线检测:识别车道线,划分车道边界
- 可行驶区域推理:结合交通规则和当前场景,判断哪些路面可以走
举个例子,在施工路段,路面可能被锥桶隔开。单纯的路面分割会认为整个路面都能走,但结合锥桶检测后,系统就知道只有左侧车道能通行。
注意:可行驶区域分割最怕的是「过分割」和「欠分割」。过分割会把路面切得支离破碎,导致路径规划找不到连续路径。欠分割则会忽略小障碍物,比如路面上的一块石头。我曾在测试中遇到过欠分割导致车辆压到路肩的情况,幸好速度慢,不然后果严重。
这里给一个简单的分割后处理思路:
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 留一手。传感器会坏、模型会误检、天气会突变——你的系统必须能优雅地降级,而不是直接崩溃。
好了,这一章的内容就到这里。记住,感知是决策的基础,基础不牢,地动山摇。
可可精品资料库