第14章 - Entwine 与 EPT 点云索引

一个 4000 万点的 LAS 文件,pdal info --summary 要读完全部数据;filters.smrf 要把它整块装进内存;到了 Web 端,用户只想要某个街区,却必须下载整个文件。点云处理的所有这些不便,都来自同一个事实:LAS/LAZ 是顺序文件,没有空间索引。

EPT(Entwine Point Tile)就是给点云补上空间索引的格式:一个八叉树目录、每层按瓦片切分、每块单独存放,ept.json 描述范围、层级、schema 和每块的点数。PDAL 通过 readers.ept 原生读取,配合 bounds/resolution/polygon 就能只下载需要的那几块。Entwine 是生成 EPT 的工具,PDAL 是消费 EPT 的工具,两者都出自 Hobu。

本章讲 EPT 的数据组织方式、用 Entwine 建索引、用 readers.ept 做空间与分辨率查询,以及一条实测过的坑:EPT 的 forward 在 writer 侧不可用。

14.1 为什么需要点云索引

14.1.1 顺序文件的三个代价

代价 具体表现
全量 I/O 查 1 km² 的街区要从头解压整个 LAZ
内存峰值 非流式算法要求整个 view 在内存里,1 亿点约 3–6 GB
无法局部更新 追加/修改任意区域都要重写文件

EPT 的解法是把点云切成”八叉树 + 瓦片”:根节点覆盖全域,每深入一层分成 8 个子节点(2×2×2),每个节点内再按点数切成若干块(ept-data/ 目录下的 .laz)。查询时先读 ept.json 拿到边界与层级,判断哪些节点与查询范围相交,只下载相交的节点。

14.1.2 EPT 与 COPC 的分工

两者都是八叉树点云格式,差别在封装:

  EPT COPC
形态 目录树:ept.json + ept-data/ + entwine-files.json 单个 .laz 文件(1.4 + 内部层级索引)
适合 云端发布、增量更新、几亿到上千亿点 分发单个文件、单文件内随机访问
PDAL 读取 readers.ept readers.copc
生成工具 Entwine(entwine build) writers.copc(PDAL 自带)

一个常见架构是:原始数据用 writers.copc 落成单文件(第 07 章),发布到 Web 时用 Entwine 转成 EPT 挂 CDN。两者不冲突。

14.2 Entwine 建索引

14.2.1 安装与构建命令

Entwine 与 PDAL 一样通过 conda-forge 分发:

conda create -n ept -c conda-forge entwine pdal
conda activate ept
entwine --version

最小构建命令(PDAL 官方 workshop 里的原样用法):

entwine build -i ./just-ground.laz -o ./dublin-ground-ept

也可以先把多个文件塞进一个文本清单:

