点云分割:从混乱中寻找秩序
点云分割,说白了就是把一堆乱七八糟的点,按照某种规则分成不同的组。你想想看,激光雷达扫出来的点云,少则几万,多则上百万个点。如果不做分割,那就是一团浆糊,根本没法用。
我个人习惯把点云分割分成两大类:基于模型的分割和基于聚类的分割。前者适合提取地面、墙面这些有明确几何形状的东西;后者适合把物体一个个拎出来。
好,咱们直接进入正题。今天聊四个最常用的方法:RANSAC平面分割、欧几里得聚类、DBSCAN聚类、区域生长分割。
RANSAC平面分割:老司机的最爱
RANSAC,全称是Random Sample Consensus。名字挺唬人,其实思路很简单——随机采样,找共识。
我在项目中遇到过最典型的场景:提取地面。自动驾驶里,地面信息太重要了。障碍物检测、路径规划,都离不开它。RANSAC就是干这个的利器。
它的核心逻辑是这样的:
- 从点云里随机挑三个点,确定一个平面
- 算算其他点到这个平面的距离
- 距离小于阈值的,算作"内点"
- 重复上面步骤N次,选内点最多的那个平面
嗯,这里要注意:迭代次数N怎么设? 设少了,可能找不到最优平面;设多了,计算量扛不住。我一般用这个公式估算:
N = log(1 - p) / log(1 - (1 - e)^s)
其中p是期望成功率(通常0.99),e是外点比例,s是每次采样点数(平面就是3)。
避坑指南:我曾经在高速场景下吃过亏。路面平整,RANSAC效果很好。但到了城市道路,有减速带、井盖,RANSAC经常把路面分割成好几块。后来我加了距离阈值自适应——根据点云密度动态调整阈值,效果好了不少。
代码实现其实不复杂,PCL里直接调:
pcl::SACSegmentation<pcl::PointXYZ> seg;
seg.setModelType(pcl::SACMODEL_PLANE);
seg.setMethodType(pcl::SAC_RANSAC);
seg.setDistanceThreshold(0.1); // 10cm阈值
seg.setMaxIterations(1000);
seg.setInputCloud(cloud);
seg.segment(*inliers, *coefficients);
欧几里得聚类:简单粗暴但有效
欧几里得聚类,名字听着高大上,其实就是根据距离远近把点分组。
怎么做的?
- 随便选一个点,找它周围半径r内的邻居
- 把这些邻居和它归为一类
- 再以这些邻居为起点,继续找邻居
- 直到找不到新的邻居为止
说白了,就是区域生长的简化版。关键参数就两个:搜索半径r和最小/最大点数。
r设大了,会把两个物体连在一起;设小了,一个物体会被切成好几块。我一般根据激光雷达的线束密度来调——Velodyne 64线,r设0.3-0.5米;16线的话,点比较稀疏,r要放到0.8-1.0米。
实战经验:欧几里得聚类最大的坑是近处物体粘连。比如两辆车并排停着,距离很近,聚类会把它们当成一个物体。我当时的解决方案是:先做地面滤除,再在聚类时加入高度信息——两个点距离近但高度差大,就不算邻居。
pcl::EuclideanClusterExtraction<pcl::PointXYZ> ec;
ec.setClusterTolerance(0.5); // 搜索半径
ec.setMinClusterSize(100); // 最少点数
ec.setMaxClusterSize(25000); // 最多点数
ec.setInputCloud(cloud);
ec.extract(cluster_indices);
DBSCAN聚类:密度说了算
DBSCAN和欧几里得聚类有点像,但更聪明。它不光看距离,还看密度。
核心概念就两个:
- 核心点:半径r内至少有minPts个点
- 边界点:在核心点的半径内,但自己不是核心点
DBSCAN的好处是:能处理任意形状的聚类。欧几里得聚类只能找到球形簇,但DBSCAN可以找到L形、S形的物体。这在处理行人、自行车这些不规则物体时特别有用。
我记得有一次做园区无人车,行人点云稀疏且形状不规则。欧几里得聚类经常把行人分成好几块,换成DBSCAN后,效果立竿见影。
注意:DBSCAN的参数调起来比欧几里得聚类麻烦。r和minPts是联动的——r太小,所有点都是噪声;r太大,所有点都连在一起。我建议先用欧几里得聚类跑一遍,把参数范围摸清楚,再微调DBSCAN。
// DBSCAN在PCL里没有直接实现,但可以用自定义方式
// 或者用Open3D的接口
open3d::geometry::PointCloud pcd;
auto labels = pcd.ClusterDBSCAN(eps=0.5, min_points=10);
区域生长分割:从种子开始
区域生长分割的思路很直观:选种子,长区域。
具体步骤:
- 选一个点作为种子
- 检查它的邻居——法向量夹角小、曲率接近的,就加入
- 新加入的点变成新种子,继续生长
- 直到没有符合条件的点为止
它和欧几里得聚类最大的区别是:不光看距离,还看法向量和曲率。所以特别适合分割平滑曲面,比如车顶、引擎盖、建筑物墙面。
我在项目中用它来分割车辆。车辆表面是平滑的,但地面和车辆之间法向量差异很大,区域生长能很好地分开。
调参心得:区域生长有两个关键阈值——法向量角度差和曲率差。角度差一般设15-30度,曲率差设0.1-0.5。设太大,会把不同物体长到一起;设太小,一个物体会碎成渣。我习惯先设大一点,看看效果,再逐步收紧。
pcl::RegionGrowing<pcl::PointXYZ, pcl::Normal> rg;
rg.setMinClusterSize(100);
rg.setMaxClusterSize(100000);
rg.setNumberOfNeighbours(30);
rg.setSmoothnessThreshold(15.0 / 180.0 * M_PI); // 15度
rg.setCurvatureThreshold(0.5);
rg.setInputCloud(cloud);
rg.setInputNormals(normals);
rg.extract(clusters);
四种方法怎么选?
我整理了一个对比表,方便你快速决策:
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| RANSAC平面分割 | 地面、墙面提取 | 鲁棒性强,抗噪声 | 只能提取平面 |
| 欧几里得聚类 | 简单物体分割 | 速度快,参数少 | 只能处理球形簇 |
| DBSCAN聚类 | 不规则形状物体 | 任意形状,自动去噪 | 参数调优麻烦 |
| 区域生长分割 | 平滑曲面分割 | 利用几何信息,精度高 | 计算量大 |
实际项目中,我很少只用一种方法。通常是组合拳:先用RANSAC去掉地面,再用欧几里得或DBSCAN做物体聚类,最后用区域生长做精细分割。
一句话总结:点云分割没有银弹。理解每种方法的原理和局限,根据场景灵活组合,才是王道。