【SLAM】VINS-MONO解析——回环检测和重定位

2023-05-16

9. 回环检测与重定位

本部分内容涉及到的代码大部分在pose_graph文件夹下,少部分在vins_estimator里。

原创内容,转载请先与我联系并注明出处,谢谢!

系列内容请点击:【SLAM】VINS-MONO解析——综述

知识点:

如何利用描述子-词袋进行回环检测运用PnP Ransac进行异常值剔除;

如何求出世界坐标系矫正矩阵和漂移矫正矩阵,和它们的区别;

如何进行图优化更新所有位姿实现位姿的平滑。

这部分最主要的作用就是求出漂移位姿矫正矩阵r_drift和t_drift。这么大一个工程,这么多行代码,保存这么多帧特征点和描述子,就是为了找出这6个自由度的变量。当然了,代码里还有w_r_vio和w_t_vio,同样也是位姿矫正矩阵,他俩是应对中途图像序列中断的情况,此时它俩会把新的图像序列的帧矫正到旧的图像序列的世界坐标系中,实现全局一致,这种情况出现的比较少,起作用的次数明显少于r_drift和t_drift。

9.1 主入口和回调函数

先进入到pose_graph_node.cpp里的main():

开始是一些初始化ros node和从配置文件里读参数,这些我们不去关注它,然后是一些重要的订阅和回调函数:

    //订阅topic并执行各自回调函数
    ros::Subscriber sub_imu_forward = n.subscribe("/vins_estimator/imu_propagate", 2000, imu_forward_callback);
    ros::Subscriber sub_vio = n.subscribe("/vins_estimator/odometry", 2000, vio_callback);
    ros::Subscriber sub_image = n.subscribe(IMAGE_TOPIC, 2000, image_callback);
    ros::Subscriber sub_pose = n.subscribe("/vins_estimator/keyframe_pose", 2000, pose_callback);
    ros::Subscriber sub_extrinsic = n.subscribe("/vins_estimator/extrinsic", 2000, extrinsic_callback);
    ros::Subscriber sub_point = n.subscribe("/vins_estimator/keyframe_point", 2000, point_callback);
    ros::Subscriber sub_relo_relative_pose = n.subscribe("/vins_estimator/relo_relative_pose", 2000, relo_relative_pose_callback);

这些订阅的数据从哪来,是什么含义,订阅之后要干什么,都非常重要。其中extrinsic_callback的含义比较明显,就不介绍了。

然后是发布的一些topic,在这里,我们先不管它发布的什么,在主流程碰到了再去研究:

    //发布的topic
    pub_match_img = n.advertise<sensor_msgs::Image>("match_image", 1000);
    pub_camera_pose_visual = n.advertise<visualization_msgs::MarkerArray>("camera_pose_visual", 1000);
    pub_key_odometrys = n.advertise<visualization_msgs::Marker>("key_odometrys", 1000);
    pub_vio_path = n.advertise<nav_msgs::Path>("no_loop_path", 1000);
    pub_match_points = n.advertise<sensor_msgs::PointCloud>("match_points", 100);

随后是创建回环检测和重定位的主流程函数process(),这里创建了一个线程干这个事情:

    measurement_process = std::thread(process);

9.1.1 话题 “imu_propagate”的数据来源和回调函数imu_forward_callback()

数据来源是vins_estimator下visualization.cpp的pubLatestOdometry(),这个函数是在estimator_node.cpp的imu_callback()下被调用的。我们知道后端优化的位姿是以image的频率实现的,而IMU的频率是高于image的,所以在imu_callback()中,它会找一个最新优化的位姿,在这个基础上依托这个位姿之后收到的imu数据,积分在这个位姿上,实现输出一个高频的里程计的作用,所以 “imu_propagate”话题下接收的是一个高频的实时里程计。进入到imu_forward_callback(),你会发现这个回调函数把这个位姿先跟进重定位的结果进行了矫正:

        vio_t = posegraph.w_r_vio * vio_t + posegraph.w_t_vio;
        vio_q = posegraph.w_r_vio *  vio_q;

        vio_t = posegraph.r_drift * vio_t + posegraph.t_drift;
        vio_q = posegraph.r_drift * vio_q;

这里有个非常有趣的位姿变换,就是先用w_r_vio和w_t_vio矫正一次,再用r_drift和t_drift矫正一次。我目前对他们的理解是前者是把当前位姿从错误的世界坐标系纠正到正确的坐标系上,后者是对短期位姿漂移的矫正。

然后把这个位姿从body->world变成cam->world:

        Vector3d vio_t_cam;
        Quaterniond vio_q_cam;
        vio_t_cam = vio_t + vio_q * tic;
        vio_q_cam = vio_q * qic;   

最后用于可视化。

        cameraposevisual.reset();
        cameraposevisual.add_pose(vio_t_cam, vio_q_cam);
        cameraposevisual.publish_by(pub_camera_pose_visual, forward_msg->header);

9.1.2 话题 “odometry”的数据来源和回调函数vio_callback()

数据来源是vins_estimator下visualization.cpp的pubOdometry(),这个函数是在estimator_node.cpp的主线程process()下调用的,我们看看它发的是哪个的位姿:

        nav_msgs::Odometry odometry;
        odometry.header = header;
        odometry.header.frame_id = "world";
        odometry.child_frame_id = "world";
        Quaterniond tmp_Q;
        tmp_Q = Quaterniond(estimator.Rs[WINDOW_SIZE]);
        odometry.pose.pose.position.x = estimator.Ps[WINDOW_SIZE].x();
        odometry.pose.pose.position.y = estimator.Ps[WINDOW_SIZE].y();
        odometry.pose.pose.position.z = estimator.Ps[WINDOW_SIZE].z();
        odometry.pose.pose.orientation.x = tmp_Q.x();
        odometry.pose.pose.orientation.y = tmp_Q.y();
        odometry.pose.pose.orientation.z = tmp_Q.z();
        odometry.pose.pose.orientation.w = tmp_Q.w();
        odometry.twist.twist.linear.x = estimator.Vs[WINDOW_SIZE].x();
        odometry.twist.twist.linear.y = estimator.Vs[WINDOW_SIZE].y();
        odometry.twist.twist.linear.z = estimator.Vs[WINDOW_SIZE].z();
        pub_odometry.publish(odometry);

发布的位姿是滑窗的最新帧,假如说滑窗大小是10,那发布的是第11帧的位姿。再看看它的回调函数,实际上和前一个差不多,先根据重定位的结果进行矫正,再转化为cam->world的表示:

    vio_t = posegraph.w_r_vio * vio_t + posegraph.w_t_vio;
    vio_q = posegraph.w_r_vio * vio_q;

    vio_t = posegraph.r_drift * vio_t + posegraph.t_drift;
    vio_q = posegraph.r_drift * vio_q;
    vio_t_cam = vio_t + vio_q * tic;
    vio_q_cam = vio_q * qic;  

把相机位姿的平移分类放到buffer里:

odometry_buf.push(vio_t_cam);

这个buffer是为了给key_odometrys提供相机运动过程中一系列空间坐标数据的:

    for (unsigned int i = 0; i < odometry_buf.size(); i++)
    {
        geometry_msgs::Point pose_marker;
        Vector3d vio_t;
        vio_t = odometry_buf.front();
        odometry_buf.pop();
        pose_marker.x = vio_t.x();
        pose_marker.y = vio_t.y();
        pose_marker.z = vio_t.z();
        key_odometrys.points.push_back(pose_marker);
        odometry_buf.push(vio_t);
    }

而key_odometrys的作用在vins中也是用于可视化的。

 pub_key_odometrys.publish(key_odometrys)

9.1.3 话题"keyframe_pose"的数据来源和回调函数pose_callback()

数据来源是vins_estimator下visualization.cpp的pubKeyframe(),这个函数是在estimator_node.cpp的主线程process()下调用的,我们看看它发的是哪个的位姿:

        int i = WINDOW_SIZE - 2;
        Vector3d P = estimator.Ps[i];
        Quaterniond R = Quaterniond(estimator.Rs[i]);
        nav_msgs::Odometry odometry;
        odometry.header = estimator.Headers[WINDOW_SIZE - 2];
        odometry.header.frame_id = "world";
        odometry.pose.pose.position.x = P.x();
        odometry.pose.pose.position.y = P.y();
        odometry.pose.pose.position.z = P.z();
        odometry.pose.pose.orientation.x = R.x();
        odometry.pose.pose.orientation.y = R.y();
        odometry.pose.pose.orientation.z = R.z();
        odometry.pose.pose.orientation.w = R.w();
        pub_keyframe_pose.publish(odometry);

