A-LOAM 与 LeGO-LOAM 特征提取前处理差异分析
A-LOAM 和 LeGO-LOAM 在前端特征提取时,最终目标都是从一帧 LiDAR 点云中提取两类特征:边缘点 edge/corner feature 和 平面点 surface/planar feature。二者的基本依据都是局部曲率:曲率大的点通常表示周围几何变化剧烈,更可能是边缘点;曲率小的点通常表示周围比较平滑,更可能是平面点。但是在真正进行曲率排序和特征点选择之前,A-LOAM 和 LeGO-LOAM 对点云的预处理方式差异很明显。
简单来说,A-LOAM 更偏向于点级别的局部筛选,它主要在 scan line 上根据前后邻近点的距离变化,剔除遮挡点、距离突变点和不稳定点;而 LeGO-LOAM 更偏向于区域级别的结构筛选,它会先将点云投影成 range image,再进行地面检测和非地面连通域分割,先保留稳定区域,再从稳定点云中提取边缘点和平面点。

1. 二者共同点:最终都依赖曲率选特征
无论是 A-LOAM 还是 LeGO-LOAM,后续选边缘点和平面点时,都需要计算点的局部曲率。常见做法是取当前点前后若干个邻近点,比较当前点和邻域点之间的坐标变化。如果当前点相对于周围点变化很大,说明它可能处在边缘、角点、物体轮廓或者遮挡边界附近;如果变化很小,则说明它可能位于平滑平面上。
典型曲率计算逻辑可以简化理解为:
float diffX = cloud[i – 5].x + cloud[i – 4].x + cloud[i – 3].x + cloud[i – 2].x + cloud[i – 1].x
– 10 * cloud[i].x
+ cloud[i + 1].x + cloud[i + 2].x + cloud[i + 3].x + cloud[i + 4].x + cloud[i + 5].x;
float diffY = cloud[i – 5].y + cloud[i – 4].y + cloud[i – 3].y + cloud[i – 2].y + cloud[i – 1].y
– 10 * cloud[i].y
+ cloud[i + 1].y + cloud[i + 2].y + cloud[i + 3].y + cloud[i + 4].y + cloud[i + 5].y;
float diffZ = cloud[i – 5].z + cloud[i – 4].z + cloud[i – 3].z + cloud[i – 2].z + cloud[i – 1].z
– 10 * cloud[i].z
+ cloud[i + 1].z + cloud[i + 2].z + cloud[i + 3].z + cloud[i + 4].z + cloud[i + 5].z;
cloudCurvature[i] = diffX * diffX + diffY * diffY + diffZ * diffZ;
这里的核心思想是:如果当前点和前后邻域点差异很大,cloudCurvature[i] 就会比较大,后续更可能被选为边缘点;如果当前点和周围点变化平滑,曲率就比较小,后续更可能被选为平面点。
但是问题在于:曲率大不一定真的代表稳定边缘,也可能是遮挡、噪声、离群点造成的假边缘。 所以在曲率排序之前,必须先做一定的坏点过滤。A-LOAM 和 LeGO-LOAM 的差异主要就体现在这里。
2. A-LOAM:点级别局部筛选
A-LOAM 的处理方式比较直接。它会先按照 scan line 整理点云,然后计算曲率,再在选特征点之前标记一些不可靠点。典型的不可靠点包括:遮挡边界点、距离突变点、与前后邻居差异过大的点。
例如两个点虽然在点云数组中相邻,但一个点打在近处柱子上,另一个点打在远处墙面上,那么它们在 scan line 上是邻近点,但在真实三维空间中并不连续。如果这种点不提前过滤,它们会因为距离突变导致曲率很大,从而被误选为边缘点。
A-LOAM 中常见的遮挡点标记逻辑可以概括为:
float depth1 = sqrt(cloud[i].x * cloud[i].x +
cloud[i].y * cloud[i].y +
cloud[i].z * cloud[i].z);
float depth2 = sqrt(cloud[i + 1].x * cloud[i + 1].x +
cloud[i + 1].y * cloud[i + 1].y +
cloud[i + 1].z * cloud[i + 1].z);
int columnDiff = std::abs(int(cloud[i + 1].intensity) – int(cloud[i].intensity));
if (columnDiff < 10)
{
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;
}
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;
}
}
这段逻辑的作用是:如果两个相邻点的深度差突然变大,就说明这里可能是遮挡边界。A-LOAM 会把当前点附近的一小段邻域标记为 cloudNeighborPicked = 1,表示这些点后续不适合参与特征提取。
A-LOAM 还会检查当前点与前后点的距离差,如果当前点和两侧邻居都不连续,也会将其标记为不可靠点:
float diff1 = (cloud[i].x – cloud[i – 1].x) * (cloud[i].x – cloud[i – 1].x)
+ (cloud[i].y – cloud[i – 1].y) * (cloud[i].y – cloud[i – 1].y)
+ (cloud[i].z – cloud[i – 1].z) * (cloud[i].z – cloud[i – 1].z);
float diff2 = (cloud[i].x – cloud[i + 1].x) * (cloud[i].x – cloud[i + 1].x)
+ (cloud[i].y – cloud[i + 1].y) * (cloud[i].y – cloud[i + 1].y)
+ (cloud[i].z – cloud[i + 1].z) * (cloud[i].z – cloud[i + 1].z);
float dis = cloud[i].x * cloud[i].x +
cloud[i].y * cloud[i].y +
cloud[i].z * cloud[i].z;
if (diff1 > 0.0002 * dis && diff2 > 0.0002 * dis)
{
cloudNeighborPicked[i] = 1;
}
这说明 A-LOAM 并不是完全直接从原始点云中选特征,它也会提前过滤明显不稳定的点。但它的处理粒度主要是点级别和局部邻域级别。它关注的是“这个点附近是否有明显距离突变”“这个点是否处在遮挡边界附近”,而不是判断一整片点云区域是不是稳定结构。
3. A-LOAM 后续如何选边缘点和平面点
在标记完不可靠点之后,A-LOAM 会按 scan line 分段,每段内部根据曲率排序。曲率大的点会被选为边缘点,曲率小的点会被选为平面点。
简化逻辑如下:
for (int i = 0; i < N_SCANS; i++)
{
for (int j = 0; j < 6; j++)
{
int sp = scanStartInd[i] + (scanEndInd[i] – scanStartInd[i]) * j / 6;
int ep = scanStartInd[i] + (scanEndInd[i] – scanStartInd[i]) * (j + 1) / 6 – 1;
std::sort(cloudSmoothness.begin() + sp,
cloudSmoothness.begin() + ep,
by_value());
}
}
每根 scan line 分成 6 段的目的,是让特征点分布更加均匀。否则如果直接对整帧点云排序,边缘点和平面点可能集中在某一个局部区域,导致后续匹配约束分布不均。
边缘点选择逻辑可以概括为:
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 <= 2)
{
cloudLabel[ind] = 2; // sharp corner
cornerPointsSharp.push_back(cloud[ind]);
cornerPointsLessSharp.push_back(cloud[ind]);
}
else if (largestPickedNum <= 20)
{
cloudLabel[ind] = 1; // less sharp corner
cornerPointsLessSharp.push_back(cloud[ind]);
}
else
{
break;
}
cloudNeighborPicked[ind] = 1;
}
}
平面点选择逻辑可以概括为:
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; // flat surface
surfPointsFlat.push_back(cloud[ind]);
smallestPickedNum++;
if (smallestPickedNum >= 4)
break;
cloudNeighborPicked[ind] = 1;
}
}
因此,A-LOAM 的整体逻辑可以总结为:先在 scan line 上剔除遮挡点和局部不稳定点,再把每根线束分成若干段,在每段内按曲率选择边缘点和平面点。
4. LeGO-LOAM:区域级结构筛选
LeGO-LOAM 的处理更加结构化。它不是直接在 scan line 上做局部坏点剔除,而是先将点云投影成二维 range image。这个 range image 中,行表示激光线束编号,列表示水平角索引。
range image:
col 0 col 1 col 2 col 3 …
row 0 p p p p
row 1 p p p p
row 2 p p p p
…
row N p p p p
有了这个二维结构之后,LeGO-LOAM 可以做两件 A-LOAM 中没有明确做的事情:
第一,基于低线束之间的几何关系检测地面点;
第二,对非地面点做连通域分割,判断哪些点属于稳定连续结构。
地面检测逻辑大致如下:
for (int j = 0; j < Horizon_SCAN; ++j)
{
for (int i = 0; i < groundScanInd; ++i)
{
lowerInd = j + i * Horizon_SCAN;
upperInd = j + (i + 1) * Horizon_SCAN;
lowerPoint = fullCloud->points[lowerInd];
upperPoint = fullCloud->points[upperInd];
diffX = upperPoint.x – lowerPoint.x;
diffY = upperPoint.y – lowerPoint.y;
diffZ = upperPoint.z – lowerPoint.z;
angle = atan2(diffZ, sqrt(diffX * diffX + diffY * diffY)) * 180 / M_PI;
if (abs(angle – sensorMountAngle) <= 10)
{
groundMat.at<int8_t>(i, j) = 1;
groundMat.at<int8_t>(i + 1, j) = 1;
}
}
}
这一步的核心是:在同一列中比较上下相邻线束的点,如果它们之间的连线角度接近雷达安装角,就认为这两个点可能位于同一个地面平面上。地面点会被标记出来,后续不参与普通非地面连通域分割,但仍然可能作为平面特征参与优化。
接着,LeGO-LOAM 会对非地面点做连通域分割。它会从 labelMat = 0 的未处理点开始,检查上下左右邻居,如果邻居点和当前点在几何上连续,就把它们归为同一个 segment。
连续性判断的核心逻辑可以概括为:
float d1 = std::max(rangeMat.at<float>(fromIndX, fromIndY),
rangeMat.at<float>(thisIndX, thisIndY));
float d2 = std::min(rangeMat.at<float>(fromIndX, fromIndY),
rangeMat.at<float>(thisIndX, thisIndY));
float alpha = segmentAlphaX; // 左右邻居使用水平角分辨率
// float alpha = segmentAlphaY; // 上下邻居使用垂直角分辨率
float angle = atan2(d2 * sin(alpha), d1 – d2 * cos(alpha));
if (angle > segmentTheta)
{
labelMat.at<int>(thisIndX, thisIndY) = labelCount;
queueIndX[queueSize] = thisIndX;
queueIndY[queueSize] = thisIndY;
queueSize++;
}
这段代码的含义是:如果两个相邻点的距离变化比较平滑,计算出来的 angle 会比较大,说明它们更可能属于同一个连续表面,于是被放入同一个 segment;如果距离突变明显,说明它们可能位于遮挡边界或不同物体上,就不把它们连在一起。
LeGO-LOAM 在完成一个 segment 的生长后,还会判断这个 segment 是否有效。典型判断包括:点数是否足够多,是否跨越足够多的扫描线。如果一个 segment 点数太少,或者只出现在很局部的位置,就可能被认为是小簇噪声或离群结构。
bool feasibleSegment = false;
if (allPushedIndSize >= 30)
{
feasibleSegment = true;
}
else if (allPushedIndSize >= segmentValidPointNum)
{
if (lineCount >= segmentValidLineNum)
feasibleSegment = true;
}
if (feasibleSegment == true)
{
labelCount++;
}
else
{
for (size_t i = 0; i < allPushedIndSize; ++i)
{
labelMat.at<int>(allPushedIndX[i], allPushedIndY[i]) = 999999;
}
}
这一步就是 LeGO-LOAM 和 A-LOAM 的一个关键区别:
A-LOAM 主要判断某个点附近是否不稳定;LeGO-LOAM 则判断一整片点云区域是否是可靠结构。
5. LeGO-LOAM 后续如何选边缘点和平面点
经过地面检测和连通域分割之后,LeGO-LOAM 会生成 segmentedCloud。这个点云不是原始点云,而是经过筛选后的点云,里面主要包含有效非地面 segment 点和部分地面点,不可靠小簇和离群点已经被过滤掉。
后续 LeGO-LOAM 也会按照 scan line 分成若干小段,例如每根线束分成 6 段,然后在每一段中按曲率排序,选择边缘点和平面点。
for (int i = 0; i < N_SCAN; i++)
{
for (int j = 0; j < 6; j++)
{
int sp = segmentedCloudScanStartInd[i] +
(segmentedCloudScanEndInd[i] – segmentedCloudScanStartInd[i]) * j / 6;
int ep = segmentedCloudScanStartInd[i] +
(segmentedCloudScanEndInd[i] – segmentedCloudScanStartInd[i]) * (j + 1) / 6 – 1;
std::sort(cloudSmoothness.begin() + sp,
cloudSmoothness.begin() + ep,
by_value());
}
}
这里需要注意:LeGO-LOAM 的“6 段”不是把某个平面分成 6 块,而是把每根 scan line 在水平方向上的点序列按索引范围分成 6 个子区间。然后在每个子区间中,从 segmentedCloud 的稳定有效点里根据曲率大小选择特征。
边缘点通常要求曲率大,并且一般不是地面点:
if (cloudNeighborPicked[ind] == 0 &&
cloudCurvature[ind] > edgeThreshold &&
segInfo.segmentedCloudGroundFlag[ind] == false)
{
cloudLabel[ind] = 1;
cornerPointsSharp.push_back(segmentedCloud->points[ind]);
}
平面点通常要求曲率小,地面点也可以作为平面点候选:
if (cloudNeighborPicked[ind] == 0 &&
cloudCurvature[ind] < surfThreshold)
{
cloudLabel[ind] = -1;
surfPointsFlat.push_back(segmentedCloud->points[ind]);
}
因此,LeGO-LOAM 的特征提取可以理解为:先通过地面检测和区域分割得到更干净的 segmentedCloud,再在 segmentedCloud 中按 scan line 和 6 个子区间进行曲率排序,最后选取边缘点和平面点。
6. 二者核心区别
A-LOAM 和 LeGO-LOAM 在特征提取前的处理可以总结为两种不同思路。
A-LOAM 是“局部点级筛选”。它直接在 scan line 上检查相邻点之间的距离变化,把遮挡边界、深度突变、不稳定邻域点标记掉。它不会显式判断一整片点云区域是不是稳定结构,也不会专门进行地面检测。它的优点是实现简单、通用性强,更接近原始 LOAM 的思路;缺点是在低线束雷达和复杂地面场景下,对小簇噪声、地面结构和非地面结构的区分不如 LeGO-LOAM 细。
LeGO-LOAM 是“区域级结构筛选”。它先将点云投影成 range image,然后利用低线束之间的几何关系检测地面点,再对非地面点进行连通域分割,保留稳定 segment,过滤小簇、噪声和离群点。这样进入曲率排序的 segmentedCloud 已经比原始点云干净很多,后续选出来的边缘点和平面点通常更稳定。尤其对于地面移动机器人和低线束 LiDAR,LeGO-LOAM 的地面约束和区域分割能有效提高特征提取质量。
7. 总结
A-LOAM 和 LeGO-LOAM 的特征提取最终都依赖曲率排序,但两者在曲率排序之前的处理粒度不同。A-LOAM 更关注局部点的可靠性,它通过深度差、遮挡关系和邻域距离变化,提前标记不适合作为特征的点,然后在 scan line 的分段中选择曲率大的边缘点和曲率小的平面点。它属于典型的点级别过滤方法,结构简洁,适合理解 LOAM 的基本特征提取思想。
LeGO-LOAM 则更强调结构层面的稳定性。它不是直接从 scan line 中选特征,而是先通过 range image 建立二维邻接关系,再进行地面检测和非地面连通域分割。地面点被单独标记,非地面点被分割成一个个 segment,小簇和离群点被过滤,最后再从 segmentedCloud 中按 scan line 和 6 个子区间提取边缘点和平面点。因此,LeGO-LOAM 的特征候选点经过了更强的结构筛选,尤其适合低线束激光雷达和地面车场景。
一句话概括:A-LOAM 是先剔除明显坏点,再按曲率选特征;LeGO-LOAM 是先找地面和稳定区域,再在稳定点云中按曲率选特征。A-LOAM 的处理粒度是点级别,LeGO-LOAM 的处理粒度是区域级别。
版权声明: 辛苦码字不易,转载请注明原文出处和作者信息,谢谢理解!
欢迎分享与交流,但拒绝任何形式的商业转载或洗稿。

![[特殊字符]DeepSeek‑Harness(DSH)小白保姆教程-171主机测评](https://www.171host.com/wp-content/uploads/2026/08/20260816085112-6a817a009aabf-220x150.png)
