第4章:点云数据基础
各位同学,今天我们来聊聊点云数据。说实话,我刚入行那会儿,觉得点云就是一堆三维坐标点,没什么特别的。直到第一次做机器人抓取,点云数据乱七八糟,算法根本跑不动,我才意识到——点云处理,基础不牢,地动山摇。
这一章,我带你从点云的数据结构开始,一步步搞定可视化、滤波。这些都是后续一切高级操作的地基。
4.1 点云数据结构
点云说白了,就是三维空间中一堆点的集合。每个点通常包含 (x, y, z) 坐标,有时候还带着颜色、法向量、强度等信息。
目前最主流的两个库:PCL(Point Cloud Library)和 Open3D。我个人习惯,做机器人抓取时用 Open3D 更多,因为它 Python 接口友好,调试起来快。但 PCL 在工业界积累深厚,很多底层算法更成熟。
4.1.1 PCL 中的点云结构
PCL 的核心数据结构是 pcl::PointCloud<T>。T 可以是多种点类型:
| 点类型 | 包含字段 | 典型用途 |
|---|---|---|
| pcl::PointXYZ | x, y, z | 纯几何位置 |
| pcl::PointXYZRGB | x, y, z, r, g, b | 带颜色的点云 |
| pcl::PointNormal | x, y, z, normal_x, normal_y, normal_z | 带法向量的点云 |
举个例子,创建一个点云并添加点:
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
cloud->width = 100;
cloud->height = 1; // 无序点云
cloud->points.resize(cloud->width * cloud->height);
for (size_t i = 0; i < cloud->points.size(); ++i) {
cloud->points[i].x = 1024 * rand() / (RAND_MAX + 1.0f);
cloud->points[i].y = 1024 * rand() / (RAND_MAX + 1.0f);
cloud->points[i].z = 1024 * rand() / (RAND_MAX + 1.0f);
}
嗯,这里要注意:height = 1 表示无序点云。如果是 organized point cloud(比如从深度图转来的),height 就是图像高度。
4.1.2 Open3D 中的点云结构
Open3D 就简单多了。核心类是 open3d.geometry.PointCloud。它的 points 是一个 numpy 数组,操作起来非常 Pythonic。
import open3d as o3d
import numpy as np
# 创建点云
points = np.random.rand(100, 3).astype(np.float64)
pcd = o3d.geometry.PointCloud()
pcd.points = o3d.utility.Vector3dVector(points)
# 添加颜色
colors = np.random.rand(100, 3).astype(np.float64)
pcd.colors = o3d.utility.Vector3dVector(colors)
你看,代码量少了很多。我在项目中经常用 Open3D 做快速原型验证,效率很高。
4.2 点云可视化
可视化是点云处理中最直观的调试手段。你想想看,一堆数字你很难看出问题,但一显示出来,离群点、空洞、噪声一目了然。
4.2.1 PCL 可视化
PCL 用 pcl::visualization::CloudViewer 或 PCLVisualizer。前者简单,后者功能强。
pcl::visualization::CloudViewer viewer("Simple Cloud Viewer");
viewer.showCloud(cloud);
while (!viewer.wasStopped()) {
// 保持窗口打开
}
我曾经踩过一个坑:在循环里反复调用 showCloud,结果界面卡死。后来发现,加个 spinOnce 就好了。
4.2.2 Open3D 可视化
Open3D 的可视化就优雅多了:
o3d.visualization.draw_geometries([pcd],
window_name="点云可视化",
width=800,
height=600)
一行代码搞定。而且支持鼠标交互——旋转、平移、缩放,非常方便。
o3d.visualization.draw_geometries 的 non_blocking 模式,或者干脆用 o3d.io.write_point_cloud 保存下来,下载到本地看。
4.3 体素滤波
体素滤波(Voxel Grid Filter),说白了就是降采样。为什么要做?因为原始点云太密了,动辄几十万、上百万个点,计算起来慢得要命。
它的原理很简单:把空间划分成一个个小立方体(体素),每个体素内只保留一个点(通常是重心)。
代码实现:
# Open3D 体素滤波
voxel_size = 0.05 # 5cm 体素
downsampled_pcd = pcd.voxel_down_sample(voxel_size=voxel_size)
// PCL 体素滤波
pcl::VoxelGrid<pcl::PointXYZ> sor;
sor.setInputCloud(cloud);
sor.setLeafSize(0.05f, 0.05f, 0.05f);
sor.filter(*cloud_filtered);
4.4 统计滤波
统计滤波(Statistical Outlier Removal)用来去除离群点。原理很直观:计算每个点到它 k 个最近邻的平均距离。如果这个距离超过全局均值一定标准差,就认为是离群点。
为什么会需要它?传感器噪声、边缘跳变都会产生一些孤立的点。这些点对后续的配准、分割影响很大。
# Open3D 统计滤波
cl, ind = pcd.remove_statistical_outlier(nb_neighbors=20, std_ratio=2.0)
filtered_pcd = pcd.select_by_index(ind)
// PCL 统计滤波
pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;
sor.setInputCloud(cloud);
sor.setMeanK(50);
sor.setStddevMulThresh(1.0);
sor.filter(*cloud_filtered);
参数怎么调?nb_neighbors(或 setMeanK)我一般设 20-50。std_ratio 设 1.0-2.0。值越小,剔除得越狠。
4.5 半径滤波
半径滤波(Radius Outlier Removal)是另一种去噪方法。它检查每个点周围指定半径内有多少个邻居。如果邻居数量少于阈值,就干掉它。
和统计滤波的区别?统计滤波看的是距离分布,半径滤波看的是局部密度。说白了,统计滤波适合去除稀疏的离群点,半径滤波适合去除孤立的点簇。
# Open3D 半径滤波
cl, ind = pcd.remove_radius_outlier(nb_points=16, radius=0.05)
filtered_pcd = pcd.select_by_index(ind)
// PCL 半径滤波
pcl::RadiusOutlierRemoval<pcl::PointXYZ> outrem;
outrem.setInputCloud(cloud);
outrem.setRadiusSearch(0.05);
outrem.setMinNeighborsInRadius(10);
outrem.filter(*cloud_filtered);
参数选择上,radius 我一般设成体素大小的 2-3 倍。nb_points 设 10-20 比较常见。
- 体素滤波:降采样,减少数据量。必用。
- 统计滤波:去除稀疏离群点。推荐先用这个。
- 半径滤波:去除孤立小点簇。如果统计滤波效果不好,再试这个。
好了,这一章的内容就到这里。点云数据结构是基础,可视化是调试利器,三种滤波是预处理三板斧。把这些练熟了,后面的配准、分割、抓取姿态估计才能跑得顺。