你发现了吗,是滑窗内倒数第二帧的位姿!也就是说,pose_graph收到的数据,处理的对象,都是由estimator滑窗在各个时刻的倒数第二帧提供的。我们再回到这个回调函数pose_callback():

void pose_callback(const nav_msgs::Odometry::ConstPtr &pose_msg)
{
    if(!LOOP_CLOSURE) return;
    m_buf.lock();
    pose_buf.push(pose_msg);
    m_buf.unlock();
}

把位姿放到pose_buf里去,pose_buf会在process()里用到。

9.1.4 话题 “keyframe_point”的数据来源和回调函数point_callback()

数据来源是vins_estimator下visualization.cpp的pubKeyframe(),这个函数是在estimator_node.cpp的主线程process()下调用的,我们看看它发的是哪个的点:

        sensor_msgs::PointCloud point_cloud;
        point_cloud.header = estimator.Headers[WINDOW_SIZE - 2];
        //遍历滑窗内的所有点
        for (auto &it_per_id : estimator.f_manager.feature)
        {
            //如果在WINDOW_SIZE - 2这一帧上看到过
            int frame_size = it_per_id.feature_per_frame.size();
            if(it_per_id.start_frame < WINDOW_SIZE - 2 && it_per_id.start_frame + frame_size - 1 >= WINDOW_SIZE - 2 && it_per_id.solve_flag == 1)
            {
                //找到这些特征点的世界坐标
                int imu_i = it_per_id.start_frame;
                Vector3d pts_i = it_per_id.feature_per_frame[0].point * it_per_id.estimated_depth;
                Vector3d w_pts_i = estimator.Rs[imu_i] * (estimator.ric[0] * pts_i + estimator.tic[0])
                                      + estimator.Ps[imu_i];
                geometry_msgs::Point32 p;
                p.x = w_pts_i(0);
                p.y = w_pts_i(1);
                p.z = w_pts_i(2);
                point_cloud.points.push_back(p);

                //找到这些特征点在WINDOW_SIZE - 2这一帧上的归一化平面坐标,像素坐标,id
                int imu_j = WINDOW_SIZE - 2 - it_per_id.start_frame;
                sensor_msgs::ChannelFloat32 p_2d;
                p_2d.values.push_back(it_per_id.feature_per_frame[imu_j].point.x());
                p_2d.values.push_back(it_per_id.feature_per_frame[imu_j].point.y());
                p_2d.values.push_back(it_per_id.feature_per_frame[imu_j].uv.x());
                p_2d.values.push_back(it_per_id.feature_per_frame[imu_j].uv.y());
                p_2d.values.push_back(it_per_id.feature_id);
                point_cloud.channels.push_back(p_2d);
            }

        }
        pub_keyframe_point.publish(point_cloud);

仍然是是滑窗内倒数第二帧的点。它首先扫描了滑窗内所有的点,如果某一个点在倒数第二帧前被看到,过了倒数第二帧仍然被看到,那么就把他的w系下的坐标,倒数第二帧的归一化坐标,像素坐标记录下来,发布出去。

再回到回调函数,你会发现和上一个也是类似的:

void point_callback(const sensor_msgs::PointCloudConstPtr &point_msg)
{
    if(!LOOP_CLOSURE)  return;
    m_buf.lock();
    point_buf.push(point_msg);
    m_buf.unlock();
}

9.1.5 话题 IMAGE_TOPIC的数据来源和回调函数image_callback()

void image_callback(const sensor_msgs::ImageConstPtr &image_msg)
{
    //ROS_INFO("image_callback!");
    if(!LOOP_CLOSURE)
        return;
    m_buf.lock();
    image_buf.push(image_msg);
    m_buf.unlock();
    //printf(" image time %f \n", image_msg->header.stamp.toSec());

    // detect unstable camera stream
    if (last_image_time == -1)
        last_image_time = image_msg->header.stamp.toSec();
    //若新到达的图像时间已超过上一帧图像时间1s或小于上一帧,则是新的图像序列
    else if (image_msg->header.stamp.toSec() - last_image_time > 1.0 || image_msg->header.stamp.toSec() < last_image_time)
    {
        ROS_WARN("image discontinue! detect a new sequence!");
        new_sequence();
    }
    last_image_time = image_msg->header.stamp.toSec();
}

你会发现,在这里,如果image时间戳错乱或者跟丢了的话,他会new_sequence()一下,sequence这个概念在pose_graph里面多次出现,而且概念非常绕:

void new_sequence()
{
    printf("new sequence\n");
    sequence++;
    printf("sequence cnt %d \n", sequence);
    if (sequence > 5)
    {
        ROS_WARN("only support 5 sequences since it's boring to copy code for more sequences.");
        ROS_BREAK();
    }
    //重新初始化,重新构建地图。
    posegraph.posegraph_visualization->reset();
    posegraph.publish();
    m_buf.lock();
    while(!image_buf.empty())
        image_buf.pop();
    while(!point_buf.empty())
        point_buf.pop();
    while(!pose_buf.empty())
        pose_buf.pop();
    while(!odometry_buf.empty())
        odometry_buf.pop();
    m_buf.unlock();
}

它不仅会清空buffer里的数据,还会sequence++。在这里,作者把一系列连续输入的帧作为一个sequence,如果数据中断了,那么就会创建一个新的sequence。然后代码里只要找到机会(具有两个回环关系的两帧在不同的sequence里被发现了),那么他会把新sequence里的帧的位姿全部矫正到与旧sequence一致的坐标系里。

9.2 主线程process()

首先经过时间戳的对比操作,在各个buffer里淘出具有相同时间戳的pose_msg、image_msg、point_msg ;

然后控制频率,每隔SKIP_CNT帧进行一次,让CPU别那么累:

            if (skip_cnt < SKIP_CNT)
            {
                skip_cnt++;
                continue;
            }

然后当前的关键帧相对上一个处理过的关键帧的平移也得大于一个距离,系统才会继续处理当前帧:

(T - last_t).norm() > SKIP_DIS

