想象一下,你坐在行驶的汽车里,伸手去拿副驾驶座位上的水杯。你的手相对于你的身体有一个位置,你的身体相对于汽车座椅有一个位置,而汽车本身又在道路上移动。要准确描述水杯最终相对于地面的位置,你需要把这一连串的“相对位置”叠加起来。在机器人学中,齐次坐标变换就是用来做这件事的数学工具,它能把旋转和平移这两种运动统一在一个简洁的矩阵运算里。
简单来说,齐次坐标就是给普通的空间坐标(比如三维的x, y, z)“升维”,增加一个额外的分量。对于一个三维点,它的齐次坐标表示为 [x, y, z, 1]。这个额外的“1”就像一个开关,它本身没有直接的几何意义,但能让旋转和平移的数学计算变得整齐划一。
在三维空间中,描述一个坐标系相对于另一个坐标系的关系,通常需要两部分信息:旋转(R)和平移(t)。旋转告诉我们坐标轴的方向如何变化,平移告诉我们原点移动了多少。如果没有齐次坐标,一个点的变换需要两步计算:先旋转,再加平移。
齐次坐标和齐次变换矩阵的精妙之处在于,它将这两步合并为一步矩阵乘法。一个标准的4x4齐次变换矩阵 T 结构如下:
| 矩阵区块 | 内容 | 物理意义 |
|---|---|---|
| 左上3x3 | 旋转矩阵 R | 描述坐标系的旋转 |
| 右上3x1 | 平移向量 t | 描述坐标系原点的平移 |
| 左下1x3 | [0, 0, 0] | 为齐次坐标运算保留的固定行 |
| 右下1x1 | 1 | 齐次坐标的比例因子,通常为1 |
用数学形式表示就是:
[ R t ]
T = [ ]
[ 0 1 ]
假设点 P_A = [x, y, z, 1]^T 是在坐标系A下的齐次坐标,那么它在坐标系B下的坐标 P_B 可以通过一次矩阵乘法得到:
P_B = T_B_A * P_A
这里的 T_B_A 读作“坐标系A到坐标系B的变换矩阵”。这个记法很重要:它指明了变换的方向是从A到B。
变换矩阵 T_destination_source 表示将点从“源(source)”坐标系变换到“目标(destination)”坐标系。我在项目初期经常混淆这个顺序,导致机器人运动方向完全反了。记住:矩阵左乘的点,其坐标系标签与矩阵的“源”下标一致。
齐次变换最大的优势体现在处理多个坐标系串联变换时。回到开头的汽车例子,假设我们有:手相对于身体(变换 T_body_hand),身体相对于汽车(变换 T_car_body),汽车相对于地面(变换 T_ground_car)。要求手相对于地面的位置,我们不需要分三步计算,只需要将这三个变换矩阵按顺序相乘即可。
对应的矩阵运算为:
T_ground_hand = T_ground_car * T_car_body * T_body_hand
注意乘法顺序是从右到左,即从最内部的变换(手→身体)开始,逐步向外。一个点从手坐标系变换到地面坐标系的完整计算就是:
P_ground = T_ground_hand * P_hand
T_A_B 来将点从A系转换到B系。务必明确矩阵下标的意义。编者提示: 在实际编程中,我强烈建议使用成熟的数学库来处理这些矩阵运算,比如C++中的Eigen库或Python中的NumPy。它们经过高度优化,能避免手动实现时容易出现的性能问题和精度错误。例如,使用Eigen库,你可以用 Eigen::Isometry3d 类型直接表示一个齐次变换,它内部保证了矩阵的右下角为1、左下角为0的约束,比直接用 Matrix4d 更安全。
有了从A到B的变换 T_B_A,我们自然需要知道如何从B回到A,这个变换就是逆变换,记作 T_A_B。幸运的是,对于表示刚体运动的齐次变换矩阵(即旋转矩阵是正交矩阵,且右下角为1),其逆矩阵有非常简洁的形式。
如果 T_B_A = [R, t; 0, 1],那么它的逆矩阵为:
[ R^T -R^T * t ]
T_A_B = inv(T_B_A) = [ ]
[ 0 1 ]
其中 R^T 是旋转矩阵R的转置(对于正交矩阵,转置等于逆)。物理意义很直观:要回去,就先反着平移(-t),但要注意这个平移量是在原坐标系B下度量的,所以需要用B系的旋转矩阵的逆(即 R^T)转换到A系下,也就是 -R^T * t。
T_cam_obj。T_ee_cam(末端到相机)。T_base_obj,以进行抓取。T_base_lidar 和 T_base_imu。T_imu_lidar = inv(T_base_imu) * T_base_lidar。下面我们用Python的NumPy库来演示一个简单的齐次变换例子。假设机器人末端执行器相对于基座有一个变换:绕Z轴旋转45度,并在X方向平移0.5米。
import numpy as np
import math
# 1. 定义旋转和平移
theta = math.radians(45) # 旋转45度,转换为弧度
translation = np.array([0.5, 0.0, 0.0]) # X方向平移0.5米
# 2. 构建绕Z轴的旋转矩阵 (3x3)
R_z = np.array([
[math.cos(theta), -math.sin(theta), 0],
[math.sin(theta), math.cos(theta), 0],
[0, 0, 1]
])
# 3. 构建4x4齐次变换矩阵 T_base_ee (基座到末端)
T_base_ee = np.identity(4) # 先创建一个4x4单位矩阵
T_base_ee[0:3, 0:3] = R_z # 填入旋转部分
T_base_ee[0:3, 3] = translation # 填入平移部分
# 最后一行保持为 [0, 0, 0, 1],单位矩阵已满足
print("变换矩阵 T_base_ee:")
print(T_base_ee)
# 4. 假设末端上有一个点,在其自身坐标系下的坐标为 (0.1, 0, 0.2)
point_in_ee = np.array([0.1, 0.0, 0.2, 1.0]) # 注意是齐次坐标,最后补1
# 5. 计算该点在基座坐标系下的坐标
point_in_base = T_base_ee @ point_in_ee # 使用 @ 进行矩阵乘法
print("\n末端上的点在基座坐标系下的坐标:")
print(point_in_base[:3]) # 只输出前三个分量 (x, y, z)
运行这段代码,你会看到该点经过旋转和平移后,在基座坐标系下的新坐标。通过这个简单的例子,你可以直观地理解齐次变换矩阵如何将旋转和平移一次性作用在一个点上。
1. 修改代码体验:将上面的Python代码复制到你的开发环境中运行。尝试修改旋转角度(`theta`)和平移向量(`translation`),观察输出点的变化。例如,将平移改为 `[0, 0.3, 0.1]`,感受点在空间中的移动。
2. 手动计算验证:对于上面例子中的 `T_base_ee` 和 `point_in_ee`,拿出纸笔,按照矩阵乘法的规则,手动计算一步 `point_in_base`。将你的计算结果与程序输出对比,确保你理解了整个运算过程。
3. 尝试逆变换:在代码中,根据逆变换公式,计算 `T_ee_base`(即 `T_base_ee` 的逆矩阵)。然后用它乘以前面计算出的 `point_in_base`,验证是否能得到原来的 `point_in_ee`。你可以使用 `np.linalg.inv()` 函数求逆,也可以手动根据公式构造。
1. 判断题:齐次坐标 `[2, 3, 5, 1]` 和 `[4, 6, 10, 2]` 表示三维空间中的同一个点。
2. 选择题:已知变换矩阵 `T_B_A` 将点从坐标系A变换到坐标系B。现在有一个在坐标系C下的点 `P_C`,要得到它在坐标系A下的坐标 `P_A`,已知 `T_B_C` 和 `T_B_A`,正确的计算式是: A) `P_A = T_B_A * T_B_C * P_C` B) `P_A = inv(T_B_A) * T_B_C * P_C` C) `P_A = inv(T_B_A) * inv(T_B_C) * P_C`
3. 判断题:一个标准的、表示刚体运动的齐次变换矩阵,其逆矩阵总是等于其转置矩阵。
T_destination_source 的下标指明了变换方向:从源(source)坐标系到目标(destination)坐标系。