3、Lattice Planner详解:Frenet坐标系、横向与纵向轨迹采样、代价函数设计、轨迹选择与发布

各位同学,今天我们来啃一块硬骨头——Lattice Planner。

说实话,这个算法在工业界用得非常多。我最早接触它是在2018年,当时我们团队要做一个高速场景下的换道决策。传统的DWA和A*在结构化道路上表现还行,但一遇到曲率变化大的弯道,轨迹就变得很“拧巴”。后来我翻到了MIT的一篇论文,才真正理解了Frenet坐标系的价值。

说白了,Lattice Planner的核心思想就一句话:把三维时空的规划问题,拆成两个一维问题来处理。一个管横向(左右偏移),一个管纵向(前后速度)。这样拆开之后,计算量小了很多,而且轨迹质量反而更高。

3.1 Frenet坐标系:为什么非它不可?

我们先聊聊坐标系。Cartesian坐标系(就是咱们常见的x-y坐标系)在直道上用着挺顺手,但一遇到弯道就麻烦了。你想想看,一条弯曲的道路,用x-y描述车道中心线,得用一堆多项式去拟合,而且横向偏移和纵向距离是耦合的。

Frenet坐标系就不一样了。它把道路中心线当作参考线,用两个变量来描述车辆状态:

  • s:沿着参考线走的弧长(纵向)
  • d:偏离参考线的距离(横向,左正右负)

这样一来,横向控制和纵向控制就解耦了。我在项目中遇到过一个问题:用Cartesian坐标系做换道规划,车辆在弯道里总是“画龙”。后来换成Frenet坐标系,同样的控制参数,轨迹平滑了很多。原因很简单——Frenet坐标系天然贴合道路几何。

核心公式:

在Frenet坐标系下,车辆状态表示为:

纵向状态:[s, s_dot, s_ddot]  (位置、速度、加速度)
横向状态:[d, d_dot, d_ddot]  (位置、速度、加速度)

转换到Cartesian坐标时,需要用到参考线的曲率κ(s)。

嗯,这里要注意一点:Frenet坐标系依赖参考线的质量。如果参考线本身不平滑,那后面生成的轨迹也会出问题。我建议在构建参考线时,至少做一次三次样条插值,保证曲率连续。

3.2 横向轨迹采样:怎么生成候选路径?

横向轨迹,说白了就是车辆怎么从当前车道变到目标车道。Lattice Planner的做法是:在终点状态空间里采样,然后反推轨迹。

具体来说,我们会在目标车道的横向位置d_target附近,采样一组终点状态:

  • d_end:在目标车道中心线附近,比如 -0.5m, 0m, 0.5m
  • d_dot_end:横向速度,通常设为0(平稳切入)
  • d_ddot_end:横向加速度,通常设为0

然后,我们用五次多项式来拟合从当前状态到终点状态的轨迹。为什么是五次多项式?因为它能同时约束位置、速度、加速度——六个边界条件,刚好对应五次多项式的六个系数。

我的经验:

采样密度不是越高越好。我曾经试过每0.1米采一个点,结果生成了上百条轨迹,计算耗时翻了三倍,但最终选出来的轨迹和0.5米采样的结果差不多。现在我的习惯是:横向采样步长0.3~0.5米,纵向采样时间步长0.5~1.0秒。够用就好。

3.3 纵向轨迹采样:速度怎么规划?

纵向轨迹解决的是“什么时候到”的问题。我们需要考虑前方障碍物的运动状态、交通信号灯、限速等因素。

纵向采样的思路和横向类似:在终点时间T和终点速度v_end上采样。常见的采样策略包括:

  • 巡航模式:保持当前速度,终点速度 = 当前速度
  • 跟车模式:终点速度 = 前车速度,终点位置 = 前车位置 - 安全距离
  • 停车模式:终点速度 = 0,终点位置 = 停止线

对于每种模式,我们会采样不同的T值(比如4秒、6秒、8秒),然后用四次或五次多项式生成纵向轨迹。

