4、点云分割基础:基于RANSAC的平面分割、圆柱分割、自定义形状分割

点云分割,说白了就是把一堆乱七八糟的点,按形状归类。

你拿到一帧激光雷达数据,里面有地面、有墙壁、有树干、有行人。怎么把它们分开?这就是分割要干的事。我个人习惯把分割看作是「找规律」——找到那些符合某种数学模型的点,把它们拎出来。

这一章,咱们就聊聊最经典的方法:RANSAC。别被名字吓到,它背后的逻辑其实很朴素。

4.1 RANSAC 的核心思想

RANSAC 的全称是 Random Sample Consensus,随机采样一致性。名字很长,但核心就一句话:从乱点中,找到符合某个模型的那一批点

举个例子。你面前有一堆点,大部分是地面,少部分是噪声。你想把地面点找出来。怎么做?

  1. 随机选三个点,确定一个平面。
  2. 看看其他点离这个平面有多远。
  3. 距离近的,就是「内点」;距离远的,就是「外点」。
  4. 重复以上步骤,直到找到内点最多的那个平面。

嗯,就是这么简单。我在项目中遇到过一个问题:如果地面有坡度,RANSAC 会把它切成好几块。后来我加了点预处理,先做一次粗略的平面拟合,再局部优化,效果就好多了。

关键参数

  • 距离阈值:判断一个点是否属于模型的距离上限。设小了,漏掉很多点;设大了,噪声全进来了。
  • 迭代次数:理论上越多越好,但实际中够用就行。我一般设 100-200 次。
  • 最小内点数:认为找到模型的最低点数。低于这个数,就认为没找到。

4.2 平面分割实战

平面分割是 RANSAC 最经典的应用。地面、墙面、桌面,都是平面。

咱们用 PCL 库来实现。代码不长,但每一步都有讲究。

// 创建分割对象
pcl::SACSegmentation<pcl::PointXYZ> seg;
pcl::PointIndices::Ptr inliers(new pcl::PointIndices);
pcl::ModelCoefficients::Ptr coefficients(new pcl::ModelCoefficients);

// 设置模型类型和距离阈值
seg.setModelType(pcl::SACMODEL_PLANE);
seg.setMethodType(pcl::SAC_RANSAC);
seg.setDistanceThreshold(0.01);  // 单位:米

// 输入点云
seg.setInputCloud(cloud);
seg.segment(*inliers, *coefficients);

这段代码跑完,inliers 里存的就是地面点的索引。coefficients 里是平面方程 ax + by + cz + d = 0 的四个参数。

我的经验:距离阈值设 0.01 米(1厘米)对大多数室内场景够用。如果是室外,地面起伏大,可以放宽到 0.03-0.05 米。但别超过 0.1 米,否则会把路沿石也算进去。

你可能会问:为什么不用最小二乘法?最小二乘法对噪声敏感,一个离群点就能把平面拉偏。RANSAC 天生抗噪,这是它的最大优势。

4.3 圆柱分割实战

圆柱分割比平面麻烦一点。因为圆柱有 7 个参数:轴线方向(3个)、轴线上一点(3个)、半径(1个)。

但思路一样:随机采样,找内点最多的圆柱模型。

pcl::SACSegmentation<pcl::PointXYZ> seg;
seg.setModelType(pcl::SACMODEL_CYLINDER);
seg.setMethodType(pcl::SAC_RANSAC);
seg.setDistanceThreshold(0.05);
seg.setRadiusLimits(0.1, 0.5);  // 半径范围:10cm 到 50cm

seg.setInputCloud(cloud);
seg.segment(*inliers, *coefficients);

这里多了个 setRadiusLimits。为什么要限制半径?因为如果不限制,RANSAC 可能把一堆乱点拟合成一个半径很大的圆柱,那就不对了。

我记得有一次做树干检测,树干的半径大概 15-20 厘米。我设了 0.1 到 0.3 米的半径范围,效果很好。但旁边的路灯杆半径只有 5 厘米,就被漏掉了。后来我改成多尺度检测,先粗后细,才把路灯杆也抓出来。

注意:圆柱分割对点云密度敏感。如果点太稀疏,RANSAC 很难找到稳定的圆柱。我建议先做体素滤波降采样,让点云均匀一些。

4.4 自定义形状分割

平面和圆柱是 PCL 内置的模型。但实际项目中,你可能会遇到奇奇怪怪的形状——比如锥形、球体、甚至是不规则曲面。

这时候就需要自定义形状分割了。说白了,就是你自己定义一个数学模型,然后告诉 RANSAC 怎么算距离、怎么判断内点。

PCL 提供了 SACSegmentation 的扩展接口。你需要继承 pcl::SACModel,并实现几个虚函数:

  • computeModelCoefficients:从随机采样的点中计算模型参数。
  • countWithinDistance:统计内点数量。
  • projectPoints:将点投影到模型上(可选)。

举个例子。假设你要分割一个圆锥体。圆锥的方程是 (x^2 + y^2) / z^2 = tan^2(θ)。你需要写一个类,实现上述三个函数。

class SACModelCone : public pcl::SACModel<pcl::PointXYZ> {
public:
  bool computeModelCoefficients(const std::vector<int> &samples) override {
    // 从 samples 中选三个点,计算圆锥参数
    // 返回 true 表示成功
  }

  int countWithinDistance(const Eigen::VectorXf &model_coefficients,
                          double threshold) override {
    // 遍历所有点,计算到圆锥的距离
    // 距离小于 threshold 的算内点
  }
};

然后你就可以像用平面分割一样,用这个自定义模型了。

自定义模型的坑

  • 模型参数越多,需要的随机采样点就越多。平面要 3 个点,圆柱要 5 个点,圆锥可能要 6 个点。采样点越多,迭代次数就得增加。
  • 距离函数要写对。我曾经写错过一个距离公式,结果 RANSAC 跑了一整天都没收敛。后来发现是符号搞反了。

4.5 避坑指南与调参心得

RANSAC 看起来简单,但调参是个手艺活。我踩过不少坑,分享几个给你。

我曾经... 在室外场景用默认参数跑平面分割,结果地面被分成了好几块。原因是地面有坡度,RANSAC 把不同坡度的区域当成了不同平面。后来我加了法向量约束,只保留法向量朝上的平面,问题就解决了。

调参的几个原则:

  • 距离阈值:先看你的点云精度。Velodyne 16 线的点,垂直方向误差大概 2-3 厘米,阈值设 0.03 米比较合理。如果是固态激光雷达,精度高,可以设 0.01 米。
  • 迭代次数:别盲目设大。100 次和 1000 次,结果差别不大,但时间差了 10 倍。我一般先设 50 次看看效果,不够再加。
  • 模型选择:能选平面就别选圆柱。平面参数少,收敛快,鲁棒性高。

你想想看,RANSAC 的本质是「暴力枚举+投票」。它不聪明,但很可靠。只要你的模型定义得对,参数调得合适,它就能把点云拆得明明白白。

嗯,这一章就到这里。下一章咱们聊聊更高级的分割方法——基于区域生长的分割。到时候你会发现,RANSAC 和区域生长结合起来,效果会更好。


公众号:蓝海资料掘金营,微信deep3321