代码实战 | 用LeGO-LOAM实现BFS点云聚类和噪点剔除

原文链接:代码实战 | 用LeGO-LOAM实现BFS点云聚类和噪点剔除

作者介绍:Zach,移动机器人从业者,热爱移动机器人行业,立志于科技助力美好生活。他也是我们课程学员:基于LiDAR的多传感器融合SLAM:LOAM、LeGO-LOAM、LIO-SAM

LeGO-LOAM的软件框架分为五个部分:

  1. 分割聚类:这部分主要操作是分离出地面点云;同时,对剩下的点云进行聚类,剔除噪声(数量较少的点云簇,被标记为噪声);
  2. 特征提取:对分割后的点云(排除地面点云部分)进行边缘点和面点特征提取;
  3. Lidar里程计:在连续帧之间(边缘点和面点)进行特征匹配找到连续帧之间的位姿变换矩阵;
  4. Lidar Mapping:对特征进一步处理,然后在全局的 Point Cloud Map 中进行配准;
  5. Transform Integration:Transform Integration 融合了来自 Lidar Odometry 和 Lidar Mapping 的 pose estimation 进行输出最终的 pose estimate。

上一章代码实战 | 用LeGO-LOAM实现地面提取我们分析了LeGO-LOAM是如何对地面点云进行提取分离的,这一章我们将详细介绍LeGO-LOAM是如何对地表上点云进行聚类分割,以便剔除噪声点。

LeGO-LOAM论文中对 地表上点云进行聚类分割参考了论文《Fast Range Image-based Segmentation of Sparse 3D Laser Scans for Online Operation》,这篇文章的作者号称分割速度极快,到底有多快!大家可以看看原论文。

我们先学习一下这篇论文实现 地表点云分割原理,再详细分析一下LeGO-LOAM框架是如何实现的。

地表点云聚类分割的原理

作者先把激光雷达扫面的数据投影到一个2D的深度图上(与LeGO-LOAM的 Range Map 一样),深度图的 长/横向是激光雷达单条 scan 扫描的点云,深度图的 高/纵向是激光雷达的线束。需要注意的是,作者对横向数据进行了压缩。例如,870*32 表示横向个870点,32条scan。

代码实战 | 用LeGO-LOAM实现BFS点云聚类和噪点剔除

图1 Rang Map. 栅格里的值表示聚类值, -1表示无返回值.

有一点需要注意的是,2D的Range Map 左边界和右边界是相连的,因为激光雷达的扫描线是环形的。

代码实战 | 用LeGO-LOAM实现BFS点云聚类和噪点剔除

图2 点云快速分割原理示意图

代码实战 | 用LeGO-LOAM实现BFS点云聚类和噪点剔除

上述方法也有一些缺陷,当激光雷达距离墙面很近时,墙上离激光雷达较远的点容易判定为另一个目标。作者认为这并不影响上述算法的实际有效性。 从直观上来看,该方法确实能区分深度差异较大的点。

作者在整个分割聚类的流程中使用了 邻居的 BFS 搜索,极大的加快了聚类分割的速度,伪代码如下:

代码实战 | 用LeGO-LOAM实现BFS点云聚类和噪点剔除

图3 Range Image Label

  • 遍历Rang Map 上所有点(Line 1–8)
  • 遍历方式是逐行遍历,先行后列(Line 4–9);
  • 如果遇到未标记的像素,则执行BFS操作,并打标签(Line 6–8);
  • 对每个点进行四领域BFS搜索(Line 9–19)
  • 建立一个队列,push种子点 (Line 10)
  • 从队列中取出一个点和该点标签,判断该点与其四领域是否是同类点(Line 12–17);如果是同类点,则填入队列并打上标签(Line 18)
  • 从队列中删除刚刚取出的点(Line 19)

如果我们深层次的去思考的话,会发现BFS起到了在Rang Map 上的聚类,这样聚类出来的点云,要么同类簇点云,要么只是深度距离值存在明显差异的点云;然后再进一步使用 角度阈值分离出在深度距离上存在明显差异的不同类点云;最后,对点云起到了一个很好的聚类分割效果。

; LeGO-LOAM源码实现地表点云聚类分割

点云分割的主要流程是先进行地面提取(在上一篇文章中已进行说明),然后对剩下的点云进行分割聚类,最后拿分割好的点云进一步进行特征提取。在这个过程之后,只保留大物体的点云,例如地面和树干(剔除了树叶和树枝等点云),以供进一步处理。如下图所是:

代码实战 | 用LeGO-LOAM实现BFS点云聚类和噪点剔除

图4 点云聚类分割效果图

上图(a) 是原始点云,图(b)是经过聚类分割后的点云,红色的点表示地面点,剩下的点是分割后的点云。

