想象一下你在一个陌生的多层商场里问路,保安告诉你:“从你现在的位置(A点),先上到三楼(变换1),然后往东走到底(变换2),再坐扶梯下一层(变换3),就能看到餐厅(B点)。” 这一系列连续的指引,就是一个典型的“变换链”。在机器人中,我们同样需要将多个坐标变换串联起来,才能描述一个部件(如机械臂末端)相对于另一个遥远参考系(如世界地图)的位置。
变换链是指将多个已知的、相邻坐标系之间的变换关系,通过数学运算串联起来,从而得到两个非直接相邻坐标系之间变换关系的过程。它是机器人学中解决“A相对于C在哪里”这类问题的核心工具。
一个变换链由一系列已知的齐次变换矩阵组成。每个矩阵描述了一个坐标系相对于其前一个坐标系的位姿。我们用符号 T_A^B 表示从坐标系 A 到坐标系 B 的变换矩阵。
上图中,我们已知机器人基座在世界中的位姿 T_W^B,机械臂关节相对于基座的位姿 T_B^J,以及末端工具相对于关节的位姿 T_J^T。那么,要计算末端工具在世界坐标系中的位姿 T_W^T,就需要将这些变换串联起来。
变换链的串联通过矩阵乘法实现,但顺序至关重要。规则是:从目标坐标系出发,沿着链“往回”乘到参考坐标系。
对于上图的例子,要得到 T_W^T(工具相对于世界),计算顺序如下:
用数学公式表示:
// 计算末端工具在世界坐标系中的位姿
Eigen::Matrix4f T_W_T = T_W_B * T_B_J * T_J_T;
T_J^T * T_B^J * T_W^B。这会导致完全错误的结果,因为矩阵乘法不满足交换律。p' = M * p)的约定,此时链式乘法顺序恰好相反(从右往左)。ROS的TF2、Eigen等现代库普遍采用左乘约定,本章也基于此。实际开发中,务必确认你所使用库的约定。T_A^B 意味着“从A看B”,如果你有 T_B^A(B相对于A),需要先求逆才能参与链式乘法。我在一个机械臂抓取项目中踩过这个坑。代码里把基座到世界的变换和关节到基座的变换顺序写反了,导致仿真中机械臂动作看起来一切正常(因为运动是相对的),但一旦要求它去抓取一个在世界坐标系中指定位置的物体,爪子总是跑到莫名其妙的地方去。调试了半天才发现是变换链的乘法顺序错了。
更常见的情况是,我们拥有一个复杂的变换链,但需要计算其中任意两个坐标系之间的关系,而不仅仅是首尾两端。
T_W^B, T_B^J, T_J^T,求 T_B^T(工具相对于基座)。T_B^T = T_B^J * T_J^T。T_W^B(基座在世界中),求 T_B^W(世界在基座中)。T_B^W = (T_W^B).inverse()。T_W^A 和 T_W^B,求 T_A^B。T_A^B = (T_W^A).inverse() * T_W^B。为了更清晰地展示不同需求下的计算路径,可以参考下表:
| 已知变换 | 目标变换 | 计算公式(左乘约定) | 生活类比 |
|---|---|---|---|
T_A^B, T_B^C | T_A^C | T_A^C = T_A^B * T_B^C | 知道家到地铁站、地铁站到公司,求家到公司。 |
T_A^B | T_B^A | T_B^A = (T_A^B).inverse() | 知道家相对于公司的方位,求公司相对于家的方位。 |
T_W^A, T_W^B | T_A^B | T_A^B = (T_W^A).inverse() * T_W^B | 知道家和公司各自在地图上的位置,求从家看公司的方向。 |
T_A^B, T_A^C | T_B^C | T_B^C = (T_A^B).inverse() * T_A^C | 知道家到公司、家到超市,求公司到超市。 |
编者提示:性能与数值稳定性
在实际的机器人控制循环中(如每秒100次),频繁进行矩阵乘法和求逆会消耗大量CPU资源。对于固定的变换链部分(如机器人基座与相机之间的刚性连接),应在初始化时预计算好最终的变换矩阵,避免在循环内重复计算。另外,直接对变换矩阵求逆在数学上等价于转置旋转部分并取反平移向量,手动实现这个操作通常比调用通用的矩阵求逆函数更快、数值更稳定。
让我们看一个移动机器人搭载机械臂的完整例子。假设我们有以下坐标系:
{World}: 世界地图坐标系。{Base}: 移动机器人底盘中心。{Arm_Base}: 机械臂的安装基座(固定在底盘上)。{Arm_End}: 机械臂末端执行器。{Camera}: 安装在末端的手眼相机。{Object}: 一个待抓取的物体(其位姿由相机检测得到)。现在,任务是将物体在相机坐标系中的坐标 p_Camera,转换到世界坐标系 p_World,以便规划机器人的移动和抓取。
首先,我们需要建立从 {World} 到 {Object} 的变换链:
对应的变换矩阵乘法为:
// 已知各个相邻坐标系间的变换
Eigen::Matrix4f T_W_B; // 来自机器人里程计或定位系统
Eigen::Matrix4f T_B_ArmB; // 机械臂基座安装偏移,固定值
Eigen::Matrix4f T_ArmB_ArmE; // 机械臂正向运动学结果,随关节角变化
Eigen::Matrix4f T_ArmE_Cam; // 手眼标定结果,固定值
Eigen::Matrix4f T_Cam_Obj; // 由视觉算法得出,物体相对于相机
// 计算物体在世界坐标系中的位姿
Eigen::Matrix4f T_W_Obj = T_W_B * T_B_ArmB * T_ArmB_ArmE * T_ArmE_Cam * T_Cam_Obj;
// 如果已知物体在相机坐标系中的坐标点 p_camera
Eigen::Vector4f p_camera(x, y, z, 1.0);
Eigen::Vector4f p_world = T_W_Obj * p_camera; // 注意:这里T_W_Obj已将物体坐标系对齐到世界,所以直接乘p_camera即可得到p_world
// 更准确地说,应该是:p_world = T_W_Cam * p_camera,其中 T_W_Cam = T_W_B * ... * T_ArmE_Cam
// 而 p_camera 本身是 T_Cam_Obj 的平移部分,或者由 T_Cam_Obj 变换零向量得到。
关键点:理解“变换点”与“变换坐标系”
在上面的代码注释中有一个精妙之处。我们通常说“用变换矩阵 T_A^B 将点从坐标系 A 变换到坐标系 B”,公式为 p_B = T_A^B * p_A。但在变换链末端,当我们有 T_Cam_Obj 时,它表示物体坐标系相对于相机坐标系的位姿。物体本身的坐标在其自己的坐标系中原点是 (0,0,0)。所以,物体在世界中的位置,其实就是 T_W_Obj 这个矩阵的平移向量部分。如果视觉算法直接给出了物体在相机坐标系中的三维坐标点 p_camera,那么它应该是由 T_Cam_Obj 变换零向量得到的,即 p_camera = T_Cam_Obj * [0,0,0,1]^T 的平移部分。这时,用 T_W_Cam 去乘 p_camera 才是正确的。实际开发中,一定要厘清你手中的数据是“一个位姿变换矩阵”还是一个“在某个坐标系下的三维点”。
这个例子展示了变换链如何将感知(相机看到的物体)、控制(机械臂关节角度)和状态估计(机器人在地图中的位置)融合到一个统一的数学框架中。如果其中任何一个变换不准确,最终计算出的物体世界位姿就会出错,导致抓取失败。
假设你有一个简单的机器人系统,已知以下固定变换(单位:米,角度为绕Z轴旋转):
T_Base_Laser: 激光雷达安装在机器人基座前方0.2米,正前方,无旋转。平移为 (0.2, 0, 0)。T_Base_Camera: 相机安装在基座上方0.1米,后方0.05米,绕X轴向下倾斜30度(即俯仰角-30度)。现在,激光雷达检测到正前方1米处有一个障碍物,点在激光雷达坐标系中的坐标为 (1.0, 0, 0)。
T_Base_Camera的逆)T_A^B 和 T_B^C,要计算点p在坐标系C中的坐标,正确的计算顺序是 p_C = T_B^C * T_A^B * p_A。T_W^B)和机械臂末端到基座(T_B^E)的变换矩阵。现在需要计算机械臂末端在世界坐标系中的位置,你应该:
T_W^E = T_W^B * T_B^ET_W^E = T_B^E * T_W^BT_W^E = (T_W^B).inverse() * T_B^ET_W^E = (T_B^E).inverse() * T_W^BT_A^B 和 T_B^A 是相等的。