这些都满足了,那就把当前关键帧的点找出来:

            //将距上一关键帧距离(平移向量的模)超过SKIP_DIS的图像创建为关键帧
            if((T - last_t).norm() > SKIP_DIS)
            {
                vector<cv::Point3f> point_3d;                  //世界坐标
                vector<cv::Point2f> point_2d_uv;             //归一化平面坐标(关键帧所在坐标系)
                vector<cv::Point2f> point_2d_normal;     //像素坐标(关键帧所在坐标系)
                vector<double> point_id;                        //点id(为啥是double?)

                for (unsigned int i = 0; i < point_msg->points.size(); i++)
                {
                    cv::Point3f p_3d;
                    p_3d.x = point_msg->points[i].x;
                    p_3d.y = point_msg->points[i].y;
                    p_3d.z = point_msg->points[i].z;
                    point_3d.push_back(p_3d);                         

                    cv::Point2f p_2d_uv, p_2d_normal;
                    double p_id;
                    p_2d_normal.x = point_msg->channels[i].values[0]; 
                    p_2d_normal.y = point_msg->channels[i].values[1];
                    p_2d_uv.x = point_msg->channels[i].values[2];
                    p_2d_uv.y = point_msg->channels[i].values[3];
                    p_id = point_msg->channels[i].values[4];
                    point_2d_normal.push_back(p_2d_normal); 
                    point_2d_uv.push_back(p_2d_uv);           
                    point_id.push_back(p_id);

                    //printf("u %f, v %f \n", p_2d_uv.x, p_2d_uv.y);
                }

利用当前关键帧点,位姿,时间戳和当前关键帧在pose_graph里的idx构建一个关键帧:

                KeyFrame* keyframe = new KeyFrame(pose_msg->header.stamp.toSec(), frame_index, T, R, image,
                                   point_3d, point_2d_uv, point_2d_normal, point_id, sequence);   

我们进入到KeyFrame的构造函数里,除了传递输入数据给到当前keyframe对象外,还进行了brief描述子的计算:

KeyFrame::KeyFrame(double _time_stamp, int _index, Vector3d &_vio_T_w_i, Matrix3d &_vio_R_w_i, cv::Mat &_image,
		           vector<cv::Point3f> &_point_3d, vector<cv::Point2f> &_point_2d_uv, vector<cv::Point2f> &_point_2d_norm,
		           vector<double> &_point_id, int _sequence)
{
	time_stamp = _time_stamp;
	index = _index;
	vio_T_w_i = _vio_T_w_i;
	vio_R_w_i = _vio_R_w_i;
	T_w_i = vio_T_w_i;
	R_w_i = vio_R_w_i;
	origin_vio_T = vio_T_w_i;		
	origin_vio_R = vio_R_w_i;
	image = _image.clone();
	cv::resize(image, thumbnail, cv::Size(80, 60));
	point_3d = _point_3d;
	point_2d_uv = _point_2d_uv;
	point_2d_norm = _point_2d_norm;
	point_id = _point_id;
	has_loop = false;
	loop_index = -1;
	has_fast_point = false;
	loop_info << 0, 0, 0, 0, 0, 0, 0, 0;
	sequence = _sequence;
	computeWindowBRIEFPoint();
	computeBRIEFPoint();
	if(!DEBUG_IMAGE)
		image.release();
}

computeWindowBRIEFPoint()是对外部传入的点的像素坐标进行brief描述子的计算,分别放到 window_keypoints和window_brief_descriptors里:

void KeyFrame::computeWindowBRIEFPoint()
{
	BriefExtractor extractor(BRIEF_PATTERN_FILE.c_str());
	for(int i = 0; i < (int)point_2d_uv.size(); i++)
	{
	    cv::KeyPoint key;
	    key.pt = point_2d_uv[i];
	    window_keypoints.push_back(key);
	}
	extractor(image, window_keypoints, window_brief_descriptors);
}

而computeBRIEFPoint()会另外提取一些角点,计算描述子和归一化坐标,分别放到keypoints,brief_descriptors和keypoints_norm。

//额外检测新特征点并计算所有特征点的描述子,为了回环检测
void KeyFrame::computeBRIEFPoint()
{
	BriefExtractor extractor(BRIEF_PATTERN_FILE.c_str());
	const int fast_th = 20;
    cv::FAST(image, keypoints, fast_th, true);
	extractor(image, keypoints, brief_descriptors);

	for (int i = 0; i < (int)keypoints.size(); i++)
	{
		Eigen::Vector3d tmp_p;
		m_camera->liftProjective(Eigen::Vector2d(keypoints[i].pt.x, keypoints[i].pt.y), tmp_p);
		cv::KeyPoint tmp_norm;
		tmp_norm.pt = cv::Point2f(tmp_p.x()/tmp_p.z(), tmp_p.y()/tmp_p.z());
		keypoints_norm.push_back(tmp_norm);
	}
}

创建完关键帧之后,正戏开始了,往位姿图添加关键帧!

                posegraph.addKeyFrame(keyframe, 1);

完事之后帧号+1:

                frame_index++;

9.3 往位姿图添加关键帧posegraph.addKeyFrame(keyframe, 1)

posegraph对象是在pose_graph_node.cpp最前面定义的一个全局变量:

PoseGraph posegraph;

在Posegraph的构造函数里,它定义了几个重要的变量,同时创建了一个4Dof优化位姿的线程:

PoseGraph::PoseGraph()
{
	t_optimization = std::thread(&PoseGraph::optimize4DoF, this);
    earliest_loop_index = -1;
    t_drift = Eigen::Vector3d(0, 0, 0);
    yaw_drift = 0;
    r_drift = Eigen::Matrix3d::Identity();
    w_t_vio = Eigen::Vector3d(0, 0, 0);
    w_r_vio = Eigen::Matrix3d::Identity();
    global_index = 0;
    sequence_cnt = 0;
    sequence_loop.push_back(0);
    base_sequence = 1;
}

earliest_loop_index:被发现是回环帧的那些帧中,时间最早的那一帧的global_index;

global_index:PoseGraph里面所有帧的序号;

r_drift/yaw_drift:相当于同一个东西,把当前里程计位姿转到纠正后位姿的旋转变换,由于只优化yaw角的旋转变换,所以这两者等价;

t_drift: 当前里程计位姿相对于纠正后位姿的平移变换;

w_r_vio/w_t_vio:如果说r_drift/t_drift是对位姿短期的快速矫正的话,w_r_vio/w_t_vio是对位姿长期的矫正,如果image跟丢了,那么就会新建一个sequence,w_r_vio/w_t_vio的作用就是找到新旧sequence之间的位姿变换,把新sequence的帧变换到旧sequence的坐标系中;

sequence_loop:如果当前sequence与之前的sequence的坐标系相同,也就是说新的sequence的帧都位姿矫正到旧sequence坐标系中了,则设为1;

我们进入到addKeyFrame()函数,这个函数主要做的事情是:

①先利用当前的坐标系矫正矩阵把最新帧的位姿的坐标系变换到正确的世界坐标系下;

②然后找回环帧并求出回环帧和最新帧的位姿变换;

③但是代码并没有把这个位姿变换乘以老帧的位姿得到的结果矩阵给到最新帧,而是计算现有最新帧位姿(经过①变换后的)和这个结果矩阵的变化量;

④如果回环两帧不在同一个序列下,就用③求得的变化量对新帧所在序列的所有帧都进行世界坐标系的矫正,包括最新帧;

⑤用现有的漂移矫正矩阵更新最新帧的位姿。漂移矫正矩阵的数据来源是vins_estimator.

一开始先判断最新帧的序列号和poseGraph里现有的序列号是否相同,如果是不同的序列,当前帧是全新的序列,那么世界坐标系矫正矩阵和漂移矫正矩阵都重置:

    if (sequence_cnt != cur_kf->sequence)
    {
        sequence_cnt++;
        sequence_loop.push_back(0);
        w_t_vio = Eigen::Vector3d(0, 0, 0);
        w_r_vio = Eigen::Matrix3d::Identity();
        m_drift.lock();
        t_drift = Eigen::Vector3d(0, 0, 0);
        r_drift = Eigen::Matrix3d::Identity();
        m_drift.unlock();
    }

然后把当前帧的里程计进行世界坐标系的矫正,再还给它:

    //获取当前帧的位姿vio_P_cur、vio_R_cur并更新
    cur_kf->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;
    cur_kf->updateVioPose(vio_P_cur, vio_R_cur);
    cur_kf->index = global_index;
    global_index++;
	int loop_index = -1;

下一步,对当前帧进行回环检测,找到它的回环帧,具体的实现放到后面介绍,在正常的运行里,因为外部调用时flag_detect_loop一直为true,所以执行的都是detectLoop(),   而addKeyFrameIntoVoc()是预先在硬盘里加载现有位姿图时才执行的函数,所以先不看了。

    if (flag_detect_loop)
    {
        TicToc tmp_t;
        //回环检测,返回回环候选帧的索引
        loop_index = detectLoop(cur_kf, cur_kf->index);
    }
    else
    {
        addKeyFrameIntoVoc(cur_kf);
    }

如果说回环检测找到了,并且这两帧的描述子也匹配上了,那么才可以继续走下一步,findConnection()也在后面的部分分析:

	if (loop_index != -1)
	{
        //printf(" %d detect loop with %d \n", cur_kf->index, loop_index);
        
        //获取回环候选帧
        KeyFrame* old_kf = getKeyFrame(loop_index);

        //当前帧与回环候选帧进行描述子匹配
        if (cur_kf->findConnection(old_kf))
        {

如果找到的这个回环帧帧号是最早的,那么就记录这个帧号:

            //earliest_loop_index为最早的回环候选帧
            if (earliest_loop_index > loop_index || earliest_loop_index == -1)
                earliest_loop_index = loop_index;

接下来,找到这两帧的位姿,计算两帧直接的位姿变换,把这个位姿变换作用到老帧上,获得新帧理论上的位姿w_P_cur和w_R_cur,getLoopRelativeT()我们仍然后面介绍,现在先只把主流程理解清楚:

            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);
            cur_kf->getVioPose(vio_P_cur, vio_R_cur);

            //获取当前帧与回环帧的相对位姿relative_q、relative_t
            Vector3d relative_t;
            Quaterniond relative_q;
            relative_t = cur_kf->getLoopRelativeT();
            relative_q = (cur_kf->getLoopRelativeQ()).toRotationMatrix();

            //重新计算当前帧位姿w_P_cur、w_R_cur
            w_P_cur = w_R_old * relative_t + w_P_old;
            w_R_cur = w_R_old * relative_q;

代码里并没有把w_P_cur和w_R_cur直接给到最新帧的位姿,而是用它计算和之前原有位姿之间的变化shift_r和shift_t,其中旋转只提取yaw的变化:

           //回环得到的位姿和VIO位姿之间的偏移量shift_r、shift_t
            double shift_yaw;
            Matrix3d shift_r;
            Vector3d shift_t; 
            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; 

接下来不好理解的来了,如果具有回环关系的两帧不在同一个序列,并且新帧所在的序列所在的世界坐标系没有进行过矫正,那么才用shift_r和shift_t对新帧所在的序列的所有的帧的位姿所在的世界坐标系进行矫正,否则shift_r和shift_t虽然求出来了,那么也不用它,这里是w_r_vio/w_t_vio被更新的地方:

            // shift vio pose of whole sequence to the world frame
            //如果当前帧所在序列和回环帧序列不同,那么把当前帧所在序列的所有帧都转到回环帧所在序列的世界坐标下
            if (old_kf->sequence != cur_kf->sequence && sequence_loop[cur_kf->sequence] == 0)
            {  
                w_r_vio = shift_r;
                w_t_vio = shift_t;
                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)
                    {
                        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;
            }

把当前帧放到优化缓存buffer里去,等待optimize4DoF()的优化:

            //将当前帧放入优化队列中
            m_optimize_buf.lock();
            optimize_buf.push(cur_kf->index);
            m_optimize_buf.unlock();
        }
	}

