☰
A*全局路径规划与DWA动态避障:轮式机器人路径规划源码解析
2026/10/31 5:59:52 网站建设 项目流程

简介:一套用Python实现的轮式机器人路径规划源码,融合A星全局路径规划与DWA动态窗口法,解决静态全局寻路与运动过程中局部避障相结合的问题。面向机器人、自动驾驶方向学习者及开发者,适合在ROS/Gazebo仿真环境中完成导航功能验证、开展课程设计与毕业设计参考。压缩包共50个文件,容量148KB,主要包含13个Python脚本、5个launch启动配置、3个rviz可视化配置、2个yaml参数文件与2个xacro模型描述,另有gazebo仿真世界、world地图、sdf模型和使用说明文档,目录结构清晰,便于按模块查阅。目前已有281人学习下载。全部源码经本地编译运行通过,评审达到95分以上。主程序基于A星算法规划两点间路径,dwa模块在基础路径上增加动态窗口避障,两个算法独立封装;通过鼠标左键设起点、右键设终点、中键添加障碍物、空格键启动规划,即可直观观察路径规划与实时避障的完整决策流程。

1. 轮式机器人路径规划:一份把 A* 静态寻路和 DWA 动态避障串起来的 Python 源码

做轮式机器人路径规划的人,基本都会遇到同一个问题:全局规划算出来的路径,小车在实际跑的时候往往跟不住,因为地图里的静态障碍和路上的动态障碍是两回事。这份资源有意思的地方在于,它没有把 A* 和 DWA 分开讲,而是直接把两个算法接在了一起——A* 负责在栅格地图上算出一条全局路径,DWA 负责让小车沿着这条路径跑起来的同时,实时绕开突然出现的障碍物。也就是说,你不用自己写接口、不用自己拼坐标系,下载下来跑通 main.py 就能看到完整的规划效果。适合正在做 AGV 路径规划、ROS 导航入门或者毕设里需要动态避障 demo 的人,也适合想搞清楚「全局路径和局部轨迹到底怎么衔接」的算法学习者。下面我把整个包拆开讲,从算法原理到鼠标交互,再到 Gazebo 仿真,最后是调参和踩坑。

2. A* 与 DWA 的算法链路:全局路径如何变成小车能跟的局部轨迹

2.1 A* 在栅格地图上的寻路逻辑:启发函数与代价优先级

A* 的核心是在栅格地图上维护两个代价:从起点到当前节点已经花掉的实际代价 g(n),以及从当前节点到终点估算的启发代价 h(n),总代价 f(n) = g(n) + h(n)。每次从 open list 里取 f 值最小的节点扩展,直到把终点加入 close list。这个包里的 AStarPlanner.py 用的是最简单的四邻域或八邻域搜索,具体实现看地图的分辨率参数,默认在二维栅格上做。

h(n) 的选择决定了 A* 的行为倾向。用曼哈顿距离时,路径会偏向先走横再走竖,转折比较生硬;用欧氏距离时,路径会更贴合两点之间的直线。实际测试这个项目时,我习惯把启发函数切成欧氏距离,因为轮式机器人(尤其是差速底盘)不喜欢 90 度直角转弯,欧氏启发出来的路径折角更小,后续 DWA 跟踪时的角速度压力也小很多。

# AStarPlanner.py 中启发函数的核心逻辑(简化) def calc_heuristic(n1, n2): # 欧氏距离启发,适合轮式机器人的运动特性 return math.hypot(n1.x - n2.x, n1.y - n2.y) def planning(self, sx, sy, gx, gy): # open list 用优先队列,按 f = g + h 排序 while open_list: node = heapq.heappop(open_list) # 到达终点则回溯路径 if node.x == gx and node.y == gy: return self.extract_path(node) for dx, dy in self.motion: # 8 邻域搜索,代价按运动方向区分 cost = self.calc_cost(dx, dy) self.update_node(node, dx, dy, cost)

这里的 motion 数组一般包含八个方向,但斜向运动的代价如果和直行一样,A* 会倾向于疯狂走斜线,导致路径贴墙。我的习惯是把斜向代价乘上 1.414,这样规划出来的路径更符合车体实际运动。

A* 跑完之后输出的是一串栅格坐标点。注意这些点并不是给小车直接跟踪的轨迹,因为栅格路径存在折角,直接下发到控制层会让小车在每一个拐点停下来转向。所以这个包的处理方式是:把 A* 路径上的点作为 DWA 局部规划的目标点序列,DWA 在每个控制周期只跟踪当前最近的几个点。

2.2 DWA 动态窗口的核心:速度空间采样与轨迹评价

DWA 的思路一句话就能说清:在速度空间 (v, ω) 里采样一组可行速度,对每个速度前向模拟一小段时间的轨迹,然后用评价函数打分,选得分最高的速度作为当前控制指令。「动态窗口」四个字指的是:不是所有理论上可达到的速度都能选,还要受当前电机加速度限制,下一时刻能切换到的速度必须落在当前速度附近的一个窗口内。

# dwa.py 中动态窗口的计算逻辑(关键参数) def calc_dynamic_window(v, config): # Vs: 机器人硬约束速度边界 Vs = [config.min_speed, config.max_speed, -config.max_yaw_rate, config.max_yaw_rate] # Vd: 受当前速度和加速度限制的动态窗口 Vd = [v[0] - config.max_accel * config.dt, v[0] + config.max_accel * config.dt, v[1] - config.max_dyaw_rate * config.dt, v[1] + config.max_dyaw_rate * config.dt] # 取交集得到真正的动态窗口 dw = [max(Vs[0], Vd[0]), min(Vs[1], Vd[1]), max(Vs[2], Vd[2]), min(Vs[3], Vd[3])] return dw

这段代码里的 max_accel 和 max_dyaw_rate 是两个最影响实际效果的量。如果设得太大,窗口很宽,小车看起来会「很贼」,稍微有点空间就猛打方向;设得太小,窗口变窄,小车反应迟钝,遇到突然出现的障碍物容易刹不住。我一般先把 max_accel 调到 0.5 左右,再根据仿真效果逐步加,直到车在转弯时不明显侧滑为止。

前向模拟的时间 predict_time 也很关键。模拟时间越长,DWA 越能「看」得更远,但计算量线性增长;模拟时间太短,小车像近视眼,走到障碍物跟前才发现。这个包默认值通常设在 2~3 秒,模拟步长 dt 取 0.1 秒,也就是每个控制周期要模拟 20~30 个轨迹点。

2.3 A* 和 DWA 怎么衔接:目标点切换与避障优先级

两套算法真正衔接起来,靠的是 Vplanner.py 里的统一接口。Vplanner 先调用 AStarPlanner 算出全局路径,把路径转换成 DWA 能消费的局部目标点列表,然后进入主循环:每次 DWA 算出当前最优速度后,判断机器人距离当前目标点是否小于某个阈值,如果是,就切换到路径上的下一个点。

# Vplanner.py 中全局路径与局部避障的结合逻辑(简化) while not goal_reached: # 取全局路径上当前段的目标点 target = get_current_target(global_path, robot_pos, target_idx) # DWA 根据目标点做局部规划,返回速度指令 u, trajectory = dwa.plan(robot_pos, target, obstacles) # 判断是否切换下一个目标点 if dist(robot_pos, target) < config.goal_radius: target_idx += 1 # 执行速度指令 robot.step(u)

这个切换半径 goal_radius 需要和 DWA 的障碍物膨胀半径配合考虑。如果 goal_radius 设得比障碍物膨胀半径还大,小车可能在一个障碍物还没完全让开时就切换目标,导致绕行到一半突然转向下一个点,路径变得很别扭。建议 goal_radius 取膨胀半径的 1.5~2 倍。

避障优先级的问题是这样的:DWA 的评价函数里有三个项——方位角代价(朝向目标)、 clearance 代价(离障碍物距离)和速度代价(鼓励快跑)。三个权重如果平衡不好,小车要么贴墙走(方位角权重过大),要么原地打转(clearance 权重过大),要么直愣愣撞上去(速度权重过大)。这个包里默认权重是 0.05、0.1、0.2 这个量级,我的习惯是先固定 clearance 权重,再去调方位角,把「能绕开但不绕远路」作为验收标准。

3. 跑通仿真并调参:鼠标交互、速度空间与实际参数标定

3.1 环境准备与运行方式:Python 版本和依赖库

先说环境。这个包是基于 Python 写的,主要依赖 numpy 和 matplotlib,没有额外装 ROS 的时候也能跑纯 Python 仿真。建议用 Python 3.8 或 3.9,我在 3.10 上也跑过,但 matplotlib 的版本如果太新,个别绘图接口会有兼容性问题。装依赖就一条命令:

pip install numpy matplotlib

如果你之前装过其他版本的 numpy,注意一下版本冲突。这个项目的栅格地图计算密集,numpy 版本低于 1.20 时广播机制在某些边界条件下会报错,我遇到过一次,升级 numpy 后就好了。装完依赖后,在项目根目录直接运行:

