Files
gplan-2/tools/quantize_ui_skin.py
T

160 lines
7.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""UI 框材组量化改色脚本。
♻ 复用件 → 复用闸①色板量化:全部产物按明度重映射到 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 冻结形制;
# 战=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())