VINS-Mono+Fusion源码解析系列(十一):后端processIMU与processImage

1. IMU后端优化estimator.processIMU

​ 主要是维持滑窗中的预积分量pre_integrations,为后续的优化提高状态初值。

vins_estimator/src/estimator.cpp
/**
 * @brief 对imu数据进行处理,包括更新预积分量,和提供优化状态量的初始值
 * 
 * @param[in] dt 
 * @param[in] linear_acceleration 
 * @param[in] angular_velocity 
 */
void Estimator::processIMU(
                    double dt, 
                    const Vector3d &linear_acceleration, 
                    const Vector3d &angular_velocity)
{
   
    // 若为第一个imu数据,则直接将imu测量值(加速度linear_acceleration和角速度angular_velocity)
    // 赋给相应变量(acc_0和gyr_0)
    if (!first_imu)
    {
   
        first_imu = true;
        acc_0 = linear_acceleration;
        gyr_0 = angular_velocity;
    }

    // 判断滑窗中第frame_count帧的预积分量是否存在
    // 若不存在,则new一个IntegrationBase,然后在下面调用push_back计算预积分量
    if (!pre_integrations[frame_count])  
    {
   
        pre_integrations[frame_count] = new IntegrationBase{
   acc_0, gyr_0, 
                                            Bas[frame_count], Bgs[frame_count]};
    }
/*
  滑窗中保留11帧,frame_count表示现在处理第几帧,一般处理到第11帧时就保持不变了(一直表示最后一帧)
  在滑窗中维持11个预积分量,由于预积分是帧间约束,因此第1个预积分量实际上是用不到的,同时避免第一帧对应的imu数据量过大造成出错
*/
    if (frame_count != 0)
    {
   
        // 调用push_back, 计算预积分量以及更新雅可比与协方差矩阵, 便于以imu的频率发布出去
        pre_integrations[frame_count]->push_back(dt, linear_acceleration, 	
                                                     angular_velocity);
        // tmp_pre_integration中备份此时的预积分量,后面用来做初始化
        tmp_pre_integration->push_back(dt, linear_acceleration, angular_velocity);
        // 保存传感器数据
        dt_buf[frame_count].push_back(dt);
        linear_acceleration_buf[frame_count].push_back(linear_acceleration);  // 线加速度
        angular_velocity_buf[frame_count].push_back(angular_velocity);  // 角加速度
        // 又是一个中值积分,更新滑窗中状态量(Rs, Ps, Vs),本质是给滑窗中的非线性优化提供可信的初始值 
        int j = frame_count;         
        Vector3d un_acc_0 = Rs[j] * (acc_0 - Bas[j]) - g;
        Vector3d un_gyr = 0.5 * (gyr_0 + angular_velocity) - Bgs[j];
        Rs[j] *= Utility::deltaQ(un_gyr * dt).toRotationMatrix();
        Vector3d un_acc_1 = Rs[j] * (linear_acceleration - Bas[j]) - g;
        Vector3d un_acc = 0.5 * (un_acc_0 + un_acc_1);
        Ps[j] += dt * Vs[j] + 0.5 * dt * dt * un_acc;
        Vs[j] += dt * un_acc;
    }
    acc_0 = linear_acceleration;
    gyr_0 = angular_velocity;
}

2. 图像帧后端优化estimator.processImage

2.1 VINS中特征点的管理方法

​ 如下图所示,对于当前帧的所有特征点,使用list<FeaturPerId>存储它们的id。对于每一个特征点,它具有的属性包括:

  • 该特征点的id
  • 第一个观测到该特征点的图像帧id(起始帧id)
  • 该特征点在起始帧下可恢复的3D点的深度
  • 该特征点在起始帧中的求解状态(是否可成功三角化恢复出3D点)
  • 所有观测到该特征点的图像帧属性vector<FeaturePerFrame>,具体包括如下:
    • 该特征点在被观测帧中的像素坐标
    • 该特征点在被观测帧中的归一化相机坐标
    • 该特征点在被观测帧中的速度

特征点管理

2.2 processImage的实现

(1)大致流程

  • 通过addFeatureCheckParallax进行关键帧的判断,从而确定边缘化的标志marginalization_flag。如果上一帧(倒数第2帧)是关键帧,则滑窗中最老的帧就要被移出滑窗;否则移出上一帧(倒数第2帧)。
  • 维护变量all_image_frame和tmp_pre_integration。其中,tmp_pre_integration为前面在processIMU中计算出的当前帧预积分量,all_image_frame存储滑窗起始到当前的所有帧信息,它将图像帧与预积分绑定在一起。
  • 在旋转外参标志ESTIMATE_EXTRINSIC为2的前提下,若当前帧不是第一帧,则通过getCorresponding获取当前帧与上一帧的特征点,然后在CalibrationExRotation中计算旋转外参。
  • 根据是否需要进行初始化,执行相应的状态优化操作:
    • 若未初始化,则执行的操作如下:
      • 基于图像的纯视觉单目SLAM初始化(SFM)
      • 非线性优化求解VIO(solveOdometry)
      • 更新滑动窗口(slideWindow)
      • 移除无效地图点(removeFailures)
    • 若已进行了初始化,则执行的操作如下:
      • 非线性优化求解VIO(solveOdometry)
      • 更新滑动窗口(slideWindow)
      • 移除无效地图点(removeFailures)

(2)代码实现

void Estimator::processImage(const map<int, 
                             vector<pair<int, Eigen::Matrix<double, 7, 1>>>> &image, 
                             const std_msgs::Header &header)
{
   
    // Step 1 将特征点信息加到f_manager这个特征点管理器中,同时进行是否关键帧的检查
    if (f_manager.addFeatureCheckParallax(frame_count, image, td))
        // 如果上一帧(倒数第2帧)是关键帧,则滑窗中最老的帧就要被移出滑窗
        marginalization_flag = MARGIN_OLD;
    else
        // 否则移除上一帧(倒数第2帧)
        marginalization_flag = MARGIN_SECOND_NEW;

    Headers[frame_count] = header;
    
    // 根据对齐的当前帧image和时间戳定义ImageFrame对象
    ImageFrame imageframe(image, header.stamp.toSec());  
    // 获取当前帧的预积分量    每一帧的预积分量tmp_pre_integration是在processIMU中计算得到的
    imageframe.pre_integration = tmp_pre_integration;    
    // all_image_frame: 维护滑窗起始帧到当前帧之间的所有帧状态,用于后续初始化
    all_image_frame.insert(make_pair(header.stamp.toSec(), imageframe));
    // 初始化时:当前帧处理完之后,无论当前帧是否为关键帧,都认为它已经得到了两帧之间的预积分结果
    // 那么需要将预积分复位,为下一帧做准备
    tmp_pre_integration = new IntegrationBase{
   acc_0, gyr_0, Bas[frame_count], 
                                              Bgs[frame_count]};
    
    // Step 2: 外参初始化  
    // 2表示没有任何外参的先验初值,需要进行初始化
    // 1表示初始外参比较可靠,不需要通过初始化来计算旋转外参,只需要将初始外参值送给后端进行滑窗优化即可
    // 0表示初始外参比较准确,在滑窗中固定住,不对其进行优化
    if(ESTIMATE_EXTRINSIC == 2)
    {
   
        ROS_INFO("calibrating extrinsic param, rotation movement is needed");
        if (frame_count != 0)
        {
   
/*
   标定imu和相机的旋转外参的初值  因为预积分是相邻图像帧的约束,因此这里得到的图像关联也是相邻的
   通过getCorresponding获取当前图像帧frame_count和上一图像帧frame_count-1观察到的特征点在各自图像帧下的坐标corres
*/
            vector<pair<Vector3d, Vector3d>> corres = f_manager.getCorresponding(
                										frame_count - 1, frame_count);
            Matrix3d calib_ric;
            // 将预积分的delta_q和刚才求出的特征点在两相邻帧上的坐标corres作为参数
            // 传入CalibrationExRotation函数中进行外参初始化,得到标定后的外参结果calib_ric
            if (initial_ex_rotation.CalibrationExRotation(
                			corres, pre_integrations[frame_count]->delta_q, calib_ric))
            {
   
                ric[0] = calib_ric;
                RIC[0] = calib_ric;
                // 标志位设置成可信的外参初值
                ESTIMATE_EXTRINSIC = 1;
            }
        }
    }
    
    // 基于图像的纯视觉单目SLAM ———— SFM
    // solver_flag == INITIAL表示需要进行初始化  solver_flag = NON_LINEAR表示不需要进行初始化
    if (solver_flag == INITIAL)
    {
   
        // 是否有足够的帧数,滑窗是否已满(后面在滑窗中,frame_count会一直等于滑窗大小)
        if (frame_count == WINDOW_SIZE) 
        {
   
            // 要有可信的旋转外参值,同时距离上次初始化不成功至少相邻0.1s
            // Step 3: VIO初始化
            if( ESTIMATE_EXTRINSIC!=2 && (header.stamp.toSec()-initial_timestamp) > 0.1)
            {
   
                result = initialStructure();  // 单目视觉SLAM的三维重建
                initial_timestamp = header.stamp.toSec();
            }
            if(result)
            {
   
                // solver_flag = INITIAL表示处于初始化阶段 
                // solver_flag = NON_LINEAR表示初始化已完成  
                solver_flag = NON_LINEAR;
                // Step 4: 非线性优化求解VIO
                solveOdometry();
                // Step 5: 更新滑动窗口
                slideWindow();
                // Step 6: 移除无效地图点
                f_manager.removeFailures(); // 移除无效地图点
                ROS_INFO("Initialization finish!");
                last_R = Rs[WINDOW_SIZE];   // 滑窗里最新的位姿
                last_P = Ps[WINDOW_SIZE];
                last_R0 = Rs[0];    // 滑窗里最老的位姿
                last_P0 = Ps[0];
                
            }
            else
                // 更新滑动窗口
                slideWindow();
        }
        else
            frame_count++;
    }
    else  // 不需要进行初始化的清空
    {
   
        TicToc t_solve;
        solveOdometry();
        ROS_DEBUG("solver costs: %fms", t_solve.toc());
        // 检测VIO是否正常
        if (failureDetection())
        {
   
            ROS_WARN("failure detection!");
            failure_occur = 1;
            // 如果异常,重启VIO
            clearState();
            setParameter();
            ROS_WARN("system reboot!");
            return;
        }

        TicToc t_margin;
        slideWindow();
        f_manager.removeFailures();
        ROS_DEBUG("marginalization costs: %fms", t_margin.toc());
        // prepare output of VINS
        // 给可视化用的
        key_poses.clear();
        for (int i = 0; i <= WINDOW_SIZE; i++)
            key_poses.push_back(Ps[i]);

        last_R = Rs[WINDOW_SIZE];  // 保存最新帧信息
        last_P = Ps[WINDOW_SIZE];
        last_R0 = Rs[0];  // 保存最老帧信息
        last_P0 = Ps[0];
    }
}

2.3 关键帧判断addFeatureCheckParallax

(1)大致流程

  • 遍历传入的特征点信息image,完善特征点属性
    • 对遍历到的每个特征点,将它的xyz_uv_velocity信息封装到f_per_fra中
    • 对遍历到的每个特征点,在特征点存放容器feature(list<FeaturePerId>类型)中查找是否已经存在这个特征点
      • 若没有查找到,则表明遍历到的这个特征点是一个新特征点,则向特征点存放容器feature中添加这个特征点的一整套属性,即FeaturePerId属性与对应的FeaturePerFrame属性
      • 若查找到的,则只需完善该特征点的FeaturePerFrame属性,然后添加到特征点存放容器feature中,同时更新成功追踪的特征点数目last_track_num
  • 在完善传入的特征点属性之后, 进行关键帧判断
    • 通过上一帧在滑窗中的索引frame_count和成功追踪的特征点数目,先提前判断倒数第二帧(上一帧)是否为关键帧
    • 遍历feature中的所有特征点,对于每个特征点,在它能够被滑窗中倒数第二帧和倒数第三帧观测到的前提下,在compensatedParallax2中计算该特征点在滑窗中倒数第二帧与倒数第三帧中的视差,然后累加每个特征点的视差
      • 若累积视差为0,则直接判定倒数第二帧为关键帧
      • 否则,若平均视差大于视差阈值,则也认为倒数第二帧是关键帧

(2)addFeatureCheckParallax代码实现

// vins_estimator/src/feature_manager.cpp
/**
 * @brief 增加特征点信息,同时检查上一帧是否时关键帧
 * 
 * @param[in] frame_count 
 * @param[in] image 
 * @param[in] td 
 * @return true 
 * @return false 
 */

