本文是我在学习深蓝学院的激光slam课程第四次实践的笔记。本次作业主要是熟悉 imls-icp 的过程以及对 csm 接口调用,除此之外还有对课上讲的四种 icp 算法进行总结以及想一下 icp 的改进思路。
实现两帧之间的 IMLS-ICP 激光匹配
补充代码,实现两帧间的 IMLS-ICP 激光匹配;
前提准备
编程环境: ubuntu 18.04 + ROS medodic IDE: CLion, 安装官方教程配置:
- 以项目方式打开想要编译的 package 文件夹中的
CMakeLists.txt
- CMake设置中: Generation path 设置成
WORKSPACE_PATH/build
- Cmake Option:
-DCATKIN_DEVEL_PREFIX:PATH=WORKSPACE_PATH/devel
本次作业只给了模块代码,所以可以放在上次给的工作空间/自己的工作空间里编译运行。首先,需要安装好一些需要的模块:
- champion_nav_msgs:这个模块在上一次作业也有用到,如果上次安装了这次就不用了,否则可以根据内部的
readme.md
自行编译安装,注意 ros 版本的差异更改对应文件的对应位置。 - libnabo: 参考这篇教程
- csm:第二次作业有用到,可以根据这篇教程选择自己喜欢的方式安装,安装完成后在
CMakeLists.txt
修改csm的对应路径位置。
另外注意一下 melodic 下安装的 pcl 版本貌似是1.8的,需要在对应 hpp
, CMakeLists.txt
下修改需要的 pcl 版本。
代码结构
老规矩,还是先阅读给定的框架代码,了解代码结构再考虑补全。
main.cpp
:和之前的作业类似,在一个定义好的类 imlsDebug
里面完成所有项目需要的基本函数(数据读取、初始化、话题订阅/发布),用另一个类 imls_icp
完成作业需要的功能(针对这次作业而言,就是激光数据配准)。
imlsDebug
:六个函数和一些成员变量,主要功能如下:
-
构造函数:初始化两条路径的发布话题信息( imls-icp 生成的和 odom 生成的)、利用
rosbag:View
来读取 rosbag 中的话题信息(激光和里程计数据),并逐条处理,手动进入相应的 callback函数。(rosbag 读取的 api 可以参考官方文档) -
void odomCallback(...)
和pubPath(const nav_msgs::OdometryConstPtr& msg, ...)
: 简单的接收里程计数据并发布 -
void ConvertChampionLaserScanToEigenPointCloud(...)
:工具函数,将激光扫描数据(距离,角度)换算成2d坐标(x, y) -
void championLaserScanCallback(...)
:先将激光数据转换成点云,调用imls_icp
中的方法对上一阵点云数据进行匹配,匹配成功的话输出当前帧位姿(通过上一阵位姿乘相对位姿变换)并发布 -
void pubPath(Eigen::Vector3d& pose, nav_msgs::Path &path, ros::Publisher &mcu_path_pub_)
:发布由 imls-icp 生成的位姿
imls_icp
类中功能函数比较多,大部分函数名字和注释都能说明功能,这里不赘述,只介绍一下几个核心的功能函数:
-
projSourcePtToSurface(...)
:将当前帧点云投影到上一阵点云构造的曲面上(对每个店调用下面的函数),并去除匹配不合格的点,计算输出匹配后的点云和法向量 -
bool ImplicitMLSFunction(Eigen::Vector2d x, double& height)
:将当前激光点投影到上一帧曲面上,计算高度;大致流程是利用 kd 数搜索距离当前点最近的三个点(需要具有代表性)构造平面,进行投影。 -
SolveMotionEstimationProblem()
:已知匹配后的对应点和法向量,计算相对位姿,这部分已经写好了
我们需要完成的有三个函数:computeNormal()
(给定某个点周围的 n 个点,计算法向量),ImplictMLSFunction()
的高度计算部分(参考课上学习的公式),projSourcePtToSurface()
将匹配后的点转换成点云和法向量输出。
代码补充
ComputeNormal
:补充如下,基本参考课上内容,利用 pca 求解法向量
/**
* @brief IMLSICPMatcher::ComputeNormal
* 计算法向量
* @param nearPoints 某个点周围的所有激光点
* @return
*/
Eigen::Vector2d IMLSICPMatcher::ComputeNormal(std::vector<Eigen::Vector2d> &nearPoints)
{
Eigen::Vector2d normal;
//根据周围的激光点计算法向量,参考ppt中NICP计算法向量的方法
// 计算周围点的几何中心
Eigen::Vector2d meanPoints = Eigen::Vector2d::Zero();
for (auto p: nearPoints) {
meanPoints += p;
}
meanPoints /= nearPoints.size();
// 计算方差
Eigen::Matrix2d sigma = Eigen::Matrix2d::Zero();
for (auto p: nearPoints) {
Eigen::Vector2d diff = p - meanPoints;
sigma += diff * diff.transpose();
}
sigma /= nearPoints.size();
// 特征分解
Eigen::EigenSolver<Eigen::Matrix2d> solver(sigma);
// 取出特征值和特征向量
Eigen::MatrixXd eigenvalue = solver.eigenvalues().real();
Eigen::MatrixXd eigenvector = solver.eigenvectors().real();
Eigen::Index evalsMin;
// 找到最小特征值的位置
eigenvalue.rowwise().sum().minCoeff(&evalsMin);
// 取出该列
normal << eigenvector(0, evalsMin), eigenvector(1, evalsMin);
return normal;
}
ImplicitMLSFunction
:这个函数主要是补充高度计算部分,按照论文里的公式计算即可:
//TODO
//根据函数进行投影.计算height,即ppt中的I(x)
double sum1 = 0.0, sum2 = 0.0;
for (int i = 0; i < nearPoints.size(); i++) {
auto p_i = nearPoints[i];
auto normal_i = nearNormals[i];
auto diff = x - p_i;
double wi = exp(- (diff.squaredNorm()) / (m_h * m_h));
sum1 += (wi * (diff.dot(normal_i)));
sum2 += wi;
}
height = sum1 / sum2;
//end of TODO
projSourcePtToSurface
这个主要是补充计算匹配后的点云yi
,同样按照论文公式计算即可
//TODO
//计算yi.
yi = xi - height * nearNormal;
//end of TODO
运行结果
补充完成后运行结果如下所示:红色为根据 imls 算出的位姿轨迹,蓝色为 odom 的位姿轨迹。虽然由于没有进行后端优化导致后面轨迹飘了,但是整体来看大致方向是对的。
用 csm 进行激光匹配比较结果
将第一题 IMLS-ICP 匹配的接口换成第二次作业中 CSM 库的 ICP 匹配接口,并生成激光匹配的轨迹
代码修改
这一题主要是熟悉 csm 的接口并调用,主要是以下几个部分:
添加以下成员变量:
// csm 进行 pl-icp 需要的变量
bool run_pl_icp = true;
LDP m_prevLDP;
sm_params m_PIICPParams;
sm_result m_OutputResult;
添加 csm 相关的函数,注意部分变量的修改以及 LaserScan
和 ChampionNavLaserScan
之间的差异
// 进行pl-icp的相关函数
void SetPIICPParams() {...}
void LaserScanToLDP(const champion_nav_msgs::ChampionNavLaserScanConstPtr& pScan,
LDP& ldp) {
int nPts = pScan->ranges.size(); // intensity 在 ChampionNavLaserScan 没用
...
ldp->theta[i] = pScan->angles[i]; // ChampionNavLaserScan 不规定统一角度间隔,所以可以直接取
...
}
Eigen::Vector3d PIICPBetweenTwoFrames(LDP& currentLDPScan,
Eigen::Vector3d tmprPose) {...}
然后在构造函数中进行 csm 的相关设置:
imlsDebug()
{
...
// 进行 pl-icp 相关设置
m_prevLDP = NULL;
SetPIICPParams();
// 读取 rosbag 信息
rosbag::Bag bag;
bag.open(bagfile, rosbag::bagmode::Read);
...
}
最后在激光数据的 callback
函数中增加使用 pl-icp 匹配的分支:
if(m_isFirstFrame == true)
{
std::cout <<"First Frame"<<std::endl;
m_isFirstFrame = false;
m_prevLaserPose = Eigen::Vector3d(0, 0, 0);
pubPath(m_prevLaserPose, m_imlsPath, m_imlsPathPub);
// 针对不同匹配算法做不一样的设置
if (!run_pl_icp) {
ConvertChampionLaserScanToEigenPointCloud(msg, m_prevPointCloud);
} else {
LaserScanToLDP(msg, m_prevLDP);
}
return ;
}
if (!run_pl_icp) { // 使用 imls-icp 匹配
...
} else { // 用 csm 进行 pl-icp
//把当前的激光数据转换为 pl-icp能识别的数据 & 进行矫正
//d_point_scan就是用激光计算得到的两帧数据之间的旋转 & 平移
LDP currentLDP;
LaserScanToLDP(msg, currentLDP);
Eigen::Vector3d d_point_scan = PIICPBetweenTwoFrames(currentLDP, Eigen::Vector3d::Zero());
Eigen::Matrix3d lastPose, rPose;
lastPose << cos(m_prevLaserPose(2)), -sin(m_prevLaserPose(2)), m_prevLaserPose(0),
sin(m_prevLaserPose(2)), cos(m_prevLaserPose(2)), m_prevLaserPose(1),
0, 0, 1;
rPose << cos(d_point_scan(2)), -sin(d_point_scan(2)), d_point_scan(0),
sin(d_point_scan(2)), cos(d_point_scan(2)), d_point_scan(1),
0, 0, 1;
Eigen::Matrix3d nowPose = lastPose * rPose;
m_prevLaserPose << nowPose(0, 2) , nowPose(1, 2), atan2(nowPose(1, 0), nowPose(0, 0));
pubPath(m_prevLaserPose, m_imlsPath, m_imlsPathPub);
}
}
pl-icp 运行结果
运行结果如下所示,使用 pl-icp 的速度明显变快,但由于没有优化结果同样也发生了漂移。
比较各类 ICP 算法的特点以及异同
阅读 ICP 相关论文,总结课上所学的几种 ICP 及其相关变型并简述其异同(ICP,PL-ICL,NICP, IMLS-ICP)
课上讲解的几种 ICP 算法的基本思路都是类似的:找到前后两帧激光扫描数据的匹配关系,并根据匹配关系迭代找到相对位姿的最优值,主要的差异就是寻找对应点(匹配关系)以及计算误差两方面,几种 ICP 算法的详细说明可以参考我的上篇博客。
ICP 算法改进思路
简答题,开放性答案:现在你已经了解了多种 ICP 算法,你是否也能提出一种改进的 ICP 算法,或能提 升 ICP 总体匹配精度或速度的技巧?请简述你的改进策略。
两个方面:
- 精度上可以采用合理的方法提供初值(目前是人工设置):其他传感器(odom等),用 Ransac 去除 outlier
- 效率上可以对点云采样(提高匹配速度)