ls /data/2024/*.laz > files.txt
entwine build -i files.txt -o /data/ept/2024 --threads 8

构建结束后目录结构大致如下(层级数取决于数据范围,块大小取决于点数):

dublin-ground-ept/
├── ept.json                # 元数据:bounds、schema、span、层级、每块点数
├── ept-data/               # 点数据瓦片,按 层级-坐标.laz 命名
│   ├── 0-0-0-0.laz
│   ├── 1-0-0-0.laz
│   └── ...
├── ept-sources/            # 每个源文件的边界与 SRS 信息
│   ├── list.json
│   └── 0.json
└── entwine-files.json      # 源文件清单(origin 选项要用)

14.2.2 ept.json 里有什么

ept.json 是 EPT 的入口,PDAL 打开 readers.ept 时第一件事就是取它。关键字段:

字段 含义 对查询的影响
bounds / boundsConforming 数据范围(前者可能被投影或离群点夸大) bounds 查询的重叠判断
schema 每个维度的名字、类型、size addons 覆盖同名维度时按此匹配
span 每个节点在 X/Y 方向的瓦片格数 决定节点内分块数量
hierarchyType 层级编码方式(json 或 gzip) 读层级表的解码方式
points 总点数 pdal info 的 count 上限
srs 水平/垂直 SRS 的 WKT 与 authority 与 readers.ept 的 spatialreference 交互

bounds 与 boundsConforming 的区别值得记住:bounds 是轴对齐外接盒,可能因为投影或离群点而远大于真实数据;boundsConforming 是贴合真实数据的范围。写查询时用后者更省流量。

14.3 readers.ept 的查询能力

14.3.1 参数表

参数 类型 默认 作用
filename string 必填 必须指向 ept.json 本身(PDAL 2.2+ 起的行为)
bounds string 整个资源 2D/3D 范围,格式 ([minx,maxx],[miny,maxy],[minz,maxz])
resolution double 不限 网格边长(坐标单位);0.1 米约等于 100 点/m²
polygon WKT 无 裁剪多边形,可重复指定并取并集;可跟 /SRS 后缀
ogr JSON 无 用 OGR 查询取多边形,等价于 polygon
addons JSON 无 维度名: addon 路径,覆盖同名维度
origin string/int 无 只取某个源文件的点(路径子串或 OriginId)
requests int 15(最小 4) 并发请求数
ignore_unreadable bool false 忽略缺失/损坏的瓦片
spatialreference string 无 覆盖数据自带 SRS

filename 必须是 ept.json 的完整 URL 或路径,这是 PDAL 2.2 的行为变更——旧代码里写目录或以 / 结尾,升级后会打不开。

14.3.2 按范围取数

官方示例,从纽约 47 亿点数据集里取自由女神像周边:

[
  {
    "type": "readers.ept",
    "filename": "https://na-c.entwine.io/nyc/ept.json",
    "bounds": "([-8242669, -8242529], [4966549, 4966674])"
  },
  "statue-of-liberty.las"
]

bounds 是两段还是三段决定了是否裁剪 Z。两段只做水平裁剪,三维范围的写法是 ([minx,maxx],[miny,maxy],[minz,maxz])。

14.3.3 按分辨率取数:先降采样再算

resolution 是 EPT 最有用的参数。它的语义是”网格单元边长”,单位跟资源坐标系一致:米制坐标下 0.1 表示每平方米最多 100 点。PDAL 会选一个”不低于该精度”的层级,所以实际点数可能略多于预期。

做区域统计、生成缩略视图、试算参数时,先用 resolution 把数据降到百万级再迭代,比全量跑一遍快一到两个数量级:

pdal info "https://s3-us-west-2.amazonaws.com/usgs-lidar-public/IA_FullState/ept.json" \
  --readers.ept.bounds="([-10425171.940, -10423171.940], [5164494.710, 5166494.710])" \
  --readers.ept.resolution=2.0 \
  --summary

这里的 --readers.ept.xxx=... 是命令行的 option 覆盖写法,和 pdal pipeline 的 JSON 等价。

14.3.4 多边形裁剪与 SRS 后缀

polygon 接受 WKT,可以给多个(数组形式,取并集),每个后面能跟 /SRS 声明自己的坐标系:

{
  "type": "readers.ept",
  "filename": "https://example.com/ept/ept.json",
  "polygon": [
    "POLYGON((500000 4500000, 500500 4500000, 500500 4500500, 500000 4500500, 500000 4500000))/EPSG:32633"
  ]
}

bounds 与 polygon 同时给时取交集。这一点官方文档写得很明确,但实践中容易误以为 polygon 会覆盖 bounds。

14.4 EPT 的增量属性:addon

14.4.1 为什么不重写整个索引

给一个已经发布的 EPT 加一种分类结果(比如重跑 SMRF 得到的 Classification),最笨的办法是重建 EPT——几亿点要跑几小时。EPT 提供了 addon 机制:新维度独立存放,读的时候按名字挂上去。

writers.ept_addon 的 addons 是 addon 路径: 维度名(注意方向),而 readers.ept 的 addons 是 维度名: addon 路径——两边相反,官方文档专门用 note 强调过。

写入:

[
  { "type": "readers.ept", "filename": "http://na.entwine.io/autzen/ept.json" },
  { "type": "filters.assign", "assignment": "Classification[:]=0" },
  { "type": "filters.smrf" },
  {
    "type": "writers.ept_addon",
    "addons": { "~/entwine/addons/autzen/smrf": "Classification" }
  }
]

读取时覆盖:

[
  {
    "type": "readers.ept",
    "filename": "http://na.entwine.io/autzen/ept.json",
    "addons": { "Classification": "~/entwine/addons/autzen/smrf" }
  },
  { "type": "writers.las", "filename": "autzen-ept-smrf.las" }
]

addon 的数据与 EPT 主体分开存放,所以写 addon 不需要对 EPT 资源有写权限。这对发布在 S3 上的只读数据集很关键:可以在本地或另一个桶里维护自己的分类结果。

writers.ept_addon 的 threads 也有个细节:无论传多少,最小按 4 个线程执行。

14.5 云端读取的机制与坑

14.5.1 只下载需要的那几块

readers.ept 支持 http(s)://、s3://、gs:// 等 URL(依赖 GDAL 的 VSI 层)。查询流程是:取 ept.json → 取层级表 → 对每个候选节点发 HTTP Range 请求取该块 → 解压 → 与本地的其他 stage 一样流式处理。requests 控制同时在途的请求数,默认 15;调大能提升高延迟链路的吞吐,但云端按请求计费时要注意成本。

实测经验:跨洲访问 S3 时,requests=15 的默认值通常已经能把带宽打满;真正拖慢速度的是 resolution 选得太细——数据量按 4 倍阶梯增长。

14.5.2 坑一:forward 对 EPT 无效

writers.las/writers.copc 的 forward 依赖”单一源文件的头信息”。EPT 是多个源文件混合而成的,没有统一可转发的元数据。官方文档明确写了:forward 与 EPT 不能配合使用,输出选项必须显式设置。

具体后果:如果想把 EPT 读出来写成 LAS 1.4,得自己指定 minor_version、dataformat_id、scale_x/y/z、offset_x/y/z,否则会拿到 writer 的默认值(1.4、格式 7、scale 0.01)。在投影坐标下 0.01 米没问题;转到地理坐标就会踩第 11 章讲过的精度坑。

14.5.3 坑二:origin 选中的点数是上限

origin 按源文件挑点,可以传路径子串或整数 OriginId。字符串形式要求子串唯一:官方例子给了 one.las、two.las、two.bpf 三个源文件,"one" 能唯一选中,"two" 会匹配两个文件而失败。

另一个官方 note:配合 pdal info --summary 时,origin 会让 bounds 裁剪到选中源的范围,但点数只是上限——因为 summary 不是精确计数。做入库对账时不能把这个数当准数。

14.5.4 坑三:无 bounds 的查询

不写 bounds 就取整个资源。Iowa 全州 3DEP 公开数据集约 1600 亿点,官方教程专门加了一句警告。任何面向用户的查询接口都应该强制要求 bounds 或 polygon,并在 resolution 上设下限。

14.6 排错清单

现象 原因 修法
Unable to open ... ept.json filename 给的是目录而非 ept.json PDAL 2.2+ 必须给完整文件名
404 或某个瓦片读不到 瓦片缺失或 addon 路径错 开 ignore_unreadable 跳过;检查 addon 路径
查询返回 0 点 bounds 落在数据范围外 用 pdal info 读 ept.json 的 bounds 修正范围
点数远超预期 没设 resolution,选中了全部层级 加 resolution,按目标精度估算
读取很慢 requests 太小,或跨洲访问 调大 requests(≥4);就近部署
addon 维度没生效 读写两侧 addons 方向写反 reader 是 维度: 路径,writer 是 路径: 维度
origin 报歧义 子串匹配到多个源文件 用更长的路径子串或整数 OriginId
输出 SRS/scale 丢了 用了 forward EPT 不支持 forward,显式设 writer 参数

14.7 生产建议

原始数据保留 COPC 或 LAZ,发布层才用 EPT。 EPT 的目录结构适合对象存储 + CDN,但文件碎片多(百万点级数据集可能上万个瓦片),备份、迁移、校验都比单文件麻烦。COPC 单文件更适合归档与分发。

resolution 作为服务端强制参数。 面向 Web 的取数接口按视距换算 resolution(例如屏幕 1 像素对应 1 米时取 1.0),而不是让客户端直接读全精度。

addon 承载派生属性。 分类结果、强度归一化结果、自研特征都做成 addon,原始 EPT 保持只读。这样重跑算法不触碰主体数据,也不需要重建索引。

监控请求数与流量。 requests 与 resolution 决定了单次查询的请求数和字节数;把这两个值记进访问日志,容量规划时才有依据。

14.8 本章小结

主题 结论
EPT 本质 八叉树 + 瓦片目录,ept.json 为入口,适合云端发布
建索引 entwine build -i <清单或目录> -o <输出目录>
读取入口 readers.ept,filename 必须是 ept.json 完整路径(2.2+)
空间查询 bounds(2D/3D)、polygon(WKT,可多个取并集),两者同时给取交集
分辨率 resolution 为网格边长,米制下 0.1 ≈ 100 点/m²,实际可能更精细
并发 requests 默认 15,最小 4
增量属性 writers.ept_addon 写、readers.ept 的 addons 读,两侧键值方向相反
已知限制 forward 不适用于 EPT;origin 的 summary 点数是上限