☰
Apollo Lattice Planner深入浅出:解耦采样、代价评估与实车调参
2026/10/3 11:09:01 网站建设 项目流程

做自动驾驶规划控制有段时间的朋友,大概率都翻过Apollo的代码。源码仓库里规划模块那一大坨,第一次打开的时候是真容易劝退,尤其是lattice_planner这个目录,看起来文件不多,但里面又是Trajectory1d又是LatticeTrajectory1d,类跟类之间的关系绕得人头疼。我最早接触Lattice Planner是在一个园区低速接驳项目上,那时候团队里没人真正啃过Apollo源码,大家在EM Planner和Lattice之间纠结了很久,最后还是选了Lattice,原因很简单——它思路直白,采样、选优、碰撞检测,每一步都能单独拎出来看,出了问题也好定位。等真正把代码一行行理完、又把参数调到能在实车上稳定跑起来之后,我对这套算法的评价是:它未必是效果上限最高的规划器,但绝对是最适合拿来学习轨迹规划、也最适合在限定场景里快速落地的方案之一。

这篇内容适合几类人:刚入门自动驾驶规划,想知道轨迹规划到底在解决什么问题的人;已经在用Apollo但主要调参、没深入看过Lattice实现的人;或者你在自研规划模块,想找一套能抄作业的采样+代价函数框架。下面我会把Lattice Planner从思想到实现再到实车调参的完整链路拆开讲,里面的参数和踩坑记录都来自实际项目,你可以直接把结论拿去用。

1. 轨迹规划为什么不能靠“打点连线”解决

先搞清楚一个容易被忽略的问题:全局导航给出的路径,和车真正要走的轨迹,根本是两码事。导航路径是一条几何线,它不包含“什么时候到”“以多快速度经过”这些信息,更不关心这条线上某个位置会不会撞上旁边突然窜出来的行人。轨迹规划要输出的,是一条带时间戳的序列——每个时刻车在哪儿、朝向是多少、速度是多少、加速度是多少。这条序列必须满足车辆运动学约束,比如前轮转角有限制,横摆角速度不能无限大;必须满足舒适性约束,也就是加速度变化率不能太夸张;还必须满足安全性约束,在动态障碍物的包围下不能撞。

1.1 路径规划与轨迹规划的分工边界

在Apollo的架构里,routing模块负责全局路径,planning模块负责局部轨迹。Lattice Planner属于planning的一部分,它接收上游的参考线(ReferenceLine)、障碍物信息、定位和底盘状态,然后在参考线附近搜索一条满足约束的局部轨迹。这里有一个很关键的点:Lattice是“局部”规划器,它的视野有限,通常只看前方几十到一百多米,视野之外靠全局路径兜底,所以它不需要、也不可能做出长距离的战术决策。它要做的,是“在接下来几秒内,把车从当前状态安全、舒适地挪到某个目标状态”。

这个定位决定了Lattice的很多设计取舍。比如它不太擅长处理需要长距离预判的复杂博弈场景,但在结构化道路、低速园区、限定区域的场景里,它的简单直接反而是优势。

1.2 为什么选Lattice而不是EM Planner

Apollo里面还有一套更出名的EM Planner,它的核心思路是“EM迭代+动态规划/二次规划”,把轨迹规划拆成路径规划和速度规划两个步骤,用动态规划提供粗解,再用二次规划平滑。EM Planner的上限更高,能处理更复杂的场景,但代价是代码复杂,参数多,调起来非常痛苦。Lattice的思路不一样:它先把问题简化——横向和纵向解耦,然后在Frenet坐标系下分别采样,生成一堆候选轨迹,最后用代价函数打分选出最优。整个过程没有迭代优化,没有动态规划,逻辑非常线性,出了问题很容易追溯到是采样没采到、还是代价函数权重不对、还是碰撞检测误报。

