第四章:动态窗口法(DWA)实战

动态窗口法,简称DWA,是我个人非常喜欢的一个局部路径规划算法。为什么?因为它简单、直观,而且效果出奇的好。说白了,DWA就是在每一时刻,只考虑机器人当前速度附近的一个小窗口,在这个窗口里采样、预测、评价,然后选出最优的那组速度。

我刚开始接触DWA时,觉得这名字挺唬人。后来发现,核心思想就三个字:找速度。找什么速度?能让机器人既跑得快,又不撞墙的速度。

核心思想:DWA将路径规划问题转化为速度空间中的优化问题。在每一控制周期,算法在允许的速度范围内采样多组(v, ω),预测每组速度对应的轨迹,通过评价函数选出最优轨迹对应的速度。

4.1 速度空间采样

速度空间采样,说白了就是「我能用哪些速度」。但这里有个关键约束——不是所有速度都能用。

我遇到过不少新手,上来就把速度范围设得很大,结果机器人疯狂打转。嗯,这里要注意,DWA的采样空间受三个因素限制:

  1. 机器人自身限制:最大/最小线速度、角速度。比如差速底盘,v一般在[-0.5, 0.5] m/s,ω在[-π/3, π/3] rad/s。
  2. 电机加减速能力:当前速度不能突变。假设当前v=0.2 m/s,最大加速度0.5 m/s²,那下一时刻v只能在[0.2-0.5*dt, 0.2+0.5*dt]范围内。
  3. 安全约束:采样速度必须能在撞到障碍物前停下来。这个我后面细说。

采样过程其实很简单,就是遍历所有可能的(v, ω)组合。我习惯用这样的参数:

# 速度空间采样参数
v_min = -0.5      # 最小线速度 (m/s)
v_max = 0.5       # 最大线速度 (m/s)
w_min = -np.pi/3  # 最小角速度 (rad/s)
w_max = np.pi/3   # 最大角速度 (rad/s)

v_res = 0.01      # 线速度分辨率 (m/s)
w_res = np.pi/180 # 角速度分辨率 (rad/s)

# 当前速度
v_current = 0.2
w_current = 0.1

# 加速度限制
v_acc_max = 0.3   # 最大线加速度 (m/s²)
w_acc_max = 0.5   # 最大角加速度 (rad/s²)

# 动态窗口范围
v_dw_min = max(v_min, v_current - v_acc_max * dt)
v_dw_max = min(v_max, v_current + v_acc_max * dt)
w_dw_min = max(w_min, w_current - w_acc_max * dt)
w_dw_max = min(w_max, w_current + w_acc_max * dt)

# 生成采样空间
v_samples = np.arange(v_dw_min, v_dw_max, v_res)
w_samples = np.arange(w_dw_min, w_dw_max, w_res)

我的经验:分辨率别设太细。v_res=0.01,w_res=π/180就够用了。太细了计算量大,效果提升不明显。我曾经试过把分辨率提高10倍,结果机器人反应慢了半拍,反而更容易撞。

4.2 轨迹预测

有了速度样本,下一步就是预测轨迹。说白了,就是假设机器人以(v, ω)匀速运动一段时间,看看会走到哪里。

对于差速底盘,运动模型很简单:

def predict_trajectory(x, y, theta, v, w, dt, steps):
    """
    预测轨迹
    x, y, theta: 当前位姿
    v, w: 线速度、角速度
    dt: 时间步长
    steps: 预测步数
    """
    trajectory = []
    for _ in range(steps):
        x += v * np.cos(theta) * dt
        y += v * np.sin(theta) * dt
        theta += w * dt
        trajectory.append([x, y, theta])
    return np.array(trajectory)

这里有个细节:预测步数steps很关键。步数太少,机器人看不到远处的障碍物;步数太多,计算量大,而且预测不准。我一般设8-12步,每步0.1秒,总共预测0.8-1.2秒。

