#!/usr/bin/env python3 """character-sheet-v1.png의 완성 흉상을 안전한 public neutral.png로 게시한다. 깨진 실험 파츠가 사용자 화면에 바로 노출되지 않도록, 앱 렌더 기본값은 완성 흉상 컷아웃을 사용한다. 세부 파츠 리깅은 별도 parts 실험 파일로 유지한다. """ from pathlib import Path from PIL import Image, ImageFilter import numpy as np ROOT = Path("D:/workspace/vignette") SHEET = ROOT / "docs/avatar-art/seoyeon/character-sheet-v1.png" PUB = ROOT / "apps/web/public/avatar/seoyeon" OUT = PUB / "neutral.png" CANVAS = (900, 1125) def remove_white_bg(im: Image.Image) -> Image.Image: arr = np.array(im.convert("RGBA")).astype(np.float32) rgb = arr[:, :, :3] border = np.concatenate( [ rgb[:6, :, :].reshape(-1, 3), rgb[-6:, :, :].reshape(-1, 3), rgb[:, :6, :].reshape(-1, 3), rgb[:, -6:, :].reshape(-1, 3), ], axis=0, ) bg = np.median(border, axis=0) dist_bg = np.sqrt(((rgb - bg.reshape(1, 1, 3)) ** 2).sum(axis=2)) dist_white = np.sqrt(((255.0 - rgb) ** 2).sum(axis=2)) alpha_bg = np.clip((dist_bg - 15.0) * 255.0 / (58.0 - 15.0), 0, 255) alpha_white = np.clip((dist_white - 18.0) * 255.0 / (58.0 - 18.0), 0, 255) alpha = np.minimum(alpha_bg, alpha_white) # Keep linework/hair/clothes solid once away from the paper background. chroma = rgb.max(axis=2) - rgb.min(axis=2) dark = rgb.max(axis=2) < 205 alpha = np.where((dark | (chroma > 18)) & (alpha > 60), np.maximum(alpha, 235), alpha) alpha = np.minimum(alpha, arr[:, :, 3]) arr[:, :, 3] = alpha arr[alpha < 1, :3] = 0 return Image.fromarray(arr.astype(np.uint8), "RGBA").filter(ImageFilter.GaussianBlur(0.12)) def main() -> int: sheet = Image.open(SHEET).convert("RGBA") # Left assembled bust from the generated parts sheet. crop = sheet.crop((0, 60, 570, 835)) cut = remove_white_bg(crop) cut.thumbnail((820, 1040), Image.Resampling.LANCZOS) canvas = Image.new("RGBA", CANVAS, (0, 0, 0, 0)) x = (CANVAS[0] - cut.width) // 2 y = 52 canvas.alpha_composite(cut, (x, y)) OUT.parent.mkdir(parents=True, exist_ok=True) canvas.save(OUT, optimize=True) for name in ["sad", "tired", "anxious", "warm", "startled", "eyes-closed", "speaking"]: canvas.save(PUB / f"{name}.png", optimize=True) print(f"published {OUT}") return 0 if __name__ == "__main__": raise SystemExit(main())