MSCKF5讲:后端代码分析

MSCKF5讲:后端代码分析

文章目录

  • MSCKF5讲:后端代码分析
  • 1 初始化initialize()
    • 1.1 加载参数
    • 1.2 初始化`IMU`连续噪声协方差矩阵
    • 1.3 卡方检验
    • 1.4 接收与订阅话题createRosIO()
  • 2 IMU静止初始化
  • 3 重置resetCallback()
  • 4 featureCallback
    • 4.1 IMU初始化判断
    • 4.2 IMU积分
      • 4.2.1 batchImuProcessing
      • 4.2.2 processModel
        • ① 移除imu测量偏置
        • ② 计算系数矩阵F和噪声矩阵G(连续,误差状态)
        • ③ 计算状态转移矩阵Phi(离散化的F,误差状态)
        • ④ IMU状态预测
        • ⑤ 可观性约束
        • ⑥ 更新0空间约束后的状态转移矩阵phi和误差状态协方差矩阵
        • ⑦ 更新imu状态量与相机状态量交叉的部分
        • ⑧ 强制对称协方差矩阵
        • ⑨ 更新零空间
    • 4.3 状态增广 stateAugmentation
      • 4.3.1 利用最新的imu状态计算cam状态
      • 4.3.2 记录相机状态- `imu_state.id`
      • 4.3.3 更新协方差矩阵
        • ① 计算雅可比矩阵
        • ② 计算增广协方差矩阵
    • 4.4 添加特征点观测
    • 4.5 利用视觉观测更新状态
      • 4.5.1 特征点管理
      • 4.5.2 计算误差量雅可比与重投影误差
        • ① featureJacobian()
        • ② measurementJacobian()
          • <1> 取出观测z和位姿Rwc、路标点p_w
          • <2> 计算雅可比H
          • <3> 对H矩阵的可观测性约束
          • <4> 计算残差r
      • ③ 左零空间投影
      • 4.5.3 状态更新
        • ① 降维
        • ② 卡尔曼滤波更新
    • 4.6 pruneCamStateBuffer
    • 4.7 发布位姿publish
    • 4.8 是否重置系统onlineReset

这里程序实际上是两个进程

msckf_vio_nodelet.h

namespace msckf_vio {
class MsckfVioNodelet : public nodelet::Nodelet {
public:
  MsckfVioNodelet() { return; }
  ~MsckfVioNodelet() { return; }

private:
  virtual void onInit();	// 虚函数
  MsckfVioPtr msckf_vio_ptr;		// boost::shared_ptr<MsckfVio>类指针
};
} // end namespace msckf_vio
  1. nodelet/nodelet.h
    • 提供了 Nodelet 类的定义,是用于实现节点的基类。
    • Nodelet 是一种轻量级的ROS节点,它可以在同一个进程中共享节点管理和ROS通信,以提高系统的效率。
    • Nodelet 允许将不同的ROS节点合并到一个进程中,从而减少通信开销。
  2. pluginlib/class_list_macros.h
    • 提供了一组宏,用于导出和加载ROS插件,包括节点类(nodelets)。
    • PLUGINLIB_EXPORT_CLASS 是该库中的一个重要宏,用于将一个类导出为ROS插件,以便它可以在运行时被动态加载。
    • 通过这些宏,ROS能够在运行时根据类名查找和加载相应的插件。

msckf_vio_nodelet.cpp

  在这里,getPrivateNodeHandle() 用于获取一个私有节点句柄,然后将这个句柄传递给 MsckfVio 类的构造函数。这样做的目的可能是为了让 MsckfVio 类能够在私有命名空间下访问参数,主题等ROS资源。

  在ROS中,私有节点句柄的名称通常是在节点命名空间后添加 “~” 符号来表示的。例如,如果节点的名称是 /my_node,那么私有节点句柄的名称将是 /my_node/。这有助于确保节点的参数和主题不会与其他节点的冲突。

#include <msckf_vio/msckf_vio_nodelet.h>

namespace msckf_vio {
void MsckfVioNodelet::onInit() {
  // 通过 reset 方法初始化为一个新的 MsckfVio 对象
  // 该对象使用 getPrivateNodeHandle() 进行初始化
  msckf_vio_ptr.reset(new MsckfVio(getPrivateNodeHandle()));
  if (!msckf_vio_ptr->initialize()) {
    ROS_ERROR("Cannot initialize MSCKF VIO...");
    return;
  }
  return;
}

// MsckfVioNodelet 类被导出,以便 ROS 可以动态加载它
PLUGINLIB_EXPORT_CLASS(msckf_vio::MsckfVioNodelet,
    nodelet::Nodelet);

}
struct StateServer {
    IMUState imu_state;
    Cam cam_states;

    // State covariance matrix	
    Eigen::MatrixXd state_cov;				// 误差状态(包括IMU和Cam)协方差矩阵
    Eigen::Matrix<double, 12, 12> continuous_noise_cov;		// IMU连续噪声协方差
};

1 初始化initialize()

  我们要搞清楚,在帧Frame的构造函数中,做了那些工作,与哪些函数相关联。

后端初始化函数内容
1 加载参数loadParameters()
2 初始化IMU噪声协方差矩阵初始化state_server.continuous_noise_cov n g , n a , n b g , n b a n_{g},n_{a},n_{bg},n_{ba} ng,na,nbg,nba
3 卡方检验chi_squared_test_table
4 订阅与发布ROS话题调用createRosIO()

1.1 加载参数

bool MsckfVio::initialize() 
{
  // 1. 加载参数
  if (!loadParameters()) return false;			
  ...
  return true;
}


bool MsckfVio::loadParameters() {
  // 世界坐标系、里程计坐标系
  nh.param<string>("fixed_frame_id", fixed_frame_id, "world");
  nh.param<string>("child_frame_id", child_frame_id, "robot");

  // 发布坐标
  nh.param<bool>("publish_tf", publish_tf, true);
  
  // 帧率
  nh.param<double>("frame_rate", frame_rate, 40.0);
  
  // 没有”跟丢“概念	判断是否发散  	状态的协方差应该在一个范围内,超过阈值即resize
  nh.param<double>("position_std_threshold", position_std_threshold, 8.0);

  // 判断是否删除状态
  nh.param<double>("rotation_threshold", rotation_threshold, 0.2618);
  nh.param<double>("translation_threshold", translation_threshold, 0.4);
  nh.param<double>("tracking_rate_threshold", tracking_rate_threshold, 0.5);

  // Feature optimization parameters  
  nh.param<double>("feature/config/translation_threshold",
      Feature::optimization_config.translation_threshold, 0.2);

  // IMU参数(标准差)  预测			默认值是一些经验值,便宜的IMU这方面不好说影响
  nh.param<double>("noise/gyro", IMUState::gyro_noise, 0.001);				// ng
  nh.param<double>("noise/acc", IMUState::acc_noise, 0.01);					// na
  nh.param<double>("noise/gyro_bias", IMUState::gyro_bias_noise, 0.001);	// nbg
  nh.param<double>("noise/acc_bias", IMUState::acc_bias_noise, 0.01);		// nba
  
  // 观测---特征噪声---经验值---
  nh.param<double>("noise/feature", Feature::observation_noise, 0.01);

  // 标定得到是标准差,协方差矩阵应该是方差,使用方差代替上面标准差
  IMUState::gyro_noise *= IMUState::gyro_noise;
  IMUState::acc_noise *= IMUState::acc_noise;
  IMUState::gyro_bias_noise *= IMUState::gyro_bias_noise;
  IMUState::acc_bias_noise *= IMUState::acc_bias_noise;
  Feature::observation_noise *= Feature::observation_noise;

   /*设置IMU状态*/
  // 对于旋转R和偏移t,初始化大都默认设置为0.但是对于初始的速度v和偏差b,设置为0是否合理?
  // 设置v = 0
  nh.param<double>("initial_state/velocity/x",state_server.imu_state.velocity(0), 0.0);
  nh.param<double>("initial_state/velocity/y",state_server.imu_state.velocity(1), 0.0);
  nh.param<double>("initial_state/velocity/z",state_server.imu_state.velocity(2), 0.0);

  // The initial covariance of orientation and position can be set to 0. 
  // But for velocity, bias and extrinsic parameters,
  // there should be nontrivial uncertainty.
  // 初始化误差状态的协方差。 (这里和上面标准差不同之处在于,上面是IMU模型,这里是指状态量!)
    
  // 误差状态量:速度 、 g 、加速度
  double gyro_bias_cov, acc_bias_cov, velocity_cov;
  nh.param<double>("initial_covariance/velocity",velocity_cov, 0.25);
  nh.param<double>("initial_covariance/gyro_bias",gyro_bias_cov, 1e-4);
  nh.param<double>("initial_covariance/acc_bias",acc_bias_cov, 1e-2);

  // 旋转、平移
  double extrinsic_rotation_cov, extrinsic_translation_cov;
  nh.param<double>("initial_covariance/extrinsic_rotation_cov",extrinsic_rotation_cov, 3.0462e-4);
  nh.param<double>("initial_covariance/extrinsic_translation_cov",extrinsic_translation_cov, 1e-4);

  
  // 对应论文,不过那个讲师说后面那个外参协方差在其他里面优化效果不好。为什么旋转平移就可以是0?因为在正式开始之前我们通过初始化找好了重力方向,确定了第一帧的位姿
  // 0~3 角度
  // 3~6 陀螺仪偏置
  // 6~9 速度 
  // 9~12 加速度计偏置
  // 12~15 位移
  // 15~18 外参旋转
  // 18~21 外参平移
  state_server.state_cov = MatrixXd::Zero(21, 21);
  for (int i = 3; i < 6; ++i)
    state_server.state_cov(i, i) = gyro_bias_cov;
  for (int i = 6; i < 9; ++i)
    state_server.state_cov(i, i) = velocity_cov;
  for (int i = 9; i < 12; ++i)
    state_server.state_cov(i, i) = acc_bias_cov;
  for (int i = 15; i < 18; ++i)
    state_server.state_cov(i, i) = extrinsic_rotation_cov;
  for (int i = 18; i < 21; ++i)
    state_server.state_cov(i, i) = extrinsic_translation_cov;

  // Transformation offsets between the frames involved.
  Isometry3d T_imu_cam0 = utils::getTransformEigen(nh, "cam0/T_cam_imu");
  Isometry3d T_cam0_imu = T_imu_cam0.inverse();

  state_server.imu_state.R_imu_cam0 = T_cam0_imu.linear().transpose();
  state_server.imu_state.t_cam0_imu = T_cam0_imu.translation();
  CAMState::T_cam0_cam1 = utils::getTransformEigen(nh, "cam1/T_cn_cnm1");
  IMUState::T_imu_body = utils::getTransformEigen(nh, "T_imu_body").inverse();

  // Maximum number of camera states to be stored
  nh.param<int>("max_cam_state_size", max_cam_state_size, 30);
  return true;
}

1.2 初始化IMU连续噪声协方差矩阵

  为什么都是三维呢?因为角速度和角速度都是三个轴xyz,所以就是3*3的协方差矩阵,为什么初始只有对角线有值(自身方差),即认为一开始的时候,任意两个轴之间应该是没有关系的。协方差越小,联系越小。

  实际上应该可以表示为3*12的矩阵,但是估计写成方阵比较好计算吧。

n g , n a , n b g , n b a n_{g},n_{a},n_{bg},n_{ba} ng,na,nbg,nba

state_server.continuous_noise_cov = Matrix<double, 12, 12>::Zero();
state_server.continuous_noise_cov.block<3, 3>(0, 0) = Matrix3d::Identity() * IMUState::gyro_noise;
state_server.continuous_noise_cov.block<3, 3>(3, 3) = Matrix3d::Identity() * IMUState::gyro_bias_noise;
state_server.continuous_noise_cov.block<3, 3>(6, 6) = Matrix3d::Identity() * IMUState::acc_noise;
state_server.continuous_noise_cov.block<3, 3>(9, 9) = Matrix3d::Identity() * IMUState::acc_bias_noise;

1.3 卡方检验

在这里插入图片描述

  在统计学中,卡方分布的分位数表示了在给定自由度下,该分布中随机变量落在分位数值以下的概率。具体来说,3.841 是指在 1 自由度的卡方分布中,有 5% 的概率随机变量取值小于或等于 3.841。

  这段代码的目的是计算不同自由度下卡方分布的 5% 分位数,并将结果保存在 chi_squared_test_table

bool MsckfVio::initialize() 
{

    // 卡方检验表
    // Initialize the chi squared test table with confidence
    // level 0.95.
    for (int i = 1; i < 100; ++i)
    {
        boost::math::chi_squared chi_squared_dist(i);
        chi_squared_test_table[i] = boost::math::quantile(chi_squared_dist, 0.05);
    }

  return true;
}

1.4 接收与订阅话题createRosIO()

bool MsckfVio::initialize() 
{
    if (!createRosIO())
        return false;
}

bool MsckfVio::createRosIO()
{
    // 发送位姿信息与三维点
    odom_pub = nh.advertise<nav_msgs::Odometry>("odom", 10);
    feature_pub = nh.advertise<sensor_msgs::PointCloud2>("feature_point_cloud", 10);

    // 重置     ----服务通信
    reset_srv = nh.advertiseService("reset", &MsckfVio::resetCallback, this);

    // 接收imu数据与前端跟踪的特征
    imu_sub = nh.subscribe("imu", 100, &MsckfVio::imuCallback, this);
    feature_sub = nh.subscribe("features", 40, &MsckfVio::featureCallback, this);

    // 接受真值,动作捕捉发来的,然后再发送出去,为了再rviz可视化
    mocap_odom_sub = nh.subscribe("mocap_odom", 10, &MsckfVio::mocapOdomCallback, this);
    mocap_odom_pub = nh.advertise<nav_msgs::Odometry>("gt_odom", 1);

    return true;
}

2 IMU静止初始化

  imuCallback说白了IMU初始化之后就是不停的订阅IMU数据,放到buffer,直到处理下一帧图像再使用。

void MsckfVio::imuCallback(const sensor_msgs::ImuConstPtr &msg)
{   
    // IMU数据没有立即被处理,等待下一帧图像
    // 1. 存放imu数据
    imu_msg_buffer.push_back(*msg);             // std::vector<sensor_msgs::Imu> imu_msg_buffer;

    // 2. 用200个imu数据做静止初始化,不够则不做
    if (!is_gravity_set)
    {
        if (imu_msg_buffer.size() < 200)
            return;
        initializeGravityAndBias();		// 静态初始化
        is_gravity_set = true;
    }
}

initializeGravityAndBias

   imu初始化,200个数据必须都是静止时采集的。这里面没有判断是否成功,也就是一开始如果运动会导致轨迹飘。

  现在分析一下为什么要做IMU初始化,从IMU噪声模型分析。

  对于角速度,静止时,理论时角速度应该为0,但实际不为0,也就是存在偏置 b g b_g bg,所以sum_angular_vel/n求取陀螺仪均值作为偏置;对于加速度,我们得到的时IMU坐标系下的加速度,但实际要的时世界系下的加速度,目的就是为了求解模型中 R b w {R}_{bw} Rbw.静止时, a w a^w aw是0,忽略了加速度偏置,既可以得到最简单的计算公式, a b = R b w g w {a}^b ={R}_{bw}{g}^w ab=Rbwgw
ω ~ b = ω b + b g + η g a ~ b = R b w ( a w − g w ) + b a + η a \begin{aligned}\tilde{\boldsymbol{\omega}}^b&=\mathbf{\omega}^b+\mathbf{b}_g+\mathbf{\eta}_g\\\tilde{\mathbf{a}}^b&=\mathbf{R}_{bw}(\mathbf{a}^w{-}\mathbf{g}^w)+\mathbf{b}_a+\mathbf{\eta}_a\end{aligned} ω~ba~b=ωb+bg+ηg=Rbw(awgw)+ba+ηa
  实际上,如果我们认为g的模长是定值,如9.8,那么也是可以估计加速度偏置的!

