【SLAM】LIO-SAM解析——点云特征提取FeatureExtraction(3)

 系列文章链接:

【SLAM】LIO-SAM解析——流程图(1)

【SLAM】LIO-SAM解析——数据预处理imageProjection(2)

【SLAM】LIO-SAM解析——特征提取featureTrack(3)

【SLAM】LIO-SAM解析——IMU预计分IMU-Preintegration(4)

【SLAM】LIO-SAM解析——后端优化MapOptimization(5)

【SLAM】LIO-SAM解析——里程计融合transformFusion(6) 

知识点:

如何用一帧雷达/深度图数据每一个点/像素坐标的深度值确定当前点是角点还是平面点。

这一部分会接收来自imageProjection处理完的lidar帧数据,对应的点云是去畸变的,也就是在同一个坐标系下。这部分对此帧的点云进行面点,角点特征的分类。
这一章内容比前一章还要简单一些,作者在这里做了大幅度的点云降采样,有些奢侈了。工业中很少会使用密度如此高,精度如此高的传感器,数据处理往往是业界关注的问题。

我的思考:在实际的业务需求里,有必要对点云进行角点和平面点的区分吗?一定要单独写成一个独立的node/进程吗?这样的数据传输的成本是不是过高了?如果一定要按照这种方式区分角点和平面点,能不能在imageProjection里做了,省去数据传输的成本?

laserCloudInfoHandler():

输入:来自imageProjection的去畸变的一帧lidar点云;

输出:平面点和角点点云,由mapOptimization订阅。

3. 功能入口

int main(int argc, char** argv)
{
    ros::init(argc, argv, "lio_sam");

    FeatureExtraction FE;

    ROS_INFO("\033[1;32m----> Feature Extraction Started.\033[0m");
   
    ros::spin();

    return 0;
}

核心内容就在创建FeatureExtraction对象时进行的,看一下他的构造函数:

    FeatureExtraction()
    {
        // 订阅当前激光帧运动畸变校正后的点云信息
        subLaserCloudInfo = nh.subscribe<lio_sam::cloud_info>("lio_sam/deskew/cloud_info", 1, &FeatureExtraction::laserCloudInfoHandler, this, ros::TransportHints().tcpNoDelay());

        // 发布当前激光帧提取特征之后的点云信息
        pubLaserCloudInfo = nh.advertise<lio_sam::cloud_info> ("lio_sam/feature/cloud_info", 1);
        // 发布当前激光帧的角点点云
        pubCornerPoints = nh.advertise<sensor_msgs::PointCloud2>("lio_sam/feature/cloud_corner", 1);
        // 发布当前激光帧的面点点云
        pubSurfacePoints = nh.advertise<sensor_msgs::PointCloud2>("lio_sam/feature/cloud_surface", 1);
        
        // 初始化
        initializationValue();
    }

它的回调函数只有一个laserCloudInfoHandler(),接收来自imageProjection处理后的点云。

3.1 点云特征提取 laserCloudInfoHandler()

    void laserCloudInfoHandler(const lio_sam::cloud_infoConstPtr& msgIn)
    {
        cloudInfo = *msgIn; 
        cloudHeader = msgIn->header; 
        // 当前激光帧运动畸变校正后的有效点云
        pcl::fromROSMsg(msgIn->cloud_deskewed, *extractedCloud); 

        // 计算当前激光帧点云中每个点的曲率
        calculateSmoothness();

        // 标记属于遮挡、平行两种情况的点,不做特征提取
        markOccludedPoints();

        // 点云角点、平面点特征提取
        // 1、遍历扫描线,每根扫描线扫描一周的点云划分为6段,针对每段提取20个角点、不限数量的平面点,加入角点集合、平面点集合
        // 2、认为非角点的点都是平面点,加入平面点云集合,最后降采样
        extractFeatures();
        
        // 发布角点、面点点云,发布带特征点云数据的当前激光帧点云信息
        publishFeatureCloud();
    }


3.1.1 曲率计算calculateSmoothness()

    void calculateSmoothness()
    {
        // 遍历当前激光帧运动畸变校正后的有效点云
        int cloudSize = extractedCloud->points.size();
        for (int i = 5; i < cloudSize - 5; i++)
        {
            // 用当前激光点前后5个点计算当前点的曲率,平坦位置处曲率较小,角点处曲率较大;这个方法很简单但有效
            float diffRange = cloudInfo.pointRange[i-5] + cloudInfo.pointRange[i-4]
                            + cloudInfo.pointRange[i-3] + cloudInfo.pointRange[i-2]
                            + cloudInfo.pointRange[i-1] - cloudInfo.pointRange[i] * 10
                            + cloudInfo.pointRange[i+1] + cloudInfo.pointRange[i+2]
                            + cloudInfo.pointRange[i+3] + cloudInfo.pointRange[i+4]
                            + cloudInfo.pointRange[i+5];            

            // 距离差值平方作为曲率
            cloudCurvature[i] = diffRange*diffRange;

            cloudNeighborPicked[i] = 0;
            cloudLabel[i] = 0;
            
            // 存储该点曲率值、激光点一维索引
            cloudSmoothness[i].value = cloudCurvature[i];
            cloudSmoothness[i].ind = i;
        }
    }

