当前位置: 首页 > news >正文

VINS-Mono学习(六):回环检测

VINS最后一个节点 pose_graph 是和回环检测相关内容。

 pose_graph 节点订阅 图像话题 和 vins_estimator节点发布的话题。

具体流程

1.ros初始化、设置句柄;

2.创建一些publisher;

3.从launch文件读取参数和参数文件config中的参数;

4.回环检测下的处理,如从yaml文件读取参数、读取训练好的二进制词袋、描述子pattern、如果需要加载离线地图及对应的处理;

5.订阅七个话题;

6.创建五个话题的发布;

7.主线程process;

8.接收键盘回调值的线程,“s” 保存地图。

重点在于七个订阅话题及回调函数的处理 和主线程process。

1.订阅话题及回调

1 订阅vins_estimator节点发布的imu_propagate话题,话题中的内容是初始化成功后上一时刻PVQ递推得到当前时刻的PVQ。在回调函数中对位姿进行修正,发布经过回环修正的位姿话题camera_pose_visual。

ros::Subscriber sub_imu_forward = n.subscribe("/vins_estimator/imu_propagate", 2000, imu_forward_callback);

2 订阅vins_estimator节点发布odometry话题,话题中的内容是优化后滑窗中最新的PVQ。在回调函数对位姿进行修正,然后转到相机坐标系,后续可视化相关操作。

ros::Subscriber sub_vio = n.subscribe("/vins_estimator/odometry", 2000, vio_callback);

3 订阅图像话题,将图像放到 image_buf

ros::Subscriber sub_image = n.subscribe(IMAGE_TOPIC, 2000, image_callback);

4 订阅vins_estimator发布的关键帧位姿,也就是滑窗中 WINDOW_SIZE - 2 帧的位姿,回调中将关键帧位姿放到 pose_buf

ros::Subscriber sub_pose = n.subscribe("/vins_estimator/keyframe_pose", 2000, pose_callback);

5 订阅vins_estimator发布的外参话题,回调中保存R_bc和t_bc的值

ros::Subscriber sub_extrinsic = n.subscribe("/vins_estimator/extrinsic", 2000, extrinsic_callback);

6 订阅vins_estimator发布的关键帧地图点话题,该话题中的内容是 关键帧所能看到的地图点坐标(世界系)以及地图点在归一化相机坐标系下的坐标。回调函数中将关键帧地图点信息放到point_buf

ros::Subscriber sub_point = n.subscribe("/vins_estimator/keyframe_point", 2000, point_callback);

7 订阅vins_estimator发布的 优化后的回环帧位姿 话题,话题中的内容是优化后的回环帧位姿、回环帧的yaw、回环帧对应的当前帧在回环节点中的idx。回调函数中将回环帧的信息放到loop_info中,updateKeyFrameLoop()函数利用回环帧的位姿信息来修正当前帧的位姿。

ros::Subscriber sub_relo_relative_pose = n.subscribe("/vins_estimator/relo_relative_pose", 2000, relo_relative_pose_callback);

2.主线程process

2.1 时间戳对齐

原图,KF位姿以及KF对应地图点 三个 buf 中的数据需要对齐;

2.2 取时间戳对齐的数据

时间戳对齐的原图,KF和地图点信息,构造关键帧KF对象,构造函数中计算已有特征点的描述子,同时额外提取fast角点并计算描述子;

2.3 回环检测 addKeyFrame()

posegraph.addKeyFrame(keyframe, 1);

先看posegraph对象的类PoseGraph的构造函数,创建了一个线程optimize4DoF,线程一直处于休眠状态,直到optimize_buf中有了数据。

1)通过词袋进行回环检测,寻找候选的回环帧  detectLoop() 

调用词袋查询函数query(),当前KF的候选回环帧结果在ret中,并且把当前KF的描述子放到关键帧数据库中;

/*
     调用词袋查询接口,参数:所有特征点的描述子,查询结果ret,最多返回4个备选KF,
                           查找距离当前至少50帧的KF(太近的话累计误差会比较小)
     db相当于是数据库(是由关键帧所组成的),所有特征点的描述子brief_descriptors送进接口进行查询
*/
db.query(keyframe->brief_descriptors, ret, 4, frame_index - 50);


