4、点云分割基础:基于RANSAC的平面分割、圆柱分割、自定义形状分割
点云分割,说白了就是把一堆乱七八糟的点,按形状归类。
你拿到一帧激光雷达数据,里面有地面、有墙壁、有树干、有行人。怎么把它们分开?这就是分割要干的事。我个人习惯把分割看作是「找规律」——找到那些符合某种数学模型的点,把它们拎出来。
这一章,咱们就聊聊最经典的方法:RANSAC。别被名字吓到,它背后的逻辑其实很朴素。
4.1 RANSAC 的核心思想
RANSAC 的全称是 Random Sample Consensus,随机采样一致性。名字很长,但核心就一句话:从乱点中,找到符合某个模型的那一批点。
举个例子。你面前有一堆点,大部分是地面,少部分是噪声。你想把地面点找出来。怎么做?
- 随机选三个点,确定一个平面。
- 看看其他点离这个平面有多远。
- 距离近的,就是「内点」;距离远的,就是「外点」。
- 重复以上步骤,直到找到内点最多的那个平面。
嗯,就是这么简单。我在项目中遇到过一个问题:如果地面有坡度,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