矿彩国风锚 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>
107 lines
5.0 KiB
Python
Executable File
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)
|