配置文件
在進入正題之前先做一些鋪墊,在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)的坐標值。
//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};
程序中計算的就是歸一化平面上的座標值,然后將該座標值再次進行歸一化。
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個,通常並沒有這么多特征點,是否可以減小?
- 只匹配原始層上的特征點?