《视觉SLAM十四讲》第13讲 设计SLAM系统 回环检测线程的实现

这个学期看完了高翔老师的《视觉SLAM十四讲》,学到了很多,首先是对计算机视觉的基本知识有了一个更加全面系统的理解,其次是动手去做实验的过程中,也更加理解了很多有关g2o,opencv,sophus等等工具的使用。

在第13讲的实践部分,高翔老师已经写好了一个基本SLAM框架的前端,后端部分,因此本篇博文主要记录第三个线程回环检测的实现。

先简单介绍一下三个线程的功能:

  1. 前端(Frontend)线程: 前端线程负责处理相机图像流并提取特征点对应关系,然后使用这些对应关系来跟踪相机的运动。该线程负责实时地定位相机并估计其在三维空间中的轨迹。前端的主要任务包括:

    • 特征提取和匹配:从相机图像中提取特征点,通常使用角点检测器或特征描述子来找到关键点,并将它们与先前帧中的特征点进行匹配。

    • 运动估计:使用特征点的匹配信息来估计相机的运动,例如使用视觉里程计(Visual Odometry)技术来估计相机的位姿变化。

    • 姿态跟踪:将估计的相机运动与地图中的特征点进行匹配,从而跟踪相机的姿态并实时更新相机的位姿。

  2. 后端(Backend)线程: 后端线程负责对前端估计的相机轨迹和地图进行优化,以获得更准确和一致的估计。该线程处理由前端产生的轨迹误差,并通过最小化重投影误差或其他优化准则来优化相机的位姿和地图点。后端的主要任务包括:

    • 图优化:建立一个图结构,其中相机位姿和地图点是图的节点,一般优化loss使用重投影误差是,然后通过图优化算法(例如g2o)来最小化误差并优化位姿和地图点。
  3. 回环检测(Loop Closure)线程: 回环检测线程负责检测相机是否再次经过先前经过的地点。当检测到回环时,回环检测会触发回环优化过程,以修正先前轨迹的漂移和误差,从而提高整个系统的一致性和精度。回环检测的主要任务包括:

    • 回环检测:使用特征点的描述子或其他特征匹配技术来检测先前访问过的地标或地图点。

    • 回环优化:当检测到回环时,执行回环优化算法来修正先前轨迹的误差,以获得一致和精确的轨迹和地图。

下面是回环检测的相关代码:

KeyframeDatabase

KeyframeDatabase主要实现保存关键帧,以及关键帧对应的特征描述子,可以用来查询。

KeyframeDatabase.h

//
// Created by xin on 23-6-21.
// #ifndef MYSLAM_KEYFRAMEDATABASE_H
#define MYSLAM_KEYFRAMEDATABASE_H #include "common_include.h"
#include "frame.h"
#include "DBoW3/DBoW3.h" namespace myslam {
class Frame; class KeyframeDatabase {
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW;
typedef std::shared_ptr<KeyframeDatabase> Ptr;
typedef std::unordered_map<unsigned long, Frame::Ptr> KeyframesType; KeyframeDatabase() : keyframe_database_(DBoW3::Vocabulary("../vocabulary.yml.gz"), false, 0) {}; void InsertKeyFrame(Frame::Ptr frame); // 由于访问 database 和 insertKeyframe 可能还冲突,所以需要对 database需要加锁
DBoW3::Database& getDatabase(); KeyframesType GetAllKeyFrames();
public: DBoW3::Database keyframe_database_;
std::mutex mutex_; // 定义一个互斥锁
KeyframesType keyframes_; // all key-frames }; } // myslam #endif //MYSLAM_KEYFRAMEDATABASE_H

KeyframeDatabase.cpp

//
// Created by xin on 23-6-21.
// #include "myslam/keyframeDatabase.h"
#include "DBoW3/DBoW3.h" namespace myslam { DBoW3::Database &KeyframeDatabase::getDatabase() {
std::unique_lock<std::mutex> lck(mutex_);
return keyframe_database_;
} KeyframeDatabase::KeyframesType KeyframeDatabase::GetAllKeyFrames() {
std::unique_lock<std::mutex> lck(mutex_);
return keyframes_;
} void KeyframeDatabase::InsertKeyFrame(Frame::Ptr frame) {
// std::unique_lock<std::mutex> lck(mutex_);
if (keyframes_.find(frame->keyframe_id_) == keyframes_.end()) {
keyframes_.insert(make_pair(frame->keyframe_id_, frame));
} else {
keyframes_[frame->keyframe_id_] = frame;
} keyframe_database_.add(frame->descriptors_);
} } // myslam

