最近更新日期:2022.12.03(已完结,可能会再理解扩充)
OpenVSLAM
是包含ORB-SLAM2
内容并重新改写扩充后的仓库,因此和ORB-SLAM2源代码一些代码构造和变量名称不完全一样。
虽然已经OpenVSLAM
仓库已经停用了,但因为原来项目所用,因此继续延续以这个复习。
(以下为之前的学习笔记,重新捡起,逐步更新补充中。。。)
大体的学习步骤就是根据b站的ORB-SLAM2
的代码讲解视频进行跟进
看的过程中去对应OpenVSLAM
里的相应实现
分为三大线程
其中tracking就是简单的对于每一个读如的帧都进行处理。但后两步都是一个死循环,从上一个步骤接收到一个就处理一个。
局部建图和回环检测线程,都会开辟一个单独的thread进行运行。
三个线程之间是通过关键帧队列
来进行通信。
对于单目摄像头来说,就是传入一张图片
第一步tracking,要所有帧都参与的,因为每过一帧,都要估计相应相机的位姿
但是对于后边两个都是比较重的任务,所以只有keyframe才参与
(判断当前frame是否是keyframe的函数在tracking里)
所以keyframe是三者的桥梁
三个线程,所以需要锁进行限制读写(Mutex变量)
大括号结束就意味着释放锁。
// dequeue
{
std::lock_guard<std::mutex> lock(mtx_keyfrm_queue_);
// dequeue -> cur_keyfrm_
cur_keyfrm_ = keyfrms_queue_.front();
keyfrms_queue_.pop_front();
}
构造函数:
tracking线程本身就是system类的主线程
mapping和global optimization是其里边创建的子线程
但逻辑上三者是并行的
在OpenVSLAM中是在data/Frame.cc
类里,调用ORBExtractor类
ORBExtractor只有一个(tracking_module.h中可以看到),成百上千个的Frame共享同一个ORBExtractor
所以说下一帧来的时候会把原图像的金字塔信息覆盖掉,但提取到的特征点信息不会覆盖(保存在了frame里)。
所以ORB-SLAM对于每个图像只保留固定个数的特征点,是稀疏重建。
ORB:oriented BRIEAF
FAST特征点和ORB描述子本身不具有尺度信息
其中金字塔中层级越高图片分辨率越低,ORB特征点越大,相当于离相机越近
参数在yaml配置文件中设定。
另外,umax用于后续计算特征子主方向。需要半径为16的圆。
通过构建图像金字塔来得到特征点尺度信息将输入图片逐级缩放得到图像金字塔
具体来说:把图像缩放到对应每个金字塔层级,并补19的边(FAST特征点需要半径为3,ORB描述子需要半径为16)
将30*30的像素图像划分为一个网格。遍历每一个网格搜索:
对于BRIEF描述子,是在r=16的圆内计算256个点对,每个点对1位,共256对。
对于Oriented BRIEF来说,还要在取点对之前先将特征点周围r=19像素旋转到主方向上,再做BRIEF的操作(在r=16上取256对的点对)。
ORB-SLAM中的MapPoint
相当于OpenVSLAM代码里的landmarks
ORB-SLAM里的Map信息就是特指(特征点、KeyFrame)两个数组
landmark.cc
中std::map
KeyFrame.cc
中的std::vector
observations_中的索引就是在对应KeyFrame中改地图点的索引
num_observations_ 代表有多少相机观测到这个地图点了,如果是单目相机,就多一个相机就+1。(后续mapping过程来判断该MapPoint的质量)
最小/最大观测距离
超过这个范围就是相机无法观测到的特征点了
当前观测距离已知:(假设层之间的scale都是1.2倍)
1.要是得到最大观测距离,就可以知道当前是第几个level
2.要是得到第几个level,就可以得到最大观测距离
landmark.cc
中的update_normal_and_depth()函数就是用来更新当前landmark的平均观测方向和距离。
!!此函数的调用时机:landmark本身 / KeyFrame对该landmark观测发生变化
平均方向
是由所有观测到此landmark的keyframe(observations_)平均得到的,平均距离
是参考关键帧(ref_keyfrm)得到的。
那参考关键帧是哪里来的?
其一般是和该地图点关系最紧密,视角最好的
第一条在landmark.h中有说明:
//! get reference keyframe, a keyframe at the creation of a given 3D point
keyframe* get_ref_keyframe() const;
第二条在erase_observation函数中
if (ref_keyfrm_ == keyfrm) { // 如果对于参考关键帧的观测被删除,取第一个观测到该地图点的keyframe为refKeyFrame
ref_keyfrm_ = observations_.begin()->first;
}
很多keyframe(图中红框)都观测到了这个landmark(图中红圈),每个landmark在这些keyframe中都有对应的特征点(图中红叉)。
选择哪一个作为此landmark的descriptor?
选最有代表性
的。
具体来说就是找出特征点descriptor距离其他特征点descriptor距离之和最小。(256位二进制,可以求距离)
因此,一旦对该landmark的观测序列变了的话,就要重新计算landmark描述子
作用:PnP问题
解决3D特征点到2D点投影的时候,怎么知道哪个特征点和这个地图点对应?
就是看此keyframe中哪个特征点的descriptor和landmark的descriptor最像。
亮点:先不物理删除,先标记一下,即逻辑删除
因为landmark复杂,删除耗时。
如果要停下,浪费时间;如果不停,别的线程还在用。
那就先将其标记为will_be_erased(ORB-SLAM: bad)
具体标的函数是prepare_for_erasing(ORB-SLAM: set_bad_flag)
其他线程用的话都要看看标记,这样具体删的过程都不用加了,系统得空了再弄。
对于判断bad点的条件,在local mapping里有个函数负责
与删除操作的思想类似,先标记,后操作
直接替换的话也是耗时,而且可能会在替换过程中产生读写冲突。
因此设置一个replaced_
指针,默认为NULL。
如果当前被替换,则直接指向新地图点(不用来找我了,去找指针指向的)。
然后剩下的步骤慢慢做就行了
对理解很有帮助:
所有非临时landmark都是keyFrame建立的
非keyFrame建立的landmark都很快被删除,只是增强帧间的匹配。
其中创建landmark的2、3条,只有双目/RGBD相机会创建地图点。
Frame
是人民群众,KeyFrame
是人大代表,KeyFrame
从Frame
中来。
Frame
只能解决当前帧pose等问题,但KeyFrame
才能参与解决整体的建图等问题。
因为所有的相机的内参都是固定的,因此所有Frame中关于相机内参的变量都是static(只有一份)。通过tracking线程由yaml文件读入,只有第一次构建Frame来构建内参参数。
OpenVSLAM中是在camera/base.h
文件中读入,然后frame.h
再创建base对象:
//! camera model
camera::base* camera_ = nullptr;
之前写的:
ORB_SLAM2代码里把相机好多参数都作为Frame对象,而且有一些还不是static
但OpenVSLAM里就都封装在了camera对象里,传给frame对象的构造函数
在Frame.h中针对双目相机的变量,只需要记录左目特征点在右目中匹配特征点的横坐标信息stereo_x_right_,原因是纵坐标与左目的是相同的,横坐标由于有视差所以有差别
双目为例:通过两个相机进行立体匹配,得到每个特征点的深度,即双目特征点
如果过深,误差过大,则变为单目特征点(用左相机)
深度的threshold在yaml文件中有参数⬇️
并且对于深度相机,利用左目坐标和深度信息,构建虚拟的右目坐标。这样后续深度和双目相机可以同样形式处理。
双目相机特征点的匹配过程:
畸变矫正
畸变矫正只发生在单目相机和RGBD相机的左目上
双目相机预先进行了,所以矫正参数为0。
矫正之后使得两个相机的极线处于一个平面,所以在后来匹配的时候才可以认为其纵坐标一样
网格加速
将特征点分配到网格上,加速匹配。
OpenVSLAM代码中,tracking_module就是track线程
每来一帧,就调用track_monocular_image创建一个frame对象
与Frame相关的只有last_frm_和curr_frm_两个变量,tracking完curr就变味last
Covisibility Graph
和Spanning Tree
都是描述关键帧的共视关系,前者更加稠密
共视关系:两个KeyFrame间有共同观测到的landmark
权重:共同观测到的landmark越多,关系越紧密
共视图关系(绿色):过于稠密,肯定不会优化他。
ORB_SLAM2代码里:对于共视图是在KeyFrame里存储的。
OpenVSLAM代码里是单独开辟出graph_node
类来存储共视图相关变量和函数。
用于存储与其共视的关键帧和权重:
//! all connected keyframes and their weights
std::map<keyframe*, unsigned int> connected_keyfrms_and_weights_;
对于connected_keyfrms_and_weights_
中的keys和values按照权重降序存储
//! covisibility keyframe in descending order of weights
std::vector<keyframe*> ordered_covisibilities_;
//! weights in descending order
std::vector<unsigned int> ordered_weights_;
update_connections()
是通过对地图点的观测,重新构造Covisibility Graph
其实是rebuild,把之前的变量都清空了,是可以考虑优化的位置
keyfrm_weights
weight_thr_=15
的存入weight_covisibility_pairs
ordered_covisibilities_
和ordered_weights_
update_connections()
调用时机:
关键帧与地图点间的连接关系发生变化:即KeyFrame创建和地图点重新匹配关键帧特征点
add_connections()
和erase_connection
都是在内部调用的,但设为了public,不合理
生成树是Covisibility Graph的简化,对其可以进行BA优化,运算量小。
父node只有一个,子node可以有很多。spanning_parent_is_not_set_
代表该关键帧是否还未加入到生成树中。
//! parent of spanning tree
keyframe* spanning_parent_ = nullptr;
//! children of spanning tree
std::set<keyframe*> spanning_children_;
//! flag which indicates spanning tree is not set yet or not
bool spanning_parent_is_not_set_;
在update_connections()
中,一旦关键帧被新创建,第一次调用这个函数,则spanning_parent_is_not_set_
就是True,那么第4步中就会将其加入生成树中。
if (spanning_parent_is_not_set_ && owner_keyfrm_->id_ != 0) {
// set the parent of spanning tree
assert(*nearest_covisibility == *ordered_covisibilities.front());
spanning_parent_ = nearest_covisibility;
spanning_parent_->graph_node_->add_spanning_child(owner_keyfrm_); // 共视程度最高的作为父关键帧
spanning_parent_is_not_set_ = false;
}
生成树的改变
只有在关键帧删除时,才会牵连生成树的变动。
比删除地图点需要多考虑的地方:
与关键帧的删除相关的变量:
will_be_erased_
就相当于标记bad_flag。cannot_be_erased_
相当于标记是否具有不被删除的权利(即参与回环检测的帧)
//-----------------------------------------
// flags
//! flag which indicates this keyframe is erasable or not
std::atomic<bool> cannot_be_erased_{false};
//! flag which indicates this keyframe will be erased
std::atomic<bool> will_be_erased_{false};
ORB-SLAM2里还有一项mbToBeErased
是用来标记有特权的关键帧是否需要删除的,OpenVSLAM边没有对这一项的操作。应该是通过阶段的方式来控制的
prepare_for_erasing
函数中包含操作较多
其中维护生成树部分操作比较复杂,具体在graph_node.cc
文件的recover_spanning_connections()
中
每次寻找与候选父亲节点组的权重最大的一个孤儿节点,纳入到候选父亲节点组中
下图有问题,4到7的权重最大,因此4应该接在7后边,而不是2后
调用对于keyFrame中观测landmark列表的增删时机:
生成树 + 回环闭合点 + 强共视关系点(权重超过100的边)
创建就是在Tracking线程中对于KeyFrame进行的创建,说明要用它来建图了。
删除就是LocalMapping线程有专门用于销毁KeyFrame的地方。
这个类是只针对单目相机的,ORB-SLAM1
就有的代码
双目和深度相机都只需要一张图片就可以初始化,左目图片超500特征点就初始化成功
但单目最少两个才能初始化
OpenVSLAM
中初始化对象是在initializer.h
中,但一些具体的关键数据结构都在base.h
里分离出来,initializer.h中再创建一个叫initializer_
的base
对象
base.h
中:ref_undist_keypts_
参考帧中特征点,cur_undist_keypts_
当前帧特征点,ref_cur_matches_
上述两帧之间的匹配点索引。
//! undistorted keypoints of reference frame
const std::vector<cv::KeyPoint> ref_undist_keypts_;
//! undistorted keypoints of current frame
std::vector<cv::KeyPoint> cur_undist_keypts_;
//! matching between reference and current frames
std::vector<std::pair<int, int>> ref_cur_matches_;
几个类关系:
调用关系:
run_image_slam.cc
的main
函数调用mono_tracking
函数。mono_tracking
函数创建system
类对象SLAM,每读入一帧就调用一次system.feed_monocular_frame
system.feed_monocular_frame
调用system
类中的tracking_module
对象的track_monocular_image
方法track
函数,track
里调用initialize
函数initializer
类对象initializer_.initialize
方法initializer_.initialize
第一步调用create_initializer()
,将本身的initializer_
对象(此处为base对象)特化成了perspective
类对象initializer_.initialize
第二步try_initialize_for_monocular
方法中再调用initialize
方法,就是调用 perspective
类中的计算F和H矩阵的方法了当前两个帧之间进行特征点匹配,计算F和H。两个矩阵方法哪个得分高用哪个方法。(具体计算方法见下)
内点、外点:
内点就是应该算作正确的点,外点是不该算作正确的点(图像匹配任务就是不该匹配上的点)
算法思想【以直线拟合任务为例】:
由于增大数据采样量也会增加外点数量,因此不能作为解决办法。因此少量多次
少量指的是每次拟合只使用少量的点,多次指的是多次拟合。
这样总能找到一种解使得包含最多的内点
使用好RANSAC算法的方式(减少采样轮数):
1.减少外点的比例 2.减少每次采样所利用的样本个数(下图两个坐标)
F矩阵等概念和计算过程是多视图几何内容,先当做一个黑匣子
先让所有特征点归一化,然后对于每一个点对八点法计算F矩阵,利用RANSAC循环k(500)次找到最优解(其中每次计算完F矩阵都要反归一化,并进行卡方检验)。
其实就是看期望结果和实际结果间的差别大不大。
方式是通过构造卡方统计量。
单目特征点重投影误差自由度为2,因此当卡方大于5.99,就说有95%置信度说错的离谱了(即用到了outliers)。
卡方检验函数check_inliers最后返回的score就用来后续比较,k次RANSAC哪次计算出的值是好的,记录最好的那一次。
以perspective.cc
中的reconstruct_with_H
为例,调用base.cc
中的find_most_plausible_pose
,找出其中那个假设有效点的数量明显大于其他假设有效点的数量。
ORB-SLAM2来说,这里假设的数量写死了,三角话就是以下四种可能性。
而OpenVSLAM,假设的数量更加灵活了。用了变量代替。
已被证明:计算F矩阵只需要7对点,H矩阵只需要4对点。但作者在计算的时候都是用的8对点来做。原因可能是为了卡方检验时候的标准的统一,因为理论上肯定用的点多更精确。
E和F矩阵的rank都是2,即不满rank。这对分析F的自由度有影响。
极线都交于一点,叫极点。也是一个相机光心在另一个相机平面上的投影(e,f)
两个图搜索的时候,同一个特征点一定可以在对应的极线上找到。所以两个图矫正之后极线水平对齐,就可以水平搜索。
五个步骤:
跟踪状态:
// tracker state
enum class tracker_state_t {
NotInitialized,
Initializing,
Tracking,
Lost
};
当前帧的跟踪状态和上一帧的跟踪状态
//! latest tracking state
tracker_state_t tracking_state_ = tracker_state_t::NotInitialized;
//! last tracking state
tracker_state_t last_tracking_state_ = tracker_state_t::NotInitialized;
这一部分的步骤,包括提取extract_orb
提取ORB特征点、undistort_keypoints
计算关键点、计算bearing vector
等,都是在frame.cc
中frame的构造函数中做的。也就是说来一帧图像创建一个frame就会自动调用预处理的过程。
上一个初始化成功的帧就设为参考关键帧ref_keyfrm_
local_keyfrms_
和local_landmarks_
构成了local map,初始化成功后向其中添加
//! reference keyframe
data::keyframe* ref_keyfrm_ = nullptr;
//! local keyframes
std::vector<data::keyframe*> local_keyfrms_;
//! local landmarks
std::vector<data::landmark*> local_landmarks_;
OpenVSLAM中将初始化的过程都打散了,放在initializer.initialize
里了。
其中包含三个函数:
create_initializer(curr_frm)
:将init_frm_
初始化为当前读入的帧【因为mono所以只有一帧】,并将其关键点位置初始化为之前匹配的点(prev_matched_coords_
)。将初始化器特化为perspective类型。try_initialize_for_monocular(curr_frm)
:首先在init_frm_和curr_frm
间进行空间和角度的连续性匹配,若匹配特征点太少【min_num_triangulated_】则reset。最终进行perspective中的计算H、F矩阵等操作。create_map_for_monocular
:【下一部分具体说】 case camera::setup_type_t::Monocular: {
// construct an initializer if not constructed
if (state_ == initializer_state_t::NotReady) {
create_initializer(curr_frm);
return false;
}
// try to initialize
if (!try_initialize_for_monocular(curr_frm)) {
// failed
return false;
}
// create new map if succeeded
create_map_for_monocular(curr_frm);
break;
}
create_map_for_monocular
函数:
map_db_
相当于ORB-SLAM2里的mpMap
)map_db_
BoW(词袋)
词袋本质上是一种特征点描述子聚类的结果。
在特征点匹配的时候,就是确定其是属于哪一类的。
如果两个特征点是同一类就在内部搜寻加速匹配;如果不是同一类肯定就会是同一个特征点了。
值得注意的
初始化构建局部地图的这个过程中,处理完一个地图点之后没有显式更新共视图。而是将其传递给local mapping线程,其中有关于更新共视图的函数进行处理。(如下,交给了mapper_)
if (tracking_state_ == tracker_state_t::Initializing) {
if (!initialize()) {
return;
}
// update the reference keyframe, local keyframes, and local landmarks
update_local_map();
// pass all of the keyframes to the mapping module
const auto keyfrms = map_db_->get_all_keyframes();
for (const auto keyfrm : keyfrms) {
// spdlog::info("insert init keyframe");
mapper_->queue_keyframe(keyfrm);
}
// state transition to Tracking mode
tracking_state_ = tracker_state_t::Tracking;
}
Pose Prediction:指的是估计相机的坐标和朝向
这里就看作是从上一帧到当前的一个平移➕旋转的过程(实际就是一个RT旋转矩阵)
OpenVSLAM中单独用frame_tracker类来编写三种方法
其中比ORB里多了一个robust_match_based_track()方法
主要思想就是假设相机移动速度是恒定的
然后根据速度和上一帧的位姿得到假设当前帧的位姿,然后进行特征点匹配
其中,速度velocity_
来自之前的跟踪过程,完成一次tracking过程,就会update_motion_model()
获取跟踪的速度,并优先调用运动模型。
if (velocity_is_valid_ && last_reloc_frm_id_ + 2 < curr_frm_.id_) {
// if the motion model is valid
if (!succeeded){
succeeded = frame_tracker_.motion_based_track(curr_frm_, last_frm_, velocity_);
}
}
motion_based_track() 步骤:
主要思想就是找最相近的关键帧或者是当前帧的参考关键帧进行估计
为什么可以用last_frame的pose设为cur_frame的pose初始值??
因为BoW匹配的过程相当于已经将一些landmark注册到了cur_frame上(知道了landmark和cur_frame的关系),那么BA优化的时候就可以往与cur_frame真实的pose方向自动优化。
与bow_match_based_track()
类似,也是利用ref_keyframe进行匹配。但不再使用bow_matcher
快速搜索,而是利用robust_matcher
暴力搜索。
具体过程参考bow_match_based_track()
如果用上一帧的检测失败,即状态是Lost时。对于这一帧的检测就要采用重定位了。
cur_frame
相近的帧作为候选参考关键帧reloc_candidates
(而不是前两个方法那样采用最近的ref_keyframe),相近与否的判断方式是根据BoW来判断是否属于同一类的。reloc_candidates
,利用BoW进行匹配。然后根据3D的landmark和2D的cur_frame构建PnPsolver,来求解cur_frame的pose【使用RANSAC】。再使用BA优化cur_frame的pose,剔除outliers。然后再重复一次,利用BoW估计pose + BA优化pose。(防止误重定位)。如果cur_frame
匹配数量不足min_num_valid_obs_
个landmark,那么就再重复估计pose + BA优化的过程。map = keyframe + landmark,因此这一部分就是更新这两部分
acquire_local_map()
,其中第一步find_local_keyframes()
将keyframe分为first order和second order。first order就是分别找到所有landmark的共视keyframe,second order是first order的四倍size,包括与first order keyframe共视关系top10的keyframe、以及first order keyframe在graph_node上的父子keyframe。first order在前、second order在后,共同组成了local_keyfrms_
。【并将共视关系最强的标记下】acquire_local_map()
,其中第二步find_local_landmarks()
就直接把获得的local_keyfrms_
中所有landmark放进列表,组成local_lms_
。local_keyfrms_
和local_landmarks_
,并将共视关系最强的设为curr_frm_.ref_keyfrm_
。map_db_
num_tracked_lms_thr
);其他情况就用1倍 thr 的就OK了。判定条件:(代码得空再看)
原则:只要系统能处理的过来就进行创建插入,多创建点后续构建的地图就更详细
因为后续local mapping过程有地图点剔除的过程,所以这里能插即插。
mapper_
的处理队列里(如果mapper is paused【mapper处理不过来了】就插入失败)。ref keyFrame
【插入失败就不设】第一种情况,如果状态是Initializing
:
9.2
部分。update_local_map()
:9.4.1
。keyFrame
传给mapping module
处理,注意此处包含对于Covisibility Graph
更新等操作。(mapping module
具体看)Tracking
第二种情况,如果状态是Tracking / Lost
:
ref keyframe
确定last_frm_
的pose。pose estimation
9.3
。9.4
,更新恒速模型的速度等。五个步骤:
注意:
ORB_SLAM2里Local Mapping线程没有利用到Tracking过程构建的临时地图点,而是重新开始,是一个比较不好的方式
tracking
和local mapping
线程是用keyfrms_queue_
进行交互的
//! queue for keyframes
std::list<data::keyframe*> keyfrms_queue_;
keyfrm_acceptability_
代表local mapping
线程是否还愿意接受关键帧,作为tracking线程是否产生关键帧的参考因素之一。(如果系统很需要keyFrame,就算不愿意也得生成并接收)因为只要tracking生成了关键帧,local mapping就得接收
//! flag for keyframe acceptability
std::atomic<bool> keyfrm_acceptability_{true};
线程函数run()
就是一直在判断keyfrms_queue_
中是否还有keyframe。
keyfrm_acceptability_
设置为false(表示正在mapping,不愿意接收新的keyFrame)。mapping_with_new_keyframe()
,进行上述五个步骤。global_optimizer_
对列里。keyfrm_acceptability_
置回为true。cur_keyfrm_
)cur_keyfrm_
所有的landmark
观测序列:判断landmark
是否能看到cur_keyfrm_
(见9.7部分
)。如果看得到说明是由新keyFrame创建的新landmark,则将其放入local_map_cleaner_
类的未经考验landmark序列fresh_landmarks_
。如果看不到,说明是通过tracking中ORBmatcher匹配到的老landmark,增加landmark到keyFrame的观测并更新landmark的信息【平均尺度、描述子】。(不过OpenVSLAM中好像新老landmark都更新了)cur_keyfrm_
加入全局地图库中。这一步是用来剔除【冗余/不好的】landmark
不好的判断标准:
其中Valid
只在遍历队列中删除,Invalid
还要被标记为will be erased。
if (lm->will_be_erased()) {
// in case `lm` will be erased
// remove `lm` from the buffer
lm_state = lm_state_t::Valid;
}
else if (lm->get_observed_ratio() < observed_ratio_thr) {
// if `lm` is not reliable
// remove `lm` from the buffer and the database
lm_state = lm_state_t::Invalid;
}
else if (num_reliable_keyfrms + lm->first_keyfrm_id_ <= cur_keyfrm_id
&& lm->num_observations() <= num_obs_thr) {
// if the number of the observers of `lm` is small after some keyframes were inserted
// remove `lm` from the buffer and the database
lm_state = lm_state_t::Invalid;
}
else if (num_reliable_keyfrms + 1U + lm->first_keyfrm_id_ <= cur_keyfrm_id) {
// if the number of the observers of `lm` is sufficient after some keyframes were inserted
// remove `lm` from the buffer
lm_state = lm_state_t::Valid;
}
注意
search_local_landmarks()
中有修改landmark
的num_observable_
值的操作。
num_observable_
增加观测到的landmark数量。local landmark
,如果存在于其视锥范围内,则理应也被观测到,所以num_observable_
增加。optimize_current_frame_with_local_map()
中有修改landmark
的num_observable_
值的操作。
num_observed_
增加。去掉了冗余的地图点,就创建些好的地图点来补充。
融合cur_frame
和其共视keyFrame
的地图点。
fuse_landmark_duplication()
中融合的两个步骤:
具体的融合函数是利用的matcher.replace_duplication
融合过程中存在两种情况:
局部BA优化cur_frame的局部地图
去除冗余的keyFrame。
去除标准:90%以上的地图点能被超过3个其他keyFrame观测到,则被认为冗余,设为will be erased。
注意
此次新加入的关键帧不会被删除,只会删除之前的关键帧。