在这里插入图片描述

  补充:IMU初始化是为了惯性变量获得良好的初始值,这些惯性变量包括重力方向和IMU零偏。先说零偏,MU的零偏不是固定的,是随时间变化的量。由于零偏对MU的影响较大,因此通常作为一个独立的状态来优化。再说重力方向,在视觉惯性模式下,系统以视觉地图初始化成功的第一帧作为世界坐标系原点,此时我们是不知道坐标系中重力的方向的,如果不进行MU初始化,则无法消除重力对IMU积分的影响。IMU初始化的目的就是把图像建立的世界坐标系的之轴拉到和重力方向平行的状态。

/**
 * @brief imu初始化,计算陀螺仪偏置,重力方向以及初始姿态,必须都是静止,且不做加速度计的偏置估计
 */
void MsckfVio::initializeGravityAndBias()
{

    // Initialize gravity and gyro bias.
    
    // 1. 角速度与加速度的和
    Vector3d sum_angular_vel = Vector3d::Zero();
    Vector3d sum_linear_acc = Vector3d::Zero();

    for (const auto &imu_msg : imu_msg_buffer)
    {
        Vector3d angular_vel = Vector3d::Zero();
        Vector3d linear_acc = Vector3d::Zero();
        
		// tf::vectorMsgToEigen 函数将ROS消息对象 imu_msg 中的角速度信息提取并转换为Vector3d 类型
        tf::vectorMsgToEigen(imu_msg.angular_velocity, angular_vel);
        tf::vectorMsgToEigen(imu_msg.linear_acceleration, linear_acc);

        sum_angular_vel += angular_vel;
        sum_linear_acc += linear_acc;
    }

    // 2. 因为假设静止的,因此陀螺仪理论应该都是0,额外读数包括偏置+噪声,但是噪声属于高斯分布
    // 因此这一段相加噪声被认为互相抵消了,所以剩下的均值被认为是陀螺仪的初始偏置
    state_server.imu_state.gyro_bias = sum_angular_vel / imu_msg_buffer.size();

    // 3. 计算重力,忽略加速度计的偏置,那么剩下的就只有重力了
    Vector3d gravity_imu = sum_linear_acc / imu_msg_buffer.size();

    // 初始化重力本来的方向,使估计与惯性系一致,因为一开始测得不一定是(0,0,-g),所以需要转换
    // 注意,一个向量不会因为坐标系变化而发生变化,所以其模长固定!
    double gravity_norm = gravity_imu.norm();
    IMUState::gravity = Vector3d(0.0, 0.0, -gravity_norm);	// 得到

    // 求出当前imu状态的重力方向与实际重力方向的旋转---说白了就是IMU坐标系到世界系的旋转Rwi
    Quaterniond q0_i_w = Quaterniond::FromTwoVectors(gravity_imu, -IMUState::gravity);
    // 得出姿态
    state_server.imu_state.orientation = rotationToQuaternion(q0_i_w.toRotationMatrix().transpose());

    return;
}

3 重置resetCallback()

/**
 * @brief 重置
 */
bool MsckfVio::resetCallback(std_srvs::Trigger::Request &req,std_srvs::Trigger::Response &res)
{

    // 暂时不订阅相关的数据
    feature_sub.shutdown();
    imu_sub.shutdown();

    // 重置IMU状态.
    IMUState &imu_state = state_server.imu_state;
    imu_state.time = 0.0;
    imu_state.orientation = Vector4d(0.0, 0.0, 0.0, 1.0);
    imu_state.position = Vector3d::Zero();
    imu_state.velocity = Vector3d::Zero();
    imu_state.gyro_bias = Vector3d::Zero();
    imu_state.acc_bias = Vector3d::Zero();
    imu_state.orientation_null = Vector4d(0.0, 0.0, 0.0, 1.0);
    imu_state.position_null = Vector3d::Zero();
    imu_state.velocity_null = Vector3d::Zero();

    // Remove all existing camera states.
    state_server.cam_states.clear();

    // Reset the state covariance.
    double gyro_bias_cov, acc_bias_cov, velocity_cov;
    nh.param<double>("initial_covariance/velocity", velocity_cov, 0.25);
    nh.param<double>("initial_covariance/gyro_bias", gyro_bias_cov, 1e-4);
    nh.param<double>("initial_covariance/acc_bias", acc_bias_cov, 1e-2);

    double extrinsic_rotation_cov, extrinsic_translation_cov;
    nh.param<double>("initial_covariance/extrinsic_rotation_cov", extrinsic_rotation_cov, 3.0462e-4);
    nh.param<double>("initial_covariance/extrinsic_translation_cov", extrinsic_translation_cov, 1e-4);

    state_server.state_cov = MatrixXd::Zero(21, 21);
    for (int i = 3; i < 6; ++i)
        state_server.state_cov(i, i) = gyro_bias_cov;
    for (int i = 6; i < 9; ++i)
        state_server.state_cov(i, i) = velocity_cov;
    for (int i = 9; i < 12; ++i)
        state_server.state_cov(i, i) = acc_bias_cov;
    for (int i = 15; i < 18; ++i)
        state_server.state_cov(i, i) = extrinsic_rotation_cov;
    for (int i = 18; i < 21; ++i)
        state_server.state_cov(i, i) = extrinsic_translation_cov;

    // Clear all exsiting features in the map.
    map_server.clear();

    // Clear the IMU msg buffer.
    imu_msg_buffer.clear();

    // Reset the starting flags.
    is_gravity_set = false;
    is_first_img = true;

    // Restart the subscribers.
    imu_sub = nh.subscribe("imu", 100, &MsckfVio::imuCallback, this);
    feature_sub = nh.subscribe("features", 40, &MsckfVio::featureCallback, this);

    // TODO: When can the reset fail?
    res.success = true;
    ROS_WARN("Resetting msckf vio completed...");
    return true;
}

4 featureCallback

/**
 * @brief 后端主要函数,处理新来的数据
 */
void MsckfVio::featureCallback(const CameraMeasurementConstPtr &msg)	// msg图像消息
{
	...
    // 1 IMU初始化判断
        
    // 2 IMU积分
    batchImuProcessing(msg->header.stamp.toSec());
    
}

4.1 IMU初始化判断

    // 1. 必须经过imu初始化
    if (!is_gravity_set)
        return;

    // Start the system if the first image is received. The frame where the first image is received will be the origin.
    // 开始递推状态的第一个时刻为初始化后的第一帧
    if (is_first_img)
    {
        is_first_img = false;
        state_server.imu_state.time = msg->header.stamp.toSec();
    }

4.2 IMU积分

  遍历imu_msg_buffer容器中合法的IMU数据,对每一个数据进行processModel积分处理

4.2.1 batchImuProcessing

  这个函数的主要功能就是筛选出两帧图像之间的imu消息去进行processModel处理。

在这里插入图片描述

/**
 * @brief imu积分,批量处理imu数据
 * @param  time_bound 处理到这个时间---就是当前新图像msg这个时间
 */
void MsckfVio::batchImuProcessing(const double &time_bound)
{
    // Counter how many IMU msgs in the buffer are used.
    int used_imu_msg_cntr = 0;

    // 取出两帧之间的imu数据去递推位姿
    // 这里有个细节问题,time_bound表示新图片的时间戳,
    // 但是IMU就积分到了距time_bound最近的一个,导致时间会差一点点
    for (const auto &imu_msg : imu_msg_buffer)
    {
        double imu_time = imu_msg.header.stamp.toSec();
        // 小于,说明这个数据比较旧,因为state_server.imu_state.time代表已经处理过的imu数据的时间
        if (imu_time < state_server.imu_state.time)
        {
            ++used_imu_msg_cntr;
            continue;
        }
        // 超过的供下次使用
        if (imu_time > time_bound)
            break;

        // Convert the msgs.
        Vector3d m_gyro, m_acc;
        tf::vectorMsgToEigen(imu_msg.angular_velocity, m_gyro);
        tf::vectorMsgToEigen(imu_msg.linear_acceleration, m_acc);

        // Execute process model.
        // 递推位姿,核心函数
        processModel(imu_time, m_gyro, m_acc);
        ++used_imu_msg_cntr;
    }

    // Set the state ID for the new IMU state.
    // 新的状态,更新id,相机状态的id也根据这个赋值
    state_server.imu_state.id = IMUState::next_id++;

    // 删掉已经用过
    // Remove all used IMU msgs.
    imu_msg_buffer.erase(
        imu_msg_buffer.begin(),
        imu_msg_buffer.begin() + used_imu_msg_cntr);

    return;
}

4.2.2 processModel


void MsckfVio::processModel(
    const double &time, const Vector3d &m_gyro, const Vector3d &m_acc)
{
    // 以引用的方式取出
    IMUState &imu_state = state_server.imu_state;

    // 1. imu读数减掉偏置
    Vector3d gyro = m_gyro - imu_state.gyro_bias;
    Vector3d acc = m_acc - imu_state.acc_bias;  // acc_bias 初始值是0
    double dtime = time - imu_state.time;
 
    // state_server.imu_state应该就是左边界!
    
    ...
        
    // 更新时间,time即当前处理的那个imu数据的时间戳!
    state_server.imu_state.time = time;
    return;    
}    

  所以,每一次调用这个函数,都是指两个imu数据之间的一个数据处理(预测等)。比如,dt时间内的状态预测,对于姿态,认为角速度dt时间内保持不变,即欧拉积分,直接利用time时刻即imu数据时刻得到的角速度w去预测这个时刻的位姿q。每计算完一个dt,就会更新时间state_server.imu_state.time = time。所以在观测来到前,所有IMU数据依次在上一个数据上预测。这也表明,如果没有引入外部的观测,这个预测的值一定会发散的!

在这里插入图片描述

① 移除imu测量偏置
/**
 * @brief 来一个新的imu数据更新协方差矩阵与状态积分
 * @param  time 新数据的时间戳
 * @param  m_gyro 角速度
 * @param  m_acc 加速度
 */
void MsckfVio::processModel(
    const double &time, const Vector3d &m_gyro, const Vector3d &m_acc)
{

    // 以引用的方式取出
    IMUState &imu_state = state_server.imu_state;

    // 1. imu读数减掉偏置
    Vector3d gyro = m_gyro - imu_state.gyro_bias;
    Vector3d acc = m_acc - imu_state.acc_bias;  // acc_bias 初始值是0
    double dtime = time - imu_state.time;
 
    // state_server.imu_state应该就是左边界!
    
    ...
        
    // 更新时间,time即当前处理的那个imu数据的时间戳
    state_server.imu_state.time = time;
    return;    
}    
② 计算系数矩阵F和噪声矩阵G(连续,误差状态)

  写成矩阵形式
δ x ˙ = F c ⋅ δ x + G c ⋅ n \dot{\delta\mathbf{x}}=\mathbf{F}_c\cdot\delta\mathbf{x}+\mathbf{G}_c\cdot\mathbf{n} δx˙=Fcδx+Gcn
  变量:
δ x = ( G I δ θ ⊤ δ b g ⊤ G δ v I ⊤ δ b a ⊤ G δ p I ⊤ C I δ θ ⊤ I δ p C ⊤ ) ⊤ \left.\delta\mathbf{x}=\left(\begin{array}{cccccc}^I_G\delta\boldsymbol{\theta}^\top&\delta\mathbf{b}_g^\top&^G\delta\mathbf{v}_I^\top&\delta\mathbf{b}_a^\top&^G\delta\mathbf{p}_I^\top&{}^I_C\delta\boldsymbol{\theta}^\top&{}^I\delta\mathbf{p}_C^\top\end{array}\right.\right)^\top δx=(GIδθδbgGδvIδbaGδpICIδθIδpC)

n = ( n g n w g n a n w a ) ⊤ \left.\mathbf{n}=\left(\begin{array}{ccc}\mathbf{n_g}&\mathbf{n_{wg}}&\mathbf{n_a}&\mathbf{n_{wa}}\end{array}\right.\right)^\top n=(ngnwgnanwa)

矩阵F注意点:

① 速度:即第三行,忽略了相关因素影响

② 外参:相机与IMU的转换,这个是不变的,所以第6、7行都是0,6、7列也是0,这里没写出来。

③ 这里只是5个变量,S-MSCKF论文以及代码中都还有外参两个自变量,所以F实际是21*21矩阵。7个变量,每个都是3×3

F = ( − ⌊ ω ^ × ⌋ − I 3 0 3 × 3 0 3 × 3 0 3 × 3 0 3 × 3 0 3 × 3 0 3 × 3 0 3 × 3 0 3 × 3 − C ( I C q ^ ) ⊤ ⌊ a ^ × ⌋ 0 3 × 3 0 3 × 3 − C ( I G q ^ ) ⊤ 0 3 × 3 0 3 × 3 0 3 × 3 0 3 × 3 0 3 × 3 0 3 × 3 0 3 × 3 0 3 × 3 I 3 0 3 × 3 0 3 × 3 0 3 × 3 0 3 × 3 0 3 × 3 0 3 × 3 0 3 × 3 0 3 × 3 0 3 × 3 0 3 × 3 0 3 × 3 0 3 × 3 ) \mathbf{F}=\begin{pmatrix}-\lfloor\hat{\boldsymbol{\omega}}_{\times}\rfloor&-\mathbf{I}_{3}&\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3}\\\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3}\\-C\left(\frac IC\hat{\mathbf{q}}\right)^\top\left\lfloor\hat{\mathbf{a}}_{\times}\right\rfloor&\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3}&-C\left(\frac IG\hat{\mathbf{q}}\right)^\top&\mathbf{0}_{3\times3}\\\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3}\\\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3}&\mathbf{I}_{3}&\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3}\\\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3}\\\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3} \end{pmatrix} F= ω^×03×3C(CIq^)a^×03×303×303×303×3I303×303×303×303×303×303×303×303×303×303×3I303×303×303×303×3C(GIq^)03×303×303×303×303×303×303×303×303×303×303×3

矩阵G注意:原论文可能给出是下面式子,代码中有一个地方对不上,应该公式是写错了。只有位姿q、速度v、以及两个偏置b有噪声项。

G.block<3, 3>(9, 9) = Matrix3d::Identity();

