4、IMU运动学模型:离散时间下的IMU运动学方程、四元数更新、位置与速度更新

好,咱们进入正题。IMU的测量值——加速度和角速度——都是连续的物理量。但我们的算法跑在计算机上,计算机只认离散的时间点。所以,必须把连续的运动学方程,改写成离散形式。

说白了,就是要把「微分」变成「差分」。我刚开始做VIO的时候,觉得这一步不就是套个欧拉法嘛,有啥难的?结果真跑起来,发现姿态发散得厉害。嗯,这里面的门道,咱们得仔细捋一捋。

4.1 从连续到离散:核心思想

连续时间下,IMU的状态由下面几个微分方程描述:

四元数导数:    q̇ = 0.5 * q ⊗ ω
速度导数:      v̇ = R * a + g
位置导数:      ṗ = v

其中ω是角速度,a是加速度(都是IMU坐标系下的测量值)。R是旋转矩阵,由q转换而来。

离散化,就是给定一个时间间隔Δt(比如IMU的采样周期10ms),用上一时刻的状态,推算下一时刻的状态。你想想看,这个过程其实就是在做数值积分。

核心原则: 离散化精度直接影响VIO系统的稳定性。尤其是四元数更新,用错了方法,姿态误差会像滚雪球一样越来越大。

4.2 四元数更新:别再犯我当年的错

四元数更新,是IMU运动学里最容易被忽视的坑。我见过不少新手,直接套用欧拉法:

q_{k+1} = q_k + 0.5 * q_k ⊗ ω_k * Δt

这样做,四元数会很快失去归一化约束。你想想,四元数代表旋转,模长必须是1。一旦偏离,整个旋转就扭曲了。

正确的做法,是利用角速度的「积分」来构造一个旋转增量。我个人习惯用下面的方法:

// 假设角速度 ω_k 在 Δt 内恒定
// 1. 计算角速度的模长
double theta = norm(ω_k) * Δt;

// 2. 如果角速度接近零,直接跳过更新
if (theta < 1e-12) {
    q_{k+1} = q_k;
} else {
    // 3. 构造旋转四元数
    Eigen::Vector3d axis = ω_k / norm(ω_k);
    Eigen::Quaterniond dq(cos(theta/2), 
                          axis.x()*sin(theta/2),
                          axis.y()*sin(theta/2),
                          axis.z()*sin(theta/2));
    // 4. 更新
    q_{k+1} = q_k * dq;
}

这里有个关键假设:角速度在Δt内恒定。实际上IMU采样率够高(200Hz以上),这个假设基本成立。我曾经在低端IMU上试过50Hz采样,结果姿态发散得一塌糊涂。所以,采样率是硬门槛。

小技巧: 如果两个时刻之间的角速度变化剧烈(比如剧烈旋转),可以用「中值法」:取ω_k和ω_{k+1}的平均值来构造旋转增量。精度更高,但计算量也大一点。

4.3 位置与速度更新:小心重力

位置和速度的更新,相对直观一些。但有一个细节:加速度测量值是在IMU坐标系下的,必须旋转到世界坐标系才能积分。

离散形式如下:

// 速度更新
v_{k+1} = v_k + (R_k * a_k + g) * Δt

// 位置更新
p_{k+1} = p_k + v_k * Δt + 0.5 * (R_k * a_k + g) * Δt²

注意这里的R_k,是用上一时刻的姿态q_k构造的旋转矩阵。为什么不用更新后的q_{k+1}?因为位置和速度更新,通常和姿态更新是「串行」的——先更新姿态,再用旧姿态更新位置速度。这样做,在Δt足够小时,误差可以忽略。

但如果你追求极致精度,可以用「并行更新」:先计算中间时刻的姿态,再用它来更新位置速度。嗯,这个我一般在做高精度后端的优化时才用,前端实时跑的话,串行就够了。

避坑指南: 我曾经在室外大场景测试时,发现位置漂移异常大。排查了半天,发现是重力加速度g的方向搞反了。记住:g是重力加速度矢量,通常指向地心,即(0, 0, -9.81)。千万别写成(0, 0, 9.81),否则你的位置会「飞」起来。

4.4 完整的离散时间运动学方程

把上面三块拼起来,就是完整的离散时间IMU运动学模型:

// 输入:上一时刻状态 (q_k, v_k, p_k),IMU测量值 (ω_k, a_k),时间间隔 Δt
// 输出:当前时刻状态 (q_{k+1}, v_{k+1}, p_{k+1})

// 1. 四元数更新
double theta = norm(ω_k) * Δt;
if (theta > 1e-12) {
    Eigen::Vector3d axis = ω_k / norm(ω_k);
    Eigen::Quaterniond dq(cos(theta/2), 
                          axis.x()*sin(theta/2),
                          axis.y()*sin(theta/2),
                          axis.z()*sin(theta/2));
    q_{k+1} = q_k * dq;
} else {
    q_{k+1} = q_k;
}

// 2. 速度更新
Eigen::Matrix3d R_k = q_k.toRotationMatrix();
v_{k+1} = v_k + (R_k * a_k + g) * Δt;

// 3. 位置更新
p_{k+1} = p_k + v_k * Δt + 0.5 * (R_k * a_k + g) * Δt * Δt;

这个模型,是VIO前端中IMU预积分的「地基」。你想想看,如果没有这个离散模型,我们根本没法把IMU数据塞进优化框架里。

4.5 知识体系一览

为了让你更直观地理解这一章的核心逻辑,我画了一张流程图:

离散时间IMU运动学模型核心流程 输入:q_k, v_k, p_k, ω_k, a_k, Δt 四元数更新:构造旋转增量 速度更新:v_{k+1} = v_k + (R_k·a_k + g)·Δt 位置更新:p_{k+1} = p_k + v_k·Δt + 0.5·(R_k·a_k+g)·Δt² 输出:q_{k+1}, v_{k+1}, p_{k+1} 关键假设: • 角速度在Δt内恒定 • 加速度在Δt内恒定 • 采样率足够高(≥200Hz) ⚠ 常见错误: • 四元数未归一化 • 重力方向搞反 • 加速度未旋转到世界系

这张图把离散时间IMU运动学模型的「三步走」讲清楚了。你照着这个流程写代码,基本不会跑偏。

4.6 一些心里话

说实话,IMU运动学模型是整个VIO系统里「最物理」的部分。它不像后端优化那样充满矩阵运算,也不像前端特征匹配那样依赖图像。它就是纯粹的刚体运动学——你给它正确的输入,它就给你正确的输出。

我见过太多人,一上来就调后端优化参数,结果发现前端IMU积分都飘了。其实,先把离散运动学方程写对、测准,VIO系统就成功了一半。剩下的,无非是噪声建模和状态估计的问题。

嗯,这一章的内容就到这里。记住:离散化不是简单的「抄公式」,而是要理解每个假设背后的物理意义。下次你写IMU预积分代码的时候,不妨多想想——这个Δt内,角速度真的恒定吗?


专注资料整理