资讯动态

四旋翼在线路径规划:贝塞尔曲线与飞行走廊的工程实践

发布时间:2026/10/22 13:33:49 来源:尧图企业网站定制
简介面向自主四旋翼飞行器与无人机自主导航场景的在线规划框架基于C实现可在先前未知环境中生成安全、动态可行的飞行轨迹。框架前后端分离前端提供速度场上的FM与网格地图A两种路径查找方案后端利用贝塞尔曲线将分段轨迹严格约束在飞行走廊和动力学限制内并配置ROS消息、launch启动与benchmark测试基线。压缩包共133个文件涵盖50个hpp头文件、24个cpp实现、13个h头文件以及so动态库、cfg配置、png可视化结果、md说明文档等整体大小约9.71MB。下载内容包括完整项目源码、README与注释说明可辅助理解前端路径搜索与后端轨迹优化的衔接逻辑已有146人学习下载适合相关专业学生用于课程设计、毕业设计或无人机规划方向的进阶实践。1. 在线无人机规划框架让四旋翼在未知地图里边探边飞关于自主四旋翼飞行器的路径规划多数开源实现是「先建图、再离线规划」一旦环境在飞行中变化就得整体重来。这份 C 实现的在线无人机规划框架走的是另一条路前端用 FM* 或 A* 在速度场/网格地图上找粗路径把路径膨胀成由立方体组成的飞行走廊后端利用贝塞尔曲线的凸包与导数性质做分段优化把轨迹完全限制在走廊和动力学边界内从而在先前未知环境中边探边飞每轮迭代都产出安全且动态可行的轨迹。这正是无人机避障类项目里最缺的一块。适合做无人机课设、毕设的在校生以及想直接抄一套完整规划管线来二次开发的从业者。2. 前端路径查找FM* 与 A* 怎么选走廊立方体怎么搭2.1 两段式架构的合理性未知环境里别在一张图上解最优先说清楚为什么要拆成「前端路径查找 后端轨迹优化」两段。直接在整张栅格地图上做带动力学约束的轨迹优化非线性约束的规模大到你根本跑不出在线频率而如果只做不带任何约束的 A*出来的路径满是折角四旋翼根本飞不出来。所以常见做法是分层处理前端只解决拓扑问题在低分辨率地图上找一条不与障碍碰撞的粗路径哪怕它有锯齿、有冗余点都没关系后端只解决动力学问题在粗路径划定的小范围走廊内做平滑优化。这个拆分带来的直接收益是求解规模可控。前端搜索在稀疏图上进行代价低后端每轮只需要处理走廊内的少量贝塞尔段在一个普通桌面 CPU 上单轮迭代能跑到几十赫兹这才配叫「在线」。如果哪一天你觉得这个框架跑得慢先别急着怀疑求解器回头看看前端地图分辨率是不是设太高了。2.2 两种前端替代方案纯网格 A* 与速度场 FM*包里的前端提供了两个不能同时开启的替代方案选型逻辑在配置里用一个开关切。纯网格地图上的 A* 好理解把地图离散成栅格用启发式搜索找一条代价最小的路径。优点是实现简单、可预期性强只要启发式设计得当通常用欧氏距离保证可采纳性找到的路径在拓扑上一定正确缺点是路径必然带离散网格的方向性八邻域下会出现 45 度折线十六邻域稍好但搜索空间翻倍这些折线最终要靠后端来抹平。速度场上的 FM*Fast Marching 在速度场上的变体是更顺滑的选择。它把地图理解成连续速度场从起点做波前传播波前到达终点的路径天然是平滑曲线。相比 A*FM* 出的路径更接近直觉上的「绕行方向」走廊构建时更容易得到规整的膨胀结果代价是数值求解比离散搜索更费时间而且速度场的构造质量直接决定路径质量速度场没建好FM* 出来的路径反而比 A* 更离谱。实际跑这个包的经验是低分辨率、障碍稀疏的测试场景用 A* 足够场景大、走廊窄、要求飞行姿态好看的切到 FM*。配置里就是一行开关的事两套都跑一遍对比单帧耗时和路径总长再做决定这也是这个包设计成双前端替代方案的意义。2.3 飞行走廊构建从路径点长出安全立方体前端输出的只是无碰撞路径点。要让后端去做约束优化必须先给轨迹圈定一个「活动范围」——这就是飞行走廊。这个包里走廊由基于路径的立方体组成实现集中在 collision_map.cpp 和 tagged_object_collision_map.cpp 这两份文件里。走廊构建的核心逻辑可以简化成下面的膨胀过程从每个路径点出发初始化一个小立方体反复检查当前尺寸是否与碰撞地图冲突不冲突就向外扩一轮冲突就回退到上一轮安全尺寸。立方体参数初始半边长、膨胀步长、最大半径直接决定走廊的松紧。// 走廊立方体膨胀从路径点出发逐步扩张直到碰触障碍 // 参数说明 // seed_radius 初始半边长建议取 0.3m 以上 // 小于机体半径时走廊会过度约束后端 bool growCorridor(const CollisionMap map, const Eigen::Vector3d center, double seed_radius, double step, double max_radius, Eigen::Vector3d extent) { extent.setConstant(seed_radius); while (extent.x() max_radius) { // 用 AABB 包围盒近似立方体检查 8 个角点是否全部自由 if (!map.isFree(computeBoundingBox(center, extent))) { extent.array() - step; // 回退一个步长保证安全 return true; } extent.array() step; } return true; }这段代码的逻辑是逐层试探每次检查的其实是立方体八个角点与 tagged_object_collision_map 的碰撞关系角点自由就继续扩不自由就整体回退。把回退写成extent.array() - step而不是直接丢弃该轮结果是保证走廊不与障碍「贴脸」的关键否则后端优化出来的轨迹哪怕只偏一个微小量都会撞上障碍物。两个容易忽略的参数seed_radius 太小会让后端找不到可行解step 太大则走廊会膨胀过头轨迹看起来贴着障碍飞视觉上很吓人。我一般会把 step 设为 seed_radius 的 1/4先跑一版看轨迹与障碍的间隙再微调。走廊这一段的参数组合多少带点玄学但记住一条原则就够了这些参数是给求解器留余量不是给障碍物留面子。3. 后端轨迹优化贝塞尔曲线如何把轨迹锁进走廊与动力学边界3.1 凸包性质安全保证的数学根源后端选择的表达是分段贝塞尔曲线几乎所有安全性结论都来自贝塞尔曲线的两条基础性质。第一凸包性质贝塞尔曲线整段落在其控制点构成的凸包内。这意味着只要把所有控制点约束在走廊立方体内部整条曲线就不可能穿出走廊——这是「安全」的数学保证不需要事后逐点检查。第二导数性质贝塞尔曲线的导数仍然是贝塞尔曲线且导数控制点可以由原始控制点的差分直接算出。速度、加速度、加加速度的约束全部可以写成对控制点差分的线性约束这让后端优化能保持在一个可高效求解的凸优化框架内。这也是为什么走廊必须用凸的几何体立方体来构建的原因。如果你的走廊是任意凹多边形贝塞尔控制点落在其内部并不能保证曲线也在内部凸包性质就失效了。该框架用立方体正是看中它的凸性配得上贝塞尔曲线的凸包约束。3.2 约束建模位置、速度、加速度怎么写进求解器后端要做的事情可以归纳成一句话给定 M 段贝塞尔曲线和 M 个走廊立方体找一组控制点使每一段都被对应走廊包容同时各段连接处满足连续性、整条轨迹的速度/加速度/加加速度不超限并且总飞行时间尽量短。常见的约束写法如下// 每段贝塞尔曲线的约束装配骨架伪代码风格贴近包内实现 // 该框架中轨迹分 M 段每段阶次 n 在配置里统一指定 struct SegmentConstraints { // 位置约束控制点 p_{k,i} 必须落在立方体 C_k 内 // 由凸包性质保证整段曲线在走廊内 std::vectorEigen::Vector3d lower_bounds, upper_bounds; // 速度与加速度约束导数控制点由差分得到 // 速度控制点 v_{k,i} n * (p_{k,i1} - p_{k,i}) / t_k // 要求 |v_{k,i}| v_max加速度同理由二阶差分得到 double t_k; // 该段飞行时间由时间分配模块给出 }; // 相邻段连接处要求位置、速度、加速度连续 // 即 p_{k,n} p_{k1,0} // n*(p_{k,n}-p_{k,n-1})/t_k n*(p_{k1,1}-p_{k1,0})/t_{k1}写约束时有两个容易写错的细节。一个是速度控制点的分母贝塞尔曲线对参数 u∈[0,1] 求导后还要乘上 1/t_k 才能得到对真实时间的导数漏乘 t_k 会让约束松了 n 倍优化出来的轨迹实际速度直接超标。另一个是连续性的阶数如果只约束位置连续段间会出现速度跳变四旋翼的推力方向会瞬间翻转实践上至少要约束到加速度连续加加速度连续则看飞行器硬件是否支持一般四旋翼做到 jerk 连续已经足够平滑这也是「动态可行」这四个字的最低门槛。3.3 时间分配与求解器配置Mosek 里结果好坏看这几个参数约束装配好之后优化目标通常是总时间加权最小化和轨迹平滑项的折中。时间 t_k 在这里不是一个常数它要么作为优化变量要么先由梯形速度剖面粗分配。这块包内的实现里两者都有痕迹实际体验是初值给得好Mosek 收敛快很多。包里的求解器是 Mosek 8.1对应用到的 libmosek64.so.8.1求解器参数在 cfg 文件里以数值形式给出常见涉及的有下面几项参数项作用建议求解容差相对间隙控制优化精度与速度的折中测试用宽松值benchmark 收紧最大迭代时间防止离线整段卡死在线场景建议限制在几十 ms可行域缩放把米制单位转到求解器内部数值范围地图过大时留意数值稳定性如果求解器报 primal infeasible九成不是求解器的问题而是时间分配给得太短导致速度/加速度约束与走廊约束无法同时满足。先拉长总时间让问题可行再逐步缩短时间逼近性能边界这是调这个包最常用的一个工作流下面第 4 章还会展开讲。4. 构建、运行与排查依赖版本、配置文件与五条踩坑记录4.1 文件清单先认清包里每一块的实际角色下载解压后不要急着编译先对照文件清单确认手里有什么。这份资源的核心文件与角色大致如下文件角色判断b_traj_node.cpp主程序入口串联前端搜索、走廊构建、后端优化collision_map.cpp碰撞地图的查询与更新封装tagged_object_collision_map.cpp带标签物体的碰撞检测区分静态与动态障碍libmosek64.so.8.1Mosek 8.1 求解器动态库后端优化的核心依赖libcilkrts.so.5Cilk 并行运行时老版本 GCC 自带test.cfg / benchmark.cfg测试模式与压测模式的配置入口2_150.cfg / 2_300.cfg / 2_500.cfg不同场景参数按任务规模区分4.2 依赖与编译Mosek 8.1 和 Cilk 运行时是两个大坑点先说编译环境。从 libcilkrts.so.5 可以判断这个包面向 GCC 7 左右的工具链因为 Cilk Plus 运行时在 GCC 8 之后被移除了。如果你的系统是较新的发行版大概率没有这个库。两条路一是装老版本 GCC 7 并指定为编译器二是从老工具链里单独拷贝 libcilkrts.so.5 放到可执行文件同目录再用 LD_LIBRARY_PATH 指向它。其次是 Mosek。libmosek64.so.8.1 对应 Mosek 8.1而不是常见的 9.x / 10.x。如果你机器上装的是新版 Mosek动态库文件名对不上运行时会直接报找不到共享库。一个稳妥的编译与运行流程# 1. 确认 Mosek 8.1 的动态库路径 export LD_LIBRARY_PATH/opt/mosek/8.1/tools/platform/linux64x86/bin:$LD_LIBRARY_PATH # 2. 编译Cilk 运行时依赖 GCC 7 系列 g -stdc11 -O2 b_traj_node.cpp collision_map.cpp \ tagged_object_collision_map.cpp \ -I/opt/mosek/8.1/tools/platform/linux64x86/h \ -o b_traj_node -lmosek64 -lcilkrts # 3. 运行前用 ldd 检查动态库是否全部命中 ldd ./b_traj_node | grep -E mosek|cilk # 4. 启动测试场景 ./b_traj_node --config test.cfgldd 这一步很重要它能在一秒钟内把动态库缺没缺、路径对不对暴露出来比运行时报错再回头排查快得多。4.3 配置文件差异test、benchmark 与 2_xxx 场景分别怎么用这套配置文件的命名有明确分工。test.cfg 是功能自检用地图小、障碍少用来验证管线能跑通benchmark.cfg 是压测用地图大、场景复杂用来测试算法在极限情况下的鲁棒性2_150.cfg、2_300.cfg、2_500.cfg 从命名习惯看2 很可能是场景编号150/300/500 对应不同尺度的任务距离或地图尺寸。具体差异打开文件对比一下自然清楚里面主要是地图尺寸、走廊最大边长、速度上限、求解器容差这几类取值。如果只是想验证代码能跑先用 test.cfg想复现性能数据用 benchmark.cfg 并把运行时间记录成表格对比不同规模场景下的 FPS 变化。提示改 cfg 前先备份一份原始文件benchmark 压测最容易把参数调到不可行区间这份备份就是你最后的后悔药。4.4 五条高频踩坑记录现象、原因、解决坑 1启动报 libmosek64.so.8.1 找不到现象error while loading shared libraries: libmosek64.so.8.1: cannot open shared object file。 原因装的是 Mosek 9/10或 LD_LIBRARY_PATH 指向了错误目录。 解决回到 Mosek 8.1 的安装路径把tools/platform/linux64x86/bin配进 LD_LIBRARY_PATH实在找不到 8.1可以用符号链接把新版库伪装成 8.1 的文件名试一下但成功率取决于 API 兼容性不推荐作为长期方案。坑 2libcilkrts.so.5 缺失现象编译通过运行时报同一个 cannot open shared object file。 原因GCC 8 之后移除了 Cilk Plus 运行时。 解决用 GCC 7 重编整个项目或从老系统拷贝 libcilkrts.so.5 到工程目录再export LD_LIBRARY_PATH.。两个方法都验证过后者更省事。坑 3轨迹在走廊边缘频繁穿出现象可视化里轨迹贴着立方体边界走偶尔越界。 原因走廊膨胀时 step 太大回退后的边界离障碍太近后端优化一有数值误差就越界。 解决把 step 调小到 seed_radius 的 1/41/8并在后端约束里给走廊留 0.1~0.2m 的内缩量给数值误差留出缓冲。坑 4A找的路径钻进窄缝*现象路径穿过地图上看似可过、实际宽度小于机体的缝隙后端怎么优化都穿模。 原因前端分辨率太低窄缝在离散地图上被错误标记为可通过。 解决提高前端地图分辨率或在 tagged_object_collision_map 里把障碍物膨胀半径直接调大到机体半宽以上让窄缝在拓扑层面就被排除。坑 5求解器报 primal infeasible现象Mosek 返回不可行整段轨迹优化失败。 原因某段走廊太窄同时速度上限又设得很低两约束互相打架。 解决先拉长该段飞行时间让速度约束松下来若还不行检查时间分配模块给的总时间是不是短于物理下限。这是我调这个包遇到最多的一类问题九成出在时间分配上而不是约束代码上。5. 验证与进阶调参轨迹不仅要安全还要敢飞得快5.1 离线采样重放把优化结果送回碰撞地图过一遍后端优化的「安全」是数学保证但真实环境下还有数值误差、地图更新滞后、机体惯性这几层不确定性。我拿到这份资源后做的第一件事不是直接上设备而是写了一个离线采样验证脚本把优化出来的分段贝塞尔曲线按固定时间步长密集采样逐个点送到碰撞地图里重新检查。# 对优化结果做离线重放验证密集采样并核对碰撞 def verify_trajectory(control_points_list, segment_times, collision_map, dt0.02, margin0.15): for seg_idx, (P, T) in enumerate(zip(control_points_list, segment_times)): n len(P) - 1 # 贝塞尔阶数 控制点数 - 1 t 0.0 while t T 1e-9: u t / T # 归一化参数 pos eval_bezier(P, n, u) # 递推计算曲线上的点 if not collision_map.is_free_with_margin(pos, margin): return False, seg_idx, t, pos t dt return True, None, None, None这里的 margin 是比后端约束更严格的安全余量。常见做法是先用大 margin 跑一遍再把 margin 从 0.15 逐步压到 0.05看轨迹在哪个余量下开始越界这个临界值就是整条管线的真实安全边界比求解器内部报告的可信得多。5.2 时间重分配把安全余量换算成速度提升验证通过后剩下要压的就是速度。框架里总飞行时间直接决定平均速度所以压速度的本质是压时间。我一般会从 benchmark.cfg 给出的初始总时间开始每次整体缩短 10%跑完离线验证直到轨迹开始触碰 margin 下限再回退一档。这里特别提醒每次改完时间一定要重新过一遍 5.1 的验证因为时间变化会等比放大速度控制点之前安全的位置约束在速度约束被顶到极限后会出现新的不可行区间。从那以后我每次拿到新的规划包都会强制走一遍「配置文件参数扫描 离线重放验证」这个组合拳宁可多跑两轮脚本也不愿意带着未验证的轨迹参数上设备。这套流程同样适用于你拿到的这份源码——先复现再压榨最后才谈部署顺序反了必翻车。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