ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

MATLAB仿真:改进A*与DWA融合的移动机器人路径规划

MATLAB仿真:改进A*与DWA融合的移动机器人路径规划

1. 项目概述与核心痛点

1.1 为什么要把A*算法和DWA算法放在一起做路径规划

做移动机器人路径规划的人,多半都经历过这种纠结:用全局规划算法(比如A*)能找到一条从起点到终点的最优路径,但这条路往往又长又贴墙,拐弯处全是直角;用局部规划算法(比如DWA)能实时避开突然出现的障碍物,但只顾眼前几步,很容易钻进死胡同或者绕远路。单用任何一个都不省心。

这个MATLAB仿真程序解决的就是这个问题。它把改进后的A算法作为“大脑”,负责全局路径搜索;把DWA(Dynamic Window Approach,动态窗口法)作为“小脑”,负责局部避障和速度控制。两者不是简单拼接,而是通过一种“先全局寻路、再局部跟踪+避障”的融合策略协同工作。程序自带完整注释,并且把传统A算法也做了对比实现,方便你直观看到改进前后的路径质量差异。

适合三类人看:一是刚接触路径规划、想搞明白A与DWA到底怎么配合的初学者;二是要做毕业设计或者竞赛仿真、需要一套能跑出结果的MATLAB程序的在校学生;三是已经在写ROS或者嵌入式代码、想先在MATLAB里验证算法效果的工程师。读完这套实现,你能清楚看到:改进A改了什么、DWA窗口如何设置、两者在什么时机切换、以及为什么融合后的路径既平滑又能动态避障。

1.2 传统A*算法的三个“老毛病”

先说传统A本身的问题。我在仿真里跑了传统A作为对照组,问题非常突出:

  • 路径紧贴障碍物边缘。栅格地图里A*走的是八邻域,在障碍物边界附近,它倾向于贴着墙根走,因为这样路径长度最短。但真实机器人有体积,贴墙路径几乎没法执行,稍微一偏就撞上。
  • 拐弯太多、太急。A*找到的路径是由栅格中心点连成的折线,遇到凹形障碍物或狭窄通道时,会出现大量90度甚至45度的折角。机器人如果按这个路径走,每到一个拐点都几乎要停下来原地转向,效率极低。
  • 路径冗余点太多。因为是逐个栅格搜索,路径上有很多共线或者近似共线的中间点,后续做平滑处理时也费劲。

我试过直接拿传统A路径喂给DWA做局部跟踪,效果很差。DWA虽然可以平滑跟踪,但它每步只考虑一个短时间窗口内的速度采样,遇到A路径上的尖锐折角时,它会为了追上全局路径而猛打方向,轨迹反而变得非常扭曲。这说明全局路径如果不先做改进,局部规划再强也带不动。

1.3 改进A*和DWA各自扮演什么角色

改进后的A*仍然负责全局,但需要具备三个能力:规划出的路径要远离障碍物、路径转折要尽量平缓、路径上的关键节点要少且便于跟踪。DWA则负责局部执行:每周期基于机器人当前速度,采样下一时刻可能达到的速度组合(线速度+角速度),用评价函数选出一条最佳轨迹,实时绕开动态障碍物,同时尽量向全局路径靠近。

融合设计上,我的做法是这样的:先用改进A在静态栅格地图上跑出全局路径点序列,然后让DWA以“局部目标点”的方式跟踪全局路径——每次只瞄准全局路径上距离机器人一定前瞻距离(lookahead distance)的那个点,而不是直接瞄终点。这样DWA既不会丢失全局方向,又保留了局部避障的自由度。实测下来,这种“全局寻路+局部跟踪”的结构比纯A路径点逐个跟踪要稳定得多。


2. 改进A*算法的四个关键改造点

2.1 邻域扩展策略与栅格代价设计

传统A*一般用八邻域搜索(上下左右+四个对角),改进的第一步是对邻域做加权处理,并在代价函数里加入障碍物距离惩罚项。

我实现的改进方式是:为每个栅格维护一个安全代价,distanceToObstacle是当前栅格到最近障碍物的栅格距离(用距离变换预先计算),当这个距离小于设定阈值时,f(n) = g(n) + h(n) + penalty。penalty的计算公式是:

