返回

Imls-icp 代码实践

本文是我在学习深蓝学院的激光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的,需要在对应 hppCMakeLists.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 相关的函数,注意部分变量的修改以及 LaserScanChampionNavLaserScan 之间的差异

// 进行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
  • 效率上可以对点云采样(提高匹配速度)
Built with Hugo
Theme Stack designed by Jimmy