1. 为什么SLAM需要ESKF?
在SLAM(同步定位与地图构建)系统中,我们需要实时估计机器人自身的位姿(位置和姿态)以及周围环境的地图。传统卡尔曼滤波器(KF)在这个任务中遇到了一个根本性的问题:它假设所有状态都存在于欧几里得空间中,可以进行简单的加减运算。但现实情况是,旋转这样的状态量实际上是定义在流形上的。
举个例子,我们用四元数表示机器人的姿态时,两个四元数相加的结果很可能不再是一个有效的旋转四元数(模长不为1)。这就好比在地球表面行走时,如果你简单地把两个经纬度坐标相加,得到的新坐标很可能已经不在球面上了。这种"脱离流形"的现象会导致滤波器失效,产生无效的状态估计。
ESKF(误差状态卡尔曼滤波器)的提出正是为了解决这个问题。它的核心思路是将状态分解为两部分:名义状态(在流形上)和误差状态(在正切空间中)。这样既保持了状态的几何特性,又能在局部使用线性滤波方法。在实际SLAM系统中,这种处理方式显著提高了姿态估计的精度和稳定性。
2. 理解流形:从地球仪到SLAM
2.1 流形的直观理解
想象你是一个地球仪上的小蚂蚁。从你的视角看,地球表面每个小区域看起来都像是平坦的二维平面,这就是流形的局部欧几里得性质。但整体来看,地球表面显然不是一个平面,它有曲率,这就是流形的全局非线性特性。
在SLAM中,旋转矩阵、四元数等姿态表示都构成了特定的流形结构。比如所有单位四元数构成一个三维球面(S³),而所有旋转矩阵构成SO(3)群。这些流形空间与欧几里得空间有着本质区别:
- 不能直接相加:两个旋转矩阵相加可能不再是旋转矩阵
- 距离定义不同:旋转之间的"距离"应该用角度差,而不是矩阵元素的差值
- 更新方式特殊:旋转需要用乘法来组合,而不是加法
2.2 正切空间的作用
回到地球仪的比喻,虽然你不能直接在地球表面画直线,但在每个点都可以想象一个与球面相切的平面(正切空间)。在这个平面上,你可以进行常规的向量运算,只要确保最终结果投影回球面即可。
ESKF正是利用了这一思想:
- 名义状态始终保持在流形上
- 误差状态在正切空间中更新
- 更新完成后将误差状态"注入"回名义状态
这种方法既保持了状态的几何特性,又能在局部使用线性滤波技术。在实际代码实现中,我们经常会看到这样的模式:
// 伪代码示例 Quaternion nominal_state; // 流形上的名义状态 Vector3d error_state; // 正切空间中的误差状态 // 预测步骤 error_state = F * error_state; nominal_state = nominal_state * Exp(dt * angular_velocity); // 更新步骤 Vector3d innovation = z - H * error_state; Matrix3d K = P * H.transpose() * (H * P * H.transpose() + R).inverse(); error_state = error_state + K * innovation; nominal_state = nominal_state * Exp(K * innovation); // 将更新注入回名义状态 P = (I - K * H) * P;3. ESKF的核心思想与实现
3.1 状态分解的艺术
ESKF最精妙之处在于它对状态的特殊分解方式。与传统KF不同,ESKF将系统状态表示为:
x = x_nominal ⊕ x_error
其中⊕表示流形上的组合运算。对于旋转来说,这通常是指数映射: q = q_nominal ⊗ exp(x_error)
这种分解带来了三个关键优势:
- 误差状态x_error始终很小,线性化误差小
- 名义状态x_nominal始终保持在流形上
- 误差状态的协方差矩阵有明确的物理意义
在实际SLAM系统中,这种分解使得滤波器能够:
- 正确处理旋转的几何约束
- 保持数值稳定性
- 提供准确的协方差估计
3.2 ESKF的完整流程
一个完整的ESKF实现通常包含以下步骤:
初始化:
- 设置名义状态初值(保证在流形上)
- 误差状态初始化为零
- 初始化误差状态协方差矩阵
预测步骤:
- 预测名义状态(使用流形上的运算)
- 预测误差状态(在正切空间中线性传播)
- 更新误差状态协方差
更新步骤:
- 计算观测残差
- 计算卡尔曼增益
- 更新误差状态
- 将误差状态注入名义状态
- 更新协方差矩阵
重置步骤:
- 将更新后的误差状态重置为零
- 调整协方差矩阵以反映重置操作
这个过程看似复杂,但实际实现时可以借助现代SLAM库(如GTSAM或ceres-solver)中的流形工具来简化。例如,在C++中可以这样实现旋转部分的更新:
// 使用Sophus库处理SO(3)流形 SO3d nominal_rotation; Vector3d error_rotation; Matrix3d P_rotation; // 更新步骤 Vector3d innovation = z - H * error_rotation; Matrix3d K = P_rotation * H.transpose() * (H * P_rotation * H.transpose() + R).inverse(); error_rotation += K * innovation; nominal_rotation = nominal_rotation * SO3d::exp(K * innovation); P_rotation = (Matrix3d::Identity() - K * H) * P_rotation;4. ESKF在SLAM中的实际应用
4.1 处理IMU数据
在视觉-惯性SLAM系统中,ESKF特别适合处理IMU数据的积分。IMU提供的高频角速度测量需要连续时间积分得到姿态变化,这个过程中ESKF的优势非常明显:
- 名义状态使用四元数进行精确积分
- 误差状态捕捉积分过程中的小量误差
- 协方差矩阵反映积分累积的不确定性
实测表明,使用ESKF的IMU积分比直接使用EKF能提高约30%的姿态估计精度,特别是在剧烈运动场景下。这是因为ESKF避免了四元数线性化带来的误差积累。
4.2 多传感器融合
现代SLAM系统通常融合多种传感器数据,如激光雷达、相机、IMU等。ESKF为这种融合提供了统一的框架:
- 激光雷达:提供位置观测,更新位置误差状态
- 视觉里程计:提供相对位姿观测,更新全状态
- GPS:提供绝对位置观测,更新全局状态
在实现时,关键是要为每种观测设计合适的观测矩阵H,将误差状态映射到观测空间。例如,对于视觉特征点观测,H矩阵可能包含相机投影模型的雅可比。
4.3 实际部署中的技巧
经过多个SLAM项目的实践,我总结出一些ESKF实现的实用技巧:
协方差初始化:误差状态的初始协方差不宜设得太小,否则滤波器会过于"自信",导致收敛缓慢。
数值稳定性:定期对协方差矩阵进行对称化处理(P = (P + P')/2),防止数值误差积累。
重置策略:误差状态注入后,可以采用一阶或二阶重置方法,保持协方差的一致性。
故障检测:设置合理的卡方检验阈值,检测异常观测,提高系统鲁棒性。
参数调优:过程噪声Q和观测噪声R需要根据传感器特性仔细调整,通常需要实际数据测试。
在机器人实际运行中,一个好的ESKF实现应该能够处理以下挑战:
- 传感器数据丢失
- 剧烈运动导致的线性化误差
- 计算资源限制
- 不同传感器的时间同步问题
5. 从理论到代码:实现你自己的ESKF
5.1 基础框架搭建
要实现一个基础的ESKF,我们需要以下几个核心组件:
状态表示:
- 名义状态:位置、速度、姿态(四元数或旋转矩阵)
- 误差状态:位置误差、速度误差、角度误差(3D向量)
流形运算:
- 指数映射:将正切空间向量映射到流形
- 对数映射:将流形元素映射到正切空间
- ⊕运算:组合名义状态和误差状态
协方差管理:
- 误差状态协方差矩阵
- 过程噪声和观测噪声矩阵
一个最小化的C++类框架可能长这样:
class ESKF { public: struct State { Eigen::Vector3d position; Eigen::Vector3d velocity; Eigen::Quaterniond orientation; }; struct ErrorState { Eigen::Vector3d position; Eigen::Vector3d velocity; Eigen::Vector3d angle; }; ESKF(); void predict(const Eigen::Vector3d& acc, const Eigen::Vector3d& gyro, double dt); void update(const Eigen::VectorXd& z, const Eigen::MatrixXd& H, const Eigen::MatrixXd& R); private: State nominal_state_; ErrorState error_state_; Eigen::MatrixXd P_; // 误差状态协方差 Eigen::Matrix3d Q_imu_; // IMU过程噪声 };5.2 关键算法实现
预测步骤的实现需要特别注意流形上的积分:
void ESKF::predict(const Eigen::Vector3d& acc, const Eigen::Vector3d& gyro, double dt) { // 名义状态预测(流形上的积分) nominal_state_.position += nominal_state_.velocity * dt; nominal_state_.velocity += (nominal_state_.orientation * acc - gravity_) * dt; Eigen::Quaterniond delta_q = Eigen::Quaterniond( 1, 0.5 * gyro.x() * dt, 0.5 * gyro.y() * dt, 0.5 * gyro.z() * dt).normalized(); nominal_state_.orientation = (nominal_state_.orientation * delta_q).normalized(); // 误差状态预测(正切空间中的线性传播) Eigen::MatrixXd F = computeTransitionMatrix(nominal_state_, dt); Eigen::MatrixXd G = computeNoiseMatrix(nominal_state_, dt); error_state_ = F * error_state_; // 实际实现中误差状态通常重置为零 P_ = F * P_ * F.transpose() + G * Q_imu_ * G.transpose(); }更新步骤则需要处理误差状态的更新和注入:
void ESKF::update(const Eigen::VectorXd& z, const Eigen::MatrixXd& H, const Eigen::MatrixXd& R) { // 计算卡尔曼增益 Eigen::MatrixXd S = H * P_ * H.transpose() + R; Eigen::MatrixXd K = P_ * H.transpose() * S.inverse(); // 更新误差状态 Eigen::VectorXd innovation = z - H * error_state_; error_state_ = error_state_ + K * innovation; // 将误差状态注入名义状态 nominal_state_.position += error_state_.position; nominal_state_.velocity += error_state_.velocity; nominal_state_.orientation = nominal_state_ * Eigen::Quaterniond( 1, 0.5 * error_state_.angle.x(), 0.5 * error_state_.angle.y(), 0.5 * error_state_.angle.z()).normalized(); // 更新协方差 P_ = (Eigen::MatrixXd::Identity(P_.rows(), P_.cols()) - K * H) * P_; // 重置误差状态 error_state_ = ErrorState::Zero(); }5.3 测试与验证
实现完ESKF后,建议使用以下方法进行验证:
仿真测试:生成带有已知噪声的IMU和观测数据,验证滤波器输出是否收敛到真实值。
公开数据集测试:使用EuRoC MAV或KITTI等标准数据集,与已有实现进行对比。
实机测试:在真实机器人上部署,检查实际运行效果。
一个简单的仿真测试案例可以这样实现:
# Python仿真测试示例 import numpy as np from scipy.spatial.transform import Rotation as R # 生成真实轨迹 t = np.linspace(0, 10, 1000) true_position = np.column_stack([np.sin(t), np.cos(t), 0.1*t]) true_orientation = R.from_euler('z', t).as_quat() # 生成带噪声的IMU测量 gyro_noise = 0.01 acc_noise = 0.1 gyro_measurements = np.gradient(true_orientation, axis=0) + np.random.normal(0, gyro_noise, (1000, 4)) acc_measurements = np.gradient(true_position, axis=0, edge_order=2) + np.random.normal(0, acc_noise, (1000, 3)) # 初始化ESKF eskf = ESKF() # 运行滤波器 estimated_states = [] for i in range(1000): eskf.predict(acc_measurements[i], gyro_measurements[i], 0.01) # 假设每10步有一次位置观测 if i % 10 == 0: position_measurement = true_position[i] + np.random.normal(0, 0.1, 3) eskf.update(position_measurement, H_position, R_position) estimated_states.append(eskf.get_state()) # 绘制结果对比 plot_comparison(true_position, [s.position for s in estimated_states])在实际项目中,ESKF的实现往往需要根据具体应用场景进行调整。比如在无人机SLAM中,可能需要考虑风扰等外部因素;而在自动驾驶中,则需要处理大规模环境下的长期稳定性问题。