File size: 7,867 Bytes
cca6827
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/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()