参考资料
1. Stanley算法
之前学习了纯追踪算法和PID算法在轨迹跟踪上的应用,现在学习Stanley算法。
1.1 算法思想
前轮反馈控制(Front wheel feedback)也就是常说的Stanley方法,其核心思想是基于车辆前轴中心点的路径跟踪偏差量对方向盘转向控制量进行计算。
Stanley方法是一种基于横向跟踪误差(前轴中心到最近路径点的距离,注意,这里横向跟踪误差的定义与前文pure pursuit和PID中的轨迹跟踪定义的不太一样,后两者是基于后轴中心定义的)的非线性反馈函数,并且能实现横向跟踪误差指数收敛于0。
1.2 公式推导
前轮转角控制变量
δ
\delta
δ由两部分构成:
- 一部分是航向误差引起的转角,即当前车身方向与参考轨迹最近点的切线方向的夹角
θ
e
\theta_e
θe?;
- 另一部分是横向误差引起的转角,即前轮速度方向与参考轨迹最近点的切线方向所成的夹角
δ
e
\delta_e
δe?。
根据上图展示的几何关系,易得
δ
=
θ
e
+
δ
e
(1)
\tag{1} \delta =\theta_e+\delta_e
δ=θe?+δe?(1)
显然,
δ
e
\delta_e
δe?满足
δ
e
=
arctan
?
k
e
y
d
=
arcsin
?
k
e
y
v
(2)
\tag{2} \delta_e= \arctan{\frac{ke_y}{d}}= \arcsin{\frac{ke_y}{v}}
δe?=arctandkey??=arcsinvkey??(2) 其中,
d
d
d是与车速
v
v
v相关的。
这里值得注意的是,
δ
e
\delta_e
δe?在许多论文中都是表示成
δ
e
=
arctan
?
k
e
y
v
(3)
\tag{3} \delta_e = \arctan{\frac{ke_y}{v}}
δe?=arctanvkey??(3) 猜测的一个可能原因是小角度假设,当
δ
e
\delta_e
δe?很小时公式(3)是成立的;另外一个可能原因是因为增益系数
k
k
k可以任意选择,当
k
k
k选择恰当时,公式(3)是可以成立的。后面最终表达式也选用公式(3)的形式。
另外,根据几何关系,
θ
e
\theta_e
θe?满足
θ
e
=
ψ
t
?
ψ
(4)
\tag{4} \theta_e = \psi_t-\psi
θe?=ψt??ψ(4) 其中,
ψ
\psi
ψ表示车辆航向角,
ψ
t
\psi_t
ψt?表示离车辆前轴中心最近目标路径点处的航向角。
结合等式(1)(3)(4),得前轮转角控制量
δ
\delta
δ的最终表达式为
δ
=
ψ
t
?
ψ
+
arctan
?
k
e
y
v
(5)
\tag{5} \delta = \psi_t-\psi+\arctan{\frac{ke_y}{v}}
δ=ψt??ψ+arctanvkey??(5)
1.3 横向误差变化率
横向误差的变化率为
e
˙
y
=
?
v
sin
?
δ
e
(6)
\tag{6} \dot e_y = -v\sin{\delta_e}
e˙y?=?vsinδe?(6)
带负号是因为在公式(5)的控制下,横向误差会越来越小,因此横向偏差变化率会有负号。
其中
sin
?
δ
e
\sin \delta_e
sinδe? 根据几何关系可知:
sin
?
δ
e
=
e
y
d
(
t
)
2
+
e
y
2
(7-1)
\tag{7-1} \sin \delta_{e}=\frac{e_{y}}{\sqrt{d(t)^{2}+e_{y}^{2}}}
sinδe?=d(t)2+ey2?
?ey??(7-1) 根据公式(3),进一步也有
sin
?
δ
e
=
k
e
y
v
2
+
(
k
e
y
)
2
(7-2)
\tag{7-2} \sin \delta_{e}=\frac{k e_{y}}{\sqrt{v^{2}+\left(k e_{y}\right)^{2}}}
sinδe?=v2+(key?)2
?key??(7-2)
故有:
e
˙
y
=
?
k
v
e
y
v
2
+
(
k
e
y
)
2
=
?
k
e
y
1
+
(
k
e
y
v
)
2
(8)
\tag{8} \dot{e}_{y}=\frac{-k v e_{y}}{\sqrt{v^{2}+\left(k e_{y}\right)^{2}}}=\frac{-k e_{y}}{\sqrt{1+\left(\frac{k e_{y}}{v}\right)^{2}}}
e˙y?=v2+(key?)2
??kvey??=1+(vkey??)2
??key??(8) 当横向跟踪误差
e
y
e_{y}
ey? 很小时,上式改写为:
e
˙
y
≈
?
k
e
y
(9)
\tag{9} \dot{e}_{y} \approx-k e_{y}
e˙y?≈?key?(9) 积分上式(一阶线性微分方程的求解),得:
e
y
(
t
)
=
e
y
(
0
)
×
e
?
k
t
(10)
\tag{10} e_{y}(t)=e_{y}(0) \times e^{-k t}
ey?(t)=ey?(0)×e?kt(10) 因此,当
t
→
∞
t\rightarrow\infty
t→∞时,横向误差以指数形式收敛于 0,参数
k
k
k 决定了收敛速度。
1.4 算法伪代码
- 输入:当前车辆位置、航向角
ψ
\psi
ψ、速度
v
v
v?,当前目标路点和离车辆前轴中心最近目标路径点处的航向角
ψ
t
\psi_t
ψt?
- 计算:
- 计算横向误差
e
y
e_y
ey?(这一步很重要)
- 计算
δ
=
ψ
t
?
ψ
+
arctan
?
k
e
y
v
\delta = \psi_t-\psi+\arctan{\frac{ke_y}{v}}
δ=ψt??ψ+arctanvkey??
- 输出:前轮转角控制量
δ
\delta
δ
2. python代码实现
2.1 车辆模型
设车辆运动学模型为以后轴中心为车辆中心的单车运动学模型(具体可参考笔者之前的博客),即
{
x
˙
=
V
cos
?
(
ψ
)
y
˙
=
V
sin
?
(
ψ
)
ψ
˙
=
V
L
tan
?
δ
f
V
˙
=
a
\left\{\begin{array}{l} \dot{x}=V \cos (\psi) \\ \dot{y}=V \sin (\psi) \\ \dot{\psi}=\frac{V}{L}\tan{\delta_f}\\ \dot{V}=a \end{array}\right.
????????x˙=Vcos(ψ)y˙?=Vsin(ψ)ψ˙?=LV?tanδf?V˙=a? python实现代码如下。
import math
class KinematicModel_3:
"""假设控制量为转向角delta_f和加速度a
"""
def __init__(self, x, y, psi, v, L, dt):
self.x = x
self.y = y
self.psi = psi
self.v = v
self.L = L
self.dt = dt
def update_state(self, a, delta_f):
self.x = self.x+self.v*math.cos(self.psi)*self.dt
self.y = self.y+self.v*math.sin(self.psi)*self.dt
self.psi = self.psi+self.v/self.L*math.tan(delta_f)*self.dt
self.v = self.v+a*self.dt
def get_state(self):
return self.x, self.y, self.psi, self.v
2.2 相关参数设置
k=0.2
dt=0.1
L=2
v = 2
x_0=0
y_0=-3
psi_0=0
2.3 搜索目标临近点
def cal_target_index(robot_state, refer_path):
"""得到临近的路点
Args:
robot_state (_type_): 当前车辆位置
refer_path (_type_): 参考轨迹(数组)
Returns:
_type_: 最近的路点的索引
"""
dists = []
for xy in refer_path:
dis = np.linalg.norm(robot_state-xy)
dists.append(dis)
min_index = np.argmin(dists)
return min_index
2.4 角度归一化
def normalize_angle(angle):
"""
Normalize an angle to [-pi, pi].
:param angle: (float)
:return: (float) Angle in radian in [-pi, pi]
copied from https://atsushisakai.github.io/PythonRobotics/modules/path_tracking/stanley_control/stanley_control.html
"""
while angle > np.pi:
angle -= 2.0 * np.pi
while angle < -np.pi:
angle += 2.0 * np.pi
return angle
2.5 Stanley 算法实现
def stanley_control(robot_state,refer_path, refer_path_psi):
"""stanley控制
Args:
robot_state (_type_): 机器人位姿,包括x,y,yaw,v
refer_path (_type_): 参考轨迹的位置
refer_path_psi (_type_): 参考轨迹上点的切线方向的角度
last_target_index (_type_): 上一个目标临近点
Returns:
_type_: _description_
"""
current_target_index = cal_target_index(robot_state[0:2],refer_path)
if current_target_index>=len(refer_path):
current_target_index=len(refer_path)-1
current_ref_point = refer_path[-1]
psi_t = refer_path_psi[-1]
else:
current_ref_point=refer_path[current_target_index]
psi_t = refer_path_psi[current_target_index]
if(robot_state[0]-current_ref_point[0])*psi_t-(robot_state[1]-current_ref_point[1])>0:
e_y=np.linalg.norm(robot_state[0:2]-current_ref_point)
else:
e_y = -np.linalg.norm(robot_state[0:2]-current_ref_point)
psi = robot_state[2]
v = robot_state[3]
theta_e = psi_t-psi
delta_e = math.atan2(k*e_y,v)
delta = normalize_angle(theta_e+delta_e)
return delta,current_target_index
2.6 主函数
from celluloid import Camera
refer_path = np.zeros((1000, 2))
refer_path[:, 0] = np.linspace(0, 100, 1000)
refer_path[:, 1] = 2*np.sin(refer_path[:, 0]/3.0) + 2.5*np.cos(refer_path[:, 0]/2.0)
refer_path_psi = [math.atan2(refer_path[i+1,1]-refer_path[i,1],refer_path[i+1,0]-refer_path[i,0]) for i in range(len(refer_path)-1)]
ugv = KinematicModel_3(x_0, y_0, psi_0, v, L, dt)
x_ = []
y_ = []
fig = plt.figure(1)
camera = Camera(fig)
for i in range(500):
robot_state = np.zeros(4)
robot_state[0] = ugv.x
robot_state[1] = ugv.y
robot_state[2]=ugv.psi
robot_state[3]=ugv.v
delta,ind = stanley_control(robot_state,refer_path,refer_path_psi)
ugv.update_state(0, delta)
x_.append(ugv.x)
y_.append(ugv.y)
plt.cla()
plt.plot(refer_path[:, 0], refer_path[:, 1], '-.b', linewidth=1.0)
plt.plot(x_, y_, "-r", label="trajectory")
plt.plot(refer_path[ind,0], refer_path[ind,1], "go", label="target")
plt.grid(True)
plt.pause(0.001)
plt.figure(2)
plt.plot(refer_path[:, 0], refer_path[:, 1], '-.b', linewidth=1.0)
plt.plot(x_, y_, 'r')
plt.show()
跟踪正弦曲线,效果如下:
跟踪非正弦曲线,效果如下:
可见效果不是很好,猜测原因有三:一个原因可能是控制增益没有调好;第二,也可能因为我计算目标路点的切线角度(航向角
ψ
t
\psi_t
ψt?)时用的是近似计算的;最后,有可能因为我计算横向误差时方式不太对,代码中的横向误差计算是参考别人的,目前也不是很理解那个判断是怎么来的,有知道怎么计算横向误差的朋友们可以在评论区解个惑呀。谢谢~~
跟踪直线的效果还是挺好的
完整python代码文件见github仓库
3. 小结:Pure pursuit与Stanley 算法简单对比
Pure pursuit 与 Stanley 两个方法都是基于对前轮转角进行控制来消除横向误差。
Pure pursuit算法的关键在于预瞄点的选取:其距离越短,控制精度越高,但可能会产生震荡;预瞄距离越长,控制效果趋于平滑,震荡减弱。实际调试只需根据上述规则以及应用需求调整预瞄系数即可。
Stanley 算法的控制效果取决于控制增益,它缺少Pure pursuit算法的规律性,调试时需要花一定精力去寻找合适的控制增益值。
|