python main.py

main.py 是纯 Python 仿真入口,会弹出一个 matplotlib 窗口,窗口里是一个栅格地图。如果看到窗口弹出但地图空白,检查一下是不是用了虚拟环境,以及 matplotlib 后端是不是 TkAgg。Linux 服务器上常见这个问题,改成export MPLBACKEND=TkAgg再跑即可。

运行python dwa.py则直接进入 DWA 局部避障的单独演示,不依赖 A* 预先算好的全局路径,适合单独验证 DWA 参数。Vplanner.py 则是两个算法的完整组合流程,也是我推荐你重点读的文件,因为它的注释和逻辑顺序最接近实际工程里的导航栈。

3.2 鼠标交互操作:左右键与中键的语义

这个仿真器的交互设计很直观:鼠标左键点击地图放置起点,右键点击放置终点,中键点击放置障碍物,放好之后按空格键开始规划。起点、终点和障碍物在地图上以不同颜色标识,运行过程中小车会沿着规划路径运动。

# main.py 中鼠标事件绑定的核心代码(简化) fig.canvas.mpl_connect('button_press_event', on_click) fig.canvas.mpl_connect('key_press_event', on_key) def on_click(event): if event.button == 1: # 左键:起点 start = (event.xdata, event.ydata) elif event.button == 3: # 右键:终点 goal = (event.xdata, event.ydata) elif event.button == 2: # 中键:障碍物 obstacles.append((event.xdata, event.ydata)) def on_key(event): if event.key == ' ': # 触发 A* 寻路 + DWA 跟踪主循环 vplanner.run(start, goal, obstacles)

注意这里中键在有些笔记本触摸板上不太好按,我一般外接鼠标或者改代码把障碍物放到右键加 Shift 的组合上。另外事件回调里拿到的是 matplotlib 的坐标,单位是地图坐标而不是像素坐标,做障碍物膨胀时要注意换算。放置障碍物时如果点得太密,DWA 的 clearance 计算会变得很敏感,因为障碍物点云太密会导致可行轨迹被过度惩罚,小车会出现「不敢动」的现象。

3.3 核心参数在哪改:速度、加速度、预测时间与权重

参数集中在 dwa.py 和 Vplanner.py 的 Config 类里。几个关键参数我用表格列出来,方便对照调整:

参数名位置默认值参考影响
max_speeddwa.py Config1.0 m/s最大线速度,越大跑得越快但刹车距离变长
max_yaw_ratedwa.py Config40.0 deg/s最大角速度,影响转弯灵活性
max_acceldwa.py Config0.2 m/s²线加速度,决定速度切换快慢
max_dyaw_ratedwa.py Config50.0 deg/s²角加速度,决定转向响应速度
predict_timedwa.py Config3.0 s轨迹前向模拟时长
goal_radiusVplanner.py Config0.5 m目标点切换阈值
obstacle_radiusVplanner.py Config0.3 m障碍物膨胀半径
to_goal_cost_gaindwa.py evaluation0.1方位角评价权重
speed_cost_gaindwa.py evaluation0.3速度评价权重
obstacle_cost_gaindwa.py evaluation0.05避障评价权重

我最常调的就是 predict_time 和 obstacle_cost_gain 的搭配。predict_time 如果从 3.0 改到 1.0,你会发现小车明显变得「近视」,路径不再平滑;obstacle_cost_gain 从 0.05 改到 0.3,小车会变得过度保守,明明能绕过去的窄道它也会停下来重新规划。这几个参数的组合效果不是线性的,建议每次只动一个参数,记录下路径长度和运行时间两个指标再对比。

DWA 的评价函数部分,得分不是简单相加,而是每个项先归一化再加权。归一化处理在轨迹模拟完成后做——把所有采样轨迹的各个代价分别放到同一个量纲下,避免某个项因为数值太大直接盖过其他项。这个细节很容易被忽略,改权重的时候如果发现调了半天没反应,大概率是归一化逻辑被破坏或者某个代价项超出预期范围。

4. 从 Python 仿真走向 Gazebo:ROS 包里的模型、导航与控制器

4.1 资源里带的 Gazebo 包结构:course_agv 系列包的关系

这个 zip 里除了纯 Python 部分,还带了一套完整的 Gazebo 仿真包,目录是 gazebo 下面,包含 course_agv_control、course_agv_nav、course_agv_description、course_agv_gazebo 四个包。这套结构是典型的 ROS 机器人仿真工程布局:description 放 URDF 模型,gazebo 放 launch 文件和 world 文件,nav 放导航配置,control 放控制器。