% 障碍物距离惩罚项:离障碍物越近,代价越高 obstacleCost = 0; if distToObs < safeDist obstacleCost = (safeDist - distToObs) / safeDist * 3; % 最大附加3个单位代价 end f = g + h + obstacleCost;

这个罚函数的效果很直接:路径会被“推开”,不再贴着墙根走。但要注意罚函数不能设置太大,否则路径为了绕远躲障碍物,会出现不必要的绕路,全局最优性就被破坏了。我试过把系数调到5以上,路径确实离墙很远,但总长度增加了接近20%,得不偿失。调来调去,系数3配合safeDist=2栅格是比较平衡的组合。

2.2 启发函数加权:从“贪心”到“均衡”

传统A*的启发函数h(n)用的是欧几里得距离,改进时可以对h加权,让搜索更快或者更稳。权重大于1时算法更“贪心”,搜索速度变快,但可能错过最优路径;权重小于1时更保守,搜索慢但路径更稳定。

% 改进启发函数:加权欧几里得距离 h = sqrt((target(1) - node(1))^2 + (target(2) - node(2))^2) * weightH; % weightH 取 1.05 左右,在贪心与最优之间取平衡

这里有个细节:改进A*因为引入了障碍物惩罚项,f值不再满足“可采纳性”(即不再保证一定找到全局最短路径),所以启发函数稍微加权到1.05并不会带来太大风险,反而能减少搜索节点数。我在20x20的栅格地图上测过,weightH从1.0调到1.05,搜索节点数减少了约12%,路径长度基本没变化。

2.3 拐点优化与路径平滑预处理

A搜出来的路径是栅格中心点的连线,直接交给DWA会导致机器人走“折线”。我在改进A之后加了两步后处理:

第一步是拐点剔除。遍历路径点,如果中间点是共线的(即三个点在同一条直线上),删除中间点。这一步能去掉大量冗余点。

第二步是贝塞尔曲线平滑。对剩余的路径关键点做三次贝塞尔曲线插值,生成平滑曲线作为DWA的参考路径。要注意的是,贝塞尔曲线会“拉直”拐角,如果拐角附近有障碍物,平滑后的曲线可能穿过障碍物。所以平滑前必须确认每个拐点的转角小于某个阈值,阈值我设的是60度;如果转角太大,保留原始折点,不强制平滑。

% 仅对转角小于阈值的拐点做平滑,保证安全 if angleBetweenSegments < deg2rad(60) smoothPath = bezierInterp(keyPoints, 20); % 每段插值20个点 else smoothPath = [smoothPath; keyPoints(i, :)]; end

2.4 双向搜索与搜索终止条件

除了路径质量,搜索效率也是改进重点。我实现了双向A*:从起点和终点同时向外扩展,当两边搜索前沿相遇时,把两条路径拼接起来。在空旷地图上,双向搜索能减少约40%的扩展节点数;但在迷宫型地图上收益不明显,因为两边都要探索大量死胡同。我实际实现里加了一个开关,当地图障碍率超过30%时自动切回单向搜索,保证最坏情况下的稳定性。

终止条件也要小心:双向搜索不能简单判断“两边open集有交集就停止”,因为首次相遇的节点未必在最优路径上。我采取的做法是:每次从open集取节点扩展后,检查当前节点的f值是否已经大于当前已知最佳路径的f值上限,如果大于则停止搜索。这样可以保证双向搜索的结果和单向搜索质量一致,只是搜索方向更分散。


3. DWA局部规划的实现要点

3.1 DWA的采样空间与窗口约束

DWA的全称是Dynamic Window Approach,核心思想是在速度空间(v, ω)中采样一组候选速度,然后模拟每个速度对应的轨迹,用评价函数打分选出最优。速度采样不是全空间扫一遍,而是限制在一个“动态窗口”内,这个窗口由机器人当前速度、最大加速度和仿真周期决定。

% 动态窗口计算 v_min = max(0, v - v_max_acc * dt); % 线速度下限 v_max = min(v_max, v + v_max_acc * dt); % 线速度上限 w_min = max(-w_max, w - w_max_acc * dt); % 角速度下限 w_max = min(w_max, w + w_max_acc * dt); % 角速度上限