bool FeatureManager::addFeatureCheckParallax(int frame_count, const map<int, vector<pair<int, Eigen::Matrix<double, 7, 1>>>> &image, double td)
{
   
    double parallax_sum = 0;
    int parallax_num = 0;
    last_track_num = 0;
    // 遍历每个特征点    
    for (auto &id_pts : image)
    {
   
        // 用遍历到的当前特征点信息(xyz_uv_velocity)以及时间戳td构造一个对象,即它在最新帧中的属性
        // xyz: 归一化像素坐标    uv: 像素坐标系下的坐标    velocity: 特征点速度
        FeaturePerFrame f_per_fra(id_pts.second[0].second, td);

        // first: feature_id    
        // second: [camera_id0--xyz_uv_velocity0, ..., camera_idn--xyz_uv_velocityn]
        // 前面的id_pts.second[0].second就表示xyz_uv_velocity0
        int feature_id = id_pts.first;  // 获取特征点的id  
        // 在已有的特征点id中寻找是否有相同的特征点    
        // feature表示存储当前帧中所有特征点的list容器
        auto it = find_if(feature.begin(), feature.end(), 
                          				   [feature_id](const FeaturePerId &it)
                          {
   
            return it.feature_id == feature_id;    // 若查找到了,则直接返回
                          });
        // 若it为feature.end(),表示没有找到相同id的特征点,即这是一个新的特征点
        // 因此,需要将这个新的特征点更新到特征点管理器中
        if (it == feature.end())
        {
   
            // 根据FeaturePerId的构造函数,新创建一个特征点信息,然后将创建的对象push到feature中
            // 这里的frame_count就是该特征点在滑窗中的当前位置,作为这个特征点的起始位置
            // feature_id: 特征点id    frame_count: 第一个观测到该特征点的图像帧,即起始帧
            feature.push_back(FeaturePerId(feature_id, frame_count));
            // 对于feature中的最后一个元素feature.back(), 也是刚才上一步push进去的特征点
            // 添加观测到该特征点的所有图像帧的属性feature_per_frame
            feature.back().feature_per_frame.push_back(f_per_fra);
        }
        // 如果这是一个已有的特征点,就在对应的“组织”下增加一个帧属性
        else if (it->feature_id == feature_id)
        {
   
            // 在观测到当前特征点it的图像帧中添加对应的特征点属性f_per_fra
            it->feature_per_frame.push_back(f_per_fra);  // 增加帧属性
            last_track_num++;   // 更新追踪到上一帧的特征点数目
        }
    }
    // 判断当前帧是否为KF: 前两帧全部设置为KF,追踪过少也认为是KF
    // 若当前帧为前两帧(第0帧或第1帧),则认为当前帧为关键帧
    // 若追踪到上一帧的特征点数目小于20,表示当前帧的特征关联比较弱了,则认为当前帧为关键帧
    if (frame_count < 2 || last_track_num < 20)
        return true;
    // 进行简单的视差计算: 判断倒数第2帧和倒数第3帧之间的视差是否足够大,以此来决定倒数第2帧是否为关键帧
    for (auto &it_per_id : feature)  // 遍历所有的特征点
    {
   
/*
   通过判断倒数第二帧(frame_count - 1)和倒数第三帧(frame_count - 2)的视差是否足够大,来确定倒数第二帧是否为关键帧。因此起始帧至少得是frame_count - 2(至少要从倒数第三帧开始被看到),同时至少覆盖到frame_count - 1帧(倒数第二帧)
*/
        if (it_per_id.start_frame <= frame_count - 2 && it_per_id.start_frame +
            int(it_per_id.feature_per_frame.size()) - 1 >= frame_count - 1)
        {
   
            parallax_sum += compensatedParallax2(it_per_id, frame_count);  // 计算视差
            parallax_num++;
        }
    }
    // 若累计视差为0,则表示倒数第二帧与倒数第三帧之间不存在特征关联,认为倒数第二帧为关键帧
    if (parallax_num == 0)
    {
   
        return true;
    }
    else
    {
   
        // 看看平均视差是否超过所设阈值MIN_PARALLAX  总视差 / 总特征点数  
        // 若为true,表明视差较大,二者之间的特征关联较弱,也认为倒数第2帧为关键帧
        return parallax_sum / parallax_num >= MIN_PARALLAX;
    }
}

计算视差compensatedParallax2

  • 大致思路

​ 首先,获取该特征点在滑窗中倒数第二帧与倒数第三帧中的索引。然后,根据索引获取它在倒数第二帧与倒数第三帧中的归一化坐标。最后,根据归一化坐标,计算这两个归一化点之间的距离,即为视差。

  • 实现代码
