引用参考如下:
博客园解释:https://www.cnblogs.com/dlutjwh/p/11158233.html 这篇博客园写的贼棒!我当时就是一边对着论文一边对着他这篇来看的,所以==大部分论文的文字也来源于此==
原文论文:https://ieeexplore.ieee.org/document/580977 没有账号的话 就去sci-hub吧,这里是preprint版:colli.dvi (cmu.edu)
Python代码:https://github.com/AtsushiSakai/PythonRobotics
ROS代码:http://wiki.ros.org/dwa_local_planner
其实这篇我本科毕业论文用到了 当时很仔细的看过一遍,但是因为没有对着代码看,仅看了一下论文的数学公式推导等,就觉得很厉害,想到这样的方式去表示和限制。这次专门复习再来一次 结合着代码看一次好了。这次直接使用Typro打的markdown文件,这样在博客园的格式应该很好看了,目录也是在左边~
格式优美版在博客园,后续相关更新也在博客园内哦
虽然我感觉CSDN好像也挺优美的 hhh
首先,我们要了解这个干什么的,在路径规划算法 主要包括全局路径规划和局部路径规划。局部路径规划主要用于动态环境下的导航和避障,对于无法预测的障碍物DWA算法可以较好地解决。DWA算法的优点是计算负复杂度较低,由于考虑到速度和加速度的限制,只有安全的轨迹会被考虑,且每次采样的时间较短,因此轨迹空间较小。采样的速度即形成了一个动态窗口
直接跳转可以先看看运行的效果,感受一下
DWA的整体轨迹评价函数主要是三个方面:
简而言之就是在局部规划出一条路径,希望与目标越来越近,且速度较快,与障碍物尽可能远。评价函数权衡以上三个部分得到一条最优路径。
该论文相对于之前的创新点在于:
这部分对比的时候,因为年代的原因是随着全局规划一起对比的
为了使运动学方程更加接近实际,将模型的速度设为随时间变化的分段函数,在该假设下,机器人轨迹可看做许多的圆弧积分组成,采用该方法使得障碍物碰撞检测很方便,因为圆弧与障碍物的交点很好求。
其中关于公式参数的提前解释:
x ( t n ) = x ( t 0 ) + ∫ t 0 t n v ( t ) ⋅ cos θ ( t ) d t (1) x\left(t_{n}\right)=x\left(t_{0}\right)+\int_{t_{0}}^{t_{n}} v(t) \cdot \cos \theta(t) d t \tag{1} x(tn)=x(t0)+∫t0tnv(t)⋅cosθ(t)dt(1)
y ( t n ) = y ( t 0 ) + ∫ t 0 t n v ( t ) ⋅ sin θ ( t ) d t (2) y\left(t_{n}\right)=y\left(t_{0}\right)+\int_{t_{0}}^{t_{n}} v(t) \cdot \sin \theta(t) d t \tag{2} y(tn)=y(t0)+∫t0tnv(t)⋅sinθ(t)dt(2)
由此得知坐标是根据速度来得到的,而这个速度不能说直接给他设置,有限制的。例如,机器人速度 v ( t ) v(t) v(t) 取决于初始时刻 t 0 t_0 t0 的速度和时间 t 0 t_0 t0,和速度一样的, θ ( t ) \theta(t) θ(t) 也是初始转向角 θ ( t 0 ) \theta(t_0) θ(t0) 函数
x ( t n ) = x ( t 0 ) + ∫ t 0 t n ( v ( t 0 ) + ∫ t 0 t v ˙ ( t ^ ) d t ^ ) ⋅ cos ( θ ( t 0 ) + ∫ t 0 t ( ( w ( t 0 ) + ∫ t 0 t ˉ w ˙ ( t ~ ) d t ~ ) d t ~ ) d t (3) x\left(t_{n}\right)=x\left(t_{0}\right)+\int_{t_{0}}^{t_{n}}\left(v\left(t_{0}\right)+\int_{t_{0}}^{t} \dot{v}(\hat{t}) d \hat{t}\right) \cdot \cos \left(\theta\left(t_{0}\right)+\int_{t_{0}}^{t}\left(\left(w\left(t_{0}\right)+\int_{t_{0}}^{\bar{t}} \dot{w}(\tilde{t}) d \tilde{t}\right) d \tilde{t}\right) d t\right. \tag{3} x(tn)=x(t0)+∫t0tn(v(t0)+∫t0tv˙(t^)dt^)⋅cos(θ(t0)+∫t0t((w(t0)+∫t0tˉw˙(t~)dt~)dt~)dt(3)
此时机器人的轨迹由初始时刻的状态以及加速度决定,可以认为这些状态是可控的,同时由于机器人内部结构原因,其加速度也不是一直变化(类似于连续函数),因此可以将 t 0 t_0 t0 到 t n t_n tn 看作是很多个时间片,积分可以转换为求和,假设有 n n n个时间片,在每个 [ t i , t i + 1 ] [t_i,t_{i+1}] [ti,ti+1],机器人的加速度 v ˙ i \dot v_i v˙i和 w ˙ \dot w w˙ 保持不变,设 Δ t i = t − t i \Delta^i_t=t-t_i Δti=t−ti,那么(3) 又可以再一步:
x ( t n ) = x ( t 0 ) + ∑ i = 0 n − 1 ∫ t i t i + 1 ( v ( t i ) + v ˙ i ⋅ Δ t i ) ⋅ cos ( θ ( t i ) + w ( t i ) ⋅ Δ t i + 1 2 w ˙ i ⋅ ( Δ t i ) 2 ) d t (4) x\left(t_{n}\right)=x\left(t_{0}\right)+\sum_{i=0}^{n-1} \int_{t_{i}}^{t_{i+1}}\left(v\left(t_{i}\right)+\dot{v}_{i} \cdot \Delta_{t}^{i}\right) \cdot \cos \left(\theta\left(t_{i}\right)+w\left(t_{i}\right) \cdot \Delta_{t}^{i}+\frac{1}{2} \dot{w}_{i} \cdot\left(\Delta_{t}^{i}\right)^{2}\right) d t \tag{4} x(tn)=x(t0)+i=0∑n−1∫titi+1(v(ti)+v˙i⋅Δti)⋅cos(θ(ti)+w(ti)⋅Δti+21w˙i⋅(Δti)2)dt(4)
式(4)虽然与机器人的动力控制相关,但是不能决定机器人具体的驾驶方向,对于障碍物与机器人轨迹的交点也很难求出,可以继续进行简化,既然时间间隔很小,那么我们就把,那么就可以得到(5)
x ( t n ) = x ( t 0 ) + ∑ i = 0 n − 1 ∫ t i t i + 1 v i ⋅ cos ( θ ( t i ) + w i ∗ ( t ^ − t i ) ) d t ^ (5) x\left(t_{n}\right)=x\left(t_{0}\right)+\sum_{i=0}^{n-1} \int_{t_{i}}^{t_{i+1}} v_{i} \cdot \cos \left(\theta\left(t_{i}\right)+w_{i} *\left(\hat{t}-t_{i}\right)\right) d \hat{t} \tag{5} x(tn)=x(t0)+i=0∑n−1∫titi+1vi⋅cos(θ(ti)+wi∗(t^−ti))dt^(5)
最后解这个积分方程,简化为:
x ( t n ) = x ( t 0 ) + ∑ i = 0 n − 1 F x i ( t i + 1 ) (6) x\left(t_{n}\right)=x\left(t_{0}\right)+\sum_{i=0}^{n-1} F^i_x(t_{i+1}) \tag{6} x(tn)=x(t0)+i=0∑n−1Fxi(ti+1)(6)
其中 F F F 展开就是:
F x i ( t i ) = { v i w i ( sin θ ( t i ) − sin ( θ ( t i ) + w i ⋅ ( t − t i ) ) ) , w i ≠ 0 v i cos ( θ ( t i ) ) ⋅ t , w i = 0 (7) F_{x}^{i}\left(t_{i}\right)=\left\{\begin{array}{l} \frac{v_{i}}{w_{i}}\left(\sin \theta\left(t_{i}\right)-\sin \left(\theta\left(t_{i}\right)+w_{i} \cdot\left(t-t_{i}\right)\right)\right), w_{i} \neq 0 \\ v_{i} \cos \left(\theta\left(t_{i}\right)\right) \cdot t, w_{i}=0 \end{array}\right. \tag{7} Fxi(ti)={wivi(sinθ(ti)−sin(θ(ti)+wi⋅(t−ti))),wi=0vicos(θ(ti))⋅t,wi=0(7)
以上都是推导 x x x 坐标,对于 y y y 整个过程也是一样的,就是 F F F 里面的一个三角函数变换了
y ( t n ) = y ( t 0 ) + ∑ i = 0 n − 1 F y i ( t i + 1 ) (8) y\left(t_{n}\right)=y\left(t_{0}\right)+\sum_{i=0}^{n-1} F^i_y(t_{i+1}) \tag{8} y(tn)=y(t0)+i=0∑n−1Fyi(ti+1)(8)
F y i ( t i ) = { − v i w i ( cos θ ( t i ) − cos ( θ ( t i ) + w i ⋅ ( t − t i ) ) ) , w i ≠ 0 v i sin ( θ ( t i ) ) ⋅ t , w i = 0 (9) F_{y}^{i}\left(t_{i}\right)=\left\{\begin{array}{l} -\frac{v_{i}}{w_{i}}\left(\cos \theta\left(t_{i}\right)-\cos \left(\theta\left(t_{i}\right)+w_{i} \cdot\left(t-t_{i}\right)\right)\right), w_{i} \neq 0 \\ v_{i} \sin \left(\theta\left(t_{i}\right)\right) \cdot t, w_{i}=0 \end{array}\right. \tag{9} Fyi(ti)={−wivi(cosθ(ti)−cos(θ(ti)+wi⋅(t−ti))),wi=0visin(θ(ti))⋅t,wi=0(9)
当 w i = 0 w_i=0 wi=0 时,机器人行走轨迹为一条直线
当 w i ≠ 0 w_i \not = 0 wi=0 时,机器人轨迹为圆弧,设:
M x i = − v i w i ⋅ sin θ ( t i ) (10) M_{x}^{i}=-\frac{v_{i}}{w_{i}} \cdot \sin \theta\left(t_{i}\right) \tag{10} Mxi=−wivi⋅sinθ(ti)(10)
M y i = v i w i ⋅ cos θ ( t − i ) (11) M_{y}^{i}=\frac{v_{i}}{w_{i}} \cdot \cos \theta(t-i) \tag{11} Myi=wivi⋅cosθ(t−i)(11)
然后,奇妙的事情就发生了,我们可以得到这样一个式子:
( F x i − M x i ) 2 + ( F x i − M x i ) 2 = ( v i w i ) 2 (12) \left(F_{x}^{i}-M_{x}^{i}\right)^{2}+\left(F_{x}^{i}-M_{x}^{i}\right)^{2}=\left(\frac{v_{i}}{w_{i}}\right)^{2} \tag{12} (Fxi−Mxi)2+(Fxi−Mxi)2=(wivi)2(12)
这个式子就是圆在平面的公式,其中这个圆的圆心为 ( M x i , M y i ) (M^i_x,M^i_y) (Mxi,Myi),半径为 v i w i \frac{v_i}{w_i} wivi。根据上述公式可以求出机器人的轨迹,即通过一系列分段的圆弧和直线来拟合轨迹。
将机器人轨迹进行分段会在控制点之间产生线性误差,即 t i + 1 − t i t_{i+1}−t_i ti+1−ti之间的误差,设x坐标和y坐标的误差分别为 E x i E^i_x Exi和 E y i E^i_y Eyi, Δ t i = t i + 1 − t i \Delta t_i=t_{i+1}−t_i Δti=ti+1−ti,由于 i ∈ [ v ( t i ) , v ( t i + 1 ) ] i∈[v(t_i),v(t_{i+1})] i∈[v(ti),v(ti+1)],故最大误差 E x i , E y i ≤ ∣ v ( t i + 1 ) − v ( t i ) ∣ ⋅ Δ t i E^i_x,E^i_y≤|v(t_{i+1})−v(t_i)|\cdot \Delta t_i Exi,Eyi≤∣v(ti+1)−v(ti)∣⋅Δti,在 Δ t i \Delta t_i Δti内是线性的。注意该上界误差仅仅可用于机器人内部预测,而实际机器人位置一般通过里程计测量。
动态窗口法在速度空间中进行速度采样,并对随机采样的速度进行限制,减小采样数目,在使用代价函数进行评价。
根据以下三点进行速度空间降采样
代价函数方程:
G ( v , w ) = σ ( α ⋅ heading ( v , w ) + β ⋅ dist ( v , w ) + γ ⋅ vel ( v , w ) ) (13) G(v,w)=\sigma(\alpha \cdot \text{heading}(v,w)+\beta \cdot \text{dist}(v,w) + \gamma \cdot \text{vel}(v,w)) \tag{13} G(v,w)=σ(α⋅heading(v,w)+β⋅dist(v,w)+γ⋅vel(v,w))(13)
最大值即使最优值最大,其中 σ \sigma σ使得三个部分的权重更加平滑,使得轨迹与障碍物之间保持一定的间隙。
Target heading: heading用于评价机器人与目标位置的夹角,当机器人朝着目标前进时,该值取最大;举个例子:表示机器人与目标点的对齐程度,用 180 − θ 180−θ 180−θ表示, θ θ θ为机器人与目标夹角,夹角越大,代价值越小。
Clearance: dist 用于表示与机器人轨迹相交的最近的障碍物距离;如果障碍物与机器人轨迹不相交,则设为一个较大的值
Velocity: vel 表示机器人的前向移动速度,支持快速移动
机器人能够在撞掉障碍物之前停下, dist ( v , w ) \text{dist}(v,w) dist(v,w) 为机器人轨迹上与障碍物的最近距离,设刹车时的加速度为 v ˙ b , w ˙ b \dot v_b, \dot w_b v˙b,w˙b ,则 V a V_a Va 为机器人不与障碍物碰撞的速度集合:
V a = { v , w ∣ v ≤ 2 ⋅ dist ( v , w ) ⋅ v ˙ b ∩ w ≤ 2 ∗ dist ( v , w ) ∗ w ˙ b } V_{a}=\left\{v, w \mid v \leq \sqrt{2 \cdot \text{dist}(v, w) \cdot \dot{v}_{b}} \cap w \leq \sqrt{2 * \text{dist}(v, w) * \dot{w}_{b}}\right\} Va={v,w∣v≤2⋅dist(v,w)⋅v˙b∩w≤2∗dist(v,w)∗w˙b}
考虑到机器人的动力加速度,搜索空间降采样到动态窗口,只保留以当前加速度可到达的速度,设 t t t为时间间隔, ( v a , w a ) (v_a,w_a) (va,wa)为实际速度,则动态窗口的速度集合为 V d V_d Vd:该集合以外的速度都不能在该时间间隔内达到。
V d = { v , w ∣ v ∈ [ v a − v ˙ ⋅ t , v a + v ˙ ⋅ t ] ∩ w ∈ [ w a − w ˙ ⋅ t , w a + w ˙ ⋅ t ] } V_{d}=\left\{v, w \mid v \in\left[v_{a}-\dot{v} \cdot t, v_{a}+\dot{v} \cdot t\right] \cap w \in\left[w_{a}-\dot{w} \cdot t, w_{a}+\dot{w} \cdot t\right]\right\} Vd={v,w∣v∈[va−v˙⋅t,va+v˙⋅t]∩w∈[wa−w˙⋅t,wa+w˙⋅t]}
综上,最终的搜索空间:
V r = V s ∩ V a ∩ V d V_{r}=V_{s} \cap V_{a} \cap V_{d} Vr=Vs∩Va∩Vd
最终的代价函数的限制就是在这个搜索空间内的
平滑处理:
评价函数的三个部分都被正则化在 [ 0 , 1 ] [0,1] [0,1]上,实验中设置了 α = 2 α=2 α=2, β = 0.2 β=0.2 β=0.2, γ = 0.2 γ=0.2 γ=0.2,平滑处理可以使机器人与障碍物之间有一定的间隙(裕度)。
实现细节:
参数设定:
这里先说一下整体的思路,主要是对着参考的Python代码的,有空我把ROS_wiki那边也补充完整
主要是根据现在的状态和默认的参数进行的设置:
def calc_dynamic_window(x, config):
"""
calculation dynamic window based on current state x
"""
# Dynamic window from robot specification
Vs = [config.min_speed, config.max_speed,
-config.max_yaw_rate, config.max_yaw_rate]
# Dynamic window from motion model
Vd = [x[3] - config.max_accel * config.dt,
x[3] + config.max_accel * config.dt,
x[4] - config.max_delta_yaw_rate * config.dt,
x[4] + config.max_delta_yaw_rate * config.dt]
# [v_min, v_max, yaw_rate_min, yaw_rate_max]
dw = [max(Vs[0], Vd[0]), min(Vs[1], Vd[1]),
max(Vs[2], Vd[2]), min(Vs[3], Vd[3])]
return dw
可以从中看出 主要是速度的整个窗口,读取机器人的默认速度配置,然后计算加速度时间后的速度取最小最大的,形成一个取速度的范围
根据现在的状态和前面的速度动态窗口,配置中的速度分辨率(就是以多少间隔生成速度值)
# evaluate all trajectory with sampled input in dynamic window
# for v in np.arange(dw[0], dw[1], config.v_resolution):
# for y in np.arange(dw[2], dw[3], config.yaw_rate_resolution):
# trajectory = predict_trajectory(x_init, v, y, config)
def predict_trajectory(x_init, v, y, config):
"""
predict trajectory with an input
"""
x = np.array(x_init)
trajectory = np.array(x)
time = 0
while time <= config.predict_time:
x = motion(x, [v, y], config.dt)
trajectory = np.vstack((trajectory, x))
time += config.dt
return trajectory
其中motion就是由车辆运动的模型得出来的这时刻状态加速度得到下一时刻的状态,根据前面可知:v
是速度,y
是yaw角速度,x
的各个位置为: [ x , y , θ , v , ω ] [x,y,\theta,v,\omega] [x,y,θ,v,ω]
def motion(x, u, dt):
"""
motion model
"""
x[2] += u[1] * dt # v=v+a*t
x[0] += u[0] * math.cos(x[2]) * dt # x=x+v*cos(theta)
x[1] += u[0] * math.sin(x[2]) * dt # y=y+v*sin(theta)
x[3] = u[0]
x[4] = u[1]
return x
这个cost也就是前面最优化里面提到的heading cost,朝向
def calc_to_goal_cost(trajectory, goal):
"""
calc to goal cost with angle difference
"""
dx = goal[0] - trajectory[-1, 0]
dy = goal[1] - trajectory[-1, 1]
error_angle = math.atan2(dy, dx)
cost_angle = error_angle - trajectory[-1, 2]
cost = abs(math.atan2(math.sin(cost_angle), math.cos(cost_angle)))
return cost
速度cost之间是根据得出的轨迹的速度和最大速度做个差值再乘一个速度cost的系数得出来的
speed_cost = config.speed_cost_gain * (config.max_speed - trajectory[-1, 3])
这么一看 果然还是圆形比较适合做碰撞检测,怪不得杰哥中间也是将车膨胀成两个圆
def calc_obstacle_cost(trajectory, ob, config):
"""
calc obstacle cost inf: collision
"""
ox = ob[:, 0]
oy = ob[:, 1]
dx = trajectory[:, 0] - ox[:, None]
dy = trajectory[:, 1] - oy[:, None]
r = np.hypot(dx, dy)
if config.robot_type == RobotType.rectangle:
yaw = trajectory[:, 2]
rot = np.array([[np.cos(yaw), -np.sin(yaw)], [np.sin(yaw), np.cos(yaw)]])
rot = np.transpose(rot, [2, 0, 1])
local_ob = ob[:, None] - trajectory[:, 0:2]
local_ob = local_ob.reshape(-1, local_ob.shape[-1])
local_ob = np.array([local_ob @ x for x in rot])
local_ob = local_ob.reshape(-1, local_ob.shape[-1])
upper_check = local_ob[:, 0] <= config.robot_length / 2
right_check = local_ob[:, 1] <= config.robot_width / 2
bottom_check = local_ob[:, 0] >= -config.robot_length / 2
left_check = local_ob[:, 1] >= -config.robot_width / 2
if (np.logical_and(np.logical_and(upper_check, right_check),
np.logical_and(bottom_check, left_check))).any():
return float("Inf")
elif config.robot_type == RobotType.circle:
if np.array(r <= config.robot_radius).any():
return float("Inf")
min_r = np.min(r)
return 1.0 / min_r # OK
这一步直接计算了所有的障碍物的,也得益于python的方便,直接计算轨迹与各个障碍物的直线距离,判断是否对于圆半径,只要有一个障碍物碰到了 这条轨迹就被抛弃了,如果没有的话 输出最小的那个距离,然后分之一进行输出以便计算cost
综合下来就是:
# calc cost
to_goal_cost = config.to_goal_cost_gain * calc_to_goal_cost(trajectory, goal)
speed_cost = config.speed_cost_gain * (config.max_speed - trajectory[-1, 3])
ob_cost = config.obstacle_cost_gain * calc_obstacle_cost(trajectory, ob, config)
final_cost = to_goal_cost + speed_cost + ob_cost
在原基础上,把其他速度窗口内的轨迹也画出来了,但是因为选cost最小的也就是红色那个哈
这么一看我好像知道我曾经用ROS DWA那边有啥问题了 刷新路径dt太快了?走一下发现下一步的轨迹需要左移,然后右移,然后左移 emmm 这样就S形了?或者是把障碍物的cost因子调低一点?好像是看到了就躲一下,进了再躲一下;然后再贴一下ROS那边的(我找到了我本科毕设答辩的PPT截图出来的 hhh)其实当时我答辩录屏了,但是当时录制的时候忘记点语音了 然后就是无声的,hhh 留个留念的机会都没有了
这么一看 比frenet简单太多了 hhhh 毕竟是1997年的了,frenet都是2010年的了:Frenet 博客园的论文阅读与代码实例
主要在于:
至此,再次“复习”完一个,这么看当初问阿冰哥的答案也能更懂了: