ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

DWA动态窗口算法:AGV局部避障原理、仿真实现与调参实践

DWA动态窗口算法:AGV局部避障原理、仿真实现与调参实践

这几年做了不少AGV相关的仿真验证项目,最常用的局部避障方案就是动态窗口算法,也就是大家常说的DWA(Dynamic Window Approach)。说实话,这个算法名字听起来很高端,但它解决的是一个非常朴素的问题:车已经有了目标方向和全局路径,可眼前突然冒出来一个障碍物,下一步应该往哪走、走多快,才不会撞上、也不会绕远路。

我用这个思路搭过完整的仿真避障环境,也在真实底盘上做过迁移验证。DWA最大的好处理解门槛低、计算开销小、输出直接就是底盘能执行的线速度和角速度,特别适合AGV这种对实时性要求高的场景。这篇就按我自己的实操路径,把DWA从原理到仿真实现再到调参排坑,完整串一遍。如果你是做机器人路径规划的学生,或者正在搞AGV产品研发、物流自动化方案验证,这篇应该能帮你省下不少试错时间。

很多人一上来就写代码,结果算法参数没吃透,调起来一头雾水。我建议反过来,先把思路理清楚:DWA到底在干什么。理解了设计逻辑,后面的代码和参数就自然对上了。

1. 先理清思路:AGV避障为什么需要DWA

1.1 全局规划与局部避障的分工

AGV要完成导航任务,通常拆成两块:全局路径规划和局部避障。全局规划负责“走哪条路”,基于已知地图用A*、Dijkstra之类的方法规划出一条从起点到终点的路径;局部避障负责“这一段路怎么安全地走过去”,处理那些全局规划没预料到的情况,比如临时出现的纸箱、行人,或者车身定位偏差导致的偏移。

这两块是配合关系。全局路径只是一串几何点,它不含时间信息,也不知道车能不能快速拐过去;局部避障则要考虑底盘的运动学和动力学约束,比如转弯半径、加速上限、刹车距离。DWA干的就是后者:在每个控制周期内,根据当前状态和环境感知,算出一对底盘可以立刻执行的速度指令。

我在仿真里见过不少同学只做全局规划,然后沿着路径点直接跟踪,结果一遇到动态障碍物就直接撞上去。原因就是少了局部避障这一层。AGV真正上线跑的时候,环境永远不可能和地图完全一致,局部避障才是保证安全的关键闭环。

1.2 几条不同的局部避障路子

局部避障不是只有DWA一种方案,常见的有VFH(向量场直方图)、纯跟踪、MPC(模型预测控制)等。我在这做个对比,方便你选型的时候心里有数。

方案核心思路优点缺点
VFH把障碍物分布做成直方图,找“空隙”走计算快、能处理密集障碍物不考虑运动学约束,输出方向和速度需要额外转换
纯跟踪跟踪全局路径前方一个目标点路径跟踪平滑、实现简单避障能力弱,遇到障碍物基本靠全局重新规划
MPC在线求解带约束的最优控制问题效果好、能同时优化安全性与效率计算量很大,对嵌入式设备不友好
DWA在速度空间采样,预测轨迹并评价计算量小、直接输出速度指令、参数直观容易陷入局部最优,动态密集场景表现一般

从表格能看出来,DWA最适合的是差速或阿克曼底盘的室内AGV场景。它不需要精确建立环境模型,把速度采样和运动学模型结合,天然就能避开“指令太激进导致刹不住”的问题。

1.3 为什么最终选择DWA

我在选型的时候不只是看算法效果,还要看工程落地成本。DWA有几个很实在的优点:

其一,它不挑地图形式。栅格地图、拓扑地图、特征地图都能用,因为算法根本不直接依赖地图结构,只用距离信息。对于仿真阶段快速验证,栅格地图就够了。

