第01章 - PDAL 概述与点云生态
1.1 问题:格式与算法之间的 N×M 组合
一个真实的点云项目通常长这样:数据来源有机载 LiDAR 的 LAS 1.2、无人机后处理的 LAZ 1.4、扫描仪的 E57,目标产出是一块范围内的 DEM 和一份入库用的压缩点云。传统做法是拼工具链——las2las 裁范围、las2dem 出栅格、laszip 压缩,每个工具的开关都不一样,中间结果反复落盘。换一种输入格式,整条链路推倒重来。
问题的根源不是工具不够好,而是点云处理长期缺少一层统一抽象:N 种格式要对接 M 种算法,就需要 N×M 份粘合代码。栅格和矢量领域在 2000 年前后由 GDAL/OGR 解决了这个问题——上层只面对一套 Dataset 接口,底下接几十种驱动。点云领域对应的答案是 PDAL(Point Data Abstraction Library,点数据抽象库)。它的 README 只有一句话:PDAL is GDAL for point cloud data。
这句话描述的是工程结构,不是营销类比:reader 把各种格式解析成统一的维度表,filter 只认维度不认格式,writer 再把维度表序列化回各种容器。N×M 于是塌缩成 N+M。
表 1-1 点云处理的两条技术路线
| 维度 | 工具链拼装 | PDAL pipeline |
|---|---|---|
| 流程组织 | shell 串联,中间文件落盘 | 进程内 stage 链,尽量不落地 |
| 格式 | 每个工具自带解析器 | 统一 reader,维度归一 |
| 算法复用 | 复制粘贴命令行 | filter 任意组合、顺序即语义 |
| 可复现 | 依赖 shell 历史 | pipeline JSON 即处理配方 |
| 扩展 | 改工具源码 | 写插件 stage,不动本体 |
1.2 PDAL 的边界
PDAL 是处理库 + 命令行工具,不是桌面 GIS,也不是三维引擎。边界划清楚,才不会拿它做不该做的事。
它负责:格式互转(LAS/LAZ/COPC/BPF/PCD/PLY/E57/文本/EPT 等)、坐标重投影、几何裁剪、去噪与滤波、地面分类、点云栅格化、特征计算、切片与瓦片索引、Pipeline 序列化与元数据导出。
它不负责:点云的可视化渲染(Potree、CloudCompare、Cesium 的活)、交互式编辑(手动删点、手工改分类)、制图符号化、通用栅格影像处理(交给 GDAL)、服务化本身(PDAL 只提供读写驱动,服务由 pgpointcloud 等承担)。
还有一条容易被忽略的边界是算法深度。PDAL 的滤波器以工程可用为目标:假设明确、参数少、结果稳定。地面分类用的是 SMRF / PMF / CSF 这类成熟方法,不是研究前沿;要做深度学习语义分割,得自己在 Python 里另接。PDAL 提供的是稳定的骨架,算法层是可替换的。
1.3 版本基线与许可证
写教程必须钉住版本,否则默认值对不上。本章及后续章节的事实基线是 PDAL 2.10.0,对应源码树 CMakeLists.txt 里的 project(PDAL VERSION 2.10.0 LANGUAGES CXX C)。
许可证是 BSD 3-Clause(版权方 Hobu, Inc.),允许商用、闭源分发和二次开发,没有传染性条款。同领域功能重叠最大的 LAStools 采用混合许可(部分工具闭源收费),这是不少团队选型时的分水岭。
表 1-2 近年发布记录(节选自官方 download 文档)
| 版本 | 发布日期 |
|---|---|
| 2.9.3 | 2025-11-26 |
| 2.8.4 | 2025-02-03 |
| 2.7.1 | 2024-03-27 |
| 2.6.3 | 2024-02-05 |
| 2.5.6 | 2023-08-18 |
| 2.4.0 | 2022-03-18 |
主版本号长期停在 2.x 说明 API 稳定,但小版本之间默认值会变。实例:writers.las 的 minor_version 默认值在 PDAL 1.0–2.7 为 2,从 2.8 起改为 4。同一条 pipeline 在 2.7 和 2.10 上跑,输出文件的 LAS 版本可能不同,进而影响能否使用 point format 6/7/8。这类差异不读 release note 是查不出来的。
命令行里能直接读到版本事实:
pdal --version
# ----------------------------------------------------------------
# pdal 2.10.0 (git-version: Release)
# ----------------------------------------------------------------
这一行由 Config::fullVersionString() 拼出;非 Release 构建会把 40 位 git sha 截成 6 位,用于确认“这个二进制到底编译自哪个提交”。
1.4 三层核心抽象
PDAL 的 API 很宽,但核心只有三层。把这三层吃透,后面每个 stage 的文档都能自己读。
1.4.1 Dimension:带名字和类型的列
PDAL 不假设点“必须由 XYZ + 强度 + 回波组成”。每个属性是一个 dimension,有名字、有类型,注册在 pdal/Dimension.json。2.10.0 中共 132 个标准维度,例如:
- 坐标:
X/Y/Z/W,类型double - 回波:
Intensity(uint16)、ReturnNumber/NumberOfReturns(uint8) - 分类:
Classification(uint8,别名class、cls) - 颜色:
Red/Green/Blue - 时间:
GpsTime、OffsetTime - 派生:
HeightAboveGround、NormalX、Curvature、ClusterID
类型决定精度上限与内存占用。Classification 是 uint8,只能表达 0–255;把 32 位强度值赋给 Intensity 会被静默截断,不报错——这是最隐蔽的一类数据损失。
表里没有的属性也不会丢:reader 可以用 extra_dims 以“名字=类型”的形式声明额外维度,LAS 的 extra bytes VLR 内容就是这么进来的。这让 PDAL 能承载比 LAS 标准更宽的数据。
1.4.2 PointView:内存中的一批点
PointView 是数据容器,相当于“一张点表”。一条 pipeline 在标准模式下,数据以 PointView 为单位在 stage 之间流动。
关键特性是一个 stage 可以输出多个 PointView:filters.splitter 按瓦片切、filters.divider 按容量或轮询切、filters.crop 给多个多边形就产出多个视图。下游 writer 只要文件名里带 # 占位符,就会为每个视图写一个文件:
[
"input.las",
{
"type" : "filters.crop",
"bounds" : [ "([0, 75], [0, 75])", "([50, 125], [50, 125])" ]
},
"output#.las"
]
结果是 output1.las 与 output2.las,各自只含对应裁剪区的点。
底层存储是按列组织:PointTable 为每个 dimension 分配一块连续内存,PointView 持有的是一组行索引。这解释了两个现象:新增一个维度是 O(1) 的列操作,与点数无关;视图取子集很便宜(只挪索引),但排序或重排要复制列数据。
1.4.3 Stage:reader / filter / writer 的统一基类
所有处理单元都继承 Stage,按职责分三类:
- Reader:入口。
readers.las、readers.copc、readers.ept、readers.gdal、readers.text… - Filter:中间处理。
filters.crop、filters.reprojection、filters.smrf、filters.outlier… - Writer:出口。
writers.las、writers.gdal、writers.copc、writers.text…
每个 stage 只做三件事:声明需要哪些维度、准备资源、处理点。stage 之间通过 PointView 连接,所以任意组合都不需要事先约定数据格式。
2.10.0 的 stage 规模(按官方文档页统计):
表 1-3 PDAL 2.10.0 的 stage 规模
| 类别 | 文档页数 | 代表 stage |
|---|---|---|
| readers | 38 | las、copc、ept、gdal、text、e57、ply、pcd |
| filters | 90 | crop、reprojection、range、outlier、smrf、hag_nn、hexbin |
| writers | 23 | las、copc、gdal、text、ply、ogr |
注意“文档页数 ≠ 默认安装可用数”。部分 stage 依赖第三方库(HDF5、TileDB、Matlab、E57、PostgreSQL、Arrow),编译时没开就用不了,以 pdal --drivers 的实时输出为准。
1.5 Stage 从哪里来:静态注册与插件
PDAL 的 stage 有两种存在形式。绝大多数常用 stage 是静态注册的,直接编进 libpdalcpp 共享库(Windows 上是 pdalcpp.dll);其余做成插件——独立共享库,运行时加载。
插件命名有硬约定:
libpdal_plugin_<type>_<name>.<ext>
│ └─ 名字须以字母或数字开头,后续可用字母、数字、下划线
└─ reader | writer | filter | kernel
查找顺序:先找 PDAL 库所在目录,再找相对当前工作目录的 .、./lib、../lib、./bin、../bin。要加自定义路径,用环境变量:
# Linux / macOS
export PDAL_DRIVER_PATH=/opt/pdal-plugins:/usr/local/lib
# Windows (cmd)
set PDAL_DRIVER_PATH=D:\pdal-plugins;D:\PDAL\bin
分隔符在 Windows 是 ;,其他平台是 :。写错不会报错,插件只是静默不加载:pdal --drivers 里看不到那个 stage,pipeline 运行时报 Couldn't create stage,错误信息不会告诉你是路径没配对。
1.6 两种执行模式:standard 与 stream
这是 PDAL 架构里对内存和性能影响最大的设计,也是最常被忽略的。
standard(标准)模式:先把全部点读进内存形成完整 PointTable,再按 stage 顺序处理。需要全局视图的算法只能在标准模式运行——排序、全局统计量、需要遍历全部点才能定阈值的算法。
stream(流式)模式:点被切成块,逐块穿过整条流水线。内存占用与总点数基本无关,只与块大小和 filter 内部缓存有关。
pdal translate 与 pdal pipeline 默认尽量用流式,做不到就静默回退到标准模式。想强制指定:
pdal translate input.las output.laz --stream # 要求流式,做不到就退出
pdal translate input.las output.laz --nostream # 强制标准模式
想提前判断一条 pipeline 能不能流式,用 pdal pipeline --validate:它只校验不处理,并报告该 pipeline 是否可流式。
差距有多大?以 10 亿点为例,仅 X/Y/Z 三个 double 维度就是 24 GB,再加上强度、回波、颜色,很容易突破 40 GB;流式模式的稳态内存通常是几百 MB 到几 GB。所以“大文件处理不动”的抱怨,九成答案在模式选择上。
流式不是免费的:块边界会切断邻域关系。filters.smrf 要栅格化全局点云、filters.outlier 要全局统计量,它们必然不是流式。反过来,filters.crop、filters.range、filters.assign、filters.reprojection 都是逐点可算的,可以安全流式。选 stage 时先问一句:这个算法需要看见全部点吗?
1.7 跑通第一条 pipeline
不依赖任何数据文件也能验证环境——readers.faux 可以造点。保存为 hello.json:
[
{
"type": "readers.faux",
"mode": "ramp",
"bounds": "([0,99],[0,99],[0,9])",
"count": 100
},
{
"type": "writers.las",
"filename": "hello.las"
}
]
pdal pipeline hello.json
pdal info --summary hello.las
readers.faux 的 ramp 模式把 count 个点从 bounds 最小值均匀铺到最大值,步长为 (max-min)/(count-1)(见 FauxReader::initialize())。因此 100 个点在 X/Y 上正好落在 0、1、2 … 99,这份数据是确定性的——同一份 JSON,谁跑结果都一样,适合当测试夹具。
pdal info --summary 的字段结构(键名来自 QuickInfo::dumpSummary(),挂在 summary 下):
{
"filename": "hello.las",
"pdal_version": "2.10.0 (git-version: Release)",
"summary": {
"num_points": 100,
"bounds": { "minx": 0, "miny": 0, "minz": 0,
"maxx": 99, "maxy": 99, "maxz": 9 },
"dimensions": "X, Y, Z, OffsetTime"
},
"reader": "readers.las",
"now": "2026-…",
"file_size": 2187
}
dimensions 里出现了 pipeline 没声明过的 OffsetTime,这不是多余的字段:维度集合由 reader 声明,filter 与 writer 只增不减。
三种 info 模式要分清:不带选项等价于 --stats(扫全部点,O(N));--summary 只读文件头(大文件上是 O(1));--all 同时输出 stats、schema 和 metadata。
1.8 PDAL 在点云生态中的位置
采集 索引 / 服务 处理 应用
┌──────────┐ ┌────────────────────┐ ┌──────────────┐ ┌──────────────┐
│ 机载 LiDAR │ │ Entwine → EPT │ │ │ │ Potree │
│ 车载 / 背包│──▶│ Untwine → COPC/EPT │──▶│ PDAL │──▶│ CloudCompare │
│ 三维扫描 │ │ COPC(单文件) │ │ │ │ Cesium │
└──────────┘ └────────────────────┘ └──────┬───────┘ └──────────────┘
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
PostgreSQL GeoTIFF / DEM 文本 / JSON
(pgpointcloud) (writers.gdal) (writers.text)
几个关键关系:
- Entwine / EPT:Hobu 的点云索引工具,把海量 LAS 组织成八叉树,PDAL 通过
readers.ept读取,可带bounds、resolution做按需加载。EPT 读是流式的,适合几百 GB 级别的在线处理。 - Untwine:同样是索引工具,但采用自底向上的构建方式,产出 COPC 或 EPT。
- COPC:Cloud Optimized Point Cloud,本质是 LAS 1.4 + LAZ + 空间聚簇八叉树,单文件即可支持 HTTP Range 按块读取。PDAL 提供
readers.copc/writers.copc,也能直接读远程 URL。 - PCL:互补而非替代。PCL 偏机器人、实时与算法研究;PDAL 偏格式抽象与工程流水线。两者通过 PCD 格式互通。
- LAStools:功能重叠最大。命令行工具成熟,但许可模式不同;PDAL 用声明式 JSON 组织流程、可嵌入 C++/Python,更适合做产品化的批处理系统。
- python-pdal:官方 Python 绑定,把 pipeline 结果暴露为 NumPy 数组。2.1 之后,Python 相关能力(嵌入的
readers.numpy/filters.python与扩展本身)统一从 PyPI 分发。
1.9 常见误解与真实报错
误解一:PDAL 与 GDAL 二选一。 不对,PDAL 强依赖 GDAL。源码里 cmake/gdal.cmake 要求 GDAL ≥ 3.8,cmake/proj.cmake 要求 PROJ ≥ 9.0,cmake/geotiff.cmake 要求 GeoTIFF ≥ 1.7.0,cmake/curl.cmake 把 curl 列为 REQUIRED。装 PDAL 等于装一整套 GDAL 依赖栈。
误解二:装了 PDAL 就有全部 stage。 见 1.5,先跑 pdal --drivers 再谈“文档里有”。
误解三:pipeline 里 stage 顺序无所谓。 有所谓,而且有语义:stage 按数组顺序执行;不写 inputs 时使用默认输入列表,写了 inputs 就用指定 tag 的输出并替换默认列表。乱序会得到“后面 stage 读到了不该读的数据”这类很难排查的结果。
误解四:--stream 一定更快。 不一定。流式省内存,但块边界会让某些算法退化;小文件上标准模式没有明显额外开销。
真实报错与修法:
| 报错 / 现象 | 原因 | 修法 |
|---|---|---|
Command 'xxx' not recognized |
子命令拼错,或该 kernel 未编译 | pdal --list-commands 查可用命令 |
Couldn't create stage xxx |
stage 名错,或插件未加载 | pdal --drivers 确认;检查 PDAL_DRIVER_PATH |
Unable to open file |
路径、权限或远程协议不支持 | 先用绝对路径;远程 URL 需要编译时带 curl |
| 输出文件 0 个点 | 上游 filter 把点全过滤了 | 删 stage 二分定位;pdal info --stats 看分布 |
| Z 精度到厘米就停 | LAS 默认 scale_z = .01 |
writer 显式设 scale_z,或用 offset_z: "auto" |
1.10 生产建议
锁版本。生产镜像里钉住 minor 版本,并把 pdal --version 的输出写进部署记录。2.7 → 2.8 那次 writers.las.minor_version 默认值变化说明小版本也会改行为。
能流式就别标准模式。挑逐点可算的 filter 组流水线,把必须全局的(地面分类、离群点)放到最小必要位置。先用 pdal pipeline --validate 验证再跑。
少落地中间文件。一条 pipeline 干完,比“转格式 → 裁切 → 再转格式”少好几轮磁盘 I/O,也避免中间文件的坐标系与 scale 被意外改写。
pipeline 进版本库。JSON 就是处理配方;配合 --pipeline-serialization(pdal pipeline)或 --pipeline(pdal translate)把实际执行的 stage 参数存档,出问题可复现。
远程数据优先 COPC/EPT。两者都支持带 bounds/resolution 的降采样读取,比“先下载 40 GB 再裁”快一个数量级,这几乎是 PDAL 相对本地工具链最实用的优势。
1.11 小结
表 1-4 本章要点
| 主题 | 关键事实 |
|---|---|
| 定位 | 点云版 GDAL:统一读写抽象 + 流水线框架,BSD 3-Clause |
| 版本基线 | 2.10.0;小版本可能改默认值(LAS minor_version 2→4) |
| 核心抽象 | Dimension(132 个标准维度)、PointView(可多视图)、Stage |
| 规模 | 文档口径 readers 38 / filters 90 / writers 23,实际以 pdal --drivers 为准 |
| 插件 | libpdal_plugin_<type>_<name>,路径由 PDAL_DRIVER_PATH 追加 |
| 执行模式 | standard 全内存、stream 分块;默认自动择优,--stream/--nostream 强制 |
| 编译依赖 | GDAL ≥ 3.8、PROJ ≥ 9.0、GeoTIFF ≥ 1.7.0、curl |
| 排错入口 | --drivers、--list-commands、pipeline --validate、info --summary |
下一章进入数据层:LAS/LAZ 的文件头字段、point format 0–10 分别携带什么、VLR 与 extra bytes 如何映射成 PDAL 的维度,以及为什么从 LAS 读出来的维度数量常常比“标准字段”多。