92 lines
4.8 KiB
Python
92 lines
4.8 KiB
Python
"""병과 단색 심볼 만들기(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()
|