4. 平移与旋转的复合变换:把多个动作揉成一个矩阵
各位,前面我们分别聊了平移和旋转。但在实际项目中,你很少只做单一操作。比如一个机械臂抓取零件,它既要旋转关节,又要平移末端。你想想看,如果每次变换都单独算一遍,代码会写成什么样?
我个人习惯,是把所有变换“打包”成一个矩阵。说白了,就是用一个 4×4 的矩阵,同时描述“先转多少度,再移到哪”。这样不仅代码干净,实时性也高。
4.1 为什么要复合?
我记得刚入行时,做过一个视觉引导的涂胶项目。相机定位到工件后,机器人需要先旋转对准角度,再平移到涂胶起点。当时我傻乎乎地先算旋转,再算平移,结果每次都要调两个函数,还容易搞错顺序。
后来我意识到:多个变换可以合并成一个矩阵。只要把平移矩阵 T 和旋转矩阵 R 乘起来,得到一个复合矩阵 M,后面所有点都直接用 M 去变换。效率提升不是一星半点。
核心思想:复合变换矩阵 = 各个基本变换矩阵的乘积。顺序很重要,因为矩阵乘法不满足交换律。
4.2 复合变换的数学原理
假设我们有一个点 P,先旋转 R,再平移 T。那么变换后的点 P' 为:
P' = T × (R × P) = (T × R) × P
这里 T × R 就是复合矩阵。注意:先执行的变换放在右边,后执行的放在左边。这是很多新手容易搞反的地方。
举个例子,绕 Z 轴旋转 45°,再沿 X 轴平移 5 个单位:
R_z(45°) = | cos45 -sin45 0 0 |
| sin45 cos45 0 0 |
| 0 0 1 0 |
| 0 0 0 1 |
T_x(5) = | 1 0 0 5 |
| 0 1 0 0 |
| 0 0 1 0 |
| 0 0 0 1 |
M = T_x(5) × R_z(45°)
这个 M 就是复合矩阵。以后任何点,只要左乘 M,就完成了“先旋转再平移”的操作。
4.3 顺序陷阱:先平移还是先旋转?
这里有个坑,我吃过亏。假设你要让一个物体绕自身中心旋转,然后移动到某个位置。正确的顺序是:先旋转,再平移。
为什么?因为旋转是相对于原点进行的。如果你先平移,物体中心不在原点了,再旋转就会绕着原点转,结果物体像卫星一样“公转”而不是“自转”。
| 操作顺序 | 矩阵表达式 | 效果 |
|---|---|---|
| 先旋转,再平移 | M = T × R | 物体自转后移动到目标位置(正确) |
| 先平移,再旋转 | M = R × T | 物体绕原点公转(通常不是你要的) |
我曾经在调试一个 AGV 小车的导航算法时,把旋转和平移顺序搞反了。结果小车在转弯时总是偏离路线,查了两天才发现是复合矩阵的顺序问题。从那以后,我每次写变换代码都会先画个草图确认顺序。
4.4 复合变换的代码实现
在 Python 里,我习惯用 NumPy 来处理矩阵。下面是一个完整的例子:
import numpy as np
import math
def translation_matrix(tx, ty, tz):
"""创建平移矩阵"""
return np.array([
[1, 0, 0, tx],
[0, 1, 0, ty],
[0, 0, 1, tz],
[0, 0, 0, 1]
])
def rotation_z_matrix(theta):
"""创建绕Z轴旋转矩阵"""
c = math.cos(theta)
s = math.sin(theta)
return np.array([
[c, -s, 0, 0],
[s, c, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
])
# 定义点 P
P = np.array([1, 0, 0, 1]) # 齐次坐标
# 先旋转45°,再平移(5, 0, 0)
theta = math.radians(45)
R = rotation_z_matrix(theta)
T = translation_matrix(5, 0, 0)
# 复合矩阵:先执行的R在右边
M = T @ R
# 变换点
P_new = M @ P
print(f"变换后的点: {P_new[:3]}")
输出结果:
变换后的点: [5.70710678 0.70710678 0. ]
你看,点 (1,0,0) 先旋转到 (0.707, 0.707, 0),再平移到 (5.707, 0.707, 0)。完全符合预期。
4.5 复合变换的 SVG 结构图
下面这张图展示了复合变换的流程:
嗯,这里要注意:复合矩阵的乘法顺序是从右向左读。M = T × R 表示“先 R 后 T”。如果你写成 M = R × T,那就是“先 T 后 R”,效果完全不同。
4.6 实际项目中的应用
我在做机器人手眼标定时,经常用到复合变换。比如相机检测到工件位置,需要把像素坐标转换成机器人基座标系下的坐标。这中间涉及多个坐标系变换:
- 相机到机械臂末端:一个固定的旋转+平移(手眼矩阵)
- 末端到基座:根据关节角度实时变化的变换
- 基座到世界:如果机器人装在移动平台上,还有一层变换
把这些变换全部乘起来,得到一个总的复合矩阵。每次相机给出新坐标,只需要一次矩阵乘法就能得到机器人需要的目标位置。实时性杠杠的。
我建议:在代码里把复合矩阵的计算和点的变换分开。复合矩阵只在参数变化时重新计算(比如关节角度变了),而点的变换可以高频调用。这样能省下不少 CPU 时间。
4.7 避坑指南
最后,分享几个我踩过的坑:
- 齐次坐标别忘了最后一维是 1。如果你用三维向量去乘 4×4 矩阵,会报错。我习惯在点坐标后面加个 1,变成 (x, y, z, 1)。
- 矩阵乘法用 @ 运算符,别用 *。* 是逐元素相乘,不是矩阵乘法。这个错误我见过太多次了。
- 角度单位统一。NumPy 的三角函数默认弧度,如果你传了角度进去,结果会非常离谱。
- 复合矩阵可以缓存。如果连续多个点用同一个变换,别每次都重新算矩阵,直接复用。