四元数基础:定义、乘法、转换与更新

各位同学,今天我们来聊聊四元数。说实话,我刚入行那会儿,看到四元数这四个字就头疼。什么虚部实部、什么超复数,听着就玄乎。但后来做项目做多了,我慢慢发现——四元数这东西,其实就是个旋转的数学工具,没那么神秘。

为什么我们要用四元数?你想想看,欧拉角有万向锁问题,方向余弦矩阵又太笨重(9个参数啊)。四元数呢?4个参数,没有奇点,计算还快。我在做某型无人机飞控时,就是靠四元数才把姿态更新跑到了200Hz。嗯,今天我们就把它彻底搞明白。

1. 四元数的定义

四元数,说白了就是一个超复数。它由一个实部和三个虚部组成:

q = q0 + q1*i + q2*j + q3*k

其中 i、j、k 满足:

i² = j² = k² = ijk = -1
ij = k, jk = i, ki = j
ji = -k, kj = -i, ik = -j

嗯,这里要注意,四元数的乘法不满足交换律。也就是说 q1*q2 不等于 q2*q1。这个特性很重要,后面讲旋转合成时会用到。

我们通常把四元数写成向量形式:

q = [q0, q1, q2, q3]ᵀ

或者更直观地:

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

其中 n 是单位旋转轴,θ 是旋转角度。这个形式我特别喜欢,因为它把旋转的几何意义说得很清楚。

核心要点:单位四元数(模长为1)才表示纯旋转。非单位四元数会引入缩放,这在惯导解算中是要避免的。

2. 四元数乘法

四元数乘法,也叫哈密顿积。两个四元数 p 和 q 相乘:

p = [p0, p1, p2, p3]
q = [q0, q1, q2, q3]

p ⊗ q = [p0*q0 - p1*q1 - p2*q2 - p3*q3,
         p0*q1 + p1*q0 + p2*q3 - p3*q2,
         p0*q2 - p1*q3 + p2*q0 + p3*q1,
         p0*q3 + p1*q2 - p2*q1 + p3*q0]

写成矩阵形式会更清晰:

p ⊗ q = [p0  -p1  -p2  -p3]   [q0]
         [p1   p0  -p3   p2] * [q1]
         [p2   p3   p0  -p1]   [q2]
         [p3  -p2   p1   p0]   [q3]

我个人习惯用这个矩阵形式来写代码,因为矩阵乘法在Python里用NumPy实现起来特别方便。我曾经在一个项目里手写四元数乘法,结果写错了符号,导致姿态解算跑飞了整整两天才找到bug。嗯,从那以后我就老老实实用矩阵形式了。

小技巧:四元数乘法对应旋转的合成。q1 ⊗ q2 表示先做 q2 旋转,再做 q1 旋转。注意顺序!

3. 四元数与旋转矩阵的转换

这个转换在惯导里太常用了。我们经常需要把四元数转成旋转矩阵,用来把加速度计数据从载体坐标系转到导航坐标系。

四元数 → 旋转矩阵:

给定单位四元数 q = [q0, q1, q2, q3],对应的旋转矩阵为:

C = [q0²+q1²-q2²-q3²,  2(q1*q2 - q0*q3),    2(q1*q3 + q0*q2)  ]
    [2(q1*q2 + q0*q3),   q0²-q1²+q2²-q3²,   2(q2*q3 - q0*q1)  ]
    [2(q1*q3 - q0*q2),   2(q2*q3 + q0*q1),   q0²-q1²-q2²+q3²  ]

旋转矩阵 → 四元数:

这个稍微麻烦一点。给定旋转矩阵 C = [Cij],我们可以这样求四元数:

q0 = 0.5 * sqrt(1 + C11 + C22 + C33)
q1 = 0.5 * sign(C32 - C23) * sqrt(1 + C11 - C22 - C33)
q2 = 0.5 * sign(C13 - C31) * sqrt(1 - C11 + C22 - C33)
q3 = 0.5 * sign(C21 - C12) * sqrt(1 - C11 - C22 + C33)

这里 sign() 是符号函数。为什么要加符号?因为四元数 (q) 和 (-q) 表示同一个旋转。我们需要通过符号来保证旋转方向的一致性。

避坑指南:我曾经在转换时忘记对四元数做归一化,结果旋转矩阵的精度越来越差。记住:转换前一定要确保四元数是单位四元数!

4. 四元数更新

终于到了最核心的部分。在捷联惯导里,陀螺仪输出的是角速度,我们需要用角速度来更新四元数。

微分方程形式:

dq/dt = 0.5 * q ⊗ ω

其中 ω = [0, ωx, ωy, ωz] 是角速度的四元数形式。

离散化更新(一阶龙格-库塔法):

Δθ = ω * Δt
q(t+Δt) = q(t) + 0.5 * q(t) ⊗ [0, Δθx, Δθy, Δθz]

写成代码就是:

def quaternion_update(q, gyro, dt):
    """
    q: 当前四元数 [q0, q1, q2, q3]
    gyro: 角速度 [wx, wy, wz] (rad/s)
    dt: 时间间隔 (s)
    """
    wx, wy, wz = gyro
    
    # 构造角增量四元数
    dq = np.array([0, wx*dt/2, wy*dt/2, wz*dt/2])
    
    # 四元数乘法更新
    q_new = q + quaternion_multiply(q, dq)
    
    # 归一化
    q_new = q_new / np.linalg.norm(q_new)
    
    return q_new

这里有个关键点:为什么更新完要归一化?因为数值积分会引入误差,导致四元数模长偏离1。如果不归一化,旋转矩阵就会失去正交性,姿态精度会逐渐下降。

实战经验:我在做某型导弹惯导时,发现单纯用一阶龙格-库塔法在高动态环境下误差偏大。后来改用了二阶龙格-库塔法,精度提升了一个数量级。如果你的系统角速度变化剧烈,建议用更高阶的积分方法。

更高精度的更新方法(二阶龙格-库塔法):

def quaternion_update_rk2(q, gyro, dt):
    wx, wy, wz = gyro
    
    # 第一步:计算中间点
    k1 = 0.5 * quaternion_multiply(q, [0, wx, wy, wz])
    q_mid = q + k1 * dt/2
    q_mid = q_mid / np.linalg.norm(q_mid)
    
    # 第二步:用中间点计算最终更新
    k2 = 0.5 * quaternion_multiply(q_mid, [0, wx, wy, wz])
    q_new = q + k2 * dt
    q_new = q_new / np.linalg.norm(q_new)
    
    return q_new

嗯,这里要注意,二阶法虽然精度高,但计算量也翻倍了。如果你的处理器性能有限,一阶法配合高采样率也是可行的。我个人的经验是:采样率在200Hz以上时,一阶法就够用了;低于100Hz时,建议用二阶法。

总结一下

四元数这东西,说白了就是四个数搞定旋转。定义要记牢,乘法要小心顺序,转换要归一化,更新要选对方法。我在实际项目中踩过的坑,今天都给你们说了。希望你们少走弯路。

下一章我们会讲姿态解算的完整流程,到时候四元数会贯穿始终。嗯,今天就到这里,有问题随时问我。