团队做技术选型的时候,如果追求快速落地、场景可控、团队技术水平还在爬坡期,Lattice是性价比很高的选择。如果要做开放道路、复杂交互场景,EM或者基于学习的方法才是正路,但学习曲线也陡得多。

2. Lattice Planner的思想拆解:横向和纵向为什么能分开

Lattice最核心的思想是解耦。车辆运动本身是横纵耦合的,转向会影响纵向速度,加速也会改变横向响应。但如果把问题放到Frenet坐标系里,事情就变得清爽很多。

Frenet坐标系是沿着参考线建立的:纵向用s表示沿参考线方向的距离,横向用l表示偏离参考线的距离。在这个坐标系里,一条道路可以被想象成一个“拉直的管道”,车辆在这个管道里的运动被拆成“顺着管道走”和“在管道内左右偏”两个独立维度。Lattice Planner干的事,就是在s-t和l-s两个平面里分别采样目标状态,各自生成一条一维轨迹,再把这两条一维轨迹合并成二维轨迹。

2.1 Frenet坐标系怎么理解

打个比方,参考线像一条河道,车辆像一条船。船在河道里的位置,可以用“离河口多远”(对应s)和“离左岸/右岸多远”(对应l)两个数字来描述。船既可以往前走,也可以左右调整航向,但这两个动作在“河道坐标系”里是相对独立的。Frenet坐标系最大的好处是,把“沿路走”和“靠左靠右”解开了,道路的弯曲程度被“拉直”了,很多计算在数学上就简单了。

这一点特别重要,因为如果直接在笛卡尔坐标系下做采样和多项式拟合,道路一转弯,轨迹就很难描述,代价函数也很难写。而在Frenet坐标系里,一条弯道的横向偏移计算和直道几乎没有区别。

2.2 横向轨迹与纵向轨迹的解耦逻辑

横向轨迹描述的是l随时间或随s的变化曲线,它决定了车在车道里的横向位置——是从车道中心走,还是往左变道,还是往右靠边。纵向轨迹描述的是s随时间的变化曲线,它决定了车的速度轮廓——是匀速巡航、加速超车、还是减速停车。

这两个维度在生成的时候是独立的,但最后合并的时候要互相校验。比如我打算变道到左侧车道,采样终态的时候在横向上生成一条从当前车道中心到左侧车道中心的轨迹,在纵向上生成一条带目标速度的跟车轨迹。合并之后才能检查这条“边变道边加速”的轨迹会不会撞车。如果碰撞检测不通过,就换一组横纵向组合继续试。

2.3 采样,不是撒点而是撒“目标状态”

“撒点采样”听起来像是乱试,但Lattice的采样其实非常讲究。它不是在(x, y)空间里随便撒点,而是在(s, l, t, v, a)这个状态空间里,针对每一类驾驶意图生成一组“终点状态候选”。比如巡航场景,纵向采样会以不同的目标速度、不同的到达时间生成终点;跟车场景,会以不同的跟车距离生成终点;停车场景,会以不同的减速度、停车位置生成终点。横向采样则围绕当前车道的中心线,生成一组不同横向偏移的目标位置,比如0米(居中)、±1米(轻微靠边)、±2米(接近车道边界附近)等。

每个终点状态都是一组目标值,然后用多项式拟合出一条从当前状态到目标状态的平滑轨迹。通过控制终点状态的分布密度和范围,就能控制轨迹搜索的覆盖范围——采得越密,覆盖越全,计算量也越大。

3. 实操拆解:Apollo Lattice Planner的关键环节

看源码的时候,建议按这条主线走:先看LatticePlanner::Plan,它把整个流程串起来;然后依次看LatticeTrajectory1d、Trajectory1dGenerator、LatticeEvaluator这几个核心类。下面把每个环节的关键细节和参数选择逻辑都过一遍。

3.1 输入处理:参考线和障碍物怎么进Lattice

