街景 3DGS 补课 · 第四课 · 2026-09-03

插值、外推,以及为什么会崩

前三课讲的是模型怎么建、代码在哪。这一课讲怎么量它——而这恰恰是整个课题里最容易出错、也最有话可说的地方。你 09-02 那晚做的分区评测协议,是目前手上最扎实的一块东西。

前置:第二课 §7(训练测试怎么切)· §6.2(曝光仿射)
数据来源:仓库 results/preexpB/ 的 37 份评测 JSON、results/figs/ 的渲染对比图
协议实现:preexp/eval_heldout_masked.py

0.0开场

为什么这一课最要紧

一个模型好不好,取决于你怎么问它。换一种留法,同一个模型可以从 27 dB 掉到 16 dB——不是模型变了,是问题变了。所以在讨论「哪个方法更好」之前,得先说清楚「好」是按什么量的。

这一课要建立的核心区分只有一条:

常规 NVS 协议留出整条相机
留什么每隔 10 个时间步整列留出某几条相机的全部帧
测试视角离训练视角多远时间上前后各半秒,空间上几米空间上偏离整条轨迹
本质在问能不能在轨迹附近插值能不能看到轨迹之外
典型读数27 dB 左右整图 15 到 16 dB
能支撑的结论重建质量外推能力,但必须分区才有意义

最后那句加粗的是本课的重点,也是 §4 到 §7 要展开的东西。

1.0插值

常规协议测的是时间插值

第二课 §7 讲过切法:test_image_stride=10,每隔 10 个时间步整列进测试集,150 帧里留出 14 个时间步、42 张图。关键在于整列一起留——一个时间步被划走,这一刻三个相机的图全都走。

后果是:每个测试帧的左右两侧各有一个训练帧,时间上差 0.1 秒,空间上车往前走了几米。而且那两个训练帧上,几乎所有内容都跟测试帧重合。所以这个协议问的是「你能不能在两张见过的图之间插一张」。

DriveStudio 的评测入口是 tools/eval.py 的 do_evaluation(),它跑两遍:测试集出 metrics/images_test_*.json,全集出 images_full_*.json。分区指标(occupied_psnr / vehicle_psnr / human_psnr)是靠第二课 §1.1 说的 class_labels 做掩码重渲得到的。

这不是说常规协议没用

它测的是真问题:重建保真度。你八月那批工程加速实验,判据就该是它——因为你要证明的是「加速之后重建质量没掉」,那正是插值质量。问题只出在把它的读数当成外推能力的证据。27 dB 说明模型能在轨迹附近插值,不说明它知道路边那栋楼的侧面长什么样。

2.0外推

换一种留法:留出整条相机

要测外推,就得让测试视角在空间上离开训练轨迹。最省事的做法是:把某几条相机的所有帧全部划走,一帧都不给模型看。

你做了两组,设定不一样,正好互为对照:

场景ego 位移留出训练用测试帧数
s230 米(车停着)相机 2(前右)相机 0、1151
s552约 28 米相机 3、4(左右侧)相机 0、1、2282

这两组的差别不只是场景。s23 的车根本没动(第二课 §3 讲过判据,这个场景连刚体节点都开不出来),所以它的三个相机就是三个固定视点,测的是「从一个固定视点跳到另一个固定视点」;s552 有 28 米位移,测的是「从行驶轨迹跳到轨迹侧面」。

代码上要动的地方在 preexp/patch_heldout_cam.py:把留出相机从训练 split 里摘掉,同时保证它们仍然进测试 split。第三课 §6 的改动索引里「改训练测试切分」那条指的就是这件事——现有代码只有「按时间步切」的概念,「按相机切」得自己加。

3.0看图

第一眼:大半画面根本没人看过

在讨论任何指标之前,先看一眼渲染出来是什么样。这是 s23,留出的前右相机:

留出相机的渲染结果与真值并排:渲染图上只有最左侧一条窄带有内容,其余是彩色涂抹;真值是一个正常的街角,有停车标志、楼房、行人
图 1左:模型渲染的留出相机画面。右:真值。只有最左边那条窄带勉强对得上,其余整片是彩色涂抹——那些地方从来没有任何训练相机看过,模型只能靠边界高斯往里糊。这一帧里被看过的像素占 11.0%。

再看 s552,行驶场景的侧相机。第三块是分区掩码,红色是没人看过的,绿色是被至少一个训练视角看过的:

三联图:左为渲染结果,中为真值街景,右为分区掩码,红色区域占大部分、绿色只在右侧一条
图 2s552 第 70 帧。左:渲染。中:真值。右:分区掩码(红=未观测,绿=观测)。这一帧观测区只占 28.1%,整图 PSNR 13.41,但拆开看观测区有 19.65、未观测区只有 12.27。

两个场景的整体情况:

场景观测区未观测区天空逐帧观测比例的跨度
s23(静止,留前右)11.0%82.0%7.0%每帧都是 11.0%(车没动,构型不变)
s552(行驶,留侧相机)67.2%32.8%0.0%中位 82.6%,但最低到 9.6%、最高 100%

