LIO-SAM回环检测模块代码解析

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
全局流量管理 GTM,标准版 1个月
简介: LIO-SAM回环检测模块代码解析

前端里程计是一个不断递推相邻帧位姿的过程,其必定存在累计漂移。为了解决这一问题,回环检测并进行后端优化是一个重要的环节。下面以典型激光SLAM框架LIO-SAM为例进行解析,学习其进行回环检测的策略。

分配线程

首先,查看mapOptmization.cpp文件中的main函数,可以看到给回环检测处理分配一个线程。

std::thread loopthread(&mapOptimization::loopClosureThread, &MO);

查看loopClosureThread函数:

    void loopClosureThread()
    {
        if (loopClosureEnableFlag == false)
            return;
        ros::Rate rate(loopClosureFrequency);
        while (ros::ok())
        {
            rate.sleep();
            TicToc t_loopclosure;
            performLoopClosure();
            visualizeLoopClosure();
            if(t_loopclosure.toc() > 1)
            {
                std::cout << "t_loopclosure: " << t_loopclosure.toc() << std::endl;
                loopClosureTime += t_loopclosure.toc();
                frameCount2++;
            }
        }
    }

这里并没有太多复杂的处理逻辑,判断loopClosureEnableFlag是否开启了回环检测。若没有开启则直接返回不处理。开启了回环检测,则使用下述的loopClosureFrequency频率进行处理。

performLoopClosure函数