Lattice Planner不是直接吃全局导航路径的。上游会先把全局路径投影到车道内部、做平滑,生成一条局部参考线。参考线的质量直接影响Lattice的上限——如果参考线本身有锯齿或者曲率跳变,后面生成的所有轨迹都会继承这些问题。所以实际项目中,reference_line_provider的平滑参数很重要,smoother的w_curvature、w_smoothness这些权重不要用默认值怼到复杂弯道上,得根据场景调。

障碍物信息进来的时候,会被投影到Frenet坐标系下,变成一个或多个ST图里的障碍物矩形。这个过程看着简单,其实坑不少:障碍物形状投影到ST图时,要考虑自车在换道过程中是否会扫过障碍物的“膨胀区域”;低速场景下的行人、自行车轨迹预测不准,ST图里的矩形位置就可能偏。这里需要给障碍物地图加一层安全缓冲,实测下来加上缓冲和没加缓冲,实车表现差异非常大。

3.2 横向采样:从l=0到l=±4.5米的一组候选

在典型配置里,横向采样的目标偏移集合大概是这样的:

  • 保持当前车道:l = 0
  • 微调:l = ±1.0米
  • 靠边/让行倾向:l = ±2.0米
  • 贴近车道线附近:l = ±3.0米
  • 变道目标车道中心附近:l = ±4.5米

这组数字不是拍脑袋定的。车道宽度一般在3.5米左右,车道中心线到车道边界的距离约1.75米;如果车辆宽度取2米,那车辆边界离车道线差不多还有0.75米余量。l=±3.0米已经非常贴近边界,除非场景需要,一般不作为首选目标。±4.5米通常对应相邻车道中心线(两边各3.5米车道),用于变道场景。

横向轨迹用五次多项式来拟合,从当前l、l'、l''到目标l、l'=0、l''=0。五次多项式保证了轨迹在起点和终点的位置、速度、加速度都连续,这一点直接决定了车辆的横向控制会不会抖动。有些论文会用三次多项式,但三次只能保证位置和速度连续,加速度会跳变,实车上会感觉一顿一顿的。

3.3 纵向采样:速度、距离、时间三个维度的组合

纵向采样的设计比横向更复杂,因为纵向意图非常多。常见配置是把纵向采样分成几类:

  • 巡航:目标速度v = 0.5、1.0、1.5倍当前限速,时间t = 4.0、6.0、8.0秒,终点距离Δs由速度和时间的乘积决定;
  • 跟车:根据前车速度和距离,采样多个目标跟车距离,通常等于前车速度乘一个时间间隔(如1.2s、1.8s、2.4s车头时距),再加最小停车间距;
  • 停车:目标速度v=0,目标位置在前方停止线或者障碍物前0.5~1.0米,时间采样覆盖4~8秒。

纵向轨迹的拟合通常对位移使用四次多项式或者五阶多项式,这样能同时约束s、v、a在起终点连续。这里有个细节我觉得值得单独说:纵向在时间上的采样密度直接决定了计算量。如果把时间轴从4秒到8秒每0.5秒采一个点,那么每个目标距离又会对应一批候选轨迹,最后数量和横向候选一做笛卡尔积,总轨迹数量可能大几百条。每条都要做碰撞检测和代价计算,算力压力就在这儿。

3.4 轨迹生成之后:合并、碰撞检测、代价评估

横向候选和纵向候选两两组合,就会生成“一条候选纵向轨迹×一条候选横向轨迹”的二维轨迹。在Apollo里,这一步会得到DiscretizedPath和对应的速度曲线。先做粗碰撞检测:用自车的包络矩形沿轨迹扫一遍,看是否与障碍物的膨胀矩形相交;然后做细碰撞检测:把轨迹离散成若干个点,在每个点用车体矩形(或者圆形)与障碍物计算距离。只有通过碰撞检测的轨迹才能进代价评估。

