ThreeKingdomsSlingshot/Tools/class_symbols.py

92 lines
4.8 KiB
Python
Raw Permalink Normal View History

"""병과 단색 심볼 만들기(PD 2026-11-09 「병과 표시 = 기병 말 · 보병 검 · 궁병 활 · 창병 창 · 책사 깃털부채 심볼 · 예시 = 어두운 원 위 베이지 말머리」).
새로 그리지 않는다 — 확정 납품 메달 아이콘(Assets/UI/Samguk/class/cls_<병과>.png)을 만들 때 쓴 투명 바탕 사물 그림
(아트 R&D `C:/ArtRnD/samguk_ui/_work/class_symbol/rgba/cls_<병과>_s21A_rgba.png` · picked_auto.json = seed 21)의 투명도 = 형태.
기본 = 검은 바깥 테두리 + 그 안쪽 흰 채움 · 안쪽 선 없음(PD 2026-11-09 「배경 프레임 없이 · 검은 선 안쪽은 흰색 · 입체감이 느껴지지 않도록」 · --inner-lum 40 = 원본 안쪽 선 남김) · --mono = 단색(채움 한 색 · 안쪽 선 = 뚫린 선).
실행: <ComfyUI venv python> -I -B Tools/class_symbols.py [--src <rgba 폴더>] [--fill #RRGGBB] [--line #RRGGBB] [--out Assets/UI/Symbols] [--preview <png>]
출력 = <out>/cls_sym_<병과>.png(256 정사각 · 투명) · --preview = 어두운 원 위 5종 확인 그림.
"""
import argparse
import os
import numpy as np
from PIL import Image, ImageDraw, ImageFilter
TYPES = ["infantry", "spear", "cavalry", "archer", "strategist"]
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SRC = "C:/ArtRnD/samguk_ui/_work/class_symbol/rgba"
def hexrgb(s):
s = s.lstrip("#")
return tuple(int(s[i:i + 2], 16) for i in (0, 2, 4))
def symbol(path, fill, line, size=256, pad=14, stroke=8, inner_lum=0, erode_passes=6, mono=True):
src = Image.open(path).convert("RGBA")
bb = src.getchannel("A").point(lambda v: 255 if v > 40 else 0).getbbox()
src = src.crop(bb)
# 안쪽 선 = 원본 해상도에서 형태 안쪽(바깥 외곽선 두께 ≈ 30 px 보다 깊이)의 진한 픽셀 — 눈 · 갈기 · 칼날 경계 등
core = src.getchannel("A").point(lambda v: 255 if v > 128 else 0)
for _ in range(erode_passes):
core = core.filter(ImageFilter.MinFilter(11))
rgb = np.asarray(src.convert("RGB")).astype(np.float32)
lum = rgb[:, :, 0] * 0.299 + rgb[:, :, 1] * 0.587 + rgb[:, :, 2] * 0.114
inner = Image.fromarray((((np.asarray(core) > 127) & (lum < inner_lum)) * 255).astype(np.uint8))
s = (size - 2 * pad - 2 * stroke) / max(src.size)
dim = (max(1, round(src.width * s)), max(1, round(src.height * s)))
alpha = src.getchannel("A").resize(dim, Image.LANCZOS)
inner = inner.resize(dim, Image.LANCZOS)
canvas = Image.new("L", (size, size), 0)
off = ((size - dim[0]) // 2, (size - dim[1]) // 2)
canvas.paste(alpha, off)
lines = Image.new("L", (size, size), 0)
lines.paste(inner, off)
if mono:
# 단색(PD 2026-11-09 「배경 프레임 없이 단색」) = 채움 한 색 · 테두리 없음 · 안쪽 선 = 뚫린 선(형태 구분)
out = Image.new("RGBA", (size, size), fill + (255,))
a = np.asarray(canvas).astype(np.float32) * (1.0 - np.asarray(lines).astype(np.float32) / 255.0)
out.putalpha(Image.fromarray(a.clip(0, 255).astype(np.uint8)))
return out
out = Image.new("RGBA", (size, size), line + (0,))
out.putalpha(canvas.filter(ImageFilter.MaxFilter(stroke * 2 + 1))) # 바깥 테두리
body = Image.new("RGBA", (size, size), fill + (255,))
body.putalpha(canvas)
out.alpha_composite(body)
ink = Image.new("RGBA", (size, size), line + (255,))
ink.putalpha(lines)
out.alpha_composite(ink)
return out
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--src", default=SRC)
ap.add_argument("--seed", default="s21")
ap.add_argument("--fill", default="#FFFFFF")
ap.add_argument("--line", default="#1A1A22")
ap.add_argument("--out", default="Assets/UI/Symbols")
ap.add_argument("--preview", default="")
ap.add_argument("--inner-lum", type=float, default=0.0, help="안쪽 선(원본의 진한 선) 밝기 기준 · 0 = 안쪽 선 없음(기본 · 입체감 없이 평면)")
ap.add_argument("--mono", action="store_true", help="단색(채움 한 색 · 안쪽 선 = 뚫린 선 · 테두리 없음) · 기본 = 검은 선 + 흰 채움")
a = ap.parse_args()
out = os.path.join(ROOT, a.out)
os.makedirs(out, exist_ok=True)
tiles = []
for t in TYPES:
g = symbol(os.path.join(a.src, "cls_%s_%sA_rgba.png" % (t, a.seed)), hexrgb(a.fill), hexrgb(a.line), inner_lum=a.inner_lum, mono=a.mono)
g.save(os.path.join(out, "cls_sym_%s.png" % t))
tile = Image.new("RGBA", (300, 300), (0, 0, 0, 0))
tile.alpha_composite(g.resize((200, 200), Image.LANCZOS), (50, 50))
tiles.append(tile)
if a.preview:
prev = Image.new("RGBA", (300 * len(tiles), 300), (40, 42, 60, 255))
for i, t in enumerate(tiles):
prev.alpha_composite(t, (i * 300, 0))
prev.save(a.preview)
print("ok ->", out)
if __name__ == "__main__":
main()