lili 86df18c4d8
Some checks failed
contract-gates / contract-gates (push) Has been cancelled
docs-gate / docs-gate (push) Has been cancelled
feat(art): 山海风格版批次一——42 样张+产线三件真跑,创始人眼验拍锁锚开批产
矿彩国风锚 r1 定稿:《山海行纪》30 帧(hero×6/狰梼杌穷奇×15/法器×3/UI×2/场景/兽潮/Boss/拾取)+
《山海巡异录》12 张(卡面 3 稀有度×2 职业/敌兽 2/场景 2/结算 2);同名兽狰两款同谱面可辨=
「一条美术管线供两款」首检成立;clean-room 五条全程生效(prompt 零现代作品专名、兽形古籍演绎、
参考图仅自产、全量留痕 manifest.jsonl 含 12 张淘汰逐张原因)。
产线三件真跑演示:色键抠图+除杂(角落印章自动清)/图集 2 页 png 4006KB→webp 599KB(实测修正:
free-tex-packer 不直出 webp 补 PIL 转码)/字体子集 780 字→176KB woff2(LXGW OFL)。
成本良率实测:57 呼 ¥1.44(官方口径)/¥1.88(实测口径),首过率 34/42=81%,重摇≤4 轮填满;
工艺发现 7 条入批次 README(兽名不入 prompt/「鲛」prime 龙形/subject-ref 证伪/UI 伪字走程序排字等)。
创始人裁定(2026-07-07):锁锚开批产——两款按锚 r1 进 M3/M4 资产批产,主角跨帧漂移用
「定妆帧作参考图+关键姿态逐帧」工艺压,不换通道。台账:作战清单 M2 波全记账+_index 三行 M2 已交+设定档裁定戳。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 19:20:47 -07:00

107 lines
5.0 KiB
Python
Executable File

