4、数学基础(下):齐次坐标与变换矩阵(4x4),刚体变换的链式法则

上一讲我们把旋转矩阵、平移向量这些基础概念捋了一遍。说实话,那些东西单独看都不难,但一旦要把它们组合起来描述一个完整的刚体运动,就会遇到一个很烦人的问题——旋转是乘法,平移是加法,混在一起写公式特别容易出错。

这一讲我们就来解决这个问题。核心工具就是齐次坐标和4x4变换矩阵。我个人觉得,这是整个多传感器融合里最优雅的一个数学设计,没有之一。

4.1 为什么需要齐次坐标?

先看一个简单的场景:一个点 P 在世界坐标系下的坐标是 (x, y, z),它先绕 Z 轴旋转了 30°,再沿着 X 轴平移了 5 米。用我们之前学的公式,你得写成:

P' = R * P + t

这看起来还行对吧?但如果你有连续多个变换呢?比如:

P''' = R3 * (R2 * (R1 * P + t1) + t2) + t3

嗯,括号套括号,写起来累,读起来更累。而且你没法把多个变换“合并”成一个简洁的表达式。

齐次坐标的核心思想:给三维向量多加一个维度,把旋转和平移统一成一次矩阵乘法。

具体做法很简单——把一个三维点 (x, y, z) 表示成四维齐次坐标 (x, y, z, 1)。多出来的那个 1 就是“缩放因子”,我们一般固定为 1。

齐次坐标 vs 欧氏坐标

类型 表示方式 变换操作 优点
欧氏坐标 (x, y, z) 旋转用乘法,平移用加法 直观,占用内存少
齐次坐标 (x, y, z, 1) 旋转和平移统一为矩阵乘法 可合并、可求逆、可链式

4.2 4x4 变换矩阵的结构

一个标准的 4x4 变换矩阵长这样:

| R11  R12  R13  tx |
| R21  R22  R23  ty |
| R31  R32  R33  tz |
|  0    0    0    1  |

左上角的 3x3 是旋转矩阵 R,右上角的 3x1 是平移向量 t,最后一行固定是 [0, 0, 0, 1]。

用这个矩阵去乘一个齐次坐标点,效果等价于先旋转再平移:

| R  t |   | x |   | R*x + t |
| 0  1 | * | y | = |   ...   |
            | z |
            | 1 |

你看,一次矩阵乘法就搞定了旋转和平移。我刚开始做融合的时候,手动推导公式经常搞混旋转和平移的顺序,自从用了齐次矩阵,再也没出过这种低级错误。

我的习惯:在代码里,我一般用 Eigen 库的 Isometry3d 或 Affine3d 来管理变换矩阵。Eigen 会自动帮你处理齐次坐标的转换,你只需要关心旋转和平移的数值。

4.3 刚体变换的链式法则

这才是实战中的重头戏。在自动驾驶里,一个激光雷达点要经过好几次坐标变换才能投影到图像上:

  1. 激光雷达坐标系 → 车辆本体坐标系
  2. 车辆本体坐标系 → 惯性导航坐标系
  3. 惯性导航坐标系 → 世界坐标系
  4. 世界坐标系 → 相机坐标系
  5. 相机坐标系 → 像素坐标系

如果每个变换都用独立的旋转矩阵和平移向量去算,代码会变得又臭又长。但用齐次变换矩阵,你可以把它们乘在一起:

T_total = T_5 * T_4 * T_3 * T_2 * T_1

然后直接用这个 T_total 去变换原始点:

P_pixel = T_total * P_lidar

这就是链式法则——多个刚体变换的复合,等价于它们对应的齐次变换矩阵按顺序相乘。

我曾经踩过的坑:矩阵乘法不满足交换律!T1 * T2 和 T2 * T1 的结果完全不同。在链式法则里,变换的顺序必须严格遵循坐标系之间的层级关系。我见过有人把外参矩阵和内参矩阵乘反了,结果点云投影到图像上全跑到了左上角,排查了半天才发现是顺序问题。

4.4 逆变换与求逆

有时候我们需要求一个变换的逆——比如已知车辆到激光雷达的变换,想求激光雷达到车辆的变换。对于齐次变换矩阵,求逆有很简洁的公式:

T = | R  t |
    | 0  1 |

T^(-1) = | R^T  -R^T * t |
         |  0      1      |

注意,这里利用了旋转矩阵的逆等于它的转置这个性质。平移部分不是简单的取负号,而是要把旋转的影响也考虑进去。

我个人习惯在代码里直接用 Eigen 的 .inverse() 方法,但理解这个公式能帮你快速判断计算结果是否合理。

4.5 知识体系总览

下面这张图把这一讲的核心逻辑串起来了:

齐次坐标与变换矩阵知识体系 齐次坐标与变换矩阵知识体系 问题:旋转乘法 + 平移加法 → 公式混乱 核心方案:齐次坐标 (x, y, z, 1) + 4x4 变换矩阵 统一旋转与平移 链式法则合并变换 简洁的逆变换公式 点云投影到图像 多传感器外参标定 SLAM位姿优化 ⚠ 避坑:矩阵乘法顺序不可交换!T1*T2 ≠ T2*T1

4.6 代码实战:用 Eigen 实现链式变换

光说不练假把式。我们直接写一段代码,把刚才的理论落地:

#include <Eigen/Dense>
#include <iostream>

int main() {
    // 定义两个变换矩阵
    // T1: 绕 Z 轴旋转 30° 并平移 (1, 2, 3)
    Eigen::Isometry3d T1 = Eigen::Isometry3d::Identity();
    T1.rotate(Eigen::AngleAxisd(M_PI/6, Eigen::Vector3d::UnitZ()));
    T1.pretranslate(Eigen::Vector3d(1, 2, 3));
    
    // T2: 绕 X 轴旋转 45° 并平移 (4, 5, 6)
    Eigen::Isometry3d T2 = Eigen::Isometry3d::Identity();
    T2.rotate(Eigen::AngleAxisd(M_PI/4, Eigen::Vector3d::UnitX()));
    T2.pretranslate(Eigen::Vector3d(4, 5, 6));
    
    // 链式法则:先 T1 再 T2
    Eigen::Isometry3d T_total = T2 * T1;
    
    // 原始点 (1, 0, 0)
    Eigen::Vector4d p(1, 0, 0, 1);
    
    // 应用变换
    Eigen::Vector4d p_transformed = T_total.matrix() * p;
    
    std::cout << "变换后的点: " << p_transformed.transpose() << std::endl;
    
    // 求逆变换
    Eigen::Isometry3d T_inv = T_total.inverse();
    Eigen::Vector4d p_back = T_inv.matrix() * p_transformed;
    
    std::cout << "逆变换回原点: " << p_back.transpose() << std::endl;
    
    return 0;
}

这段代码里,Isometry3d 是 Eigen 专门为刚体变换设计的类型,它内部维护的就是一个 4x4 齐次变换矩阵。你想想看,如果不用齐次坐标,你要手动写多少行代码才能实现同样的功能?

一个小技巧:在调试时,我经常把变换矩阵打印出来,检查最后一行是不是 [0, 0, 0, 1]。如果发现最后一行变了,说明某个地方不小心做了仿射变换或投影变换,而不是刚体变换。这在传感器融合里通常意味着代码有 bug。

4.7 总结

齐次坐标和 4x4 变换矩阵,说白了就是把旋转和平移打包成一个整体。它的好处在链式法则里体现得最明显——多个坐标系之间的变换可以合并成一个矩阵,代码简洁,逻辑清晰,不容易出错。