旋转矩阵与欧拉角:三维空间中的旋转表示,万向锁问题

说到三维空间里的旋转,这可能是我们感知工程师每天都要打交道的东西。你想想看,一辆车在路上跑,摄像头装在车身上,车身在动,摄像头跟着动,那图像里的世界自然也在动。怎么描述这个「动」?说白了,就是旋转。

我个人习惯把旋转理解成「坐标系之间的相对姿态」。今天咱们就聊聊两种最常见的表示方式:旋转矩阵和欧拉角。嗯,还有一个绕不开的坑——万向锁。

旋转矩阵:最老实但最可靠的方式

旋转矩阵,说白了就是一个 3x3 的矩阵。它能把一个向量从一个坐标系转到另一个坐标系。比如你有一个点 P 在相机坐标系下的坐标,乘以旋转矩阵 R,就能得到它在车体坐标系下的坐标。

数学上长这样:

R = [[r11, r12, r13],
     [r21, r22, r23],
     [r31, r32, r33]]

这个矩阵有个特点:它是正交矩阵,行列式为 +1。什么意思呢?就是它不会改变向量的长度,只会改变方向。我在项目中遇到过有人直接把任意 3x3 矩阵当旋转矩阵用,结果算出来的坐标越偏越远——嗯,那肯定不对。

核心性质:

  • RT = R-1(转置等于逆)
  • det(R) = 1
  • 三个列向量两两正交,且模长为 1

旋转矩阵的好处是稳定、无歧义。坏处呢?9 个数字,冗余。而且你很难直观地看出它到底转了多大角度、绕哪个轴转的。

欧拉角:直观但暗藏杀机

欧拉角就直观多了。它用三个角度来描述旋转:绕 X 轴转多少(roll)、绕 Y 轴转多少(pitch)、绕 Z 轴转多少(yaw)。

在自动驾驶里,我们常用的是 ZYX 顺序:先绕 Z 轴转 yaw,再绕 Y 轴转 pitch,最后绕 X 轴转 roll。这个顺序不是随便定的,它对应了车辆的运动习惯——先看车头朝向(yaw),再看俯仰(pitch),最后看侧倾(roll)。

从欧拉角到旋转矩阵的转换,代码实现是这样的:

import numpy as np

def euler_to_rotation_matrix(yaw, pitch, roll):
    """
    将 ZYX 顺序的欧拉角转换为旋转矩阵
    yaw: 绕 Z 轴 (rad)
    pitch: 绕 Y 轴 (rad)
    roll: 绕 X 轴 (rad)
    """
    cy, sy = np.cos(yaw), np.sin(yaw)
    cp, sp = np.cos(pitch), np.sin(pitch)
    cr, sr = np.cos(roll), np.sin(roll)

    R = np.array([
        [cy*cp, cy*sp*sr - sy*cr, cy*sp*cr + sy*sr],
        [sy*cp, sy*sp*sr + cy*cr, sy*sp*cr - cy*sr],
        [-sp,   cp*sr,            cp*cr]
    ])
    return R

反过来,从旋转矩阵提取欧拉角:

def rotation_matrix_to_euler(R):
    """
    从旋转矩阵提取 ZYX 欧拉角
    注意:当 pitch 接近 ±90° 时,会出现万向锁
    """
    pitch = np.arcsin(-R[2, 0])
    
    if np.abs(np.cos(pitch)) > 1e-6:
        yaw = np.arctan2(R[1, 0], R[0, 0])
        roll = np.arctan2(R[2, 1], R[2, 2])
    else:
        # 万向锁情况
        yaw = 0.0
        roll = np.arctan2(R[0, 1], R[0, 2])
    
    return yaw, pitch, roll

我的经验:在实际项目中,我一般用欧拉角做可视化调试,因为直观。但内部计算,尤其是做融合、优化的时候,我坚决用旋转矩阵或四元数。为什么?因为欧拉角有万向锁这个坑。

万向锁:一个让新手崩溃的问题

为什么会这样?

万向锁发生在第二个旋转角(pitch)接近 ±90° 的时候。这时候,第一个和第三个旋转轴会变得共线,导致你丢失了一个自由度。

举个例子:假设你先把车头朝北(yaw=0°),然后 pitch 转到 90°(车头朝天),这时候你再转 roll,你会发现车头还是在绕垂直轴转——跟 yaw 的效果一样。也就是说,yaw 和 roll 变得无法区分了。

我曾经在做一个多传感器标定项目时,用欧拉角做优化变量。结果发现优化器怎么都不收敛,最后定位到是万向锁的问题。从那以后,我只要涉及优化,一律用旋转矩阵或四元数。

避坑指南:

  • 不要用欧拉角做插值——会出现奇怪的路径
  • 不要用欧拉角做优化——万向锁会导致雅可比矩阵奇异
  • 如果一定要用,限制 pitch 范围在 (-90°, 90°) 之外

三种表示方式的对比

特性 旋转矩阵 欧拉角 四元数
参数数量 9 3 4
直观性 差 好 中
万向锁 无 有 无
插值 困难 有奇异性 容易(球面插值)
计算效率 中 高 高
适合场景 内部计算、优化 可视化、人机交互 插值、融合

知识体系结构图

下面这张图展示了三维旋转表示的核心逻辑和它们之间的关系:

三维空间旋转表示 — 知识体系 旋转矩阵 (3×3) 欧拉角 (yaw, pitch, roll) 四元数 (w, x, y, z) 无奇异性 · 冗余参数 直观 · 有万向锁问题 紧凑 · 适合插值 内部计算 · 优化 · 标定 可视化 · 人机交互 · 调试 姿态融合 · SLAM · 插值 核心建议:可视化用欧拉角,计算用旋转矩阵或四元数

实战中的选择建议

说了这么多,到底该用哪个?我的建议很简单:

  • 做可视化、写日志、调参数:用欧拉角。一眼就能看出车是不是翻了、头是不是抬起来了。
  • 做优化、做融合、做标定:用旋转矩阵或四元数。别给自己找麻烦。
  • 做插值、做平滑:用四元数。球面线性插值(SLERP)效果非常好。

一个小技巧:我在代码里经常同时保留两种表示。欧拉角用来给人看,旋转矩阵用来给机器算。中间加个转换函数,两边都舒服。

嗯,旋转这件事,说难不难,说简单也不简单。关键是理解每种表示方式的优缺点,知道什么时候该用哪个。下次咱们聊聊四元数,那个东西虽然看着抽象,但用起来是真的香。

可可精品资料库