第四章:点云分割基础

点云分割,说白了就是把一堆乱七八糟的点,按规则分成几堆。我刚开始接触这玩意儿的时候,觉得不就是分类嘛,后来才发现——这里面的门道可不少。

今天咱们聊四种最常用的分割方法:RANSAC平面分割、欧几里得聚类、区域生长、还有模型分割。每种方法都有它的脾气,选对了事半功倍,选错了...嗯,你懂的。

点云分割方法体系 点云分割 RANSAC 平面分割 随机采样 + 迭代拟合 欧几里得聚类分割 距离阈值 + 连通域 区域生长分割 种子点 + 生长准则 模型分割 圆柱 / 球体拟合 地面/墙面提取 物体实例分割 平滑曲面分割 工业零件检测

4.1 RANSAC 平面分割

RANSAC,全称 Random Sample Consensus。名字挺唬人,其实核心思想就一句话:从乱糟糟的点里,找到那个最符合规律的面

我当年第一次用RANSAC做地面分割,心想这玩意儿能靠谱吗?结果跑完一看,地面点被干干净净地拎出来了,连路沿石都分得清清楚楚。那一刻我服了。

4.1.1 基本原理

RANSAC的套路是这样的:

  1. 随机选三个点,确定一个平面
  2. 算算其他点到这个平面的距离
  3. 距离小于阈值的,算作"内点"
  4. 重复以上步骤N次,选内点最多的那个平面

说白了就是"瞎蒙+验证",蒙的次数多了,总能蒙对。

关键参数:

  • 距离阈值:决定哪些点算"内点"。设太小,平面不完整;设太大,会把噪声包进来。我一般先设0.1米试试。
  • 迭代次数:越多越准,但越慢。通常1000次就够用了。
  • 最小内点数:低于这个数,就认为没找到平面。

4.1.2 代码实战

用PCL实现RANSAC平面分割,代码其实不长:

// PCL 平面分割示例
pcl::SACSegmentation<pcl::PointXYZ> seg;
pcl::PointIndices::Ptr inliers(new pcl::PointIndices);
pcl::ModelCoefficients::Ptr coefficients(new pcl::ModelCoefficients);

seg.setOptimizeCoefficients(true);
seg.setModelType(pcl::SACMODEL_PLANE);
seg.setMethodType(pcl::SAC_RANSAC);
seg.setDistanceThreshold(0.1);  // 距离阈值
seg.setMaxIterations(1000);     // 迭代次数
seg.setInputCloud(cloud);
seg.segment(*inliers, *coefficients);

// 提取平面点
pcl::ExtractIndices<pcl::PointXYZ> extract;
extract.setInputCloud(cloud);
extract.setIndices(inliers);
extract.setNegative(false);
extract.filter(*plane_cloud);

我的小技巧: 如果场景里有多个平面,可以循环执行RANSAC——每次提取一个平面,把内点移除,再对剩余点继续跑。这样就能把地面、墙面、桌面一个个拎出来。

4.2 欧几里得聚类分割

欧几里得聚类,名字听着高大上,其实就是按距离分堆。两个点离得近,就归为一类;离得远,就各玩各的。

这方法特别适合做物体检测。比如自动驾驶里,把激光雷达扫到的点云,按距离聚类,就能分出哪堆是车、哪堆是行人、哪堆是树。

4.2.1 算法流程

步骤很简单:

  1. 建一棵KD-Tree,方便快速找近邻
  2. 随便选一个点,找它半径r内的所有邻居
  3. 对每个邻居,继续找它的邻居
  4. 直到找不到新邻居了,这一堆就算一个聚类
  5. 换下一个没被访问的点,重复以上步骤

这里有个坑——半径r怎么选? 设太小,一个物体会被切成好几块;设太大,两个物体又粘在一起。我一般先看点云密度,密度大的设小点,密度小的设大点。

避坑指南: 我曾经在一个项目里,把聚类半径设成0.5米,结果一辆自行车被分成了三块——车把、车身、后轮各成一堆。后来改成0.3米,才把自行车完整地拎出来。所以,半径一定要根据物体尺寸来调

4.2.2 代码示例

// PCL 欧几里得聚类
pcl::search::KdTree<pcl::PointXYZ>::Ptr tree(new pcl::search::KdTree<pcl::PointXYZ>);
tree->setInputCloud(cloud_filtered);

std::vector<pcl::PointIndices> cluster_indices;
pcl::EuclideanClusterExtraction<pcl::PointXYZ> ec;
ec.setClusterTolerance(0.3);      // 聚类半径
ec.setMinClusterSize(100);        // 最小点数
ec.setMaxClusterSize(25000);      // 最大点数
ec.setSearchMethod(tree);
ec.setInputCloud(cloud_filtered);
ec.extract(cluster_indices);

