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 结构图

下面这张图展示了复合变换的流程:

复合变换流程 原始点 P 旋转 R 平移 T 变换后 P' 复合矩阵 M = T × R P' = M × P = (T × R) × P

嗯,这里要注意:复合矩阵的乘法顺序是从右向左读。M = T × R 表示“先 R 后 T”。如果你写成 M = R × T,那就是“先 T 后 R”,效果完全不同。

4.6 实际项目中的应用

我在做机器人手眼标定时,经常用到复合变换。比如相机检测到工件位置,需要把像素坐标转换成机器人基座标系下的坐标。这中间涉及多个坐标系变换:

  • 相机到机械臂末端:一个固定的旋转+平移(手眼矩阵)
  • 末端到基座:根据关节角度实时变化的变换
  • 基座到世界:如果机器人装在移动平台上,还有一层变换

把这些变换全部乘起来,得到一个总的复合矩阵。每次相机给出新坐标,只需要一次矩阵乘法就能得到机器人需要的目标位置。实时性杠杠的。

我建议:在代码里把复合矩阵的计算和点的变换分开。复合矩阵只在参数变化时重新计算(比如关节角度变了),而点的变换可以高频调用。这样能省下不少 CPU 时间。

4.7 避坑指南

最后,分享几个我踩过的坑:

  1. 齐次坐标别忘了最后一维是 1。如果你用三维向量去乘 4×4 矩阵,会报错。我习惯在点坐标后面加个 1,变成 (x, y, z, 1)。
  2. 矩阵乘法用 @ 运算符,别用 *。* 是逐元素相乘,不是矩阵乘法。这个错误我见过太多次了。
  3. 角度单位统一。NumPy 的三角函数默认弧度,如果你传了角度进去,结果会非常离谱。
  4. 复合矩阵可以缓存。如果连续多个点用同一个变换,别每次都重新算矩阵,直接复用。