从零开始搭二维激光SLAM --- 基于PL-ICP的帧间匹配

2024-04-02 22:18

本文主要是介绍从零开始搭二维激光SLAM --- 基于PL-ICP的帧间匹配,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

上一篇文章将了如何使用PCL中的ICP算法进行相邻帧间的坐标变换,ICP的计算时间以及精度都不太好.

这篇文章我使用一种ICP的改进算法PL-ICP算法来计算相邻帧间的坐标变换.

1 PL-ICP

PL-ICP(Point to Line ICP) 使用点到线距离最小的方式进行ICP的计算,收敛速度快很多,同时精度也更高一些.

具体的pl-icp的介绍请看其论文,作者也开源了pl-icp的代码,作者将实现pl-icp的代码命名为csm( Canonical Scan Matcher).

论文与代码的详情请看作者的网站https://censi.science/software/csm/.

看看情况以后再来对这个算法进行一下公式推倒.

2 scan_tools

ros中有人使用使用csm包进行了ros下的实现,进行了扫描匹配与位姿累加的实现,但是没有发布odometry的topic与tf.包的名字为 laser_scan_matcher,是scan_tools包集中的一个.

首先介绍一下scan_tools包集,这个包里提供了很多操作二维激光雷达数据的功能,虽然不一定能直接用,但是其处理数据的思路是非常值得借鉴的.

2.1 wiki地址

其wiki的地址为: http://wiki.ros.org/scan_tools?distro=kinetic

2.2 功能简介

其包含的功能包名字如下所示,并对其功能进行了简要介绍.

  • laser_ortho_projector: 将切斜的雷达数据投影到平面上.
  • laser_scan_matcher: 基于pl-icp的扫描匹配的实现,并进行了位姿累加
  • laser_scan_sparsifier: 对雷达数据进行稀疏处理
  • laser_scan_splitter: 将一帧雷达数据分段,并发布出去
  • ncd_parser: 读取New College Dataset,转换成ros的scan 与 odometry 发布出去
  • polar_scan_matcher: 基于Polar Scan Matcher的扫描匹配器的ros实现
  • scan_to_cloud_converter: 将 sensor_msgs/LaserScan 数据转成 sensor_msgs/PointCloud2 的数据格式.

我之前的那篇将雷达数据转成 sensor_msgs/PointCloud2 的文章,就是参考scan_to_cloud_converter包来实现的.

如果想要深入学习一下的请去wiki的网址,看介绍及源码,这里就不再过多介绍了.

3 代码

3.1 获取代码

代码已经提交在github上了,如果不知道github的地址的朋友, 请在我的公众号: 从零开始搭激光SLAM 中回复 开源地址 获得。

推荐使用 git clone 的方式进行下载, 因为代码是正处于更新状态的, git clone 下载的代码可以使用 git pull 很方便地进行更新.

本篇文章对应的代码为lesson3

3.2 依赖

由于这次的包依赖了csm这个包,所以要先安装一下csm

sudo apt-get install ros-kinetic-csm

3.3 CMakeLists.txt

需要在CMakeLists.txt中添加如下内容,以在程序中使用csm

# Find csm project
find_package(PkgConfig)
pkg_check_modules(csm REQUIRED csm)
include_directories(include${catkin_INCLUDE_DIRS}${csm_INCLUDE_DIRS} 
)
link_directories(${csm_LIBRARY_DIRS})#Create library
add_executable(${PROJECT_NAME}_scan_match_plicp_node src/scan_match_plicp.cc)#Note we don't link against pcl as we're using header-only parts of the library
target_link_libraries( ${PROJECT_NAME}_scan_match_plicp_node 
${catkin_LIBRARIES} 
${csm_LIBRARIES}
)add_dependencies(${PROJECT_NAME}_scan_match_plicp_node 
${csm_EXPORTED_TARGETS} 
${catkin_EXPORTED_TARGETS}
)

3.4 代码解析

3.4.1 回调函数

首先要进行初始化,初始化的工作做了第一帧雷达数据的赋值,第一帧时间的赋值.

并且计算了雷达数据对应的每个角度值的cos与sin,用于节省计算量.

之后再将雷达数据转换成csm需要的格式.

再调用ScanMatchWithPLICP进行匹配计算.

