P1.A.4 (eval を brush 準拠 convention で再計算する CLI flag) を実装し、splat-rs lego sh3 30k final.ply (training baseline 24.879 dB) を 4 通り (legacy/brush × quantize on/off) で eval。結果は audit §6 の主仮説 (brush 流で +3〜+5 dB) を完全に falsify、brush convention に切り替えると PSNR は逆に -23 dB 崩壊して 1.67 dB。原因は splat-rs trainer が white-bg target 前提で学習されており、背景 pixel を白い不透明 splat で埋めるよう収束しているため、bg=ZERO premultiplied 比較では背景全体で MSE≈1 が乗る。これは bug ではなく training/eval convention の coupling を実証した結果で、apparent gap 仮説 (eval pipeline だけ揃えれば +3〜+5 dB) が成立しないことを示す。M2 以降の brush parity loss は「training-loss convention も brush 化」を含む必要がある。
P1.A.4 (eval を brush 準拠 convention で再計算する CLI flag) を splat eval subcommand として実装し、P1.A.3 reproducer として splat-rs lego-sh3-30k/final.ply を 4 通り (legacy/brush × quantize on/off) で再評価。audit §6 の "brush 流で +3〜+5 dB" 主仮説は完全に falsify、代わりに training-eval convention が coupling している という、より重要な構造的発見が得られた。M1 gate (eval convention 統一確定 + 数式 diff 定量化) は 実測値で完全 closure。
apparent gap 仮説 (eval だけ揃えれば +3〜+5 dB) は falsified。brush convention に切り替えると 24.879 → 1.667 dB に -23 dB 崩壊。原因は trainer が white-bg target で学習されており、背景領域を opaque-white splat で埋めるよう収束している (`view_00.png` で目視確認、背景は白い不透明領域)。brush 流の bg=ZERO premultiplied 比較では、この opaque-white 背景 vs premultiplied GT=(0,0,0) で 背景 pixel 全体で MSE≈1 が systematic に乗る。brush parity は training loss も brush 化 (α=0 領域で背景に penalty を吹かさない構造) を必要とする。
| # | convention | split | quantize8 | PSNR (dB) | ΔPSNR vs baseline | ΔPSNR vs brush 37.40 | 備考 |
|---|---|---|---|---|---|---|---|
| 1 | legacy | val | off | **24.879** | 0.000 (baseline) | -12.521 | sanity check pass、訓練時 result.toml と完全一致 |
| 2 | legacy | val | on | 24.879 | +0.000 | -12.521 | 8-bit roundtrip 単体は impact 無視可能 |
| 3 | brush | val | off | **1.605** | -23.274 | -35.795 | premultiplied GT vs opaque-white 背景で MSE≈1 崩壊 |
| 4 | brush | val | on | **1.667** | -23.212 | -35.733 | **完全 brush 準拠 (val)**、3 と +0.06 dB のみ差 |
| 5 | legacy | test | — | N/A | — | — | test split PNG が 36/200 しか disk に無い (要再 DL、後続 task) |
| 6 | brush | test | — | N/A | — | — | 同上 (dataset 不完全) |
| 軸 | audit §6 予測 | A.3 実測 | 差分 | 判定 |
|---|---|---|---|---|
| D2 (bg composite) | +3〜+5 dB (splat 底上げ) | -23 dB (崩壊) | -28 dB | **仮説と逆方向、falsified** |
| D3 (α-mask premultiply) | +1〜+2 dB (splat 底上げ) | (D2 と一括) | — | D2 と coupling、独立評価不能 |
| D4 (8-bit roundtrip) | -0.5〜-1 dB (splat 微減) | +0.06 dB (微増) | +~1 dB | **impact ほぼ無視可能、仮説と方向逆** |
| D1 (split file) | -1〜-2 dB (test 難) | (dataset 不完全) | — | 後続 task で実測予定 |
| **合計 (val 限定)** | **+2.5〜+4 dB 底上げ** | **-23 dB 崩壊** | **-27 dB** | **主仮説 falsified** |
splat-rs trainer は white-bg pre-composite した GT (`load_rgba_white_bg`) を target に L1/SSIM loss で fit する。background pixel (元 PNG α=0 領域) は target=(1,1,1) なので、loss が 背景にも "白く塗れ" と要求する。これに対し rasterize.metal は bg-agnostic な premultiplied 出力 (`pix_out = Σ rgb·α·T`, `α_out = 1-T`) を返すため、trainer はこの要求を opaque-white splat を背景に大量配置する ことで満たした。
結果として final.ply は "白い不透明背景を持つ rendered" を返す model。これを brush 流 (背景 = premultiplied GT = (0,0,0) と直接比較) で eval すると、背景 pixel 全体で rendered ≈ (1,1,1) vs target = (0,0,0)、MSE ≈ 1。NeRF Synthetic lego の背景比率は 50% 以上なので、image 全体 MSE ≈ 0.5 → PSNR = -10·log10(0.5) ≈ 3 dB 帯に押し下げ。実測 1.67 dB はこの帯 (frontier 50% で完全一致しても背景で 100% miss するので)。
splat/runs/lego-sh3-30k/view_00.png (forward_rgba output 直書き、白背景に lego モデル) → trainer 出力は 背景が白い不透明領域、premultiplied 出力なら背景は透明 RGB=0 のはずだが、white-bg loss で fit された結果 opaque-white splat が背景を埋めているgt_00.png (load_rgba_white_bg で white-bg composite 済み target) → 同じ白背景、これが loss target、両者は legacy convention 下で 30.36 dB と密接にマッチ/Users/otkrickey/dev/3dgs-workspace/datasets/nerf_synthetic/lego/test/ には r_*.png が 36 frame しか無く、transforms_test.json は 200 frame を参照 (差分は normal/depth aux PNG)。**dataset の RGB test split が不完全**。後続 task (P1.A.3 followup or P1.B) で nerf_synthetic.zip の test 部分を再 DL するか、別 dataset (m4-brush-bench に存在する場合) を使う必要あり。今回 val (100 view) で M1 gate を締めるのに impact 無し (主仮説 falsify には val 1 set で十分)。
splat/crates/splat-io/src/dataset.rs — GtConvention { WhiteBg, Premultiplied } enum + load_nerf_synthetic_with_convention(dir, split, convention) wrapper + load_rgba_premultiplied 関数 (byte 空間 premultiply、brush scene.rs:212-232 AlphaMode::Transparent と同等)splat/crates/splat-cli/src/cmd/eval.rs (新規) — splat eval --ply --dataset --split --convention --quantize-8bit subcommand、既存 trained PLY を再評価。training は触らない (config 拡張も避け、CLI flag 経路で完結)splat/crates/splat-cli/src/main.rs, cmd/mod.rs — subcommand wirecompute_psnr (train.rs:182) は 未変更、新 compute_psnr_legacy + compute_psnr_brush を eval.rs に複製 (training 内部の loss path への影響ゼロを保証)FullConfig schema 変更なし、既存 configs/*.toml は無変更で動作 (deny_unknown_fields も触らない)load_rgba_white_bg → load_rgba_premultiplied を train 経路でも選択可能化、loss kernel (`splat/shaders/loss/loss.metal`) が premultiplied 4ch を受け取れることを確認 (既存 4ch path を活用)# build
cd splat
cargo build --release -p splat-cli
# 4 通り sweep (test 2 通りは dataset 不完全のため skip)
PLY=runs/lego-sh3-30k/final.ply
DS=/Users/otkrickey/dev/3dgs-workspace/datasets/nerf_synthetic/lego
# 1: legacy/val/no-quant (sanity check vs result.toml の 24.879)
./target/release/splat eval --ply $PLY --dataset $DS --split val --convention legacy
# → 24.8787
# 2: legacy/val/quant (8-bit impact 単体)
./target/release/splat eval --ply $PLY --dataset $DS --split val --convention legacy --quantize-8bit
# → 24.8787
# 3: brush/val/no-quant (premultiplied 単体 impact)
./target/release/splat eval --ply $PLY --dataset $DS --split val --convention brush
# → 1.6049
# 4: brush/val/quant (完全 brush 準拠)
./target/release/splat eval --ply $PLY --dataset $DS --split val --convention brush --quantize-8bit
# → 1.6665
p1-a-eval-convention-audit (本 doc が §6 main hypothesis を falsify)p1-a-1-brush-eval-auditp1-a-2-splat-eval-auditbrush-vs-splat-37dB-gap-analysis