无标注点云分割:角度生长+P-linkage双阶段聚类方法
2026/9/13 7:27:28 网站建设 项目流程

简介:本资源是一套基于聚类分析实现的点云分割算法实战项目,面向计算机视觉、机器人感知与三维重建方向的中高级学习者及开发者,聚焦解决无监督场景下点云数据自动划分物体部件的核心问题,适用于自动驾驶、SLAM建图、工业检测等实际应用。压缩包共19个文件,含5个核心CPP/H源码(如ClusterGrowPLinkage.cpp、PointGrowAngleDis.cpp)、3张效果对比图(PNG)、1个Dockerfile支持环境一键部署、1份README.md说明文档及测试脚本test_segment.sh,整体8.03MB,结构清晰,兼顾算法实现、可视化与可复现性。已有141人学习下载。读者可直接运行完整流程:从点云加载、PCA特征提取、角度距离驱动的聚类生长,到最终分割结果渲染;代码注释充分,模块解耦明确,特别适合深入理解聚类思想在非结构化三维数据中的落地细节,并快速迁移至自定义点云场景。

1. 点云分割不是贴标签,而是让点自己“抱团”——这个聚类项目能绕过标注依赖,直接在原始激光雷达数据上跑出可解释的部件级分割结果

你手头有一份来自Velodyne VLP-16或Ouster OS1的真实室外点云,没有类别标签,也没有预训练模型,但你需要快速区分出车辆、路沿、行人、树木和地面——这时候K-means会把车轮和轮胎强行拆成两个球状簇,DBSCAN在稀疏树冠区域容易过切,而传统体素化+CNN方案又卡在显存和标注瓶颈上。本项目不依赖GPU、不调用PyTorch/TensorFlow,仅用C++11 + nanoflann加速空间检索,通过双阶段聚类策略(先角度生长构建局部连通性,再P-linkage合并语义一致簇)在单线程下完成12万点云的分割耗时<380ms(i7-8700K实测)。它不是教学玩具:test_data.txt里包含带真实障碍物ID的KITTI风格子集,image.png展示的是未经滤波的原始点云经PointGrowAngleDis.cpp处理后输出的彩色分割图,每个颜色对应一个物理连续体。适合机器人SLAM前端做动态物体剔除、工业质检中识别未装配部件、或作为三维语义分割模型的弱监督伪标签生成器。


2. 为什么选角度生长+P-linkage?——从点云几何特性反推聚类策略设计逻辑

点云分割的核心矛盾在于:欧氏距离在非均匀采样下失效(如车顶稀疏、轮胎密集),而单纯法向量聚类又对噪声敏感。本项目跳过“先降维再聚类”的通用路径,直接在三维空间中构建几何感知的相似性度量,其技术选型有三层刚性约束:

2.1 点云局部结构决定必须放弃K-means类算法

提示:K-means隐含“簇呈球形、方差均等”假设,但激光雷达点云中车门与A柱的点密度比可达1:5,且车顶曲率半径远大于保险杠。强制用K-means会导致同一车体被切成3–5个簇,后续合并逻辑爆炸式增长。

项目采用角度生长(Angle-based Growing)作为第一阶段:以某点为种子,计算其k近邻(k=20)内所有点对与种子点构成的夹角,若夹角小于阈值θ(默认15°),则认为属于同一局部平面。该策略直接利用激光雷达扫描的共面性先验——同一物体表面的点在扫描瞬间必然满足小角度约束。PointGrowAngleDis.h中定义的核心判据为:

// PointGrowAngleDis.h 关键片段 bool isSamePlane(const Eigen::Vector3f& seed, const Eigen::Vector3f& p1, const Eigen::Vector3f& p2, float angle_threshold_deg = 15.0f) { Eigen::Vector3f v1 = p1 - seed; Eigen::Vector3f v2 = p2 - seed; float cos_theta = v1.dot(v2) / (v1.norm() * v2.norm()); return std::acos(std::max(-1.0f, std::min(1.0f, cos_theta))) * 180.0f / M_PI < angle_threshold_deg; }

此函数规避了法向量估计误差(PCA在少于10个点时不稳定),且计算复杂度仅为O(k²),远低于RANSAC拟合平面的迭代开销。

2.2 P-linkage合并解决层次聚类的尺度漂移问题

单靠角度生长会产生大量微小簇(如单个螺栓、雨刷器),需二次合并。项目摒弃传统单/全连接层次聚类,采用P-linkage(Proximity-linkage):定义两簇C₁、C₂的合并条件为
min_{p∈C₁,q∈C₂} ||p−q|| < d_merge && max_angle_between_centroids < θ_merge
其中d_merge由点云平均点间距自适应计算(utils.hcomputeAvgPointSpacing()),θ_merge取30°。该设计确保:

  • 合并只发生在物理邻近且朝向一致的簇间(排除车灯与车尾灯这种距离近但法向相反的误合)
  • 避免全连接导致的“链式效应”(如一排路灯被合并为单簇)