loop_closure

回环检测线程主要实现当一个关键帧插入时,启动回环检测,检查当前插入的关键帧是否与之前的关键帧形成回环,如果存在,则根据查询到的关键帧id以及当前帧的id,按照顺序取出这些帧对应的地图点和位姿并执行优化函数。

由于第一次回环检测优化之后,由于还是在相同的地点,所以后续还会检测到回环,但这些回环其实只有第一次就足够,所以实现了一个小trick,即第一次回环检测成功之后,后续的五次回环检测将不执行优化,这个在代码void LoopClosure::LoopClosureLoop()中有所体现。

loop_closure.h

//
// Created by xin on 23-6-21.
// #ifndef MYSLAM_LOOP_CLOSURE_H
#define MYSLAM_LOOP_CLOSURE_H #include "common_include.h"
#include "keyframeDatabase.h"
#include "frame.h"
#include "map.h" /*
* 回环检测线程,在检测到回环时启动优化
* Map更新由前端触发
*/ namespace myslam {
class Map; class LoopClosure {
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW;
typedef std::shared_ptr<LoopClosure> Ptr; /// 构造函数中启动回环检测优化线程并挂起
LoopClosure(); /// 设置左右目相机,用来获取相机内外参
void SetCameras(Camera::Ptr left, Camera::Ptr right) {
cam_left_ = left;
cam_right_ = right;
} /// 设置地图
void SetMap(std::shared_ptr<Map> map) { map_ = map; } void InsertKeyframe(Frame::Ptr frame); /// 触发地图更新,启动优化
void UpdateMap(); /// 关闭回环检测线程
void Stop(); private:
/// 回环检测线程
void LoopClosureLoop(); int DetectLoop(); /// 对回环上的关键帧和路标点进行优化
void Optimize(Map::KeyframesType &keyframes, Map::LandmarksType &landmarks); public: private: double min_loop_score = 0.4;
double max_loop_score = 0.9; unsigned int frame_passed_ = 0; std::thread loop_closure_thread_; // 建立一个回环检测线程
std::mutex data_mutex_; // 互斥锁,用于在多线程环境下对共享数据进行保护
std::condition_variable map_update_; // 用于通知闭环检测线程有新的地图数据可用,从而触发闭环检测的执行。
std::atomic<bool> loop_closure_running_; // 用来启动或停止闭环检测线程,以及检查线程是否正在运行。 std::shared_ptr<KeyframeDatabase> database_;
Camera::Ptr cam_left_ = nullptr, cam_right_ = nullptr;
std::shared_ptr<Map> map_; std::shared_ptr<Frame> current_frame_; }; } // myslam #endif //MYSLAM_LOOP_CLOSURE_H

loop_closure.cpp

