WaterModifier
一个 AI 辅助的桌面工具,用于编辑地理地形数据集中的水域掩码。浏览卫星瓦片地图,点几个前景 / 背景点,由 Segment Anything 分割出水域,结果直接写回 Cesium quantized-mesh .terrain 瓦片——并自动同步到各 LOD 等级。工具在真实 GIS 数据集(瑶湖机场,maxzoom 18)上完成验证。
由两个协作进程构成:Unreal Engine 5.4 前端(地图浏览、可视化、交互)+ Python 后端(SAM 推理 + 地形文件二进制改写),通过 TCP socket(长度前缀协议)通信。
为什么做这个
在 Cesium quantized-mesh 数据集里,水域掩码不是一张能单独打开、单独保存的图层,而是藏在每一个 .terrain 瓦片文件末尾的一段扩展记录里。它的偏移还不是固定的:要先跳过 88 字节文件头,再按各段自带的长度前缀依次跳过顶点数据、三角形索引(索引宽度按三角形数在 2 字节与 4 字节之间切换)、以及西 / 南 / 东 / 北四条边索引表,才能扫到扩展记录、判断里面有没有类型 2。同一片水域在 LOD 金字塔上还被复制了很多份——改动一级,下面每一级都有四倍数量的瓦片需要跟着改。
于是"这片水库应该是水"这样一个人类尺度的编辑意图,落到数据上就是成千上万个瓦片文件的字节级重写。WaterModifier 要做的就是把这两端接起来:一端是按真实经纬度铺在屏幕上的瓦片金字塔和几次鼠标点击,另一端是批量的二进制改写与逐级同步。

前端(UE 5.4,C++)
- 🗺️ TMS 瓦片地图浏览器 — 顶视正交相机,拖拽 / 滚轮缩放;瓦片管理器每帧 diff 视口覆盖的瓦片范围,仅增量加载新进入视野的瓦片
- 🌐 实时地理坐标 — 由瓦片集的
units-per-pixel元数据实时换算经纬度;切换 LOD 时相机重新锚定到相同地理位置 - 💧 水域可视化 — C++ 直接解析 quantized-mesh
.terrain二进制格式(顶点 / 索引块、边索引、扩展记录——类型 2 即水域掩码),将现有水域以蓝色叠加显示
后端(Python + PyTorch)
- 🤖 SAM 分割 — 当前视口导出为 EXR、色调映射后喂给 Segment Anything(ViT-B,CUDA);左键为前景提示点、右键为背景点,可反复加点迭代直到掩码满意
- ✍️ 手动模式 — Photoshop 钢笔式多边形选区,射线法点内测试栅格化
- 📝 地形文件原地改写 — 逐瓦片掩码合并,支持覆盖与叠加两种模式,兼容 1 字节统一掩码和完整 256×256 掩码,并能为从未有过水域掩码的瓦片追加扩展
- 🧹 形态学清理 — 开 / 闭运算去除分割噪点,再经平滑卷积让水域边缘自然过渡
- 🔁 LOD 同步 — 修改递归下推至更高缩放等级:父瓦片掩码四象限拆分 + 最近邻上采样,一路同步到数据集最高 LOD
- 📊 实时进度 — 批量写入期间,定时线程每 0.5 秒向 UI 回传已修改瓦片数
关键技术点
- 双端二进制格式一致性 — Cesium quantized-mesh-1.0 的解析在两端各实现一遍:Python 端负责写(手动走 header / 顶点 / 索引块 / 边块 / 扩展记录,索引宽度按三角形数自适应 2 / 4 字节),UE C++ 端负责读(可视化),两端对格式的理解必须逐字节一致
- 相机 ↔ 瓦片坐标换算 — C++ 函数库实现相机视图 → 瓦片编号 → 地理坐标的完整链路(含 tilemapresource.xml 解析、
units-per-pixel换算)
设计取舍
- 🔌 socket 只走指令,像素走文件系统 — 协议里只有指令关键字加字符串参数(选点坐标、LOD、地形根目录、左下角瓦片编号、视野范围、瓦片尺寸),回复是
SegmentDone/ModifyDone这类短字符串。两张图片改在工作目录里交接:UE 把当前视口的 Render Target 导出为 EXR,Python 读它做分割;Python 写出掩码 PNG,UE 再加载成运行时贴图。被放弃的方案是把像素缓冲直接塞进 socket;代价是两个进程被绑在同一个工作目录上。 - 🧭 逐段跳过,而不是直接定位掩码 — 掩码偏移取决于顶点数、三角形数与索引宽度,固定偏移这条路走不通,只能按每段自带的长度前缀一段段跳。代价是同一套遍历逻辑在两端各实现一遍——Python 端负责写、UE C++ 端负责读——两边对字段偏移与跳转规则的理解必须始终一致。
- 📦 写入端始终输出完整网格 — 掩码负载要么是 1 字节的全水 / 全陆统一标志,要么是完整的 256×256 = 65536 字节网格。遇到 1 字节负载时直接升格:改写长度域、删掉那一个字节、追加完整网格;完全没有掩码扩展的瓦片则在文件末尾追加一条扩展记录。保留紧凑的 1 字节形式本可以省空间,但下游每一处都要维护两条分支;代价是整文件的读—改—重写,而不是原地字节修补。
- 📄 三个文件名就是数据集契约 — 瓦片地图根目录要有
tilemapresource.xml(提供各 LOD 的 units-per-pixel 列表)与meta.json(提供maxzoom),地形根目录要有layer.json。maxzoom是读文件得来的,而不是从磁盘上的金字塔结构推断的:好处是不必扫目录、行为可预期,代价是数据集必须按约定提供这个文件。 - ⏱️ 进度用自我重挂的定时器回传 — 批量修改期间由一个 0.5 秒定时器把已完成瓦片数写回 UI,每写回一个
.terrain文件计数加一(含递归同步到的子瓦片)。好处是实现简单、不侵入写入循环;代价是这条数据绕开了带长度前缀的协议,接收端要单独识别。
技术栈
UE 5.4 / C++ · Python / PyTorch — segment-anything(ViT-B)、NumPy / SciPy / OpenCV、TcpSocketPlugin;作用于 TMS 瓦片金字塔与 Cesium quantized-mesh-1.0 地形数据。 分割使用 SAM ViT-B(segment_anything 锁定到上游特定 commit,torch 2.4.1 + cu124);界面提供 256 / 512 / 768 / 1024 / 2048 五档视野范围,默认 1024。