python的机器人基础原理工业场景模拟第二十八篇:焊接作业仿真,笛卡尔空间直线轨迹规划,生成末端执行器直线运动位姿序列。
《Python机器人基础原理》第二十八篇:焊接作业仿真——笛卡尔空间直线轨迹规划,生成末端执行器直线运动位姿序列
1. 实际工业应用场景
周三下午,某汽车零部件焊接工段。一台3-DOF垂直关节臂(基座偏航θ₁ + 大臂俯仰θ₂ + 小臂俯仰θ₃)搭载焊枪末端执行器,负责在汽车排气管法兰盘上沿直线焊缝进行MAG焊接。机器人构型参数:大臂长450 mm、质量4.2 kg(质心距基座0.22 m),小臂长350 mm、质量2.1 kg(质心距肘部0.18 m),末端焊枪通过腕部法兰刚性连接,质量0.8 kg,TCP(工具中心点)位于法兰正下方120 mm处。关节限位:θ₁±180°、最大角速度2.8 rad/s、最大角加速度7.0 rad/s²;θ₂±130°、最大角速度2.2 rad/s、最大角加速度5.0 rad/s²;θ₃±150°、最大角速度3.0 rad/s、最大角加速度8.0 rad/s²。
焊缝是一条空间直线段:起点在基座坐标系中的位置为 (0.35 m, 0.12 m, 0.30 m),终点为 (0.55 m, 0.12 m, 0.30 m),长度200 mm,位于水平面内。要求焊枪TCP沿此直线以恒定线速度 0.05 m/s 匀速运动,焊枪姿态保持垂直向下(焊枪轴线与重力方向平行,TCP始终垂直于工件表面)。
小赵接到任务:在离线仿真中验证这条直线轨迹是否可行——生成TCP的笛卡尔空间直线位姿序列,通过逆运动学求解各关节角度随时间的变化,输出关节空间的位置/速度/加速度曲线,校验全程是否超出关节限位。
他先在纸上画了个草图:"TCP从(0.35, 0.12, 0.30)到(0.55, 0.12, 0.30),沿Y=0.12的水平线走200 mm。速度0.05 m/s,需要4.0秒。"
小李路过看了一眼:"你打算怎么让TCP走直线?直接给起点终点关节角度做插值?"
"对,关节空间五次多项式。"
"那TCP走的是直线吗?"
小赵愣了一下。
"关节空间里做插值,TCP在笛卡尔空间里走的是曲线。你的焊枪火焰轨迹会弯,焊缝直接报废。"
"……那怎么办?"
"笛卡尔空间直线规划。先在笛卡尔空间把TCP的直线轨迹按时间采样,每个采样点通过逆运动学算出对应的关节角度。这样TCP走的是真直线,但各关节的速度不会均匀——映射回来之后是非线性的。"
"逆解失败怎么办?比如某个点够不到?"
"离线校验工具的正确行为是直接报出来——哪一段不可达、为什么。不能偷偷用上一个点的角度填上,那样后面的速度差分全是假的。"
本篇解决的问题:给定TCP起点和终点的笛卡尔坐标及恒定姿态,在笛卡尔空间生成直线位置序列(姿态恒定,四元数Slerp退化为恒等映射,相关代码为后续姿态渐变场景预留),通过逆运动学求解各关节角度,输出关节空间轨迹曲线,全程校验限位,逆解不可达点直接中断并报告。
⚠️ 声明:本篇全部代码为课程离线仿真,运行在普通PC上,用于离线焊接轨迹规划校验。不可直接用于真实机器人控制器、焊接电源或产线设备。真实工业现场需结合焊接工艺参数(电流、电压、送丝速度)、实时逆解、奇异点回避、外部轴同步。代码作者不对直接使用本代码于真实设备造成的任何后果负责。
2. 痛点分析
课程模块 本篇对应的工程痛点
第2章 位姿描述与坐标变换 → 2.3 旋转矩阵与四元数 姿态表示需从旋转矩阵转换为四元数,为后续球面线性插值(Slerp)打基础
第3章 连杆参数及运动学求解 → 3.2 逆运动学 笛卡尔空间每个采样点需通过逆运动学求解关节角度,解析解存在多解需选最近解
第6章 轨迹规划与控制 → 6.5 笛卡尔空间轨迹规划 关节空间插值无法保证TCP走直线→必须在笛卡尔空间规划→逆解映射
本篇代码解决 L2 离线笛卡尔轨迹规划校验层。输入:TCP起点/终点位姿、运动时间、采样频率。在笛卡尔空间线性插值生成位置序列,调用逆运动学求解器逐点计算关节角度,输出关节空间轨迹曲线、限位校验报告、不可达点报告。
不覆盖:焊接工艺参数建模(熔池动力学、热变形)、实时迭代逆解、奇异点回避策略、力控焊接(接触力反馈)、姿态渐变焊缝(如船形焊、环缝,需启用Slerp)。
3. 核心逻辑讲解
物理直觉
想象你手里拿着一支笔,要在纸上画一条直线。你不会去想"手腕转多少度、肘关节弯多少度"——你只关心笔尖在纸上的轨迹。这就是笛卡尔空间规划:先确定末端(TCP)要走的路径,再反推每个关节该怎么动。
但机器人不是人的手臂,它有严格的关节限位和速度上限。所以你需要把笛卡尔路径"翻译"成关节的运动指令。翻译的过程就是逆运动学。
关键难点:笛卡尔空间的匀速直线运动,映射回关节空间后,各关节的角速度不是常数。靠近奇异构型时,某个关节需要快速旋转来补偿TCP的小位移——这就是"奇异点问题"。本篇的3-DOF垂直关节臂在完全伸直或完全折叠时会出现奇异,代码中通过限位校验和不可达检测来暴露这个问题。
位置线性插值 + 姿态处理
TCP位置从起点到终点做线性插值:p(t) = p₀ + (p_T - p₀) · (t/T_total)。每一项含义:p₀是起点坐标,p_T - p₀是总位移向量,t/T_total是归一化时间(0到1),乘起来就是当前时刻的位置。匀速直线运动的直接结果。
关于姿态:本篇焊缝要求焊枪始终垂直向下,起点和终点的姿态四元数完全相同(q₀ = q₁)。因此四元数球面线性插值(Slerp)退化为恒等映射——每个采样点输出相同的四元数。代码中
"transforms.py"实现了完整的Slerp(含最短路径检测和线性退化保护),但本篇不调用它参与逆解。这是有意为之的预留设计:当后续场景需要姿态渐变(如船形焊、角焊缝、法兰环缝起弧段)时,Slerp结果将作为逆解的腕部方向约束输入。本篇重点验证位置→逆解→关节轨迹的核心链路。
逆运动学几何法
将3-DOF垂直关节臂投影到垂直平面。已知TCP的(x, y, z),腕部在TCP正上方tcp_offset处(因为焊枪垂直向下),所以腕部位置为(x, y, z + tcp_offset)。
- θ₁:基座偏航,由腕部在基座平面的投影决定,θ₁ = atan2(y, x)
- 腕部到基座的水平距离 r = √(x² + y²),高度 h = z + tcp_offset
- 腕部到基座的直线距离 d = √(r² + h²)
- 余弦定理求θ₃:cos(θ₃) = (d² - L₁² - L₂²) / (2·L₁·L₂)
- θ₂ = atan2(h, r) - atan2(L₂·sin(θ₃), L₁ + L₂·cos(θ₃))
可达性检查:d > L₁ + L₂(外不可达)或 d < |L₁ - L₂|(内不可达/奇异)时,该点不可达,直接中断。
业务逻辑 → 代码映射
输入:TCP起点位姿 (x0,y0,z0, q0)、终点位姿 (xT,yT,zT, qT)、
运动时间 T_total、采样数 N
↓
步骤1:笛卡尔空间位置线性插值
p(t) = p0 + (pT - p0) · (t/T_total)
姿态:q(t) = q0(本篇恒定,Slerp预留)
↓
步骤2:逆运动学求解(几何法,逐点)
对每点 (x, y, z):
计算腕部位置 → θ₁ = atan2(y, x)
余弦定理求θ₃ → 两个分支(肘上/肘下)
求θ₂ → 最近解策略选与上一时刻差值最小的解
可达性检查:不可达则中断并报告
↓
步骤3:有限差分计算速度和加速度
ω_j = Δθ/Δt(中心差分,端点用前向/后向)
α_j = Δω/Δt
↓
步骤4:全程校验
角度限位、角速度、角加速度逐点校验
不可达点报告
↓
输出:4张图表 + 校验报告
关键公式文字翻译
位置线性插值:p(t) = p₀ + (p_T - p₀) × (t/T)。匀速直线运动的直接离散化。
余弦定理逆解:cos(θ₃) = (d² - L₁² - L₂²) / (2·L₁·L₂)。d是腕部到基座的距离,L₁和L₂是大小臂长度。这个公式来源于三角形三边关系——大小臂和腕部-基座连线构成一个三角形,已知三边求内角。
中心差分速度:ω[i] = (θ[i+1] - θ[i-1]) / (2Δt)。用前后两个点来估计中间点的导数,比前向差分精度更高(O(Δt²)),但端点只能用前向/后向差分。
4. OOP代码实现
技术栈
严格限定:numpy, scipy, matplotlib。不引入pybullet、roboticstoolbox等任何第三方机器人库。
项目结构树
welding_cartesian_trajectory_sim/
├── run_welding_cartesian_trajectory_sim.py
├── requirements.txt
├── robot/
│ ├── __init__.py
│ ├── models.py
│ ├── transforms.py
│ ├── inverse_kinematics.py
│ ├── trajectory_generator.py
│ └── visualize_trajectory.py
├── tests/
│ ├── __init__.py
│ └── test_welding_cartesian_trajectory_sim.py
└── results/
核心源码
"robot/models.py" — 数据模型
"""
机器人数据模型:连杆参数、关节限位、TCP位姿
3-DOF垂直关节臂:基座偏航θ₁ + 大臂俯仰θ₂ + 小臂俯仰θ₃
"""
from dataclasses import dataclass, field
from typing import List
import numpy as np
@dataclass
class LinkParams:
"""单连杆几何与质量参数"""
name: str
length: float # 连杆长度 (m)
mass: float # 连杆质量 (kg)
com_x: float # 质心x坐标(沿连杆方向)(m)
com_y: float # 质心y坐标 (m)
com_z: float # 质心z坐标 (m)
inertia_xx: float = 0.0 # 惯性张量Ixx (kg·m²)
inertia_yy: float = 0.0
inertia_zz: float = 0.0
@dataclass
class JointLimit:
"""单关节限位"""
joint_name: str
min_angle: float # 最小角度 (rad)
max_angle: float # 最大角度 (rad)
max_velocity: float # 最大角速度 (rad/s)
max_acceleration: float # 最大角加速度 (rad/s²)
@dataclass
class JointLimitSet:
"""关节限位集合"""
limits: List[JointLimit] = field(default_factory=list)
def check_all_positions(self, angles: List[float]) -> tuple:
"""校验关节角度是否全部在限位内"""
violations = []
for i, angle in enumerate(angles):
if i < len(self.limits):
if angle < self.limits[i].min_angle or angle > self.limits[i].max_angle:
violations.append(i)
return len(violations) == 0, violations
def check_all_velocities(self, velocities: List[float]) -> tuple:
"""校验角速度是否全部在限位内"""
violations = []
for i, vel in enumerate(velocities):
if i < len(self.limits):
if abs(vel) > self.limits[i].max_velocity:
violations.append(i)
return len(violations) == 0, violations
def check_all_accelerations(self, accelerations: List[float]) -> tuple:
"""校验角加速度是否全部在限位内"""
violations = []
for i, acc in enumerate(accelerations):
if i < len(self.limits):
if abs(acc) > self.limits[i].max_acceleration:
violations.append(i)
return len(violations) == 0, violations
@dataclass
class TCPPose:
"""TCP位姿:位置(x,y,z) + 姿态四元数(qw,qx,qy,qz)"""
x: float
y: float
z: float
qw: float = 1.0
qx: float = 0.0
qy: float = 0.0
qz: float = 0.0
def position(self) -> np.ndarray:
return np.array([self.x, self.y, self.z])
def quaternion(self) -> np.ndarray:
return np.array([self.qw, self.qx, self.qy, self.qz])
"robot/transforms.py" — 坐标变换与四元数运算
"""
坐标变换工具:旋转矩阵、四元数、齐次变换矩阵
四元数球面线性插值(Slerp)——本篇姿态恒定时退化为恒等映射,为后续姿态渐变场景预留
"""
import numpy as np
def euler_to_rotation_matrix(roll: float, pitch: float, yaw: float) -> np.ndarray:
"""
将ZYX欧拉角转换为3×3旋转矩阵
roll=绕x轴, pitch=绕y轴, yaw=绕z轴
"""
cr, sr = np.cos(roll), np.sin(roll)
cp, sp = np.cos(pitch), np.sin(pitch)
cy, sy = np.cos(yaw), np.sin(yaw)
R = np.array([
[cy*cp, cy*sp*sr - sy*cr, cy*sp*cr + sy*sr],
[sy*cp, sy*sp*sr + cy*cr, sy*sp*cr - cy*sr],
[-sp, cp*sr, cp*cr]
])
return R
def rotation_matrix_to_quaternion(R: np.ndarray) -> np.ndarray:
"""
将3×3旋转矩阵转换为单位四元数 [qw, qx, qy, qz]
使用Shepperd法避免数值不稳定
"""
q = np.zeros(4)
trace = np.trace(R)
if trace > 0:
s = 0.5 / np.sqrt(trace + 1.0)
q[0] = 0.25 / s
q[1] = (R[2, 1] - R[1, 2]) * s
q[2] = (R[0, 2] - R[2, 0]) * s
q[3] = (R[1, 0] - R[0, 1]) * s
else:
if R[0, 0] > R[1, 1] and R[0, 0] > R[2, 2]:
s = 2.0 * np.sqrt(1.0 + R[0, 0] - R[1, 1] - R[2, 2])
q[0] = (R[2, 1] - R[1, 2]) / s
q[1] = 0.25 * s
q[2] = (R[0, 1] + R[1, 0]) / s
q[3] = (R[0, 2] + R[2, 0]) / s
elif R[1, 1] > R[2, 2]:
s = 2.0 * np.sqrt(1.0 + R[1, 1] - R[0, 0] - R[2, 2])
q[0] = (R[0, 2] - R[2, 0]) / s
q[1] = (R[0, 1] + R[1, 0]) / s
q[2] = 0.25 * s
q[3] = (R[1, 2] + R[2, 1]) / s
else:
s = 2.0 * np.sqrt(1.0 + R[2, 2] - R[0, 0] - R[1, 1])
q[0] = (R[1, 0] - R[0, 1]) / s
q[1] = (R[0, 2] + R[2, 0]) / s
q[2] = (R[1, 2] + R[2, 1]) / s
q[3] = 0.25 * s
return q / np.linalg.norm(q)
def quaternion_slerp(q0: np.ndarray, q1: np.ndarray, t: float) -> np.ndarray:
"""
四元数球面线性插值(Slerp)
本篇焊缝姿态恒定(q0=q1),此函数退化为恒等映射返回q0
为后续姿态渐变场景(船形焊、环缝)预留
q0, q1: 单位四元数 [qw, qx, qy, qz]
t: 插值参数,0≤t≤1
"""
# 数值保护:确保四元数为单位四元数
q0 = q0 / np.linalg.norm(q0)
q1 = q1 / np.linalg.norm(q1)
dot = np.dot(q0, q1)
# 如果点积为负,取q1的反方向以确保最短路径
if dot < 0.0:
q1 = -q1
dot = -dot
# 如果点积接近1,使用线性插值避免除零
if dot > 0.9995:
result = q0 + t * (q1 - q0)
return result / np.linalg.norm(result)
theta_0 = np.arccos(np.clip(dot, -1.0, 1.0))
theta = theta_0 * t
s0 = np.sin(theta_0 - theta) / np.sin(theta_0)
s1 = np.sin(theta) / np.sin(theta_0)
return s0 * q0 + s1 * q1
def quaternion_to_rotation_matrix(q: np.ndarray) -> np.ndarray:
"""将单位四元数转换为3×3旋转矩阵"""
qw, qx, qy, qz = q
R = np.array([
[1 - 2*(qy**2 + qz**2), 2*(qx*qy - qw*qz), 2*(qx*qz + qw*qy)],
[2*(qx*qy + qw*qz), 1 - 2*(qx**2 + qz**2), 2*(qy*qz - qw*qx)],
[2*(qx*qz - qw*qy), 2*(qy*qz + qw*qx), 1 - 2*(qx**2 + qy**2)]
])
return R
"robot/inverse_kinematics.py" — 逆运动学求解器
"""
3-DOF垂直关节臂逆运动学求解器
几何法解析求解:θ₁=atan2(y,x), θ₂和θ₃由余弦定理求得
支持最近解选择策略避免关节跳变
不可达点直接返回失败标志,不静默填充
"""
import numpy as np
from typing import List, Tuple, Optional
from robot.models import JointLimitSet
class InverseKinematicsSolver:
"""
3-DOF垂直关节臂逆运动学
构型:基座偏航θ₁ + 大臂俯仰θ₂ + 小臂俯仰θ₃
焊枪垂直向下,TCP在腕部法兰正下方 tcp_offset 处
"""
def __init__(self, L1: float, L2: float, tcp_offset: float,
joint_limits: JointLimitSet):
self.L1 = L1
self.L2 = L2
self.tcp_offset = tcp_offset
self.limits = joint_limits
def solve(self, tcp_x: float, tcp_y: float, tcp_z: float,
prev_angles: Optional[List[float]] = None) -> Tuple[np.ndarray, bool]:
"""
逆运动学求解
输入:TCP在基座坐标系中的位置 (m)
输出:(关节角度数组 [θ₁, θ₂, θ₃], 是否成功)
不可达时返回 (零数组, False),由调用方决定是否中断
"""
# θ₁:基座偏航
theta1 = np.arctan2(tcp_y, tcp_x)
# 腕部位置:焊枪垂直向下,腕部在TCP正上方tcp_offset处
wrist_x = tcp_x
wrist_y = tcp_y
wrist_z = tcp_z + self.tcp_offset
# 腕部到基座的水平距离和高度
r = np.sqrt(wrist_x**2 + wrist_y**2)
h = wrist_z
# 腕部到基座的直线距离
d = np.sqrt(r**2 + h**2)
# 可达性检查:外不可达和内奇异
if d > self.L1 + self.L2:
return np.array([0.0, 0.0, 0.0]), False
if d < abs(self.L1 - self.L2):
return np.array([0.0, 0.0, 0.0]), False
cos_theta3 = (d**2 - self.L1**2 - self.L2**2) / (2 * self.L1 * self.L2)
cos_theta3 = np.clip(cos_theta3, -1.0, 1.0)
# 两个解分支:肘部向上(负角)和肘部向下(正角)
theta3_option1 = -np.arccos(cos_theta3)
theta3_option2 = np.arccos(cos_theta3)
solutions = []
for theta3 in [theta3_option1, theta3_option2]:
alpha = np.arctan2(h, r)
beta = np.arctan2(self.L2 * np.sin(theta3),
self.L1 + self.L2 * np.cos(theta3))
theta2 = alpha - beta
angles = np.array([theta1, theta2, theta3])
solutions.append(angles)
# 检查限位并选择最优解
valid_solutions = []
for sol in solutions:
is_valid, _ = self.limits.check_all_positions(sol.tolist())
if is_valid:
valid_solutions.append(sol)
if len(valid_solutions) == 0:
return np.array([0.0, 0.0, 0.0]), False
if prev_angles is None:
return valid_solutions[0], True
# 最近解策略
prev = np.array(prev_angles)
best_sol = valid_solutions[0]
best_diff = np.sum(np.abs(valid_solutions[0] - prev))
for sol in valid_solutions[1:]:
diff = np.sum(np.abs(sol - prev))
if diff < best_diff:
best_diff = diff
best_sol = sol
return best_sol, True
"robot/trajectory_generator.py" — 笛卡尔空间直线轨迹生成
"""
笛卡尔空间直线轨迹规划
位置线性插值 + 逆运动学逐点求解
逆解失败时直接中断并报告不可达点
"""
import numpy as np
from typing import List, Dict
from robot.models import TCPPose, JointLimitSet
from robot.inverse_kinematics import InverseKinematicsSolver
class CartesianLinearTrajectoryGenerator:
"""
笛卡尔空间直线轨迹生成器
TCP从起点到终点沿直线运动,姿态恒定(垂直向下)
每个采样点通过逆运动学求解关节角度
逆解失败直接中断,不静默填充
"""
def __init__(self, ik_solver: InverseKinematicsSolver,
joint_limits: JointLimitSet):
self.ik = ik_solver
self.limits = joint_limits
def generate(self, start_pose: TCPPose, end_pose: TCPPose,
total_time: float, n_samples: int) -> Dict:
if total_time <= 0:
raise ValueError("运动时间必须大于0")
if n_samples < 2:
raise ValueError("采样数至少为2")
t_array = np.linspace(0, total_time, n_samples)
dt = total_time / (n_samples - 1)
tcp_positions = np.zeros((n_samples, 3))
joint_angles = np.zeros((n_samples, 3))
joint_velocities = np.zeros((n_samples, 3))
joint_accelerations = np.zeros((n_samples, 3))
ik_success_flags = np.zeros(n_samples, dtype=bool)
unreachable_indices = [] # 记录不可达的采样点索引
prev_angles = None
start_pos = start_pose.position()
for i, t in enumerate(t_array):
tau = t / total_time
# 位置线性插值
tcp_positions[i, :] = start_pos + (end_pose.position() - start_pos) * tau
# 逆运动学求解——失败直接中断
angles, success = self.ik.solve(
tcp_positions[i, 0], tcp_positions[i, 1],
tcp_positions[i, 2], prev_angles
)
if not success:
unreachable_indices.append(i)
# 不可达点不填充假数据,标记失败并停止后续计算
break
joint_angles[i, :] = angles
ik_success_flags[i] = True
prev_angles = angles.tolist()
# 如果有不可达点,截断有效数据到第一个不可达点之前
if len(unreachable_indices) > 0:
first_fail = unreachable_indices[0]
n_valid = first_fail
else:
n_valid = n_samples
# 有限差分计算速度和加速度(仅对有效数据)
if n_valid >= 2:
for j in range(3):
# 中心差分(内部点)
if n_valid > 2:
joint_velocities[1:n_valid-1, j] = (
joint_angles[2:n_valid, j] - joint_angles[0:n_valid-2, j]
) / (2 * dt)
# 端点前向/后向差分
joint_velocities[0, j] = (
joint_angles[1, j] - joint_angles[0, j]
) / dt
if n_valid > 1:
joint_velocities[n_valid-1, j] = (
joint_angles[n_valid-1, j] - joint_angles[n_valid-2, j]
) / dt
for j in range(3):
if n_valid > 2:
joint_accelerations[1:n_valid-1, j] = (
joint_velocities[2:n_valid, j] - joint_velocities[0:n_valid-2, j]
) / (2 * dt)
joint_accelerations[0, j] = (
joint_velocities[1, j] - joint_velocities[0, j]
) / dt
if n_valid > 1:
joint_accelerations[n_valid-1, j] = (
joint_velocities[n_valid-1, j] - joint_velocities[n_valid-2, j]
) / dt
# 全程限位校验(仅对有效数据)
velocity_violations = []
acceleration_violations = []
for i in range(n_valid):
vel_list = joint_velocities[i, :].tolist()
acc_list = joint_accelerations[i, :].tolist()
_, v_viol = self.limits.check_all_velocities(vel_list)
_, a_viol = self.limits.check_all_accelerations(acc_list)
for j in v_viol:
velocity_violations.append((i, j, vel_list[j],
self.limits.limits[j].max_velocity))
for j in a_viol:
acceleration_violations.append((i, j, acc_list[j],
self.limits.limits[j].max_acceleration))
return {
'time': t_array[:n_valid],
'tcp_positions': tcp_positions[:n_valid],
'joint_angles': joint_angles[:n_valid],
'joint_velocities': joint_velocities[:n_valid],
'joint_accelerations': joint_accelerations[:n_valid],
'ik_success_flags': ik_success_flags[:n_valid],
'unreachable_indices': unreachable_indices,
'velocity_violations': velocity_violations,
'acceleration_violations': acceleration_violations,
'n_valid_samples': n_valid,
'n_total_samples': n_samples
}
"robot/visualize_trajectory.py" — 可视化模块
"""
焊接作业笛卡尔轨迹规划可视化
4张核心图表:TCP三维直线轨迹、关节角度曲线、关节角速度曲线、关节角加速度曲线
"""
import numpy as np
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
class TrajectoryVisualizer:
def __init__(self, results_dir="results"):
self.results_dir = results_dir
import os
os.makedirs(results_dir, exist_ok=True)
def plot_tcp_3d_trajectory(self, tcp_positions, start_pos, end_pos):
"""
① TCP三维直线轨迹:验证笛卡尔空间直线运动
"""
fig = plt.figure(figsize=(10, 8))
ax = fig.add_subplot(111, projection='3d')
ax.plot(tcp_positions[:, 0], tcp_positions[:, 1], tcp_positions[:, 2],
'b-', linewidth=1.5, label='TCP Trajectory')
ax.scatter([start_pos[0]], [start_pos[1]], [start_pos[2]],
c='green', marker='o', s=80, label='Start', zorder=5)
ax.scatter([end_pos[0]], [end_pos[1]], [end_pos[2]],
c='red', marker='s', s=80, label='End', zorder=5)
ax.set_xlabel('X (m)')
ax.set_ylabel('Y (m)')
ax.set_zlabel('Z (m)')
ax.set_title('TCP 3D Linear Trajectory in Cartesian Space')
ax.legend(fontsize=8)
ax.grid(True, alpha=0.3)
plt.tight_layout()
path = f"{self.results_dir}/01_tcp_3d_linear_trajectory.png"
plt.savefig(path, dpi=150)
plt.close()
return path
def plot_joint_angles(self, timestamps, joint_angles):
"""
② 关节角度曲线:验证逆运动学求解的平滑性
"""
fig, ax = plt.subplots(figsize=(14, 5))
joint_names = ['θ₁ (Base Yaw)', 'θ₂ (Elbow Pitch)', 'θ₃ (Wrist Pitch)']
colors = ['steelblue', 'orange', 'green']
for j in range(3):
ax.plot(timestamps, joint_angles[:, j], '-',
color=colors[j], linewidth=1.2, label=joint_names[j])
ax.set_xlabel('Time (s)')
ax.set_ylabel('Joint Angle (rad)')
ax.set_title('Joint Angle Profiles from Cartesian Linear Trajectory')
ax.legend(loc='upper right', fontsize=8)
ax.grid(True, alpha=0.3)
plt.tight_layout()
path = f"{self.results_dir}/02_joint_angle_curves.png"
plt.savefig(path, dpi=150)
plt.close()
return path
def plot_joint_velocities(self, timestamps, joint_velocities, joint_limits):
"""
③ 关节角速度曲线+限位线:验证速度不超限
"""
fig, axes = plt
利用AI解决实际问题,如果你觉得这个工具好用,欢迎关注长安牧笛!
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)