//
// Created by xin on 23-6-21.
// #include "myslam/loop_closure.h"
#include "myslam/algorithm.h"
#include "myslam/feature.h"
#include "myslam/g2o_types.h"
#include "myslam/map.h"
#include "myslam/mappoint.h"
#include "DBoW3/DBoW3.h" namespace myslam {
LoopClosure::LoopClosure() {
database_ = KeyframeDatabase::Ptr(new KeyframeDatabase);
loop_closure_running_.store(true);
loop_closure_thread_ = std::thread(std::bind(&LoopClosure::LoopClosureLoop, this));
} void LoopClosure::Stop() {
loop_closure_running_.store(false);
map_update_.notify_one();
loop_closure_thread_.join();
} void LoopClosure::UpdateMap() {
std::unique_lock<std::mutex> lock(data_mutex_);
map_update_.notify_one();
} int LoopClosure::DetectLoop(){
DBoW3::Database &db = database_->getDatabase();
DBoW3::QueryResults ret;
db.query(current_frame_->descriptors_, ret, 4);
for (const auto& result : ret) {
unsigned int imageId = result.Id; // 获取图像ID
double score = result.Score; // 获取匹配分数
if(score > min_loop_score && score < max_loop_score ){
return imageId;
}
}
return -1;
} void LoopClosure::InsertKeyframe(Frame::Ptr frame) {
current_frame_ = frame;
database_->InsertKeyFrame(frame);
} void LoopClosure::LoopClosureLoop() { while (loop_closure_running_.load()){
std::unique_lock<std::mutex> lock(data_mutex_);
map_update_.wait(lock); int loop_start_index = DetectLoop();
LOG(INFO) <<"Loop Detecting"; if(loop_start_index >= 0){
frame_passed_++;
//这里是由于重复的回环不会使得精度再有提升,因此一次回环检测之后的后五次检测不执行更新
if(frame_passed_ == 1){
int loop_end_index = current_frame_->keyframe_id_;
LOG(INFO) << "\n\n\n\n\n" <<"Loop Detected at" << loop_start_index << "--------" << loop_end_index << "\n\n\n\n"; Map::KeyframesType loop_kfs = map_->GetLoopKeyFrames(loop_start_index, loop_end_index);
Map::LandmarksType loop_landmarks = map_->GetLoopMapPoints(loop_start_index, loop_end_index); /// 回环检测线程优化 回环上的 frames 和 landmarks
Optimize(loop_kfs, loop_landmarks); }
else if (frame_passed_ > 5)
frame_passed_ = 0;
else {
frame_passed_++;
} } }
} void LoopClosure::Optimize(Map::KeyframesType &keyframes, Map::LandmarksType &landmarks) { // setup g2o
typedef g2o::BlockSolver_6_3 BlockSolverType;
typedef g2o::LinearSolverCSparse<BlockSolverType::PoseMatrixType>
LinearSolverType;
auto solver = new g2o::OptimizationAlgorithmLevenberg(
g2o::make_unique<BlockSolverType>(
g2o::make_unique<LinearSolverType>()));
g2o::SparseOptimizer optimizer;
optimizer.setAlgorithm(solver); // pose 顶点,使用Keyframe id
std::map<unsigned long, VertexPose *> vertices;
unsigned long max_kf_id = 0;
for (auto &keyframe : keyframes) {
auto kf = keyframe.second;
VertexPose *vertex_pose = new VertexPose(); // camera vertex_pose
vertex_pose->setId(kf->keyframe_id_);
vertex_pose->setEstimate(kf->Pose());
optimizer.addVertex(vertex_pose);
if (kf->keyframe_id_ > max_kf_id) {
max_kf_id = kf->keyframe_id_;
} vertices.insert({kf->keyframe_id_, vertex_pose});
} // 路标顶点,使用路标id索引
std::map<unsigned long, VertexXYZ *> vertices_landmarks; // K 和左右外参
Mat33 K = cam_left_->K();
SE3 left_ext = cam_left_->pose();
SE3 right_ext = cam_right_->pose(); // edges
int index = 1;
double chi2_th = 5.991; // robust kernel 阈值
std::map<EdgeProjection *, Feature::Ptr> edges_and_features; for (auto &landmark : landmarks) {
if (landmark.second->is_outlier_) continue;
unsigned long landmark_id = landmark.second->id_;
auto observations = landmark.second->GetObs();
for (auto &obs : observations) {
if (obs.lock() == nullptr) continue;
auto feat = obs.lock();
if (feat->is_outlier_ || feat->frame_.lock() == nullptr) continue; auto frame = feat->frame_.lock();
EdgeProjection *edge = nullptr;
if (feat->is_on_left_image_) {
edge = new EdgeProjection(K, left_ext);
} else {
edge = new EdgeProjection(K, right_ext);
} // 如果landmark还没有被加入优化,则新加一个顶点
if (vertices_landmarks.find(landmark_id) ==
vertices_landmarks.end()) {
VertexXYZ *v = new VertexXYZ;
v->setEstimate(landmark.second->Pos());
v->setId(landmark_id + max_kf_id + 1);
v->setMarginalized(true);
vertices_landmarks.insert({landmark_id, v});
optimizer.addVertex(v);
} edge->setId(index);
edge->setVertex(0, vertices.at(frame->keyframe_id_)); // pose
edge->setVertex(1, vertices_landmarks.at(landmark_id)); // landmark
edge->setMeasurement(toVec2(feat->position_.pt));
edge->setInformation(Mat22::Identity());
auto rk = new g2o::RobustKernelHuber();
rk->setDelta(chi2_th);
edge->setRobustKernel(rk);
edges_and_features.insert({edge, feat}); optimizer.addEdge(edge); index++;
}
} // do optimization and eliminate the outliers
optimizer.initializeOptimization();
optimizer.optimize(10); int cnt_outlier = 0, cnt_inlier = 0;
int iteration = 0;
while (iteration < 5) {
cnt_outlier = 0;
cnt_inlier = 0;
// determine if we want to adjust the outlier threshold
for (auto &ef : edges_and_features) {
if (ef.first->chi2() > chi2_th) {
cnt_outlier++;
} else {
cnt_inlier++;
}
}
double inlier_ratio = cnt_inlier / double(cnt_inlier + cnt_outlier);
if (inlier_ratio > 0.5) {
break;
} else {
chi2_th *= 2;
iteration++;
}
} for (auto &ef : edges_and_features) {
if (ef.first->chi2() > chi2_th) {
ef.second->is_outlier_ = true;
// remove the observation
ef.second->map_point_.lock()->RemoveObservation(ef.second);
} else {
ef.second->is_outlier_ = false;
}
} LOG(INFO) << "Outlier/Inlier in optimization: " << cnt_outlier << "/"
<< cnt_inlier; // Set pose and lanrmark position
for (auto &v : vertices) {
keyframes.at(v.first)->SetPose(v.second->estimate());
}
for (auto &v : vertices_landmarks) {
landmarks.at(v.first)->SetPos(v.second->estimate());
} } } // myslam

