4、姿态解算算法(二):四元数基础

各位同学,欢迎回来。上一讲我们聊了欧拉角,大家应该还记得那个让人头疼的「万向锁」问题。说实话,我第一次在无人机飞控里遇到万向锁时,飞机直接翻了跟头,吓得我一身冷汗。从那以后,我就彻底倒向了四元数。

今天这一讲,我们来啃四元数这块硬骨头。别怕,我会用最接地气的方式讲明白。

4.1 从复数到四元数:一个自然的推广

先问大家一个问题:二维平面上的旋转,用什么表示最方便?

对,复数。

一个复数 \( z = a + bi \),乘以 \( e^{i\theta} \) 就相当于旋转了 \( \theta \) 角度。这个操作,我们在信号处理、电路分析里都用烂了。

那三维空间里的旋转呢?

嗯,问题来了。三维旋转有三个自由度,复数只有两个分量,不够用。数学家哈密顿当年也被这个问题折磨了很久。据说1843年的一天,他在都柏林的布鲁姆桥上散步时,突然灵光一闪——为什么不试试四个分量?

于是,四元数诞生了。

四元数的形式很简单:

q = w + xi + yj + zk

其中 \( w \) 是实部,\( x, y, z \) 是虚部。\( i, j, k \) 是三个虚数单位,它们的关系是:

  • \( i^2 = j^2 = k^2 = -1 \)
  • \( ij = k, ji = -k \)
  • \( jk = i, kj = -i \)
  • \( ki = j, ik = -j \)

你看,这个乘法规则是不是很眼熟?其实就是叉积的代数版本。我在做IMU标定时,经常用这个性质来快速验证四元数乘法的代码有没有写对。

核心要点:四元数可以看作复数的三维推广。复数用一个实部加一个虚部表示二维旋转,四元数用一个实部加三个虚部表示三维旋转。

4.2 四元数的运算法则

既然要拿四元数干活,基本的运算法则得烂熟于心。我建议你把下面这几条记在笔记本上,面试时也经常考。

4.2.1 加法与减法

跟向量一样,对应分量相加减:

q1 + q2 = (w1+w2) + (x1+x2)i + (y1+y2)j + (z1+z2)k

4.2.2 乘法(最关键!)

四元数乘法不满足交换律,也就是说 \( q_1 q_2 \neq q_2 q_1 \)。这一点跟矩阵乘法一样,千万别搞混了。

两个四元数相乘的结果是:

q1 * q2 = (w1w2 - x1x2 - y1y2 - z1z2) +
          (w1x2 + x1w2 + y1z2 - z1y2)i +
          (w1y2 - x1z2 + y1w2 + z1x2)j +
          (w1z2 + x1y2 - y1x2 + z1w2)k

看着复杂是吧?我一般用向量形式来记:

q1 = [w1, v1], q2 = [w2, v2]
q1 * q2 = [w1w2 - v1·v2, w1v2 + w2v1 + v1×v2]

这样就好记多了。点积和叉积,都是老朋友了。

我的小技巧:写代码时,我习惯把四元数乘法封装成一个函数,然后反复测试乘法结合律和分配律。如果测试通过,基本就不会有bug。

4.2.3 共轭与模长

四元数的共轭很简单,虚部取反:

q* = w - xi - yj - zk

模长就是各分量的平方和开根号:

||q|| = sqrt(w² + x² + y² + z²)

单位四元数的模长为1,这是我们最常用的。

4.3 四元数表示旋转

好了,重头戏来了。怎么用四元数表示三维旋转?

假设我们要绕单位向量 \( \mathbf{n} = (n_x, n_y, n_z) \) 旋转 \( \theta \) 角度,对应的四元数是:

q = cos(θ/2) + sin(θ/2)(n_x i + n_y j + n_z k)

注意,这里用的是半角!我第一次写代码时忘了除以2,结果旋转角度翻倍,找了半天bug。嗯,这种坑我踩过,你们就别再踩了。

旋转操作本身也很优雅:

p' = q * p * q*