其二,它天然输出线速度和角速度,不需要额外转换层。VFH输出的是方向角,你还得把它换算成差速底盘的两个轮速;MPC输出的是最优控制序列,你得考虑怎么取第一个指令。DWA直接在速度空间采样,算出来什么速度就用什么速度,和底盘控制层的接口非常干净。

其三,参数调整直观。权重改一改,车是更激进还是更保守,立刻就能从仿真实测里看出来。这一点对工程调试特别重要,比黑盒优化器实用得多。

我在实际项目中,DWA并不是孤立跑的,而是把全局路径作为引导信息传给它,相当于“全局指路,局部纠偏”。这种组合在绝大多数AGV场景下已经足够稳定了。

2. 核心原理拆解:DWA到底在算什么

2.1 速度空间是个什么空间

差速AGV底盘的执行命令可以简化成两个量:线速度 v 和角速度 ω。一对(v, ω)就代表车辆的一种运动状态。理论上,速度是连续的,但机器人没法在一个控制周期内穷举所有速度组合,所以我们要对速度空间进行离散采样。

比如线速度范围是0到1.0 m/s,采样步长0.05 m/s,那就是21个线速度候选;角速度范围-1.0到1.0 rad/s,采样步长0.1 rad/s,那就是21个角速度候选。两两组合,一共441对候选速度。

这个二维空间就是DWA里的“速度空间”。每次控制周期,算法从这个速度空间里挑一对最优速度执行。采样密度决定了两件事:密度越高,候选速度越多,理论上能找到更平滑的解;但代价是计算量成倍上升。仿真里我通常取每组几十到几百个候选,这样在普通笔记本上也能跑到几十毫秒一轮。

2.2 动态窗口是怎么来的

候选速度不是全都可以用,比如车速已经1 m/s了,但突然想让它瞬间停下来,底盘加速度再大也需要时间,车会冲出去一段距离。所以DWA要做三查:

第一查,速度不超极限。候选速度不能超过底盘的线速度上限和角速度上限。

第二查,窗口期可达。考虑一个固定时间窗 Δt(通常是0.1到0.2秒),候选速度必须是在这个窗口期内,从当前速度加减速度能到达的范围。比如当前线速度是0.5 m/s,最大加速度是0.5 m/s²,Δt是0.1 s,那么候选线速度必须在0.45到0.55 m/s之间。这个范围就是“窗口”的由来。

第三查,刹得住。以当前候选速度行驶,如果前方出现障碍物,AGV必须能在撞上之前停下来。所以要用候选速度对应的刹车距离和前方障碍物距离做比较,刹不住的速度直接丢弃。

三查之后,速度空间就被压缩成一个很小的“动态窗口”,剩下的是当前状态下真正安全可达的速度。这个窗口会随着车辆当前速度变化而变化,所以叫“动态窗口”。我在调参的时候最关注第三查,因为室内AGV如果刹不住,再好的路径也没有意义。

2.3 轨迹怎么预测

每个候选速度都要对应一条预测轨迹,DWA才能评价它好不好。因为速度指令会在一个预测时间窗口内保持,假设底盘是差速模型,那么车辆在这段时间内的运动轨迹可以用圆弧来近似。

差速底盘的轨迹推算很简单。设当前位姿为(x, y, θ),若保持(v, ω)一段时间,小步长递推公式是:

θ_new = θ + ω * Δt x_new = x + v * Δt * cos(θ) y_new = y + v * Δt * sin(θ)

如果 ω 不为零,更精确一点可以用圆弧模型:

x_new = x + (v / ω) * (sin(θ + ω*Δt) - sin(θ)) y_new = y - (v / ω) * (cos(θ + ω*Δt) - cos(θ)) θ_new = θ + ω * Δt

这段公式你可以不用背,但必须理解一件事:预测轨迹是车辆在候选速度下未来的“足迹”。算法预测的不只是一小段,而是整个预测时间窗口内的路径,这一段路径才用来做碰撞检测和评价。预测时间太短,车看不到前面的危险;预测时间太长,计算量变大,而且轨迹太远意义不大。我习惯设2秒左右。

