ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于粒子群算法的无人机山地三维路径规划与Matlab实现

基于粒子群算法的无人机山地三维路径规划与Matlab实现

简介:针对无人机在山地环境下的三维路径规划问题,这份资源提供了基于粒子群算法(PSO)的完整Matlab实现。资源面向无人机导航、路径规划相关领域的研究者、工程师及学生,解决复杂地形下如何生成安全且较优飞行路径的核心需求。压缩包共7个文件,包含4个Matlab脚本(完成地形建模、PSO寻优、适应度计算与结果可视化)、2张路径规划结果图及1份粒子群算法原理讲解PPT,整体大小仅1.11MB,轻量便于学习。文件构成清晰,从defMap定义三维地形,到calFitness评估路径优劣,再到plotFigure输出,形成完整实验链。目前已有139人学习下载,说明该资源具备一定参考价值。通过运行源码,读者可直观理解PSO参数调节与三维避障的交互影响,并可直接修改地形和约束条件,迁移到自己的飞行任务中。适合希望快速上手无人机路径规划算法仿真、开展课程设计或科研预研的读者。

1. 无人机山地三维路径规划:为什么粒子群算法是首选

山区航测、电力巡检这类任务里,无人机起飞后要越过一连串山头,直线飞行必然撞山,绕路太多又费电。所谓无人机山地三维路径规划,就是在起点和终点之间找一条满足安全高度、总路径短、转弯平滑的可行航线。粒子群算法是这类优化问题里最常用的求解器之一,因为它不要求目标函数可导,实现成本低,Matlab 生态里能找到大量可参考的示例。一个反直觉的结论是:粒子群算法本身根本不认识“山”,它只知道在一个代价函数的引导下找最小值;真正决定路径质量的是地形建得准不准、代价函数量纲配不配。这套方案适合正在做无人机路径规划课题的学生、需要快速验证航线的算法工程师,以及想给飞控系统补一层离线规划能力的开发者。

2. 把山地路径变成PSO能解的数学问题:建模、代价函数与约束

2.1 无人机三维路径规划的数学模型:航迹点如何编码成粒子位置

连续三维空间里的路径有无穷多条,PSO不能直接搜索连续曲线,必须先把路径离散化。常见做法是固定起点和终点,在它们之间插入若干个航迹点,用这些点的坐标拼接成一条候选路径。每个粒子保存一条候选路径的全部信息,粒子群迭代的过程就是不断调整这些航迹点位置、让整条路径的代价降低的过程。

编码方式直接影响搜索难度。我一般会把起点到终点的水平投影方向作为 x 轴,然后把 x 方向等距切分,粒子只编码每个航迹点的 y 坐标和 z 坐标。这样每个粒子就是一个 2N 维向量,其中 N 是中间航迹点数量,向量形式为 [y1, z1, y2, z2, ..., yN, zN]。这样做有两个好处:一是 x 方向严格递增,路径不会出现回头路;二是搜索维度从 3N 降到了 2N,粒子群在高维空间里的收敛速度会明显变慢,少一维就是少一大块搜索量。

startPoint = [0, 0, 50]; % 起点坐标 (x, y, z) endPoint = [500, 450, 90]; % 终点坐标 nP = 10; % 中间航迹点数量 dim = 2 * nP; % 每个粒子的维度

在这个例子里,起点到终点被切成了 11 段,中间有 10 个待优化的航迹点,粒子维度是 20。起点和终点不参与编码,它们永远固定在原位置,这样能保证算法不会把航线起点或终点“优化”到别处去。

2.2 山地地形建模:用网格插值把高度图变成可查询的函数

代价函数需要知道任意坐标 (x, y) 处的地形高度,所以地形必须能被快速查询。真实项目里通常用 DEM 高程数据,导入 Matlab 后得到一个二维高度矩阵,再用 interp2 做双线性插值。做仿真验证时没有现成 DEM,可以直接用 Matlab 自带的 peaks 函数生成一块模拟山体,高度缩放到合理范围即可。

% 生成模拟山地高程网格 mapSize = 500; % 地图范围 500m x 500m nGrid = 50; % 网格数量 [Xq, Yq] = meshgrid(linspace(0, mapSize, nGrid)); Zq = 20 * peaks(nGrid) + 60; % 把Peaks缩放到60m以上的山体

