Files
gplan-2/tools/glow_discrimination_sheet.py
DKChan 8d170007be 工程+文档(三关色板终值冻结批): 互辨抽测闭合落文——art-bible v3.43 §6.3 三关两槽终值改指针式冻结(本节不抄色值字面量:终值=各关三块底型瓦件顶面受光档实测最亮值,wall_ambient 同关同槽、零新增色值;凭据句写明样图同哈希故目检面未重跑+§1.4 样张重跑 30/31 唯一红为改前基线缺位属在途素材区环境坑);§4.1 对比硬指标句按用户新立规矩去数值化(「未开格底 L≥0.30/饱和≤0.55/避禁入带」数值子句删,改述底槽-微光-已开格底相对亮度与相对饱和关系+指路执行件登记面);抽测执行件代表值口径订正(H2_VALUES 硬写的换色锚槽表值退役,改由 lit_slots() 从在役底型件像素按线性相对亮度实测;六道门值单点化为 FLOOR_L/CEIL_SAT/BANNED_HUE/NEAR_EPS/B_MIN_CH_DIFF/C_MODEL_TOL,run_checks 内两处内联 8/2 同批收编——本件即这些数值的唯一源);art-pipeline-tools.md 登记该件(选工具表一行+专节含命令/门值单点/坑);reports/读数表补 S3 目检判定表(四条全过)+S4 抽测通过结论;待办清单抽测条整条删除+新登记「美术圣经去数值化」条(状态待文档设计)。
本步凭据: 抽测件实跑 17 项 FAIL 0(dungeon 实测 #B49069 L=0.307 过门余量 0.007/underdark #BAC1D2 0.532/deep_underdark #969EBA 0.345)+样图与 2026-11-05 出图件逐字节同哈希(15d33fabb75256677254…,目检面未变)+§1.4 格子样张重跑 30/31 绿(唯一红 V10=在途素材区无改前基线件,拒用 --save-baseline 造「改前=改后」空对拍)+run_all_gates --docs 闸况与基线一致无新增红(流水线闸 PASS,条目 27;术语闸抓到「不得」两处已按 STC 词表改「禁止」)+目检口径记档(用户回话「没有问题,我觉得是可以分清楚的」,经追问四条覆盖范围答「四条全过」)。

订正备案: 本会话中代理一度判「dungeon 顶面受光档 L=0.235 不达圣经地板」并拟立案,系测量用 sRGB 加权亮度排序选像素、与该门线性域相对亮度口径不同尺度的失误;按件内口径实测 0.307 过门,未据此立任何条。
2026-11-06 06:18:59 +08:00

466 lines
20 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.
# 三关色相 × 五组微光互辨抽测——S1 取铺底值(机检 D)+ S2 出并列样图(机检 A/B/C)。
# 执行依据:reports/提案-三关色相互辨抽测方案-2026-10-29.md(抽测任务=待办清单
# 「三关色相 × 五组微光互辨抽测」条)。产物:并列样图(artwork/,不入 git)+逐对读数表(reports/)。
# 口径出处(冲突时以 owner 为准):
# 微光色值与呈现参数 = game/presentation/views/ui_kit.gd TG_GLOW_*/TYPE_GLOW_*(v2 冻结);
# 合成公式 = game/presentation/views/cell_type_glow.gdshader(blend_mix 距离场柔光);
# 带域/格心取样 = game/probe/type_glow_shot_probe.gd(de<格边×0.125 / 内缩 20%);
# 准入门值(亮度地板/饱和上限/禁入色相带/近色容差)单点在本件 FLOOR_L·CEIL_SAT·
# BANNED_HUE·NEAR_EPS;art-bible §4.1 只述「未开格底与微光存在相对亮度关系」并指路本件。
# 既有色板比对池 = game/tests/golden/palette_baseline/ 两张条带并集(palette_check.py 同源)。
# 方案 §2.3 机检 C 口径订正:模型 0.49/0.51 是反解近似(ui_kit.gd 头注「模型域与实测的差
# = shader 距离场权重非扁平混合」),比较对象改为 shader 精确合成模型(逐像素复刻),判据线
# ±2/255 沿用在役值。
# 方案 §7 表二行数笔误订正:表二实为 90 行(3 关 × 3 底型 × 10 组对),非 30 行。
#
# 用法:
# python tools/glow_discrimination_sheet.py --art <artwork/mockups 目录> [--no-sheet]
# 输出:
# <art>/glow-discrimination-sheet-2026-11-05.png(样图:正图段+灰度副本段+图例)
# reports/读数-三关色相互辨抽测-2026-11-05.md(逐对表 45 行+组间表 90 行+机检汇总)
import argparse
import math
import os
import sys
from PIL import Image
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# ── ui_kit.gd 冻结常量(逐字转写,禁改值)──
GLOWS = [
("BUFF", (247, 223, 67)), # 通用增益·芥末黄
("HEAL", (104, 208, 66)), # 治疗·黄绿
("PIERCE", (48, 166, 166)), # 穿刺·青
("MAGIC", (123, 91, 214)), # 魔法·紫罗兰
("AOE", (166, 29, 55)), # 群伤·绯红
]
GLOW_ALPHA = 170.0 / 255.0
BAND_FRAC = 0.125
SOFT = 0.55
CORNER = 2.0
CELL = 45 # D 区实比格边
BAND = CELL * BAND_FRAC # 5.625
CORE_PAD = int(CELL * 0.20) # 格心内缩(探针 CORE_PAD_FRAC=0.20)
# ── 铺底准入门值单点(文档侧只述「未开格底与微光存在相对亮度关系」,数值归本件)──
FLOOR_L = 0.30 # 未开格底相对亮度地板(线性域)
CEIL_SAT = 0.55 # 饱和度上限
BANNED_HUE = (300.0, 345.0) # 禁入色相带(Boss 紫赤独占)
NEAR_EPS = 8 # 近色容差(三通道差全 <此值 即近色)
B_MIN_CH_DIFF = 8 # 机检 B|格底-微光可辨底线(三通道至少一通道 ≥此值)
C_MODEL_TOL = 2 # 机检 C|样图与合成模型容差(±此值 /255)
BOARD_TYPES = ["dungeon", "underdark", "deep_underdark"]
BASE_VARIANTS = [ # (底型名,瓦片件名后缀)——素砖 + 两种装饰变体(高墙瓦片词表在册件)
("素砖", "terrain_wall_center"),
("裂纹", "terrain_wall_center_crack"),
("碎石斑", "terrain_wall_center_speckle"),
]
TILE_DIR = os.path.join(REPO, "game", "assets", "art", "board")
BASELINE_DIR = os.path.join(REPO, "game", "tests", "golden", "palette_baseline")
CHECKS = []
def check(cid, name, ok, detail=""):
CHECKS.append((cid, name, bool(ok), detail))
print("[%s] %s %s — %s%s" % ("PASS" if ok else "FAIL", cid, name, "✓" if ok else "✗",
(";" + detail) if detail else ""))
# ── 色彩数学 ──
def to_linear(v):
c = v / 255.0
return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
def rel_lum(rgb):
return 0.2126 * to_linear(rgb[0]) + 0.7152 * to_linear(rgb[1]) + 0.0722 * to_linear(rgb[2])
def gray709(rgb):
return 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2]
def hsv(rgb):
r, g, b = [v / 255.0 for v in rgb]
mx, mn = max(r, g, b), min(r, g, b)
d = mx - mn
if d == 0:
h = 0.0
elif mx == r:
h = 60 * ((g - b) / d % 6)
elif mx == g:
h = 60 * ((b - r) / d + 2)
else:
h = 60 * ((r - g) / d + 4)
s = 0.0 if mx == 0 else d / mx
return h % 360, s
def wcag(c1, c2):
l1, l2 = rel_lum(c1), rel_lum(c2)
return (max(l1, l2) + 0.05) / (min(l1, l2) + 0.05)
def smoothstep(e0, e1, x):
t = max(0.0, min(1.0, (x - e0) / (e1 - e0)))
return t * t * (3.0 - 2.0 * t)
def glow_weight(px, py):
"""shader 复刻:像素中心 (px,py) 的微光权重 = glow_alpha × a(d) × mask。"""
h = CELL / 2.0
d = min(px, py, CELL - px, CELL - py)
r = min(CORNER, CELL * 0.25)
qx = abs(px - h) - (h - r)
qy = abs(py - h) - (h - r)
outside = math.hypot(max(qx, 0.0), max(qy, 0.0)) + min(max(qx, qy), 0.0) - r
mask = 1.0 - smoothstep(-1.0, 0.5, outside)
a = 0.0
if d < BAND:
a = 1.0 - smoothstep(SOFT, 1.0, d / BAND)
return GLOW_ALPHA * a * mask
# ── 素材装载 ──
def load_tiles():
tiles = {}
for bt in BOARD_TYPES:
for vn, stem in BASE_VARIANTS:
path = os.path.join(TILE_DIR, "%s_%s.png" % (stem, bt))
if not os.path.exists(path):
print("[阻塞] 素材缺位:%s" % path, file=sys.stderr)
sys.exit(2)
img = Image.open(path).convert("RGB")
if img.size != (CELL, CELL):
img = img.resize((CELL, CELL), Image.NEAREST)
tiles[(bt, vn)] = img.load()
return tiles
def lit_slots(tiles):
"""铺底代表值 = 该关三块底型瓦片像素里线性亮度最大的实测色(顶面受光档)。
零表值:不引用任何色表常量,素材换色后读数自动跟随。"""
slots = {}
for bt in BOARD_TYPES:
best = None
for vn, _ in BASE_VARIANTS:
px = tiles[(bt, vn)]
for y in range(CELL):
for x in range(CELL):
c = px[x, y]
if best is None or rel_lum(c) > rel_lum(best):
best = c
slots[bt] = best
return slots
# ── 带域/格心取样(探针口径)──
def band_pixels():
"""带域像素集(整数格坐标 de < 格边×0.125,type_glow_shot_probe 口径)。"""
return [(x, y) for x in range(CELL) for y in range(CELL)
if min(x, y, CELL - 1 - x, CELL - 1 - y) < BAND]
def core_pixels():
"""格心像素集(内缩 20%)。"""
return [(x, y) for x in range(CELL) for y in range(CELL)
if min(x, y, CELL - 1 - x, CELL - 1 - y) >= CELL * 0.20]
BAND_PX = band_pixels()
CORE_PX = core_pixels()
def mean_rgb(px, pts):
n = len(pts)
return tuple(round(sum(px[x, y][i] for x, y in pts) / n) for i in range(3))
def model_band_mean(base_px, glow_rgb):
"""shader 精确合成模型:逐像素 blend_mix 理论带内均值(独立实现路径)。"""
n = len(BAND_PX)
acc = [0.0, 0.0, 0.0]
for x, y in BAND_PX:
w = glow_weight(x + 0.5, y + 0.5)
b = base_px[x, y]
for i in range(3):
acc[i] += w * glow_rgb[i] + (1.0 - w) * b[i]
return tuple(round(v / n) for v in acc)
# ── 机检 D:铺底代表值五门 ──
def load_baseline_palette():
cols = set()
for fn in ["ui_tokens.png", "art_snapshot.png"]:
p = os.path.join(BASELINE_DIR, fn)
img = Image.open(p).convert("RGB")
for x in range(img.width):
for y in range(img.height):
cols.add(img.getpixel((x, y)))
return sorted(cols)
def gate_d(tiles):
baseline = load_baseline_palette()
lit = lit_slots(tiles)
rows = []
all_pass = True
for bt in BOARD_TYPES:
rgb = lit[bt]
h, s = hsv(rgb)
lin = rel_lum(rgb)
l_ok = lin >= FLOOR_L
s_ok = s <= CEIL_SAT
hue_ok = not (BANNED_HUE[0] <= h <= BANNED_HUE[1])
# 近色门:三值=在役素材色(已在基线池内),palette_check 双门②只判新增色——报读数不判红
near = [b for b in baseline if all(abs(rgb[i] - b[i]) < NEAR_EPS for i in range(3))]
ok = l_ok and s_ok and hue_ok
all_pass = all_pass and ok
rows.append((bt, rgb, lin, s, h, len(near), ok))
check("D-" + bt, "铺底代表值门(底型瓦片实测顶面受光档;亮度/饱和/禁入带,近色只报读数)", ok,
"RGB=%s L=%.3f 饱和=%.3f 色相=%.1f° 近色对=%d(在役素材色,非新增)%s%s%s"
% (rgb, lin, s, h, len(near),
"" if l_ok else " [L红]", "" if s_ok else " [饱和红]",
"" if hue_ok else " [禁入带红]"))
# 语义检查=设计面人工核对(palette_check 同口径),打印读数不判红
print(" [语义] %s=%s 环境域(不占红/金/蓝/紫赤/绿语义段)——人工核对" % (bt, rgb))
# 渲染面带域均值 = 读数不判红(方案机检 D 只判槽值;本读数供观感定性参考)
for bt in BOARD_TYPES:
for vn, _ in BASE_VARIANTS:
m = mean_rgb(tiles[(bt, vn)], BAND_PX)
lin = rel_lum(m)
print(" [读数] 渲染面带域均值 %s/%s RGB=%s L=%.3f(不判红)" % (bt, vn, m, lin))
return all_pass, rows
# ── 样图绘制 ──
PAGE_W, PAGE_H = 640, 1120
MARGIN, HEAD, BLOCK_TITLE, GAP, LEGEND = 10, 60, 20, 30, 80
def draw_sheet(out_path):
import argparse # noqa: F401
from PIL import ImageDraw, ImageFont
img = Image.new("RGB", (PAGE_W, PAGE_H), (255, 255, 255))
d = ImageDraw.Draw(img)
try:
f_zh = ImageFont.truetype("C:/Windows/Fonts/msyh.ttc", 13)
f_zh_s = ImageFont.truetype("C:/Windows/Fonts/msyh.ttc", 11)
except OSError:
f_zh = ImageFont.load_default()
f_zh_s = f_zh
blocks = []
y = MARGIN + HEAD
for bt in BOARD_TYPES:
d.text((MARGIN + 4, y + 3), "%s —— 列0=基准格,列1–5=五组微光(阶梯序)" % bt, fill=(0, 0, 0), font=f_zh)
y += BLOCK_TITLE
blocks.append((bt, y))
y += 3 * CELL
tiles = load_tiles()
# 画正图段 54 格
cell_pngs = {}
for bt, y0 in blocks:
for ri, (vn, _) in enumerate(BASE_VARIANTS):
for ci, gname in enumerate(["BASE"] + [g[0] for g in GLOWS]):
base = tiles[(bt, vn)]
cell = Image.new("RGB", (CELL, CELL))
cp = cell.load()
if gname == "BASE":
for x in range(CELL):
for yy in range(CELL):
cp[x, yy] = base[x, yy]
else:
glow = dict(GLOWS)[gname]
for x in range(CELL):
for yy in range(CELL):
w = glow_weight(x + 0.5, yy + 0.5)
b = base[x, yy]
cp[x, yy] = tuple(round(w * glow[i] + (1 - w) * b[i]) for i in range(3))
gx = MARGIN + 170 + ci * CELL
gy = y0 + ri * CELL
img.paste(cell, (gx, gy))
cell_pngs[(bt, vn, gname, "main")] = (gx, gy)
# 行标注
for ri, (vn, _) in enumerate(BASE_VARIANTS):
d.text((MARGIN, y0 + ri * CELL + 16), vn, fill=(0, 0, 0), font=f_zh_s)
# 灰度副本段(等幅面,正图段整段 Rec.709 去色)
main_top = MARGIN + HEAD
main_bot = y
gray_top = main_bot + GAP
for xx in range(MARGIN, PAGE_W - MARGIN):
for yy in range(main_top, main_bot):
r, g, b = img.getpixel((xx, yy))
v = int(round(gray709((r, g, b))))
img.putpixel((xx, gray_top + (yy - main_top)), (v, v, v))
d.text((MARGIN + 4, gray_top - 2), "灰度副本段(Rec.709 去色——色弱通道目检面)", fill=(80, 80, 80), font=f_zh_s)
# 图例段
leg_y = gray_top + (main_bot - main_top) + 10
d.text((MARGIN + 4, leg_y), "图例(五组冻结色值 ui_kit.gd TG_GLOW_*;阶梯序=合成空间亮度递减)", fill=(0, 0, 0), font=f_zh_s)
for i, (gn, rgb) in enumerate(GLOWS):
x0 = MARGIN + 170 + i * (CELL + 4)
d.rectangle([x0, leg_y + 18, x0 + CELL, leg_y + 18 + 28], fill=rgb, outline=(0, 0, 0))
d.text((x0, leg_y + 48), gn, fill=(0, 0, 0), font=f_zh_s)
img.save(out_path)
return img, cell_pngs, tiles, blocks, gray_top, main_top, main_bot
# ── 机检 A/B/C + 灰度/净空机检 ──
def run_checks(img, cell_pngs, tiles, blocks, gray_top, main_top, main_bot):
sheet_px = img.load()
table1 = [] # 45 行逐对表
table2 = [] # 90 行组间表
band_means = {}
fail_b, fail_c = [], []
lit = lit_slots(tiles)
for bt, y0 in blocks:
for vn, _ in BASE_VARIANTS:
# 列 0 基准格带域均值 = 该底型的模型底值
gx0, gy0 = cell_pngs[(bt, vn, "BASE", "main")]
band_base = mean_rgb(tiles[(bt, vn)], BAND_PX)
for gi, (gname, glow) in enumerate(GLOWS):
gx, gy = cell_pngs[(bt, vn, gname, "main")]
px = tiles[(bt, vn)]
# 样图带内均值(样图渲染面读数)
n = len(BAND_PX)
acc = [0.0, 0.0, 0.0]
for x, yy in BAND_PX:
p = sheet_px[gx + x, gy + yy]
for i in range(3):
acc[i] += p[i]
band_mean = tuple(round(v / n) for v in acc)
core_mean = mean_rgb(px, CORE_PX)
model = model_band_mean(px, glow)
diff_model = max(abs(band_mean[i] - model[i]) for i in range(3))
ch_diff = [abs(band_mean[i] - core_mean[i]) for i in range(3)]
b_ok = max(ch_diff) >= B_MIN_CH_DIFF
c_ok = diff_model <= C_MODEL_TOL
if not b_ok:
fail_b.append("%s/%s/%s" % (bt, vn, gname))
if not c_ok:
fail_c.append("%s/%s/%s" % (bt, vn, gname))
hue_gap = abs(hsv(lit[bt])[0] - hsv(glow)[0])
hue_gap = min(hue_gap, 360 - hue_gap)
table1.append((bt, vn, gname, band_mean, core_mean, model,
diff_model, ch_diff, hue_gap, b_ok and c_ok))
band_means[(bt, vn, gname)] = band_mean
# 机检 A:组间表 90 行
for bt, y0 in blocks:
for vn, _ in BASE_VARIANTS:
for i in range(len(GLOWS)):
for j in range(i + 1, len(GLOWS)):
a, b = band_means[(bt, vn, GLOWS[i][0])], band_means[(bt, vn, GLOWS[j][0])]
cr = wcag(a, b)
ok = cr >= 1.20
table2.append((bt, vn, "%s/%s" % (GLOWS[i][0], GLOWS[j][0]),
cr, gray709(a), gray709(b), ok))
stair_ok = True
for bt, y0 in blocks:
for vn, _ in BASE_VARIANTS:
seq = [gray709(band_means[(bt, vn, GLOWS[k][0])]) for k in range(len(GLOWS))]
mono = all(seq[k] > seq[k + 1] for k in range(len(seq) - 1))
stair_ok = stair_ok and mono
check("A-stair-%s-%s" % (bt, vn[:4]), "明度阶梯单调递减", mono,
"→".join("%.3f" % v for v in seq))
min_cr = min(t[3] for t in table2)
worst = min(table2, key=lambda t: t[3])
check("A-CR", "组间合成对比最弱对 ≥1.20(90 对)", min_cr >= 1.20,
"最弱 %s/%s CR=%.3f" % (worst[0], worst[2], min_cr))
check("B", "格底-微光可辨底线(45 行三通道差不全<8)", len(fail_b) == 0,
"违例 %d 行:%s" % (len(fail_b), ", ".join(fail_b[:6]) or "无"))
check("C", "绘制参数校准(45 行模型差≤±2/255)", len(fail_c) == 0,
"违例 %d 行:%s" % (len(fail_c), ", ".join(fail_c[:6]) or "无"))
# 灰度段零彩色像素
color_hit = 0
for xx in range(MARGIN, PAGE_W - MARGIN):
for yy in range(gray_top + BLOCK_TITLE, gray_top + (main_bot - main_top)):
r, g, b = sheet_px[xx, yy]
if not (r == g == b):
color_hit += 1
check("GRAY", "灰度副本段零彩色像素", color_hit == 0, "命中 %d" % color_hit)
# 四缘净空(A5 同款)
edge_hit = 0
for xx in range(PAGE_W):
for yy in range(PAGE_H):
if xx < MARGIN or xx >= PAGE_W - MARGIN or yy < MARGIN or yy >= PAGE_H - MARGIN:
if sheet_px[xx, yy] != (255, 255, 255):
edge_hit += 1
check("EDGE", "四缘 10px 带内零内容像素", edge_hit == 0, "命中 %d" % edge_hit)
return table1, table2
def write_report(table1, table2, d_rows):
rep = os.path.join(REPO, "reports", "读数-三关色相互辨抽测-2026-11-05.md")
lines = [
"# 读数:三关色相 × 五组微光互辨抽测(2026-11-05)",
"",
"> 接收方:待办清单「三关色相 × 五组微光互辨抽测——三关图类型色板终值冻结的前置抽测」条。",
"> 执行依据:reports/提案-三关色相互辨抽测方案-2026-10-29.md(S1/S2 步)。",
"> 样图:artwork/mockups/glow-discrimination-sheet-2026-11-05.png(不入 git)。",
"",
"## 机检汇总",
"",
"| 项 | 判定 | 说明 |",
"|---|---|---|",
]
for cid, name, ok, detail in CHECKS:
lines.append("| %s | %s | %s:%s |" % (cid, "PASS" if ok else "FAIL", name, detail))
lines += ["", "## S1 机检 D:铺底代表值(底型瓦片实测顶面受光档)五门", "",
"| 关 | RGB | L(线性域) | 饱和 | 色相 | 近色对 | 判定 |", "|---|---|---|---|---|---|---|"]
for bt, rgb, lin, s, h, near, ok in d_rows:
lines.append("| %s | %s | %.3f | %.3f | %.1f° | %d | %s |" % (bt, rgb, lin, s, h, near, "过" if ok else "红"))
lines += ["", "## 表一 逐对表(45 行)", "",
"| 关 | 底型 | 微光组 | 带内合成均值 | 格心素底均值 | 模型带内均值 | 模型差 | 三通道差 | 色相差° | 判定 |",
"|---|---|---|---|---|---|---|---|---|---|"]
for bt, vn, gn, bm, cm, md, dm, cd, hg, ok in table1:
lines.append("| %s | %s | %s | %s | %s | %s | %d | %s | %.0f | %s |"
% (bt, vn, gn, bm, cm, md, dm, cd, hg, "过" if ok else "红"))
lines += ["", "## 表二 组间表(90 行)", "",
"| 关 | 底型 | 组对 | 合成对比 | 灰度亮度a | 灰度亮度b | 判定 |", "|---|---|---|---|---|---|---|"]
for bt, vn, pair, cr, ga, gb, ok in table2:
lines.append("| %s | %s | %s | %.3f | %.3f | %.3f | %s |" % (bt, vn, pair, cr, ga, gb, "过" if ok else "红"))
n_fail = sum(1 for c in CHECKS if not c[2])
lines += ["", "---", "", "机检 %d 项,FAIL %d 项。" % (len(CHECKS), n_fail),
"", "目检(S3)归用户:四条判据见方案 §2.2(逐对指认 45/45/灰度副本 45/45/相邻并列 36 对可辨/装饰不抢读)。", ""]
with open(rep, "w", encoding="utf-8") as f:
f.write("\n".join(lines))
print("[报告] %s" % rep)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--art", default=os.path.join(REPO, "artwork", "mockups"))
ap.add_argument("--no-sheet", action="store_true")
args = ap.parse_args()
tiles = load_tiles()
print("== S1 机检 D:三关铺底代表值 ==")
d_ok, d_rows = gate_d(tiles)
if args.no_sheet:
return 0 if d_ok else 1
os.makedirs(args.art, exist_ok=True)
out = os.path.join(args.art, "glow-discrimination-sheet-2026-11-05.png")
print("== S2 出并列样图 ==")
img, cell_pngs, tiles2, blocks, gray_top, main_top, main_bot = draw_sheet(out)
print("[样图] %s" % out)
t1, t2 = run_checks(img, cell_pngs, tiles2, blocks, gray_top, main_top, main_bot)
write_report(t1, t2, d_rows)
n_fail = sum(1 for c in CHECKS if not c[2])
print("== 抽测机检完成:%d 项,FAIL %d ==" % (len(CHECKS), n_fail))
return 0 if n_fail == 0 else 1
if __name__ == "__main__":
sys.exit(main())