当前位置:   article > 正文

手撕 视觉slam14讲 ch13 代码(7)后端优化 Backend::Optimize()_slam后端优化代码

slam后端优化代码

在上一篇 手撕(6)中的InsertKeyframe()插入关键帧的函数里,有一个 Backend::UpdateMap() 函数 ,从这里通过条件变量 map_update_ 来激活后端优化。

backend.h:

  1. // * 有单独优化线程,在Map更新时启动优化
  2. // * Map更新由前端触发
  3. #ifndef MYSLAM_BACKEND_H
  4. #define MYSLAM_BACKEND_H
  5. #include "MYSLAM/common_include.h"
  6. #include "MYSLAM/frame.h"
  7. #include "MYSLAM/map.h"
  8. namespace MYSLAM{
  9. class Map;
  10. // 后端
  11. class Backend {
  12. public:
  13. EIGEN_MAKE_ALIGNED_OPERATOR_NEW;
  14. typedef std::shared_ptr<Backend> Ptr;//智能指针
  15. /// 构造函数中启动优化线程并挂起
  16. Backend();
  17. // 设置左右目的相机,用于获得内外参
  18. void SetCameras(Camera::Ptr left , Camera::Ptr right){
  19. cam_left_=left;
  20. cam_right_=right;
  21. }
  22. // 设置地图,让backend自己的地图指针指向当前的地图,而不是对当前地图进行修改,不需要锁
  23. void SetMap(std::shared_ptr<Map>map){
  24. map_=map;
  25. }
  26. // 关闭后端线程
  27. void Stop();
  28. // 触发地图更新,启动优化 (notify),
  29. // 主要应该由前端触发,当追踪点少时,添加关键帧并触发更新地图
  30. void UpdateMap();
  31. private:
  32. // 后端线程
  33. void BackendLoop();
  34. // 对给定关键帧和路标点进行优化
  35. void Optimize(Map::KeyframesType &keyframes,Map::LandmarksType &landmarks);
  36. std::shared_ptr<Map>map_; //地图
  37. Camera::Ptr cam_left_ =nullptr , cam_right_=nullptr; // 左右目相机
  38. std::thread backend_thread_;//后端线程
  39. std::mutex data_mutex_;//线程锁,与std::unique_lock <std::mutex> lck(mtx)来对变量上锁
  40. // 线程同步的工具,用于实现线程间的条件变量等待和通知机制。 在多线程编程中,条件变量通常和互斥锁(std::mutex)一起使用,以避免死锁等问题
  41. //用于对运行中的线程进行管理,wait和notify
  42. std::condition_variable map_update_;
  43. // std::atomic 是模板类,一个模板类型为 T 的原子对象中封装了一个类型为 T 的值。
  44. // 原子类型对象不同线程同时访问不会产生数据竞争。
  45. std::atomic<bool> backend_running_;//后端是否没有上锁
  46. };
  47. }//namespace MYSLAM
  48. #endif // MYSLAM_BACKEND_H

后端在构造时,就构建了一个启动优化线程Backend::BackendLoop,并上锁,等待前端唤醒

  1. // 构造函数 启动优化线程并挂起
  2. Backend::Backend(){
  3. // 创建一个线程,线程执行的函数是BackendLoop,并将this绑定到函数,
  4. // 即这是this指向的类的成员函数
  5. backend_running_.store(true);// 设置原子类型值
  6. backend_thread_ = std::thread(std::bind(&Backend::BackendLoop, this));//上锁,唤醒一个wait的线程
  7. }

然后在前端的InsertKeyframe()插入关键帧的函数里,通过 Backend::UpdateMap() 函数 ,触发地图更新,启动优化:

  1. // 触发地图更新,启动优化
  2. void Backend::UpdateMap(){
  3. std::unique_lock<std::mutex> lock(data_mutex_);
  4. map_update_.notify_one(); // 唤醒一个正在等待的线程
  5. }

 其中的条件变量 map_update_ 被激活,使得Backend::BackendLoop()函数解锁开始运行,分别获取激活关键帧和激活地图点,之后调用执行Optimize()函数执行优化:

  1. // 后端线程
  2. void Backend::BackendLoop(){
  3. // load读取backend_running的值
  4. // 实际上当后端在运行时,这是一个死循环函数,但是会等待前端的激活,即前端激活一次,就运行此函数,进行一次后端优化
  5. while (backend_running_.load())// 读取原子类型值
  6. {
  7. std::unique_lock<std::mutex>lock(data_mutex_);
  8. map_update_.wait(lock);//锁住当前线程
  9. }
  10. // 后端仅优化激活的Frames和mappoints
  11. Map::KeyframesType active_kfs = map_->GetActiveKeyFrames(); // 获取激活关键帧
  12. Map::LandmarksType active_landmarks =map_->GetActiveMapPoints(); // 获取激活地图点
  13. Optimize(active_kfs,active_landmarks); //执行优化
  14. }

