3. 点云分割与聚类:平面分割(RANSAC)、欧几里得聚类(Euclidean Clustering)、DBSCAN聚类、区域生长分割

点云分割与聚类,说白了就是把一堆杂乱无章的点,按照它们的空间关系或几何特征,分成有意义的几块。我刚开始做自动驾驶感知时,总觉得这一步可有可无——反正后面有目标检测嘛。直到有一次,我们的车在停车场里把一辆黑色轿车和它后面的墙识别成了一个物体……嗯,从那以后我再也不敢跳过分割这一步了。

这一章,我们聊聊四种最常用的方法:RANSAC平面分割、欧几里得聚类、DBSCAN聚类,以及区域生长分割。每种方法都有它的脾气,选对了事半功倍。

点云分割与聚类方法总览 原始点云 RANSAC 平面分割 欧几里得聚类 距离阈值 DBSCAN 密度聚类 区域生长 法向量+曲率 地面/墙面提取 任意形状聚类 物体精细分割

3.1 RANSAC平面分割——老司机的最爱

RANSAC,全称是Random Sample Consensus。名字挺唬人,其实核心思想很简单:从一堆点里,随机挑几个点拟合一个模型,然后看看有多少点支持这个模型。反复试,找到支持者最多的那个模型。

我个人习惯用它来提取地面。为什么?因为地面在激光雷达点云里,通常是一个大平面。RANSAC对这种「多数派」结构特别敏感。

核心步骤:

  1. 随机选3个点(平面最少需要3个点)
  2. 计算这3个点确定的平面方程
  3. 计算所有点到这个平面的距离
  4. 距离小于阈值的点,算作「内点」
  5. 重复1-4步N次,保留内点最多的平面
// PCL中的RANSAC平面分割示例
pcl::SACSegmentation<pcl::PointXYZ> seg;
seg.setModelType(pcl::SACMODEL_PLANE);
seg.setMethodType(pcl::SAC_RANSAC);
seg.setDistanceThreshold(0.1);  // 单位:米
seg.setMaxIterations(1000);
seg.setInputCloud(cloud);
seg.segment(*inliers, *coefficients);

我的经验:距离阈值别设太大。0.1米对于16线激光雷达刚刚好。设大了会把路沿、小坡都算成地面,设小了地面又提取不干净。我曾经在一条坑洼路面上吃过亏——阈值设成0.05米,结果地面被分成了好几块。

注意:RANSAC不是万能的。如果场景里平面特别多(比如建筑工地),它可能会把墙面误认为地面。我建议先做一次粗略的滤波,把高度明显不对的点去掉。

3.2 欧几里得聚类——最直观的分组方式

欧几里得聚类,说白了就是「挨个找邻居」。从一个点出发,把距离小于某个阈值的点都拉进来,然后继续找这些点的邻居……直到没有新点加入为止。

你想想看,这像不像「朋友圈」扩散?一个人拉他的朋友,朋友再拉他们的朋友。最后形成一个圈子。

// PCL欧几里得聚类
pcl::EuclideanClusterExtraction<pcl::PointXYZ> ec;
ec.setClusterTolerance(0.5);      // 邻居搜索半径
ec.setMinClusterSize(100);        // 最少点数
ec.setMaxClusterSize(25000);      // 最多点数
ec.setSearchMethod(tree);
ec.setInputCloud(cloud);
ec.extract(cluster_indices);

这里有个关键参数:聚类容差(Cluster Tolerance)。它决定了「多远算邻居」。设小了,一个物体会被切成好几块;设大了,两个物体又粘在一起。

场景 推荐容差 说明
高速公路 0.5 - 1.0 m 车辆间距大,容差可以大一些
城市道路 0.3 - 0.5 m 行人、自行车密集,需要精细
停车场 0.2 - 0.4 m 车辆紧挨着,容差要小

