磁刻技术实录 · 第 18 篇 · 工程实践 · 技术基线 2026-10-31
我们正在做的「磁刻(Cike)」,是一款鸿蒙原生的生活记录与个性化创作应用。用户可以把照片做成数字冰箱贴,在洞洞板上自由摆放、组合自己的生活片段,也可以把喜欢的内容做成桌面卡片,用来回看、互动和分享。
在空间记录上,我们引入了 3D Gaussian Splatting(3DGS,三维高斯泼溅),把拍摄的视频重建为可以转动、靠近观察的三维场景。为此,我们重构了视频重建链路,实现自己的 C++ 训练核心,并结合鸿蒙原生的媒体处理、模型查看和分享能力,希望让用户保存一段经历时,也能留下当时的空间关系。
我们需要知道一次训练修改究竟改善了什么,才能持续提升磁刻的空间记录质量。这篇分享从梯度检查到留出视角的验证方法,并结合已有实验说明如何解读指标。我们希望读者看到改进成立的条件,也能看见仍然失败的视角。
训练损失下降只能证明优化器找到了更适合当前目标的参数,不能单独证明渲染数学、几何结构或新视角质量正确。磁刻把验证拆成局部数学、结构回归、合成训练和真实样本四个层次,让不同测试分别约束系统的一部分。
以下代码按当前实现整理,保留关键运算与控制顺序;省略导入、日志和完整类型,部分辅助函数采用说明性命名,片段需结合上下文阅读。
这种组织方式尤其适合自研训练器。若只看最终图片,颜色与位置可能掩盖旋转梯度错误;若只看某个梯度检查,又无法发现数据集读取、相机映射或动态致密化的生命周期问题。需要让简单测试为复杂测试提供可信起点。
图 1:逐层增加复杂度,每层回答不同问题
前向与反向分别建立参照
前向使用结构简单的逐像素实现与 Tile 路径比较,固定相机、阈值和背景条件。反向使用中心差分检查位置、尺度、旋转、不透明度与颜色。为了避免硬截断边界影响,梯度测试需要专门控制支撑范围与过滤阈值。
测试场景不能只有球形高斯。倾斜各向异性椭球能够暴露旋转与协方差问题,接近饱和的 Alpha 能够暴露透明度递推问题,多相机则检查投影与坐标处理。错误样本越接近问题机制,修复越容易解释。
结构性测试比数量更重要
世界与相机一起做一致变换后,投影关系应保持对应;单线程与多线程应在定义的误差范围内一致;PLY 写入回读后,字段与参数应保持协议语义。每类测试验证一个不变量,而不是简单重复执行相同输入来增加通过数量。
动态训练还需要检查新生点的 Adam 计步、点数预算、透明度维护和失败清理。输入错误尺寸、零四元数、丢失帧或非有限值时,系统应在可解释位置失败,不能继续产生貌似正常的模型。
留出视角必须真正独立
若一张图没有参与损失计算,却参与了初始化特征提取或种子重建,那么它仍然给模型提供了信息。更严格的对照需要记录留出规则,说明相机和种子如何产生。项目真实 RGB 小样本实验使用 9 帧训练、3 帧留出,并仅从训练帧重新三角化实验种子。
PSNR 可以提供像素误差量化,但不应只报告平均值。某个角度的几何覆盖差,可能被其他容易视角掩盖。逐视角图像和分数可以让读者看到改善发生在哪里,哪里仍然有明显缺失。
让梯度与并行结果都有独立参照
# 验证组织示例;误差容限按被测量的量级设置。
scene = make_anisotropic_overlap_scene()
single = render_and_backward(scene, threads=1)
parallel = render_and_backward(scene, threads=4)
assert_close(single.image, parallel.image, image_tolerance)
assert_close(single.gradient, parallel.gradient, gradient_tolerance)
numeric = finite_difference(scene, parameter="rotation")
assert_close(single.rotation_gradient, numeric, numeric_tolerance)
各向异性和遮挡让旋转、协方差及 Alpha 分支真正参与测试。若只用孤立球形高斯,旋转错误很可能被形状对称性掩盖。单线程结果可以帮助定位并行归约差异,但它也可能共享同一个数学错误,所以还需要有限差分这个独立参照。
浮点求和顺序变化会产生小误差,不应机械要求逐比特相同。容限应基于量级和用途设定,并同时观察最大误差与整体误差。不能为了让测试通过不断放宽容限,却不解释差异来自数值顺序还是实际的数据竞争。
图 2:2026-10-26:OHOS x86_64 模拟器,9 帧训练、3 帧留出
如何解读已有真实样本结果
2026-10-26 的仓库记录使用 OHOS x86_64 模拟器 CPU、270×360 分辨率、1,500 次迭代。留出均值从 20.976 dB 提升到 22.491 dB,同时高斯数从 3,553 增至 12,000,CPU 时间从 93.4 秒增至 115.0 秒。
这个结果支持“在该小样本配置下质量改善,并付出更多计算”的结论。第三个留出角度仍只有约 10.369 dB,种子覆盖不足的问题没有消失。因此,不能把均值提升改写成任意角度都清晰,也不能把模拟器时间当成用户手机耗时。
让证据可以被重新解释
实验应保留输入清单、参数、随机种子、程序版本或哈希、逐视角结果以及失败样本。后续发现显示参数变化时,才能判断旧结论对应哪一层。报告应明确是 CPU 参数隔离、完整 GLES 显示,还是 App 内端到端结果。
质量指标必须在正确的图像范围计算
import numpy as np
def psnr(rendered, reference):
a = np.asarray(rendered, dtype=np.float64) / 255.0
b = np.asarray(reference, dtype=np.float64) / 255.0
mse = np.mean((a - b) ** 2)
return float("inf") if mse == 0 else -10 * np.log10(mse)
scores = [psnr(render(camera), image) for camera, image in heldout]
print("mean", np.mean(scores), "per_view", scores)
示例输入是八位图像,因此除以 255。如果渲染结果已经在零到一之间,就不能再次归一化。预测图与参考图还必须在尺寸、颜色约定和相机上对应,否则分数会混入预处理差异,而不是单纯评价模型质量。
逐视角结果与平均值一起输出,是为了看见局部失败。本文已有小样本实验中,均值提升并不意味着每个留出视角都良好。用这段代码组织新实验时,还应记录训练与留出划分、分辨率、迭代预算和运行环境;它不是对文中历史数据重新运行的声明。
对于技术传播,展示一个仍有局限但条件完整的实验,比给出没有样本背景的漂亮数字更有价值。它让读者知道实现已经走到哪里,也让下一轮改进有明确比较基线。