// 当然也会把当前帧(描述子的信息)送进数据库中,便于后续帧的查询
db.add(keyframe->brief_descriptors);

从 ret 中找到符合要求的回环帧:

// ret 按照得分大小降序排列,确保返回的候选 KF 数目至少一个且得分满足要求
// 有效的结果 >= 1,最优的得分 > 0.05,满足这个要求后我们才认为可能检测出关键帧的回环
if (ret.size() >= 1 && ret[0].Score > 0.05) 
{
    // 开始遍历其他候选帧
    for (unsigned int i = 1; i < ret.size(); i++) 
    {
        // 别的关键帧得分 >= 0.015
        if (ret[i].Score > 0.015) 
        {          
            find_loop = true;   // 就认为找到回环了
            int tmp_index = ret[i].Id;

            if (DEBUG_IMAGE && 0) //可能是调试时临时禁用的代码
            {
                auto it = image_pool.find(tmp_index);
                cv::Mat tmp_image = (it->second).clone();
                putText(tmp_image, "loop score:" + to_string(ret[i].Score), 
                       cv::Point2f(10, 50), CV_FONT_HERSHEY_SIMPLEX, 0.4, cv::Scalar(255));
                cv::hconcat(loop_result, tmp_image, loop_result);
            }
        }
    }
}

// 如果检测到回环且当前帧序号 > 50(避免早期帧误匹配)
if (find_loop && frame_index > 50)  
{
    int min_index = -1;  // 初始化最小索引为无效值

    // 遍历所有候选关键帧,寻找满足条件的最小ID
    for (unsigned int i = 0; i < ret.size(); i++)
    {
        // 条件:当前候选帧得分 > 0.015,且ID比已记录的min_index更小(或min_index未初始化)
        if (ret[i].Score > 0.015 && (min_index == -1 || ret[i].Id < min_index))
        {
            min_index = ret[i].Id;  // 更新最小索引
        }
    }

    // 返回找到的最小ID(形成回环的候选关键帧)
    return min_index;   
}
else
{
    // 未检测到回环或帧序号不足,返回-1表示失败
    return -1;  
}

2) 如果找到回环帧,进行回环校验  findConnection()

之前是通过词袋找到了候选,还需要通过描述子的判断以及几何的校验进行最终的确认

将当前帧的描述子依次和回环帧描述子进行匹配,计算汉明距离,满足条件bestDist < 80认为匹配成功,记录回环帧匹配点的像素坐标和归一化的相机坐标

/**
 * @brief 将当前帧的描述子依次和回环帧描述子进行匹配,得到匹配结果
 * 
 * @param[out] matched_2d_old 匹配回环帧点的像素坐标集合
 * @param[out] matched_2d_old_norm 匹配回环帧点的归一化相机坐标集合
 * @param[out] status 状态位
 * @param[in] descriptors_old  回环帧的描述子集合
 * @param[in] keypoints_old    回环帧的像素坐标
 * @param[in] keypoints_old_norm 回环帧的归一化坐标
 */
void KeyFrame::searchByBRIEFDes(std::vector<cv::Point2f> &matched_2d_old,//前三个出参,后三个入参
								std::vector<cv::Point2f> &matched_2d_old_norm,
                                std::vector<uchar> &status,
                                const std::vector<BRIEF::bitset> &descriptors_old,
                                const std::vector<cv::KeyPoint> &keypoints_old,
                                const std::vector<cv::KeyPoint> &keypoints_old_norm)
{
    // 遍历当前KF 光流追踪的特征点的描述子
		for(int i = 0; i < (int)window_brief_descriptors.size(); i++)
    {
        cv::Point2f pt(0.f, 0.f);
        cv::Point2f pt_norm(0.f, 0.f);
		/*
			暴力匹配法,通过遍历所有的候选描述子得到最佳匹配
		    参数:(入参)当前帧的一个描述子、回环帧的描述子集合、回环帧像素坐标集合、回环帧归一化坐标集合、
			      (出参)最佳匹配的像素坐标、最佳匹配的归一化相机坐标
		*/
        if (searchInAera(window_brief_descriptors[i], descriptors_old, keypoints_old, keypoints_old_norm, pt, pt_norm))
           	status.push_back(1);	// 匹配上了状态位置1
        else
          status.push_back(0);	// 没匹配上状态位置0
        
		matched_2d_old.push_back(pt);	// 对应的像素坐标和归一化坐标存起来
        matched_2d_old_norm.push_back(pt_norm);
    }

}