G = ( − I 3 0 3 × 3 0 3 × 3 0 3 × 3 0 3 × 3 I 3 0 3 × 3 0 3 × 3 0 3 × 3 0 3 × 3 − C ( I G q ^ ) ⊤ 0 3 × 3 0 3 × 3 0 3 × 3 0 3 × 3 0 3 × 3 0 3 × 3 0 3 × 3 0 3 × 3 I 3 0 3 × 3 0 3 × 3 0 3 × 3 0 3 × 3 0 3 × 3 0 3 × 3 0 3 × 3 0 3 × 3 ) \left.\mathbf{G}=\left(\begin{array}{cccc}-\mathbf{I}_3&\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3}\\\mathbf{0}_{3\times3}&\mathbf{I}_3&\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3}\\\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3}&-C\left(\frac IG\hat{\mathbf{q}}\right)^\top&\mathbf{0}_{3\times3}\\\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3}\\\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3}&\mathbf{I}_3\\\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3}\\\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3}\end{array}\right.\right) G= I303×303×303×303×303×303×303×3I303×303×303×303×303×303×303×3C(GIq^)03×303×303×303×303×303×303×303×3I303×303×3


    // 2. 计算F G矩阵
    // Compute discrete transition and noise covariance matrix
    Matrix<double, 21, 21> F = Matrix<double, 21, 21>::Zero();
    Matrix<double, 21, 12> G = Matrix<double, 21, 12>::Zero();

    // 误差为真值(观测) - 预测
    // F矩阵表示的是误差的导数的微分方程,其实就是想求δ的递推公式
    // δ`= F · δ + G    δ表示误差
    // δn+1 = (I + F·dt)·δn + G·dt·Q    Q表示imu噪声

    // 两种推法,一种是通过论文中四元数的推,这个过程不再重复
    // 下面给出李代数推法,我们知道msckf的四元数使用的是反人类的jpl
    // 也就是同一个数值的旋转四元数经过两种不同定义得到的旋转是互为转置的
    // 这里面的四元数转成的旋转表示的是Riw,所以要以李代数推的话也要按照Riw推
    // 按照下面的旋转更新方式为左乘,因此李代数也要用左乘,且jpl模式下左乘一个δq = Exp(-δθ)
    // δQj * Qjw = Exp(-(w - b) * t) * δQi * Qiw
    // Exp(-δθj) * Qjw = Exp(-(w - b) * t) * Exp(-δθi) * Qiw
    // 其中Qjw = Exp(-(w - b) * t) * Qiw
    // 两边除得到 Exp(-δθj) = Exp(-(w - b) * t) * Exp(-δθi) * Exp(-(w - b) * t).t()
    // -δθj = - Exp(-(w - b) * t) * δθi
    // δθj = (I - (w - b)^ * t)  * δθi  得证

    // 关于偏置一样可以这么算,只不过式子变成了
    // δQj * Qjw = Exp(-(w - b - δb) * t) * Qiw
    // 上式使用bch近似公式可以得到 δθj = -t * δb
    // 其他也可以通过这个方法推出,是正确的

    F.block<3, 3>(0, 0) = -skewSymmetric(gyro);
    F.block<3, 3>(0, 3) = -Matrix3d::Identity();

									// imu_state.orientation---qbw  转置即qwb
    F.block<3, 3>(6, 0) =			// acc是imu坐标系下的,已经在①中减去了偏置
        -quaternionToRotation(imu_state.orientation).transpose() * skewSymmetric(acc);
    F.block<3, 3>(6, 9) = -quaternionToRotation(imu_state.orientation).transpose();
    F.block<3, 3>(12, 6) = Matrix3d::Identity();

    G.block<3, 3>(0, 0) = -Matrix3d::Identity();
    G.block<3, 3>(3, 3) = Matrix3d::Identity();
    G.block<3, 3>(6, 6) = -quaternionToRotation(imu_state.orientation).transpose();
    G.block<3, 3>(9, 9) = Matrix3d::Identity();

    // Approximate matrix exponential to the 3rd order,
    // which can be considered to be accurate enough assuming
    // dtime is within 0.01s.	这里主要是为了把连续时间矩阵F离散为状态转移矩阵,指数的泰勒3阶展开,再乘以时间dt
    Matrix<double, 21, 21> Fdt = F * dtime;				// 1阶近似
    Matrix<double, 21, 21> Fdt_square = Fdt * Fdt;		// 2阶近似
    Matrix<double, 21, 21> Fdt_cube = Fdt_square * Fdt;	// 3阶近似

  把一个连续系统离散化,在线性系统理论中讲过,系统矩阵 F F F变为了 状态转移矩阵 Φ ( t + Δ t , t ) = e x p ( F t ) \Phi(t+\Delta t,t)= exp(Ft) Φ(t+Δt,t)=exp(Ft)
Φ ( t + Δ t , t ) = exp ⁡ ( F c Δ t ) = I 21 × 21 + F c Δ t + 1 2 ! F c 2 Δ t 2 + 1 3 ! F c 3 Δ t 3 + . . . \begin{aligned} \Phi(t+\Delta t,t)& =\exp\left(\mathbf{F}_{c}\Delta t\right) \\ &=\mathbf{I}_{21\times21}+\mathbf{F}_{c}\Delta t+\frac{1}{2!}\mathbf{F}_{c}^{2}\Delta t^{2}+\frac{1}{3!}\mathbf{F}_{c}^{3}\Delta t^{3} + ... \end{aligned} Φ(t+Δt,t)=exp(FcΔt)=I21×21+FcΔt+2!1Fc2Δt2+3!1Fc3Δt3+...
  其中幂指数(注意这里F是简化版,道理是一样的):
F c = [ − ⌊ ω ^ × ⌋ − I 3 × 3 0 3 × 3 0 3 × 3 ] , F c 2 = [ ⌊ ω ^ × ⌋ 2 ⌊ ω ^ × ⌋ 0 3 × 3 0 3 × 3 ] F c 3 = [ − ⌊ ω ^ × ⌋ 3 − ⌊ ω ^ × ⌋ 2 0 3 × 3 0 3 × 3 ] , F c 4 = [ ⌊ ω ^ × ⌋ 4 ⌊ ω ^ × ⌋ 3 0 3 × 3 0 3 × 3 ] \begin{aligned}&\mathbf{F}_c=\begin{bmatrix}-\lfloor\hat{\boldsymbol{\omega}}\times\rfloor&-\mathbf{I}_{3\times3}\\\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3}\end{bmatrix}\quad,\quad\mathbf{F}_c^2=\begin{bmatrix}\lfloor\hat{\boldsymbol{\omega}}\times\rfloor^2&\lfloor\hat{\boldsymbol{\omega}}\times\rfloor\\\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3}\end{bmatrix}\\&\mathbf{F}_c^3=\begin{bmatrix}-\lfloor\hat{\boldsymbol{\omega}}\times\rfloor^3&-\lfloor\hat{\boldsymbol{\omega}}\times\rfloor^2\\\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3}\end{bmatrix}\quad,\quad\mathbf{F}_c^4=\begin{bmatrix}\lfloor\hat{\boldsymbol{\omega}}\times\rfloor^4&\lfloor\hat{\boldsymbol{\omega}}\times\rfloor^3\\\mathbf{0}_{3\times3}&\mathbf{0}_{3\times3}\end{bmatrix}\end{aligned} Fc=[ω^×03×3I3×303×3],Fc2=[ω^×203×3ω^×03×3]Fc3=[ω^×303×3ω^×203×3],Fc4=[ω^×403×3ω^×303×3]

③ 计算状态转移矩阵Phi(离散化的F,误差状态)
    // 3. 计算转移矩阵Phi矩阵
    Matrix<double, 21, 21> Phi =
        Matrix<double, 21, 21>::Identity() + Fdt +
        0.5 * Fdt_square + (1.0 / 6.0) * Fdt_cube;
④ IMU状态预测

  注意这里是状态而不是误差状态!同样,我们要把连续时间下IMU状态运动学方程转换为离散时间下IMU运动学方程。为了更加精确的计算,对于速度v和位置p进行了四阶龙格库塔法积分。

  下面是连续时间IMU运动学方程(微分方程),后面要离散化差分方程
G I q ˉ ˙ ( t ) = 1 2 Ω ( ω ( t ) ) G I q ˉ ( t ) , b ˙ g ( t ) = n w g ( t ) v ˙ I ( t ) = G a ( t ) , b ˙ a ( t ) = n w a ( t ) , p ˙ I ( t ) = G v I ( t ) \begin{aligned} {^{I}_{G}\dot{\bar{q}}(t)}&=\frac{1}{2}\boldsymbol{\Omega}\big(\boldsymbol{\omega}(t)\big)_{G}^{I}\bar{q}(t),&\dot{\mathbf{b}}_{g}(t)&=\mathbf{n}_{wg}(t)\\\dot{\mathbf{v}}_{I}(t)&={}^{G}\mathbf{a}(t),&\dot{\mathbf{b}}_{a}(t)&=\mathbf{n}_{wa}(t),&\dot{\mathbf{p}}_{I}(t)&={}^{G}\mathbf{v}_{I}(t)\end{aligned} GIqˉ˙(t)v˙I(t)=21Ω(ω(t))GIqˉ(t),=Ga(t),b˙g(t)b˙a(t)=nwg(t)=nwa(t),p˙I(t)=GvI(t)

    // Propogate the state using 4th order Runge-Kutta
    // 4. 四阶龙格库塔积分预测状态
    predictNewState(dtime, gyro, acc);
<4.1> Omega矩阵

4.1 Omega矩阵用在向量和四元数的乘积中,例如用于四元数导数中

Ω ( ω ) = [ 0 ω z − ω y ω x − ω z 0 ω x ω y ω y − ω x 0 ω z − ω x − ω y − ω z 0 ] = [ − ⌊ ω × ⌋ ω − ω T 0 ] \left.\boldsymbol{\Omega}(\boldsymbol{\omega})=\left[\begin{array}{cccc}0&\omega_z&-\omega_y&\omega_x\\-\omega_z&0&\omega_x&\omega_y\\\omega_y&-\omega_x&0&\omega_z\\-\omega_x&-\omega_y&-\omega_z&0\end{array}\right.\right] \left.=\left[\begin{array}{cc}-\lfloor\boldsymbol{\omega}\times\rfloor&\boldsymbol{\omega}\\-\boldsymbol{\omega}^\mathrm{T}&0\end{array}\right.\right] Ω(ω)= 0ωzωyωxωz0ωxωyωyωx0ωzωxωyωz0 =[ω×ωTω0]

/**
 * @brief 来一个新的imu数据做积分,应用四阶龙哥库塔法
 * @param  dt 相对上一个数据的间隔时间
 * @param  gyro 角速度减去偏置后的
 * @param  acc 加速度减去偏置后的
 */
void MsckfVio::predictNewState(const double &dt, const Vector3d &gyro, const Vector3d &acc)
{

    // TODO: Will performing the forward integration using
    //    the inverse of the quaternion give better accuracy?

    // 角速度,标量
    double gyro_norm = gyro.norm();		// 用于选择离散公式
    // 4.1 计算Omega矩阵
    Matrix4d Omega = Matrix4d::Zero();
    Omega.block<3, 3>(0, 0) = -skewSymmetric(gyro);
    Omega.block<3, 1>(0, 3) = gyro;
    Omega.block<1, 3>(3, 0) = -gyro;
 
    ...
        
}
<4.2> 获取k时刻的imu状态
void MsckfVio::predictNewState(const double &dt, const Vector3d &gyro, const Vector3d &acc)
{
    ...
    
    // 4.2 获取k时刻的imu状态----注意这里是引用,所以后面预测会直接更改这里的变量。predictNewState处理之后imu_state就是k+1时刻的预测值了。
	Vector4d &q = state_server.imu_state.orientation;
    Vector3d &v = state_server.imu_state.velocity;
    Vector3d &p = state_server.imu_state.position;
  	
    ...
}
<4.3> 预测姿态 G I q T _G^Iq^T GIqT

  采用论文Indirect Kalman Filter for 3D Attitude Estimation1.6节0阶离散时间差分方程(微分方程差分化)
∣ ω ^ ∣ > 1 0 − 5  时:  G I q ^ ( t + Δ t ) = ( cos ⁡ ( ∣ ω ^ ∣ 2 Δ t ) ⋅ I 4 × 4 + 1 ∣ ω ^ ∣ sin ⁡ ( ∣ ω ^ ∣ 2 Δ t ) ⋅ Ω ( ω ^ ) ) G I q ^ ( t ) ∣ ω ^ ∣ ≤ 1 0 − 5  时:  G I q ^ ( t + Δ t ) = ( I 4 × 4 − Δ t 2 Ω ( ω ^ ) ) G I q ^ ( t ) \begin{aligned}&|\hat{\omega}|>10^{-5}\text{ 时: }^I_G\hat{q}(t+\Delta t)=\left(\cos\left(\frac{|\hat{\omega}|}2\Delta t\right)\cdot I_{4\times4}+\frac1{|\hat{\omega}|}\sin\left(\frac{|\hat{\omega}|}2\Delta t\right)\cdot\Omega(\hat{\omega})\right) {^I_G\hat{q}}(t)\\&|\hat{\omega}|\leq10^{-5}\text{ 时: }^I_G\hat{q}(t+\Delta t)=\left(I_{4\times4}-\frac{\Delta t}2\Omega(\hat{\omega})\right){^I_G\hat{q}(t)}\end{aligned} ω^>105 GIq^(t+Δt)=(cos(2ω^Δt)I4×4+ω^1sin(2ω^Δt)Ω(ω^))GIq^(t)ω^105 GIq^(t+Δt)=(I4×42ΔtΩ(ω^))GIq^(t)

    // Some pre-calculation
    // dq_dt表示积分n到n+1
    // dq_dt2表示积分n到n+0.5 算龙哥库塔用的
    Vector4d dq_dt, dq_dt2;
    if (gyro_norm > 1e-5)	// 角速度模长
    {
        dq_dt =
            (cos(gyro_norm * dt * 0.5) * Matrix4d::Identity() +
            1 / gyro_norm * sin(gyro_norm * dt * 0.5) * Omega) * q;
        dq_dt2 =
            (cos(gyro_norm * dt * 0.25) * Matrix4d::Identity() +
            1 / gyro_norm * sin(gyro_norm * dt * 0.25) * Omega) * q;
    }
    else
    {
        // 当角增量很小时的近似
        dq_dt = (Matrix4d::Identity() + 0.5 * dt * Omega) * cos(gyro_norm * dt * 0.5) * q;
        dq_dt2 = (Matrix4d::Identity() + 0.25 * dt * Omega) * cos(gyro_norm * dt * 0.25) * q;
    }
    // Rwi  转换为旋转矩阵
    Matrix3d dR_dt_transpose = quaternionToRotation(dq_dt).transpose();
    Matrix3d dR_dt2_transpose = quaternionToRotation(dq_dt2).transpose();

    q = dq_dt;
    quaternionNormalize(q);		// 一定要记得归一化!
<4.4> 预测速度v和位置p

四阶龙格库塔法