下面对照官方代码详细说明这个过程是如何实现的。

函数 void cloudSegmentation() 实现

点云分割的代码主要由: LEGO-LOAM/src/imageProjection.cpp文件中的函数 void cloudSegmentation() 实现。该函数的核心流程和作用如下图所示:

代码实战 | 用LeGO-LOAM实现BFS点云聚类和噪点剔除

图5 函数 void cloudSegmentation()流程

我们先从整理上来分析这个函数的作用,后细致分析里面的具体细节。

  • Step 1: 按行遍历 RangMap 对所有的点进行聚类分割,分割的结果存储在 labelMat 中, 这部分基本上实现了图3中的算法流程,稍后会对这部分进行详细的分析说明
// 1. 按行遍历所有点进行分割聚类,更新labelMat
for (size_t i = 0; i < N_SCAN; ++i) {
    for (size_t j = 0; j < Horizon_SCAN; ++j) {
        // labelMat &#x5B58;&#x50A8;&#x4E86;&#x6BCF;&#x4E2A;&#x70B9;&#x7684;&#x805A;&#x7C7B;&#x6807;&#x8BB0;
        if (labelMat.at<int>(i,j) == 0) {
            // &#x5BF9;&#x672A;&#x88AB;&#x6807;&#x8BB0;&#x7684;&#x70B9;&#x6267;&#x884C;BFS, &#x540C;&#x65F6;&#x8FDB;&#x884C;&#x805A;&#x7C7B;
            labelComponents(i, j);
        }
    }
}

</int>
  • Step 2: 经过 Step 1,就获得了点云分割的结果,其结果存储在 labelMat 中,其中同类点云具有相同的标号,噪声的标号为 999999。代码如下:
// 2. &#x5BF9;&#x566A;&#x58F0;/&#x5730;&#x9762;&#x70B9;/&#x5206;&#x9694;&#x70B9;&#x8FDB;&#x4E00;&#x6B65;&#x5904;&#x7406;
int sizeOfSegCloud = 0;
// extract segmented cloud for lidar odometry
for (size_t i = 0; i < N_SCAN; ++i) {
    // &#x8BB0;&#x5F55;&#x6BCF;&#x4E00;&#x6761;scan&#x4E2D;&#x5206;&#x5272;&#x51FA;&#x7684;&#x6709;&#x6548;&#x70B9;&#x8D77;&#x59CB;index&#x548C;&#x7EC8;&#x6B62;index
    segMsg.startRingIndex[i] = sizeOfSegCloud-1 + 5;

    for (size_t j = 0; j < Horizon_SCAN; ++j) {
        // &#x53EA;&#x5904;&#x7406;&#x6709;&#x6548;&#x7684;&#x805A;&#x7C7B;&#x70B9;&#x6216;&#x8005;&#x5730;&#x9762;&#x70B9;
        if (labelMat.at<int>(i, j) > 0 || groundMat.at<int8_t>(i, j) == 1) {
            // outliers that will not be used for optimization (always continue) &#x4E0D;&#x7528;&#x4E8E;&#x4F18;&#x5316;&#x7684;&#x5F02;&#x5E38;&#x503C;
            // 2.1. &#x8FC7;&#x6EE4;&#x6389;&#x6240;&#x6709;&#x7684;&#x566A;&#x58F0;&#x70B9;, &#x53E6;&#x4F5C;&#x5B58;&#x50A8;;
            if (labelMat.at<int>(i, j) == 999999) { // &#x5F02;&#x5E38;&#x70B9;
                // &#x662F;&#x566A;&#x70B9;&#xFF0C;&#x566A;&#x70B9;&#x6A2A;&#x8F74;&#x5750;&#x6807;&#x662F;5&#x7684;&#x500D;&#x6570;&#x5C31;&#x8FDB;&#x884C;&#x5B58;&#x50A8;
                if (i > groundScanInd && j % 5 == 0) {
                    outlierCloud->push_back(fullCloud->points[j + i*Horizon_SCAN]);
                    continue;
                } else {
                    continue;
                }
            }

            // majority of ground points are skipped
            // 2.2. &#x538B;&#x7F29;&#x5730;&#x9762;&#x70B9;, &#x53EA;&#x53D6;&#x4E94;&#x5206;&#x4E4B;&#x4E00;&#x7684;&#x5730;&#x9762;&#x70B9;&#x8FDB;&#x884C;&#x5B58;&#x50A8;;
            if (groundMat.at<int8_t>(i, j) == 1) {
                if (j%5 !=0 && j>5 && j<horizon_scan-5) continue; } 2.3. 保留所有的有效分割点; mark ground points so they will not be considered as edge features later 标记地面点,以便以后不会将其视为边缘特征 segmsg.segmentedcloudgroundflag[sizeofsegcloud]="(groundMat.at<int8_t">(i,j) == 1);
            // mark the points' column index for marking occlusion later
            // &#x6807;&#x8BB0;&#x70B9;&#x7684;&#x5217;&#x7D22;&#x5F15;, &#x7A0D;&#x540E;&#x6807;&#x8BB0;&#x906E;&#x6321;
            segMsg.segmentedCloudColInd[sizeOfSegCloud] = j;
            // save range info &#x4FDD;&#x5B58;&#x8303;&#x56F4;&#x4FE1;&#x606F;
            segMsg.segmentedCloudRange[sizeOfSegCloud]  = rangeMat.at<float>(i,j);
            // save seg cloud &#x4FDD;&#x5B58;&#x5206;&#x5272;&#x70B9;&#x4E91;
            segmentedCloud->push_back(fullCloud->points[j + i*Horizon_SCAN]); // &#x65E2;&#x6709;&#x5206;&#x5272;&#x7684;&#x5730;&#x8868;&#x70B9;&#x4E91;&#xFF0C;&#x53C8;&#x6709;&#x5730;&#x9762;&#x70B9;&#x4E91;
            // size of seg cloud &#x66F4;&#x65B0;&#x5206;&#x5272;&#x70B9;&#x4E91;&#x6570;&#x91CF;
            ++sizeOfSegCloud;
        }
    }

    segMsg.endRingIndex[i] = sizeOfSegCloud-1 - 5;
}

