Files
gplan-2/tools/palette_check.py

166 lines
6.5 KiB
Python
Raw Permalink 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.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""B2 色板检查器(圣经 §8.3①挂点实现)。
⚠⚠ 口径纪律(**勿改回**)——两处极易写错:
1. **48 色上限不是「每张 PNG 的色数」**。§4.1 的「全作色板硬上限 48 色」指
**设计色板 token**(UI 语义色/墨色/材质色那套),不是像素画的独立色数。
像素画单张另有**32 色冻结**口径,由 standee_asset_check 管(像素域)。
本器口径:单张色数**只报读数不判红**;48 只在 --tokens 模式下校验色板表。
2. **近色 ≥8/255 是「新色入板双门」,不是存量两两互查**。§4.1 原文:
「**新色**入板须同时过两道门……②与既有任一色的 RGB 三通道差 ≥8/255」。
它管的是**新增色 vs 既有色板**。对存量色两两互查会把 AI 生成的明暗渐变
全部打成近色(实测 drider 22 组 / 巨蜘蛛 82 组)⇒ 噪声淹没真问题。
本器口径:近色**只对新增色集**判(新增色 vs 基线色板)。
另:必须排除全透明像素 (0,0,0,0)(同standee_asset_check 的坑)。
基线(双门②「新增色 vs 既有色板」的既有色板机检承载物):
目录 = game/tests/golden/palette_baseline/,两张条带 PNG(1×N,一色一像素列):
ui_tokens.png = ui_kit.gd 全量 Color8 token 快照(token 板现役值)
art_snapshot.png = 现役素材全量唯一色快照(像素板+各屏环境子板)
两张并集=「既有色板」。缺一张即漏判——基线刷新必须重生成全套。
门①语义检查属设计面人工核对(§4.2 色相语义判域),不在本器执法面。
用法:
# 主用法(闸机色板闸承载):扫目录,新增色相对 --baseline 判定
python tools/palette_check.py <目录> --baseline <基线目录>
# 无基线:只出读数(单张色数/ 近色对计数),不判红
python tools/palette_check.py <目录>
# 基线建档与刷新:重扫现役素材∪token 重生成基线条带(素材批收尾动作,随批提交)
python tools/gen_palette_baseline.py
退出码: 0 通过 / 1 有红灯(仅新增色近色不过门②时)
"""
import os
import sys
from PIL import Image
# §4.1 硬上限(**设计色板 token 口径**,非像素画单张色数)
TOKEN_MAX = 48
NEAR_TOL = 8 # §4.1 新色双门:与既有任一色三通道差 ≥8/255
# 像素画单张色数(像素域32 色冻结)——由 standee_asset_check 执法,
# 本器只作读数提示,避免两处口径打架
PIXEL_COLOR_SOFT_MAX = 32
def palette_of(path):
"""返回 (不透明像素唯一色集合, 像素数)。排除全透明。"""
im = Image.open(path).convert("RGBA")
px = im.load()
w, h = im.size
cols = set()
n = 0
for y in range(h):
for x in range(w):
c = px[x, y]
if c[3] == 0:
continue
cols.add(c[:3])
n += 1
return cols, n
def nearest_to(col, pool, tol):
"""返回 col 在 pool 中最近的色及其差值(差 < tol 算近色)。"""
best, bd = None, 999
for c in pool:
d = max(abs(col[k] - c[k]) for k in range(3))
if d < bd:
best, bd = c, d
return best, bd
def hexs(c):
return "#%02X%02X%02X" % c
def scan(d, baseline_cols, report_only):
files = []
for dirpath, _, names in os.walk(d):
for fn in sorted(names):
if fn.lower().endswith(".png"):
files.append(os.path.join(dirpath, fn))
files.sort()
print("=== %s(%d 张 PNG)===" % (d, len(files)))
n_fail = 0
all_new = set()
for p in files:
cols, npx = palette_of(p)
rel = os.path.relpath(p, d)
notes = []
msgs = []
over = len(cols) > PIXEL_COLOR_SOFT_MAX
notes.append("色%d%s" % (len(cols), "(>32 像素域冻结)" if over else ""))
new = set()
if baseline_cols is not None:
new = cols - baseline_cols
if new:
# §4.1 新色双门②:新增色须与既有任一色差 ≥8/255
bad = []
for c in sorted(new):
near, d3 = nearest_to(c, baseline_cols, 999)
if d3 < NEAR_TOL:
bad.append((c, near, d3))
all_new |= new
notes.append("新增%d" % len(new))
for c, near, d3 in bad[:5]:
msgs.append("新增色 %s 与既有 %s 三通道差 %d < %d"
"(§4.1 新色双门②,须走换色合并)"
% (hexs(c), hexs(near), d3, NEAR_TOL))
if len(bad) > 5:
msgs.append("…… 另有 %d 个新增色同样不过门" % (len(bad) - 5))
else:
notes.append("新增0")
line = "FAIL " if msgs else ("WARN " if (report_only and over) else "PASS ")
print("%s %-46s %s" % (line, rel, " ".join(notes)))
for m in msgs:
print(" └─ %s" % m)
n_fail += 1 if msgs else 0
print("小计:%d 张,%d 张有红灯\n" % (len(files), n_fail))
return n_fail, len(files), all_new
def main():
args = sys.argv[1:]
baseline = args[args.index("--baseline") + 1] if "--baseline" in args else None
dirs = [a for a in args if not a.startswith("--")
and os.path.isdir(a) and a != baseline]
if not dirs:
print(__doc__)
sys.exit(2)
baseline_cols = None
if baseline:
baseline_cols = set()
for dirpath, _, names in os.walk(baseline):
for fn in names:
if fn.lower().endswith(".png"):
cols, _ = palette_of(os.path.join(dirpath, fn))
baseline_cols |= cols
print("基线色板:%s,唯一色 %d" % (baseline, len(baseline_cols)))
total_fail = total_files = 0
total_new = set()
report_only = baseline_cols is None
for d in dirs:
f, n, new = scan(d, baseline_cols, report_only)
total_fail += f
total_files += n
total_new |= new
print("色板机检完成:%d 张 PNG,%d 张有红灯,累计新增色 %d"
% (total_files, total_fail, len(total_new)))
if report_only:
print("提示:未给 --baseline ⇒ 本次为只读模式(不判红)。"
"『色数>32』标 WARN 属像素域提示,最终执法在 standee_asset_check。")
sys.exit(1 if total_fail else 0)
if __name__ == "__main__":
main()