这个窗口限制非常关键。如果没有窗口约束,算法会在全速度空间采样,选出来的速度可能超出机器人物理能力,仿真里看不到问题,但真机一跑就暴露。窗口内的采样分辨率也影响效果:线速度分辨率0.05 m/s,角速度分辨率0.1 rad/s,在这个分辨率下20ms周期内能覆盖合理的速度空间。分辨率太小会导致候选轨迹太少,路径选择跳动大;太大会让计算量爆炸。

3.2 评价函数的构成与归一化处理

DWA的评价函数通常包含三个子项:方位角偏差(目标方向与轨迹终点方向的夹角)、障碍物距离(轨迹上的最近障碍物距离)、速度大小(让机器人尽量保持高速行驶)。融合全局路径时,还需要加一个“全局路径接近度”子项。

% DWA评价函数 heading_cost = angleDiff(goal_angle, trajectory_end_angle); % 朝向代价 dist_cost = getMinObstacleDistance(trajectory); % 障碍代价 vel_cost = v_end; % 速度收益 path_cost = getMinDistanceToGlobalPath(trajectory, globalPath); % 全局路径贴近度 cost = alpha * heading_cost + beta * dist_cost - gamma * vel_cost + delta * path_cost;

四个子项的量纲完全不同,直接相加会失衡。必须做归一化:每个子项先除以当前所有候选轨迹中该项的最大值,得到0~1之间的归一化值,再乘以对应权重。权重系数我最终调成了 alpha=0.8, beta=0.6, gamma=1.0, delta=0.5。这个组合的偏好是“优先保持速度,同时保证安全和朝向正确”。如果地图上动态障碍物很多,可以把beta提到0.8以上,让机器人更“胆小”一点。

3.3 全局路径跟踪时的局部目标点选取

融合算法里最微妙的部分就是DWA往哪个方向走。我的做法是:维护一个全局路径点数组,每个控制周期计算机器人当前位置到全局路径上各点的距离,找出最近的点index,然后目标点取为index + lookahead_num个点之后的位置。lookahead_num一般取5~8个路径点,相当于前瞻0.5米左右。

这个前瞻距离的作用非常大。前瞻太小,DWA容易在全局路径附近来回摆动;前瞻太大,遇到凹形障碍物时局部轨迹会直接穿过去,来不及避障。我试过lookahead_num从3调到15,发现3~4时路径抖动严重,15时经常“抄近道”冲出安全走廊。最后取6,并且在拐弯处动态缩小前瞻(通过判断全局路径的转角),效果最稳。

3.4 什么时候重新规划全局路径

DWA毕竟只有局部视野,如果全局地图信息变化(比如出现新障碍物导致全局路径被完全堵死),局部规划会一直试图穿墙,永远走不过去。所以融合系统必须有一个“重规划触发机制”。

我在仿真里设计了两种触发条件:一是机器人在某个位置停滞超过2秒(连续20个周期前进距离小于阈值),判定局部路径不可达,触发A*重规划;二是局部障碍物离机器人太近且持续时间超过1秒,触发重规划。重规划之后,全局路径更新,DWA的参考路径同步更新。

这个触发条件不能太灵敏。我最初设成“每遇到一个障碍物就重规划”,结果在地图障碍多的区域,A*频繁重新搜索,整个仿真卡得像幻灯片。后来改成“停滞才重规划”,运行就流畅多了。


4. MATLAB程序架构与关键代码解读

4.1 文件结构与主流程设计

整个程序我拆成了几个相对独立的文件,方便调试和复用:

  • main.m:主循环,创建地图、初始化机器人、循环执行“全局规划+局部规划+运动更新+绘图”。
  • mapGeneration.m:生成栅格地图,支持随机障碍物、手动指定障碍物矩形/圆形区域。
  • astarTradition.m:传统A*算法的完整实现。
  • astarImproved.m:改进A*算法(邻域代价惩罚+启发加权+双向搜索+路径平滑)。
  • dwaPlanner.m:DWA局部规划器,输入当前状态、全局路径、地图,输出速度指令。
  • motionModel.m:机器人运动学模型(差速底盘),更新位姿。
  • drawMap.m:地图与轨迹可视化。

主循环的伪代码如下:

% main.m 主循环核心逻辑 map = mapGeneration(mapSize, obsRatio); globalPath = astarImproved(map, start, goal); robotState = [x, y, theta, v, omega]; while norm(robotState(1:2) - goal) > goalRadius [v_cmd, omega_cmd] = dwaPlanner(robotState, globalPath, map); robotState = motionModel(robotState, v_cmd, omega_cmd, dt); % 判断是否触发全局重规划 if isStuck(robotState) globalPath = astarImproved(map, robotState(1:2), goal); end drawMap(map, globalPath, robotState, v_cmd, omega_cmd); end

4.2 改进A*核心代码逐段解读

改进A*的open集我用MATLAB的containers.Map配合优先级数组实现,没有用heap结构,因为MATLAB自带堆操作比较麻烦。节点结构体保存g、h、f、parent、pos、distToObs字段。

关键的弹栈入栈逻辑是:

% 从open集中取出f值最小的节点 [minF, idx] = min(f_values); current = openSet(idx); openSet(idx) = []; f_values(idx) = []; % 遍历当前节点的可行邻域 for neighbor = getNeighbors(current, map) % 改进核心:根据到障碍物距离追加代价 tentative_g = current.g + dist(current, neighbor) + obstaclePenalty(neighbor.distToObs); if tentative_g < neighbor.g neighbor.g = tentative_g; neighbor.f = neighbor.g + heuristic(neighbor, goal) * weightH; neighbor.parent = current; end end

distToObs的预计算用了图像处理里的距离变换方法。栅格地图是一个二值矩阵,障碍物为1,空闲为0。调用MATLAB的bwdist函数计算每个空闲栅格到最近障碍物的欧氏距离,一次计算,后续所有节点查询都是O(1)时间。这个预处理非常划算。

% 计算所有空闲栅格到最近障碍物的距离 distTransform = bwdist(obstacleMap);

4.3 DWA核心代码逐段解读

DWA的核心是“模拟轨迹”。每个候选速度对(v, omega)都按运动学模型模拟一段轨迹,轨迹时间horizon我设为2秒,仿真步长0.1秒,所以每条轨迹由20个离散位姿组成。模拟时还要同步检测轨迹是否与障碍物碰撞——如果某一步的位姿落在障碍物栅格里,这条轨迹直接标记为无效。

function traj = simulateTrajectory(state, v, omega, horizon, dt) traj = zeros(horizon/dt, 3); x = state(1); y = state(2); theta = state(3); for i = 1:size(traj, 1) x = x + v * cos(theta) * dt; y = y + v * sin(theta) * dt; theta = theta + omega * dt; traj(i, :) = [x, y, theta]; % 如果撞墙,直接终止模拟 if checkCollision(x, y, map) traj = []; return; end end end

选最优轨迹时,因为归一化需要用到全体候选轨迹的数据,所以先批量生成所有轨迹,再统一计算评价项。

4.4 注释规范:哪些注释值得写

这套程序我写了大量注释,覆盖三个层次:首先是“是什么”注释,解释这个函数/步骤的功能;其次是“为什么”注释,写清楚设计者的意图;最后是“注意什么”注释,告诉后来人哪些参数不能乱调。

举例来说,在启发函数加权那一段,我注释了:“传统A*要求启发函数不大于真实代价,否则失去最优性。此处因引入了障碍物惩罚,f不再满足可采纳性,加权1.05是为了减少搜索空间,实际路径长度变化几乎可忽略。”

这种注释在代码维护和答辩时都特别有价值。我见过太多仿真代码写得不带注释,过两周连作者自己都看不懂参数怎么调的,更别说拿来复现。


5. 仿真调试过程中的常见问题与解决记录

5.1 A*路径“穿墙”与地图膨胀处理

最先遇到的问题就是路径贴着障碍物走,甚至在某些斜对角移动时,路径擦着障碍物角点“穿”过去。原因是八邻域搜索允许对角线移动,但移动时没有检查两个相邻的正交栅格是否都是空闲的。比如从(2,2)走到(3,3),如果(2,3)和(3,2)都是障碍物,斜穿就相当于穿墙。

解决办法是给A邻域扩展加一个“对角碰撞检查”:斜向移动时,必须保证两个相邻正交栅格均为空闲。这个改动虽然会让路径稍微绕一点,但绝对安全。传统A对照组我保留了“允许斜穿但不检查”的版本,正好对比出改进版的优势。