代价函数是Lattice的“灵魂”,它决定了在无碰撞的轨迹里选哪一条。Apollo的LatticeEvaluator会统计一系列分数:

  • 舒适性代价:轨迹的加加速度(jerk)积分值,越大越不舒适;
  • 效率代价:到达目标的时间、平均速度与期望速度的偏差;
  • 横向偏移代价:l偏离参考线越大,扣分越多;
  • 终点朝向代价:轨迹终点航向与参考线切向的夹角;
  • 靠右行驶倾向(一些版本):在右侧通行的场景里,对偏左的轨迹加额外惩罚。

这些代价不是简单相加,每项的权重决定了车辆的行为风格。比如把效率权重调高,车会更激进;把舒适性权重调高,车会更温柔但可能压住后车。实车调参的时候,这些权重是体感差别最直接的地方。

3.5 代价函数权重:一套可落地的初始配置

我基于Apollo的默认权重和几个落地项目的经验,给出一套可以当起点的权重配置:

代价项默认参考值调高后效果注意点
jerk积分1.0车更“柔”,启动/停车更缓太高会让车变肉、容易被加塞
横向偏移1.0~2.0更贴车道中心/更保守不配合变道场景会导致变道延迟
目标速度偏差5.0~10.0更愿意追限速太高会让车顶着前车屁股走
终点朝向偏差1.0更早回正方向在匝道、弯道要降低
停车距离偏差10.0停车更准高到一定程度会牺牲舒适性

这套配置在园区低速场景(最高时速30km/h以下)表现比较稳。上了快速路(60~80km/h),需要把jerk权重降一点、效率权重升一点,否则后车会闪灯。

4. 碰撞检测与ST图:Lattice的杀手锏和隐藏的坑

Lattice是采样式规划器,它能不能保证安全,完全取决于“采样是否覆盖了安全解”。如果在一个场景里,所有采样出的轨迹都会碰撞,那Lattice就无解了,它不会像EM那样通过优化硬生生“挤”出一条安全路径。所以在设计采样范围时,要确保覆盖足够多的安全驾驶行为。这看着有点“笨”,但反过来也带来了好处——因为每一组采样意图都很明确,所以你能直接看出算法为什么选了某条轨迹,调试很方便。

4.1 碰撞检测的两种主流方式:矩形扫掠与圆形近似

Apollo碰撞检测里,比较核心的是把自车离散成若干个覆盖圆(也有的版本用矩形包络),然后沿轨迹逐点检测圆(或矩形)与障碍物边界的距离。用圆形近似车辆的优点是计算快、无方向性;缺点是四个角上会有虚检,明明车头角已经擦到障碍物了,但圆和障碍物还有距离,这会增加“看起来能过其实过不了”的情况。用矩形包络更精确,但计算量更大。

实际项目中建议两步走:先用粗碰撞检测把所有明显撞上的轨迹筛掉,再用细碰撞检测对剩下的轨迹逐点检查。这样能把单帧规划耗时控制在几十毫秒以内,不至于把算力全耗在碰撞检测上。

4.2 ST图如何辅助速度规划

Lattice虽然没有显式维护一个ST图优化模块,但它的纵向采样结果可以理解为是在隐式地对ST图做离散搜索。ST图的横轴是时间t,纵轴是纵向距离s,障碍物在ST图里是一块块“禁入区域”。如果前方有一辆慢车,ST图里就会出现一个随时间向右上方移动的障碍物区域,Lattice纵向采样生成的各种速度曲线,本质上就是在尝试绕过这块区域——要么减速让行,要么加速超过。

这里有个实操要点:ST图里的障碍物速度怎么定,会显著影响效果。如果直接用当前速度外推,障碍物一旦急刹,ST图里的禁入区域会失真,采出的轨迹可能离真实障碍物位置太近。我见过一个项目里把所有动态障碍物在ST图里的矩形都加宽了时间轴方向上的膨胀量,效果立竿见影,车离行人、电动车远远的。