/** 回调函数 进行数据处理*/
void ScanMatchPLICP::ScanCallback(const sensor_msgs::LaserScan::ConstPtr &scan_msg)
{// 如果是第一帧数据,首先进行初始化if (!initialized_){// 将雷达各个角度的sin与cos值保存下来,以节约计算量CreateCache(scan_msg);// 将 prev_ldp_scan_,last_icp_time_ 初始化LaserScanToLDP(scan_msg, prev_ldp_scan_);last_icp_time_ = scan_msg->header.stamp;initialized_ = true;}// step1 进行数据类型转换start_time_ = std::chrono::steady_clock::now();LDP curr_ldp_scan;LaserScanToLDP(scan_msg, curr_ldp_scan);end_time_ = std::chrono::steady_clock::now();time_used_ = std::chrono::duration_cast<std::chrono::duration<double>>(end_time_ - start_time_);std::cout << "\n转换数据格式用时: " << time_used_.count() << " 秒。" << std::endl;// step2 使用PLICP计算雷达前后两帧间的坐标变换start_time_ = std::chrono::steady_clock::now();ScanMatchWithPLICP(curr_ldp_scan, scan_msg->header.stamp);end_time_ = std::chrono::steady_clock::now();time_used_ = std::chrono::duration_cast<std::chrono::duration<double>>(end_time_ - start_time_);std::cout << "PLICP计算用时: " << time_used_.count() << " 秒。" << std::endl;
}

3.4.2 计算sin, cos值

由于雷达数据的每个点的角度值是固定的,所以对应的cos与sin值都是一样的.可以提前计算出来,以节省计算量.(本篇文章并没有用到提前计算好的cos值…)

/*** 雷达数据间的角度是固定的,因此可以将对应角度的cos与sin值缓存下来,不用每次都计算*/
void ScanMatchPLICP::CreateCache(const sensor_msgs::LaserScan::ConstPtr &scan_msg)
{a_cos_.clear();a_sin_.clear();double angle;for (unsigned int i = 0; i < scan_msg->ranges.size(); i++){angle = scan_msg->angle_min + i * scan_msg->angle_increment;a_cos_.push_back(cos(angle));a_sin_.push_back(sin(angle));}// min_reading 与 max_reading也不会变了,可以直接赋值进去input_.min_reading = scan_msg->range_min;input_.max_reading = scan_msg->range_max;
}

3.4.3 数据格式转换函数

要将ros的雷达数据格式,转换成csm所需要的数据格式.操作和之前的基本都一样,不再进行详细说明了.

/*** 将雷达的数据格式转成 csm 需要的格式*/
void ScanMatchPLICP::LaserScanToLDP(const sensor_msgs::LaserScan::ConstPtr &scan_msg, LDP &ldp)
{unsigned int n = scan_msg->ranges.size();// 调用csm里的函数进行申请空间ldp = ld_alloc_new(n);for (unsigned int i = 0; i < n; i++){// calculate position in laser framedouble r = scan_msg->ranges[i];if (r > scan_msg->range_min && r < scan_msg->range_max){// 填充雷达数据ldp->valid[i] = 1;ldp->readings[i] = r;}else{ldp->valid[i] = 0;ldp->readings[i] = -1; // for invalid range}ldp->theta[i] = scan_msg->angle_min + i * scan_msg->angle_increment;ldp->cluster[i] = -1;}ldp->min_theta = ldp->theta[0];ldp->max_theta = ldp->theta[n - 1];ldp->odometry[0] = 0.0;ldp->odometry[1] = 0.0;ldp->odometry[2] = 0.0;ldp->true_pose[0] = 0.0;ldp->true_pose[1] = 0.0;ldp->true_pose[2] = 0.0;
}

3.4.3 进行plicp匹配

调用sm_icp函数进行帧间位姿的计算.

input_.first_guess是提供plicp匹配的初值,可以通过速度估计出来,也可以通过里程计或者imu来估计出来,这里的代码直接给0了,就是不提供先验,所以在机器人动起来的情况下匹配的效果可能不好.会在下篇文章中进行实现.

