bert_simpson / forgebench /code /build_fb150.py
Ronaldo-GOAT's picture
Add files using upload-large-folder tool
4c3d957 verified
Raw History Blame Contribute Delete
14.3 kB
#!/usr/bin/env python3
"""FORGE3DBench-150: select + mark 150 objects, pick 4v views, build 1v/4v exps.
Selection (seed 0):
* candidates = the 674 objects with an existing ReconViaGen 1v mesh
(.debug/forgebench_eval/gen/rvg/*.glb) + for scenes with NO such object, the
remaining forgebench_1v (808-set) objects of that scene.
* each asset used at most once; every one of the 120 scenes gets >=1 object
(max bipartite matching scene->asset, RVG-having candidates preferred),
then 30 extra objects (distinct unused assets, distinct scenes, RVG-having
only, stratified round-robin over env/anchor).
* each object keeps its 1v view (cam) from the 808-set manifest.
4v views: [1v cam] + 3 random other cams of the same scene with vis>=0.40
(random.Random(f"{uid}/4v/{SEED}")); fallback = highest-vis remaining cams (flagged).
Tags: companions assigned to Hunyuan3D-2mv slots by relative azimuth
(min-cost assignment over the 6 permutations): side=right, back=back, oside=left,
using the image-space criterion of .debug_hy2mv/LOG.md A4 (F.r>0 -> right).
Exps: fb150/exp_1v (files copied from exp_faithfulness/forgebench_1v),
fb150/exp_4v (front copied from 1v; side/back/oside built with
build_forgebench_1v.py conventions).
"""
import os, sys, json, random, itertools, shutil, numpy as np, trimesh
from pathlib import Path
from PIL import Image
from concurrent.futures import ProcessPoolExecutor
sys.path.insert(0, "/lp-dev/jonghoon/mv-mesh/.debug/forgebench_eval")
import build_forgebench_1v as B # crop_bbox, pointmap_from_depth, R_FIX, ROOT
SEED = 0
VIS_THR = 0.40
FB = Path("/lp-dev/jonghoon/mv-mesh/.debug/forgebench_eval")
FB150 = FB / "fb150"
EXP1 = Path("/lp-dev/jonghoon/mv-mesh/exp_faithfulness/forgebench_1v")
HOME = Path("/home/nvidia/jonghoon/mv-mesh/.debug/forgebench150")
TAGS4 = ["front", "side", "back", "oside"]
SLOT = {"side": "right", "back": "back", "oside": "left"}
def scene_key(s):
return (s["env"], s["anchor"], s["scene"])
def select():
man = json.load(open(FB / "manifest.json"))["selections"]
rvg = {f[:-4] for f in os.listdir(FB / "gen/rvg") if f.endswith(".glb") and ".tmp" not in f}
for s in man:
s["has_rvg"] = s["object"] in rvg
scenes = sorted({scene_key(s) for s in man})
rvg_scenes = {scene_key(s) for s in man if s["has_rvg"]}
rng = random.Random(SEED)
# candidate lists per scene
cand = {}
for sk in scenes:
objs = [s for s in man if scene_key(s) == sk]
pool = [s for s in objs if s["has_rvg"]] if sk in rvg_scenes else objs
pool = sorted(pool, key=lambda s: s["object"])
rng.shuffle(pool)
cand[sk] = pool
# max bipartite matching scene -> asset (Kuhn), scenes in random order
order = scenes[:]
rng.shuffle(order)
match_asset = {} # asset -> scene
def try_scene(sk, seen):
for s in cand[sk]:
a = s["asset"]
if a in seen:
continue
seen.add(a)
if a not in match_asset or try_scene(match_asset[a], seen):
match_asset[a] = sk
return True
return False
for sk in order:
if not try_scene(sk, set()):
raise SystemExit(f"cannot cover scene {sk} with distinct assets")
scene_asset = {sk: a for a, sk in match_asset.items()}
chosen = []
for sk in scenes:
a = scene_asset[sk]
s = [c for c in cand[sk] if c["asset"] == a][0]
chosen.append(dict(s, role="coverage"))
used = {c["asset"] for c in chosen}
# 30 extras: RVG-having, unused assets, distinct scenes, round-robin over anchors
groups = {}
for sk in sorted(rvg_scenes):
groups.setdefault((sk[0], sk[1]), []).append(sk)
for g in groups.values():
rng.shuffle(g)
gkeys = sorted(groups)
extras, used_sc = [], set()
while len(extras) < 30:
progressed = False
for gk in gkeys:
if len(extras) >= 30:
break
while groups[gk]:
sk = groups[gk].pop()
if sk in used_sc:
continue
pool = [s for s in cand[sk] if s["asset"] not in used and s["has_rvg"]]
if not pool:
continue
s = pool[0]
extras.append(dict(s, role="extra")); used.add(s["asset"]); used_sc.add(sk)
progressed = True
break
if not progressed:
raise SystemExit("ran out of extras")
sel = sorted(chosen + extras, key=lambda s: s["object"])
assert len(sel) == 150 and len({s["asset"] for s in sel}) == 150
assert len({scene_key(s) for s in sel}) == 120
return sel, rvg
def vis_all(s):
"""per-cam visibility (modal/amodal), exactly as build_forgebench_1v."""
p = s["src_scene"]; oid = s["obj_id"]; mc = np.array(s["mask_color"], np.int16)
cams = json.load(open(f"{p}/cameras.json"))["cameras"]
ncam = min(len(cams), B.NCAM)
vis = []
for ci in range(ncam):
mp = f"{p}/mask/cam{ci:02d}.png"; amp = f"{p}/mask_amodal/cam{ci:02d}/{oid}.png"
if not (os.path.exists(mp) and os.path.exists(amp)):
vis.append(0.0); continue
m = np.array(Image.open(mp).convert("RGB")).astype(np.int16)
ma = int((np.abs(m - mc).sum(2) <= 8).sum())
aa = int((np.array(Image.open(amp).convert("L")) > 0).sum())
vis.append(round(ma / aa, 4) if aa > 0 else 0.0)
return vis
def horiz(v):
v = np.array(v, float).copy(); v[2] = 0
return v / (np.linalg.norm(v) + 1e-12)
def assign_slots(s, front, comps):
cams = json.load(open(f"{s['src_scene']}/cameras.json"))["cameras"]
ctr = np.array(s["center"])
def pos(ci): return np.array(cams[ci]["extrinsic_c2w"])[:3, 3]
def right(ci): return np.array(cams[ci]["extrinsic_c2w"])[:3, 0]
F = horiz(pos(front) - ctr)
feats = {}
for c in comps:
d = horiz(pos(c) - ctr); r = horiz(right(c))
a = float(F @ d); sr = float(F @ r)
az = float(np.degrees(np.arctan2(np.cross(F, d)[2], F @ d)))
feats[c] = dict(cos_to_front=round(a, 4), F_dot_right=round(sr, 4), rel_azimuth_deg=round(az, 2))
ideal = {"side": (0.0, 1.0), "back": (-1.0, 0.0), "oside": (0.0, -1.0)}
best = None
for perm in itertools.permutations(comps):
cost = sum((feats[c]["cos_to_front"] - ideal[t][0]) ** 2 + (feats[c]["F_dot_right"] - ideal[t][1]) ** 2
for t, c in zip(["side", "back", "oside"], perm))
if best is None or cost < best[0]:
best = (cost, perm)
return dict(zip(["side", "back", "oside"], best[1])), feats, best[0]
def pick_4v(s, vis):
front = s["cam"]
qual = [c for c in range(len(vis)) if c != front and vis[c] >= VIS_THR]
r = random.Random(f"{s['object']}/4v/{SEED}")
flag = None
if len(qual) >= 3:
comps = r.sample(sorted(qual), 3)
else:
rest = sorted([c for c in range(len(vis)) if c != front and c not in qual], key=lambda c: -vis[c])
comps = sorted(qual) + rest[:3 - len(qual)]
flag = f"only {len(qual)} other cams with vis>=0.40; filled by highest-vis"
tagmap, feats, cost = assign_slots(s, front, comps)
return comps, tagmap, feats, cost, flag, len(qual)
def build_view(s, ci, tag, out):
"""side/back/oside view files; same code path as build_forgebench_1v.build_object."""
p = s["src_scene"]; uid = s["object"]; mc = np.array(s["mask_color"], np.int16)
scale_r = s["scale_r"]; center = np.array(s["center"])
cam = json.load(open(f"{p}/cameras.json"))["cameras"][ci]; Kd = cam["intrinsics"]
c2w = np.array(cam["extrinsic_c2w"]).copy(); c2w[:3, 3] = (c2w[:3, 3] - center) / scale_r
modal = np.array(Image.open(f"{p}/mask/cam{ci:02d}.png").convert("RGB")).astype(np.int16)
sel = (np.abs(modal - mc).sum(2) <= 8)
raw = np.array(Image.open(f"{p}/depth/cam{ci:02d}.png")).astype(np.float64)
sel = sel & (raw > 0)
if sel.sum() == 0:
raise RuntimeError(f"empty mask {uid} cam{ci}")
dmm = np.zeros(raw.shape, np.float64); dmm[sel] = raw[sel] / scale_r
dmm = np.clip(np.rint(dmm), 0, 65535).astype(np.uint16)
bb = B.crop_bbox(sel); y0, y1, x0, x1 = bb
rgb = np.array(Image.open(f"{p}/rgb/cam{ci:02d}.png").convert("RGB"))
alpha = np.where(sel, 255, 0).astype(np.uint8)
Image.fromarray(np.dstack([rgb, alpha])[y0:y1, x0:x1], "RGBA").save(out / "inputs" / f"{uid}_{tag}.png")
np.savez_compressed(out / "renders" / f"{uid}_{tag}.npz", depth_mm=dmm, fx=float(Kd["fx"]), fy=float(Kd["fy"]),
cx=float(Kd["cx"]), cy=float(Kd["cy"]), c2w_cv=c2w, bbox=np.array(bb, np.int64),
res=int(raw.shape[0]), up_axis="y")
def pm_of(out, uid, tag):
d = np.load(out / "renders" / f"{uid}_{tag}.npz")
Kv = {k: float(d[k]) for k in ("fx", "fy", "cx", "cy")}
return B.pointmap_from_depth(d["depth_mm"], Kv, tuple(d["bbox"]))
def work(s):
vis = vis_all(s)
assert abs(vis[s["cam"]] - s["visibility"]) < 1e-3, (s["object"], vis[s["cam"]], s["visibility"])
comps, tagmap, feats, cost, flag, nq = pick_4v(s, vis)
uid = s["object"]
e1, e4 = FB150 / "exp_1v", FB150 / "exp_4v"
for e in (e1, e4):
for sub in ("inputs", "renders"):
(e / sub).mkdir(parents=True, exist_ok=True)
# 1v exp: copy existing files (identical inputs to the 808-set run)
for src, dst in [(EXP1 / "inputs" / f"{uid}_front.png", "inputs"),
(EXP1 / "renders" / f"{uid}_front.npz", "renders"),
(EXP1 / "renders" / f"{uid}_canon.glb", "renders")]:
for e in (e1, e4):
shutil.copy2(src, e / dst / src.name)
(e1 / "npz_1v" / uid).mkdir(parents=True, exist_ok=True)
shutil.copy2(EXP1 / "npz_1v" / uid / "da3_output.npz", e1 / "npz_1v" / uid / "da3_output.npz")
for tag in ("side", "back", "oside"):
build_view(s, tagmap[tag], tag, e4)
pms = np.stack([pm_of(e4, uid, t) for t in TAGS4])
for v in (1, 2, 4):
dd = e4 / f"npz_{v}v" / uid; dd.mkdir(parents=True, exist_ok=True)
np.savez(dd / "da3_output.npz", pointmaps_sam3d=pms[:v], image_files=np.array([f"v{k+1}.png" for k in range(v)]))
cams4 = [s["cam"]] + [tagmap[t] for t in ("side", "back", "oside")]
return dict(object=uid, all_cam_visibility=vis, n_qual_other=nq,
random_draw_order=comps, cams=cams4, tags=TAGS4,
visibility=[vis[c] for c in cams4],
hy3d2mv_slots={"front": s["cam"], **{SLOT[t]: tagmap[t] for t in ("side", "back", "oside")}},
tag_to_hy3d2mv_slot={"front": "front", **SLOT},
companion_geometry={str(c): feats[c] for c in comps}, slot_assign_cost=round(cost, 4),
flag=flag, seed=SEED)
def main():
sel, rvg = select()
with ProcessPoolExecutor(32) as ex:
v4 = list(ex.map(work, sel))
v4d = {r["object"]: r for r in v4}
keep = ["object", "env", "anchor", "scene", "asset", "obj_id", "cam", "visibility", "n_qual",
"role", "has_rvg", "mask_color", "scale_r", "center", "extent", "src_scene", "front", "modal_area"]
recs = [{k: s[k] for k in keep} for s in sel]
for r in recs:
r["episode"] = f"{r['env']}/{r['anchor']}/{r['scene']}"
# scene coverage
scenes = sorted({(r["env"], r["anchor"], r["scene"]) for r in recs})
per_anchor = {}
for r in recs:
per_anchor.setdefault(f"{r['env']}/{r['anchor']}", set()).add(r["scene"])
cov = {"n_objects": len(recs), "n_scenes_covered": len(scenes), "n_scenes_total": 120,
"n_unique_assets": len({r["asset"] for r in recs}),
"per_anchor_scenes": {k: len(v) for k, v in sorted(per_anchor.items())},
"per_anchor_objects": {k: sum(1 for r in recs if f"{r['env']}/{r['anchor']}" == k) for k in sorted(per_anchor)},
"n_rvg_reused": sum(r["has_rvg"] for r in recs),
"rvg_to_generate": sorted(r["object"] for r in recs if not r["has_rvg"])}
HOME.mkdir(parents=True, exist_ok=True)
json.dump({"seed": SEED, "vis_thr": VIS_THR, "source_manifest": str(FB / "manifest.json"),
"coverage": cov, "selections": recs}, open(HOME / "selection_150.json", "w"), indent=1)
lines = ["# FORGE3DBench-150 scene coverage", "",
f"objects={cov['n_objects']} scenes covered={cov['n_scenes_covered']}/120 unique assets={cov['n_unique_assets']}",
f"RVG 1v reused={cov['n_rvg_reused']} RVG 1v to generate={len(cov['rvg_to_generate'])}", "",
"| env/anchor | scenes covered | objects |", "|---|---:|---:|"]
for k in cov["per_anchor_scenes"]:
lines.append(f"| {k} | {cov['per_anchor_scenes'][k]}/20 | {cov['per_anchor_objects'][k]} |")
lines += ["", "RVG 1v generated fresh (scenes without any existing RVG mesh):"] + [f"- {o}" for o in cov["rvg_to_generate"]]
lines += ["", "| scene | objects |", "|---|---|"]
for sk in scenes:
lines.append(f"| {'/'.join(sk)} | " + ", ".join(r["object"] for r in recs if (r['env'], r['anchor'], r['scene']) == sk) + " |")
(HOME / "scene_coverage.md").write_text("\n".join(lines) + "\n")
json.dump({"seed": SEED, "vis_thr": VIS_THR,
"rule": "cams[0]=1v anchor cam; 3 companions = random.Random(f'{object}/4v/0').sample(other cams with vis>=0.40, 3); "
"fallback highest-vis if <3 qualify (flag). Companions assigned to tags side/back/oside by min-cost "
"match to Hunyuan3D-2mv slots right/back/left (features: F.d cos to front, F.r image-right criterion, LOG.md A4).",
"tag_to_hy3d2mv_slot": {"front": "front", **SLOT},
"n_flagged": sum(1 for r in v4 if r["flag"]),
"views": v4d}, open(HOME / "views_4v.json", "w"), indent=1)
for e in ("exp_1v", "exp_4v"):
json.dump({"source": f"forgebench150_{e[-2:]}", "seed": SEED, "vis_thr": VIS_THR, "n": len(recs),
"selections": [dict(r, **({"views4": v4d[r['object']]['cams']} if e == 'exp_4v' else {})) for r in recs]},
open(FB150 / e / "selection.json", "w"), indent=1)
print(json.dumps(cov, indent=1))
print("flagged 4v:", [r["object"] for r in v4 if r["flag"]])
if __name__ == "__main__":
main()