备注

值得注意的是,由于高翔老师使用的是光流法进行前端的运动估计,我把系统修改为ORB特征点匹配的方法进行前端运动估计和回环检测。

frame类里面添加一个 descriptors_ 成员变量。

frontend.cpp

int Frontend::DetectFeatures() {

    cv::Mat mask(current_frame_->left_img_.size(), CV_8UC1, 255);
for (auto &feat : current_frame_->features_left_) {
cv::rectangle(mask, feat->position_.pt - cv::Point2f(10, 10),
feat->position_.pt + cv::Point2f(10, 10), 0, CV_FILLED);
} std::vector<cv::KeyPoint> keypoints;
// 修改为orb特征
orb_detector_->detectAndCompute(current_frame_->left_img_, mask, keypoints, current_frame_->descriptors_); // gftt_->detect(current_frame_->left_img_, keypoints, mask);
int cnt_detected = 0;
for (auto &kp : keypoints) {
current_frame_->features_left_.push_back(
Feature::Ptr(new Feature(current_frame_, kp)));
cnt_detected++;
} LOG(INFO) << "Detect " << cnt_detected << " new features";
return cnt_detected;
}

最后的运行效果:

代码仅供参考:

https://github.com/CuriosityWang/MySLAM

《视觉SLAM十四讲》第13讲 设计SLAM系统 回环检测线程的实现的更多相关文章

  1. 高翔《视觉SLAM十四讲》从理论到实践

    目录 第1讲 前言:本书讲什么:如何使用本书: 第2讲 初始SLAM:引子-小萝卜的例子:经典视觉SLAM框架:SLAM问题的数学表述:实践-编程基础: 第3讲 三维空间刚体运动 旋转矩阵:实践-Ei ...

  2. 《视觉SLAM十四讲》visual studio 19 + PCL点云创建图像与现实

    SLCM真是博大精深.之前简单的学习了OpenCV,主要是是使用python语言,现在学习SLAM需要使用C++,略难,但比起SLAM本身,不值一提. <视觉SLAM十四讲>里面的环境主要 ...

  3. 浅读《视觉SLAM十四讲:从理论到实践》--操作1--初识SLAM

    下载<视觉SLAM十四讲:从理论到实践>源码:https://github.com/gaoxiang12/slambook 第二讲:初识SLAM 2.4.2 Hello SLAM(书本P2 ...

  4. 高博-《视觉SLAM十四讲》

    0 讲座 (1)SLAM定义 对比雷达传感器和视觉传感器的优缺点(主要介绍视觉SLAM) 单目:不知道尺度信息 双目:知道尺度信息,但测量范围根据预定的基线相关 RGBD:知道深度信息,但是深度信息对 ...

  5. 《视觉SLAM十四讲》第2讲

    目录 一 视觉SLAM中的传感器 二 经典视觉SLAM框架 三 SLAM问题的数学表述 注:原创不易,转载请务必注明原作者和出处,感谢支持! 本讲主要内容: (1) 视觉SLAM中的传感器 (2) 经 ...

  6. 《视觉SLAM十四讲》第1讲

    目录 一 视觉SLAM 注:原创不易,转载请务必注明原作者和出处,感谢支持! 一 视觉SLAM 什么是视觉SLAM? SLAM是Simultaneous Localization and Mappin ...

  7. 视觉slam十四讲第七章课后习题6

    版权声明:本文为博主原创文章,转载请注明出处: http://www.cnblogs.com/newneul/p/8545450.html 6.在PnP优化中,将第一个相机的观测也考虑进来,程序应如何 ...

  8. 视觉slam十四讲第七章课后习题7

    版权声明:本文为博主原创文章,转载请注明出处:http://www.cnblogs.com/newneul/p/8544369.html  7.题目要求:在ICP程序中,将空间点也作为优化变量考虑进来 ...

  9. 视觉SLAM十四讲:从理论到实践 两版 PDF和源码

    视觉SLAM十四讲:从理论到实践 第一版电子版PDF 链接:https://pan.baidu.com/s/1SuuSpavo_fj7xqTYtgHBfw提取码:lr4t 源码github链接:htt ...

  10. 《视觉SLAM十四讲》学习日志(二)——初识SLAM

    小萝卜机器人的例子: 就像这种机器人,它的下面有一组轮子,脑袋上有相机(眼睛),为了让它能够探索一个房间,它需要知道: 1.我在哪——定位 2.周围环境怎么样——建图 定位和建图可以理解成感知的 &q ...

