四元数与旋转:用四元数表示三维空间旋转
各位同学好,我是你们的老朋友。今天咱们来聊聊四元数怎么表示三维空间旋转。说实话,我刚接触四元数那会儿,也觉得这东西挺抽象的。但后来在项目里用多了,发现它其实比欧拉角、旋转矩阵都顺手。
先问大家一个问题:为什么我们要用四元数来表示旋转?
嗯,原因很简单。欧拉角有万向锁问题,旋转矩阵有冗余。而四元数,它紧凑、无奇点、插值平滑。我在做无人机姿态解算时,就吃过欧拉角的亏——飞机抬头到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数据融合时,不小心把半角写成了全角,结果姿态解算完全乱套。嗯,从那以后我每次写四元数更新代码,都会特意检查角度参数。
四元数旋转的几何意义
为了帮助大家理解,我画了一张图:
从图中可以看到,向量 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。我曾经忘记归一化,结果姿态越飘越远。
- 乘法顺序:不同的文献可能用不同的乘法约定。有的用左乘,有的用右乘。一定要搞清楚你的坐标系和约定。
- 角度单位:半角公式里,θ的单位是弧度。如果用角度,记得先转换。
我的习惯:在代码里统一用弧度,并在关键位置加断言检查四元数的模长。这样能尽早发现问题。
好了,关于四元数表示旋转的内容就讲到这里。记住,四元数不是魔法,它只是一种优雅的数学工具。多动手推导,多写代码验证,你很快就能掌握它。