已知当前帧的位姿和地图点(世界系)和回环帧的像素坐标,PnP计算回环帧的位姿T_wb

/**
 * @brief 通过PNP对当前帧和回环是否构成回环进行校验
 * 
 * @param[in] matched_2d_old_norm 回环帧2d归一化坐标
 * @param[in] matched_3d 当前帧3d地图点
 * @param[out] status 
 * @param[out] PnP_T_old 
 * @param[out] PnP_R_old 
 */
void KeyFrame::PnPRANSAC(const vector<cv::Point2f> &matched_2d_old_norm,
                         const std::vector<cv::Point3f> &matched_3d,
                         std::vector<uchar> &status,
                         Eigen::Vector3d &PnP_T_old, Eigen::Matrix3d &PnP_R_old)
{
    cv::Mat r, rvec, t, D, tmp_r;
    cv::Mat K = (cv::Mat_<double>(3, 3) << 1.0, 0, 0, 0, 1.0, 0, 0, 0, 1.0);	// 设置单位阵
    Matrix3d R_inital;
    Vector3d P_inital;
	// imu坐标系转成相机坐标系(当前帧的位姿)
    Matrix3d R_w_c = origin_vio_R * qic;	
    Vector3d T_w_c = origin_vio_T + origin_vio_R * tic;

	// Twc -> Tcw
    R_inital = R_w_c.inverse();
    P_inital = -(R_inital * T_w_c);

	// 转成opencv格式
    cv::eigen2cv(R_inital, tmp_r);
    cv::Rodrigues(tmp_r, rvec);
    cv::eigen2cv(P_inital, t);

    cv::Mat inliers;
    TicToc t_pnp_ransac;

    if (CV_MAJOR_VERSION < 3)//对opencv版本进行检查
        solvePnPRansac(matched_3d, matched_2d_old_norm, K, D, rvec, t, true, 100, 10.0 / 460.0, 100, inliers);
    else
    {
        if (CV_MINOR_VERSION < 2)
            solvePnPRansac(matched_3d, matched_2d_old_norm, K, D, rvec, t, true, 100, sqrt(10.0 / 460.0), 0.99, inliers);
        else
            // 使用当前帧位姿作为起始位姿,考虑到回环帧距离起始帧并不远
			// 参数:当前帧的3D点,回环帧2d归一化坐标,k内参置成单位阵,D畸变系数(因为去完畸变了所以不用考虑),
			//       使用初始估计,RANSAC 迭代次数,重投影误差阈值(像素),置信度,输出的内点索引
			solvePnPRansac(matched_3d, matched_2d_old_norm, K, D, rvec, t, true, 100, 10.0 / 460.0, 0.99, inliers);
    }
	
	// 初始化状态位status全是0
    for (int i = 0; i < (int)matched_2d_old_norm.size(); i++)
        status.push_back(0);
	// inlier部分状态位status全部置1
    for( int i = 0; i < inliers.rows; i++)
    {
        int n = inliers.at<int>(i);
        status[n] = 1;
    }
	// 转回eigen,以及Tcw -> Twc -> Twi
    cv::Rodrigues(rvec, r);
    Matrix3d R_pnp, R_w_c_old;
    cv::cv2eigen(r, R_pnp);
    R_w_c_old = R_pnp.transpose();
    Vector3d T_pnp, T_w_c_old;
    cv::cv2eigen(t, T_pnp);
    T_w_c_old = R_w_c_old * (-T_pnp);

	// 这是是回环帧在VIO坐标系下的位姿
    PnP_R_old = R_w_c_old * qic.transpose();	
    PnP_T_old = T_w_c_old - PnP_R_old * tic;

}

