代码解读 | VINS 视觉前端
本文作者是计算机视觉life公众号成员蔡量力,由于格式问题部分内容显示可能有问题,更好的阅读体验,请查看原文链接:代码解读 | VINS 视觉前端
vins前端概述
在搞清楚VINS前端之前,首先要搞清楚什么是SLAM前端?
SLAM的前端、后端系统本身没有特别明确的划分,但是在实际研究中根据处理的先后顺序一般认为特征点提取和跟踪为前端部分,然后利用前端获取的数据进行优化、回环检测等操作,从而将优化、回环检测等作为后端。
而在VINS_MONO中将视觉跟踪模块(feature_trackers)为其前端。在视觉跟踪模块中,首先,对于每一幅新图像,KLT稀疏光流算法对现有特征进行跟踪。然后,检测新的角点特征以保证每个图像特征的最小数目,并设置两个相邻特征之间像素的最小间隔来执行均匀的特征分布。接着,将二维特征点去畸变,然后在通过外点剔除后投影到一个单位球面上。最后,利用基本矩阵模型的RANSAC算法进行外点剔除。
VINS_MONO原文中还将关键帧的选取作为前端分,本文暂不讨论, 后续文章会详细介绍。
VINS-Mono将前端封装为一个ROS节点,该节点的实现在feature_tracker目录下的src中,src里共有3个头文件和3个源文件:
feature_tracker_node.cpp构造了一个ROS节点feature_tracker_node,该节点订阅相机图像话题数据后,提取特征点,然后用KLT光流进行特征点跟踪。feature_tracker节点将跟踪的特征点作为话题进行发布,供后端ROS节点使用。同时feature_tracker_node还会发布标记了特征点的图片,可供Rviz显示以供调试。如下表所示:
操作 话题 消息类型 功能 Subscribe image sensor_msgs::ImageConstPtr 订阅原始图像,传给回调函数 Publish feature sensor_msgs::PointCloud 跟踪的特征点,供后端优化使用 Publish feature_img sensor_msgs::Image 跟踪特征点图片,输出给RVIZ,调试用 feature_tracker.h和feature_tracker.cpp实现了一个类FeatureTracker,用来完成特征点提取和特征点跟踪等主要功能,该类中主要函数和实现的功能如下:
函数 功能 bool inBorder() 判断跟踪的特征点是否在图像边界内 void reduceVector() 去除无法跟踪的特征点 void FeatureTracker::setMask() 对跟踪点进行排序并去除密集点 void FeatureTracker::addPoints() 添将新检测到的特征点n_pts void FeatureTracker::readImage() 对图像使用光流法进行特征点跟踪 void FeatureTracker::rejectWithF() 利用F矩阵剔除外点 bool FeatureTracker::updateID() 更新特征点id void FeatureTracker::readIntrinsicParameter() 读取相机内参 void FeatureTracker::showUndistortion() 显示去畸变矫正后的特征点 void FeatureTracker::undistortedPoints() 对角点进行去畸变矫正,并计算每个角点的速度 tic_toc.h中是作者自己封装的一个类TIC_TOC,用来计时;
parameters.h和parameters.cpp处理前端中需要用到的一些参数;
流程图

