第04章 - 命令行工具与日常操作
4.1 pdal 命令的整体形状
pdal 只有一个可执行文件,所有功能通过子命令分发。这和 GDAL 用一堆独立程序(gdal_translate、gdalwarp…)的做法不同:PDAL 的子命令(源码里叫 kernel)是同一个二进制里注册的对象,所以它们共享日志、选项解析与 pipeline 引擎。
命令行形态只有两种:
pdal <全局选项>
pdal <命令> <命令选项>
全局选项定义在 apps/pdal.cpp 的 addArgs() 里:
| 选项 | 作用 |
|---|---|
--version |
打印版本与 git sha |
--drivers |
列出可用 stage;加 --showjson 输出 JSON |
--options <stage> / --options all |
打印某个(或全部)stage 的选项表 |
--list-commands |
列出可用子命令 |
--help, -h |
帮助 |
--verbose, -v <0-8> |
日志级别 |
--log <target> |
日志目标,接受 stderr、stdout、stdlog、devnull |
--logtiming |
日志带时间戳 |
--verbose 的值域是 0–8,默认 None。排错时用 3–5 级最合适:能看到 stage 的准备与统计信息,又不至于被逐点日志淹没。--debug 是旧写法,等价于把级别设为 Debug,源码注释里已标 “option deprecated”。
--log 默认是 stderr,所以 pdal info a.las > a.json 不会让日志污染 JSON。要彻底静音用 --log devnull。
4.2 pdal info:最常用的探针
info 是使用频率最高的子命令,因为它是唯一“不写数据、只读状态”的工具。选项(依据 doc/apps/info.md 与 kernels/InfoKernel.cpp):
| 选项 | 作用 | 开销 |
|---|---|---|
--summary |
点数、范围、维度表、SRS | 只读文件头,O(1) |
--stats |
各维度 min/max/mean/count | 扫全部点,O(N) |
--metadata |
reader 元数据(LAS 头字段、VLR 等) | 读头 + 部分扫描 |
--schema |
维度 schema(名字/类型/大小) | 读头 |
--all |
stats + schema + metadata | O(N) |
--point, -p <range> |
打印指定点,如 -p "1-5,10,100-200" |
只读需要的点 |
--query x,y[,z][/n] |
按距离取最近 n 个点 | 需要空间索引 |
--boundary |
计算点云轮廓(六边形) | O(N) |
--dimensions <list> |
限定统计维度 | 减少计算 |
--enumerate <list> |
枚举维度取值及计数 | O(N) |
--pipeline-serialization <file> |
导出 info 构造的 pipeline | — |
--stdin, -s |
从标准输入读 pipeline | — |
不带任何选项时默认等价于 --stats。
4.2.1 summary 与 stats 的取舍
--summary 走 QuickInfo,只读文件头,对 40 GB 文件也是秒回。输出只有四块:num_points、bounds(minx…maxz)、srs(有效时)、dimensions(逗号分隔字符串)。
--stats 必须扫完全部点,代价是 O(N)。价值在于看分布而非范围——分类值有哪些、各有多少、强度是否异常:
pdal info --stats --dimensions Classification,Intensity input.las
--enumerate 更进一步,直接给出每个取值的个数,检查分类是否合规最有效:
pdal info --stats --enumerate Classification input.las
4.2.2 按坐标查最近点
--query 的参数形状是 X,Y[,Z][/count]:
# 取平面距离 (636601.87, 849018.59) 最近的 5 个点
pdal info input.las --query "636601.87,849018.59/5"
# 带 Z 时按三维距离
pdal info input.las --query "636601.87,849018.59,425.10/1"
排查“某个位置的点为什么异常”时很省事,不用写代码。
4.2.3 输出是 JSON,就按 JSON 用
pdal info 的输出是 JSON,根节点含 filename、pdal_version、summary/stats/metadata/schema、reader、now、file_size。可以直接被 jq 或 Python 消费:
# 取点数
pdal info --summary input.las | jq '.summary.num_points'
# 批量统计一批文件的点数
for f in *.laz; do
printf '%s ' "$f"
pdal info --summary "$f" | jq '.summary.num_points'
done
注意 pdal_version 字段有历史包袱:文档示例里是 PDAL 1.0.0.b1 (116d7d) with GeoTIFF 1.4.1 GDAL 1.11.2 LASzip 2.2.0 这种长串,当前源码写的是 Config::fullVersionString(),形如 2.10.0 (git-version: Release)。解析时按字符串处理,别假设固定格式。
4.3 pdal translate:一行命令做转换
translate 是最短路径的格式转换工具:
pdal translate [options] input output [filter ...]
input、output 必填;filter 是位置参数,可以给多个,按顺序串起来:
# 裁切 + 重投影 + 压缩,一条命令
pdal translate input.las output.laz \
filters.crop filters.reprojection \
--filters.crop.bounds="([500000,501000],[4000000,4001000])" \
--filters.reprojection.out_srs="EPSG:4326"
| 选项 | 作用 |
|---|---|
--input, -i / --output, -o |
输入/输出文件名 |
--filter, -f |
指定 filter 类型,可重复 |
--reader, -r / --writer, -w |
显式指定 reader/writer,默认按扩展名推断 |
--json |
从一段 pipeline JSON 里提取 filter(reader/writer 由命令行替换) |
--pipeline, -p <file> |
只输出由命令行构造的 pipeline,不执行 |
--metadata, -m <file> |
把执行元数据写到文件 |
--dims <list> |
限定加载维度;X/Y/Z 总是加载 |
--stream / --nostream |
强制流式 / 标准模式 |
--pipeline 是最被低估的选项。它允许你用一行命令拼出若干 filter,然后把等价的 pipeline JSON 导出来:
pdal translate input.las output.laz -f range \
--filters.range.limits="Classification[2:2]" \
-p out.json
cat out.json
导出的 JSON 能存进版本库,交给 pdal pipeline 重复执行。这条路径把“临时调试命令”沉淀成“可审计的处理配方”,比手写 JSON 快且不易错。
--json 方向相反:从现成 pipeline 里取 filter,用命令行的 input/output 覆盖 reader/writer。文档写明两条限制——不能与命令行 filter 同时用,也不能与 --filter 同时用。
4.4 pdal pipeline:正式批处理入口
translate 适合一次性转换,生产形态是 pdal pipeline:
pdal pipeline jobs/dem.json
| 选项 | 作用 |
|---|---|
--input, -i |
pipeline 文件名(也可作位置参数) |
--dims <list> |
限定加载维度,X/Y/Z 恒加载 |
--validate |
只校验不执行,并报告是否可流式 |
--progress <file> |
进度输出文件或 FIFO(必须已存在) |
--pipeline-serialization <file> |
输出实际执行的 pipeline |
--metadata <file> |
输出执行元数据 |
--stdin, -s |
从标准输入读 pipeline |
--stream / --nostream |
强制模式 |
4.4.1 先 validate,再运行
--validate 只做两件事:校验 JSON 与 stage 可创建性,判断能否流式。它不碰数据,对大 pipeline 也只是几秒钟:
pdal pipeline --validate jobs/big.json
CI 里加上这一步,能挡住“stage 名拼错”“插件缺失”“不能流式却按流式提交”三类低级故障。
4.4.2 用 FIFO 看进度
--progress 接受文件或命名管道,且要求目标已存在,PDAL 不会代建。Linux 上实时看进度:
mkfifo /tmp/pdal-progress
pdal pipeline big.json --progress /tmp/pdal-progress &
cat /tmp/pdal-progress
Windows 没有 FIFO,给普通文件,任务结束后读内容。
4.4.3 命令行覆盖 pipeline 选项
pdal pipeline 支持 --<stage类型>.<选项>=<值> 形式的替换:
pdal pipeline translate.json \
--readers.las.filename=input.las \
--writers.las.filename=output.laz
同类型 stage 有多个时,这种写法会同时覆盖全部。要精确命中某一个,用它自己的 tag:
pdal pipeline multi.json \
--stage.las1.filename=file1.las \
--stage.las2.filename=file2.las
文档写明优先级:按 tag 指定的选项覆盖按 stage 类型指定的选项。两者混用时 tag 那条说了算。
4.4.4 从标准输入喂 pipeline
调度系统里常常不想落地 JSON 文件,用 --stdin 配 heredoc:
pdal pipeline --stdin <<'EOF'
[
"input.las",
{ "type": "filters.range", "limits": "Classification[2:2]" },
"ground.las"
]
EOF
引号很重要:<<'EOF' 阻止 shell 展开变量,pipeline 里若含 $ 之类的片段必须这么写。
4.5 合并、切分与瓦片
pdal merge 负责最朴素的合并:多个文件合成一个。它不做任何滤波与重投影,要处理数据请用 pipeline。
pdal merge a.las b.laz c.bpf out.laz
最后一个参数是输出文件,其余都是输入。输入格式可以互不相同,输出格式按扩展名推断。
pdal split 反向操作,把一个文件拆成多个:
# 按容量拆:每个文件不超过 10 万点
pdal split --capacity 100000 infile.laz outfile.bpf
# 按瓦片边长拆,并指定原点
pdal split --length 500 --origin_x 0 --origin_y 0 infile.laz out.bpf
两条关键行为(doc/apps/split.md):
--length与--capacity都不给时,隐式按capacity = 100000处理;- 输出参数是模板。给
file.ext会产出file_1.ext、file_2.ext;给以路径分隔符结尾的目录,则用输入文件名拼模板。split 不创建目录,目录必须预先存在。
pdal tile 与 split 类似,差异在四处:
- 用流式模式,内存占用受限;
- 输出文件名必须含
#占位符,替换成瓦片的 X/Y 索引(out0_0.las、out1_0.las); - 支持
--out_srs先统一重投影,保证各瓦片坐标系一致; - 输入文件名支持 glob,一次处理多个文件。
pdal tile "data/*.laz" "tiles/tile_#.laz" --length 1000 --buffer 50 --out_srs EPSG:25832
--buffer 给每块加重叠,做接边时必用;--length 默认 1000。
pdal sort 按 XY 做 Morton 排序(内部用 filters.mortonorder),让空间相邻的点在文件里也相邻,提高后续分块读取的局部性:
pdal sort input.las sorted.las
pdal sort --compress -m input.las sorted.laz
--compress, -z 压缩输出,--metadata, -m 转发元数据(VLR、头字段)。写 COPC 前先排序是常见的性能习惯,因为 COPC 要求点按空间聚簇排列。
4.6 pdal tindex:给一堆点云建空间索引
文件多了,第一个问题永远是“哪几个文件盖住了这个范围”。tindex 借鉴 GDAL 的 gdaltindex:
# 建索引
pdal tindex create tiles.gpkg "data/tiles/*.laz"
# 用索引按范围提取点云
pdal tindex extract --tindex tiles.gpkg \
--bounds "([500000,505000],[4000000,4005000])" merged.las
建索引时的关键选项:
| 选项 | 作用 |
|---|---|
--tindex <file> |
输出的 OGR 可写索引(GeoPackage、Shapefile…) |
--glob <pattern> |
要索引的文件通配 |
--filelist <file> |
从文本文件读文件清单(每行一个) |
--stdin, -s |
从标准输入读文件清单 |
--fast_boundary |
用矩形范围代替精确轮廓,快很多 |
--lyr_name / --tindex_name |
OGR 图层名 / 索引列名 |
--ogrdriver, -f |
OGR 驱动名 |
--lco |
图层创建选项 NAME=VALUE |
--t_srs |
索引的目标 SRS |
--a_srs |
给没有 SRS 的文件赋一个 |
--write_absolute_path |
写绝对路径而非相对路径 |
--skip_different_srs |
拒绝 SRS 不一致的文件 |
--threads |
计算边界时的线程数 |
--simplify / --threshold / --resolution |
精确轮廓的简化与六边形阈值 |
--where |
只处理满足表达式的点 |
--fast_boundary 与精确轮廓是性能分水岭:前者只读文件头拿矩形范围,几百个文件几秒完成;后者要扫点算六边形边界,慢一到两个数量级,但能避免“矩形重叠而数据不重叠”的误命中。文件上万时先用 --fast_boundary 建粗索引,需要时再对子集做精确边界。
4.7 已废弃的子命令
源码里有一批 kernel 仍能注册、但描述字符串已带 [DEPRECATED]:
| 命令 | 状态 | 替代做法 |
|---|---|---|
pdal ground |
已标废弃 | JSON pipeline:assign → outlier → smrf → expression |
pdal density |
已标废弃 | filters.hexbin 的 density 选项 |
pdal delta |
已标废弃 | Python + NumPy 计算 |
pdal eval |
已标废弃 | 同上,自算混淆矩阵 |
pdal chamfer |
已标废弃 | Python + SciPy/NumPy |
pdal hausdorff |
已标废弃 | Python + SciPy |
pdal random |
已标废弃 | readers.faux 的 normal/uniform 模式 |
文档写的是“2.7 从默认安装移除、2.8 完全移除”,但 2.10.0 源码里这些 *Kernel.cpp 仍在,仍通过 CREATE_STATIC_KERNEL 注册,所以 pdal --list-commands 可能还能看到它们。不要在新流程里用;改用等价 pipeline,行为可控且可版本化。
以 pdal ground 为例,它内部做的事在 GroundKernel::execute() 里写得很直白:可选 filters.assign 把 Classification 归零、可选 filters.outlier 去噪、然后 filters.smrf、可选 filters.range 抽出 Classification[2:2]、最后写文件。等价 JSON(官方文档给出的版本):
[
"input.laz",
{ "type": "filters.assign", "value": "Classification = 0" },
{ "type": "filters.outlier" },
{
"type": "filters.smrf",
"window": 18.0,
"threshold": 0.5,
"slope": 0.15,
"cell": 1.0,
"cut": 0.0,
"scalar": 1.25,
"returns": "last, only"
},
{ "type": "filters.expression", "expression": "Classification==2" },
"output.laz"
]
注意参数名:filters.smrf 的选项是 window,而 pdal ground 的命令行参数是 --max_window_size,内部映射到 window。这种“命令行参数名 ≠ stage 选项名”的情况在废弃命令里很常见,迁移时要逐个核对。
4.8 排错与踩坑
| 现象 | 原因 | 处理 |
|---|---|---|
Command 'xxx' not recognized |
子命令拼错或未编译 | pdal --list-commands |
Unexpected argument 'file.json' |
把 pipeline 文件当成全局参数 | 必须写 pdal pipeline file.json |
--progress 报文件不存在 |
PDAL 不创建进度文件 | 先 touch 或 mkfifo |
pdal split 报目录不存在 |
split 不建目录 | 先 mkdir -p |
pdal tile 报缺少占位符 |
输出名没有 # |
改成 "out_#.laz" |
translate 输出 0 点 |
filter 条件过严 | 去掉 filter 逐步二分 |
--json 与 filter 冲突 |
两者不能同时用 | 二选一 |
| 覆盖选项不生效 | 被 tag 形式的选项盖掉 | 检查有无 --stage.<tag>.* |
| 输出点数与输入不一致 | 文件头点数与实际不符 | pdal info --stats 核对 |
几条实操建议:
先 --validate 再跑批。 批量循环里一条 pipeline 出错会中断整轮,提前校验比事后补跑便宜得多。
translate 用完就导 pipeline。 调试期命令行最快,定稿时用 -p out.json 导出,之后统一走 pdal pipeline。命令行的便利与 JSON 的可审计性都拿到了。
别用高 --verbose 跑大数据。 6 级以上会输出大量逐块信息,重定向到文件能把磁盘写满。
日志与数据分流。 --log 默认 stderr 是好设计,别用 2>&1 把日志混进 JSON 输出。
4.9 小结
| 主题 | 关键事实 |
|---|---|
| 命令形态 | pdal <全局选项> 或 pdal <kernel> <选项>,kernel 是注册在同一二进制里的子命令 |
| 全局选项 | --version/--drivers/--options/--list-commands/--verbose 0-8/--log |
| info | 默认等价 --stats;--summary 只读头(O(1)),--stats 全扫(O(N)) |
| translate | 位置参数接 filter;--pipeline 导出等价 JSON;--json 反向复用 pipeline |
| pipeline | --validate、--progress(文件须已存在)、--stdin、--pipeline-serialization |
| 选项替换 | --<stage>.<opt>=<val> 覆盖全部同类 stage;--stage.<tag>.<opt> 只覆盖单个且优先级更高 |
| 批处理 | merge 合并;split 按容量或边长拆(默认 capacity 100000);tile 需 # 占位 |
| 索引 | tindex create / tindex extract;--fast_boundary 只读头求范围 |
| 废弃命令 | ground/density/delta/eval/chamfer/hausdorff/random 已标 DEPRECATED,改用 pipeline |