最后,把当前位姿进行漂移矫正,并把当前帧存起来,当然,有一些用于可视化和对外发布的代码,我们把它们跳过:

	m_keyframelist.lock();
    Vector3d P;
    Matrix3d R;

    //获取VIO当前帧的位姿P、R,根据偏移量得到实际位姿
    cur_kf->getVioPose(P, R);
    P = r_drift * P + t_drift;
    R = r_drift * R;

    //更新当前帧的位姿P、R
    cur_kf->updatePose(P, R);

	keyframelist.push_back(cur_kf);
	m_keyframelist.unlock();
}

r_drift和w_drift从哪来的,我们后面分析。

9.3.1 回环检测detectLoop()

上一部分把posegraph.addKeyFrame()函数的主要流程讲了,现在继续抠里面一些函数的细节。detectLoop()的作用是找到当前帧的回环帧是哪一帧。

第一步:向词袋字典传入当前帧brief描述子,找到和当前帧最像的4个回环帧备胎,并且它们不能出现在与当前帧序号差50帧的范围内,搜索的结果返回给ret:

int PoseGraph::detectLoop(KeyFrame* keyframe, int frame_index)
{
    //查询字典数据库,得到与每一帧的相似度评分ret
    QueryResults ret;
    db.query(keyframe->brief_descriptors, ret, 4, frame_index - 50);

第二步:搜索完成后,把当前帧的描述子加入到词袋字典里:

    //添加当前关键帧到字典数据库中
    db.add(keyframe->brief_descriptors);
    
    bool find_loop = false;
    cv::Mat loop_result;

第三步:如果有评分>0.015的话,就认为有回环帧:

    //确保与相邻帧具有好的相似度评分
    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;
        }

第四步:如果有多个帧的评分都>0.015,那么选择其中最早的那一帧:

    //对于索引值大于50的关键帧才考虑进行回环
    //返回评分大于0.015的最早的关键帧索引min_index
    if (find_loop && frame_index > 50)
    {
        int min_index = -1;
        for (unsigned int i = 0; i < ret.size(); i++)
        {
            if (min_index == -1 || (ret[i].Id < min_index && ret[i].Score > 0.015))
                min_index = ret[i].Id;
        }
        return min_index;
    }
    else return -1;
}

9.3.2 找到当前帧和回环帧的相对位姿findConnection()

这个函数在keyFrame.cpp中,里面很多内容在论文里基本都浓墨重彩讲的介绍过。

第一步:定义了一些用于保存当前帧和回环帧中具有对应关系的点的容器,名为cur的容器对应当前帧,old对应回环帧。matched_2d保存像素坐标,matched_2d_norm保存归一化坐标,matched_3d是3D点,注意,它们的3D坐标系是cur帧的世界坐标系,也就是位姿矫正前的世界坐标系。名为matched用于保存匹配成功的点的相关坐标,序号是一一对应的。

bool KeyFrame::findConnection(KeyFrame* old_kf)
{
    TicToc tmp_t;
    //printf("find Connection\n");
    vector<cv::Point2f> matched_2d_cur, matched_2d_old;
    vector<cv::Point2f> matched_2d_cur_norm, matched_2d_old_norm;
    vector<cv::Point3f> matched_3d;
    vector<double> matched_id;
    vector<uchar> status;

    matched_3d = point_3d;
    matched_2d_cur = point_2d_uv;
    matched_2d_cur_norm = point_2d_norm;
    matched_id = point_id;

第二步,利用brief描述子找到当前帧和回环帧特征点的对应关系,然后去除匹配失败的点。

    //关键帧与回环帧进行BRIEF描述子匹配,剔除匹配失败的点
    searchByBRIEFDes(matched_2d_old, matched_2d_old_norm, status, old_kf->brief_descriptors, old_kf->keypoints, old_kf->keypoints_norm);
    reduceVector(matched_2d_cur, status);
    reduceVector(matched_2d_old, status);
    reduceVector(matched_2d_cur_norm, status);
    reduceVector(matched_2d_old_norm, status);
    reduceVector(matched_3d, status);
    reduceVector(matched_id, status);

匹配前,要知道两帧都是有各自特征点的,然后每一个特征点都有自己的描述子。searchByBRIEFDes()干的事情就是对于当前帧的每一个特征点,都和回环帧的每一个特征点进行描述子匹配,选择最接近的那一个,这样他俩就配对上了,它们就是同一个世界特征点在各自帧上的像素坐标。

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)
{
    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);
        // use cur descriptors to find corresponding keypoints in old frame
        if (searchInAera(window_brief_descriptors[i], descriptors_old, keypoints_old, keypoints_old_norm, pt, pt_norm))
          status.push_back(1);
        else
          status.push_back(0);
        matched_2d_old.push_back(pt);
        matched_2d_old_norm.push_back(pt_norm);
    }
}

window_brief_descriptors是当前帧特征点的描述子,如果在老帧里找到了对应特征点,那么就保存下它的像素坐标和归一化坐标,我们再看看searchInAera()是怎么做到对于当前帧某一个特征点的描述子,在老帧找到对应点的:
 

bool KeyFrame::searchInAera(const BRIEF::bitset window_descriptor,
                            const std::vector<BRIEF::bitset> &descriptors_old,
                            const std::vector<cv::KeyPoint> &keypoints_old,
                            const std::vector<cv::KeyPoint> &keypoints_old_norm,
                            cv::Point2f &best_match,
                            cv::Point2f &best_match_norm)
{
    cv::Point2f best_pt;
    int bestDist = 128;
    int bestIndex = -1;

    for(int i = 0; i < (int)descriptors_old.size(); i++)
    {

        int dis = HammingDis(window_descriptor, descriptors_old[i]);
        if(dis < bestDist)
        {
            bestDist = dis;
            bestIndex = i;
        }
    }
    //printf("best dist %d", bestDist);
    //找到汉明距离小于80的最小值和索引即为该特征点的最佳匹配
    if (bestIndex != -1 && bestDist < 80)
    {
      best_match = keypoints_old[bestIndex].pt;
      best_match_norm = keypoints_old_norm[bestIndex].pt;
      return true;
    }
    else
      return false;
}

实际上就是拿当前帧的当前点的描述子和回环帧的每一个点的描述子进行汉明距离的计算,选择最小距离的那个就OK了:

int KeyFrame::HammingDis(const BRIEF::bitset &a, const BRIEF::bitset &b)
{
    BRIEF::bitset xor_of_bitset = a ^ b;
    int dis = xor_of_bitset.count();
    return dis;
}