代码解读
feature_tracker_node系统入口main() 函数:
ROS初始化和输出调试信息:
//ros初始化和设置句柄
ros::init(argc, argv, "feature_tracker");
ros::NodeHandle n("~");
//设置logger的级别。 只有级别大于或等于level的日志记录消息才会得到处理。
ros::console::set_logger_level(ROSCONSOLE_DEFAULT_NAME, ros::console::levels::Info);
读取配置参数:
//读取config->euroc->euroc_config.yaml中的一些配置参数
readParameters(n);
读取相机内参读取每个相机对应内参,单目时NUM_OF_CAM=1:
for (int i = 0; i < NUM_OF_CAM; i++)
trackerData[i].readIntrinsicParameter(CAM_NAMES[i]);
判断是否加入鱼眼mask来去除边缘噪声
订阅话题IMAGE_TOPIC,当有图像进来的时候执行回调函数:
ros::Subscriber sub_img = n.subscribe(IMAGE_TOPIC, 100, img_callback);
将处理完的图像信息用PointCloud实例feature_points和Image的实例ptr消息类型,发布到"feature"和"feature_img"的topic
pub_img = n.advertise<sensor_msgs::PointCloud>("feature", 1000);
pub_match = n.advertise<sensor_msgs::Image>("feature_img",1000);
pub_restart = n.advertise<std_msgs::Bool>("restart",1000);
回调函数imf_callback
判断是否为第一帧,若为第一帧,将该帧的时间赋给 first_image_time和last_image_time ,然后返回
if(first_image_flag)
{
first_image_flag = false;
first_image_time = img_msg->header.stamp.toSec();//记录图像帧的时间
last_image_time = img_msg->header.stamp.toSec();
return;
}
通过判断时间间隔,有问题则restart
if (img_msg->header.stamp.toSec() - last_image_time > 1.0 || img_msg->header.stamp.toSec() < last_image_time)
发布频率控制(不是每来一张图像都要发布,但是都要传入readImage()进行处理),保证每秒钟处理的图像不超过FREQ,此处为每秒10帧
if (round(1.0 * pub_count / (img_msg->header.stamp.toSec() - first_image_time)) <= FREQ)
{
PUB_THIS_FRAME = true;
// 时间间隔内的发布频率十分接近设定频率时,更新时间间隔起始时刻,并将数据发布次数置0
if (abs(1.0 * pub_count / (img_msg->header.stamp.toSec() - first_image_time) - FREQ) < 0.01 * FREQ)
{
first_image_time = img_msg->header.stamp.toSec();
pub_count = 0;
}
}
else
PUB_THIS_FRAME = false;
将图像编码8UC1转换为mono8
处理图片:readImage()
判断是否显示去畸变矫正后的特征点
更新全局ID,将新提取的特征点赋予全局id
for (unsigned int i = 0;; i++)
{
bool completed = false;
for (int j = 0; j < NUM_OF_CAM; j++)
if (j != 1 || !STEREO_TRACK)
completed |= trackerData[j].updateID(i);
if (!completed)
break;
}
将特征点id,矫正后归一化平面的3D点(x,y,z=1),像素2D点(u,v),像素的速度(vx,vy),封装成sensor_msgs::PointCloudPtr类型的feature_points实例中,发布到pub_img,将图像封装到cv_bridge::cvtColor类型的ptr实例中发布到pub_match
发布消息的数据:
pub_img.publish(feature_points)
pub_match.publish(ptr->toImageMsg())
readimage()
判断EQUALIZE的值,决定是否对图像进行直方图均衡化处理:createCLAHE()
若为第一次读入图片,则:prev_img = cur_img = forw_img = img;若不是第一帧,则:forw_img = img,其中cur_img 和 forw_img 分别是光流跟踪的前后两帧,forw_img 才是真正的当前帧,cur_img 实际上是上一帧,prev_img 是上一次发布的帧。
prev_img = cur_img = forw_img = img;//避免后面使用到这些数据时,它们是空的
调用 cv::calcOpticalFlowPyrLK()进行光流跟踪,跟踪前一帧的特征点 cur_pts 得到 forw_pts,根据 status 把跟踪失败的点剔除(注意 prev, cur, forw, ids, track_cnt都要剔除),而且还需要将跟踪到图像边界外的点剔除。
cv::calcOpticalFlowPyrLK(cur_img, forw_img, cur_pts, forw_pts, status, err, cv::Size(21, 21), 3);
判断是否需要发布该帧图像:
否(PUB_THIS_FRAME=0):当前帧 forw 的数据赋给上一帧 cur,然后在这一步就结束了。
是(PUB_THIS_FRAME=0):
调用rejectWithF()对prev_pts和forw_pts做RANSAC剔除outlier,函数里面主要是调用了cv::findFundamentalMat() 函数,然后将然后所有剩下的特征点的 track_cnt 加1,track_cnt数值越大,说明被追踪得越久。
void FeatureTracker::rejectWithF()
{
if (forw_pts.size() >= 8)
{
ROS_DEBUG("FM ransac begins");
TicToc t_f; vector<cv::Point2f> un_cur_pts(cur_pts.size()), un_forw_pts(forw_pts.size());
for (unsigned int i = 0; i < cur_pts.size(); i++)
{ Eigen::Vector3d tmp_p;
//根据不同的相机模型将二维坐标转换到三维坐标
m_camera->liftProjective(Eigen::Vector2d(cur_pts[i].x, cur_pts[i].y), tmp_p);
//转换为归一化像素坐标
tmp_p.x() = FOCAL_LENGTH * tmp_p.x() / tmp_p.z() + COL / 2.0;
tmp_p.y() = FOCAL_LENGTH * tmp_p.y() / tmp_p.z() + ROW / 2.0;
un_cur_pts[i] = cv::Point2f(tmp_p.x(), tmp_p.y()); m_camera->liftProjective(Eigen::Vector2d(forw_pts[i].x, forw_pts[i].y), tmp_p);
tmp_p.x() = FOCAL_LENGTH * tmp_p.x() / tmp_p.z() + COL / 2.0;
tmp_p.y() = FOCAL_LENGTH * tmp_p.y() / tmp_p.z() + ROW / 2.0;
un_forw_pts[i] = cv::Point2f(tmp_p.x(), tmp_p.y());
} vector<uchar> status;
//调用cv::findFundamentalMat对un_cur_pts和un_forw_pts计算F矩阵
cv::findFundamentalMat(un_cur_pts, un_forw_pts, cv::FM_RANSAC, F_THRESHOLD, 0.99, status);
int size_a = cur_pts.size();
reduceVector(prev_pts, status);
reduceVector(cur_pts, status);
reduceVector(forw_pts, status);
reduceVector(cur_un_pts, status);
reduceVector(ids, status);
reduceVector(track_cnt, status);
ROS_DEBUG("FM ransac: %d -> %lu: %f", size_a, forw_pts.size(), 1.0 * forw_pts.size() / size_a);
ROS_DEBUG("FM ransac costs: %fms", t_f.toc());
}
}
调用setMask()函数,先对跟踪到的特征点 forw_pts 按照跟踪次数降序排列(认为特征点被跟踪到的次数越多越好),然后遍历这个降序排列,对于遍历的每一个特征点,在 mask中将该点周围半径为 MIN_DIST=30 的区域设置为 0,在后续的遍历过程中,不再选择该区域内的点。
在mask中不为0的区域,调用goodFeaturesToTrack提取新的角点n_pts,通过addPoints()函数push到forw_pts中,id初始化-1,track_cnt初始化为1(由于跟踪过程中,上一帧特征点由于各种原因无法被跟踪,而且为了保证特征点均匀分布而剔除了一些特征点,如果不补充新的特征点,那么每一帧中特征点的数量会越来越少)。
cv::goodFeaturesToTrack(forw_img, n_pts, MAX_CNT - forw_pts.size(), 0.01, MIN_DIST, mask);
调用undistortedPoints() 函数根据不同的相机模型进行去畸变矫正和深度归一化,计算速度。
reference
https://github.com/QingSimon/VINS-Mono-code-annotation/blob/master/VINS-Mono%E8%AF%A6%E8%A7%A3.pdf
https://qingsimon.github.io/post/
关注公众号,点击“学习圈子”,“SLAM入门“”,从零开始学习三维视觉核心技术SLAM,3天内无条件退款。早就是优势,学习切忌单打独斗,这里有教程资料、练习作业、答疑解惑等,优质学习圈帮你少走弯路,快速入门!