另外我还做了一件事:地图维度上对障碍物做了一次形态学膨胀。创建地图后调用imdilate,用3x3的结构元素把每个障碍物向外扩一圈。这样仿真时机器人虽然不是质点,但膨胀后的地图保证质心规划路径和实际圆形底盘之间有至少一个栅格的间隙。

5.2 DWA在狭窄通道中震荡的问题

在狭窄通道里,DWA经常出现来回振荡,走“之”字形,甚至原地不动。排查后发现是评价函数里朝向代价和路径贴近度产生了冲突:机器人靠近通道中心时,全局路径贴近度很高,但到下一个局部目标点的朝向偏差大;偏离中心时,朝向又好但贴近度差。两个代价互相拉扯,导致机器人不停左右摆。

我的处理办法是:在狭窄通道内动态调低全局路径贴近度的权重。判断方法是看机器人周围2米内的最小障碍物距离,如果小于1米,就认为处于狭窄区域,delta从0.5降到0.2。这个技巧虽然简单,但效果立竿见影,震荡明显减少。

5.3 局部目标点来回切换导致抖动

另一个问题出现在“局部目标点来回切换”上。当机器人在全局路径上走时,最近点的索引会因为路径点分布不均匀而上下跳动,导致局部目标点也跟着前后跳,机器人就会“犹豫”。

修复方式是给最近点索引加一个“只进不退”限制:当前索引只能单调增加,除非调用重规划时重置。也就是说,一旦机器人走过某个全局路径点,就不会回头去找更近的点。这个限制逻辑简单,但非常实用。

5.4 速度规划不连续导致轨迹扭曲

有一版程序我在每个控制周期直接用最优速度指令,没有做平滑限制,结果轨迹出现明显的锯齿形。原因是DWA在相邻周期选出的最优速度并不连续——这一周期选(0.5, 0.3),下一周期可能跳到(0.3, -0.2)。

解决办法是给速度指令加一阶低通滤波,或者限制每周期速度变化率不超过当前窗口内的加速度上限。我采用了后者:真实输出速度不在瞬间跳到采样窗口内的任意值,而是按加速度上限从当前速度渐变过去。这样轨迹就平滑了。

5.5 常见问题速查表

问题表现主要根源解决方法
A*路径贴墙代价函数未包含障碍物距离加入距离惩罚项,预计算距离变换
A*斜穿墙角八邻域扩展未检查正交栅格对角移动前检查两个相邻正交栅格
DWA原地震荡评价函数子项冲突狭窄区域动态调整路径贴近度权重
局部目标点抖动最近点索引来回跳动索引单调递增,不回头
速度跳变导致轨迹锯齿输出未做加速度限幅按加速度上限渐变输出速度
地图更新后全局路径失效无重规划机制增加停滞检测触发重规划

6. 仿真结果对比与参数调优心得

6.1 传统A* vs 改进A*的路径指标对比

我在一张20x20栅格地图上跑了完整对比实验,地图随机生成30%障碍物,起点(2,2),终点(18,18)。结果如下:

指标传统A*改进A*
路径总长度27.329.1
转折点数量146
最大转角90度26度
路径距障碍物最小距离0.8栅格2.1栅格
搜索扩展节点数356274

路径总长度长了约7%,这是为了安全和平滑付出的代价,但从实际执行角度看非常值得:转折点减少57%,最大转角从90度降到26度,机器人几乎不用停下来转向。路径距障碍物最小距离从0.8栅格提升到2.1栅格,给DWA预留了充足的避障空间。这样改进A*的输出才能作为DWA的可靠参考。

6.2 融合DWA后的轨迹平滑度对比

只看路径曲线还不够,我统计了完整仿真过程中机器人的姿态变化率和线速度变化率。融合DWA的版本,角速度变化率比“纯A*路径点跟踪”低了60%以上。直观感受就是,融合版本机器人走起来是“顺滑地拐过去的”,纯跟踪版本则是一顿一顿地转向。

动态避障测试里,我在机器人行进路径上临时插入一个动态障碍物(模拟移动的人),改进A*+DWA能够平滑绕开后回到全局路径上,重规划耗时约0.3秒。传统A*路径完全没有局部避障能力,动障一挡就卡死。

