1. 从一次“鬼探头”说起:Apollo 3.5 planning 模块到底在解决什么问题
我第一次接触 Apollo 3.5 的 planning 模块,是在一个封闭园区做低速接驳车的项目上。当时遇到一个非常典型的场景:车辆沿着园区道路正常行驶,右侧停了一排车,突然从两辆停靠车之间的缝隙里窜出一辆外卖电动车。那一瞬间,感知模块把障碍物丢给了 planning,而 planning 需要在几十毫秒内决定——是急刹、是向左微打方向绕过去、还是保持原轨迹减速。这个决策做得好不好,直接决定了这辆车是“像个老司机”还是“像个马路杀手”。
Apollo 3.5 的 planning 模块,本质上就是自动驾驶大脑里负责“想清楚下一步怎么走”的那一层。它接收来自感知、预测、定位、地图、底盘等模块的信息,输出一条带速度规划的轨迹,交给控制模块去执行。听起来简单,但真正拆开看,它要同时处理路由寻径、轨迹生成、速度决策、交通规则约束、障碍物避让、场景切换这一整套问题。3.5 这个版本在 planning 上最大的变化,是把原来偏“规则堆叠”的架构,往场景化 + 分层决策的方向推了一大步,引入了基于场景的插件式管理,让不同驾驶场景(车道保持、跟车、借道绕行、路口通行、泊车等)可以各自独立配置和调度。
这篇文章适合谁看?如果你正在做自动驾驶规划控制相关的开发,或者想搞清楚 Apollo planning 的代码结构和设计思路,又或者你只是好奇“一辆车到底是怎么决定往哪开的”,那这篇内容应该能给你一些可以直接参考的东西。我会尽量把 3.5 版本 planning 的核心设计、关键模块、实操配置和踩过的坑讲透,不堆术语,讲人话。
2. planning 模块的整体架构与设计思路拆解
2.1 为什么 3.5 要做“场景化”重构
在 3.5 之前,Apollo 的 planning 更像一条长长的流水线:先做参考线生成,再跑一堆决策器,最后做轨迹优化。问题是,城市道路场景太复杂了,跟车、变道、路口、避让行人,这些场景对规划的要求完全不同。如果全塞进一套逻辑里,代码会变成巨大的 if-else 泥潭,改一个场景很容易影响另一个场景。
3.5 的做法是引入Scenario(场景)这个概念,把 planning 拆成“场景判断 + 场景内规划”两层。系统先根据当前环境判断处于哪个场景,比如是“车道跟随”还是“借道绕行”,然后调用对应场景的规划器。每个场景内部再走自己的决策和优化流程。这样做的好处很直接:场景之间解耦,新增场景不用动老代码,调试时也能快速定位是哪个场景出了问题。
我个人的理解是,这有点像餐厅后厨的分工。以前是一个厨师从头做到尾,什么菜都他炒;现在是切配、炒锅、凉菜各管一摊,来了什么单子就派给对应的档口。效率不一定立刻翻倍,但可控性和可维护性上了一个台阶。
2.2 核心数据流:从感知到轨迹的一条链路
planning 模块的输入输出,用一句话概括就是:吃进去的是世界状态,吐出来的是未来几秒的轨迹。具体来说,它订阅的主要话题包括:
- 定位信息(
/apollo/localization/pose):车在哪、朝哪、速度多少 - 感知障碍物(
/apollo/perception/obstacles):周围有什么、在哪、什么类型 - 预测轨迹(
/apollo/prediction):其他交通参与者接下来可能怎么动 - 高精地图(
/apollo/map):车道线、路口、限速、停止线 - 底盘反馈(
/apollo/canbus/chassis):当前油门、刹车、转向状态 - 路由信息(
/apollo/routing):从起点到终点的全局路径
输出则是ADCTrajectory,里面包含一系列带时间戳的轨迹点,每个点有位置、速度、加速度、曲率等信息。控制模块拿到这条轨迹后,再去算油门刹车和方向盘角度。
这里有个容易被忽略的点:planning 输出的轨迹不是“一条线”,而是“一条带时间维度的线”。也就是说,它不仅告诉你车要经过哪些位置,还告诉你每个位置大概在什么时刻到达、当时速度多少。这就是为什么 planning 里速度规划和路径规划必须耦合在一起考虑,分开做很容易出现“路径很漂亮但速度对不上”的情况。
2.3 分层结构:ReferenceLine、Frame、Scenario 三者关系
Apollo 3.5 planning 的代码结构里,有三个概念绕不开:ReferenceLine(参考线)、Frame(帧)、Scenario(场景)。
ReferenceLine 可以理解为“一条理想的行车参考路径”,通常由地图车道中心线平滑而来。它提供了路径的几何信息,也承载了各种决策标记,比如哪里要减速、哪里要换道。Frame 是每一帧规划的计算容器,里面装着当前时刻的所有输入、参考线、障碍物信息。Scenario 则是基于 Frame 判断出来的当前驾驶场景。
它们的关系是:每个规划周期,planning 先根据定位和地图生成或更新 ReferenceLine,然后组装成一个 Frame,接着 ScenarioManager 根据 Frame 里的信息判断当前场景,最后调用对应 Scenario 的 Process 方法,在 Frame 上做决策和优化,输出轨迹。
我踩过的一个坑是:ReferenceLine 的生成质量直接决定了后续规划的上限。如果参考线本身抖动厉害或者偏离车道中心太多,后面的优化器再怎么调也救不回来。所以调试 planning 时,我习惯先把参考线可视化出来看一眼,确认它是不是平滑、是不是贴合车道。
3. 核心模块细节与实操要点解析
3.1 参考线生成:一切规划的起点
参考线生成的核心逻辑,是把地图里的车道中心线离散成点,然后做平滑处理。Apollo 3.5 里主要用离散点平滑的方法,通过优化一个代价函数,让平滑后的点既贴近原始点,又满足曲率约束。
代价函数大致包含三部分:平滑项(让相邻点之间的向量变化小)、几何项(让平滑后的点别偏离原始点太远)、长度项(防止点挤在一起)。求解时用类似二次规划的方法,迭代出最优解。
实操中我建议重点关注两个参数:平滑权重和最大曲率约束。平滑权重太大,参考线会变得过于“软”,在弯道处可能切内弯;太小则抖动明显。最大曲率约束要根据车辆的最小转弯半径来设,一般乘用车最小转弯半径在 5 米左右,对应曲率约 0.2,但实际规划里会留余量,设得更保守一些。
提示:参考线生成后,一定要检查它在路口、匝道、车道变宽变窄处的表现。这些地方地图数据本身可能就不准,参考线容易出问题。
3.2 决策器与优化器:谁负责“想”,谁负责“算”
planning 里决策和优化是分开的两步。决策器负责给出“意图”,比如“保持车道”“向左换道”“减速让行”;优化器负责在意图约束下,算出一条具体可行的轨迹。
3.5 里常见的决策器包括:
- PathDecider:处理静态障碍物,决定是绕行还是等待
- SpeedDecider:处理动态障碍物和交通规则,决定加减速
- LaneChangeDecider:判断是否满足换道条件
- StopSignDecider / TrafficLightDecider:处理停止标志和信号灯
优化器主要是OpenSpacePlanner和PublicRoadPlanner里的轨迹优化部分。公共道路用的是基于Frenet 坐标系的优化,把二维轨迹分解成横向(Lateral)和纵向(Longitudinal)两个一维问题,分别用二次规划求解。这样做的好处是降维,计算量小,实时性好。
但这里有个坑:Frenet 坐标系在参考线曲率大的地方会失真。因为 Frenet 的横纵分解假设参考线局部是直的,弯道急的时候,横向偏移和实际距离对不上,优化出来的轨迹可能贴着障碍物过。解决办法是在大曲率路段加密参考线点,或者切换到笛卡尔坐标系做局部优化。
3.3 场景插件机制:ScenarioManager 怎么调度
ScenarioManager 是 3.5 planning 的调度中心。它的工作流程是:遍历所有注册的场景,每个场景有一个IsTransferable方法,判断当前 Frame 是否满足进入该场景的条件。如果满足,就切换过去,调用该场景的Process方法。
场景的优先级很重要。比如“借道绕行”的优先级要高于“车道跟随”,否则遇到障碍物时系统会一直傻傻地跟着车道走,不会绕。Apollo 里通过配置scenario_type和优先级列表来控制。
我实际调试时发现,场景切换的边界条件最容易出问题。比如车辆在绕行过程中,障碍物突然消失,这时候是立刻切回车道跟随,还是等绕行完成?如果切换太急,轨迹会跳变;切换太慢,车可能一直压着对向车道走。Apollo 的做法是加了一个“场景保持”机制,进入某个场景后至少维持一段时间,避免频繁抖动。
4. 完整实操流程:从零跑通一次 planning 仿真
4.1 环境准备与依赖检查
要跑通 planning 模块,最省事的方式是用 Apollo 官方提供的仿真环境。你需要准备:
- Ubuntu 18.04 或 20.04(3.5 版本对 18.04 支持最好)
- Docker 环境
- Apollo 3.5 的源码或镜像
- 一张支持 CUDA 的显卡(如果要用感知模块,纯 planning 仿真可以不用)
启动仿真前,先确认dreamview能正常起来。DreamView 是 Apollo 的可视化工具,能看到车辆位置、参考线、障碍物、规划轨迹。命令大致是:
./scripts/bootstrap.sh start然后浏览器打开localhost:8888,选择对应的地图和车辆配置。
注意:第一次启动会比较慢,因为要拉镜像和编译。建议提前把 Docker 镜像拉好,别等到演示的时候现场下载。
4.2 配置 planning 参数文件
Apollo 3.5 的 planning 参数放在modules/planning/conf/目录下,核心文件是planning.conf和planning_config.pb.txt。前者是全局配置,后者是具体参数。
几个关键配置项:
| 配置项 | 作用 | 建议值 |
|---|---|---|
max_planning_length | 规划轨迹最大长度 | 200 米左右 |
planning_frequency | 规划频率 | 10 Hz |
default_cruise_speed | 默认巡航速度 | 根据场景设,园区可设 5 m/s |
speed_limit | 速度上限 | 不超过地图限速 |
trajectory_time_length | 轨迹时间长度 | 8 秒左右 |
改完参数后需要重新编译或者重启 planning 模块。我一般用cyber_monitor看 planning 的输出话题,确认轨迹有没有正常发布。
4.3 跑一个简单的车道跟随场景
最简单的验证场景是让车在一条直道上跑起来。步骤是:
- 在 DreamView 里选好地图和车辆
- 设置一个路由,起点和终点都在同一条车道上
- 打开 planning、prediction、routing、localization 模块
- 观察车辆是否沿着车道中心线行驶,速度是否平滑
如果车不动,先看 routing 有没有成功,再看 planning 有没有输出轨迹。常见问题是定位没初始化,或者地图没加载对。
4.4 加入障碍物测试避让逻辑
车道跟随跑通后,可以在车前方放一个静态障碍物,看 planning 怎么处理。Apollo 3.5 默认对静态障碍物的策略是:如果障碍物在参考线上,且左右有空间,就尝试绕行;如果没有空间,就停车等待。
测试时注意看 DreamView 里的决策信息,它会显示当前场景和决策原因。比如显示STOP就说明在等待,显示LANE_FOLLOW说明在正常跟车。如果绕行轨迹很突兀,多半是参考线或者障碍物边界处理有问题。
5. 常见问题与排查技巧实录
5.1 轨迹抖动或跳变
这是 planning 调试里最常见的问题。表现是车辆行驶不平稳,或者 DreamView 里轨迹线一帧一个样。原因通常有三类:
- 参考线抖动:检查参考线平滑参数,必要时加大平滑权重
- 决策频繁切换:比如在绕行和跟车之间反复横跳,需要调整场景保持时间
- 优化器无解:优化失败时会回退到上一帧轨迹,如果上一帧和当前帧差异大,就会跳变
排查方法:先把参考线单独可视化,确认它稳定;再看决策日志,确认场景有没有频繁切换;最后看优化器有没有报 infeasible。
5.2 车辆在路口犹豫不决
路口场景对 planning 是个大考验。常见问题是车辆在停止线前反复起步停车,或者该走的时候不走。这通常和信号灯决策、停止线检测有关。
Apollo 3.5 里,路口通行依赖TrafficLightDecider和地图里的停止线信息。如果地图数据里停止线位置不准,或者信号灯话题没收到,planning 就会保守处理,导致犹豫。
我的经验是:先把地图数据核对一遍,确认停止线、路口边界都对;然后检查信号灯话题有没有正常发布;最后再调决策参数,比如把“红灯转绿灯后的起步延迟”调小一点。
5.3 避让行人时过于保守或激进
行人避让的尺度很难拿捏。太保守,车一直停着不走,影响通行效率;太激进,又可能吓到行人。Apollo 3.5 里通过PedestrianDecider和障碍物类型来区分处理。
如果发现避让过于保守,可以检查行人的预测轨迹是不是过于发散,导致 planning 认为行人可能突然冲出来。如果预测轨迹合理,但 planning 还是不敢走,可以适当放宽安全距离参数。但这里要特别小心,安全距离不能为了通行效率牺牲太多,毕竟安全是第一位的。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 车辆不动 | 路由失败、定位丢失、规划无输出 | 检查 routing、localization、planning 话题 |
| 轨迹抖动 | 参考线不平滑、决策切换频繁 | 看参考线、看场景切换日志 |
| 路口犹豫 | 停止线不准、信号灯缺失 | 核对地图、检查信号灯话题 |
| 避让保守 | 预测轨迹发散、安全距离过大 | 看预测输出、调安全距离参数 |
| 优化失败 | 约束冲突、参考线曲率过大 | 看优化器日志、检查参考线 |
6. 几个我踩过的坑和实操心得
第一个坑是过度依赖默认参数。Apollo 的默认参数是针对乘用车和城市道路调的,如果你做的是园区低速车或者物流车,直接拿来用大概率不合适。比如默认巡航速度可能偏高,安全距离可能偏大。我的做法是先把所有和速度、距离相关的参数列出来,根据自己车辆的实际尺寸和场景重新算一遍。
第二个坑是忽略预测模块的影响。planning 的很多决策依赖 prediction 的输出,如果预测不准,planning 再怎么调也是白搭。有一次我们发现车辆对旁边车道的车过度避让,查了半天 planning 参数,最后发现是 prediction 把旁边车的轨迹预测成要并线进来。所以调试 planning 时,一定要同时看 prediction 的输出。
第三个心得是善用仿真回放。Apollo 支持把实车数据录成 bag,然后在仿真里回放。这对于复现问题特别有用。我习惯把出问题的片段录下来,反复回放,改一次参数跑一次,对比轨迹变化。这比在实车上试效率高得多。
第四个心得是关注计算耗时。planning 是实时模块,一般要求单帧计算在 100 毫秒以内。如果场景复杂,优化器可能超时。我一般会在代码里加耗时统计,看每个阶段花了多少时间。如果某个决策器特别慢,就要考虑简化逻辑或者降频处理。
最后说一个关于场景扩展的想法。Apollo 3.5 的场景插件机制其实留了很好的扩展接口。如果你有一个特殊场景,比如园区里的窄路会车,完全可以自己写一个 Scenario 插件,注册进去,然后在IsTransferable里写判断条件。这样既不影响原有逻辑,又能快速验证新想法。我自己就这么干过,从写插件到跑通仿真,大概花了两天时间,比改老代码快多了。