// vins_estimator/src/feature_manager.cpp
// it_per_id: 遍历到的当前特征点    frame_count: 当前帧索引
double FeatureManager::compensatedParallax2(const FeaturePerId &it_per_id, int frame_count)
{
   
// 找到相邻两帧
// frame_count - 2 - it_per_id.start_frame表示倒数第3帧在it_per_id.feature_per_frame中的索引
// frame_count - 1 - it_per_id.start_frame表示倒数第2帧在it_per_id.feature_per_frame中的索引
    const FeaturePerFrame &frame_i = it_per_id.feature_per_frame[frame_count - 2 - it_per_id.start_frame];  // 取出倒数第3帧
    const FeaturePerFrame &frame_j = it_per_id.feature_per_frame[frame_count - 1 - it_per_id.start_frame];  // 取出倒数第2帧

    double ans = 0;
    Vector3d p_j = frame_j.point;  // 倒数第2帧中的归一化坐标

    double u_j = p_j(0);
    double v_j = p_j(1);

    Vector3d p_i = frame_i.point;  // 倒数第3帧中的归一化坐标
    Vector3d p_i_comp;
    
    p_i_comp = p_i;
    // 归一化操作,其实没必要,因为存的就是归一化值
    double dep_i = p_i(2);
    double u_i = p_i(0) / dep_i;
    double v_i = p_i(1) / dep_i;
    // 特征点在倒数第三帧和倒数第二帧中归一化点的坐标差
    double du = u_i - u_j, dv = v_i - v_j;  
    // 当都是归一化坐标系时,他们两个都是一样的
    double dep_i_comp = p_i_comp(2);
    double u_i_comp = p_i_comp(0) / dep_i_comp;
    double v_i_comp = p_i_comp(1) / dep_i_comp;
    double du_comp = u_i_comp - u_j, dv_comp = v_i_comp - v_j;

    // 当前特征点it_per_id在倒数第2帧和倒数第3帧归一化平面上的距离(视差)
    ans = max(ans, sqrt(min(du * du + dv * dv, du_comp * du_comp + dv_comp * dv_comp)));

    return ans;
}

获取当前帧与倒数第二帧中的特征点getCorresponding

/**
 * @brief 得到同时被frame_count_l frame_count_r帧看到的特征点在各自帧下的坐标
 * 
 * @param[in] frame_count_l 
 * @param[in] frame_count_r 
 * @return vector<pair<Vector3d, Vector3d>> 
 */

vector<pair<Vector3d, Vector3d>> FeatureManager::getCorresponding(int frame_count_l, int frame_count_r)
{
   
    vector<pair<Vector3d, Vector3d>> corres;
    for (auto &it : feature)  // 遍历所有的特征点
    {
   
        // 保证需要的特征点被这两帧都观察到
        if (it.start_frame <= frame_count_l && it.endFrame() >= frame_count_r)
        {
   
            Vector3d a = Vector3d::Zero(), b = Vector3d::Zero();
           // 获取两图像帧frame_count_l和frame_count_r在feature_per_frame(vector类型)中的索引
            int idx_l = frame_count_l - it.start_frame;
            int idx_r = frame_count_r - it.start_frame;

            // 获取当前特征点it在第idx_l帧中去畸变后的归一化坐标
            a = it.feature_per_frame[idx_l].point;  
			// 获取当前特征点it在第idx_r帧中去畸变后的归一化坐标
            b = it.feature_per_frame[idx_r].point;  
            
            corres.push_back(make_pair(a, b));  // 返回该两帧下同时观测到的特征点的各自归一化坐标
        }
    }
    return corres;
}

2.4 外参标定CalibrationExRotation

(1)大致思路

  • 根据传入的两相邻图像帧之间的特征点,通过对极约束计算该两相邻图像帧之间的旋转

  • 根据传入的预积分,获取两相邻图像帧对应的imu旋转

  • 根据公式将两相邻图像帧对应的IMU之间的旋转转到图像帧上

  • 构建求取外参方程的系数矩阵

  • 使用SVD分解求取旋转外参,并根据特征值大小判断求解出的旋转外参是否有效(需要有足够大的置信度)

(2)求取外参的原理

​ 在两关键帧之间,可通过IMU积分得到IMU旋转为 q b k b k + 1 q_{b_kb_{k+1}} qbkbk+1。通过光流追踪可以得到两相邻图像帧之间的特征点匹配,然后使用对极约束可以得到两相邻图像帧之间的位姿变换。其中,平移是带尺度的,但在标定旋转时不需要平移,故可假设旋转为 q c k c k + 1 q_{c_kc_{k+1}} qckck+1

​ 由于相机与IMU之间的旋转外参 q c b q_{cb} qcb在标定完成之后是不会变的,所以 q c b = q c k b k = q c k + 1 b k + 1 q_{cb}=q_{c_kb_k} = q_{c_{k+1}b_{k+1}}

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值