VINS_Fusion回环检测学习笔记
创始人
2025-05-30 22:46:39
0

回环检测的任务主要是为了检测机器人是否到达之前相同的位置,并且消累计的误差。VINS_Fusion的回环检测和VINS_Mono基本相似。只是loop_fusion中读取的很多参数是直接设定好的。回环部分我们先从pose_graph_node.cpp开始。

一、pose_graph_node.cpp

1.1 读取参数

首先看主函数,主要开头是设置基本的参数和读取.yaml文件和字典文件等。这部分其实没有太多需要理解的,就是简单的读取参数和复制,并且完成了ROS节点的初始化工作。

ros::init(argc, argv, "loop_fusion");ros::NodeHandle n("~");posegraph.registerPub(n);//和mono不同之处是直接赋值VISUALIZATION_SHIFT_X = 0;VISUALIZATION_SHIFT_Y = 0;SKIP_CNT = 0;SKIP_DIS = 0;if(argc != 2){printf("please intput: rosrun loop_fusion loop_fusion_node [config file] \n""for example: rosrun loop_fusion loop_fusion_node ""/home/tony-ws1/catkin_ws/src/VINS-Fusion/config/euroc/euroc_stereo_imu_config.yaml \n");return 0;}string config_file = argv[1];printf("config_file: %s\n", argv[1]);cv::FileStorage fsSettings(config_file, cv::FileStorage::READ);if(!fsSettings.isOpened()){std::cerr << "ERROR: Wrong path to settings" << std::endl;}//设置可视化参数cameraposevisual.setScale(0.1);cameraposevisual.setLineWidth(0.01);std::string IMAGE_TOPIC;int LOAD_PREVIOUS_POSE_GRAPH;//与Mono不同直接不要判断是否回环ROW = fsSettings["image_height"];COL = fsSettings["image_width"];std::string pkg_path = ros::package::getPath("loop_fusion");string vocabulary_file = pkg_path + "/../support_files/brief_k10L6.bin";cout << "vocabulary_file" << vocabulary_file << endl;BRIEF_PATTERN_FILE = pkg_path + "/../support_files/brief_pattern.yml";cout << "BRIEF_PATTERN_FILE" << BRIEF_PATTERN_FILE << endl;int pn = config_file.find_last_of('/');std::string configPath = config_file.substr(0, pn);std::string cam0Calib;fsSettings["cam0_calib"] >> cam0Calib;std::string cam0Path = configPath + "/" + cam0Calib;printf("cam calib path: %s\n", cam0Path.c_str());// 和前面一样,生成一个相机模型回环读入的yaml文件m_camera = camodocal::CameraFactory::instance()->generateCameraFromYamlFile(cam0Path.c_str());fsSettings["image0_topic"] >> IMAGE_TOPIC;        fsSettings["pose_graph_save_path"] >> POSE_GRAPH_SAVE_PATH;fsSettings["output_path"] >> VINS_RESULT_PATH;fsSettings["save_image"] >> DEBUG_IMAGE;LOAD_PREVIOUS_POSE_GRAPH = fsSettings["load_previous_pose_graph"];VINS_RESULT_PATH = VINS_RESULT_PATH + "/vio_loop.csv";//保存路径文件std::ofstream fout(VINS_RESULT_PATH, std::ios::out);fout.close();int USE_IMU = fsSettings["imu"];posegraph.setIMUFlag(USE_IMU);fsSettings.release();if (LOAD_PREVIOUS_POSE_GRAPH)//如果有先前的位姿图{printf("load pose graph\n");m_process.lock();posegraph.loadPoseGraph();m_process.unlock();printf("load pose graph finish\n");load_flag = 1;}else       {printf("no previous pose graph\n");load_flag = 1;}

1.2 回调函数

这部分就是接收前端得到的地图点和特征点的话题,并且完成参数的赋值。

//vio回调函数根据pose_msg中的位姿得到imu位姿和cam位姿ros::Subscriber sub_vio = n.subscribe("/vins_estimator/odometry", 2000, vio_callback);//将image_msg放入到image_buf中,同时更具时间挫检测是否有新的图像序列ros::Subscriber sub_image = n.subscribe(IMAGE_TOPIC, 2000, image_callback);//图像位姿回调函数,把pose_msg放入到pose_bufros::Subscriber sub_pose = n.subscribe("/vins_estimator/keyframe_pose", 2000, pose_callback);//imu的外参回调函数ros::Subscriber sub_extrinsic = n.subscribe("/vins_estimator/extrinsic", 2000, extrinsic_callback);//地图的点云,存放到point_bufros::Subscriber sub_point = n.subscribe("/vins_estimator/keyframe_point", 2000, point_callback);//可视化处理ros::Subscriber sub_margin_point = n.subscribe("/vins_estimator/margin_cloud", 2000, margin_point_callback);

