YAOTU INSIGHTS

Apollo规划模块深度解析:架构、算法与调试实战

Apollo规划模块深度解析:架构、算法与调试实战
1. 从“规划”二字说起Apollo规划模块到底在解决什么问题很多人第一次接触Apollo注意力往往被感知模块吸引——摄像头、激光雷达、毫米波雷达融合出来的鸟瞰图确实直观。但真正让一辆车“会开车”的其实是规划模块。感知告诉车“周围有什么”规划告诉车“接下来该怎么走”。这个“该怎么走”包含了路径、速度、避让、跟车、换道、路口通行等一系列决策是自动驾驶系统里最接近人类驾驶员思考过程的部分。Apollo的规划模块官方叫Planning在整个架构里处于中间层上游接收感知、定位、地图、预测模块的输出下游把规划好的轨迹交给控制模块去执行。它的输入包括障碍物信息、自车状态、高精地图、路由信息输出是一条带有时间戳的轨迹点序列每个点包含位置、速度、加速度、朝向、曲率等信息。控制模块拿到这条轨迹再去算油门、刹车、方向盘转角。这篇文章适合谁看如果你正在做自动驾驶相关的开发或者想理解Apollo规划模块的内部结构又或者你只是好奇“一辆无人车是怎么决定下一步动作的”那这篇内容应该能给你一个比较完整的图景。我会从模块架构、核心算法、场景插件、调参经验、常见坑这几个角度展开尽量把“为什么这么设计”讲清楚而不是只罗列代码结构。需要提前说明的是Apollo的版本迭代很快不同版本之间规划模块的目录结构和实现细节有差异。我下面讲的内容主要基于比较经典的架构思路具体到某个版本时你需要对照实际代码去看。但核心的设计哲学和算法逻辑变化并不大。2. Planning模块的骨架一张图看懂数据怎么流动2.1 从Cyber RT到PlanningComponentApollo用的是自己的一套通信中间件早期叫Cyber RT。规划模块作为一个Component存在通过Reader订阅上游话题通过Writer发布规划结果。你如果去看planning_component.cc会发现它的主循环逻辑其实很清晰收到一帧数据调用RunOnce里面依次做数据校验、调用规划器、发布结果。这里有个容易被忽略的点PlanningComponent并不是每收到一帧就立刻规划一次。它有一个planning_loop的概念实际规划频率和感知频率不一定一致。感知可能10Hz规划可能也是10Hz但两者是异步的。如果某一帧感知数据没来规划模块要么用上一帧的结果要么直接跳过。这个机制在调试时经常让人困惑——“为什么我的规划结果没更新”很可能就是上游数据没到位。2.2 数据输入的三条主线规划模块的输入可以分成三条主线第一条是自车状态线包括定位信息位置、姿态、速度、底盘信息方向盘转角、油门开度、挡位。这条线决定了“我在哪、我什么状态”。第二条是环境感知线包括障碍物列表、预测轨迹、交通灯状态、车道线信息。这条线决定了“周围有什么、它们要干什么”。第三条是地图与路由线包括高精地图的车道拓扑、路由请求给出的起点终点。这条线决定了“我要去哪、有哪些路可以走”。这三条线在PlanningComponent里被组装成一个PlanningData结构体然后传给具体的规划器。理解这个结构体的字段含义是读懂后续所有算法的前提。我建议你在调试时先把PlanningData里每个字段的来源和更新频率搞清楚这比直接去看算法实现更有效。2.3 规划器的插件化设计Apollo规划模块最值得称道的一点是它的插件化架构。规划器不是写死的一个函数而是一系列Scenario场景和Task任务的组合。每个Scenario负责识别一种特定的驾驶场景比如车道保持、换道、路口通行、泊车。每个Task负责一个具体的计算步骤比如路径生成、速度生成、碰撞检测、决策制定。这种设计的好处是扩展性强。你想加一个新的驾驶场景只需要写一个新的Scenario插件注册到ScenarioManager里不需要改动核心框架。你想调整某个计算步骤只需要替换对应的Task。这种“搭积木”的思路在工程上非常实用因为自动驾驶的场景太多了不可能用一个统一的算法覆盖所有情况。但插件化也带来了一个副作用调试链路变长了。一个规划结果不对可能是Scenario选错了可能是某个Task的参数不对也可能是Task之间的数据传递出了问题。你需要有一套系统的排查方法这个我后面会专门讲。3. 场景驱动的规划逻辑Scenario与Stage的分层设计3.1 ScenarioManager怎么选场景ScenarioManager的核心逻辑是遍历所有注册的Scenario调用每个Scenario的IsTransferable函数看当前帧的数据是否满足进入该场景的条件。如果多个场景同时满足就按优先级选最高的那个。这里的关键是优先级设计。比如一辆车同时满足“车道保持”和“避让障碍物”的条件那肯定优先进入避让场景。Apollo里给每个Scenario配了一个优先级数值数值越大越优先。这个优先级不是随便定的需要根据实际道路测试的结果反复调整。我见过有团队因为优先级配错导致车辆在障碍物前不减速直接冲过去非常危险。还有一个细节Scenario的切换不是瞬间完成的。从当前场景退出到新场景进入中间有一个过渡过程。Apollo里用Stage来管理这个过渡。一个Scenario可以包含多个Stage比如“换道”场景可能包含“换道准备”“换道执行”“换道取消”三个Stage。Stage之间的切换条件比Scenario更细通常和具体的时间、距离、速度阈值相关。3.2 典型场景拆解车道保持车道保持是最基础也是最常用的场景。它的逻辑看起来简单——沿着当前车道中心线走——但实际实现要考虑很多因素。首先是参考线生成。规划模块不是直接用车道的几何中心线而是要先根据路由信息、地图车道线、自车位置生成一条平滑的参考线。这条参考线要满足几个条件曲率连续、与车道边界保持安全距离、在路口处能正确连接。Apollo里用ReferenceLineProvider来做这件事内部会做平滑、拼接、降采样等操作。然后是路径优化。有了参考线规划模块要在参考线附近生成一条可行驶的路径。这条路径不是简单复制参考线而是要避开障碍物、保持舒适性。Apollo里常用的是基于二次规划的路径优化把路径表示成一串离散点然后构造一个优化问题最小化路径与参考线的偏差、最小化曲率变化、满足障碍物距离约束。求解出来就是一条平滑的路径。最后是速度规划。路径定了还要决定每个点上的速度。速度规划要考虑限速、障碍物、舒适性、交通规则。Apollo里用STGraph来做速度规划横轴是时间纵轴是路程把障碍物投影到这个图上然后找一条无碰撞的、满足加速度约束的曲线。3.3 换道场景的决策逻辑换道比车道保持复杂得多因为它涉及到与周围车辆的博弈。Apollo的换道场景通常包含这几个步骤第一步是换道意图触发。可能是路由要求前方需要转弯可能是效率考虑当前车道太慢可能是避障需要前方有障碍物。不同的触发条件对应不同的换道策略。第二步是换道可行性检查。要检查目标车道前后是否有足够的安全距离要检查目标车道是否有车正在换入要检查换道过程中是否会产生碰撞。这个检查不是一次性的而是持续进行的因为周围车辆的状态在变。第三步是换道轨迹生成。Apollo里常用多项式曲线或者贝塞尔曲线来生成换道轨迹。轨迹要满足几个约束起点与当前状态连续、终点与目标车道对齐、曲率不超过车辆能力、换道时间在合理范围内。第四步是换道执行与取消。换道过程中如果发现风险比如目标车道后车突然加速要能取消换道回到原车道。这个取消逻辑非常关键很多换道事故就是因为取消不及时导致的。3.4 路口场景的特殊处理路口是规划模块最头疼的场景之一。没有车道线、交通参与者多、通行规则复杂。Apollo的路口场景通常要做这几件事路口识别根据地图和路由信息判断自车是否接近路口。这个判断要提前不能等到路口跟前才反应。通行顺序决策如果有停车标志或者交通灯要决定是停还是走。如果是无保护路口要判断与对向车辆、行人、非机动车的通行顺序。Apollo里用Decision模块来做这件事内部有一套基于规则的逻辑也会结合预测模块的输出。路口内轨迹生成路口内没有明确的车道线规划模块需要自己生成一条从入口到出口的轨迹。这条轨迹要避开其他车辆和行人要符合交通规则要尽量平滑。Apollo里常用的是基于搜索或者采样的方法在路口区域内生成候选轨迹然后选一条最优的。4. 核心算法拆解路径优化与速度规划怎么落地4.1 路径优化的数学本质路径优化本质上是一个带约束的优化问题。Apollo里常用的形式是minimize w1 * ||path - ref||^2 w2 * ||path||^2 w3 * ||path||^2 subject to path 在可行驶区域内 path 与障碍物保持安全距离 path 的曲率不超过车辆能力其中path是待优化的路径点序列ref是参考线path和path分别是二阶和三阶差分用来衡量曲率和平滑度。w1、w2、w3是权重需要根据实际效果调整。这个优化问题通常用二次规划QP来求解。Apollo里用的是OSQP或者类似的求解器。QP的好处是求解速度快、有全局最优解适合实时性要求高的场景。但QP也有局限它只能处理线性约束和二次目标。如果约束是非线性的比如障碍物距离约束就需要做线性化近似或者用其他方法。我实际调试时发现路径优化的效果很大程度上取决于参考线的质量。如果参考线本身抖动厉害优化出来的路径也会抖。所以ReferenceLineProvider里的平滑参数非常关键不能随便设。4.2 速度规划的STGraph方法速度规划的核心是回答一个问题在路径上的每个点我应该以什么速度通过Apollo里用STGraph来解决这个问题。横轴是时间t纵轴是路程s。自车在ST图上的投影是一条曲线曲线的斜率就是速度。障碍物在ST图上投影成一个个矩形区域表示在某个时间段内某个路程区间被占用。速度规划的任务就是找一条从起点到终点的曲线避开所有障碍物矩形同时满足速度、加速度、加加速度jerk的约束。这条曲线的生成方法有几种一种是基于动态规划DP在ST图上离散化找一条代价最小的路径另一种是基于二次规划QP在DP结果的基础上做平滑还有一种是基于采样生成多条候选曲线然后选最优的。Apollo里通常用DPQP的组合先用DP找一个粗略的解再用QP做精细优化。这里有个经验ST图的离散化精度很关键。如果时间步长太大障碍物矩形会变得很粗可能导致规划出的速度曲线过于保守如果时间步长太小计算量会急剧增加。我一般会把时间步长设在0.1秒左右路程步长设在0.5米左右这个精度在大多数场景下够用。4.3 碰撞检测的实现细节规划模块输出的轨迹必须保证不与任何障碍物碰撞。碰撞检测听起来简单实际实现要考虑很多细节。首先是障碍物的表示。Apollo里障碍物通常用矩形或者多边形表示带有位置、朝向、尺寸。对于动态障碍物还要考虑它的预测轨迹也就是未来一段时间内它会在哪。然后是碰撞检测的时机。不是只在规划完成后检测一次而是在规划过程中就要不断检测。比如路径优化时要把障碍物作为约束加进去速度规划时要把障碍物投影到ST图上。规划完成后还要做一次最终的碰撞检测确保没有遗漏。最后是碰撞检测的保守性。实际系统里障碍物的位置和预测都有误差所以碰撞检测通常要留一定的安全余量。这个余量不能太大否则车辆会过于保守该走的时候不走也不能太小否则有碰撞风险。我一般会根据障碍物的类型和速度来动态调整安全距离比如对静态障碍物留0.5米对动态障碍物留1米以上。4.4 轨迹平滑与舒适性规划出的轨迹不仅要安全还要舒适。舒适性主要体现在几个方面加速度不能太大、加加速度不能太大、曲率不能太大、曲率变化率不能太大。Apollo里在路径优化和速度规划之后通常会加一个平滑步骤。路径平滑可以用样条曲线拟合速度平滑可以用低通滤波或者二次规划。平滑的力度要适中太强了会导致轨迹跟踪不上太弱了又起不到平滑效果。我实际测试时发现舒适性调参是最耗时间的。因为不同的人对“舒适”的定义不一样有的人喜欢激进的驾驶风格有的人喜欢保守的。Apollo里有一堆舒适性相关的参数比如max_acceleration、max_jerk、max_curvature这些参数需要根据车辆的实际能力和乘客的接受度来调。5. 调试与调参那些文档里不会写的经验5.1 规划结果不对怎么排查规划结果不对是规划模块调试中最常见的问题。排查思路可以按这个顺序来第一步看输入数据。规划模块的输出不对很多时候是输入有问题。检查定位是否跳变、障碍物列表是否为空、地图信息是否正确、路由是否合理。我遇到过好几次规划结果异常是因为定位模块输出的位置突然跳了几米导致规划模块以为自车在另一个车道。第二步看Scenario选择。在Apollo的调试工具里可以看到当前帧进入了哪个Scenario、哪个Stage。如果Scenario选错了后面的Task再正确也没用。比如该进路口场景却进了车道保持那规划出的轨迹肯定不对。第三步看Task执行。每个Task都有输入输出可以逐个检查。比如路径优化Task看它输出的路径是否合理速度规划Task看它输出的速度曲线是否合理。如果某个Task的输出异常就重点查那个Task的参数和逻辑。第四步看最终轨迹。把规划出的轨迹画出来和参考线、障碍物、地图叠加在一起看。很多时候一眼就能看出问题在哪比如轨迹穿过了障碍物、轨迹偏离了车道、轨迹抖动厉害。5.2 参数调整的优先级Apollo规划模块的参数非常多全部调一遍不现实。我的经验是按这个优先级来调最高优先级安全相关参数。比如碰撞检测的安全距离、最大减速度、最大加速度。这些参数直接关系到会不会撞车必须先调好。次高优先级场景切换相关参数。比如进入换道场景的速度阈值、进入路口场景的距离阈值。这些参数决定了车辆在什么情况下做什么决策影响很大。中等优先级舒适性相关参数。比如最大jerk、路径平滑权重。这些参数影响乘坐体验但不影响安全。较低优先级性能相关参数。比如优化求解器的迭代次数、ST图的离散化精度。这些参数影响计算耗时在算力充足的情况下可以最后调。5.3 常见坑与避坑方法坑一参考线抖动导致规划抖动。参考线是规划的基础如果参考线本身不平滑规划出的轨迹也会抖。避坑方法是加强参考线的平滑或者在路径优化时增大平滑权重。坑二障碍物预测不准导致规划保守。预测模块给出的障碍物未来轨迹如果过于保守比如预测所有车都会突然变道规划模块就会过于谨慎该走的时候不走。避坑方法是和预测模块的同事对齐预测的置信度和保守程度。坑三Scenario切换频繁导致规划震荡。如果两个Scenario的切换条件设置得太接近车辆可能会在两个场景之间反复切换导致规划结果震荡。避坑方法是给Scenario切换加一个滞回区间比如进入条件比退出条件更严格。坑四ST图离散化太粗导致速度规划不合理。ST图的离散化精度直接影响速度规划的质量。如果太粗障碍物矩形会失真可能导致规划出的速度曲线过于保守或者过于激进。避坑方法是根据实际场景调整离散化精度在计算资源和规划质量之间找平衡。坑五调试工具用不对导致排查效率低。Apollo有一套可视化调试工具但很多人不熟悉排查问题时只能看日志效率很低。避坑方法是花时间熟悉调试工具学会看Scenario、Stage、Task的状态学会把规划结果和输入数据叠加显示。6. 从模块到系统规划与其他模块的协作边界6.1 规划与感知的接口规划模块依赖感知模块提供障碍物信息。这个接口看似简单实际有很多细节。感知模块输出的障碍物列表包含每个障碍物的位置、速度、朝向、尺寸、类型、置信度。规划模块用这些信息来做碰撞检测和决策。但感知的输出不是完美的会有漏检、误检、延迟。规划模块必须对这些不完美有一定的容忍度。比如感知偶尔会漏掉一个远处的车辆规划模块如果完全依赖感知就可能忽略这个车辆。所以实际系统里规划模块通常会结合预测模块的输出对感知结果做一定的补全和校验。6.2 规划与预测的接口预测模块给出障碍物未来一段时间内的可能轨迹。规划模块用这些轨迹来做决策比如判断是否可以在某辆车前面换道。预测的输出通常是一个概率分布每个障碍物有多条可能的轨迹每条轨迹有一个概率。规划模块怎么用这个概率分布是一个设计选择。保守的做法是考虑所有可能的轨迹激进的做法是只考虑概率最高的轨迹。Apollo里通常是折中考虑概率超过某个阈值的轨迹。6.3 规划与控制的接口规划模块输出一条轨迹控制模块跟踪这条轨迹。这个接口的关键是轨迹的表示形式和时间对齐。Apollo里规划输出的轨迹通常是离散点序列每个点带时间戳。控制模块根据当前时间和轨迹的时间戳插值出当前应该跟踪的目标点。这里有个细节规划轨迹的时间戳必须和控制模块的时间基准对齐否则会出现跟踪滞后或者超前。6.4 规划与地图的接口高精地图给规划模块提供车道线、路口、交通标志等信息。地图的质量直接影响规划的质量。如果地图的车道线不准规划出的路径就会偏如果地图的路口信息不全规划模块就不知道该怎么通过路口。实际项目中地图和规划往往是两个团队在做接口对齐非常重要。我建议在项目早期就定义好地图数据的格式和更新机制避免后期返工。7. 写在最后一些个人体会Apollo的规划模块代码量很大涉及的算法很多刚开始看会觉得无从下手。我的建议是不要一上来就啃代码先理解它的设计哲学场景驱动、插件化、分层决策。理解了这些再看代码就会清晰很多。另外规划模块的调试非常依赖工具和数据。光看代码是看不出问题的必须结合实际数据去跑、去看、去分析。我见过很多开发者代码读得很熟但一遇到实际问题就懵了就是因为缺乏实际调试经验。最后规划模块的安全性是第一位的。任何参数调整、算法改动都要经过充分的测试验证。不要为了追求性能或者舒适性牺牲安全性。这个原则怎么强调都不为过。