随机推荐

  1. Hugo 静态博客部署

    I. 前提条件 1.1 安装 Hugo 1.1.1 Windows 1.下载 Hugo(建议下载扩展版):Hugo(github.com) 2.解压 Hugo 压缩包到指定目录. 3.[Win + R ...

  2. Wgpu图文详解(05)纹理与绑定组

    前言 什么是纹理? 纹理是图形渲染中用于增强几何图形视觉效果的一种资源.它是一个二维或三维的数据数组,通常包含颜色信息,但也可以包含其他类型的数据,如法线.高度.环境光遮蔽等.纹理的主要目的是为几何图 ...

  3. 面向对象-下(复习:关键字static、单例模式、main()的使用说明、类的结构代码块、属性的赋值顺序、关键字final)

    一.关键字:static static:静态的1.可以用来修饰的结构:主要用来修饰类的内部结构属性.方法.代码块.内部类2.static修饰属性:静态变量(或类变量) 2.1 属性,是否使用stati ...

  4. 认识soui4js(第1篇)

    源代码:https://github.com/soui4js/soui4js soui4js是soui4+quickjs的结合体. soui4是一套c++ directui客户端开发框架,soui4j ...

  5. Python内存管理机制和垃圾回收机制的简单理解

    一.内存管理机制 1.由c开发出来的cpython 2.include / objests 3.需要下载python源码包 4.Pyobject:float PyVarObject: 5.在pytho ...

  6. Luogu P9869 NOIp2023 三值逻辑 题解 [ 绿 ] [ 带权并查集 ]

    三值逻辑:有点坑并且细节较繁琐,但有点板子的并查集. 修改操作 发现对于每个点,只有对他的最后一次操作才是有用的,所以记录下最终的祖先即可. 然而这里并不能用并查集来实现,因为并查集它具有的是传递性, ...

  7. 【忍者算法】从生活到代码:解密链表大数相加的美妙算法|LeetCode第2题"两数相加"

    从生活到代码:解密链表大数相加的美妙算法 从超市收银说起 想象你是一个超市收银员,正在计算两位顾客的购物总和.每位顾客的商品都按照从个位到高位的顺序摆放(比如54元就是先放4元商品,再放50元商品). ...

  8. css的度量单位:px、em、rem、vh、vw、vmin、vmax、百分比

    css的度量单位 px,像素数量,适用于比较固定的场景,比如边框宽度,分割线宽度 em em:是描述相对于应用在当前元素的字体尺寸,所以它也是相对长度单位.一般浏览器字体大小默认为16px,则2em ...

  9. QT5笔记:9. QT的容器类

    QList 中存放对象指针,QVector直接存放对象,所以访问性能更高 QMap中key不可以重复,QMultiMap中key可以重复 QMap在内存中顺序存储,QHash不是顺序存储的(hash算 ...

  10. 项目愿景 (Product Vision)、产品目标 (Product Goal) 、Sprint目标 (Sprint Goal) 及 示例

    愿景(Vision) 是制定业务目标(Business Goal)的基础,后者为确定正确的产品目标 (Product Goal) 创造了环境.同样,每个产品目标作为识别有用的冲刺目标的基础.换句话说, ...