File size: 4,937 Bytes
2c0472d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/usr/bin/env python3
"""Materialise HELD-OUT ground-truth views for FORGE3DBench rest-658 (the 808-set objects NOT in FB150).

Copy of the FB150 branch of eval_heldout_nv/prepare_heldout.py (_fb_work / prep_fb), unchanged logic;
only the source selection / views json and the output root differ:
  selection = forgebench_eval/rest658/exp_4v/selection.json   (built by build_rest658.py = build_fb150.work)
  views4    = forgebench_eval/rest658/views_4v.json           (all_cam_visibility = modal/amodal area, vis_all)
ALL-VIEWS build (coordinator request 2026-09-26): EVERY scene cam (of 24) NOT in views4 gets an index record
with its visibility vis (modal/amodal area, views_4v.json all_cam_visibility) and modal px. Files are materialised
with build_fb150.build_view (identical crop/mask/camera code as the 4 input views; alpha = modal mask & depth>0)
whenever the modal mask is non-empty (px<400 views INCLUDED, flagged px_ok=False). Cams with an empty modal mask
(object fully hidden / out of frame) cannot be cropped: record only, files=False, px=0. Tag = h<cam:02d>.
The FB150 protocol set (vis>=0.20 & px>=400 & files) is applied at eval time by eval_rest658_wrapper.py
(--min-vis 0.2, --min-px 400); with those filters the NV set equals the FB150 rule exactly.
Outputs: <OUT>/rest658_heldout/{inputs,renders}, <OUT>/heldout_rest658.json (schema of heldout_fb150.json),
         <OUT>/inventory_rest658.json
"""
import argparse, json, sys
from pathlib import Path
from concurrent.futures import ProcessPoolExecutor
import numpy as np

HERE = Path("/lp-dev/jonghoon/mv-mesh/.debug/hub_rest658_heldout")
FB150 = Path("/lp-dev/jonghoon/mv-mesh/.debug/forgebench_eval/fb150")        # build_fb150.py lives here
REST = Path("/lp-dev/jonghoon/mv-mesh/.debug/forgebench_eval/rest658")
VIEWS4 = REST / "views_4v.json"
MIN_PX = 400


def _fb_work(a):   # == prepare_heldout._fb_work
    s, v4, out = a
    sys.path.insert(0, str(FB150))
    import build_fb150 as BF  # build_view (same code path as input views)
    from PIL import Image
    vis = v4["all_cam_visibility"]; ins = set(v4["cams"])
    recs = []
    for ci, vv in enumerate(vis):
        if ci in ins:
            continue
        tag = f"h{ci:02d}"
        try:
            BF.build_view(s, ci, tag, Path(out))
        except RuntimeError:          # empty modal mask after depth>0 -> record only
            recs.append(dict(tag=tag, cam=ci, vis=float(vv), px=0, px_ok=False, files=False, root=str(out)))
            continue
        png = Path(out) / "inputs" / f"{s['object']}_{tag}.png"
        px = int((np.asarray(Image.open(png))[..., 3] > 0).sum())
        recs.append(dict(tag=tag, cam=ci, vis=float(vv), px=px, px_ok=px >= MIN_PX, files=True, root=str(out)))
    return s["object"], recs


def summ(idx, key=None):
    n = [len([r for r in v if key is None or key(r)]) for v in idx.values()]
    return dict(n_obj=len(n), total=int(sum(n)), min=int(min(n)), median=float(np.median(n)),
                max=int(max(n)), mean=round(float(np.mean(n)), 2), n_obj_zero=int(sum(x == 0 for x in n)))


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--workers", type=int, default=64)
    ap.add_argument("--limit", type=int, default=0)
    a = ap.parse_args()
    out = HERE / "rest658_heldout"
    for d in ("inputs", "renders"):
        (out / d).mkdir(parents=True, exist_ok=True)
    sel = json.load(open(REST / "exp_4v/selection.json"))["selections"]
    v4 = json.load(open(VIEWS4))["views"]
    assert len(sel) == 658 and set(v4) == {s["object"] for s in sel}
    for s in sel:   # the 4 input cams in the exp == the cams excluded here
        assert list(s["views4"]) == list(v4[s["object"]]["cams"]), s["object"]
    if a.limit:
        sel = sel[:a.limit]
    jobs = [(s, v4[s["object"]], str(out)) for s in sel]
    idx = {}
    with ProcessPoolExecutor(a.workers) as ex:
        for obj, recs in ex.map(_fb_work, jobs):
            idx[obj] = recs
    json.dump(idx, open(HERE / "heldout_rest658.json", "w"), indent=1)
    inv = {"rest658": {"rule": f"ALL cams not in views4 (files if modal mask non-empty); protocol filter vis>=0.20 & px>={MIN_PX} at eval",
                       "all_records": summ(idx), "with_files": summ(idx, lambda r: r["files"]),
                       "fb150_rule_vis>0_px>=400": summ(idx, lambda r: r["files"] and r["px_ok"] and r["vis"] > 0),
                       "PROTOCOL_vis>=0.20_px>=400": summ(idx, lambda r: r["files"] and r["px_ok"] and r["vis"] >= 0.20),
                       "vis>=0.20_any_px": summ(idx, lambda r: r["files"] and r["vis"] >= 0.20),
                       "empty_mask_no_files": summ(idx, lambda r: not r["files"]),
                       "vis>=0.40_px>=400": summ(idx, lambda r: r["files"] and r["px_ok"] and r["vis"] >= 0.40)}}
    json.dump(inv, open(HERE / "inventory_rest658.json", "w"), indent=1)
    print(json.dumps(inv, indent=1))


if __name__ == "__main__":
    main()