点云聚类算法
摘要:本文整理欧氏聚类与体素网格聚类的原理、参数和实现过程,分别按处 理步骤拆解两种算法的核心代码,说明 PCL 邻域搜索、连通扩展、体素质心与原始点回填,通过代码理解聚类阈值对目标粘连、过分割和计算量的影响。
1. 点云聚类的目标
地面分割后,将空间上连通的非地面点组织成点簇,为包围盒拟合、分类与跟踪提供输入。欧氏聚类输出的是几何分组,不保证每个点簇都对应一个真实物体。
参考:
https://autowarefoundation.github.io/autoware_universe/main/perception/autoware_euclidean_cluster/
2. Euclidean Cluster(欧氏聚类)
设两个点为 与 ,三维欧氏距离为:
不使用高度时,距离改为:
用距离阈值 建立邻域连接,再通过连通扩展形成点簇。A 与 B 接近、B 与 C 接近时,即使 A 与 C 超过阈值,也可能进入同一个点簇。因此阈值限制的是邻域距离,并非点簇直径。
流程:
- 选取一个尚未访问的点作为种子,不要求随机选取
- 以它为起点,找附近距离小于阈值的点
- 把邻近的点加入同一类
- 再从这些新加入的点继续找邻居
- 扩散到找不到为止
- 连通的点组成一个点簇(cluster)
- 从下一个尚未访问的点继续,直到所有点均被访问
核心参数:
tolerance_m是默认距离阈值
min_points_per_cluster是有效物体最小点数,滤掉噪声
max_cluster_size 是有效点簇的最大点数;PCL 的 setMinClusterSize 控制最小点数。
use_height=false 时只按 x-y 距离搜索邻域,高度差不会阻止连接,但不能由此认定这些点属于同一真实物体。
| Name | Type | Description | Default | Range |
|---|---|---|---|---|
| max_cluster_size | integer | 如果一个聚类中的点数超过该值,通常认为它不是一个正常的独立目标,可能是墙面、建筑物、大面积植被,或者多个目标粘连在一起。 | 1000 | N/A |
| min_points_per_cluster | integer | 点数少于该值的聚类会被认为是噪声或无效目标。 | 10 | N/A |
| tolerance_m | float | 在 L2 欧氏空间中,如果两个点之间的距离小于该阈值,它们可能会被归入同一个 cluster。调大:更容易把点粘成一团,但不同物体可能被合并。调小:物体更容易分开,但同一个物体可能被拆成多个 cluster。 | 0.7 | N/A |
| use_height | boolean | 是否使用点的 z 坐标参与聚类。false:只根据 x-y 平面距离聚类,适合俯视障碍物分组。true:使用 x、y、z 三维距离聚类,高度差也会影响聚类结果。 | false | N/A |
2.1 代码讲解:准备搜索点云并建立 KdTree
以下按算法流程拆解 C++ 核心片段,并非完整 ROS 节点。cloud 是已去除地面与无效点的 pcl::PointCloud<pcl::PointXYZ> 输入,单位为米。
using PointT = pcl::PointXYZ;
using Cloud = pcl::PointCloud<PointT>;
Cloud::Ptr search_cloud(new Cloud(*cloud));
if (!use_height) {
for (auto& p : search_cloud->points) p.z = 0.0f;
}
pcl::search::KdTree<PointT>::Ptr tree(new pcl::search::KdTree<PointT>);
tree->setInputCloud(search_cloud);
use_height=false 时只压平搜索副本,以三维搜索接口实现二维距离判断。原始点云保留真实高度,且副本没有删除或重排点,因此索引仍然对应。KdTree 为半径邻域查询建立搜索结构,聚类扩展时可以反复查找附近点。
2.2 代码讲解:邻域扩展与点数约束
pcl::EuclideanClusterExtraction<PointT> ec;
ec.setInputCloud(search_cloud);
ec.setSearchMethod(tree);
ec.setClusterTolerance(tolerance_m);
ec.setMinClusterSize(min_points_per_cluster);
ec.setMaxClusterSize(max_cluster_size);
std::vector<pcl::PointIndices> clusters;
ec.extract(clusters);
extract 内部完成种子选取、邻域搜索、访问标记与连通扩展。tolerance_m 控制每次半径搜索的连接范围;点簇形成后,再按最少与最多点数决定是否输出。标准 PCL 超大簇会被排除,不会因 setMaxClusterSize 自动拆分。
也可以把其内部连通扩展理解为下面的队列过程。此片段用于解释原理,不应在调用 ec.extract 后再运行一遍:
std::vector<bool> visited(search_cloud->size(), false);
for (int seed = 0; seed < static_cast<int>(search_cloud->size()); ++seed) {
if (visited[seed]) continue;
std::vector<int> queue{seed};
visited[seed] = true;
for (std::size_t head = 0; head < queue.size(); ++head) {
std::vector<int> neighbors;
std::vector<float> squared_distances;
tree->radiusSearch(search_cloud->points[queue[head]], tolerance_m,
neighbors, squared_distances);
for (int index : neighbors) {
if (!visited[index]) {
visited[index] = true;
queue.push_back(index);
}
}
}
// queue 为一个完整连通簇,此时再按点数判断是否保留。
}
新邻居入队后继续搜索,使连接具有传递性。入队时就标记 visited,可避免一个点被重复加入;radiusSearch 返回的距离数组保存平方距离,搜索半径本身仍以米传入。
2.3 代码讲解:恢复原始三维输出
std::vector<Cloud> output;
for (const auto& indices : clusters) {
Cloud cluster_cloud;
pcl::copyPointCloud(*cloud, indices.indices, cluster_cloud);
output.push_back(cluster_cloud);
}
使用原始 cloud 和聚类索引复制点,可保留真实 z。若搜索前执行了裁剪、排序或下采样,这一索引对应关系就不再自动成立。主要头文件是 <pcl/search/kdtree.h>、<pcl/segmentation/extract_clusters.h> 与 <pcl/common/io.h>。
3. Voxel Grid Based Euclidean Cluster(体素网格聚类)
流程:
- 用
pcl::VoxelGrid计算每个体素的质心 - 对质心使用
pcl::EuclideanClusterExtraction聚类 - 根据聚类后的质心,把原始点分配回各个cluster
核心参数:
voxel_leaf_size_m 表示 x、y 方向的网格尺寸。本文参考的 Autoware 二维实现将不同高度合并到同一平面网格;普通 PCL VoxelGrid 是三维体素过滤器。
tolerance_m:质心距离是否在阈值内
min_points_per_voxel:单个体素至少包含的原始点数。
max_voxels_per_cluster:单个输出点簇的体素数上限,超限时按参考版本的切分逻辑处理。
| Name | Type | Description | Default | Range |
|---|---|---|---|---|
| tolerance_m | float | 当两个被占用 voxel 的 2D 质心,即 x-y 平面质心距离小于该值时,它们会被合并到同一个 cluster。该合并具有传递性:A 接近 B,B 接近 C,则 A、B、C 可能属于同一 cluster。聚类时使用 voxel 质心,并将 z 坐标压平为 0。 | 0.7 | N/A |
| voxel_leaf_size_m | float | x 和 y 方向的 voxel 尺寸 [m]。z 方向的 voxel 尺寸等效为无限大,因此该 grid 本质上是 2D 网格。同一个 x-y cell 内的所有高度点都会被压到同一个 voxel 中。该值决定聚类和切分时的平面 footprint 分辨率。 | 0.3 | N/A |
| min_points_per_voxel | integer | 单个 voxel 至少需要包含的原始点数。点数少于该值的 voxel 会在聚类前被丢弃,用于过滤稀疏噪声点。 | 1 | N/A |
| min_points_per_cluster | integer | 输出 cluster 至少需要包含的原始点数。小于该点数的 cluster 会在最终过滤阶段被丢弃。 | 10 | N/A |
| max_voxels_per_cluster | integer | 单个 cluster 允许包含的最大 voxel 数量。如果某个 cluster 的 voxel 数超过该值,算法会沿其主方向,也就是最长轴方向,递归二分成更小的子 cluster,直到每个子 cluster 的 voxel 数都不超过该阈值。注意:大 cluster 不会因为过大被直接丢弃,而是会被切分后输出为多个有界部分。 | 800 | ≥1 |
| large_cluster_voxel_count_threshold | integer | 大 cluster 判定阈值。当某个 cluster 的 voxel 数超 过该值时,会被认为是 large cluster,并启用每个 voxel 的点数上限控制。voxel 数不超过该值的 cluster 会保留全部原始点。 | 65 | N/A |
| large_cluster_max_points_per_voxel | integer | large cluster 中每个 voxel 最多保留的点数。当 cluster 的 voxel 数超过 large_cluster_voxel_count_threshold 时生效。超出该数量的点会被丢弃,以限制后续处理计算量。点会以确定性打乱顺序进行处理,使保留下来的点尽量均匀采样每个 voxel。 | 10 | N/A |
| use_height | boolean | 是否使用点的 z 坐标参与聚类。当前仅实现了 2D 路径,即 use_height=false。因此聚类实际基于 x-y 平面距离,高度 z 不参与聚类判断。 | false | N/A |
3.1 配置版本与参数单位
原笔记中的 perception/euclidean_cluster/config/voxel_grid_based_euclidean_cluster.param.yaml 是旧版目录示例。旧版的 tolerance、voxel_leaf_size、min_points_number_per_voxel 与 min_cluster_size,不能直接与上表的新名称混用。尤其要区分原始点数与体素数,max_cluster_size 不等同于 max_voxels_per_cluster。表中的默认值仅作参考,部署前应核对节点实际声明的参数和 YAML 层级。