优化函数:Backend::Optimize():

这里的优化流程 和前端的 EstimateCurrentPose() 函数有点类似,不同的地方是,在前端做这个优化的时候,只有一个顶点,也就是仅有化当前帧位姿这一个变量,因此边也都是一元边。

而在后端优化里面,局部地图中的所有关键帧位姿和地图点都是顶点,边也是二元边,在 g2o_types.h 文件中 class EdgeProjection 的 linearizeOplus()函数中,新增了一项 重投影误差对地图点的雅克比矩阵,见《视觉slam14讲》第二版 187页,公式(7.48)

优化流程依然:

  • 步骤一: 创建线性方程求解器,确定分解方法
  • 步骤二: 构造线性方程的矩阵块,并用上面定义的线性求解器初始化
  • 步骤三: 创建总求解器 solver,并从 GN, LM, DogLeg 中选一个,再用上述块求解器 BlockSolver 初始化
  • 步骤四: 创建稀疏优化器(SparseOptimizer)
  • 步骤五: 自定义图的顶点和边,并添加到 SparseOptimizer 优化器中
  • 步骤六: 设置优化参数,开始执行优化
  • 步骤七: 使用卡方检查来剔除外点,同时调整阈值
  • 步骤八:迭代优化完成之后,我们把优化估计的pose和地图点给关键帧,并把关键帧对应的异常特征去除,最后返回内点个数。
  1. //后端优化函数
  2. void Backend::Optimize(Map::KeyframesType &keyframes, Map::LandmarksType &landmarks){
  3. // 设定g2o
  4. typedef::g2o::BlockSolver_6_3 BlockSolverType;
  5. typedef::g2o::LinearSolverCSparse<BlockSolverType::PoseMatrixType>LinearSolverType; //线性求解器类型
  6. // 块求解器BlockSolver
  7. auto solver = new g2o::OptimizationAlgorithmLevenberg (
  8. g2o::make_unique<BlockSolverType>( g2o::make_unique<LinearSolverType>())); // 选择梯度下降法
  9. g2o::SparseOptimizer optimizer; //稀疏求解
  10. optimizer.setAlgorithm(solver); //设置求解器
  11. // vertex(优化量 顶点)
  12. // pose 顶点 使用Keyframe id
  13. std::map<unsigned long ,VertexPose *>vertices; // 定义了一个名为vertices的std::map,它的键是unsigned long类型,值是指向位姿VertexPose对象的指针。
  14. unsigned long max_kf_id = 0;
  15. // 遍历关键帧,确定第一个顶点
  16. for(auto &keyframe : keyframes){
  17. auto kf=keyframe.second;
  18. VertexPose *vertex_pose=new VertexPose();
  19. vertex_pose->setId(kf->keyframe_id_);// 定义节点编号
  20. vertex_pose->setEstimate(kf->Pose());//设置初值
  21. optimizer.addVertex(vertex_pose);//把节点添加到图中
  22. if (kf->keyframe_id_>max_kf_id)
  23. {
  24. max_kf_id=kf->keyframe_id_;
  25. }
  26. vertices.insert({kf->keyframe_id_,vertex_pose});
  27. }
  28. // 路标顶点,使用路标id索引
  29. std::map<unsigned long, VertexXYZ *> vertices_landmarks;
  30. // 内参和左右外参
  31. Mat33 K=cam_left_->K();
  32. SE3 left_ext =cam_left_->pose();
  33. SE3 right_ext =cam_right_->pose();
  34. // edge边
  35. int index =1 ;
  36. double chi2_th = 5.991; // robust kernel 阈值
  37. std::map<EdgeProjection*,Feature::Ptr>edges_and_features;
  38. // 每一个landmark均需要建立一条边,所以landmark vertex的定义与edge的定义同步进行
  39. // 遍历地图点,取出观测到该路标点的特征
  40. for(auto &landmark : landmarks){
  41. if (landmark.second->is_outlier_) //外点不优化
  42. {
  43. continue;
  44. }
  45. unsigned long landmark_id = landmark.second->id_; //路标点id
  46. auto observations=landmark.second->GetObs();// 取出观测到该路标点的特征
  47. // 再对特征进行遍历,得到该特征所处的帧
  48. for(auto obs: observations){
  49. if (obs.lock()=nullptr)
  50. {
  51. continue;
  52. }
  53. auto feat=obs.lock();
  54. if (feat->is_outlier_ || feat->frame_.lock() ==nullptr)
  55. {
  56. continue;
  57. }
  58. auto frame=feat->frame_.lock();
  59. EdgeProjection *edge=nullptr;
  60. // 提供内参矩阵K,和初始化的初值
  61. if (feat->is_on_left_image_)
  62. {
  63. edge=new EdgeProjection(K,left_ext);
  64. }else
  65. {
  66. edge=new EdgeProjection(K,right_ext);
  67. }
  68. // 如果landmark还没有被加入优化,则新加一个顶点
  69. if (vertices_landmarks.find(landmark_id) == vertices_landmarks.end())
  70. {
  71. VertexXYZ *v = new VertexXYZ;
  72. v->setId(landmark_id + max_kf_id + 1);// 定义节点编号
  73. v->setEstimate(landmark.second->Pos());//设置初值
  74. v->setMarginalized(true); //边缘化
  75. vertices_landmarks.insert({landmark_id, v});
  76. optimizer.addVertex(v);//把节点添加到图中
  77. }
  78. // 设置edge的参数
  79. edge->setId(index);
  80. edge->setVertex(0,vertices.at(frame->keyframe_id_));// 设置连接的顶点:pose
  81. edge->setVertex(1, vertices_landmarks.at(landmark_id)); // 设置连接的顶点:landmark
  82. edge->setMeasurement(toVec2(feat->position_.pt));//传入观测值
  83. edge->setInformation(Mat22::Identity());// 信息矩阵
  84. //鲁棒核函数
  85. auto rk =new g2o::RobustKernelHuber();
  86. rk->setDelta(chi2_th);
  87. edge->setRobustKernel(rk);
  88. edges_and_features.insert({edge, feat});
  89. optimizer.addEdge(edge);//把边添加到图中
  90. index++;
  91. }
  92. }
  93. // do optimization and eliminate the outliers
  94. // 执行优化并去除外点
  95. optimizer.initializeOptimization();// 设置优化初始值
  96. optimizer.optimize(10);// 设置优化次数
  97. //设置内点和外点数量
  98. int cnt_outlier = 0, cnt_inlier = 0;
  99. int iteration = 0;
  100. while (iteration<5)
  101. {
  102. cnt_outlier = 0;
  103. cnt_inlier = 0;
  104. // 统计内点和外点
  105. for(auto &ef : edges_and_features){
  106. if (ef.first->chi2()>chi2_th)
  107. {
  108. cnt_outlier++;
  109. }else{
  110. cnt_inlier++;
  111. }
  112. }
  113. //确保内点占1/2以上,否则调整阈值*2,直到迭代结束
  114. double inlier_ratio = cnt_inlier/ double(cnt_inlier + cnt_outlier);
  115. if (inlier_ratio>0.5)
  116. {
  117. break;
  118. }else{
  119. chi2_th *= 2;
  120. iteration++;
  121. }
  122. }
  123. // 记录是否为异常特征
  124. for(auto &ef : edges_and_features){
  125. if (ef.first->chi2()>chi2_th){
  126. ef.second->is_outlier_=true; //记为为异常特征
  127. // remove the observation
  128. ef.second->map_point_.lock()->RemoveObservation(ef.second);
  129. }else{
  130. ef.second->is_outlier_=false;//记为为正常特征
  131. }
  132. }
  133. LOG(INFO) << "Outlier/Inlier in optimization: " << cnt_outlier << "/"<< cnt_inlier;
  134. // Set pose and lanrmark position
  135. for(auto &v: vertices){
  136. keyframes.at(v.first)->SetPose(v.second->estimate());
  137. }
  138. for (auto &v : vertices_landmarks) {
  139. landmarks.at(v.first)->SetPos(v.second->estimate());
  140. }
  141. }

 其中我们依然重点关注 步骤五 自定义的顶点和边

