ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

自动驾驶规划算法Lattice Planner深度拆解:从Frenet坐标到轨迹生成

自动驾驶规划算法Lattice Planner深度拆解:从Frenet坐标到轨迹生成 如果你翻过几篇——不哪怕只看过一眼——自动驾驶规划相关的论文或者代码仓库你可能已经注意到一个现象Lattice Planner这个名字反复出现。它最早可以追溯到上世纪末的论文在深度学习、端到端自动驾驶、大模型几乎成了流量入口的年代它看起来像个“老古董”。但我在实际做自动驾驶规划控制算法落地和仿真调试时发现今天很多量产车里装的规划器内部仍然躺着一个Lattice Planner或者它的直接变体。原因不复杂它结构清楚、逻辑可解释、参数可调出了问题能一层层翻开来看。这篇文章我想从一个做规划的人的视角把Lattice Planner从头到尾拆一遍——坐标系怎么选、轨迹怎么生成、代价怎么算、工程里容易在哪翻车以及它真正的能力边界。适合刚接触规划算法的同学作为入门地图也适合已经在写代码但常被调参折磨的工程师对照着找思路。1. 它解决的是规划问题里的哪一半1.1 先定位Lattice Planner在软件栈里的位置一套完整的自动驾驶软件系统大致可以拆成感知、预测、决策、规划、控制这几层。前面的感知负责回答“现在周围有什么”预测负责回答“这些障碍物接下来会怎么动”决策负责给出“我们该执行什么驾驶意图”比如直行、换道、靠边停车。而规划模块的输入是自车状态、参考线、障碍物预测结果和决策指令输出则是一条带时间戳的轨迹。这里有个容易混淆的概念决策和规划不是一回事。Lattice Planner本质上是“在给定驾驶意图下做运动轨迹生成”它不会去思考“下一路口该左转还是右转”。如果决策层说“三秒后换到左车道”Lattice Planner就负责把“怎么换、每秒钟在什么位置、车速怎么变”这件事用数学方式算出来。如果你让Lattice Planner在没有行为指令的情况下自由发挥它通常会按照默认的巡航逻辑跑遇到障碍物只会保守地减速或绕行绝对不会主动做出“这个路口我要连续变两条道”这样带有博弈色彩的选择。整体上看规划问题有两种常见的建模思路一种是先算一条几何路径再做速度规划另一种是直接在一个“空间时间”的联合空间里找一条时空轨迹。Lattice Planner走的是后一条路线而且它用了一个非常聪明的坐标系变换把时空轨迹问题拆成了两个更容易处理的子问题。1.2 为什么说Lattice是一种“采样评估”范式Lattice Planner属于采样类规划方法。它的工作方式可以概括成三个词枚举、评估、挑一个。具体来说它会提前定义好一组“未来可能到达的状态”比如3秒后横向偏移到左侧1.5米、纵向速度保持在15米每秒5秒后回到车道中心、速度收敛到12米每秒。每一个这样的状态组合都会生成一条候选轨迹然后所有候选轨迹通过代价函数打分、约束检查过滤最后选一个既安全又舒适的轨迹发给控制模块。这种“宁可多算也要保证兜底”的思路和优化类的EM Planner不太一样。EM Planner是先把路径和速度当成两个最优化问题交替迭代而Lattice Planner更像是在答案空间里撒网捞鱼。撒多密的网、用什么标准判断鱼好不好决定了捞上来的轨迹质量。2. 把轨迹拆成“横”“纵”两路Frenet坐标系究竟解决了什么2.1 笛卡尔坐标系到底哪里不好用很多初学者一上来就困惑我们明明在直角坐标系下也能画一条连续光滑的曲线为什么要绕道去Frenet坐标系假设你在一个半径200米的弯道上开车用笛卡尔坐标描述车辆位置是(x, y)。现在前方有一个静止车辆你需要判断“它到底在我的车道内还是车道外”。在笛卡尔坐标下你得找参考线、算相对距离、判断左右边界数学非常绕。但如果换成Frenet坐标系事情就变成s表示沿道路中心线的弧长d表示相对于道路中心线的横向偏移。那个障碍物如果也在我的车道内它的d值和我的d值会很接近判断逻辑直接变成“横向偏差是否在安全范围”。更关键的是在Frenet坐标下横向规划和纵向规划可以彻底解耦。横向运动方向盘在控制的量只需要关注d随时间的变化纵向运动油门和刹车在控制的量只需要关注s随时间的变化。把一个二维轨迹规划拆成两个一维多项式拟合计算量直接降了一个数量级而且每个量的物理意义非常直观。2.2 坐标转换从全局坐标到Frenet坐标实际工程里坐标转换通常是这么做的先把事先规划好的参考线离散成一串点然后在每个控制周期里寻找自车在参考线上的最近点。这个最近点对应的弧长就是s自车到参考线的带符号距离就是d。从笛卡尔转换回Frenet也很直接s参考线上离车辆最近点的累计弧长d车辆位置到该最近点的有向直线距离左侧通常定义为正而由Frenet变换回笛卡尔则依赖于参考线上匹配点的坐标(rx, ry)和单位法向量(nx, ny)x rx d * nx y ry d * ny听起来不复杂但工程上的坑在“匹配点怎么找”。如果每个周期都从参考线第一点开始遍历计算代价会随路径长度线性增长如果不做处理车辆在急弯附近可能会出现匹配点跳变也就是常说的“s方向来回抖动”。常见的做法是从上一周期的匹配点索引附近开始搜索并约束匹配点不能倒退太多保证s的单调性。2.3 横向轨迹和纵向轨迹各自负责什么在Frenet坐标系下横向轨迹的变量是d(t)它描述了车辆相对车道中心线的横向位置随时间的变化。换道、绕障、压线修正都体现在这个变量里。纵向轨迹的变量是s(t)它描述了车辆沿道路前进的进度跟车距离控制、红绿灯前停车、限速收敛都由它决定。两者最终被合并成一条完整轨迹。合并时每个时间点都有一对(s(t), d(t))通过参考线坐标变换还原成全局坐标下的(x(t), y(t))同时可以通过对d和s求导得到每个时刻的航向角、曲率、速度、加速度等控制量。我从实际经验说说为什么这种拆法好用。有一次我在现场遇到一个问题车辆在高速弯道上总是轻微画龙方向控制一直在小幅抖动。后来定位到原因是横向轨迹的目标偏移量设置得太“死板”没有随参考线曲率变化做调整。如果一开始就在Frenet坐标下做这件事你一眼就能看出问题出在d方向的目标序列上而不是x、y两个方向互相纠缠。3. 五次多项式与四阶多项式从边界条件反推一条平滑轨迹3.1 为什么偏偏是多项式Lattice Planner候选轨迹的生成本质上是“给定起点状态和终点状态构造一条满足边界条件的曲线”。多项式是干这件事最顺手的工具只要边界条件数量足够系数就能唯一确定而且多项式的求导非常方便速度、加速度、加加速度可以一路求下去。横向规划通常使用五次多项式d(t) a0 a1*t a2*t^2 a3*t^3 a4*t^4 a5*t^5为什么是五阶因为横向轨迹需要同时约束起点的位置、速度、加速度以及终点的位置、速度、加速度一共6个边界条件对应6个未知系数。换句话说你要保证从当前时刻开始换道那一刻的姿态是平滑过渡的结束换道时车也已经稳稳地回到车道中心或目标偏移位置不能有突兀的横向速度或横向加速度跳变。纵向规划则通常使用四阶多项式s(t) b0 b1*t b2*t^2 b3*t^3 b4*t^4纵向轨迹需要约束起点的位置、速度、加速度以及终点的位置、速度一共5个边界条件所以四阶够用。有些实现里纵向也会用五阶主要是为了把终点加速度也约束住让车辆在停车或跟车时更平顺。3.2 六元一次方程组的矩阵解法横向五次多项式的6个系数可以通过线性方程组解出来。我们在工程里普遍的做法是写一个统一的求解函数把边界条件塞进矩阵import numpy as np def solve_quintic(x0, x1, v0, v1, a0, a1, T): 求五次多项式系数 x0, v0, a0: 起点的位置、速度、加速度 x1, v1, a1: 终点的位置、速度、加速度 T: 轨迹总时长 返回 [a0, a1, a2, a3, a4, a5] A np.array([ [1, 0, 0, 0, 0, 0 ], [0, 1, 0, 0, 0, 0 ], [0, 0, 2, 0, 0, 0 ], [1, T, T**2, T**3, T**4, T**5 ], [0, 1, 2*T, 3*T**2, 4*T**3, 5*T**4], [0, 0, 2, 6*T, 12*T**2, 20*T**3], ]) b np.array([x0, v0, a0, x1, v1, a1]) return np.linalg.solve(A, b)之前有人看到这种矩阵写法觉得过于“数学”但其实它只是在解一个六元一次方程组。你可以把它理解成起点有3个约束条件终点有3个约束条件刚好把6个系数锁死。没有自由度留给你调形状所有候选轨迹的形状完全由边界条件和总时长T决定。纵向四阶多项式类似def solve_quartic(x0, x1, v0, v1, a0, T): 求四阶多项式系数 x0, v0, a0: 起点的位置、速度、加速度 x1, v1: 终点的位置、速度 T: 轨迹总时长 返回 [a0, a1, a2, a3, a4] A np.array([ [1, 0, 0, 0, 0], [0, 1, 0, 0, 0], [0, 0, 2, 0, 0], [1, T, T**2, T**3, T**4], [0, 1, 2*T, 3*T**2, 4*T**3], ]) b np.array([x0, v0, a0, x1, v1]) return np.linalg.solve(A, b)拿到系数之后在轨迹的每个采样时刻计算位置、速度和加速度就非常简单了def evaluate(coeffs, t): t2 t * t t3 t2 * t t4 t3 * t t5 t4 * t x coeffs[0] coeffs[1]*t coeffs[2]*t2 coeffs[3]*t3 coeffs[4]*t4 coeffs[5]*t5 v coeffs[1] 2*coeffs[2]*t 3*coeffs[3]*t2 4*coeffs[4]*t3 5*coeffs[5]*t4 a 2*coeffs[2] 6*coeffs[3]*t 12*coeffs[4]*t2 20*coeffs[5]*t3 j 6*coeffs[3] 24*coeffs[4]*t 60*coeffs[5]*t2 return x, v, a, j这里的j就是加加速度jerk后面代价函数的主角之一。3.3 一个直观例子换道和跟车的轨迹差异拿换道来举例。假设当前车辆在右侧车道横向偏移d -1.75米以车道中心为0、左正右负纵向速度15米/秒。决策层说“2.5秒后换到左侧车道”那么横向目标就是d 1.75米横向速度、横向加速度都归零纵向目标可以是“终点速度仍然15米/秒”。用上面的矩阵解出五项式你会得到一条S形的横向轨迹一开始横向加速度缓增中段反向末尾收敛到零对应方向盘“先左打一点、再回正”的流畅动作。纵向四阶则保持速度几乎不变这样合并出来的轨迹就是一次稳定换道。跟车场景就完全不同。假如前方200米有辆慢车以10米/秒行驶决策层给出“降到10米/秒并保持距离”的指令那么纵向目标就从15米/秒收敛到10米/秒四阶多项式会生成一条平滑的减速曲线。横向保持车道中心偏移不变。从这个例子可以看出Lattice Planner非常擅长“给定明确目标后生成平滑过渡轨迹”这类问题。4. 采样空间与代价函数候选轨迹的生成逻辑和筛选原则4.1 采样到底在采什么生成一条候选轨迹需要确定三组参数总时长T、终点横向状态、终点纵向状态。Lattice Planner的做法是对这三组参数分别做等间隔采样形成笛卡尔积然后把每个组合都生成一条轨迹。我常用的采样参数大致如下参数采样范围步长说明总时长T1.0 ~ 8.0 秒0.2 ~ 0.5 秒太短完不成动作太长预测不可靠横向偏移d_target-3.5 ~ 3.5 米0.2 ~ 0.5 米覆盖相邻车道中心及中间位置纵向终点速度v_target0 ~ 当前最大限速1 ~ 2 米/秒决定巡航快慢与跟车收敛速度纵向终点位置s_target当前s v*T 附近视场景用于停车、跟车等明确目标位置以T取21个点、横向21个点、纵向11个点为例一次规划周期的候选轨迹数量在几千条量级。Lattice Planner 的核心矛盾就在这里采样越密越可能覆盖最优轨迹但计算量越大筛选和碰撞检测越慢。所以采样密度必须在“覆盖能力”和“实时性”之间做精细权衡。4.2 代价函数是怎么逼出一条“舒服又能走”的轨迹候选轨迹有了怎么比较谁好谁坏答案就是代价函数。代价函数本质上是把多个优化目标加权求和。下面是我自己实现和调参时惯用的几个代价项横向偏移代价期望车辆贴近参考线/目标车道中心表达式可以用终点横向偏移的平方也可以累加全程横向偏移的平方。加加速度代价jerk车辆舒适性的核心指标分别对横向jerk和纵向jerk积分。jerk越大乘客越容易晕车。纵向速度/位置偏差代价希望车辆按决策层给的目标速度行驶或者到达目标位置。时间代价动作时间越长占用道路资源越久效率越低所以对较大的T施加惩罚。与上一帧轨迹的一致性代价希望当前周期选出的轨迹和上一周期尽量接近不给控制器造成跳变。一个典型的代价函数伪代码如下delta_d d_target - traj.d[-1] # 最终横向偏差 delta_v v_target - traj.s_d[-1] # 最终纵向速度偏差 delta_s s_target - traj.s[-1] # 最终纵向位置偏差 # 横向代价 traj.cd K_J * traj.d_ddd_integral K_D * delta_d**2 K_T * T # 纵向代价 traj.cv K_J * traj.s_ddd_integral K_V * delta_v**2 K_S * delta_s**2 # 总代价 traj.cf traj.cd traj.cv权重的相对大小直接决定了车辆的“性格”。K_D大车辆会死死贴住车道中心换道动作会比较“急”K_J大车辆会很佛系慢慢悠悠地变道后面的车可能会骂人K_V大则会让车辆拼命追目标速度可能跟前车贴得很近。4.3 候选轨迹的无约束筛选流程这里有个细节需要注意绝对不能把所有候选轨迹直接丢进代价函数里选最小。正确顺序一定是一层一层过滤——先滤掉那些明显不满足物理约束和碰撞安全的轨迹再从剩余的“安全集”里选代价最低的。不然很可能选出一条数学代价很低、但会直接撞上护栏的轨迹。我的过滤顺序一般是检查轨迹末状态的横向偏移是否越出道路边界或目标车道范围检查全程每个时刻的曲率是否超过车辆最小转弯半径对应的最大曲率检查纵向速度是否超过当前道路限速、加速度是否超过舒适性阈值做碰撞检测把和静态/动态障碍物距离过近的轨迹一律剔除把从当前车辆状态无法起始的轨迹剔除比如起点速度不匹配只有通过所有约束检查的轨迹才有资格进入代价排序。这一步在工程里极其重要因为它保证了“选出来的轨迹即使不是最优也一定安全”。5. 可行性与碰撞检测从“数学上优美”到“物理上能开”5.1 车辆不能只看成一个点很多教程做碰撞检测时直接把车辆简化为一个点这在开阔场景里没太大问题但在窄路、桩桶阵、停车场这类场景里一定会出事。真实车辆的轮廓是长宽大约4.8米×1.9米的一个矩形规划时不能用点代替。实践中一种用得最多的方案是多圆近似法。把车辆俯视图沿着纵轴切成三段或多段每一段用一个圆来近似圆的半径要能把对应矩形区域完全包住。碰撞检测时逐个检查每个圆是否与障碍物也近似成圆或多边形相交。好处是圆与圆之间的距离计算比多边形相交要快得多适合在几千条轨迹上反复执行。在代码里大致是这样def check_collision(traj, obstacle_list, vehicle_circles): # vehicle_circles: [(offset_x, radius), ...] 相对车辆中心的圆 for i in range(len(traj.t)): # 轨迹当前时刻车辆的全局坐标和航向 vx, vy, yaw traj.x[i], traj.y[i], traj.yaw[i] for offset, radius in vehicle_circles: cx vx offset * np.cos(yaw) cy vy offset * np.sin(yaw) for obs_x, obs_y, obs_r in obstacle_list: dist np.hypot(cx - obs_x, cy - obs_y) if dist radius obs_r: return False return True对动态障碍物不能直接用当前时刻的位置判断必须结合预测模块给出的“障碍物未来若干秒的预测轨迹”。最朴素的预测是假设障碍物保持匀速直线运动复杂一点则可以给每个时间点的置信椭圆。5.2 运动学约束曲率、加速度和jerk碰撞安全只是必要条件一条轨迹如果让车辆以超过物理极限的横向加速度转弯那也是不可执行的。常见的运动学约束有以下几点曲率约束轨迹在任意一点的曲率不能超过车辆最小转弯半径对应的曲率上限。在Frenet坐标系下曲率近似可以用横向二阶导表示但更稳妥的做法是转换到笛卡尔坐标后按曲率公式精确计算。加速度约束纵向加速度必须落在油门/制动能够提供的范围内同时也要符合舒适性预期。一般乘用车纵向加速度控制在 ±3 m/s² 以内急刹除外。横向加速度约束高速变道时横向加速度过大乘客会感到明显侧倾。经验值一般控制在 2 m/s² 以下具体看车型和路况。jerk约束乘用车的纵向jerk一般不超过 2 m/s³否则顿挫感很强。我在实测中发现很多新人只检查曲率而忽略横向加速度。高速上曲率不大但横向速度变化很快照样能让乘客吓一跳。所以曲率检查 横向加速度检查要同时上。5.3 动态障碍物的“相对运动”陷阱Lattice Planner对动态障碍物处理的核心思路是每个采样时刻车辆在轨迹上的位置和障碍物预测位置之间的距离要足够大。这看起来简单实际上有一个很隐蔽的问题障碍物预测轨迹和自车轨迹在时间上要对齐。举个例子自车在3.5秒时到达某点而障碍物预测说它在2.8秒时经过同一个点。如果只看几何路径两条线确实相交但时间上错开了所以并不一定碰撞。反过来几何距离够大但时间上可能恰好挤在一起。所以检查时必须把“时间对齐后的空间距离”作为判断依据而不是单纯算两个轨迹的空间交点。动态场景还有一个经典难题如果障碍物在相邻车道而它恰好也在3秒后变到自车车道那自车一切基于“它保持当前车道”的假设都会失效。这就是Lattice Planner上限受限的地方它只能根据预测结果做“条件式安全”无法主动和对方博弈。后续如果预测模块的置信度不足更稳妥的做法是大幅降低速度拉大时间间隔。6. 一个可以动手跑的简化实现骨架Python看完前面的理论我建议真正动手写一遍。下面这个简化实现的核心逻辑和工程版本一致——横向用五次多项式纵向用四阶多项式采样生成多条轨迹再按代价筛选。为了好读我砍掉了大量与场景相关的预处理只保留了主题框架。import numpy as np class FrenetPath: def __init__(self): self.t [] self.d [] self.d_d [] self.d_dd [] self.d_ddd [] self.s [] self.s_d [] self.s_dd [] self.s_ddd [] self.cd 0.0 self.cv 0.0 self.cf 0.0 self.x [] self.y [] self.yaw [] def generate_frenet_paths(s0, sd0, sdd0, d0, dd0, ddd0, target_speed, dt, road_width): 生成候选轨迹 s0/d0 系列当前Frenet状态 target_speed决策层给的目标纵向速度m/s dt轨迹采样间隔 road_width道路宽度用于限制横向采样范围 paths [] # 对总时长T和横向终点偏移d_target做采样 for T in np.arange(2.0, 6.0, 0.5): for d_target in np.arange(-road_width/2, road_width/2 1e-6, 0.5): # 纵向起点状态固定终点速度设为target_speed coeff_s solve_quartic(s0, s0 target_speed * T, sd0, target_speed, sdd0, T) # 横向起点状态固定终点横向速度/加速度归零 coeff_d solve_quintic(d0, d_target, dd0, 0.0, ddd0, 0.0, T) fp FrenetPath() for t in np.arange(0.0, T dt, dt): fp.t.append(t) s, sd, sdd, sddd evaluate(coeff_s, t) d, dd, ddd, dddd evaluate(coeff_d, t) # 注意使用横向系数 fp.s.append(s) fp.s_d.append(sd) fp.s_dd.append(sdd) fp.s_ddd.append(sddd) fp.d.append(d) fp.d_d.append(dd) fp.d_dd.append(ddd) fp.d_ddd.append(dddd) paths.append(fp) return paths上面的solve_quartic、solve_quintic、evaluate直接用上一章代码块里的版本就行。生成轨迹后再对每条轨迹做代价评估def calc_cost(fp, target_speed, target_d): # 横向jerk和纵向jerk的离散积分 ddd_integral np.sum(np.abs(np.diff(fp.d_ddd))) * dt sdd_integral np.sum(np.abs(np.diff(fp.s_ddd))) * dt delta_d target_d - fp.d[-1] delta_v target_speed - fp.s_d[-1] fp.cd 1.5 * ddd_integral 1.0 * delta_d**2 0.1 * fp.t[-1] fp.cv 1.5 * sdd_integral 1.0 * delta_v**2 fp.cf fp.cd fp.cv return fp.cf这个骨架没有做碰撞检测和Frenet到笛卡尔的完整变换但如果你把它接到一条平滑参考线上寻找最近点、计算法向量就能看到一个二维平面上的换道轨迹动起来。我在学习阶段就是这么做的理解效果非常好。7. 实车与仿真中容易翻车的细节轨迹拼接、参考线质量与调参顺序7.1 轨迹拼接解决“每帧规划都从零开始”的问题我最初在仿真里跑Lattice Planner时遇到一个特别典型的毛病车辆在巡航时总是轻微左右画龙。后来发现原因不在控制器而是规划器每周期都从车辆当前状态重新规划导致输出的轨迹在帧与帧之间落下零点几米的偏移控制模块来回跟踪这些偏移车身就开始画龙。正确做法是轨迹拼接。具体来说上一周期选中的轨迹在本次规划时刻附近的状态要作为下一周期轨迹采样的起点。或者在代价函数中加入“与上一帧轨迹终点距离”的惩罚项让新的候选轨迹更倾向于延续上一帧的选择。我在工程里更偏向前者把上一周期轨迹在当前时刻之后剩余的部分保留下来作为“拼接段”再把新规划的轨迹和拼接段做平滑衔接。这能保证即使采样空间里的轨迹在某一帧发生了细微变化给到控制模块的轨迹序列也不会突变。7.2 参考线质量决定一切Lattice Planner所有计算都建立在参考线之上。参考线不平滑、方向突变Frenet坐标里的s和d就会变得不稳定横向代价的计算结果自然不可信。我踩过的坑是用传感器拼接出的原始路径当作参考线直接送进规划器。那条路径在弯道上有肉眼可见的锯齿导致车辆在一段看似平直的路上周期性抖动。后来用样条平滑或基于二次规划的平滑算法预处理参考线问题立刻消失。工程上有一个实用指标参考线曲率导数的最大值。如果这个值在相邻几个点之间跳动过大那说明平滑度不够Lattice Planner在这种参考线上面生成的轨迹质量会非常差。所以遇到轨迹抖动时我第一个查的往往不是采样密度而是参考线的平滑度。7.3 调参数的先后顺序舒适性先行效率次之避障最后和很多参数化算法一样Lattice Planner的调参没有银弹但有一个经验顺序可以少走弯路。先调jerk权重。把横向和纵向jerk权重调到能让车辆在正常巡航时无明显顿挫的量级这是舒适性的底线。再调横向偏移权重。确保车辆在直道和弯道上都能稳定贴线不要出现“两头摆”的现象。然后调纵向速度权重。让车辆在无车时能稳定收敛到目标速度不忽快忽慢。最后调碰撞相关权重和采样范围。保证在遮挡场景下车辆会提前减速而不是冲到离障碍物很近才做紧急避让。每调一个参数都要在仿真中观察至少几个完整动作周期尤其是换道、跟车、停车再起步这三个场景。7.4 预测不准的时候怎么办Lattice Planner对动态障碍物的表现高度依赖预测模块。预测给的轨迹误差大再好的规划器也会“踩空”。在实车上当预测置信度不高时最稳妥的兜底策略是做保守减速把车控制在一个不会和任何不确定障碍物发生碰撞的速度区间。具体实现上我习惯给动态障碍物的碰撞检测半径加上一个和不确定性相关的膨胀系数。障碍物预测轨迹越不置信膨胀系数越大等效于把障碍物“画粗”。代价是车辆会更早降速、变道会更保守但换来的是安全裕度。7.5 无解时的兜底策略采样规划还有一个躲不掉的问题极端场景下可能没有任何一条轨迹满足所有约束。比如前方突然出现一个很宽的障碍物横向采样范围不足以绕开纵向速度采样又没有一个能来得及刹停。绝对不能直接输出空轨迹或者停在原处。工程上要准备一条“紧急减速轨迹”——通常只做纵向减速不做横向避让尽量把车安全停在当前车道内。如果再不行就得往更上层发异常请求比如请求决策模块降低目标速度或开放更宽的可行驶区域。8. 它的天花板在哪里以及我的使用建议Lattice Planner最大的优点是可解释性和工程可控性强。你可以在日志里看到每一条候选轨迹的每一个代价分量知道它为什么选了A而不是B。这一点在量产出问题的排查阶段是巨大的优势。但它也有明确的边界。高度动态的交互场景比如无保护左转、环岛合流、密集行人过街它很容易因为预测置信度不够而被迫变得极度保守表现为“不敢走”“等半天”。博弈层面的决策问题本质上已经超出了轨迹规划的范畴需要行为决策甚至更上层的行为预测模型去解决。另外采样分辨率带来的算力问题也一直存在。要覆盖更复杂的换道场景采样数量会膨胀实时性就会下降。所以今天很多量产方案会把它和优化类方法结合用Lattice做粗选、用QP或MPC做精修和跟踪或者像Apollo那样用EM Planner的分层思想替代部分功能。我个人的建议是如果你是刚接触自动驾驶规划Lattice Planner是性价比最高的入门算法没有之一。它把坐标系变换、多项式拟合、代价函数设计、碰撞检测这些规划基础课题全部串了起来。最好亲自动手把骨架代码跑通再看一遍Apollo的实现源码然后试着把横向采样改成更智能的分布或者加入障碍物避让约束。等你真正理解它在哪个环节强、哪个环节弱再去看MPC、EM Planner甚至端到端方案会有完全不一样的判断力。就我自己带项目的经验来说最后能快速定位和解决量产问题的工程师往往不是那些背了很多论文公式的人而是对这类基础算法“拆过、装过、踩过坑”的人。Lattice Planner恰恰是这样一个值得拆开再装回去的经典。
返回列表