/*** 使用PLICP进行帧间位姿的计算*/
void ScanMatchPLICP::ScanMatchWithPLICP(LDP &curr_ldp_scan, const ros::Time &time)
{// CSM is used in the following way:// The scans are always in the laser frame// The reference scan (prevLDPcan_) has a pose of [0, 0, 0]// The new scan (currLDPScan) has a pose equal to the movement// of the laser in the laser frame since the last scan// The computed correction is then propagated using the tf machineryprev_ldp_scan_->odometry[0] = 0.0;prev_ldp_scan_->odometry[1] = 0.0;prev_ldp_scan_->odometry[2] = 0.0;prev_ldp_scan_->estimate[0] = 0.0;prev_ldp_scan_->estimate[1] = 0.0;prev_ldp_scan_->estimate[2] = 0.0;prev_ldp_scan_->true_pose[0] = 0.0;prev_ldp_scan_->true_pose[1] = 0.0;prev_ldp_scan_->true_pose[2] = 0.0;input_.laser_ref = prev_ldp_scan_;input_.laser_sens = curr_ldp_scan;// 位姿的预测值为0,就是不进行预测input_.first_guess[0] = 0;input_.first_guess[1] = 0;input_.first_guess[2] = 0;// 调用csm里的函数进行plicp计算帧间的匹配,输出结果保存在output里sm_icp(&input_, &output_);if (output_.valid){std::cout << "transfrom: (" << output_.x[0] << ", " << output_.x[1] << ", " << output_.x[2] * 180 / M_PI << ")" << std::endl;}else{std::cout << "not Converged" << std::endl;}// 删除prev_ldp_scan_,用curr_ldp_scan进行替代ld_free(prev_ldp_scan_);prev_ldp_scan_ = curr_ldp_scan;last_icp_time_ = time;
}

4 运行

4.1 launch文件

本篇文章对应的数据包, 请在我的公众号中回复 lesson1 获得,并将launch中的bag_filename更改成您实际的目录名。

<launch><!-- bag的地址与名称 --><arg name="bag_filename" default="/home/lx/bagfiles/lesson1.bag"/><!-- 使用bag的时间戳 --><param name="use_sim_time" value="true" /><!-- 启动节点 --><node name="lesson3_scan_match_plicp_node"pkg="lesson3" type="lesson3_scan_match_plicp_node" output="screen" /><!-- launch rviz --><!-- <node name="rviz" pkg="rviz" type="rviz" required="false"args="-d $(find lesson2)/launch/scan_match_icp.rviz" /> --><!-- play bagfile --><node name="playbag" pkg="rosbag" type="play"args="--clock $(arg bag_filename)" /></launch>

4.2 编译与运行

下载代码后,请放入您自己的工作空间中,通过 catkin_make 进行编译.

由于是新增的包,所以需要通过 rospack profile 命令让ros找到这个新的包.

之后, 使用source命令,添加ros与工作空间的地址到当前终端下.

roslaunch lesson3 scan_match_plicp.launch

4.3 运行结果

运行之后,终端中会显示如下内容

转换数据格式用时: 0.00018146 秒。
transfrom: (6.94938e-310, 1.20452e-316, 9.05622e-321)
PLICP计算用时: 0.000325277 秒。转换数据格式用时: 0.000162974 秒。
transfrom: (6.94938e-310, 1.20452e-316, 9.05622e-321)
PLICP计算用时: 0.00057314 秒。

4.4 运行结果分析

4.4.1 运行时间分析

处理一次plicp需要大概0.5ms的时间,这个时间已经非常短了,能够满足实时的匹配计算.

4.4.2 精度分析

上面的输出是在机器人静止情况下的结果,x, y, theta的值虽然不为0,但是是10的负300次方,这基本上就是0了,也挺好的.

这次的实现没有做位姿的预测,在雷达频率低(10hz)的情况下,最好还是有个位姿的预测,作为plicp的初值,能够提高匹配的精度与速度,将在下次进行实现.

5 Next

本次实现的是帧间匹配,只是最基础的帧间坐标变换的计算.接下来,将添加以下功能来实现激光里程计.

  • 位姿预测功能:根据当前的速度,来对下一时刻的位姿进行预测,作为plicp的初值
  • 使用 tf2 记录位姿变换,将雷达坐标系的运动转成base_link下的运动,并进行累加
  • 将累加的位姿作为里程计发布出去

文章将在 公众号: 从零开始搭SLAM 进行同步更新,欢迎大家关注,以在文章更新的第一时间通知您。

