VINS-MONO代码解读6----pose_graph

开始pose_graph部分,本部分记住一句话无论是快速重定位还是正常重定位,求出 T w 1 w 2 T_{w_1w_2} Tw1w2就是终极目标。

还剩一个整体Pipeline~~

1. pose_graph_node.cpp

注意,定义全局变量时即实例化了一个对象

PoseGraph posegraph; //定义全局的位姿图优化对象

加载配置
订阅topic及对应的callback()
注册publisher
定义主线程和键盘线程,
若需要loop则
取出对应的msg,时间戳对齐
最重要的是新建KF,添加KF到posegraph

2. pose_graph.cpp

2.1 回环检测

int PoseGraph::detectLoop(KeyFrame* keyframe, int frame_index)
相关的debug语句就不进行介绍。

  1. 查询字典,给出查询结果ret,并将描述子添加到数据库中,db.query()的传参见注释。
    //first query; then add this frame into database!QueryResults ret;TicToc t_query;//查询字典数据库,得到与每一帧的相似度评分ret//param:描述子;返回结果(主要是Entry id和相似度评分Score);最多返回几个;<=max_id的才被返回db.query(keyframe->brief_descriptors, ret, 4, frame_index - 50);TicToc t_add;//描述子加到数据库中db.add(keyframe->brief_descriptors);
  1. 判定是否找到loop
    //至少两个,且第1个的score>0.05才可能是loopif (ret.size() >= 1 &&ret[0].Score > 0.05)for (unsigned int i = 1; i < ret.size(); i++){//if (ret[i].Score > ret[0].Score * 0.3)//后面只要再有一个score>0.015的就判定找到了loopif (ret[i].Score > 0.015){          find_loop = true;//
  1. 返回 50帧之后,最远的,score>0.015的结果
    //只有50帧之后才返回loop查询结果if (find_loop && frame_index > 50){int min_index = -1;for (unsigned int i = 0; i < ret.size(); i++){//找最远的,且score>0.015的结果if (min_index == -1 || (ret[i].Id < min_index && ret[i].Score > 0.015))min_index = ret[i].Id;}return min_index;}elsereturn -1;

2.2 PnP 求解相对位姿

bool KeyFrame::findConnection(KeyFrame* old_kf)
其中关键函数是searchByBRIEFDes()PnPRANSAC()
需要说明,searchByBRIEFDes()找到的是 i i i帧的2D点,而PnPRANSAC()最终输出在PnP_T_old, PnP_R_old中的是 T w 2 b i T_{w_2b_i} Tw2bi

//关键帧WINDOW内的描述子与回环帧的描述子(brief_descriptors,不知道后面有没有把WINDOW内的描述子合并到这里面来)进行BRIEF描述子匹配,剔除匹配失败的点
searchByBRIEFDes(matched_2d_old, matched_2d_old_norm, status, old_kf->brief_descriptors, old_kf->keypoints, old_kf->keypoints_norm);......//如果能匹配的特征点能达到最小回环匹配个数,则用RANSAC PnP检测再去除误匹配的点,求出的是(PnP_T_old, PnP_R_old)=Tw2_bi
if ((int)matched_2d_cur.size() > MIN_LOOP_NUM)
{status.clear();PnPRANSAC(matched_2d_old_norm, matched_3d, status, PnP_T_old, PnP_R_old);reduceVector(matched_2d_cur, status);//根据status去除outlierreduceVector(matched_2d_old, status);reduceVector(matched_2d_cur_norm, status);reduceVector(matched_2d_old_norm, status);reduceVector(matched_3d, status);reduceVector(matched_id, status);
......   
}

T w 2 b i T_{w_2b_i} Tw2bi这个量是不是很熟悉?对,这就是estimator中的relo_Pose,有了这个量,就能求出 T b i b j T_{b_ib_j} Tbibj,存在loop_info中,后面就可以算出 T w 1 w 2 T_{w_1w_2} Tw1w2执行矫正了:

  //相对位姿检验