第三步,当前帧和回环帧的对应点找到了,相关的归一化坐标,像素坐标,世界坐标(cur帧的世界坐标系,有drift)也已知,接下来就是求当前帧和回环帧之间的相对位姿关系了。很多人都说这块是RANSAC PnP去除误匹配,的确是的,但是除此之外,还求出两帧之间的相对位姿变换。但是,求出的这个相对位姿变换,最后并不是用来求r_drift和t_drift,而是用来求w_r_vio和w_t_vio的,所以这个相对位姿没有那么的重要。

    Eigen::Vector3d PnP_T_old;  //回环帧在当前帧的世界坐标系下的位姿
    Eigen::Matrix3d PnP_R_old;  //回环帧在当前帧的世界坐标系下的位姿
    Eigen::Vector3d relative_t; //回环帧和当前帧的相对平移变换
    Quaterniond relative_q;     //回环帧和当前帧的相对旋转变换
    double relative_yaw;        //回环帧和当前帧的相对旋转变换中的yaw角分量

    //若达到最小回环匹配点数
    if ((int)matched_2d_cur.size() > MIN_LOOP_NUM)
    {
		//RANSAC PnP检测去除误匹配的点
		status.clear();
		PnPRANSAC(matched_2d_old_norm, matched_3d, status, PnP_T_old, PnP_R_old);
		reduceVector(matched_2d_cur, status);
		reduceVector(matched_2d_old, status);
		reduceVector(matched_2d_cur_norm, status);
		reduceVector(matched_2d_old_norm, status);
		reduceVector(matched_3d, status);
		reduceVector(matched_id, status);
	}

PnPRANSAC()利用当前帧世界坐标系下的3D点和回环帧对应点的归一化坐标,求出回环帧在当前帧世界坐标系下的位姿,首先把当前帧位姿从body->world系转到camera->world系下,作为回环帧位姿的初始估计:

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)
{
	//for (int i = 0; i < matched_3d.size(); i++)
	//	printf("3d x: %f, y: %f, z: %f\n",matched_3d[i].x, matched_3d[i].y, matched_3d[i].z );
	//printf("match size %d \n", matched_3d.size());
    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;
    Matrix3d R_w_c = origin_vio_R * qic;
    Vector3d T_w_c = origin_vio_T + origin_vio_R * tic;

    R_inital = R_w_c.inverse();
    P_inital = -(R_inital * T_w_c);

    cv::eigen2cv(R_inital, tmp_r);
    cv::Rodrigues(tmp_r, rvec);
    cv::eigen2cv(P_inital, t);

然后调用OpenCV的函数,直接求出回环帧位姿:

    cv::Mat inliers;
    solvePnPRansac(matched_3d, matched_2d_old_norm, K, D, rvec, t, true, 100, 10.0 / 460.0, 0.99, inliers);

标记出利用PnP RANSAC去除outliner后,还有哪些点活着,然后会利用status去除outliers:

    for (int i = 0; i < (int)matched_2d_old_norm.size(); i++)
        status.push_back(0);

    for( int i = 0; i < inliers.rows; i++)
    {
        int n = inliers.at<int>(i);
        status[n] = 1;
    }

最后求出回环帧在当前帧的世界坐标系下的位姿,是body->world的表示:

    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);

    PnP_R_old = R_w_c_old * qic.transpose();
    PnP_T_old = T_w_c_old - PnP_R_old * tic;
}

