LIO-SAM中的mapOptmization

本文深入剖析LIO-SAM源码,重点解读mapOptmization.cpp文件,涵盖残差构建原理、scan-to-map匹配及因子图优化等内容。通过特征匹配提高定位准确性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

最近在学习LIO-SAM源码的时候,发现LIO-SAM这套代码调用了比较多库的内置API,里面涉及的一些细节也比较多,整个工程还是比较清晰的,值得学习!

LIO-SAM这个框架主要由四个大的模块组成,其中这个图优化模块的内容最多,里面也涉及最多细节,这里主要记录下mapOptmization.cpp这个文件的解读,包括:残差构建原理、scan-to-map、因子图优化等。scan-to-map的原理部分参考我的另外一篇博客:LIO-SAM中的scan_to_map原理剖析

学习这份代码的时候参考了很多LIO-SAM-DetailedNote这位大哥的注释,我在他的基础上修正了一些我个人感觉不太对的地方,增加了一些细节的注释。对于整个工程的详细注释放在了我的个人GithubLIO-SAM-note,欢迎大家批评指正,一起交流学习!

模块功能简述:

1、scan-to-map匹配:提取当前激光帧特征点(角点、平面点),局部关键帧map的特征点,执行scan-to-map迭代优化,更新当前帧位姿;

2、关键帧因子图优化:关键帧加入因子图,添加激光里程计因子、GPS因子、闭环因子,执行因子图优化,更新所有关键帧位姿;

3、闭环检测:在历史关键帧中找距离相近,时间相隔较远的帧设为匹配帧,匹配帧周围提取局部关键帧map,同样执行scan-to-map匹配,得到位姿变换,构建闭环因子数据,加入因子图优化。

image-20221013194909336

mapOptmization 类中通过构造函数对订阅的话题消息用回调函数进行处理,主要的工作在回调函数 laserCloudInfoHandler() 中实现,另外开了一个线程做回环检测因子的构建loopClsureThread(),以及另外开了一个可视化线程visualizeGlobalMapThread()

laserCloudInfoHandler()

updateInitialGuess()

这个函数主要通过IMU里程计来的信息对当前帧位姿进行初始化,每一帧都获得一个粗糙的 T_wr,以便后面进行优化

这里有一个细节是,对不同的帧分了三种处理方式

  1. 第 0 帧的时候没有里程计的输入,所以第0帧的位姿初始化是直接初始化为IMU的纯旋转
  2. 第 1 帧的时候有里程计的输入,但是没有上一帧的里程计输入,所以第1帧的位姿初始化是通过IMU获得第0帧的第1帧之间的增量变换,然后叠加到第0帧的位姿上
  3. 从第2帧开始,有了上一帧和当前帧的里程计结果,此后都是通过里程计获得当前帧和前一帧的增量变换,然后把增量变换叠加到上一帧的位姿作为当前帧的初始化位姿,以便后续进行优化
