覆盖扫描解决了“看哪里”的问题,2D detection 和 3D verification 解决了“怎么判断”的问题。实际落地时,还有一个容易被低估的问题:模型检测到的是截图里的位置,但最终需要在 3D 场景里创建准确的标注。
如果这一步不稳定,模型识别对了也没用。marker 偏到旁边的管道、平台或者地面上,用户看到的就是错误结果。
这一篇主要讲从截图检测到 3D 标注之间的工程链路,以及我们在 Cesium 里遇到的一些问题。
从截图坐标到世界坐标
视觉模型看到的是一张截图。它返回的是截图里的位置,可能是一个点,也可能是一个框。但资产盘点需要的是 3D 场景里的实体位置。
中间大致有这样一条链路:
Camera pose
↓
Rendered frame
↓
VLM detection
↓
Pixel coordinate
↓
World coordinate
↓
Marker / callout这条链路里任何一步出错,最后表现出来都可能像是“模型不准”。但很多时候问题不在模型,而在截图质量、相机状态、坐标映射或者场景深度解析。
所以我们后来把这部分单独拿出来处理。模型只负责判断图里可能有什么,几何和坐标问题尽量用确定性方法解决。
正射视角下不要依赖 depth picking
最早我们尝试直接用 Cesium 的 scene.pickPosition,把检测点从屏幕坐标转换回世界坐标。这个方法在透视视角下有时能工作,但在正射视角下很容易出问题。
当时我们看到过类似这样的日志:
pickPosition: [115.7441, 57.9449, -6343534]
globePick: [115.7887, -32.1432, -27.8]pickPosition 得到的纬度和高度明显不对,高度甚至接近负一个地球半径。最后的 marker 当然会严重偏移。
这类问题很容易误判成模型定位不准。实际上模型可能已经在截图里指出了正确位置,但像素到世界坐标的解析错了。
后面我们在正射扫描里改成直接使用相机和 tile 的已知参数做解析映射。每个 tile 的中心、span、canvas aspect 和检测点的 pixel coordinate 都是已知的,所以可以直接算出它对应的地面位置。
在这个流程里,不再需要依赖 pickPosition 做深度重建。
相机状态要记录完整
另一个问题来自相机状态恢复。
在透视视角下,缩放通常会改变相机位置。但在正射视角下,zoom 改变的是 frustum.width,相机位置和方向可能没有变化。
这会影响很多看起来无关的地方,比如:
- pick cache 是否应该失效
- restore camera 后是否真的回到了原来的截图状态
- detection resolve 时使用的缩放是否和截图时一致
我们遇到过一个很典型的偏移问题:截图是在某个正射缩放下生成的,但后面解析 detection 时,相机恢复不完整,frustum.width 被重置了。结果检测点离画面中心越远,映射到世界坐标后的偏移越明显。
这个 bug 从结果上看很像“模型框歪了”。但真正的问题是相机状态没有完整记录。
后来我们把正射相机的关键状态都显式保存下来,尤其是 frustum.width。只要涉及缓存、恢复相机、解析检测结果,都不能只看 camera position 和 direction。
Tile 覆盖要按真实画面尺寸计算
覆盖扫描里还有一个容易出错的地方:tile 的画面 footprint 不是正方形。
一开始我们用同一个 span 同时作为横向和纵向步长,等于默认每张截图覆盖的是一个 span × span 的正方形区域。但实际正射画面的宽高由 canvas aspect 决定。
如果画面宽高比是 1.88,那么当横向覆盖宽度是 span 时,纵向覆盖高度大约是:
frameH = span / 1.88也就是说,实际高度只有宽度的一半多一点。
如果仍然按 span 去排布纵向 tile,中间就会出现很大的空隙。系统看起来扫了很多 tile,但实际上有些区域从来没有进入截图。
后面我们把 tile 规划改成按真实 footprint 计算:
frameW = span
frameH = span / aspect然后根据 frameW 和 frameH 分别计算列数和行数。边缘 tile 也要做 clamp,避免画面超出扫描边界。
这里有两个目标需要同时满足:
- 扫描画面不要跑到边界外太多
- 边界内不要留下覆盖空洞
这部分如果做错,会直接影响第一篇里提到的覆盖扫描效果。覆盖率不是 tile 数量多就自然成立,它必须按照相机真实画面范围来计算。
截图质量也是检测链路的一部分
VLM 看到的不是 Cesium 场景本身,而是某一帧截图。
这句话后来变得很重要。因为很多检测问题看起来像模型能力问题,实际上是截图质量问题。
在扫描过程中,如果 3D Tiles 还没有加载到足够精细的 LOD,截图里的设备会很糊。阀门本来就是小目标,LOD 不够时很容易和管道、平台混在一起。
我们后来在扫描期间临时调整 tileset 的 maximumScreenSpaceError,让当前 tile 尽量加载更细的 LOD。扫描完成后再恢复原来的设置。
另一个问题是等待时机。早期我们用“画面不再明显变化”来判断截图是否稳定,但这个信号不可靠。画面不变,可能是加载完成,也可能是加载卡住了。
后来我们更依赖 Cesium 的 tilesLoaded 状态,确保当前 tile 的数据真正加载完成后再截图。
这一步对检测质量影响很大。输入图像如果不清楚,后面再强的模型也很难稳定识别。
给人看的 overlay 不应该进入模型输入
我们还遇到过一个很实际的问题:为了让人看到扫描进度,我们在场景里画过 tile overlay,用不同颜色表示 planned、active、done、skipped。
这个 UI 对调试很有帮助。人一看就知道系统扫到哪里了。
但后来发现,这些 overlay 会出现在检测截图里。模型看到的就不再是干净的工厂画面,而是带着彩色覆盖层的画面。这样会污染视觉输入,影响检测结果。
后面我们把人类可视化和模型输入分开。扫描进度可以显示在侧边栏、HUD 或单独的调试层里,但不能进入送给模型的截图。
这类问题很容易被忽略。人类观察界面和模型观察界面不一定应该是同一个东西。
标注结果需要能被复盘
资产标注不是一次性的截图分析。最终结果会留在 3D 场景里,供用户检查、修改、导出或者进入后续流程。
所以每个标注最好能追溯到它是怎么来的:
- 来自哪个 tile
- 截图时相机状态是什么
- 检测点在图像中的位置
- 映射到世界坐标时用了什么参数
- 是否经过 3D verification
- verification 的结果是什么
这些信息对调试很重要。用户看到一个 marker 偏了,系统需要能判断问题出在 detection、pixel-to-world mapping、tile LOD、还是 verification。
没有这些中间信息,所有问题最后都会变成一句模糊的“模型不准”。
小结
在这个过程中,最重要的一点是:几何问题要尽量用几何方法解决。正射视角下的像素到世界坐标映射、tile footprint、相机 frustum、LOD 加载状态,这些都不应该交给模型猜。