这里我们主挑出一个讲解,比如vio_callback这个回调函数,当接收到/usb_cam/image_raw这个话题时,就会调用回调函数。这里设计ROS的话题基本通讯不了解的可以查看我之前写的文章:ROS学习笔记——通讯机制_每日亿学的博客-CSDN博客

这部分的代码也很容易理解,就是定义和话题类型相同的对象,接收数据并且完成赋值。

void vio_callback(const nav_msgs::Odometry::ConstPtr &pose_msg)
{//ROS_INFO("vio_callback!");Vector3d vio_t(pose_msg->pose.pose.position.x, pose_msg->pose.pose.position.y, pose_msg->pose.pose.position.z);Quaterniond vio_q;vio_q.w() = pose_msg->pose.pose.orientation.w;vio_q.x() = pose_msg->pose.pose.orientation.x;vio_q.y() = pose_msg->pose.pose.orientation.y;vio_q.z() = pose_msg->pose.pose.orientation.z;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;nav_msgs::Odometry odometry;odometry.header = pose_msg->header;odometry.header.frame_id = "world";odometry.pose.pose.position.x = vio_t.x();odometry.pose.pose.position.y = vio_t.y();odometry.pose.pose.position.z = vio_t.z();odometry.pose.pose.orientation.x = vio_q.x();odometry.pose.pose.orientation.y = vio_q.y();odometry.pose.pose.orientation.z = vio_q.z();odometry.pose.pose.orientation.w = vio_q.w();pub_odometry_rect.publish(odometry);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, pose_msg->header);}

1.3 创建线程

回环部分的核心内容就是在process这个线程中了。

    std::thread measurement_process;std::thread keyboard_command_process;measurement_process = std::thread(process);keyboard_command_process = std::thread(command);

二、process函数

2.1 时间戳对齐

一开始我们读取了,原始图像、前端的位姿和地图点。但是对应的时间戳并没有对齐,因为我们得到的位姿都是上一个原图处理的,所以位姿和地图点的时间都晚于原图的时间。所以VINS中一开始就是基于时间戳的大小来对齐,如果原图的时间撮大于则删除这帧的图像。

if(!image_buf.empty() && !point_buf.empty() && !pose_buf.empty()&&!Mono_buf.empty()){// 原图时间戳比另外两个晚,只能扔掉早于第一个原图的消息if (image_buf.front()->header.stamp.toSec() > pose_buf.front()->header.stamp.toSec()){pose_buf.pop();printf("throw pose at beginning\n");}else if (image_buf.front()->header.stamp.toSec() > point_buf.front()->header.stamp.toSec()){point_buf.pop();printf("throw point at beginning\n");}// 上面确保了image_buf <= point_buf && image_buf <= pose_buf// 下面根据pose时间找时间戳同步的原图和地图点else if (image_buf.back()->header.stamp.toSec() >= pose_buf.front()->header.stamp.toSec() && point_buf.back()->header.stamp.toSec() >= pose_buf.front()->header.stamp.toSec()){pose_msg = pose_buf.front();    // 取出来posepose_buf.pop();while (!pose_buf.empty())   // 清空所有的pose,回环的帧率慢一些没关系pose_buf.pop();// 找到对应pose的原图while (image_buf.front()->header.stamp.toSec() < pose_msg->header.stamp.toSec()){image_buf.pop();}Mono_msg = Mono_buf.front();//传递符合的一帧if(!Mono_buf.empty())Mono_buf.pop();     //移动到下一帧// 找到对应的地图点while (point_buf.front()->header.stamp.toSec() < pose_msg->header.stamp.toSec())point_buf.pop();point_msg = point_buf.front();point_buf.pop();}

2.2 降采样

回环检测频率不需要很高,所以每帧的间隔需要一定的时间。首先一开始有一个简单的降频,SKIP_FIRST_CNT为10,skip_first_cnt加到大于10就向下。但是,skip_first_cnt它没有地方置位为0!之后skip_cnt < SKIP_CNT这里在初始化开始skip_cnt和SKIP_CNT都是0意味着skip_cnt要加到溢出才能进入下面内容!这里如果你希望关键帧之间间隔时间加长可以在skip_cnt < SKIP_CNT将skip_first_cnt置位并且更改SKIP_FIRST_CNT大小。

if (skip_first_cnt < SKIP_FIRST_CNT){skip_first_cnt++;continue;}if (skip_cnt < SKIP_CNT)// 降频,每隔SKIP_CNT帧处理一次超过SKIP_CNT=0{skip_cnt++;continue;}else{skip_cnt = 0;}// 通过cvbridge得到opencv格式的图像cv_bridge::CvImageConstPtr ptr;

完成降采样之后,就是对地图点和位姿进行赋值了,之后给关键帧进行初始化。

三、创建KeyFrame

创建关键帧需时间戳,关键帧序号,位姿、原始图像、地图点、特征点。

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的构造函数,基本就是对关键帧类中的成员变量进行赋值。主要重要的函数为:

computeWindowBRIEFPoint()和computeBRIEFPoint()。

// create keyframe online
/*** @brief Construct a new Key Frame:: Key Frame object,创建一个KF对象,计算已有特征点的描述子,同时额外提取fast角点并计算描述子* * @param[in] _time_stamp KF的时间戳* @param[in] _index KF的索引* @param[in] _vio_T_w_i vio节点中的位姿* @param[in] _vio_R_w_i * @param[in] _image 对应的原图* @param[in] _point_3d KF对应VIO节点中的世界坐标* @param[in] _point_2d_uv 像素坐标* @param[in] _point_2d_norm 归一化相机坐标* @param[in] _point_id 地图点的idx* @param[in] _sequence 序列号*/
// create keyframe online
KeyFrame::KeyFrame(double _time_stamp, int _index, Vector3d &_vio_T_w_i, Matrix3d &_vio_R_w_i, cv::Mat &_image,vector &_point_3d, vector &_point_2d_uv, vector &_point_2d_norm,vector &_point_id, int _sequence)
{time_stamp = _time_stamp;//传递时间戳  单目相机的时间戳使用cam0的时间戳index = _index;	//索引vio_T_w_i = _vio_T_w_i;vio_R_w_i = _vio_R_w_i;		//转化到转化到w->iT_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;	//地图点的idxhas_loop = false;	// 注意默认还没有检测到回环 这里可以修改是否产生回环loop_index = -1;	//和那一帧发生回环has_fast_point = false;loop_info << 0, 0, 0, 0, 0, 0, 0, 0;sequence = _sequence;		//注意序列号暂时和单目相机同步 computeWindowBRIEFPoint();	//计算描述子   因为mono没有前端所以不提取特征点描述子computeBRIEFPoint();		//提取fast角点和描述子if(!DEBUG_IMAGE){image.release();}}

3.1 计算关键帧描述子

这里就是计算之前对齐时间戳的特征点进行描述子提取,BRIEF_PATTERN_FILE在main函数参数读取时已经玩出赋值。之后将之前的角点转化为opencv的keypoint类,之后调用extractor的仿函数(如果不太了解这个可以理解为把类的名字当作函数来使用,其实就是重构了()运算符号)。

/*** @brief 计算已有特征点的描述子* */
void KeyFrame::computeWindowBRIEFPoint()
{// 定义一个描述子计算的对象BriefExtractor extractor(BRIEF_PATTERN_FILE.c_str());//point_2d_uv检测到角点的像素坐标,这循环相当于转化为opencv之后的keypointfor(int i = 0; i < (int)point_2d_uv.size(); i++){cv::KeyPoint key;	//转化为opencv的格式key.pt = point_2d_uv[i];	// 像素坐标用来计算描述子window_keypoints.push_back(key);	// 转成opencv格式的特征点坐标}//注意window_brief_descriptor输出的描述子//注意window_keypoints为当前帧的特征点extractor(image, window_keypoints, window_brief_descriptors);
}

进入这个仿函数,就是直接调用DBOW的接口函数从而来计算描述子。 

/// @brief 提取描述子
/// @param im 输入图像
/// @param keys 角点
/// @param descriptors 输出的描述子
void BriefExtractor::operator() (const cv::Mat &im, vector &keys, vector &descriptors) const
{//调用dbow的接口计算描述子m_brief.compute(im, keys, descriptors);
}

3.3  额外提取FAST角点和描述子

VINS中为了防止前端传递特征点数目过少,这里格外检测FAST角点并且计算它的描述子。代码中直接调用opencv中的cv::FAST检测,并且将检测的角点归一化存储。

// 额外提取fast特征点并计算描述子
void KeyFrame::computeBRIEFPoint()
{//同样的步骤提取描述子文件BriefExtractor extractor(BRIEF_PATTERN_FILE.c_str());const int fast_th = 20; //注意 corner detector response threshold这里可以修改阈值if(1)cv::FAST(image, keypoints, fast_th, true);//提取fast特征点else{vector tmp_pts;cv::goodFeaturesToTrack(image, tmp_pts, 500, 0.01, 10);for(int i = 0; i < (int)tmp_pts.size(); i++){cv::KeyPoint key;key.pt = tmp_pts[i];keypoints.push_back(key);}}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);}
}

这里就完成了关键帧的初始化。之后开始检测回环。

相关内容

热门资讯

A股“苹果产业链”巨头冲刺港交... 来源:大河财立方 【大河财立方消息】A股市值近3000亿元的“苹果产业链”巨头——立讯精密拟登陆港交...
为正生物IPO辅导:80后女总... 瑞财经 刘治颖 近日,厦门为正生物科技股份有限公司(以下简称:为正生物)披露关于向不特定合格投资者公...
沪深300ETF指数基金:7月... 证券之星消息,7月24日,沪深300ETF指数基金(515390)融资买入224.73万元,融资偿还...
泰鹏智能:7月24日融资买入9... 证券之星消息,7月24日,泰鹏智能(873132)融资买入94.8万元,融资偿还71.38万元,融资...
宁波银行业绩快报:上半年净利润... 7月24日晚间,宁波银行对外披露2025年半年度业绩快报,公司上半年营业收入371.60亿元,同比增...
我省油气田企业增值税管理办法出... 为加强石油天然气企业增值税征收管理,根据《财政部 税务总局关于印发〈油气田企业增值税管理办法〉的通知...
深圳能源2025年度科技创新大... 7月24日,深圳能源召开2025年度科技创新大会。受访单位供图 深圳能源成立科技创新咨询委员会。受...
期货市场沉淀资金创7783亿历... 近期期货市场资金流入呈现加速态势,沉淀资金总量达到近7783亿元的历史新高。这一数据自6月25日以来...
经学与中国古典学的关系 □景海峰 “以小学为经学”和“即经学史以为经学”的现状 经学曾经是儒学的主干形式,自晚清瓦解以来,已...
价格法修正草案公布,强调“反内... 政策面再出“反内卷”利好,7月24日,《中华人民共和国价格法修正草案》公开征求意见:明确不正当价格行...
原创 特... 关税风暴:特朗普贸易战的全球冲击波 8月1日,一场由美国总统特朗普发起的全球贸易战即将进入高潮。看似...
2024消费品上市公司研究报告... 《2024消费品上市公司研究报告》 (内容出品方:和君咨询x新华网) 报告共计:40页 本研报通过对...
特斯拉Q2营收下滑12%,马斯... 特斯拉于7月24日公布了2025年第二季度财报,数据显示总营收降至225.0亿美元,同比下滑12%;...
衣食住行跟我逛|“今年更甜!”... 盛夏时节,誉称为“紫水晶”的黑葡萄迎来最佳赏味期。近日,扬子晚报/紫牛新闻记者走访苏州市场发现,多数...
棕榈油与油菜籽:印尼库存降,马... 【印尼5月末棕榈油库存下滑,马棕7月上旬增产,加菜籽预估调整】印尼棕榈油协会数据显示,因出口激增,印...
康佳易主华润 半导体业务整合成... 康佳集团股份有限公司是深圳首家营业收入超百亿元的工业企业。 来源:康佳集团 靴子落地。在长达三个多月...
英特尔营收超预期,宣布裁员,C... 英特尔数据中心+AI收入超预期难掩盈利困境,英特尔CEO未能证明公司将扭亏为盈,股价应声下跌。 周四...
我国新药好药呈现快速增长态势:... 央视网消息:国家药监局最新统计显示,我国上半年批准创新药43个,同比增长59%,接近2024年批准创...
赛峰集团宣布完成对柯林斯宇航飞... 赛峰集团宣布完成对柯林斯宇航(Collins Aerospace)飞行控制与作动业务的收购。该业务为...
中国最大农业互联网公司即将登录... 来源:ACN亚太商讯 近日,中国领先的农产品B2B数字化服务公司一亩田集团向美国证券交易委员会(SE...