void updateInitialGuess()
    {
        // 前一帧的lidar位姿
        incrementalOdometryAffineFront = trans2Affine3f(transformTobeMapped);
        static Eigen::Affine3f lastImuTransformation;
        
        // 1. 第0帧的处理方法,只有IMU旋转变换,没有里程计输入,所以第0帧位姿初始化为IMU的纯旋转
        if (cloudKeyPoses3D->points.empty())
        {
            // 当前帧位姿(map坐标系下),用激光帧信息中的RPY(来自imu原始数据)初始化
            transformTobeMapped[0] = cloudInfo.imuRollInit;
            transformTobeMapped[1] = cloudInfo.imuPitchInit;
            transformTobeMapped[2] = cloudInfo.imuYawInit;

            if (!useImuHeadingInitialization)
                transformTobeMapped[2] = 0;
            lastImuTransformation = pcl::getTransformation(0, 0, 0, cloudInfo.imuRollInit, cloudInfo.imuPitchInit, cloudInfo.imuYawInit); 
            return;
        }

        static bool lastImuPreTransAvailable = false;
        static Eigen::Affine3f lastImuPreTransformation;
        // 2. 第1帧开始有里程计输入了,用当前帧和前一帧对应的imu里程计计算相对位姿变换,再用前一帧的位姿与相对变换,计算当前帧的位姿,存transformTobeMapped
        if (cloudInfo.odomAvailable == true)
        {
            // 当前帧的初始估计位姿(来自imu里程计),后面用来计算增量位姿变换
            Eigen::Affine3f transBack = pcl::getTransformation(cloudInfo.initialGuessX,    cloudInfo.initialGuessY,     cloudInfo.initialGuessZ, 
                                                              cloudInfo.initialGuessRoll, cloudInfo.initialGuessPitch, cloudInfo.initialGuessYaw);
            // 第1帧的处理方法
            if (lastImuPreTransAvailable == false)  
            {
                lastImuPreTransformation = transBack;
                lastImuPreTransAvailable = true;
            }
            // 第2帧包括之后的帧的处理方法 
            else {
                // 当前帧相对于前一帧的位姿变换,imu里程计计算得到
                Eigen::Affine3f transIncre = lastImuPreTransformation.inverse() * transBack;
                // 前一帧的位姿
                Eigen::Affine3f transTobe = trans2Affine3f(transformTobeMapped);
                // 当前帧的位姿
                Eigen::Affine3f transFinal = transTobe * transIncre;
                // 更新当前帧位姿transformTobeMapped
                pcl::getTranslationAndEulerAngles(transFinal, transformTobeMapped[3], transformTobeMapped[4], transformTobeMapped[5], 
                                                              transformTobeMapped[0], transformTobeMapped[1], transformTobeMapped[2]);
                // 递归
                lastImuPreTransformation = transBack;
                lastImuTransformation = pcl::getTransformation(0, 0, 0, cloudInfo.imuRollInit, cloudInfo.imuPitchInit, cloudInfo.imuYawInit); // save imu before return;
                return;
            }
        }

        // 只在处理第1帧调用,因为此时还没有上一帧的里程计输入,所以第0帧和第1帧的位姿变换通过IMU获得,然后把增量位姿叠加到上一帧的位姿获得当前帧到map坐标系的变换
        if (cloudInfo.imuAvailable == true)
        {
            // 当前帧的姿态角(来自原始imu数据)
            Eigen::Affine3f transBack = pcl::getTransformation(0, 0, 0, cloudInfo.imuRollInit, cloudInfo.imuPitchInit, cloudInfo.imuYawInit);
            // 当前帧相对于前一帧的姿态变换
            Eigen::Affine3f transIncre = lastImuTransformation.inverse() * transBack;  // T_r(i)_r(i+1) = T_wr(i).inv() * T_wr(i+1)

            // 前0帧的位姿(map坐标系下) Twri
            Eigen::Affine3f transTobe = trans2Affine3f(transformTobeMapped);
            // 当前帧的位姿 T_wr(i+1) = T_wr(i) * T_r(i)r(i+1)
            Eigen::Affine3f transFinal = transTobe * transIncre;
            // 更新当前帧位姿transformTobeMapped
            pcl::getTranslationAndEulerAngles(transFinal, transformTobeMapped[3], transformTobeMapped[4], transformTobeMapped[5], 
                                                          transformTobeMapped[0], transformTobeMapped[1], transformTobeMapped[2]);
            lastImuTransformation = pcl::getTransformation(0, 0, 0, cloudInfo.imuRollInit, cloudInfo.imuPitchInit, cloudInfo.imuYawInit); // save imu before return;
            return;
        }
    }
extractSurroundingKeyFrames()

这个函数里面主要是调用了 extractNearby() 把相邻(注意:是空间上的)帧的特征点提取出来构造一个局部点云地图,以便后面进行 scan-to-map 。 这里主要得到两个数据结构,它把角点和平面点分开保存了:

downSizeFilterCorner