注意:轨迹预测假设机器人匀速运动。实际中机器人速度会变化,但DWA在每个控制周期都会重新规划,所以这个假设可以接受。如果你发现机器人轨迹和预测偏差太大,检查一下控制周期是不是太长了。

4.3 评价函数

评价函数是DWA的灵魂。它决定了机器人「喜欢」什么样的速度。标准DWA用三个指标:

指标 含义 计算公式
方位角评价 轨迹终点朝向与目标方向的偏差 heading = 180° - |目标角 - 终点朝向角|
距离评价 轨迹与最近障碍物的距离 dist = min(轨迹点到所有障碍物的距离)
速度评价 速度大小,鼓励快速移动 vel = |v|

最终得分是加权和:

def evaluate_trajectory(traj, goal, obstacles, v):
    """
    评价一条轨迹
    traj: 轨迹点序列
    goal: 目标点 (x, y)
    obstacles: 障碍物列表 [(x, y), ...]
    v: 线速度
    """
    # 1. 方位角评价
    end_x, end_y, end_theta = traj[-1]
    goal_angle = np.arctan2(goal[1] - end_y, goal[0] - end_x)
    heading = np.pi - abs(end_theta - goal_angle)
    
    # 2. 距离评价
    min_dist = float('inf')
    for obs in obstacles:
        for point in traj:
            dist = np.sqrt((point[0] - obs[0])**2 + (point[1] - obs[1])**2)
            min_dist = min(min_dist, dist)
    # 如果距离太近,直接淘汰
    if min_dist < 0.2:  # 安全距离阈值
        return -float('inf')
    
    # 3. 速度评价
    vel = abs(v)
    
    # 加权求和(需要归一化)
    alpha, beta, gamma = 0.5, 0.3, 0.2
    score = alpha * heading + beta * min_dist + gamma * vel
    return score

归一化很重要:三个指标的量纲不同,必须归一化到同一量级。我习惯用最大最小值归一化,或者除以理论最大值。不归一化的话,某个指标会主导决策,机器人行为会很奇怪。

4.4 参数调优

参数调优是DWA最磨人的部分。我当年调参数调了整整一周,才摸清门道。这里分享几个关键参数:

  • α (方位角权重):调大,机器人会直奔目标,但容易撞墙。我一般设0.4-0.6。
  • β (距离权重):调大,机器人会远离障碍物,但可能绕远路。我一般设0.2-0.4。
  • γ (速度权重):调大,机器人跑得快,但可能不够安全。我一般设0.1-0.3。
  • 预测时间:0.8-1.5秒。场景复杂用短点,空旷用长点。
  • 安全距离:0.15-0.3米。取决于机器人尺寸和传感器精度。

调参技巧:先固定α和γ,只调β。β调到机器人能在障碍物前平稳停下,再调α让机器人更直接地走向目标。最后调γ,在安全和速度之间找平衡。我曾经在一个仓库项目里,花了三天才找到最优参数组合。

4.5 在仿真中部署DWA

部署DWA到仿真环境,我推荐用ROS + Gazebo。下面是一个完整的部署流程:

#!/usr/bin/env python3
import rospy
from geometry_msgs.msg import Twist, PoseStamped
from sensor_msgs.msg import LaserScan
from nav_msgs.msg import Odometry
import numpy as np