生成之后,任意航迹点的地形高度都用 interp2(Xq, Yq, Zq, x, y, 'linear', 0) 查询。这里有个容易忽略的点:网格分辨率要和航迹点间距匹配。如果地形网格有 50×50 个点,而一条路径只有十几个航迹点,插值得到的其实是“爬过山顶”的粗略高度,路径会显得很钝;反过来网格太密、航迹点太少,插值结果又容易漏掉狭窄山谷。

为了模拟更真实的山区,我习惯在地形函数里再加几个高斯形状的山峰,而不是只用 peaks。做法是在 Zq 上叠加若干 exp(-((x-cx)^2+(y-cy)^2)/(2*sigma^2)) 的项,这样能控制山峰位置和陡峭程度,方便测试不同危险地形的应对能力。

2.3 代价函数:油耗、安全、平滑三者的加权怎么定

路径质量由代价函数定义,PSO 只负责把它最小化。山地场景下的代价函数至少包含三部分:路径总长度对应油耗和时间;航迹点低于地形加上安全裕度的穿越惩罚;路径拐弯过急带来的平滑度惩罚。写成公式就是 J = w1 * L + w2 * H + w3 * S,其中 L 是相邻航迹点欧氏距离之和,H 是地形穿透惩罚,S 是相邻线段夹角的惩罚项。

function J = cost_path(x, startPt, endPt, Xq, Yq, Zq) nP = length(x) / 2; y = x(1:nP); z = x(nP+1:end); xPos = linspace(startPt(1), endPt(1), nP+2); yPos = [startPt(2), y, endPt(2)]; zPos = [startPt(3), z, endPt(3)]; terrainH = interp2(Xq, Yq, Zq, xPos, yPos, 'linear', 0); safeH = 10; % 安全高度裕度 penetration = max(0, terrainH + safeH - zPos); H = sum(penetration) * 1000; % 穿透惩罚放大 L = sum(sqrt(diff(xPos).^2 + diff(yPos).^2 + diff(zPos).^2)); S = sum(abs(diff(zPos, 2))) * 10; % 垂直方向急变惩罚 J = L + H + S; end

这段代码里 H 的惩罚系数取了 1000,因为穿透代价必须远大于路径长度代价,否则算法会倾向于“穿山抄近路”。S 用 z 的二阶差分近似垂直方向上的急变程度,对爬升角过大的路径施加惩罚。权重的初值没有绝对标准,但有一个原则:先保证 H 主导约束,再调 L 和 S 的比例。最稳妥的做法是先把 S 系数设成 0,跑通一条不撞山的路径,再逐步加大平滑惩罚,观察路径是否变得更舒展。

约束处理之所以用惩罚函数而不是硬截断,是因为 PSO 本身没有约束处理机制。惩罚函数把约束变成目标函数的一部分,粒子能够从“撞山”的状态逐步被拉回安全区,而不是被直接丢弃。这个特性在前期探索阶段很重要,因为一开始大量粒子都在山里,直接丢弃会让种群迅速失去多样性。

3. 粒子群算法原理与Matlab实现:从公式到可运行代码

3.1 粒子群算法原理:速度-位置更新公式与惯性权重

粒子群算法原理可以概括为:一群粒子在搜索空间里飞行,每个粒子记住自己历史上找到的最好位置(个体极值 pbest),同时整个群体共享当前全局最好位置(全局极值 gbest)。每次迭代,粒子根据这两个极值调整自己的速度,再更新位置。

速度更新公式是 v(i) = w * v(i) + c1 * r1 * (pbest(i) - x(i)) + c2 * r2 * (gbest - x(i)),位置更新是 x(i) = x(i) + v(i)。其中 w 是惯性权重,控制上一时刻速度对当前速度的影响;c1、c2 是学习因子,分别决定粒子向自身经验和群体经验学习的强度;r1、r2 是 0 到 1 的随机数,用来保持随机性。