4.3 容易被忽略的坑:采样分辨率与计算量

Lattice的轨迹数量可以粗略估算:假设横向候选有11条,纵向每个意图下候选有20条,总候选就是220条。每条轨迹做一次全路程碰撞检测(比如100米、0.1米一个点,1000个点),再算一次代价函数(要积分jerk等),单帧计算量就在几十毫秒这个量级。如果参考线再长一点、采样再密一点,算力就会爆。

更隐蔽的坑是:采样点太疏可能导致某个意图下所有轨迹都被碰撞检测拦掉,车就会在正常场景下频繁“无解”。比如在穿过一个窄门或者绕桩场景,横向偏移采样只有0、±1.0、±2.0,而绕桩需要偏移2.3米,那这组采样里就没有一条能安全通过。遇到这种情况,唯一的办法是加采样点,或者调整参考线让门桩在参考线上的投影更居中。这个问题的排查思路是:先看日志里是不是频繁出现“无可行轨迹”,再逐步调高横向采样的范围,检查能通过的轨迹起点偏移。

5. 常见问题与排查技巧实录

做Lattice落地,有几个问题几乎每个项目都会遇到。我把它们整理成一张速查表,后面再展开说几个我印象最深的排查过程。

现象可能原因排查思路与解法
频繁报“无可行轨迹”采样范围不够、碰撞检测过于保守扩大横向偏移集合、降低障碍物膨胀系数、检查参考线是否被障碍物切断
车在弯道里抖动参考线平滑不足、横向目标点超出车道检查参考线平滑权重、把横向采样范围限制到车道边界内、降低代价函数里l变化的惩罚
变道动作太慢、犹豫变道意图没有对应合适的纵向轨迹检查变道的目标车速是否过低、增加Δs较大的一组纵向候选、提高效率代价权重
停车不够准,总是过冲或提前停停车采样终点距离设置不合理增加0.5米间隔的多组停车距离候选、把停车距离代价项的权重调高
高速场景下舒适性差jerk权重太低、速度采样间隔太大调高jerk积分代价、加密速度采样(如0.1m/s间隔)
单帧计算耗时>100ms轨迹候选过多、碰撞检测过细减少纵向时间采样点、粗碰撞检测前置、把自车圆形数量减少但适当加大半径

5.1 实例一:窄路绕障场景频繁无解

做园区项目的时候,有一段路两边停满了车,中间留出来的通道特别窄。车开着开着就停在原地不动,日志里全是“No feasible trajectory”。一开始以为是障碍物检测太灵敏,把膨胀半径调小了,但没解决。后来把横向采样的候选打印出来才发现,最靠边的候选只有l=±2.0米,而通道的有效宽度要求车至少偏移2.3米才能不碰上旁边的车。把横向采样集合扩展成0, ±1.0, ±1.5, ±2.0, ±2.5, ±3.0,同时增加了靠边场景对应的纵向低速候选,问题就消失了。

这个排查让我长了个记性:先确认采样范围是否覆盖了当前场景的解空间,再动代价参数。代价函数只能排序不能无中生有,采样范围没覆盖安全解,再怎么调权重都没用。

5.2 实例二:连续弯道上车身姿态来回摆

另一个项目里,车在连续S弯里会出现方向盘反复修正、车身姿态来回摆的情况。看了轨迹之后发现,横向轨迹在弯道里被反复规划成“先偏左再偏右再偏左”。根因是参考线在弯道处的平滑度不够,导致相邻两帧的参考线横向偏移基准发生了跳变,Lattice基于跳变的参考算出的最优轨迹也就不一样。

解决方法是两级处理:一是把参考线平滑的曲率权重调大,让连续弯道上的参考线更“顺”;二是在代价函数里增加了一个相邻帧横向偏移变化率的惩罚项,强迫两次规划的横向轨迹不要差太多。严谨一点的做法是用“上一次最优轨迹的横向偏移”作为参考基准之一,让新的轨迹不要突然跳变。这一项加进去之后,实车明显稳了很多。

