想象一下,你想描述一个陀螺在空中旋转的状态。用三个角度(欧拉角)描述时,在某些特定姿态下,描述会突然丢失一个自由度,就像陀螺被“锁住”了一样,这就是万向节死锁。而四元数提供了一种更“平滑”的描述方式,它用一个四维的“超复数”来编码旋转,从根本上避免了死锁问题。
技术定义:四元数是一个包含一个实部和三个虚部的数学对象,通常表示为 \( q = w + xi + yj + zk \),其中 \( w, x, y, z \) 是实数,\( i, j, k \) 是满足特定乘法规则的虚数单位。它可以用来紧凑且无奇异地表示三维空间中的任意旋转。
一个四元数 \( q \) 通常写成 \( q = [w, (x, y, z)] \),其中 \( w \) 称为标量部分,\( (x, y, z) \) 构成向量部分。它表示的旋转可以理解为:绕一个单位向量轴 \( \mathbf{u} = (u_x, u_y, u_z) \) 旋转 \( \theta \) 角度。其与四元数分量的关系由以下公式给出:
核心公式:给定旋转轴 \( \mathbf{u} \)(单位向量)和旋转角 \( \theta \),对应的单位四元数为:
[ q = \left[ \cos\left(\frac{\theta}{2}\right), \ \sin\left(\frac{\theta}{2}\right) \cdot (u_x, u_y, u_z) \right] ]
这个公式是理解四元数如何编码旋转的关键。注意旋转角被平分了,这是四元数表示的一个特性。
这意味着,一个表示“无旋转”(即旋转0度)的四元数是 \( [1, (0, 0, 0)] \)。而绕X轴旋转180度的四元数则是 \( [0, (1, 0, 0)] \)。
为了让你直观理解为什么需要四元数,我们直接对比三种主流的旋转表示法。在实际的机器人项目中,比如机械臂控制或无人机姿态解算,选择哪种表示法取决于具体任务。
| 表示法 | 构成 | 优点 | 缺点 | 典型应用场景 |
|---|---|---|---|---|
| 旋转矩阵 | 3x3矩阵 (9个数) | 直观,变换向量直接相乘;易于复合多个旋转 | 参数多(9个),有冗余;可能不满足正交性(数值误差导致) | 坐标变换、正向运动学 |
| 欧拉角 | 3个角度 (如 roll, pitch, yaw) | 人类最易理解;存储空间小(3个数) | 存在万向节死锁;旋转计算复杂(涉及三角函数) | 机器人初始姿态设定、用户界面显示 |
| 四元数 | 4个数 (w, x, y, z) | 无万向节死锁;插值平滑(球面线性插值);计算效率较高 | 概念抽象,不易直观理解;需要单位化约束 | 姿态插值、传感器融合(IMU)、SLAM优化 |
从表格可以看出,四元数在需要连续、平滑旋转计算的场景中优势明显。我在一个无人机姿态估计项目中,最初使用欧拉角进行互补滤波,在俯仰角接近±90度时,航向角数据会跳变,这就是死锁的典型表现。切换到四元数表示后,这个问题彻底消失。
理解了四元数是什么之后,我们来看看用它做什么。四元数的核心运算包括乘法、共轭、单位化,以及用它来旋转一个三维空间点。
下面我们用一段伪代码来展示四元数的基本操作。在实际开发中,你通常会使用成熟的数学库,如Eigen(C++)、numpy.quaternion(Python)或ROS2的geometry_msgs/msg/Quaternion,但了解底层原理至关重要。
// 假设我们有一个四元数结构体
struct Quaternion {
double w, x, y, z;
};
// 1. 单位化:确保四元数模长为1
void normalize(Quaternion &q) {
double norm = sqrt(q.w*q.w + q.x*q.x + q.y*q.y + q.z*q.z);
if (norm > 1e-12) { // 避免除以零
q.w /= norm;
q.x /= norm;
q.y /= norm;
q.z /= norm;
}
}
// 2. 四元数乘法:用于旋转的复合(注意顺序!)
Quaternion multiply(const Quaternion &q1, const Quaternion &q2) {
Quaternion result;
result.w = q1.w*q2.w - q1.x*q2.x - q1.y*q2.y - q1.z*q2.z;
result.x = q1.w*q2.x + q1.x*q2.w + q1.y*q2.z - q1.z*q2.y;
result.y = q1.w*q2.y - q1.x*q2.z + q1.y*q2.w + q1.z*q2.x;
result.z = q1.w*q2.z + q1.x*q2.y - q1.y*q2.x + q1.z*q2.w;
return result;
}
// 3. 用四元数旋转一个三维向量v = [vx, vy, vz]
void rotateVectorByQuaternion(const Quaternion &q,
double vx, double vy, double vz,
double &out_vx, double &out_vy, double &out_vz) {
// 将向量视为纯四元数 p = [0, (vx, vy, vz)]
// 旋转公式: v‘ = q * p * q^-1 (其中q^-1是q的共轭,因为q是单位四元数)
// 以下为优化后的计算,避免显式构造中间四元数
double tx = 2 * (q.y * vz - q.z * vy);
double ty = 2 * (q.z * vx - q.x * vz);
double tz = 2 * (q.x * vy - q.y * vx);
out_vx = vx + q.w * tx + (q.y * tz - q.z * ty);
out_vy = vy + q.w * ty + (q.z * tx - q.x * tz);
out_vz = vz + q.w * tz + (q.x * ty - q.y * tx);
}
编者提示:在实际的嵌入式平台(如STM32)上进行四元数运算时,如果处理器没有硬件浮点单元(FPU),大量浮点运算会成为性能瓶颈。一个实用的优化技巧是使用“定点数四元数”或快速近似单位化方法。例如,单位化时可以不使用开销大的平方根倒数,而使用几次牛顿迭代来近似,这在要求高速率的IMU数据处理中很常见。但前提是必须评估近似带来的精度损失是否在系统容忍范围内。
在实际系统中,我们经常需要在四元数、旋转矩阵和欧拉角之间转换。传感器(如IMU)可能输出欧拉角,控制系统内部使用四元数计算,最终显示又需要变回欧拉角。掌握这些转换是打通各个环节的关键。
这里给出一个从四元数计算常见“航向-俯仰-横滚”(Yaw-Pitch-Roll, 对应Z-Y-X旋转顺序)欧拉角的示例。注意,当俯仰角Pitch为±90度时,分母为零,公式失效,这就是欧拉角固有的奇点问题。
// 将单位四元数 [w, x, y, z] 转换为欧拉角 (Z-Y-X顺序),弧度制
void quaternionToEulerZYX(const Quaternion &q,
double &yaw, double &pitch, double &roll) {
// 使用旋转矩阵元素关系推导出的公式
double sinp = 2 * (q.w * q.y - q.z * q.x);
if (fabs(sinp) >= 1) {
// 处理俯仰角为±90度的边界情况(万向节死锁)
pitch = copysign(M_PI / 2, sinp); // +-90度
roll = 0;
yaw = atan2(2 * (q.x * q.y + q.w * q.z),
q.w * q.w + q.x * q.x - q.y * q.y - q.z * q.z);
} else {
pitch = asin(sinp);
roll = atan2(2 * (q.w * q.x + q.y * q.z),
1 - 2 * (q.x * q.x + q.y * q.y));
yaw = atan2(2 * (q.w * q.z + q.x * q.y),
1 - 2 * (q.y * q.y + q.z * q.z));
}
}
四元数最不可替代的优势在于姿态的平滑插值。假设你的机器人需要从姿态A匀速旋转到姿态B,如果用欧拉角直接对三个角度进行线性插值,得到的中间姿态的旋转轴会胡乱变化,运动轨迹不自然。而四元数提供了球面线性插值(Spherical Linear Interpolation, SLERP)。
生活类比:想象地球表面上的两个城市,线性插值就像穿过地心挖一条直线隧道,这不符合球面运动规律。球面线性插值则是沿着连接两城市的大圆航线飞行,这是球面上的最短路径,也是最自然的过渡。
技术定义:SLERP 是在单位四元数所在的四维超球面上,沿着连接两个四元数的最短弧进行匀速插值。它能保证插值过程中角速度恒定,从而得到最平滑自然的旋转动画。
SLERP的公式涉及点积和反三角函数,但概念直观:它计算两个四元数之间的“角度”,然后在这个角度上进行线性插值。几乎所有3D引擎和机器人中间件(如ROS的TF2库)都内置了SLERP的实现。当你需要让机械臂末端平滑移动到某个姿态,或者让相机视角平滑过渡时,SLERP是标准工具。
1. 使用数学库验证:在Python中安装 numpy 和 scipy 库。使用 scipy.spatial.transform.Rotation 类,创建一个绕Y轴旋转90度的四元数,再创建一个绕Z轴旋转45度的四元数。将它们相乘得到复合旋转的四元数,并将其转换回欧拉角看看结果。尝试修改旋转顺序,观察结果如何变化。
2. 体验死锁与插值:用欧拉角表示一个俯仰角为89度(接近90度)的姿态,然后尝试对其航向角进行微小调整(比如加1度),观察计算出的旋转矩阵或四元数是否发生剧烈变化。接着,用两个不同的四元数,使用线性插值(直接混合w,x,y,z)和SLERP分别计算中间姿态,并观察用它们旋转同一个向量,谁的路径更平滑(角速度更恒定)。
1. 判断题:四元数 [0.707, 0.707, 0, 0] 和 [-0.707, -0.707, 0, 0] 表示的是两个不同的三维旋转。
2. 选择题:在下列哪种场景下,使用四元数相比欧拉角最有优势?
A) 需要向用户显示机器人的“航向、俯仰、横滚”角时。
B) 需要将多个连续旋转合并成一个旋转时。
C) 需要对两个姿态进行平滑的动画过渡时。
D) 需要将一个点从一个坐标系变换到另一个坐标系时。
3. 判断题:单位四元数的模(w²+x²+y²+z²)必须等于1,如果不等于1,就不能用来正确表示旋转。