2.4 如何选出一个好的速度

候选速度经过三查后,剩下的仍然可能是几十对,怎么挑最优?DWA用三个打分项来评价。

第一项是目标朝向得分。预测轨迹末端,车辆的朝向与“轨迹末端指向目标点”的方向越一致,得分越高。这样车不会傻傻地背离目标前进。

第二项是安全距离得分。预测轨迹上距离最近障碍物的距离越大越好。如果轨迹上某个点撞上障碍物,直接淘汰。

第三项是速度得分。速度越快,得分越高,因为这代表效率更高。

三项得分加权求和,总分最高的一对(v, ω)就是本周期要执行的速度。

这里有个关键的坑:三个量纲完全不一样。目标朝向是角度,安全距离是米,速度是米每秒。如果不做归一化直接加权,速度项可能把其他两项压倒。所以实际实现里,要把所有候选的这三个分数分别做归一化(比如除以本周期所有候选的最大值),再乘权重求和。权重怎么给,后面调参部分我会单独说。

3. 仿真环境搭建与算法实现细节

3.1 仿真环境怎么搭

DWA的仿真不需要花哨的3D物理引擎,一个2D栅格地图加一个简单的运动模型就够用。地图用二维数组表示,障碍物位置标成1,可行区域标成0。AGV用一个圆来近似,圆心是车辆几何中心,半径是车辆外轮廓半径加上安全余量。

这种简化在仿真阶段完全够用。我见过有人一上来就搞3D物理引擎,结果大半天时间都花在装环境、调渲染、配传感器模型上,DWA本身反而没精力深入。做算法验证,最轻量的方案就是最有价值的方案。

额外的关键是膨胀层。障碍物本身的栅格虽然标成了1,但AGV不能贴着障碍物边缘走,因为车是圆的,有实际尺寸。所以要在障碍物周围做膨胀处理:把障碍物周围一定半径的栅格都标记为不可通行,这个半径通常等于车辆半径加安全距离。膨胀完之后,碰撞检测就退化成一个点和一个栅格标志的查表操作,非常快。

3.2 核心模块代码框架

DWA的核心实现可以拆成几个独立函数:速度采样、动态窗口计算、轨迹预测、碰撞检测、评价函数。下面是我基于Python实现的一个精简框架,保留了最核心的算法流程:

import numpy as np def calc_dynamic_window(state, params): """计算速度采样窗口,包含极限速度、加减速度和刹车距离约束""" v_cur, w_cur = state[2], state[3] v_max, v_min = params['max_speed'], params['min_speed'] w_max, w_min = params['max_omega'], -params['max_omega'] a_max, a_min = params['max_accel'], -params['max_accel'] w_accel = params['max_omega_accel'] dt = params['dt'] # 极限速度约束 vs = [v_min, v_max, w_min, w_max] # 窗口期可达约束 ws = [v_cur - a_max * dt, v_cur + a_max * dt, w_cur - w_accel * dt, w_cur + w_accel * dt] # 刹车距离约束:根据当前速度估算停车距离 brake_dist = v_cur * v_cur / (2 * abs(a_min)) if state['dist_to_obstacle'] < brake_dist: v_max = min(v_max, v_cur - a_max * dt) # 取交集 v_win_min = max(vs[0], ws[0]) v_win_max = min(vs[1], ws[1]) w_win_min = max(vs[2], ws[2]) w_win_max = min(vs[3], ws[3]) return [v_win_min, v_win_max, w_win_min, w_win_max] def predict_trajectory(x, y, theta, v, w, params, steps=6): """根据候选速度预测未来一段轨迹点""" dt = params['dt'] traj = [] for _ in range(steps): if abs(w) > 1e-6: x += (v / w) * (np.sin(theta + w * dt) - np.sin(theta)) y -= (v / w) * (np.cos(theta + w * dt) - np.cos(theta)) else: x += v * np.cos(theta) * dt y += v * np.sin(theta) * dt theta += w * dt traj.append((x, y, theta)) return traj def check_collision(traj, grid_map, robot_radius): """检查轨迹是否与障碍物碰撞,grid_map已做膨胀处理""" for point in traj: grid_x, grid_y = world_to_grid(point[0], point[1]) if grid_map[grid_y, grid_x] == 1: return True return False def evaluate_trajectory(traj, goal, obstacles, v, w, params): """评价轨迹:目标朝向、安全距离、速度三部分加权""" heading_score = calc_heading_score(traj, goal) dist_score = calc_dist_score(traj, obstacles) vel_score = abs(v) # 归一化处理,再加权 score = (params['w_heading'] * heading_score + params['w_dist'] * dist_score + params['w_vel'] * vel_score) return score def dwa_control(state, goal, grid_map, params): """DWA主循环:每个控制周期调用一次,返回最优速度""" v_cur, w_cur = state[2], state[3] window = calc_dynamic_window(state, params) candidates = sample_speed(window, params) best_score = float('-inf') best_v, best_w = 0, 0 for v, w in candidates: traj = predict_trajectory(state[0], state[1], state[4], v, w, params) if check_collision(traj, grid_map, params['robot_radius']): continue score = evaluate_trajectory(traj, goal, grid_map, v, w, params) if score > best_score: best_score = score best_v, best_w = v, w return best_v, best_w

