第2章:点云数据基础
好,咱们正式开始聊点云数据。说实话,很多初学者一上来就急着调配准算法,结果连点云里存的是什么都没搞清楚。我见过太多人把PCD文件当文本打开,看到一堆数字就懵了。别急,这一章咱们把地基打牢。
2.1 点云的数据结构
点云说白了就是一堆三维点的集合。但怎么组织这些点,不同库有不同玩法。我主要用PCL和Open3D,这两个基本覆盖了工业场景的90%需求。
2.1.1 PCL中的点云结构
PCL里最核心的就是pcl::PointCloud模板类。它像个大容器,里面装着点,还带着宽高、是否有序这些元信息。
// 定义一个XYZ点云
pcl::PointCloud<pcl::PointXYZ> cloud;
cloud.width = 640; // 如果是有序点云,这就是宽度
cloud.height = 480; // 高度,无序点云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);
}
这里有个坑,我刚开始用PCL时经常搞混:有序点云和无序点云。有序点云就像一张深度图,每个像素对应一个点,宽高就是图像的宽高。无序点云就是一堆散点,height固定为1。
重要:PCL里常见的点类型有:
PointXYZ— 只有xyz坐标PointXYZI— 坐标+强度PointXYZRGB— 坐标+颜色PointNormal— 坐标+法向量PointXYZRGBNormal— 坐标+颜色+法向量,最全但最占内存
2.1.2 Open3D中的点云结构
Open3D就清爽多了。它用open3d.geometry.PointCloud,属性直接挂在对象上。
import open3d as o3d
import numpy as np
# 创建点云
pcd = o3d.geometry.PointCloud()
points = np.random.rand(100, 3) # 100个随机点
pcd.points = o3d.utility.Vector3dVector(points)
# 添加颜色
colors = np.random.rand(100, 3)
pcd.colors = o3d.utility.Vector3dVector(colors)
# 添加法向量
normals = np.random.rand(100, 3)
pcd.normals = o3d.utility.Vector3dVector(normals)
我个人更喜欢Open3D的写法,更Pythonic。但PCL在C++环境下的性能确实更好,尤其是处理百万级点云时。
我的经验:做原型验证用Open3D,部署到嵌入式设备用PCL。两个库我都踩过坑,Open3D的Python接口调试快,但PCL的C++版本在ARM上跑得更稳。
2.2 点云的存储格式
存点云用什么格式?这问题我当年也纠结过。PCD、PLY、LAS各有各的适用场景。我一个个说。
2.2.1 PCD格式
PCD是PCL的亲儿子。它最大的好处是灵活。你可以只存坐标,也可以存颜色、法向量、甚至自定义字段。
# PCD文件头示例
VERSION .7
FIELDS x y z rgb
SIZE 4 4 4 4
TYPE F F F F
COUNT 1 1 1 1
WIDTH 640
HEIGHT 480
VIEWPOINT 0 0 0 1 0 0 0
POINTS 307200
DATA ascii
0.1 0.2 0.3 4294967295
...
注意那个rgb字段,它是个32位整数,不是三个float。我刚开始写解析代码时就被这个坑过——直接把rgb当三个float读,结果颜色全乱套了。
避坑指南:PCD的二进制格式比ASCII快10倍以上。我曾经处理一个2亿点的激光雷达数据,用ASCII存了8GB,换成二进制直接降到1.2GB。所以生产环境一定要用二进制模式。
2.2.2 PLY格式
PLY是斯坦福大学搞出来的,在计算机图形学里用得特别多。它支持顶点和面片两种元素,所以不光能存点云,还能存网格模型。
ply
format ascii 1.0
element vertex 100
property float x
property float y
property float z
property uchar red
property uchar green
property uchar blue
element face 50
property list uchar int vertex_indices
end_header
0.1 0.2 0.3 255 0 0
...
PLY的property list是个好东西,可以存变长数据。比如每个三角形面片有3个顶点索引,但四边形有4个。用list就能灵活处理。
2.2.3 LAS格式
LAS是激光雷达扫描的行业标准。它不光存点,还存分类信息(地面、建筑、植被等)、回波次数、扫描角度等。
| 字段 | 说明 | 我的使用场景 |
|---|---|---|
| X, Y, Z | 坐标(通常用整数缩放) | 所有场景 |
| Intensity | 回波强度(0-65535) | 区分不同材质 |
| Classification | 点类别(2=地面, 6=建筑) | 做语义分割时用 |
| Number of Returns | 该脉冲的总回波数 | 过滤植被时有用 |
LAS格式有个特点:坐标用整数存,配合缩放因子和偏移量还原成真实坐标。这样做的好处是精度高、体积小。我处理过的一个项目,原始LAS文件500MB,转成PCD二进制后反而大了30%,就是因为PCD用float存坐标,精度浪费了。
2.3 点云的属性
点云不只是xyz。每个点可以带一堆属性。我按重要程度排个序。
2.3.1 坐标(XYZ)
这是最基本的。但要注意坐标系问题。激光雷达通常是右手系,Z轴向上。但有些深度相机是Z轴向前。我遇到过合作方给的PCD文件,坐标系是左手系,配准时怎么都对不上,折腾了两天才发现是坐标系搞反了。
2.3.2 法向量
法向量是点云处理里最重要的派生属性。它描述了局部表面的朝向。计算法向量通常用PCA分析邻域点。
// PCL中估计法向量
pcl::NormalEstimation<pcl::PointXYZ, pcl::Normal> ne;
ne.setInputCloud(cloud);
pcl::search::KdTree<pcl::PointXYZ>::Ptr tree(new pcl::search::KdTree<pcl::PointXYZ>());
ne.setSearchMethod(tree);
pcl::PointCloud<pcl::Normal>::Ptr cloud_normals(new pcl::PointCloud<pcl::Normal>);
ne.setRadiusSearch(0.03); // 搜索半径3cm
ne.compute(*cloud_normals);
这里有个参数调优的问题。搜索半径太小,法向量受噪声影响大;太大,又会平滑掉细节。我一般先看点云密度,取平均点间距的5-10倍作为半径。
2.3.3 颜色
颜色信息对配准帮助不大,但对可视化调试特别有用。比如你要看两个点云的重叠区域,把颜色一换,肉眼就能看出来。
RGB在PCL里是打包成一个整数的,提取时要拆开:
uint32_t rgb = *reinterpret_cast<int*>(&cloud.points[i].rgb);
uint8_t r = (rgb >> 16) & 0x0000ff;
uint8_t g = (rgb >> 8) & 0x0000ff;
uint8_t b = (rgb) & 0x0000ff;
这段代码我写过不下50次,每次都要查一下移位对不对。后来干脆封装成一个函数。
2.3.4 强度
强度是激光雷达特有的属性。它反映了目标物体对激光的反射能力。不同材质反射率不同:白色墙面反射率高,黑色轮胎反射率低。
强度值在配准时可以当辅助特征用。比如两个点云在同一个位置,如果强度值差异很大,那可能是不同物体,不应该匹配。我在做室内场景配准时,就利用强度过滤掉了玻璃窗上的点——玻璃反射率低,强度值明显偏低。
总结一下:点云的属性就像人的身份证信息。坐标是姓名,法向量是身高体重,颜色是照片,强度是指纹。不同场景下,你需要关注不同的属性。
2.4 本章知识体系
下面这张图是我自己画的,把本章的核心逻辑串起来了。你看一眼就能明白点云数据到底是怎么回事。
嗯,这一章的内容就这些。说白了,点云数据就是一堆带属性的三维点。你掌握了PCL和Open3D的数据结构,搞清楚了PCD、PLY、LAS的区别,再理解坐标、法向量、颜色、强度这些属性的用途,后面学配准算法就会轻松很多。
一个小建议:刚开始学的时候,别急着写代码。先拿一个PCD文件,用文本编辑器打开看看里面的数据长什么样。我当年就是这么干的,看完之后对点云的理解瞬间上了一个台阶。
公众号:蓝海资料掘金营,微信deep3321