☰
A星算法无人机路径规划实战:栅格地图构建与调参避坑指南
2026/10/10 15:15:24 网站建设 项目流程

最近在做一套低空物流无人机的路径规划Demo,核心算法选的是教科书里最经典的标准A星。说白了就是给无人机在栅格地图上找一条从起点到终点的无碰撞路径,听起来简单,但真正落地到无人机飞行场景时,网格怎么建、启发函数怎么选、参数怎么调,每一步都有不少值得掰开讲的东西。这篇文章把我从头到尾的思路、实现细节和踩过的坑都整理出来,适合正准备入无人机路径规划的开发者,也适合已经会用A星但想把它做得更贴合飞行任务的朋友参考。

1. 项目背景与选型逻辑:为什么是标准A星

1.1 需求描述:先求有路,再求最优

项目需求很朴素:一台多旋翼无人机需要在一个中型园区内自主飞行,从停机坪起飞,绕过几栋楼、几排树和一些临时围挡,降落到目标点。地图由实际环境预先构建,障碍物区域已知,飞行高度固定,所以问题可以约化为二维平面上的路径规划。

为什么要强调“先求有路,再求最优”?因为实际飞行对路径的第一要求是安全、可执行,而不是数学意义上的最短。很多人在入门时急着上各种高级算法,但连一条“能飞的路”都还没稳定跑通,后面所有优化都无从谈起。标准A星天然适合这个阶段:它确定、可控、可解释,每一步扩展都能追溯,出了问题也容易定位。

我在项目里把它当基准路径生成器,和后面的平滑模块、速度规划模块解耦。A星只负责给出“不撞障碍的粗略路线”,后续再对转折点做处理。这样做的收益是,即使后面要换成其他算法,代价都只集中在一个模块内部。

1.2 为什么不上RRT、JPS这些“更厉害”的算法

刚开始我也犹豫过,要不要直接上RRT快速探索随机树,或者用JPS跳点搜索来提升性能。后来还是回到标准A星,原因是:

  • RRT适合高维空间和连续状态空间,但在二维栅格上它反而会引入随机性,路径抖动大,且不保证最优,需要额外做平滑和剪枝。
  • JPS在规则栅格上确实比A星快很多,但它的实现细节多,跳点规则容易写错,而且对环境变化更敏感。
  • 标准A星在中小尺寸栅格上的性能完全够用,比如500×500的栅格,只要结构写对,单次规划时间在毫秒级,完全满足我当前的需求。

还有一个更实际的理由:在项目早期,我需要一个绝对可信的“基准线”来验证建图是否正确。如果路径规划算法本身引入了不确定性,出了问题就分不清是地图的问题还是算法的问题。标准A星在这个角色上非常称职。

2. 标准A星的核心拆解:f=g+h背后的实际含义

2.1 状态空间与f=g+h的含义

标准A星的核心公式是f(n) = g(n) + h(n),但很多初学者只记公式,没理解每个量在无人机场景下的物理意义。

  • g(n):从起点走到当前节点n已付出的实际代价。对无人机来说,这个代价可以是飞行距离,也可以是预估能耗。
  • h(n):从当前节点n到终点的估计代价。它是启发式估计,不要求精确,但要求不高估真实代价,否则会丢失最优性。
  • f(n):从起点经过n再到终点的总代价估计。A星每次从开放列表中取出f值最小的节点扩展,直到终点被弹出。

可以类比成一个送货问题:你已经走了5公里(g),目测还有3公里到目的地(h),总预计要跑8公里(f)。A星会优先尝试那些总预计最短的路线,而不是只看已经走了多远。

在无人机路径规划中,这里的“距离”可以用物理距离,也可以用能耗模型。标准A星本身不关心代价值怎么定义,只要满足非负且可相加即可。这给后面调优留了很大空间。

2.2 启发函数的选择:曼哈顿距离还是Octile距离

这是最容易被忽略、影响却很大的一个选择。很多教程直接写曼哈顿距离abs(dx)+abs(dy),但那是给四邻域移动设计的。如果无人机允许斜向飞行,再用曼哈顿距离就会高估真实代价,A星会失去最优性保证,跑出来的路径可能明显不自然。

八邻域移动对应的启发函数叫Octile距离,公式是:

h(n) = min(dx, dy) * 1.414 + abs(dx - dy)

其中dx = abs(n.x - goal.x),dy = abs(n.y - goal.y)。这个公式的含义是:先尽量斜着走,剩下的部分再走直线,对应八邻域下的最短可能路径。

