使用路径规划可以得到一系列的路径点,如下图所示有5个路径点,这条路径是很曲折的,这不利于机器人的移动,试想一个机器人直接沿着这条路径进行移动,当到达一个路径点后速度减到零,原地旋转,指向下一个路径点,再继续移动,这不仅智障,而且浪费能量。于是我们就想,能不能规划出一条轨迹,使机器人能够在每一个路径点处能够平滑过渡,从而表现得机灵一点。这就是轨迹规划问题。
路径规划得到的路径点 | 轨迹优化得到的轨迹(橙色) |
二次规划(Quadratic Programming, QP)
由于轨迹规划问题可以归结为一个二次规划问题,所以这里先对二次规划进行一个简单的介绍
二次型
含有个变量的二次齐次函数:
称为二次型。取,则有,上式可表示为:
矩阵表示为:
二次规划问题
当目标函数为二次型,且约束为线性约束时,该优化问题就是二次规划问题,一般形式表述如下:
二次规划是一类凸优化问题,目前有很多商业或者开源的求解器来求解这类问题,不再赘述
轨迹表示形式
使用路径规划可以得到一系列的路径点,这些路径点是不带时间信息的,而轨迹则是时间的函数,一般用阶多项式表示,即:
其中是轨迹参数,也是我们的优化参数。将写成向量相乘形式,得到:
对轨迹函数求导,还能写出速度、加速度、jerk等参数随时间变化的函数:
其中:,概括得到轨迹导数的通式,表示阶导数:
一段复杂的轨迹常常需要用多个多项式来表示,可以将完整轨迹按照时间划分成多段,如下,将完整轨迹划分为段多项式轨迹:
其中:表示第段轨迹的第个参数,每段轨迹的时长都是预先分配好的,分配方法以后再叙述,本文暂时设置为固定值。
Minimum-jerk
以下将以Minimum-jerk算法为例来介绍轨迹优化算法的设计流程,Minimum-snap等算法的设计流程与之同理。
Minimum-jerk,顾名思义就是求解每段轨迹的系数使得总的jerk最小,同时还要满足约束条件,所以这是一个带约束的最优化问题,通过后面的证明知道,它其实是一个二次规划问题。
多项式阶次选择
jerk是轨迹的3阶导数,所以令。Minimum-jerk会直接对首尾的位置、速度和加速度进行约束,这就有6个等式约束了,所以优化参数必须提供6个以上的自由度,而5阶多项式有6个系数,所以符合要求的多项式的最小阶次为 。同理Minimum-snap还会对首尾的jerk进行约束,所以Minimum-snap的多项式最小阶数为
因此可以选择五阶多项式来表示每一段轨迹,得到:
其中包含段多项式轨迹,每一段多项式轨迹有个系数,一共有个系数
构造目标函数
可表示为:
要使其最小,也就是使在整段时间上的积分最小,在minimum-jerk中选择使的2-范数最小即:
所以目标函数定义如下:
令,对求平方,得到:
令,得到:
所以的积分为:
令的积分为矩阵,则:
最终的目标函数可表述为:
其中:
- 是优化变量,它是每一段多项式轨迹系数的列组合,其维数为。
- Q是一个维数为的实对称矩阵
显然,是一个二次型,所以基于Minimum-jerk的轨迹优化问题可以转换为一个二次规划问题。
以上定义了Minimum-jerk中的目标函数,接下来添加约束。如果不考虑障碍物,主要有两类约束,一类是导数约束(Derivative Constraint),它约束了轨迹的初始状态和终止状态,以及每一段轨迹的开始/结束位置,也就是用路径规划得到的路径点对轨迹进行约束;另一类约束是连续性约束(Continuity Constraint),它可以使相邻轨迹平滑过渡。
导数约束
包含以下约束:
- 初始状态和终止状态约束,如位置、速度、加速度等状态,即:。
- 每一段轨迹的初始位置由路径点给出,也即:,表示第段轨迹,取值为。
其中表示路径规划得到的路径点。
对于有个路径点的轨迹,一共有个导数约束
连续性约束
在两段轨迹的连接点处,我们希望这个点处的轨迹是平滑的,可以通过施加连续性约束来实现这个要求,也就是使相邻两段轨迹在连接点处的阶导数分别相等,即:
其中表示第段轨迹,取值为,对于有个路径点的轨迹,一共有段多项式轨迹,其中的连续性约束一共有个。
对于Minimum-jerk,要求每一段轨迹连接处的位置、速度、加速度一致,也就是:
编程实现
使用Python实现一维的Minimum-jerk算法,其中求解二次规划问题需要用到cvxopt
库,安装方法:
pip install cvxopt
完整程序如下:
# -*- coding: utf-8 -*-
"""
Created on Fri Dec 10 2021
一维轨迹优化
@author: ghowoght
"""
#%% 生成路径点
import numpy as np
import matplotlib.pyplot as plt
path = [[1, 3], [3, 5], [4, 2], [2.5, 1.2], [2, -2.5]]
path = np.array(path)
# plt.plot(path[:, 0], path[:, 1])
# plt.plot(path[:, 0], path[:, 1], "*")
# plt.show()
#%% 一维轨迹优化
x = path[:, 0]
# 分配时间
deltaT = 2 # 每一段2秒
T = np.linspace(0, deltaT * (len(x) - 1), len(x))
########### 目标函数 ###########
######## 1/2xTQx + qTx ########
K = 3 # jerk为3阶导数,取K=3
n_order = 2 * K - 1 # 多项式阶数
M = len(x) - 1 # 轨迹的段数
N = M * (n_order + 1) # 矩阵Q的维数
def getQk(T_down, T_up):
Q = np.zeros((6, 6))
Q[3][4] = 72 * (T_up**2 - T_down**2)
Q[3][5] = 120 * (T_up**3 - T_down**3)
Q[4][5] = 360 * (T_up**4 - T_down**4)
Q = Q + Q.T # Q为对称矩阵
Q[3][3] = 36 * (T_up**1 - T_down**1)
Q[4][4] = 192 * (T_up**3 - T_down**3)
Q[5][5] = 720 * (T_up**5 - T_down**5)
return Q
Q = np.zeros((N, N))
for k in range(1, M + 1):
Qk = getQk(T[k - 1], T[k])
Q[(6 * (k - 1)) : (6 * k), (6 * (k - 1)) : (6 * k)] = Qk
Q = Q * 2 # 因为标准目标函数为: 1/2xTQx + qTx,所以要乘2
########### 约束 ###########
# 1.导数约束 Derivative Constraint
A0 = np.zeros((2 * K + M - 1, N))
b0 = np.zeros(len(A0))
# 添加首末状态约束(包括位置、速度、加速度)
for k in range(K):
for i in range(k, 6):
c = 1
for j in range(k):
c *= (i - j)
A0[0 + k * 2][i] = c * T[0]**(i - k)
A0[1 + k * 2][(M - 1) * 6 + i] = c * T[M]**(i - k)
b0[0] = x[0]
b0[1] = x[M]
# 添加每段轨迹的初始位置约束
for m in range(1, M):
for i in range(6):
A0[6 + m - 1][m * 6 + i] = T[m]**i
b0[6 + m - 1] = x[m]
# 2.连续性约束 Continuity Constraint
A1 = np.zeros(((M - 1) * 3, N))
b1 = np.zeros(len(A1))
for m in range(M - 1):
for k in range(3): # 最多两阶导数相等
for i in range(k, 6):
c = 1
for j in range(k):
c *= (i - j)
index = m * 3 + k
A1[index][m * 6 + i] = c * T[m + 1]**(i - k)
A1[index][(m + 1)* 6 + i] = -c * T[m + 1]**(i - k)
A = np.vstack((A0, A1))
b = np.hstack((b0, b1))
#%% 解二次规划问题
from cvxopt import matrix, solvers
# 目标函数
Q = matrix(Q)
q = matrix(np.zeros(N))
# 等式约束: Ax = b
A = matrix(A)
b = matrix(b)
result = solvers.qp(Q, q, A=A, b=b)
p_coff = np.asarray(result['x']).flatten()
#%% 可视化x轴优化结果
Pos = []
Vel = []
Acc = []
for k in range(M):
t = np.linspace(T[k], T[k + 1], 100)
t_pos = np.vstack((t**0, t**1, t**2, t**3, t**4, t**5))
t_vel = np.vstack((t*0, t**0, 2 * t**1, 3 * t**2, 4 * t**3, 5 * t**4))
t_acc = np.vstack((t*0, t*0, 2 * t**0, 3 * 2 * t**1, 4 * 3 * t**2, 5 * 4 * t**3))
coef = p_coff[k * 6 : (k + 1) * 6]
coef = np.reshape(coef, (1, 6))
pos = coef.dot(t_pos)
vel = coef.dot(t_vel)
acc = coef.dot(t_acc)
Pos.append([t, pos[0]])
Vel.append([t, vel[0]])
Acc.append([t, acc[0]])
Pos = np.array(Pos)
Vel = np.array(Vel)
Acc = np.array(Acc)
plt.subplot(3, 1, 1)
plt.plot(Pos[:, 0, :].T, Pos[:, 1, :].T)
# plt.title("position")
plt.xlabel("time(s)")
plt.ylabel("position(m)")
plt.subplot(3, 1, 2)
plt.plot(Vel[:, 0, :].T, Vel[:, 1, :].T)
# plt.title("velocity")
plt.xlabel("time(s)")
plt.ylabel("velocity(m/s)")
plt.subplot(3, 1, 3)
plt.plot(Acc[:, 0, :].T, Acc[:, 1, :].T)
# plt.title("accel")
plt.xlabel("time(s)")
plt.ylabel("accel(m/s^2)")
plt.show()
x轴上的规划结果如下,可以看到生成轨迹的位置、速度和加速度曲线都是连续的。
分别求X轴和Y轴方向的轨迹,合并后的结果如下图,程序见这里