根据PnP的内点数进行判断,足够才认为回环可能成功。然后计算当前帧到回环帧的相对位姿,relative_q和relative_t,以及yaw角差,由loop_info保存这些信息。如果yaw角小于30度,并且平移距离小于20,才认为真正找到了回环

// 根据PNP的内点数进行判断,足够才认为回环可能成功
if ((int)matched_2d_cur.size() > MIN_LOOP_NUM)  // MIN_LOOP_NUM通常设为25
{
    /*
      计算当前帧到回环帧的相对位姿 T12(当前帧为2,回环帧为1)
      P1 = T12 * P2
      通过世界坐标系转换:
      Pw = T_wb(2) * P2
      P1 = T_bw(1) * Pw
      因此:
      T12 = T_bw(1) * T_wb(2) = [R_bw(1)  t_bw(1)] * [R_wb(2)  t_wb(2)]
                                [0        1       ]   [0        1       ]
    */
    relative_t = PnP_R_old.transpose() * (origin_vio_T - PnP_T_old);
    relative_q = PnP_R_old.transpose() * origin_vio_R;

    // 计算yaw角差(roll/pitch由重力观测约束,yaw不可观需特殊处理)
    relative_yaw = Utility::normalizeAngle(
        Utility::R2ypr(origin_vio_R).x() - Utility::R2ypr(PnP_R_old).x()
    );
   
    // 合理性检查:yaw角差<30度且平移量<20米
    if (abs(relative_yaw) < 30.0 && relative_t.norm() < 20.0) 
    {
        has_loop = true;                      // 确认回环有效
        loop_index = old_kf->index;           // 记录回环关键帧索引
        
        // 存储相对位姿信息(平移+旋转四元数+yaw差)
        loop_info << relative_t.x(), relative_t.y(), relative_t.z(),
                     relative_q.w(), relative_q.x(), relative_q.y(), relative_q.z(),
                     relative_yaw;

        // 快速重定位模式下的数据发布
        if (FAST_RELOCALIZATION) 
        {
            sensor_msgs::PointCloud msg_match_points;
            msg_match_points.header.stamp = ros::Time(time_stamp);
            
            // 填充回环帧的2D归一化坐标和特征点ID
            for (int i = 0; i < (int)matched_2d_old_norm.size(); i++) 
            {
                geometry_msgs::Point32 p;
                p.x = matched_2d_old_norm[i].x;
                p.y = matched_2d_old_norm[i].y;
                p.z = matched_id[i];  // 特征点ID
                msg_match_points.points.push_back(p);
            }

            // 打包回环帧的位姿(世界坐标系下)
            Eigen::Vector3d T = old_kf->T_w_i;
            Eigen::Matrix3d R = old_kf->R_w_i;
            Quaterniond Q(R);
            
            sensor_msgs::ChannelFloat32 t_q_index;
            t_q_index.values.push_back(T.x());  // 平移X
            t_q_index.values.push_back(T.y());  // 平移Y
            t_q_index.values.push_back(T.z());  // 平移Z
            t_q_index.values.push_back(Q.w());  // 四元数w
            t_q_index.values.push_back(Q.x());  // 四元数x
            t_q_index.values.push_back(Q.y());  // 四元数y
            t_q_index.values.push_back(Q.z());  // 四元数z
            t_q_index.values.push_back(index);  // 当前帧索引
            
            msg_match_points.channels.push_back(t_q_index);
            // !vins_estimator节点订阅publish的话题
		    // 发布的是回环帧的位姿
            pub_match_points.publish(msg_match_points); 
        }
        return true;  // 回环检测成功
    }
}
return false;  // 默认返回失败

: 函数最后有一个话题发布,发布回环帧的位姿、特征点信息。vins_estimator节点订阅这个话题进行回环帧的处理。  (VINS-Mono学习(三):vins_estimator  5.4内容)

3)修正当前帧位姿