顶点(Vertex)

位姿顶点pose 和之前VertexPose的定义一样

  1. // 位姿顶点
  2. class VertexPose : public g2o::BaseVertex<6,SE3>{//优化量的参数模板
  3. public:
  4. EIGEN_MAKE_ALIGNED_OPERATOR_NEW;
  5. // 重置,设定被优化变量的原始值
  6. virtual void setToOriginImpl() override
  7. {
  8. _estimate =SE3();
  9. }
  10. // 更新
  11. virtual void oplusImpl(const double * update) override
  12. {
  13. Vec6 update_eigen;
  14. update_eigen << update[0], update[1], update[2], update[3], update[4], update[5];
  15. _estimate= SE3::exp(update_eigen) * _estimate; // 左乘更新 SE3 - 旋转矩阵R
  16. }
  17. // 存盘、读盘
  18. virtual bool read(std::istream &in) override { return true; } //存盘
  19. virtual bool write(std::ostream &out) const override { return true; } //读盘
  20. };

地图点顶点:

  1. class VertexXYZ : public g2o::BaseVertex<3,Vec3>{//优化量的参数模板
  2. public:
  3. EIGEN_MAKE_ALIGNED_OPERATOR_NEW;
  4. // 重置,设定被优化变量的原始值
  5. virtual void setToOriginImpl() override
  6. {
  7. _estimate =Vec3::Zero();
  8. }
  9. // 更新
  10. virtual void oplusImpl(const double *update) override
  11. {
  12. _estimate[0] += update[0];
  13. _estimate[1] += update[1];
  14. _estimate[2] += update[2];
  15. }
  16. // 存盘、读盘
  17. virtual bool read(std::istream &in) override { return true; } //存盘
  18. virtual bool write(std::ostream &out) const override { return true; } //读盘
  19. };