downSizeFilterSurf

    void extractNearby()
    {
        pcl::PointCloud<PointType>::Ptr surroundingKeyPoses(new pcl::PointCloud<PointType>());
        pcl::PointCloud<PointType>::Ptr surroundingKeyPosesDS(new pcl::PointCloud<PointType>());
        std::vector<int> pointSearchInd;
        std::vector<float> pointSearchSqDis;

        // kdtree的输入,全局关键帧位姿集合(历史所有关键帧集合)
        kdtreeSurroundingKeyPoses->setInputCloud(cloudKeyPoses3D); 
        // 对最近的一帧关键帧,在半径区域内搜索空间区域上相邻的关键帧集合
        kdtreeSurroundingKeyPoses->radiusSearch(cloudKeyPoses3D->back(), (double)surroundingKeyframeSearchRadius, pointSearchInd, pointSearchSqDis);
        // 遍历搜索结果,pointSearchInd存的是结果在cloudKeyPoses3D下面的索引
        for (int i = 0; i < (int)pointSearchInd.size(); ++i)
        {
            int id = pointSearchInd[i];
            // 加入相邻关键帧位姿集合中
            surroundingKeyPoses->push_back(cloudKeyPoses3D->points[id]);
        }
        // 降采样一下
        downSizeFilterSurroundingKeyPoses.setInputCloud(surroundingKeyPoses);
        downSizeFilterSurroundingKeyPoses.filter(*surroundingKeyPosesDS);
        // 加入时间上相邻的一些关键帧,比如当载体在原地转圈,这些帧加进来是合理的
        int numPoses = cloudKeyPoses3D->size();
        for (int i = numPoses-1; i >= 0; --i)
        {
            if (timeLaserInfoCur - cloudKeyPoses6D->points[i].time < 10.0)
                surroundingKeyPosesDS->push_back(cloudKeyPoses3D->points[i]);
            else
                break;
        }
        // 将相邻关键帧集合对应的角点、平面点,加入到局部map中,作为scan-to-map匹配的局部点云地图
        extractCloud(surroundingKeyPosesDS);
    }
downsampleCurrentScan()

对当前激光帧进行降采样,这里同样划分为边角点集合以及平面点集合

void downsampleCurrentScan(){
    // 当前激光帧角点集合降采样
    laserCloudCornerLastDS->clear();
    downSizeFilterCorner.setInputCloud(laserCloudCornerLast);
    downSizeFilterCorner.filter(*laserCloudCornerLastDS);
    laserCloudCornerLastDSNum = laserCloudCornerLastDS->size();
    // 当前激光帧平面点集合降采样
    laserCloudSurfLastDS->clear();
    downSizeFilterSurf.setInputCloud(laserCloudSurfLast);
    downSizeFilterSurf.filter(*laserCloudSurfLastDS);
    laserCloudSurfLastDSNum = laserCloudSurfLastDS->size();
}
scan2MapOptimization();

这个函数的作用是把两拨点云匹配,求出相对位姿变换,然后作用到当前位姿上,实现位姿矫正

为什么要做scan-to-map,ICP不好吗?

答:scan-to-map是特征匹配,ICP是点与 点之间的匹配;ICP匹配对初始化位姿和点云质量要求比较高,但是特征匹配利用的是点与线、点与面之间的匹配,可以提高鲁棒性

这个函数主要是用scan-to-map的方式优化当前帧的位姿, 流程如下:

1、要求当前帧特征点数量足够多,且匹配的点数够多,才执行优化

2、以下内容迭代30次(上限)优化

​ 1) 当前激光帧角点寻找局部map匹配点

这里使用OpenMP的一个多线程加速的技巧,因为根据当前帧的点去找近邻点是完全可以并行化处理的,他们相互之间没有干涉。

image-20221017112439040

​ a.更新当前帧位姿,将当前帧角点坐标变换到map系下,在局部map中查找5个最近点,距离小于1m,且5个点构成直线(注意:并不是直接用这五个点拟合一个曲线,而是用距离中心点的协方差矩阵,特征值进行判断),则认为匹配上了

​ b.计算当前帧角点到直线的距离、垂线的单位向量,存储为角点参数