通过之前计算的当前帧和回环帧的位姿变换 矫正当前帧的位姿

        // 如果确定两者回环,findConnection确认是否构成真正意义上的回环
        if (cur_kf->findConnection(old_kf))
        {
            // 更新最早回环帧,用来确定全局优化的范围
            if (earliest_loop_index > loop_index || earliest_loop_index == -1)
                earliest_loop_index = loop_index;

            Vector3d w_P_old, w_P_cur, vio_P_cur;
            Matrix3d w_R_old, w_R_cur, vio_R_cur;
            old_kf->getVioPose(w_P_old, w_R_old);     // 回环帧的VIO的pose
            cur_kf->getVioPose(vio_P_cur, vio_R_cur); // 当前关键帧的VIO的pose

            Vector3d relative_t;
            Quaterniond relative_q;
            
            // 得到当前帧和回环帧之间相对的平移. (findConnection函数中计算相对位姿)
            relative_t = cur_kf->getLoopRelativeT(); 
            // 得到当前帧和回环帧之间相对的旋转
            relative_q = (cur_kf->getLoopRelativeQ()).toRotationMatrix();

            // !回环矫正后当前帧的位姿
            // !实际上此时有两个当前帧的位姿,一个通过回环检测后当前帧的位姿,一个是VIO计算的当前帧位姿
            w_P_cur = w_R_old * relative_t + w_P_old;
            w_R_cur = w_R_old * relative_q;
            double shift_yaw;
            Matrix3d shift_r;//两个地图点之间的位姿变换
            Vector3d shift_t; 
            // 回环矫正前的位姿认为是T_w'_cur
            // 下面求得是 T_w_cur * T_cur_w' = T_w_w' 两个坐标系之间的相对位姿变换
            shift_yaw = Utility::R2ypr(w_R_cur).x() - Utility::R2ypr(vio_R_cur).x();
            shift_r = Utility::ypr2R(Vector3d(shift_yaw, 0, 0));
            shift_t = w_P_cur - w_R_cur * vio_R_cur.transpose() * vio_P_cur; 


            // 如果这两个不是同一个sequence,并且当前sequence没有跟之前合并(类似是一个多地图合并的操作)
            if (old_kf->sequence != cur_kf->sequence && sequence_loop[cur_kf->sequence] == 0)//首先判断当前和原来的是不是同一个地图,当前的这个地图是不是没有合并的
            {  //下面是合并相关的事情
                w_r_vio = shift_r;
                w_t_vio = shift_t;
                // T_w_w' * T_w'_cur //当前这个点的位姿转移到了老地图坐标系下的位姿
                vio_P_cur = w_r_vio * vio_P_cur + w_t_vio;//旋转和平移的转化
                vio_R_cur = w_r_vio *  vio_R_cur;
                cur_kf->updateVioPose(vio_P_cur, vio_R_cur);    // 更新当前帧位姿
                list<KeyFrame*>::iterator it = keyframelist.begin();
                // 同时把这个序列当前帧之间的位姿都更新过来
                for (; it != keyframelist.end(); it++)   //除了当前帧(已经转化过了)的所有帧全部转到上一个地图的参考系下面去
                {
                    if((*it)->sequence == cur_kf->sequence)//sequence和当前的相同,我们就把它取出来
                    {
                        Vector3d vio_P_cur;
                        Matrix3d vio_R_cur;
                        (*it)->getVioPose(vio_P_cur, vio_R_cur);//取出来
                        vio_P_cur = w_r_vio * vio_P_cur + w_t_vio;//做一个补偿
                        vio_R_cur = w_r_vio *  vio_R_cur;
                        (*it)->updateVioPose(vio_P_cur, vio_R_cur);//结果赋值回去
                    }
                }
                // 代表这个序列已经跟之前的序列合并过了,这里实现的也就是一个序列的合并
                sequence_loop[cur_kf->sequence] = 1;
            }//这样所有新地图的关键帧坐标系全部统一回了老地图的坐标系下去
            m_optimize_buf.lock();
            // !
            optimize_buf.push(cur_kf->index);   // 相当于已经告诉你回环信息了,通知4dof优化线程开始干活
            m_optimize_buf.unlock();
        }