5.3 实例三:变道场景忽快忽慢、犹豫不决

变道的时候,车经常出现“加速超过旁边车又减速缩回去”的诡异行为。分析下来是纵向采样里,超车和跟车两类意图的候选轨迹分数接近:超车轨迹因为速度高,效率分数好,但舒适性差一点;跟车轨迹因为稳,舒适性好,但效率分数低。两者差距非常小,于是算法在帧间来回切换,表现为车一会儿激进一会儿保守。

这类问题没有标准答案,实际做法是给“变道意图”加上一个状态机:当变道指令已经下发,就只允许搜索变道相关的轨迹集合,同时把代价函数里效率项的权重临时调高,避免中途放弃。等变道完成,再恢复到正常权重。这其实是在Lattice外围做了一层简单的决策逻辑,但非常有效。

5.4 独门技巧:离线回放调参

我强烈建议,所有人在上车之前先做“离线回放”。把实车采集的/planning、/localization、/perception话题数据录成bag,然后在离线环境里重放,把规划结果和实际行驶轨迹叠加在一起看。这一步能做两件事:第一,快速重现问题,不用在车上反复试;第二,调参后立刻看效果,不用一趟一趟跑场地。我们团队后来形成了一条习惯:任何参数修改,先在离线数据上跑一遍对比,验证有效再上实车。整个Lattice调参时间大概缩短了三分之二。

6. Lattice Planner的上限与适用边界

讲到这里,还是要泼一盆冷水:Lattice Planner不是万能的。它的采样式思想决定了它在高维复杂场景里会力不从心。

6.1 什么时候该用Lattice,什么时候该换方案

Lattice非常适合结构化程度高的场景:园区接驳、封闭停车场、高速巡航、城市快速路。在这些场景里,道路结构清晰、驾驶意图明确、障碍物类型相对简单,Lattice的效率和可靠性都很好。但如果你要做开放道路、复杂城区、大量行人非机动车混行,或者需要做高度博弈的变道/汇入决策,Lattice的结构就绷不住了。不是说完全不能跑,而是你为了让它跑通,会在外围堆越来越多的规则和状态机,最后整个系统变得又复杂又脆弱。

这时候,EM Planner的迭代优化思路甚至基于学习的算法,可能更合适。但它们对团队工程能力的要求也高很多。很多团队其实是在Lattice外面包一层行为决策,用“场景分类+规则限制”的方式,把复杂场景拆成一个个简单场景,每个简单场景再交给Lattice去算。这算是工业界最务实的做法。

6.2 往后的扩展方向:从Lattice到 learning-based planner

在Lattice框架上做扩展,比较现实的路径是:把采样过程oracle化——用学习模型预测更可能安全的终态,而不是靠固定网格;或者用强化学习调整代价函数权重,让行为风格能自适应场景。这些方向都有学术界在推进,但距离工程成熟还有距离。对大多数自动驾驶团队来说,把Lattice吃透、调好、再加上一层稳定的决策状态机,已经能覆盖非常多的量产项目需求了。

结尾

我在实际项目里把Lattice Planner从看懂到调好,前后折腾了快两个月。回头看,真正卡住我的不是代码读不懂,而是“采样范围没覆盖安全解”和“代价函数权重和场景不匹配”这两个问题反复出现。如果你也在啃这套算法,我的建议是:先别急着调参,先把每一帧规划里的采样轨迹、碰撞检测结果、代价分数三项数据打印出来,盯着看几帧,你对这套算法的理解会立刻上一个台阶。等你把横向偏移集合、纵向时间采样密度、代价权重这三组参数和你自己的场景对齐了,这辆车开着顺手了,你才算真正把Lattice Planner变成了自己的工具。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询