brush trainer (37.40 dB) と splat-rs (24.879 dB) の 12.56 dB gap の真因切り分けとして、両 trainer の eval pipeline を file:line 単位で完全 audit。両者は逆方向の convention を採用しており、特に (1) split file (splat=val 100view / brush=test 200view)、(2) background composite (splat=GT に white pre-comp / rendered 未合成 vs brush=両者 premultiplied-α で bg=ZERO)、(3) 8-bit roundtrip 有無、の 3 軸が apparent gap の主因候補。PSNR formula 自体 (MAX=1, log10, RGB only, per-view mean) は同等で、algorithmic gap ではない。P1.M1 gate 充足: 数式 diff の定量化完了、A.3 reproducer で apparent/real gap 仕分け実施予定。
P1.M1 gate (eval convention 統一確定 + 数式 diff 定量化) 充足のための統合 finding doc。並列で実行した p1-a-1-brush-eval-audit (brush 側、12 axes) と p1-a-2-splat-eval-audit (splat-rs 側、7 軸 + 内部 inconsistency 1 軸) を突合し、両者の diff を 7 axes (D1-D7) に整理。これが P1.A.3 reproducer (同 PLY を 2 convention で測る script) と P1.A.4 (splat-rs eval を brush 準拠に統一、config flag 化) の input となる。
両 trainer の eval pipeline 数式・dataset 範囲・前処理は完全に file:line 単位で定式化された。両者は (PSNR formula 本体は同等だが) 逆方向の convention を採用しており、apparent gap (eval convention 差で見かけ上膨らんだ部分) と real gap (algorithmic 真差) を切り分けるための前提資料が揃った。M1 gate pass、P1.A.3 (reproducer) → P1.A.4 (統一実装) → P1.B (互換 config 起草) へ進行可能。
| # | diff 軸 | splat-rs (本実装) | brush 標準 | 推定 PSNR impact | 確信度 |
|---|---|---|---|---|---|
| D1 | split file | transforms_val.json (100 view) | transforms_test.json (200 view) | ±1〜2 dB (起点違い) | 確定 |
| D2 | background composite | rendered 未合成 (T 残部 RGB=0) + GT に white-bg pre-comp | 両者 premultiplied-α 空間で bg=ZERO 比較 | -2〜-4 dB (splat 不利) | 確定 |
| D3 | α-mask / premultiply | なし (target α=1.0 hardcode) | AlphaMode::Transparent で GT を byte 空間 premultiply | -1〜-3 dB (splat 不利) | 確定 |
| D4 | 8-bit roundtrip / clamp | raw f32 (clamp / quant なし) | pred を (*255).round()/255 で量子化 | +0.5〜1 dB (splat 微高) | 確定 |
| D5 | PSNR formula 本体 | MAX=1, log10, RGB only, per-view mean | MAX=1, log10, RGB only, per-view mean | neutral | 確定 (同等) |
| D6 | α channel in MSE | 除外 (training は 4ch、eval は RGB only) | 除外 (eval は RGB only) | neutral | 確定 |
| D7 | training-eval consistency | training=4ch loss / eval=RGB only (inconsistency) | training=match_alpha=0.1 / eval=RGB only | 間接 (convergence 影響) | 推定 |
brush は GT を premultiplied-α 化して rendered (bg=ZERO 出力) と直接比較。NeRF Synthetic の透明領域 (lego 周辺など、画面の 50% 以上) では premultiplied GT = (0,0,0,0) かつ rendered = (0,0,0,1-T≈0) で 完全一致、MSE 分母に 0 寄与が大量に入り PSNR を構造的に +3 dB 以上 嵩上げ。
一方 splat-rs は GT を white-bg pre-composite (rgb·α + (1-α)·1) して、rendered (bg 未合成、T 残部 RGB=0) と比較。透明領域では GT = (1,1,1) vs rendered = (0,0,0) で MSE = 1 が pixel あたり残り、convergence の不完全分が PSNR を直接押し下げる (training α-loss で吸収を試みるが完全には収束しない)。
両者の convention を揃えるだけで -3〜-6 dB の apparent gap が消える可能性が高い (A.3 reproducer で実証予定)。
入力:
rendered : f32 H·W·4 RGBA (chunks(4) で per-pixel、α 列 = 1-T、bg 未合成)
target : f32 H·W·4 RGBA (chunks(4) で per-pixel、α 列 = 1.0 hardcode、white-bg pre-comp 済み)
MSE = (1 / (3·H·W)) · Σ_{p ∈ pixels} Σ_{c ∈ {R,G,B}} (rendered[p,c] − target[p,c])²
PSNR = -10 · log10(MSE) if MSE > 1e-12 else 100.0
split: transforms_val.json (100 view)
per-view PSNR の mean (mean-PSNR、not PSNR-of-mean-MSE)
clamp / quantize なし
入力:
pred : f32 H·W·3 RGB (slice(s![.., .., 0..3])、bg=Vec3::ZERO で α-blend 済み、premultiplied)
pred' : f32 H·W·3 RGB (pred を (*255).round()/255 で 8-bit roundtrip)
gt : u8 H·W·4 RGBA (AlphaMode::Transparent → byte 空間で α premultiply 済み)
MSE = mean((pred' − gt_rgb_normalized)²) over H·W·3 (α channel は loss 対象外)
PSNR = 10 · log10(1 / MSE) = -10 · log10(MSE) (MAX² = 1 暗黙)
split: transforms_val.json → transforms_test.json (val 無いとき fallback)
NeRF Synthetic は test (200 view) を使用、subsample なし、全 view を逐次回し
per-view PSNR の mean
PSNR の本体定義式 (-10 · log10(MSE), MAX = 1.0, RGB only, per-view mean) は 完全同等。違いは入力画像 (pred / gt) の作られ方:
splat-rs=val (100 view)、brush=test (200 view) は 完全に異なる camera set。NeRF Synthetic の train/val/test split は paper 標準で 100/100/200 で、val は train から近い角度、test は新規 novel view。同 trainer / 同 ply でも val/test では 1-2 dB 程度の差が出る (一般的に test の方が難しく PSNR 低い)。
| 軸 | 推定 dB shift | 備考 |
|---|---|---|
| D2 (bg composite) | +3〜+5 dB (splat-rs 側で底上げ) | 両者を brush 流 (premultiplied + bg=ZERO) に揃えると splat-rs 側が +3〜+5 dB |
| D3 (α-mask / premultiply) | +1〜+2 dB (splat-rs 側で底上げ) | GT premultiply で透明領域の "free 0 match" を獲得 |
| D4 (8-bit roundtrip) | -0.5〜-1 dB (splat-rs 側で打ち消し) | raw f32 から u8 quant に揃えると微減 |
| D1 (split file) | -1〜-2 dB (splat-rs 側で減点) | val (100view) から test (200view) に揃えると novel-view 難しさで減 |
| D5-D7 | neutral or 二次効果 | formula 本体は同等、training-eval inconsistency は間接 |
| **合計** | **+2.5〜+4 dB (splat-rs 底上げ)** | **apparent gap 縮小、24.879 → 27〜29 dB に近づく可能性** |
brush 37.4 dB vs splat-rs 24.879 dB の 12.56 dB gap のうち、推定 -3〜-6 dB は eval convention 差 (apparent)、残り -6〜-9 dB が真の algorithmic gap。A.3 reproducer で同 PLY を両 convention で評価し、apparent / real の仕分けを実証する。仮説が正しければ splat-rs の brush convention 数字は 27〜30 dB 圏 となり、P1.M3 生命線 (Lego 30k で PSNR > 30 dB) に そもそも近い距離まで来ている可能性 がある。
--alpha-mode=masked で取られていた場合: premultiply 経路を通らず、masked-only loss で評価される。この場合 D2/D3 の apparent 嵩上げ仮説は無効化 → algorithmic gap が大きい。目的: 同 trained ply / 同 GT camera から、D1-D4 を 1 軸ずつ切り替えて PSNR を測り、各 convention の dB 寄与を実証する。
brush-c API、要調査)docs/findings/p1-a-3-cross-eval-reproducer.toml + scripts/p1-a-3-eval-bridge.py、apparent/real gap の dB 内訳を確定[eval] convention = "brush" (default) or "legacy_splat"、split = "test" (default) or "val"train.rs:115 split path 設定可能化、(ii) train.rs:182 compute_psnr を brush 準拠 path 追加、(iii) load_rgba_white_bg と並行で load_rgba_premultiplied 追加、(iv) eval 時 rendered に bg 合成オプション追加| 役割 | trainer | path:line |
|---|---|---|
| PSNR 関数本体 | splat-rs | splat/crates/splat-cli/src/cmd/train.rs:182-200 |
| eval driver | splat-rs | splat/crates/splat-cli/src/cmd/train.rs:112-151 |
| split file hardcode | splat-rs | splat/crates/splat-cli/src/cmd/train.rs:115 (val) |
| target loader (white-bg) | splat-rs | splat/crates/splat-io/src/dataset.rs:140-166 |
| rasterize (bg 未合成) | splat-rs | splat/shaders/forward/rasterize.metal:140-218 |
| forward driver | splat-rs | splat/crates/splat-train-v1/src/trainer.rs:97-104 |
| training loss (α 含む 4ch) | splat-rs | splat/shaders/loss/loss.metal:31-88 |
| eval entry (PSNR/SSIM) | brush | /Users/otkrickey/dev/brush/crates/brush-train/src/eval.rs:22-63 |
| eval loop + 平均 | brush | /Users/otkrickey/dev/brush/crates/brush-process/src/train_stream.rs:455-513 |
| GT premultiply | brush | /Users/otkrickey/dev/brush/crates/brush-dataset/src/scene.rs:212-232 |
| AlphaMode 決定 | brush | /Users/otkrickey/dev/brush/crates/brush-dataset/src/scene.rs:38-62 |
| image_loss_eval | brush | /Users/otkrickey/dev/brush/crates/brush-loss/src/lib.rs:1090-1100 |
| split file loader | brush | /Users/otkrickey/dev/brush/crates/brush-dataset/src/formats/nerfstudio.rs:259-308 |
| render_splats (bg=ZERO) | brush | /Users/otkrickey/dev/brush/crates/brush-render/src/gaussian_splats.rs:252-314 |
p1-a-1-brush-eval-auditp1-a-2-splat-eval-auditbrush-vs-splat-37dB-gap-analysism4-brush-bencha-4-nerf-synthetic-scene-resultsp1-a-3-cross-eval-reproducer (未作成、A.3 で生成予定)