做过几年无人机自主导航,也调过不少运动规划算法。要说最让人头疼的环节,不是感知也不是控制,而是“明明路径出来了,飞起来却像坐过山车”——要么急刹猛拐,要么贴障碍物太近让人手心冒汗。后来把方案切换到基于飞行走廊的轨迹优化之后,问题一下子清晰了很多。这篇文章就把这套方法的思路、数学模型、工程实现细节和踩坑经验一次说清楚,给正在折腾运动规划算法的朋友一份能直接参考的笔记。
飞行走廊(Flight Corridor)本质上是把障碍物之间的自由空间抽象成一连串凸多面体,让轨迹优化只有一个明确任务:在这些凸边界内生成一条平滑、动态可行、时间合理的轨迹。相比直接在栅格地图上做轨迹规划,飞行走廊剥离了大量离散化和分辨率带来的干扰,把“避障”和“平滑”两个问题解耦,是工程上非常实用的中间表达。
这篇文章适合三类人看:一是刚接触无人机轨迹优化、想知道怎么把“路径”变成“轨迹”的初学者;二是已经在跑A*、RRT*等前端规划,但后端轨迹总是抖或者撞障碍物的开发者;三是做机器人运动规划的工程师,想了解如何用凸优化工具约束轨迹空间。我会从走廊生成讲到轨迹求解,再到实际调参和故障排查,尽量把每一个坑都标出来。
1. 飞行走廊是怎么来的,为什么它能让轨迹优化变简单
1.1 先理清路径和轨迹的区别
很多人在这一步就拧巴了。路径是一串离散的几何点,比如从起点到终点经过的10个坐标点;轨迹则是带时间戳的连续函数,要回答“t=1.2秒的时候,飞机在哪个位置,速度是多少,加速度是多少”。
运动规划算法里常用两段式:前端用图搜索或采样方法找一条安全路径,后端再做轨迹优化。如果没有飞行走廊,后端通常会把路径点当作硬约束,或者用势场把轨迹推向路径。但路径点一旦落在障碍物附近,或者路径本身有锯齿,轨迹优化就会非常痛苦——为了穿过一个狭窄的路径点,轨迹可能需要在很小的空间里剧烈转弯,产生的加速度和jerk会严重超标,飞控根本跟不住。
飞行走廊解决的就是这个“中间表达”问题。它不要求轨迹精确经过路径点,而是给轨迹划出一片连续的、凸的、安全的“通道”。轨迹优化只负责在这个通道里找最平滑、最符合动力学约束的曲线。路径点的精细位置不再重要,重要的是这条轨迹不能跑出通道。
1.2 凸多面体为什么是天然的“安全信封”
飞行走廊最常见的表示是凸多面体(convex polytope),每个面用一组线性不等式表示:
a_i^T x <= b_i, i = 1, 2, ..., m
比如一个立方体走廊,实际上就是上下左右前后六个半空间的交集。轨迹上的任意一点都满足所有这些不等式,就能保证这个点一定在走廊内部,也就一定安全。
凸多面体有三个好处,工程上特别关键。
第一,凸性保证了优化问题的良态。一个点如果始终满足所有线性不等式,那么这段轨迹上的任意线性组合也在走廊内。这样,轨迹优化可以放心地用凸优化求解器,比如OSQP、MOSEK、CVXPY后端,不需要处理非凸的避障约束,也不用反复做碰撞检测。
第二,距离度量直观。点到凸多面体边界的距离,可以通过求解一个线性规划或者用半空间的几何关系快速估算。做安全裕度检查、走廊收缩、轨迹膨胀检测时,这个能力非常有用。
第三,数据量小。几十个面就能表达很大一片安全区域。相比在高分辨率栅格地图上逐点检查碰撞,走廊的表达效率高出好几个数量级。
1.3 为什么不是直接B样条或者人工势场
有人会问:既然B样条也可以平滑,势场也能避障,为什么非要飞行走廊?
B样条的问题在于约束是局部的,调控制点才能影响轨迹,但控制点多了之后,碰撞约束还是需要逐点检查,而且怎么把“不能进障碍物”这个二值约束连续化很麻烦。势场最大的坑是局部极小值,无人机可能在两个障碍物之间来回震荡,或者停在“看起来安全”但实际死胡同的地方。
飞行走廊最大的价值是“把几何问题代数化”。障碍物变成线性不等式,轨迹优化变成一个带约束的二次规划(QP)或更一般的凸优化问题。这样做的结果就是,理论上一定能找到局部最优解,工程上可以稳定复现,不像启发式方法那样每次跑结果都不一样。
2. 从地图到走廊:前端几何生成的完整流程
2.1 先建图,再膨胀,把自由空间提取出来
飞行走廊的前端一般是占据栅格地图或者八叉树地图。对于无人机,我常用的是OctoMap,它能够增量更新,内存占用也远小于密集栅格。拿到地图之后,第一步不是直接生成走廊,而是对障碍物做膨胀处理,把无人机的半径、动力学限制和控制器跟踪误差都留出余量。
膨胀系数怎么定,我一般按这个经验公式:
膨胀半径 = 无人机半宽 + 安全裕量 + 控制误差补偿 + 感知误差补偿
比如一架轴距300mm的小型无人机,半宽约0.15m,安全裕量0.2m,控制误差0.1m,感知误差0.1m,那么膨胀半径就是0.55m。这个参数直接决定了走廊的宽度。膨胀太小,轨迹优化出来的结果在真实飞行中很容易“擦墙”;膨胀太大,狭窄空间可能直接被堵死,规划直接失败。
膨胀之后,地图里的自由空间就变成了一个个连通区域。接下来可以从种子点开始做区域生长,或者用形态学方法提取自由空间骨架。比较经典的做法是先跑一个A*或JPS(Jump Point Search)找到一条参考路径,然后沿着路径生成一串立方体或凸多面体走廊。
2.2 用PCA做方向感知的走廊生长
生成立方体走廊最朴素的做法是:沿着参考路径每隔一段距离放一个固定大小的正方体,然后剔除和障碍物相交的方块。但这样生成的小方块之间可能错位,轨迹优化时容易在方块交界处产生不必要的拐弯。
更优雅的做法是PCA(主成分分析)走廊生长。原理是:先取一个包含参考路径局部片段的点集,对这些点做主成分分析,得到主方向,然后沿着主方向生成一个“椭球形”的候选走廊,再在最大内部椭球的基础上,把椭球切成长方体。这个方法生成的长方体会自然贴合路径走向,走廊数量少,且边界方向合理。
不过PCA法也有个前提:点集要相对规整,如果参考路径在狭窄空间里绕来绕去,PCA的主方向可能失真。工程上我是这样处理的:先把参考路径做一次低通滤波或B样条拟合,让方向变化平滑一些,再做PCA。
2.3 从“最大内接椭球”到凸多面体
一个高质量的走廊,最好在满足安全性的前提下尽量宽。怎么找“尽量宽”的凸区域?经典算法是“内接最大椭球迭代扩展”。
大致步骤是:
- 在一个种子点附近,用线性规划找一个最大内接椭球(用椭球表面积或体积做目标)。
- 椭球确定后,计算椭球各个方向的支撑平面,把这些支撑平面作为候选面。
- 用候选面切割冗余空间,逐步把椭球扩展成多面体。
- 重复迭代,直到多面体碰到障碍物或达到体积上限。
这个过程的数学实现有些繁琐,但有现成的库可以借用,比如Debra Devaurs等人提出的“Inscribed Ellipsoid”方法,在很多开源运动规划库中都能找到对应实现。关键是理解思路:用椭球做近似,用线性不等式做安全包络。
实际在快速原型阶段,我不会一上来就上最大内接椭球。先用分段立方体走通全流程,后面再替换成多面体细化,这样调试难度低很多。
2.4 走廊生成的质量标准
走廊好不好,直接决定了轨迹优化的上限。我总结了一条检查清单:
- 连贯性:相邻走廊必须有足够大的重叠区域,不能出现仅有一个点相接的“咽喉”,否则轨迹很难自然通过。
- 覆盖性:整套走廊要覆盖参考路径的绝大部分,不要让轨迹优化有“无路可走”的感觉。
- 边界方向合理性:长走廊的长轴方向应顺着路径走向,避免出现斜切路径的边界,否则轨迹为了躲边界会产生额外拐弯。
- 数量适中:走廊段数太多,分段多项式的段数也会多,计算量增加,且段间连续性约束变多;段数太少,每段走廊体积变小,轨迹活动空间不足。一般10米长的路径,走廊段数在5~10段比较合适。
走廊生成在工程里其实占了规划的很大一部分工作量。很多轨迹优化做不好,问题不在后端,而在前端把走廊搞得太碎或者太窄。
3. 轨迹优化:在走廊里求一条平滑轨迹
3.1 目标函数:为什么大家都爱最小化snap
拿到走廊,下一步就是求轨迹。目前主流做法是把轨迹表示成分段多项式,比如每段是一个5阶或7阶多项式:
p(t) = c0 + c1 t + c2 t^2 + c3 t^3 + c4 t^4 + c5 t^5 + c6 t^6 + c7 t^7
然后最小化加加速度(jerk,三阶导)或 snap(四阶导)。对无人机来说,最小化jerk会让加速度变化平缓,适合常规飞行;最小化snap则更注重螺旋桨推力变化率,适合高速或者视觉避障场景,因为snap小意味着机载相机的角速度和加速度变化小,图像不会因为剧烈抖动而模糊。
目标函数一般写成:
J = ∫ (p^(3)(t))^2 dt (最小化jerk) 或 J = ∫ (p^(4)(t))^2 dt (最小化snap)
加上权重之后,可以用余弦变换或直接构造二次型:
J = c^T Q c
其中c是多项式系数向量,Q是根据时间段积分预计算出来的Hessian矩阵。
3.2 分段多项式的连续性和时间分配
分段多项式的优点是简单,但麻烦在于段与段之间的连续性。一般要求相邻多项式在连接点处位置、速度、加速度连续,即:
p_i(T_i) = p_{i+1}(0) p'i(T_i) = p'{i+1}(0) p''i(T_i) = p''{i+1}(0)
这些连续性约束都是线性等式约束,可以统一写成A_eq c = b_eq,扔进二次规划求解器即可。
时间分配也是个关键。每段路径分配多少时间,直接影响速度和加速度大小。最简单的做法是按每段路径长度等比例分配,然后乘一个平均速度系数。但这样容易出现“长段时间很多,短段时间很少”的不均匀现象,导致轨迹速度忽快忽慢。
更合理的做法是从一个初始时间分配开始,先求解轨迹,再根据轨迹峰值速度/加速度调整时间,迭代几次。我曾经用过一个很实用的方法:第一次规划用偏大的时间,保证平滑;然后用轨迹的实际速度分布反推最短可行时间,再调低,循环三次左右就能得到比较激进而又可行的轨迹。
3.3 走廊约束如何变成线性不等式
走廊约束是这次优化的核心。如果轨迹在t时刻的位置为p(t),而p(t)是多项式,那么约束p(t)在走廊内本来是非凸的——因为p(t)是非线性的,t是变量。
工程上常用的招是“离散时间检查”:把每一段轨迹按时间均匀采样十几个点,在每个采样点处要求:
a_i^T p(t_k) <= b_i
其中a_i、b_i是当前走廊边界对应的半空间参数。由于p(t_k)关于多项式系数是线性的,所以这些约束仍然是线性不等式。采样点越密,约束越严格,但计算量也越大。我一般每一段取8~12个采样点,既能保证不穿墙,又不至于让QP规模爆炸。
还有一个细节:同一时刻的采样点要对应到正确的走廊段。因为走廊是按空间分段生成的,轨迹段和走廊段不一定一一对应,需要根据参考路径的索引做映射。最好是在生成走廊时就约定好“第几段路径对应第几段走廊”,这样轨迹分段和走廊分段天然对齐。
3.4 一个最小QP的完整数学形式
把上面这些拼在一起,一个最简单的基于飞行走廊的轨迹优化问题长这样:
minimize J = c^T Q c + w1 * 软约束惩罚项 subject to A_eq c = b_eq (起点终点和连续性约束) A_ineq c <= b_ineq (走廊采样点约束) |v_max|、|a_max|、|j_max| 等可选约束
这里的软约束惩罚项可以用来处理“尽量贴近路径”的期望,或者是对时间、能量做加权。硬约束是必须满足的,比如起点终点的位置、速度、加速度;走廊约束可以做成硬约束,也可以做成软约束。
实操中我倾向于把走廊约束做硬约束,把“贴近参考路径”做软约束。因为走廊已经保证了安全,路径贴近度只是让轨迹看起来自然,没必要用硬约束把轨迹绑死在参考路径上。
3.5 求解器选择与代码框架
这个问题最终落到二次规划求解上。对于几百维的小规模QP,OSQP非常快,而且支持热启动,适合在规划频率10Hz~50Hz下持续求解。MOSEK也很稳,但商业授权不是所有人都方便用。如果后面要加整数变量,比如时间最优分配与拓扑选择联合优化,那就得上Gurobi或者SCIP这类MILP求解器,不过这是另一个话题了。
代码框架上,我习惯把流程拆成三个模块:
- 地图与走廊模块:负责膨胀、路径搜索、走廊生成,输出走廊参数。
- 轨迹优化模块:输入走廊和端点约束,构造QP,调用OSQP求解。
- 检查与输出模块:对求解结果做插值、碰撞复查、动力学可行性审查,输出给控制器。
这个框架的好处是模块之间只通过标准数据接口通信,换地图表示、换求解器、换轨迹阶数都不影响其他部分。
4. 从仿真到真机:绕不开的工程细节和坑
4.1 数值稳定性:多项式基函数的选择
分段多项式最容易被忽视的问题就是数值稳定性。直接用幂基(c0 + c1 t + ... + c7 t^7),当时间t比较大(比如一段轨迹飞5秒),t^7会非常巨大,和系数相乘之后,数值范围可能跨越十几个数量级。QP求解器内部做矩阵分解时,很容易出现数值病态,导致解出来的系数看起来合理但轨迹点验算时严重超差。
我踩过最大的坑就是这里:仿真能飞,真机一飞就偏,检查半天发现是多项式数值条件数太差。
解决办法有两个。一是做时间缩放,把每段时间归一化到[0,1],这样t的幂次不会爆炸,但要注意缩放后的连续性约束需要修正。二是改用B样条或者最小二乘拟合的系数空间,比如用Bernstein基,数值稳定性会好很多。轻量级方案里,我推荐做时间归一化再构造目标函数,改动最小,见效最快。
4.2 走廊边缘的“贴地飞行”问题
轨迹优化很容易出现一种现象:轨迹在走廊里大面积贴着边界走,因为贴着边界的路径可能看起来更“平滑”(弯曲更少)或者目标函数更小。但真实飞行中,边界附近气压测量和视觉特征往往不稳定,稍微有一点干扰就可能撞进障碍物。
对策是生成走廊时再做一次“内缩”。比如走廊每个面的约束从:
a_i^T x <= b_i
缩成:
a_i^T x <= b_i - margin_i
margin_i 一般取0.1~0.2m,或者按走廊窄边的比例动态设置。内缩之后,轨迹即使在边界附近,也还有一层物理安全兜底。
我见过有人贪图效率取消这层内缩,结果真机在走廊边上触发了一次紧急避障。那次的教训是:安全裕量永远不要省在最后一层。
4.3 时间分配对轨迹可行性的影响
刚刚说过时间分配需要迭代。但迭代时间过长会影响实时性,所以还要设置上限,比如最多迭代三次,超出上限就采用当前最优可行解。
另外,时间分配和走廊约束有耦合。如果某段时间过于紧张,轨迹为了满足走廊约束,可能不得不剧烈加速,导致峰值速度极高。这种情况下,即使QP有解,物理上也可能飞不出来。我的做法是:求解后检查峰值速度和峰值加速度是否在飞行器极限的80%以内,如果超了,就把对应段的时间乘以1.3,重新求解。
检查公式很简单:
v_peak = max |p'(t)| <= 0.8 * v_limit a_peak = max |p''(t)| <= 0.8 * a_limit
注意这里的0.8不是死规矩。如果是竞速无人机,可以放到0.95;如果云台相机需要稳定画面,可能压到0.5。
4.4 失败后的Fallback策略
没有哪个系统能保证100%规划成功。走廊生成失败、QP无解、轨迹碰撞检查不过,这些都要有应对方案。
我常用的fallback策略分成三个层次:
- 第一层:重新生成走廊。可能是参考路径穿过了过于狭窄的区域,调整膨胀半径或者改变路径搜索的启发式权重,再试一次。
- 第二层:放宽走廊约束。把软约束权重加大,或者把硬约束走廊适当“放松”一些(但要重新检查碰撞)。
- 第三层:切换到安全悬停或保守路径。让无人机沿原路径减速停住,再重新做全局规划。
现实中第三层其实用到不多,但必须存在,否则系统一遇到复杂环境就失去保护。
5. 常见问题速查:你大概率会遇到这几个
5.1 优化结果轨迹直接穿墙
先检查走廊约束是否真的加进QP了。很多人写完目标函数,忘了把走廊不等式拼接进约束矩阵,结果轨迹只满足起点终点连续性,自然随便穿。其次检查走廊和轨迹段的对应关系:轨迹在t时刻应该满足的是当前所在空间位置的走廊约束,而不是索引固定的走廊约束。如果轨迹段和走廊段错位,也会穿墙。
5.2 QP无解或者求解很慢
无解往往是因为约束太紧。尝试增加每段时间,扩大走廊内缩量;或者把部分硬约束改成软约束,比如把“速度必须小于某值”改成“速度超过阈值的部分加惩罚”。求解慢则检查约束矩阵是否稀疏,如果用稠密矩阵存稀疏约束,OSQP也救不了你。另外,热启动估计,在连续规划周期之间复用上一次的解,通常能将每次求解时间降低一半以上。
5.3 走廊太碎或者前后不连贯
多半是膨胀半径太大导致窄通道被截断,或者参考路径在窄通道里弯曲太多,导致PCA生成长走廊失败。可以试试降低膨胀半径里的“控制误差补偿”项,或者改用沿路径法线方向扩展的非轴对齐长方体。窄通道里做非轴对齐走廊,效果立竿见影。
5.4 轨迹看起来平滑,但飞起来抖
地面站显示的轨迹曲线平滑,不代表实际的微分信号平滑。检查一下是不是jerk或者snap目标函数的权重太小,或者说你只用了位置约束,但速度和加速度连续性没加全。还有一个容易忽略的点:轨迹跟踪控制器有延迟,如果轨迹加速度过大,控制器滞后会导致实际飞行轨迹超调。这时候适当降低目标函数对“快”的权重,或者对轨迹做低通滤波都能缓解。
5.5 参数速查表
| 参数项 | 建议初值 | 调节方向 |
|---|---|---|
| 膨胀半径 | 0.5m(小型无人机) | 太小容易碰撞,太大窄通道直接失败 |
| 走廊内缩margin | 0.1~0.2m | 视觉传感器噪声大时增大 |
| 每段采样点数 | 8~12 | 太少穿墙,太多计算量大 |
| 时间分配迭代次数 | 3 | 实在紧迫可以1~2次 |
| 速度安全系数 | 0.8 | 竞速可放宽,云台稳定需收紧 |
| 分段多项式阶数 | 7(或5) | 阶数高更平滑,但数值风险更大 |
6. 我个人的一些体会
做了这么久运动规划,我觉得飞行走廊这套体系最大的价值,不是某个算法多炫酷,而是它把无人机的运动规划拆成了一条清晰的流水线:建图、搜索、走廊生成、轨迹优化、检查输出。每个环节能单独调试,也能单独替换。你不需要一次性面对一个巨大的非线性优化问题,而是用多个简单问题的组合去逼近一个复杂问题。
真机飞行和仿真最大的不同,是你会发现地图、感知、控制每一环都有误差。飞行走廊的安全裕量,本质上是为这些误差买的保险。我习惯在仿真里把裕量压到很小来测试算法极限,但一旦上真机,我会立刻把裕量恢复到保守值。先安全再激进,这个顺序不要搞反。
最后分享一个小的调试技巧:在可视化工具里,把走廊的半透明体、参考路径、优化轨迹同时显示出来,运行一遍完整的规划循环。如果发现轨迹明显偏向走廊一侧,去看看那一侧对应的感知数据,往往能找到传感器盲区或者地图更新延迟的问题。这比盯着数字调参直观得多。
飞行走廊和轨迹优化是运动规划算法里一个很值得深耕的方向,希望这篇笔记能帮你少走几步弯路。