3. 数学基础(中):旋转矩阵、欧拉角、四元数——三种旋转表示方法及其转换

好,咱们接着聊旋转的数学表示。上一节我们把刚体变换拆成了旋转+平移,平移很简单,就是个三维向量加减。但旋转这玩意儿,说实话,坑不少。

我在项目里见过不少同事,一上来就用欧拉角,结果遇到万向锁,整个系统直接炸了。也有人死磕旋转矩阵,16个浮点数存着,看着就心疼内存。所以这一节,我把三种主流旋转表示法——旋转矩阵、欧拉角、四元数——掰开了讲清楚。你以后做融合时,该用哪个、怎么切换,心里就有数了。

核心观点:没有完美的旋转表示法。旋转矩阵直观但冗余,欧拉角易理解但会锁死,四元数高效但反直觉。实际工程中,我习惯用四元数做运算,用欧拉角做调试,用旋转矩阵做中间转换。

三种旋转表示法对比框架 旋转矩阵 R 3×3 正交矩阵 9个元素,6个约束 优点:可组合、可逆 缺点:冗余、不直观 欧拉角 (α, β, γ) 绕XYZ轴依次旋转 优点:直观、参数少 缺点:万向锁、插值难 顺序依赖性强 四元数 q w + xi + yj + zk 优点:无奇点、可插值 优点:紧凑、高效 缺点:反直觉、难调试 工程建议: • 传感器外参标定 → 用旋转矩阵(便于链式乘法) • 人机交互/调试 → 用欧拉角(直观,但注意万向锁) • SLAM位姿优化/插值 → 用四元数(无奇点、省内存) • 三种表示法之间可以互相转换,但要注意精度损失

3.1 旋转矩阵:最正统,但也最啰嗦

旋转矩阵,说白了就是一个3×3的正交矩阵,行列式为+1。它描述了一个坐标系相对于另一个坐标系的朝向。你想想看,一个点p在A系下的坐标,乘以R_AB,就得到了它在B系下的坐标。

公式很简单:p_B = R_AB * p_A

旋转矩阵的好处是——可以链式乘法。比如你有三个传感器,激光雷达→IMU→车身,外参一乘就完事了。我在做多传感器标定时,最喜欢用旋转矩阵做中间表示,因为乘法不丢精度。

但缺点也很明显:9个元素,只有3个自由度,剩下6个是冗余约束。你存一个旋转矩阵,要存9个float,而四元数只要4个。嵌入式平台上,这差距就出来了。

我的习惯:旋转矩阵只做中间转换用,不长期存储。每次标定完外参,我都会转成四元数存到配置文件中,省内存、也方便后续做插值。

3.2 欧拉角:直观,但小心「万向锁」

欧拉角用三个角度表示旋转:绕X轴转α(横滚roll)、绕Y轴转β(俯仰pitch)、绕Z轴转γ(偏航yaw)。顺序很重要!常见的顺序有ZYX、XYZ等,不同顺序得到的结果完全不同。

我在调试自动驾驶车辆时,经常用欧拉角看IMU的姿态。比如车在平路上,pitch接近0,roll接近0,yaw就是车头朝向。非常直观,一眼就能看出问题。

但欧拉角有个致命缺陷——万向锁。当pitch接近±90°时,roll和yaw的旋转轴会重合,丢失一个自由度。这时候你无论怎么调roll和yaw,效果都一样,系统就「锁死」了。

避坑指南:我曾经在一个无人机项目中,用欧拉角做姿态控制。飞机做大仰角机动时,pitch接近90°,控制器直接失稳,差点炸机。后来全部换成四元数,问题解决。所以——任何涉及大角度运动的场景,别用欧拉角做内部运算。

欧拉角转旋转矩阵的公式(以ZYX顺序为例):

R = R_z(γ) * R_y(β) * R_x(α)

其中:
R_x(α) = [1,      0,       0;
           0,  cosα,  -sinα;
           0,  sinα,   cosα]

R_y(β) = [ cosβ,  0,  sinβ;
              0,   1,     0;
          -sinβ,  0,  cosβ]

R_z(γ) = [cosγ, -sinγ,  0;
           sinγ,  cosγ,  0;
              0,     0,  1]

3.3 四元数:SLAMer的瑞士军刀

四元数,很多人一听就头大。其实没那么玄乎。它就是一个超复数:q = w + xi + yj + zk,其中w是实部,(x,y,z)是虚部,且满足i² = j² = k² = ijk = -1。

