这里记录常见的点云滤波方式,主要介绍滤波在项目中的主要作用,常用滤波的场景原理和使用方式,主要记录了体素下采样、最大聚类、半径滤波,并附有python和cpp的实现方式。

场景和问题

项目数据中的点云来自于深度相机,相机有视场,在范围内的点云都会被采集,这里面存在非目标的点云物体,这会干扰后续处理。
所以需要进行一系列滤波操作,将非目标点云去除,保留目标点云。
主要使用到的方式包括下采样滤波、半径滤波、最大聚类滤波等。

体素下采样滤波

深度相机点云和深度图像的像素数目一致,数据量太大,后续处理速度慢,所以需要减少数据的同时保留关键信息。
体素下采样滤波可以在保留形状的情况下减少点云数量。

下采样滤波原理

将三维空间划分为多个立方体,如果立方体中存在点云,这个点云的中心就作为下采样后的点云。
这样就可以通过设计立方体大小,来控制下采样后的点云数量。

下采样滤波实现


down_pcd=pcd.voxel_down_sample(voxel_size=10)
print(f"下采样操作,下采样前点数: {len(pcd.points)}, 下采样后点数: {len(down_pcd.points)}")

pcd是处理前的点云,down_pcd是处理后的点云。
参数voxel_size为立方体大小,通过设置这个参数,可以控制下采样后的点云数量,此处假设点云是mm单位,设置10代表体素大小10mm。

// 对点云进行体素下采样
double voxel_size = 10.0; // 体素大小,单位:毫米
auto downsampled_cloud = point_cloud->VoxelDownSample(voxel_size);
std::cout << "下采样后点云点数: " << downsampled_cloud->points_.size() << std::endl;

最大聚类滤波

从方案设计的角度来说,目标物体会被放在视场的中心,占点云比例最大,所以直接提取点云中占比最大的聚类,就可以得到目标点云。
因此需要使用最大聚类滤波把最大聚类点云提取出来。

最大聚类滤波原理

将点云从距离和密度的角度进行聚类分析,这就是bdscan的原理。
如果一个点云周边存在其他点云满足一定距离限制,同时满足这个距离限制的点足够多则满足聚类需求。
将这个聚类里面所有点云都当作一个中心点云,再次根据距离和密度生长,直到不满足聚类条件,则得到一个聚类。
聚类出来就是一个簇,聚类不出来的点云就是噪声点云。
将所有聚类中占比最大的聚类提取出来,就是最大聚类滤波。

最大聚类滤波实现

1.先使用open3d的cluster_dbscan方法进行聚类,得到聚类标签。
2.统计聚类标签,得到最大聚类标签。
3.根据最大聚类标签,根据最大标签生成提取掩码。
4.根据掩码提取点云。

# 聚类,分离无关点云
labels = np.array(down_pcd.cluster_dbscan(eps=30, min_points=10, print_progress=False))
max_label = labels.max()  # 查看分为了几类
num_clusters = max_label + 1  # 因为标签是从 0 开始的
num_noise = np.sum(labels == -1)  # 计算噪声点数量 (标签为 -1 的点)
total_points = len(labels)
print(f"聚类结果统计:")
print(f"- 总点数: {total_points}")
print(f"- 簇的数量 (分类数): {num_clusters}")
print(f"- 噪声点数量 (无关点): {num_noise}")
print(f"- 噪声点占比: {num_noise / total_points * 100:.2f}%")
# 提取最大点云
unique_labels, counts = np.unique(labels[labels >= 0], return_counts=True)
max_index = np.argmax(counts)
largest_label = unique_labels[max_index]
print(f"检测到 {len(unique_labels)} 个簇。")
print(f"最大的簇是标签 {largest_label},包含 {counts[max_index]} 个点。")
# 获取所有属于该簇的点的索引
indices = np.where(labels == largest_label)[0]
# 使用 select_by_index 提取点
largest_cluster_pcd = down_pcd.select_by_index(indices)
o3d.io.write_point_cloud("process/largest_cluster.pcd", largest_cluster_pcd)

其中bdscan的聚类方式为:

labels = np.array(down_pcd.cluster_dbscan(eps=30, min_points=10, print_progress=False))