</float></horizon_scan-5)></int8_t></int></int8_t></int>

代码采取按行遍历,通过条件语句 if (labelMat.at<int>(i, j) > 0 || groundMat.at<int8_t>(i, j) == 1)</int8_t></int> 限制了处理对象只包括 地面点云分割点云

2.1 过滤掉所有的噪声点

if (labelMat.at<int>(i, j) == 999999) { // &#x5F02;&#x5E38;&#x70B9;
    // &#x662F;&#x566A;&#x70B9;&#xFF0C;&#x566A;&#x70B9;&#x6A2A;&#x8F74;&#x5750;&#x6807;&#x662F;5&#x7684;&#x500D;&#x6570;&#x5C31;&#x8FDB;&#x884C;&#x5B58;&#x50A8;
    if (i > groundScanInd && j % 5 == 0) {
        outlierCloud->push_back(fullCloud->points[j + i*Horizon_SCAN]);
        continue;
    } else {
        continue;
    }
}

</int>

上述代码表面,如果是噪声点,则不执行双层 for循环的余下代码,相当于过滤掉这些噪声点。把噪声点另外存储在 outlierCloud 中,存储的时候时压缩存储的,只有遇到点的横轴坐标遇 5整除才进行存储。

2.2 压缩地面点

// majority of ground points are skipped
// 2.2. &#x538B;&#x7F29;&#x5730;&#x9762;&#x70B9;, &#x53EA;&#x53D6;&#x4E94;&#x5206;&#x4E4B;&#x4E00;&#x7684;&#x5730;&#x9762;&#x70B9;&#x8FDB;&#x884C;&#x5B58;&#x50A8;;
if (groundMat.at<int8_t>(i, j) == 1) {
    if (j%5 !=0 && j>5 && j<horizon_scan-5) continue; } < code></horizon_scan-5)></int8_t>

如果检测到的是地面点,会过滤掉绝大部分的地面点,只保留横轴遇5整除的点作进一步处理。

2.3 保留所有有效分割点和压缩后的地面点

// 2.3. &#x4FDD;&#x7559;&#x6240;&#x6709;&#x7684;&#x6709;&#x6548;&#x5206;&#x5272;&#x70B9;;
// mark ground points so they will not be considered as edge features later
// &#x6807;&#x8BB0;&#x5730;&#x9762;&#x70B9;&#xFF0C;&#x4EE5;&#x4FBF;&#x4EE5;&#x540E;&#x4E0D;&#x4F1A;&#x5C06;&#x5176;&#x89C6;&#x4E3A;&#x8FB9;&#x7F18;&#x7279;&#x5F81;
segMsg.segmentedCloudGroundFlag[sizeOfSegCloud] = (groundMat.at<int8_t>(i,j) == 1);
// mark the points' column index for marking occlusion later
// &#x6807;&#x8BB0;&#x70B9;&#x7684;&#x5217;&#x7D22;&#x5F15;, &#x7A0D;&#x540E;&#x6807;&#x8BB0;&#x906E;&#x6321;
segMsg.segmentedCloudColInd[sizeOfSegCloud] = j;
// save range info &#x4FDD;&#x5B58;&#x8303;&#x56F4;&#x4FE1;&#x606F;
segMsg.segmentedCloudRange[sizeOfSegCloud]  = rangeMat.at<float>(i,j);
// save seg cloud &#x4FDD;&#x5B58;&#x5206;&#x5272;&#x70B9;&#x4E91;
segmentedCloud->push_back(fullCloud->points[j + i*Horizon_SCAN]); // &#x65E2;&#x6709;&#x5206;&#x5272;&#x7684;&#x5730;&#x8868;&#x70B9;&#x4E91;&#xFF0C;&#x53C8;&#x6709;&#x5730;&#x9762;&#x70B9;&#x4E91;
// size of seg cloud &#x66F4;&#x65B0;&#x5206;&#x5272;&#x70B9;&#x4E91;&#x6570;&#x91CF;
++sizeOfSegCloud;

