1. 为什么需要TopologyPRM?从“局部极小值”这个坑说起
大家好,我是老张,在无人机路径规划这个领域摸爬滚打了十来年。今天咱们来啃一块硬骨头——Fast-Planner里的TopologyPRM算法。很多朋友在用Fast-Planner做无人机实时避障时,可能都遇到过一种情况:无人机飞到一些像狭窄“山谷”或者障碍物“脊背”的地方时,突然就“卡住”不动了,或者开始原地“抽搐”。优化算法明明在跑,但就是找不到出去的路。这就是典型的陷入了局部极小值。
你可以把局部极小值想象成一个光滑的碗底。你的无人机就像一颗小球,从碗边滚下去,最后肯定会停在碗底。梯度下降法(GTO的核心)就是那个让小球往下滚的力。在碗底,四面八方都是上坡,小球自然就停在那里了。但在真实的三维复杂环境里,这个“碗”的形状千奇百怪。比如在两个靠得很近的障碍物中间,ESDF(欧几里得符号距离场)提供的梯度信息可能会把轨迹往两个相反的方向推,导致优化“精神分裂”,最终卡死。
Fast-Planner的第二篇核心论文,也就是我们今天要讲的,它的主要贡献就是提出了PGO(路径引导优化)来治这个病。而TopologyPRM,就是为PGO生成“药引子”的。它的任务不是直接给出最终轨迹,而是快速找到几条在拓扑结构上不同的、安全的“路网草图”。有了这几条不同的草图,后续的轨迹优化就有了多个高质量的初始解,从而大大降低了掉进局部极小值这个坑的概率。所以,理解TopologyPRM,是理解Fast-Planner如何实现“鲁棒”重规划的关键一步。
2. TopologyPRM算法全景:一张图看懂五大步骤
在一头扎进代码之前,我们先从高处俯瞰一下TopologyPRM的全貌。它的工作流程非常清晰,就像一个经验丰富的探险家绘制迷宫地图,主要分为五个阶段,对应代码中的五个核心函数模块。
- 构建拓扑路图:这是算法的基石,对应
createGraph函数。它会在起点和终点之间的空间里,智能地撒下两种“路标”:守卫点和连接点,形成一张稀疏但能反映环境拓扑结构的图。 - 深度搜索路径:图建好了,就要找路。对应
searchPaths函数。它使用深度优先搜索,从起点出发,探索所有能到达终点的路径,并初步收集起来。 - 路径缩短优化:找到的原始路径往往弯弯绕绕。对应
shortcutPaths函数。它像一个精益工程师,尝试“拉直”路径,在保证安全的前提下尽可能缩短距离。 - 剪枝等效路径:不同的搜索路径可能在拓扑意义上是“同一条路”。对应
pruneEquivalent函数(在selectShortPaths中也会调用)。它会利用均匀可视变形准则,剔除那些本质重复的路径,保留拓扑多样的候选集。 - 选择最短路径集:最后,对应
selectShortPaths函数。从拓扑多样的路径中,再挑选出长度最短的几条,作为输出,交给后面的PGO模块使用。
整个过程的输入是起点、终点和环境ESDF地图,输出就是几条拓扑不同且较短的3D路径。下面,我们就一个模块一个模块地拆开看,我会结合我实际调试中的经验,把原理和代码实现给你讲透。
2.1 核心引擎:拓扑路图是如何构建的?
构建拓扑路图是整个算法的第一步,也是最精妙的一步,代码实现在TopologyPRM::createGraph函数里。它借鉴了经典的PRM(概率路图)思想,但做了关键的改进,使其生成的图能更好地捕捉空间的拓扑特征。
第一步:初始化与坐标系构建算法首先将起点和终点标记为两个特殊的“守卫点”,并赋予它们ID 0和1。接下来一个关键操作是建立一个局部坐标系。这个坐标系的原点设在起点和终点的中点,X轴指向从原点到终点的方向。这个操作非常实用,它把采样区域从全局坐标系转换到一个与任务方向对齐的局部坐标系中,使得后续的采样更有方向性,提高了效率。代码里通过计算xtf,ytf,ztf三个正交向量来实现。
第二步:循环采样与分类处理然后进入主循环,在设定的时间和数量限制内随机采样。对于每一个采样点pt,算法会做两件事:
- 安全性检查:查询ESDF地图,计算该点到最近障碍物的距离。如果小于安全阈值
clearance_,直接舍弃。这一步保证了所有图节点都在安全区域内。 - 可见性查询:调用
findVisibGuard,找出当前采样点能“看见”的所有已有的守卫点。“看见”意味着两点连线不与障碍物相交。
接下来,根据可见守卫点的数量,分三种情况处理,这是算法的核心逻辑:
- 情况一:可见守卫点数为0。说明这个点是一片“新大陆”,与现有路图没有直接可见连接。那就把它提升为新的守卫点,加入图中。守卫点可以理解为路网的主要“十字路口”。
- 情况二:可见守卫点数为2。这是最有趣的情况。如果这两个守卫点之间还没有直接连接,那么这个采样点就可能成为一个有价值的连接点。但并不是所有这样的点都需要。这里用到了一个
needConnection函数来判断。它的内部通常基于均匀可视变形检查:如果连接这两个守卫点的新路径(通过当前采样点)与任何现有路径在拓扑上是等价的,那么这个连接可能就是冗余的,不需要添加。如果需要,则创建连接点,并把它与两个守卫点双向连接起来。 - 情况三:可见守卫点数为1或其他。简单忽略,继续采样。因为1个可见点无法形成新的连接,多于2个则可能使图过于复杂。
我踩过的一个坑:这里的clearance_和采样区域sample_r_参数设置非常关键。clearance_设小了,在图构建阶段就可能把一些狭窄但能通过的通道节点给过滤掉,导致最后找不到路径。我一般会把它设得比无人机实际物理半径稍大一点,留出一点余量。sample_r_则决定了搜索范围,在复杂迷宫环境里可以适当调大,但要注意计算量会增加。
2.2 路径发现:深度优先搜索的实战应用
路图构建好后,searchPaths函数负责从中找出所有从起点到终点的路径。这里用的是最经典的深度优先搜索。
代码里实现了一个递归函数depthFirstSearch。它维护一个visited列表,记录当前路径已访问的节点。从起点开始,递归地探索每一个邻居节点。如果邻居节点是终点(ID为1),就把当前路径记录下来。如果邻居节点是未访问过的普通节点,就把它加入visited列表,继续深入探索。完成一个分支的探索后,通过vis.pop_back()进行回溯,尝试其他分支。
这里有一个重要的性能优化点:原始DFS可能会搜出非常多条路径,尤其是图比较稠密的时候。代码中做了两处处理:第一,设置max_raw_path_上限,防止搜索爆炸;第二,在searchPaths函数最后,对所有找到的路径按节点数量(可以近似看作路径复杂度)排序,只保留节点数最少的那一批(数量由max_raw_path2_控制)。这个策略很聪明,因为通常节点数少的路径,其几何长度也更可能较短,拓扑上也更简单,能为后续优化提供更好的起点。
在实际使用中,如果发现搜索时间过长,可以适当调低max_raw_path_和max_raw_path2_。但要注意别调得太低,否则可能把一些有价值的拓扑路径过早地过滤掉。我一般会先保持默认值,如果遇到性能瓶颈,再根据日志输出的raw path num来调整。
2.3 化曲为直:路径缩短的碰撞感知优化
通过DFS找到的路径,是严格沿着图节点走的,就像沿着街道网格走路,难免有直角弯。shortcutPaths函数的目标就是“抄近道”,在保证安全的前提下把路径拉直。它对应论文中的算法2,是提升路径质量的关键一步。
其核心函数shortcutPath采用了一种迭代贪心策略,我把它叫做“推土机”算法:
- 离散化:将当前路径用密集的点序列表示。
- 可见性缩短:从路径起点开始,尝试用直线连接当前点与后方远处的点。用
lineVisib函数检查这条线段是否完全可见(无碰撞)。 - 碰撞处理:如果发生碰撞,
lineVisib会返回碰撞点colli_pt。算法会计算该点处的ESDF梯度grad(指向远离障碍物的方向)。然后,计算一个“推离方向”push_dir,这个方向是梯度方向在线段垂直方向上的投影。最后,将碰撞点沿推离方向移动一个步长resolution_,得到一个新的路径点。 - 迭代:将新点加入路径,然后从新点开始继续向后尝试拉直,直到到达终点。
这个过程会迭代多次(iter_num),每次都在上一次缩短的基础上继续优化。代码中还支持多线程并行处理多条路径(parallel_shortcut_选项),这在需要处理大量候选路径时能显著加速。
一个实用的调试技巧:shortcutPath的效果非常依赖于resolution_(路径离散化和推离的步长)和ESDF地图的精度。如果resolution_设置得比地图网格分辨率大很多,可能会“感知”不到一些细小障碍物,导致缩短后的路径不安全。我通常会把resolution_设置成与地图网格分辨率相同或略小。另外,可以观察缩短前后路径长度的变化,如果某条路径缩短后长度几乎没变,说明它可能已经接近最优,或者环境约束非常强。
2.4 去芜存菁:基于均匀可视变形的路径剪枝
经过缩短,我们得到了一批优化后的路径。但它们之间可能存在拓扑等价的情况。比如,在同一个障碍物的同一侧,两条稍微有点弯曲的路径,其实对于无人机来说飞起来没啥本质区别。TopologyPRM使用UVD(均匀可视变形)来判断两条路径是否拓扑等价。
核心函数是sameTopoPath。它的逻辑很直观:
- 取两条待比较的路径。
- 将它们分别离散成相同数量的点(数量由较长路径的长度除以
resolution_决定)。 - 依次检查两条路径上对应位置的点之间的连线是否可见(无碰撞)。
- 如果所有对应点连线都可见,则认为这两条路径是UVD等价的,即同属一个拓扑类别。
pruneEquivalent函数就利用这个准则,从一个路径集合中筛选出拓扑独特的子集。而selectShortPaths函数则在此基础上,进一步筛选出长度最短的几条路径。它的策略是:先选出绝对最短的一条,然后保留那些长度不超过最短路径ratio_to_short_倍的其他拓扑路径。这样,最终输出既保证了质量(短),又保证了多样性(拓扑不同)。
这里有个关键参数ratio_to_short_。默认值通常是1.2或1.3。这个值设得越大,保留的备选路径就越多,后续PGO找到更好解的可能性越大,但计算量也增加。在计算资源紧张的机载计算机上,我有时会把它调到1.1,优先保证实时性。在离线规划或者算力充足时,可以调到1.5,给优化器更多选择。
3. 关键参数调优心得:让你的无人机飞得更稳
纸上得来终觉浅,绝知此事要躬行。看懂代码只是第一步,要把TopologyPRM用得好,参数调优是关键。下面我结合自己的项目经验,把几个最核心的参数拉出来讲讲,你可以把它们当作调试的起点。
| 参数名 | 所在文件/函数 | 默认值(参考) | 作用与调优建议 |
|---|---|---|---|
clearance_ | topo_prm.cpp | 0.2m? (需确认) | 安全距离。图节点与障碍物的最小允许距离。调大更安全,但可能丢失狭窄通道;调小更激进,能发现更多路径,但风险增加。建议设为无人机半径+0.05~0.1m。 |
max_sample_time_/max_sample_num_ | createGraph | 例如 1.0s / 2000 | 采样限制。控制建图阶段的时间和采样次数。环境复杂时,调大这些值可以提高找到路径的概率,但会增加前期计算时间。如果总是找不到路径,优先增加这两个值。 |
resolution_ | 类成员变量 | 0.1m? (需确认) | 分辨率。影响路径离散化、UVD检查、缩短步长。调小精度高,更安全,但计算量剧增;调大计算快,但可能“踩线”或忽略细节。建议与全局规划栅格地图分辨率保持一致。 |
ratio_to_short_ | selectShortPaths | 1.2 | 长度比率阈值。控制最终输出路径的多样性。调大,输出更多拓扑不同的路径,优化效果可能更好,但耗时增加;调小,输出更少、更聚焦于最短路径,实时性高。在实时性要求高的场景,可以设为1.1。 |
parallel_shortcut_ | 类成员变量 | true | 并行缩短开关。如果CPU核心数多于1,强烈建议保持开启,能大幅缩短shortcutPaths阶段的时间。 |
除了这些,还有像sample_inflate_(采样区域膨胀系数)、reserve_num_(保留路径数)等参数,都可以根据实际情况微调。我的建议是:不要一次性改动太多参数。先理解每个参数的意义,然后从一两个最可能影响你当前问题的参数开始调。比如无人机在狭窄环境老是规划失败,就先看clearance_和采样参数;如果规划速度跟不上,就先看resolution_和并行开关。
4. 与PGO的协同:从拓扑路径到光滑轨迹
最后,我们聊聊TopologyPRM在整个Fast-Planner流水线中的位置。它本身不产生最终控制无人机飞行的轨迹,它的产出是几条3D的折线路径。这些路径会被送给PGO(路径引导优化)模块。
PGO会以每一条拓扑路径为“引导”,在其周围构建一个隧道状的约束区域,然后在这个区域内进行基于梯度的轨迹优化(GTO)。由于有多条拓扑不同的初始引导,即使其中一条引导对应的优化陷入了局部极小值(比如卡在“山谷”里),其他引导也可能将优化拉出来,找到全局更优的解。这就是“路径引导”的真正威力——用拓扑多样性来对抗局部极小值。
在代码层面,TopologyPRM类通常会被FastPlannerManager这样的高层管理器调用。管理器在收到重规划请求后,会调用TopologyPRM::findTopoPaths获取多条拓扑路径,然后依次调用轨迹优化模块。因此,在你自己集成或调试时,不仅要关注TopologyPRM本身的输出是否正确,还要确保它输出的路径格式与后续优化模块的输入要求匹配。
我自己在项目里就遇到过一个问题:TopologyPRM输出的路径点序是好的,但后续优化时总是碰撞。后来发现是坐标系没对齐,TopologyPRM工作在全局坐标系,而我的优化器误以为输入是在另一个局部坐标系里。所以,数据流的衔接和坐标系的一致性,是除了算法本身之外,另一个需要高度重视的实战细节。