{ y n + 1 = y n + h 6 ( k 1 + 2 k 2 + 2 k 3 + k 4 ) k 1 = f ( x n , y n ) k 2 = f ( x n + h 2 , y n + h 2 k 1 ) k 3 = f ( x n + ℏ 2 , y n + ℏ 2 k 2 ) k 4 = f ( x n + h , y n + h k 3 ) \left.\left\{\begin{array}{l}y_{n+1}=y_n+\frac h6\left(k_1+2\boldsymbol{k}_2+2k_3+k_4\right)\\k_1=f\left(x_n,y_n\right)\\k_2=f\left(x_n+\frac h2,y_n+\frac h2k_1\right)\\k_3=f\left(x_n+\frac\hbar2,y_n+\frac\hbar2\boldsymbol{k}_2\right)\\\boldsymbol{k}_4=\boldsymbol{f}\left(\boldsymbol{x}_n+\boldsymbol{h},\boldsymbol{y}_n+\boldsymbol{h}\boldsymbol{k}_3\right)\end{array}\right.\right. yn+1=yn+6h(k1+2k2+2k3+k4)k1=f(xn,yn)k2=f(xn+2h,yn+2hk1)k3=f(xn+2,yn+2k2)k4=f(xn+h,yn+hk3)

利用龙格库塔法预测速度v和位移p

v ^ ( t + Δ t ) = v ^ ( t ) + Δ t 6 ( k v 1 + 2 k v 2 + 2 k v 3 + k v 4 ) k v 1 = R ^ ( t ) a ^ + g k v 2 = R ^ ( t + Δ t / 2 ) a ^ + g k v 3 = R ^ ( t + Δ t / 2 ) a ^ + g k v 4 = R ^ ( t + Δ t ) a ^ + g p ^ ( t + Δ t ) = p ^ ( t ) + Δ t 6 ( k p 1 + 2 k p 2 + 2 k p 3 + k p 4 ) k p 1 = v ^ ( t ) k p 2 = v ^ ( t ) + k v 1 Δ t / 2 k p 3 = v ^ ( t ) + k v 2 Δ t / 2 k p 4 = v ^ ( t ) + k v 3 Δ t \begin{aligned} &\hat{v}(t+\Delta t)=\hat{v}(t)+\frac{\Delta t}6\left(k_{v_1}+2k_{v_2}+2k_{v_3}+k_{v_4}\right) \\ &k_{v_1}=\hat{R}(t)\hat{a}+g \\ &k_{v_2}=\hat{R}(t+\Delta t/2)\hat{a}+g \\ &k_{v_3}=\hat{R}(t+\Delta t/2)\hat{a}+g \\ &k_{v_4}=\hat{R}(t+\Delta t)\hat{a}+g \\ &\hat{p}(t+\Delta t)=\hat{p}(t)+\frac{\Delta t}6\left(k_{p_1}+2k_{p_2}+2k_{p_3}+k_{p_4}\right) \\ &k_{p_1}=\hat{v}(t) \\ &k_{p_2}=\hat{v}(t)+k_{v_1}\Delta t/2 \\ &k_{p_3}=\hat{v}(t)+k_{v_2}\Delta t/2 \\ &k_{p_4}=\hat{v}(t)+k_{v_3}\Delta t \end{aligned} v^(t+Δt)=v^(t)+6Δt(kv1+2kv2+2kv3+kv4)kv1=R^(t)a^+gkv2=R^(t+Δt/2)a^+gkv3=R^(t+Δt/2)a^+gkv4=R^(t+Δt)a^+gp^(t+Δt)=p^(t)+6Δt(kp1+2kp2+2kp3+kp4)kp1=v^(t)kp2=v^(t)+kv1Δt/2kp3=v^(t)+kv2Δt/2kp4=v^(t)+kv3Δt

    // k1 = f(tn, yn)  ----kv1
    Vector3d k1_v_dot = quaternionToRotation(q).transpose() * acc + IMUState::gravity;
    Vector3d k1_p_dot = v;		// kp1

    // k2 = f(tn+dt/2, yn+k1*dt/2)
    // 这里的4阶LK法用了匀加速度假设,即认为前一时刻的加速度和当前时刻相等
    Vector3d k1_v = v + k1_v_dot * dt / 2;
    Vector3d k2_v_dot = dR_dt2_transpose * acc + IMUState::gravity;
    Vector3d k2_p_dot = k1_v;

    // k3 = f(tn+dt/2, yn+k2*dt/2)
    Vector3d k2_v = v + k2_v_dot * dt / 2;
    Vector3d k3_v_dot = dR_dt2_transpose * acc + IMUState::gravity;
    Vector3d k3_p_dot = k2_v;

    // k4 = f(tn+dt, yn+k3*dt)
    Vector3d k3_v = v + k3_v_dot * dt;
    Vector3d k4_v_dot = dR_dt_transpose * acc + IMUState::gravity;
    Vector3d k4_p_dot = k3_v;

    // yn+1 = yn + dt/6*(k1+2*k2+2*k3+k4)
    v = v + dt / 6 * (k1_v_dot + 2 * k2_v_dot + 2 * k3_v_dot + k4_v_dot);
    p = p + dt / 6 * (k1_p_dot + 2 * k2_p_dot + 2 * k3_p_dot + k4_p_dot);

    return;
}
⑤ 可观性约束

  这部分不好理解,但公式可以对上。

N r , k + 1 = Φ k N r , k → [ C ( q ^ c , k + 1 ∣ k ) c g 0 3 − ⌊ c v ^ t , k + 1 ∣ k × ⌋ c g 0 3 − ⌊ c p ^ t , k + 1 ∣ k × ⌋ c g ] = [ Φ 11 Φ 12 0 3 0 3 0 3 0 3 0 3 I 3 0 3 0 3 0 3 Φ 31 Φ 32 I 3 0 34 0 3 0 3 0 3 0 3 I 3 0 3 0 51 Φ 52 δ t I 3 Φ 54 I 3 ] [ C ( q ^ c , k ∣ k − 1 ) c g 0 3 − ⌊ q ^ t , k ∣ k − 1 × ⌋ c g 0 3 0 3 ] . \mathbf{N}_{r,k+1}=\mathbf{\Phi}_k\mathbf{N}_{r,k}\quad\to\quad\begin{bmatrix}\mathbf{C}\left(\hat{q}_{c,k+1|k}\right)^{c}\mathbf{g}\\\mathbf{0}_{3}\\-\lfloor c\hat{\mathbf{v}}_{t,k+1|k}\times\rfloor^{c}\mathbf{g}\\\mathbf{0}_{3}\\-\lfloor{}^{c}\hat{\mathbf{p}}_{t,k+1|k}\times\rfloor^{c}\mathbf{g}\end{bmatrix}=\begin{bmatrix}\Phi_{11}&\Phi_{12}&\mathbf{0}_{3}&\mathbf{0}_{3}&\mathbf{0}_{3}&\mathbf{0}_{3}\\\mathbf{0}_{3}&\mathbf{I}_{3}&\mathbf{0}_{3}&\mathbf{0}_{3}&\mathbf{0}_{3}\\\Phi_{31}&\Phi_{32}&\mathbf{I}_{3}&\mathbf{0}_{34}&\mathbf{0}_{3}\\\mathbf{0}_{3}&\mathbf{0}_{3}&\mathbf{0}_{3}&\mathbf{I}_{3}&\mathbf{0}_{3}\\\mathbf{0}_{51}&\Phi_{52}&\delta t\mathbf{I}_{3}&\Phi_{54}&\mathbf{I}_{3}\end{bmatrix}\begin{bmatrix}\mathbf{C}\left(\hat{q}_{c,k|k-1}\right)^{c}\mathbf{g}\\\mathbf{0}_{3}\\-\lfloor\hat{q}_{t,k|k-1}\times\rfloor^{c}\mathbf{g}\\\mathbf{0}_{3}\\\mathbf{0}_{3}\end{bmatrix}. Nr,k+1=ΦkNr,k C(q^c,k+1∣k)cg03cv^t,k+1∣k×cg03cp^t,k+1∣k×cg = Φ1103Φ3103051Φ12I3Φ3203Φ520303I303δtI30303034I3Φ5403030303I303 C(q^c,kk1)cg03q^t,kk1×cg0303 .
  我们把这个矩阵乘出来,能得到3个等式(主要是修改Φ11,31,51):

第1行:可以直接接出来

C ( ′ q ˉ ^ G , k + 1 ∣ k ) c g = Φ 11 C ( ′ q ˉ ^ G , k ∣ k − 1 ) c g → Φ 11 = C ( ι , k + 1 ∣ k q ~ ^ ι , k ∣ k − 1 ) . \mathbf{C}\left({}^{\prime}\hat{\bar{q}}_{G,k+1|k}\right)^{c}\mathbf{g}=\Phi_{11}\mathbf{C}\left({}^{\prime}\hat{\bar{q}}_{G,k|k-1}\right)^{c}\mathbf{g}\quad\to\Phi_{11}=\mathbf{C}\left({}^{\iota,k+1|k}\hat{\tilde{q}}_{\iota,k|k-1}\right). C(qˉ^G,k+1∣k)cg=Φ11C(qˉ^G,kk1)cgΦ11=C(ι,k+1∣kq~^ι,kk1).

第3、5行:论文中提到线性相关,无法直接求,或者说由多个解(等式中包含了反对
称矩阵导致实际右侧如果按照第⼀个式子那么算就会导致得到矩阵是线性相关的)

Φ 31 C ( q ^ c , k ∣ k − 1 ) G g = ⌊ G v ^ l , k ∣ k − 1 × ⌋ G g − ⌊ G v ^ l , k + 1 ∣ k × ⌋ G g Φ 51 C ( I q ^ G , k ∣ k − 1 ) G g = δ t ⌊ G v ^ l , k ∣ k − 1 × ⌋ G g − ⌊ G p ^ l , k + 1 ∣ k × ⌋ G g \begin{aligned}\Phi_{31}\mathbf{C}\left(\hat{q}_{c,k|k-1}\right)^G\mathbf{g}&=\lfloor{}^G\hat{\mathbf{v}}_{l,k|k-1}\times\rfloor^G\mathbf{g}-\lfloor{}^G\hat{\mathbf{v}}_{l,k+1|k}\times\rfloor^G\mathbf{g}\\\Phi_{51}\mathbf{C}\left({}^I\hat{q}_{G,k|k-1}\right)^G\mathbf{g}&=\delta t\lfloor{}^G\hat{\mathbf{v}}_{l,k|k-1}\times\rfloor^G\mathbf{g}-\lfloor{}^G\hat{\mathbf{p}}_{l,k+1|k}\times\rfloor^G\mathbf{g}\end{aligned} Φ31C(q^c,kk1)GgΦ51C(Iq^G,kk1)Gg=Gv^l,kk1×GgGv^l,k+1∣k×Gg=δtGv^l,kk1×GgGp^l,k+1∣k×Gg

  然后论文里面引入了一个最小二乘类似的约束,其中 A=Φ31 或 Φ51

min ⁡ A ∗ ∥ A ∗ − A ∥ F 2 , s.t.  A ∗ u = w \min_{\mathbf{A}^{*}}\left\|\mathbf{A}^{*}-\mathbf{A}\right\|_{\mathcal{F}}^{2},\quad\text{s.t. }\mathbf{A}^{*}\mathbf{u}=\mathbf{w} AminAAF2,s.t. Au=w

A ∗ = A − ( A u − w ) ( u T u ) − 1 u T \mathbf{A}^*=\mathbf{A}-(\mathbf{A}\mathbf{u}-\mathbf{w})\left(\mathbf{u}^T\mathbf{u}\right)^{-1}\mathbf{u}^T A=A(Auw)(uTu)1uT

    // 5. Observability-constrained VINS 可观性约束
    // Modify the transition matrix
    // 5.1 修改phi_11
    // imu_state.orientation_null为上一个imu数据递推后保存的
    // 这块可能会有疑问,因为当上一个imu假如被观测更新了,
    // 导致当前的imu状态是由更新后的上一个imu状态递推而来,但是这里的值是没有更新的,这个有影响吗
    // 答案是没有的,因为我们更改了phi矩阵,保证了零空间
    // 并且这里必须这么处理,因为如果使用更新后的上一个imu状态构建上一时刻的零空间
    // 就破坏了上上一个跟上一个imu状态之间的0空间
    // Ni-1 = phi_[i-2] * Ni-2
    // Ni = phi_[i-1] * Ni-1^
    // 如果像上面这样约束,那么中间的0空间就“崩了”
    Matrix3d R_kk_1 = quaternionToRotation(imu_state.orientation_null);
    Phi.block<3, 3>(0, 0) =
        quaternionToRotation(imu_state.orientation) * R_kk_1.transpose();

    // 5.2 修改phi_31
    Vector3d u = R_kk_1 * IMUState::gravity;
    RowVector3d s = (u.transpose() * u).inverse() * u.transpose();

    Matrix3d A1 = Phi.block<3, 3>(6, 0);
    Vector3d w1 =
        skewSymmetric(imu_state.velocity_null - imu_state.velocity) * IMUState::gravity;
    Phi.block<3, 3>(6, 0) = A1 - (A1 * u - w1) * s;

    // 5.3 修改phi_51
    Matrix3d A2 = Phi.block<3, 3>(12, 0);
    Vector3d w2 =
        skewSymmetric(
            dtime * imu_state.velocity_null + imu_state.position_null -
            imu_state.position) *
        IMUState::gravity;
    Phi.block<3, 3>(12, 0) = A2 - (A2 * u - w2) * s;
⑥ 更新0空间约束后的状态转移矩阵phi和误差状态协方差矩阵

P k + 1 ∣ k = Φ k P k ∣ k Φ k T + Q d , k Q d , k = ∫ t k t k + 1 Φ ( t k + 1 , τ ) G c ( τ ) Q c G c T ( τ ) Φ T ( t k + 1 , τ ) d τ \begin{gathered}\mathbf{P}_{k+1|k}=\Phi_k\mathbf{P}_{k|k}\Phi_k^T+\mathbf{Q}_{d,k} \\ \mathbf{Q}_{d,k}=\int_{t_k}^{t_{k+1}}\boldsymbol{\Phi}\left(t_{k+1},\tau\right)\mathbf{G}_c(\tau)\mathbf{Q}_c\mathbf{G}_c^\mathrm{T}(\tau)\boldsymbol{\Phi}^\mathrm{T}\left(t_{k+1},\tau\right)\mathrm{d}\tau\end{gathered}\\ Pk+1∣k=ΦkPkkΦkT+Qd,kQd,k=tktk+1Φ(tk+1,τ)Gc(τ)QcGcT(τ)ΦT(tk+1,τ)dτ

    // Propogate the state covariance matrix.
    // 6. 使用0空间约束后的phi计算积分后的新的协方差矩阵
	// Q是高斯白噪声带来的协方差矩阵,代码中简单计算了
    Matrix<double, 21, 21> Q =
        Phi * G * state_server.continuous_noise_cov * G.transpose() * Phi.transpose() * dtime;	// G就是上面求得F、G

	// 误差状态协方差矩阵
    state_server.state_cov.block<21, 21>(0, 0) =
        Phi * state_server.state_cov.block<21, 21>(0, 0) * Phi.transpose() + Q;
⑦ 更新imu状态量与相机状态量交叉的部分

P k + 1 ∣ k = ( P I I k + 1 ∣ k Φ k P I C k ∣ k P I C k ∣ k ⊤ Φ k ⊤ P C C k ∣ k ) \left.\mathbf{P}_{k+1|k}=\left(\begin{matrix}\mathbf{P}_{II_{k+1|k}}&\mathbf{\Phi}_{k}\mathbf{P}_{IC_{k|k}}\\\mathbf{P}_{IC_{k|k}}^{\top}\mathbf{\Phi}_{k}^{\top}&\mathbf{P}_{CC_{k|k}}\end{matrix}\right.\right) Pk+1∣k=(PIIk+1∣kPICkkΦkΦkPICkkPCCkk)
在这里插入图片描述

    // 7. 如果有相机状态量,那么更新imu状态量与相机状态量交叉的部分
    if (state_server.cam_states.size() > 0)
    {
        // 起点是0 21  然后是21行 state_server.state_cov.cols() - 21 列的矩阵
        // 也就是整个协方差矩阵的右上角,这部分说白了就是imu状态量与相机状态量的协方差,imu更新了,这部分也需要更新
        state_server.state_cov.block(0, 21, 21, state_server.state_cov.cols() - 21) =
            Phi * state_server.state_cov.block(0, 21, 21, state_server.state_cov.cols() - 21);

        // 同理,这个是左下角
        state_server.state_cov.block(21, 0, state_server.state_cov.rows() - 21, 21) =
            state_server.state_cov.block(21, 0, state_server.state_cov.rows() - 21, 21) *
            Phi.transpose();
    }
⑧ 强制对称协方差矩阵
    // 8. 强制对称,因为协方差矩阵就是对称的
    MatrixXd state_cov_fixed = 
        (state_server.state_cov + state_server.state_cov.transpose()) / 2.0;
    state_server.state_cov = state_cov_fixed;
⑨ 更新零空间

    // Update the state correspondes to null space.
    // 9. 更新零空间,供下个IMU来了使用---
	// imu_state.orientation这种目前就是上一个时刻状态预测值!
    imu_state.orientation_null = imu_state.orientation;
    imu_state.position_null = imu_state.position;
    imu_state.velocity_null = imu_state.velocity;

    // 更新时间
    state_server.imu_state.time = time;
    return;
}

4.3 状态增广 stateAugmentation

  在没有图像进来时,对IMU状态进行预测,并计算系统误差状态协方差矩阵;在有图像进来时,根据相机与IMU的相对外参计算当前相机的位姿。然后将最新的相机状态加入到系统状态向量中去,然后扩增误差状态协方差矩阵

