bert_simpson / forgebench /code /eval /heldout /eval_heldout.py
Ronaldo-GOAT's picture
FORGE3DBench: final eval protocol + eval_final.py, batched Ours inference, held-out view tars, missing-object lists, README
cca6827 verified
Raw History Blame Contribute Delete
7.87 kB
#!/usr/bin/env python3
"""Held-out-GT-view novel-view (NV) appearance evaluator.
NV protocol: render the PREDICTED mesh at held-out dataset cameras (views never
fed to the model; see prepare_heldout.py) and compare with the dataset's own GT
image of that view, restricted to the GT mask (FB150: modal/visible mask, so
occluders and occluded object parts are never scored; Omni: GT alpha).
Everything except the held-out camera list and the mask restriction is
IMPORTED from metrics/appeval (unchanged):
* IV + geometry (+ optional legacy 24-orbit NV): evaluate_appforce.eval_object
* renderer: render.prepare_mesh / render.render_input_view (nvdiffrast)
* metrics : appearance.appearance_metrics / average_views
* crop : held-out npz bbox (y0,y1,x0,x1) + RGBA crop png, identical format
and code path as the input views (eval_object IV block).
Mask restriction (the only new metric logic): pred alpha is multiplied by the GT
mask M before appearance_metrics composites both over white, so pixels outside
M are white in both images (ignored) and M-pixels the pred does not cover are
scored as white-vs-GT (penalised).
"""
from __future__ import annotations
import argparse, json, sys
from pathlib import Path
import numpy as np
sys.path.insert(0, "/home/nvidia/jonghoon/mv-mesh/metrics/appeval")
import render as R # noqa: E402
import evaluate_appforce as EA # noqa: E402
from appearance import appearance_metrics, average_views, composite_white # noqa: E402
from geometry import geometry_metrics # noqa: E402
HERE = Path("/lp-dev/jonghoon/mv-mesh/.debug/eval_heldout_nv")
DS = {
"fb150": dict(exp=Path("/lp-dev/jonghoon/mv-mesh/.debug/forgebench_eval/fb150/exp_4v"),
index=HERE / "heldout_fb150.json"),
"omni300": dict(exp=Path("/lp-dev/jonghoon/mv-mesh/exp_faithfulness/omni3d300_rand"),
index=HERE / "heldout_omni300.json"),
}
MKEYS = ("lpips", "ssim", "clip", "psnr")
def heldout_view(g, rec, obj, ctx, gt_g=None):
"""One held-out view: returns metrics dict (+ coverage diagnostics)."""
root = Path(rec["root"])
z = np.load(root / "renders" / f"{obj}_{rec['tag']}.npz")
K = {k: float(z[k]) for k in ("fx", "fy", "cx", "cy")}
res = int(z["res"]); y0, y1, x0, x1 = z["bbox"].tolist()
ref = EA.read_input_png(root / "inputs" / f"{obj}_{rec['tag']}.png")
pred = R.render_input_view(g, K, z["c2w_cv"], res, res, ctx=ctx).cpu().numpy()[y0:y1, x0:x1]
h, w = min(pred.shape[0], ref.shape[0]), min(pred.shape[1], ref.shape[1])
pred, ref = pred[:h, :w].copy(), ref[:h, :w]
M = (ref[..., 3] > 0.5).astype(np.float32)
pred[..., 3] *= M # restrict to GT mask
ref = ref.copy(); ref[..., 3] = M
m = appearance_metrics(pred, ref)
m["coverage"] = float((pred[..., 3] > 0.5).sum() / max(M.sum(), 1)) # frac of mask hit by pred
m.update(view=rec["tag"], cam=rec["cam"], mask_px=int(M.sum()))
if "vis" in rec:
m["vis"] = rec["vis"]
return m, pred, ref
def run_object(a, obj, views, ctx, save_debug):
gt_path = EA.resolve_gt(a.exp / "renders", obj)
pred_path = gt_path if a.pred_is_gt else a.meshes / f"{obj}.glb"
if not pred_path.exists():
return {"object": obj, "error": "missing pred glb"}
# ---- IV + geometry (+legacy orbit NV) via the UNCHANGED existing code path
if a.pred_is_gt:
row = {"object": obj, "pred": "GT"}
else:
row = EA.eval_object(a.exp, a.meshes, a.exp / "renders", obj, EA.VIEW_TAGS[a.views],
a.legacy_novel, a.out, save_debug=False, ctx=ctx)
if "error" in row:
return row
if not a.legacy_novel and not a.no_geometry:
row["geometry"] = geometry_metrics(EA.load_mesh(pred_path), EA.load_mesh(gt_path))
# ---- held-out NV
g = R.prepare_mesh(EA.load_mesh(pred_path))
per, sheet = [], []
for rec in views:
m, p, r = heldout_view(g, rec, obj, ctx)
per.append(m)
if save_debug and len(sheet) < 8:
sheet += [composite_white(r), composite_white(p)]
if a.pred_is_gt: # pipeline identity: GT render vs itself in the mask
ident = appearance_metrics(p, p)
m["identity_lpips"], m["identity_psnr"] = ident["lpips"], ident["psnr"]
if per:
row["heldout_nv"] = {k: float(np.mean([d[k] for d in per])) for k in MKEYS + ("coverage",)}
row["heldout_nv"]["n_views"] = len(per)
row["heldout_nv"]["per_view"] = per
if sheet:
EA.save_sheet(sheet, a.out / "debug" / f"{obj}_heldout.png")
return row
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--dataset", choices=list(DS), required=True)
ap.add_argument("--meshes", type=Path, help="DIR with {object}.glb (canonical GT frame)")
ap.add_argument("--pred-is-gt", action="store_true", help="sanity: score the GT canon mesh")
ap.add_argument("--out", type=Path, required=True)
ap.add_argument("--views", choices=list(EA.VIEW_TAGS), default="quad", help="IV tags")
ap.add_argument("--omni-set", choices=["saved", "pool", "all"], default="saved")
ap.add_argument("--min-vis", type=float, default=0.0, help="FB150: min modal/amodal visibility")
ap.add_argument("--legacy-novel", action="store_true", help="also run old 24-orbit NV (+geometry)")
ap.add_argument("--no-geometry", action="store_true")
ap.add_argument("--objects", nargs="*"); ap.add_argument("--limit", type=int, default=0)
ap.add_argument("--shard", type=int, default=0); ap.add_argument("--nshards", type=int, default=1)
a = ap.parse_args()
a.exp = DS[a.dataset]["exp"]
idx = json.load(open(DS[a.dataset]["index"]))
objs = sorted(idx)
if a.objects:
objs = [o for o in objs if o in set(a.objects)]
if a.limit:
objs = objs[:a.limit]
objs = objs[a.shard::a.nshards]
a.out.mkdir(parents=True, exist_ok=True)
ctx = R.get_ctx()
res = []
for i, obj in enumerate(objs):
v = idx[obj]
if a.dataset == "fb150":
v = [r for r in v if r["vis"] >= a.min_vis]
elif a.omni_set != "all":
v = [r for r in v if r["kind"] == a.omni_set]
try:
r = run_object(a, obj, v, ctx, save_debug=i < 6)
except Exception as e:
import traceback; traceback.print_exc()
r = {"object": obj, "error": f"{type(e).__name__}: {e}"}
res.append(r)
hn = r.get("heldout_nv", {})
print(f"[{i+1}/{len(objs)}] {obj} n={hn.get('n_views')} lpips={hn.get('lpips', float('nan')):.4f} "
f"psnr={hn.get('psnr', float('nan')):.2f} cov={hn.get('coverage', float('nan')):.3f}", flush=True)
(a.out / "results.json").write_text(json.dumps(res, indent=1))
ok = [r for r in res if "heldout_nv" in r]
summ = {"n_objects": len(ok), "n_views_total": int(sum(r["heldout_nv"]["n_views"] for r in ok)),
"heldout_nv": {k: float(np.mean([r["heldout_nv"][k] for r in ok])) for k in MKEYS + ("coverage",)}}
iv = [r for r in ok if r.get("input_view")]
if iv:
summ["input_view"] = {k: float(np.mean([r["input_view"][k] for r in iv])) for k in MKEYS}
ge = [r for r in ok if r.get("geometry")]
if ge:
summ["geometry"] = {k: float(np.mean([r["geometry"][k] for r in ge]))
for k in ("cd_l1", "f01", "f02", "f05", "normal_consistency") if k in ge[0]["geometry"]}
nv = [r for r in ok if r.get("novel_view")]
if nv:
summ["legacy_orbit_nv"] = {k: float(np.mean([r["novel_view"][k] for r in nv])) for k in MKEYS}
(a.out / "summary.json").write_text(json.dumps(summ, indent=1))
print(json.dumps(summ, indent=1))
if __name__ == "__main__":
main()