同时,也希望您将这个公众号推荐给您身边做激光SLAM的人们,大家共同进步。

如果您对我写的文章有什么建议,或者想要看哪方面功能如何实现的,请直接在公众号中回复,我可以收到,并将认真考虑您的建议。

在这里插入图片描述

这篇关于从零开始搭二维激光SLAM --- 基于PL-ICP的帧间匹配的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/871202

相关文章

CnPlugin是PL/SQL Developer工具插件使用教程

《CnPlugin是PL/SQLDeveloper工具插件使用教程》:本文主要介绍CnPlugin是PL/SQLDeveloper工具插件使用教程,具有很好的参考价值,希望对大家有所帮助,如有错... 目录PL/SQL Developer工具插件使用安装拷贝文件配置总结PL/SQL Developer工具插

详解如何使用Python从零开始构建文本统计模型

《详解如何使用Python从零开始构建文本统计模型》在自然语言处理领域,词汇表构建是文本预处理的关键环节,本文通过Python代码实践,演示如何从原始文本中提取多尺度特征,并通过动态调整机制构建更精确... 目录一、项目背景与核心思想二、核心代码解析1. 数据加载与预处理2. 多尺度字符统计3. 统计结果可

Nginx路由匹配规则及优先级详解

《Nginx路由匹配规则及优先级详解》Nginx作为一个高性能的Web服务器和反向代理服务器,广泛用于负载均衡、请求转发等场景,在配置Nginx时,路由匹配规则是非常重要的概念,本文将详细介绍Ngin... 目录引言一、 Nginx的路由匹配规则概述二、 Nginx的路由匹配规则类型2.1 精确匹配(=)2

Nginx location匹配模式与规则详解

《Nginxlocation匹配模式与规则详解》:本文主要介绍Nginxlocation匹配模式与规则,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、环境二、匹配模式1. 精准模式2. 前缀模式(不继续匹配正则)3. 前缀模式(继续匹配正则)4. 正则模式(大

Java 正则表达式URL 匹配与源码全解析

《Java正则表达式URL匹配与源码全解析》在Web应用开发中,我们经常需要对URL进行格式验证,今天我们结合Java的Pattern和Matcher类,深入理解正则表达式在实际应用中... 目录1.正则表达式分解:2. 添加域名匹配 (2)3. 添加路径和查询参数匹配 (3) 4. 最终优化版本5.设计思

C++中初始化二维数组的几种常见方法

《C++中初始化二维数组的几种常见方法》本文详细介绍了在C++中初始化二维数组的不同方式,包括静态初始化、循环、全部为零、部分初始化、std::array和std::vector,以及std::vec... 目录1. 静态初始化2. 使用循环初始化3. 全部初始化为零4. 部分初始化5. 使用 std::a

Python中使用正则表达式精准匹配IP地址的案例

《Python中使用正则表达式精准匹配IP地址的案例》Python的正则表达式(re模块)是完成这个任务的利器,但你知道怎么写才能准确匹配各种合法的IP地址吗,今天我们就来详细探讨这个问题,感兴趣的朋... 目录为什么需要IP正则表达式?IP地址的基本结构基础正则表达式写法精确匹配0-255的数字验证IP地

浅谈配置MMCV环境,解决报错,版本不匹配问题

《浅谈配置MMCV环境,解决报错,版本不匹配问题》:本文主要介绍浅谈配置MMCV环境,解决报错,版本不匹配问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录配置MMCV环境,解决报错,版本不匹配错误示例正确示例总结配置MMCV环境,解决报错,版本不匹配在col

详解nginx 中location和 proxy_pass的匹配规则

《详解nginx中location和proxy_pass的匹配规则》location是Nginx中用来匹配客户端请求URI的指令,决定如何处理特定路径的请求,它定义了请求的路由规则,后续的配置(如... 目录location 的作用语法示例:location /www.chinasem.cntestproxy

Nginx中location实现多条件匹配的方法详解

《Nginx中location实现多条件匹配的方法详解》在Nginx中,location指令用于匹配请求的URI,虽然location本身是基于单一匹配规则的,但可以通过多种方式实现多个条件的匹配逻辑... 目录1. 概述2. 实现多条件匹配的方式2.1 使用多个 location 块2.2 使用正则表达式