这个公式看起来简单,但工程上有几个关键点。惯性权重 w 是最值得调的参数:w 大,粒子飞行惯性大,不容易被 pbest 和 gbest 带走,全局搜索能力强;w 小,粒子快速向当前最好解靠拢,收敛快但容易陷入局部最优。常见的做法是让 w 随迭代线性递减,前期用 0.9 左右做大范围搜索,后期衰减到 0.4 左右精细打磨,这就是后面第 4 章要展开的内容。

3.2 最小可运行的Matlab代码:单文件跑通PSO三维路径规划

把前面的建模和 PSO 核心公式合到一起,就是一个 60 行左右的单文件程序。直接复制到 Matlab 里,按 F5 就能跑通一条从起点到终点的山地路径。这段代码把 PSO 更新逻辑、边界约束和代价函数调用都放在一个文件里,方便理解整体流程。

% pso_3d_path.m % 基于粒子群算法的无人机山地三维路径规划最小示例 clear; clc; close all; rng(1); % 固定随机种子,保证可复现 % ---- 1. 地形与起终点 ---- mapSize = 500; nGrid = 50; [Xq, Yq] = meshgrid(linspace(0, mapSize, nGrid)); Zq = 20 * peaks(nGrid) + 60; startPoint = [0, 0, 50]; endPoint = [500, 450, 90]; % ---- 2. 粒子初始化 ---- nP = 10; % 中间航迹点数量 dim = 2 * nP; nParticles = 50; % 粒子数 nIter = 200; % 迭代次数 lb_y = 0; ub_y = mapSize; lb_z = 30; ub_z = 150; pos = zeros(nParticles, dim); pos(:, 1:nP) = rand(nParticles, nP) * (ub_y - lb_y) + lb_y; pos(:, nP+1:2*nP) = rand(nParticles, nP) * (ub_z - lb_z) + lb_z; vel = zeros(nParticles, dim); Vmax = 20; % 速度上限 % ---- 3. 初始化 pbest 和 gbest ---- cost = zeros(nParticles, 1); for i = 1:nParticles cost(i) = cost_path(pos(i, :), startPoint, endPoint, Xq, Yq, Zq); end pbest = pos; pbest_cost = cost; [gbest_cost, idx] = min(cost); gbest = pos(idx, :); % ---- 4. PSO 主循环 ---- w = 0.9; c1 = 2.0; c2 = 2.0; for iter = 1:nIter w = 0.9 - (0.9 - 0.4) * iter / nIter; % 惯性权重线性递减 for i = 1:nParticles r1 = rand(1, dim); r2 = rand(1, dim); vel(i, :) = w * vel(i, :) + ... c1 * r1 .* (pbest(i, :) - pos(i, :)) + ... c2 * r2 .* (gbest - pos(i, :)); vel(i, :) = max(min(vel(i, :), Vmax), -Vmax); pos(i, :) = pos(i, :) + vel(i, :); end % 边界截断 pos(:, 1:nP) = max(min(pos(:, 1:nP), ub_y), lb_y); pos(:, nP+1:2*nP) = max(min(pos(:, nP+1:2*nP), ub_z), lb_z); % 更新 pbest 和 gbest for i = 1:nParticles cost(i) = cost_path(pos(i, :), startPoint, endPoint, Xq, Yq, Zq); if cost(i) < pbest_cost(i) pbest(i, :) = pos(i, :); pbest_cost(i) = cost(i); end end [min_cost, idx] = min(cost); if min_cost < gbest_cost gbest_cost = min_cost; gbest = pos(idx, :); end end % ---- 5. 输出最终路径 ---- yBest = [startPoint(2), gbest(1:nP), endPoint(2)]; zBest = [startPoint(3), gbest(nP+1:2*nP), endPoint(3)]; xBest = linspace(startPoint(1), endPoint(1), nP+2); plot3(xBest, yBest, zBest, 'LineWidth', 2); hold on; mesh(Xq, Yq, Zq); axis equal;

代码里有几个地方值得说明。粒子初始化阶段,y 坐标在 0 到 500 的范围内均匀随机,z 坐标在 30 到 150 米范围内随机,这就保证初始路径不会出现在极端高度。Vmax 取 20,意味着每次迭代粒子最多移动 20 米,这个值太小会让收敛变慢,太大会让粒子在边界之间来回穿越。