pointRange是指该点到lidar坐标系原点的距离,所以说作者把曲率是用深度值差的平方来描述的。

3.1.2 选出(剔除)与激光方向平行或被遮挡的激光点markOccludedPoints()

    void markOccludedPoints()
    {
        int cloudSize = extractedCloud->points.size();
        // mark occluded points and parallel beam points

        for (int i = 5; i < cloudSize - 6; ++i)
        {
            // 当前点和下一个点的深度值
            float depth1 = cloudInfo.pointRange[i];
            float depth2 = cloudInfo.pointRange[i+1];

            // 用于判断这两个点是不是在同一个ring上
            int columnDiff = std::abs(int(cloudInfo.pointColInd[i+1] - cloudInfo.pointColInd[i]));

            //1. 判断是否存在遮挡
            // 两个点在同一扫描线上
            if (columnDiff < 10){
                // 前者深度更大,说明后者把前者遮挡了,所以前者标记为1后续就不用了
                if (depth1 - depth2 > 0.3){
                    cloudNeighborPicked[i - 5] = 1;
                    cloudNeighborPicked[i - 4] = 1;
                    cloudNeighborPicked[i - 3] = 1;
                    cloudNeighborPicked[i - 2] = 1;
                    cloudNeighborPicked[i - 1] = 1;
                    cloudNeighborPicked[i] = 1;
                }
                // 后者深度更大,说明前者把后者遮挡了,所以后者标记为1后续就不用了
                else if (depth2 - depth1 > 0.3){
                    cloudNeighborPicked[i + 1] = 1;
                    cloudNeighborPicked[i + 2] = 1;
                    cloudNeighborPicked[i + 3] = 1;
                    cloudNeighborPicked[i + 4] = 1;
                    cloudNeighborPicked[i + 5] = 1;
                    cloudNeighborPicked[i + 6] = 1;
                }
            }

            //2. 判断当前点是否在与激光帧平行的平面上
            // 用前后相邻点判断当前点所在平面是否与激光束方向平行
            float diff1 = std::abs(float(cloudInfo.pointRange[i-1] - cloudInfo.pointRange[i]));
            float diff2 = std::abs(float(cloudInfo.pointRange[i+1] - cloudInfo.pointRange[i]));

            // 说明当前点所在平面/直线几乎与激光帧平行,则被剔除
            if (diff1 > 0.02 * cloudInfo.pointRange[i] && diff2 > 0.02 * cloudInfo.pointRange[i])
                cloudNeighborPicked[i] = 1;
        }
    }
    


3.1.3 根据曲率把平面点和角点提取出来extractFeatures()