其中 \( p \) 是待旋转的向量(写成纯虚四元数 \( 0 + xi + yj + zk \)),\( q* \) 是 \( q \) 的共轭。结果 \( p' \) 就是旋转后的向量。

为什么用半角?因为四元数旋转本质上是「两次镜像反射」的复合。每次反射对应一个半角旋转,两次加起来就是全角。这个解释有点抽象,你只要记住:绕n轴转θ,四元数的角度参数是θ/2。

4.4 四元数微分方程与更新

在实际系统中,陀螺仪输出的是角速度,我们需要用角速度来更新姿态。这就用到了四元数微分方程。

四元数随时间的变化率与角速度的关系是:

dq/dt = 0.5 * q * ω

其中 \( \omega = 0 + \omega_x i + \omega_y j + \omega_z k \) 是角速度四元数。

写成矩阵形式更便于编程:

| dq0/dt |   | 0    -ωx  -ωy  -ωz | | q0 |
| dq1/dt | = | ωx    0    ωz  -ωy | | q1 |
| dq2/dt |   | ωy  -ωz    0    ωx | | q2 |
| dq3/dt |   | ωz    ωy  -ωx    0 | | q3 |

有了微分方程,我们就可以做数值积分了。最常用的是一阶龙格-库塔法(RK1):

q(t+Δt) = q(t) + dq/dt * Δt

不过,我建议你用更高阶的方法,比如RK4。我在一个水下机器人项目里用过RK1,结果姿态发散得厉害,换成RK4后稳如老狗。

注意:数值积分后,四元数会偏离单位模长。必须做归一化处理,否则姿态会慢慢漂移。这个问题我们下一节细说。

4.5 四元数转欧拉角

虽然四元数做内部运算很爽,但最终输出给用户或者上层控制算法时,往往需要转成欧拉角。毕竟,俯仰、横滚、偏航这三个角度,人类更容易理解。

转换公式如下(ZYX顺序):

roll  = atan2(2(wx + yz), 1 - 2(x² + y²))
pitch = asin(2(wy - zx))
yaw   = atan2(2(wz + xy), 1 - 2(y² + z²))

这里要注意几点:

  • 俯仰角用 asin,当角度接近 ±90° 时,数值不稳定。这就是万向锁的另一种表现形式。
  • atan2 比 atan 好,能自动判断象限。
  • 不同旋转顺序(ZYX、XYZ等)的转换公式不一样,别搞混了。

我的经验:在嵌入式平台上,我一般只在日志记录和地面站显示时做这个转换。控制回路内部全程用四元数,避免欧拉角的奇异性问题。

4.6 四元数归一化

最后,我们来聊聊归一化。这步看似简单,但极其重要。

为什么需要归一化?因为数值计算中的舍入误差、积分步长选择不当,都会让四元数的模长偏离1。一旦模长偏离,旋转矩阵就不再是正交矩阵,姿态信息就会失真。

归一化的方法很简单:

norm = sqrt(w² + x² + y² + z²)
w /= norm
x /= norm
y /= norm
z /= norm

就这么几行代码。但我建议你:

  • 每次更新后都做归一化,不要偷懒
  • 如果计算资源紧张,可以用快速归一化算法(比如用牛顿迭代法近似求倒数平方根)
  • 检查归一化后的模长,如果偏离1超过某个阈值(比如1e-6),说明系统可能有问题

我曾经踩过的坑:在一个无人机项目中,我为了省CPU周期,每10步才归一化一次。结果飞行5分钟后,姿态误差累积到20度,飞机直接侧翻。从那以后,我每步必归一化,再也不敢省了。

小结

这一讲我们覆盖了四元数的核心内容:

  • 从复数到四元数的推广思路
  • 四元数的加减乘除、共轭、模长
  • 用四元数表示三维旋转(半角是关键)
  • 四元数微分方程与数值更新
  • 四元数与欧拉角的互转
  • 归一化的必要性和方法

下一讲,我们会把这些知识串起来,实现一个完整的姿态解算算法。到时候,你会看到四元数、陀螺仪、加速度计、磁力计是怎么协同工作的。

好,今天就到这里。有问题随时找我。


公众号:蓝海资料掘金营,微信deep3321