接下来写一个开源SLAM算法系列吧,本期介绍BLAM算法。BLAM算法是伯克利的一位大牛写的,首先名字就很有意思,BLAM表示

B(erkeley) L(localization) A(nd) M(apping)!

早在三年前就已经进行了开源,算法使用iSAM2搭建因子图,属于SAM系列弄潮儿之一,常常与LEGO-LOAM、LIO-SAM等算法一同被提起。

BLAM对新手友好,因此决定在这里写几篇博客,对BLAM在线部分进行解析,分享自己的见解,欢迎交流、点赞!

========================================================================

Source Code:

github:https://github.com/erik-nelson/blam

码云:https://gitee.com/meadowair/BLAM

========================================================================

从整个系统的main函数读起,找到其位于src\blam_slam\src\blam_slam.cc中。

blam_slam.cc文件直接进行初始化:

  if (!bs.Initialize(n, false /* online processing */)) {

其中两个参数,n代表ros句柄,false代表不进行日志记录。

让我们跳转到BlamSlam.cc,看一下初始化的作用:

首先在句柄n的命名空间blam_slam中,附加命名空间BlamSlam:

name_ = ros::names::append(n.getNamespace(), "BlamSlam");

这样这个节点所发出的所有话题、订阅者发布者等,在名称前均被冠以“blam_slam/BlamSlam”

过滤器初始化

接下来初始化过滤器filter_

  if (!filter_.Initialize(n)) {

后面的内容先放放,接下来就看看初始化过滤器做了什么:
跳转至src\point_cloud_filter\src\PointCloudFilter.cc,

首先在命名空间后方继续追加PointCloudFilter:

  name_ = ros::names::append(n.getNamespace(), "PointCloudFilter");

紧接着加载参数:

  if (!LoadParameters(n)) {

下面列出加载的参数,均存在于src\point_cloud_filter\config\parameters.yaml:

bool PointCloudFilter::LoadParameters(const ros::NodeHandle& n) {// Load filtering parameters.if (!pu::Get("filtering/grid_filter", params_.grid_filter)) return false;if (!pu::Get("filtering/grid_res", params_.grid_res)) return false;if (!pu::Get("filtering/random_filter", params_.random_filter)) return false;if (!pu::Get("filtering/decimate_percentage", params_.decimate_percentage))return false;if (!pu::Get("filtering/outlier_filter", params_.outlier_filter)) return false;if (!pu::Get("filtering/outlier_std", params_.outlier_std)) return false;if (!pu::Get("filtering/outlier_knn", params_.outlier_knn)) return false;if (!pu::Get("filtering/radius_filter", params_.radius_filter)) return false;if (!pu::Get("filtering/radius", params_.radius)) return false;if (!pu::Get("filtering/radius_knn", params_.radius_knn)) return false;// Cap to [0.0, 1.0].params_.decimate_percentage =std::min(1.0, std::max(0.0, params_.decimate_percentage));return true;
}

过滤器参数有:

grid_filter:是否使用体素滤波,默认为true;

grid_res:体素滤波分辨率,默认为0.2,即20cm;

random_filter:是否使用随机滤波,默认为true;

decimate_percentage:随机滤波百分比,即滤波后保留多少,默认为0.9,即保留90%;

outlier_filter:是否使用统计滤波,默认为false,我不建议打开统计滤波,因为较为消耗计算量;

outlier_std:统计滤波标准差阈值,默认为1;

outlier_knn:统计滤波聚类阈值,默认为10;

radius_filter:是否使用半径滤波,默认为false,当点云稀疏时不必打开;

radius:半径滤波的作用域,默认为0.15,即每个点0.15cm的范围内进行寻找临近点;

radius_knn:半径内临近点阈值,默认为3,即此点作用域内没有超过3个的其他点,则此点被过滤。

接下来继续过滤器的初始化。创建一个局部的ros句柄来管理回调。

bool PointCloudFilter::RegisterCallbacks(const ros::NodeHandle& n) {// Create a local nodehandle to manage callback subscriptions.ros::NodeHandle nl(n);return true;
}

里程计初始化

让我们回到src\blam_slam\src\BlamSlam.cc,看到接下来是里程计odometry_的初始化:

追加命名空间:

name_ = ros::names::append(n.getNamespace(), "PointCloudOdometry");

加载里程计相关参数,均位于src\blam_example\config\blam_frames.yaml和src\point_cloud_odometry\config\parameters.yaml和:

bool PointCloudOdometry::LoadParameters(const ros::NodeHandle& n) {// Load frame ids.if (!pu::Get("frame_id/fixed", fixed_frame_id_)) return false;if (!pu::Get("frame_id/odometry", odometry_frame_id_)) return false;// Load initial position.double init_x = 0.0, init_y = 0.0, init_z = 0.0;double init_roll = 0.0, init_pitch = 0.0, init_yaw = 0.0;if (!pu::Get("init/position/x", init_x)) return false;if (!pu::Get("init/position/y", init_y)) return false;if (!pu::Get("init/position/z", init_z)) return false;if (!pu::Get("init/orientation/roll", init_roll)) return false;if (!pu::Get("init/orientation/pitch", init_pitch)) return false;if (!pu::Get("init/orientation/yaw", init_yaw)) return false;gu::Transform3 init;init.translation = gu::Vec3(init_x, init_y, init_z);init.rotation = gu::Rot3(init_roll, init_pitch, init_yaw);integrated_estimate_ = init;// Load algorithm parameters.if (!pu::Get("icp/tf_epsilon", params_.icp_tf_epsilon)) return false;if (!pu::Get("icp/corr_dist", params_.icp_corr_dist)) return false;if (!pu::Get("icp/iterations", params_.icp_iterations)) return false;if (!pu::Get("icp/transform_thresholding", transform_thresholding_)) return false;if (!pu::Get("icp/max_translation", max_translation_)) return false;if (!pu::Get("icp/max_rotation", max_rotation_)) return false;return true;
}

里程计参数有:

frame_id/fixed:全局固定系,默认为world;

* frame_id/odometry:里程计坐标系,也就是机器人系,默认为base;

init_x,init_y,init_z,init_roll,init_pitch,init_yaw:初始位姿,应用时可均设置为0,以上内容被综合为init.translation和init.rotation,init类型为gu::Transform3,底层也是由Eigen进行搭建的;

integrated_estimate_:积分估计,这里先被赋予init;

icp_tf_epsilon:icp迭代的阈值,小于此值icp终止。默认为0.0000000001;

icp_corr_dist:icp过程中,大于此距离的对应点认为是不可靠点,进行滤除,默认为2m;

icp_iterations:icp迭代次数,默认为10;

transform_thresholding_:是否使用icp相对变换阈值,超过阈值则认为此icp结果不被接受,默认为false;

max_translation_:最大平移量阈值0.09,即9cm;

max_rotation_:最大旋转量阈值0.1弧度,即大概5.7度。

接下来使用RegisterCallbacks创建局部句柄,值得注意的是之后创建了发布者:

  query_pub_ = nl.advertise<PointCloud>("odometry_query_points", 10, false);reference_pub_ =nl.advertise<PointCloud>("odometry_reference_points", 10, false);incremental_estimate_pub_ = nl.advertise<geometry_msgs::PoseStamped>("odometry_incremental_estimate", 10, false);integrated_estimate_pub_ = nl.advertise<geometry_msgs::PoseStamped>("odometry_integrated_estimate", 10, false);

query_pub_、reference_pub_均发布点云类型pcl::PointCloud,具体干什么以后再说,incremental_estimate_pub_和integrated_estimate_pub_均发布位姿数据,前者是强调“递增”,后者强调“积分”,不知道为什么,以后再说。

回环检测初始化

让我们回到src\blam_slam\src\BlamSlam.cc,看到接下来是回环模块loop_closure_的初始化:

追加命名空间:

  name_ = ros::names::append(n.getNamespace(), "LaserLoopClosure");

值得注意的是,回环模块有自己的过滤器,也就是loop_closure_自己的成员变量,但是加载的参数是与外部的过滤器保持一致的:

  if (!filter_.Initialize(n)) {

接下来加载回环模块的参数,与前面的有部分重复,包括坐标系名称、icp的相关参数、初始位姿、位姿噪声,都是加载的同样的参数,就不一一列举了,列举关键参数,主要来自于src\laser_loop_closure\config\parameters.yaml:

* SAM框架参数,relinearize_skip,由于对sam理解不深,因此目前无法做出解释,但是一般设置为1,此处也默认为1;

  if (!pu::Get("relinearize_skip", relinearize_skip)) return false;

* 平移阈值,translation_threshold,即机器人运动了多长的平移距离后,才考虑向因子图中加入新的位姿节点(以下说的位姿,均指位姿节点),默认为0.5m,增大此值可以降低计算量,提高实时性;

  if (!pu::Get("translation_threshold", translation_threshold_)) return false;

* 回环检测的搜索范围,proximity_threshold,即机器人处在当前位姿进行回环的时候,仅找此范围内的旧位姿进行回环检测,增加他会提高计算量,降低实时性,但是可以检测到更广的回环;

  if (!pu::Get("proximity_threshold", proximity_threshold_)) return false;

* 回环检测分数阈值,max_tolerable_fitness,当前位姿与旧位姿进行回环检测icp的时候,低于此阈值才认为回环正确,大于此值则认为并不存在回环。简而言之,此值越小,回环越难;

  if (!pu::Get("max_tolerable_fitness", max_tolerable_fitness_)) return false;

* 回环检测排除范围,skip_recent_poses,即当前位姿与刚过去的几个位姿不要去检测回环,没什么必要,默认为20,即刚过去的20个位姿不要去检测回环;

  if (!pu::Get("skip_recent_poses", skip_recent_poses_)) return false;

* 回环检测关闭区段,poses_before_reclosing,即当完成一次正确的闭环后,多少个位姿之内不要再进行闭环。也就是说,当机器人闭环一次后,至少需要行走poses_before_reclosing*translation_threshold米才能进行新的回环。translation_threshold是位姿节点加入的间隔距离;

  if (!pu::Get("poses_before_reclosing", poses_before_reclosing_)) return false;

* 下面构建SAM结构,为:

  // Create the ISAM2 solver.ISAM2Params parameters;parameters.relinearizeSkip = relinearize_skip;parameters.relinearizeThreshold = relinearize_threshold;isam_.reset(new ISAM2(parameters));// Set the initial position.Vector3 translation(init_x, init_y, init_z);Rot3 rotation(Rot3::RzRyRx(init_roll, init_pitch, init_yaw));Pose3 pose(rotation, translation);// Set the covariance on initial position.Vector6 noise;noise << sigma_x, sigma_y, sigma_z, sigma_roll, sigma_pitch, sigma_yaw;LaserLoopClosure::Diagonal::shared_ptr covariance(LaserLoopClosure::Diagonal::Sigmas(noise));// Initialize ISAM2.NonlinearFactorGraph new_factor;Values new_value;new_factor.add(MakePriorFactor(pose, covariance));new_value.insert(key_, pose);  // 插入初始位姿的因子isam_->update(new_factor, new_value);  // 更新因子图values_ = isam_->calculateEstimate();  // 得到当前的估计key_++; // 因子键向后移动// Set the initial odometry.  // 设置初始里程计,gtsam::Pose3类型odometry_ = Pose3::identity();

接下来继续回环模块的初始化,RegisterCallbacks

  if (!RegisterCallbacks(n)) {

除了创建局部句柄以外,设定了6个发布者,odometry_edge_pub_、loop_edge_pub_、graph_node_pub_、closure_area_pub_、scan1_pub_、scan2_pub_,均为可视化回环检测相关结果的。

定位模块初始化

回到BlamSlam.cc的

  if (!localization_.Initialize(n)) {

关注定位模块localization_的初始化。即在src\point_cloud_localization\src\PointCloudLocalization.cc中:

还是同样的方式,追加局部命名空间,然后加载参数,关键参数主要位于src\point_cloud_localization\config\parameters.yaml,具体为:

integrated_estimate_:对积分估计初始化,与里程计模块类似。

* 与icp相关的,之前也都介绍过,但是这是位于不同配置文件的:

tf_epsilon: 0.0000000001

corr_dist: 1.0

iterations: 10

transform_thresholding: false

max_translation: 0.05

max_rotation: 0.1

接下来仍然是RegisterCallbacks,除了创建局部句柄外,定义了5个发布者,分别为发布点云类型的query_pub_、reference_pub_、aligned_pub_,和发布位姿类型的incremental_estimate_pub_、integrated_estimate_pub_。

建图模块初始化

仍然是回到BlamSlam.cc,接着向下走

  if (!mapper_.Initialize(n)) {

加载的参数就一个位于src\point_cloud_mapper\config\parameters.yaml中

* octree_resolution:八叉树地图分辨率,默认为0.05 (这个八叉树是不是可以考虑到为规划提供地图????)

紧接着直接建立八叉树地图:

  map_octree_.reset(new Octree(octree_resolution_));map_octree_->setInputCloud(map_data_);

接下来仍然是不出意外的RegisterCallbacks,除了建立局部句柄以外,建立两个发布者,发布当前地图和累积的地图,类型都是点云类型PointCloud。

总参数加载

仍然是回到BlamSlam.cc,接着向下走

  if (!LoadParameters(n)) {

附上代码

bool BlamSlam::LoadParameters(const ros::NodeHandle& n) {// Load update rates.if (!pu::Get("rate/estimate", estimate_update_rate_)) return false;if (!pu::Get("rate/visualization", visualization_update_rate_)) return false;// Load frame ids.if (!pu::Get("frame_id/fixed", fixed_frame_id_)) return false;if (!pu::Get("frame_id/base", base_frame_id_)) return false;return true;
}

值得注意的是,除了位于src\blam_example\config\blam_frames.yaml的坐标系配置外,加载了位于src\blam_example\config\blam_rates.yaml文件中的帧率信息,即:

estimate:位姿更新频率;

visualization:可视化信息更新频率,当可视化消耗太多资源时,可降低数值。

总回调注册

生成局部句柄外,根据from_log参数,也就是一开始在  if (!bs.Initialize(n, false /* online processing */)) {的时候加载的参数,默认为false,根据他决定是进入RegisterLogCallbacks还是RegisterOnlineCallbacks,默认进入RegisterOnlineCallbacks,因此我们重点关注RegisterOnlineCallbacks

bool BlamSlam::RegisterOnlineCallbacks(const ros::NodeHandle& n) {ROS_INFO("%s: Registering online callbacks.", name_.c_str());// Create a local nodehandle to manage callback subscriptions.ros::NodeHandle nl(n);estimate_update_timer_ = nl.createTimer(estimate_update_rate_, &BlamSlam::EstimateTimerCallback, this);pcld_sub_ = nl.subscribe("pcld", 100, &BlamSlam::PointCloudCallback, this);return CreatePublishers(n);
}

estimate_update_timer_是一个定时器,由createTimer函数进行创建,定时进行回调,回调的频率由第一个参数设置,回调的函数为第二个参数,第三个参数表示将this传到回调函数中。

pcld_sub_为订阅者,订阅激光数据,因此可以看作程序的入口,下一节我们将从这里入手进行介绍,也就是回调函数&BlamSlam::PointCloudCallback。

CreatePublishers创建了发布者,base_frame_pcld_pub_,即发布当前帧点云,rviz显示的那些好看的、车辆周围的点云就是他发布的。

下一节从激光雷达回调函数入手,看看激光数据是如何激活整个SLAM的。

BLAM源码解析(一)—— 模块初始化相关推荐

  1. Libuv源码解析 - uv_loop整个初始化模块

    Libuv源码解析 - uv_loop整个初始化模块 loop_default_loop static uv_loop_t default_loop_struct; static uv_loop_t* ...

  2. thttpd源码解析 定时器模块

    thttpd源码解析 定时器模块 thttpd是非常轻量级的http服务器,可执行文件仅50kB.名称中的第一个t表示tiny, turbo, 或throttling 与lighttpd.memcac ...

  3. datax源码解析-JobContainer的初始化阶段解析

    datax源码解析-JobContainer的初始化阶段解析 写在前面 此次源码分析的版本是3.0.因为插件是datax重要的组成部分,源码分析过程中会涉及到插件部分的源码,为了保持一致性,插件都已大 ...

  4. nginx源码分析之模块初始化

    在nginx启动过程中,模块的初始化是整个启动过程中的重要部分,而且了解了模块初始化的过程对应后面具体分析各个模块会有事半功倍的效果.在我看来,分析源码来了解模块的初始化是最直接不过的了,所以下面主要 ...

  5. BLAM源码解析(五)—— 回环检测

    上一节介绍了BLAM的帧间匹配和帧图匹配,代码简洁明了. 本节介绍BLAM的回环检测模块.具体代码块如下: // Check for new loop closures.bool new_keyfra ...

  6. linux内核radeon gpu源码解析3 —— Radeon初始化

    解析DRM代码,以从底层介绍显卡驱动的初始化过程,显卡类型是AMD的radeon r600以后的系列显卡.基本的过程就是驱动载入,硬件初始化,设置硬件独立的模块(如内存管理器),设置显示(分辨率等). ...

  7. Mybatis源码解析之Mybatis初始化过程

    一.搭建一个简单的Mybatis工程 为了了解Mybatis的初始化过程,这里需要搭建一个简单的Mybatis工程操作数据库,工程结构如下: 一个UserBean.java private int i ...

  8. BLAM源码解析(四)—— 基于ICP的位姿更新

    第三节我们介绍了定时器的定时回调,实现对激光数据的批量循环处理,在每一个激光数据的循环当中,除了一开始filter_的点云过滤,最重要的其实是下面的基于ICP的位姿更新,即 if (!odometry ...

  9. BLAM源码解析(二)—— 从激光回调入手

    上一节(https://blog.csdn.net/weixin_44684139/article/details/118279360)我们介绍了系统各个模块的初始化,加载了许多的参数,创建了许多的发 ...

最新文章

  1. 论文:Multi-Objective Modified Grey Wolf Optimizer for Optimal Power Flow-最优潮流
  2. Oracle数据库查看表空间是否为自增的
  3. hdoj1423 最长上升公共子序列
  4. overfitting
  5. Java基础之扩展GUI——添加状态栏(Sketcher 1 with a status bar)
  6. Java生鲜电商平台-SpringCloud微服务架构高并发参数优化实战
  7. leetcode —— 1038. 从二叉搜索树到更大和树
  8. ncnn arm linux,arm ncnn
  9. Cookie字符串转Map集合方法
  10. thinkphp5--文章发布后台管理系统
  11. JavaScript 数据类型检测终极解决方案
  12. 三维人脸重建:精读3dmm.py
  13. 买了北京亲子年票但没有小孩的朋友,接下来的一年我都给你安排好啦!!...
  14. Windows+Caffe+VS2013+python接口配置过程
  15. Android 添加GridView中图片的圆角样式
  16. DNA计算 与 肽展公式 推导 AOPM-A 变胸腺苷, AOPM-O尿胞变腺苷, AOPM-P尿胞变鸟苷, AOPM-M鸟腺苷的 S形螺旋纹 血氧峰 触发器分子式 严谨完整过程
  17. 通过数据采集推动内容营销
  18. IPv4地址不够怎么解决
  19. idea插件开发-环境搭建
  20. 医生说 | 当心!长期情绪压抑,正在毁掉你的身体

热门文章

  1. JavaScript头像图片上传插件支持上传类型大小尺寸验证
  2. TMS320F28377S 学习笔记2 BGA封装的焊接
  3. 云原生爱好者周刊:美国国家安全局发布网络安全指南
  4. 微服务拆分原则之 AKF
  5. C++的双缓冲队列机制
  6. BAMBOOROSY编舞,灵感来自THE SEA【大型圣诞狂欢派对系列宣传】
  7. 网易2018校园招聘:射击游戏 [python]
  8. java long 空_long类型判断是否为空
  9. 位数不足前面补0mysql语句_SQL语句 不足位数补0
  10. Matlab矩阵基本操作(定义,运算)