这个细节直接决定路径质量。我用一个简单例子验证过:起点(0,0)、终点(5,5),如果所有格子可通行,八邻域下最优路径就是斜着走5步,代价约7.07。如果用曼哈顿距离做启发,估计值是10,远超真实代价,算法就会优先向“看起来更近”的横向/纵向扩展,导致搜索范围明显变大,甚至可能因为高估而给出非最短路径。换成Octile距离后,估计值等于真实值,算法几乎一路直奔终点,扩展节点数大幅下降。

2.3 开闭表与路径回溯的标准实现

直接给一个可运行的核心伪代码实现。这里用Python风格的伪代码,重点是结构,不是语法糖:

import heapq def octile_distance(a, b): dx = abs(a[0] - b[0]) dy = abs(a[1] - b[1]) return min(dx, dy) * 1.414 + abs(dx - dy) def reconstruct_path(came_from, current): path = [current] while came_from[current] is not None: current = came_from[current] path.append(current) path.reverse() return path def a_star(grid, start, goal, w=1.0): # 开放列表用最小堆,保持f值最小的节点在堆顶 open_heap = [] counter = 0 # 用于打破相同f值时的平局 heapq.heappush(open_heap, (0.0, counter, start)) came_from = {start: None} g_score = {start: 0.0} closed_set = set() while open_heap: _, _, current = heapq.heappop(open_heap) if current == goal: return reconstruct_path(came_from, current) if current in closed_set: continue closed_set.add(current) for neighbor, move_cost in neighbors(grid, current): if neighbor in closed_set: continue tentative_g = g_score[current] + move_cost if tentative_g < g_score.get(neighbor, float('inf')): came_from[neighbor] = current g_score[neighbor] = tentative_g f = tentative_g + w * octile_distance(neighbor, goal) counter += 1 heapq.heappush(open_heap, (f, counter, neighbor)) return None # 无可行路径

几点说明:

  • closed_set用Python的set,查找O(1),比列表快得多。
  • open_heap用最小堆,每次弹出f值最小的节点,不用全列表扫描。
  • counter字段很重要。当两个节点f值相同时,完全相同的元组比较会导致比较错误或跳过节点,增加一个自增序号可以安全避免。
  • 路径回溯从终点沿着came_from一路回到起点,再反转,就是最终路径。

3. 栅格地图建模:决定路径质量的第一层关卡

3.1 栅格化流程:从障碍到可通行标记

无人机飞行时拿到的不是整齐的格子,而是一堆障碍物轮廓、高度数据。我是这样处理成A星能用的栅格地图的:

  1. 把园区地图按固定分辨率栅格化,比如每格0.5米×0.5米。
  2. 每个格子根据是否有障碍物标记为0(可通行)或1(障碍)。
  3. 无人机固定飞行高度,所以只考虑二维投影,遇到高于飞行安全高度的物体才标记为障碍。
  4. 把障碍物边界向外扩展,这个环节后面单独说。

栅格粒度直接影响结果。粒度太粗,窄通道会被吞掉,路径可能不存在;粒度太细,地图巨大,搜索速度下降。0.5米分辨率对一个翼展约0.6米的小型无人机来说是一个比较合理的起点,但最终要根据实际飞行环境反复调整。

3.2 八邻域和对角穿墙检测

允许斜向移动后,必须考虑一个经典问题:斜着从障碍物的角上穿过去。假如当前节点在(0,0),目标对角节点在(1,1),而(0,1)或(1,0)是障碍物,那这条斜线实际上是在“擦边”走,一个没有完全膨胀的障碍物边缘可能会刮到无人机。

解决方式是在生成邻居时增加一次阻挡判断:

def neighbors(grid, node): x, y = node rows, cols = len(grid), len(grid[0]) # 四邻域 for dx, dy in [(1,0), (-1,0), (0,1), (0,-1)]: nx, ny = x+dx, y+dy if 0 <= nx < rows and 0 <= ny < cols and grid[nx][ny] == 0: yield (nx, ny), 1.0 # 八邻域(对角移动) for dx, dy in [(1,1), (1,-1), (-1,1), (-1,-1)]: # 检查相邻的两个正交方向是否可通行 if grid[x+dx][y] == 0 and grid[x][y+dy] == 0: nx, ny = x+dx, y+dy if 0 <= nx < rows and 0 <= ny < cols and grid[nx][ny] == 0: yield (nx, ny), 1.414

这里的判断逻辑是:斜向移动被允许的前提是两侧的正交邻居都可行。换句话说,无人机不是“穿墙”过去的,而是从一个开放空间斜向滑进另一个开放空间。

3.3 膨胀半径:把无人机当成一个有体积的物体