避坑指南:我曾经在停车场场景里,把容差设成了0.8米。结果两辆并排停着的车被识别成了一个「大胖子」物体。后来我改成0.3米,问题就解决了。记住:宁可过分割,也不要欠分割。过分割还能用后处理合并,欠分割就彻底没救了。

3.3 DBSCAN聚类——密度说了算

DBSCAN和欧几里得聚类有点像,但思路更巧妙。它不只看距离,还看密度。一个点周围至少有MinPts个点,才算「核心点」。核心点之间连起来,就形成了簇。

为什么需要DBSCAN?因为欧几里得聚类有个毛病:它假设所有簇的密度是一样的。但实际点云不是这样——远处的物体点很稀疏,近处的点很密集。DBSCAN能自适应地处理这种情况。

DBSCAN的两个关键参数:

  • Eps(邻域半径):和欧几里得聚类的容差类似
  • MinPts(最小点数):一个点周围至少要有多少个点,才算核心点
// 伪代码:DBSCAN核心逻辑
for each point p in cloud:
    if p is already classified: continue
    neighbors = points within Eps of p
    if len(neighbors) < MinPts:
        mark p as noise
    else:
        create new cluster C
        expand_cluster(p, neighbors, C, Eps, MinPts)

我个人习惯把MinPts设成10-20。太小了会把噪声也当成簇,太大了又会漏掉小物体。Eps的话,我一般先试0.5米,然后根据结果微调。

注意:DBSCAN的计算量比欧几里得聚类大不少。如果你处理的是64线激光雷达的实时数据,建议用欧几里得聚类。DBSCAN更适合离线处理或者对精度要求高的场景。

3.4 区域生长分割——从种子开始长

区域生长分割的思路,和前面几种都不一样。它不靠距离,而是靠法向量和曲率。先选几个「种子点」,然后看周围的点法向量和种子点是否接近。接近就归为一类,继续长。

这个方法特别适合分割曲面物体,比如车顶、树干、路牌。因为它的判断依据是几何特征,而不是空间距离。

// PCL区域生长分割
pcl::RegionGrowing<pcl::PointXYZ, pcl::Normal> rg;
rg.setMinClusterSize(100);
rg.setMaxClusterSize(100000);
rg.setNumberOfNeighbours(30);       // 邻居数量
rg.setSmoothnessThreshold(3.0);     // 法向量角度差阈值(度)
rg.setCurvatureThreshold(1.0);      // 曲率阈值
rg.setInputCloud(cloud);
rg.setInputNormals(normals);
rg.extract(clusters);

这里有两个关键阈值:

  • SmoothnessThreshold:法向量角度差。设成3度,意味着法向量夹角小于3度的点才能长到一起。适合分割平滑曲面。
  • CurvatureThreshold:曲率阈值。曲率大的地方(比如边缘)会停止生长。

我的经验:区域生长分割对法向量计算质量非常敏感。我建议先对点云做一次体素滤波,把点云均匀化。否则,近处点密、远处点稀,法向量算出来会很不稳定。有一次我在一个稀疏的树干上做区域生长,结果法向量全乱了,分割出来的形状像狗啃的一样。

3.5 四种方法怎么选?

嗯,到这里四种方法都讲完了。你可能会问:到底用哪个?我个人的经验是这样的:

  • 提取地面/墙面:首选RANSAC。快、稳、准。
  • 分割独立物体(车辆、行人):欧几里得聚类。简单高效,实时性好。
  • 密度不均匀的场景:DBSCAN。能自适应处理稀疏和密集区域。
  • 精细分割曲面物体:区域生长。利用几何特征,分割更准确。

当然,实际项目中往往是组合使用。比如先RANSAC去掉地面,再用欧几里得聚类分割物体。或者先欧几里得粗分,再用区域生长细分。灵活搭配才是王道。

好了,这一章的内容就到这里。记住:没有最好的方法,只有最合适的方法。多试、多调参,你慢慢就会找到感觉。