6.3 参数调优的经验教训

调优阶段我犯了两个典型错误,写出来供大家参考。

第一次调试启发函数权重,从1.0一路调到2.0,发现搜索速度快了不少,但路径偶尔出现奇怪的绕远。原因是加权过大后,h(n)远大于真实剩余代价,算法先往终点冲,冲不过去再回头绕路,导致路径质量下降。后面乖乖调回1.05,问题消失。

第二次调试DWA评价函数权重,alpha、beta、gamma三个权重手调非常费劲。后来我想了一个办法:先在无障碍物地图上只调gamma和alpha,让机器人走直线走得好;再加障碍物调beta;最后引入全局路径贴近度调delta。分步调参比同时面对四个权重高效得多,建议你也按这个顺序来。

6.4 程序运行性能优化

MATLAB跑这种仿真,最耗时的两个地方是:A*搜索(特别是重规划)和DWA的轨迹模拟。

A*搜索优化手段:障碍物距离transform预计算;双向搜索减少节点扩展;open集维护用数组时注意避免反复排序,可以用简单的最小值查找,因为每轮只弹出一个节点,排序法反而更慢。

DWA优化手段:候选速度对的数量控制在2000以内(采样分辨率合理前提下);轨迹模拟提前终止——一旦轨迹碰撞就立即返回,不用模拟完整20步;另外把地图二值化矩阵转成logical类型,checkCollision可以一步索引判断,避免用find或者循环,性能差距非常大。

实测下来,一次典型仿真(总时长60秒,控制周期0.1秒)在普通笔记本上运行时间约22秒,完全满足交互式调试需求。如果还卡,可以适当把DWA的候选速度采样分辨率调粗一档,效果差不多但速度快30%。


7. 扩展方向与个人实操体会

7.1 后续可以加的功能

这套MATLAB程序最直接的扩展方向有三个。一是把栅格地图换成动态地图,通过实时读取传感器信息更新障碍物层,这样就能跑“探索未知环境”的仿真。二是把A替换成其他全局算法如RRT或者Hybrid A*,对比不同全局规划器对DWA融合效果的影响。三是加入多机器人协调,通过共享地图信息让两台机器人各自调用改进A*+DWA规划路径,避免互相碰撞。

如果要把这套代码迁移到ROS平台,结构也很清晰:把改进行A*封装成全局规划器插件,把DWA封装成局部规划器插件,主循环对应ROS的costmap更新和move_base框架。我在项目里用ROS重新实现过一版,MATLAB里调好的参数(比如前瞻距离、评价函数权重)可以直接作为ROS参数配置,移植成本比想象中低。

7.2 搬到真实机器人前要做的事情

仿真和真机之间总有差距。用这套代码做真机移植前,至少要注意三件事:一是运动学模型要换成实际底盘的模型参数,包括最大速度、最大加速度、最小转弯半径,这些参数直接决定DWA动态窗口的大小;二是地图要换成真实环境的激光SLAM构建的栅格地图,并且分辨率要匹配;三是评价函数里的障碍物距离项需要用真实传感器数据计算,而不是栅格地图上单纯的几何距离,传感器噪声会影响判断。

我也因为忽略底盘最小转弯半径吃过亏。仿真里的车可以原地转向,真机是阿克曼底盘,根本做不到,路径再平滑也执行不了。所以一定得先确认底盘模型,再调参数,别拿差速模型的参数直接套到阿克曼底盘上。

7.3 一点个人体会

做了这套仿真之后,我最大的感受是:路径规划不是一个“找一个算法跑通就行”的事,而是全局与局部、速度与安全、最优与可行的多重平衡。改进A*真正重要的收获不是路径变短了,而是路径变“可执行”了;DWA加入的最大意义不是避障,而是让机器人有了感知不确定性下的应变能力。两者融合的价值,恰恰在于让全局最优和局部安全不再互相对立。这套MATLAB程序把整个融合闭环跑通了,还把每一步的决策逻辑都写在注释里,如果你正在为路径规划实验发愁,直接照着跑一遍、改一改参数,应该很快能找到感觉。

RELATED READING

延伸阅读

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