maliang / scripts /genbench4 /gate2.py
teawhite's picture
Add files using upload-large-folder tool
51ac7cc verified
Raw History Blame Contribute Delete
14.3 kB
#!/usr/bin/env python3
"""genbench4 · 首帧门槛 v2(PCE env pce_f3,Qwen3-VL-32B)。v1(gate.py)的通过帧经 3 个独立复核 agent 检查仍常有语义矛盾
(随机 6 张里 4 张):train_v9_04 的 9 张里 Boss 全被画成石头 / 水晶 / 圆顶 / 蘑菇(逐角色比对被「形状像角色的物体」骗过);多出小人
(only2 卡在 0.5 放行);铺地变水面 / 熔岩池、画面边缘加塔 / 小屋(open 格只取 ≥ 92 % 空地的内部格,边缘几乎没查);train_v9_10 的玻璃
建筑被画成可走的地面(全图没有空地格,open 检查根本没跑)。v2 在 v1 的整图题(复用 <cand>.vlm.json 的 p_yes / charcrop)上加:
* 格子按语义类分题:terrain 格「只有天然地面」、paving 格「只有铺装 / 搭建的地板,没变成水 / 熔岩」、architecture 格「是建筑 / 构筑物
的一部分,不是地面 / 植物 / 散石」;内部格纯度 ≥ 0.92,**边缘一圈的格 ≥ 0.75**(FLUX 爱在边缘加框);离角色 ≥ 12 px;
* 角色:每个角色的放大框再问「中间是不是一个活的角色(有身体 / 头 / 四肢),不是石头 / 植物 / 雕像 / 建筑 / 物件」;整图 chars 题恢复为
门槛(以原帧对原帧为基线:基线 < 0.5 的源不判);
* 阈值收紧:layout ≥ 0.5、only2 ≥ 0.8、scene ≥ 0.6、open_ok ≥ 0.9、arch_ok ≥ 0.8、obj_ok ≥ 0.85(obj 沿用 v1 的连通块题)。
所有格 / 角色题都在原帧上问一遍作基线(sources/<tag>/regions2.json),只有原帧答 Yes 的才计分。
输出 <cand>.vlm2.json;--merge ⇒ gate2.json + jobs.json(与 v1 的 jobs 不同的槽才需要重跑 rollout)。
usage: CUDA_VISIBLE_DEVICES=0 $FPY scripts/genbench4/gate2.py --shard 0 --nshards 8 ;$FPY scripts/genbench4/gate2.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"]
import gate as G1 # noqa: E402
from scenes import SCENES # noqa: E402
CROP = G1.CROP
Q_CELL = {
1: CROP + "Does this crop show only natural open ground (for example dirt, sand, snow, grass, moss, mud, ash, cracked "
"earth, rock floor, ice, or shallow water and puddles), possibly with cracks, pebbles, small debris or small "
"tufts, with no walls, buildings, towers, fences, cliffs, rock formations, trees, large plants, large objects "
"or characters standing in it? Answer Yes or No.",
2: CROP + "Does this crop show only a paved or built floor (for example tiles, flagstones, cobblestones, bricks, wooden "
"planks or a boardwalk, metal plates or concrete), possibly worn, cracked, wet or with small debris, with no "
"walls, buildings, towers, fences, trees, large plants, large objects or characters standing on it, and not "
"turned into open water or lava? Answer Yes or No.",
3: CROP + "Does this crop show part of a building or built structure (for example a wall, roof, tower, pillar, stairs, "
"bridge, glass dome or glass panels), rather than open ground, plants or loose rocks? Answer Yes or No.",
}
Q_CREATURE = ("This is a close-up crop from a top-down isometric action RPG game. Is there a living character (a person, "
"monster or creature with a body, head and limbs) in the middle of this crop, and not just a rock, plant, "
"statue, building or object? Answer Yes or No.")
CELL, PURE_IN, PURE_BORDER, CHAR_GAP = 96, 0.92, 0.75, 12
MAX_OPEN, MAX_ARCH = 14, 6
GATE = {"layout": 0.5, "only2": 0.8, "scene": 0.6, "open_ok": 0.9, "arch_ok": 0.8, "obj_ok": 0.85, "edge_rel": 0.4,
"chars": 0.5, "charcrop": 0.5, "creature": 0.5}
# B 档(角色安全、布局适中):A 档(GATE 全过)在 220 槽里只有 12 个 —— FLUX 换重主题场景几乎总在边缘 / 铺地上加装饰,补边放大(padk_test)
# 与 RePaint 修补(repair.py,klein 4 步会照周围把城墙 / 蘑菇重画回去)都没解决。B 档:角色题(creature / charcrop / only2 ≥ 0.8)、
# scene、arch 照旧严,空地格允许 ≤ 25 % 不过、layout ≥ 0.3、obj ≥ 0.7,不用整图 chars 题(失败 1184 张,多数是 VLM 在小 / 暗角色上认不出)
TIER_B = {"open_ok": 0.75, "layout": 0.3, "obj_ok": 0.7}
TIER_B_SKIP = ("chars",)
def cells(ids):
"""[{kind: cell, cls ∈ {1,2,3}, box, border}]:每格取 {terrain, paving, architecture} 里的多数类,纯度够(边缘格放宽)才收。"""
H, W = ids.shape
chars = ndimage.binary_dilation(np.isin(ids, G1.PLAYER + G1.BOSS), iterations=CHAR_GAP)
oy, ox = (H % CELL) // 2, (W % CELL) // 2
ys, xs = list(range(oy, H - CELL + 1, CELL)), list(range(ox, W - CELL + 1, CELL))
out = {1: [], 2: [], 3: []}
for i, y in enumerate(ys):
for j, x in enumerate(xs):
sl = (slice(y, y + CELL), slice(x, x + CELL))
if chars[sl].any():
continue
border = i in (0, len(ys) - 1) or j in (0, len(xs) - 1)
blk = ids[sl]
fr = {c: float((blk == c).mean()) for c in (1, 2, 3)}
c = max(fr, key=fr.get)
pure = PURE_BORDER if border else PURE_IN
# 边缘格:terrain + paving 合起来算「空地」纯度(两者都是可走地面),题按多数类问
if c in (1, 2) and (fr[1] + fr[2] if border else fr[c]) >= pure or c == 3 and fr[3] >= pure:
out[c].append({"kind": "cell", "cls": c, "box": (y, y + CELL, x, x + CELL), "border": border, "area": CELL * CELL})
sel = []
open_ = out[1] + out[2]
if len(open_) > MAX_OPEN: # 边缘格优先(FLUX 加框的地方),其余均匀取
b = [r for r in open_ if r["border"]]
n = [r for r in open_ if not r["border"]]
b = [b[k] for k in np.linspace(0, len(b) - 1, min(len(b), MAX_OPEN // 2 + 2)).round().astype(int)] if b else []
k = MAX_OPEN - len(b)
n = [n[q] for q in np.linspace(0, len(n) - 1, min(len(n), k)).round().astype(int)] if n and k > 0 else []
open_ = b + n
arch = out[3]
if len(arch) > MAX_ARCH:
arch = [arch[q] for q in np.linspace(0, len(arch) - 1, MAX_ARCH).round().astype(int)]
sel = open_ + arch
return sel
def cell_items(img, cs):
return [([G1.crop_img(img, r)], Q_CELL[r["cls"]]) for r in cs]
def creature_items(img, boxes):
from vlm_gate import zoom
return [([zoom(img, b, 336)], Q_CREATURE) for b in boxes.values()]
def baseline(vlm, tag, src, cs, boxes):
path = f"{G}/sources/{tag}/regions2.json"
if os.path.exists(path):
return json.load(open(path))
p = vlm.yes_prob(cell_items(src, cs) + creature_items(src, boxes) + [([src, src], G1.Q["chars"])])
nc, nb = len(cs), len(boxes)
out = {"cells": [{"cls": r["cls"], "box": [int(v) for v in r["box"]], "border": r["border"]} for r in cs],
"p_cells": [round(float(x), 4) for x in p[:nc]],
"creature": {r: round(float(x), 4) for r, x in zip(boxes, p[nc:nc + nb])},
"chars": round(float(p[nc + nb]), 4)}
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", ".vlm2.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]
files = [f for f in files if os.path.exists(f[:-5] + ".vlm.json") and not os.path.exists(f[:-5] + ".vlm2.json")]
print(f"[gate2] shard {a.shard}/{a.nshards}: {len(files)} 张", flush=True)
if not files:
return
vlm = QwenVL()
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")
cs, boxes = cells(ids), G1.char_boxes(ids)
cache = {tag: (cs, boxes, baseline(vlm, tag, src, cs, boxes))}
cs, boxes, base = cache[tag]
cand = Image.open(f[:-5] + ".png").convert("RGB")
t0 = time.time()
p = vlm.yes_prob(cell_items(cand, cs) + creature_items(cand, boxes))
nc = len(cs)
out = {"p_cells": [round(float(x), 4) for x in p[:nc]],
"creature": {r: round(float(x), 4) for r, x in zip(boxes, p[nc:])}, "sec": round(time.time() - t0, 2)}
json.dump(out, open(f[:-5] + ".vlm2.json", "w"), indent=1)
print(f"[gate2] {tag} {info['name']} cells {sum(x >= 0.5 for x in out['p_cells'])}/{nc} {out['creature']}", flush=True)
def cell_ok(base, pc, kinds):
idx = [i for i, c in enumerate(base["cells"]) if c["cls"] in kinds and base["p_cells"][i] >= 0.5]
if not idx:
return None, 0
return round(float(np.mean([pc[i] >= 0.5 for i in idx])), 4), len(idx)
def merge(a):
rows = []
for f in sorted(glob.glob(f"{G}/cands/*/*.json")):
if f.endswith((".vlm.json", ".vlm2.json")) or not os.path.exists(f[:-5] + ".vlm2.json"):
continue
info = json.load(open(f))
tag = info["tag"]
o = json.load(open(f"{G}/sources/{tag}/f0_metrics.json"))
v1 = json.load(open(f[:-5] + ".vlm.json"))
v2 = json.load(open(f[:-5] + ".vlm2.json"))
b1 = json.load(open(f"{G}/sources/{tag}/regions.json"))
b2 = json.load(open(f"{G}/sources/{tag}/regions2.json"))
v, rg = v1["p_yes"], v1["region"]
ck = {"edge": info["metrics"]["layout"].get("edge_recall", 0) >= GATE["edge_rel"] * o["layout"]["edge_recall"]}
for q in ("layout", "only2", "scene"):
ck[q] = v[q] >= GATE[q]
ck["chars"] = b2["chars"] < 0.5 or v["chars"] >= GATE["chars"]
ck["charcrop"] = all(v1["charcrop"].get(r, 0) >= GATE["charcrop"] for r, x in b1["char_base"].items() if x >= 0.5)
ck["creature"] = all(v2["creature"].get(r, 0) >= GATE["creature"] for r, x in b2["creature"].items() if x >= 0.5)
open_ok, n_open = cell_ok(b2, v2["p_cells"], (1, 2))
arch_ok, n_arch = cell_ok(b2, v2["p_cells"], (3,))
ck["open_ok"] = open_ok is None or open_ok >= GATE["open_ok"]
ck["arch_ok"] = arch_ok is None or arch_ok >= GATE["arch_ok"]
ck["obj_ok"] = rg.get("obj_ok") is None or rg["obj_ok"] >= GATE["obj_ok"]
ckb = dict(ck)
ckb["open_ok"] = open_ok is None or open_ok >= TIER_B["open_ok"]
ckb["layout"] = v["layout"] >= TIER_B["layout"]
ckb["obj_ok"] = rg.get("obj_ok") is None or rg["obj_ok"] >= TIER_B["obj_ok"]
for k in TIER_B_SKIP:
ckb.pop(k)
tier = "A" if all(ck.values()) else "B" if all(ckb.values()) else None
rows.append({"tag": tag, "name": info["name"], "scene": info["scene"], "gen": info["gen"], "seed": info["seed"],
"checks": ck, "pass": tier is not None, "tier": tier, "vlm": v, "open_ok": open_ok, "n_open": n_open, "arch_ok": arch_ok,
"n_arch": n_arch, "obj_ok": rg.get("obj_ok"), "creature": v2["creature"], "charcrop": v1["charcrop"],
"score": round(v["layout"] + v["scene"] + v["only2"] + (1 if open_ok is None else open_ok)
+ (1 if arch_ok is None else arch_ok) + (1 if rg.get("obj_ok") is None else rg["obj_ok"]), 4)})
picks = {}
for r in rows:
k = (r["tag"], r["scene"])
rank = lambda x: ({"A": 2, "B": 1}.get(x["tier"], 0), x["score"])
if k not in picks or rank(r) > rank(picks[k]):
picks[k] = r
sel = list(picks.values())
by_scene, fails = {}, {}
print("passing candidates by tier:", {x: sum(1 for r in rows if r["tier"] == x) for x in ("A", "B")})
for r in sel:
by_scene.setdefault(r["scene"], []).append(r["pass"])
for r in rows:
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"]),
"check_fail_counts": fails, "pass_cands": f"{sum(r['pass'] for r in rows)}/{len(rows)}",
"slot_pass_by_scene": {s: f"{sum(v)}/{len(v)}" for s, v in sorted(by_scene.items())},
"slot_tier": {f"{t}/{s}": r["tier"] for (t, s), r in picks.items() if r["pass"]},
"tier_counts": {x: sum(1 for r in sel if r["tier"] == x) for x in ("A", "B")}}
json.dump(out, open(f"{G}/gate2.json", "w"), indent=1)
print(json.dumps({k: out[k] for k in ("pass_cands", "check_fail_counts", "slot_pass_by_scene", "tier_counts")}, 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"], "tier": r["tier"], "f0": f"{G}/cands/{s['tag']}/{r['name']}.png"})
json.dump(jobs, open(f"{G}/jobs2.json", "w"), indent=1)
print(len(jobs), "个 rollout 任务(含 orig)→ jobs2.json")
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")
a = ap.parse_args()
merge(a) if a.merge else run(a)
if __name__ == "__main__":
main()