File size: 8,916 Bytes
23e73f9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 | #!/usr/bin/env python
"""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
# ---------- Probe 17: exposure.json 结构 ----------
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
# ---------- Probe 18: 应用 exposure 后重算 PSNR ----------
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)
# exposure name 映射策略:尝试多种 key 形式
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
# sanity: check key匹配率
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) # (H, W, 3)
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) # (H, W, 3)
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)
# render (H,W,3) @ A.T + b (3DGS 官方公式)
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()
|