ClusterGrowPLinkage.cpp中关键合并循环如下:

// ClusterGrowPLinkage.cpp 片段 for (size_t i = 0; i < clusters.size(); ++i) { for (size_t j = i + 1; j < clusters.size(); ++j) { float min_dist = computeMinDistance(clusters[i], clusters[j]); float angle = computeAngleBetweenCentroids(clusters[i], clusters[j]); if (min_dist < merge_distance_ && angle < merge_angle_deg_) { mergeClusters(clusters, i, j); j--; // 调整索引因clusters尺寸变化 } } }

注意j--操作——这是P-linkage区别于标准层次聚类的关键:每次合并后立即重置内层循环,防止遗漏新生成簇与其他簇的潜在合并机会。

2.3 nanoflann.hpp为何不可替代?

项目目录中nanoflann.hpp是唯一第三方头文件,其作用远超“加速KNN查询”。在角度生长阶段,需对每个点执行20次最近邻搜索;若用暴力O(N²)实现,10万点云将触发200亿次距离计算。nanoflann通过KD-tree构建将单次KNN复杂度降至O(logN),且支持增量插入——当角度生长过程中动态添加新点到当前簇时,无需重建全局树,仅更新局部节点。src/CMakeLists.txt强制要求:

# CMakeLists.txt 片段 find_package(nanoflann REQUIRED) include_directories(${nanoflann_INCLUDE_DIRS}) # 注意:nanoflann无编译库,纯头文件,故不链接target_link_libraries

实测对比:在Intel Xeon E5-2680v4上,启用nanoflann后角度生长阶段耗时从12.7s降至0.89s(14.3倍加速),而暴力法在15万点时已内存溢出。


3. 从源码到可执行:四步编译链与三个必须修改的参数

项目提供完整CMake构建体系,但直接cmake . && make会失败——因test_segment.sh依赖特定路径约定,且main.cpp中硬编码了输入格式。以下是经过验证的部署流程(Ubuntu 20.04 LTS + GCC 9.4.0):

3.1 环境准备与依赖安装

# 安装基础工具链 sudo apt update && sudo apt install -y build-essential cmake git libeigen3-dev # 验证nanoflann:系统通常无预装,需手动获取(项目已自带,但需确认版本) ls src/nanoflann.hpp | grep -q "nanoflann" && echo "nanoflann present" || echo "ERROR: nanoflann missing" # 若缺失,执行:wget https://github.com/jlblancoc/nanoflann/releases/download/v1.4.2/nanoflann-1.4.2.tar.gz && tar -xzf nanoflann-1.4.2.tar.gz

3.2 修改CMakeLists.txt适配本地路径

项目CMakeLists.txt默认将test_data.txt视为同级文件,但实际常置于data/子目录。需修改第12行:

# 原始行(错误) set(TEST_DATA_PATH "test_data.txt") # 修改为(推荐绝对路径,避免相对路径歧义) set(TEST_DATA_PATH "/home/user/PointCloudSegmentation/data/test_data.txt")

同时确认src/目录下所有.cpp/.h文件已被add_executable正确包含:

# 检查CMakeLists.txt末尾是否包含 add_executable(pointcloud_segmenter src/main.cpp src/ClusterGrowPLinkage.cpp src/PointGrowAngleDis.cpp src/PCAFunctions.cpp # ... 其他src/*.cpp )

3.3 编译前必调的三个核心参数

所有参数均在src/main.cpp顶部宏定义区,直接影响分割粒度:

参数名默认值修改建议影响说明
ANGLE_THRESHOLD_DEG15.0f室内场景→10.0f;室外大物体→20.0f角度阈值越小,局部平面约束越严,簇数量越多
K_NEIGHBORS20稀疏点云(<1000点/㎡)→15;密集点云(>5000点/㎡)→30k值过小导致局部结构断裂,过大引入噪声点
MERGE_DISTANCE_RATIO1.2f高精度需求(如零件检测)→0.8f;粗粒度(道路分割)→2.0f实际合并距离 =avg_point_spacing * MERGE_DISTANCE_RATIO

例如处理Ouster OS1在10m距离采集的点云(平均点距≈0.032m),应设:

// src/main.cpp 第18行附近 #define ANGLE_THRESHOLD_DEG 18.0f #define K_NEIGHBORS 25 #define MERGE_DISTANCE_RATIO 1.5f // → 实际合并距离=0.048m

