在机器人定位与建图领域,FAST-LIO因其高效的激光-惯性里程计融合算法而备受关注。但真正让开发者头疼的,往往是那些隐藏在数学公式背后的工程实现细节。本文将带您深入FAST-LIO的核心代码层,揭示状态传播与雅可比计算的实现奥秘。
FAST-LIO的状态变量构成了整个系统的基础框架。让我们先看代码中state_ikfom结构体的定义:
cpp复制struct state_ikfom {
Eigen::Vector3d pos = Eigen::Vector3d(0,0,0); // 全局位置 (x,y,z)
Sophus::SO3 rot = Sophus::SO3(Eigen::Matrix3d::Identity()); // 全局旋转
Sophus::SO3 offset_R_L_I = Sophus::SO3(Eigen::Matrix3d::Identity()); // LiDAR到IMU的旋转偏移
Eigen::Vector3d offset_T_L_I = Eigen::Vector3d(0,0,0); // LiDAR到IMU的平移偏移
Eigen::Vector3d vel = Eigen::Vector3d(0,0,0); // 线速度
Eigen::Vector3d bg = Eigen::Vector3d(0,0,0); // 陀螺仪偏置
Eigen::Vector3d ba = Eigen::Vector3d(0,0,0); // 加速度计偏置
Eigen::Vector3d grav = Eigen::Vector3d(0,0,-G_m_s2); // 重力向量
};
这个24维状态向量可以分解为以下几个关键部分:
pos和旋转rot表示机器人在全局坐标系中的位姿offset_R_L_I和offset_T_L_I描述LiDAR与IMU之间的外参vel反映当前运动状态bg和ba分别补偿陀螺仪和加速度计的测量偏差grav表示重力加速度向量注意:在实际应用中,重力向量通常初始化为(0,0,-9.81),但会被作为状态变量参与优化。
状态转移函数f(x,u)描述了系统状态如何随时间演化。以下是get_f函数的完整实现:
cpp复制Eigen::Matrix<double, 24, 1> get_f(state_ikfom s, input_ikfom in) {
Eigen::Matrix<double, 24, 1> res = Eigen::Matrix<double, 24, 1>::Zero();
Eigen::Vector3d omega = in.gyro - s.bg; // 补偿偏置后的角速度
Eigen::Vector3d a_inertial = s.rot.matrix() * (in.acc - s.ba); // 世界坐标系下的加速度
for (int i = 0; i < 3; i++) {
res(i) = s.vel[i]; // 速度分量
res(i + 3) = omega[i]; // 角速度分量
res(i + 12) = a_inertial[i] + s.grav[i]; // 加速度分量
}
return res;
}
这个函数对应着以下数学表达:
f(x,u) =
\begin{bmatrix}
v \
\omega - b_g \
0 \
0 \
R(a - b_a) + g \
0 \
0 \
0
\end
关键实现细节:
omega = in.gyro - s.bg实现了对陀螺仪测量值的偏置补偿a_inertial将加速度从IMU坐标系转换到世界坐标系a_inertial[i] + s.grav[i]将重力加速度纳入状态方程雅可比矩阵在误差状态传播中起着关键作用。FAST-LIO需要计算两个雅可比矩阵:∂f/∂x和∂f/∂w。
cpp复制Eigen::Matrix<double, 24, 24> df_dx(state_ikfom s, input_ikfom in) {
Eigen::Matrix<double, 24, 24> cov = Eigen::Matrix<double, 24, 24>::Zero();
cov.block<3, 3>(0, 12) = Eigen::Matrix3d::Identity(); // ∂v/∂v
Eigen::Vector3d acc_ = in.acc - s.ba; // 补偿偏置后的加速度
cov.block<3, 3>(12, 3) = -s.rot.matrix() * Sophus::SO3::hat(acc_); // ∂a/∂θ
cov.block<3, 3>(12, 18) = -s.rot.matrix(); // ∂a/∂ba
cov.template block<3, 3>(12, 21) = Eigen::Matrix3d::Identity(); // ∂a/∂g
cov.template block<3, 3>(3, 15) = -Eigen::Matrix3d::Identity(); // ∂ω/∂bg
return cov;
}
这个实现对应论文中的公式(7),主要包含以下几个关键块:
| 矩阵块 | 位置 | 物理意义 | 代码实现 |
|---|---|---|---|
| I₃ | (0,12) | 速度对速度的导数 | cov.block<3,3>(0,12) |
| -R[a]× | (12,3) | 加速度对旋转的导数 | -s.rot.matrix()*Sophus::SO3::hat(acc_) |
| -R | (12,18) | 加速度对加速度偏置的导数 | -s.rot.matrix() |
| I₃ | (12,21) | 加速度对重力的导数 | Eigen::Matrix3d::Identity() |
| -I₃ | (3,15) | 角速度对陀螺偏置的导数 | -Eigen::Matrix3d::Identity() |
cpp复制Eigen::Matrix<double, 24, 12> df_dw(state_ikfom s, input_ikfom in) {
Eigen::Matrix<double, 24, 12> cov = Eigen::Matrix<double, 24, 12>::Zero();
cov.block<3, 3>(12, 3) = -s.rot.matrix(); // ∂a/∂wa
cov.block<3, 3>(3, 0) = -Eigen::Matrix3d::Identity(); // ∂ω/∂wω
cov.block<3, 3>(15, 6) = Eigen::Matrix3d::Identity(); // ∂bg/∂wbg
cov.block<3, 3>(18, 9) = Eigen::Matrix3d::Identity(); // ∂ba/∂wba
return cov;
}
噪声雅可比矩阵描述了过程噪声对状态的影响:
前向传播predict函数整合了前述所有组件:
cpp复制void predict(double &dt, Eigen::Matrix<double, 12, 12> &Q, const input_ikfom &i_in) {
Eigen::Matrix<double, 24, 1> f_ = get_f(x_, i_in); // 计算f(x,u)
Eigen::Matrix<double, 24, 24> f_x_ = df_dx(x_, i_in); // 计算Fx
Eigen::Matrix<double, 24, 12> f_w_ = df_dw(x_, i_in); // 计算Fw
x_ = boxplus(x_, f_ * dt); // 状态传播
f_x_ = Matrix<double, 24, 24>::Identity() + f_x_ * dt; // 离散化
// 协方差传播
P_ = (f_x_)*P_ * (f_x_).transpose() + (dt * f_w_) * Q * (dt * f_w_).transpose();
}
前向传播过程分为三个关键步骤:
boxplus操作实现状态更新提示:boxplus操作实现了流形上的状态更新,正确处理了SO(3)旋转矩阵的特殊性质。
FAST-LIO使用boxplus和boxminus操作来处理流形上的状态更新:
cpp复制state_ikfom boxplus(state_ikfom x, Eigen::Matrix<double, 24, 1> f_) {
state_ikfom x_r;
x_r.pos = x.pos + f_.block<3, 1>(0, 0);
x_r.rot = x.rot * Sophus::SO3::exp(f_.block<3, 1>(3, 0));
x_r.offset_R_L_I = x.offset_R_L_I * Sophus::SO3::exp(f_.block<3, 1>(6, 0));
x_r.offset_T_L_I = x.offset_T_L_I + f_.block<3, 1>(9, 0);
x_r.vel = x.vel + f_.block<3, 1>(12, 0);
x_r.bg = x.bg + f_.block<3, 1>(15, 0);
x_r.ba = x.ba + f_.block<3, 1>(18, 0);
x_r.grav = x.grav + f_.block<3, 1>(21, 0);
return x_r;
}
这个实现有几个值得注意的细节:
对应的boxminus操作实现了状态差的计算:
cpp复制vectorized_state boxminus(state_ikfom x1, state_ikfom x2) {
vectorized_state x_r = vectorized_state::Zero();
x_r.block<3, 1>(0, 0) = x1.pos - x2.pos;
x_r.block<3, 1>(3, 0) = Sophus::SO3(x2.rot.matrix().transpose() * x1.rot.matrix()).log();
x_r.block<3, 1>(6, 0) = Sophus::SO3(x2.offset_R_L_I.matrix().transpose() * x1.offset_R_L_I.matrix()).log();
x_r.block<3, 1>(9, 0) = x1.offset_T_L_I - x2.offset_T_L_I;
x_r.block<3, 1>(12, 0) = x1.vel - x2.vel;
x_r.block<3, 1>(15, 0) = x1.bg - x2.bg;
x_r.block<3, 1>(18, 0) = x1.ba - x2.ba;
x_r.block<3, 1>(21, 0) = x1.grav - x2.grav;
return x_r;
}
关键点在于旋转差的计算使用了SO3的对数映射,这保证了旋转差值始终在切空间(so(3))中表示。