</float></int8_t>

凡是没有过滤掉的点,都执行上述的数据保存工作。最后的 segmentedCloud 保存的既有稳定的分割点(剔除噪声点),又有压缩的地面点。

  • Step 3: 给分割的点进行着色,同类点云的强度值相同。
// 3. &#x53EF;&#x89C6;&#x5316;&#x5206;&#x5272;&#x540E;&#x7684;&#x70B9;&#x4E91;(&#x540C;&#x7C7B;&#x70B9;&#x4E91;&#x5206;&#x914D;&#x76F8;&#x540C;&#x7684;&#x5F3A;&#x5EA6;&#x503C;)&#xFF0C;&#x4E0D;&#x5305;&#x62EC;&#x5730;&#x9762;&#x70B9;&#x4E91;
// extract segmented cloud for visualization
if (pubSegmentedCloudPure.getNumSubscribers() != 0) {
    // &#x904D;&#x5386;range map &#x4E2D;&#x7684;&#x70B9;&#x4E91;
    for (size_t i = 0; i < N_SCAN; ++i){
        for (size_t j = 0; j < Horizon_SCAN; ++j) {
            if (labelMat.at<int>(i,j) > 0 && labelMat.at<int>(i,j) != 999999){
                // &#x7ED9;&#x70B9;&#x4E91;&#x5206;&#x914D;&#x5F3A;&#x5EA6;&#x503C;: &#x540C;&#x7C7B;&#x70B9;&#x4E91;, &#x5F3A;&#x5EA6;&#x503C;&#x76F8;&#x540C;
                segmentedCloudPure->push_back(fullCloud->points[j + i*Horizon_SCAN]);
                segmentedCloudPure->points.back().intensity = labelMat.at<int>(i,j);
            }
        }
    }
}

</int></int></int>

通过限制条件可知,上述代码只给分割点进行着色,不包括地面点云。

在函数 void cloudSegmentation()中,除了 Step 1 部分外,其他的都很好理解。 Step 1 里面的核心部分是函数 void labelComponents(int row, int col),其包含了使用BFS进行聚类,使用角度判断是否同属一类,还涉及到一些经验参数的设置。但是,大体上该函数的实现是依据论文《Fast Range Image-based Segmentation of Sparse 3D Laser Scans for Online Operation》而来。如果文章的前半部分看懂了,理解这部分代码应该没有什么问题。

函数 void labelComponents(int row, int col) 实现

函数 void labelComponents(int row, int col) 的形参是一个点在 range map 上的横纵坐标,该函数就是以点 [row, col] 为中心,进行BFS(广度优先搜索),在BFS的基础之上进行角度对比以进一步判断两点是否为同类点。最终的结果是,获得点 [row, col] 所在区域的同类点,结果更新在 labelMat 上。

在详细分析代码之前,为了缕清结构,我们先看一下大致的代码流程:

代码实战 | 用LeGO-LOAM实现BFS点云聚类和噪点剔除

代码实战 | 用LeGO-LOAM实现BFS点云聚类和噪点剔除

图6 函数 void labelComponents()流程

  • Step 1: 初始化队列信息
// 1. &#x4F7F;&#x7528;C++ &#x5BB9;&#x5668;&#x4F1A;&#x5BFC;&#x81F4;&#x8BA1;&#x7B97;&#x901F;&#x5EA6;&#x53D8;&#x6162;, &#x4F5C;&#x8005;&#x4F7F;&#x7528;&#x6570;&#x7EC4;&#x6A21;&#x62DF;&#x4E86;&#x4E00;&#x4E2A;&#x961F;&#x5217;
float d1, d2, alpha, angle;
int fromIndX, fromIndY, thisIndX, thisIndY;
bool lineCountFlag[N_SCAN] = {false};