推荐阅读
如何从零开始系统化学习视觉SLAM?
从零开始一起学习SLAM | 为什么要学SLAM?
从零开始一起学习SLAM | 学习SLAM到底需要学什么?
从零开始一起学习SLAM | SLAM有什么用?
从零开始一起学习SLAM | C++新特性要不要学?
从零开始一起学习SLAM | 为什么要用齐次坐标?
从零开始一起学习SLAM | 三维空间刚体的旋转
从零开始一起学习SLAM | 为啥需要李群与李代数?
从零开始一起学习SLAM | 相机成像模型
从零开始一起学习SLAM | 不推公式,如何真正理解对极约束?
从零开始一起学习SLAM | 神奇的单应矩阵
从零开始一起学习SLAM | 你好,点云
从零开始一起学习SLAM | 给点云加个滤网
从零开始一起学习SLAM | 点云平滑法线估计
从零开始一起学习SLAM | 点云到网格的进化
从零开始一起学习SLAM | 理解图优化,一步步带你看懂g2o代码
从零开始一起学习SLAM | 掌握g2o顶点编程套路
从零开始一起学习SLAM | 掌握g2o边的代码套路
从零开始一起学习SLAM | 用四元数插值来对齐IMU和图像帧
零基础小白,如何入门计算机视觉?
SLAM领域牛人、牛实验室、牛研究成果梳理
我用MATLAB撸了一个2D LiDAR SLAM
可视化理解四元数,愿你不再掉头发
最近一年语义SLAM有哪些代表性工作?
视觉SLAM技术综述
汇总 | VIO、激光SLAM相关论文分类集锦
研究SLAM,对编程的要求有多高?
2018年SLAM、三维视觉方向求职经验分享
2018年SLAM、三维视觉方向求职经验分享
深度学习遇到SLAM | 如何评价基于深度学习的DeepVO,VINet,VidLoc?
AI资源对接需求汇总:第1期
AI资源对接需求汇总:第2期
AI资源对接需求汇总:第3期
代码解读 | VINS 视觉前端的更多相关文章
- 优秀开源代码解读之JS与iOS Native Code互调的优雅实现方案
简介 本篇为大家介绍一个优秀的开源小项目:WebViewJavascriptBridge. 它优雅地实现了在使用UIWebView时JS与ios 的ObjC nativecode之间的互调,支持消息发 ...
- Hybrid----优秀开源代码解读之JS与iOS Native Code互调的优雅实现方案-备
本篇为大家介绍一个优秀的开源小项目:WebViewJavascriptBridge. 它优雅地实现了在使用UIWebView时JS与ios 的ObjC nativecode之间的互调,支持消息发送.接 ...
- Jsoup代码解读之四-parser
Jsoup代码解读之四-parser 作为Java世界最好的HTML 解析库,Jsoup的parser实现非常具有代表性.这部分也是Jsoup最复杂的部分,需要一些数据结构.状态机乃至编译器的知识.好 ...
- weex官方demo weex-hackernews代码解读(上)
一.介绍 weex 是阿里出品的一个类似RN的框架,可以使用前端技术来开发移动应用,实现一份代码支持H5,IOS和Android.最新版本的weex已默认将vue.js作为前端框架,而weex-hac ...
- Android MVP模式 谷歌官方代码解读
Google官方MVP Sample代码解读 关于Android程序的构架, 当前(2016.10)最流行的模式即为MVP模式, Google官方提供了Sample代码来展示这种模式的用法. Repo ...
- SoftmaxLayer and SoftmaxwithLossLayer 代码解读
SoftmaxLayer and SoftmaxwithLossLayer 代码解读 Wang Xiao 先来看看 SoftmaxWithLoss 在prototext文件中的定义: layer { ...
- 《编写高质量代码--Web前端开发修炼之道》读书笔记
前言 这两周参加公司的新项目,采用封闭式开发(项目成员在会议室里开发),晚上加班到很晚,所以没时间和精力写原创博客了,今天就分享下这篇<编写高质量代码--Web前端开发修炼之道>读书笔记吧 ...
- Jsoup代码解读之六-防御XSS攻击
Jsoup代码解读之八-防御XSS攻击 防御XSS攻击的一般原理 cleaner是Jsoup的重要功能之一,我们常用它来进行富文本输入中的XSS防御. 我们知道,XSS攻击的一般方式是,通过在页面输入 ...
- Jsoup代码解读之五-实现一个CSS Selector
Jsoup代码解读之七-实现一个CSS Selector 当当当!终于来到了Jsoup的特色:CSS Selector部分.selector也是我写的爬虫框架webmagic开发的一个重点.附上一张s ...
随机推荐
- Python基础之变量,常量,注释,数据类型
由于上学期学了C语言,对于这一块的内容肯定算熟悉,只是注释的方法有些不同,但得还是一步一步的来!没有基础的同学看了这篇随笔也会大有助益的! 什么是变量?所谓变量就是将一些运算的中间结果暂存到内存中,以 ...
- Python生成器和构造器
什么是生成器? 参考link:http://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/00 ...
- spark 源码分析之三 -- LiveListenerBus介绍
LiveListenerBus 官方说明如下: Asynchronously passes SparkListenerEvents to registered SparkListeners. 即它的功 ...
- 0728 history
-- :: cd /etc/yum.repos.d/ -- :: wget http://download.virtualbox.org/virtualbox/rpm/rhel/virtualbox. ...
- 【iOS】获取应用程序本地路径
Xcode 会为每一个应用程序生成一个私有目录,并随机生成一个数字和字母串作为目录名,在每一次应用程序启动时,这个字母数字串都是不同于上一次. 所以通常使用 Documents 目录进行数据持久化的保 ...
- memcached.c 源码分析
上文分析了memcached的autoconf过程以及configure, make过程,可以看到,memcached可执行文件是由memcached-memcached.o以及其他文件连接后编译出来 ...
- Java集合系列(一)List集合
List的几种实现的区别与联系 List主要有ArrayList.LinkedList与Vector几种实现. ArrayList底层数据结构是数组, 增删慢.查询快; 线程不安全, 效率高; 不可以 ...
- go单元测试
testing模块 测试代码放在当前包以_test.go结尾的文件中 测试函数以Test为名称前缀 测试命令(go test) 正常编译操作(go build/install)会忽略测试文件 单例模式 ...
- Redis优化建议
优化的一些建议 1.尽量使用短的key 当然在精简的同时,不要完了key的"见名知意".对于value有些也可精简,比如性别使用0.1. 2.避免使用keys * keys *, ...
- 疯子的算法总结(二) STL Ⅰ 算法 ( algorithm )
写在前面: 为了能够使后续的代码具有高效简洁的特点,在这里讲一下STL,就不用自己写堆,写队列,但是做为ACMer不用学的很全面,我认为够用就好,我只写我用的比较多的. 什么是STL(STl内容): ...