很长一大段,说白了就是根据曲率把点分为平面点和角点罢了。不过作者的传感器真NB啊,这么多点都丢了,我们开发用的传感器很low,不敢这么整。

    void extractFeatures()
    {
        cornerCloud->clear();
        surfaceCloud->clear();

        pcl::PointCloud<PointType>::Ptr surfaceCloudScan(new pcl::PointCloud<PointType>());
        pcl::PointCloud<PointType>::Ptr surfaceCloudScanDS(new pcl::PointCloud<PointType>());

        // 遍历扫描线
        for (int i = 0; i < N_SCAN; i++)
        {
            surfaceCloudScan->clear();

            // 将一条扫描线扫描一周的点云数据,划分为6段,每段分开提取有限数量的特征,保证特征均匀分布
            for (int j = 0; j < 6; j++)
            {
                // 每段点云的起始、结束索引;startRingIndex为扫描线起始第5个激光点在一维数组中的索引
                int sp = (cloudInfo.startRingIndex[i] * (6 - j) + cloudInfo.endRingIndex[i] * j) / 6;
                int ep = (cloudInfo.startRingIndex[i] * (5 - j) + cloudInfo.endRingIndex[i] * (j + 1)) / 6 - 1;

                if (sp >= ep)
                    continue;

                // 按照曲率从小到大排序点云
                std::sort(cloudSmoothness.begin()+sp, cloudSmoothness.begin()+ep, by_value());

                // 按照曲率从大到小遍历
                int largestPickedNum = 0;
                for (int k = ep; k >= sp; k--)
                {
                    // 激光点的索引
                    int ind = cloudSmoothness[k].ind;
                    // 当前激光点还未被处理,且曲率大于阈值,则认为是角点
                    if (cloudNeighborPicked[ind] == 0 && cloudCurvature[ind] > edgeThreshold)
                    {
                        // 每段只取20个角点,如果单条扫描线扫描一周是1800个点,则划分6段,每段300个点,从中提取20个角点
                        largestPickedNum++;
                        if (largestPickedNum <= 20){
                            // 标记为角点
                            cloudLabel[ind] = 1;
                            // 加入角点点云
                            cornerCloud->push_back(extractedCloud->points[ind]);
                        } else {
                            break;
                        }

                        // 标记已被处理
                        cloudNeighborPicked[ind] = 1;
                        // 同一条扫描线上后5个点标记一下,不再处理,避免特征聚集
                        for (int l = 1; l <= 5; l++)
                        {
                            int columnDiff = std::abs(int(cloudInfo.pointColInd[ind + l] - cloudInfo.pointColInd[ind + l - 1]));
                            if (columnDiff > 10)
                                break;
                            cloudNeighborPicked[ind + l] = 1;
                        }
                        // 同一条扫描线上前5个点标记一下,不再处理,避免特征聚集
                        for (int l = -1; l >= -5; l--)
                        {
                            int columnDiff = std::abs(int(cloudInfo.pointColInd[ind + l] - cloudInfo.pointColInd[ind + l + 1]));
                            if (columnDiff > 10)
                                break;
                            cloudNeighborPicked[ind + l] = 1;
                        }
                    }
                }

                // 按照曲率从小到大遍历
                for (int k = sp; k <= ep; k++)
                {
                    // 激光点的索引
                    int ind = cloudSmoothness[k].ind;
                    // 当前激光点还未被处理,且曲率小于阈值,则认为是平面点
                    if (cloudNeighborPicked[ind] == 0 && cloudCurvature[ind] < surfThreshold)
                    {
                        // 标记为平面点
                        cloudLabel[ind] = -1;
                        // 标记已被处理
                        cloudNeighborPicked[ind] = 1;

                        // 同一条扫描线上后5个点标记一下,不再处理,避免特征聚集
                        for (int l = 1; l <= 5; l++) {

                            int columnDiff = std::abs(int(cloudInfo.pointColInd[ind + l] - cloudInfo.pointColInd[ind + l - 1]));
                            if (columnDiff > 10)
                                break;

                            cloudNeighborPicked[ind + l] = 1;
                        }
                        // 同一条扫描线上前5个点标记一下,不再处理,避免特征聚集
                        for (int l = -1; l >= -5; l--) {

                            int columnDiff = std::abs(int(cloudInfo.pointColInd[ind + l] - cloudInfo.pointColInd[ind + l + 1]));
                            if (columnDiff > 10)
                                break;

                            cloudNeighborPicked[ind + l] = 1;
                        }
                    }
                }

                // 平面点和未被处理的点,都认为是平面点,加入平面点云集合
                for (int k = sp; k <= ep; k++)
                {
                    if (cloudLabel[k] <= 0){
                        surfaceCloudScan->push_back(extractedCloud->points[k]);
                    }
                }
            }

            // 平面点云降采样
            surfaceCloudScanDS->clear();
            downSizeFilter.setInputCloud(surfaceCloudScan);
            downSizeFilter.filter(*surfaceCloudScanDS);

            // 加入平面点云集合
            *surfaceCloud += *surfaceCloudScanDS;
        }
    }


  3.1.4 把提取的角点和平面点发布出去publishFeatureCloud()

    void publishFeatureCloud()
    {
        // 清理
        freeCloudInfoMemory();
        // 发布角点、面点点云,用于rviz展示
        cloudInfo.cloud_corner  = publishCloud(&pubCornerPoints,  cornerCloud,  cloudHeader.stamp, lidarFrame);
        cloudInfo.cloud_surface = publishCloud(&pubSurfacePoints, surfaceCloud, cloudHeader.stamp, lidarFrame);
        // 发布当前激光帧点云信息,加入了角点、面点点云数据,发布给mapOptimization
        pubLaserCloudInfo.publish(cloudInfo);
    }
    


 