单位四元数(模长为1)可以用来表示旋转。绕单位向量n旋转θ角,对应的四元数是:

q = [cos(θ/2), n_x*sin(θ/2), n_y*sin(θ/2), n_z*sin(θ/2)]

四元数的好处太多了:

  • 无奇点——不会像欧拉角那样锁死
  • 紧凑——4个浮点数,比旋转矩阵少一半多
  • 可插值——SLAM中做位姿平滑、关键帧插值,都用球面线性插值(SLERP)
  • 组合高效——两个四元数相乘,就是旋转的复合

但缺点也有:不直观。你拿一个四元数给我,我看不出它代表什么姿态。所以我调试时,会先把四元数转成欧拉角,打印出来看。

工程经验:在LOAM系列算法中,位姿优化全部用四元数做参数化。为什么?因为优化过程中要不断迭代,如果用欧拉角,迭代到万向锁附近就崩了。四元数没有这个问题,而且雅可比矩阵也更好算。

3.4 三种表示法的相互转换

实际工程中,这三种表示法经常需要互相转换。我整理了一张表,你直接拿去用:

转换方向 方法简述 注意事项
旋转矩阵 → 四元数 从矩阵迹中提取w,再反算x,y,z 注意数值稳定性,当迹接近-1时需特殊处理
四元数 → 旋转矩阵 直接用四元数元素构造矩阵 确保四元数已归一化,否则矩阵不正交
旋转矩阵 → 欧拉角 从矩阵元素中反解三个角度 注意万向锁判断(cosβ接近0时)
欧拉角 → 旋转矩阵 按顺序乘三个基本旋转矩阵 顺序必须明确,ZYX和XYZ结果不同
四元数 → 欧拉角 先转矩阵,再转欧拉角;或直接公式 同样注意万向锁,建议用atan2避免象限问题
欧拉角 → 四元数 先转矩阵,再转四元数;或直接公式 顺序一致性是关键

下面给一段C++代码,展示四元数转旋转矩阵的典型实现。这段代码我在多个项目里用过,很稳:

// 四元数转旋转矩阵
// 输入: 单位四元数 q = [w, x, y, z]
// 输出: 3x3 旋转矩阵 R
Eigen::Matrix3d quatToRotMat(const Eigen::Quaterniond& q) {
    double w = q.w(), x = q.x(), y = q.y(), z = q.z();
    Eigen::Matrix3d R;
    R(0,0) = 1 - 2*y*y - 2*z*z;
    R(0,1) = 2*x*y - 2*w*z;
    R(0,2) = 2*x*z + 2*w*y;
    R(1,0) = 2*x*y + 2*w*z;
    R(1,1) = 1 - 2*x*x - 2*z*z;
    R(1,2) = 2*y*z - 2*w*x;
    R(2,0) = 2*x*z - 2*w*y;
    R(2,1) = 2*y*z + 2*w*x;
    R(2,2) = 1 - 2*x*x - 2*y*y;
    return R;
}

小技巧:用Eigen库的话,直接 Eigen::Quaterniond q; Eigen::Matrix3d R = q.toRotationMatrix(); 一行搞定。但面试时,我建议你还是能手写出来,很多公司会考这个。

3.5 工程中怎么选?我的决策框架

说了这么多,到底什么时候用哪个?我个人的决策逻辑是这样的:

  1. 做标定、做外参链式乘法 → 旋转矩阵。因为乘法直观,不容易出错。
  2. 做SLAM位姿优化、做插值 → 四元数。无奇点、省内存、可SLERP。
  3. 做可视化、做调试打印 → 欧拉角。一眼就能看出车是不是翻了。
  4. 做IMU姿态解算 → 四元数。因为IMU更新频率高,四元数积分比欧拉角稳定得多。

嗯,这里要注意一点:转换过程中一定要做归一化。尤其是四元数,经过多次乘法后,浮点误差会累积,模长不再为1。我习惯每帧都对四元数做一次归一化,成本很低,但能避免很多诡异的问题。

曾经踩过的坑:有一次做多雷达融合,两个雷达的外参用四元数存着,但中间经过几次坐标变换后,四元数模长变成了1.002。结果融合出来的点云出现重影,查了两天才发现是四元数没归一化。从那以后,我所有四元数运算后都加一行 q.normalize()。

好了,三种旋转表示法就讲到这里。你只要记住:旋转矩阵是「通用语」,欧拉角是「人话」,四元数是「机器语」。在不同的场景下,选对工具,你的代码会干净很多。