第四步,把相对位姿给到loop_info,注意了,此时loop_info保存的是当前帧与回环帧的相对位姿变换。

    //若达到最小回环匹配点数
    if ((int)matched_2d_cur.size() > MIN_LOOP_NUM)
    {
	relative_t = PnP_R_old.transpose() * (origin_vio_T - PnP_T_old);
	relative_q = PnP_R_old.transpose() * origin_vio_R;
	relative_yaw = Utility::normalizeAngle(Utility::R2ypr(origin_vio_R).x() - Utility::R2ypr(PnP_R_old).x());
	
	//相对位姿检验
	if (abs(relative_yaw) < 30.0 && relative_t.norm() < 20.0)
	{

	    has_loop = true;
	    loop_index = old_kf->index;
	    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;

第五步,把这些匹配点在回环帧上的坐标,回环帧的位姿(在他自己的世界坐标系下)发布出去,由vins_estimator订阅,用于计算r_drift和t_drift。这部分是一个复杂过程,请见9.4部分。

这一切完成之后,请回到9.3开头的部分,你会发现目前这个loop_info是用于计算w_r_vio和w_t_vio的。

9.4 位姿矫正矩阵r_drift和t_drift的计算/快速重定位

作者在代码里管这部分叫快速重定位,可能的确很快吧,但是我觉得这部分的作用是为了求出位姿矫正矩阵r_drift和t_drift的。我们回到9.3.2 的第五步,我们看看它是怎么做的。

9.4.1 初始数据由pose_graph发布

它把回环帧在自己世界坐标系的位姿(准),当前帧时间戳,RANSAC去除outliner后剩下的匹配的特征点的全局id,归一化坐标,封装到msg_match_points,由pub_match_points以话题"match_points"发布。


	    //快速重定位
	    if(FAST_RELOCALIZATION)
	    {
			sensor_msgs::PointCloud msg_match_points;
			msg_match_points.header.stamp = ros::Time(time_stamp);   //当前帧时间戳
			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());
			t_q_index.values.push_back(T.y());
			t_q_index.values.push_back(T.z());
			t_q_index.values.push_back(Q.w());
			t_q_index.values.push_back(Q.x());
			t_q_index.values.push_back(Q.y());
			t_q_index.values.push_back(Q.z());
			t_q_index.values.push_back(index);
			msg_match_points.channels.push_back(t_q_index);
			pub_match_points.publish(msg_match_points);
	    }

9.4.2 初始数据由vins_estimator订阅

在estimator_node.cpp中由sub_relo_points订阅,引发回调函数relocalization_callback(),这个函数会把9.4.1发布的数据保存到relo_buf里。在process()主线程中,它会取出relo_buf里最新的数据,给到estimator对象:

            //取出最后一个重定位帧
            while (!relo_buf.empty())
            {
                relo_msg = relo_buf.front();
                relo_buf.pop();
            }

            if (relo_msg != NULL)
            {
                vector<Vector3d> match_points;
                double frame_stamp = relo_msg->header.stamp.toSec();
                for (unsigned int i = 0; i < relo_msg->points.size(); i++)
                {
                    Vector3d u_v_id;
                    u_v_id.x() = relo_msg->points[i].x;
                    u_v_id.y() = relo_msg->points[i].y;
                    u_v_id.z() = relo_msg->points[i].z;
                    match_points.push_back(u_v_id);
                }
                Vector3d relo_t(relo_msg->channels[0].values[0], relo_msg->channels[0].values[1], relo_msg->channels[0].values[2]);
                Quaterniond relo_q(relo_msg->channels[0].values[3], relo_msg->channels[0].values[4], relo_msg->channels[0].values[5], relo_msg->channels[0].values[6]);
                Matrix3d relo_r = relo_q.toRotationMatrix();
                int frame_index;
                frame_index = relo_msg->channels[0].values[7];

                estimator.setReloFrame(frame_stamp, frame_index, match_points, relo_t, relo_r);
            }

进入到setReloFrame(),它把这些数据存到自己的内存里去:

void Estimator::setReloFrame(double _frame_stamp, int _frame_index, vector<Vector3d> &_match_points, Vector3d _relo_t, Matrix3d _relo_r)
{
    relo_frame_stamp = _frame_stamp; //pose_graph里要被重定位的关键帧的时间戳
    relo_frame_index = _frame_index; //回环帧在pose_graph里的id
    match_points.clear();
    match_points = _match_points;    //两帧共同的特征点在回环帧上的归一化坐标
    prev_relo_t = _relo_t;           //回环帧在自己世界坐标系的位姿(准)
    prev_relo_r = _relo_r;           //回环帧在自己世界坐标系的位姿(准)
    for(int i = 0; i < WINDOW_SIZE; i++) 
    {
        if(relo_frame_stamp == Headers[i].stamp.toSec())//如果pose_graph里要被重定位的关键帧还在滑窗里
        {
            relo_frame_local_index = i; //找到它的id
            relocalization_info = 1;    //告诉后端要重定位
            for (int j = 0; j < SIZE_POSE; j++)
                relo_Pose[j] = para_Pose[i][j]; //用这个关键帧的位姿(当前滑窗的世界坐标系下,不准)初始化回环帧的位姿
        }
    }
}

9.4.3 vins_estimator的后端求回环帧在当前帧世界坐标系下的位姿

数据现在estimator拿到了,那么依然是用最小化重投影误差的方式,即将滑窗内的3D特征点重投影到回环帧上来求回环帧在当前滑窗世界坐标系下的位姿,目的是让回环帧和重定位帧的位姿都在同一个世界坐标系下表示,从而求出这两帧之间的相对位姿,我们回到Estimator::optimization()下,梦开始的地方:

    //添加闭环检测残差
    if(relocalization_info)
    {
        ceres::LocalParameterization *local_parameterization = new PoseLocalParameterization();
        problem.AddParameterBlock(relo_Pose, SIZE_POSE, local_parameterization);
        int retrive_feature_index = 0;
        int feature_index = -1;
		//遍历滑窗内的每一个点
        for (auto &it_per_id : f_manager.feature)
        {
		    //更新这个点在滑窗内出现的次数
            it_per_id.used_num = it_per_id.feature_per_frame.size();
			
			//当前点至少被观测到2次,并且首次观测不晚于倒数第二帧
            if (!(it_per_id.used_num >= 2 && it_per_id.start_frame < WINDOW_SIZE - 2)) continue;
            ++feature_index;
            int start = it_per_id.start_frame;
			
			//当前这个特征点至少要在重定位帧被观测到
            if(start <= relo_frame_local_index)
            {
			    //如果都扫描到滑窗里第i个特征了,回环帧上还有id序号比i小,那这些肯定在滑窗里是没有匹配点的
                while((int)match_points[retrive_feature_index].z() < it_per_id.feature_id)
                {
                    retrive_feature_index++;
                }
				
				//如果回环帧某个特征点和滑窗内特征点匹配上了
                if((int)match_points[retrive_feature_index].z() == it_per_id.feature_id)
                {
				    //找到这个特征点在回环帧上的归一化坐标
                    Vector3d pts_j = Vector3d(match_points[retrive_feature_index].x(), match_points[retrive_feature_index].y(), 1.0);
                    //找到这个特征点在滑窗内首次出现的那一帧上的归一化坐标
					Vector3d pts_i = it_per_id.feature_per_frame[0].point;
                    //构造重投影的损失函数
                    ProjectionFactor *f = new ProjectionFactor(pts_i, pts_j);
					//构造这两帧之间的残差块
                    problem.AddResidualBlock(f, loss_function, para_Pose[start], relo_Pose, para_Ex_Pose[0], para_Feature[feature_index]);
                    //回环帧上的这个特征点被用上了,继续找下一个特征点
					retrive_feature_index++;
                }
            }
        }
    }

优化完成后,回环帧在当前帧世界坐标系下的位姿relo_Pose就求出来了,然后我们看Estimator::double2vector():

    // relative info between two loop frame
    if(relocalization_info)
    { 
		//回环帧在当前帧世界坐标系下的位姿
        Matrix3d relo_r;
        Vector3d relo_t;
        relo_r = rot_diff * Quaterniond(relo_Pose[6], relo_Pose[3], relo_Pose[4], relo_Pose[5]).normalized().toRotationMatrix();
        relo_t = rot_diff * Vector3d(relo_Pose[0] - para_Pose[0][0],
                                     relo_Pose[1] - para_Pose[0][1],
                                     relo_Pose[2] - para_Pose[0][2]) + origin_P0;
									 
		//回环帧在当前帧世界坐标系和他自己世界坐标系下位姿的相对变化,这个会用来矫正滑窗内世界坐标系
        double drift_correct_yaw;
        drift_correct_yaw = Utility::R2ypr(prev_relo_r).x() - Utility::R2ypr(relo_r).x();
        drift_correct_r = Utility::ypr2R(Vector3d(drift_correct_yaw, 0, 0));
        drift_correct_t = prev_relo_t - drift_correct_r * relo_t; 
		
		//回环帧和重定位帧相对的位姿变换
        relo_relative_t = relo_r.transpose() * (Ps[relo_frame_local_index] - relo_t);
        relo_relative_q = relo_r.transpose() * Rs[relo_frame_local_index];
        relo_relative_yaw = Utility::normalizeAngle(Utility::R2ypr(Rs[relo_frame_local_index]).x() - Utility::R2ypr(relo_r).x());
        relocalization_info = 0;
    }

9.4.4 获取drift矫正矩阵

这个drift_correct_r和drift_correct_t在vins_estimator文件夹下visualization.cpp的pubOdometry()函数使用,用于矫正滑窗内世界坐标系,而pubOdometry()会被estimator_node.cpp的process()主线程中被调用。

对于pose_graph而言,它关心relo_relative_t和relo_relative_q,这两个是在visualization.cpp的pubRelocalization()函数使用,用于矫正滑窗内世界坐标系,而pubRelocalization()也会被estimator_node.cpp的process()主线程中被调用:

void pubRelocalization(const Estimator &estimator)
{
    nav_msgs::Odometry odometry;
    odometry.header.stamp = ros::Time(estimator.relo_frame_stamp);
    odometry.header.frame_id = "world";
    odometry.pose.pose.position.x = estimator.relo_relative_t.x();
    odometry.pose.pose.position.y = estimator.relo_relative_t.y();
    odometry.pose.pose.position.z = estimator.relo_relative_t.z();
    odometry.pose.pose.orientation.x = estimator.relo_relative_q.x();
    odometry.pose.pose.orientation.y = estimator.relo_relative_q.y();
    odometry.pose.pose.orientation.z = estimator.relo_relative_q.z();
    odometry.pose.pose.orientation.w = estimator.relo_relative_q.w();
    odometry.twist.twist.linear.x = estimator.relo_relative_yaw;
    odometry.twist.twist.linear.y = estimator.relo_frame_index;

    pub_relo_relative_pose.publish(odometry); //话题"relo_relative_pose"
}

9.4.5 pose_graph订阅回环帧和重定位帧相对的位姿变换

细心的读者会发现,在9.1部分我有一个重要的回调函数没有讲,因为他是在这起作用的。

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

进入到这个回调函数:

//重定位回调函数,将重定位帧的相对位姿放入loop_info,updateKeyFrameLoop()进行回环更新
void relo_relative_pose_callback(const nav_msgs::Odometry::ConstPtr &pose_msg)
{
    Vector3d relative_t = Vector3d(pose_msg->pose.pose.position.x,
                                   pose_msg->pose.pose.position.y,
                                   pose_msg->pose.pose.position.z);
    Quaterniond relative_q;
    relative_q.w() = pose_msg->pose.pose.orientation.w;
    relative_q.x() = pose_msg->pose.pose.orientation.x;
    relative_q.y() = pose_msg->pose.pose.orientation.y;
    relative_q.z() = pose_msg->pose.pose.orientation.z;
    double relative_yaw = pose_msg->twist.twist.linear.x;
    int index = pose_msg->twist.twist.linear.y;
    //printf("receive index %d \n", index );
    Eigen::Matrix<double, 8, 1 > loop_info;
    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;
    posegraph.updateKeyFrameLoop(index, loop_info);
}

这个回调函数执行了个updateKeyFrameLoop(),你会发现漂移矫正矩阵r_drift和t_drift实际上是在这里更新的!到这里pose_graph最主要的算法就完成了。

void PoseGraph::updateKeyFrameLoop(int index, Eigen::Matrix<double, 8, 1 > &_loop_info)
{
    //更新当前帧与回环帧的相对位姿到当前帧的loop_info
    KeyFrame* kf = getKeyFrame(index);
    kf->updateLoop(_loop_info);

    if (abs(_loop_info(7)) < 30.0 && Vector3d(_loop_info(0), _loop_info(1), _loop_info(2)).norm() < 20.0)
    {
        if (FAST_RELOCALIZATION)
        {
            KeyFrame* old_kf = getKeyFrame(kf->loop_index);
            Vector3d w_P_old, w_P_cur, vio_P_cur;
            Matrix3d w_R_old, w_R_cur, vio_R_cur;
            old_kf->getPose(w_P_old, w_R_old);
            kf->getVioPose(vio_P_cur, vio_R_cur);

            Vector3d relative_t;
            Quaterniond relative_q;
            relative_t = kf->getLoopRelativeT();
            relative_q = (kf->getLoopRelativeQ()).toRotationMatrix();
            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; 
            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; 

            m_drift.lock();
            yaw_drift = shift_yaw;
            r_drift = shift_r;
            t_drift = shift_t;
            m_drift.unlock();
        }
    }
}

9.5 4DoF位姿图优化

整个工程里面,相对位姿的旋转都只选择了yaw,没有选择pitch和roll,因为IMU能够识别pitch和roll,所以矫正只针对剩下4个自由度。

第一步,取出buffer里第一帧,同时标记好最早的回环帧:

//四自由度位姿图优化
void PoseGraph::optimize4DoF()
{
    while(true)
    {
        int cur_index = -1;
        int first_looped_index = -1;

        //取出最新一个待优化帧作为当前帧
        m_optimize_buf.lock();
        while(!optimize_buf.empty())
        {
            cur_index = optimize_buf.front();
            first_looped_index = earliest_loop_index;
            optimize_buf.pop();
        }
        m_optimize_buf.unlock();

第二步,设置图优化的size:

        if (cur_index != -1)
        {
            printf("optimize pose graph \n");
            TicToc tmp_t;

            m_keyframelist.lock();
            KeyFrame* cur_kf = getKeyFrame(cur_index);

            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求解器:

            ceres::Problem problem;
            ceres::Solver::Options options;
            options.linear_solver_type = ceres::SPARSE_NORMAL_CHOLESKY;
            options.max_num_iterations = 5;
            ceres::Solver::Summary summary;
            ceres::LossFunction *loss_function;
            loss_function = new ceres::HuberLoss(0.1);
            ceres::LocalParameterization* angle_local_parameterization =
                AngleLocalParameterization::Create();

第三步,遍历位姿图的每一帧,但是只从第一个回环帧开始优化,对于被扫描的当前帧,拿到他的位姿,序列号,并构建参数块,对于处于第一个序列的帧或者是第一个回环帧,他们的参数块需要固定:

            list<KeyFrame*>::iterator it;
            int i = 0;
            for (it = keyframelist.begin(); it != keyframelist.end(); it++)
            {
                //找到第一个回环候选帧
                if ((*it)->index < first_looped_index)  continue;
                (*it)->local_index = i;
                Quaterniond tmp_q;
                Matrix3d tmp_r;
                Vector3d tmp_t;
                (*it)->getVioPose(tmp_t, tmp_r);
                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();
                euler_array[i][1] = euler_angle.y();
                euler_array[i][2] = euler_angle.z();

                sequence_array[i] = (*it)->sequence;

                problem.AddParameterBlock(euler_array[i], 1, angle_local_parameterization);
                problem.AddParameterBlock(t_array[i], 3);

                if ((*it)->index == first_looped_index || (*it)->sequence == 0)
                {   
                    problem.SetParameterBlockConstant(euler_array[i]);
                    problem.SetParameterBlockConstant(t_array[i]);
                }

第四步,如果当前帧和它前面紧挨着的5帧处于同一个序列中,那么当前帧和这5帧都设置一个残差块:

                //add edge
                for (int j = 1; j < 5; j++)
                {
                  if (i - j >= 0 && sequence_array[i] == sequence_array[i-j])
                  {
                    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]);
                    relative_t = q_array[i-j].inverse() * relative_t;
                    double relative_yaw = euler_array[i][0] - euler_array[i-j][0];
                    ceres::CostFunction* cost_function = FourDOFError::Create( relative_t.x(), relative_t.y(), relative_t.z(), relative_yaw, euler_conncected.y(), euler_conncected.z());
                    problem.AddResidualBlock(cost_function, NULL, euler_array[i-j],   t_array[i-j],  euler_array[i],   t_array[i]);
                  }
                }

第五步,如果当前帧有回环帧,那么这两帧之间也增加一个残差块:

                //add loop edge
                if((*it)->has_loop)
                {
                    assert((*it)->loop_index >= first_looped_index);
                    int connected_index = getKeyFrame((*it)->loop_index)->local_index;
                    Vector3d euler_conncected = Utility::R2ypr(q_array[connected_index].toRotationMatrix());
                    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],  t_array[i]);
                }
                
                if ((*it)->index == cur_index) break;
                i++;
            }
            m_keyframelist.unlock();