<think>嗯,用户想了解LIO-SAM中的特征提取与可视化方法。首先,我需要回忆一下LIO-SAM的基本结构和相关引用内容。记得LIO-SAM是基于激光雷达和IMU的紧耦合SLAM框架,它改进了LEGO-LOAM,特别是在特征提取方面可能有不同的处理方式。 根据引用[1],LEGO-LOAM在特征提取时会将点云分为边缘和平面,分别加入不同的集合。而LIO-SAM可能继承了类似的思路,但可能有优化。需要结合用户提供的引用内容,特别是引用[3]提到GICP算法使用协方差矩阵进行加权,这可能与特征提取中的协方差分析有关。 用户的问题涉及特征提取的具体步骤和可视化方法。首先,我需要分步骤解释特征提取的流程,可能包括曲率计算、分类、筛选等步骤。然后,可视化部分可能需要使用ROS中的工具,比如RViz,或者PCL库进行点云显示。 在引用[4]中提到了LIO-SAM的mapOptimization模块,虽然这里主要处理闭环检测,但可能涉及特征的使用。此外,引用[2]中的extractNearby和extractCloud函数可能涉及点云数据的处理,但用户提供的引用中没有详细说明特征提取的具体代码,可能需要结合LIO-SAM的源码进行分析。 特征提取的关键可能包括计算每个的曲率,根据曲率大小将分类为边缘或平面,并进行筛选以确保特征的质量。例如,边缘通常选择曲率大的,而平面选择曲率小的。此外,可能需要避免在遮挡区域或不可靠区域选择特征,这涉及到点云的法向量或平滑度分析。 可视化方面,可能需要将提取的特征发布为ROS话题,然后在RViz中订阅这些话题,使用不同的颜色或标记来区分边缘和平面。同时,可能需要在代码中将特征存储为PointCloud2类型,并发布到相应的话题上。 需要注意的是,用户提供的引用中有提到协方差矩阵的SVD分解用于判断是否为直线特征,这可能与特征提取中的几何验证有关,确保所选符合线或面的结构。这部分可能在LIO-SAM中有所应用,用于优化特征的选择。 最后,用户可能需要具体的代码示例或步骤来实现特征提取和可视化,因此回答中需要包含伪代码或代码片段,并说明如何配置可视化工具。同时,生成的相关问题需要围绕特征提取的方法、优化、评估以及与其他传感器数据的融合等方面展开。</think>### LIO-SAM特征提取与可视化方法 #### 一、特征提取流程 1. **点云预处理** 原始点云通过体素滤波或距离滤波去除噪声,例如仅保留$r < 50m$的点云[^2]。 2. **曲率计算与分类** - 对每个计算局部曲率,公式为: $$ \sigma = \frac{1}{N} \sum_{i=1}^{N} \left( d_i - \bar{d} \right)^2 $$ 其中$d_i$为到相邻的距离,$\bar{d}$为均值。 - 根据曲率将分为两类: - **边缘**:曲率高于阈值(如$\sigma > 0.1$) - **平面**:曲率低于阈值(如$\sigma < 0.05$)[^1] 3. **特征筛选** - 排除遮挡区域:若某邻域内密度突降,判定为遮挡边界 - 平面要求局部法向量与激光束夹小于阈值(如$30^\circ$) #### 二、可视化实现方法 1. **ROS话题发布** 将特征分类后发布为`sensor_msgs/PointCloud2`话题: ```cpp // 伪代码示例(基于LIO-SAM源码逻辑) pcl::PointCloud<pcl::PointXYZI> edgePoints, planarPoints; // ...特征提取过程... sensor_msgs::PointCloud2 edgeMsg, planarMsg; pcl::toROSMsg(edgePoints, edgeMsg); pcl::toROSMsg(planarPoints, planarMsg); edgeMsg.header.frame_id = "lidar_link"; planarMsg.header.frame_id = "lidar_link"; pubEdge.publish(edgeMsg); // 发布边缘 pubPlanar.publish(planarMsg); // 发布平面 ``` 2. **RViz显示配置** - 添加两个`PointCloud2`显示层,分别设置: - 边缘:红色显示,大小3px - 平面:蓝色显示,大小2px - 坐标系固定为`lidar_link`或`map` 3. **动态效果增强** 通过`tf`发布传感器位姿,结合`RViz`的`Axes`插件显示运动轨迹。 #### 三、关键优化策略 1. **协方差矩阵验证** 对候选特征进行SVD分解,验证几何一致性: $$ \text{SVD}(\Sigma) = U \Lambda V^T $$ 当最大特征值$\lambda_1 \gg \lambda_2, \lambda_3$时判定为有效边缘[^3]。 2. **运动畸变校正** 使用IMU数据补偿激光雷达在扫描周期内的位姿变化,公式: $$ \mathbf{p}_i^{corrected} = T_{imu}^{end} \cdot T_{imu}^{i} \cdot \mathbf{p}_i $$ #### 四、实验验证方法 1. **特征密度评估** 统计单位面积内有效特征数,要求边缘密度$>5pts/m^2$,平面密度$>20pts/m^2$。 2. **配准精度验证** 使用GICP算法计算相邻帧配准误差,要求平移误差$<0.1m$,旋转误差$<1^\circ$[^3]。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值