这是所有路径规划里最容易忽略、却至关重要的一步。A星规划出的路径只是一条几何线,但无人机是有物理尺寸的。如果直接用原始障碍物地图规划,路径很可能从距离墙壁5厘米的地方掠过,现实中早就撞上了。

我的做法是:在规划前对栅格地图执行形态学膨胀,把所有障碍物向外扩展若干格。膨胀半径需要考虑几个因素:

因素说明
无人机半翼展/半径最基本的物理尺寸
定位误差GPS或室内定位系统的偏差
控制误差飞行控制器跟踪路径时的横向偏差
安全距离根据飞行环境和任务要求额外保留的裕度

实际项目中,如果无人机半径0.3米、定位误差0.2米、控制误差0.2米,再加上安全的0.3米,膨胀半径大约1.0米。在0.5米分辨率的栅格上就是向外扩2格。这个裕度宁多勿少,因为路径规划更差一点可以接受,撞上障碍物则是任务失败。

提示:膨胀阶段的半径过大虽然安全,但可能导致窄通道完全被堵死,路径规划直接返回“无解”。先检查膨胀后地图的连通性,再决定是否调整分辨率。

4. 无人机调参实战:权重、转弯代价与平滑处理

4.1 启发权重w:更快但不保证最优

标准A星里h(n)前面经常乘一个权重w,这就是加权A星。w=1.0时是标准最优A星;w>1时搜索更快,但路径可能是次优的。

为什么要在这个项目里讨论w?因为无人机平台的算力有时受限,而且任务场景下的路径不必是严格最短。实际测试中,w=1.2左右可以在几乎不影响路径长度的前提下减少大约20%到30%的扩展节点,规划速度明显提升。

但要注意:w不是越大越好。w过大时算法会过度偏向终点方向,容易被局部障碍骗进死胡同,反而需要回头扩展大量节点,甚至在极端情况下给出明显绕路的路径。我在测试w=2.0时,多次出现无人机贴着障碍物绕行的“蠢路径”,看起来完全不像经验丰富的飞行器该走的路线。

建议从w=1.0开始,在保证最优路径可用的前提下逐步提高,每次跑同一组测试用例对比规划时间和路径长度,找到一个对当前场景最合适的折中值。

4.2 避免高频转向:代价函数与平滑

标准A星规划出的路径经常是“锯齿状”的,尤其是在复杂障碍环境中:一个个45度转角连接起来,无人机如果要完全照飞,就会频繁横向摆动,不仅能量消耗大,姿态传感器在这时候也容易震荡。

我在项目中做了两层处理:

第一层,在代价函数中加入微小的转向惩罚。标准A星的节点状态只有坐标,并不知道无人机从哪个方向来,所以严格意义上无法精确计算转向代价。一个变通做法是:把状态空间从(x, y)扩展为(x, y, heading),其中heading表示上一段的移动方向。这样在计算g(n)时可以判断当前移动方向是否和上一方向一致,不一致就追加一个额外代价。代价扩展后的节点数量变成原来的8倍,但换来的是更平滑的路径。

第二层,做路径后处理平滑。把A星输出的粗略路径作为控制点,做三次样条插值或者保留短路优化。我的做法是:从起点开始,尝试跳过中间节点,如果两点连线不经过膨胀后的障碍物,就删掉中间节点,继续向前尝试。这个过程能把一段20个节点的锯齿路径压缩到五六个关键的直线段,飞行姿态稳定很多。

4.3 实时重规划的时间预算

标准A星是静态规划算法,但无人机实际飞行时几乎总会遇到动态变化——临时出现的人、移动的车辆、突发的风场影响。我当时的应对很简单:把规划模块做成可重入的,以每秒钟几次的频率检查地图变化,一旦发现原路径上出现了新的障碍节点,就立即以当前位置为起点、原目标为终点重新规划。

这里有个时间预算的坑:在较大的地图上重新规划不能卡顿太久,否则无人机悬停等待时已经在漂移了。解决方案是限制单次规划的最大执行时间,如果超时就用上一次成功的路径先维持飞行,等待重新规划完成。标准A星在500×500栅格、8邻域、未加权的情况下单次规划通常不超过几十毫秒,所以通过代码层面的优化,实时重规划的压力并不大。

5. 仿真测试与踩坑记录:三个影响飞行效果的问题

5.1 基础用例与基准结果

我在仿真环境里布置了几组典型场景:普通障碍随机分布、U形障碍通道、窄缝通道、大范围空旷区域加少量点状障碍。每组场景都记录规划时间、路径长度、转折点数量三个指标。

基准数据如下表:

