blur-slam-bpn-code / scripts /eval_comogaussian_renders.py
zhaoshiwen's picture
Initial upload: BPN deblur pipeline code (scripts, triangle-splatting, BAGS, EVSSM forks)
c75b162 verified
Raw
History Blame Contribute Delete
12.3 kB
#!/usr/bin/env python3
"""
Evaluate CoMoGaussian rendered test views across pipelines.
Reads TEST/ renders produced by train.py, computes PSNR/SSIM/LPIPS,
and saves visual comparison grids.
Pipeline dirs expected:
outputs/<pipeline>/scannet/<scene>/TEST/img_7000_NNN.png (renders)
outputs/<pipeline>/scannet/<scene>/TEST/GT_NNN.png (GT, saved at first test iter)
outputs/<pipeline>/scannet/<scene>/psnr.txt (inline metrics from training)
Outputs:
outputs/eval/comogaussian/per_scene/<scene>_metrics.json
outputs/eval/comogaussian/per_scene/<scene>_grid.jpg
outputs/eval/comogaussian/summary.json
outputs/eval/comogaussian/summary.md
Usage:
python eval_comogaussian_renders.py
python eval_comogaussian_renders.py --pipelines turtle_comogaussian evssm_comogaussian
python eval_comogaussian_renders.py --scenes scene0000_00 --iter 7000
"""
import argparse, json, re
from pathlib import Path
import numpy as np
from PIL import Image, ImageDraw
BASE = Path(__file__).parent.parent
DEFAULT_PIPELINES = ["blur_comogaussian", "turtle_comogaussian", "evssm_comogaussian"]
def parse_args():
p = argparse.ArgumentParser()
p.add_argument("--out-root", default=str(BASE / "outputs"))
p.add_argument("--pipelines", nargs="*", default=None)
p.add_argument("--scenes", nargs="*", default=None)
p.add_argument("--eval-dir", default=str(BASE / "outputs/eval/comogaussian"))
p.add_argument("--iter", type=int, default=7000)
p.add_argument("--n-vis", type=int, default=4)
return p.parse_args()
def psnr_np(img1, img2):
mse = np.mean((img1.astype(np.float64) - img2.astype(np.float64)) ** 2)
if mse < 1e-10:
return 100.0
return 20.0 * np.log10(255.0 / np.sqrt(mse))
def ssim_np(img1, img2):
from skimage.metrics import structural_similarity
return float(structural_similarity(img1, img2, channel_axis=2, data_range=255))
def lpips_score(img1_t, img2_t, lpips_fn):
import torch
return float(lpips_fn(img1_t * 2 - 1, img2_t * 2 - 1).item())
def to_tensor(arr):
import torch
return torch.from_numpy(arr.astype(np.float32) / 255.0).permute(2, 0, 1).unsqueeze(0).cuda()
def parse_psnr_txt(psnr_path):
"""Parse psnr.txt written by CoMoGaussian train.py."""
result = {}
try:
with open(psnr_path) as f:
for line in f:
m = re.search(r'ITER (\d+).*?(PSNR|SSIM|LPIPS)\s+([\d.]+)', line)
if m:
it, metric, val = int(m.group(1)), m.group(2), float(m.group(3))
if it not in result:
result[it] = {}
result[it][metric] = val
except Exception:
pass
return result
def make_label(text, w, h=26, bg=(30, 30, 30), fg=(255, 255, 255)):
img = np.full((h, w, 3), bg, dtype=np.uint8)
pil = Image.fromarray(img)
ImageDraw.Draw(pil).text((6, 4), text, fill=fg)
return np.array(pil)
def load_test_renders(model_dir, iteration, scale_factor=1):
"""
Load renders and GTs from render.py output:
<model>/test/ours_<iter>/test_preds_<scale>/NNNNN.png
<model>/test/ours_<iter>/gt_<scale>/NNNNN.png
Falls back to train.py inline output:
<model>/TEST/img_<iter>_NNN.png + GT_NNN.png
"""
# render.py output (preferred)
render_path = Path(model_dir) / "test" / f"ours_{iteration}" / f"test_preds_{scale_factor}"
gt_path = Path(model_dir) / "test" / f"ours_{iteration}" / f"gt_{scale_factor}"
if render_path.exists() and gt_path.exists():
renders = [np.array(Image.open(f).convert("RGB")) for f in sorted(render_path.glob("*.png")) if "depth" not in f.name]
gts = [np.array(Image.open(f).convert("RGB")) for f in sorted(gt_path.glob("*.png"))]
return renders, gts
# fallback: train.py inline TEST/ output
test_dir = Path(model_dir) / "TEST"
if test_dir.exists():
renders = [np.array(Image.open(f).convert("RGB")) for f in sorted(test_dir.glob(f"img_{iteration}_*.png"))]
gts = [np.array(Image.open(f).convert("RGB")) for f in sorted(test_dir.glob("GT_*.png"))]
return renders, gts
return [], []
def load_results_json(model_dir):
"""Load metrics.py output results.json: {method: {SSIM, PSNR, LPIPS}}"""
p = Path(model_dir) / "results.json"
if p.exists():
with open(p) as f:
return json.load(f)
return {}
def evaluate_scene_pipeline(scene, pipeline, out_root, iteration, lpips_fn):
model_dir = Path(out_root) / pipeline / "scannet" / scene
if not model_dir.exists():
return None
renders, gts = load_test_renders(model_dir, iteration)
if not renders:
print(f" [{pipeline}] no renders at iter {iteration}")
# fall back to psnr.txt
psnr_data = parse_psnr_txt(model_dir / "psnr.txt")
if psnr_data and iteration in psnr_data:
return {"source": "psnr.txt", **psnr_data[iteration],
"n_frames": 0, "renders": [], "gts": []}
return None
import torch
metrics_per_frame = []
for r, g in zip(renders, gts):
g_resized = np.array(Image.fromarray(g).resize((r.shape[1], r.shape[0])))
p = psnr_np(r, g_resized)
s = ssim_np(r, g_resized)
lp = None
if lpips_fn is not None:
try:
lp = lpips_score(to_tensor(r), to_tensor(g_resized), lpips_fn)
except Exception:
pass
metrics_per_frame.append({"psnr": round(p, 4), "ssim": round(s, 4),
"lpips": round(lp, 4) if lp is not None else None})
avg_psnr = float(np.mean([x["psnr"] for x in metrics_per_frame]))
avg_ssim = float(np.mean([x["ssim"] for x in metrics_per_frame]))
lpips_vals = [x["lpips"] for x in metrics_per_frame if x["lpips"] is not None]
avg_lpips = float(np.mean(lpips_vals)) if lpips_vals else None
return {
"source": "renders",
"PSNR": round(avg_psnr, 4),
"SSIM": round(avg_ssim, 4),
"LPIPS": round(avg_lpips, 4) if avg_lpips is not None else None,
"n_frames": len(renders),
"per_frame": metrics_per_frame,
"renders": renders,
"gts": gts,
}
def save_comparison_grid(pipeline_renders, out_path, n_vis=4):
"""pipeline_renders: dict pipeline -> {"renders": [...], "gts": [...]}"""
pipelines = list(pipeline_renders.keys())
if not pipelines:
return
# use GT from first available pipeline
gts = None
for p in pipelines:
if pipeline_renders[p]["gts"]:
gts = pipeline_renders[p]["gts"]
break
if gts is None:
return
n_frames = min(len(gts), min(
len(pipeline_renders[p]["renders"]) for p in pipelines
if pipeline_renders[p]["renders"]
) if any(pipeline_renders[p]["renders"] for p in pipelines) else 0)
if n_frames == 0:
return
idxs = np.linspace(0, n_frames - 1, min(n_vis, n_frames), dtype=int)
rows = []
for idx in idxs:
cols = []
for p in pipelines:
rlist = pipeline_renders[p]["renders"]
if rlist and idx < len(rlist):
img = rlist[idx]
label = make_label(f"{p}", img.shape[1])
cols.append(np.vstack([label, img]))
# GT column
gt = gts[idx]
label = make_label("GT", gt.shape[1])
cols.append(np.vstack([label, gt]))
if cols:
rows.append(np.hstack(cols))
if rows:
grid = np.vstack(rows)
Image.fromarray(grid).save(out_path, quality=92)
def main():
args = parse_args()
try:
import torch, lpips as lpips_lib
lpips_fn = lpips_lib.LPIPS(net='alex').cuda()
except Exception as e:
print(f"[warn] LPIPS unavailable ({e}), skipping")
lpips_fn = None
out_root = Path(args.out_root)
pipelines = args.pipelines or DEFAULT_PIPELINES
# find available scenes
if args.scenes:
scenes = args.scenes
else:
scene_set = set()
for pl in pipelines:
d = out_root / pl / "scannet"
if d.exists():
scene_set.update(p.name for p in d.iterdir() if p.is_dir())
scenes = sorted(scene_set)
print(f"Pipelines: {pipelines}")
print(f"Scenes: {len(scenes)}")
eval_dir = Path(args.eval_dir)
(eval_dir / "per_scene").mkdir(parents=True, exist_ok=True)
all_metrics = {}
for scene in scenes:
print(f"\n[{scene}]")
scene_metrics = {}
pipeline_vis = {}
for pl in pipelines:
result = evaluate_scene_pipeline(scene, pl, str(out_root), args.iter, lpips_fn)
if result is None:
continue
p_str = f"PSNR={result.get('PSNR', '?'):.4f}" if isinstance(result.get('PSNR'), float) else "PSNR=?"
s_str = f"SSIM={result.get('SSIM', '?'):.4f}" if isinstance(result.get('SSIM'), float) else "SSIM=?"
print(f" {pl:30s} {p_str} {s_str} n={result.get('n_frames',0)}")
scene_metrics[pl] = {k: v for k, v in result.items()
if k not in ("renders", "gts", "per_frame")}
pipeline_vis[pl] = {"renders": result.get("renders", []),
"gts": result.get("gts", [])}
all_metrics[scene] = scene_metrics
# save per-scene JSON
with open(eval_dir / "per_scene" / f"{scene}_metrics.json", "w") as f:
json.dump({"scene": scene, "iter": args.iter, "metrics": scene_metrics}, f, indent=2)
# visual comparison grid
if any(pipeline_vis[p]["renders"] for p in pipeline_vis):
save_comparison_grid(pipeline_vis, eval_dir / "per_scene" / f"{scene}_grid.jpg", n_vis=args.n_vis)
print(f" grid saved -> {eval_dir}/per_scene/{scene}_grid.jpg")
# summary JSON
summary = {}
for pl in pipelines:
psnrs = [all_metrics[s][pl]["PSNR"] for s in all_metrics
if pl in all_metrics[s] and isinstance(all_metrics[s][pl].get("PSNR"), float)]
ssims = [all_metrics[s][pl]["SSIM"] for s in all_metrics
if pl in all_metrics[s] and isinstance(all_metrics[s][pl].get("SSIM"), float)]
lpips_vals = [all_metrics[s][pl]["LPIPS"] for s in all_metrics
if pl in all_metrics[s] and isinstance(all_metrics[s][pl].get("LPIPS"), float)]
if psnrs:
summary[pl] = {
"PSNR_mean": round(float(np.mean(psnrs)), 4),
"SSIM_mean": round(float(np.mean(ssims)), 4),
"LPIPS_mean": round(float(np.mean(lpips_vals)), 4) if lpips_vals else None,
"n_scenes": len(psnrs),
}
with open(eval_dir / "summary.json", "w") as f:
json.dump({"per_scene": all_metrics, "overall": summary}, f, indent=2)
# markdown table
lines = [
"# CoMoGaussian Render Quality (Test Views)\n",
f"Iteration: {args.iter}\n",
"| Scene | " + " | ".join(f"{pl} PSNR" for pl in pipelines) +
" | " + " | ".join(f"{pl} SSIM" for pl in pipelines) + " |",
"|-------|" + "|".join(["------"] * len(pipelines)) + "|" +
"|".join(["------"] * len(pipelines)) + "|",
]
for scene in sorted(all_metrics):
m = all_metrics[scene]
psnr_cols = [f"{m[pl]['PSNR']:.4f}" if pl in m and isinstance(m[pl].get('PSNR'), float) else "—" for pl in pipelines]
ssim_cols = [f"{m[pl]['SSIM']:.4f}" if pl in m and isinstance(m[pl].get('SSIM'), float) else "—" for pl in pipelines]
lines.append(f"| {scene} | " + " | ".join(psnr_cols) + " | " + " | ".join(ssim_cols) + " |")
lines.append(f"\n**Overall mean:**")
for pl, s in summary.items():
lp = f" LPIPS={s['LPIPS_mean']:.4f}" if s.get("LPIPS_mean") else ""
lines.append(f"- {pl}: PSNR={s['PSNR_mean']:.4f} SSIM={s['SSIM_mean']:.4f}{lp} (n={s['n_scenes']})")
with open(eval_dir / "summary.md", "w") as f:
f.write("\n".join(lines))
print(f"\n{'='*60}")
print("Summary:")
for pl, s in summary.items():
print(f" {pl:30s} PSNR={s['PSNR_mean']:.4f} SSIM={s['SSIM_mean']:.4f}")
print(f"\nOutputs -> {eval_dir}/")
if __name__ == "__main__":
main()