
一、A-LOAM 角点 / 平面点选取流程
A-LOAM 的特征提取继承了 LOAM 的经典做法,核心思想是:**一帧点云先按 scan ring,也就是激光线束编号分组,然后每一根 scan ring 再划分成 6 个 sector,在每个 sector 内独立计算曲率、排序、选角点和平面点。**这里重点是“每根线分 6 段”,不是“把所有线束分成 6 根”。比如 16 线雷达有 16 根 scan ring,那么每根线都分成 6 段,最后总共就是 16 × 6 个小区域,每个小区域都单独选特征。这样做的目的很明确:保证特征点在空间上分布均匀,避免角点和平面点都集中在某一块结构特别丰富的区域。
A-LOAM 会先计算每个点的曲率。曲率可以理解成:当前点和前后邻域点之间的几何变化程度。如果当前点在墙面、地面这类平滑区域,它和前后点变化不大,曲率就小,更像平面点;如果当前点在墙角、柱子边缘、物体边界处,它和前后点变化明显,曲率就大,更像角点。A-LOAM 计算曲率时通常会取当前点前后各 5 个点,也就是 10 个邻域点,和当前点做差。
关键代码可以简化成这样:
float diffX = cloud[i-5].x + … + cloud[i+5].x – 10 * cloud[i].x;
float diffY = cloud[i-5].y + … + cloud[i+5].y – 10 * cloud[i].y;
float diffZ = cloud[i-5].z + … + cloud[i+5].z – 10 * cloud[i].z;
cloudCurvature[i] = diffX * diffX + diffY * diffY + diffZ * diffZ;
cloudSortInd[i] = i;
cloudNeighborPicked[i] = 0;
cloudLabel[i] = 0;
这段代码里,cloudCurvature[i] 就是第 i 个点的曲率,数值越大,说明这个点越可能是边缘点;数值越小,说明这个点越可能是平面点。cloudNeighborPicked[i] 是邻域抑制标志,表示这个点或者它附近的点是否已经被选过,避免特征点扎堆。cloudLabel[i] 是类别标记,用来区分这个点后面是角点、平面点,还是普通点。
接着,A-LOAM 会对每根 scan ring 分 6 段。典型逻辑是这样的:
for (int i = 0; i < N_SCANS; i++)
{
for (int j = 0; j < 6; j++)
{
int sp = (scanStartInd[i] * (6 – j) + scanEndInd[i] * j) / 6;
int ep = (scanStartInd[i] * (5 – j) + scanEndInd[i] * (j + 1)) / 6 – 1;
// 在 sp 到 ep 这个 sector 内排序、选特征
}
}
这里的 N_SCANS 是雷达线数,比如 16、32、64;scanStartInd[i] 和 scanEndInd[i] 是第 i 根激光线在点云数组中的起止位置;sp 和 ep 就是当前 sector 的起点和终点。后面所有角点和平面点的选择,都是在这个 sp ~ ep 范围内独立完成的。
A-LOAM 的角点选择是从曲率大的点开始。每个 sector 内先按照曲率排序,然后从曲率最大的点往下遍历。如果点没有被邻域抑制,并且曲率大于阈值,就可以作为角点候选。它会进一步区分两类角点:sharp corner 和 less sharp corner。每个 sector 最多选 2 个 sharp corner,最多选 20 个 less sharp corner,并且 sharp corner 也会被放进 less sharp corner 里面。
关键代码可以概括成这样:
std::sort(cloudSortInd + sp, cloudSortInd + ep + 1, comp);
int largestPickedNum = 0;
for (int k = ep; k >= sp; k–)
{
int ind = cloudSortInd[k];
if (cloudNeighborPicked[ind] == 0 && cloudCurvature[ind] > 0.1)
{
largestPickedNum++;
if (largestPickedNum <= 2)
{
cloudLabel[ind] = 2;
cornerPointsSharp.push_back(cloud[ind]);
cornerPointsLessSharp.push_back(cloud[ind]);
}
else if (largestPickedNum <= 20)
{
cloudLabel[ind] = 1;
cornerPointsLessSharp.push_back(cloud[ind]);
}
cloudNeighborPicked[ind] = 1;
// 抑制当前点附近的点,避免重复选择
}
}
这里 largestPickedNum <= 2 对应最尖锐的角点,也就是 cornerPointsSharp;largestPickedNum <= 20 对应次尖锐角点,也就是 cornerPointsLessSharp。注意,前 2 个 sharp corner 也会被加入 cornerPointsLessSharp,所以不是两类完全互斥,而是 sharp corner 属于 less sharp corner 的子集。邻域抑制非常关键,因为激光点是连续排列的,如果一个边缘附近连续好几个点曲率都很大,不抑制的话,这些点会被重复选中,导致特征集中在局部,影响后续匹配稳定性。
A-LOAM 的平面点选择是从曲率小的点开始。每个 sector 中,曲率最小、没有被选过的点,会被作为平面点候选。每个 sector 最多选 4 个 flat surface,也就是特别平的平面点。除此之外,其他曲率较小、没有被选成角点的点,会作为 less flat surface,最后再做体素下采样。
关键代码可以简化成这样:
int smallestPickedNum = 0;
for (int k = sp; k <= ep; k++)
{
int ind = cloudSortInd[k];
if (cloudNeighborPicked[ind] == 0 && cloudCurvature[ind] < 0.1)
{
cloudLabel[ind] = -1;
surfPointsFlat.push_back(cloud[ind]);
smallestPickedNum++;
if (smallestPickedNum >= 4)
break;
cloudNeighborPicked[ind] = 1;
// 抑制邻域点
}
}
这段说明:A-LOAM 的真正 flat surface 每段最多只有 4 个,不是“曲率最小的 20 个都是平面点”。平面点数量少但质量高,适合用于前端稳定匹配。后面还有一类点叫 surfPointsLessFlat,它来自所有没有被选为角点的相对平滑点:
if (cloudLabel[k] <= 0)
{
surfPointsLessFlatScan.push_back(cloud[k]);
}
cloudLabel[k] <= 0 表示这个点不是角点,可能是 flat surface,也可能是普通较平滑点。因为这一类点数量很大,所以会做 voxel downsampling,也就是体素下采样,减少点数,同时尽量保留平面结构。
A-LOAM 小总结
A-LOAM 的特征提取可以理解成一个四分类特征筛选流程。它把每根 scan ring 分成 6 段,每段独立计算曲率并排序。角点从曲率大的点里选,每段最多 2 个 cornerPointsSharp,最多 20 个 cornerPointsLessSharp,并且 sharp corner 也包含在 less sharp corner 中;平面点从曲率小的点里选,每段最多 4 个 surfPointsFlat,其余较平滑的非角点作为 surfPointsLessFlat,最后做体素下采样。A-LOAM 的特点是分类非常清楚,输出四类特征,分别服务于不同阶段的 odometry 和 mapping。它不是简单粗暴地取“曲率最大前 20”和“曲率最小后 20”,而是结合了分段、排序、阈值判断、数量限制和邻域抑制。
二、LIO-SAM 角点 / 平面点选取流程
LIO-SAM 的特征提取和 A-LOAM 有相似之处,但它的前置处理更完整。LIO-SAM 在进入 featureExtraction.cpp 之前,会先经过 imageProjection.cpp。imageProjection 会完成点云去畸变、range image 投影、无效点过滤、点云按 ring 和列号组织等工作,然后把这些信息放进 cloudInfo 里。也就是说,LIO-SAM 的特征提取不是直接面对一堆无序点云,而是面对已经整理好的有序点云。
LIO-SAM 也会计算每个点的曲率,不过它通常用的是点的 range,也就是点到 LiDAR 的距离,而不是像 A-LOAM 那样直接用 x、y、z 三维坐标差。它会取当前点前后邻域的 range 值,计算这个点附近距离变化是否剧烈。距离变化越剧烈,smoothness / curvature 越大,越可能是角点;距离变化越平滑,smoothness 越小,越可能是平面点。
核心代码可以简化成这样:
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;
cloudSmoothness[i].value = cloudCurvature[i];
cloudSmoothness[i].ind = i;
cloudNeighborPicked[i] = 0;
cloudLabel[i] = 0;
这里的 cloudInfo.pointRange[i] 是第 i 个点的距离值。cloudSmoothness[i].value 存的是曲率 / 平滑度数值,cloudSmoothness[i].ind 存的是点索引,后面排序时会用。虽然名字叫 smoothness,但在选特征时依然可以理解成曲率指标:值大说明局部变化大,更像角点;值小说明局部变化小,更像平面。
LIO-SAM 比 A-LOAM 多了一个很重要的步骤:异常点标记。它会提前标记遮挡点、平行束异常点,以及已经被选过或邻域被抑制的点。这样做是因为有些点虽然曲率很大,但并不是真正稳定的边缘,比如遮挡边界处的距离突变。这种点如果被当成角点,后续 scan-to-map 匹配可能会变差。
关键逻辑可以简化成这样:
float depth1 = cloudInfo.pointRange[i];
float depth2 = cloudInfo.pointRange[i + 1];
if (abs(cloudInfo.pointColInd[i + 1] – cloudInfo.pointColInd[i]) < 10)
{
if (depth1 – depth2 > 0.3)
{
cloudNeighborPicked[i] = 1; // 遮挡附近点,不优先选
}
else if (depth2 – depth1 > 0.3)
{
cloudNeighborPicked[i + 1] = 1; // 遮挡附近点,不优先选
}
}
这段代码的意思是:如果相邻点在 range image 里的列号接近,但深度差突然很大,就说明这里可能存在遮挡边界。遮挡边界附近的点虽然曲率可能很大,但并不一定是稳定的几何边缘,所以会被标记为 picked 或不可优先选择。LIO-SAM 后面选角点和平面点时都会检查 cloudNeighborPicked[ind] == 0,只有没有被标记的点才有资格成为特征点。
接着,LIO-SAM 也会把每根 scan ring 分成 6 个 sector。这个思想和 A-LOAM 一样,都是为了保证特征分布均匀。代码形式大致如下:
for (int i = 0; i < N_SCAN; i++)
{
for (int j = 0; j < 6; j++)
{
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;
std::sort(cloudSmoothness.begin() + sp,
cloudSmoothness.begin() + ep,
by_value());
}
}
startRingIndex 和 endRingIndex 对应每根 scan ring 的起止索引,sp 和 ep 是每个 sector 的范围。排序以后,smoothness 小的点在前面,smoothness 大的点在后面。所以选角点时从后往前遍历,选平面点时从前往后遍历。
LIO-SAM 的角点选择逻辑比较直接:每个 sector 中,从 smoothness 最大的点开始遍历。如果这个点没有被标记为异常或已选,并且曲率大于 edgeThreshold,就可以作为 corner 特征。每个 sector 最多选 20 个角点,最后加入 cornerCloud。
关键代码可以概括成这样:
int largestPickedNum = 0;
for (int k = ep; k >= sp; k–)
{
int ind = cloudSmoothness[k].ind;
if (cloudNeighborPicked[ind] == 0 &&
cloudCurvature[ind] > edgeThreshold)
{
largestPickedNum++;
if (largestPickedNum <= 20)
{
cloudLabel[ind] = 1;
cornerCloud->push_back(extractedCloud->points[ind]);
}
else
{
break;
}
cloudNeighborPicked[ind] = 1;
// 抑制邻域点
}
}
这里和 A-LOAM 的明显区别是:LIO-SAM 没有再分 sharp corner 和 less sharp corner,而是统一放进 cornerCloud。它的角点筛选依赖几个条件:smoothness 足够大、超过 edgeThreshold、不是遮挡点、没有被 picked、所在 sector 还没有超过 20 个角点。这样选出来的角点数量适中,而且分布比较均匀。
LIO-SAM 的平面点选择从 smoothness 小的点开始。每个 sector 中,如果某个点没有被 picked,并且曲率小于 surfThreshold,就会被标记为平面点。常见逻辑是每段先标记少量最平的点,通常最多 4 个。
关键代码可以简化成这样:
int smallestPickedNum = 0;
for (int k = sp; k <= ep; k++)
{
int ind = cloudSmoothness[k].ind;
if (cloudNeighborPicked[ind] == 0 &&
cloudCurvature[ind] < surfThreshold)
{
cloudLabel[ind] = -1;
smallestPickedNum++;
if (smallestPickedNum >= 4)
break;
cloudNeighborPicked[ind] = 1;
// 抑制邻域点
}
}
但这里一定要注意:LIO-SAM 的 surfaceCloud 并不只是这 4 个最平点。后面它会把所有 cloudLabel <= 0 的点都加入 surface 候选:
if (cloudLabel[k] <= 0)
{
surfaceCloudScan->push_back(extractedCloud->points[k]);
}
这句代码非常关键。cloudLabel[k] <= 0 表示这个点不是角点。它可能是刚才标记出的非常平的点,也可能是普通的非角点、较平滑点。也就是说,LIO-SAM 的 surfaceCloud 实际上是一个较大的平面候选集合,不是只有曲率最小的几个点。最后还会对 surfaceCloud 做体素下采样:
downSizeFilter.setInputCloud(surfaceCloudScan);
downSizeFilter.filter(surfaceCloudScanDS);
*surfaceCloud += *surfaceCloudScanDS;
这样可以降低平面点数量,减少后续 scan-to-map 匹配的计算量,同时保留主要平面结构。
LIO-SAM 小总结
LIO-SAM 的特征提取可以理解成一个基于有序点云、异常点剔除和两类特征输出的筛选流程。它先通过 imageProjection 得到 cloudInfo、range image 等辅助信息,再用 range 差计算每个点的 smoothness。然后它会标记遮挡点、平行束异常点和 picked 点,避免这些不稳定点进入特征集合。之后每根 scan ring 仍然分成 6 个 sector,每段独立排序。角点从 smoothness 大的点中选,每段最多 20 个,输出到 cornerCloud;平面点从 smoothness 小的点中选,先标记少量最平点,再把大量非角点和较平滑点加入 surfaceCloud,最后做体素下采样。LIO-SAM 不像 A-LOAM 那样强调四类点云输出,而是更关注 cornerCloud 和 surfaceCloud 两类结果,并且异常点剔除更加明确。
三、最终总总结
A-LOAM 和 LIO-SAM 都继承了 LOAM 的核心思想:按 scan ring 组织点云、每根线分 6 个 sector、计算曲率 / smoothness、按排序选择角点和平面点、通过邻域抑制避免特征扎堆。二者都不是简单地“取曲率最大的前 20 个作为角点、取曲率最小的倒数 20 个作为平面点”,而是有分段、阈值、数量限制和邻域抑制。
区别在于,A-LOAM 更强调四类特征的明确划分:cornerPointsSharp、cornerPointsLessSharp、surfPointsFlat、surfPointsLessFlat。它每段最多选 2 个 sharp corner、20 个 less sharp corner、4 个 flat surface,其余较平滑点作为 less flat surface 并下采样。这个流程更接近经典 LOAM 的四类特征设计。
而 LIO-SAM 更强调工程上的稳定性和两类输出:cornerCloud 和 surfaceCloud。它先依赖 imageProjection 得到有序点云和 range image 信息,然后计算 smoothness,先剔除遮挡点、平行束异常点和 picked 点,再选 corner 和 surface。LIO-SAM 的 surfaceCloud 不是少量最平点,而是大量非角点 / 较平滑点经过体素下采样后的结果。简单记就是:A-LOAM 是“四类特征筛选”,LIO-SAM 是“异常点剔除后的 corner / surface 两类特征筛选”。
版权声明: 辛苦码字不易,转载请注明原文出处和作者信息,谢谢理解!
欢迎分享与交流,但拒绝任何形式的商业转载或洗稿。