gazebo/ ├── course_agv_control/ # 控制器:底盘速度指令到轮速的转换 ├── course_agv_nav/ # 导航栈:map_server、amcl、move_base 配置 ├── course_agv_description/ # URDF 模型:车体、轮子、传感器 └── course_agv_gazebo/ # 仿真环境:world 文件与 launch 文件

四个包之间有明确的依赖关系。description 定义车体模型后,gazebo 包负责把 model 放进仿真环境;nav 包提供全局和局部规划器的参数配置;control 包把 move_base 下发的速度指令转成仿真车每个轮子的转速。如果你在 ROS 里直接catkin_make,要注意包的编译顺序,description 和 gazebo 之间如果缺少依赖声明,launch 的时候会报 package not found。

4.2 URDF 模型与 move_base 配置文件的对应关系

course_agv_description 里的 URDF 文件定义了车体参数,包括轴距、轮径、底盘尺寸这些。这些数值不是随便填的——DWA 的 max_yaw_rate 和 max_accel 必须和 URDF 里的物理参数匹配,否则仿真里小车会打滑或者转不动。我遇到过一个典型情况:URDF 里轮径写的是 0.2m,但路径规划参数里 max_speed 设 2.0 m/s,结果小车在仿真里像在冰面上漂移。原因是轮子线速度和角速度的换算关系变了,控制器的速度指令和底盘实际响应不一致。

<!-- course_agv_description 中轮式底盘的 URDF 关键部分 --> <link name="base_link"> <inertial> <mass value="10.0"/> <origin xyz="0 0 0" rpy="0 0 0"/> <inertia ixx="0.1" iyy="0.1" izz="0.2"/> </inertial> </link> <link name="left_wheel"> <inertial> <mass value="0.5"/> </inertial> </link>

惯性参数 ixx、iyy、izz 直接影响小车在 Gazebo 里的动态响应。这个包的轮式底盘我印象里默认用差速驱动,所以转弯时靠左右轮速差实现。你如果觉得小车转弯太贼,先别改 DWA 的角速度限制,去 URDF 里把 izz(绕 Z 轴的转动惯量)调大一点,物理上更接近真实底盘,DWA 的角速度窗口会自动收敛。

move_base 的配置在 course_agv_nav 里,主要是 costmap 的膨胀半径和局部规划器的选择。这里的局部规划器可以直接指向 Python 里 DWA 的 ROS 移植版本,但要注意:ROS 里的 DWA 参数格式和 Python 版本的 Config 类不完全一样,ROS 用的是 YAML 参数服务器,键名是 latin 风格的(如 max_vel_x、min_vel_x、acc_lim_x),迁移时对照着改。

4.3 启动 Gazebo 仿真:launch 文件的执行顺序与常见报错

启动 Gazebo 仿真的 launch 文件在 course_agv_gazebo 包的 launch 目录下。启动前先确认 ROS 环境没问题,我建议按顺序做三件事:先roscore,再启动 Gazebo world,最后启动导航栈。

# 启动 Gazebo 仿真环境的常用命令(在 catkin_ws 根目录) source devel/setup.bash roslaunch course_agv_gazebo course_agv_world.launch roslaunch course_agv_nav move_base.launch

第一次启动时如果 Gazebo 黑屏或者模型加载不出来,九成是模型路径没配置。把 course_agv_description 的路径加到 ROS_PACKAGE_PATH 里就能解决:

export ROS_PACKAGE_PATH=$ROS_PACKAGE_PATH:/path/to/your/ws/src

这里我踩过一个坑:course_agv_gazebo 的 launch 文件里加载 URDF 时用的是$(find course_agv_description),如果工作空间没编译或者 description 包没装到环境里,launch 会直接报错退出。每次跑之前先catkin_make一次,再source devel/setup.bash,能省掉很多玄学问题。

ROS 自带的 Rviz 里会同时显示全局路径和局部轨迹,这比 Python 仿真的可视化直观得多。在 Rviz 里你能看到:绿色是 A* 算出来的全局路径,红色是 DWA 实时规划的局部轨迹,小车沿着绿色走但红色轨迹会根据障碍物动态变化。这个可视化效果是理解两套算法配合方式的最直接途径,建议花点时间盯一下局部轨迹的变化规律。

5. 避坑指南:跑 A* + DWA 组合时最常见的五个翻车点

5.1 现象:A* 规划的路径穿过障碍物边缘

原因:栅格地图的障碍物膨胀半径没有覆盖到车体宽度。A* 是在栅格层面做规划,如果膨胀半径小于车宽的一半,规划出来的路径在几何上是可行的,但真实车体过不去。