这个框架去掉了不少工程细节,但核心逻辑是完整的。你跑仿真主循环时,每个周期拿到DWA输出的(best_v, best_w),喂给底盘运动模型,更新AGV位置,再重新感知环境,循环往复。

真正工程化的时候,还要加低通滤波、速度平滑和越界保护,避免DWA输出的速度突变导致底盘抖动。我一般会在DWA后面加一层一阶低通,让底盘的加减速更温和。

3.3 参数标定顺序与经验

参数标定是DWA调试最花时间的部分。我整理了一份常用的初始参数表,你可以用它起步:

参数含义参考取值
max_speed最大线速度 m/s1.0
min_speed最小线速度 m/s0.0
max_omega最大角速度 rad/s1.2
max_accel最大线加速度 m/s²0.5
max_omega_accel最大角加速度 rad/s²1.0
dt控制周期 s0.1
predict_time轨迹预测时长 s2.0
v_resolution线速度采样步长 m/s0.05
w_resolution角速度采样步长 rad/s0.1
w_heading目标朝向权重0.8
w_dist安全距离权重0.6
w_vel速度权重0.2

注意权重这一栏是没有归一化前的“初始倾向值”。我实际的标定顺序是:先定底盘运动学参数(max_speed、max_accel这些),再定predict_time和碰撞安全半径,最后调三个权重。

运动学参数一定要和真实底盘能力一致。仿真里可以随便写大加速度,但迁移到真车的时候,如果加速度参数比底盘实际大得多,DWA规划出来的轨迹会让底盘执行机构跟不上,出现明显的路径偏差。我踩过这个坑,后来养成了先看底盘选型参数表再填算法参数的习惯。

predict_time是个很有意思的参数。调得太短,车只看得到跟前的路,速度一快就容易在最后一个时刻才发现障碍物,导致急刹车;调得太长,计算量变大,而且远处的轨迹受模型误差影响变大,车会变得“神经质”。一般设为2秒左右比较稳,障碍物密集时可以缩小到1.5秒。

三个权重怎么feel?这是最难用文字描述的,我给个直观经验:如果车老往障碍物方向贴,把w_dist加大;如果车在目标点附近转圈,把w_heading加大;如果车慢吞吞像“老太太”,把w_vel加大。一次只动一个权重,改完跑一遍仿真看行为,不要同时改多个,否则出问题都不知道是哪个参数引起的。

4. 常见问题与排查:从卡死、绕圈到震荡

4.1 目标点附近绕圈

