第三节:四元数——旋转的“瑞士军刀”

说实话,我刚入行那会儿,看到四元数就头疼。

四个数?还带虚部?旋转矩阵不香吗?

直到我在项目里遇到了万向锁——嗯,就是那个让机械臂突然“抽筋”的问题。从那以后,我老老实实把四元数啃了下来。今天咱们就聊聊这个感知领域绕不开的工具。

3.1 四元数的数学基础

四元数说白了就是一个扩展版的复数。复数有实部和虚部,四元数有1个实部和3个虚部。

数学上写成:

q = w + xi + yj + zk

其中 i、j、k 满足一个很关键的关系:

i² = j² = k² = ijk = -1

我习惯把四元数看作一个向量形式:

q = [w, x, y, z]ᵀ

这里 w 是实部,[x, y, z] 是虚部向量。单位四元数满足 w² + x² + y² + z² = 1,这个条件在旋转表示里特别重要。

核心理解:单位四元数表示三维空间中的旋转。一个旋转轴 n 和旋转角 θ,对应的四元数是:

q = [cos(θ/2), n·sin(θ/2)]

注意角度是 θ/2,不是 θ。这个细节我当年踩过坑。

3.2 四元数与旋转矩阵的转换

实际工程中,我们经常需要在四元数和旋转矩阵之间来回切换。为什么?因为四元数适合插值和拼接,旋转矩阵适合变换点云——各有所长。

3.2.1 四元数 → 旋转矩阵

给定单位四元数 q = [w, x, y, z],对应的旋转矩阵 R 为:

R = [[1-2(y²+z²),   2(xy-wz),   2(xz+wy)],
     [  2(xy+wz), 1-2(x²+z²),   2(yz-wx)],
     [  2(xz-wy),   2(yz+wx), 1-2(x²+y²)]]

这个公式看着复杂,但写代码时直接套用就行。我一般封装成一个函数:

def quat_to_rotmat(q):
    """四元数转旋转矩阵"""
    w, x, y, z = q
    R = np.array([
        [1-2*(y**2+z**2), 2*(x*y-w*z), 2*(x*z+w*y)],
        [2*(x*y+w*z), 1-2*(x**2+z**2), 2*(y*z-w*x)],
        [2*(x*z-w*y), 2*(y*z+w*x), 1-2*(x**2+y**2)]
    ])
    return R

3.2.2 旋转矩阵 → 四元数

反过来转换稍微麻烦一点。给定旋转矩阵 R,提取四元数的方法:

w = 0.5 * sqrt(1 + R[0,0] + R[1,1] + R[2,2])
x = (R[2,1] - R[1,2]) / (4*w)
y = (R[0,2] - R[2,0]) / (4*w)
z = (R[1,0] - R[0,1]) / (4*w)

注意:当 w 接近 0 时,这个公式会出问题。我曾经在调试IMU数据时遇到过——旋转接近180度,w ≈ 0,分母爆炸。这时候需要用其他方法,比如从对角线元素入手。

我建议用现成库函数,比如 scipy 的 Rotation.from_matrix(),但理解原理能帮你排查bug。

3.3 四元数的插值

这是四元数最实用的地方。你想想看,两个姿态之间怎么平滑过渡?用欧拉角插值会出问题,用旋转矩阵插值得到的矩阵可能不是正交的。四元数插值就优雅多了。

3.3.1 线性插值(LERP)

最简单的做法:

q(t) = (1-t) * q₁ + t * q₂

但这样插出来的四元数不是单位四元数,需要归一化:

q(t) = normalize((1-t) * q₁ + t * q₂)

LERP 的问题是角速度不均匀。当两个四元数夹角较大时,中间会“忽快忽慢”。

3.3.2 球面线性插值(SLERP)

这才是正经做法。SLERP 在单位球面上沿大圆弧匀速运动:

q(t) = [sin((1-t)θ) / sin(θ)] * q₁ + [sin(tθ) / sin(θ)] * q₂

其中 θ 是 q₁ 和 q₂ 之间的夹角:

cos(θ) = q₁ · q₂

代码实现:

def slerp(q1, q2, t):
    """四元数球面线性插值"""
    dot = np.dot(q1, q2)
    
    # 处理负点积——取最短路径
    if dot < 0:
        q2 = -q2
        dot = -dot
    
    # 防止数值问题
    if dot > 0.9995:
        return normalize((1-t) * q1 + t * q2)
    
    theta = np.arccos(dot)
    sin_theta = np.sin(theta)
    
    q = (np.sin((1-t)*theta) / sin_theta) * q1 + \
        (np.sin(t*theta) / sin_theta) * q2
    return q

实战经验:我在做激光雷达和相机联合标定时,需要用SLERP插值IMU的姿态。注意两点:一是先检查点积符号,取最短路径;二是当夹角很小时直接退化为LERP,避免除零。

3.4 知识体系总览

下面这张图概括了四元数在感知中的核心逻辑:

四元数知识体系 四元数 q = [w, x, y, z] 数学基础 复数扩展 · 单位约束 与旋转矩阵转换 双向转换 · 数值稳定性 插值方法 LERP · SLERP 虚部 i, j, k 关系 旋转轴 + 旋转角表示 四元数 → 旋转矩阵 旋转矩阵 → 四元数 LERP(线性插值) SLERP(球面插值) 应用场景:姿态估计 · 传感器融合 · 轨迹平滑

3.5 实战中的避坑指南

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

  • 四元数顺序问题:不同库的存储顺序不一样。Eigen 是 [x, y, z, w],ROS 是 [x, y, z, w],scipy 是 [w, x, y, z]。我曾经混用过,结果旋转方向全反了。
  • 单位化检查:每次从传感器读取四元数后,先检查模长是否接近1。如果偏差超过1e-6,手动归一化。数值误差会累积。
  • SLERP 的负点积:如果 q₁ · q₂ 为负,说明走的是长弧路径。取反后插值,路径更短。
  • 不要用欧拉角做插值:除非你明确知道自己在做什么。欧拉角的万向锁问题在插值时尤其明显。

一句话总结:四元数是感知工程中旋转表示的“最佳实践”。它没有万向锁,插值平滑,存储紧凑。花点时间把四元数搞透,绝对值。

可可精品资料库