结合书籍《自动驾驶与机器人中的SLAM技术:从理论到实践》和配套源码,来理解ESKF(Error state Kalman Filter,误差卡尔曼滤波器)。
ESKF代码入口
代码入口
ch3目录下的run_eskf_gins.cc文件主函数main()
int main(int argc, char** argv) {
...
io.SetIMUProcessFunc([&](const sad::IMU& imu) {
/// IMU 处理函数
if (!imu_init.InitSuccess()) {
imu_init.AddIMU(imu);
return;
}
/// 需要IMU初始化
if (!imu_inited) {
// 读取初始零偏,设置ESKF
sad::ESKFD::Options options;
// 噪声由初始化器估计
options.gyro_var_ = sqrt(imu_init.GetCovGyro()[0]);
options.acce_var_ = sqrt(imu_init.GetCovAcce()[0]);
eskf.SetInitialConditions(options, imu_init.GetInitBg(), imu_init.GetInitBa(), imu_init.GetGravity());
imu_inited = true;
return;
}
if (!gnss_inited) {
/// 等待有效的RTK数据
return;
}
/// GNSS 也接收到之后,再开始进行预测
eskf.Predict(imu);
/// predict就会更新ESKF,所以此时就可以发送数据
auto state = eskf.GetNominalState();
if (ui) {
ui->UpdateNavState(state);
}
/// 记录数据以供绘图
save_result(fout, state);
usleep(1e3);
})
.SetGNSSProcessFunc([&](const sad::GNSS& gnss) {
/// GNSS 处理函数
if (!imu_inited) {
return;
}
sad::GNSS gnss_convert = gnss;
if (!sad::ConvertGps2UTM(gnss_convert, antenna_pos, FLAGS_antenna_angle) || !gnss_convert.heading_valid_) {
return;
}
/// 去掉原点
if (!first_gnss_set) {
origin = gnss_convert.utm_pose_.translation();
first_gnss_set = true;
}
gnss_convert.utm_pose_.translation() -= origin;
// 要求RTK heading有效,才能合入ESKF
eskf.ObserveGps(gnss_convert);
auto state = eskf.GetNominalState();
if (ui) {
ui->UpdateNavState(state);
}
save_result(fout, state);
gnss_inited = true;
})
...
}
初始化
IMU初始化:使用静止初始化方法。
变量和类型定义
类型定义
class ESKF {
public:
/// 类型定义
using SO3 = Sophus::SO3<S>; // 旋转变量类型
using VecT = Eigen::Matrix<S, 3, 1>; // 向量类型
using Vec18T = Eigen::Matrix<S, 18, 1>; // 18维向量类型
using Mat3T = Eigen::Matrix<S, 3, 3>; // 3x3矩阵类型
using MotionNoiseT = Eigen::Matrix<S, 18, 18>; // 运动噪声类型
using OdomNoiseT = Eigen::Matrix<S, 3, 3>; // 里程计噪声类型
using GnssNoiseT = Eigen::Matrix<S, 6, 6>; // GNSS噪声类型
using Mat18T = Eigen::Matrix<S, 18, 18>; // 18维方差类型
using NavStateT = NavState<S>; // 整体名义状态变量类型
配置项
struct Options {
Options() = default;
/// IMU 测量与零偏参数
double imu_dt_ = 0.01; // IMU测量间隔
// NOTE IMU噪声项都为离散时间,不需要再乘dt,可以由初始化器指定IMU噪声
double gyro_var_ = 1e-5; // 陀螺测量标准差
double acce_var_ = 1e-2; // 加计测量标准差
double bias_gyro_var_ = 1e-6; // 陀螺零偏游走标准差
double bias_acce_var_ = 1e-4; // 加计零偏游走标准差
/// 里程计参数
double odom_var_ = 0.5;
double odom_span_ = 0.1; // 里程计测量间隔
double wheel_radius_ = 0.155; // 轮子半径
double circle_pulse_ = 1024.0; // 编码器每圈脉冲数
/// RTK 观测参数
double gnss_pos_noise_ = 0.1; // GNSS位置噪声
double gnss_height_noise_ = 0.1; // GNSS高度噪声
double gnss_ang_noise_ = 1.0 * math::kDEG2RAD; // GNSS旋转噪声
/// 其他配置
bool update_bias_gyro_ = true; // 是否更新陀螺bias
bool update_bias_acce_ = true; // 是否更新加计bias
};
/// 成员变量
double current_time_ = 0.0; // 当前时间
名义状态、误差状态、协方差、噪声矩阵
/// 名义状态
VecT p_ = VecT::Zero();
VecT v_ = VecT::Zero();
SO3 R_;
VecT bg_ = VecT::Zero();
VecT ba_ = VecT::Zero();
VecT g_{0, 0, -9.8};
/// 误差状态
Vec18T dx_ = Vec18T::Zero();
/// 协方差阵
Mat18T cov_ = Mat18T::Identity();
/// 噪声阵
MotionNoiseT Q_ = MotionNoiseT::Zero();
OdomNoiseT odom_noise_ = OdomNoiseT::Zero();
GnssNoiseT gnss_noise_ = GnssNoiseT::Zero();
/// 标志位
bool first_gnss_ = true; // 是否为第一个gnss数据
/// 配置项
Options options_;
}
IMU初始化
设置初始条件
/**
* 设置初始条件
* @param options 噪声项配置
* @param init_bg 初始零偏 陀螺
* @param init_ba 初始零偏 加计
* @param gravity 重力
*/
void SetInitialConditions(Options options, const VecT& init_bg, const VecT& init_ba,
const VecT& gravity = VecT(0, 0, -9.8)) {
BuildNoise(options);
options_ = options;
bg_ = init_bg;
ba_ = init_ba;
g_ = gravity;
cov_ = Mat18T::Identity() * 1e-4;
}
设置噪声
void BuildNoise(const Options& options) {
double ev = options.acce_var_;
double et = options.gyro_var_;
double eg = options.bias_gyro_var_;
double ea = options.bias_acce_var_;
double ev2 = ev; // * ev;
double et2 = et; // * et;
double eg2 = eg; // * eg;
double ea2 = ea; // * ea;
设置过程噪声
// 设置过程噪声
Q_.diagonal() << 0, 0, 0, ev2, ev2, ev2, et2, et2, et2, eg2, eg2, eg2, ea2, ea2, ea2, 0, 0, 0;
设置其他噪声
// 设置里程计噪声
double o2 = options_.odom_var_ * options_.odom_var_;
odom_noise_.diagonal() << o2, o2, o2;
// 设置GNSS状态
double gp2 = options.gnss_pos_noise_ * options.gnss_pos_noise_;
double gh2 = options.gnss_height_noise_ * options.gnss_height_noise_;
double ga2 = options.gnss_ang_noise_ * options.gnss_ang_noise_;
gnss_noise_.diagonal() << gp2, gp2, gh2, ga2, ga2, ga2;
}
IMU初始化
初始化逻辑:
(1)对于imu的数据,将其放入双端队列deque中,若队列中的imu数据不足init_imu_queue_max_size_(默认2000)个,继续接收并添加imu的数据;
(2)当deque中的数据个数大于 init_imu_queue_max_size_时,队列pop出最老的imu数据;(deuqe中保持最新的imu数据);
(3)初始化系统状态 state(包括cov_gyro_、cov_acce_、init_bg_、init_ba_、gravity_等等)。对deque中的imu数据求均值与方差:
陀螺仪的均值mean_gyro、方差cov_gyro,如果方差太大(默认值为0.5),则初始化失败。
加速度计的均值mean_acce、方差cov_acce,如果方差太大(默认值为0.05),则初始化失败。
IMU初始化成功、接收到GPS数据后,开始预测。
public:
struct Options {
Options() {}
double init_time_seconds_ = 10.0; // 静止时间
int init_imu_queue_max_size_ = 2000; // 初始化IMU队列最大长度
int static_odom_pulse_ = 5; // 静止时轮速计输出噪声
double max_static_gyro_var = 0.5; // 静态下陀螺测量方差
double max_static_acce_var = 0.05; // 静态下加计测量方差
double gravity_norm_ = 9.81; // 重力大小
bool use_speed_for_static_checking_ = true; // 是否使用odom来判断车辆静止(部分数据集没有odom选项)
};
/// 构造函数
StaticIMUInit(Options options = Options()) : options_(options) {}
/// 添加IMU数据
bool AddIMU(const IMU& imu);
/// 添加轮速数据
bool AddOdom(const Odom& odom);
/// 判定初始化是否成功
bool InitSuccess() const { return init_success_; }
/// 获取各Cov, bias, gravity
Vec3d GetCovGyro() const { return cov_gyro_; }
Vec3d GetCovAcce() const { return cov_acce_; }
Vec3d GetInitBg() const { return init_bg_; }
Vec3d GetInitBa() const { return init_ba_; }
Vec3d GetGravity() const { return gravity_; }
private:
/// 尝试对系统初始化
bool TryInit();
Options options_; // 选项信息
bool init_success_ = false; // 初始化是否成功
Vec3d cov_gyro_ = Vec3d::Zero(); // 陀螺测量噪声协方差(初始化时评估)
Vec3d cov_acce_ = Vec3d::Zero(); // 加计测量噪声协方差(初始化时评估)
Vec3d init_bg_ = Vec3d::Zero(); // 陀螺初始零偏
Vec3d init_ba_ = Vec3d::Zero(); // 加计初始零偏
Vec3d gravity_ = Vec3d::Zero(); // 重力
bool is_static_ = false; // 标志车辆是否静止
std::deque<IMU> init_imu_deque_; // 初始化用的数据
double current_time_ = 0.0; // 当前时间
double init_start_time_ = 0.0; // 静止的初始时间
AddIMU
bool StaticIMUInit::AddIMU(const IMU& imu) {
if (init_success_) {
return true;
}
if (options_.use_speed_for_static_checking_ && !is_static_) {
LOG(WARNING) << "等待车辆静止";
init_imu_deque_.clear();
return false;
}
if (init_imu_deque_.empty()) {
// 记录初始静止时间
init_start_time_ = imu.timestamp_;
}
// 记入初始化队列
init_imu_deque_.push_back(imu);
double init_time = imu.timestamp_ - init_start_time_; // 初始化经过时间
if (init_time > options_.init_time_seconds_) {
// 尝试初始化逻辑
TryInit();
}
// 维持初始化队列长度
while (init_imu_deque_.size() > options_.init_imu_queue_max_size_) {
init_imu_deque_.pop_front();
}
current_time_ = imu.timestamp_;
return false;
}
TryInit()
bool StaticIMUInit::TryInit() {
if (init_imu_deque_.size() < 10) {
return false;
}
// 计算均值和方差
Vec3d mean_gyro, mean_acce;
math::ComputeMeanAndCovDiag(init_imu_deque_, mean_gyro, cov_gyro_, [](const IMU& imu) { return imu.gyro_; });
math::ComputeMeanAndCovDiag(init_imu_deque_, mean_acce, cov_acce_, [this](const IMU& imu) { return imu.acce_; });
// 以acce均值为方向,取9.8长度为重力
LOG(INFO) << "mean acce: " << mean_acce.transpose();
gravity_ = -mean_acce / mean_acce.norm() * options_.gravity_norm_;
// 重新计算加计的协方差
math::ComputeMeanAndCovDiag(init_imu_deque_, mean_acce, cov_acce_,
[this](const IMU& imu) { return imu.acce_ + gravity_; });
// 检查IMU噪声
if (cov_gyro_.norm() > options_.max_static_gyro_var) {
LOG(ERROR) << "陀螺仪测量噪声太大" << cov_gyro_.norm() << " > " << options_.max_static_gyro_var;
return false;
}
if (cov_acce_.norm() > options_.max_static_acce_var) {
LOG(ERROR) << "加计测量噪声太大" << cov_acce_.norm() << " > " << options_.max_static_acce_var;
return false;
}
// 估计测量噪声和零偏
init_bg_ = mean_gyro;
init_ba_ = mean_acce;
LOG(INFO) << "IMU 初始化成功,初始化时间= " << current_time_ - init_start_time_ << ", bg = " << init_bg_.transpose()
<< ", ba = " << init_ba_.transpose() << ", gyro sq = " << cov_gyro_.transpose()
<< ", acce sq = " << cov_acce_.transpose() << ", grav = " << gravity_.transpose()
<< ", norm: " << gravity_.norm();
LOG(INFO) << "mean gyro: " << mean_gyro.transpose() << " acce: " << mean_acce.transpose();
init_success_ = true;
return true;
}
预测
根据当前状态对IMU数据进行递推。预测过程中,需要计算名义状态变量的更新过程及协方差矩阵的递推过程。
- 状态预测:使用非线性状态转移函数。
- 状态转移矩阵线性化:计算雅可比矩阵。
- 协方差预测:利用线性化状态转移矩阵。
template <typename S>
bool ESKF<S>::Predict(const IMU& imu) {
assert(imu.timestamp_ >= current_time_);
double dt = imu.timestamp_ - current_time_;
if (dt > (5 * options_.imu_dt_) || dt < 0) {
// 时间间隔不对,可能是第一个IMU数据,没有历史信息
LOG(INFO) << "skip this imu because dt_ = " << dt;
current_time_ = imu.timestamp_;
return false;
}
名义状态预测
名义状态变量的离散时间运动学方程:
// nominal state 递推
VecT new_p = p_ + v_ * dt + 0.5 * (R_ * (imu.acce_ - ba_)) * dt * dt + 0.5 * g_ * dt * dt;
VecT new_v = v_ + R_ * (imu.acce_ - ba_) * dt + g_ * dt;
SO3 new_R = R_ * SO3::exp((imu.gyro_ - bg_) * dt);
R_ = new_R;
v_ = new_v;
p_ = new_p;
// 其余状态维度不变
误差状态预测
计算雅可比矩阵F
// error state 递推
// 计算运动过程雅可比矩阵 F,
// F实际上是稀疏矩阵,也可以不用矩阵形式进行相乘而是写成散装形式,这里为了教学方便,使用矩阵形式
Mat18T F = Mat18T::Identity(); // 主对角线
F.template block<3, 3>(0, 3) = Mat3T::Identity() * dt; // p 对 v
F.template block<3, 3>(3, 6) = -R_.matrix() * SO3::hat(imu.acce_ - ba_) * dt; // v对theta
F.template block<3, 3>(3, 12) = -R_.matrix() * dt; // v 对 ba
F.template block<3, 3>(3, 15) = Mat3T::Identity() * dt; // v 对 g
F.template block<3, 3>(6, 6) = SO3::exp(-(imu.gyro_ - bg_) * dt).matrix(); // theta 对 theta
F.template block<3, 3>(6, 9) = -Mat3T::Identity() * dt; // theta 对 bg
误差状态预测
// mean and cov prediction
dx_ = F * dx_; // 这行其实没必要算,dx_在重置之后应该为零,因此这步可以跳过,但F需要参与Cov部分计算,所以保留
噪声协方差预测
cov_ = F * cov_.eval() * F.transpose() + Q_;
current_time_ = imu.timestamp_;
return true;
}
更新
- 观测预测:使用非线性观测函数。
- 观测矩阵线性化:计算雅可比矩阵。
- 卡尔曼增益:计算增益矩阵。
- 状态更新:根据实际观测更新状态。
- 协方差更新:更新状态协方差。
更新过程
z为观测数据, v为观测噪声, V为该噪声的协方差矩阵,h为观测方程。
template <typename S>
bool ESKF<S>::ObserveGps(const GNSS& gnss) {
/// GNSS 观测的修正
assert(gnss.unix_time_ >= current_time_);
if (first_gnss_) {
R_ = gnss.utm_pose_.so3();
p_ = gnss.utm_pose_.translation();
first_gnss_ = false;
current_time_ = gnss.unix_time_;
return true;
}
assert(gnss.heading_valid_);
ObserveSE3(gnss.utm_pose_, options_.gnss_pos_noise_, options_.gnss_ang_noise_);
current_time_ = gnss.unix_time_;
return true;
}
template <typename S>
bool ESKF<S>::ObserveSE3(const SE3& pose, double trans_noise, double ang_noise) {
/// 既有旋转,也有平移
/// 观测状态变量中的p, R,H为6x18,其余为零
Eigen::Matrix<S, 6, 18> H = Eigen::Matrix<S, 6, 18>::Zero();
H.template block<3, 3>(0, 0) = Mat3T::Identity(); // P部分
H.template block<3, 3>(3, 6) = Mat3T::Identity(); // R部分(3.66)
// 卡尔曼增益和更新过程
Vec6d noise_vec;
noise_vec << trans_noise, trans_noise, trans_noise, ang_noise, ang_noise, ang_noise;
//噪声协方差矩阵
Mat6d V = noise_vec.asDiagonal();
计算卡尔曼增益
K为卡尔曼增益, Ppred为预测的协方差矩阵,V为噪声协方差矩阵,H为观测方程相比于误差状态的雅可比矩阵。
Eigen::Matrix<S, 18, 6> K = cov_ * H.transpose() * (H * cov_ * H.transpose() + V).inverse();
状态更新
// 更新x和cov
Vec6d innov = Vec6d::Zero();
innov.template head<3>() = (pose.translation() - p_); // 平移部分
innov.template tail<3>() = (R_.inverse() * pose.so3()).log(); // 旋转部分(3.67)
dx_ = K * innov;//这里dx的计算和上述公式有差异,待分析
协方差更新
K为卡尔曼增益, Ppred为预测的协方差矩阵,最后的 P为修正后的协方差矩阵。
cov_ = (Mat18T::Identity() - K * H) * cov_;
UpdateAndReset();
return true;
}
误差状态后处理
误差状态归入名义状态,重置ESKF。
/// 更新名义状态变量,重置error state
void UpdateAndReset() {
p_ += dx_.template block<3, 1>(0, 0);
v_ += dx_.template block<3, 1>(3, 0);
R_ = R_ * SO3::exp(dx_.template block<3, 1>(6, 0));
if (options_.update_bias_gyro_) {
bg_ += dx_.template block<3, 1>(9, 0);
}
if (options_.update_bias_acce_) {
ba_ += dx_.template block<3, 1>(12, 0);
}
g_ += dx_.template block<3, 1>(15, 0);
ProjectCov();
//误差状态均值归零
dx_.setZero();
}
/// 对P阵进行投影,参考式(3.63)
void ProjectCov() {
计算雅可比矩阵
Mat18T J = Mat18T::Identity();
J.template block<3, 3>(6, 6) = Mat3T::Identity() - 0.5 * SO3::hat(dx_.template block<3, 1>(6, 0));
协方差矩阵进行线性变化变换
cov_ = J * cov_ * J.transpose();
}
参考资料:
1、简明ESKF推导:https://zhuanlan.zhihu.com/p/441182819
2、ESKF代码:https://github.com/gaoxiang12/slam_in_autonomous_driving/tree/master/src/ch3
3、《Quaternion kinematics for the error-state Kalman filter》https://arxiv.org/abs/1711.02508