| |
| """Round-5 — exposure compensation hypothesis (decisive).""" |
| import os, sys, json, math, inspect |
| 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 |
|
|
| DATASET_ROOT = "/root/autodl-tmp/dataset/tnt" |
| OUTPUT_ROOT = "/root/autodl-tmp/SplatAtlas/outputs" |
| SCENES = [("truck", "PASS"), ("lighthouse", "FAIL")] |
|
|
|
|
| def sec(t): |
| print("\n" + "=" * 70); print(f" {t}"); print("=" * 70) |
|
|
|
|
| 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 probe_exposure_structure(scene): |
| sec(f"PROBE 17 — exposure.json structure [{scene}]") |
| p = os.path.join(OUTPUT_ROOT, f"vanilla_3dgs_{scene}", "exposure.json") |
| if not os.path.exists(p): |
| print(f"[!] not found") |
| return None |
| data = json.load(open(p)) |
| if not isinstance(data, dict): |
| print(f"Not a dict: {type(data)}") |
| return None |
| keys = list(data.keys()) |
| print(f"Entries : {len(keys)}") |
| print(f"Sample keys : {keys[:5]}") |
|
|
| sample = np.asarray(data[keys[0]]) |
| print(f"Sample shape: {sample.shape}") |
| print(f"Sample[0]:\n{sample}") |
|
|
| all_m = np.stack([np.asarray(data[k]) for k in keys]) |
| print(f"\nAll exposures shape: {all_m.shape}") |
|
|
| if all_m.ndim == 3 and all_m.shape[1] == 3 and all_m.shape[2] >= 3: |
| linear = all_m[:, :3, :3] |
| bias = all_m[:, :3, 3] if all_m.shape[2] == 4 else np.zeros((len(keys), 3)) |
|
|
| eye = np.eye(3)[None] |
| lin_dev = np.abs(linear - eye).reshape(len(keys), -1).max(axis=1) |
| bias_mag = np.linalg.norm(bias, axis=1) |
|
|
| print(f"\nLinear max |dev from I|:") |
| print(f" min={lin_dev.min():.5f} p50={np.percentile(lin_dev,50):.5f} " |
| f"p95={np.percentile(lin_dev,95):.5f} max={lin_dev.max():.5f}") |
| print(f"Bias magnitude ||b||:") |
| print(f" min={bias_mag.min():.5f} p50={np.percentile(bias_mag,50):.5f} " |
| f"p95={np.percentile(bias_mag,95):.5f} max={bias_mag.max():.5f}") |
|
|
| trivial = ((lin_dev < 0.01) & (bias_mag < 0.01)).sum() |
| large = ((lin_dev >= 0.05) | (bias_mag >= 0.05)).sum() |
| print(f"\nNear-identity (lin_dev<0.01 & bias<0.01): {trivial} / {len(keys)}") |
| print(f"Large deviation (>=0.05) : {large} / {len(keys)}") |
| if trivial > len(keys) * 0.95: |
| print("[VERDICT] 曝光几乎都是单位阵 → 曝光补偿不是根因") |
| elif large > 0: |
| print("[VERDICT] 存在显著曝光偏移 → 曝光补偿极可能是根因") |
| return data |
|
|
|
|
| |
| def probe_exposure_corrected(scene, exposures): |
| sec(f"PROBE 18 — Apply exposure to render & recompute PSNR [{scene}]") |
| if exposures is None: |
| print("[!] no exposure data, skip") |
| return |
|
|
| from scene.dataset_readers import readColmapSceneInfo |
| from utils.graphics_utils import getWorld2View2 |
| from utils.general_utils import PILtoTorch |
|
|
| source_path = os.path.join(DATASET_ROOT, scene) |
| cell = os.path.join(OUTPUT_ROOT, f"vanilla_3dgs_{scene}") |
| img_dir = "images_2"; resolution = 2 |
| scene_info = readColmapSceneInfo(*build_args(source_path, img_dir)) |
| test_cams = scene_info.test_cameras |
|
|
| pd = PlyData.read(os.path.join(cell, "point_cloud", "iteration_30000", "point_cloud.ply")) |
| v = pd['vertex'] |
| device = torch.device("cuda") |
| 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']))) |
| f_dc = t32(np.stack((v['f_dc_0'], v['f_dc_1'], v['f_dc_2']), -1)).unsqueeze(1) |
| f_rest = t32(np.stack([v[f'f_rest_{i}'] for i in range(45)], -1)).view(-1, 3, 15).transpose(1, 2) |
| shs = torch.cat([f_dc, f_rest], dim=1) |
| bg_color = torch.tensor([0., 0., 0.], device=device) |
|
|
| |
| def get_exp(image_name): |
| candidates = [image_name, |
| os.path.splitext(image_name)[0], |
| image_name.lower(), |
| os.path.splitext(image_name)[0].lower()] |
| for c in candidates: |
| if c in exposures: |
| return torch.tensor(np.asarray(exposures[c]), dtype=torch.float32, device=device) |
| return None |
|
|
| |
| matched = sum(1 for c in test_cams if get_exp(c.image_name) is not None) |
| print(f"Exposure key-match rate: {matched} / {len(test_cams)} test cams") |
| if matched == 0: |
| print(f" test cam names: {[c.image_name for c in test_cams[:3]]}") |
| print(f" exposure keys : {list(exposures.keys())[:3]}") |
| print("[!] 无法匹配,请检查 key 命名") |
| return |
|
|
| raw_psnrs, corr_psnrs = [], [] |
| print(f"\n {'i':>3} {'cam':>14} {'raw':>8} {'corrected':>10} {'gain':>8}") |
| print(" " + "-" * 50) |
| for i, c in enumerate(test_cams): |
| w = int(round(c.width / resolution)); h = int(round(c.height / resolution)) |
| viewmat = t32(getWorld2View2(np.array(c.R), np.array(c.T))).unsqueeze(0) |
| fx = w / (2 * math.tan(c.FovX / 2)); fy = h / (2 * math.tan(c.FovY / 2)) |
| K = t32(np.array([[fx,0,w/2],[0,fy,h/2],[0,0,1]])).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=3, packed=True, render_mode='RGB', |
| backgrounds=bg_color.unsqueeze(0)) |
| 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_raw = ((render - gt) ** 2).mean().item() |
| raw_p = 10 * math.log10(1.0 / max(mse_raw, 1e-10)) |
| raw_psnrs.append(raw_p) |
|
|
| exp_mat = get_exp(c.image_name) |
| if exp_mat is not None: |
| A = exp_mat[:3, :3]; b = exp_mat[:3, 3] if exp_mat.shape[1] >= 4 else torch.zeros(3, device=device) |
| |
| corrected = render @ A.T + b |
| corrected = corrected.clamp(0, 1) |
| mse_c = ((corrected - gt) ** 2).mean().item() |
| corr_p = 10 * math.log10(1.0 / max(mse_c, 1e-10)) |
| else: |
| corr_p = raw_p |
| corr_psnrs.append(corr_p) |
|
|
| if i < 15 or (corr_p - raw_p) > 2: |
| print(f" {i:>3} {c.image_name:>14} {raw_p:>8.2f} {corr_p:>10.2f} {corr_p - raw_p:>+8.2f}") |
|
|
| raw_arr, corr_arr = np.array(raw_psnrs), np.array(corr_psnrs) |
| print(f"\n Ours RAW mean PSNR: {raw_arr.mean():.4f} dB") |
| print(f" Ours CORRECTED mean PSNR: {corr_arr.mean():.4f} dB") |
|
|
| native_psnr = None |
| bp = os.path.join(cell, "metrics_test_iter30000.json") |
| if os.path.exists(bp): |
| bd = json.load(open(bp)) |
| native_psnr = bd.get("photometric", {}).get("PSNR", bd.get("PSNR")) |
| print(f" Native baseline PSNR : {native_psnr:.4f} dB") |
| if native_psnr: |
| print(f" Δ raw : {raw_arr.mean() - native_psnr:+.4f} dB") |
| print(f" Δ corrected : {corr_arr.mean() - native_psnr:+.4f} dB") |
| if abs(corr_arr.mean() - native_psnr) < 0.1: |
| print(f"\n [VERDICT CONFIRMED] 曝光补偿就是根因。") |
| elif abs(corr_arr.mean() - native_psnr) < abs(raw_arr.mean() - native_psnr) - 0.3: |
| print(f"\n [PARTIAL] 曝光补偿解释了大部分差异,但残差>0.3dB,可能还有次级因素") |
| else: |
| print(f"\n [REJECTED] 曝光补偿影响有限,需另寻根因") |
|
|
|
|
| def main(): |
| for scene, label in SCENES: |
| print(f"\n\n{'#'*70}\n# SCENE: {scene} [{label}]\n{'#'*70}") |
| exp = probe_exposure_structure(scene) |
| probe_exposure_corrected(scene, exp) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|