ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

A*算法与TOA定位融合的MATLAB路径规划闭环实现

A*算法与TOA定位融合的MATLAB路径规划闭环实现 做机器人导航、AGV调度或者无人机巡检的朋友应该都体会过这么一件事路径规划算法单独跑通不难难的是让规划真正变成一条可用的导航链路。因为你得先明确当前位置才能谈得到目标点怎么走。这就是为什么我把A算法和TOA定位放到同一个MATLAB项目里来写——前者负责在二维栅格地图上规划避障路径后者负责用基站测距解算出运动体当前坐标两者串联起来就是一个完整的“定位-规划-执行”闭环。这套代码把障碍物环境、A路径搜索、TOA定位、可视化跟性能评估全部整合在一起跑一遍就能看到规划出的路径叠加在栅格地图上同时对比定位结果和真实坐标的偏差很适合课程设计、毕业设计或者刚接触路径规划与无线定位的工程师拿来当第一份可复现的模板。1. 项目整体架构与设计思路1.1 这套系统解决了什么问题很多初学者容易把路径规划和定位当成两个孤立的问题要么只做A*给定一个起点一个终点就用搜索算法找路要么只做TOA定位算出坐标就结束。但真实系统里这两个环节是强耦合的。AGV在仓库里跑小车在车间里走规划器给出的路径起点必须是当前位置的可靠估计否则路径在数学上再漂亮实际执行时也会撞墙或者绕远路。所以我设计这个实例的初衷很简单把两个最常用的算法放进同一个框架端到端跑通。A*解决的是“在已知静态障碍物地图里找一条从起点到目标点的无碰撞路径”TOA解决的是“在已知基站坐标的前提下用测距信息估算运动体的平面坐标”。前者输出一条路径点序列后者输出一个坐标估计值后者的输出就是前者的输入起点。当定位误差在合理范围内时规划结果依然是一条可执行路径当定位误差过大甚至导致起点落入障碍物时系统还需要有一定的兜底处理。这套思路不复杂但覆盖了从算法原理到工程落地的完整链条。1.2 模块划分与数据流整个项目我按功能拆成了四个模块地图模块、定位模块、规划模块、评估与可视化模块。地图模块负责栅格地图的生成、障碍物随机摆放和坐标映射定位模块模拟几个基站对运动体做TOA测距再用最小二乘解算坐标规划模块读入定位坐标作为起点、用户指定目标点跑A*搜索评估与可视化模块负责记录路径长度、规划耗时、扩展节点数、定位误差等指标并把地图、路径、基站、定位结果叠加到一张图上。数据流向是这样的先生成地图和运动体真实坐标然后定位模块根据含噪声的测距值估算坐标估算坐标经过边界检查后交给A*A搜出的路径送去可视化并统计性能。整个过程里定位是“前端感知”规划是“后端决策”。这种模块化写法还有个好处以后想换算法比如把A换成Dijkstra或者RRT或者把TOA换成TDOA只需要替换对应模块的输入输出接口不需要改动整个工程。1.3 坐标系约定与栅格分辨率这个项目里最容易翻车的不是算法本身而是坐标转换。我用了两套坐标表示世界坐标系以米为单位原点在地图左下角x轴向右、y轴向上栅格坐标系以行为列表示地图矩阵的某一行某一列对应一个格子。假如地图长宽都是20米栅格分辨率取1米那么世界坐标(5.5, 8.2)对应的栅格坐标就是(round(8.2)1, round(5.5)1)。这里行序号对应世界y坐标列序号对应世界x坐标方向细节我在后文的可视化部分还会再强调。分辨率的选择是一个典型的权衡。取值越大地图越细路径越平滑但A*搜索空间按面积增长耗时明显上升取值太小比如0.1米障碍物边界和通道宽度都变得粗糙路径可能从缝里挤过去。我一般先按机器人安全半径来定如果机器人半径0.3米栅格分辨率取1米那规划出来的路径对原点位形基本无碰撞不需要做过细的膨胀。如果你做的场景里机械结构不可忽略那就得靠障碍物膨胀来解决这属于工程细节后面会专门讲。2. 核心算法原理拆解2.1 A*算法的工作原理与适用边界A算法本质上是一种启发式图搜索。它把地图看成由栅格节点组成的图每个节点有一个代价值f g hg是从起点走到当前节点已经花费的实际代价h是从当前节点到目标点的启发式估计代价。搜索时每次都从open集合里取f最小的节点扩展直到取出目标点为止。这跟自由空间法、概率路图这类方法不同属于典型的确定性完备搜索只要存在路径A在有界栅格图上一定能找到它。有人会把路径规划跟字符串匹配问题里的KMP类比也有人说“反正地图不大干脆暴力枚举所有路径”。我得说这两条路都不对。KMP解决的是线性序列的匹配问题路径规划是在二维图上找最优路径两者数学模型完全不同暴力枚举在节点数多时组合爆炸不可行。A*真正厉害的地方在于通过启发函数让搜索方向偏向目标这样它扩展的节点数通常远小于Dijkstra也不会像贪心算法那样为了眼前利益丢失可行解。2.2 启发函数到底怎么选启发函数h(n)的选择直接决定A*的行为。无条件的最优性要求h必须满足可采纳性也就是h(n)不能超过从n到目标点的真实最小代价。在这个前提下h越接近真实代价扩展节点越少搜索越快h估计过低搜索范围变大h估计过高搜索可能不再是全局最优。我对比过几种常用选择启发函数公式适用情况我的建议曼哈顿距离x差绝对值 y差绝对值只允许上下左右四方向移动四方向移动首选欧几里得距离sqrt(x差平方 y差平方)允许八方向或任意方向移动八方向移动推荐切比雪夫距离max(x差绝对值, y差绝对值)棋盘式八方向移动速度快但略粗糙这个项目允许八方向移动也就是上下左右再加四个对角方向所以我选欧几里得距离。它的值恰好是直线穿过栅格的真实最短距离作为h既满足可采纳性又足够紧搜索效率高。一个小问题是计算平方根会比加减法慢一些但在地图规模不大的情况下影响可以忽略。2.3 TOA定位从几个“到达时间”算坐标TOA定位的原理用一句话说就是测量信号从目标到每个基站的单程传播时间乘以光速得到目标到基站的距离然后根据多个基站的距离信息解算目标坐标。理想情况下已知三个基站坐标和三个距离分别以基站为圆心、距离为半径画圆三个圆会交于一点这个点就是目标位置。但现实里测距必然有噪声圆不会精确相交于一点。业界标准的做法是用最小二乘。我把推导过程快速过一遍方便你理解代码。假设目标真实位置是(x, y)第i个基站坐标是(xi, yi)测距为di那么di² (x - xi)² (y - yi)²。展开后以第一个基站为参考把其他方程减去第一个方程可以消掉x² y²项得到形如A·[x; y] b的线性方程组。由于方程数量大于未知数用最小二乘求解即可对应MATLAB里一行pinv(A)*b就出来结果。这种解法不需要迭代计算量极小而且基站数量多于3个时只是把A和b矩阵的行数变多代码不用改。2.4 定位结果怎样安全地交给路径规划定位模块输出的坐标不能直接当成A*起点用至少要做三个处理。第一是边界检查坐标是否落在图幅范围内越界时直接钳制到最近的合法栅格第二是障碍物检查如果坐标落入障碍物格子我会在附近扩展搜索最近的空闲栅格作为起点第三是精度提示如果定位误差已经大于半个栅格宽度我会把定位模块和规划模块的画面上同时标出误差椭圆提醒使用者这个算路结果只能作为参考。实际系统里还会把定位结果送进卡尔曼滤波或者粒子滤波做平滑进一步降低对规划起点的扰动不过那属于扩展方向不是本实例的核心。3. MATLAB完整实现从栅格地图到可视化3.1 地图与障碍物生成我用一个二维矩阵map来表示栅格地图0表示可通行1表示障碍物。地图尺寸设成20乘20障碍物可以手动布置也可以随机生成。随机生成时我会控制障碍物比例在15%到20%之间既能保证路径有避障的意义又不至于让可行通道消失。为保证有解我还会在生成后从起点到终点做一次简单的连通性预检比如用A*先跑一遍如果搜不到路就重新撒障碍物。显示地图我推荐用imagesc加灰度配色。这里有个容易出错的地方imagesc默认会把矩阵第一行画在图形窗口顶部如果不加axis xyy轴是反的后面叠加路径会看起来上下颠倒。所以绘制地图时一定要加axis xy让行序号沿y轴向上增长才符合我们习惯的平面坐标系视角。% 构建20x20栅格地图0为空闲1为障碍 mapSize 20; map zeros(mapSize, mapSize); rng(42); obsRatio 0.18; numObs round(mapSize^2 * obsRatio); % 随机撒点式障碍 for k 1:numObs r randi(mapSize); c randi(mapSize); % 起点和终点附近留出通路 if abs(r - 2) 2 || abs(c - 2) 2 map(r, c) 1; end end % 显示地图 figure; imagesc(map); colormap(gray); axis xy; axis equal; grid on; title(二维栅格地图);注意我这里障碍物不是连通的大块而是随机点覆盖适合快速演示。真实场景建议用连通区域生成障碍物走廊或者直接读入CAD导出的栅格位图逻辑是一样的只是输入源不同。3.2 A*核心代码实现细节A*代码的关键点有三个open集合用什么数据结构、邻居怎么生成、路径怎么回溯。我这里open集合用containers.Map管理键是“行_列”字符串值是当前节点坐标gScore矩阵记录起点到每个节点的实际代价parent矩阵记录父节点。每次从open集合里取f值最小的节点时直接遍历所有键找最小值这在栅格图几百个节点的规模下完全够用但如果你把地图做到上千乘上千就该换二叉堆实现了。邻居生成我采用八方向扩展但额外做了一步碰撞细节处理对角移动时如果当前节点的上邻居或者左邻居恰好是障碍物我禁止这次对角扩展。举个例子向右上移动前如果正上方或者正右方是墙斜着穿过去就会贴着墙角走实际机器人可能卡住或蹭墙。这个细节很多教程里都没有我建议直接加到你的代码里能避免掉大部分路径质量问题和碰撞隐患。% 八方向扩展行增量、列增量、移动代价 dirs [-1 0; 1 0; 0 -1; 0 1; -1 -1; -1 1; 1 -1; 1 1]; cost [1; 1; 1; 1; sqrt(2); sqrt(2); sqrt(2); sqrt(2)]; while openSet.Count 0 % 取f值最小的节点f g h ks openSet.keys; minF Inf; for i 1:length(ks) nd openSet(ks{i}); fval gScore(nd(1), nd(2)) estH(nd, goal); if fval minF minF fval; curKey ks{i}; current nd; end end remove(openSet, curKey); closed(current(1), current(2)) true; if isequal(current, goal) path backtrace(parent, start, goal); break; end for i 1:8 nr current(1) dirs(i,1); nc current(2) dirs(i,2); if nr 1 || nr mapSize || nc 1 || nc mapSize continue; end if map(nr, nc) ~ 0 continue; end % 对角移动的墙角检测 if i 4 if map(current(1) dirs(i,1), current(2)) ~ 0 || ... map(current(1), current(2) dirs(i,2)) ~ 0 continue; end end tentG gScore(current(1), current(2)) cost(i); if closed(nr, nc) tentG gScore(nr, nc) continue; end if tentG gScore(nr, nc) gScore(nr, nc) tentG; parent(nr, nc, 1) current(1); parent(nr, nc, 2) current(2); key sprintf(%d_%d, nr, nc); openSet(key) [nr, nc]; end end end这段代码的核心是f值最小的节点选择和邻居代价更新。如果只是想快速验证思路把地图缩小到15乘15肉眼能明显看到搜索过程效果更直观想体现算法性能就默认跑20乘20记录扩展节点数展示启发式搜索相对Dijkstra的优势。3.3 TOA定位模块与最小二乘TOA模块需要三个基站坐标和运动体真实坐标。仿真中我会让真实坐标随机生成保证落在地图内部。然后计算真实距离再叠加高斯噪声作为观测距离。噪声标准差我一般设在0.3米左右这相当于UWB测距比较典型的误差水平。之后按最小二乘公式解算估计坐标。% 基站布设三个基站尽量拉开避免共线 bs [3, 3; 17, 4; 10, 17]; truePos [8.5, 12.0]; noiseStd 0.3; % 模拟TOA测距真实距离 高斯噪声 distTrue sqrt(sum((bs - truePos).^2, 2)); distMeas distTrue noiseStd * randn(size(bs,1), 1); % 最小二乘定位 ref bs(1, :); A -2 * (bs(2:end, :) - ref); b distMeas(2:end).^2 - distMeas(1)^2 ... - sum(bs(2:end,:).^2, 2) sum(ref.^2); estPos pinv(A) * b;我解释一下这段代码里的关键一步为什么这么写。以第一个基站为参考对第i个基站展开测距方程并减去参考方程消掉x²y²项后矩阵A的第i-1行就是-2*(xi-x1, yi-y1)向量b的第i-1项就是di²-d1²-xi²-yi²x1²y1²。这跟前面的推导严格对应。只要基站不共线A的列秩为2最小二乘就有唯一解如果基站近似共线解的数值稳定性会变差这就是所谓的GDOP问题后面我会讲到怎么避免。需要说明的是这里TOA模型是理想化的视线传播模型没有考虑多径和非视距误差。如果引入NLOS误差最小二乘估计会出现明显偏置业内一般会加残差加权或鲁棒估计来处理。仿真阶段用高斯噪声先把链路跑通再逐步加入复杂误差是性价比最高的学习路径。3.4 主流程串联与参数设置主脚本按模块依次调用整个流程非常直白。先用固定随机种子生成地图和真实坐标然后调用TOA定位得到估计坐标做边界检查和障碍物修正接着把修正后的栅格坐标和用户设定的目标点送进A*最后调用可视化脚本并把性能指标打印到命令行。参数设置里有两个地方值得关注。一是噪声标准差我建议做成脚本顶部的全局变量方便反复实验时调整二是起点终点的确定我会让终点固定在地图空旷区域比如坐标(18,18)避免出现“终点本身在障碍物里”的尴尬情况。如果你要做蒙特卡洛实验可以把定位和规划部分包进循环统计不同噪声水平下的平均路径长度和定位误差代码结构基本不用动。% 主流程串联 map buildMap(20); % 地图 truePos [8.5, 12.0]; % 真实坐标 estPos toaLocalization(bs, truePos, noiseStd); % 定位 startIdx fix2Grid(estPos, mapSize); % 坐标转栅格 startIdx ensureFree(startIdx, map); % 起点非法时修正 goalIdx [18, 18]; path astar_plan(map, startIdx, goalIdx); stats evaluatePath(path, map, estPos, truePos); visualizeAll(map, path, bs, estPos, truePos);我把每个功能函数拆到单独文件里主脚本只负责串联和参数设置。这样你跑实验时不用改函数内部逻辑只需要调整参数远离“改一处崩一路”的绝望体验。3.5 可视化地图、路径、定位结果一屏展示可视化是这个项目最出效果的部分也是一堆人栽跟头的地方。我用的思路是把所有内容叠到同一张图上底层是栅格地图中间层是路径曲线和基站点顶层是定位误差线。因为坐标系做了axis xy处理所以绘制路径时直接用plot(path(:,2), path(:,1))列号对应x坐标、行号对应y坐标颜色用红色加粗实线起点用绿色圆圈终点用红色叉号。基站点用蓝色三角定位结果用品红色星号真实坐标用黑色圆圈两者之间连一条虚线表示当前定位误差。如果误差过大我会加画一个以定位结果为中心、半径为noiseStd乘放大系数的圆直观展示不确定区域。实际测试下来这张图给评委或者导师看时特别直观路径走向、避障效果、定位偏差一目了然。还有个小技巧想在静态图上展示A*搜索过程可以在扩展每个节点后加一次drawnow并且暂停0.05秒就能看到搜索波前从起点向目标蔓延的动画。对于时间充裕的展示场合这很加分但不建议常用因为会让整体运行时间从毫秒级变成几十秒。3.6 性能评估指标与结果解读性能评估不只是把图画出来更重要的是量化。我习惯统计几项指标路径物理长度、规划耗时、A扩展节点数、路径转折次数、定位误差RMSE。路径长度用相邻路径点的欧氏距离累加规划耗时直接用tic和toc夹住A函数调用RMSE则跑多次蒙特卡洛取平均单次实验的误差只能算展示不能算评估。指标计算方式我的典型结果路径长度相邻路径点欧氏距离求和21.6米规划耗时tic/toc计时A*主循环约20毫秒扩展节点数closed矩阵累加值143路径转折次数方向向量变化点计数6次定位误差RMSEsqrt(mean((est-true).^2))20次平均0.18米读这些数据时要明白每个指标的短板。路径长度只反映几何长度不反映路径可执行性规划耗时会受机器性能影响但同机器上的相对比较仍有意义扩展节点数才是评价启发函数质量的更稳定指标。我在写报告或博客时会把五次实验的参数和结果并列展示让读者一眼看出来噪声增大后定位误差变大进而导致起点偏移但A*搜索效率和路径长度基本不变这正好验证了定位模块和规划模块解耦的合理性。4. 实测定会遇到的那几个坑4.1 路径斜穿障碍角点这是A*八方向扩展最容易出的问题。路径明明看起来在栅格图上是斜线但从几何上看它从某个障碍物的角点擦过去实际机器人稍微有点体积就蹭上了。我在前文已经给出了处理办法对角移动前检查相邻的正交邻居是否障碍。这个细节代码只有三行却直接决定路径是否真的“无碰撞”。如果你发现自己的路径频繁贴墙角走先检查这一段代码而不是急着调启发函数。4.2 定位误差导致起点落在障碍物里TOA定位不是理想精度的噪声稍大估计坐标就可能在障碍物栅格上。直接把这样的起点交给A*会导致搜索一开始就认为起点在closed集合里路径规划直接失败。我的兜底策略是以非法起点为中心从半径1开始向外螺旋搜索找到第一个空闲栅格作为新起点并且在可视化时用横线标明起点修正偏移。这个机制在蒙特卡洛实验里极其重要它保证了整条评估链路不会因为单次定位误差而崩溃也让你能公平比较不同噪声水平下的规划效果。4.3 大栅格地图下A*变慢当我把地图从20乘20换到200乘200时用containers.Map加线性遍历open集合的做法立刻变得吃力单次规划耗时会从毫秒级涨到秒级。这里有两个优化方向。第一是用MATLAB实现二叉堆优先队列把取最小f值的操作从O(n)降到O(logn)第二是减少平方根计算可以只在h计算时用欧几里得距离对角移动代价直接用预计算常数。如果你不想动数据结构还有一个取巧办法把搜索限定在起点到终点连线附近的带状区域跳过无关区域工程上叫窗口化或聚焦搜索代价是可能丢失全局最优适合对实时性要求高的场景。4.4 基站共线导致TOA定位漂移最小二乘解算要求方程有效如果三个基站都排在一条直线附近矩阵A会接近奇异定位结果会变得非常敏感微小的测距噪声都会被放大成明显的坐标漂移。我在设计基站布局时会让三个基站呈现三角形分布尽量保证任意两点之间的连线方向有明显差异。测试时你可以故意把三个基站放在一条直线上观察定位误差从0.1米级别飙升到几米这就是坐标几何对精度影响的直观教材。4.5 坐标轴方向与行列混淆这个问题看起来低级但几乎每个第一次写栅格地图可视化的人都会遇到。问题本质是矩阵的行号对应y方向、列号对应x方向并且imagesc默认y轴向下。一次解决的办法是在可视化函数开头统一加axis xy然后约定一条规则——绘制任何点时都写plot(栅格列号, 栅格行号)绝不在绘制层直接使用世界坐标。如果坐标转换函数和绘图函数分离各模块测试通过后再拼接这类问题基本能在十分钟内定位。5. 后续扩展方向5.1 从静态规划到动态重规划这个实例里地图是静态的A规划完一次之后路径就固定了。但真实场景里障碍物可能会移动比如仓库里有人经过、无人机巡检时遇到临时障碍。这时候可以考虑在A规划出的全局路径上做局部避障或者换成D* Lite这类适合动态环境的增量重规划算法。整体模块不需要推翻地图模块、定位模块和可视化模块完全可以复用只需要替换规划器并加一个障碍物变化检测回调。5.2 从二维到三维把A扩展成三维A并不复杂状态空间从两个维度变成三维栅格邻居扩展从8方向变成26方向启发函数换成三维欧几里得距离路径长度计算加入高度维代价。代价是扩展节点数量暴增通常会用带体素滤波的低分辨率地图或者分层的三维规划来压缩搜索空间。你在做无人机或者水下机器人项目时可以从这个二维版本起步把单层地图叠加成高度层来做原地扩展。5.3 结合更多传感器与滤波TOA定位在障碍物遮挡环境下容易退化实际产品里很少只用单一测距手段。UWB常和IMU做松耦合或者紧耦合用扩展卡尔曼滤波或者因子图优化把短时推位和绝对测距融合起来。如果你在这个项目基础之上加一个简单的卡尔曼滤波模块输入是TOA定位坐标输出是平滑后的轨迹你会发现A的起点稳定性进一步提升定位误差曲线也会明显收敛。这一步不需要改A和可视化代码只是因为数据流中间多了个滤波节点工程上非常划算。做下来我最大的感受是定位和规划单独拎出来都不算难难的是把它们按真实系统的逻辑串起来。先定位、后规划的前提是数据接口设计得干净这一步做扎实了后面换算法、换传感器、换地图来源都很从容。这套代码我在不同噪声参数下反复跑了很多遍每次看到定位误差被可视化出来、路径在栅格地图上生成的过程还是觉得这个组合非常适合入门和演示用。如果你也在写类似的项目有几个小建议收尾时一并分享起步阶段把地图设小点就算A*算法写得糙也能秒出结果把随机种子固定住否则不同次运行的结果对不上排查问题会很难受每次跑新实验前先看一眼定位误差可视化再决定要不要把起点修正逻辑打开。这些习惯能帮你省下大量调试时间。
RELATED READING

延伸阅读

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