| |
| """Ranking consistency probe — DSLR-FAIL scenes × golden methods. |
| Decides whether the ~1.2 dB systematic offset still preserves within-benchmark |
| method ordering. |
| """ |
| import os, sys, inspect, math, json, time |
| import numpy as np |
| import torch |
| from PIL import Image, ImageOps |
| from plyfile import PlyData |
|
|
| sys.path.insert(0, '/root/autodl-tmp/3dgsAtlas_official') |
| import gsplat |
| try: |
| from scipy.stats import spearmanr |
| HAS_SCIPY = True |
| except ImportError: |
| HAS_SCIPY = False |
|
|
| DATASET_ROOT = "/root/autodl-tmp/dataset/tnt" |
| OUTPUT_ROOT = "/root/autodl-tmp/SplatAtlas/outputs" |
|
|
| METHODS = ["vanilla_3dgs", "erankgs", "ges", "lightgaussian", "opti3dgs", |
| "reactgs", "steepgs", "absgs", "gaussianpro", "minisplatting", "pixelgs"] |
| SCENES = ["palace", "lighthouse", "francis", "temple", "auditorium"] |
|
|
|
|
| def build_args(source_path, img_dir): |
| from scene.dataset_readers import readColmapSceneInfo |
| sig = inspect.signature(readColmapSceneInfo) |
| a = [] |
| for i, (k, p) in enumerate(sig.parameters.items()): |
| if i == 0: a.append(source_path) |
| elif i == 1: a.append(img_dir) |
| elif k == "eval": a.append(True) |
| elif k == "train_test_exp": a.append(False) |
| else: a.append(p.default if p.default != inspect.Parameter.empty else "") |
| return a |
|
|
|
|
| def find_cell(method, scene): |
| target = f"{method}_{scene}".lower() |
| if not os.path.isdir(OUTPUT_ROOT): |
| return None |
| for d in os.listdir(OUTPUT_ROOT): |
| if d.lower() == target: |
| return os.path.join(OUTPUT_ROOT, d) |
| return None |
|
|
|
|
| def load_ply_tensors(ply_path, device): |
| v = PlyData.read(ply_path)['vertex'] |
| props = v.data.dtype.names |
| def t32(x): return torch.tensor(x, dtype=torch.float32, device=device) |
| means = t32(np.stack((v['x'], v['y'], v['z']), -1)) |
| quats = t32(np.stack((v['rot_0'], v['rot_1'], v['rot_2'], v['rot_3']), -1)) |
| scales = torch.exp(t32(np.stack((v['scale_0'], v['scale_1'], v['scale_2']), -1))) |
| opacities = torch.sigmoid(t32(np.asarray(v['opacity']))) |
| n_rest = sum(1 for p in props if p.startswith('f_rest_')) |
| sh_per_ch = n_rest // 3 |
| sh_degree_map = {0: 0, 3: 1, 8: 2, 15: 3} |
| sh_degree = sh_degree_map.get(sh_per_ch, min(3, int(math.sqrt(sh_per_ch + 1)) - 1)) |
| f_dc = t32(np.stack((v['f_dc_0'], v['f_dc_1'], v['f_dc_2']), -1)).unsqueeze(1) |
| if n_rest > 0: |
| f_rest = t32(np.stack([v[f'f_rest_{i}'] for i in range(n_rest)], -1)) |
| f_rest = f_rest.view(-1, 3, sh_per_ch).transpose(1, 2) |
| shs = torch.cat([f_dc, f_rest], dim=1) |
| else: |
| shs = f_dc |
| return means, quats, scales, opacities, shs, sh_degree |
|
|
|
|
| def render_cell(method, scene, scene_info, device): |
| from utils.graphics_utils import getWorld2View2 |
| from utils.general_utils import PILtoTorch |
|
|
| cell = find_cell(method, scene) |
| if cell is None: |
| return None, None, "NO_CELL" |
|
|
| bp = os.path.join(cell, "metrics_test_iter30000.json") |
| native = None |
| if os.path.exists(bp): |
| bd = json.load(open(bp)) |
| native = bd.get("photometric", {}).get("PSNR") |
| if native is None: |
| native = bd.get("PSNR") |
|
|
| ply_path = os.path.join(cell, "point_cloud", "iteration_30000", "point_cloud.ply") |
| if not os.path.exists(ply_path): |
| return None, native, "NO_PLY" |
|
|
| try: |
| means, quats, scales, opacities, shs, sh_degree = load_ply_tensors(ply_path, device) |
| except Exception as e: |
| return None, native, f"PLY_ERR:{type(e).__name__}" |
|
|
| source_path = os.path.join(DATASET_ROOT, scene) |
| img_dir = "images_2" |
| resolution = 2 |
| test_cams = scene_info.test_cameras |
| bg = torch.tensor([0., 0., 0.], device=device) |
|
|
| psnrs = [] |
| try: |
| for c in test_cams: |
| w = int(round(c.width / resolution)) |
| h = int(round(c.height / resolution)) |
| viewmat = torch.tensor(getWorld2View2(np.array(c.R), np.array(c.T)), |
| dtype=torch.float32, device=device).unsqueeze(0) |
| fx = w / (2 * math.tan(c.FovX / 2)) |
| fy = h / (2 * math.tan(c.FovY / 2)) |
| K = torch.tensor([[fx, 0, w/2], [0, fy, h/2], [0, 0, 1]], |
| dtype=torch.float32, device=device).unsqueeze(0) |
| with torch.no_grad(): |
| colors, _, _ = gsplat.rasterization( |
| means=means, quats=quats, scales=scales, opacities=opacities, |
| colors=shs, viewmats=viewmat, Ks=K, width=w, height=h, |
| sh_degree=sh_degree, packed=True, render_mode='RGB', |
| backgrounds=bg.unsqueeze(0), |
| rasterize_mode='classic') |
| render = colors[0].clamp(0, 1) |
| pil = Image.open(os.path.join(source_path, img_dir, c.image_name)) |
| pil = ImageOps.exif_transpose(pil) |
| gt_chw = PILtoTorch(pil, (w, h)).to(device) |
| if gt_chw.shape[0] == 4: gt_chw = gt_chw[:3] |
| gt = gt_chw.permute(1, 2, 0) |
| mse = ((render - gt) ** 2).mean().item() |
| psnrs.append(10 * math.log10(1.0 / max(mse, 1e-10))) |
| except Exception as e: |
| return None, native, f"RENDER_ERR:{type(e).__name__}" |
|
|
| del means, quats, scales, opacities, shs |
| torch.cuda.empty_cache() |
| return float(np.mean(psnrs)), native, "OK" |
|
|
|
|
| def print_matrix(mat, methods, scenes, title, fmt="{:>10.3f}"): |
| print(f"\n {title}") |
| header = " " + " " * 16 + " ".join(f"{s[:10]:>10}" for s in scenes) |
| print(header) |
| for mi, method in enumerate(methods): |
| row = " ".join( |
| (fmt.format(mat[mi, si]) if not np.isnan(mat[mi, si]) else f"{'—':>10}") |
| for si in range(len(scenes))) |
| print(f" {method:>16} {row}") |
|
|
|
|
| def rank_from_psnr(arr, valid_mask): |
| """Higher PSNR → rank 1. NaN / invalid → NaN.""" |
| ranks = np.full_like(arr, np.nan, dtype=float) |
| valid_idx = np.where(valid_mask)[0] |
| if len(valid_idx) == 0: |
| return ranks |
| vals = arr[valid_idx] |
| order = np.argsort(-vals) |
| for r, o in enumerate(order): |
| ranks[valid_idx[o]] = r + 1 |
| return ranks |
|
|
|
|
| def spearman_rho(a, b): |
| """Naive Spearman; fallback if scipy not available.""" |
| if HAS_SCIPY: |
| rho, p = spearmanr(a, b) |
| return rho, p |
| ra = np.argsort(np.argsort(a)) |
| rb = np.argsort(np.argsort(b)) |
| mean_a, mean_b = ra.mean(), rb.mean() |
| num = ((ra - mean_a) * (rb - mean_b)).sum() |
| den = math.sqrt(((ra - mean_a) ** 2).sum() * ((rb - mean_b) ** 2).sum()) |
| return (num / den if den > 0 else float('nan'), float('nan')) |
|
|
|
|
| def main(): |
| from scene.dataset_readers import readColmapSceneInfo |
| device = torch.device("cuda") |
|
|
| print("Preloading scene infos...") |
| scene_infos = {} |
| for sc in SCENES: |
| source_path = os.path.join(DATASET_ROOT, sc) |
| if not os.path.exists(source_path): |
| print(f" [!] dataset path missing: {source_path}") |
| continue |
| scene_infos[sc] = readColmapSceneInfo(*build_args(source_path, "images_2")) |
| print(f" [preloaded] {sc}: {len(scene_infos[sc].test_cameras)} test cams") |
|
|
| ours_mat = np.full((len(METHODS), len(SCENES)), np.nan) |
| native_mat = np.full((len(METHODS), len(SCENES)), np.nan) |
| status_mat = [["-"] * len(SCENES) for _ in METHODS] |
|
|
| print("\nRunning ...\n") |
| t_all = time.time() |
| for mi, method in enumerate(METHODS): |
| for si, scene in enumerate(SCENES): |
| if scene not in scene_infos: |
| status_mat[mi][si] = "NOSRC" |
| continue |
| t0 = time.time() |
| ours, native, status = render_cell(method, scene, scene_infos[scene], device) |
| dt = time.time() - t0 |
| if ours is not None: ours_mat[mi, si] = ours |
| if native is not None: native_mat[mi, si] = native |
| status_mat[mi][si] = status |
| delta = (ours - native) if (ours is not None and native is not None) else float('nan') |
| ours_s = f"{ours:7.3f}" if ours is not None else " × " |
| native_s = f"{native:7.3f}" if native is not None else " × " |
| delta_s = f"{delta:+7.3f}" if not (isinstance(delta, float) and math.isnan(delta)) else " × " |
| print(f" [{method:<14} {scene:<11}] ours={ours_s} native={native_s} Δ={delta_s} " |
| f"({dt:5.1f}s {status})") |
| print(f"\nTotal time: {time.time() - t_all:.1f}s") |
|
|
| |
| print("\n" + "=" * 90) |
| print(" RANKING CONSISTENCY REPORT") |
| print("=" * 90) |
|
|
| print_matrix(ours_mat, METHODS, SCENES, "OURS PSNR (gsplat classic)", "{:>10.3f}") |
| print_matrix(native_mat, METHODS, SCENES, "NATIVE baseline PSNR", "{:>10.3f}") |
| delta_mat = ours_mat - native_mat |
| print_matrix(delta_mat, METHODS, SCENES, "Δ (ours - native)", "{:>+10.3f}") |
|
|
| valid_delta = delta_mat[~np.isnan(delta_mat)] |
| valid_ours = ours_mat[~np.isnan(ours_mat)] |
| print(f"\n === Δ stats (N={len(valid_delta)}) ===") |
| print(f" mean = {valid_delta.mean():+.4f} std = {valid_delta.std():.4f}") |
| print(f" min = {valid_delta.min():+.4f} max = {valid_delta.max():+.4f}") |
| print(f" |Δ|>1dB : {(np.abs(valid_delta) > 1).sum()}/{len(valid_delta)}") |
| print(f" |Δ|>2dB : {(np.abs(valid_delta) > 2).sum()}/{len(valid_delta)}") |
| print(f" |Δ|>3dB : {(np.abs(valid_delta) > 3).sum()}/{len(valid_delta)}") |
|
|
| print(f"\n === OURS absolute value distribution ===") |
| print(f" min = {valid_ours.min():.3f} max = {valid_ours.max():.3f}") |
| print(f" <10 dB : {(valid_ours < 10).sum()}/{len(valid_ours)} [CATASTROPHIC]") |
| print(f" <15 dB : {(valid_ours < 15).sum()}/{len(valid_ours)}") |
| print(f" <20 dB : {(valid_ours < 20).sum()}/{len(valid_ours)}") |
|
|
| |
| cat_cells = [] |
| for mi in range(len(METHODS)): |
| for si in range(len(SCENES)): |
| if not np.isnan(ours_mat[mi, si]) and ours_mat[mi, si] < 15: |
| cat_cells.append((METHODS[mi], SCENES[si], ours_mat[mi, si], |
| native_mat[mi, si], delta_mat[mi, si])) |
| if cat_cells: |
| print(f"\n === Cells with OURS < 15 dB ===") |
| for m, s, o, n, d in sorted(cat_cells, key=lambda x: x[2]): |
| print(f" {m:>16} × {s:<12} ours={o:7.3f} native={n:7.3f} Δ={d:+.3f}") |
|
|
| |
| print(f"\n === Per-scene ranking (Spearman ρ, higher PSNR = rank 1) ===") |
| print(f" {'scene':>12} {'ρ':>8} {'p':>8} {'N':>3} concordant_pairs") |
| for si, scene in enumerate(SCENES): |
| valid = ~np.isnan(ours_mat[:, si]) & ~np.isnan(native_mat[:, si]) |
| n = valid.sum() |
| if n < 3: |
| print(f" {scene:>12} {'—':>8} {'—':>8} {n:>3}") |
| continue |
| rho, p = spearman_rho(ours_mat[valid, si], native_mat[valid, si]) |
| r_ours = rank_from_psnr(ours_mat[:, si], valid) |
| r_native = rank_from_psnr(native_mat[:, si], valid) |
| |
| rank_diff = np.abs(r_ours - r_native) |
| max_rd = np.nanmax(rank_diff) |
| p_str = f"{p:.4f}" if not math.isnan(p) else "n/a" |
| print(f" {scene:>12} {rho:>+8.4f} {p_str:>8} {n:>3} max_rank_shift={max_rd:.0f}") |
|
|
| |
| print(f"\n === Per-method ranking across {len(SCENES)} scenes ===") |
| for mi, method in enumerate(METHODS): |
| valid = ~np.isnan(ours_mat[mi, :]) & ~np.isnan(native_mat[mi, :]) |
| n = valid.sum() |
| if n < 3: continue |
| rho, _ = spearman_rho(ours_mat[mi, valid], native_mat[mi, valid]) |
| print(f" {method:>16} ρ={rho:+.4f} (N={n})") |
|
|
| |
| out_json = "/root/autodl-tmp/SplatAtlas/scripts/phase1_validation/ranking_consistency.json" |
| out = { |
| "methods": METHODS, "scenes": SCENES, |
| "ours_psnr": ours_mat.tolist(), |
| "native_psnr": native_mat.tolist(), |
| "delta": delta_mat.tolist(), |
| "status": status_mat, |
| } |
| with open(out_json, "w") as f: |
| json.dump(out, f, indent=2, default=str) |
| print(f"\n Saved: {out_json}") |
|
|
| |
| print(f"\n === VERDICT ===") |
| ok_abs = (valid_ours >= 15).all() |
| ok_delta = np.abs(valid_delta).mean() < 2.0 |
| n_rho_high = 0 |
| if HAS_SCIPY or not HAS_SCIPY: |
| for si, scene in enumerate(SCENES): |
| valid = ~np.isnan(ours_mat[:, si]) & ~np.isnan(native_mat[:, si]) |
| if valid.sum() >= 3: |
| rho, _ = spearman_rho(ours_mat[valid, si], native_mat[valid, si]) |
| if rho > 0.8: |
| n_rho_high += 1 |
| print(f" OURS absolute ≥15dB: {'YES' if ok_abs else 'NO'}") |
| print(f" |Δ|mean < 2dB: {'YES' if ok_delta else 'NO'}") |
| print(f" Scenes with ρ>0.8: {n_rho_high}/{len(SCENES)}") |
| if ok_abs and n_rho_high >= len(SCENES) - 1: |
| print(f" → Benchmark 内部一致性良好。可以收工。") |
| else: |
| print(f" → 有异常,需要看具体哪些 cell 不稳。") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|