这是DWA仿真里最经典的现象:AGV明明已经快到终点了,却在周围一圈一圈地绕,不进点。原因是目标朝向得分这时候很微妙:当车辆正好在目标点旁边,轨迹预测末端朝向无论怎么转,指向目标的方向和车身朝向之间总是有夹角,评价值差不多,于是算法在几个候选速度之间来回切换,输出就成了绕圈。

我解决这个问题的方法是在DWA外面加一个停车判据:当AGV距离目标点小于某个阈值(比如0.3米)时,直接降低目标权重、加大停车指令,不再依赖DWA继续导航。阈值可以按车辆尺寸设置,AGV越大阈值越大。

另一种做法是把目标点戳到车前方一个适当距离处。这个“适当距离”很关键:太近容易绕圈,太远会让AGV把临时目标点当作终点,实际物理位置还没到就转向。我习惯取预测轨迹长度的一半作为目标点偏移距离,实测效果比较平衡。

4.2 障碍物前急停或震荡

仿真里AGV在障碍物面前突然急刹车,或者前前后后反复加减速,这也是DWA的典型毛病。原因通常是安全检测距离太短,或刹车距离约束和评价函数安全权重匹配不上。

我遇到过一种情况:安全半径设得没问题,但predict_time设得太短,车第一次“看到”障碍物时已经晚了,只能急刹。把predict_time从1.2秒加到2.5秒后明显改善。

还有一种没这么容易发现的原因:安全距离得分函数在障碍物边缘产生剧烈变化。当AGV处于障碍物正前方时,候选速度的安全距离分数都差不多,微小的噪声就会导致算法在“绕左”和“绕右”之间跳变,视觉上就是左右震荡。这种情况我通常给速度输出加平滑滤波,或者把安全距离项做成对数函数压缩,降低变化剧烈程度。

4.3 AGV速度慢吞吞

有时候算法跑通了,但AGV在空旷区域也走得很慢,效率低得让人着急。第一反应是提高速度权重,但往往收效甚微,因为根本原因是候选速度被窗口约束“掐”住了。

典型情况是当前线速度已经很低,如果加速约束的dt太短,或者刹车距离约束太保守,候选速度窗口被限制在很窄的低速范围内,车自然快不起来。这时候不要只调权重,要看窗口相关参数:适当增大dt、放宽刹车距离约束的系数,让候选速度空间恢复“活力”。

另外还要检查速度采样上限。有时候max_speed是1.0,但采样步长太大,候选速度集中在高端,低速候选被跳过太多,导致评价函数没有平滑过渡,算法次优解也不够好。把采样步长调细一点,效果立竿见影。

4.4 卡在局部极小点

DWA本质上是个贪心算法,看的是当前局部窗口内的最优速度,所以遇到U形障碍物、凹形障碍物,或者两个障碍物形成死胡同的时候,很可能困在局部出不来。你从仿真里看到的现象是:AGV在一个小范围内来回试探,怎么都过不去,但全局路径明明知道绕一下就行。

解决这个问题最标准的做法是给DWA加全局路径引导。简单说,从全局路径上取当前车辆前方一定距离的路径点,把它作为DWA的临时目标点,而不是直接把最终目标点给DWA。这样DWA只会想着去追那个“前方点”,不会因为追求全局最优而让自己陷入死角。

我在实现里做了一步简化:计算全局路径上距离当前车辆位置最近的点索引,然后往后数N个点(N由车速和预测时间决定),用那个点做临时目标。这样DWA在每个局部时刻都“追”着全局路线走,遇到障碍物可以局部绕,绕完之后又会回到全局路径附近。

4.5 问题速查表

现象可能原因调参方向
目标点附近绕圈目标朝向权重偏低、缺少停车判据提高w_heading、加停车阈值判断
障碍物前急刹predict_time太短、安全半径过小增大predict_time、增大安全半径
障碍物附近左右震荡安全距离分数突变、速度输出不平滑加低通滤波、压缩安全距离函数
空旷区域速度慢候选速度窗口被压制、速度权重低调整dt和刹车约束、适度提高w_vel
陷入U形障碍物DWA仅看局部、无全局引导加载全局路径引导、用路径点做临时目标
频繁切换最优速度采样密度不足、权重区分度低增加采样分辨率、拉开权重差距

