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
验证方法
- 输入准备:准备一个包含两个球体(半径0.5m,间距2m)和随机噪声点的
input.xyz文件,每行x y z。 - 运行:执行上述命令,设置
eps=0.3、minPts=5。 - 通过条件:程序输出
Clusters found: 2;用可视化工具(如CloudCompare)打开output.xyz,按label着色,两个球体分别显示为不同颜色,噪声点(若有)显示为单独颜色。 - 参数调整验证:将
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。
常见问题
- 编译错误:确认PCL头文件路径正确,使用
pkg-config自动获取编译参数。 - 运行卡顿:点云超过10万点时,KdTree搜索仍可能较慢,可先体素滤波降采样。
- 簇数异常:若所有点都被分为噪声,说明
eps过小或minPts过大;若所有点合并为一簇,则eps过大。