代码最后把当前关键帧的索引放到optimize_buf中,线程optimize4DoF开始工作。

到此主线程process内容基本结束。

3.回环帧位姿优化

线程optimize4DoF,优化四自由度的关键帧位姿

:ORB-SLAM 采用 单目相机,由于单目相机无法直接感知真实尺度(scale),整个系统的漂移可能发生在 7个自由度 上:3个平移自由度(X, Y, Z)、3个旋转自由度(roll, pitch, yaw)、1个尺度自由度(scale)。IMU 的加速度计提供了一个绝对的尺度参考,不会像纯视觉那样出现尺度不确定性;陀螺仪提供了准确的角速度测量,使得系统可以很好地估计和校正 roll 和 pitch,IMU 无法感知绝对的方向(除非有磁力计),所以偏航角(yaw)仍然可能漂移。因此,单目VINS 只会在 4 个自由度上漂移(X, Y, Z 位置 + yaw 角),而不会在 scale、roll 和 pitch 上漂移。

/**
 * @brief 如果找到回环,该线程就执行位姿优化
 * 
 */
void PoseGraph::optimize4DoF()//四自由度位姿的优化是个线程
{
    while(true)
    {
        int cur_index = -1;
        int first_looped_index = -1;
        m_optimize_buf.lock();
        // 取出最新的形成回环的当前帧
        while(!optimize_buf.empty())//检查是否为空,一直在等待buffer是否在填充
        {
            cur_index = optimize_buf.front();//当前最新帧的id
            first_looped_index = earliest_loop_index;   // 找到最早的回环帧
            optimize_buf.pop();
        }
        m_optimize_buf.unlock();
        if (cur_index != -1)//检测出回环
        {
            printf("optimize pose graph \n");
            TicToc tmp_t;
            m_keyframelist.lock();
            KeyFrame* cur_kf = getKeyFrame(cur_index);  // 取出当前帧对应的KF指针

            int max_length = cur_index + 1; // 预设最大长度,总之优化帧数不可能超过这么多

            // w^t_i   w^q_i
            double t_array[max_length][3];
            Quaterniond q_array[max_length];
            double euler_array[max_length][3];
            double sequence_array[max_length];
            // 定义一个ceres优化问题,这里只优化位移和yaw角
            ceres::Problem problem;
            ceres::Solver::Options options;
            options.linear_solver_type = ceres::SPARSE_NORMAL_CHOLESKY;
           
            options.max_num_iterations = 5;//设置最大迭代次数为5
            ceres::Solver::Summary summary;
            ceres::LossFunction *loss_function;
            loss_function = new ceres::HuberLoss(0.1);//设置了核函数
           
            // !由于yaw不满足简单的增量更新方式,因此需要自定义其local param形式
            ceres::LocalParameterization* angle_local_parameterization =
                AngleLocalParameterization::Create();  //保证yaw角在正负Π之间

            list<KeyFrame*>::iterator it;

            int i = 0;
            // 遍历KF的list
            for (it = keyframelist.begin(); it != keyframelist.end(); it++)
            {
                if ((*it)->index < first_looped_index)  // idx小于最早回环帧就算了
                    continue;
                (*it)->local_index = i; // 这个是在本次优化中的idx,从0开始累计
                Quaterniond tmp_q;
                Matrix3d tmp_r;
                Vector3d tmp_t;
                (*it)->getVioPose(tmp_t, tmp_r);    //  得到VIO的位姿
                tmp_q = tmp_r;
                t_array[i][0] = tmp_t(0);//平移转成数组的形式
                t_array[i][1] = tmp_t(1);
                t_array[i][2] = tmp_t(2);
                q_array[i] = tmp_q;//旋转是四元数数组
                // 四元数转欧拉角
                Vector3d euler_angle = Utility::R2ypr(tmp_q.toRotationMatrix());//分解成欧拉角的形式
                euler_array[i][0] = euler_angle.x();//yaw角
                euler_array[i][1] = euler_angle.y();//pitch角
                euler_array[i][2] = euler_angle.z();//roll角

                sequence_array[i] = (*it)->sequence;
                // 只有yaw角参与优化,成为参数块
                problem.AddParameterBlock(euler_array[i], 1, angle_local_parameterization);//(yaw角,一个自由度,声明需要局部参数化不符合普通的加法,用自定义的加法选择更新方式)
                problem.AddParameterBlock(t_array[i], 3);//优化问题的每一个平移都会加到问题中来
                // 最早回环帧以及加载进来的地图保持不变,不进行优化
                if ((*it)->index == first_looped_index || (*it)->sequence == 0)//起始的第一帧索引需要固定住防止0空间漂移,sequence == 0代表的是事先加载的地图,vins不对事先加载的地图做任何处理
                {   
                    problem.SetParameterBlockConstant(euler_array[i]);//yaw角固定住
                    problem.SetParameterBlockConstant(t_array[i]);//平移向量固定住
                }

                // 建立约束,每一帧和之前5帧建立约束关系,找到这一帧前面5帧,并且需要他们是一个序列中的KF
                for (int j = 1; j < 5; j++)
                {
                  if (i - j >= 0 && sequence_array[i] == sequence_array[i-j])//前面不然没有意义//后面同一个地图点里面才会有比较好的约束效果
                  {
                    // 计算T_i-j_w * T_w_i = T_i-j_i
                    Vector3d euler_conncected = Utility::R2ypr(q_array[i-j].toRotationMatrix());//前几帧欧拉角的信息
                    Vector3d relative_t(t_array[i][0] - t_array[i-j][0], t_array[i][1] - t_array[i-j][1], t_array[i][2] - t_array[i-j][2]);//当前帧的平移-前几帧(i-j)的平移得到相对的平移关系
                    relative_t = q_array[i-j].inverse() * relative_t;//在i-j帧以它为单位阵相对的平移向量
                    double relative_yaw = euler_array[i][0] - euler_array[i-j][0];//Δyaw
                    ceres::CostFunction* cost_function = FourDOFError::Create( relative_t.x(), relative_t.y(), relative_t.z(),
                                                   relative_yaw, euler_conncected.y(), euler_conncected.z());//用的是自动求导(入参:两帧之间相对的平移变化,两帧之间相对的yaw角的变化,i-j帧相对的pitch和ro'l'l角)
                    // 对i-j帧和第i帧都成约束
                    problem.AddResidualBlock(cost_function, NULL, euler_array[i-j], //相邻帧不需要核函数,i-j帧的欧拉角
                                            t_array[i-j], //i-j帧的平移
                                            euler_array[i], //第i帧的欧拉角
                                            t_array[i]);//第i帧的平移
                  }
                }

                //add loop edge
                // 如果这一帧有回环帧
                if((*it)->has_loop)
                {
                    assert((*it)->loop_index >= first_looped_index);//回环帧的id一定要>=最早回环帧的id,否则代码崩掉
                    int connected_index = getKeyFrame((*it)->loop_index)->local_index;  // 得到回环帧在这次优化中的idx
                    Vector3d euler_conncected = Utility::R2ypr(q_array[connected_index].toRotationMatrix());//四元数q_array转成旋转矩阵toRotationMatrix再转成欧拉角R2ypr
                    Vector3d relative_t;
                    relative_t = (*it)->getLoopRelativeT(); // 得到当前帧和回环帧的相对位姿
                    double relative_yaw = (*it)->getLoopRelativeYaw();
                    ceres::CostFunction* cost_function = FourDOFWeightError::Create( relative_t.x(), relative_t.y(), relative_t.z(),
                                                                               relative_yaw, euler_conncected.y(), euler_conncected.z());
                    problem.AddResidualBlock(cost_function, loss_function, euler_array[connected_index], //回环帧
                                                                  t_array[connected_index], 
                                                                  euler_array[i], //当前帧(第i帧)
                                                                  t_array[i]);
                    
                }
                
                if ((*it)->index == cur_index)  // 到当前帧了,不会再有添加了,结束
                    break;
                i++;
            }
            m_keyframelist.unlock();

            ceres::Solve(options, &problem, &summary);
           
            m_keyframelist.lock();
            i = 0;
            // 将优化后的位姿恢复
            for (it = keyframelist.begin(); it != keyframelist.end(); it++)//遍历所有的列表
            {
                if ((*it)->index < first_looped_index)//如果小于第一个回环帧
                    continue;
                Quaterniond tmp_q;
                tmp_q = Utility::ypr2R(Vector3d(euler_array[i][0], euler_array[i][1], euler_array[i][2]));//欧拉角--->旋转矩阵--->四元数
                Vector3d tmp_t = Vector3d(t_array[i][0], t_array[i][1], t_array[i][2]);//平移向量
                Matrix3d tmp_r = tmp_q.toRotationMatrix();
                (*it)-> updatePose(tmp_t, tmp_r);   // 更新位姿

                if ((*it)->index == cur_index)//一直更新到当前帧为止,跳出去
                    break;
                i++;
            }

            Vector3d cur_t, vio_t;
            Matrix3d cur_r, vio_r;
            cur_kf->getPose(cur_t, cur_r);  // 最新优化后的位姿
            cur_kf->getVioPose(vio_t, vio_r);   // 当前帧VIO的位姿
            m_drift.lock();
            // 计算当前帧的VIO位姿和优化后位姿差
            yaw_drift = Utility::R2ypr(cur_r).x() - Utility::R2ypr(vio_r).x();//两帧间的yaw角通过相对关系计算出来
            r_drift = Utility::ypr2R(Vector3d(yaw_drift, 0, 0));//vio的位姿和优化后的位姿的位姿差
            t_drift = cur_t - r_drift * vio_t;
            m_drift.unlock();
           
            it++;
            // 遍历当前帧之后的所有位姿,根据算得的位姿差进行调整
            for (; it != keyframelist.end(); it++)//一直调整到最后一帧
            {
                Vector3d P;
                Matrix3d R;
                (*it)->getVioPose(P, R);//把VIO位姿取出来
                P = r_drift * P + t_drift;//偏移量补偿上去
                R = r_drift * R;
                (*it)->updatePose(P, R);//更新
            }
            m_keyframelist.unlock();
            // 可视化部分
            updatePath();
        }

        std::chrono::milliseconds dura(2000);
        std::this_thread::sleep_for(dura);
    }
}