class DWAController:
    def __init__(self):
        rospy.init_node('dwa_controller')
        
        # 参数
        self.v_max = 0.5
        self.w_max = np.pi/3
        self.v_res = 0.01
        self.w_res = np.pi/180
        self.dt = 0.1
        self.predict_steps = 10
        self.safe_dist = 0.2
        
        # 权重
        self.alpha = 0.5
        self.beta = 0.3
        self.gamma = 0.2
        
        # 状态
        self.pose = None
        self.goal = None
        self.laser_data = None
        
        # 发布器与订阅器
        self.cmd_pub = rospy.Publisher('/cmd_vel', Twist, queue_size=1)
        rospy.Subscriber('/odom', Odometry, self.odom_callback)
        rospy.Subscriber('/scan', LaserScan, self.laser_callback)
        rospy.Subscriber('/move_base_simple/goal', PoseStamped, self.goal_callback)
    
    def odom_callback(self, msg):
        # 提取位姿
        self.pose = msg.pose.pose
    
    def laser_callback(self, msg):
        # 将激光数据转为障碍物点
        self.laser_data = msg
        # 转换逻辑略...
    
    def goal_callback(self, msg):
        self.goal = msg.pose.position
    
    def run(self):
        rate = rospy.Rate(10)  # 10Hz控制频率
        while not rospy.is_shutdown():
            if self.pose and self.goal and self.laser_data:
                # 1. 速度空间采样
                v_samples, w_samples = self.sample_velocity_space()
                
                # 2. 轨迹预测与评价
                best_score = -float('inf')
                best_v, best_w = 0, 0
                
                for v in v_samples:
                    for w in w_samples:
                        traj = self.predict_trajectory(v, w)
                        score = self.evaluate_trajectory(traj, v)
                        
                        if score > best_score:
                            best_score = score
                            best_v, best_w = v, w
                
                # 3. 发布控制指令
                cmd = Twist()
                cmd.linear.x = best_v
                cmd.angular.z = best_w
                self.cmd_pub.publish(cmd)
            
            rate.sleep()

if __name__ == '__main__':
    controller = DWAController()
    controller.run()

部署注意事项:

  • 控制频率至少10Hz,否则机器人反应迟钝
  • 激光雷达数据要过滤掉无效点(NaN或Inf)
  • 目标点到达判断:距离小于0.1米就认为到达
  • 如果机器人卡住,可以加一个「原地旋转」的逃逸策略

4.6 DWA核心逻辑流程图

下面这张图,是我自己总结的DWA核心流程。每次调参时我都会看一眼,提醒自己别漏了关键步骤。

DWA动态窗口法核心流程 开始控制周期 步骤1:获取当前位姿、速度、障碍物信息 从里程计和激光雷达获取数据 步骤2:计算动态窗口 考虑速度限制 + 加速度限制 + 安全约束 步骤3:速度空间采样 在动态窗口内均匀采样(v, ω)组合 步骤4:轨迹预测与评价 预测每条轨迹,计算方位角/距离/速度得分 步骤5:选择最优速度并执行 发布/cmd_vel,进入下一控制周期 关键参数 • 控制频率:10Hz • 预测步数:8-12步 • 安全距离:0.15-0.3m • α(方位角):0.4-0.6 • β(距离):0.2-0.4 • γ(速度):0.1-0.3 避坑提醒 • 记得归一化评价指标 • 激光数据要过滤无效点 • 卡住时加逃逸策略 • 目标到达阈值设0.1m 调参顺序 先调β → 再调α → 最后调γ

这张图把DWA的五个核心步骤串起来了。你想想看,从获取数据到执行速度,每一步都环环相扣。我曾经在一个项目中,因为忘了过滤激光雷达的无效点,机器人对着空气绕了半天圈。嗯,从那以后我再也不敢跳过数据预处理了。

DWA虽然简单,但用好了效果惊人。它不像A*那样需要全局地图,也不像MPC那样计算量大。说白了,DWA就是「在有限的选择里,做出当下最好的决定」。这种贪心策略在大多数场景下都够用,而且实时性极好。

如果你在仿真中部署DWA,建议先从简单场景开始——空旷环境,一个目标点,一个障碍物。调通后再加复杂场景。我当年就是这么一步步来的,稳扎稳打,反而比一上来就搞复杂场景快得多。

最后说一句:DWA不是万能的。在狭窄通道、U型障碍物等场景,DWA可能会陷入局部最优。这时候需要结合全局规划(比如A*)或者增加记忆机制。但作为局部规划器,DWA绝对是最值得掌握的算法之一。

可可精品资料库