第六步,利用ceres求解,把优化后的位姿给到每一个参加优化的帧:

            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++;
            }

第七步,求出当前帧优化前后的位姿差异,这个差异再次更新给r_drift和t_drift

            Vector3d cur_t, vio_t;
            Matrix3d cur_r, vio_r;
            cur_kf->getPose(cur_t, cur_r);
            cur_kf->getVioPose(vio_t, vio_r);
            m_drift.lock();
            yaw_drift = Utility::R2ypr(cur_r).x() - Utility::R2ypr(vio_r).x();
            r_drift = Utility::ypr2R(Vector3d(yaw_drift, 0, 0));
            t_drift = cur_t - r_drift * vio_t;
            m_drift.unlock();

第八步,由于参与位姿图优化的最后一帧就是当前帧,所以对于时间戳更靠后的那些帧,再利用t_drift和r_drift进行位姿矫正:

            it++;
            for (; it != keyframelist.end(); it++)
            {
                Vector3d P;
                Matrix3d R;
                (*it)->getVioPose(P, R);
                P = r_drift * P + t_drift;
                R = r_drift * R;
                (*it)->updatePose(P, R);
            }
            m_keyframelist.unlock();
            updatePath();
        }
    }
}

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【SLAM】VINS-MONO解析——回环检测和重定位 的相关文章

  • linux C 遍历目录及其子目录 opendir -> readdir -> closedir

    在 linux 下遍历某一目录下内容 LINUX 下历遍目录的方法一般是这样的 xff1a 打开目录 gt 读取 gt 关闭目录 相关函数是 opendir gt readdir gt closedir xff0c 其原型如下 xff1a
  • 最全面的 linux 信号量解析

    一 xff0e 什么是信号量 信号量的使用主要是用来保护共享资源 xff0c 使得资源在一个时刻只有一个进程 xff08 线程 xff09 所拥有 信号量的值为正的时候 xff0c 说明它空闲 所测试的线程可以锁定而使用它 若为 0 xff
  • vue3学习一:let和const

    在函数的内部 xff0c 定义name变量 xff0c 当i等于false时 xff0c 按照Java语言 xff0c else里是拿不到name的 xff0c 并且会显示报错 xff0c 但是却不会报错 xff0c 这是因为javascr
  • PHP 中最全的设计模式(23种)

    PhpDesignPatterns PHP 中的设计模式 一 Introduction 介绍 设计模式 xff1a 提供了一种广泛的可重用的方式来解决我们日常编程中常常遇见的问题 设计模式并不一定就是一个类库或者第三方框架 xff0c 它们
  • 异常详细信息: System.NullReferenceException: 未将对象引用设置到对象的实例。

    我遇到的出现这种错误的原因一般是以下几种情况 xff1a 1 在绑定数据控件的时候 xff0c 建立数据库连接 OleDbConnection conn 61 new OleDbConnection 34 provider 61 micro
  • 连接不上Github,网络超时的检查和解决办法

    先附上图片吧 连接不上github但是其它网站都是正常上网 解决办法 win 43 r打开运行输入cmd 再命令行里输入 ping github com 看看返回值 我的全是请求超时 xff0c 发送的数据包全丢 xff08 我先ping了
  • 2018校招笔试真题汇总

    2018校招笔试真题汇总 最近看好多牛油贡献了很多考试的真题 xff0c 我把他们汇总在一起给到大家 xff0c 也感谢这些牛油的贡献 xff0c 只要进这个汇总贴的 xff0c 你们都将每人获得一份牛客送出的礼物一份 科大讯飞 xff1a
  • 【2】Docker的启动与停止

    1 xff09 启动 docker systemctl start docker 2 xff09 查看 docker 状态 systemctl status docker 3 xff09 查看 docker 概要信息 docker info
  • 【8】Docker中部署Redis

    1 xff09 拉取镜像 docker pull redis 这是我在 VMware 的 CentOS 中装过的 redis 版本 xff0c 拉取该指定版本使用 docker pull redis 5 0 12 命令 xff0c 不过下面
  • 操作系统学习之系统调用

    目录 一 操作系统学习之系统调用 1 什么是系统调用 2 系统调用有什么用 3 为什么需要系统调用 4 系统调用的具体流程 1 xff09 执行过程 2 如何实现用户态与内态之间的切换 3 系统调用常见名词 4 系统调用如何返回 传递返回值
  • 22-Docker-常用命令详解-docker pull

    常用命令详解 docker pull 前言docker pull语法格式options说明 使用示例未指定tag a 拉取所有 tagged 镜像 前言 本篇来学习docker pull命令 docker pull 作用 xff1a 从镜像
  • 720p,1080p对应像素解释

    720P是1280 720 61 921600 xff0c 即 分辨率为921600 xff0c 即大约92万像素 xff0c 921600接近100万像素 xff08 1280是按照16 9算出来的 xff0c 4 3的另算 xff0c
  • vue3学习二:模板字符串

    模板字符串是为了解决字符串拼接问题 xff0c 在es5中 xff0c 字符串拼接是这样的 xff1a let name 61 34 wjdsg 34 console log 34 您好 34 43 name 而在es6中可以使用模板字符串
  • Canal 读取 mysql bin_log

    场景 xff1a 在微服务开发的过程中多个项目协同完成一个功能 xff0c 工程与工程之间存在数据上的解耦 xff0c 底层服务为上层服务提供数据 而底层服务有需要对数据进行管理 解决方案 xff1a 基本底层服务 通过 canal 获取
  • PuTTY连接Linux服务器被拒绝问题

    PuTTY连接Linux服务器被拒绝问题 1 使用命令 xff1a ssh localhost 查看是否安装ssh1 2需要手动安装ssh1 2 1 输入命令 xff1a 1 2 2 若是出现下图所示 xff1a 1 2 3 查看进程 xf
  • 实时数据同步工具<Maxwell 操作案例>

    文章目录 案例一 xff1a 监控MySQL中的数据并输出到控制台案例二 xff1a Maxwell监控mysql的数据输出到kafka案例三 xff1a 监控MySQL指定表的数据并输出到kafka 案例一 xff1a 监控MySQL中的
  • Docker 镜像 Tag 管理

    Author xff1a rab 良好的镜像版本命名习惯能让我们更好的管理和使用镜像 xff08 如项目上线失败后可有效的进行版本回退等 xff09 xff0c 以下是 Docker 社区常用的 tag 方案 比如我现在已经构建了一个 co
  • APM与Pixhawk间的关系

    1 APM 本文APM指代 xff1a https github com ArduPilot ardupilot 2 Pixhawk 本文Pixhawk指代 xff1a https github com PX4 Firmware 3 关系
  • Pixhawk串口名称与硬件接口对应关系

    Pixhawk提供的串口较多 xff0c 通过ls dev 可以看到有如下7个tty设备 xff1a ttyACM0 ttyS0 ttyS1 ttyS2 ttyS3 ttyS4 ttyS5 ttyS6 但每个串口名称对应到Pixhawk硬件
  • Linux系统大小端判断

    大端模式 大端模式 xff0c 是指数据的低位保存在内存的高地址中 xff0c 而数据的高位保存在内存的低地址中 小端模式 小端模式 xff0c 是指数据的低位保存在内存的低地址中 xff0c 而数据的高位保存在内存的高地址中 判断程序 文