思维导图

相关文章:

  • react antd 项目报错Warning: Each child in a list should have a unique “key“prop
  • Python入门(5):异常
  • Dart 实现与蓝牙设备的通信
  • JSON 基础知识(一)
  • 碰一碰发视频系统--基于H5场景开发
  • 【一起来学kubernetes】32、kubectl使用详解
  • 视频分析设备平台EasyCVR安消一体化解决方案,打造社区/园区全面可视化智能安消应用
  • HNSW(Hierarchical Navigable Small World,分层可导航小世界)用来高效搜索高维向量的最近邻
  • JAVA常见的 JVM 参数及其典型默认值
  • CMMI(能力成熟度模型集成)简介
  • 蓝桥复习2(温度开始)
  • 系统思考反馈
  • 写Prompt的技巧和基本原则
  • PyQt6基础_界面控件简单介绍III
  • 【夜话系列】DelayQueue延迟队列(上):原理剖析与实现机制
  • 公网专线IP和私网专线IP之间的区别是什么?
  • 定时任务(python)
  • nodejs:midi-writer-js 将基金净值数据转换为 midi 文件
  • 多线程猜数问题
  • AI CUDA 工程师:Agentic CUDA 内核发现、优化和组合
  • 特写|银耳种植“北移”到沧州盐山,村民入伙可年增收4万元
  • 北方首场高温将进入鼎盛阶段,江南华南多地需警惕降雨叠加致灾
  • 民间打拐志愿者上官正义遭人身安全威胁,杭州公安:已立案
  • 大陆非遗项目打铁花、英歌舞将在台演出
  • 马上评|文玩字画竞拍轻松赚差价?严防这类新型传销
  • 定制基因编辑疗法治愈罕见遗传病患儿