用例栅格尺寸起点到终点直线距离规划耗时转折点数说明
简单障碍300×300280格8ms6路径合理
U形障碍400×400180格25ms12绕行正确
窄缝通道400×400120格30ms5未卡死
空旷区域500×500450格3ms2基本直线

这些数据说明了标准A星在常规场景下性能足够好,真正的问题并不在算法本体,而在使用细节。

5.2 坑一:对角“擦边”路径

第一次跑完仿真直接看路径,最明显的问题是很多转折点距离障碍物的角非常近。当时我还没有加对角穿墙判断,路径会从两个对角相邻的障碍物之间斜穿过去,视觉上就是一个“擦着墙皮走”的路线。

排查过程其实不复杂:我在某个出现擦边路径的节点处打印邻居信息,发现算法在生成斜向邻居时根本没有检查相邻正交格子。加完grid[x+dx][y] == 0 and grid[x][y+dy] == 0这个判断后,擦边现象立刻消失。

这个坑给所有做栅格路径规划的人提了个醒:八邻域不是简单地把8个方向都列出来就叫八邻域,斜向移动是有前提条件的。

5.3 坑二:转折点过多导致横滚反复

仿真中有一段路径命中了一串交替的45度转向,无人机按路径飞行时,横向姿态来回切换,飞控日志里能看到期望横滚角在短时间内反复快速变化。虽然路径在几何上是合法的,但飞起来非常难受。

这个问题在纯A星层面很难彻底解决,因为它产生的原因是栅格离散化后的折线本质。我最终的方案是规划后加平滑和关键点压缩,把路径拆成更少的大转折段,然后在每个转折点附近用一段圆弧过渡。这样既保留了A星安全有界的优点,又满足实际飞控的平滑性要求。

5.4 坑三:固定网格粒度与任务不匹配

有一段时间我一直在0.5米分辨率上调试,没有思考这个值是不是合理。后来在仿真里放了一条只有0.4米宽的小通道,0.5米分辨率的栅格直接把通道格子标记成了障碍,路径规划返回无解。

我最初以为是膨胀半径设大了,调整膨胀参数后依然无解,这才意识到是分辨率的问题。把栅格分辨率改成0.3米后,通道被正确识别,A星也顺利找到了路径。这个教训说明:建图和规划是一个整体,不能只调规划算法,建图分辨率拉跨了,再好用的A星也无能为力。

6. 跑通标准A星之后:可扩展方向与我的建议

6.1 双向A星和JPS:提升规划速度的经典思路

如果地图特别大,标准A星的扩展节点数会成为瓶颈。两个经典优化方向是双向A星和JPS跳点搜索。

双向A星的本质是从起点和终点同时向外搜索,让两棵搜索树在中间相遇。这么做的收益是,在高维或大尺寸栅格中,搜索面积从圆形变成更小的纺锤形,需要处理的节点数量会明显少于单向A星。

JPS则是在规则栅格中利用了“直路无分支”的特性,只扩展有“跳点”的方向,跳过大量方向单调的格子。在开阔区域,JPS带来的加速非常显著,可以轻松达到标准A星几倍到十几倍的速度。代价是代码复杂度上升,且地图必须是规则栅格,不规则代价场里它发挥不出来。

6.2 D* Lite:处理动态障碍的增量式解法

标准A星在静态地图上表现很好,但一旦地图频繁变化,每次都全量重规划会造成浪费。D* Lite这类增量式算法可以复用上一次规划的搜索结果,只更新受影响的部分,适合无人机在未知环境中边飞行边建图边规划的场景。

从标准A星过渡到D* Lite有一个比较好的路径:先把A星的开放列表、节点代价这些概念吃透,再去理解D* Lite的rhs值、优先队列更新逻辑,会平滑很多。直接上手D* Lite往往会陷入各种队列更新细节里出不来。

6.3 三维空间与能耗代价模型

如果后续任务需要无人机跨高度飞行,二维栅格A星可以扩展为三维栅格,节点从(x, y)变成(x, y, z),八邻域变成二十六邻域。启发函数从二维Octile距离扩展成三维版本。代码结构不变,但地图数据量和计算量会成倍增长,这时候就需要考虑JPS或分层规划了。

回到这个项目本身,我个人最大的体会是:标准A星的价值不在于算法有多"高级",而在于它足够简单、足够可靠,可以把复杂的真实问题一层层剥出来,让建图、膨胀、平滑、飞行控制这些环节都分得清清楚楚。如果你也要做无人机路径规划,我强烈建议先把标准A星按这个思路完整跑通一次,再往更花哨的方向走。这个基础打牢了,后面换算法、加约束、调参数都会非常有底气。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询