// 遍历每个聚类
for (const auto& indices : cluster_indices) {
    pcl::PointCloud<pcl::PointXYZ>::Ptr cluster(new pcl::PointCloud<pcl::PointXYZ>);
    for (const auto& idx : indices.indices) {
        cluster->push_back((*cloud_filtered)[idx]);
    }
    // 处理每个聚类...
}

4.3 区域生长分割

区域生长,思路跟欧几里得聚类有点像,但多了一个条件——不光看距离,还要看法向量和曲率

你想想看,一个平面上的点,法向量应该差不多。如果两个点虽然离得近,但法向量差很大,那它们大概率不在同一个面上。

4.3.1 生长准则

区域生长需要两个条件同时满足:

  • 距离条件:点与种子点的距离小于阈值
  • 法向量条件:点与种子点的法向量夹角小于阈值

我习惯先算曲率,曲率小的点优先当种子。因为曲率小的地方比较"平",生长起来不容易跑偏。

参数调优建议:

参数 作用 典型值
法向量夹角阈值 控制生长方向一致性 15° ~ 30°
曲率阈值 控制曲面平滑度 0.05 ~ 0.1
邻域搜索半径 控制生长范围 0.1 ~ 0.5 m

4.3.2 适用场景

区域生长特别适合做室内场景分割。比如墙面、天花板、地板,这些面都是平滑的,法向量变化小,用区域生长一抓一个准。

但如果是室外场景,树啊、灌木啊这些乱七八糟的东西,法向量乱飞,区域生长就容易翻车。这时候我宁愿用欧几里得聚类。

4.4 基于模型的圆柱/球体分割

最后聊一个"高级"玩法——用数学模型去拟合点云

比如你有一堆点,看起来像个圆柱。那就可以用RANSAC的思路,只不过这次不是拟合平面,而是拟合圆柱。同理,球体、圆锥、圆环都能拟合。

4.4.1 圆柱拟合

圆柱有7个参数:轴方向(3个)、轴上一点(3个)、半径(1个)。所以最少需要7个点才能确定一个圆柱。

实际用RANSAC拟合时,流程跟平面分割一模一样,只是把模型类型从SACMODEL_PLANE换成SACMODEL_CYLINDER。

// PCL 圆柱拟合
pcl::SACSegmentationFromNormals<pcl::PointXYZ, pcl::Normal> seg;
seg.setOptimizeCoefficients(true);
seg.setModelType(pcl::SACMODEL_CYLINDER);
seg.setMethodType(pcl::SAC_RANSAC);
seg.setNormalDistanceWeight(0.1);  // 法向量权重
seg.setMaxIterations(10000);
seg.setDistanceThreshold(0.05);
seg.setRadiusLimits(0.1, 1.0);     // 半径范围
seg.setInputCloud(cloud);
seg.setInputNormals(normals);
seg.segment(*inliers, *coefficients);

我的经验: 圆柱拟合对法向量质量要求很高。如果法向量算得不准,拟合出来的圆柱半径能差一倍。所以做圆柱分割前,一定要先做好法向量估计,最好用MLS(移动最小二乘)平滑一下。

4.4.2 球体拟合

球体更简单,4个参数:球心(3个) + 半径(1个)。最少4个点就能确定一个球。

球体分割在工业检测里用得挺多。比如检测轴承里的滚珠、管道里的球阀。我做过一个项目,用球体分割去检测管道接头,准确率能到98%。

// PCL 球体拟合
seg.setModelType(pcl::SACMODEL_SPHERE);
seg.setDistanceThreshold(0.02);
seg.setRadiusLimits(0.05, 0.5);
seg.setInputCloud(cloud);
seg.segment(*inliers, *coefficients);

// coefficients 里存的是 [cx, cy, cz, r]

4.5 方法对比与选型

说了这么多,到底该用哪个?我整理了一个对比表,方便你快速决策:

方法 适用场景 优点 缺点
RANSAC平面 地面、墙面、桌面 速度快、抗噪强 只能提取平面
欧几里得聚类 物体检测、实例分割 简单、通用 对密度敏感
区域生长 平滑曲面分割 分割精细 参数多、调参麻烦
模型分割 工业零件检测 精度高、可解释性强 只适用于规则形状

选型其实没有标准答案。我个人的习惯是:先试最简单的。如果欧几里得聚类能搞定,就别上区域生长。如果RANSAC能提取出地面,就别搞模型拟合。简单的方法往往最可靠。

好了,这四种分割方法就聊到这儿。每种方法都有它的脾气,多试试、多调调参数,你就能摸透它们的性格。下次遇到点云分割的问题,心里就有底了。


专注资料整理