最后一列值得多看两眼。s552 的平均值 67% 掩盖了巨大的逐帧差异:有的帧全被看过,有的帧只剩不到一成。只报一个平均数会把这个结构完全抹掉。

4.0协议

怎么判定一个像素「被看过」

掩码不是拍脑袋画的,是算出来的。preexp/eval_heldout_masked.py 对留出相机的每一帧做四步:

  1. 渲染,拿到 RGB、深度、不透明度。
  2. 反投影:用渲染出来的深度,把每个像素还原成一个三维点。
  3. 视锥测试:把这个三维点投影到所有训练相机的所有帧上,数它落进了几个视锥。
  4. 分三类:天空(用数据集的天空掩码)、观测(落进 ≥ min_views 个视锥)、未观测(其余)。
$$\text{observed}(p) \;=\; \neg\,\text{sky}(p) \;\wedge\; \Bigl(\textstyle\sum_{c\,\in\,\text{train}}\sum_{f}\mathbb{1}\bigl[\pi_{c,f}(P_p)\in\text{画面内} \,\wedge\, z>z_{\text{near}}\bigr] \;\ge\; k\Bigr)$$

\(P_p\) 是像素 \(p\) 反投影出来的三维点,\(\pi_{c,f}\) 是训练相机 \(c\) 在第 \(f\) 帧的投影,\(k\) 就是 min_views,默认 1。

直觉上就是问一句:这块东西,训练的时候有没有哪怕一台相机、哪怕一帧,把它框进过画面里?没有的话,模型对它的一切都是编的。

仪器 1 · 视锥重叠决定了观测比例

俯视示意。灰色是三个前向训练相机的视锥并集,橙色是留出的侧相机。拖它的朝向和位置,看重叠部分怎么变。

70° 50°

朝向拉到 0° 时侧相机跟前向相机看同一片,重叠接近满;拉到 90° 时它看的是正侧方,跟前向视锥几乎不沾边。真实的六相机 rig 就落在中间某处,所以留出侧相机会得到一个「一部分重合、一部分全新」的画面——这正是需要分区的原因。

5.0诚实

这个协议自己的弱点

要拿它写论文,就得先把它的毛病摆在明面上。三条,按严重程度排:

5.1 分区用的是渲染深度,不是真深度

第二步的反投影靠模型自己渲染出来的深度。如果几何本身是错的,分区也跟着错。而未观测区恰恰是几何最不可信的地方——一个死循环。

这不是纸上谈兵。你自己的数据里就有一个实例:s23 的 15k 步 checkpoint,算出来观测比例是 0.0%,观测区 PSNR 直接是 nan。同一个场景的 5k 和 30k 都是稳定的 11.0%。

s23 三个 checkpoint 的分区结果(同一个场景、同一条留出相机)
checkpoint观测未观测天空观测区 PSNR
5 000 步11.0%82.0%7.0%21.58
15 000 步0.0%99.9%0.1%nan
30 000 步11.0%82.0%7.0%22.59

中间那行显然不是「模型在 15k 步时突然什么都看不见了」,而是那一版渲染深度把像素反投影到了离谱的位置,全部落在所有训练视锥之外。要写进论文的话,这一格必须解释或重跑,不能当作一个数据点混在里面。

5.2 视锥测试不查遮挡

「落进画面」不等于「看得见」。一个点可能在训练相机的视锥里,但被前面的墙挡住了。协议没做遮挡测试,所以「观测」这一类是偏乐观的——里面混着一些实际上被挡住、模型同样没有信息的像素。

这个偏差的方向是明确的:它让观测区的 PSNR 偏低(掺了没信息的像素),让两区的差距被低估。所以你现在测到的差距是个下界,这对你的结论是有利方向,写的时候可以直说。

5.3 min_views = 1 是最宽松的门槛

只要被任何一帧扫到过一次就算「观测」。但看过一次和看过五十次,对重建的意义完全不同——这正是你八月共视性统计的出发点。把 \(k\) 调大,观测区会缩小、但含金量更高。这是个现成的消融维度,扫一遍 \(k=1,5,20\) 就能给出「观测充分度—质量」的曲线。

6.0陷阱

曝光:必须后拟合,还得挑地方拟合

第二课 §6.2 讲过为什么留出视角必须后拟合一个全局仿射:曝光嵌入是逐图像学的,测试时退化成平均值,残差全算进 PSNR。这一节讲的是更细的一层——仿射在哪些像素上拟合,结论会反过来。

你的评测脚本是这么写的:

# preexp/eval_heldout_masked.py
rgb_aff = affine_fit(rgb, gt, observed)   # 仿射只在观测区拟合,避免被未观测区的噪声带偏

为什么要挑地方?因为未观测区是一片彩色涂抹,把它算进最小二乘,拟出来的仿射会去迁就那片噪声,反倒把本来对得上的观测区拉歪。

