# -*- coding: utf-8 -*- """UI 框材组量化改色脚本(M-1 迭代 2 T2,2026-09-28)。 ♻ 复用件 → 复用闸①色板量化:全部产物按明度重映射到 art-bible §4.1/§7.2 token 渐变带, 不保留原包杂色;闸②禁入带=token 色天然合规;闸③灰度可辨=图标四形互异;闸④授权=全 CC0。 源包 = artwork/ui-reuse-2026-09-23/(素材不入 git——先查本仓库,缺位回退主检出 D:/code/g-plan2): oga-kenney-pixel-ui-750(CC0,OGA) 9-Slice/Ancient 面板/按钮九宫格 48×48 kenney-fantasy-ui-borders(CC0) panel-border 石框 48×48 5-石纹底噪/stone_6.png(CC0,OGA Handpainted stone) 无缝底噪 1200×1200 kenney-board-game-icons(CC0) 64px 图标四件 产物 → game/assets/art/ui/(终稿素材目录,不入 git;消费 = UiKit.load_tex 守卫,缺位程序化回退)。 用法:python tools/quantize_ui_skin.py """ from PIL import Image import os ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) MAIN_ROOT = "D:/code/g-plan2" SRC = ("artwork", "ui-reuse-2026-09-23") OUT = ("game", "assets", "art", "ui") def src_path(*parts): for base in (ROOT, MAIN_ROOT): p = os.path.join(base, *SRC, *parts) if os.path.exists(p): return p raise FileNotFoundError("/".join(SRC + parts)) def out_path(name): p = os.path.join(ROOT, *OUT, name) os.makedirs(os.path.dirname(p), exist_ok=True) return p # §4.1/§7.2 token 渐变带(量化目标值——样张 v2 冻结色板) RAMP_PANEL = [(120, 106, 88), (226, 210, 182), (243, 234, 214)] # PANEL_BORDER→PANEL_DATA→PLATE 微亮 RAMP_BUTTON = [(179, 154, 120), (201, 175, 131), (245, 234, 211)] # FLOOR→BRICK(掩尘"可按的砖")→PLATE RAMP_PRESSED = [(120, 106, 88), (179, 154, 120)] # PANEL_BORDER→FLOOR RAMP_BACKDROP = [(30, 25, 20), (120, 106, 88)] # BG_ROOT→PANEL_BORDER(低对比自然后退 §6.1) RAMP_BORDER = [(120, 106, 88), (201, 175, 131)] # PANEL_BORDER→BRICK ICON_GOLD = (240, 168, 40) # 奖励金(钥石) ICON_WARM = (240, 226, 200) # 暖白(柜台件,暗卡上可读) NODE_WARM = (240, 226, 200) # 节点图标暖白(关卡画面样张 v1 冻结——暗底态原色,当前金底乘法调制深墨) NODE_PX = 20 # 节点图标烘焙边长(52×36 方块内 20px 图标+右上编号,1:1 消费零缩放) def ramp_color(ramp, t): if t <= 0: return ramp[0] if t >= 1: return ramp[-1] seg = t * (len(ramp) - 1) i = min(int(seg), len(ramp) - 2) k = seg - i a, b = ramp[i], ramp[i + 1] return tuple(int(a[j] + (b[j] - a[j]) * k) for j in range(3)) def remap_luma(img, ramp, gamma=1.0): """明度→token 渐变带重映射(保留 alpha 与纹理结构——闸①量化)。""" im = img.convert("RGBA") px = im.load() vals = [sum(px[x, y][:3]) / 3.0 for y in range(im.size[1]) for x in range(im.size[0]) if px[x, y][3] > 8] lo, hi = min(vals), max(vals) span = max(1.0, hi - lo) for y in range(im.size[1]): for x in range(im.size[0]): r, g, b, a = px[x, y] if a <= 8: continue t = ((sum((r, g, b)) / 3.0) - lo) / span px[x, y] = ramp_color(ramp, t ** gamma) + (a,) return im def recolor_flat(img, rgb): """剪影图标单色重着色(保留 alpha 抗锯齿——形状承载识别,闸③)。""" im = img.convert("RGBA") px = im.load() for y in range(im.size[1]): for x in range(im.size[0]): r, g, b, a = px[x, y] if a > 0: px[x, y] = rgb + (a,) return im def main(): jobs = [] # 面板/按钮九宫格(pixel-ui-750 Ancient;texture_margin 取 8px——消费侧同值) jobs.append(("panel_stone.png", remap_luma(Image.open(src_path("1-UI面板与按钮", "oga-kenney-pixel-ui-750(CC0)", "9-Slice", "Ancient", "brown.png")), RAMP_PANEL))) jobs.append(("button_stone.png", remap_luma(Image.open(src_path("1-UI面板与按钮", "oga-kenney-pixel-ui-750(CC0)", "9-Slice", "Ancient", "tan.png")), RAMP_BUTTON))) jobs.append(("button_stone_pressed.png", remap_luma(Image.open(src_path("1-UI面板与按钮", "oga-kenney-pixel-ui-750(CC0)", "9-Slice", "Ancient", "tan_pressed.png")), RAMP_PRESSED))) # 石框(fantasy-ui-borders) jobs.append(("border_stone.png", remap_luma(Image.open(src_path("1-UI面板与按钮", "kenney-fantasy-ui-borders(CC0)", "PNG", "Default", "Border", "panel-border-000.png")), RAMP_BORDER))) # 石纹底噪(seamless 1200→256,暗带低对比——B 区/开始层背景) stone = Image.open(src_path("5-石纹底噪", "stone_6.png")).convert("RGB").resize((256, 256), Image.LANCZOS) jobs.append(("backdrop_stone.png", remap_luma(stone, RAMP_BACKDROP, gamma=1.6))) # 营地图标四件(board-game-icons 64px;钥石=奖励金,柜台件=暖白) icon_dir = ("3-节点图与营地图标", "kenney-board-game-icons(CC0)", "PNG", "Default (64px)") jobs.append(("icon_keystone.png", recolor_flat(Image.open(src_path(*(icon_dir + ("suit_diamonds.png",)))), ICON_GOLD))) jobs.append(("icon_forge.png", recolor_flat(Image.open(src_path(*(icon_dir + ("sword.png",)))), ICON_WARM))) jobs.append(("icon_workshop.png", recolor_flat(Image.open(src_path(*(icon_dir + ("flask_full.png",)))), ICON_WARM))) jobs.append(("icon_atlas.png", recolor_flat(Image.open(src_path(*(icon_dir + ("book_open.png",)))), ICON_WARM))) # 节点图标五件(board-game-icons 64px→20px 烘焙,WARM 单色——关卡画面样张 v1 冻结形制,M-2 迭代 4 T2; # 战=bow/赏=award/疗=campfire/王=crown_b/商=pouch,语义近似+灰度互辨,色相不承载身份 §4.6) node_jobs = (("node_battle.png", "bow.png"), ("node_reward.png", "award.png"), ("node_rest.png", "campfire.png"), ("node_boss.png", "crown_b.png"), ("node_shop.png", "pouch.png")) node_outs = {} for name, src in node_jobs: im = recolor_flat(Image.open(src_path(*(icon_dir + (src,)))), NODE_WARM) im = im.resize((NODE_PX, NODE_PX), Image.NEAREST) node_outs[name] = im jobs.append((name, im)) for name, im in jobs: im.save(out_path(name)) print("saved:", "/".join(OUT + (name,))) node_icon_gates(node_outs) print("=== 量化改色完成(%d 件,全 CC0;闸①②③④ 见脚本头注) ===" % len(jobs)) return 0 def node_icon_gates(outs): """节点图标复用闸四道自检(闸④授权=kenney-board-game-icons CC0 全包在案): ①量化=全像素 NODE_WARM 或透明(零原包杂色) ②禁入带=WARM 色相 39° 天然合规 ③灰度互辨=alpha 剪影两两错位率 ≥0.08(与样张 L1 同判据) ④见上。""" fail = [] for name, im in outs.items(): bad = sum(1 for px in im.getdata() if px[3] > 8 and px[:3] != NODE_WARM) if bad: fail.append("%s 闸①杂色 %d px" % (name, bad)) if im.size != (NODE_PX, NODE_PX): fail.append("%s 尺寸 %s" % (name, im.size)) keys = list(outs.keys()) worst = (1.0, None) sigs = {k: [1 if px[3] > 96 else 0 for px in outs[k].getdata()] for k in keys} for i, a in enumerate(keys): for b in keys[i + 1:]: diff = sum(1 for x, y in zip(sigs[a], sigs[b]) if x != y) / len(sigs[a]) if diff < worst[0]: worst = (diff, (a, b)) if worst[0] < 0.08: fail.append("闸③灰度互辨最小错位 %.2f %s" % worst) if fail: for f in fail: print("[FAIL] 节点图标闸 %s" % f) raise SystemExit(1) print("[PASS] 节点图标复用闸①②③④(量化单色/禁入带天然合规/灰度互辨最小错位 %.2f %s/CC0)" % worst) if __name__ == "__main__": import sys sys.exit(main())