​ 2) 当前激光帧平面点寻找局部map匹配点

​ a.更新当前帧位姿,将当前帧平面点坐标变换到map系下,在局部map中查找5个最近点,距离小于1m,且5个点构成平面(最小二乘拟合平面),则认为匹配上了

​ b.计算当前帧平面点到平面的距离、垂线的单位向量,存储为平面点参数

​ 3) 提取当前帧中与局部map匹配上了的角点、平面点,加入同一集合。存入到laserCloudOri coeffSel

​ 4) 对匹配特征点计算Jacobian矩阵,观测值为特征点到直线、平面的距离,构建高斯牛顿方程,迭代优化当前位姿

因为这个模块涉及的原理比较多,参考我的另外一篇博客:

https://blog.csdn.net/weixin_40599145/article/details/127398733

LIO-SAM在优化结束之后和IMU进行了加权融合

3、用imu原始RPY数据与scan-to-map优化后的位姿进行加权融合,修正当前帧位姿的roll、pitch,此外还对roll、pitch以及z坐标做了一个幅值约束,防止优化出错。

saveKeyFramesAndFactor()

这个函数主要是做因子图优化方面的工作。

1、计算当前帧与前一帧位姿变换,判断旋转和平移增量的大小,如果变化太小,不设为关键帧,反之设为关键帧

2、给因子图添加激光里程计因子

void addOdomFactor()
    {
        if (cloudKeyPoses3D->points.empty())
        {
            // 第一帧初始化先验因子。这里他把yaw和平移分量的方差设置得很大,因为我们的系统是四自由度不可观的。
            noiseModel::Diagonal::shared_ptr priorNoise = noiseModel::Diagonal::Variances((Vector(6) << 1e-2, 1e-2, M_PI*M_PI, 1e8, 1e8, 1e8).finished()); // rad*rad, meter*meter
            gtSAMgraph.add(PriorFactor<Pose3>(0, trans2gtsamPose(transformTobeMapped), priorNoise));
            // 变量节点设置初始值
            initialEstimate.insert(0, trans2gtsamPose(transformTobeMapped));
        }else{
            // 添加激光里程计因子
            noiseModel::Diagonal::shared_ptr odometryNoise = noiseModel::Diagonal::Variances((Vector(6) << 1e-6, 1e-6, 1e-6, 1e-4, 1e-4, 1e-4).finished());
            // cloudKeyPoses6D这个数据结构是存储经因子图优化之后的位姿的,所以在此时它最后一个还是上一帧的位姿
            gtsam::Pose3 poseFrom = pclPointTogtsamPose3(cloudKeyPoses6D->points.back());  
            gtsam::Pose3 poseTo   = trans2gtsamPose(transformTobeMapped);
            // 参数:前一帧id,当前帧id,前一帧与当前帧的位姿变换(作为观测值),噪声协方差
            gtSAMgraph.add(BetweenFactor<Pose3>(cloudKeyPoses3D->size()-1, cloudKeyPoses3D->size(), poseFrom.between(poseTo), odometryNoise));
            // 变量节点设置初始值
            initialEstimate.insert(cloudKeyPoses3D->size(), poseTo);
        }
    }

3、添加GPS约束因子、添加回环检测约束因子

4、执行因子图优化

这里值得注意的是,它不会对历史所有位姿进行优化,他每次优化完成之后会把因子图清空

image-20221103094140220

实际上它只优化了当前添加到因子图中的变量。例如:

第k次优化:

image-20221103192020116

第k+1次优化

image-20221103192055087

correctPoses()

这个函数在回环触发的时候才会起作用。主要作用就是更新因子图中所有变量节点的位姿,也就是所有历史关键帧的位姿,更新里程计轨迹。

publishOdometry()

发布激光里程计

publishFrames()

1、发布历史关键帧位姿集合

2、发布局部map的降采样平面点集合

3、发布历史帧(累加的)的角点、平面点降采样集合

4、发布里程计轨迹

gpsHandler()

loopInfoHandler()

主要订阅来自外部闭环检测程序提供的闭环数据