其中,eps为距离限制,min_points为满足距离限制的点数。

// 使用DBSCAN聚类分离无关点云
double eps = 30.0;   // 邻域半径,单位:毫米
int min_points = 10; // 形成簇所需的最小点数
auto labels = downsampled_cloud->ClusterDBSCAN(eps, min_points, false);

// 统计聚类结果
int max_label = *std::max_element(labels.begin(), labels.end());
int num_clusters = max_label + 1;                             // 因为标签是从0开始的
int num_noise = std::count(labels.begin(), labels.end(), -1); // 噪声点数量
int total_points = labels.size();

std::cout << "聚类结果统计:" << std::endl;
std::cout << "- 总点数: " << total_points << std::endl;
std::cout << "- 簇的数量 (分类数): " << num_clusters << std::endl;
std::cout << "- 噪声点数量 (无关点): " << num_noise << std::endl;
std::cout << "- 噪声点占比: " << static_cast<double>(num_noise) / total_points * 100 << "%" << std::endl;

// 提取最大点云簇
std::shared_ptr<open3d::geometry::PointCloud> largest_cluster_pcd = nullptr;
if (num_clusters > 0)
{
    // 统计每个簇的点数
    std::vector<int> cluster_counts(num_clusters, 0);
    for (int label : labels)
    {
    if (label >= 0)
    {
        cluster_counts[label]++;
    }
    }

    // 找到点数最多的簇
    int max_count = 0;
    int largest_label = 0;
    for (int i = 0; i < num_clusters; ++i)
    {
    if (cluster_counts[i] > max_count)
    {
        max_count = cluster_counts[i];
        largest_label = i;
    }
    }

    std::cout << "检测到 " << num_clusters << " 个簇。" << std::endl;
    std::cout << "最大的簇是标签 " << largest_label << ",包含 " << max_count << " 个点。" << std::endl;

    // 提取属于最大簇的点
    std::vector<size_t> indices;
    for (size_t i = 0; i < labels.size(); ++i)
    {
    if (labels[i] == largest_label)
    {
        indices.push_back(i);
    }
    }

    // 使用select_by_index提取点云
    largest_cluster_pcd = downsampled_cloud->SelectByIndex(indices);
}
else
{
    std::cerr << "警告: 未检测到有效的簇,使用下采样后的点云。" << std::endl;
    largest_cluster_pcd = downsampled_cloud;
}

半径滤波

提取到目标物体后,多少会存在一些噪声点,这些噪声点离目标物体较近,在提取最大点云时由于半径设计较大,会把这些噪声包含进来,所以需要除噪音。
半径滤波可以通过设置半径和最小点数,将离目标物体较近的噪声点去除。

半径滤波原理

设置一个半径,如果半径内的点数大于最小点数,则保留这个点,否则去除这个点。
这样就可以去除离目标物体较近的噪声点。

半径滤波实现

radius = 20  # 半径大小
min_neighbors = 5  # 半径内最小点数
radius_pcd = largest_cluster_pcd.remove_radius_outlier(radius, min_neighbors)
print(f"半径滤波操作,半径滤波前点数: {len(down_pcd.points)}, 半径滤波后点数: {len(radius_pcd.points)}")

radius为半径大小,min_neighbors为半径内最小点数。

double radius = 20.0;      // 半径大小,单位:毫米
int min_neighbors = 5;     // 半径内最小点数
auto result = largest_cluster_pcd->RemoveRadiusOutlier(min_neighbors, radius);
auto radius_pcd = std::get<0>(result); // 提取过滤后的点云

std::cout << "半径滤波操作,半径滤波前点数: " << largest_cluster_pcd->points_.size() 
          << ",半径滤波后点数: " << radius_pcd->points_.size() << std::endl;

扩展

除了上述滤波方式,如统计滤波、法线滤波等。
统计滤波可以作为半径滤波的补充。
法线滤波则可以过滤特定方向的点云。

总结

通过体素下采样滤波、最大聚类滤波和半径滤波,可以有效地去除点云中的噪声点,保留目标点云,为后续处理提供高质量的数据。

更多推荐