// &#x4F7F;&#x7528;&#x6570;&#x7EC4;&#x6A21;&#x62DF;&#x961F;&#x5217;
queueIndX[0] = row;
queueIndY[0] = col;
int queueSize = 1;
int queueStartInd = 0; // pop &#x6570;&#x636E;&#x7684;&#x54E8;&#x5175;
int queueEndInd = 1; // push &#x6570;&#x636E;&#x7684;&#x54E8;&#x5175;

// &#x5B58;&#x50A8;&#x5206;&#x5272;&#x7684;&#x70B9;&#x4E91;
allPushedIndX[0] = row;
allPushedIndY[0] = col;
int allPushedIndSize = 1; // &#x8BB0;&#x5F55;&#x5206;&#x5272;&#x70B9;&#x4E91;&#x7684;&#x70B9;&#x6570;&#x91CF;

作者在此处使用了两个数组来模拟队列的功能,不使用C++容器的原因是,当容器容量不够是,C++会进行两倍扩容;同时存储效率也没有数组高。这部分代码把形参当作第一个点传入队列中,设置好队列的 pop 哨兵和 push 哨兵,并初始化数组 allPushedIndXallPushedIndY

  • Step 2 使用BFS进行点云分割 这段代码最好对照图3**的伪代码流程进行查看。
// 2. &#x8FDB;&#x884C;BFS&#x70B9;&#x4E91;&#x5206;&#x5272;
while(queueSize > 0) {
    // Pop point &#x63D0;&#x53D6;&#x4E00;&#x4E2A;&#x6570;&#x636E;
    fromIndX = queueIndX[queueStartInd];
    fromIndY = queueIndY[queueStartInd];
    --queueSize;
    ++queueStartInd;
    // Mark popped point
    labelMat.at<int>(fromIndX, fromIndY) = labelCount;
    // Loop through all the neighboring grids of popped grid
    for (auto iter = neighborIterator.begin(); iter != neighborIterator.end(); ++iter){
        // 2.1. &#x63D0;&#x4F9B;&#x6709;&#x6548;&#x7684;&#x90BB;&#x5C45;&#x70B9;(&#x6B63;&#x786E;&#x7684;&#x5750;&#x6807;&#x548C;&#x672A;&#x88AB;&#x6807;&#x8BB0;)
        // new index
        thisIndX = fromIndX + (*iter).first; // &#x7EB5;&#x8F74;&#x5750;&#x6807;
        thisIndY = fromIndY + (*iter).second; // &#x6A2A;&#x8F74;&#x5750;&#x6807;
        // index should be within the boundary
        // &#x907F;&#x514D;&#x7EB5;&#x8F74;&#x5750;&#x6807;&#x6B63;&#x786E;
        if (thisIndX < 0 || thisIndX >= N_SCAN)
            continue;
        // at range image margin (left or right side)
        // &#x8C03;&#x6574;&#x6A2A;&#x8F74;&#x8FB9;&#x754C;&#x6570;&#x636E;&#x662F;&#x76F8;&#x8FDE;&#x7684;
        if (thisIndY < 0)
            thisIndY = Horizon_SCAN - 1;
        if (thisIndY >= Horizon_SCAN)
            thisIndY = 0;
        // prevent infinite loop (caused by put already examined point back)
        // &#x4FDD;&#x8BC1;&#x6240;&#x53D6;&#x70B9;&#x7684;&#x6709;&#x6548;&#x6027;:&#x5FC5;&#x987B;&#x662F;&#x672A;&#x88AB;&#x6807;&#x8BB0;&#x7684;&#x70B9;
        if (labelMat.at<int>(thisIndX, thisIndY) != 0)
            continue;

        // 2.2. &#x8BA1;&#x7B97;d1(&#x957F;&#x8FB9;)&#x548C;d2(&#x77ED;&#x8FB9;)
        d1 = std::max(rangeMat.at<float>(fromIndX, fromIndY),
                      rangeMat.at<float>(thisIndX, thisIndY));
        d2 = std::min(rangeMat.at<float>(fromIndX, fromIndY),
                      rangeMat.at<float>(thisIndX, thisIndY));

        // 2.3. &#x6839;&#x636E;&#x6240;&#x53D6;&#x70B9;&#x7684;&#x4F4D;&#x7F6E;&#x8BBE;&#x7F6E;alpha&#x503C;
        if ((*iter).first == 0) // &#x90BB;&#x5C45;&#x70B9;&#x4E0E;&#x4E2D;&#x5FC3;&#x70B9;&#x540C;&#x4E00;&#x6761;&#x626B;&#x63CF;&#x7EBF;&#x4E0A;
            alpha = segmentAlphaX;
        else // &#x90BB;&#x5C45;&#x70B9;&#x4E0E;&#x4E2D;&#x5FC3;&#x70B9;&#x5728;&#x76F8;&#x8FDE;&#x7684;&#x626B;&#x63CF;&#x7EBF;&#x4E0A;
            alpha = segmentAlphaY;

        // 2.4. &#x8BA1;&#x7B97;&#x89D2;&#x5EA6;, &#x5E76;&#x6839;&#x636E;&#x9608;&#x503C;&#x5206;&#x7C7B;
        angle = atan2(d2*sin(alpha), (d1 -d2*cos(alpha)));
        if (angle > segmentTheta){ // &#x6B64;&#x5904;segmentTheta = 60.0
            // &#x628A;&#x6B64;&#x6709;&#x6548;&#x70B9;push&#x5165;&#x961F;&#x5217;&#x4E2D;
            queueIndX[queueEndInd] = thisIndX;
            queueIndY[queueEndInd] = thisIndY;
            ++queueSize;
            ++queueEndInd;

            // &#x6807;&#x8BB0;&#x6B64;&#x70B9;&#x4E0E;&#x4E2D;&#x5FC3;&#x70B9;&#x540C;&#x6837;&#x7684;label
            labelMat.at<int>(thisIndX, thisIndY) = labelCount;
            lineCountFlag[thisIndX] = true;

            //&#x8FFD;&#x8E2A;&#x5206;&#x5272;&#x7684;&#x70B9;&#x4E91;
            allPushedIndX[allPushedIndSize] = thisIndX;
            allPushedIndY[allPushedIndSize] = thisIndY;
            ++allPushedIndSize;
        }
    }
}