解决:在 AStarPlanner.py 里找到障碍物膨胀的裁剪逻辑,把膨胀半径设成车体宽度的 0.55 倍以上。注意膨胀半径增大后,窄通道可能直接被堵死,路径规划失败,这时要检查地图分辨率是否能表达目标窄道。

5.2 现象:小车遇到障碍物后原地来回摆动

原因:DWA 的方位角权重和避障权重比例失衡,导致小车在障碍物前两个方向轮流打分,无法做出稳定决策。另一个常见原因是 predict_time 太短,每次模拟轨迹都看不到绕过去的路径。

解决:把 predict_time 从 2.0 秒调到 4.0 秒,同时把避障权重稍微调小,让小车更「敢」转向。优先调 predict_time,因为它对计算量的影响是线性的,而权重调整往往顾此失彼。

5.3 现象:鼠标点击放置障碍物没有反应

原因:matplotlib 的事件绑定在后台绘图模式下不生效。具体来说,如果用了非交互式后端(如 Agg),鼠标事件回调不会触发。

解决:确认 MPLBACKEND 是 TkAgg 或者 Qt5Agg。在代码里加一句matplotlib.use('TkAgg')在 import pyplot 之前,然后重新运行。这个问题在 macOS 的默认后端下经常出现。

5.4 现象:A* 路径算出来了但 DWA 完全不跟

原因:全局路径的坐标系和 DWA 的局部坐标系不一致。Python 仿真里如果地图原点不在机器人起始位置,目标点切换时会算出错误的相对坐标,DWA 会认为目标点永远在机器人正后方。

解决:在 Vplanner.py 的主循环里打印目标点和机器人位置,确认坐标变换正确。推荐的做法是把 A* 输出的全局路径统一转换到以机器人起始点为原点的坐标系再做目标点切换。

5.5 现象:Gazebo 仿真里小车轮胎空转、原地打滑

原因:URDF 模型里的摩擦系数设置过低,或者 DWA 的加速度超过仿真物理引擎能承受的极限。Gazebo 默认的 friction 参数是 1.0,但如果车体得过轻,轮胎附着力不够。

解决:在 URDF 的 wheel link 里显式添加 friction 参数,并适当降低 max_accel。这个包默认的 max_accel 如果直接搬到 Gazebo 环境里通常偏大,我先把它降到 0.1 再逐步往上调,直到小车正常起步和转弯为止。

6. 进阶技巧:把单点目标改造成多点 AGV 导航习惯

这套源码默认的交互是「一个起点、一个终点」,但实际 AGV 工程里更多是多点巡检:小车按顺序走 A → B → C → D,每个站点停一下,再继续走。改造思路不复杂——把鼠标右键点击的记录从单个终点改成队列,A* 每次只规划当前段的目标点,到达后再规划下一段。

# 多点导航的核心改造思路(伪代码) goal_queue = [] # 存储多个目标点 def on_click(event): if event.button == 3: goal_queue.append((event.xdata, event.ydata)) def main_loop(): while goal_queue: current_goal = goal_queue.pop(0) global_path = astar.plan(start, current_goal, obstacles) # 跟踪当前目标点直到到达 while not reached(current_goal): u, _ = dwa.plan(robot_pos, current_goal, obstacles) robot.step(u) start = current_goal # 当前站点成为下一段起点

这个改法里有个容易忽略的细节:到达点切换后,A* 的起点要用上一段的终点,否则全局路径会从头规划,浪费计算量而且路径会有重复段。我在做巡检 demo 时,目标点队列还会配合站点停留计时,这个计时不能放在 DWA 主循环里用 time.sleep,否则会把控制周期打乱,应该用独立的计时器变量在每次控制循环里累加判断。

另一个值得试试的方向是速度参数标定。每次改完底盘机械参数(比如换了轮子或者改了轴距),URDF 和 DWA 参数需要同步更新。我的习惯是先在纯 Python 仿真里把 DWA 的 max_accel 和 max_yaw_rate 调好,再原样迁到 ROS 参数服务器上,最后在 Gazebo 里验证。顺序反过来的话,Gazebo 里调参成本太高,每次改动都要重启仿真,而且物理引擎的噪声会让你搞不清参数到底合不合理。

从那以后我每次跑这套源码,都会强制走一遍完整流程:先确认 Python 仿真里 A* 和 DWA 能稳定配合,再开 Gazebo 验证物理响应,最后才敢把参数带到车上。这套习惯帮我少走了很多弯路,希望帮到你。

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

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

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

立即咨询