点云分割与地面去除:从原始数据到结构化障碍物

点云分割,说白了就是把一堆乱七八糟的点,分成「地面」和「障碍物」两大阵营。这是自动驾驶感知里最基础、也最关键的预处理步骤。我刚开始做这行时,总觉得分割嘛,不就是设个阈值?后来踩了不少坑才明白——分割做不好,后面的跟踪、预测全是白搭。

一、地面去除:先把路找出来

为什么要先去除地面?你想想看,激光雷达扫到的点,一半以上都是地面。如果不先干掉地面,聚类时地面点和障碍物点会糊在一起,根本分不清谁是谁。

1. RANSAC平面拟合

RANSAC(随机采样一致性)是地面分割的经典方法。它的核心思想很简单:从点云中随机选三个点,拟合一个平面,然后看有多少点落在这个平面上。重复这个过程,找到点数最多的那个平面,就是地面。

// C++ 伪代码:RANSAC地面拟合
int bestInliers = 0;
Plane bestPlane;

for (int i = 0; i < maxIterations; i++) {
    // 随机选3个点
    Point p1 = points[rand() % N];
    Point p2 = points[rand() % N];
    Point p3 = points[rand() % N];
    
    // 计算平面方程 Ax + By + Cz + D = 0
    Plane plane = fitPlane(p1, p2, p3);
    
    // 统计内点数量
    int inliers = 0;
    for (auto& p : points) {
        if (distanceToPlane(p, plane) < threshold) {
            inliers++;
        }
    }
    
    if (inliers > bestInliers) {
        bestInliers = inliers;
        bestPlane = plane;
    }
}
我的经验:RANSAC的迭代次数别设太少。我一般设50-100次,阈值取0.1-0.2米。如果地面起伏大(比如坡道),阈值可以放宽到0.3米。但注意——阈值太大,会把低矮障碍物也当成地面。

2. 基于高度/角度的地面分割

RANSAC虽然好用,但有个毛病:计算量大。在嵌入式平台上跑,每帧点云几万个点,RANSAC可能撑不住。

这时候可以用基于高度和角度的方法。思路是这样的:

  • 把点云按水平角度分到不同的扇形区域
  • 在每个扇形内,按距离分bin
  • 对每个bin,找最低的几个点作为地面种子
  • 然后根据相邻bin的高度差,判断哪些点是地面

我曾在项目中用过这种方法。当时车上只有一块Jetson TX2,RANSAC跑一帧要30ms,换成高度角度法后降到5ms以内。嗯,代价是精度稍微差一点,但够用。

注意:高度法对坡度敏感。如果车在上下坡,地面高度会变化,需要动态调整参考高度。我曾经因为没处理这个,导致坡上的障碍物全被当成地面滤掉了——还好测试时发现了。

二、点云聚类:把障碍物一个个拎出来

地面去掉后,剩下的就是障碍物点。但这些点还是一团乱麻,需要聚类把它们分成一个个独立的目标。

1. 欧几里得聚类

这是最直观的方法:如果两个点之间的距离小于某个阈值,就认为它们属于同一个物体。

// PCL 中的欧几里得聚类
pcl::EuclideanClusterExtraction<pcl::PointXYZ> ec;
ec.setClusterTolerance(0.5);  // 距离阈值,单位米
ec.setMinClusterSize(100);    // 最小点数
ec.setMaxClusterSize(25000);  // 最大点数

std::vector<pcl::PointIndices> clusterIndices;
ec.extract(clusterIndices);

这个方法的优点是简单、快。但有个坑:阈值不好设。设小了,一个行人会被切成好几块;设大了,两辆车挨着停会被当成一个物体。

我的建议:阈值取0.3-0.5米比较稳妥。如果场景里行人多,可以适当调小。另外,别忘了根据距离动态调整阈值——远处的点稀疏,阈值应该大一些。

2. DBSCAN聚类

DBSCAN比欧几里得聚类更聪明。它不光看距离,还看密度。核心思想是:如果一个点周围有足够多的邻居,就把它当成核心点,然后不断扩张。

DBSCAN有两个参数:

  • eps:邻域半径
  • minPts:成为核心点所需的最少邻居数

我个人的体会是,DBSCAN在处理噪声和离群点方面比欧几里得聚类强很多。比如路边的树叶、小石子,DBSCAN能自动把它们归为噪声,不会形成一堆小碎块。

注意:DBSCAN的参数调起来比欧几里得聚类麻烦。eps和minPts相互影响,需要根据点云密度反复试。我一般先用欧几里得聚类跑一遍,看看大概效果,再用DBSCAN精调。

3. 区域生长分割

区域生长是另一种思路:从种子点开始,不断把「相似」的邻居点加入进来。这里的「相似」可以是距离、法向量夹角、曲率等。

这个方法特别适合分割大块物体,比如卡车、建筑物。因为区域生长会沿着表面平滑地扩张,不会像欧几里得聚类那样把大物体切碎。

// 区域生长分割伪代码
std::vector<Point> seeds = {initialPoint};
std::vector<Point> cluster;

while (!seeds.empty()) {
    Point current = seeds.back();
    seeds.pop_back();
    cluster.push_back(current);
    
    for (auto& neighbor : getNeighbors(current)) {
        if (!visited[neighbor] && 
            isSimilar(current, neighbor)) {
            visited[neighbor] = true;
            seeds.push_back(neighbor);
        }
    }
}
避坑指南:区域生长对种子点的选择很敏感。我曾经试过随机选种子,结果同一个物体被分成了好几块。后来改成选曲率最小的点作为种子,效果好了很多。

三、知识体系总览

下面这张图总结了本章的核心内容,从地面去除到聚类,每一步都有对应的算法和适用场景。

点云分割与地面去除知识体系 原始点云 地面去除 RANSAC平面拟合 高度/角度法 其他方法 点云聚类 欧几里得聚类 DBSCAN聚类 区域生长分割 结构化障碍物簇

四、实际项目中的选择建议

场景 地面去除方法 聚类方法 原因
高速公路 RANSAC 欧几里得聚类 地面平坦,障碍物稀疏,RANSAC精度高
城市道路 高度/角度法 DBSCAN 场景复杂,需要快速处理,DBSCAN抗噪好
停车场 RANSAC + 高度法融合 区域生长 车辆密集,需要精细分割
乡村土路 高度/角度法(动态阈值) 欧几里得聚类(大阈值) 地面起伏大,点云稀疏

核心要点:没有万能的方法。我每次做新项目,都会先拿几帧数据跑一遍所有方法,看看效果再选。别迷信某个算法,数据说了算。

好了,点云分割和地面去除就聊到这儿。这些方法看着简单,但真正调好参数、适配到实际场景,还是需要花些功夫的。记住一句话:分割的质量,决定了后续跟踪的天花板。

可可精品资料库