之前学习了纯追踪算法和PID算法在轨迹跟踪上的应用,现在学习Stanley算法。
前轮反馈控制(Front wheel feedback)也就是常说的Stanley方法,其核心思想是基于车辆前轴中心点的路径跟踪偏差量对方向盘转向控制量进行计算。
Stanley方法是一种基于横向跟踪误差(前轴中心到最近路径点的距离,注意,这里横向跟踪误差的定义与前文pure pursuit和PID中的轨迹跟踪定义的不太一样,后两者是基于后轴中心定义的)的非线性反馈函数,并且能实现横向跟踪误差指数收敛于0。
前轮转角控制变量 δ \delta δ由两部分构成:
根据上图展示的几何关系,易得
δ = θ 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)
横向误差的变化率为
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+ey2ey(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)2key(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 决定了收敛速度。
设车辆运动学模型为以后轴中心为车辆中心的单车运动学模型(具体可参考笔者之前的博客),即
{ 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(ψ)ψ˙=LVtanδfV˙=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
k=0.2 # 增益系数
dt=0.1 # 时间间隔,单位:s
L=2 # 车辆轴距,单位:m
v = 2 # 初始速度
x_0=0 # 初始x
y_0=-3 #初始y
psi_0=0 # 初始航向角
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
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
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]
# 计算横向误差e_y,参考自https://blog.csdn.net/renyushuai900/article/details/98460758
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)
# 通过公式(5)计算转角,符号保持一致
psi = robot_state[2]
v = robot_state[3]
# psi_t的计算我看还有直接这么计算的
# psi_t = math.atan2(current_ref_point[1]-robot_state[1],current_ref_point[0]-robot_state[0])
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
from celluloid import Camera # 保存动图时用,pip install celluloid
# set reference trajectory
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)
# plt.ylim([-3,3])
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) # 加速度设为0,恒速
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.axis("equal")
plt.grid(True)
plt.pause(0.001)
# camera.snap()
# animation = camera.animate()
# animation.save('trajectory.gif')
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仓库
Pure pursuit 与 Stanley 两个方法都是基于对前轮转角进行控制来消除横向误差。
Pure pursuit算法的关键在于预瞄点的选取:其距离越短,控制精度越高,但可能会产生震荡;预瞄距离越长,控制效果趋于平滑,震荡减弱。实际调试只需根据上述规则以及应用需求调整预瞄系数即可。
Stanley 算法的控制效果取决于控制增益,它缺少Pure pursuit算法的规律性,调试时需要花一定精力去寻找合适的控制增益值。