主循环里惯性权重 w 是逐次递减的,前 50 代 w 接近 0.9,粒子有较强惯性去探索远处;后 50 代 w 接近 0.4,粒子逐步收敛到 gbest 附近。如果你发现某次运行中后期代价曲线还在剧烈抖动,基本可以断定是 Vmax 偏大或者 w 减得太慢。

3.3 源码包模块拆解:main、代价函数、地形与绘图脚本怎么配合

你下载的 zip 压缩包解压后,一般会看到几个 .m 文件,最核心的四个角色是主脚本、代价函数、地形生成函数和绘图脚本。主脚本负责设置参数、初始化种群、跑 PSO 循环;代价函数输入一个粒子向量,输出这条路径的代价;地形生成函数负责生成或加载高度数据;绘图脚本负责把地形网格、最终路径和迭代收敛曲线画出来。

运行顺序通常是先跑地形生成函数,再跑主脚本。如果源码包的入口不是 main.m,打开后先看哪个脚本里包含了“初始化粒子数量”和“PSO 主循环”,那个就是入口。常见的坑是直接运行绘图脚本,结果报错说找不到地形变量,因为绘图脚本依赖主脚本生成的变量。

替换成自己的地图时,只需要修改地形生成函数和起终点坐标。如果是从 DEM 文件导入,把 Zq 替换成加载的高度矩阵,同时保证 Xq、Yq 的坐标范围和单位一致。单位不统一是改数据时最常遇到的问题,DEM 数据可能是经纬度、可能是米,而路径规划里的距离计算默认按米处理,换算错一位代价函数全部失效。

4. 粒子群算法的四个必调参数:粒子数、惯性权重与学习因子

4.1 粒子数与迭代次数:先决定计算规模

粒子数决定了每一代要调用多少次代价函数。在三维路径规划里,代价函数里包含路径长度、地形插值和平滑度计算,粒子数从 30 增加到 80,单次迭代耗时几乎翻倍,但找到的最优路径质量未必同步提升。经验值是 40 到 60 个粒子对应 200 代左右迭代,对于 20 维的问题已经足够。

判断粒子数是否够用,最直接的办法是重复运行 3 次,看 gbest 的最终代价值是否稳定。如果三次结果差别很大,说明粒子数量太少,搜索空间没有被充分覆盖;如果三次结果接近但代价曲线后期还在缓慢下降,说明迭代次数不够。

迭代次数不是越大越好。粒子群算法通常在前 30% 的迭代里就找到了可行域,剩下的时间是在当前最优附近打磨。200 代对于这个规模的问题已经足够,继续加到 500 代,收益很低但耗时线性增长。

4.2 惯性权重w:从0.9衰减到0.4的工程含义

惯性权重是最值得先调的参数。w = 0.9 时,粒子受上一代速度影响大,不容易被 pbest 和 gbest 突然拉走,适合前期在整张地图上探索;w = 0.4 时,粒子几乎完全跟随当前最优,适合后期精细收敛。线性递减是被验证最多次的策略,公式是 w = w_max - (w_max - w_min) * iter / nIter。

我习惯把 w_max 设为 0.9、w_min 设为 0.4,对应代码里的一行:w = 0.9 - (0.9 - 0.4) * iter / nIter。如果你发现路径在山谷之间反复震荡、收敛很慢,可以把 w_max 降到 0.8,让粒子更早进入收敛阶段。如果你发现最终路径明显绕远、明显错过了捷径,说明前期探索不足,把 w_max 保持 0.9 甚至提到 0.95,给粒子更多自由飞行的时间。

这里有一个很容易翻车的地方:w 的最小值不能设到 0 附近。w 太小会让粒子失去惯性,速度完全由 pbest 和 gbest 引导,粒子会被吸进局部最优后再也无法出来。保留 0.3 以上的惯性,哪怕在最后阶段,也能让粒子在小范围内继续抖动,有机会跳出不好的位置。

4.3 学习因子c1、c2与速度上限Vmax:平衡探索与开采

c1 是粒子向自己历史最优学习的强度,c2 是向全局最优学习的强度。经典取值是 c1 = c2 = 2,这个组合在很多问题上都能收敛,但并不总是最优。当问题维度高、地形复杂时,c1 = 2.5、c2 = 1.5 的效果往往更好,因为更强的个体认知能让粒子在前期更坚持自己的方向,不容易被某个过早出现的 gbest 带偏。

速度上限 Vmax 的作用容易被低估。Vmax 太大,粒子一步飞过半个地图,y 方向容易直接撞到边界;Vmax 太小,粒子每一步只能挪几米,200 代只探索了地图的一小部分。经验值是 Vmax 取搜索空间宽度的 10% 到 20%。在这个例子里 y 范围是 500,z 范围是 120,Vmax = 20 相当于最大飞行速度占 y 范围的 4%,对 z 方向来说占 17%。

调参顺序有一个固定套路:先把 Vmax 固定为搜索空间宽度的 10%,然后用 c1 = 2、c2 = 2、w 线性递减 0.9 到 0.4 跑一遍,画出代价曲线。曲线如果前 50 代急速下降后完全持平,说明 w 衰减太慢或 c2 太强;曲线如果一直在锯齿状抖动,说明 Vmax 偏大;曲线如果从第 1 代到第 200 代都是缓慢下降,说明粒子数偏少或 c1 偏弱。先调 w,再调 c1、c2,最后才动 Vmax,这样才不会在多个参数之间来回折腾。

5. 山地三维路径规划避坑指南:五个最容易翻车的细节

5.1 坑一:粒子越界直接截断,导致路径贴着地形走

现象:跑完后的路径 z 坐标大量落在 lb_z = 30 这个边界上,整条航线贴着地面走,甚至穿过山体,代价函数里的穿透惩罚值不小,但算法就是不改。

原因:边界处理用了直接截断,粒子一旦越过 z 的下边界就被强制拉回 30 米。如果地形本身在 60 米以上,30 米这个位置无论怎么调整 y,代价都会很高。更关键的是,被截断的粒子失去了对“低于 30 米”这个区域的探索能力,但有时候为了绕过一座山,合理的路径是先下降再爬升,截断把这种可能性直接抹掉了。

解决:把 z 的下边界设置成地形最低点以上而不是固定 30 米,例如先扫描整个地图求出 min(Zq(:)),再设 lb_z = min(Zq(:)) + 5。更稳的方案是边界截断后做一个二次校验,如果截断位置仍然低于地形,就把 z 抬到地形高度加安全裕度,并给一个小随机扰动,保留下一次迭代继续调整的空间。

5.2 坑二:代价函数量纲不一致,长度项被惩罚项淹没

现象:路径明显绕了一个大圈,长度多出 200 米,但代价函数评分和一条更直的路径几乎一样。查看代价函数各分量后才发现,穿透惩罚系数设成 1e6,路径长度只有几百量级,长度项对总代价的贡献不到 0.1%,算法根本感知不到路径长短。

原因:路径长度单位是米,数值在几百到上千;穿透惩罚是高度差乘以一个放大系数,数值可能到十万甚至百万。不同量纲的量直接相加,小量纲项等于被忽略。

解决:先把各分量归一化再加权。比如 L 除以一个参考长度(地图对角线长度),穿透惩罚除以一个参考高度(山峰最高点),这样各分量都在 0 到 1 的区间,权重才真正起作用。我一般会在调试阶段分别打印出 L、H、S 三个分量的数值,只要看到某一个分量比其他分量大两个数量级以上,就先做归一化再继续调参。

5.3 坑三:只用一条起终点验证,换条航线就翻车

现象:同一组参数在 A 点到 B 点的任务上表现良好,路径短、不穿山、很平滑。换成长距离跨山任务后,路径频繁穿山,代价曲线抖动,最终结果完全不可用。

原因:单条航线的地形复杂度有限,参数整定的时候算法“记住”了这条航线的地形特征。山地地形的非凸性很强,局部最优很多,一组参数不可能通吃所有地形。

解决:验证阶段至少准备三条不同起终点的航线,一条短距低山、一条长距高山、一条需要绕行峡谷。记录每条航线的最终代价和是否满足安全高度约束。如果某条航线失败,先调 Vmax 和粒子数,而不是急着改代价函数权重。把“多航线验证”作为参数调好的标准动作,能避免把偶然的成功当成普遍结论。