这不是理论推演。你的结果目录里同时存着两套口径的评测,而它们给出相反的结论。

仪器 2 · 同一次渲染,两种拟合口径

下面全是 results/preexpB/ 里的真实数字。点按钮切换口径——注意不带仿射的那三行逐位相同,说明这是同一次渲染、同一个 checkpoint。

差别有多大:观测区差 2.09 dB,未观测区差 3.95 dB,而且方向相反。如果拿其中一套当基线、另一套当实验组,你会得出「这个干预让未观测区暴跌 4 dB」的结论——纯属口径不一致造成的幻觉。

⚠ 你的结果目录里真的有这个坑

BM_s552_base2_*.json 和 BM_s552_dw0.01_*.json 记录的 ckpt 路径完全一样,四个步数上 psnr、psnr_obs、psnr_unobs、frac_obs 全部逐位相同,只有带 _aff 的三项不同。也就是同一次跑被用两版仿射代码各评了一遍。

其余所有实验(aniso*、fisher*、sparseadam、dw0.1、filled*)的观测区仿射 PSNR 都落在 21.2 到 21.7,跟 base2 是一套;只有 dw0.01_* 那六份是 19.1 左右的旧口径。做对比表时基线要用 base2,dw0.01_* 那六份要么重评要么剔除。

7.0后果

不分区会得出什么错误结论

现在把前面几节合起来。假设你老老实实报一个整图 PSNR,会发生什么?

PSNR 是从 MSE 来的,而 MSE 按面积加权。所以整图的 MSE 是两区的面积加权平均:

$$\mathrm{MSE}_{\text{全图}} = f_{\text{obs}}\,\mathrm{MSE}_{\text{obs}} + f_{\text{unobs}}\,\mathrm{MSE}_{\text{unobs}},\qquad \mathrm{PSNR} = -10\log_{10}\mathrm{MSE}$$

关键在于 MSE 是线性加权而 PSNR 是对数的,所以差的那一区会以远超其面积占比的力度把整图数值拽下去。拖一下就明白:

仪器 3 · 未观测区怎么绑架整图 PSNR

两个分区的 PSNR 固定成 s23 的真实值,只拖未观测区的面积占比。

88%

这个模型不是近似——在 s23 上它精确复现了实测值(差 0.00 dB),因为那个场景每一帧的观测比例都是 11.0%。s552 上差 0.78 dB,因为它逐帧比例从 9.6% 到 100% 大幅波动,逐帧平均 PSNR 和整体加权不完全等价。

结论很直白:在 s23 那种 82% 未观测的设定下,整图 PSNR 基本就是未观测区的 PSNR,观测区做得再好也看不出来。你要是拿整图数字去比两个方法,比的其实是「谁把没信息的地方糊得更像」。

这就是 EUVS 那类 benchmark 缺的一环

外推 benchmark 报的是整图指标,所有方法在上面都大幅下跌(你调研到的是 −25% 到 −31%)。但跌幅里有多少来自「表示确实退化」、多少来自「那片区域本来就没有信息」,它没有拆开。拆开这件事就是你现在手上这套协议在做的。

8.0收尾

这一课能撑起什么

把这一课的东西按「能不能写进论文」排一下队,顺便标清楚每条还差什么:

结论证据强度还差什么
留出相机的画面里有 33% 到 82% 的像素从未被任何训练视角框进过硬把 §5.1 那个 15k 的异常格解释掉或重跑
不分区的整图 PSNR 由未观测区决定,测不出表示的性质硬可以直接写,模型和实测都对得上
留出视角评测必须后拟合仿射,且要在观测区上拟合硬有同一次渲染两种口径差 2 到 4 dB 的直接证据
训练越久外推越差弱但真实观测区 5k→30k 只掉 0.3 dB,同期训练视角涨 2 dB。效应小,要多场景才站得住
观测充分度与质量的关系还没做把 min_views 扫一遍就有,成本很低
LiDAR 初始化的颜色泄漏已修正还没做第二课 §4.1 那个漏洞,未观测区的绝对数字要打折扣

前三条是可以直接支撑一篇分析/协议型文章的骨架。后三条是接下来最划算的三件事——尤其 min_views 扫描,改一个命令行参数就能跑。

❧

自查清单

插值 vs 外推 interpolation / extrapolation
测试视角落在训练视角的包络之内还是之外。前者问重建保真度,后者问泛化。
留出相机 held-out camera
把某几条相机的全部帧移出训练集,只用作测试。制造空间外推最直接的办法。
观测区 / 未观测区 observed / unobserved
按「反投影出的三维点是否落进过任何训练视锥」划分的两类像素。
后拟合仿射 post-hoc affine fit
评测时对渲染结果拟合一个全局的 \(a\cdot I+b\) 再算指标,用来消除曝光差异这个混淆项。闭式最小二乘,无额外成本。
视锥测试 frustum test
判断一个三维点是否落在某相机的可视范围内。只看几何,不查遮挡。