四大门户网站创始人李守洪
四大门户网站创始人,李守洪,陕西网站开发公司电话,网站成功上线报道机器人轨迹规划实战#xff1a;从三次多项式到高阶插值的完整实现#xff08;附Python代码#xff09;
最近在调试一个六轴机械臂的拾放动作时#xff0c;我遇到了一个典型问题#xff1a;机械臂在路径点之间移动时#xff0c;关节运动不够平滑#xff0c;导致末端执行器…机器人轨迹规划实战从三次多项式到高阶插值的完整实现附Python代码最近在调试一个六轴机械臂的拾放动作时我遇到了一个典型问题机械臂在路径点之间移动时关节运动不够平滑导致末端执行器在关键点有明显的抖动甚至引发了轻微的共振。这让我重新审视了轨迹规划的核心——插值算法。很多开发者包括一些有经验的工程师往往只关注逆运动学解算的精度却忽略了连接这些解的“桥梁”是否足够平顺。关节空间的轨迹规划正是这座桥梁的建造蓝图。它不直接告诉你末端走到了哪里而是精细地规划每个电机关节应该如何随时间转动才能让末端既准又稳地到达目标。今天我们就抛开抽象的理论直接进入代码层面用Python从最基础的三次多项式开始一步步构建出能满足复杂约束的高阶插值方案并深入探讨其平滑性与计算效率的权衡。1. 轨迹规划的核心在关节空间构建平滑函数当我们谈论机器人从A点移动到B点时新手最容易陷入的误区是只关心A和B这两个位置。实际上轨迹Trajectory描述的是位置随时间的变化它包含了位移、速度、加速度乃至加加速度Jerk的完整信息。而路径Path只是一个空间序列不包含时间。轨迹规划的目标就是为机器人的每个关节找到一个时间函数 θ(t)这个函数及其导数必须满足我们在路径点设定的各种边界条件比如起点和终点的速度、加速度要求。为什么关节空间规划如此重要因为它直接面向驱动单元。想象一下你给伺服驱动器发送的是一系列离散的位置指令。如果这些指令对应的关节角变化不连续或突变电机就会产生冲击轻则影响寿命产生噪音重则导致定位超调甚至损坏机构。关节空间规划的优势在于其计算相对高效且能天然避免笛卡尔空间规划中可能出现的奇异性问题。它的核心思想可以概括为将笛卡尔空间的路径点通过逆运动学转换为关节角度值然后在每个关节的时间轴上用一条光滑的曲线拟合这些离散的关节路径点。这里有一个关键点常被忽视所有关节在同一路径段内的运动时间必须相同。只有这样所有关节才能同时到达各自的路径点从而保证末端执行器的位姿在预定时刻精确达到期望值。规划的本质就是在满足一系列约束条件位置、速度、加速度连续的前提下选择合适的插值函数来连接这些点。2. 基石三次多项式插值及其Python实现三次多项式插值是关节空间轨迹规划的入门砖也是最常用的一种方法。它为什么是“三次”因为它需要满足四个最基本的约束条件起点位置θ₀、终点位置θ_f、起点速度θ̇₀和终点速度θ̇_f。一个三次多项式恰好有四个系数可以完美匹配这四个约束。其标准形式为 θ(t) a₀ a₁t a₂t² a₃t³我们的任务就是求解系数向量[a₀, a₁, a₂, a₃]。根据约束条件我们可以建立以下方程组θ(0) a₀ θ₀ θ(t_f) a₀ a₁*t_f a₂*t_f² a₃*t_f³ θ_f θ̇(0) a₁ θ̇₀ θ̇(t_f) a₁ 2*a₂*t_f 3*a₃*t_f² θ̇_f其中t_f是运动的总时间。这是一个简单的线性方程组我们可以手动推导其解析解也可以用线性代数工具直接求解。下面我们用Python来实现一个通用的三次多项式轨迹生成器。import numpy as np import matplotlib.pyplot as plt def cubic_polynomial_traj(q0, qf, qd0, qdf, tf, num_points100): 生成满足起点和终点位置、速度约束的三次多项式轨迹。 参数: q0: 起始位置 (标量或向量) qf: 终止位置 (标量或向量) qd0: 起始速度 (标量或向量) qdf: 终止速度 (标量或向量) tf: 运动总时间 num_points: 轨迹上采样的点数 返回: t: 时间序列 shape (num_points,) q: 位置序列 shape (num_points,) 或 (num_points, n_joints) qd: 速度序列 shape 同 q qdd: 加速度序列 shape 同 q t np.linspace(0, tf, num_points) # 将输入转换为numpy数组以便向量化运算 q0, qf, qd0, qdf map(np.array, [q0, qf, qd0, qdf]) # 计算三次多项式系数 # 方程组矩阵形式: A * [a0, a1, a2, a3]^T [q0, qf, qd0, qdf]^T # 这里我们直接使用解析解公式 a0 q0 a1 qd0 a2 3*(qf - q0)/tf**2 - (2*qd0 qdf)/tf a3 -2*(qf - q0)/tf**3 (qd0 qdf)/tf**2 # 计算轨迹 t_reshape t.reshape(-1, 1) if q0.ndim 0 else t q a0 a1*t_reshape a2*t_reshape**2 a3*t_reshape**3 qd a1 2*a2*t_reshape 3*a3*t_reshape**2 qdd 2*a2 6*a3*t_reshape return t, q.squeeze(), qd.squeeze(), qdd.squeeze() # 示例单个关节从0度运动到90度起点和终点速度均为0 t, q, qd, qdd cubic_polynomial_traj(q00, qfnp.pi/2, qd00, qdf0, tf2.0) # 绘制轨迹 fig, axes plt.subplots(3, 1, figsize(10, 8)) axes[0].plot(t, q, label位置 (rad)) axes[0].set_ylabel(位置) axes[0].legend() axes[0].grid(True) axes[1].plot(t, qd, label速度 (rad/s), colororange) axes[1].set_ylabel(速度) axes[1].legend() axes[1].grid(True) axes[2].plot(t, qdd, label加速度 (rad/s²), colorgreen) axes[2].set_ylabel(加速度) axes[2].set_xlabel(时间 (s)) axes[2].legend() axes[2].grid(True) plt.suptitle(三次多项式轨迹 (起点/终点速度为零)) plt.tight_layout() plt.show()注意在实际多关节机器人中q0,qf等参数可以是向量例如np.array([0.1, 0.5, -0.2, 0, 0, 0])函数会为每个关节独立计算轨迹并返回矩阵。确保所有关节的tf相同。这个简单的例子展示了最基本的“点到点”运动。但现实中我们经常需要经过多个中间路径点。如何处理一个直观的思路是将整条轨迹分段每一段都用三次多项式连接并在连接点即路径点处强制速度连续。这就是“过程路径点的三次多项式”方法。我们需要为每个路径点指定一个期望的通过速度这个速度可以根据前后路径点的位置用启发式方法如“抛物线过渡”计算也可以由用户指定。def multi_point_cubic_traj(waypoints, t_waypoints, num_points_per_seg50): 生成通过多个路径点的分段三次多项式轨迹。 假设在路径点处速度连续由内部计算或给定。 参数: waypoints: 路径点位置列表每个元素是一个标量或向量长度 N t_waypoints: 路径点对应的时间列表长度 N num_points_per_seg: 每个轨迹段采样的点数 返回: 时间、位置、速度、加速度序列 waypoints np.array(waypoints) N len(waypoints) # 简化处理假设路径点处速度为零最常见情况。更复杂的做法是计算平滑速度。 velocities np.zeros_like(waypoints) # 这里可以替换为更智能的速度规划 all_t, all_q, all_qd, all_qdd [], [], [], [] for i in range(N-1): q0, qf waypoints[i], waypoints[i1] qd0, qdf velocities[i], velocities[i1] tf_seg t_waypoints[i1] - t_waypoints[i] t_seg, q_seg, qd_seg, qdd_seg cubic_polynomial_traj( q0, qf, qd0, qdf, tf_seg, num_points_per_seg ) # 偏移时间 t_seg t_waypoints[i] # 为了避免重复点最后一个点除外 if i N-2: all_t.append(t_seg[:-1]) all_q.append(q_seg[:-1]) all_qd.append(qd_seg[:-1]) all_qdd.append(qdd_seg[:-1]) else: all_t.append(t_seg) all_q.append(q_seg) all_qd.append(qd_seg) all_qdd.append(qdd_seg) return np.concatenate(all_t), np.concatenate(all_q), np.concatenate(all_qd), np.concatenate(all_qdd) # 示例三个路径点 waypoints [0, np.pi/4, np.pi/2] # 三个关节位置 t_waypoints [0, 1.5, 3.0] # 到达各点的时间 t, q, qd, qdd multi_point_cubic_traj(waypoints, t_waypoints)三次多项式解决了位置和速度连续的问题但仔细观察加速度曲线你会发现它在路径点处是不连续的在上面的简化代码中因为速度设为零加速度在路径点处会有跳变。这种加速度的突变会导致加加速度Jerk无穷大从而可能激发机械结构的振动模态。对于高速、高精度或负载敏感的应用这往往是不可接受的。3. 追求极致平滑高阶多项式插值当运动要求更为严苛时例如在半导体或医疗机器人中我们需要对加速度甚至加加速度进行约束。这时三次多项式的四个系数就不够用了必须引入更高阶的多项式。最典型的是五次多项式它拥有六个系数可以满足起点和终点的位置、速度、加速度共六个约束条件。五次多项式的形式为 θ(t) a₀ a₁t a₂t² a₃t³ a₄t⁴ a₅t⁵对应的约束方程组为θ(0) a₀ θ₀ θ(t_f) a₀ a₁*t_f a₂*t_f² a₃*t_f³ a₄*t_f⁴ a₅*t_f⁵ θ_f θ̇(0) a₁ θ̇₀ θ̇(t_f) a₁ 2*a₂*t_f 3*a₃*t_f² 4*a₄*t_f³ 5*a₅*t_f⁴ θ̇_f θ̈(0) 2*a₂ θ̈₀ θ̈(t_f) 2*a₂ 6*a₃*t_f 12*a₄*t_f² 20*a₅*t_f³ θ̈_f我们可以将其写成矩阵形式A * x b来求解系数。下面实现一个五次多项式轨迹生成器。def quintic_polynomial_traj(q0, qf, qd0, qdf, qdd0, qddf, tf, num_points100): 生成满足起点和终点位置、速度、加速度约束的五次多项式轨迹。 参数: q0, qf: 起止位置 qd0, qdf: 起止速度 qdd0, qddf: 起止加速度 tf: 运动总时间 num_points: 采样点数 t np.linspace(0, tf, num_points) q0, qf, qd0, qdf, qdd0, qddf map(np.array, [q0, qf, qd0, qdf, qdd0, qddf]) # 构建矩阵 A 和向量 b # 对于向量输入我们需要为每个关节单独求解。这里利用线性代数批量求解。 # 矩阵 A (6x6) 对于所有关节相同 A np.array([ [1, 0, 0, 0, 0, 0], # t0 位置 [1, tf, tf**2, tf**3, tf**4, tf**5], # ttf 位置 [0, 1, 0, 0, 0, 0], # t0 速度 [0, 1, 2*tf, 3*tf**2, 4*tf**3, 5*tf**4], # ttf 速度 [0, 0, 2, 0, 0, 0], # t0 加速度 [0, 0, 2, 6*tf, 12*tf**2, 20*tf**3] # ttf 加速度 ]) # 对于标量输入 if q0.ndim 0: b np.array([q0, qf, qd0, qdf, qdd0, qddf]) coeff np.linalg.solve(A, b) # 形状 (6,) # 计算轨迹 T np.column_stack([t**0, t**1, t**2, t**3, t**4, t**5]) # (N, 6) q T coeff qd T (coeff * [0, 1, 2, 3, 4, 5]) qdd T (coeff * [0, 0, 2, 6, 12, 20]) else: # 多关节情况b 的形状为 (6, n_joints) n_joints len(q0) b np.column_stack([q0, qf, qd0, qdf, qdd0, qddf]).T # (6, n_joints) coeff np.linalg.solve(A, b) # (6, n_joints) # 计算轨迹 T np.column_stack([t**0, t**1, t**2, t**3, t**4, t**5]) # (N, 6) q T coeff # (N, n_joints) qd T (coeff * np.array([0, 1, 2, 3, 4, 5]).reshape(-1,1)) qdd T (coeff * np.array([0, 0, 2, 6, 12, 20]).reshape(-1,1)) return t, q.squeeze(), qd.squeeze(), qdd.squeeze() # 对比三次和五次多项式 tf 2.0 # 三次起点终点速度为零 t_cubic, q_c, qd_c, qdd_c cubic_polynomial_traj(0, np.pi/2, 0, 0, tf) # 五次额外要求起点终点加速度为零 t_quintic, q_q, qd_q, qdd_q quintic_polynomial_traj(0, np.pi/2, 0, 0, 0, 0, tf) fig, axes plt.subplots(3, 2, figsize(14, 10)) # 位置对比 axes[0,0].plot(t_cubic, q_c, label三次多项式) axes[0,0].set_title(位置对比) axes[0,0].set_ylabel(位置 (rad)) axes[0,0].legend() axes[0,0].grid(True) axes[0,1].plot(t_quintic, q_q, label五次多项式, colorred) axes[0,1].set_title(位置对比) axes[0,1].legend() axes[0,1].grid(True) # 速度对比 axes[1,0].plot(t_cubic, qd_c, label三次速度) axes[1,0].set_ylabel(速度 (rad/s)) axes[1,0].legend() axes[1,0].grid(True) axes[1,1].plot(t_quintic, qd_q, label五次速度, colorred) axes[1,1].legend() axes[1,1].grid(True) # 加速度对比 axes[2,0].plot(t_cubic, qdd_c, label三次加速度) axes[2,0].set_xlabel(时间 (s)) axes[2,0].set_ylabel(加速度 (rad/s²)) axes[2,0].legend() axes[2,0].grid(True) axes[2,1].plot(t_quintic, qdd_q, label五次加速度, colorred) axes[2,1].set_xlabel(时间 (s)) axes[2,1].legend() axes[2,1].grid(True) plt.suptitle(三次多项式 vs 五次多项式轨迹对比) plt.tight_layout() plt.show()从对比图中可以清晰地看到五次多项式轨迹的加速度曲线在起点和终点是平滑过渡到零的而三次多项式的加速度在边界处存在阶跃。这意味着五次多项式产生的加加速度是连续的对机械系统的冲击更小。当然天下没有免费的午餐高阶多项式带来了更高的计算复杂度。多项式阶数每增加两阶就需要求解一个更大的线性方程组。对于实时性要求极高的系统如1kHz控制频率这需要仔细评估。4. 工程实践约束处理、效率优化与陷阱规避理论很美好但把代码部署到真实的机器人控制器上时你会遇到一系列工程挑战。单纯实现多项式计算只是第一步更重要的是如何使其鲁棒、高效并满足物理限制。关节限位与速度/加速度约束任何机器人的关节都有运动范围、最大速度和加速度限制。生成的轨迹必须被“裁剪”或重新规划以满足这些硬约束。一个简单的后处理方法是检查轨迹的极值def check_trajectory_limits(t, q, qd, qdd, q_limits, qd_limits, qdd_limits): 检查轨迹是否超出关节物理限制。 返回布尔值及超出限制的信息。 violations [] # 检查位置限位 if np.any(q q_limits[0]) or np.any(q q_limits[1]): violations.append(f位置超出限制: min{q_limits[0]}, max{q_limits[1]}) # 检查速度限位 if np.any(np.abs(qd) qd_limits): violations.append(f速度超出限制: max_abs{qd_limits}) # 检查加速度限位 if np.any(np.abs(qdd) qdd_limits): violations.append(f加速度超出限制: max_abs{qdd_limits}) is_valid len(violations) 0 return is_valid, violations # 示例约束 q_limits (-np.pi, np.pi) # 弧度 qd_limit 2.0 # rad/s qdd_limit 5.0 # rad/s² is_ok, msgs check_trajectory_limits(t_quintic, q_q, qd_q, qdd_q, q_limits, qd_limit, qdd_limit) if not is_ok: print(轨迹违反限制:, msgs)如果轨迹违反了限制常见的调整策略包括延长运动时间tf这是最直接的方法降低平均速度/加速度。时间缩放Time Scaling对轨迹进行整体时间变换例如将t替换为s(t)*t其中s(t)是一个缩放函数使速度、加速度按比例下降。重新规划使用考虑了约束的优化方法如梯形速度剖面、S曲线生成轨迹。计算效率优化在嵌入式系统或需要高频更新的场合实时计算多项式系数和求值可能成为瓶颈。优化策略包括预计算系数对于固定的路径点序列可以在初始化阶段一次性计算所有多项式系数运行时只需根据当前时间t进行求值。使用霍纳法则Horner‘s Method求值多项式求值应使用嵌套乘法减少运算次数。例如五次多项式q a0 t*(a1 t*(a2 t*(a3 t*(a4 t*a5))))。查表法对于周期性或固定的轨迹可以预先计算好整个轨迹表运行时直接插值查找。多路径点高阶连续性问题当使用分段三次或五次多项式连接多个路径点时为了获得整体的高阶连续性如加速度连续需要在路径点处施加额外的约束。这会导致所有段的方程耦合在一起需要求解一个更大的全局线性方程组而不是独立求解每一段。例如对于N个路径点使用五次多项式并要求位置、速度、加速度连续将涉及求解一个6(N-1)阶的线性系统。虽然计算量增大但能获得全局平滑的轨迹。下面是一个简化示例展示如何为两个相邻的五次多项式段施加连续性约束假设有三个路径点要求位置、速度、加速度在中间点连续def quintic_spline_three_points(q_points, t_points): 用五次样条连接三个路径点保证位置、速度、加速度连续。 简化版假设起点和终点的速度、加速度为零。 q_points: [q0, q1, q2] t_points: [t0, t1, t2] q0, q1, q2 q_points t0, t1, t2 t_points T1 t1 - t0 T2 t2 - t1 # 未知数每段五次多项式的6个系数共12个。 # 约束条件 # 1. 位置约束 (3个点 * 2 6个) # 2. 起点终点速度为零 (2个) # 3. 起点终点加速度为零 (2个) # 4. 中间点速度连续 (1个) # 5. 中间点加速度连续 (1个) # 总共12个约束可解。 # 构建矩阵 A (12x12) 和向量 b (12,) A np.zeros((12, 12)) b np.zeros(12) # 为了方便我们按 [a10, a11, a12, a13, a14, a15, a20, a21, a22, a23, a24, a25] 排列未知数 # 其中 aij 表示第i段多项式的第j个系数 (j从0到5)。 # 约束1: 段1在t0的位置 q0 A[0, 0] 1 # a10 b[0] q0 # 约束2: 段1在t1的位置 q1 A[1, 0:6] [1, T1, T1**2, T1**3, T1**4, T1**5] b[1] q1 # 约束3: 段2在t1的位置 q1 A[2, 6:] [1, 0, 0, 0, 0, 0] # t0 for segment 2 (relative time) b[2] q1 # 约束4: 段2在t2的位置 q2 A[3, 6:] [1, T2, T2**2, T2**3, T2**4, T2**5] b[3] q2 # 速度约束 (导数) # 约束5: 段1在t0的速度 0 A[4, 1] 1 # a11 # b[4] 0 # 约束6: 段2在t2的速度 0 A[5, 7] 1 # a21 A[5, 8] 2*T2 A[5, 9] 3*T2**2 A[5, 10] 4*T2**3 A[5, 11] 5*T2**4 # b[5] 0 # 加速度约束 (二阶导) # 约束7: 段1在t0的加速度 0 A[6, 2] 2 # 2*a12 # b[6] 0 # 约束8: 段2在t2的加速度 0 A[7, 8] 2 # 2*a22 A[7, 9] 6*T2 A[7, 10] 12*T2**2 A[7, 11] 20*T2**3 # b[7] 0 # 连续性约束 # 约束9: 段1在t1的速度 段2在t1(相对时间0)的速度 # 段1在t1的速度: a11 2*a12*T1 3*a13*T1^2 4*a14*T1^3 5*a15*T1^4 A[8, 1] 1 A[8, 2] 2*T1 A[8, 3] 3*T1**2 A[8, 4] 4*T1**3 A[8, 5] 5*T1**4 # 减去段2在t0的速度: a21 A[8, 7] -1 # b[8] 0 # 约束10: 段1在t1的加速度 段2在t1(相对时间0)的加速度 # 段1在t1的加速度: 2*a12 6*a13*T1 12*a14*T1^2 20*a15*T1^3 A[9, 2] 2 A[9, 3] 6*T1 A[9, 4] 12*T1**2 A[9, 5] 20*T1**3 # 减去段2在t0的加速度: 2*a22 A[9, 8] -2 # b[9] 0 # 还有两个自由度实际上我们还有中间点的速度和加速度未指定这里我们隐含地通过连续性约束确定了它们。 # 检查矩阵是否满秩 if np.linalg.matrix_rank(A) 12: print(警告约束矩阵可能奇异请检查输入。) coeff np.linalg.solve(A, b) coeff1 coeff[0:6] coeff2 coeff[6:] return coeff1, coeff2, T1, T2这个示例虽然只处理了三个点但揭示了多段高阶样条的核心通过建立全局方程组来保证连接点的高阶连续性。对于更多点矩阵会变得稀疏且有规律可以使用稀疏矩阵求解器来提高效率。数值稳定性问题当运动时间tf非常小或非常大时高阶多项式的系数可能变得极端导致数值计算误差。在实现时建议对时间进行归一化处理或者使用条件数更好的基函数如贝塞尔曲线、B样条。在实际项目中我通常不会从头实现这些求解器而是依赖成熟的数学库如SciPy的插值模块 (scipy.interpolate) 或CasADi这样的优化框架。但理解其背后的原理能帮助你在算法选型、调试和性能优化时做出更明智的决策。例如知道五次多项式能保证加速度连续你就明白为什么在需要高动态性能的拾放Pick-and-Place应用中它比三次多项式更受青睐同时你也清楚其计算代价从而在资源受限的控制器上做出权衡。轨迹规划不是纸上谈兵最终的评价标准是机器人的实际运动是否平滑、精确、高效。多观察实际运行时的电流曲线和振动频谱你会发现一个好的插值算法带来的提升远比想象中要大。