第07章 - Writers 写入器详解
7.1 writer 决定数据离开 pipeline 时的样子
writer 是 pipeline 的出口,也是唯一会改变数据物理形态的 stage。它做三件事:决定容器格式、决定写哪些维度、决定文件头里的元信息(版本、坐标系、scale、压缩)。
这三件事都可能悄悄改坏数据。scale 与 offset 配错,坐标精度就没了;dataformat_id 选错,颜色就丢了;forward 没给,源文件的头信息全被换成了默认值。本章按“最常用 → 次常用 → 特殊用途”的顺序讲。
7.2 writers.las:必须吃透的一个
writers.las 是使用率最高的 writer,选项也最多。核心参数:
| 选项 | 默认 | 说明 |
|---|---|---|
filename |
必填 | 输出文件名,可含 # 占位符 |
minor_version |
4 | LAS 次版本(1–4);PDAL 1.0–2.7 默认是 2 |
dataformat_id |
7 | point format,默认 7 需要 LAS 1.4 |
compression |
false | true 输出 LAZ |
scale_x/y/z |
.01 | 坐标缩放;可写 auto |
offset_x/y/z |
0 | 坐标偏移;可写 auto |
a_srs |
无 | 写入文件的坐标系(EPSG 或 WKT) |
forward |
无 | 从源 LAS 继承头字段(详见下) |
extra_dims |
无 | 额外维度列表,all 表示全部 |
pdal_metadata |
false | 把 metadata 与 pipeline 写成 VLR |
vlrs |
无 | 以 JSON 追加自定义 VLR |
discard_high_return_numbers |
false | 丢弃超出 point format 上限的回波号 |
filesource_id |
0 | File Source ID |
system_id |
“PDAL” | 生成系统字符串 |
software_id |
PDAL 版本号 | 生成软件字符串 |
creation_doy / creation_year |
无 | 创建日期 |
global_encoding |
0 | 全局编码标志;1.4 输出恒置 bit 4 |
project_id |
全零 UID | 项目 UUID |
minor_version 与 dataformat_id 的组合是最容易出错的地方。默认值 4 + 7 意味着:不写任何选项时,你得到的是 LAS 1.4 + point format 7(带 GPS 时间与 RGB)。很多老软件读不了 1.4,兼容性要求下必须显式降级:
[
"input.laz",
{
"type": "writers.las",
"filename": "legacy.las",
"minor_version": 2,
"dataformat_id": 3,
"compression": false
}
]
minor_version: 2 时 point format 只能取 0–5,写 7 会触发 LAS version '1.2' only supports point formats 0-5.。
7.2.1 scale 与 offset:精度就藏在这六个数字里
公式第 2 章讲过,这里说选值策略。默认 scale = .01 是厘米级,适合投影坐标下的地形数据。三类例外:
| 场景 | scale 建议 | 原因 |
|---|---|---|
| 投影坐标(UTM 等) | 0.01 或 0.001 | 厘米/毫米精度,够用且省字节 |
| 地理坐标(经纬度) | 1e-7 | 1e-7 度约 1.1 cm;用 0.01 会退化成公里级 |
| 超高精度扫描 | 1e-5 以下 | 视仪器标称精度而定 |
offset 用 auto 基本总是对的:它把偏移设成该维度的最小值,从而让 int32 存盘值落在 0 附近,避免溢出。
[
"input.laz",
{
"type": "writers.las",
"filename": "out.las",
"scale_x": 0.001,
"scale_y": 0.001,
"scale_z": 0.001,
"offset_x": "auto",
"offset_y": "auto",
"offset_z": "auto"
}
]
7.2.2 forward:继承源文件头
forward 是转格式时最该用的选项。它接受一组字段名:
major_version、minor_version、dataformat_id、filesource_id、global_encoding、project_id、system_id、software_id、creation_doy、creation_year、scale_x/y/z、offset_x/y/z
还有三个特殊值:
header:除 scale/offset 外的全部头字段;scale、offset:分别转发缩放与偏移组;all:等价于header+scale+offset+vlr;vlr:转发 VLR。
[
"input.las",
{
"type": "writers.las",
"filename": "copy.laz",
"compression": "true",
"forward": "all"
}
]
两个限制必须知道:
- 显式写的选项优先于转发值。 所以
forward: "all"与"minor_version": 2并存时,输出版本是 2。 - 多源合并时,不一致的头字段会被丢弃并退回默认值。 三份 LAS 的 scale 各不相同,
forward: "scale"就拿不到一致值,只能用默认.01。
VLR 转发的排除名单(文档写明):LASF_Projection、liblas、laszip encoded 三个 user_id 的 VLR 不转发;LASF_Spec 中除 record_id 0 和 3 之外的也不转发。这些是格式描述性 VLR,PDAL 会自己重建。
7.2.3 extra_dims 与 VLR
要写 LAS 标准字段之外的属性,必须用 extra_dims:
[
"input.laz",
{
"type": "filters.ferry",
"dimensions": "=>ScanAngle"
},
{
"type": "writers.las",
"filename": "out.las",
"extra_dims": "ScanAngle=float"
}
]
给它 "all" 会把所有放不进标准 point record 的维度都写进去。加 --verbose=Info 跑一遍能看到实际写了哪些维度、什么类型、什么顺序。
自定义 VLR 用 vlrs,支持三种数据来源(直接给 base64 data、给 filename、或用 metadata 引用某个元数据键):
[
"input.las",
{
"type": "writers.las",
"filename": "out.las",
"vlrs": [{
"description": "自定义记录",
"record_id": 42,
"user_id": "myorg",
"data": "dGhpcyBpcyBzb21lIHRleHQ="
}]
}
]
description 上限 32 字节,超了会被截断。
7.3 writers.gdal:从点云到栅格
这是 PDAL 里最“GIS”的 writer:把点云插值成栅格(DEM、DSM、强度图、覆盖计数图)。
[
"ground.laz",
{
"type": "writers.gdal",
"filename": "dem.tif",
"resolution": 1.0,
"output_type": "min",
"gdaldriver": "GTiff",
"dimension": "Z"
}
]
7.3.1 六个统计量对应六个波段
output_type 决定写几个波段,默认 all:
| 值 | 波段内容 |
|---|---|
min |
邻域内点的最小值 |
max |
最大值 |
mean |
平均值 |
idw |
Shepard 反距离加权值 |
count |
落入邻域的点数 |
stdev |
总体标准差 |
all |
以上六个全部(默认) |
所以不写 output_type 会得到一个 6 波段 TIFF,而不是单波段 DEM。做 DEM 时必须显式指定:
# 地面点做 DEM 用 min 最保守(避免被地物拉高)
pdal pipeline dem.json --writers.gdal.output_type=min
p<i> 形式还能写百分位波段,例如 output_type: "p5,p50,p95";百分位只在 binmode: true 时生效。
7.3.2 插值半径与分辨率的耦合
resolution 是像元边长,必填。radius 是参与计算的点到像元中心的最大距离,默认值是 resolution × √2:
| 选项 | 默认 | 说明 |
|---|---|---|
resolution |
必填 | 像元边长,X/Y 单位 |
radius |
resolution × √2 | 参与插值的半径 |
power |
1.0 | IDW 的距离指数 |
window_size |
0 | 空像元的补救插值窗口(1 = 3×3,2 = 5×5) |
nodata |
视类型 | 默认 double/float/int/short 用 -9999,unsigned 用 9999,uchar 用 255 |
output_type |
all |
输出哪些统计波段 |
binmode |
false | true 时只用像元内的点,不做距离插值 |
data_type |
视驱动 | 输出栅格的数据类型 |
gdaldriver |
GTiff |
GDAL 驱动名 |
gdalopts |
无 | 传给驱动的 NAME=VALUE |
dimension |
Z |
用哪个维度做插值 |
bounds / origin_x / origin_y / width / height |
无 | 显式控制栅格范围 |
override_srs / default_srs |
无 | 坐标系覆盖 / 补位 |
metadata |
无 | 写入 GDAL 元数据 |
pdal_metadata |
false | 把 pipeline 与 metadata 写成 base64 元数据 |
allow_empty |
false | 无点也允许写出空栅格 |
radius 默认值有个陷阱,文档专门点出来了:当圆的半径覆盖不了整个像元时,有些点根本不会被考虑,哪怕它落在像元内部。分辨率越小、点越稀,这个问题越明显。稳妥做法是把 radius 显式设成不小于 resolution × √2,或者在点云密集时用 binmode: true。
7.3.3 空像元怎么补
默认 window_size = 0,即不补。空像元直接写 nodata。设成 1 会用周围 3×3 的非空像元做反距离加权;2 是 5×5,依次类推。
{
"type": "writers.gdal",
"filename": "dem_filled.tif",
"resolution": 1.0,
"output_type": "min",
"window_size": 3,
"nodata": -9999
}
window_size 不宜开大。 它本质是给空洞做插值,窗口越大越容易把远处地形特征带进来,在不连续区域(悬崖、建筑边缘)制造假地形。要填洞,正规做法是回点云里看为什么没有点。
7.4 writers.copc:给海量数据做服务化封装
COPC 是“能被 HTTP Range 读”的 LAZ 1.4。写它比写 LAS 慢,因为要先按八叉树重排点。
[
"sorted.laz",
{
"type": "writers.copc",
"filename": "cloud.copc.laz",
"threads": 10,
"forward": "all"
}
]
| 选项 | 默认 | 说明 |
|---|---|---|
filename |
必填 | 输出 COPC 文件 |
threads |
10 | 写入线程数 |
scale_x/y/z |
.01 | 与 writers.las 相同 |
offset_x/y/z |
0 | 可设 auto |
forward |
无 | 继承头字段与 VLR |
extra_dims |
无 | 额外维度 |
a_srs |
无 | 坐标系 |
pdal_metadata |
false | 写 metadata VLR |
pipeline |
无 | 把 pipeline JSON 写成 VLR |
vlrs |
无 | 自定义 VLR |
写之前先排序是常见做法:
pdal sort cloud.laz sorted.laz && pdal translate sorted.laz out.copc.laz -w writers.copc
forward 的字段列表比 writers.las 少一个 dataformat_id(COPC 固定 LAS 1.4),VLR 排除规则相同。
7.5 writers.text:调试与交付
文本 writer 有两个用途:调试点、给非 GIS 系统供数。
| 选项 | 默认 | 说明 |
|---|---|---|
filename |
必填 | 文件路径,"STDOUT" 表示标准输出 |
format |
csv |
csv 或 geojson |
precision |
3 | 小数位数 |
order |
无 | 输出列顺序,可逐列指定精度:"X:3,Y:5,Z:0" |
keep_unspecified |
true | 是否输出未在 order 里列出的维度 |
write_header |
true | 是否写表头 |
quote_header |
true | CSV 表头是否加引号 |
delimiter |
, |
CSV 分隔符 |
newline |
\n |
换行符(Windows 常用 \r\n) |
jscallback |
无 | GeoJSON 时用函数包裹输出 |
调试时最有用的是 format: "csv" + keep_unspecified: true:一次看到所有维度,比逐个 pdal info 快。
[
"input.las",
{
"type": "writers.text",
"filename": "debug.csv",
"order": "X:3,Y:3,Z:3,Classification,Intensity",
"keep_unspecified": false,
"precision": 2
}
]
keep_unspecified: false 是关键:默认 true 会把所有维度都写上,列数可能上百,反而看不清。
7.6 其他 writer 速查
| writer | 用途 | 关键选项 |
|---|---|---|
writers.null |
丢弃输出,只要副作用 | 无(所有选项被忽略) |
writers.raster |
写 filter 已生成的栅格 | rasters(波段名列表)、nodata、gdaldriver |
writers.ogr |
输出矢量点/多点 | ogrdriver、multicount(默认 1)、measure_dim、attr_dims |
writers.ply |
PLY 点/mesh | storage_mode(ascii/little endian/big endian)、faces、dims |
writers.pcd |
PCL 互通 | ASCII/binary/压缩 |
writers.bpf |
NGA BPF v3 | 不如 LAS 常用 |
writers.ept_addon |
给 EPT 追加维度 | 与 Entwine 配合 |
writers.null 值得单独提一句:它不是“没用”,而是只要副作用的写法。比如用 filters.hexbin 生成轮廓文件、用 pdal_metadata 导出元数据,都不需要输出点云:
[
"input.las",
{
"type": "filters.hexbin",
"boundary": "hull.geojson"
},
"writers.null"
]
注意最后一行不能简写成字符串:读者靠扩展名推断 driver,而 writers.null 没有对应扩展名,必须显式写成 {"type": "writers.null"}。
7.7 多输出:# 占位符与多 writer 串联
第 5、6 章已从 pipeline 角度讲过 #,这里补 writer 侧的细节。
writers.las、writers.copc 之外的多数 writer 都支持 filename 里的单个 #:
[
"input.laz",
{ "type": "filters.splitter", "length": 500 },
"tiles/tile_#.las"
]
规则在 writers.las 文档里写得很明确:
- 输入是多个 PointView 且文件名含
#→ 每个 view 写一个文件,#替换成从 1 开始递增的整数; - 输入是多个 PointView 但文件名不含
#→ 所有 view 聚合成一个文件; - 只有一个 PointView → 正常写一个文件。
第二条规定是最容易“看起来正常、其实错了”的场景:本来想切 20 个瓦片,结果只得到 1 个合并文件,数据没丢,但瓦片结构没了。
想一个 pipeline 出两种格式,就串两个 writer:
[
"input.laz",
{ "type": "filters.range", "limits": "Classification[2:2]" },
{ "type": "writers.las", "filename": "ground.laz", "compression": "true" },
{ "type": "writers.gdal", "filename": "dem.tif", "resolution": 1.0, "output_type": "min" }
]
这是文档认可的形态(Writer → Writer)。但不能分叉:两个 writer 各自接同一个 filter 的写法会触发 “Pipeline has multiple leaf nodes”,只有第一个分支会执行。
7.8 排错
| 现象 | 原因 | 处理 |
|---|---|---|
LAS version '1.2' only supports point formats 0-5. |
版本与 point format 不匹配 | 升 minor_version 到 4,或降 dataformat_id |
| 输出文件打不开 / 其他软件报错 | 1.4 与老软件不兼容 | 显式 minor_version: 2、dataformat_id: 3 |
| 颜色丢了 | point format 不含 RGB(0/1/6) | 用 2/3/7/8 |
| 坐标只剩厘米/公里级精度 | scale 与坐标系不匹配 | 投影坐标 0.01/0.001,地理坐标 1e-7 |
| 坐标溢出或整体偏移 | offset 没设 |
offset_*: "auto" |
| 输出 DEM 是 6 波段 | 没写 output_type |
显式 output_type: "min" |
| DEM 有大片 nodata | 点太稀或 radius 太小 |
调大 radius,或 window_size 补洞 |
| DEM 出现不真实的台阶 | window_size 太大,跨地形插值 |
调小 window_size,优先补点云空洞 |
forward 没生效 |
多源头字段不一致 | 显式写目标头字段,别依赖转发 |
| extra dims 写了但读不回来 | 1.0–1.3 文件需 use_eb_vlr |
读侧加 "use_eb_vlr": true |
| 瓦片没分开 | 文件名缺 # |
加占位符 |
writers.null 报无法推断 writer |
用了字符串形式 | 显式写 {"type": "writers.null"} |
其中前两条最值得留意。默认 1.4 + format 7 对现代工具链没问题,但项目一旦要交付给只用老版本 ArcGIS 或某些国产软件的甲方,就必须降级。降级会连带丢 NIR、丢 15 次回波表达、可能丢分类码,要让需求方确认能接受哪些损失,别默默转换。
7.9 生产建议
写侧参数写全,不依赖默认值。 minor_version、dataformat_id、scale_*、offset_* 四项在每条生产 pipeline 里都显式写。默认值会随 PDAL 版本变,写全了才能保证半年后重跑结果一致。
转格式一律带 forward。 至少要 forward: "scale",否则坐标精度会在每次转换中被重新量化一遍。
DEM 生产链路固定为:分类 → 抽地面 → min 插值。 顺序错了结果就不可比:
[
"input.laz",
{ "type": "filters.smrf" },
{ "type": "filters.expression", "expression": "Classification == 2" },
{ "type": "writers.gdal", "filename": "dem.tif", "resolution": 1.0, "output_type": "min" }
]
归档用 LAZ,发布用 COPC,交付用 LAS。 三种目的不同:LAZ 省空间、COPC 支持按需取块、LAS 兼容性最好。别用同一个文件应付三种场景。
打开 pdal_metadata。 尤其是交付给外部单位的成果数据,VLR 里带着处理历史,能省掉大量扯皮。
7.10 小结
| 主题 | 关键事实 |
|---|---|
| writers.las 默认 | minor_version=4、dataformat_id=7、scale=.01、offset=0、不压缩 |
| 兼容性降级 | 1.2 + format 3 最通用;1.4 才能用 6/7/8 |
| scale 选择 | 投影 0.01/0.001;地理 1e-7;offset: auto 防溢出 |
| forward | 继承头字段与 VLR;显式选项优先;多源不一致时退回默认 |
| extra_dims | <name>=<type> 或 all;写 extra bytes VLR(LASF_Spec/4) |
| writers.gdal | resolution 必填;output_type 默认 all(6 波段);radius 默认 resolution×√2 |
| writers.gdal 补洞 | window_size 默认 0;1 = 3×3,2 = 5×5;开大易造假地形 |
| writers.copc | LAS 1.4 固定;threads 默认 10;先 sort 再写 |
| writers.text | format 默认 csv,precision 默认 3,keep_unspecified 默认 true |
| 多输出 | # 占位符逐 view 写;无 # 则聚合;writer 只能串联不能分叉 |