第4章 · 点云分割
点云分割,说白了就是把一堆散乱的点,按语义拆成不同的组。地面、车辆、行人、树……各归各的。我刚开始做激光雷达感知时,觉得分割不就是聚类嘛,后来踩了坑才明白——地面不先干掉,聚类全是噪声。这一章咱们就聊聊地面分割、欧式聚类和区域生长,都是实战里最趁手的家伙。
4.1 地面分割 —— RANSAC 与平面拟合
地面分割是点云感知的第一步,也是最重要的一步。你想想看,64线激光雷达一帧能扫出十几万个点,其中一半以上都是地面。如果不把地面去掉,后面的聚类根本没法看——地面和车辆会糊成一团。
RANSAC(随机采样一致性)是我最常用的地面分割方法。它的思路很暴力:随机选三个点,算一个平面,然后数有多少点落在这个平面上。重复几百次,选点数最多的那个平面作为地面。
核心流程:
- 从点云中随机采样3个点(不共线)
- 计算平面模型 ax+by+cz+d=0
- 计算所有点到该平面的距离,距离小于阈值 t 的点视为内点
- 重复步骤1-3,保留内点最多的平面
- 用所有内点重新拟合平面(可选加权最小二乘)
我个人习惯把距离阈值设在 0.1~0.2 米。太大会把路沿、矮障碍物也吞掉,太小又会导致地面漏检。我在项目里遇到过一段颠簸路面,RANSAC 死活拟合不出完整平面——后来加了高度先验,把种子点限制在自车附近,效果才稳下来。
// 伪代码:RANSAC 地面分割
int bestInliers = 0;
Plane bestPlane;
for (int i = 0; i < maxIterations; i++) {
// 随机选3个点
Point p1 = cloud[rand() % N];
Point p2 = cloud[rand() % N];
Point p3 = cloud[rand() % N];
Plane plane = fitPlane(p1, p2, p3);
int inliers = countInliers(cloud, plane, distThreshold);
if (inliers > bestInliers) {
bestInliers = inliers;
bestPlane = plane;
}
}
// 用所有内点重新优化平面
bestPlane = refinePlane(cloud, bestPlane, distThreshold);
4.2 欧式聚类 —— 把障碍物一个个拎出来
地面去掉之后,剩下的点基本都是障碍物了。但怎么区分哪坨点是车、哪坨点是行人?欧式聚类就是干这个的。它的原理特别直观:如果两个点距离小于某个半径,就认为它们属于同一个物体。
我最早用 PCL 的 EuclideanClusterExtraction,调参调到头秃。核心参数就两个:聚类半径和最小点数。半径设大了,会把相邻的车粘在一起;设小了,一辆大卡车会被切成好几块。
radius = baseRadius + k * distance。
下面是一个典型的欧式聚类流程:
- 构建 KD-Tree 加速近邻搜索
- 选一个未访问的点作为种子
- 在半径 r 内找邻居,加入当前簇
- 对簇内每个点重复搜索,直到没有新点加入
- 如果簇内点数 < minPoints,视为噪声丢弃
// 欧式聚类核心逻辑 (C++风格)
std::vector<Cluster> euclideanClustering(PointCloud& cloud, double radius, int minPts) {
KDTree tree(cloud);
std::vector<bool> visited(cloud.size(), false);
std::vector<Cluster> clusters;
for (int i = 0; i < cloud.size(); i++) {
if (visited[i]) continue;
Cluster cluster;
std::queue<int> q;
q.push(i);
visited[i] = true;
while (!q.empty()) {
int idx = q.front(); q.pop();
cluster.push_back(idx);
auto neighbors = tree.radiusSearch(cloud[idx], radius);
for (int n : neighbors) {
if (!visited[n]) {
visited[n] = true;
q.push(n);
}
}
}
if (cluster.size() >= minPts)
clusters.push_back(cluster);
}
return clusters;
}
4.3 区域生长 —— 让点云自己“长”出结构
区域生长和欧式聚类不同,它不光看距离,还看点的“长相”——法向量、曲率、颜色(如果有的话)。我一般用它来分割建筑物立面、树干这类有平滑表面的物体。
算法也不复杂:先选曲率最小的点作为种子(曲率小意味着局部平坦),然后检查邻居的法向量夹角。如果夹角小于阈值,就把邻居吞进来。不断生长,直到没有新点满足条件。
区域生长 vs 欧式聚类:
| 对比项 | 欧式聚类 | 区域生长 |
|---|---|---|
| 相似性度量 | 欧氏距离 | 法向量夹角 + 曲率 + 距离 |
| 适用场景 | 任意形状物体 | 平面/光滑曲面 |
| 参数敏感性 | 半径、最小点数 | 角度阈值、曲率阈值 |
| 计算速度 | 快(KD-Tree) | 中等(需计算法向量) |
我记得有一次做园区无人车,路边的灌木丛用欧式聚类总是分成好几块,因为枝叶之间有空隙。换成区域生长,把法向量平滑阈值设到 30°,居然把整棵灌木完整分割出来了。嗯,这里要注意:区域生长需要提前计算法向量,如果点云噪声大,法向量会不准,建议先做滤波。
① 体素滤波下采样 → ② RANSAC 地面分割 → ③ 欧式聚类(动态半径)→ ④ 对聚类结果做 bounding box 提取。
如果场景里有大量平面结构(比如停车场),会在第③步之前加一个区域生长,把墙面、地面残留再滤一遍。
4.4 三种方法怎么选?
没有银弹。我个人的经验是:
- 地面分割:RANSAC 是标配,简单可靠。如果路面起伏大,可以试试平面拟合+高度阈值双保险。
- 欧式聚类:通用性最强,90% 的场景都能用。调参时先固定最小点数(比如 5~10),再调半径。
- 区域生长:适合精细分割,比如区分相邻的车辆和行人(如果它们挨得很近)。但计算量稍大,实时性要求高的项目慎用。
最后说一句:点云分割没有标准答案。我在不同项目里用过完全不同的参数组合,甚至自己写过基于深度图的分割。但不管怎么变,理解每种方法背后的几何假设,才是调参的底气。