第02章 - 点云数据模型与 LAS/LAZ 格式
2.1 为什么点云需要专用容器
先用一个具体数字感受问题规模。一景机载 LiDAR 数据约 1–5 亿个点,每个点至少带 XYZ、强度、回波号、分类、GPS 时间。如果存成 CSV:
- 一行约 60 字节,1 亿点 ≈ 6 GB;
- 全精度浮点文本解析,C++ 读一遍要几分钟;
- 没有类型信息、没有空间范围、没有坐标系,一切靠约定。
LAS 用固定长度的二进制记录解决前三条:每点定长、字段偏移固定、文件头里写明范围与坐标系。代价是字段集合被标准锁死——LAS 1.0 到 1.4 只定义了 11 种 point format(0–10),想多存一个属性就得用 extra bytes VLR 绕出去。
PDAL 的定位正是夹在这两层之间:对外支持几十种容器(LAS、LAZ、COPC、BPF、PCD、PLY、E57、文本……),对内只有一套 Dimension 表。所以读 LAS 得到的维度集合,往往比 LAS 标准字段多——多出来的部分是 extra bytes 和 reader 补的。
2.2 LAS 文件的三段结构
┌──────────────────────────────────────────────────────────────┐
│ Public Header Block LAS 1.2: 227 B / 1.3: 235 B │
│ 1.4: 375 B │
├──────────────────────────────────────────────────────────────┤
│ VLR 1 … VLR n 变长,记录坐标系、extra bytes 等 │
├──────────────────────────────────────────────────────────────┤
│ Point Data Records 每点定长:20 / 26 / 28 / 34 … 67 B │
├──────────────────────────────────────────────────────────────┤
│ EVLR 1 … EVLR m 仅 LAS 1.4,变长,可存大块元数据 │
└──────────────────────────────────────────────────────────────┘
三段头长不是估算,是 LAS 规范常量,也写死在 PDAL 依赖的 laz-perf 里(vendor/lazperf/header.cpp):
const size_t header12::Size = 227;
const size_t header13::Size = 235;
const size_t header14::Size = 375;
文件头里的关键字段:版本号、point format id、point record length、点数、每回波点数、XYZ 的 scale 与 offset、min/max 范围、文件创建日期、生成软件字符串。读 LAS 的很多“怪现象”都能追到这几个字段,比如坐标只剩厘米精度、点数对不上、范围明显偏移。
2.3 Point format 0–10:每一档多了什么
LAS 的点格式不是随便编号的,每加一档就是加一组字段。按 LAS 1.4 R14 规范,各格式的记录长度如下:
表 2-1 LAS point format 与 PDAL 支持情况
| 格式 | 字节数 | 新增内容 | PDAL 支持 |
|---|---|---|---|
| 0 | 20 | XYZ + 强度 + 回波标志 + 分类 + 扫描角 + 用户数据 + 点源 ID | 是 |
| 1 | 28 | 格式 0 + GPS 时间(double) | 是 |
| 2 | 26 | 格式 0 + RGB(uint16×3) | 是 |
| 3 | 34 | 格式 1 + RGB | 是 |
| 4 | 57 | 格式 1 + 波形包(29 B) | 否 |
| 5 | 63 | 格式 3 + 波形包 | 否 |
| 6 | 30 | XYZ + 强度 + 回波(1 B)+ 分类标志(1 B)+ 分类(1 B)+ 用户数据 + 扫描角 int16 + 点源 ID + GPS 时间 | 是 |
| 7 | 36 | 格式 6 + RGB | 是 |
| 8 | 38 | 格式 7 + 近红外 NIR | 是 |
| 9 | 59 | 格式 6 + 波形包 | 否 |
| 10 | 67 | 格式 8 + 波形包 | 否 |
支持范围在源码里是硬编码的,不是文档口径。io/LasHeader.cpp 的 pointFormatSupported():
hasWave() → "PDAL does not support point formats with waveform data (4, 5, 9 and 10)"
versionAtLeast(1,4) && format > 10 → "LAS version X only supports point formats 0-10."
!versionAtLeast(1,4) && format > 5 → "LAS version 'X' only supports point formats 0-5."
两个结论:波形点格式一律不支持(PDAL 没有波形采样能力,这是设计取舍);格式 6–10 必须配 LAS 1.4,1.2 文件里塞格式 7 会被拒。
字节数与 laz-perf 的 baseCount() 完全一致——它只对 0/1/2/3/6/7/8 返回尺寸,波形格式返回 0:
switch (format & 0xF) {
case 0: return 20; case 1: return 28; case 2: return 26;
case 3: return 34; case 6: return 30; case 7: return 36;
case 8: return 38; default: return 0; // 4/5/9/10 及未定义格式
}
& 0xF 这步掩码不是多余:LAZ 会把压缩标志塞进格式号的高位,必须剥掉低位以外的位才能认出真实格式。
2.4 X/Y/Z 不是浮点数:scale 与 offset
LAS 里坐标存成 int32,用文件头中的 scale 与 offset 还原:
存盘值 = (真实值 - offset) / scale
真实值 = 存盘值 * scale + offset
而 writers.las 的默认值是 scale = .01、offset = 0(文档明确写的默认值)。offset 可以写 "auto",表示取该维度的最小值;scale 也可以写 "auto",让存盘值铺满 int32 范围。
精度就由 scale 决定:
- scale = 0.01 → 1 厘米,1:500 地形测量的下限刚好够;
- scale = 0.001 → 1 毫米,适合扫描仪数据;
- scale = 1e-7 → 约 1.1 厘米(赤道处 1 度 ≈ 111 km),这是经纬度写 LAS 的常用值。
这里有个高频事故:把点云重投影成 EPSG:4326 后直接写 LAS,忘了改 scale。经度 116.39 用 scale 0.01 存,只能落到 116.39、116.40 这类格子,横向误差以公里计。官方 filters.reprojection 文档专门给了一个示例,写地理坐标时用 scale_x = 0.0000001、scale_y = 0.0000001、scale_z 保留 0.01:
{
"type": "writers.las",
"filename": "example-geog.las",
"scale_x": "0.0000001",
"scale_y": "0.0000001",
"scale_z": "0.01",
"offset_x": "auto",
"offset_y": "auto",
"offset_z": "auto"
}
int32 的容量上限也决定了 offset 什么时候必须设。存盘值是 int32,上限 2147483647。若 X 真值 500000、scale 0.01,存盘值 5×10⁷,安全;但如果偏移量很大(例如地方坐标 8 位数带多余前缀),乘上 scale 就会溢出。此时把 offset 设成数据最小 X,把存盘值压回 0 附近,是标准解法——"auto" 做的就是这件事。
2.5 位域:分类、回波、扫描角
格式 0–5 的一个字节里塞了 8 个 bit 的分类与标志,格式 6–10 才拆成独立字节。这个差异会直接影响分类码上限:
- 格式 0–5:Classification 占 5 bit(值域 0–31),同字节的高 3 位是 Synthetic / Key-point / Withheld 标志;
- 格式 6–10:Classification 占完整 1 字节(值域 0–255),标志位挪到下一个字节,另加 Overlap 标志。
所以“LAS 1.4 才能表达 32 以上的分类码”这句话是对的。用地物分类(ASPRS 类码 0–31 是标准类,64 起为重叠点等)时,格式 0–5 够用;要接 32 以上的自定义类码,就必须 1.4 + 格式 6/7/8。
回波字段同样是位域:格式 0–5 用一个字节的低 3 位表示 ReturnNumber、中 3 位表示 NumberOfReturns(上限 7),高 2 位是扫描方向与航带边缘;格式 6–10 把 ReturnNumber 扩到 4 bit(上限 15)。这也是 1.4 文件能表达更多回波次数的原因。
扫描角(ScanAngle)在格式 0–5 里是 int8(-90…90),格式 6–10 扩成 int16,单位是 0.006°,能表达 ±30000×0.006°。做几何校正时若发现角度被截断,先看格式。
2.6 VLR 与 extra bytes:标准之外的口袋
VLR(Variable Length Record)是 LAS 的扩展槽:每条形如 {user_id, record_id, length, data},紧跟在文件头之后。几个必知的:
| user_id | record_id | 内容 | 由谁处理 |
|---|---|---|---|
LASF_Projection |
34735 / 34736 / 34737 | GeoKeyDirectory / GeoDouble / GeoAscii,即坐标系 | PDAL 解析成 SpatialReference |
LASF_Spec |
4 | Extra Bytes 描述(维度名、类型、缩放) | readers.las / writers.las |
laszip encoded |
22204 | LAZ 压缩参数 | laz-perf |
liblas |
— | 老 libLAS 元数据 | 忽略 |
Extra bytes 在 LAS 1.4 才正式纳入规范,但不少 1.2/1.3 文件也在用。PDAL 的对应开关是 use_eb_vlr:
- 读 1.4 文件:自动解析,无需设置;
- 读 1.0–1.3 文件:默认不解析,必须显式设
"use_eb_vlr": true,否则那些属性根本不出现在结果里。
写侧对应 writers.las.extra_dims。设 "extra_dims": "all" 会把所有“放不进标准 point record”的维度写进 extra bytes:
[
"input.las",
{
"type": "filters.ferry",
"dimensions": "=>MyHeight"
},
{
"type": "filters.assign",
"value": "MyHeight = Z - 100.0"
},
{
"type": "writers.las",
"filename": "output.las",
"extra_dims": "all",
"forward": "all"
}
]
一个反直觉细节:extra bytes VLR 记录的是名字、类型、顺序,所以读回来时维度顺序不等于原始列顺序。想确认到底写了什么,加 --verbose=Info 跑一遍,PDAL 会打印写出的 extra 维度名称、类型与次序。
两个例外必须记住:
- datatype 0 被忽略。官方原文:LAS 1.4 文件中 datatype 0 的 extra bytes 会被接受,但对应数据被忽略,不会创建 PDAL 维度。
- 数组类型会被拆开。datatype 11–20 是二维数组,生成
Foo0、Foo1;21–30 是三维数组,生成Bar0、Bar1、Bar2。
2.7 LAZ:同一种格式,加一层压缩
LAZ 不是新格式,而是 LAS 的点记录部分用 LASzip/laz-perf 压缩的结果:文件头仍是 LAS 头,point data 换成压缩流,压缩参数记录在 laszip encoded VLR 里。因此:
- 读:
readers.las同时支持.las与.laz,不用换 reader; - 写:
writers.las加"compression": true,默认值是false。
[
"input.las",
{
"type": "writers.las",
"filename": "output.laz",
"compression": "true",
"forward": "all"
}
]
compression 还接受 "lazperf"、"laszip" 两个历史值,行为等价于 true(都走捆进 PDAL 的 laz-perf)。
压缩率没有通用数字,取决于点位分布与维度熵。别照抄“压缩 10 倍”这类说法,用自己的一份数据实测:
pdal translate in.las out.laz
ls -lh in.las out.laz
代价有三条:压缩是 CPU 换 I/O,单线程编译时可能成为瓶颈;LAZ 不能被不认识 LASzip 的老工具读;对 LAS 1.4 + 格式 6–10,LAZ 的兼容面比 1.2 窄。
2.8 COPC:空间索引化的 LAZ 1.4
COPC(Cloud Optimized Point Cloud)不是并列的新格式,而是受约束的 LAZ 1.4:点记录按八叉树排序成 chunk,再写一个 copc 信息 VLR 描述根节点与层级。换来的是 HTTP Range 按块读取——服务器不必把整份文件发过来。
对应 stage 是 readers.copc / writers.copc,关键选项:
| 选项 | 默认值 | 作用 |
|---|---|---|
requests |
15 | 并发取块线程数;网络快而处理慢时调小,避免内存堆积 |
keep_alive |
10 | 内存中保持活跃的 chunk 数 |
resolution |
无限制 | 按期望分辨率选金字塔层级,单位与数据一致 |
bounds / polygon |
无 | 空间过滤,在取块阶段就生效 |
header / query |
— | 远程端点的 HTTP 头与查询参数 |
resolution 的语义值得单独说:对米制坐标,resolution = 0.1 表示取到地面分辨率约每平方米 100 点。实际选中的层级不必然等于该值,而是“至少这么精细的最粗层级”,所以结果可能比要求更密。
2.9 读 LAS 的关键选项
readers.las 选项不多,但每一个都会改变结果:
| 选项 | 默认值 | 作用与坑 |
|---|---|---|
filename |
必填 | 可以是远程 URL,取决于编译时的网络支持 |
start |
0 | 从第几个点开始读(0 基),配合 count 取子集 |
count |
不限制 | 最多读多少点 |
override_srs |
无 | 强制覆盖文件里的 SRS |
default_srs |
无 | 仅在文件没有 SRS 时套用;与 override_srs 互斥 |
extra_dims |
无 | 手动声明额外维度,会覆盖 extra bytes VLR |
use_eb_vlr |
false | 1.0–1.3 文件是否解析 extra bytes VLR |
fix_dims |
true | 把非法维度名中的字符换成下划线 |
nosrs |
false | 不读 SRS VLR,用于规避 SRS 解析性能问题 |
ignore_vlr |
无 | userid/record_id 列表,跳过指定 VLR |
threads |
7 | LAZ chunk table 解码线程数 |
extra_dims 的优先级要记牢:文档明确写了它会覆盖 extra bytes VLR,即使同时设了 use_eb_vlr。两者都设时以 extra_dims 为准。
2.10 踩坑清单
坑一:点数对不上。 先用 pdal info --summary 看 num_points,再与 lasinfo、QGIS 对。常见来源:文件头点数与实际点数不一致(文件被截断)、count/start 选项、上游 filter 去掉了点。LAS 1.4 同时有 legacy 与 extended 两套计数,只读 legacy 的老工具在大文件上会饱和。
坑二:坐标差一大截。 先查 SRS 是否读出来(pdal info --metadata 里的 srs 段),再查 scale/offset。典型组合错误是数据本身为 UTM,而文件里写的是地理坐标。
坑三:分类全是 0 或 1。 若上游只写了 5 bit 的 Classification,标志位会丢;另一种情况是管道里的 filters.range 把目标类滤掉了。用 pdal info --stats --dimensions Classification 看分布最快。
坑四:写出的 LAS 打不开。 多数是 point format 与版本组合非法(格式 7 配 1.2),或 extra bytes 类型不被目标软件识别。用 pdal info 回读一遍能立刻定位。
坑五:forward 没生效。 writers.las.forward 只在输入为 LAS 且来源唯一时全量生效;多文件合并时若各文件头字段不一致,会被忽略并退回默认值,文档写明了这条规则。
2.11 生产建议
scale/offset 策略写进规范。 投影坐标用 0.01(厘米)或 0.001(毫米),地理坐标用 1e-7,offset 一律 auto。转格式时用 forward: "all" 继承原 scale,避免多次转换累积量化误差。
版本与格式成对管理。 要 32 以上类码或超过 7 次回波,就上 LAS 1.4 + 格式 6/7/8;只需标准类码且要与老软件互通,1.2 + 格式 3 更稳。PDAL 2.8 起 writers.las.minor_version 默认已是 4,老流程升级要留意输出版本悄悄变了。
大文件优先 LAZ 或 COPC。 归档用 LAZ,要在网络上按需取块用 COPC。写 COPC 之前先确认下游能读——它不是所有 GIS 软件都支持。
extra bytes 不是垃圾桶。 每加一个 extra 维度都会增加每点字节数与兼容风险。只写下游真正要的字段,且把 extra_dims 显式列出,别图省事用 all。
2.12 小结
| 主题 | 关键事实 |
|---|---|
| 文件结构 | header(227/235/375 B)+ VLR + point records + EVLR(1.4) |
| 点格式 | 0–10;不支持波形格式 4/5/9/10;格式 6–10 必须配 LAS 1.4 |
| 记录长度 | 20 / 28 / 26 / 34 / — / — / 30 / 36 / 38 / — / —(字节) |
| 坐标编码 | int32 + scale/offset;默认 scale=.01、offset=0,可设 auto |
| 位域差异 | 格式 0–5 分类 5 bit、回波 3 bit;格式 6–10 分类 8 bit、回波 4 bit |
| extra bytes | LASF_Spec/4;1.0–1.3 需 use_eb_vlr=true;数组类型拆成 Foo0/Foo1 |
| LAZ | 同格式的压缩变体,compression: true 触发 |
| COPC | 受约束的 LAZ 1.4 + 八叉树,支持 HTTP Range 按块读 |
| 读取选项 | start/count/override_srs/default_srs/use_eb_vlr/threads(7) |