第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):

  1. --length 与 --capacity 都不给时,隐式按 capacity = 100000 处理;
  2. 输出参数是模板。给 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