3、刚体变换基础:旋转矩阵、平移向量、齐次坐标与齐次变换矩阵

各位同学好,我是你们的老朋友。今天我们来聊聊多传感器融合里最基础、也最绕不开的一块内容——刚体变换。

说实话,我刚入行那会儿,觉得这东西不就是矩阵乘来乘去嘛,有啥好学的?结果第一次做激光雷达和相机的联合标定,就被齐次坐标给坑了一把。嗯,从那以后我再也不敢小看这章内容了。

你想想看,我们车上装了激光雷达、摄像头、IMU、GPS,每个传感器都有自己的坐标系。怎么把激光点云投影到图像上?怎么把IMU的加速度转换到车体坐标系?说白了,全靠刚体变换在背后撑腰。

核心思想:刚体变换 = 旋转 + 平移。物体形状不变,只是位置和朝向变了。

3.1 旋转矩阵——描述“朝向”的数学工具

先聊旋转矩阵。我个人习惯把它理解成“坐标系的映射关系”。

假设有个点P,在坐标系A下的坐标是P_A。现在我想知道它在坐标系B下的坐标P_B。如果两个坐标系原点重合,只是旋转了,那这个关系就是:

P_B = R * P_A

这里的R就是旋转矩阵。它是一个3x3的矩阵,有几个重要性质:

  • 正交性:R^T * R = I,也就是说它的逆等于它的转置
  • 行列式为+1:这是右手系的要求,如果是-1那就是反射变换了
  • 三个自由度:虽然9个元素,但受正交约束,实际只有3个独立参数

我在项目中遇到过一个问题:有人直接用欧拉角做插值,结果出现了万向锁。那场景真是...无人机姿态控制直接乱跳。所以后来我学乖了,能用旋转矩阵就用旋转矩阵,别偷懒用欧拉角。

小技巧:验证一个矩阵是不是合法的旋转矩阵,直接算R^T * R,看是不是单位阵。误差在1e-6以内就算合格。

3.2 平移向量——描述“位置”的偏移

旋转搞定了,那平移就简单多了。平移向量t就是两个坐标系原点之间的位移。

完整的刚体变换公式:

P_B = R * P_A + t

这里t是3x1的向量,表示从坐标系A原点到坐标系B原点的向量,在坐标系B下的表示。

嗯,这句话容易搞混。我刚开始也经常弄反。你记住一点:t的坐标系是目标坐标系,也就是等号左边的那个坐标系。

注意:平移向量和旋转矩阵是“绑定”的。你不能随便换一个旋转矩阵,还用原来的平移向量。它们必须来自同一个变换关系。

3.3 齐次坐标——为什么要引入它?

好,问题来了。上面那个公式P_B = R * P_A + t,写起来不优雅。你想想看,如果连续做多次变换:

P_C = R2 * (R1 * P_A + t1) + t2

这式子越来越长,而且旋转和平移混在一起,看着就烦。

齐次坐标就是来解决这个问题的。它给三维点加一个维度,变成四维:

P_homogeneous = [x, y, z, 1]^T

为什么是1?因为这样平移就能用矩阵乘法表示了。如果是方向向量(比如法线),最后一位是0。

记住这个口诀:点用1,向量用0。点平移后位置变,向量平移后不变。

3.4 齐次变换矩阵——把旋转和平移打包

有了齐次坐标,我们就可以把旋转和平移合并成一个4x4的矩阵T:

T = [R  t]
    [0  1]

这样,一次变换就变成了一个矩阵乘法:

P_B_homo = T * P_A_homo

连续变换就更简洁了:

T_AC = T_BC * T_AB

注意顺序!矩阵乘法不满足交换律。先转A到B,再转B到C,这个顺序不能乱。

我记得有一次帮同事排查标定问题,他死活说外参不对。我一看代码,他把变换矩阵乘反了。嗯,这种错误我犯过不止一次,现在每次写变换之前都会先画个坐标系关系图。

3.5 知识体系总览

下面这张图是我自己整理的,把本章的核心逻辑串起来了:

刚体变换知识体系 刚体变换 旋转(3自由度) 平移(3自由度) 旋转矩阵 / 欧拉角 / 四元数 平移向量 t 齐次坐标 [x, y, z, 1]^T 齐次变换矩阵 T = [R t; 0 1]

3.6 实战中的注意事项

讲完理论,说点实际的。在做外参标定时,有几个坑我替你们踩过了:

  1. 坐标系定义要统一:前左上还是右前上?不同传感器厂商习惯不同。我见过一个项目,激光雷达用前左上,相机用右前下,结果标定出来全是错的。
  2. 变换顺序不能错:先旋转后平移,这个顺序是固定的。你要是先平移后旋转,结果完全不一样。
  3. 齐次坐标最后一维:做投影时记得归一化,把最后一维变成1。我见过有人忘了这步,点云投影到图像上全跑到画面外面去了。

我的习惯:每次写变换代码之前,先在纸上画清楚坐标系关系图。标清楚每个变换是从哪到哪,再动手写代码。这步花5分钟,能省你2小时调试时间。

3.7 代码示例:用Python实现刚体变换

最后给一段我常用的代码模板,你们可以直接拿去用:

import numpy as np

def build_transform(R, t):
    """构建齐次变换矩阵"""
    T = np.eye(4)
    T[:3, :3] = R
    T[:3, 3] = t.flatten()
    return T

def transform_point(T, point):
    """对单个点做刚体变换"""
    p_homo = np.append(point, 1.0)
    p_transformed = T @ p_homo
    return p_transformed[:3] / p_transformed[3]

def transform_points(T, points):
    """对多个点做刚体变换(Nx3矩阵)"""
    N = points.shape[0]
    points_homo = np.hstack([points, np.ones((N, 1))])
    points_transformed = (T @ points_homo.T).T
    return points_transformed[:, :3] / points_transformed[:, 3:4]

# 示例:绕Z轴旋转90度,再平移(1, 2, 3)
theta = np.pi / 2
R = np.array([[np.cos(theta), -np.sin(theta), 0],
              [np.sin(theta),  np.cos(theta), 0],
              [0,              0,             1]])
t = np.array([[1], [2], [3]])

T = build_transform(R, t)
point = np.array([1, 0, 0])
result = transform_point(T, point)
print(f"变换结果: {result}")  # 应该输出 [1, 3, 3]

这段代码我用了好几年,基本没出过问题。唯一要注意的是,传入的平移向量t要是列向量,形状是(3,1),不是(3,)。

好了,刚体变换这块就聊到这儿。说白了就是旋转加平移,齐次坐标让表达更优雅。你们自己动手写写代码,跑几个例子,很快就熟了。

可可精品资料库