四元数与旋转:用四元数表示三维空间旋转

各位同学好,我是你们的老朋友。今天咱们来聊聊四元数怎么表示三维空间旋转。说实话,我刚接触四元数那会儿,也觉得这东西挺抽象的。但后来在项目里用多了,发现它其实比欧拉角、旋转矩阵都顺手。

先问大家一个问题:为什么我们要用四元数来表示旋转?

嗯,原因很简单。欧拉角有万向锁问题,旋转矩阵有冗余。而四元数,它紧凑、无奇点、插值平滑。我在做无人机姿态解算时,就吃过欧拉角的亏——飞机抬头到90度时,航向角突然跳变,差点炸机。从那以后,我坚定地转向了四元数。

四元数的基本定义

四元数可以看作复数的推广。一个复数有实部和虚部,而四元数有一个实部和三个虚部:

q = w + xi + yj + zk

其中,w是实部,x、y、z是虚部。i、j、k满足以下乘法规则:

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

说白了,这些规则保证了四元数乘法的封闭性。我刚开始记这些规则时,就记住一个口诀:「循环相乘得正,反向相乘得负」。你试试看,是不是好记多了?

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

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

或者更紧凑地:

q = [w, v]

其中v = (x, y, z)是三维向量部分。

用四元数表示旋转

现在进入正题。怎么用四元数表示三维空间中的旋转?

假设我们要绕单位向量 u = (uₓ, uᵧ, u_z) 旋转角度 θ。那么对应的四元数是:

q = [cos(θ/2), uₓ·sin(θ/2), uᵧ·sin(θ/2), u_z·sin(θ/2)]

注意,这里用的是半角!为什么是半角?我稍后会解释。

这个四元数有个重要性质:它的模长等于1。即:

|q| = √(w² + x² + y² + z²) = 1

单位四元数,这是旋转四元数的基本要求。

核心要点:任何三维空间旋转,都可以用一个单位四元数唯一表示。反过来,任何单位四元数也对应一个三维旋转。

旋转公式推导

好,现在我们来推导旋转公式。假设有一个三维向量 p = (pₓ, pᵧ, p_z),我们要把它绕 u 旋转 θ 角。

第一步,把向量 p 表示成纯四元数:

p_q = [0, pₓ, pᵧ, p_z]

第二步,用旋转四元数 q 对 p_q 进行「双乘」操作:

p'_q = q ⊗ p_q ⊗ q⁻¹

其中 ⊗ 表示四元数乘法,q⁻¹ 是 q 的逆。对于单位四元数,逆等于共轭:

q⁻¹ = [w, -x, -y, -z]

第三步,p'_q 的虚部就是旋转后的向量 p'

为什么会这样?我们来推导一下。

设 q = [cos(θ/2), sin(θ/2)·u],那么:

p'_q = q ⊗ p_q ⊗ q⁻¹

展开四元数乘法,经过一系列化简(这里省略中间步骤,感兴趣的同学可以自己推),最终得到:

p' = p + 2·sin(θ/2)·cos(θ/2)·(u × p) + 2·sin²(θ/2)·(u × (u × p))

利用三角恒等式:

sinθ = 2·sin(θ/2)·cos(θ/2)
1 - cosθ = 2·sin²(θ/2)

代入得:

p' = p + sinθ·(u × p) + (1 - cosθ)·(u × (u × p))

这不就是罗德里格斯旋转公式吗?

个人经验:我在做惯性导航时,经常用这个公式来验证四元数更新的正确性。如果计算结果和罗德里格斯公式对不上,那肯定是代码写错了。

为什么用半角?

这个问题我当年也困惑过。为什么旋转θ角,四元数里却用θ/2?

原因在于四元数的「双乘」结构。你看,q ⊗ p_q ⊗ q⁻¹ 这个操作,相当于对向量做了两次「半角旋转」。两次半角加起来,正好是θ。

换句话说,四元数用半角是为了让双乘操作等价于一次全角旋转。这是一种数学上的巧妙设计。

我曾经在调试IMU数据融合时,不小心把半角写成了全角,结果姿态解算完全乱套。嗯,从那以后我每次写四元数更新代码,都会特意检查角度参数。

四元数旋转的几何意义

为了帮助大家理解,我画了一张图:

四元数旋转几何示意图 u (旋转轴) p p' θ q = [cos(θ/2), sin(θ/2)·u] p' = q ⊗ p ⊗ q⁻¹

从图中可以看到,向量 p 绕轴 u 旋转 θ 角后得到 p'。而四元数 q 就编码了这个旋转的全部信息。

四元数旋转的代码实现

在实际工程中,我们通常用代码来实现四元数旋转。下面是一个C语言示例:

// 四元数旋转向量
void quaternion_rotate(float q[4], float p[3], float result[3]) {
    // 将向量转为纯四元数
    float p_q[4] = {0, p[0], p[1], p[2]};
    
    // 计算 q 的共轭
    float q_conj[4] = {q[0], -q[1], -q[2], -q[3]};
    
    // 临时变量
    float temp[4], p_rot[4];
    
    // q ⊗ p_q
    quaternion_multiply(q, p_q, temp);
    
    // (q ⊗ p_q) ⊗ q_conj
    quaternion_multiply(temp, q_conj, p_rot);
    
    // 提取虚部
    result[0] = p_rot[1];
    result[1] = p_rot[2];
    result[2] = p_rot[3];
}

注意:四元数乘法不满足交换律!q ⊗ p_q 和 p_q ⊗ q 的结果完全不同。我在写代码时,总是先确认乘法顺序,再动手写。

四元数旋转与旋转矩阵的关系

四元数和旋转矩阵可以互相转换。给定单位四元数 q = [w, x, y, z],对应的旋转矩阵为:

分量 表达式
R₁₁ 1 - 2(y² + z²)
R₁₂ 2(xy - wz)
R₁₃ 2(xz + wy)
R₂₁ 2(xy + wz)
R₂₂ 1 - 2(x² + z²)
R₂₃ 2(yz - wx)
R₃₁ 2(xz - wy)
R₃₂ 2(yz + wx)
R₃₃ 1 - 2(x² + y²)

反过来,从旋转矩阵到四元数的转换稍微复杂一些,需要判断迹的大小。我在项目中一般直接用现成的库函数,但理解原理还是必要的。

避坑指南

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

  • 四元数归一化:每次更新后都要归一化,否则累积误差会让模长偏离1。我曾经忘记归一化,结果姿态越飘越远。
  • 乘法顺序:不同的文献可能用不同的乘法约定。有的用左乘,有的用右乘。一定要搞清楚你的坐标系和约定。
  • 角度单位:半角公式里,θ的单位是弧度。如果用角度,记得先转换。

我的习惯:在代码里统一用弧度,并在关键位置加断言检查四元数的模长。这样能尽早发现问题。

好了,关于四元数表示旋转的内容就讲到这里。记住,四元数不是魔法,它只是一种优雅的数学工具。多动手推导,多写代码验证,你很快就能掌握它。


专注资料整理