点云数据基础:格式解析与读写实战
做自动驾驶感知这几年,我接触最多的就是点云数据。说实话,刚入行那会儿,面对各种格式的文件——PCD、BIN、PLY,我也有点懵。每种格式都有自己的脾气,搞不清楚的话,数据读进来都是乱码。今天我就带你把这些格式彻底捋一遍。
点云数据格式:三种主流格式对比
先说说最常见的三种格式。我个人习惯把PCD和PLY归为一类——它们是文本友好的格式,方便调试。而BIN格式呢,纯粹是二进制,追求极致性能。
| 格式 | 特点 | 适用场景 | 文件大小 |
|---|---|---|---|
| PCD | Point Cloud Library 原生格式,支持ASCII和二进制 | PCL生态、算法调试 | 中等 |
| BIN | 纯二进制,无头信息,结构紧凑 | KITTI等自动驾驶数据集 | 最小 |
| PLY | Stanford格式,支持顶点和面片 | 3D重建、网格处理 | 较大 |
你想想看,为什么自动驾驶领域偏爱BIN格式?说白了就一个字——快。激光雷达一秒钟扫几十万点,如果每帧都存成文本,磁盘IO直接成为瓶颈。我在项目中遇到过,用PCD格式存一帧64线激光雷达数据,文件大小是BIN格式的3倍以上。
点云数据结构:PointXYZ vs PointXYZI
理解了文件格式,接下来看数据在内存里怎么组织。最基础的结构是PointXYZ——只有x、y、z三个坐标。但实际应用中,我们往往需要更多信息。
PointXYZI就是在XYZ基础上加了一个强度值(intensity)。这个强度值很有用——不同材质的物体反射回来的激光强度不同。比如车道线的反射强度通常比沥青路面高很多,这就是我们做车道线检测的重要依据。
核心区别:
- PointXYZ:3个float,共12字节
- PointXYZI:4个float,共16字节(intensity通常归一化到0-1)
- PointXYZRGB:6个float,共24字节(颜色信息)
嗯,这里要注意——不同传感器厂商对intensity的定义不一样。Velodyne的intensity范围是0-255,而Ouster的是0-4095。我曾经因为没做归一化,直接把原始值喂给模型,结果训练出来的模型泛化能力极差。所以,读数据时一定要确认intensity的位宽。
点云文件读写实战:Python代码示例
光说不练假把式。下面我给出三种格式的读写代码,都是我在实际项目中打磨过的。
1. 读取BIN格式(KITTI风格)
import numpy as np
def read_kitti_bin(bin_path):
"""
读取KITTI格式的BIN文件
每个点包含4个float: x, y, z, intensity
"""
points = np.fromfile(bin_path, dtype=np.float32)
points = points.reshape(-1, 4)
return points
def write_kitti_bin(points, bin_path):
"""写入BIN文件,points shape为(N, 4)"""
points.astype(np.float32).tofile(bin_path)
避坑指南:我曾经因为忘记reshape,直接把一维数组当点云用,结果可视化出来全是乱点。记住:BIN文件没有头信息,你必须事先知道每个点有几个字段。
2. 读取PCD格式(ASCII模式)
def read_pcd_ascii(pcd_path):
"""读取ASCII格式的PCD文件"""
with open(pcd_path, 'r') as f:
lines = f.readlines()
# 解析头信息
header = {}
data_start = 0
for i, line in enumerate(lines):
if line.startswith('#'):
continue
parts = line.strip().split()
if parts[0] == 'DATA':
data_start = i + 1
break
header[parts[0]] = parts[1:]
# 读取点云数据
points = []
for line in lines[data_start:]:
coords = list(map(float, line.strip().split()))
points.append(coords)
return np.array(points), header
注意:PCD文件可以是二进制格式,这时不能直接用文本方式读取。判断方法是看头部的DATA字段——如果是"DATA ascii"就是文本,如果是"DATA binary"就是二进制。我见过有人用文本方式读二进制PCD,结果读出来的全是乱码。
3. 读取PLY格式
def read_ply(ply_path):
"""读取PLY文件,支持ASCII和二进制"""
with open(ply_path, 'rb') as f:
# 读取头信息
header = b''
while True:
line = f.readline()
header += line
if line.strip() == b'end_header':
break
# 解析顶点数量
vertex_count = 0
for line in header.split(b'\n'):
if line.startswith(b'element vertex'):
vertex_count = int(line.split()[-1])
# 读取顶点数据
points = np.frombuffer(f.read(), dtype=np.float32)
points = points.reshape(-1, 3)
return points[:vertex_count]
知识体系结构图
下面这张图帮你理清点云数据处理的整体脉络:
实战中的坑与经验
最后分享几个我踩过的坑:
- 字节序问题:不同平台(x86 vs ARM)的字节序可能不同。我曾在Jetson上读x86机器生成的BIN文件,结果坐标全反了。解决方案是统一用little-endian。
- 强度值归一化:不同传感器的intensity范围不同,建议统一归一化到0-1之间再存入文件。
- 内存管理:一帧64线激光雷达数据大约12万个点,PointXYZI格式占用约1.9MB内存。如果做序列回放,注意不要一次性加载全部帧到内存。
我的建议:开发阶段用PCD格式(方便调试),部署阶段用BIN格式(追求性能)。PLY格式主要用于3D重建场景,自动驾驶感知中用得相对少一些。
好了,点云数据格式这块就讲到这里。记住一句话:格式只是载体,数据本身才是核心。搞清楚了每种格式的存储方式和字段含义,读写代码就是模板化的工作了。
可可精品资料库