先进行判断关键帧的数量是否为空:

    void performLoopClosure()
    {
        if (cloudKeyPoses3D->points.empty() == true)
            return;

若为空则直接返回,不为空则复制一份关键帧的位置(3维)与位姿(6维)到另外一份变量中,防止冲突。

        mtx.lock();
        *copy_cloudKeyPoses3D = *cloudKeyPoses3D;
        *copy_cloudKeyPoses6D = *cloudKeyPoses6D;
        mtx.unlock();

定义两个变量,存储当前关键帧索引与寻找到的最近邻关键帧索引。

        // find keys
        int loopKeyCur;
        int loopKeyPre;

查找最近邻关键帧索引,若两个函数均失败则说明没有检测到配对的关键帧,直接返回。

        if (detectLoopClosureExternal(&loopKeyCur, &loopKeyPre) == false)
            if (detectLoopClosureDistance(&loopKeyCur, &loopKeyPre) == false)
                return;

经过上述detectLoopClosureExternal与detectLoopClosureDistance函数处理之后,能够得到loopKeyCur当前帧索引,与其配对的之前最近邻关键帧loopKeyPre索引。


接着,定义cureKeyframeCloud与prevKeyframeCloud两个变量,分别存储当前关键帧点云数据、前述对应关键帧及前后historyKeyframeSearchNum帧点云组成的局部地图。

        // extract cloud
        pcl::PointCloud<PointType>::Ptr cureKeyframeCloud(new pcl::PointCloud<PointType>());
        pcl::PointCloud<PointType>::Ptr prevKeyframeCloud(new pcl::PointCloud<PointType>());

使用loopFindNearKeyframes函数加载对应关键帧,组成上述两个变量,并判断变量中的点云数量是否符合要求。

        {
            // 加载配对成功的当前关键帧,当前关键帧独立作为一个候选,仅加载当前帧点云
            loopFindNearKeyframes(cureKeyframeCloud, loopKeyCur, 0);
            // 加载配准成功的pre关键帧,加载前后historyKeyframeSearchNum的关键帧,组成局部地图
            loopFindNearKeyframes(prevKeyframeCloud, loopKeyPre, historyKeyframeSearchNum);
            if (cureKeyframeCloud->size() < 300 || prevKeyframeCloud->size() < 1000)
                return;
            if (pubHistoryKeyFrames.getNumSubscribers() != 0)
                publishCloud(pubHistoryKeyFrames, prevKeyframeCloud, timeLaserInfoStamp, odometryFrame);
        }

设置ICP配准算法的相关参数:

        // ICP Settings
        static pcl::IterativeClosestPoint<PointType, PointType> icp;
        icp.setMaxCorrespondenceDistance(historyKeyframeSearchRadius*2);
        icp.setMaximumIterations(100);
        icp.setTransformationEpsilon(1e-6);
        icp.setEuclideanFitnessEpsilon(1e-6);
        icp.setRANSACIterations(0);

配准处理cureKeyframeCloud与prevKeyframeCloud两个点云,并判断是否配准成功,若没有成功则直接返回:

        // Align clouds
        icp.setInputSource(cureKeyframeCloud);
        icp.setInputTarget(prevKeyframeCloud);
        pcl::PointCloud<PointType>::Ptr unused_result(new pcl::PointCloud<PointType>());
        icp.align(*unused_result);
        if (icp.hasConverged() == false || icp.getFitnessScore() > historyKeyframeFitnessScore)
            return;

若有接听,则发布变换后的点云:

// publish corrected cloud
        if (pubIcpKeyFrames.getNumSubscribers() != 0)
        {
            pcl::PointCloud<PointType>::Ptr closed_cloud(new pcl::PointCloud<PointType>());
            pcl::transformPointCloud(*cureKeyframeCloud, *closed_cloud, icp.getFinalTransformation());
            publishCloud(pubIcpKeyFrames, closed_cloud, timeLaserInfoStamp, odometryFrame);
        }

获取配准的位姿变换(当前关键帧与找到的回环关键帧之间):

        // Get pose transformation
        float x, y, z, roll, pitch, yaw;
        Eigen::Affine3f correctionLidarFrame;
        correctionLidarFrame = icp.getFinalTransformation();

获取在没有进行回环改正之前,原始的位姿变换:(pclPointToAffine3f函数为把PCL中的“点”类型变换成位姿Affine3f类型)

// transform from world origin to wrong pose
        Eigen::Affine3f tWrong = pclPointToAffine3f(copy_cloudKeyPoses6D->points[loopKeyCur]);

获取矫正后的位姿:

// transform from world origin to corrected pose
        Eigen::Affine3f tCorrect = correctionLidarFrame * tWrong;// pre-multiplying -> successive rotation about a fixed frame

设置gtsam位姿节点和噪声模型:

 pcl::getTranslationAndEulerAngles (tCorrect, x, y, z, roll, pitch, yaw);
        gtsam::Pose3 poseFrom = Pose3(Rot3::RzRyRx(roll, pitch, yaw), Point3(x, y, z));
        gtsam::Pose3 poseTo = pclPointTogtsamPose3(copy_cloudKeyPoses6D->points[loopKeyPre]);
        gtsam::Vector Vector6(6);
        float noiseScore = icp.getFitnessScore();
        Vector6 << noiseScore, noiseScore, noiseScore, noiseScore, noiseScore, noiseScore;
        noiseModel::Diagonal::shared_ptr constraintNoise = noiseModel::Diagonal::Variances(Vector6);

在图中添加loop约束:

        // Add pose constraint
        mtx.lock();
        loopIndexQueue.push_back(make_pair(loopKeyCur, loopKeyPre));
        loopPoseQueue.push_back(poseFrom.between(poseTo));
        loopNoiseQueue.push_back(constraintNoise);
        mtx.unlock();

使用map记录两个关键帧成为回环:

        // add loop constriant
        loopIndexContainer[loopKeyCur] = loopKeyPre;

detectLoopClosureExternal函数

其中detectLoopClosureExternal函数中有注释说不再只用,所以就先忽略了。

// this function is not used yet, please ignore it

detectLoopClosureDistance函数

进入函数内,定义两个变量,分别存储loopKeyCur当前帧索引,与其配对的之前最近邻关键帧loopKeyPre索引。

    bool detectLoopClosureDistance(int *latestID, int *closestID)
    {
        int loopKeyCur = copy_cloudKeyPoses3D->size() - 1;
        int loopKeyPre = -1;

检查是否已经添加过当前帧在loopIndexContainer变量中:

        // check loop constraint added before
        auto it = loopIndexContainer.find(loopKeyCur);
        if (it != loopIndexContainer.end())
            return false;

使用pcl自带的kdtree->radiusSearch找到与当前帧最近邻的关键帧位置。

// find the closest history key frame
        std::vector<int> pointSearchIndLoop;
        std::vector<float> pointSearchSqDisLoop;
        kdtreeHistoryKeyPoses->setInputCloud(copy_cloudKeyPoses3D);
        kdtreeHistoryKeyPoses->radiusSearch(copy_cloudKeyPoses3D->back(), historyKeyframeSearchRadius, pointSearchIndLoop, pointSearchSqDisLoop, 0);

判断找到的最近邻关键帧与当前帧之间的时间差异是否大于设置的阈值historyKeyframeSearchTimeDiff,如果满足条件则认为找到了最近邻关键帧,赋值索引,反之索引仍为初值-1:

        for (int i = 0; i < (int)pointSearchIndLoop.size(); ++i)
        {
            int id = pointSearchIndLoop[i];
            if (abs(copy_cloudKeyPoses6D->points[id].time - timeLaserInfoCur) > historyKeyframeSearchTimeDiff)
            {
                loopKeyPre = id;
                break;
            }
        }

判断是否找到了最近邻关键帧索引,若没有找到则返回false。找到了则赋值loopKeyCur与loopKeyPre的值,返回true。

        if (loopKeyPre == -1 || loopKeyCur == loopKeyPre)
            return false;
        *latestID = loopKeyCur;
        *closestID = loopKeyPre;
        return true;

loopFindNearKeyframes函数

函数作用为输入一个关键帧索引key和选取前后关键帧的数量searchNum,加载关键帧到nearKeyframes变量中:

void loopFindNearKeyframes(pcl::PointCloud<PointType>::Ptr& nearKeyframes, const int& key, const int& searchNum)

加载选取的关键帧,到变量nearKeyframes中:

        for (int i = -searchNum; i <= searchNum; ++i)
        {
            int keyNear = key + i;
            if (keyNear < 0 || keyNear >= cloudSize )
                continue;
            *nearKeyframes += *transformPointCloud(cornerCloudKeyFrames[keyNear], &copy_cloudKeyPoses6D->points[keyNear]);
            *nearKeyframes += *transformPointCloud(surfCloudKeyFrames[keyNear],   &copy_cloudKeyPoses6D->points[keyNear]);
        }

判断是否为空:

if (nearKeyframes->empty())
            return;

下采样,抽稀:

        // downsample near keyframes
        pcl::PointCloud<PointType>::Ptr cloud_temp(new pcl::PointCloud<PointType>());
        downSizeFilterICP.setInputCloud(nearKeyframes);
        downSizeFilterICP.filter(*cloud_temp);
        *nearKeyframes = *cloud_temp;

至此,我们基本了解了LIO-SAM框架中回环检测模块的代码,理解了其总体思路。

目录
相关文章
|
1月前
|
XML 数据格式 开发者
解析数据的Beautiful Soup 模块(一)
解析数据的Beautiful Soup 模块(一)
|
27天前
|
存储 安全 Java
系统安全架构的深度解析与实践:Java代码实现
【11月更文挑战第1天】系统安全架构是保护信息系统免受各种威胁和攻击的关键。作为系统架构师,设计一套完善的系统安全架构不仅需要对各种安全威胁有深入理解,还需要熟练掌握各种安全技术和工具。
77 10
|
26天前
|
前端开发 JavaScript 开发者
揭秘前端高手的秘密武器:深度解析递归组件与动态组件的奥妙,让你代码效率翻倍!
【10月更文挑战第23天】在Web开发中,组件化已成为主流。本文深入探讨了递归组件与动态组件的概念、应用及实现方式。递归组件通过在组件内部调用自身,适用于处理层级结构数据,如菜单和树形控件。动态组件则根据数据变化动态切换组件显示,适用于不同业务逻辑下的组件展示。通过示例,展示了这两种组件的实现方法及其在实际开发中的应用价值。
33 1
|
1月前
|
机器学习/深度学习 人工智能 算法
揭开深度学习与传统机器学习的神秘面纱:从理论差异到实战代码详解两者间的选择与应用策略全面解析
【10月更文挑战第10天】本文探讨了深度学习与传统机器学习的区别,通过图像识别和语音处理等领域的应用案例,展示了深度学习在自动特征学习和处理大规模数据方面的优势。文中还提供了一个Python代码示例,使用TensorFlow构建多层感知器(MLP)并与Scikit-learn中的逻辑回归模型进行对比,进一步说明了两者的不同特点。
66 2
|
1月前
|
存储 搜索推荐 数据库
运用LangChain赋能企业规章制度制定:深入解析Retrieval-Augmented Generation(RAG)技术如何革新内部管理文件起草流程,实现高效合规与个性化定制的完美结合——实战指南与代码示例全面呈现
【10月更文挑战第3天】构建公司规章制度时,需融合业务实际与管理理论,制定合规且促发展的规则体系。尤其在数字化转型背景下,利用LangChain框架中的RAG技术,可提升规章制定效率与质量。通过Chroma向量数据库存储规章制度文本,并使用OpenAI Embeddings处理文本向量化,将现有文档转换后插入数据库。基于此,构建RAG生成器,根据输入问题检索信息并生成规章制度草案,加快更新速度并确保内容准确,灵活应对法律与业务变化,提高管理效率。此方法结合了先进的人工智能技术,展现了未来规章制度制定的新方向。
36 3
|
1月前
|
前端开发 Python
解析数据的Beautiful Soup 模块(二)
解析数据的Beautiful Soup 模块(二)
|
1月前
|
JSON 前端开发 JavaScript
前端模块打包器的深度解析
【10月更文挑战第13天】前端模块打包器的深度解析
|
1月前
|
缓存 前端开发 JavaScript
Webpack技术深度解析:模块打包与性能优化
【10月更文挑战第13天】Webpack技术深度解析:模块打包与性能优化
|
1月前
|
运维 安全 网络协议
Python 网络编程:端口检测与IP解析
本文介绍了使用Python进行网络编程的两个重要技能:检查端口状态和根据IP地址解析主机名。通过`socket`库实现端口扫描和主机名解析的功能,并提供了详细的示例代码。文章最后还展示了如何整合这两部分代码,实现一个简单的命令行端口扫描器,适用于网络故障排查和安全审计。
|
1月前
|
SQL 监控 关系型数据库
SQL错误代码1303解析与处理方法
在SQL编程和数据库管理中,遇到错误代码是常有的事,其中错误代码1303在不同数据库系统中可能代表不同的含义
下一篇
无影云桌面