5.4 坑四:不设随机种子,两次运行结果完全不同

现象:同一份源码,连续运行两次,第一次路径向左绕,第二次路径向右绕,最终代价也不一样。实验记录里写不下结果,因为每次数字都在变。

原因:粒子群算法初始化用的是随机数,pbest 和 gbest 的更新也依赖随机数。不固定随机种子,每次运行本质上是独立采样,结果自然不同。

解决:在主脚本开头加一行 rng(固定数字),例如 rng(1)。想要比较不同参数的效果时,用同一个种子跑所有实验,这样差异完全来自参数而不是随机运气。调完参数后,再换一个种子跑一次,确认结果质量不依赖特定随机序列。这一步是我踩过最多坑的地方——早期调参时没固定种子,改了参数还以为没效果,白白折腾了好几天。

5.5 坑五:过早收敛,路径绕远路

现象:代价曲线前 20 代急速下降,之后 180 代完全停滞。最终路径从一座山侧面绕过去,绕了很大一圈,明显不是最优路线。

原因:某个早期粒子在地形平缓区域碰巧找到一条低代价路径,成为 gbest 后,其他粒子快速向它靠拢,种群失去多样性,再也没有力量去探索更远但可能更好的区域。这个现象在 c2 偏大、w 衰减过快时尤其严重。

解决:把 w_min 从 0.4 提高到 0.5,让粒子在后期保留更多惯性。另一个有效技巧是加“早停重置”:如果全局最优连续 40 代没有下降,就把最差的一半粒子重新随机初始化,或者给 gbest 的位置加一个随机扰动后再作为新种子加入种群,相当于让陷入停滞的搜索重启一遍。这个技巧在复杂山地场景里能明显提高最终路径质量。

6. 从仿真到可用:航迹平滑与稳定性验证

6.1 用pchip插值把折线航迹平滑成连续曲线

PSO 输出的原始路径是航迹点之间的直线连接,看起来像折线,不能直接给飞控用。先用 pchip 插值把离散航迹点变成密集连续曲线,pchip 相比 spline 的优点是过冲小,不会在航迹点之间出现明显的高度下探然后回弹的情况。

% 用pchip把稀疏航迹点插值成密集航迹 xDense = linspace(min(xBest), max(xBest), 200); yDense = interp1(xBest, yBest, xDense, 'pchip'); zDense = interp1(xBest, zBest, xDense, 'pchip'); % 画平滑后的航迹,检查有没有异常波动 figure; plot3(xDense, yDense, zDense, 'r-', 'LineWidth', 2); hold on; mesh(Xq, Yq, Zq); axis equal;

插值之后务必检查每个点的 z 是否仍然高于地形。我遇到过一次平滑后路径穿过山体的情况:航迹点本身高于地形,但 pchip 在两点之间生成了一段下凹曲线,刚好低于山峰。遇到这种问题,先提高安全裕度 safeH,再在代价函数里加入对插值后密集航迹的穿透检查,而不仅仅检查稀疏航迹点。

6.2 重复试验与统计指标:用数据判断参数是否真的有效

调参不能靠肉眼判断。我习惯固定一个参数组合,换 5 个随机种子各跑一次,记录最终代价、路径长度、是否穿山、运行时间。如果 5 次里有一次穿山或者代价标准差超过平均值 20%,说明参数不稳定,需要回到第 4 章重新调整。

指标含义合格参考
最终代价gbest 对应的代价函数值越低越好,且多次运行接近
代价标准差多次运行的稳定性小于平均值的 20%
穿山次数路径穿透地形的次数必须为 0
平滑后穿山插值后的穿透检查必须为 0
运行时间一次完整优化的耗时根据需求,通常秒级可接受

最后说一个我的习惯:每调一组参数,就把代价曲线图和三维路径图一起存下来,文件名带上参数值,比如 w09c1p50_v20。这样一周以后再回头找最优参数,不用靠记忆,直接翻文件名就行。路径规划里的参数组合远多于直觉能覆盖的范围,把每一次实验当作可复现的记录,比碰运气高效得多。希望这个方案能帮你把粒子群算法的山地路径规划跑通,也希望你在调参时少走我走过的那些弯路。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进