避坑指南:

我曾经在纵向采样时忽略了一个细节——加速度变化率(jerk)。有一次生成的轨迹,速度曲线看起来没问题,但jerk值高达15 m/s³,车上乘客直接“点头”。后来我在代价函数里加上了jerk惩罚项,才解决了这个问题。记住:乘客的舒适度,往往比到达时间更重要。

3.4 代价函数设计:怎么选出最优轨迹?

采样生成了几十上百条轨迹,怎么挑出最好的一条?这就需要设计代价函数了。

我一般把代价函数分成三部分:

代价项 含义 权重建议
横向偏移代价 偏离目标车道中心的距离 0.5 ~ 1.0
纵向加速度代价 加速度的平方和,衡量舒适度 0.3 ~ 0.8
jerk代价 加速度变化率的平方和 0.2 ~ 0.5
时间代价 到达终点的时间,越短越好 0.1 ~ 0.3
障碍物距离代价 与最近障碍物的距离,越远越好 1.0 ~ 2.0

实际项目中,权重需要根据场景动态调整。比如在高速上,时间代价的权重可以低一些,安全距离的权重高一些;在城区拥堵路段,舒适度的权重可以适当提高。

代价函数示例(伪代码):

def cost_function(trajectory, obstacles):
    cost = 0.0
    # 横向偏移代价
    cost += w_lat * sum(trajectory.d_deviation ** 2)
    # 纵向加速度代价
    cost += w_acc * sum(trajectory.acceleration ** 2)
    # jerk代价
    cost += w_jerk * sum(trajectory.jerk ** 2)
    # 时间代价
    cost += w_time * trajectory.total_time
    # 障碍物距离代价
    min_dist = min_distance_to_obstacles(trajectory, obstacles)
    if min_dist < SAFE_DISTANCE:
        cost += w_obs * (SAFE_DISTANCE - min_dist) ** 2
    return cost

3.5 轨迹选择与发布:最后一步

计算完所有轨迹的代价后,我们选代价最小的那条。但这里有个坑:代价最小的轨迹不一定是最安全的。

我遇到过一种情况:有一条轨迹代价很低,因为它离障碍物很远,但它的横向加速度很大,车辆需要猛打方向。这种轨迹虽然“安全”,但会让乘客很不舒服。所以我在选择轨迹时,会加一个“可行性检查”:

  • 最大横向加速度是否超过阈值(比如 2 m/s²)
  • 最大纵向jerk是否超过阈值(比如 5 m/s³)
  • 轨迹是否与障碍物发生碰撞(哪怕代价很低)

只有通过所有检查的轨迹,才会被发布给下游的控制模块。

发布策略:

我习惯发布未来8秒的轨迹点,时间步长0.1秒。控制模块拿到这80个点后,会做插值和跟踪。注意,发布的轨迹要包含完整的运动状态(位置、速度、加速度、航向角),这样控制模块才能做前馈补偿。

3.6 知识体系总览

下面这张图总结了Lattice Planner的核心流程。你可以看到,从Frenet坐标系开始,到横向纵向采样,再到代价评估和轨迹发布,每一步都环环相扣。

Lattice Planner 核心流程 Frenet坐标系 横向轨迹采样 纵向轨迹采样 横向+纵向组合 代价函数评估 轨迹选择与发布 注:横向和纵向采样独立进行,最后组合成完整的轨迹簇

好了,以上就是Lattice Planner的核心内容。从Frenet坐标系到轨迹发布,每一步都有它的设计哲学。我个人觉得,这个算法的精髓在于“解耦”——把复杂问题拆成简单问题,再组合起来。你在实际项目中遇到问题时,也可以试试这个思路。

一句话总结:

Lattice Planner = Frenet坐标系 + 横向五次多项式 + 纵向四次多项式 + 代价函数 + 可行性检查。记住这个公式,你就掌握了80%的内容。

可可精品资料库