做机器人运动规划的人,应该都经历过这种时刻:A*在栅格地图上跑出一条锯齿状的路径,你以为完事了,结果把路径点发给底盘,小车在每一个拐角都要停下来原地转向;换用RRT,机械臂倒是能在关节空间里避障了,但路径抖得让人怀疑人生;直到你听说Minimum Snap,满心期待给轨迹做平滑,结果一看公式全是高阶求导和二次规划,当场两眼一黑。
在机器人运动规划领域,当别人提到A*、RRT、Minimum Snap这些术语时,他们真正在讨论的是四条不同的技术路线:基于搜索的路径规划、基于采样的路径规划、基于反应式的局部规划、基于优化的轨迹生成。这篇文章不打算罗列所有算法变体,只把这四条路线上最具代表性的算法——A*、RRT/RRT*、人工势场法/DWA、Minimum Snap——挑出来逐一拆解。它们各自的原理、核心细节、在工程里踩过的坑,我会一起讲清楚,适合正在入门移动机器人导航、无人机自主飞行或者机械臂路径规划的人参考。
1. 先把问题拆清楚:路径、轨迹和规划分层
1.1 路径和轨迹是两个概念,别混着用
我经常听到有人说“规划一条轨迹”,但打开代码一看,算法输出的只是一串位置点。严格来说,那是路径而不是轨迹。路径只有几何信息,就是你在地图上画的一条线;轨迹则在这条线上附加了时间、速度和加速度。
这个区别之所以重要,是因为底层控制器没法直接执行一条“没有时间”的路径。举个例子:假设你规划出一条从A点到B点的路径,里面包含5个路点。如果把路点直接交给轮式底盘,底盘控制器只能一个个点去追。它不知道怎么在这条路径上分配时间,于是每到转折点就得停下来重新定向,整个系统就出现走走停停、转向轨迹打滑的糟糕体感。无人机更明显,直接给离散点会让位置环跟着每个点做阶跃跟踪,机体一直在抖。
所以实际项目的链路通常是:先用全局规划算法得到一串路点,也就是路径;再做轨迹生成,把路径变成带时间轮廓的连续函数;最后才给控制器执行。Minimum Snap就是做这最后一公里转化的算法。
1.2 规划系统为什么天生要分层
不搞分层,把什么都塞进一个算法里,可行吗?在极小规模环境里也许可行,但地图一大、障碍一变、维度一高,所有试图一把梭的方案都会失败。原因有三。
第一,计算开销差异太大。全局规划要处理的地图是几十米到几平方公里的,搜索空间巨大;局部规划面对的只是传感器范围内几米到十几米的动态障碍物,实时性要求高得多。第二,决策频率不同。全局规划做到1到5Hz就行,局部规划和底层控制却要跑几十甚至几百Hz。第三,地图不确定性不同。全局地图几乎可以当作静态的,而传感器数据随时会更新,局部规划必须能应对动态变化。
这也是我搭过不少项目以后越来越认可的三层结构:
- 全局路径规划:在静态地图上搜索安全、无碰撞的路径点,频率低,代表算法是A*和RRT。
- 局部路径规划:利用实时感知信息修正路径、处理动态障碍,频率高,代表算法是人工势场法和DWA。
- 轨迹优化与生成:把离散路点转换成带时间信息的平滑轨迹,并满足动力学约束,代表算法是Minimum Snap。
分层理解了,再看后面四个算法就顺了。它们不是互相替代的关系,而是每一层各选一个代表,最终组合起来用。
2. A*:全局搜索的入门基石,也是第一个绕不开的“真算法”
2.1 A*的核心逻辑:f = g + h
A*是图搜索算法家族里最有名的一个。它做的事情可以一句话概括:在图上,从起点开始,始终优先扩展“估计总代价最小”的节点,直到找到终点。
这里的总代价就是核心公式。对任意节点n,定义:
f(n) = g(n) + h(n)g(n)是从起点到节点n已经花费的真实代价,h(n)是从节点n到终点的估计代价,称为启发式函数。在四个方向或八个方向都能走的栅格地图上,g(n)就等于起点到当前格子已经走过的最短距离,h(n)则用当前格子到终点的距离去估计。
正是h(n)的存在,让A比它之前的Dijkstra聪明得多。Dijkstra是四面开花,从起点开始均匀地向所有方向扩展;A则是有方向性的,它总在猜测哪些节点离终点更近,优先去扩展它们。换句话说,Dijkstra根本不关心目标在哪里,A*是带着导航信息去扩散的。
2.2 启发式函数怎么选,决定搜索是快还是慢
启发式函数h(n)有一个硬性要求:不要高估实际代价。一旦高估,A就可能错过最优解,退化成类似深度优先搜索,找到的路径不再全局最短。只要保证h(n)小于等于真实剩余代价,也就是满足“可采纳性”,A就能保证最优。
实际里常用的三种启发式:
- 欧几里得距离:永远小于实际路径代价,严格可采纳,但搜索时张开的角度大,扩展节点多。
- 曼哈顿距离:只适合四方向移动的地图,在八方向地图上会高估,导致结果非最优。
- 对角距离:兼顾效率和正确性,栅格八方向地图里我基本都用它。
工程实践里还有个常用技巧叫加权A*,把h(n)乘上一个大于1的权重,比如1.2到1.5。代价是牺牲一点最优性,换来搜索速度大幅提升。实际项目里我更关心“路径不要绕太多、计算要快”,而不是“绝对最短”,所以加权A*用得很香。
2.3 A*在真机上的三个典型槽点
A*好理解、好调,但直接上真机容易碰到三个问题。
一是维度灾难。A需要把空间离散成网格节点,二维栅格的节点数还能接受,三维体素地图的节点数就是立方级上升。机械臂六关节如果硬要用A,状态空间是六维,每个关节离散10个位置就是10的6次方个节点,再好的优化也扛不住。这正是后面该用RRT的地方。
二是路径贴墙。网格对角穿越墙角时,明明不碰撞,但离障碍太近,实机一启动就蹭墙。我的处理办法是地图膨胀,给真实障碍物外扩出一个小机器人半径的距离。ROS里costmap的inflation layer本质上也是干这个的,原理是一样的。
三是路径锯齿和折角。栅格搜索出来的路径只有45度整数倍的折线,走起来像在跳舞。A本身输出的是路径不是轨迹,所以别试图在A里强行平滑路线,把这个任务留给后面的Minimum Snap才是正解。
3. RRT/RRT*:高维空间里靠采样杀出一条路
3.1 最基本的RRT是怎么扩展树的
RRT的全称是Rapidly-exploring Random Tree,快速探索随机树。核心思想是:在连续的配置空间里不断撒随机点,把树从起点一步步向随机点生长,最终蔓延到终点区域。它不在网格上搜索,所以不用存储海量离散节点,天生适合高维空间。
拆解一下最朴素的RRT流程:
- 把起点作为树的根节点。
- 在配置空间里随机采样一个点q_rand。
- 在现有树上找离q_rand最近的节点q_near。
- 从q_near出发,朝q_rand方向前进固定步长delta,得到新节点q_new。
- 检查q_new和q_near之间这段是否撞障碍。没撞就加入树,撞了就丢弃。
- 反复迭代,直到q_new足够接近终点,再把终点和树连接起来。
这套逻辑代码量不大,但强在高效。随机采样天然适合搜索空间里有大片自由区域的环境,高维空间里它不靠穷举,只靠概率覆盖。很多机械臂路径规划包,比如OMPL,内置了各种RRT变体,就是这个原因。
3.2 RRT*比RRT强在哪:邻居搜索与重新连线
最朴素的RRT有明显缺点:能快速找到路径,但不保证路径最优,甚至找出来的路明显绕弯。改进版RRT*在原有的树扩展基础上增加了两个关键步骤。
第一步叫“选择父节点”。q_new生成后,不是直接接受最近的q_near作为父节点,而是先以q_new为中心画一个半径r,搜索半径内所有已有节点,分别计算从这些节点连接到q_new后,从起点到q_new的总代价,选总代价最小的那条作为父节点。
第二步叫“重新连线”。加入q_new后,再看半径r内的其他旧节点:如果把它们的父节点换成q_new,从起点到它们的总代价会不会更小?如果会,就把它们从原来的父节点上剪断,重新接回q_new。
一句话总结:RRT每加入一个新节点,不但给自己选最优的上游,还顺手把周边节点优化一遍。随着随机点数增加,路径会不断变好,理论上渐近收敛到最优路径。机械臂轨迹规划、无人机航路搜索,如果后台算力充裕,多跑几秒RRT,路径质量肉眼可见提升。
3.3 工程上最常用的改进手段和避坑经验
直接在静态地图上做全局均匀采样,效率并不理想。我常用的提升手段是:
- 目标偏向采样:以20%左右的概率直接采样终点方向,让树更倾向朝目标生长。
- 路径剪枝与shortcut:先用RRT快速找出一条解,再做剪枝,跳过可达的中间节点,让整体路径更直。
- 增量采样:把采样范围限制在当前树节点的邻域,减少空白区域无效采样。
还有一个老坑:步长delta的选取。步长太大,新节点容易跳进障碍;步长太小,树生长很慢,半天找不到解。我的经验是结合机器人几何尺寸,让单步跨越距离小于窄道宽度的一半,否则树永远穿不过门缝。
再提醒一点,基于采样的算法都有“概率完备性”性质:只要时间足够长,找到可行路径的概率趋近于1,但不能保证最优,更不能保证在限定时间内一定找到解。所以实际项目里,我会给RRT加一个最长时间上限,比如1秒内找不到就退回A给出的保守路径。工程上讲,这就是务实的兜底策略。
4. 局部规划不能少:人工势场法与DWA的实战选择
4.1 为什么有了全局路径,局部规划仍然不可省
全局规划用的地图是静态的、可能过时的。真机上哪怕过了十秒,前面可能就多了一个人、一辆叉车、一扇刚关上的门。如果完全依赖全局路径,机器人就会对着新出现的障碍物一头撞上去。
局部规划这层的作用,就是在传感器感知的局部范围内,实时检测障碍物,把全局路径变成当前可以安全执行的小段轨迹。局部规划的输入是全局路径和局部障碍物信息,输出是当前控制周期内机器人应该执行的速度或控制命令,频率通常是10到50Hz,必须非常快。
4.2 人工势场法:经典款避障,胜在简单,败在局部极小值
人工势场法的核心很直观:目标点产生吸引力,障碍物产生排斥力,机器人把两个力合成起来,沿着合力方向移动。吸引力通常是离目标越远越大,引导朝目标走;排斥力在机器人靠近障碍物时陡增,距离足够远时趋近于零。
优点是简单、快、几乎不用建复杂模型。缺点也非常著名——局部极小值问题。当障碍物围成U型,或者吸引力与排斥力在某点相互抵消时,机器人就会卡在原地,表现为来回晃动,永远走不出去。
我处理APF局部极小值的方法有两个。一是给势场加一个随机扰动分量,打破平衡点;二是在APF之前让A*或RRT先算出一条全局路径,用全局路径的方向去修正局部方向。局部规划器只负责几十厘米到几米内的反应,大方向的正确性必须由全局层兜底。
4.3 DWA:移动机器人里更常用的局部规划方案
DWA,全称Dynamic Window Approach,动态窗口法,在实际移动机器人项目里用得比APF多。ROS导航栈里的base_local_planner,默认候选就是DWA。
DWA的思路完全不同。它不做力的合成,而是直接在速度空间里采样。每一步都生成一批候选速度组合(v, w),模拟如果以这个速度走一小段时间会形成的轨迹,然后根据三条标准给每条轨迹打分:
- 方向得分:轨迹末端朝向与目标方向的夹角。
- 障碍得分:轨迹与最近障碍物的距离。
- 速度得分:当前速度大小,鼓励机器人跑得快一点。
把得分最高的轨迹对应的速度下发到底盘,下个周期继续采样评估。整个过程是在一个由当前速度决定的动态窗口里反复枚举和评估,所以筛选出来的轨迹天然满足速度上限、加速度上限这些动力学条件,比APF那种纯几何方法更落地。
怎么选?高密度动态场景、轮式机器人在人群里穿行,DWA更稳;静态环境、快速反应式避障demo,APF更简单直观。两者还经常组合:APF提供势场方向,DWA在速度空间里找不违背这个方向的可行速度。
5. Minimum Snap:把生硬的路径磨成丝滑的轨迹
5.1 snap是什么?为什么要最小化它
Minimum Snap是无人机领域被讲得最多、效果也最明显的轨迹生成算法之一。名字已经说得很清楚:用优化方法,找到一条snap最小的轨迹。snap是位置的四阶导数,也就是加速度的二阶导数,物理上可以理解为“急动度变化率”。
为什么偏偏要最小化snap?因为无人机、机械臂、移动底盘做轨迹跟踪时,对高阶导数非常敏感。如果轨迹只有位置连续而加速度突变,控制器就要突然给一个很大的力矩或推力阶跃,现实中的电机跟不住,系统就会抖动。把snap最小化,等价于让每个控制周期的加加速度变化尽量小,转速、力矩的变化就平滑,最终机体运行丝滑。
直观类比就是开车:如果你把加减速踩得毫无规律,乘客会晕车。最小化snap相当于告诉司机“加减速要匀着来”,坐车的人体验一下就完全不一样了。
5.2 从数学到代码:Minimum Snap的完整构建过程
Minimum Snap的输入是一组航点,可能是A或RRT规划出来的路径点,也可能是人工指定的关键点。输出是一条由分段多项式构成的连续轨迹,每一段多项式表示相邻两个航点之间位置随时间变化的函数:
p_k(t) = c0 + c1*t + c2*t^2 + ... + c_N*t^N目标函数定义为每段时间内snap,也就是四阶导数p^(4)(t)的平方积分,所有段加起来取最小:
J = min Σ ∫ [p^(4)_k(t)]^2 dt理解这个公式就两句话:四阶导数是抖动程度的指标,平方是为了惩罚大幅抖动,积分是为了在一整段时间区间上平均地惩罚。求解这个最小化问题,本质上是解一个二次规划(QP)问题。现有求解器很成熟,常用OSQP、quadprog这类工具都能处理。
光有目标函数不够,还得写约束。Minimum Snap的约束分两类。
第一类是等式约束,保证轨迹精确经过航点、以及相邻段之间平滑衔接。包含位置航点约束,某一段的起点或终点必须严格落在给定航点上;还有导数连续约束,相邻两段的位置、速度、加速度乃至snap在衔接点全部相等,不能“折断”。代码实现时,都会整理成系数矩阵乘以多项式系数等于已知值的等式系统。
第二类是不等式约束,负责把动力学限制加进去,比如速度不超过最大速度、加速度不超过最大加速度。实际问题里引入不等式约束会让求解难度上升,但只要约束不极端,工程上都能处理得较快。
5.3 航点、时间分配和数值稳定性:三个最容易翻车的细节
第一个坑是时间分配。Minimum Snap需要先给每段设定时间长度。常见做法是给定期望速度或加速度,再根据航点之间的距离反推每段耗时。比如两点之间直线距离2米,期望速度1m/s,那这段时间就大概给2秒。如果某段时间给得太短,速度要求就会超出物理极限,QP就算解出来,轨迹也不可用。
第二个坑是中间航点不能太少。只有起点和终点,不加任何中间点,Minimum Snap求出来的平滑轨迹会是一条大弧线,很可能绕到障碍物背后去。工程上我通常先沿全局路径多丢一些中间点,让轨迹贴合全局路径的走向。但中间点也不能加得太多太密,否则轨迹会变得僵硬,退化成折线。这个度,按机器人的尺寸和每段的期望长度去调。
第三个坑是数值病态。高次多项式常用到七阶,不同次数的系数数量级差得很大,求解器很容易因为精度问题算出垃圾解。我的办法是把时间变量做归一化,每段轨迹都映射到时间区间[0,1],再在代价函数里乘上时间缩放修正系数,避免数值范围差异过大。
Minimum Snap还有一个常见兄弟叫Minimum Jerk,只最小化加速度的导数,计算成本相对低,适合移动机器人和控制频率偏低的场合。我自己做离线参考轨迹时倾向用Minimum Snap,在线动态重规划时为了省算力,常切换成Minimum Jerk。
6. 四个算法怎么选、怎么拼:给几套可直接用的组合
6.1 选型参考:一个场景一个答案
我把常见场景和推荐算法列成一张参考表:
| 场景 | 推荐算法 | 原因 |
|---|---|---|
| 二维栅格地图、全局路径、计算资源有限 | A或加权A | 效率好、实现简单、能保证最优 |
| 机械臂高维关节空间、结构化环境 | RRT/RRT* | 采样法天然适应高维空间 |
| 动态场景、轮式移动机器人实时避障 | DWA | 速度空间采样直接输出底盘可执行速度 |
| 静态环境下的快速反应式避障演示 | 人工势场法 | 代码量最小、实时性最好 |
| 无人机或机械臂的平滑轨迹生成 | Minimum Snap/Minimum Jerk | 优化法能让轨迹在物理上更可控、平滑 |
6.2 两条典型的工程组合管线
管线一,适用于移动机器人室内巡检:
A做全局路径,DWA做局部避障,底层控制器直接执行DWA输出的速度。如果想要更完美的轨迹,可以在A输出的全局路点后接一段Minimum Snap平滑,再把平滑轨迹作为DWA的参考输入,让DWA只负责循迹和绕障。这套组合下,小车既不会走走停停,也躲得开动态障碍物。
管线二,适用于无人机航点自主飞行:
先用RRT在三维空间搜索一条全局航路,或者直接手工设置航点;再用Minimum Snap对航点做平滑和轨迹生成,输出一条包含时间、速度、加速度的参考轨迹,交给位置环跟踪。飞行中如果发现新障碍,局部评估绕行,或者调用RRT局部搜索一小段,再交给Minimum Snap重新优化。这套组合在本科竞赛和中小型无人机项目里非常常见,效果稳定。
7. 现场踩坑记录:从仿真到实机最常见的问题
7.1 常见问题速查表
我把自己和身边同事经常踩的问题整理成了一个速查表:
| 现象 | 可能原因 | 处理方法 |
|---|---|---|
| A*路径贴墙走 | 没有做地图膨胀 | 把costmap外扩机器人半径再规划 |
| A*搜索太慢 | 栅格分得太密 | 先用低分辨率搜索,再用高分辨率细化 |
| RRT树迟迟连不上目标 | 采样概率太低或步长太短 | 提高目标偏向概率,调大步长 |
| RRT*路径仍然抖 | 迭代次数不够 | 延长优化时间,或增大邻居搜索半径 |
| 人工势场法卡在U型障碍里 | 局部极小值 | 叠加随机扰动,或让全局路径优先 |
| DWA原地打转 | 评分权重没调好 | 降低障碍得分权重,提高目标方向得分 |
| Minimum Snap轨迹绕大弯 | 中间航点太少 | 沿全局路径多插中间点 |
| Minimum Snap求解失败 | 时间给太紧或数值病态 | 重设每段时间,归一化时间区间 |
7.2 一个必须坚持的调试习惯
调试运动规划,最忌讳同时改好几个参数。A*和Minimum Snap尤其如此,我吃过不少亏。有次轨迹过弯时太抖,我心里想着“时间分配和代价权重都得调”,结果两个都改了,最后解出来的轨迹确实变好了,但我根本不知道是哪个参数起了作用,换个场景立刻回归抖动。
正确的做法是:每改一个参数,跑一组固定场景测试。拿同一张地图、同一组起终点和航点,单独改一个权重或一个步长,对比轨迹曲线和计算时间的变化。次数多了,你会慢慢建立起一种手感:哪个参数管平滑度,哪个参数管响应速度,哪个参数管安全距离。这种手感,是刷文档刷不出来的。
还有一点很重要:尽量在仿真里先把规划链路跑通,再上真机。真机上出了问题,你很难分清楚是规划问题、控制问题还是传感器噪声问题。先把输入输出、频率、坐标系这些底层关系在仿真里固定住,真机上才只剩下算法调参这一层问题。
我现在做运动规划项目,最有价值的一点体会,就是把A*、RRT这类负责“找方案”的算法,和Minimum Snap这类负责“打磨方案”的算法,在架构上明确分开。找方案时允许路径绕一点、丑一点,打磨环节再追求平滑和动力学可行。这样分下来,代码结构清晰,调参思路也顺,不会为了在某一个算法里强行解决所有问题,把整个系统越搞越复杂。