#!/usr/bin/env python3
# 产线件① 色键抠图:纯浅底生图 → 透明底精灵(PNG RGBA + webp q80)
# 用法: python3 key-out.py <输入.jpg> [更多输入...] [--out 输出目录] [--tol 28] [--soft 52] [--pad 8]
# 步骤:四角采样定底色 → 色距双阈值出 alpha(硬阈内=背景,软阈间=羽化)→ 连通域除杂
# (丢弃远离主体的小色块:印章/落款/尘点,mmx 国风生图的系统性杂项)→ 内容裁切+留边。
# 依赖:Pillow + numpy(pip --user 装);JPEG 边缘噪声由软阈羽化吸收。
import sys, os
from collections import deque
import numpy as np
from PIL import Image
def parse_args(argv):
ins, out, tol, soft, pad = [], None, 28.0, 52.0, 8
i = 0
while i < len(argv):
a = argv[i]
if a == '--out': out = argv[i+1]; i += 2
elif a == '--tol': tol = float(argv[i+1]); i += 2
elif a == '--soft': soft = float(argv[i+1]); i += 2
elif a == '--pad': pad = int(argv[i+1]); i += 2
else: ins.append(a); i += 1
if not ins:
sys.exit('用法: key-out.py <输入.jpg>... [--out 目录] [--tol 28] [--soft 52] [--pad 8]')
return ins, out, tol, soft, pad
def dilate(mask, it):
# 简易方块膨胀(无 scipy 依赖):把被丢弃域向外扩几圈,连羽化边一起清
m = mask.copy()
for _ in range(it):
m = m | np.roll(m, 1, 0) | np.roll(m, -1, 0) | np.roll(m, 1, 1) | np.roll(m, -1, 1)
return m
def key_one(path, out_dir, tol, soft, pad):
name = os.path.splitext(os.path.basename(path))[0]
im = Image.open(path).convert('RGB')
a = np.asarray(im, dtype=np.float32)
h, w, _ = a.shape
# ① 四角 12px 块中位数 = 背景色(生图约定纯浅底,四角必为底)
c = 12
corners = np.concatenate([a[:c, :c].reshape(-1, 3), a[:c, -c:].reshape(-1, 3),
a[-c:, :c].reshape(-1, 3), a[-c:, -c:].reshape(-1, 3)])
bg = np.median(corners, axis=0)
dist = np.sqrt(((a - bg) ** 2).sum(axis=2))
# ② 双阈值 alpha:<tol 纯背景,>soft 纯前景,之间线性羽化
alpha = np.clip((dist - tol) / max(soft - tol, 1e-6), 0.0, 1.0)
# ③ 连通域除杂:1/4 尺度上对"确定前景"BFS 标记,保主体域,丢远处小岛(印章/落款/尘点)
s = 4
fg = (dist > (tol + soft) / 2)[::s, ::s]
H, W = fg.shape
lab = np.zeros(fg.shape, dtype=np.int32)
sizes, boxes, nid = {}, {}, 0
for y in range(H):
for x in range(W):
if fg[y, x] and lab[y, x] == 0:
nid += 1
q = deque([(y, x)]); lab[y, x] = nid
n, y0, y1, x0, x1 = 0, y, y, x, x
while q:
cy, cx = q.popleft(); n += 1
if cy < y0: y0 = cy
if cy > y1: y1 = cy
if cx < x0: x0 = cx
if cx > x1: x1 = cx
for dy, dx in ((1, 0), (-1, 0), (0, 1), (0, -1)):
ny, nx = cy + dy, cx + dx
if 0 <= ny < H and 0 <= nx < W and fg[ny, nx] and lab[ny, nx] == 0:
lab[ny, nx] = nid; q.append((ny, nx))
sizes[nid], boxes[nid] = n, (y0, y1, x0, x1)
if not sizes:
print(f'[{name}] 警告:未检出前景,跳过'); return None
main = max(sizes, key=sizes.get)
my0, my1, mx0, mx1 = boxes[main]
grow = max(H, W) // 20 # 主体框外扩 5%:贴身小件(飘带/火星/链梢)保留
keep = set()
for i_, n in sizes.items():
by0, by1, bx0, bx1 = boxes[i_]
near = not (bx1 < mx0 - grow or bx0 > mx1 + grow or by1 < my0 - grow or by0 > my1 + grow)
if i_ == main or (near and n >= 0.03 * sizes[main]) or n >= 0.20 * sizes[main]:
keep.add(i_)
dropped = len(sizes) - len(keep)
drop_small = fg & ~np.isin(lab, list(keep))
drop_big = np.kron(dilate(drop_small, 2), np.ones((s, s), dtype=bool))[:h, :w]
alpha[drop_big] = 0.0
# ④ 内容裁切 + 留边
ys, xs = np.nonzero(alpha > 0.02)
if len(ys) == 0:
print(f'[{name}] 警告:除杂后无前景,跳过'); return None
y0, y1 = max(int(ys.min()) - pad, 0), min(int(ys.max()) + pad + 1, h)
x0, x1 = max(int(xs.min()) - pad, 0), min(int(xs.max()) + pad + 1, w)
rgba = np.dstack([a.astype(np.uint8), (alpha * 255).astype(np.uint8)])[y0:y1, x0:x1]
out = Image.fromarray(rgba, 'RGBA')
os.makedirs(out_dir, exist_ok=True)
p_png = os.path.join(out_dir, f'{name}.png')
p_webp = os.path.join(out_dir, f'{name}.webp')
out.save(p_png)
out.save(p_webp, quality=80, method=6)
kb = lambda p: os.path.getsize(p) / 1024
print(f'[{name}] 底色 rgb({int(bg[0])},{int(bg[1])},{int(bg[2])}) 连通域 {len(sizes)}{len(keep)} 丢杂 {dropped} '
f'{x1-x0}x{y1-y0} → png {kb(p_png):.0f}KB / webp {kb(p_webp):.0f}KB')
return p_png
if __name__ == '__main__':
ins, out, tol, soft, pad = parse_args(sys.argv[1:])
out = out or os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(ins[0]))), 'demo', 'keyed')
for p in ins:
key_one(p, out, tol, soft, pad)