Download scripts/teaser/f0lib.py from teawhite/maliang: direct link, hf CLI and curl.
- Browser
- Download file 34.5 kB
-
https://huggingface.co/teawhite/maliang/resolve/main/scripts/teaser/f0lib.py
- Command line
-
hf download hf://teawhite/maliang/scripts/teaser/f0lib.py
-
curl -L -o f0lib.py https://huggingface.co/teawhite/maliang/resolve/main/scripts/teaser/f0lib.py
34.5 kB
| """teaser · 首帧生成的共用件(纯 numpy / cv2 / PIL,img env 与 maliang venv 都能 import):读地图、派生输入图、拼提示词、 | |
| 角色框 / 贴回、对比拼图。生成后端在 f0gen.py,扫参在 f0sweep.py。 | |
| 地图目录(procmap 的输出,outputs/genbench/procmap/<case>/、outputs/teaser/maps/<case>/、outputs/teaser/f0_sweep/maps/<case>/): | |
| ids.npz([81,480,832] parts raw id,取帧 0)、map0.png(色图)、map0_shaded.png(按法线打光的体块)、depth0.png(16 bit,近亮)。 | |
| 派生(load_map 里一次算好,键见 load_map 的 docstring): | |
| * flat :firstframe_flux.flux_map 的 "white" 色表(静态类 = geometry 色,玩家蓝 / Boss 红 / 武器白),**不画铺地网格** | |
| (v4 的 16 px 网格提示会让首帧和视频里出现网格线); | |
| * shaded :map0_shaded 原样(玩家浅蓝、Boss 浅鲑红的胶囊人体 = 「人偶」,qwen21 会照画成人偶 ⇒ 提示词说是占位符 + 角色修补); | |
| * sil :shaded 但角色换成深蓝 / 深红实色(保留明暗); | |
| * depth8 :depth0 → 8 bit 灰度 RGB;edge:语义类边界(角色部件合并)白线黑底,2 px。 | |
| 提示词(scene_prompt):只写帧 0 里出现的类(像素 ≥ MIN_PX),「色 = 该场景里的这一类」(scenes.SCENES[s]["cls"]);没出现的 | |
| 建筑 / 石头 / 植被写「这一带没有、不许加」(genbench4 edit2 的经验);植被 / 道具附形状提示(体块图里的圆球 = 俯视的树冠); | |
| 角色句只写帧 0 里可见的(Boss 不可见就只有玩家);结尾禁文字 / UI / 网格 / 边框、禁沿画面边缘加墙。 | |
| 画风:场景键可写 "<场景>@<画风>"(scenes.SCENES 按需派生,title / light 带画风句);char_style / texture_phrase 让角色描述与 | |
| 纹理句跟着画风走;restyle_prompt = f0gen 的整图画风重绘提示词(photoreal / 无画风 ⇒ None)。 | |
| 其它:scene_blockout(体块图按场景色表重新上色,scb* 变体)、dummy_frac(人偶检测规则:角色掩码里「标记色且光滑」的比例)、 | |
| charfix_prompt / refine_prompt、VLM_Q + vlm_items(Qwen3-VL-8B 题)、pixelate(可选像素化后处理)、label / grid(拼图); | |
| nochar_map / real_chars / soft_mask / mask_box / tint_match / harmonize_prompt 是实例 R(realchar)加的。 | |
| """ | |
| from __future__ import annotations | |
| import glob | |
| import json | |
| import os | |
| import sys | |
| import numpy as np | |
| from PIL import Image | |
| M = "/opt/dlami/nvme/zhiyangdeng/maliang" | |
| for _p in (f"{M}/code/gc25d", f"{M}/scripts/teaser"): | |
| if _p not in sys.path: | |
| sys.path.insert(0, _p) | |
| import firstframe_flux as FF # noqa: E402 (只 import numpy / PIL;layout_metrics 要 scipy) | |
| from scenes import SCENES # noqa: E402 | |
| W, H = 832, 480 | |
| PLAYER, BOSS = (10, 12, 13, 14, 15, 16), (11, 17, 18, 19, 20, 21) | |
| MIN_PX = FF.MIN_PX # 少于 40 px 的类 / 角色不写进提示词 | |
| MAP_ROOTS = (f"{M}/outputs/genbench/procmap", f"{M}/outputs/teaser/maps", f"{M}/outputs/teaser/f0_sweep/maps") | |
| # 体块图 / 色图里每类的颜色名(geometry.PALETTE:terrain 棕、paving 浅灰、architecture 橙红、rock 深灰、vegetation 绿、prop 黄) | |
| COLOR_NAME = {0: "sky blue", 1: "brown", 2: "light grey", 3: "orange-red", 4: "dark grey", 5: "green", 6: "yellow"} | |
| COLOR_NAME_SHP = {**COLOR_NAME, 6: "the small separate objects"} # shp:道具已按场景上色,不写颜色名 | |
| SHAPE_HINT = {5: " (round green shapes are tree crowns and bushes seen from above, thin green posts are small trunks or " | |
| "stems)", | |
| 6: " (small separate objects)", 4: " (grey lumps and boulders)"} | |
| SCB_SUBJ = {0: "the empty background", 1: "the flat open ground areas", 2: "the flat paved floors and paths", | |
| 3: "the boxy buildings and walls", 4: "the lumpy grey boulders", 5: "the round crowns and posts (plants)", | |
| 6: "the small separate objects"} | |
| ABSENT = {3: "buildings, walls, towers, gates, huts or fences", 4: "rocks or boulders", 5: "trees or plants"} | |
| # ------------------------------------------------------------------ 地图 ---- | |
| def find_map(case: str) -> str: | |
| for r in MAP_ROOTS: | |
| if os.path.isfile(f"{r}/{case}/ids.npz"): | |
| return f"{r}/{case}" | |
| raise FileNotFoundError(f"找不到地图 {case}(查过 {MAP_ROOTS})") | |
| def list_maps(roots=MAP_ROOTS) -> list[str]: | |
| """所有齐全的地图目录(ids.npz + map0_shaded.png + depth0.png)。""" | |
| out = [] | |
| for r in roots: | |
| for d in sorted(glob.glob(f"{r}/*/")): | |
| d = d.rstrip("/") | |
| if all(os.path.isfile(f"{d}/{f}") for f in ("ids.npz", "map0_shaded.png", "depth0.png")): | |
| out.append(d) | |
| return out | |
| def load_ids0(path: str, frame: int = 0) -> np.ndarray: | |
| """ids.npz([F,H,W])/ .npy / id png / 色图 png → parts raw id [H,W](firstframe_flux.load_ids 的超集)。""" | |
| if path.endswith(".npy"): | |
| a = np.load(path) | |
| return np.ascontiguousarray(a[frame] if a.ndim == 3 else a).astype(np.uint8) | |
| return FF.load_ids(path, frame) | |
| def char_masks(ids: np.ndarray) -> dict: | |
| p, b = np.isin(ids, PLAYER), np.isin(ids, BOSS) | |
| return {"player": p, "boss": b, "has_p": int(p.sum()) >= MIN_PX, "has_b": int(b.sum()) >= MIN_PX, | |
| "player_px": int(p.sum()), "boss_px": int(b.sum()), "boss_weapon": int((ids == 21).sum()) >= 20, | |
| "player_weapon": int((ids == 16).sum()) >= 10} | |
| def sem_edges(ids: np.ndarray, width: int = 2) -> np.ndarray: | |
| import cv2 | |
| c = ids.copy() | |
| c[np.isin(c, PLAYER)] = 10 | |
| c[np.isin(c, BOSS)] = 11 | |
| e = np.zeros(c.shape, np.uint8) | |
| e[:, 1:] |= c[:, 1:] != c[:, :-1] | |
| e[1:, :] |= c[1:, :] != c[:-1, :] | |
| e = cv2.dilate(e * 255, np.ones((width, width), np.uint8)) | |
| return np.repeat(e[..., None], 3, -1) | |
| def recolor_chars(rgb: np.ndarray, ids: np.ndarray, player=(40, 70, 230), boss=(200, 30, 30)) -> np.ndarray: | |
| """角色像素换成实色(乘原图亮度 / 200 保留明暗)。""" | |
| out = rgb.copy() | |
| lum = rgb.astype(np.float32).mean(-1, keepdims=True) / 200.0 | |
| for m, c in ((np.isin(ids, PLAYER), player), (np.isin(ids, BOSS), boss)): | |
| out[m] = np.clip(np.asarray(c, np.float32) * lum[m], 0, 255).astype(np.uint8) | |
| return out | |
| PALETTE_JSON = f"{M}/outputs/teaser/f0_sweep/palette.json" # f0sweep.py palette 生成:{scene: {"0".."6": [r,g,b]}} | |
| _PAL: dict = {} | |
| SCB_MIN_LUM = 70.0 | |
| def prop_blockout(m: dict, scene: str) -> np.ndarray: | |
| """体块图只把道具(类 6)按场景色表重新上色(t1:qwen21 把黄道具原样画成黄方块,其余类照抄颜色问题小);场景没有色表 | |
| ⇒ 原样。shp 变体用,提示词里道具那一项不写颜色名(COLOR_NAME_SHP)。""" | |
| if not _PAL and os.path.exists(PALETTE_JSON): | |
| _PAL.update(json.load(open(PALETTE_JSON))) | |
| pal = _PAL.get(base_of(scene), {}) | |
| if "6" not in pal: | |
| return m["shaded"] | |
| ids = m["ids"] | |
| mk = ids == 6 | |
| out = m["shaded"].copy() | |
| if mk.any(): | |
| f = np.clip(m["shaded"].astype(np.float32).mean(-1)[mk] / float(np.mean(FF.GEO_COLORS[6])), 0.3, 1.6)[:, None] | |
| col = np.asarray(pal["6"], np.float32) + 4.0 | |
| col = col * max(1.0, SCB_MIN_LUM / float(col.mean())) | |
| out[mk] = np.clip(np.minimum(col, 255)[None] * f, 0, 255).astype(np.uint8) | |
| return out | |
| def scene_blockout(m: dict, scene: str, mix: float = 1.0) -> np.ndarray: | |
| """体块图按场景色表重新上色:静态类像素 = 场景里该类的代表色 × 体块图的明暗(体块图亮度 / 该类 geometry 色亮度, | |
| 夹在 [0.3, 1.6]);角色保持原来的浅蓝 / 浅鲑红人偶。t1:qwen21 会把体块图的黄 / 绿原样画出来(墓碑成黄方块), | |
| 先换成场景色 ⇒ 照抄颜色也是对的颜色。""" | |
| if not _PAL: | |
| _PAL.update(json.load(open(PALETTE_JSON))) | |
| pal = _PAL[base_of(scene)] | |
| ids, sh = m["ids"], m["shaded"].astype(np.float32) | |
| lum = sh.mean(-1) | |
| out = m["shaded"].copy() | |
| for c in range(7): | |
| mk = ids == c | |
| if not mk.any(): | |
| continue | |
| base = float(np.mean(FF.GEO_COLORS[c])) | |
| f = np.clip(lum[mk] / base, 0.3, 1.6)[:, None] | |
| col = np.asarray(pal[str(c)], np.float32) + 4.0 | |
| col = col * max(1.0, SCB_MIN_LUM / float(col.mean())) # 暗场景(地牢 / 熔岩)的色提亮到均值 ≥ SCB_MIN_LUM,保色相 | |
| col = mix * col + (1 - mix) * np.asarray(FF.GEO_COLORS[c], np.float32) # mix < 1:往标准类色拉回(保类可分) | |
| out[mk] = np.clip(np.minimum(col, 255)[None] * f, 0, 255).astype(np.uint8) | |
| return out | |
| def load_map(map_dir: str) -> dict: | |
| """→ {dir, case, ids [H,W], map0, shaded, flat, sil, depth8, edge(都是 uint8 [H,W,3]), chars(char_masks), present, | |
| n(类像素计数 [256]), source(source.json 或 {})}。""" | |
| import cv2 | |
| ids = load_ids0(f"{map_dir}/ids.npz") | |
| assert ids.shape == (H, W), ids.shape | |
| rd = lambda f: np.asarray(Image.open(f"{map_dir}/{f}").convert("RGB")) # noqa: E731 | |
| d16 = cv2.imread(f"{map_dir}/depth0.png", cv2.IMREAD_UNCHANGED) | |
| d8 = (d16.astype(np.float32) / max(1, int(d16.max())) * 255).round().astype(np.uint8) if d16.dtype == np.uint16 \ | |
| else d16.astype(np.uint8) | |
| if d8.ndim == 3: | |
| d8 = d8[..., 0] | |
| shaded = rd("map0_shaded.png") | |
| n = np.bincount(ids.reshape(-1), minlength=256) | |
| src = {} | |
| if os.path.isfile(f"{map_dir}/source.json"): | |
| src = json.load(open(f"{map_dir}/source.json")) | |
| return {"dir": map_dir, "case": os.path.basename(map_dir.rstrip("/")), "ids": ids, "map0": rd("map0.png"), | |
| "shaded": shaded, "flat": FF.flux_map(ids, "white", tiles=False), "sil": recolor_chars(shaded, ids), | |
| "depth8": np.repeat(d8[..., None], 3, -1), "edge": sem_edges(ids), "chars": char_masks(ids), | |
| "present": [c for c in range(7) if n[c] >= MIN_PX], "n": n, "source": src} | |
| def nochar_map(m: dict) -> dict: | |
| """realchar 路线的第一阶段输入:把角色从地图里去掉 —— ids 的角色像素换成最近的静态类(distance_transform 最近邻), | |
| 体块图 / 色图在(膨胀 4 px 的)角色区域里 cv2.inpaint(Telea)补成周围的地面 ⇒ 生成器只画背景,角色之后从源 clip 真实帧 | |
| 贴回(real_chars / f0gen.realchar)。返回新 dict(chars 全空,提示词自动写「没有角色」),原 dict 另存在 m['orig']。""" | |
| import cv2 | |
| from scipy import ndimage | |
| ids = m["ids"] | |
| ch = np.isin(ids, PLAYER + BOSS) | |
| _, (iy, ix) = ndimage.distance_transform_edt(ch, return_indices=True) | |
| ids2 = ids[iy, ix].astype(np.uint8) | |
| k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (9, 9)) | |
| mk = cv2.dilate(ch.astype(np.uint8), k) | |
| out = dict(m) | |
| out.update(ids=ids2, orig=m, chars=char_masks(ids2), flat=FF.flux_map(ids2, "white", tiles=False), | |
| shaded=cv2.inpaint(np.ascontiguousarray(m["shaded"]), mk, 5, cv2.INPAINT_TELEA), | |
| sil=cv2.inpaint(np.ascontiguousarray(m["shaded"]), mk, 5, cv2.INPAINT_TELEA), | |
| edge=sem_edges(ids2)) | |
| n = np.bincount(ids2.reshape(-1), minlength=256) | |
| out.update(n=n, present=[c for c in range(7) if n[c] >= MIN_PX]) | |
| return out | |
| def real_chars(m: dict) -> dict | None: | |
| """源 clip 的真实帧 0 + 真实 ids 帧 0(procmap 借的是源 clip 的相机 + 胶囊 ⇒ 角色在同一像素位置,实测 10 张地图里 | |
| 9 张 IoU = 1.00,plaza P9C 0.83 = 真实帧里玩家被场景挡了一块)→ {rgb, ids, mask(玩家 ∪ Boss), clip_id}; | |
| 地图没有 source.json / clip_id 时返回 None。""" | |
| cid = (m.get("source") or {}).get("clip_id") | |
| if not cid: | |
| return None | |
| rgb = FF.clip_frame(cid) | |
| rid = FF.clip_ids(cid) | |
| return {"rgb": rgb, "ids": rid, "mask": np.isin(rid, PLAYER + BOSS), "clip_id": cid} | |
| def soft_mask(mask: np.ndarray, dilate: int, feather: float) -> np.ndarray: | |
| """bool 掩码 → 膨胀 dilate px(≤ 0 = 腐蚀)+ 高斯羽化 → float32 ∈ [0,1](羽化不往掩码内缩:max(羽化, 硬掩码))。""" | |
| import cv2 | |
| mk = mask.astype(np.uint8) | |
| if dilate > 0: | |
| mk = cv2.dilate(mk, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2 * dilate + 1, 2 * dilate + 1))) | |
| elif dilate < 0: | |
| mk = cv2.erode(mk, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (-2 * dilate + 1, -2 * dilate + 1))) | |
| a = mk.astype(np.float32) | |
| if feather > 0: | |
| a = np.maximum(cv2.GaussianBlur(a, (0, 0), feather), a) | |
| return np.clip(a, 0, 1) | |
| def mask_box(mask: np.ndarray, margin: float = 0.6, min_side: int = 160, pad: int = 24): | |
| """bool 掩码的外接框(x0,y0,x1,y1),每边外扩 max(pad, margin·边长)、至少 min_side,夹在画面内。""" | |
| if mask.sum() < MIN_PX: | |
| return None | |
| ys, xs = np.nonzero(mask) | |
| x0, x1, y0, y1 = xs.min(), xs.max() + 1, ys.min(), ys.max() + 1 | |
| bw, bh = x1 - x0, y1 - y0 | |
| cx, cy = (x0 + x1) / 2, (y0 + y1) / 2 | |
| hw = max(bw / 2 + max(pad, margin * bw), min_side / 2) | |
| hh = max(bh / 2 + max(pad, margin * bh), min_side / 2) | |
| return int(max(0, cx - hw)), int(max(0, cy - hh)), int(min(W, cx + hw)), int(min(H, cy + hh)) | |
| def tint_match(fg: np.ndarray, bg_new: np.ndarray, bg_old: np.ndarray, ring: np.ndarray, alpha: float = 0.5): | |
| """把真实角色的整体色调往新场景拉:增益 = 新背景环带均值 / 源背景环带均值(逐通道,夹在 [0.55, 1.3]),按 alpha 混合。""" | |
| if ring.sum() < 50: | |
| return fg | |
| g = (bg_new[ring].astype(np.float32).mean(0) + 8) / (bg_old[ring].astype(np.float32).mean(0) + 8) | |
| g = np.clip(g, 0.55, 1.3) | |
| g = 1 + alpha * (g - 1) | |
| return np.clip(fg.astype(np.float32) * g, 0, 255).astype(np.uint8) | |
| # ------------------------------------------------------------------ 画风(scenes.LOOKS) ---- | |
| def look_of(scene: str) -> str | None: | |
| """"<场景>@<画风>" → 画风键;纯场景名 → None(= 默认写实提示词)。""" | |
| return SCENES[scene].get("look") | |
| def base_of(scene: str) -> str: | |
| return scene.split("@", 1)[0] | |
| def with_look(scene: str, look: str | None) -> str: | |
| """job 行的 look 键并进场景键(scenes.SCENES 认 "<场景>@<画风>");已带 @ 或 look 为空就原样。""" | |
| return scene if (not look or "@" in scene) else f"{scene}@{look}" | |
| def char_style(scene: str | None) -> str: | |
| """角色的画风修饰(LOOKS[look]["char"],如 "pixel-art" / "stylized cartoon");无画风 = 空。""" | |
| lk = look_of(scene) if scene else None | |
| if not lk: | |
| return "" | |
| import scenes as _SC | |
| return _SC.LOOKS[lk]["char"] + " " | |
| def texture_phrase(scene: str | None) -> str: | |
| lk = look_of(scene) if scene else None | |
| if not lk or lk == "photoreal": | |
| return "detailed realistic textures" | |
| import scenes as _SC | |
| return f"detailed {_SC.LOOKS[lk]['short']} textures" | |
| # ------------------------------------------------------------------ 提示词 ---- | |
| def legend(m: dict, scene: str, color_name: dict = COLOR_NAME, hints: bool = True) -> str: | |
| sc = SCENES[scene] | |
| parts = [f"{color_name[c]} = {sc['cls'][c]}" + (SHAPE_HINT.get(c, "") if hints else "") for c in m["present"]] | |
| return "; ".join(parts) | |
| def absent_text(m: dict) -> str: | |
| miss = [ABSENT[c] for c in ABSENT if c not in m["present"]] | |
| return (" This part of the level contains no " + ", no ".join(miss) + "; do not add any.") if miss else "" | |
| def char_text(m: dict, style: str, scene: str | None = None) -> str: | |
| """style: placeholder(体块图里的胶囊人偶 = 占位符)| silhouette(深蓝 / 深红实色剪影)| marker(flat 色图的蓝 / 红人形)。 | |
| scene 带画风时角色描述加 LOOKS[look]["char"](像素 / 卡通画风下角色也是像素 / 卡通的)。""" | |
| ch = m["chars"] | |
| cs = char_style(scene) | |
| hero = f"a fully detailed {cs}armored human warrior hero" + (" holding a steel sword" if ch["player_weapon"] else "") | |
| boss = f"a huge, fully detailed {cs}monster boss" + (" wielding a big weapon" if ch["boss_weapon"] else "") | |
| who = {"placeholder": ("the light-blue mannequin", "the salmon-pink mannequin"), | |
| "silhouette": ("the dark blue silhouette", "the dark red silhouette"), | |
| "marker": ("the blue humanoid figure", "the red humanoid figure")}[style] | |
| items = ([f"{who[0]} becomes the player character, {hero}"] if ch["has_p"] else []) + \ | |
| ([f"{who[1]} becomes the boss, {boss}"] if ch["has_b"] else []) | |
| if not items: | |
| return " There are no characters in this scene: do not draw any people, creatures, figures or statues." | |
| n = len(items) | |
| lead = {"placeholder": " The smooth capsule mannequins are only placeholders and must not appear in the final image: " | |
| if n == 2 else " The smooth capsule mannequin is only a placeholder and must not appear in the final image: ", | |
| "silhouette": " ", "marker": " "}[style] | |
| txt = lead + "; ".join(items) + ". " | |
| txt += ("Each character has exactly the position, pose, size, facing direction and limb directions of its " | |
| "placeholder, and looks like a living video game character with natural colours and materials, not a " | |
| "mannequin, statue or toy. ") | |
| txt += "These are the only two characters; " if n == 2 else "This is the only character; " | |
| txt += "do not add any other people or creatures." | |
| return txt | |
| QUALITY = (" {light} Bright, clear and readable game lighting: every object is easy to see. High-end isometric action RPG " | |
| "graphics with {tex} and soft shadows; no text, " | |
| "no letters, no UI, no grid lines, no outlines, no border. Do not frame the scene with new walls, cliffs, " | |
| "trees or buildings along the image borders; the borders show the same kind of region as the blockout.") | |
| NOCOPY = (" The colours of picture 1 are only a code that says what each region is: do not copy them; give every " | |
| "region the natural colours and materials it has in {title}.") | |
| def scene_prompt(m: dict, scene: str, kind: str = "blockout", chars: str = "placeholder", nocopy: bool = False) -> str: | |
| """kind: blockout(体块图 / 剪影图:qwen21 / qie2511)| flatmap(平涂色图:klein / qie)。 | |
| nocopy:加 NOCOPY 句(t1:qwen21 常把体块图的黄 / 绿原样画出来 —— 墓地的墓碑成黄方块、糖果的树成绿球)。""" | |
| sc = SCENES[scene] | |
| if kind == "scb": # 场景色体块图:颜色已经是场景色,色表改成按形状 / 类说明 | |
| head = (f"Picture 1 is a simple untextured 3D blockout of one level of an isometric action RPG video game, " | |
| f"already painted in the colours of {sc['title']}. Render it as the finished, fully detailed in-game " | |
| "screenshot, seen from exactly the same high top-down camera. Keep every region, object and figure " | |
| "exactly where it is, with the same outline, size and shape, and add nothing that is not in picture " | |
| "1: open ground and floors stay open and are only textured. In picture 1: " + | |
| "; ".join(f"{SCB_SUBJ[c]} are {sc['cls'][c]}" for c in m["present"]) + ".") | |
| return head + absent_text(m) + char_text(m, chars, scene) + \ | |
| QUALITY.format(light=sc["light"], tex=texture_phrase(scene)) | |
| if kind == "scb2": # 场景色体块图 + 标准色码体块图(看类) | |
| head = (f"Picture 1 is a simple untextured 3D blockout of one level of an isometric action RPG video game, " | |
| f"already painted in the colours of {sc['title']}; picture 2 is the same blockout colour-coded by " | |
| f"region type ({legend(m, scene)}). Render picture 1 as the finished, fully detailed in-game screenshot " | |
| "of " + sc["title"] + ", seen from exactly the same high top-down camera. Keep every region, object " | |
| "and figure exactly where it is, with the same outline, size and shape, and add nothing that is not " | |
| "in the blockout: open ground and floors stay open and are only textured.") | |
| return head + absent_text(m) + char_text(m, chars, scene) + \ | |
| QUALITY.format(light=sc["light"], tex=texture_phrase(scene)) | |
| what = {"blockout": "a colour-coded 3D blockout", "shp": "a colour-coded 3D blockout", | |
| "flatmap": "a flat colour-coded layout map"}[kind] | |
| head = (f"Picture 1 is {what} of one level of an isometric action RPG video game. Render it as the finished in-game " | |
| f"screenshot of {sc['title']}, seen from exactly the same high top-down camera. Keep every region, object " | |
| f"and figure exactly where it is, with the same outline, size and shape, and add nothing that is not in " | |
| f"picture 1: open ground and floors stay open and are only textured. Colour code: " | |
| f"{legend(m, scene, COLOR_NAME_SHP if kind == 'shp' else COLOR_NAME)}.") | |
| return head + (NOCOPY.format(title=sc["title"]) if nocopy else "") + absent_text(m) + char_text(m, chars, scene) + \ | |
| QUALITY.format(light=sc["light"], tex=texture_phrase(scene)) | |
| def klein_prompt(m: dict, scene: str) -> str: | |
| """firstframe_flux.map2frame v3 模板(无网格)+ 场景色表 + 场景句(genbench4 make_scene 的 map 模式改进版)。""" | |
| sc = SCENES[scene] | |
| txt = ("Turn image 1, a flat color-coded layout map, into a finished screenshot of a 3D action RPG video game seen " | |
| "from a high top-down isometric camera. Follow the layout of image 1 exactly: every region, object and " | |
| "character keeps its exact position, shape and size, and nothing is added that is not in the map; open " | |
| f"ground and floors stay open. Color legend of the layout map: {legend(m, scene)}.") | |
| txt += absent_text(m) + char_text(m, "marker", scene) | |
| txt += (" Replace every flat color with detailed textured materials, natural lighting and soft shadows; no flat " | |
| f"colors, no outlines, no grid lines, no text, no UI, no border. Scene: the whole level is {sc['title']}. " | |
| f"{sc['light']}") | |
| return txt | |
| def zimage_prompt(m: dict, scene: str) -> str: | |
| """Z-Image(ControlNet 只给结构):整段场景描述 + 按面积从大到小列出各类在该场景里是什么 + 角色。""" | |
| sc = SCENES[scene] | |
| order = sorted(m["present"], key=lambda c: -m["n"][c]) | |
| ch = m["chars"] | |
| chars = ("an armored human warrior hero with a sword fighting a huge monster boss" if ch["has_p"] and ch["has_b"] | |
| else "a single armored human warrior hero with a sword" if ch["has_p"] else "no characters") | |
| return (f"A screenshot of a high-end isometric action RPG video game seen from a high top-down camera: {sc['title']}. " | |
| f"The ground is {sc['cls'][order[0]]}; the level also has " + ", ".join(sc["cls"][c] for c in order[1:]) + | |
| f". In the scene: {chars}, exactly two figures at most. {sc['light']} Detailed realistic textures, soft " | |
| "shadows, polished game graphics, no text, no UI, no grid lines, no border.") | |
| def charfix_prompt(m: dict, scene: str) -> str: | |
| ch = m["chars"] | |
| items = [] | |
| if ch["has_p"]: | |
| items.append(f"the light-blue mannequin (or the blue figure) into a fully detailed {char_style(scene)}armored human " | |
| "warrior hero" + (" holding a steel sword" if ch["player_weapon"] else "")) | |
| if ch["has_b"]: | |
| items.append("the salmon-pink mannequin (or the red figure)" + (" and its pink club" if ch["boss_weapon"] else "") + | |
| f" into a huge, fully detailed {char_style(scene)}monster boss" + | |
| (" wielding a big weapon" if ch["boss_weapon"] else "")) | |
| return ("Picture 1 is a close-up crop of a video game screenshot of " + SCENES[scene]["title"] + ". Edit picture 1: " | |
| "turn " + ", and turn ".join(items) + ", each in exactly the same position, pose, size and facing " | |
| "direction. They must look like living video game characters with natural colours and materials that fit " | |
| "the scene, not mannequins, statues or toys. If a character already looks fully detailed, keep it as it " | |
| "is. Keep everything else in the image exactly unchanged; do not add any other characters or objects.") | |
| def restyle_prompt(scene: str) -> str | None: | |
| """画风重绘(f0gen 的 restyle 阶段,qwen21 整图编辑):t4 冒烟 —— 场景提示词里写画风,qwen21 / klein 都还是画成 3D 渲染 | |
| (体块图本身是 3D 渲染的样子);先按写实生成再整图「换画风、布局不动」。look 为空 / photoreal ⇒ None(不重绘)。""" | |
| lk = look_of(scene) | |
| if not lk or lk == "photoreal": | |
| return None | |
| import scenes as _SC | |
| L_ = _SC.LOOKS[lk] | |
| return (f"Redraw picture 1 completely in this art style: {L_['prompt']} Every surface, object and character must be " | |
| f"in the {L_['short']} style, nothing may stay photorealistic. Keep exactly the same composition, camera, " | |
| "layout and every object, with the same positions, outlines and sizes; keep both characters with the same " | |
| f"positions, poses, sizes and weapons, drawn as {L_['char']} game characters. Do not add or remove anything; " | |
| "no text, no UI, no border.") | |
| def pixelate(img: np.ndarray, cell: int = 4, colors: int = 48) -> np.ndarray: | |
| """确定性的像素画后处理(look = pixel):INTER_AREA 缩到 1/cell → PIL 中位切分量化到 colors 色(不抖动)→ 最近邻放回。 | |
| t4c:qwen21 画风重绘的 pixel 只有一点点像素感,加这一步才是真正的方块像素。""" | |
| import cv2 | |
| h, w = img.shape[:2] | |
| sm = cv2.resize(img, (w // cell, h // cell), interpolation=cv2.INTER_AREA) | |
| q = Image.fromarray(sm).quantize(colors=colors, method=Image.Quantize.MEDIANCUT, dither=Image.Dither.NONE) | |
| return cv2.resize(np.asarray(q.convert("RGB")), (w, h), interpolation=cv2.INTER_NEAREST) | |
| def refine_prompt(scene: str) -> str: | |
| """klein 低强度 SDEdit 补细节(f0gen 的 refine 阶段)。""" | |
| sc = SCENES[scene] | |
| return (f"Make image 1 a highly detailed, polished screenshot of a high-end isometric action RPG set in " | |
| f"{sc['title']}: add rich realistic textures and fine material detail to the ground, floors, buildings, " | |
| "rocks, plants and objects, and crisp detail to the characters. Keep exactly the same composition, layout, " | |
| "objects, characters, poses, camera, colours and lighting; do not add or remove anything; no text, no UI, " | |
| "no grid lines, no border.") | |
| def harmonize_prompt(scene: str, n_chars: int) -> str: | |
| """realchar 的融合编辑:真实角色已贴进生成背景,只修边缘 / 影子 / 光照,不改角色设计。""" | |
| sc = SCENES[scene] | |
| who = "the two characters" if n_chars == 2 else "the character" | |
| return (f"Picture 1 is a close-up crop of a video game screenshot of {sc['title']}; {who} in the middle were pasted " | |
| "in from another level. Blend them naturally into this scene: replace any leftover patches or halos of the " | |
| "other level's ground around them with the surrounding ground of this scene, add soft natural contact " | |
| f"shadows under them that match the scene lighting ({sc['light'].rstrip('.')}), and gently adjust the " | |
| f"lighting and colour tint on {who} to match the scene. Keep {who} exactly the same design, pose, size, " | |
| "position, armour, colours and weapons; do not add, remove or move anything else.") | |
| # ------------------------------------------------------------------ 角色框 / 贴回 ---- | |
| def char_box(m: dict, margin: float = 0.6, min_side: int = 160) -> tuple[int, int, int, int] | None: | |
| """所有可见角色的并集框(832×480 坐标 x0,y0,x1,y1),每边外扩 max(24, margin·边长),至少 min_side,夹在画面内。""" | |
| mk = m["chars"]["player"] | m["chars"]["boss"] | |
| if mk.sum() < MIN_PX: | |
| return None | |
| ys, xs = np.nonzero(mk) | |
| x0, x1, y0, y1 = xs.min(), xs.max() + 1, ys.min(), ys.max() + 1 | |
| bw, bh = x1 - x0, y1 - y0 | |
| px, py = max(24, margin * bw), max(24, margin * bh) | |
| cx, cy = (x0 + x1) / 2, (y0 + y1) / 2 | |
| hw, hh = max(bw / 2 + px, min_side / 2), max(bh / 2 + py, min_side / 2) | |
| x0, x1 = int(max(0, cx - hw)), int(min(W, cx + hw)) | |
| y0, y1 = int(max(0, cy - hh)), int(min(H, cy + hh)) | |
| return x0, y0, x1, y1 | |
| def paste_mask(m: dict, dilate: int = 14, feather: int = 6) -> np.ndarray: | |
| """角色并集掩码膨胀 dilate px + 高斯羽化 → float32 [H,W] ∈ [0,1](角色修补只换掩码内)。""" | |
| import cv2 | |
| mk = (m["chars"]["player"] | m["chars"]["boss"]).astype(np.uint8) | |
| k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2 * dilate + 1, 2 * dilate + 1)) | |
| a = cv2.dilate(mk, k).astype(np.float32) | |
| if feather: | |
| a = cv2.GaussianBlur(a, (0, 0), feather) | |
| a = np.maximum(a, mk.astype(np.float32)) | |
| return np.clip(a, 0, 1) | |
| def area_down(img: np.ndarray, w: int = W, h: int = H) -> np.ndarray: | |
| import cv2 | |
| img = np.asarray(img) | |
| if img.shape[:2] == (h, w): | |
| return img | |
| return cv2.resize(img, (w, h), interpolation=cv2.INTER_AREA if img.shape[1] > w else cv2.INTER_LANCZOS4) | |
| # ------------------------------------------------------------------ 人偶检测(规则) ---- | |
| def dummy_frac(img: np.ndarray, m: dict, tol: float = 40.0, smooth: float = 5.0) -> dict: | |
| """首帧(832×480)里每个可见角色掩码(腐蚀 1 px)内「人偶样」像素的比例:颜色离体块图同位置像素(浅蓝 / 浅鲑红人偶, | |
| 带明暗)、平涂色图(玩家蓝 / Boss 红 / 武器白)、剪影图(深蓝 / 深红)任一 RGB 距离 < tol,**且**局部平滑(5×5 灰度 | |
| 标准差 < smooth;真角色有盔甲 / 皮毛的细节,照抄的人偶是光滑的)。≈ 0 = 真角色,高 = 照抄了人偶 / 火柴人 / 标记色小人。 | |
| → {"player": f, "boss": f}(不可见的角色不出现)。""" | |
| import cv2 | |
| img = area_down(np.asarray(img)).astype(np.float32) | |
| g = img.mean(-1) | |
| mu = cv2.blur(g, (5, 5)) | |
| sd = np.sqrt(np.maximum(cv2.blur(g * g, (5, 5)) - mu * mu, 0)) | |
| refs = [m["shaded"], m["flat"], m["sil"]] | |
| out = {} | |
| for k in ("player", "boss"): | |
| mk = cv2.erode(m["chars"][k].astype(np.uint8), np.ones((3, 3), np.uint8)).astype(bool) | |
| if mk.sum() < MIN_PX: | |
| continue | |
| x = img[mk] | |
| d = np.min([np.linalg.norm(x - r[mk].astype(np.float32), axis=-1) for r in refs], 0) | |
| out[k] = round(float(((d < tol) & (sd[mk] < smooth)).mean()), 4) | |
| return out | |
| DUMMY_FRAC = 0.35 # dummy_frac 任一角色 ≥ 这个 ⇒ 判为人偶(f0gen --detect rule) | |
| # ------------------------------------------------------------------ VLM 题(Qwen3-VL-8B,P(yes)) ---- | |
| QWEN8 = ("/opt/dlami/nvme/zhiyangdeng/PCE/weights/hf_hub/models--Qwen--Qwen3-VL-8B-Instruct/snapshots/" | |
| "0c351dd01ed87e9c1b53cbc748cba10e6187ff3b") | |
| VLM_Q = { | |
| "scene": "Is this video game screenshot clearly set in {vlm}? Answer Yes or No.", | |
| "clean": "Is this image a clean game screenshot with no text, letters, numbers, logos, watermark, user interface, " | |
| "grid lines drawn over the ground, or frame border? Answer Yes or No.", | |
| "nodummy": "Look at the whole image. Is it free of any smooth featureless capsule-shaped mannequins, dummies or " | |
| "toy-like placeholder figures? Answer Yes or No.", | |
| "alive": "This is a close-up of a video game screenshot. Is there, at the centre of the image, a living video game " | |
| "character ({who}) rendered with detailed armour, skin, fur or clothing, and not a smooth mannequin, " | |
| "dummy, statue, rock, plant or object? Answer Yes or No.", | |
| } | |
| def zoom(img: Image.Image, mask: np.ndarray, min_side: int = 448) -> Image.Image: | |
| """mask(832×480)的外接框外扩 1/3 → 从 img(任意尺寸,同长宽比)裁出、放大到短边 ≥ min_side。""" | |
| ys, xs = np.nonzero(mask) | |
| x0, x1, y0, y1 = xs.min(), xs.max() + 1, ys.min(), ys.max() + 1 | |
| pw, ph = max(16, (x1 - x0) // 3), max(16, (y1 - y0) // 3) | |
| sx, sy = img.width / W, img.height / H | |
| box = (int(max(0, x0 - pw) * sx), int(max(0, y0 - ph) * sy), int(min(W, x1 + pw) * sx), int(min(H, y1 + ph) * sy)) | |
| c = img.crop(box) | |
| 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 vlm_items(img: Image.Image, m: dict, scene: str | None, keys=("scene", "clean", "nodummy", "alive")): | |
| """→ [(images, text)], [key]:alive 拆成 alive_p / alive_b(帧 0 里没有的角色不问)。""" | |
| batch, ks = [], [] | |
| for k in keys: | |
| if k == "alive": | |
| for kk, who, mk in (("alive_p", "the hero", m["chars"]["player"]), ("alive_b", "a monster", m["chars"]["boss"])): | |
| if mk.sum() >= MIN_PX: | |
| batch.append(([zoom(img, mk)], VLM_Q["alive"].format(who=who))) | |
| ks.append(kk) | |
| elif k == "scene": | |
| if scene: | |
| batch.append(([img], VLM_Q["scene"].format(vlm=SCENES[scene]["vlm"]))) | |
| ks.append(k) | |
| else: | |
| batch.append(([img], VLM_Q[k])) | |
| ks.append(k) | |
| return batch, ks | |
| # ------------------------------------------------------------------ 拼图 ---- | |
| def label(img: np.ndarray, text: str, scale: float = 0.5) -> np.ndarray: | |
| import cv2 | |
| out = np.ascontiguousarray(img.copy()) | |
| (tw, th), _ = cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX, scale, 1) | |
| cv2.rectangle(out, (0, 0), (tw + 8, th + 10), (0, 0, 0), -1) | |
| cv2.putText(out, text, (4, th + 4), cv2.FONT_HERSHEY_SIMPLEX, scale, (255, 255, 255), 1, cv2.LINE_AA) | |
| return out | |
| def grid(tiles: list[np.ndarray], ncol: int, tile_w: int = 416) -> np.ndarray: | |
| import cv2 | |
| th = round(tile_w * H / W) | |
| ts = [cv2.resize(t, (tile_w, th), interpolation=cv2.INTER_AREA) for t in tiles] | |
| while len(ts) % ncol: | |
| ts.append(np.zeros_like(ts[0])) | |
| return np.concatenate([np.concatenate(ts[i:i + ncol], 1) for i in range(0, len(ts), ncol)], 0) | |