随机推荐

  • C preprocessor fails sanity check

    编译某一产品固件时 xff0c 遇到如下现象 xff1a checking how to run the C preprocessor opt mipsel 24kec linux uclibc bin mipsel 24kec linux
  • VLC同时开启播放多个视频流BAT脚本

    工作中 xff0c 难免会遇到要用同一个程序连续打开多个URL资源 路径的情况 xff0c 一个窗口一个窗口的启动效率太低 这里以VLC同时播放多个码流图像为例 xff0c 写个简单的BAT脚本 xff0c 供需要者参考 PS 1 使用方式
  • 【AI】Ubuntu14.04安装OpenCV3.2.0

    在ubuntu14 04系统上安装OpenCV3 2 0 环境要求 GCC 4 4 x or later CMake 2 8 7 or higher Git if failed you can replace it with git cor
  • 若依代码生成器(mybatis-plus)

    看这篇文章之前 xff0c 先去看一下我前面的文章 xff1a 若依前后端分离整合mybatis plus wjdsg的博客 CSDN博客 用过若依都知道 xff0c 若依自带的代码生成器 xff0c 是下载下来 xff0c 然后自己粘贴到
  • 【AI】基于OpenCV开发自定义程序编译方法

    基于OpenCV开发自定义程序编译方法 OpenCV自带的程序 xff0c 编译均采用cmake统一编译 若我们要基于OpenCV开发自己的程序 xff0c 如何快速编译 xff1f 本文以OpenCV库自带的facedetect cpp程
  • H3C SNMPv3 配置

    1 xff09 H3C SNMPv3 配置 snmp agent mib view included MIB 2 mib 2 noAuthNoPriv xff1a snmp agent group v3 mygroup read view
  • 【SLAM】VINS-MONO解析——综述

    目前网上有很多分析文章 xff0c 但是都只是一些比较基础的原理分析 xff0c 而且很多量 xff0c 虽然有推倒 xff0c 但是往往没有讲清楚这些量是什么 xff0c 为什么要有这些量 xff0c 这些量是从哪来的 xff0c 也没有
  • 【SLAM】VINS-MONO解析——前端

    各个部分的讲解如下链接 xff1a SLAM VINS MONO解析 综述 SLAM VINS MONO解析 feature tracker SLAM VINS MONO解析 IMU预积分 SLAM VINS MONO解析 vins est
  • 【SLAM】VINS-MONO解析——IMU预积分

    4 IMU预积分 IMU预积分主要干了2件事 xff0c 第一个是IMU预积分获得 值 xff0c 另一个是误差传递函数的获取 本部分的流程图如下图所示 各个部分的讲解如下链接 xff1a SLAM VINS MONO解析 综述 SLAM
  • 【SLAM】VINS-MONO解析——vins_estimator流程

    5 vins estimator 基本上VINS里面绝大部分功能都在这个package下面 xff0c 包括IMU数据的处理 前端 xff0c 初始化 我觉得可能属于是前端 xff0c 滑动窗口 后端 xff0c 非线性优化 后端 xff0
  • 【SLAM】VINS-MONO解析——初始化(理论部分)

    6 初始化 第一个问题 xff0c 为什么要初始化 xff1f 对于单目系统而言 xff0c 1 视觉系统只能获得二维信息 xff0c 损失了一维信息 深度 所以需要动一下 xff0c 也就是三角化才能重新获得损失的深度信息 xff1b 2
  • 【SLAM】VINS-MONO解析——初始化(代码部分)

    6 2 代码解析 这部分代码在estimator processImage 最后面 初始化部分的代码虽然生命周期比较短 xff0c 但是 xff0c 代码量巨大 xff01 主要分成2部分 xff0c 第一部分是纯视觉SfM优化滑窗内的位姿
  • 【SLAM】VINS-MONO解析——后端优化(理论部分)

    7 后端非线性优化 7 1 理论基础 7 1 1 bayes模型 xff0c 因子图和最小二乘 这一部分主要是对董靖博士在公开课 因子图的理论基础 上的回忆和总结 1 bayes模型 假设有黄色是机器人在不同时刻的位姿 xff0c 蓝色是机
  • 【SLAM】VINS-MONO解析——后端优化(代码部分)

    7 2 代码 在estimator cpp的processImage 的最后 xff0c 代码如下 xff1a span class token keyword else span span class token comment solv
  • 51单片机通过两个按键控制流水灯方向

    按键一接单片机P3 1 xff0c 按键2接P3 0 8个流水灯接P2口 以下是代码 xff1a include lt regx52 H gt include lt INTRINS H gt 延时函数 xff0c xms等于1 xff0c
  • 【SLAM】VINS-MONO解析——sliding window

    8 sliding window 8 1 理论基础 实际上 xff0c 这一部分跟后端非线性优化是一起进行的 xff0c 这一部分对应的非线性优化的损失函数的先验部分 理论基础部分的代码基本在第7章部分 8 1 1 上一次非线性优化结束 x
  • 【SLAM】VINS-Fusion解析——流程

    VINS Fusion分析 因为时间原因 xff0c 没有像vins mono看的和写的那么具体 有时间的话我会补充完整版 vins fusion不像mono那样有三个node xff0c 它只有一个node xff0c 在rosNodeT
  • 【SLAM】VINS-MONO解析——对vins-mono的一点小改动

    vins mono刷了三遍 xff0c 手写vio刷了两遍 xff0c SLAM十四讲刷了两三遍 xff0c 从一开始完全看不懂是啥 xff0c 不知道什么是SLAM xff0c 什么是VIO xff0c 什么是VINS xff0c 什么是
  • 【SLAM】VINS-MONO解析——基于vins-mono的双目slam系统开发

    这个系统是基于香港科技大学飞行机器人组的开源框架VINS Mono开发的 xff0c 原开源框架是针对单目SLAM 本双目SLAM系统是在原单目开源框架基础上的二次深度开发 xff0c 外部接口与原框架一致 这个项目是我的研究课题项目 xf
  • 【SLAM】VINS-MONO解析——回环检测和重定位

    9 回环检测与重定位 本部分内容涉及到的代码大部分在pose graph文件夹下 xff0c 少部分在vins estimator里 原创内容 xff0c 转载请先与我联系并注明出处 xff0c 谢谢 xff01 系列内容请点击 xff1a