边(edge)

此时为包含地图的二元边,和一元边相比,新增了一项 重投影误差对地图点的雅克比矩阵_jacobianOplusXj,见《视觉slam14讲》第二版 187页,公式(7.48)

  1. //包含地图的二元边
  2. class EdgeProjection : public g2o::BaseBinaryEdge<2,Vec2,VertexPose,VertexXYZ>{
  3. public:
  4. EIGEN_MAKE_ALIGNED_OPERATOR_NEW;
  5. // 构造函数,构造时传入相机内外参
  6. EdgeProjection(const SE3 &cam_ext, const Mat33 &K ): _cam_ext(cam_ext), _K(K) {}
  7. // 计算雅克比矩阵
  8. virtual void computeError() override {
  9. const VertexPose *v0 = static_cast<VertexPose *>(_vertices[0]);
  10. const VertexXYZ *v1 = static_cast<VertexXYZ *>(_vertices[1]);
  11. SE3 T = v0->estimate();
  12. Vec3 pos_pixel = _K * (_cam_ext * (T * v1->estimate())); //估计值:T*p,得到相机坐标系下坐标,然后在利用camera2pixel()函数得到像素坐标。
  13. pos_pixel /= pos_pixel[2];
  14. _error = _measurement - pos_pixel.head<2>();
  15. }
  16. // 计算雅克比矩阵
  17. virtual void linearizeOplus() override{
  18. const VertexPose *v0=static_cast<VertexPose*>(_vertices[0]);
  19. const VertexXYZ *v1 = static_cast<VertexXYZ *>(_vertices[1]);
  20. SE3 T = v0->estimate();
  21. Vec3 pw=v1->estimate();
  22. Vec3 pos_cam=_cam_ext*T*pw;
  23. double fx = _K(0, 0);
  24. double fy = _K(1, 1);
  25. double X = pos_cam[0];
  26. double Y = pos_cam[1];
  27. double Z = pos_cam[2];
  28. double Zinv = 1.0 / (Z + 1e-18);
  29. double Zinv2 = Zinv * Zinv;
  30. //2*6的雅克比矩阵
  31. _jacobianOplusXi << -fx * Zinv, 0, fx * X * Zinv2, fx * X * Y * Zinv2,
  32. -fx - fx * X * X * Zinv2, fx * Y * Zinv, 0, -fy * Zinv,
  33. fy * Y * Zinv2, fy + fy * Y * Y * Zinv2, -fy * X * Y * Zinv2,
  34. -fy * X * Zinv;
  35. //P187 (7.48) 的雅克比矩阵
  36. _jacobianOplusXj = _jacobianOplusXi.block<2, 3>(0, 0) *
  37. _cam_ext.rotationMatrix() * T.rotationMatrix();
  38. }
  39. // 读操作
  40. virtual bool read(std::istream &in) override { return true; }
  41. // 写操作
  42. virtual bool write(std::ostream &out) const override { return true; }
  43. private:
  44. SE3 _cam_ext;
  45. Mat33 _K;
  46. };

优化函数通过最后重新设置这些关键帧的pose和地图点的pose,将后端优化的结果反馈给了前端,至此,后端优化过程结束!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/你好赵伟/article/detail/625320
推荐阅读
相关标签
  

闽ICP备14008679号