排查问题的时候,我强烈建议给仿真加上可视化输出,把当前速度窗口、候选轨迹、最终选择的轨迹实时画出来。不夸张地说,能画出候选轨迹的那一刻,你对DWA的理解至少提升一个档次。很多诡异问题,比如速度跳跃、轨迹突变,从画面上一眼就能看出来,而靠看日志很难定位。

5. 扩展思路:给DWA加一点工程化

5.1 全局路径引导,解决“近视眼”

前面提到的全局路径引导,其实是DWA工程化最值得做的一项增强。没有引导的DWA只能看见局部,有了引导它就在“全局路径这条线”附近做局部避障,既保留DWA的实时性,又克服了局部最优的短板。

实现路径引导时,我建议在代码里维护一个路径点列表,每个控制周期做一次“当前车辆到路径上最近点”的搜索。这个搜索在路径点不多时线性扫描就够了,如果路径点上千个,就用KD树或分段索引加速。我实测过,500个路径点线性扫描的时间在微秒级,完全不影响实时性。

临时目标点取法要和你预测轨迹长度匹配。预测2秒轨迹,车速1.0 m/s,预测距离就是2米;那么临时目标点应该取在大约2米远的路径点附近,太多太少都会导致车辆“抢跑”或“滞后”。

5.2 动态障碍物预测,应对多车环境

如果仿真是多台AGV协同的场景,DWA还不够。因为DWA只根据当前障碍物位置做避障,不考虑其他AGV的运动方向,结果可能是两台车互相避让时“商量不来”:左边让、右边让、又撞到一起。

简单的改进是给每个动态障碍物建立其速度和朝向的估计。在碰撞检测时,预测轨迹不是只看当前障碍物位置,而是按障碍物当前速度推算它在未来时刻的位置。这相当于在碰撞检测里增加了一个“预测障碍物轨迹”的步骤,计算量增加不大,效果提升明显。

我做过多AGV仿真,最朴素的方案就已经有效:把所有AGV当作“圆障碍物+速度向量”,碰撞检测的时间步和轨迹预测的时间步同步推进,就能避免明显的互撞。复杂场景再上协商策略。

5.3 时间与性能优化,让仿真接近实时

DWA性能优化的核心在采样密度。采样密度越大,候选速度越多,计算时间线性增长。我常用的做法是两阶段采样:先粗采样快速排除明显不可达或不安全的区域,再在粗优解附近细采样,找到更平滑的速度。这个思路和图像金字塔差不多,先看轮廓再抠细节。

另外一个容易忽略的优化是轨迹预测步长。DWA预测轨迹时不需要每个时间点都做碰撞检测,可以每检测2到3步取一个点。只要能保证两点之间不会有太小障碍物“漏过”就行。障碍物最小尺寸如果大于检测步距对应的弧长,漏检风险很低。

性能优化必须在不牺牲安全的前提下做。我给自己定的底线是:控制周期10毫秒以内必须完成一轮完整DWA计算,否则就降采样密度或减少预测步长。这样迁移到嵌入式平台时,不至于因为算法耗时导致底盘控制超时。

调过几版DWA之后,我的感受是:这个算法难的不是原理,而是参数和场景之间的微妙关系。同样的参数,换个地图、换个障碍物布局,表现可能天差地别。所以我在做仿真的时候,一定会准备多张测试地图,专门用来检验算法的泛化能力。至少包括一张空旷跑道、一张密集障碍、一张U形陷阱的图,把这三张图跑顺了,AGV避障才算真正过关。DWA好处就在这块,它简单到你可以快速跑通,又复杂到调参能调上几天。也正是这种性格,让它在AGV仿真和产品落地中一直没被淘汰。

RELATED READING

延伸阅读

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