if ((int)matched_2d_cur.size() > MIN_LOOP_NUM)
{//Tbi_bj=Tw2_bi^(-1) * Tw2_bjrelative_t = PnP_R_old.transpose() * (origin_vio_T - PnP_T_old);relative_q = PnP_R_old.transpose() * origin_vio_R;//Rbi_bj.yawrelative_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;//更新this KFde loop_info  Tbi_bj,后面要用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;//快速重定位:组装msg,发送match_points topic,estimator接收到该消息后进行快速重定位if(FAST_RELOCALIZATION)
......balabala

addKeyFrame()中,拿loop_info中的 T b i b j T_{b_ib_j} Tbibj计算出 T w 1 w 2 T_{w_1w_2} Tw1w2并执行矫正,全部变换到 w 1 w_1 w1系下:

    //若存在回环候选帧if (loop_index != -1){//printf(" %d detect loop with %d \n", cur_kf->index, loop_index);//获得回环帧KeyFrame* old_kf = getKeyFrame(loop_index);//PnP 求解Tbi_w2,进而求出Tbi_bj存放于loop_info中if (cur_kf->findConnection(old_kf)){//更新最早的回环帧indexif (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);//获得Tw1_bicur_kf->getVioPose(vio_P_cur, vio_R_cur);//Tw2_bj//relative是Tbi_bj=Told_curVector3d relative_t;Quaterniond relative_q;//读取Tbi_bjrelative_t = cur_kf->getLoopRelativeT();//这个loop_info是在findConnection()中更新的relative_q = (cur_kf->getLoopRelativeQ()).toRotationMatrix();//Tw1_bj = Tw1_bi * Tbi_bjw_P_cur = w_R_old * relative_t + w_P_old;w_R_cur = w_R_old * relative_q;//回环得到的位姿和VIO位姿之间的偏移量Tw1_w2double shift_yaw;Matrix3d shift_r;Vector3d shift_t;//Tw1_w2shift_yaw = Utility::R2ypr(w_R_cur).x() - Utility::R2ypr(vio_R_cur).x();shift_r = Utility::ypr2R(Vector3d(shift_yaw, 0, 0));//tw1_bj - Rw1_bj * Rw2_bj^(-1) * tw2_bj = tw1_w2shift_t = w_P_cur - w_R_cur * vio_R_cur.transpose() * vio_P_cur;// shift vio pose of whole sequence to the world frame// 如果存在多个图像序列,则将所有图像序列都从w2系拉回到w1系下if (old_kf->sequence != cur_kf->sequence && sequence_loop[cur_kf->sequence] == 0){  w_r_vio = shift_r;w_t_vio = shift_t;//Tw1_bj = Tw1_w2 * Tw1_bj,执行relocationvio_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++)   {//TODO:这sequence到底是啥意思?是说跟着loop上的帧后面还有一些帧,都需要relo回来吗?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;}//将当前帧放入优化队列中m_optimize_buf.lock();optimize_buf.push(cur_kf->index);m_optimize_buf.unlock();}}

3. keyframe.cpp/h

构建了两个类:

  • BriefExtractor:用于计算BRIEF描述子
  • KeyFrame:用于对KF进行相关管理(重点)
  1. BriefExtractor类主要使用node中加载的描述子的pattern来计算BRIEF描述子,其中重载了()运算符
class BriefExtractor
{
public://读取 构建字典时使用的相同的Brief模板文件,构造BriefExtractordvirtual void operator()(const cv::Mat &im, vector<cv::KeyPoint> &keys, vector<BRIEF::bitset> &descriptors) const;//运算符重载了“()”来计算描述子。BriefExtractor(const std::string &pattern_file);DVision::BRIEF m_brief;
};

  1. KeyFrame类定义了一些函数,包括计算描述子,进行匹配,PnP计算relative pose等。

构造时会分别调用以下函数计算WINDOW内已有的features的描述子,新提features、去畸变、计算描述子。均使用到了BriefExtractor中重载的()运算符,函数较简单,不进行进一步解释。

computeWindowBRIEFPoint();//计算窗口中所有特征点的描述子
computeBRIEFPoint();//对新KF再重新检出500个FAST角点,并计算所有特征点的描述子,用于loop detection

3.1 快速重定位(涉及pose_graph、estimator两个模块)

这部分稍微有些复杂,同时涉及到pose_graph和estimator两个模块,当时讲到estimator后端求解时在构建系统整体的ResidualBlock时遇到过,当时没有具体讲解,现在有了pose_graph的基础后对此部分进行详细探究。
rqt_graph中可以看出estimator接收了来自于pose_graph的topic:match_points
在这里插入图片描述
崔华坤的PDF:
在这里插入图片描述

本部分难点有二:

  1. 理解pose_graph和estimator中关于快速重定位msg的数据结构和组织形式;
  2. 理解estimator中的选点策略。

和前面一样,针对loop,old帧记为 i i i帧,cur帧记为 j j j帧。

findConnection()函数中PnPRANSAC最后会输出 T w b i T_{wb_i} Twbi,存放于PnP_T_old, PnP_R_old中,在FAST_RELOCALIZATION之前会计算出relative pose T b i b j T_{b_ib_j} Tbibj,然后开始组装并pub match_points的msg:
其中

  • msg_match_points.header.stamp j j j帧的时间戳,用于重定位时与WINDOW内的
  • msg_match_points.points前两维存的是loop上的点在 i i i帧中的归一化坐标的xy,z是该feature的id
  • T,Q i i i帧的pose: T w b i T_{wb_i} Twbi,在setReloFrame/estimator.cpp中赋给prev_relo_t, prev_relo_q
//keyframe.cpp//快速重定位:组装msg,发送match_points topic,estimator接收到该消息后进行快速重定位if(FAST_RELOCALIZATION){sensor_msgs::PointCloud msg_match_points;//特征点之间的匹配//这个是cur帧的时间戳,estimator靠该时间戳在WINDOW中找到需要重定位的帧,通过该时间戳和old_frame的index来将old与cur建立联系msg_match_points.header.stamp = ros::Time(time_stamp);//msg的point是归一化的xy和landmark的idfor (int i = 0; i < (int)matched_2d_old_norm.size(); i++){geometry_msgs::Point32 p;p.x = matched_2d_old_norm[i].x;//i帧的归一化xyp.y = matched_2d_old_norm[i].y;p.z = matched_id[i];msg_match_points.points.push_back(p);}//Tw_biEigen::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);}

estimator_node.cpp中接收到match_points的msg之后执行回调relocalization_callback将消息存入relo_buf,在主线程process中处理relo_buf中的数据,并setReloFrame()设置重定位帧。其中

  • match_points中存的是 i , j i,j i,j两帧loop上的feature在 i i i帧中的归一化xy,z是feature id,在optimization中选点时需要使用。
//estimator_node.cpp// 取relo_buf中最新的帧,设置重定位用的回环帧// set relocalization framesensor_msgs::PointCloudConstPtr relo_msg = NULL;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;//i帧归一化xyu_v_id.y() = relo_msg->points[i].y;u_v_id.z() = relo_msg->points[i].z;//feature idmatch_points.push_back(u_v_id);}//Tw_bi(i就是old帧,理解为loop frame v)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];//old frame的帧号estimator.setReloFrame(frame_stamp, frame_index, match_points, relo_t, relo_r);//设置重定位帧(relo包括loop detection等操作)}//estimator.cpp
void Estimator::setReloFrame(double _frame_stamp, int _frame_index, vector<Vector3d> &_match_points, Vector3d _relo_t, Matrix3d _relo_r)
{relo_frame_stamp = _frame_stamp;//与old frame loop上的WINDOW内的帧(j帧)的时间戳relo_frame_index = _frame_index;//j帧的帧号match_points.clear();match_points = _match_points;//i帧中与j帧中match上的点在i帧中的归一化(x,y,id)//Tw1_bi=Tw_b_oldprev_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())//{relo_frame_local_index = i;//j帧在WINDOW中的下标relocalization_info = 1;for (int j = 0; j < SIZE_POSE; j++)relo_Pose[j] = para_Pose[i][j];//将WINDOW内用于relo的pose赋值给relo_Pose}}
}

在后端求解的optimization()函数中处理了上面设置的reloframe,其中,选点策略见下图,用到了match_pointsz()保存的feature_id。

该部分的ResidualBlock目的是为了求 T w 1 b i T_{w_1b_i} Tw1bi,这里pts_j是i帧的归一化平面上的点,这里理解relo_Pose极其重要,relo_Pose实际上是 T w 2 b i T_{w_2b_i} Tw2bi,视觉重投影是从WINDOW内的start帧的camera(在w2系下),投影到i帧(在w1系下),relo_Pose中耦合了从 w 2 w_2 w2系到 w 1 w_1 w1系下的变换 T w 1 w 2 T_{w_1w_2} Tw1w2,而我们在pose_graph中是需要求出 T w 1 w 2 T_{w_1w_2} Tw1w2的。无论是快速重定位还是正常重定位,求出 T w 1 w 2 T_{w_1w_2} Tw1w2就是终极目标。

在这里插入图片描述

    //4.添加闭环检测残差,计算滑动窗口中与每一个闭环关键帧的相对位姿,这个相对位置是为后面的图优化(pose graph)准备 或者是 快速重定位(崔华坤PDF7.2节)//这里注意relo_pose是Tw2_bi = Tw2_w1 * Tw1_biif(relocalization_info){//printf("set relocalization factor! \n");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();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)//必须之前看到过{//1.先在i中match的点中找到可能是现在这个feature的id的indexwhile((int)match_points[retrive_feature_index].z() < it_per_id.feature_id)//.z()存的是i,j两帧match上的feature的id{retrive_feature_index++;}//2.如果是,则WINDOW内的it_per_id.feature_id这个id的landmark就是被loop上的landmark,取归一化坐标,if((int)match_points[retrive_feature_index].z() == it_per_id.feature_id){//pts_j是i帧的归一化平面上的点,这里理解relo_Pose及其重要,relo_Pose实际上是Tw2_bi,视觉重投影是从WINDOW内的start帧的camera(在w2系下),投影到i帧(在w1系下),耦合了Tw1_w2Vector3d 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;//start中的点ProjectionFactor *f = new ProjectionFactor(pts_i, pts_j);//relo_Pose是Tw2_biproblem.AddResidualBlock(f, loss_function, para_Pose[start], relo_Pose, para_Ex_Pose[0], para_Feature[feature_index]);retrive_feature_index++;}     }}}

double2vector()中计算了 T w 1 w 2 T_{w_1w_2} Tw1w2 T b i b j T_{b_ib_j} Tbibj,为什么叫快速重定位?就是因为此时在estimator中一轮优化之后得到了 T w 2 b i T_{w_2b_i} Tw2bi(即relo_Pose),就可以用 T w 2 b i T_{w_2b_i} Tw2bi T w 1 b i T_{w_1b_i} Tw1bi(即prev_relo_r, prev_relo_t)来算出 T w 1 w 2 T_{w_1w_2} Tw1w2,求出了终极目标就可以执行重定位了(在pubOdometry()中)。

需要注意,pitch和roll因为是可观的,所以在estimator中一直都有优化,所以我们重定位只需要矫正yaw和t即可。代码注释和计算方法如下:

// relative info between two loop frame
if(relocalization_info)
{//按照WINDOW内第一帧的yaw角变化对j帧进行矫正Matrix3d relo_r;//j帧矫正之后的TVector3d 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;//保证第[0]帧不变之后,+origin_P0转为世界系下的t//由于pitch和roll是可观的,所以我们在BA中一直都在优化pitch和roll,但由于yaw不可观,//所以即使漂了,可能还是满足我们BA的最优解,所以需要手动进行矫正//prev_relo_r=Tw1_bi, relo_Pose=Tw2_bi,这两个pose间的yaw和t的漂移Rw1_w2,tw1_w2double drift_correct_yaw;//yaw drift, Rw1_bi.yaw() - Rw2_bi.yaw=Rw1_w2.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));//tw1_w2drift_correct_t = prev_relo_t - drift_correct_r * relo_t;//Tw2_bi^(-1) * Tw2_bj = Tbi_bjrelo_relative_t = relo_r.transpose() * (Ps[relo_frame_local_index] - relo_t);relo_relative_q = relo_r.transpose() * Rs[relo_frame_local_index];//Rw2_bj.yaw() - Rw2_bi.yaw() = Rbi_bj.yaw()relo_relative_yaw = Utility::normalizeAngle(Utility::R2ypr(Rs[relo_frame_local_index]).x() - Utility::R2ypr(relo_r).x());relocalization_info = 0;    
}

在这里插入图片描述

在这里插入图片描述

pubOdometry()中的快速重定位

//重定位时计算出的Tw1_w2,把现在的拉回到之前的上面去
Vector3d correct_t;
Vector3d correct_v;
Quaterniond correct_q;
//Tdrift_correct_r = T_w1_w2把world系的漂移拉回去
correct_t = estimator.drift_correct_r * estimator.Ps[WINDOW_SIZE] + estimator.drift_correct_t;
correct_q = estimator.drift_correct_r * estimator.Rs[WINDOW_SIZE];
odometry.pose.pose.position.x = correct_t.x();
odometry.pose.pose.position.y = correct_t.y();
odometry.pose.pose.position.z = correct_t.z();
odometry.pose.pose.orientation.x = correct_q.x();
odometry.pose.pose.orientation.y = correct_q.y();
odometry.pose.pose.orientation.z = correct_q.z();
odometry.pose.pose.orientation.w = correct_q.w();

loop_info更新的几个地方:

  • KeyFrame()的第2个构造函数,在load pose garph时会使用
  • updateKeyFrameLoop中,在estimator完成FAST_RELOCATION后发送msg,pose_graph线程接收到msg后的回调函数中调用updateKeyFrameLoop更新
  • findConnection中更新,在找到KF之后求 T b i b j T_{b_ib_j} Tbibj时会更新

3.2 4DoF pose_graph优化

此部分和重定位跑在两个不同的线程

(1). 4DOF优化的残差边的构建,这里面包括两类,一是序列边(即每个关键帧和时序上前四帧之间的相对Pose),二是回环边(即回环检测成功产生的相对Pose);
(2). 另外需要注意的是,最早回环检测成功的回环关键帧之前的历史关键帧对此处的Pose_graph优化没有太大的帮助(很直观的理解);
(3). 还需要注意的是要考虑线程异步的问题,从代码中可以发现,在Pose_graph优化后只是更新关键帧列表中各个关键帧的T_w_i和R_w_i,并不直接改变关键帧的vio_T_w_i和vio_R_w_i,主要是因为若直接修正关键帧的vio_T_w_i和vio_R_w_i可能会导致VIO后端优化时出现异常,特别是当存在比较大的位姿调整时这个问题更加明显。

4DoF PoseGraph可以这样理解:观测和预测实际上计算方法是一样的,观测使用了yaw,pitch,roll,tx,ty,tz,而预测中,pitch和roll不能动(因为estimator已经在优化了),所以只能调整yaw,tx,ty,tz来减小residual。这样来理解pose graph就好理解了。


factor构建部分,其中angle_local_parameterization是定义的yaw的计算更新方式(具体解释见ceres博客4.1.2节),本部分BA使用的是autodiff。

  problem.AddParameterBlock(euler_array[i], 1, angle_local_parameterization);problem.AddParameterBlock(t_array[i], 3);//第1次loop上的帧和sequence=0的帧的欧拉角和t不优化if ((*it)->index == first_looped_index || (*it)->sequence == 0){   problem.SetParameterBlockConstant(euler_array[i]);problem.SetParameterBlockConstant(t_array[i]);}//add sequential edge 找i前面的4帧[i-j],计算(b[i-j])tb[i-j]_b[i], (w)Rb[i-j]_b[i].yaw都是从[i-j]<-[i]和论文顺序相反for (int j = 1; j < 5; j++){if (i - j >= 0 && sequence_array[i] == sequence_array[i-j]){//Rw_b[i-j]Vector3d euler_conncected = Utility::R2ypr(q_array[i-j].toRotationMatrix());//tw_b[i] - tw_b[i-j] = (w)tb[i-j]_b[i]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]);//计算观测t_diff:Rw_b[i-j]^(-1) * (w)tb[i-j]_b[i] = (b[i-j])tb[i-j]_b[i]relative_t = q_array[i-j].inverse() * relative_t;//计算观测yaw_diff:Rw_b[i].yaw - Rw_b[i-j].yaw = (w)Rb[i-j]_b[i].yaw*(是个标量,也无所谓参考系,只是知道从[i]到[i-j]的yaw即可)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 edgeif((*it)->has_loop){//loop上的帧号肯定大于第一次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();//Tbibj的tdouble relative_yaw = (*it)->getLoopRelativeYaw();//Tbibj的yaw(i,j按照loop的理解)//构建pose_graph factor//注意,ceres里面构建的Rwi是由Create()中的euler_conncected.y(), euler_conncected.z()和AddResidualBlock中的euler_array[connected_index]构建的ceres::CostFunction* cost_function = FourDOFWeightError::Create( relative_t.x(), relative_t.y(), relative_t.z(),relative_yaw, euler_conncected.y(), euler_conncected.z());//分别是loop边和序列边(sequential edge)problem.AddResidualBlock(cost_function, loss_function, euler_array[connected_index], t_array[connected_index], euler_array[i], t_array[i]);}

其中关于残差边的构建,我对于此处world系的理解:优化参数在给初值时的world是 w 2 w_2 w2,在优化后由于保持了全局一致性,就变为 w 1 w_1 w1,代码和论文中residual构建的方向相反,论文中translation是“小-大”,代码中确是“大-小”,不过只要外层观测relative_t,relative_yaw和内层operator()中的预测t_i_ij保持一致就没有问题(都是“大-小”)。

//4为residual,输入参数分别为1(yaw_i),3(tw2_i),1(yaw_j),3(tw2_j),注意这里的w在优化前是w2,在优化后由于保持了全局一致性,就变为w1
static ceres::CostFunction* Create(const double t_x, const double t_y, const double t_z,const double relative_yaw, const double pitch_i, const double roll_i) 
{//分别是sequential和loop边的yaw(1)和t(3)的residualreturn (new ceres::AutoDiffCostFunction<FourDOFError, 4, 1, 3, 1, 3>(new FourDOFError(t_x, t_y, t_z, relative_yaw, pitch_i, roll_i)));
}

优化完之后,就变为 T w 1 b j T_{w1_bj} Tw1bj,与优化前的 T w 2 b j T_{w_2b_j} Tw2bj一起计算出 T w 1 w 2 T_{w_1w_2} Tw1w2即可进行全局pose矫正,注意更新的是VIO pose还是非VIO pose。

  1. 这里仍然使用loop中的叫法: i i i帧为old帧, j 帧 j帧 j为WINDOW内的loop帧。
    solve之后更新了pose graph中所有的pose的成员T_w_i, R_w_i,并未更新vio_T_w_i, vio_R_w_i,这是因为优化后,即进行了重定位,如果有漂移的话,会被拉回去,导致world系改变,从 w 2 w_2 w2变为 w 1 w_1 w1,所以updatePose更新之后,此时*it中的T_w_i, R_w_i即为 T w 1 b j T_{w_1b_j} Tw1bj,而vio_T_w_i, vio_R_w_i仍然是 T w 2 b j T_{w_2b_j} Tw2bj,这样就可以更新 T w 1 w 2 T_{w_1w_2} Tw1w2了。

  2. 更新 T w 1 w 2 T_{w_1w_2} Tw1w2之后就可以矫正keyframelist中的当前优化帧之后的帧的非VIO pose。

  3. 最后updatePath,遍历keyframelist,获取矫正之后的非VIO pose,并pub出去。

//这里计算的应该是Tw1_w2
Vector3d cur_t, vio_t;
Matrix3d cur_r, vio_r;
cur_kf->getPose(cur_t, cur_r);//pose graph优化后的pose Tw1_bj
cur_kf->getVioPose(vio_t, vio_r);//优化前的pose  Tw2_bj
m_drift.lock();
yaw_drift = Utility::R2ypr(cur_r).x() - Utility::R2ypr(vio_r).x();//Rw1_bj.yaw - Rw2_bj.yaw= Rw1_w2.yaw
r_drift = Utility::ypr2R(Vector3d(yaw_drift, 0, 0));
t_drift = cur_t - r_drift * vio_t;//(w1)tw1_bj - Rw1_w2 * (w2)tw2_bj = (w1)tw1_w2
m_drift.unlock();//更新重定位帧之后的KF的非VIO pose
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();

至此,pose_graph的整体工作就完成,而VINS-MONO的整个框架解读也就完成了。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C++之模板

目录 泛型编程 模板 函数模板 函数模板的实例化 隐式实例化 显示实例化 类模板 我们知道STL&#xff08;标准模板库&#xff09;是C学习的精华所在&#xff0c;在学习STL之前我们得先学习一个新的知识点-------模板。那么模板究竟是什么呢&#xff1f;围绕着这个问题&a…

绘图示例---QT手动调用绘图事件,按钮控制图片

效果&#xff1a; 点击 “移动” 图片向右移动20&#xff0c;点击 “西理win嘛” 图片每秒向右移动20 QQ录屏20231212164128 下面时代码详解&#xff1a; 注意使用UI和代码实现按钮的不同 UI: ui->pushButton->setGeometry(windowWidth-105, windowHeight-25, 100, 20);…

【思考】只有实对称矩阵才能正交对角化吗?【矩阵的合同】

1&#xff1a;命题改写&#xff08;A可以正交对角化&#xff09; 2&#xff1a;左乘Q右乘Q逆&#xff08;Q转置&#xff09; 3&#xff1a;取转置 4&#xff1a;得证 总结 可以看到&#xff0c;矩阵如果可以正交对角化&#xff0c;那么一定是实对称矩阵。 另外&#xff0c;这…

【期末复习向】长江后浪推前浪之ChatGPT概述

参考文章&#xff1a;GPT系列模型技术路径演进-CSDN博客 这篇文章讲了之前称霸NLP领域的预训练模型bert&#xff0c;它是基于预训练理念&#xff0c;采用完形填空和下一句预测任务2个预训练任务完成特征的提取。当时很多的特定领域的NLP任务&#xff08;如情感分类&#xff0c…

LLM之Agent(六)| 使用AutoGen、LangChian、RAG以及函数调用构建超级对话系统

本文我们将尝试AutoGen集成函数调用功能。函数调用最早出现在Open AI API中&#xff0c;它允许用户调用外部API来增强系统的整体功能和效率。例如&#xff0c;在对话过程中根据需要调用天气API。 函数调用和Agent有各种组合&#xff0c;在这里我们将通过函数调用调用RAG检索增强…

【SpringBoot】配置文件

配置文件官网 1. 配置方式 application.propertiesapplication.yml / application.yaml 2. 自定义配置信息 将实体类中的本应该写死的信息写在属性配置文件中。 可以使用 Value("${键名}") 获取&#xff0c;也可以使用 ConfigurationProperties(prefix"前…

访谈型软文写作方式,媒介盒子告诉你

访谈型软文一般用于维护企业形象&#xff0c;分享品牌故事。但是许多企业在写访谈型软文时经常容易跑偏或者写来写去没有逻辑&#xff0c;今天媒介盒子就来和大家分享访谈型软文的写作方式&#xff0c;看完这四点&#xff0c;小白也能写好访谈型软文&#xff01; 一、 访谈对象…

C++_构造函数与析构函数

目录 1、构造函数的写法 1.2 构造函数优化写法 2、默认构造函数与默认成员函数 2.1 默认成员函数对不同类型的处理 3、对内置类型的补丁 4、析构函数 4.1 析构函数的写法 5、默认析构函数 6、初始化列表 6.1 初始化列表的写法 6.2 初始化列表的作用 6.3 回顾与总结 …

【Proteus仿真】【51单片机】电子门铃设计

文章目录 一、功能简介二、软件设计三、实验现象联系作者 一、功能简介 本项目使用Proteus8仿真51单片机控制器&#xff0c;使共阴数码管&#xff0c;按键、无源蜂鸣器等。 主要功能&#xff1a; 系统运行后&#xff0c;数码管默认显示第一种门铃音调&#xff0c;可通过K1键切…

flutter的SingleChildScrollView控件详解

文章目录 SingleChildScrollView的介绍和使用场景详细介绍 SingleChildScrollView的介绍和使用场景 SingleChildScrollView 是 Flutter 中的一个小部件&#xff0c;用于创建一个可滚动的单个子部件。它通常用于处理内容超出屏幕可见区域的情况&#xff0c;允许用户通过滚动来查…

5.3 Linux DNS 服务

1、概念介绍 DNS&#xff08;Domain Name System&#xff09;域名系统&#xff0c;是互联网的一项核心服务&#xff0c;可以作为域名和IP地址相互映射的一个分布式数据库&#xff0c;提供域名与IP地址的解析服务&#xff0c;能够使人们更加方便的使用域名访问互联网而不是记住…

Vue3-11- 【v-for】循环数组

v-for的基本介绍 v-for 是一个指令&#xff0c; 它是用来在 html 模板中实现循环的。它可以循环 普通的数组、也可以直接循环一个范围值&#xff0c;也可以循环对象的每个属性。v-for 的语法介绍 <div v-for"(item,index) in arrayName" : key"index"…

有哪些好用的运维管理软件?哪个工单管理系统的操作简单一些?

运维管理软件可以帮助企业更有效地管理公司内外的事务&#xff0c;比如现在不少公司就引入了工单管理系统来处理后勤和售后的事务。那么&#xff0c;有哪些好用的运维管理软件&#xff1f;哪个的操作简单一些呢&#xff1f;   随着技术的发展和成熟&#xff0c;现在的工单管理…

vue_域名部署无法访问后端

前言 目前部署的比较另类&#xff0c;因为服务器为windows&#xff0c;目前还不是很会nginx&#xff0c;所以现在就只能在服务器上安装nodejs&#xff0c;然后直接使用npm run dev命令行的方式运行项目 遇到的坑 使用ip访问前端的时候&#xff0c;就可以访问&#xff0c;但是…

压缩照片怎么压缩?半分钟解决!

有时候我们在平台上传照片的时候&#xff0c;会有图片大小限制&#xff0c;想要将照片压缩到限制的大小范围内&#xff0c;可以使用专业的图片压缩软件、图片处理软件或者在线网站压缩&#xff0c;下面给大家分享三个方法&#xff0c;压缩照片的同时还能保持图片清晰度哦&#…

Windows10安装Node.js环境

Windows10安装Node.js环境 文章目录 1.下载安装包2.安装配置2.1安装2.2 配置全局的安装路径和缓存路径2.3配置环境变量2.4配置镜像源2.5包管理工具 3.查看版本4.编译跑项目5.总结 1.下载安装包 官方下载网址如下&#xff1a; https://nodejs.org/enInstaller表示是安装程序&a…

【回眸】Tessy 单元测试软件使用指南(三)怎么打桩和指针测试

目录 前言 Tessy 如何进行打桩操作 普通桩 高级桩 手写桩 Tessy单元测试之指针相关测试注意事项 有类型的指针&#xff08;非函数指针&#xff09;&#xff1a; 有类型的函数指针&#xff1a; void 类型的指针&#xff1a; 结语 前言 进行单元测试之后&#xff0c;但凡…

LED透镜粘接UV胶是一种特殊的UV固化胶

LED透镜粘接UV胶是一种特殊的UV固化胶&#xff0c;用于固定和粘合LED透镜。 它具有以下特点&#xff1a; 1. 高透明度&#xff1a;LED透镜粘接UV胶具有高透明度&#xff0c;可以确保光线的透过性&#xff0c;不影响LED的亮度和效果。 2. 快速固化&#xff1a;经过UV紫外线照射…

HTTP 404错误:页面未找到,如何解决

在互联网上浏览时&#xff0c;偶尔会遇到“HTTP 404错误&#xff1a;页面未找到”的提示。这通常意味着用户尝试访问的网页不存在或无法找到。本文将探讨HTTP 404错误的原因以及如何解决这个问题。 一、HTTP 404错误的原因 HTTP 404错误可能是由多种原因引起的。以下是一些常…

SQL进阶理论篇(五):什么是Hash索引

文章目录 简介MySQL中的Hash索引与B树的区别总结参考文献 简介 hash&#xff0c;即哈希&#xff0c;也被称为是散列函数。 Hash在数据库中的应用&#xff0c;可以帮助我们大幅度提升检索数据的效率。 大名鼎鼎的MD5其实就是Hash函数的一种变体。 Hash算法&#xff0c;是通过…