Download scripts/genbench4/gate.py from teawhite/maliang: direct link, hf CLI and curl.
- Browser
- Download file 18.1 kB
-
https://huggingface.co/teawhite/maliang/resolve/main/scripts/genbench4/gate.py
- Command line
-
hf download hf://teawhite/maliang/scripts/genbench4/gate.py
-
curl -L -o gate.py https://huggingface.co/teawhite/maliang/resolve/main/scripts/genbench4/gate.py
18.1 kB
| #!/usr/bin/env python3 | |
| """genbench4 · 换场景首帧的语义一致性门槛(PCE env pce_f3,Qwen3-VL-32B,genbench3/vlm_gate.QwenVL:Yes / No 首 token 的 P(yes))。 | |
| 整图题(图 1 = 真实帧 0,图 2 = 候选): | |
| * layout:同一关卡换成新场景后布局不变,且**没有东西换了类别**(空地不许变墙 / 悬崖、石头不许变房子……;同类里换成该场景 | |
| 的东西可以 —— 冒烟 v1 的措辞「不管东西变成了哪一种」把石头 → 城墙也放过了); | |
| * chars :两个角色都在、同位置、同姿势 / 体型(只记录;门槛用下面的逐角色放大比对 charcrop);only2:画面里只有这两个角色; | |
| scene:(只看图 2)是不是该场景。 | |
| 逐角色放大比对 charcrop:同一个框(角色掩码包围框外扩 50 %)裁原帧 / 候选:「同一个角色还在、同位置 / 姿势 / 体型吗」;基线 = | |
| 原帧对原帧,基线 < 0.5 的角色不判(VLM 在原帧里就认不出:被叶子挡住 / 太小 / 叠在一起)。 | |
| 逐区域题(按帧 0 的语义图切,**原帧问同样的题作基线**,缓存在 sources/<tag>/regions.json): | |
| * open:terrain / paving 占 ≥ 92 % 且离角色 ≥ 12 px 的 96×96 格(最多 10 个,均匀取):「只是可走的地面 / 地板,里面没有 | |
| 立着的墙、建筑、悬崖、石阵、树、大物体」; | |
| * obj :architecture / rock / vegetation / prop 的连通块(≥ 300 px,最多 8 个,按面积),裁出包围框(外扩、放大到短边 ≥ 336, | |
| 品红描出该块的语义轮廓):「描出来的东西是不是 {该类的通称}」。 | |
| 区域分 = 原帧答 Yes(P ≥ 0.5)的区域里候选也答 Yes 的比例(obj 按面积加权)⇒ open_ok / obj_ok。 | |
| 自动:语义边界边缘召回 ≥ 0.4 × 原帧(只防糊成一片;场景换光照 / 材质后 0.4–0.7 是常态,genbench3 的 0.8 不适用)。 | |
| genbench3 的「角色仍填满语义轮廓」(bg_fill)不用:换成暗场景(熔岩 / 地牢)后深色 Boss 与背景对比变低,bg_fill 涨到 0.4–0.7 | |
| 而角色其实都在(冒烟目测)⇒ 角色在不在只靠 VLM chars。门槛值见 GATE(冒烟 outputs/genbench4_smoke 目测标定:区域题抓到的 | |
| 都是真违规 —— 空地角落的树篱 / 枯灌木 / 城墙块、铺地上多出的门 / 火把 / 旗帜、石头变树篱 / 木架)。 | |
| 输出 <cand>.vlm.json;--merge ⇒ outputs/genbench4/gate.json(每个 (起点, 场景) 槽在通过的候选里取分最高的)[+ --jobs ⇒ jobs.json]。 | |
| usage: CUDA_VISIBLE_DEVICES=0 $FPY scripts/genbench4/gate.py --shard 0 --nshards 8 ;$FPY scripts/genbench4/gate.py --merge --jobs""" | |
| import argparse | |
| import glob | |
| import json | |
| import os | |
| import re | |
| import sys | |
| import time | |
| import numpy as np | |
| from PIL import Image | |
| from scipy import ndimage | |
| M = "/opt/dlami/nvme/zhiyangdeng/maliang" | |
| G = os.environ.get("GB4", f"{M}/outputs/genbench4") | |
| sys.path[:0] = [f"{M}/code/gc25d", f"{M}/scripts/genbench3", f"{M}/scripts/genbench4"] | |
| from scenes import SCENES # noqa: E402 | |
| PLAYER, BOSS = (10, 12, 13, 14, 15, 16), (11, 17, 18, 19, 20, 21) | |
| HEAD = ("Image 1 is a screenshot from a top-down isometric action RPG game. It shows a smaller player hero and a large " | |
| "boss monster. ") | |
| Q = { | |
| "layout": HEAD + "Image 2 is supposed to be the same level with exactly the same layout, re-themed as {title}. Ignore " | |
| "the art style, materials, colours and lighting. Each thing may turn into a different thing of the same " | |
| "kind (a building into another building, a plant into another plant, a rock into another rock, a floor into " | |
| "another floor), but nothing may change its kind: open ground or floor must not become a wall, building, " | |
| "cliff, rock pile or tree, and a rock or plant must not become a building. Does image 2 keep every building " | |
| "or wall, path or floor, open ground area, rock, plant and prop of image 1 in the same place with the same " | |
| "footprint, size and kind, with nothing large added or removed? Answer Yes or No.", | |
| "chars": HEAD + "Ignore the art style and the characters' appearance. Are both characters of image 1 (the player hero " | |
| "and the boss monster) clearly present in image 2, at the same positions, with the same poses and sizes? " | |
| "Answer Yes or No.", | |
| "only2": HEAD + "Does image 2 contain exactly these two characters (one smaller player hero and one large boss " | |
| "monster) and no other people, creatures or characters? Answer Yes or No.", | |
| "scene": "This is a screenshot from a top-down isometric action RPG game. Does this level clearly show {vlm}? " | |
| "Answer Yes or No.", | |
| } | |
| CROP = "This is a close-up crop from a top-down isometric game level. " | |
| Q_OPEN = (CROP + "Does this crop show only open walkable ground or floor (for example dirt, sand, snow, grass, mud, ash, " | |
| "shallow water, ice, tiles, flagstones or planks, possibly with cracks, puddles, pebbles, small debris or small " | |
| "tufts), with no walls, buildings, towers, cliffs, rock formations, trees, large plants, fences or large objects " | |
| "standing in it? Answer Yes or No.") | |
| Q_OBJ = { | |
| 3: "part of a building, wall, tower, gate, hut, pillar, stairs or other built structure", | |
| 4: "a rock, boulder, stone block, crystal formation, ice block, or a pile of rubble or stones (a natural lump, not a " | |
| "building or wall)", | |
| 5: "a plant: a tree, bush, hedge, grass, reeds, cactus, mushroom, fungus, moss or other vegetation (living, dead or " | |
| "frozen)", | |
| 6: "a small object or prop, such as a crate, barrel, cart, wagon, lamp, brazier, statue, sign, fence, tombstone, boat, " | |
| "container or piece of debris (not a building, rock or plant)", | |
| } | |
| Q_OBJ_T = CROP + "Is the thing outlined in magenta {what}? Answer Yes or No." | |
| # 逐角色放大比对(整图 chars 题对小 / 重叠 / 被遮的角色常常连原帧都答 No ⇒ 全源失败,冒烟目测:train_v9_05 原帧里两人藏在竹叶后、 | |
| # train_v9_10 两人又小又叠在一起):同一个框裁原帧 / 候选,基线 = 原帧对原帧;基线 < 0.5 的角色(VLM 在原帧里就认不出)不判 | |
| WHO = {"player": "the player hero (a smaller human character)", "boss": "the boss (a large enemy monster)"} | |
| Q_CHAR = ("Image 1 is a close-up of {who} in a top-down isometric action RPG game. Image 2 is a close-up of exactly the same " | |
| "spot after the whole level was re-themed. Ignore colours, lighting, materials and art style. Is the same " | |
| "character still clearly there in image 2, at the same position, with the same pose, size and body shape? " | |
| "Answer Yes or No.") | |
| CELL, OPEN_FRAC, CHAR_GAP, MAX_OPEN = 96, 0.92, 12, 10 | |
| MIN_COMP, MAX_COMP = 300, 8 | |
| GATE = {"layout": 0.3, "chars": 0.5, "only2": 0.5, "scene": 0.5, "open_ok": 0.8, "obj_ok": 0.7, "edge_rel": 0.4} | |
| # ------------------------------------------------------------------ 区域 ---- | |
| def regions(ids): | |
| """[{kind: open|obj, cls, box (y0,y1,x0,x1), area, mask (obj 才有,box 内的 bool)}]。""" | |
| H, W = ids.shape | |
| chars = ndimage.binary_dilation(np.isin(ids, PLAYER + BOSS), iterations=CHAR_GAP) | |
| openm = np.isin(ids, (1, 2)) & ~chars | |
| cells = [] | |
| oy, ox = (H % CELL) // 2, (W % CELL) // 2 | |
| for y in range(oy, H - CELL + 1, CELL): | |
| for x in range(ox, W - CELL + 1, CELL): | |
| if openm[y:y + CELL, x:x + CELL].mean() >= OPEN_FRAC: | |
| cells.append({"kind": "open", "cls": 1, "box": (y, y + CELL, x, x + CELL), "area": CELL * CELL}) | |
| if len(cells) > MAX_OPEN: | |
| cells = [cells[i] for i in np.linspace(0, len(cells) - 1, MAX_OPEN).round().astype(int)] | |
| comps = [] | |
| for c in (3, 4, 5, 6): | |
| lab, n = ndimage.label(ids == c, structure=np.ones((3, 3))) | |
| for k, sl in enumerate(ndimage.find_objects(lab), 1): | |
| m = lab[sl] == k | |
| if m.sum() < MIN_COMP: | |
| continue | |
| y0, y1, x0, x1 = sl[0].start, sl[0].stop, sl[1].start, sl[1].stop | |
| py, px = max(12, (y1 - y0) // 4), max(12, (x1 - x0) // 4) | |
| Y0, Y1, X0, X1 = max(0, y0 - py), min(H, y1 + py), max(0, x0 - px), min(W, x1 + px) | |
| mm = np.zeros((Y1 - Y0, X1 - X0), bool) | |
| mm[y0 - Y0:y1 - Y0, x0 - X0:x1 - X0] = m | |
| comps.append({"kind": "obj", "cls": c, "box": (Y0, Y1, X0, X1), "area": int(m.sum()), "mask": mm}) | |
| comps = sorted(comps, key=lambda r: -r["area"])[:MAX_COMP] | |
| return cells + comps | |
| def crop_img(img, r, min_side=336): | |
| y0, y1, x0, x1 = r["box"] | |
| c = img.crop((x0, y0, x1, y1)) | |
| if r["kind"] == "obj": # 品红描出该块的语义轮廓(原帧 / 候选同一条线) | |
| edge = r["mask"] & ~ndimage.binary_erosion(r["mask"], iterations=1) | |
| edge = ndimage.binary_dilation(edge, iterations=1) | |
| a = np.asarray(c).copy() | |
| a[edge] = (255, 0, 255) | |
| c = Image.fromarray(a) | |
| s = max(1.0, min_side / max(1, min(c.size))) | |
| return c.resize((int(c.width * s), int(c.height * s)), Image.LANCZOS) | |
| def char_boxes(ids, pad=0.5, min_side=64): | |
| H, W = ids.shape | |
| out = {} | |
| for role, parts in (("player", PLAYER), ("boss", BOSS)): | |
| m = np.isin(ids, parts) | |
| if m.sum() < 150: | |
| continue | |
| ys, xs = np.nonzero(m) | |
| cy, cx = (ys.min() + ys.max()) / 2, (xs.min() + xs.max()) / 2 | |
| h = max(min_side, (ys.max() - ys.min() + 1) * (1 + pad)) | |
| w = max(min_side, (xs.max() - xs.min() + 1) * (1 + pad)) | |
| out[role] = (int(max(0, cy - h / 2)), int(min(H, cy + h / 2)), int(max(0, cx - w / 2)), int(min(W, cx + w / 2))) | |
| return out | |
| def char_items(src, img, boxes): | |
| from vlm_gate import zoom | |
| return [([zoom(src, b, 336), zoom(img, b, 336)], Q_CHAR.format(who=WHO[r])) for r, b in boxes.items()] | |
| def region_items(img, regs): | |
| return [([crop_img(img, r)], Q_OPEN if r["kind"] == "open" else Q_OBJ_T.format(what=Q_OBJ[r["cls"]])) for r in regs] | |
| def region_score(regs, p_orig, p_cand): | |
| out = {} | |
| for kind in ("open", "obj"): | |
| idx = [i for i, r in enumerate(regs) if r["kind"] == kind and p_orig[i] >= 0.5] | |
| if not idx: | |
| out[f"{kind}_ok"], out[f"{kind}_n"] = None, 0 | |
| continue | |
| w = np.array([regs[i]["area"] if kind == "obj" else 1.0 for i in idx], float) | |
| out[f"{kind}_ok"] = round(float((w * (np.asarray(p_cand)[idx] >= 0.5)).sum() / w.sum()), 4) | |
| out[f"{kind}_n"] = len(idx) | |
| return out | |
| def orig_regions(vlm, tag, src, regs, boxes): | |
| """原帧的区域题 + 角色比对题(原帧对原帧)基线,缓存。""" | |
| path = f"{G}/sources/{tag}/regions.json" | |
| if os.path.exists(path): | |
| out = json.load(open(path)) | |
| if "char_base" in out: | |
| return out | |
| p = [] | |
| else: | |
| p = vlm.yes_prob(region_items(src, regs)) if regs else [] | |
| out = {"regions": [{k: ([int(x) for x in v] if k == "box" else v) for k, v in r.items() if k != "mask"} for r in regs], | |
| "p_orig": [round(float(x), 4) for x in p]} | |
| pc = vlm.yes_prob(char_items(src, src, boxes)) if boxes else [] | |
| out["char_base"] = {r: round(float(v), 4) for r, v in zip(boxes, pc)} | |
| tmp = f"{path}.{os.getpid()}.tmp" # 多个分片同时算同一个源:原子写,免得读到半个文件 | |
| json.dump(out, open(tmp, "w"), indent=1) | |
| os.replace(tmp, path) | |
| return out | |
| # ------------------------------------------------------------------ 跑 ---- | |
| def run(a): | |
| from vlm_gate import QwenVL | |
| files = sorted(f for f in glob.glob(f"{G}/cands/*/*.json") if not f.endswith(".vlm.json")) | |
| if a.only: | |
| files = [f for f in files if re.search(a.only, f)] | |
| files = [f for i, f in enumerate(files) if i % a.nshards == a.shard] | |
| def todo(f): | |
| v = f[:-5] + ".vlm.json" | |
| return not os.path.exists(v) or "charcrop" not in json.load(open(v)) | |
| files = [f for f in files if todo(f)] | |
| print(f"[gate] shard {a.shard}/{a.nshards}: {len(files)} 张", flush=True) | |
| if not files: | |
| return | |
| vlm = QwenVL() | |
| print(f"[gate] Qwen3-VL-32B 加载 {vlm.load_s:.0f}s", flush=True) | |
| cache = {} | |
| for f in files: | |
| info = json.load(open(f)) | |
| tag = info["tag"] | |
| if tag not in cache: | |
| src = Image.open(f"{G}/sources/{tag}/f0.png").convert("RGB") | |
| ids = np.load(f"{G}/sources/{tag}/ids0.npy") | |
| regs, boxes = regions(ids), char_boxes(ids) | |
| cache = {tag: (src, regs, boxes, orig_regions(vlm, tag, src, regs, boxes))} | |
| src, regs, boxes, ro = cache[tag] | |
| cand = Image.open(f[:-5] + ".png").convert("RGB") | |
| vpath = f[:-5] + ".vlm.json" | |
| t0 = time.time() | |
| if os.path.exists(vpath): # 只补角色比对题 | |
| out = json.load(open(vpath)) | |
| else: | |
| sc = SCENES[info["scene"]] | |
| glob_it = [(q, [cand] if q == "scene" else [src, cand], Q[q].format(**sc)) for q in Q] | |
| p = vlm.yes_prob([(ims, tx) for _, ims, tx in glob_it] + region_items(cand, regs)) | |
| res = {q: round(float(v), 4) for (q, _, _), v in zip(glob_it, p)} | |
| pc = [round(float(x), 4) for x in p[len(glob_it):]] | |
| out = {"p_yes": res, "region": region_score(regs, ro["p_orig"], pc), "p_regions": pc} | |
| pch = vlm.yes_prob(char_items(src, cand, boxes)) if boxes else [] | |
| out["charcrop"] = {r: round(float(v), 4) for r, v in zip(boxes, pch)} | |
| out["sec"] = round(time.time() - t0, 2) | |
| json.dump(out, open(vpath, "w"), indent=1) | |
| print(f"[gate] {tag} {info['name']} {out['p_yes']} {out['region']} {out['charcrop']}", flush=True) | |
| def auto_ok(info, o): | |
| m = info["metrics"] | |
| return {"edge": m["layout"].get("edge_recall", 0) >= GATE["edge_rel"] * o["layout"]["edge_recall"]} | |
| def merge(a): | |
| rows = [] | |
| for f in sorted(glob.glob(f"{G}/cands/*/*.json")): | |
| if f.endswith(".vlm.json") or not os.path.exists(f[:-5] + ".vlm.json"): | |
| continue | |
| info = json.load(open(f)) | |
| o = json.load(open(f"{G}/sources/{info['tag']}/f0_metrics.json")) | |
| vj = json.load(open(f[:-5] + ".vlm.json")) | |
| if "region" not in vj: # 冒烟 v1 的旧格式(没有区域题) | |
| continue | |
| v, rg = vj["p_yes"], vj["region"] | |
| base = json.load(open(f"{G}/sources/{info['tag']}/regions.json")).get("char_base", {}) | |
| why = auto_ok(info, o) | |
| for q in ("layout", "only2", "scene"): | |
| why[q] = v[q] >= GATE[q] | |
| cc = vj.get("charcrop", {}) | |
| why["chars"] = all(cc.get(r, 0) >= GATE["chars"] for r, b in base.items() if b >= 0.5) | |
| for k in ("open_ok", "obj_ok"): | |
| why[k] = rg.get(k) is None or rg[k] >= GATE[k] | |
| ro, ob = rg.get("open_ok"), rg.get("obj_ok") | |
| rows.append({"tag": info["tag"], "name": info["name"], "scene": info["scene"], "gen": info["gen"], "seed": info["seed"], | |
| "checks": why, "vlm": v, "region": rg, "charcrop": vj.get("charcrop"), "pass": all(why.values()), | |
| "coarse_p": info["metrics"]["struct"]["coarse_edge_precision"], | |
| "edge_rel": round(info["metrics"]["layout"].get("edge_recall", 0) / o["layout"]["edge_recall"], 4), | |
| "score": round(v["layout"] + v["scene"] + min(list(vj.get("charcrop", {}).values()) or [1]) + (1 if ro is None else ro) | |
| + (1 if ob is None else ob), 4)}) | |
| picks = {} | |
| for r in rows: | |
| k = (r["tag"], r["scene"]) | |
| if k not in picks or (r["pass"], r["score"]) > (picks[k]["pass"], picks[k]["score"]): | |
| picks[k] = r | |
| sel = list(picks.values()) | |
| by_scene, by_gen, fails = {}, {}, {} | |
| for r in sel: | |
| by_scene.setdefault(r["scene"], []).append(r["pass"]) | |
| for r in rows: | |
| by_gen.setdefault(r["gen"], []).append(r["pass"]) | |
| for k, ok in r["checks"].items(): | |
| fails[k] = fails.get(k, 0) + (not ok) | |
| out = {"gate": GATE, "rows": rows, "picks": {f"{t}/{s}": r["name"] for (t, s), r in picks.items() if r["pass"]}, | |
| "failed_slots": sorted(f"{t}/{s}" for (t, s), r in picks.items() if not r["pass"]), | |
| "pass_rate_cands_by_gen": {g: f"{sum(v)}/{len(v)}" for g, v in sorted(by_gen.items())}, | |
| "check_fail_counts": fails, | |
| "slot_pass_by_scene": {s: f"{sum(v)}/{len(v)}" for s, v in sorted(by_scene.items())}, | |
| "picked_gen": {g: sum(1 for r in sel if r["pass"] and r["gen"] == g) for g in sorted(by_gen)}} | |
| json.dump(out, open(f"{G}/gate.json", "w"), indent=1) | |
| print(json.dumps({k: out[k] for k in ("pass_rate_cands_by_gen", "check_fail_counts", "slot_pass_by_scene", "picked_gen")}, | |
| indent=1)) | |
| print("slots pass", len(out["picks"]), "/", len(sel)) | |
| if a.jobs: | |
| jobs = [] | |
| for s in json.load(open(f"{G}/sources.json")): | |
| jobs.append({"tag": s["tag"], "variant": "orig", "kind": "orig", "f0": f"{G}/sources/{s['tag']}/f0.png"}) | |
| for sc in SCENES: | |
| r = picks.get((s["tag"], sc)) | |
| if r and r["pass"]: | |
| jobs.append({"tag": s["tag"], "variant": sc, "kind": "scene", "scene": sc, "gen": r["gen"], | |
| "seed": r["seed"], "f0": f"{G}/cands/{s['tag']}/{r['name']}.png"}) | |
| json.dump(jobs, open(f"{G}/jobs.json", "w"), indent=1) | |
| print(len(jobs), "个 rollout 任务(含 orig)") | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--shard", type=int, default=0) | |
| ap.add_argument("--nshards", type=int, default=1) | |
| ap.add_argument("--only", default=None) | |
| ap.add_argument("--merge", action="store_true") | |
| ap.add_argument("--jobs", action="store_true", help="--merge 时顺便写 jobs.json") | |
| a = ap.parse_args() | |
| merge(a) if a.merge else run(a) | |
| if __name__ == "__main__": | |
| main() | |