3. 点云数据读写
点云处理的第一步是什么?不是滤波,不是配准,而是把数据读进来。
我见过不少新手,算法写得头头是道,结果卡在文件读写上——格式不对、坐标轴反了、内存爆了。说白了,读写是基本功,但坑也不少。这一章我们就来把Open3D和PCL的读写操作捋清楚,顺便聊聊格式转换的那些事。
3.1 为什么读写这么重要?
你想想看,传感器采集到的原始点云,通常不是你能直接用的格式。Velodyne的PCAP包、Livox的LVX文件、无人机航测的LAS数据……每个厂家都有自己的“方言”。
我在项目里遇到过最头疼的事:客户给了10TB的LAS数据,结果团队里有人用Open3D读LAS,有人用PCL读PCD,还有人用Python的laspy读——最后合并数据时坐标系统全乱了。嗯,从那以后我定了个规矩:统一入口格式,再进处理管线。
3.2 Open3D读写:简单直接
Open3D的API设计得很清爽。读文件一行,写文件一行,没什么花哨的。
3.2.1 读取点云
import open3d as o3d
# 读取PCD文件
pcd = o3d.io.read_point_cloud("scan.pcd")
print(f"点云点数: {len(pcd.points)}")
print(f"点云类型: {pcd}")
# 读取PLY文件
pcd_ply = o3d.io.read_point_cloud("mesh.ply")
# 读取TXT文件(每行 x y z 或 x y z r g b)
pcd_txt = o3d.io.read_point_cloud("points.txt", format="xyz")
format="xyzrgb"。我踩过这个坑,读进来全是黑点,排查了半天才发现是格式参数没写对。
3.2.2 保存点云
# 保存为PCD(二进制格式,体积小)
o3d.io.write_point_cloud("output.pcd", pcd, write_ascii=False)
# 保存为PLY(带颜色)
o3d.io.write_point_cloud("output.ply", pcd, write_ascii=True)
# 保存为TXT
o3d.io.write_point_cloud("output.txt", pcd, write_ascii=True)
注意write_ascii参数。二进制格式读写快、体积小,但不可读;ASCII格式可以用文本编辑器打开,方便调试。我个人习惯:调试用ASCII,上线用二进制。
3.3 PCL读写:功能强大但稍显繁琐
PCL是C++的天下,但Python绑定(python-pcl)也能用。不过说实话,PCL的Python接口维护得不太好,我建议你在C++项目里用PCL,Python项目里优先用Open3D。
3.3.1 C++版本(PCL原生)
#include <pcl/io/pcd_io.h>
#include <pcl/io/ply_io.h>
#include <pcl/point_types.h>
// 读取PCD
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
if (pcl::io::loadPCDFile<pcl::PointXYZ>("scan.pcd", *cloud) == -1) {
PCL_ERROR("读取PCD失败\n");
return -1;
}
// 保存PCD(二进制压缩)
pcl::io::savePCDFileBinaryCompressed("output.pcd", *cloud);
// 读取PLY
pcl::PolygonMesh mesh;
pcl::io::loadPLYFile("mesh.ply", mesh);
// 保存PLY
pcl::io::savePLYFile("output.ply", mesh);
loadPCDFile默认只读xyz,如果你有强度(intensity)或RGB信息,必须用对应的点类型,比如pcl::PointXYZI或pcl::PointXYZRGB。否则数据读进来是空的,而且不会报错——这种“静默失败”最坑人。
3.3.2 Python版本(python-pcl)
import pcl
# 读取PCD
cloud = pcl.load("scan.pcd")
print(f"点数: {cloud.size}")
# 保存PCD
pcl.save(cloud, "output.pcd")
# 注意:python-pcl不支持PLY直接读写,需要先转格式
python-pcl的功能比Open3D弱不少,而且安装麻烦(依赖VTK、Boost)。我个人建议:除非你必须在PCL管线里做后续处理,否则别用python-pcl做读写。
3.4 常见格式对比
| 格式 | 扩展名 | 特点 | 适用场景 |
|---|---|---|---|
| PCD | .pcd | PCL原生格式,支持二进制/ASCII,可存储任意字段 | PCL管线、机器人导航 |
| PLY | .ply | 支持点云+网格,带颜色/法线,广泛用于图形学 | 三维重建、Mesh处理 |
| LAS/LAZ | .las/.laz | 航测标准格式,支持分类、强度、回波等 | 测绘、林业、城市规划 |
| TXT/CSV | .txt/.csv | 纯文本,可读性强,但体积大、读写慢 | 调试、小数据交换 |
| OBJ | .obj | Wavefront格式,常用于3D建模软件 | 与Blender/Maya交换数据 |
3.5 格式转换实战
实际项目中,格式转换是家常便饭。我整理了几种最常见的转换场景。
3.5.1 PCD ↔ PLY(Open3D实现)
import open3d as o3d
# PCD → PLY
pcd = o3d.io.read_point_cloud("input.pcd")
o3d.io.write_point_cloud("output.ply", pcd)
# PLY → PCD
pcd = o3d.io.read_point_cloud("input.ply")
o3d.io.write_point_cloud("output.pcd", pcd)
就这么简单。Open3D内部自动处理了字段映射,你不需要关心细节。
3.5.2 LAS → PCD(使用laspy + Open3D)
import laspy
import open3d as o3d
import numpy as np
# 读取LAS
las = laspy.read("input.las")
points = np.vstack((las.x, las.y, las.z)).T
# 如果有颜色信息
if hasattr(las, 'red'):
colors = np.vstack((las.red, las.green, las.blue)).T / 65535.0
else:
colors = None
# 构建Open3D点云
pcd = o3d.geometry.PointCloud()
pcd.points = o3d.utility.Vector3dVector(points)
if colors is not None:
pcd.colors = o3d.utility.Vector3dVector(colors)
# 保存为PCD
o3d.io.write_point_cloud("output.pcd", pcd)
3.5.3 TXT → PCD(批量转换)
import open3d as o3d
import glob
# 批量转换所有TXT文件
for txt_file in glob.glob("data/*.txt"):
pcd = o3d.io.read_point_cloud(txt_file, format="xyz")
pcd_file = txt_file.replace(".txt", ".pcd")
o3d.io.write_point_cloud(pcd_file, pcd)
print(f"转换完成: {txt_file} → {pcd_file}")
3.6 知识体系总览
下面这张图把本章的核心逻辑串起来了。你可以看到:数据从传感器来,经过格式转换进入处理管线,最终输出为不同格式。
3.7 避坑指南
最后,我把自己这些年踩过的坑整理成清单,你遇到类似问题可以直接查。
- 坐标轴方向:不同传感器坐标系不同(LiDAR通常是X向前,Z向上;相机是Z向前,Y向下)。读写时注意坐标轴映射,否则点云会“躺”在地上。
- 颜色值范围:PCD和PLY用0-255的整数,Open3D用0-1的浮点数,LAS用0-65535的整数。转换时务必归一化。
- 内存爆炸:读取大文件(>1亿点)时,不要一次性读入内存。用
read_point_cloud的stride参数做降采样,或者用LAS的chunk_iterator分块读取。 - 字段丢失:PCD转PLY时,强度(intensity)字段可能会丢失。如果下游需要强度信息,建议用PCL的
savePLYFile并指定字段。 - 文件损坏:我曾经遇到过PCD文件头信息错误(比如
WIDTH和POINTS不匹配),导致读取失败。可以用文本编辑器打开PCD文件,手动检查头信息。