</int></float></float></float></float></int></int>

代码:

// Pop point &#x63D0;&#x53D6;&#x4E00;&#x4E2A;&#x6570;&#x636E;
fromIndX = queueIndX[queueStartInd];
fromIndY = queueIndY[queueStartInd];
--queueSize;
++queueStartInd;
// Mark popped point
labelMat.at<int>(fromIndX, fromIndY) = labelCount;

</int>

从队列中 pop 出一个点,并给该点标记 labelCount

代码 for (auto iter = neighborIterator.begin(); iter != neighborIterator.end(); ++iter) 是以给定的形参点 [row, col] 为中心,进行上下左右四邻居遍历。

2.1 提供有效的邻居点

// 2.1. &#x63D0;&#x4F9B;&#x6709;&#x6548;&#x7684;&#x90BB;&#x5C45;&#x70B9;(&#x6B63;&#x786E;&#x7684;&#x5750;&#x6807;&#x548C;&#x672A;&#x88AB;&#x6807;&#x8BB0;)
// new index
thisIndX = fromIndX + (*iter).first; // &#x7EB5;&#x8F74;&#x5750;&#x6807;
thisIndY = fromIndY + (*iter).second; // &#x6A2A;&#x8F74;&#x5750;&#x6807;
// index should be within the boundary
// &#x907F;&#x514D;&#x7EB5;&#x8F74;&#x5750;&#x6807;&#x6B63;&#x786E;
if (thisIndX < 0 || thisIndX >= N_SCAN)
    continue;
// at range image margin (left or right side)
// &#x8C03;&#x6574;&#x6A2A;&#x8F74;&#x8FB9;&#x754C;&#x6570;&#x636E;&#x662F;&#x76F8;&#x8FDE;&#x7684;
if (thisIndY < 0)
    thisIndY = Horizon_SCAN - 1;
if (thisIndY >= Horizon_SCAN)
    thisIndY = 0;
// prevent infinite loop (caused by put already examined point back)
// &#x4FDD;&#x8BC1;&#x6240;&#x53D6;&#x70B9;&#x7684;&#x6709;&#x6548;&#x6027;:&#x5FC5;&#x987B;&#x662F;&#x672A;&#x88AB;&#x6807;&#x8BB0;&#x7684;&#x70B9;
if (labelMat.at<int>(thisIndX, thisIndY) != 0)
    continue;

</int>

遍历 上下左右的邻居点,有可能出现以下情况:1. 纵轴坐标越界,则跳过该点;2. 横轴坐标越界,则调整横轴坐标,因为range map 的左右边界是相连的;3. 所取得点如果已经被标记,则跳过该点。

2.2 计算d1和d2的边长

代码实战 | 用LeGO-LOAM实现BFS点云聚类和噪点剔除

图7 计算的d1和d2

计算 d1和d2 代码实现如下:

// 2.2. &#x8BA1;&#x7B97;d1(&#x957F;&#x8FB9;)&#x548C;d2(&#x77ED;&#x8FB9;)
d1 = std::max(rangeMat.at<float>(fromIndX, fromIndY),
              rangeMat.at<float>(thisIndX, thisIndY));
d2 = std::min(rangeMat.at<float>(fromIndX, fromIndY),
              rangeMat.at<float>(thisIndX, thisIndY));

</float></float></float></float>

2.3 根据所取点的位置设置alpha值

// 2.3. &#x6839;&#x636E;&#x6240;&#x53D6;&#x70B9;&#x7684;&#x4F4D;&#x7F6E;&#x8BBE;&#x7F6E;alpha&#x503C;
if ((*iter).first == 0) // &#x90BB;&#x5C45;&#x70B9;&#x4E0E;&#x4E2D;&#x5FC3;&#x70B9;&#x540C;&#x4E00;&#x6761;&#x626B;&#x63CF;&#x7EBF;&#x4E0A;
    alpha = segmentAlphaX;
else // &#x90BB;&#x5C45;&#x70B9;&#x4E0E;&#x4E2D;&#x5FC3;&#x70B9;&#x5728;&#x76F8;&#x8FDE;&#x7684;&#x626B;&#x63CF;&#x7EBF;&#x4E0A;
    alpha = segmentAlphaY;

代码实战 | 用LeGO-LOAM实现BFS点云聚类和噪点剔除

2.4. 计算角度, 并根据阈值分类

// 2.4. &#x8BA1;&#x7B97;&#x89D2;&#x5EA6;, &#x5E76;&#x6839;&#x636E;&#x9608;&#x503C;&#x5206;&#x7C7B;
angle = atan2(d2*sin(alpha), (d1 -d2*cos(alpha)));
if (angle > segmentTheta){ // &#x6B64;&#x5904;segmentTheta = 60.0
    // &#x628A;&#x6B64;&#x6709;&#x6548;&#x70B9;push&#x5165;&#x961F;&#x5217;&#x4E2D;
    queueIndX[queueEndInd] = thisIndX;
    queueIndY[queueEndInd] = thisIndY;
    ++queueSize;
    ++queueEndInd;

    // &#x6807;&#x8BB0;&#x6B64;&#x70B9;&#x4E0E;&#x4E2D;&#x5FC3;&#x70B9;&#x540C;&#x6837;&#x7684;label
    labelMat.at<int>(thisIndX, thisIndY) = labelCount;
    lineCountFlag[thisIndX] = true;

    //&#x8FFD;&#x8E2A;&#x5206;&#x5272;&#x7684;&#x70B9;&#x4E91;
    allPushedIndX[allPushedIndSize] = thisIndX;
    allPushedIndY[allPushedIndSize] = thisIndY;
    ++allPushedIndSize;
}

</int>

最后,计算角度值,并与角度阈值作比较。如果大于角度阈值,则表示所遍历的邻居点与所选取的点是同类点云,标记此邻居点为 labelCount,并填充到 allPushedIndX/Y 中。

直到所构建的队列为空为止。

  • Step 3: 检测分类结果是否正确
// 3. &#x5224;&#x65AD;&#x5206;&#x9694;&#x662F;&#x5426;&#x6709;&#x6548;
// check if this segment is valid
bool feasibleSegment = false;
// &#x5982;&#x679C;&#x5206;&#x5272;&#x51FA;&#x7684;&#x70B9;&#x4E91;&#x4E2A;&#x6570;&#x5927;&#x4E8E;30&#x4E2A;&#x5219;&#x8BA4;&#x4E3A;&#x662F;&#x6B64;&#x5206;&#x5272;&#x662F;&#x6709;&#x6548;&#x7684;
if (allPushedIndSize >= 30)
    feasibleSegment = true;
else if (allPushedIndSize >= segmentValidPointNum){ // &#x5982;&#x679C;&#x5206;&#x5272;&#x7684;&#x70B9;&#x4E91;&#x6570;&#x91CF;&#x4E0D;&#x6EE1;&#x8DB3;30&#x4E14;&#x5927;&#x4E8E;&#x6700;&#x5C0F;&#x70B9;&#x6570;&#x8981;&#x6C42;&#xFF0C;&#x5219;&#x8FDB;&#x4E00;&#x6B65;&#x5206;&#x6790;
    int lineCount = 0;
    // &#x7EDF;&#x8BA1;&#x884C;&#x6570;
    for (size_t i = 0; i < N_SCAN; ++i)
        if (lineCountFlag[i] == true)
            ++lineCount;
    // &#x5982;&#x679C;&#x884C;&#x6570;&#x5927;&#x4E8E;3&#xFF0C;&#x5219;&#x8BA4;&#x4E3A;&#x4E5F;&#x662F;&#x6709;&#x6548;&#x7684;&#x70B9;&#x4E91;&#x5206;&#x5272;
    if (lineCount >= segmentValidLineNum)
        feasibleSegment = true;
}

获取分割结果之后,我们还需要作进一步的验证,如果所分割的点大于30个,则认为此次分割结果是有效的;如果分割的点数小于30 ,大于5,则要作进一步的分析;如果,分割的点所占据的行数大于3 ,也认为此次分割是有效的。 因为像树干这类目标,水平点的分布很少,但是竖直点的分布较多,激光雷达在垂直方向的分辨率又比较低,所以,这个地方的线束阈值设为3 。这样可以有效排除掉 树叶,树枝等特征不稳定的点云。

  • Step 4: 后续处理
// 4. &#x5982;&#x679C;&#x5206;&#x9694;&#x6709;&#x6548;, &#x5219;&#x66F4;&#x65B0;labelCount; &#x5982;&#x679C;&#x65E0;&#x6548;, &#x5219;&#x6807;&#x8BB0;&#x4E3A;&#x566A;&#x58F0;
// segment is valid, mark these points
if (feasibleSegment == true){ // &#x5982;&#x679C;&#x662F;&#x6709;&#x6548;&#x5206;&#x5272;, &#x5219;&#x66F4;&#x65B0;labelCount
    ++labelCount;
}else{ // segment is invalid, mark these points, &#x5982;&#x679C;&#x5206;&#x5272;&#x662F;&#x65E0;&#x6548;&#x7684;, &#x5219;&#x6807;&#x8BB0;&#x4E3A;&#x65E0;&#x6548;&#x6807;&#x7B7E;&#x503C;(&#x5F88;&#x5927;&#x7684;&#x6807;&#x7B7E;&#x503C;, &#x5373;&#x4F4D;&#x566A;&#x70B9;)
    for (size_t i = 0; i < allPushedIndSize; ++i){
        labelMat.at<int>(allPushedIndX[i], allPushedIndY[i]) = 999999; //
    }
}

</int>

如果分割结果是有效的,则累加 labelCount;如果分割结果是无效的,则把此处分割的点标记为噪点。

最终呈现出来的效果图如下:

代码实战 | 用LeGO-LOAM实现BFS点云聚类和噪点剔除

我们只勾选左侧的 Segmentation Pure,右侧就呈现出来了分割点云(不包括地面点)不同的聚类效果,不同的颜色代表不同的类别。

参考资料

  1. https://github.com/RobustFieldAutonomyLab/LeGO-LOAM
  2. 《Fast Range Image-based Segmentation of Sparse 3D Laser Scans for Online Operation》
  3. 基于LiDAR的多传感器融合SLAM:LOAM、LeGO-LOAM、LIO-SAM

独家重磅课程!

1、VIO课程:VIO最佳开源算法:ORB-SLAM3超全解析课程重磅升级!

2、图像三维重建课程(第2期):视觉几何三维重建教程(第2期):稠密重建,曲面重建,点云融合,纹理贴图

3、重磅来袭!基于LiDAR的多传感器融合SLAM 系列教程:LOAM、LeGO-LOAM、LIO-SAM

4、系统全面的相机标定课程:单目/鱼眼/双目/阵列 相机标定:原理与实战

5、视觉SLAM必备基础(第2期):视觉SLAM必学基础:ORB-SLAM2源码详解

6、深度学习三维重建课程:基于深度学习的三维重建学习路线

7、激光定位+建图课程:详解最常用的激光SLAM框架Cartographer(Google团队开源)

链接:代码实战 | 用LeGO-LOAM实现BFS点云聚类和噪点剔除

全国最棒的SLAM、三维视觉学习社区↓

链接:代码实战 | 用LeGO-LOAM实现BFS点云聚类和噪点剔除

技术交流微信群

欢迎加入公众号读者群一起和同行交流,目前有 SLAM、三维视觉、传感器、 自动驾驶、 计算摄影、检测、分割、识别、 医学影像、GAN*算法竞赛 等微信群,请添加微信号 chichui502 或扫描下方加群,备注:”名字/昵称+学校/公司+研究方向”。 请按照格式备注,否则不予通过 。添加成功后会根据研究方向邀请进入相关微信群。请勿在群内发送 广告*,否则会请出群,谢谢理解~

投稿、合作也欢迎联系:simiter@126.com

链接:代码实战 | 用LeGO-LOAM实现BFS点云聚类和噪点剔除

扫描关注视频号,看最新技术落地及开源方案视频秀 ↓
链接:代码实战 | 用LeGO-LOAM实现BFS点云聚类和噪点剔除

— 版权声明 —

本公众号原创内容版权属计算机视觉life所有;从公开渠道收集、整理及授权转载的非原创文字、图片和音视频资料,版权属原作者。如果侵权,请联系我们,会及时删除。

Original: https://blog.csdn.net/electech6/article/details/121432755
Author: 计算机视觉life
Title: 代码实战 | 用LeGO-LOAM实现BFS点云聚类和噪点剔除

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/550516/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球