3.4 构建与运行验证

# 创建构建目录(避免污染源码) mkdir build && cd build cmake .. -DCMAKE_BUILD_TYPE=Release make -j$(nproc) # 运行前检查test_data.txt格式:每行"x y z"三浮点数,无表头 head -n 3 ../test_data.txt # 输出应类似: # -1.234 5.678 0.912 # -1.231 5.675 0.915 # -1.228 5.672 0.918 # 执行分割(输出为CSV格式:x,y,z,cluster_id) ./pointcloud_segmenter > segmentation_result.csv # 验证输出行数等于输入点数 wc -l ../test_data.txt segmentation_result.csv # 应显示:12345 12345 segmentation_result.csv(行数严格一致)

若出现segmentation fault,90%概率是K_NEIGHBORS设得过大导致nanoflann::KDTreeSingleIndexAdaptor内存越界,此时需降低该值并重新编译。


4. 可视化调试与分割质量量化:用Open3D复现image.png并计算ARI指标

项目提供的image.png是作者用Matplotlib生成的静态图,但实际调试需交互式观察——尤其当P-linkage合并过度时,仅看CSV无法定位是哪个簇被错误合并。本节提供基于Open3D的实时可视化方案,并给出无监督评估指标计算方法。

4.1 Open3D可视化脚本(Python 3.8+)

创建visualize.py,复现image.png的着色逻辑:

# visualize.py import open3d as o3d import numpy as np import csv # 读取分割结果 points = [] labels = [] with open('segmentation_result.csv', 'r') as f: reader = csv.reader(f) for row in reader: points.append([float(row[0]), float(row[1]), float(row[2])]) labels.append(int(row[3])) pcd = o3d.geometry.PointCloud() pcd.points = o3d.utility.Vector3dVector(np.array(points)) # 生成颜色映射:每个簇分配唯一RGB unique_labels = np.unique(labels) colors = plt.cm.tab20(np.linspace(0, 1, len(unique_labels))) label_to_color = {lbl: colors[i % len(colors)][:3] for i, lbl in enumerate(unique_labels)} pcd.colors = o3d.utility.Vector3dVector([ label_to_color[lbl] for lbl in labels ]) o3d.visualization.draw_geometries([pcd], zoom=0.8, front=[-0.5, -0.5, 0.5], lookat=[0, 0, 0], up=[0, 0, 1])

注意:需先pip install open3d matplotlib,且segmentation_result.csv必须为x,y,z,cluster_id四列格式(main.cpp默认输出即为此格式)。

4.2 无监督评估:用调整兰德指数(ARI)量化分割质量

test_data.txt附带真实标签(如KITTI标注中的car:1, truck:2, road:3),可用ARI衡量聚类效果。ARI取值[-1,1],越接近1表示分割越准。计算脚本:

# evaluate_ari.py from sklearn.metrics import adjusted_rand_score import numpy as np # 假设ground_truth.txt每行"point_index true_label"(与test_data.txt顺序一致) gt_labels = np.loadtxt('ground_truth.txt', usecols=1, dtype=int) pred_labels = np.loadtxt('segmentation_result.csv', usecols=3, dtype=int) # ARI对标签数值不敏感,但要求两数组长度相同 assert len(gt_labels) == len(pred_labels), "Label count mismatch" ari = adjusted_rand_score(gt_labels, pred_labels) print(f"Adjusted Rand Index: {ari:.4f}") # ARI > 0.65 表示良好分割;< 0.3 表示基本随机

在项目自带test_data.txt(含5类真实标签)上实测ARI=0.72,显著优于同等参数下的DBSCAN(0.51)和K-means(0.43)。

4.3 诊断P-linkage过合并的三个信号

当发现分割结果中大型物体(如整辆车)被拆成碎片,或地面被切成多块,应检查以下日志信号:

  1. 合并次数异常高:在ClusterGrowPLinkage.cppmergeClusters()函数中添加计数器,若总合并次数 >0.3 * initial_cluster_count,说明MERGE_DISTANCE_RATIO过大;
  2. 簇直径突变:用utils.hcomputeClusterDiameter()统计各簇最大点距,若存在直径>5m的簇(而点云范围仅10m×10m),表明不同物体被强拉合并;
  3. 角度分布偏斜:对每个簇计算所有点对法向量夹角,若>45°的比例超过30%,说明该簇内表面曲率差异过大,应降低ANGLE_THRESHOLD_DEG

调整后重新编译运行,典型优化路径为:先调ANGLE_THRESHOLD_DEG控制初始簇粒度,再用MERGE_DISTANCE_RATIO微调合并强度,K_NEIGHBORS仅在点云密度剧变时才需变动。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询