4. 基于DBSCAN的点云分割

任务目标

使用C++实现DBSCAN聚类算法,对激光雷达点云进行欧式距离分割,并通过命令行参数调整eps(邻域半径)与minPts(最小点数),使同一物体表面的点归为一类,不同物体或地面点被区分开。

核心实现

以下代码实现完整DBSCAN流程:输入为文本格式点云(每行x y z),输出为带聚类标签的点云(每行x y z label,label=-1表示噪声点)。算法使用KdTree加速邻域搜索,避免O(n²)遍历。

// dbscan_segmentation.cpp
#include <pcl/point_types.h>
#include <pcl/point_cloud.h>
#include <pcl/kdtree/kdtree_flann.h>
#include <fstream>
#include <sstream>
#include <vector>
#include <string>
#include <cstdlib>

struct PointXYZL {
    float x, y, z;
    int label;
};

int main(int argc, char** argv) {
    if (argc != 5) {
        std::cerr << "Usage: " << argv[0] << " input.xyz output.xyz eps minPts\n";
        return 1;
    }
    const std::string in_file = argv[1];
    const std::string out_file = argv[2];
    const float eps = std::atof(argv[3]);
    const int min_pts = std::atoi(argv[4]);

    // 读取点云
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
    std::ifstream fin(in_file);
    if (!fin.is_open()) { std::cerr << "Cannot open " << in_file << "\n"; return 1; }
    std::string line;
    while (std::getline(fin, line)) {
        std::istringstream iss(line);
        pcl::PointXYZ p;
        if (!(iss >> p.x >> p.y >> p.z)) continue;
        cloud->push_back(p);
    }
    fin.close();
    if (cloud->empty()) { std::cerr << "Empty cloud\n"; return 1; }

    // 构建KdTree
    pcl::KdTreeFLANN<pcl::PointXYZ> kdtree;
    kdtree.setInputCloud(cloud);

    // DBSCAN主流程
    const int n = cloud->size();
    std::vector<int> labels(n, -2);  // -2=未访问, -1=噪声, >=0=簇ID
    int cluster_id = 0;
    std::vector<int> neighbors, queue;

    for (int i = 0; i < n; ++i) {
        if (labels[i] != -2) continue;  // 已访问

        // 区域查询
        pcl::PointXYZ search_point = (*cloud)[i];
        std::vector<float> dists;
        kdtree.radiusSearch(search_point, eps, neighbors, dists);

        if (static_cast<int>(neighbors.size()) < min_pts) {
            labels[i] = -1;  // 噪声点
            continue;
        }

        // 新簇扩展
        labels[i] = cluster_id;
        queue.clear();
        queue.insert(queue.end(), neighbors.begin(), neighbors.end());

        while (!queue.empty()) {
            int q = queue.back();
            queue.pop_back();
            if (labels[q] == -1) labels[q] = cluster_id;  // 噪声点归入当前簇
            if (labels[q] != -2) continue;  // 已访问

            labels[q] = cluster_id;
            pcl::PointXYZ q_point = (*cloud)[q];
            std::vector<int> q_neighbors;
            std::vector<float> q_dists;
            kdtree.radiusSearch(q_point, eps, q_neighbors, q_dists);
            if (static_cast<int>(q_neighbors.size()) >= min_pts) {
                queue.insert(queue.end(), q_neighbors.begin(), q_neighbors.end());
            }
        }
        ++cluster_id;
    }

    // 输出带标签点云
    std::ofstream fout(out_file);
    if (!fout.is_open()) { std::cerr << "Cannot write " << out_file << "\n"; return 1; }
    for (int i = 0; i < n; ++i) {
        fout << (*cloud)[i].x << " " << (*cloud)[i].y << " " << (*cloud)[i].z
             << " " << labels[i] << "\n";
    }
    fout.close();

    std::cout << "Clusters found: " << cluster_id << "\n";
    return 0;
}

编译与运行

# 依赖:PCL 1.8+(含KdTree模块)
g++ -O2 dbscan_segmentation.cpp -o dbscan_seg \
    $(pkg-config --cflags --libs pcl_common pcl_kdtree)

# 运行(示例参数)
./dbscan_seg input.xyz output.xyz 0.5 10

验证方法

  1. 输入准备:准备一个包含两个球体(半径0.5m,间距2m)和随机噪声点的input.xyz文件,每行x y z。
  2. 运行:执行上述命令,设置eps=0.3、minPts=5。
  3. 通过条件:程序输出Clusters found: 2;用可视化工具(如CloudCompare)打开output.xyz,按label着色,两个球体分别显示为不同颜色,噪声点(若有)显示为单独颜色。
  4. 参数调整验证:将eps改为1.5重新运行,两个球体应被合并为1个簇(输出Clusters found: 1),验证参数对分割粒度的影响。

参数选择建议

  • eps:约为场景中最小目标物体直径的1/2~1/3。室内场景常用0.2~0.5m,室外车辆/行人场景常用0.5~1.0m。
  • minPts:通常取5~10。点云越密,minPts可适当增大;稀疏点云取较小值。
  • 若分割结果过碎(簇数过多),增大eps或减小minPts;若不同物体粘连,则减小eps或增大minPts。
原始点云 无标签,所有点同色 DBSCAN eps=0.3, minPts=5 分割结果 簇0 簇1 按label着色,噪声点label=-1 簇0 簇1 噪声

常见问题

  • 编译错误:确认PCL头文件路径正确,使用pkg-config自动获取编译参数。
  • 运行卡顿:点云超过10万点时,KdTree搜索仍可能较慢,可先体素滤波降采样。
  • 簇数异常:若所有点都被分为噪声,说明eps过小或minPts过大;若所有点合并为一簇,则eps过大。