第二篇 特征点匹配以及openvslam中的相关实现详解
配置文件
在进入正题之前先做一些铺垫,在openvslam中,配置文件是必须要正确的以.yaml格式提供,通常需要指明使用的相机模型,ORB特征检测参数,跟踪参数等。
#==============#
# Camera Model #
#==============#
Camera.name: "EuRoC monocular"
Camera.setup: "monocular"
Camera.model: "perspective"
# 相机内参
Camera.fx: 458.654
Camera.fy: 457.296
Camera.cx: 367.215
Camera.cy: 248.375
# 畸变参数
Camera.k1: -0.28340811
Camera.k2: 0.07395907
Camera.p1: 0.00019359
Camera.p2: 1.76187114e-05
Camera.k3: 0.0
# 帧率
Camera.fps: 20.0
# 图像宽高
Camera.cols: 752
Camera.rows: 480
# 颜色模式
Camera.color_order: "Gray"
#================#
# ORB Parameters #
#================#
Feature.max_num_keypoints: 1000
Feature.scale_factor: 1.2
Feature.num_levels: 8
Feature.ini_fast_threshold: 20
Feature.min_fast_threshold: 7
...
相机参数
enum class setup_type_t {
Monocular = 0,
Stereo = 1,
RGBD = 2
};
enum class model_type_t {
Perspective = 0,
Fisheye = 1,
Equirectangular = 2
};
enum class color_order_t {
Gray = 0,
RGB = 1,
BGR = 2
};
可以看到openvslam支持单目(Monocular)、双目(Stereo)以及RGBD相机,成像模型支持Perspective、Fisheye(鱼眼)、Equirectangular(全景,等距圆柱图)。
其中Perspective、Fisheye的内参、外参都和opencv一致,使用中可以用opencv做相机内参标定。
颜色模式根据输入选择就好,由于openvslam特征提取采用的ORB,最终输入的图片都会转为灰度图。
关于ORB参数第一篇中已有详细解释,不再赘述。
配置过程
我们从最基础的单目slam开始分析,即example/run_image_slam.cc。将配置文件路径传入后,会在下面的程序中作配置初始化。
// example/run_image_slam.cc:191
// load configuration
std::shared_ptr<openvslam::config> cfg;
try {
cfg = std::make_shared<openvslam::config>(config_file_path->value());
}
catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
return EXIT_FAILURE;
}
加载完相机参数后直接实例化响应的相机类型;
system初始化
// example/run_image_slam.cc:39
// build a SLAM system
openvslam::system SLAM(cfg, vocab_file_path);
system初始化内容比较多,我们先来看匹配功能需要哪些必要的条件。首先加载ORB辞典,用于作回环检测,先不用关心:
// src/openvslam/system.cc:46
bow_vocab_ = new data::bow_vocabulary();
bow_vocab_->loadFromBinaryFile(vocab_file_path);
在初始化跟踪模块时,会实例化orb特征点提取器。可以看到初始化中用的特征提取最大点数是正常的两倍。
// src/openvslam/tracking_module.cc:29
extractor_left_ = new feature::orb_extractor(cfg_->orb_params_);
if (camera_->setup_type_ == camera::setup_type_t::Monocular) {
ini_extractor_left_ = new feature::orb_extractor(cfg_->orb_params_);
ini_extractor_left_->set_max_num_keypoints(ini_extractor_left_->get_max_num_keypoints() * 2);
}
if (camera_->setup_type_ == camera::setup_type_t::Stereo) {
extractor_right_ = new feature::orb_extractor(cfg_->orb_params_);
}
跟踪器
在跟踪之前读取图像数据,然后送入跟踪器,可以看到输入的图像会被直接转为灰度图。然后进行帧初始化。
// example/run_image_slam.cc:62
SLAM.track_for_monocular(img, frame.timestamp_, mask);
帧初始化
帧初始化有三种初始化函数分别对应单目、双目和深度相机。本篇只看单目的。
// src/openvslam/tracking_module.cc:85
// create current frame object
if (tracking_state_ == tracker_state_t::NotInitialized || tracking_state_ == tracker_state_t::Initializing) {
curr_frm_ = data::frame(img_gray_, timestamp, ini_extractor_left_, bow_vocab_, camera_, cfg_->true_depth_thr_, mask);
}
else {
curr_frm_ = data::frame(img_gray_, timestamp, extractor_left_, bow_vocab_, camera_, cfg_->true_depth_thr_, mask);
}
src/openvslam/data/frame.cc:20
帧初始化
从system初始化中实例化的orb特征点提取器获取提取器的一些信息
ORB特征提取(上篇已经详细讲过)
根据相机模型去畸变(直接使用opencv函数)
将去畸变的点转为相机坐标下归一化的空间点(convert_keypoints_to_bearings)
初始化landmarks(特征点的空间位置信息)容器
特征点珊格化(assign_keypoints_to_grid)
convert_keypoints_to_bearings
下式中,\(u,v\)是图像中的点,\(X_c, Y_c,Z_c\)是对应的相机坐标系下的坐标值,\(\begin{pmatrix}X_c/Z_c\\ Y_c/Z_c \\ 1 \end{pmatrix}\)是相机坐标系下归一化平面上(z=1)的坐标值。
\begin{pmatrix}f_x & 0 & c_x\\0 & f_y & c_y\\ 0 & 0 & 1\end{pmatrix}
\begin{pmatrix}X_c\\ Y_c \\ Z_c \end{pmatrix}=
\frac{1}{Z_c}\begin{pmatrix}f_x & 0 & c_x\\0 & f_y & c_y\\ 0 & 0 & 1\end{pmatrix}\begin{pmatrix}X_c/Z_c\\ Y_c/Z_c \\ 1 \end{pmatrix}
\]
//src/openvslam/camera/perspective.cc:124
const auto x_normalized = (undist_keypts.at(idx).pt.x - cx_) / fx_;
const auto y_normalized = (undist_keypts.at(idx).pt.y - cy_) / fy_;
const auto l2_norm = std::sqrt(x_normalized * x_normalized + y_normalized * y_normalized + 1.0);
bearings.at(idx) = Vec3_t{x_normalized / l2_norm, y_normalized / l2_norm, 1.0 / l2_norm};
\begin{pmatrix}f_x & 0 & c_x\\0 & f_y & c_y\\ 0 & 0 & 1\end{pmatrix}^{-1}\begin{pmatrix}u\\ v\\ 1\end{pmatrix}
\]
程序中计算的就是归一化平面上的座标值,然后将该座标值再次进行归一化。
assign_keypoints_to_grid
将所有的特征点分配到设定的珊格中,匹配时,由于运动特征点的位置会发生变化,但是这个变化是有限的,我们只需搜索之前特征点附近的特征点,通过珊格化,可以快速的获取到指定珊格中的特征点,加速匹配过程。
// 3072个珊格
num_grid_cols_ = 64
num_grid_rows_ = 48
// src/openvslam/camera/perspective.cc:26
inv_cell_width_ = static_cast<double>(num_grid_cols_) / (img_bounds_.max_x_ - img_bounds_.min_x_);
inv_cell_height_ = static_cast<double>(num_grid_rows_) / (img_bounds_.max_y_ - img_bounds_.min_y_);
//img_bounds_ 是图像反畸变后的区间,由畸变系数和相机模型决定。
//区间的边界值可能为负,比如鱼眼相机反畸变后的图像区域肯定比原始图像大。
匹配
前面做了这么多铺垫,终于来到了正题。这里我们把匹配的内容从openvslam的流程中剥离出来分析并且与opencv中自带的算法作比较。
match_in_consistent_area
匹配问题描述:
已知参考帧(图像)的特征点(每个特征点包含哪些内容?忘记的话,看上篇文章)和当前帧的特征点信息,求当前帧与参考帧相同的特征点的过程,叫做匹配。
// match/openvslam/match/area.cc:8
/*
frm_1: 参考帧
frm_2:当前镇
prev_matched_pts: 参考帧中的特征点
matched_indices_2_in_frm_1: 经过匹配后,参考帧中的特征点在当前帧的序号
margin: 设定特征匹配时在原特征点位置搜索的范围大小,单位是像素。
*/
unsigned int area::match_in_consistent_area(data::frame& frm_1, data::frame& frm_2, std::vector<cv::Point2f>& prev_matched_pts,
std::vector<int>& matched_indices_2_in_frm_1, int margin)
空间一致匹配(只匹配金字塔第0层,即原始图像上的特征点)
每一个参考帧中的特征点在珊格化后都会有对应的珊格序号,获取当前帧相同序号以及周围一定范围的珊格中的特征点,作为匹配候选特征点
计算候选特征点与参考帧特征点的汉明距离,得到匹配的特征点具体思路看下文
通过角度检查,筛除不合格的匹配点
汉明距离
Hamming Distance:表示两个等长字符串在对应位置上不同字符的数目,数值越小说明越相似。还不清楚的话,自行查阅相关内容。
openvslam在计算候选特征点的时候会将最小和第二小的汉明距离记录下来,满足下面两个条件才认为匹配成功:
- 最小距离必须<HAMMING_DIST_THR_LOW,这里HAMMING_DIST_THR_LOW=50;回忆下前一篇文章,描述子的长度为32字节=256位,这里就要求小于50个位不同才算匹配;
- 这里有篇文章《Distinctive Image Features from Scale-Invariant Keypoints》,作者是David G. Lowe,lowe_ratio_应该是源自这篇文章,取值0.9。这里的条件就是说,要求最小值一定要比次小值有比较大的差值,也就是要求匹配到的特征点一定要是这些候选者中有较强的区分性,否则就不要,正所谓宁缺毋滥,尽可能得到准确的匹配结果。
// match/openvslam/match/area.cc:66
if (second_best_hamm_dist * lowe_ratio_ < static_cast<float>(best_hamm_dist)) {
continue;
}
关于汉明距离的计算有十分高效的方法,具体看程序。
角度检查
openvslam中首先统计所有匹配的特征点角度差的直方图(直方图的步长为360/30=12度),直方图按值由大到小排序,然后将不再前3直方图中的特征点认为无效点,排除掉。基本的思想应该是区域内的角度变化应该是一致的。这部分代码使用大量现代C++语法,对于还不是很熟悉新特性的同学来说,可以好好看看。
//match/openvslam/match/angle_checker.h:20
explicit angle_checker(const unsigned int histogram_length = 30, // 直方图的步长360/histogram_length
const unsigned int num_bins_thr = 3); // 最值有效门限
结果对比
下图为openvslam提取的特征点,分布是不是十分的均匀?原因上篇已经分析的很清楚了。
下图为使用opencv提取的ORB特征点,明显不分布不够均匀。
openvslam的匹配方法比起暴力匹配有很多优势。运动时,假设运动不是十分激烈,前后帧中的特征点的位置的变化是有限的,在上帧位置附近搜索的方法自然比暴力搜索更高效科学,而且通过珊格化分类特征点,更加速了匹配过程。
测试代码
https://github.com/hardjet/slam
问题
- true_depth_thr_的作用?
- 再次进行归一化的目的?
- 珊格有3072个,通常并没有这么多特征点,是否可以减小?
- 只匹配原始层上的特征点?
第二篇 特征点匹配以及openvslam中的相关实现详解的更多相关文章
- 第一篇 特征提取以及openvslam中的相关实现详解
参考尺度空间理论 金字塔 当用一个机器视觉系统分析未知场景时,计算机没有办法预先知道图像中物体尺度,因此,我们需要同时考虑图像在多尺度下的描述,获知感兴趣物体的最佳尺度.所以在很多时候,我们会在将图像 ...
- 第四篇 跟踪过程以及openvslam中的相关实现详解
在成功初始化之后,会创建地图以及局部地图. 创建地图 在初始化正常过后,紧接着会创建地图 // src/openvslam/module/initializer.cc:67 // create new ...
- 第三篇 视觉里程计(VO)的初始化过程以及openvslam中的相关实现详解
视觉里程计(Visual Odometry, VO),通过使用相机提供的连续帧图像信息(以及局部地图,先不考虑)来估计相邻帧的相机运动,将这些相对运行转换为以第一帧为参考的位姿信息,就得到了相机载体( ...
- 【转】PCB中3D相关功能详解
如果PCB Layout工程师能够在设计过程中,使用设计工具直观地看到自己设计板子的实际情况,将能够有效的帮助他们的工作.尤其现在PCB板的设计越来越复杂,密度越来越高,如果能够洞察多层板内部则可以帮 ...
- JScript中的条件注释详解(转载自网络)
JScript中的条件注释详解-转载 这篇文章主要介绍了JScript中的条件注释详解,本文讲解了@cc_on.@if.@set.@_win32.@_win16.@_mac等条件注释语句及可用于条件编 ...
- Android中的windowSoftInputMode属性详解
这篇文章主要介绍了Android中的windowSoftInputMode属性详解,本文对windowSoftInputMode的9个属性做了详细总结,需要的朋友可以参考下 在前面的一篇文章中 ...
- C#中的预处理指令详解
这篇文章主要介绍了C#中的预处理指令详解,本文讲解了#define 和 #undef.#if.#elif.#else和#endif.#warning和#error.#region和#endregion ...
- Python中的高级数据结构详解
这篇文章主要介绍了Python中的高级数据结构详解,本文讲解了Collection.Array.Heapq.Bisect.Weakref.Copy以及Pprint这些数据结构的用法,需要的朋友可以参考 ...
- PHP开发中常见的安全问题详解和解决方法(如Sql注入、CSRF、Xss、CC等
页面导航: 首页 → 网络编程 → PHP编程 → php技巧 → 正文内容 PHP安全 PHP开发中常见的安全问题详解和解决方法(如Sql注入.CSRF.Xss.CC等) 作者: 字体:[增加 减小 ...
随机推荐
- Python中文本文件读写操作的编码问题
Python中文本文件读写的编码问题 编码(encode): 我们输入的任何字符想要以文件(如.txt)的形式保存在计算机的硬盘上, 必须先经按照一定的规则编成计算机认识的二进制后,才能存在电脑硬盘上 ...
- TIJ学习--RTTI(Runtime-Time Type Identification)
TIJ学习--RTTI(Runtime-Time Type Identification) RTTI 运行时类型检查机制 获取一个类的Class引用的三种方法 class TestClass{} Te ...
- Shell基本语法---shell介绍
简介 1. shell是在linux系统上高效运行的脚本语言 2. 主要用来开发一些实用的.自动化的小工具,而不是用来开发具有复杂业务逻辑的中大型软件 3. shell的基本命令也是linux操作系统 ...
- Python基础总结之第十一天开始【再深入一下函数,重新认识一下】(新手可相互督促)
感谢最近大家的关注,希望我的学习笔记对大家有帮助!也感谢各位的评论和推荐,请多多指教. 在重新认识函数之前,我们先看两个函数.一个是我们在前面笔记经常用到的print() :另一个是input() ...
- codeforces 322 B Ciel and Flowers
题目链接 有红绿蓝三种颜色的画,每种拿三朵可以组成一束花,或者各拿一朵组成花束,告诉你每种花的数目,求出可能组成最多的花束. 如果你的代码过不了,考虑一下 8 8 9这种组合. 因为数据量很大,我的 ...
- input属性设置type="number"之后, 仍可输入e, E, -, + 的解决办法
<el-input v-model="scope.row.variables.leaderbuweiscores.score" @keyup.native="cha ...
- 新IT运维时代 | Docker运维之最佳实践-上篇
容器技术的发展可以分为两个阶段,第一个阶段聚焦在IaaS层,仅仅把容器当做更轻量级虚拟机来使用,解决了应用运行时进程级资源隔离的问题:随着Docker的出现,容器虚拟化才有了统一的平台,由此容器技术发 ...
- 配置VNC并远程控制服务器(电脑)
先象征性介绍一下: VNC (Virtual Network Console)是虚拟网络控制台的缩写, 它是一款基于 UNIX 和 Linux 操作系统的优秀.免费.开源的远程控制工具软件. 然后开始 ...
- Linux学习之自动配置部署——初用expect
主机A连接主机B 免密登陆 + 自动部署 expect实现自动的交互式任务 ——— send 向进程发送字符串(输入) ——— expect 从进程接受字符串 ——— spawn 启动新进程 ——— ...
- [NUnit] discover test finished: 0 found issue
%Temp%\VisualStudioTestExplorerExtensions & restart visual studio