loopClosureThread()

image-20221103205636260

performLoopClosure()

1、以当前帧为中心,选择15米以内的历史帧作为回环检测帧候选帧集合,然后选择距离当前帧时间最远的帧作为回环关键帧

2、提取当前帧特征点构造一个点云集合,提取回环帧前后25帧特征点构造一个点云集合,两个做ICP匹配,获得当前关键帧与闭环关键帧之间的位姿变换

3、构造因子图优化需要的数据,在因子图优化环节会调用这些数据,会优化回环帧到当前帧的所有位姿

visualizeLoopClosure()

主要做闭环检测的可视化,激光雷达运行过程中的那些绿色的点和黄色的线就是从这里发布出去的。

visualizeGlobalMapThread()

1、发布局部关键帧map的特征点云

2、保存全局关键帧特征点集合

<think>嗯,用户遇到了LIO-SAM的mapOptimization进程崩溃的问题,错误代码是-11。我需要根据提供的引用信息和常见解决方法来解决这个问题。首先,错误代码-11通常和内存访问有关,比如段错误。可能的原因包括依赖库问题、环境配置错误或者参数设置不当。 根据引用[2],之前有人通过移动libmetis.so库解决了类似的问题。用户可能需要检查这个库的位置是否正确,是否被正确链接。同时,引用[3]提到重新编译和source环境可能有效,所以建议用户确认编译过程没有错误,并确保环境变量正确设置。 另外,参数配置错误也可能导致进程崩溃,比如传感器话题名称不匹配或者参数文件中的设置与实际硬件不符。用户应该检查launch文件和配置文件中的话题名称是否正确,尤其是点云和IMU的话题。还有,计算资源不足,比如内存不够,也可能引发段错误,需要确保有足够的内存和计算能力。 可能的解决步骤包括:重新安装依赖库如METIS和GTSAM,确认安装版本正确;检查参数文件中的话题名称;清理并重新编译项目;调整ROS参数降低资源消耗;以及使用调试工具如gdb来追踪错误源头。如果这些步骤都尝试过还是不行,可能需要查看更详细的日志或核心转储文件来分析具体原因。</think>关于LIO-SAM地图优化进程(mapOptimization)崩溃问题(exit code -11)的分析与解决方法如下: ### 一、错误原因分析 1. **内存访问异常** 进程返回代码-11表示段错误(SIGSEGV),通常由以下原因导致: - 依赖库版本冲突(如GTSAM或METIS库未正确安装) - 点云数据话题名称不匹配 - 参数配置文件类型错误(如未正确设置IMU和点云坐标系) 2. **环境配置问题** 根据引用[2][^2],已知`libmetis.so`库路径错误会导致进程崩溃。该库是GTSAM的依赖项,用于图优化计算。 ### 二、具体修复步骤 1. **检查关键依赖库** ```bash sudo mv /usr/local/lib/libmetis.so /opt/ros/melodic/lib/ sudo ldconfig ``` 该操作可解决80%以上的段错误问题[^2] 2. **验证参数配置文件** 检查`params.yaml`中必须正确配置: ```yaml pointCloudTopic: "velodyne_points" # 需与实际雷达话题一致 imuTopic: "imu_correct" # 需与IMU传感器话题匹配 ``` 3. **重新编译部署** 执行完整编译流程: ```bash cd ~/catkin_ws catkin clean catkin build lio_sam -j4 source devel/setup.bash ``` 4. **内存优化设置** 在`launch`文件中添加资源限制: ```xml <node pkg="lio_sam" type="lio_sam_mapOptmization" name="mapOptmization" output="screen" launch-prefix="ulimit -v unlimited"> <!-- 解除内存限制 --> </node> ``` ### 三、进阶调试方法 若仍出现崩溃,使用GDB调试: ```bash gdb --args /opt/ros/melodic/lib/nodelet/nodelet standalone lio_sam/mapOptmization ``` 当崩溃发生时,执行`bt`命令获取堆栈跟踪,可定位到具体报错的代码行。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值