4.3.1 利用最新的imu状态计算cam状态

  const CameraMeasurementConstPtr &msg = msg->header.stamp.toSec()前最近的1次IMU预测的状态。理论上和相机时间戳是差了一点的,但无伤大雅,近似为同一时间。

  When new images are received, the state should be augmented with the new camera state. The pose of the new camera state can be computed from the latest IMU state as
G C q ^ = I C q ^ ⊗ G I q ^ , G p ^ C = G p ^ C + C ( G I q ^ ) ⊤ I p ^ C \left._G^C\hat{\mathbf{q}}={}_I^C\hat{\mathbf{q}}\otimes{}_G^I\hat{\mathbf{q}},\quad{}^G\hat{\mathbf{p}}_C={}^G\hat{\mathbf{p}}_C+C\left(\begin{matrix}^I_G\hat{\mathbf{q}}\end{matrix}\right.\right)^\top{}^I\hat{\mathbf{p}}_C GCq^=ICq^GIq^,Gp^C=Gp^C+C(GIq^)Ip^C

/**
 * @brief 根据时间分裂出相机状态
 * @param  time 图片的时间戳
 */
void MsckfVio::stateAugmentation(const double &time)
{
    // 1. 取出当前更新好的imu状态量
    // 1.1 取出状态量中的外参,imu到cam的外参
    const Matrix3d &R_i_c = state_server.imu_state.R_imu_cam0;
    const Vector3d &t_c_i = state_server.imu_state.t_cam0_imu;

    // Add a new camera state to the state server.
    // 1.2 取出imu旋转平移,按照外参,将这个时刻cam0的位姿算出来
    Matrix3d R_w_i = quaternionToRotation(
        state_server.imu_state.orientation);
    Matrix3d R_w_c = R_i_c * R_w_i;			//Rcw
    Vector3d t_c_w = state_server.imu_state.position +	// twc
                        R_w_i.transpose() * t_c_i;
    

}

4.3.2 记录相机状态- imu_state.id

    // 2. 注册新的相机状态到状态库中
    // 嗯。。。说人话就是找个记录的,不然咋更新
    state_server.cam_states[state_server.imu_state.id] =
        CAMState(state_server.imu_state.id);
    CAMState &cam_state = state_server.cam_states[state_server.imu_state.id];

    // 严格上讲这个时间不对,但是几乎没影响 ---cam时间略大于imu时间
    cam_state.time = time;
    cam_state.orientation = rotationToQuaternion(R_w_c);
    cam_state.position = t_c_w;

    // 记录第一次被估计的数据,不能被改变,因为改变了就破坏了之前的0空间
    cam_state.orientation_null = cam_state.orientation;
    cam_state.position_null = cam_state.position;

4.3.3 更新协方差矩阵

① 计算雅可比矩阵

6*(21+6N)

J = ( J I 0 6 × 6 N ) \mathbf{J}=\begin{pmatrix}\mathbf{J}_I&\mathbf{0}_{6\times6N}\end{pmatrix} J=(JI06×6N)

6*21

J I = [ R c b 0 0 0 0 I 0 ( R w b t b c ) ∧ 0 0 0 I 0 R w b ] \left.J_I=\left[\begin{array}{cccccccc}R_{cb}&\mathbf{0}&\mathbf{0}&\mathbf{0}&\mathbf{0}&\mathbf{I}&\mathbf{0}\\(R_{wb}t_{bc})^{\wedge}&\mathbf{0}&\mathbf{0}&\mathbf{0}&\mathbf{I}&\mathbf{0}&R_{wb}\end{array}\right.\right] JI=[Rcb(Rwbtbc)0000000II00Rwb]

    // 3. 这个雅可比可以认为是cam0位姿相对于imu的状态量的求偏导
    // 此时我们首先要知道相机位姿是 Rcw  twc
    // Rcw = Rci * Riw   twc = twi + Rwi * tic
    Matrix<double, 6, 21> J = Matrix<double, 6, 21>::Zero();
    // Rcw对Riw的左扰动导数
    J.block<3, 3>(0, 0) = R_i_c;
    // Rcw对Rci的左扰动导数
    J.block<3, 3>(0, 15) = Matrix3d::Identity();

    // twc对Riw的左扰动导数
    // twc = twi + Rwi * Exp(φ) * tic
    //     = twi + Rwi * (I + φ^) * tic
    //     = twi + Rwi * tic + Rwi * φ^ * tic
    //     = twi + Rwi * tic - Rwi * tic^ * φ
    // 这部分的偏导为 -Rwi * tic^     与论文一致
    // TODO 试一下 -R_w_i.transpose() * skewSymmetric(t_c_i)
    // 其实这里可以反过来推一下当给的扰动是
    // twc = twi + Exp(-φ) * Rwi * tic
    //     = twi + (I - φ^) * Rwi * tic
    //     = twi + Rwi * tic - φ^ * Rwi * tic
    //     = twi + Rwi * tic + (Rwi * tic)^ * φ
    // 这样跟代码就一样了,但是上下定义的扰动方式就不同了
    J.block<3, 3>(3, 0) = skewSymmetric(R_w_i.transpose() * t_c_i);
    // 下面是代码里自带的,论文中给出的也是下面的结果
    // J.block<3, 3>(3, 0) = -R_w_i.transpose()*skewSymmetric(t_c_i);

    // twc对twi的左扰动导数
    J.block<3, 3>(3, 12) = Matrix3d::Identity();
    // twc对tic的左扰动导数
    J.block<3, 3>(3, 18) = R_w_i.transpose();
② 计算增广协方差矩阵

  就是说原来只有左上角的P,现在多了其余三个部分!整体来看就是整个矩阵的行维和列维都多了6!(old_rows + 6, old_cols + 6)代码也能看出来。
P ( 21 + 6 ( N + 1 ) ) × ( 21 + 6 ( N + 1 ) ) = [ I 21 + 6 N J 6 × ( 21 + 6 N ) ] P ( 21 + 6 N ) × ( 21 + 6 N ) [ I 21 + 6 N J 6 × ( 21 + 6 N ) ] T = [ P P J T J P J P J T ] \begin{gathered} P^{(21+6(N+1))\times(21+6(N+1))} \left.=\left[\begin{array}{c}I_{21+6N}\\J_{6\times(21+6N)}\end{array}\right.\right]P^{(21+6N)\times(21+6N)}\left[\begin{array}{c}I_{21+6N}\\J_{6\times(21+6N)}\end{array}\right]^T \\ \left.=\left[\begin{array}{cc}P&PJ^T\\JP&JPJ^T\end{array}\right.\right] \end{gathered} P(21+6(N+1))×(21+6(N+1))=[I21+6NJ6×(21+6N)]P(21+6N)×(21+6N)[I21+6NJ6×(21+6N)]T=[PJPPJTJPJT]

    // 4. 增广协方差矩阵
    // 简单地说就是原来的协方差是 21 + 6n 维的,现在新来了一个伙计,维度要扩了
    // 并且对应位置的值要根据雅可比跟这个时刻(也就是最新时刻)的imu协方差计算
    // 4.1 扩展矩阵大小 conservativeResize函数不改变原矩阵对应位置的数值-----左上角不需要改变
    // Resize the state covariance matrix.
    size_t old_rows = state_server.state_cov.rows();
    size_t old_cols = state_server.state_cov.cols();
    state_server.state_cov.conservativeResize(old_rows + 6, old_cols + 6);

    // Rename some matrix blocks for convenience.
    // imu的协方差矩阵
    const Matrix<double, 21, 21> &P11 =
        state_server.state_cov.block<21, 21>(0, 0);

    // imu相对于各个相机状态量的协方差矩阵(不包括最新的)--对过去相机状态量的雅可比。就是上一次协方差的右上角!
    const MatrixXd &P12 =
        state_server.state_cov.block(0, 21, 21, old_cols - 21);


    // 4.2 计算协方差矩阵
    // 左下角
    state_server.state_cov.block(old_rows, 0, 6, old_cols) << J * P11, J * P12;

    // 右上角-----(21+6N)*6---与左下角互为转置
    state_server.state_cov.block(0, old_cols, old_rows, 6) =
        state_server.state_cov.block(old_rows, 0, 6, old_cols).transpose();

    // 右下角,关于相机部分的J都是0所以省略了----6*6矩阵
    state_server.state_cov.block<6, 6>(old_rows, old_cols) =
        J * P11 * J.transpose();

    // 强制对称
    MatrixXd state_cov_fixed = (state_server.state_cov +
                                state_server.state_cov.transpose()) /
                                2.0;
    state_server.state_cov = state_cov_fixed;

    return;
}

4.4 添加特征点观测

  现在来了一帧新的图像,那么就会有新的特征点,所以把要添加新的特征信息。

/**
 * @brief 添加特征点观测
 * @param  msg 前端发来的特征点信息,里面包含了时间,左右目上的角点及其id(严格意义上不能说是特征点)
 */
void MsckfVio::addFeatureObservations(
    const CameraMeasurementConstPtr &msg)
{
    // 这是个long long int 嗯。。。。直接当作int理解吧
    // 这个id会在 batchImuProcessing 更新
    StateIDType state_id = state_server.imu_state.id;

    // 1. 获取当前窗口内特征点数量
    int curr_feature_num = map_server.size();
    int tracked_feature_num = 0;

    // Add new observations for existing features or new
    // features in the map server.
    // 2. 添加新来的点,做的花里胡哨,其实就是在现有的特征管理里面找,
    // id已存在说明是跟踪的点,在已有的上面更新
    // id不存在说明新来的点,那么就新添加一个
    for (const auto &feature : msg->features)
    {
        if (map_server.find(feature.id) == map_server.end())
        {
            // This is a new feature.
            map_server[feature.id] = Feature(feature.id);
            map_server[feature.id].observations[state_id] =
                Vector4d(feature.u0, feature.v0,
                            feature.u1, feature.v1);
        }
        else
        {
            // This is an old feature.
            map_server[feature.id].observations[state_id] =
                Vector4d(feature.u0, feature.v0,
                            feature.u1, feature.v1);
            ++tracked_feature_num;
        }
    }

    // 这个东西计算了当前进来的跟踪的点中在总数里面的占比(进来的点有可能是新提的)
    tracking_rate =
        static_cast<double>(tracked_feature_num) /
        static_cast<double>(curr_feature_num);

    return;
}

4.5 利用视觉观测更新状态

removeLostFeatures()

4.5.1 特征点管理

  这部分与cam有了较大的联系,看代码之前要搞清楚下面几个变量。

状态量含义
map_server<FeatureIDType, Feature>存储满足要求的特征点的map容器,键-特征点id,值-对应特征点
FeatureIDType长整型long long int
feature.observations<StateIDType, Eigen::Vector4d>哪些帧观测到了这个特征点,相应的归一化坐标是什么?

  这里主要做了两个工作,一个是删除那么无效点,一个是找到哪些当前已经观测不到的有效点去进行后续优化。

/**
 * @brief 使用不再跟踪上的点来更新
 */
void MsckfVio::removeLostFeatures()
{
    // 移除哪些追踪丢失的点
    // BTW, find the size the final Jacobian matrix and residual vector.
    int jacobian_row_size = 0;

    vector<FeatureIDType> invalid_feature_ids(0);  // 无效点,最后要删的
    vector<FeatureIDType> processed_feature_ids(0);  // 待参与更新的点,用完也被无情的删掉

    // 遍历所有特征管理里面的点,包括新进来的
    for (auto iter = map_server.begin();
            iter != map_server.end(); ++iter)
    {
        // Rename the feature to be checked.
        // 引用,改变feature相当于改变iter->second,类似于指针的效果
        auto &feature = iter->second;

        // 1. 这个点被当前状态观测到,说明这个点后面还有可能被跟踪
        // 跳过这些点
        if (feature.observations.find(state_server.imu_state.id) !=
            feature.observations.end())
            continue;

        // 2. 跟踪小于3帧的点,认为是质量不高的点
        // 也好理解,三角化起码要两个观测,但是只有两个没有其他观测来验证
        if (feature.observations.size() < 3)
        {
            invalid_feature_ids.push_back(feature.id);
            continue;
        }

        // 3. 如果这个特征没有被初始化,尝试去初始化
        // 初始化就是三角化
        if (!feature.is_initialized)
        {
            // 3.1 看看运动是否足够,没有足够视差或者平移小旋转多这种不符合三角化
            // 所以就不要这些点了
            if (!feature.checkMotion(state_server.cam_states))
            {
                invalid_feature_ids.push_back(feature.id);
                continue;
            }
            else
            {
                // 3.3 尝试三角化,失败也不要了
                if (!feature.initializePosition(state_server.cam_states))
                {
                    invalid_feature_ids.push_back(feature.id);
                    continue;
                }
            }
        }

        // 4. 到这里表示这个点能用于更新,所以准备下一步计算
        // 一个观测代表一帧,一帧有左右两个观测
        // 也就是算重投影误差时维度将会是4 * feature.observations.size()
        // 这里为什么减3下面会提到
        jacobian_row_size += 4 * feature.observations.size() - 3;
        // 接下来要参与优化的点加入到这个变量中
        processed_feature_ids.push_back(feature.id);
    }


    // Remove the features that do not have enough measurements.
    // 5. 删掉非法点
    for (const auto &feature_id : invalid_feature_ids)
        map_server.erase(feature_id);
    
    // Return if there is no lost feature to be processed.
    if (processed_feature_ids.size() == 0)
        return;

4.5.2 计算误差量雅可比与重投影误差

    // 准备好误差相对于状态量的雅可比
    MatrixXd H_x = MatrixXd::Zero(jacobian_row_size,
                                    21 + 6 * state_server.cam_states.size());
    VectorXd r = VectorXd::Zero(jacobian_row_size);
    int stack_cntr = 0;


    // 6. 处理特征点
    for (const auto &feature_id : processed_feature_ids)
    {
        auto &feature = map_server[feature_id];

        vector<StateIDType> cam_state_ids(0);
        for (const auto &measurement : feature.observations)
            cam_state_ids.push_back(measurement.first);

        MatrixXd H_xj;
        VectorXd r_j;
        // 6.1 计算雅可比,计算重投影误差
        featureJacobian(feature.id, cam_state_ids, H_xj, r_j);

        // 6.2 卡方检验,剔除错误点,并不是所有点都用
        if (gatingTest(H_xj, r_j, cam_state_ids.size() - 1))
        {
            H_x.block(stack_cntr, 0, H_xj.rows(), H_xj.cols()) = H_xj;
            r.segment(stack_cntr, r_j.rows()) = r_j;
            stack_cntr += H_xj.rows();
        }

        // Put an upper bound on the row size of measurement Jacobian,
        // which helps guarantee the executation time.
        // 限制最大更新量
        if (stack_cntr > 1500)
            break;
    }
① featureJacobian()

  一个特征观测误差对于位姿的雅可比是4*6,对于地图点的雅可比是4*3.因为是双目,所以2*2共4维行向量,每一个误差都是归一化后的坐标x、y组成
H C i j = ∂ z i j ∂ C i , 1 p j ⋅ ∂ C i , 1 p j ∂ x C i , 1 + ∂ z i j ∂ C i , 2 p j ⋅ ∂ C i , 2 p j ∂ x C i , 1 H f i j = ∂ z i j ∂ C i , 1 p j ⋅ ∂ C i , 1 p j ∂ G p j + ∂ z i j ∂ C i , 2 p j ⋅ ∂ C i , 2 p j ∂ G p j \begin{gathered} \mathrm{H}_{C_{i}}^{j} =\frac{\partial\mathbf{z}_{i}^{j}}{\partial^{C_{i,1}}\mathbf{p}_{j}}\cdot\frac{\partial^{C_{i,1}}\mathbf{p}_{j}}{\partial\mathbf{x}_{C_{i,1}}}+\frac{\partial\mathbf{z}_{i}^{j}}{\partial^{C_{i,2}}\mathbf{p}_{j}}\cdot\frac{\partial^{C_{i,2}}\mathbf{p}_{j}}{\partial\mathbf{x}_{C_{i,1}}} \\ \mathbf{H}_{f_{i}}^{j} =\frac{\partial\mathbf{z}_{i}^{j}}{\partial^{C_{i,1}}\mathbf{p}_{j}}\cdot\frac{\partial^{C_{i,1}}\mathbf{p}_{j}}{\partial^{G}\mathbf{p}_{j}}+\frac{\partial\mathbf{z}_{i}^{j}}{\partial^{C_{i,2}}\mathbf{p}_{j}}\cdot\frac{\partial^{C_{i,2}}\mathbf{p}_{j}}{\partial^{G}\mathbf{p}_{j}} \end{gathered} HCij=Ci,1pjzijxCi,1Ci,1pj+Ci,2pjzijxCi,1Ci,2pjHfij=Ci,1pjzijGpjCi,1pj+Ci,2pjzijGpjCi,2pj
  整体的求雅可比过程和十四讲一致,都是利用链式法则,误差量先对相机系下点 C P ( x , y , z ) ^CP(x,y,z) CP(x,y,z)求导,然后点 C P ( x , y , z ) ^CP(x,y,z) CP(x,y,z)再分别对位姿和地图点求导。

  求雅可比的关键就是:①搞清楚误差量是什么 ②搞清楚优化变量是什么(一般是位姿和路标点)

  还有一个值得注意的点就是,这里平移量的不同表达,会使得雅可比的结果略有不同,如下所示(l表示做相机)
C P = R l w ( P w − t w l ) = R l w P w + t l w ^CP = R_{lw}(P_{w} - t_{wl}) = R_{lw}P_{w} + t_{lw} CP=Rlw(Pwtwl)=RlwPw+tlw
  后面那种表达就是十四讲上经常使用的方法,推导对于位姿和地图点的导数也和上面一致。但是MSCKF中使用的是前面一种表达,两种表达本质上一样,但对平移量的表示不一致,所以会导致对于位姿中平移量的雅可比会有差异(很明显,点 C P ^CP CP分别对两个平移量求导,一个是 R l w R_{lw} Rlw,一个是单位矩阵 I I I)。

  当然还有一点,就是MSCKF中都是jpl四元数,所有的李代数扰动都是负的。

/**
 * @brief 计算一个路标点的雅可比
 * @param  feature_id 路标点id
 * @param  cam_state_ids 这个点对应的所有的相机状态id
 * @param  H_x 雅可比
 * @param  r 误差
 */
void MsckfVio::featureJacobian(
    const FeatureIDType &feature_id,
    const std::vector<StateIDType> &cam_state_ids,
    MatrixXd &H_x, VectorXd &r)
{
    // 取出特征
    const auto &feature = map_server[feature_id];

    // Check how many camera states in the provided camera
    // id camera has actually seen this feature.
    // 1. 统计有效观测的相机状态,因为对应的个别状态有可能被滑走了
    vector<StateIDType> valid_cam_state_ids(0);
    for (const auto &cam_id : cam_state_ids)
    {
        if (feature.observations.find(cam_id) ==
            feature.observations.end())
            continue;

        valid_cam_state_ids.push_back(cam_id);
    }

    int jacobian_row_size = 0;
    // 行数等于4*观测数量,一个观测在双目上都有,所以是2*2
    // 此时还没有0空间投影
    jacobian_row_size = 4 * valid_cam_state_ids.size();

    // 误差相对于状态量的雅可比,没有约束列数,因为列数一直是最新的
    // 21+6N---21是imu状态、外参、N表示了多少个
    MatrixXd H_xj = MatrixXd::Zero(jacobian_row_size,
                                    21 + state_server.cam_states.size() * 6);
    // 误差相对于三维点的雅可比
    MatrixXd H_fj = MatrixXd::Zero(jacobian_row_size, 3);
    // 误差
    VectorXd r_j = VectorXd::Zero(jacobian_row_size);
    int stack_cntr = 0;

    // 2. 计算每一个观测(同一帧左右目这里被叫成一个观测)的雅可比与误差
    for (const auto &cam_id : valid_cam_state_ids)
    {
		// 一个观测对位姿雅可比-4*6  对地图点的雅可比--4*3
        Matrix<double, 4, 6> H_xi = Matrix<double, 4, 6>::Zero();
        Matrix<double, 4, 3> H_fi = Matrix<double, 4, 3>::Zero();
        Vector4d r_i = Vector4d::Zero();
        // 2.1 计算一个左右目观测的雅可比
        measurementJacobian(cam_id, feature.id, H_xi, H_fi, r_i);

        // 计算这个cam_id在整个矩阵的列数,因为要在大矩阵里面放
        auto cam_state_iter = state_server.cam_states.find(cam_id);
        int cam_state_cntr = std::distance(
            state_server.cam_states.begin(), cam_state_iter);

        // Stack the Jacobians.
        H_xj.block<4, 6>(stack_cntr, 21 + 6 * cam_state_cntr) = H_xi;
        H_fj.block<4, 3>(stack_cntr, 0) = H_fi;
        r_j.segment<4>(stack_cntr) = r_i;
        stack_cntr += 4;
    }


② measurementJacobian()

误差量对相机系下点的导数,以左相机 C i , 1 C_{i,1} Ci,1为例

∂ z i j ∂ C i , 1 p j = 1 C i , 1 Z ^ j ( 1 0 − C i , 1 X ^ j C i , 1 Z ^ j 0 1 − C i , 1 Y ^ j C i , 1 Z ^ j 0 0 0 0 0 0 ) \left.\frac{\partial\mathbf{z}_i^j}{\partial^{C_{i,1}}\mathbf{p}_j}=\frac{1}{C_{i,1}\hat{Z}_j}\left(\begin{array}{ccc}1&0&-\frac{C_{i,1}\hat{X}_j}{C_{i,1}\hat{Z}_j}\\0&1&-\frac{C_{i,1}\hat{Y}_j}{C_{i,1}\hat{Z}_j}\\0&0&0\\0&0&0\end{array}\right.\right) Ci,1pjzij=Ci,1Z^j1 10000100Ci,1Z^jCi,1X^jCi,1Z^jCi,1Y^j00

相机系点对位姿的雅可比

∂ C i , 1 p j ∂ x C i , 1 = ( ⌊ C i , 1 p ^ j × ⌋ − C ( C i , 1 G q ^ ) ) \left.\left.\frac{\partial^{C_{i,1}}\mathbf{p}_j}{\partial\mathbf{x}_{C_{i,1}}}=\left(\left.\lfloor C_{i,1}\hat{\mathbf{p}}_{j\times}\rfloor\quad-C\left(\begin{matrix}C_{i,1}\\G\end{matrix}\right.\right.\right.\hat{\mathbf{q}}\right)\right) xCi,1Ci,1pj=(Ci,1p^j×C(Ci,1Gq^))

相机系点对地图点的雅可比

∂ C i , 1 p j ∂ G p j = C ( C i , 1 G q ^ ) \frac{\partial^{C_{i,1}}\mathbf{p}_j}{\partial^G\mathbf{p}_j}=\left.C\left(\begin{matrix}C_i,1\\G\end{matrix}\right.\mathbf{\hat{q}}\right) GpjCi,1pj=C(Ci,1Gq^)

<1> 取出观测z和位姿Rwc、路标点p_w
/**
 * @brief 计算一个路标点的雅可比
 * @param  cam_state_id 有效的相机状态id
 * @param  feature_id 路标点id
 * @param  H_x 误差相对于位姿的雅可比
 * @param  H_f 误差相对于三维点的雅可比
 * @param  r 误差
 */
void MsckfVio::measurementJacobian(
    const StateIDType &cam_state_id,
    const FeatureIDType &feature_id,
    Matrix<double, 4, 6> &H_x, Matrix<double, 4, 3> &H_f, Vector4d &r)
{

    // 1. 取出相机状态与特征
    const CAMState &cam_state = state_server.cam_states[cam_state_id];
    const Feature &feature = map_server[feature_id];

    // 2. 取出左目位姿,根据外参计算右目位姿
    // Cam0 pose.	Rc0w
    Matrix3d R_w_c0 = quaternionToRotation(cam_state.orientation);
    const Vector3d &t_c0_w = cam_state.position;	// twc0

    // Cam1 pose.	
    Matrix3d R_c0_c1 = CAMState::T_cam0_cam1.linear();
     // Rc1w
    Matrix3d R_w_c1 = CAMState::T_cam0_cam1.linear() * R_w_c0;
    Vector3d t_c1_w = t_c0_w - R_w_c1.transpose() * CAMState::T_cam0_cam1.translation();	// twc1= twc0-Rwc1*tc1c0

    // 3. 取出三维点坐标--地图点与观测值z(前端发来的是归一化坐标)
    const Vector3d &p_w = feature.position;	// 这个特征对于世界系地图点坐标
    const Vector4d &z = feature.observations.find(cam_state_id)->second;	// 这个特征在当前观测帧下的归一化坐标-----观测值
<2> 计算雅可比H
    // 4. 转到左右目相机坐标系下
    // Convert the feature position from the world frame to
    // the cam0 and cam1 frame.
    Vector3d p_c0 = R_w_c0 * (p_w - t_c0_w);
    Vector3d p_c1 = R_w_c1 * (p_w - t_c1_w);
    // p_c1 = R_c0_c1 * R_w_c0 * (p_w - t_c0_w + R_w_c1.transpose() * t_cam0_cam1)
    //      = R_c0_c1 * (p_c0 + R_w_c0 * R_w_c1.transpose() * t_cam0_cam1)
    //      = R_c0_c1 * (p_c0 + R_c0_c1 * t_cam0_cam1)


    // 5. 计算雅可比
    // 左相机归一化坐标点(误差量)相对于左相机坐标系下的点的雅可比
    // (x, y) = (X / Z, Y / Z)
    Matrix<double, 4, 3> dz_dpc0 = Matrix<double, 4, 3>::Zero();
    dz_dpc0(0, 0) = 1 / p_c0(2);
    dz_dpc0(1, 1) = 1 / p_c0(2);
    dz_dpc0(0, 2) = -p_c0(0) / (p_c0(2) * p_c0(2));
    dz_dpc0(1, 2) = -p_c0(1) / (p_c0(2) * p_c0(2));

    // 与上同理
    Matrix<double, 4, 3> dz_dpc1 = Matrix<double, 4, 3>::Zero();
    dz_dpc1(2, 0) = 1 / p_c1(2);
    dz_dpc1(3, 1) = 1 / p_c1(2);
    dz_dpc1(2, 2) = -p_c1(0) / (p_c1(2) * p_c1(2));
    dz_dpc1(3, 2) = -p_c1(1) / (p_c1(2) * p_c1(2));

    // 左相机坐标系下的三维点相对于左相机位姿的雅可比 先r后t
    Matrix<double, 3, 6> dpc0_dxc = Matrix<double, 3, 6>::Zero();
    dpc0_dxc.leftCols(3) = skewSymmetric(p_c0);
    dpc0_dxc.rightCols(3) = -R_w_c0;

    // 右相机坐标系下的三维点相对于左相机位姿的雅可比 先r后t
    Matrix<double, 3, 6> dpc1_dxc = Matrix<double, 3, 6>::Zero();
    dpc1_dxc.leftCols(3) = R_c0_c1 * skewSymmetric(p_c0);
    dpc1_dxc.rightCols(3) = -R_w_c1;

    // Vector3d p_c0 = R_w_c0 * (p_w - t_c0_w);
    // Vector3d p_c1 = R_w_c1 * (p_w - t_c1_w);
    // 对地图点的雅可比
    // p_c0 对 p_w
    Matrix3d dpc0_dpg = R_w_c0;
    // p_c1 对 p_w
    Matrix3d dpc1_dpg = R_w_c1;

    // 两个雅可比
    H_x = dz_dpc0 * dpc0_dxc + dz_dpc1 * dpc1_dxc;
    H_f = dz_dpc0 * dpc0_dpg + dz_dpc1 * dpc1_dpg;
<3> 对H矩阵的可观测性约束

  H矩阵就是误差状态的雅可比矩阵。

  下面代码u就是对于了这个大矩阵(右),A就是对位姿的雅可比矩阵(左)。约束限制就是 A u = 0 = w Au=0=w Au=0=w。所以下面公式里的w其实是0.
H c a m [ H θ G H p l ] [ C ( q ^ G , k ∣ k − 1 ) G g ( ⌊ G f ^ k ∣ k − 1 × ⌋ − ⌊ G p ^ I , k ∣ k − 1 × ⌋ ) G g ] = 0. \mathbf{H}_{cam}\begin{bmatrix}\mathbf{H}_{\boldsymbol{\theta}_G}&\mathbf{H}_{\mathbf{p}_l}\end{bmatrix}\begin{bmatrix}\mathbf{C}\left(\hat{\boldsymbol{q}}_{G,k|k-1}\right)^G\mathbf{g}\\\left(\left\lfloor G\hat{\mathbf{f}}_{k|k-1}\times\right\rfloor-\left\lfloor{}^G\hat{\mathbf{p}}_{I,k|k-1}\times\right\rfloor\right)^G\mathbf{g}\end{bmatrix}=\mathbf{0}. Hcam[HθGHpl] C(q^G,kk1)Gg(Gf^kk1×Gp^I,kk1×)Gg =0.

H c a m \mathbf{H}_{cam} Hcam对应代码dz_dpc,即残差对相机系点 C P ^CP CP

H θ G \mathbf{H}_{\theta_G} HθG对应代码dpc_dxc,即相机系点 C P ^CP CP对旋转的雅可比

H p l \mathbf{H}_{\mathbf{p}_l} Hpl对应代码dpc_dxc,即相机系点 C P ^CP CP对平移的雅可比

H f \mathbf{H}_{\mathbf{f}} Hf对应代码dpc_dpg,即相机系点 C P ^CP CP对路标点 W P ^WP WP的雅可比

A ∗ = A − ( A u − w ) ( u T u ) − 1 u T \mathbf{A}^*=\mathbf{A}-(\mathbf{A}\mathbf{u}-\mathbf{w})\left(\mathbf{u}^T\mathbf{u}\right)^{-1}\mathbf{u}^T A=A(Auw)(uTu)1uT

解决来之后,根据下面公式依次对应新的雅可比。代码中是双目,所以行维都是4.

H c a m H θ G = A 1 : 2 , 1 : 3 ′ , H c a m H p l = A 1 : 2 , 4 : 6 ′ , H c a m H f = − A 1 : 2 , 4 : 6 ′ \mathbf{H}_{cam}\mathbf{H}_{\theta_G}=\mathbf{A}_{1:2,1:3}^{\prime},\mathbf{H}_{cam}\mathbf{H}_{\mathbf{p}_l}=\mathbf{A}_{1:2,4:6}^{\prime},\mathbf{H}_{cam}\mathbf{H}_{\mathbf{f}}=-\mathbf{A}_{1:2,4:6}^{\prime} HcamHθG=A1:2,1:3,HcamHpl=A1:2,4:6,HcamHf=A1:2,4:6

    // Modifty the measurement Jacobian to ensure
    // observability constrain.
    // 6. OC
    Matrix<double, 4, 6> A = H_x;
    Matrix<double, 6, 1> u = Matrix<double, 6, 1>::Zero();
    u.block<3, 1>(0, 0) = 
        quaternionToRotation(cam_state.orientation_null) * IMUState::gravity;
    u.block<3, 1>(3, 0) =
        skewSymmetric(p_w - cam_state.position_null) * IMUState::gravity;
    H_x = A - A * u * (u.transpose() * u).inverse() * u.transpose();
    H_f = -H_x.block<4, 3>(0, 3);//4*3大小,从0行3列开始取,对应公式
<4> 计算残差r

  残差 = 观测 - 预测

  对于观测,就是视觉中经过匹配得到的结果。

  对于预测,就是利用imu状态估计出来的cam状态进行预测的。

    // 7. 计算归一化平面坐标误差 = 观测 - 预测
    r = z - Vector4d(p_c0(0) / p_c0(2), p_c0(1) / p_c0(2),
                        p_c1(0) / p_c1(2), p_c1(1) / p_c1(2));

③ 左零空间投影

  消除路标点带来的不确定性
r j = H x j x ~ + H f j   G p ~ j + n j \mathbf{r}^{j}=\mathbf{H}_{\mathrm{x}}^{j}\tilde{\mathbf{x}}+\mathbf{H}_{f}^{j~G}\tilde{\mathbf{p}}_{j}+\mathbf{n}^{j} rj=Hxjx~+Hfj Gp~j+nj
  左零空间投影
r o j = V ⊤ r j = V ⊤ H x j x ~ + V ⊤ n j = H x , o j x ~ + n o j \mathbf{r}_o^j=\mathbf{V}^\top\mathbf{r}^j=\mathbf{V}^\top\mathbf{H}_\mathrm{x}^j\tilde{\mathbf{x}}+\mathbf{V}^\top\mathbf{n}^j=\mathbf{H}_{\mathrm{x},o}^j\tilde{\mathbf{x}}+\mathbf{n}_o^j roj=Vrj=VHxjx~+Vnj=Hx,ojx~+noj

注意,jacobian_row_size - 3,为什么是减三,因为对特征点列雅可比的维度是3。理论分析哪里用QR分解解释了,把Q分为Q1和Q2,选择右边的Q2,R是jacobian_row_size*3上三角

但是原作者代码实现用的是SVD分解,得到结果和QR分解结果一致!

    // Project the residual and Jacobians onto the nullspace
    // of H_fj.
    // 零空间投影
    JacobiSVD<MatrixXd> svd_helper(H_fj, ComputeFullU | ComputeThinV);
    MatrixXd A = svd_helper.matrixU().rightCols(
        jacobian_row_size - 3);

    // 上面的效果跟QR分解一样,下面的代码可以测试打印对比
    // Eigen::ColPivHouseholderQR<MatrixXd> qr(H_fj);
	// MatrixXd Q = qr.matrixQ();
    // std::cout << "spqr_helper.matrixQ(): " << std::endl << Q << std::endl << std::endl;
    // std::cout << "A: " << std::endl << A << std::endl;

    // 0空间投影
    H_x = A.transpose() * H_xj;
    r = A.transpose() * r_j;

    return;
}

4.5.3 状态更新

  零空间投影后的H矩阵和残差r
r o j = V ⊤ r j = V ⊤ H x j x ~ + V ⊤ n j = H x , o j x ~ + n o j \mathbf{r}_o^j=\mathbf{V}^\top\mathbf{r}^j=\mathbf{V}^\top\mathbf{H}_\mathrm{x}^j\tilde{\mathbf{x}}+\mathbf{V}^\top\mathbf{n}^j=\mathbf{H}_{\mathrm{x},o}^j\tilde{\mathbf{x}}+\mathbf{n}_o^j roj=Vrj=VHxjx~+Vnj=Hx,ojx~+noj

   // resize成实际大小
    H_x.conservativeResize(stack_cntr, H_x.cols());
    r.conservativeResize(stack_cntr);

    // 7. 使用误差及雅可比更新状态
    measurementUpdate(H_x, r);

  在理论部分中,为了降低计算量,利用QR分解的特点(行维>>列维,这部分可以参考QR分解推导线性方程那部分),对H矩阵进行降维,详见后端理论推导。

① 降维

  这段代码通过QR分解,将输入矩阵 H 投影到QR分解的Q的转置上,然后提取其中的前几行,从而实现对矩阵 H 的降维。这种处理通常用于稀疏矩阵,可以减少计算量。

留待后续

/**
 * @brief 更新
 * @param  H 雅可比
 * @param  r 误差
 */
void MsckfVio::measurementUpdate(
    const MatrixXd &H, const VectorXd &r)
{

    if (H.rows() == 0 || r.rows() == 0)
        return;

    // Decompose the final Jacobian matrix to reduce computational
    // complexity as in Equation (28), (29).
    MatrixXd H_thin;
    VectorXd r_thin;

    if (H.rows() > H.cols())
    {
        // Convert H to a sparse matrix.
        SparseMatrix<double> H_sparse = H.sparseView();

        // Perform QR decompostion on H_sparse.
        // 利用H矩阵稀疏性,QR分解
        // 这段结合零空间投影一起理解,主要作用就是降低计算量
        SPQR<SparseMatrix<double>> spqr_helper;
        spqr_helper.setSPQROrdering(SPQR_ORDERING_NATURAL);
        spqr_helper.compute(H_sparse);

        MatrixXd H_temp;
        VectorXd r_temp;
        (spqr_helper.matrixQ().transpose() * H).evalTo(H_temp);
        (spqr_helper.matrixQ().transpose() * r).evalTo(r_temp);

        H_thin = H_temp.topRows(21 + state_server.cam_states.size() * 6);
        r_thin = r_temp.head(21 + state_server.cam_states.size() * 6);

        // HouseholderQR<MatrixXd> qr_helper(H);
        // MatrixXd Q = qr_helper.householderQ();
        // MatrixXd Q1 = Q.leftCols(21+state_server.cam_states.size()*6);

        // H_thin = Q1.transpose() * H;
        // r_thin = Q1.transpose() * r;
    }
    else
    {
        H_thin = H;
        r_thin = r;
    }
② 卡尔曼滤波更新

  最关键的卡尔曼滤波更新

  r即计算的残差,对于代码delta_x
Δ X = K r n \Delta\mathbf{X}=\mathbf{Kr}_n ΔX=Krn

计算卡尔曼增益,进而计算误差状态的更新过程,V是观测方程的高斯噪声。

K = P pred H ⊤ ( H P pred H ⊤ + V ) − 1 , δ x = K ( z − h ( x p r e d ) ) = K r , x = x p r e d + δ x , P = ( I − K H ) P p r e d . \begin{aligned} &\boldsymbol{K}&& =P_\text{pred}H^\top(HP_\text{pred}H^\top+V)^{-1}, \\ &\delta x&& =K(z-h(\boldsymbol{x_\mathrm{pred}})) = Kr, \\ &\boldsymbol{x}&& =x_{\mathrm{pred}}+\delta\boldsymbol{x}, \\ &\text{P}&& =(\boldsymbol{I}-\boldsymbol{K}\boldsymbol{H})\boldsymbol{P}_{\mathrm{pred}}. \end{aligned} KδxxP=PpredH(HPpredH+V)1,=K(zh(xpred))=Kr,=xpred+δx,=(IKH)Ppred.

    // 2. 标准的卡尔曼计算过程
    // Compute the Kalman gain.
    const MatrixXd &P = state_server.state_cov;
    MatrixXd S = H_thin * P * H_thin.transpose() +
                    Feature::observation_noise * MatrixXd::Identity(
                                                    H_thin.rows(), H_thin.rows());
    // MatrixXd K_transpose = S.fullPivHouseholderQr().solve(H_thin*P);
    MatrixXd K_transpose = S.ldlt().solve(H_thin * P);
    MatrixXd K = K_transpose.transpose();

    // Compute the error of the state.
    VectorXd delta_x = K * r_thin;	// δx

    // Update the IMU state.
    const VectorXd &delta_x_imu = delta_x.head<21>();

    if ( // delta_x_imu.segment<3>(0).norm() > 0.15 ||
            // delta_x_imu.segment<3>(3).norm() > 0.15 ||
        delta_x_imu.segment<3>(6).norm() > 0.5 ||
        // delta_x_imu.segment<3>(9).norm() > 0.5 ||
        delta_x_imu.segment<3>(12).norm() > 1.0)
    {
        printf("delta velocity: %f\n", delta_x_imu.segment<3>(6).norm());
        printf("delta position: %f\n", delta_x_imu.segment<3>(12).norm());
        ROS_WARN("Update change is too large.");
        // return;
    }

    // 3. 更新到imu状态量
    const Vector4d dq_imu =
        smallAngleQuaternion(delta_x_imu.head<3>());
    // 相当于左乘dq_imu
    state_server.imu_state.orientation = quaternionMultiplication(
        dq_imu, state_server.imu_state.orientation);
    state_server.imu_state.gyro_bias += delta_x_imu.segment<3>(3);
    state_server.imu_state.velocity += delta_x_imu.segment<3>(6);
    state_server.imu_state.acc_bias += delta_x_imu.segment<3>(9);
    state_server.imu_state.position += delta_x_imu.segment<3>(12);

    // 外参
    const Vector4d dq_extrinsic =
        smallAngleQuaternion(delta_x_imu.segment<3>(15));
    state_server.imu_state.R_imu_cam0 =
        quaternionToRotation(dq_extrinsic) * state_server.imu_state.R_imu_cam0;
    state_server.imu_state.t_cam0_imu += delta_x_imu.segment<3>(18);

    // Update the camera states.
    // 更新相机姿态
    auto cam_state_iter = state_server.cam_states.begin();
    for (int i = 0; i < state_server.cam_states.size(); ++i, ++cam_state_iter)
    {
        const VectorXd &delta_x_cam = delta_x.segment<6>(21 + i * 6);
        const Vector4d dq_cam = smallAngleQuaternion(delta_x_cam.head<3>());
        cam_state_iter->second.orientation = quaternionMultiplication(
            dq_cam, cam_state_iter->second.orientation);
        cam_state_iter->second.position += delta_x_cam.tail<3>();
    }

    // Update state covariance.
    // 4. 更新协方差
    MatrixXd I_KH = MatrixXd::Identity(K.rows(), H_thin.cols()) - K * H_thin;
    // state_server.state_cov = I_KH*state_server.state_cov*I_KH.transpose() +
    //   K*K.transpose()*Feature::observation_noise;
    state_server.state_cov = I_KH * state_server.state_cov;

    // Fix the covariance to be symmetric
    MatrixXd state_cov_fixed = (state_server.state_cov +
                                state_server.state_cov.transpose()) /
                                2.0;
    state_server.state_cov = state_cov_fixed;

    return;
}

4.6 pruneCamStateBuffer

/**
 * @brief 当cam状态数达到最大值时,挑出若干cam状态待删除
 */
void MsckfVio::pruneCamStateBuffer()
{
    // 数量还不到该删的程度,配置文件里面是20个
    if (state_server.cam_states.size() < max_cam_state_size)
        return;

    // Find two camera states to be removed.
    // 1. 找出该删的相机状态的id,两个
    vector<StateIDType> rm_cam_state_ids(0);
    findRedundantCamStates(rm_cam_state_ids);

    // Find the size of the Jacobian matrix.
    // 2. 找到删减帧涉及的观测雅可比大小
    int jacobian_row_size = 0;
    for (auto &item : map_server)
    {
        auto &feature = item.second;
        // Check how many camera states to be removed are associated
        // with this feature.
        // 2.1 在待删去的帧中统计能观测到这个特征的帧
        vector<StateIDType> involved_cam_state_ids(0);
        for (const auto &cam_id : rm_cam_state_ids)
        {
            if (feature.observations.find(cam_id) !=
                feature.observations.end())
                involved_cam_state_ids.push_back(cam_id);
        }

        if (involved_cam_state_ids.size() == 0)
            continue;
        // 2.2 这个点只在一个里面有观测那就直接删
        // 只用一个观测更新不了状态
        if (involved_cam_state_ids.size() == 1)
        {
            feature.observations.erase(involved_cam_state_ids[0]);
            continue;
        }
        // 程序到这里的时候说明找到了一个特征,先不说他一共被几帧观测到
        // 到这里说明被两帧或两帧以上待删减的帧观测到
        // 2.3 如果没有做过三角化,做一下三角化,如果失败直接删
        if (!feature.is_initialized)
        {
            // Check if the feature can be initialize.
            if (!feature.checkMotion(state_server.cam_states))
            {
                // If the feature cannot be initialized, just remove
                // the observations associated with the camera states
                // to be removed.
                for (const auto &cam_id : involved_cam_state_ids)
                    feature.observations.erase(cam_id);
                continue;
            }
            else
            {
                if (!feature.initializePosition(state_server.cam_states))
                {
                    for (const auto &cam_id : involved_cam_state_ids)
                        feature.observations.erase(cam_id);
                    continue;
                }
            }
        }

        // 2.4 最后的最后得出了行数
        // 意味着有involved_cam_state_ids.size() 数量的观测要被删去
        // 但是因为待删去的帧间有共同观测的关系,直接删会损失这部分信息
        // 所以临删前做最后一次更新
        jacobian_row_size += 4 * involved_cam_state_ids.size() - 3;
    }

    // cout << "jacobian row #: " << jacobian_row_size << endl;

    // Compute the Jacobian and residual.
    // 3. 计算待删掉的这部分观测的雅可比与误差
    // 预设大小
    MatrixXd H_x = MatrixXd::Zero(jacobian_row_size,
                                    21 + 6 * state_server.cam_states.size());
    VectorXd r = VectorXd::Zero(jacobian_row_size);
    int stack_cntr = 0;

    // 又做了一遍类似上面的遍历,只不过该三角化的已经三角化,该删的已经删了
    for (auto &item : map_server)
    {
        auto &feature = item.second;
        // Check how many camera states to be removed are associated
        // with this feature.
        // 这段就是判断一下这个点是否都在待删除帧中有观测
        vector<StateIDType> involved_cam_state_ids(0);
        for (const auto &cam_id : rm_cam_state_ids)
        {
            if (feature.observations.find(cam_id) !=
                feature.observations.end())
                involved_cam_state_ids.push_back(cam_id);
        }

        // 一个的情况已经被删掉了
        if (involved_cam_state_ids.size() == 0)
            continue;

        // 计算出待删去的这部分的雅可比
        // 这个点假如有多个观测,但本次只用待删除帧上的观测
        MatrixXd H_xj;
        VectorXd r_j;
        featureJacobian(feature.id, involved_cam_state_ids, H_xj, r_j);

        if (gatingTest(H_xj, r_j, involved_cam_state_ids.size()))
        {
            H_x.block(stack_cntr, 0, H_xj.rows(), H_xj.cols()) = H_xj;
            r.segment(stack_cntr, r_j.rows()) = r_j;
            stack_cntr += H_xj.rows();
        }

        // 删去观测
        for (const auto &cam_id : involved_cam_state_ids)
            feature.observations.erase(cam_id);
    }

    H_x.conservativeResize(stack_cntr, H_x.cols());
    r.conservativeResize(stack_cntr);

    // Perform measurement update.
    // 4. 用待删去的这些观测更新一下
    measurementUpdate(H_x, r);

    // 5. 直接删掉对应的行列,直接干掉
    // 为啥没有做类似于边缘化的操作?
    // 个人认为是上面做最后的更新了,所以信息已经更新到了各个地方
    for (const auto &cam_id : rm_cam_state_ids)
    {
        int cam_sequence = std::distance(
            state_server.cam_states.begin(), state_server.cam_states.find(cam_id));
        int cam_state_start = 21 + 6 * cam_sequence;
        int cam_state_end = cam_state_start + 6;

        // Remove the corresponding rows and columns in the state
        // covariance matrix.
        if (cam_state_end < state_server.state_cov.rows())
        {
            state_server.state_cov.block(cam_state_start, 0,
                                            state_server.state_cov.rows() - cam_state_end,
                                            state_server.state_cov.cols()) =
                state_server.state_cov.block(cam_state_end, 0,
                                                state_server.state_cov.rows() - cam_state_end,
                                                state_server.state_cov.cols());

            state_server.state_cov.block(0, cam_state_start,
                                            state_server.state_cov.rows(),
                                            state_server.state_cov.cols() - cam_state_end) =
                state_server.state_cov.block(0, cam_state_end,
                                                state_server.state_cov.rows(),
                                                state_server.state_cov.cols() - cam_state_end);

            state_server.state_cov.conservativeResize(
                state_server.state_cov.rows() - 6, state_server.state_cov.cols() - 6);
        }
        else
        {
            state_server.state_cov.conservativeResize(
                state_server.state_cov.rows() - 6, state_server.state_cov.cols() - 6);
        }

        // Remove this camera state in the state vector.
        state_server.cam_states.erase(cam_id);
    }

    return;
}

4.7 发布位姿publish

void MsckfVio::publish(const ros::Time &time)
{

    // Convert the IMU frame to the body frame.
    // 1. 计算body坐标,因为imu与body相对位姿是单位矩阵,所以就是imu的坐标
    const IMUState &imu_state = state_server.imu_state;
    Eigen::Isometry3d T_i_w = Eigen::Isometry3d::Identity();
    T_i_w.linear() = quaternionToRotation(
                            imu_state.orientation)
                            .transpose();
    T_i_w.translation() = imu_state.position;

    Eigen::Isometry3d T_b_w = IMUState::T_imu_body * T_i_w *
                                IMUState::T_imu_body.inverse();
    Eigen::Vector3d body_velocity =
        IMUState::T_imu_body.linear() * imu_state.velocity;

    // Publish tf
    // 2. 发布tf,实时的位姿,没有轨迹,没有协方差
    if (publish_tf)
    {
        tf::Transform T_b_w_tf;
        tf::transformEigenToTF(T_b_w, T_b_w_tf);
        tf_pub.sendTransform(tf::StampedTransform(
            T_b_w_tf, time, fixed_frame_id, child_frame_id));
    }

    // Publish the odometry
    // 3. 发布位姿,能在rviz留下轨迹的
    nav_msgs::Odometry odom_msg;
    odom_msg.header.stamp = time;
    odom_msg.header.frame_id = fixed_frame_id;
    odom_msg.child_frame_id = child_frame_id;

    tf::poseEigenToMsg(T_b_w, odom_msg.pose.pose);
    tf::vectorEigenToMsg(body_velocity, odom_msg.twist.twist.linear);

    // Convert the covariance.
    // 协方差,取出旋转平移部分,以及它们之间的公共部分组成6自由度的协方差
    Matrix3d P_oo = state_server.state_cov.block<3, 3>(0, 0);
    Matrix3d P_op = state_server.state_cov.block<3, 3>(0, 12);
    Matrix3d P_po = state_server.state_cov.block<3, 3>(12, 0);
    Matrix3d P_pp = state_server.state_cov.block<3, 3>(12, 12);
    Matrix<double, 6, 6> P_imu_pose = Matrix<double, 6, 6>::Zero();
    P_imu_pose << P_pp, P_po, P_op, P_oo;

    // 转下坐标,但是这里都是单位矩阵
    Matrix<double, 6, 6> H_pose = Matrix<double, 6, 6>::Zero();
    H_pose.block<3, 3>(0, 0) = IMUState::T_imu_body.linear();
    H_pose.block<3, 3>(3, 3) = IMUState::T_imu_body.linear();
    Matrix<double, 6, 6> P_body_pose = H_pose *
                                        P_imu_pose * H_pose.transpose();

    // 填充协方差
    for (int i = 0; i < 6; ++i)
        for (int j = 0; j < 6; ++j)
            odom_msg.pose.covariance[6 * i + j] = P_body_pose(i, j);

    // Construct the covariance for the velocity.
    // 速度协方差
    Matrix3d P_imu_vel = state_server.state_cov.block<3, 3>(6, 6);
    Matrix3d H_vel = IMUState::T_imu_body.linear();
    Matrix3d P_body_vel = H_vel * P_imu_vel * H_vel.transpose();
    for (int i = 0; i < 3; ++i)
        for (int j = 0; j < 3; ++j)
            odom_msg.twist.covariance[i * 6 + j] = P_body_vel(i, j);

    // 发布位姿
    odom_pub.publish(odom_msg);

    // Publish the 3D positions of the features that
    // has been initialized.
    // 4. 发布点云
    boost::shared_ptr<pcl::PointCloud<pcl::PointXYZ>> feature_msg_ptr(
        new pcl::PointCloud<pcl::PointXYZ>());
    feature_msg_ptr->header.frame_id = fixed_frame_id;
    feature_msg_ptr->height = 1;
    for (const auto &item : map_server)
    {
        const auto &feature = item.second;
        if (feature.is_initialized)
        {
            Vector3d feature_position =
                IMUState::T_imu_body.linear() * feature.position;
            feature_msg_ptr->points.push_back(pcl::PointXYZ(
                feature_position(0), feature_position(1), feature_position(2)));
        }
    }
    feature_msg_ptr->width = feature_msg_ptr->points.size();

    feature_pub.publish(feature_msg_ptr);

    return;
}

4.8 是否重置系统onlineReset

void MsckfVio::onlineReset()
{

    // Never perform online reset if position std threshold
    // is non-positive.
    if (position_std_threshold <= 0)
        return;
    static long long int online_reset_counter = 0;

    // Check the uncertainty of positions to determine if
    // the system can be reset.
    double position_x_std = std::sqrt(state_server.state_cov(12, 12));
    double position_y_std = std::sqrt(state_server.state_cov(13, 13));
    double position_z_std = std::sqrt(state_server.state_cov(14, 14));

    if (position_x_std < position_std_threshold &&
        position_y_std < position_std_threshold &&
        position_z_std < position_std_threshold)
        return;

    ROS_WARN("Start %lld online reset procedure...",
                ++online_reset_counter);
    ROS_INFO("Stardard deviation in xyz: %f, %f, %f",
                position_x_std, position_y_std, position_z_std);

    // Remove all existing camera states.
    state_server.cam_states.clear();

    // Clear all exsiting features in the map.
    map_server.clear();

    // Reset the state covariance.
    double gyro_bias_cov, acc_bias_cov, velocity_cov;
    nh.param<double>("initial_covariance/velocity",
                        velocity_cov, 0.25);
    nh.param<double>("initial_covariance/gyro_bias",
                        gyro_bias_cov, 1e-4);
    nh.param<double>("initial_covariance/acc_bias",
                        acc_bias_cov, 1e-2);

    double extrinsic_rotation_cov, extrinsic_translation_cov;
    nh.param<double>("initial_covariance/extrinsic_rotation_cov",
                        extrinsic_rotation_cov, 3.0462e-4);
    nh.param<double>("initial_covariance/extrinsic_translation_cov",
                        extrinsic_translation_cov, 1e-4);

    state_server.state_cov = MatrixXd::Zero(21, 21);
    for (int i = 3; i < 6; ++i)
        state_server.state_cov(i, i) = gyro_bias_cov;
    for (int i = 6; i < 9; ++i)
        state_server.state_cov(i, i) = velocity_cov;
    for (int i = 9; i < 12; ++i)
        state_server.state_cov(i, i) = acc_bias_cov;
    for (int i = 15; i < 18; ++i)
        state_server.state_cov(i, i) = extrinsic_rotation_cov;
    for (int i = 18; i < 21; ++i)
        state_server.state_cov(i, i) = extrinsic_translation_cov;

    ROS_WARN("%lld online reset complete...", online_reset_counter);
    return;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/428657.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

其他组件分析

对于数据大屏&#xff0c;基于网络安全大赛来看的话主要有 团队队伍的分析界面&#xff1a;分为两类&#xff1a; 一类是对队伍的解题所考虑的知识点画一个5星图&#xff0c;每个方面占一角&#xff0c;可以更直观看到队伍的擅长方面&#xff1a;&#xff08;这部分可能用不到…

EI论文部分复现:含VSC-HVDC的交直流系统内点法最优潮流计算Simulink模型!

适用平台&#xff1a;MatlabSimulink&#xff1b;复现内容&#xff1a;VSC-HVDC模型 简介 高压直流传输系统主要包括换流站、输电线路和终端设备&#xff0c;其中换流站起着关键作用&#xff0c;他可以实现交流整流和直流逆变。常见的HVDC系统有全桥式、半桥式和两水平VSC等。…

聊聊国内「类Sora模型」发展现状,和 Sora 的差距到底有多大?

2024 年 2 月 16 日。 就在谷歌发布他新一代的多模态大模型 Gemini 1.5 Pro 的同一天&#xff0c;OpenAI 带着新一代的文生视频模型 Sora 再次抓住了全世界人们的眼球。 “颠覆”、“炸裂”、“变天”、“疯狂”&#xff0c;类似的形容词一夜之间簇拥在 Sora 周围&#xff0c;…

计算机网络实验 基于ENSP的协议分析

实验二 基于eNSP的协议分析 一、实验目的&#xff1a; 1&#xff09;熟悉VRP的基本操作命令 2&#xff09;掌握ARP协议的基本工作原理 3&#xff09;掌握IP协议的基本工作原理 4&#xff09;掌握ICMP协议的基本工作原理 二、实验内容&#xff1a; 1、场景1&#xff1a;两台PC机…

【海贼王的数据航海:利用数据结构成为数据海洋的霸主】链表—双向链表

目录 往期 1 -> 带头双向循环链表(双链表) 1.1 -> 接口声明 1.2 -> 接口实现 1.2.1 -> 双向链表初始化 1.2.2 -> 动态申请一个结点 1.2.3 -> 双向链表销毁 1.2.4 -> 双向链表打印 1.2.5 -> 双向链表判空 1.2.6 -> 双向链表尾插 1.2.7 -&…

前端CSS常考问题总结

目录 CSS盒模型 CSS选择器的优先级 隐藏元素的方法 px和rem的区别是什么? 重绘重排有什么区别? 重排&#xff08;回流&#xff09;&#xff1a; 重绘&#xff1a; 浏览器的渲染机制: 浏览器如何解析CSS&#xff1f; 元素水平垂直居中的方式 CSS的哪些属性哪些可以…

VMwareWorkstation17.0虚拟机安装搭建PcDos2000虚拟机(完整图文详细步骤教程)

VMwareWorkstation17.0虚拟机安装搭建PcDos2000虚拟机&#xff08;完整图文详细步骤教程&#xff09; 一、PcDos20001.PcDos2000简介2.PcDos2000下载 二、创建PcDos2000虚拟机1.新建虚拟机2.类型配置3.类型配置4.选择版本5.命名、存位置6.磁盘容量7.调整虚拟配置7.1 调整虚拟配…

嵌入式学习 Day 29

函数: 1.函数的定义 2.函数的调用 3.函数的声明 1.函数传参: 1.赋值传递&#xff08;复制传递&#xff09; 函数体内部想要使用函数体外部变量值的时候使用复制传递 2.全局变量传递 3.地址传递 函数体内部想要修改函数体外部变量值的时候使用地址传递 函数…

Java多态性的作用及解析

多态性是 Java 面向对象编程的一个重要特性,它的主要作用包括以下几个方面: 提高代码的可扩展性:多态性使得我们可以在不修改现有代码的情况下,通过继承和重写方法来添加新的行为。这意味着我们可以在不影响现有功能的前提下,对代码进行扩展和修改。 增强代码的可读性:使…

STM32F103--基于正点原子的 FreeRTOS 移植(完整教程)附测试代码

前言 在看正点原子的FreeRTOS开发手册移植的时候&#xff0c;发现开发手册的描述并不全面&#xff0c;有几处遗漏。下面我展示出完整的教程&#xff0c;希望大家在学习的时候能够轻松点。 一、准备工作 1、正点原子的FreeRTOS官方资料 大家可自行到官方下载&#xff0c;或者在…

基于springboot+vue的健身房管理系统(前后端分离)

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、阿里云专家博主、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战&#xff0c;欢迎高校老师\讲师\同行交流合作 ​主要内容&#xff1a;毕业设计(Javaweb项目|小程序|Pyt…

FPGA之加法逻辑运算

由于FPGA需要被反复烧写&#xff0c;它实现组合逻辑的基本结构不可能像ASIC 那样通过固定的与非门来完成&#xff0c;而只能采用一种易于反复配置的结构。查找表可以很好地满足这一要求&#xff0c;目前主流FPGA都采用了基于SRAM 工艺的查找表结构。LUT本质上就是一个RAM。它把…

leetcode 热题 100_找到字符串中所有字母异位词

题解一&#xff1a; 滑动窗口&#xff1a;类似于字符串匹配&#xff0c;但匹配异位词需要包含相同的字母及个数&#xff0c;可以分别用两个数组存储字符串s滑动窗口和字符串p的字母及个数&#xff0c;再用Array.equals()进行比对。对于s.length()<p.length()的情况需要特判。…

【Linux】线程概念|线程理解|线程控制

文章目录 线程概念Linux中线程是否存在的讨论线程创建和线程控制线程的终止和等待&#xff08;三种终止方式 pthread_join()的void**retval&#xff09; 线程概念 线程就是进程内部的一个执行流&#xff0c;线程在进程内运行&#xff0c;线程在进程的地址空间内运行&#xff0…

Redis集群(主从)

1.主从集群 集群结构: 一.单机安装redis 1.上传压缩包并解压&#xff0c;编译 tar -xzf redis-6.2.4.tar.gz cd redis-6.2.4 make && make install 2.修改redis.config的配置并启动redis # 绑定地址&#xff0c;默认是127.0.0.1&#xff0c;会导致只能在本地访问。…

SpringBoot源码解读与原理分析(四十)基于jar/war包的运行机制

文章目录 前言第14章 运行SpringBoot应用14.1 部署打包的两种方式14.1.1 以可独立运行jar包的方式14.1.2 以war包的方式 14.2 基于jar包的独立运行机制14.2.1 可独立运行jar包的相关知识14.2.2 SpringBoot的可独立运行jar包结构14.2.3 JarLauncher的设计及工作原理14.2.3.1 Jar…

2核4G云服务器租用价格_2核4G云主机优惠价格_2024年报价

租用2核4G服务器费用价格&#xff0c;2核4G云服务器多少钱一年&#xff1f;1个月费用多少&#xff1f;阿里云2核4G服务器30元3个月、轻量应用服务器2核4G4M带宽165元一年、企业用户2核4G5M带宽199元一年&#xff1b;腾讯云轻量2核4G服务器5M带宽165元一年、252元15个月、540元三…

毕业生信息招聘平台|基于springboot+ Mysql+Java的毕业生信息招聘平台设计与实现(源码+数据库+文档+PPT)

目录 论文参考 摘 要 数据库设计 系统详细设计 文末获取源码联系 论文参考 摘 要 随着社会的发展&#xff0c;社会的各行各业都在利用信息化时代的优势。计算机的优势和普及使得各种信息系统的开发成为必需。 毕业生信息招聘平台&#xff0c;主要的模块包括查看管理员&a…

力扣经典题目解析--最小覆盖子串

原题地址: . - 力扣&#xff08;LeetCode&#xff09; 给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串&#xff0c;则返回空字符串 "" 。 注意&#xff1a; 对于 t 中重复字符&#xff0c;我们寻找…

Finetuning Large Language Models: Sharon Zhou

Finetuning Large Language Models 课程地址&#xff1a;https://www.deeplearning.ai/short-courses/finetuning-large-language-models/ 本文是学习笔记。 Goal&#xff1a; Learn the fundamentals of finetuning a large language model (LLM). Understand how finetu…