from __future__ import annotations import json from pathlib import Path from PIL import Image, ImageDraw, ImageFilter ROOT = Path(r"D:\Work_AI\Dansori") WORK = ROOT / "Characters_Build_Docs" SRC = WORK / "tmp" / "imagegen" / "leesori_v3_source_alpha.png" APP_ASSETS = ROOT / "DansoriEQ" / "src" / "DansoriEQ.App" / "Assets" PUPPET = APP_ASSETS / "Characters" / "Puppets" / "LeeSoriV3" IMAGES = PUPPET / "Images" PREVIEW = APP_ASSETS / "Characters" / "Live2DPreview" / "leesori.png" def alpha_bbox(im: Image.Image) -> tuple[int, int, int, int]: return im.getchannel("A").getbbox() or (0, 0, im.width, im.height) def prepare_source() -> Image.Image: src = Image.open(SRC).convert("RGBA") src = src.crop(alpha_bbox(src)) target_h = 1120 target_w = int(round(src.width * target_h / src.height)) src = src.resize((target_w, target_h), Image.Resampling.LANCZOS) margin = 44 out = Image.new("RGBA", (target_w + margin * 2, target_h + margin * 2), (0, 0, 0, 0)) out.alpha_composite(src, (margin, margin)) return out def mask_from_polygon(size: tuple[int, int], points: list[tuple[float, float]], blur: float = 1.0) -> Image.Image: w, h = size pts = [(int(x * w), int(y * h)) for x, y in points] mask = Image.new("L", size, 0) ImageDraw.Draw(mask).polygon(pts, fill=255) if blur: mask = mask.filter(ImageFilter.GaussianBlur(blur)) return mask def make_part(src: Image.Image, name: str, points: list[tuple[float, float]], blur: float = 1.0) -> None: mask = mask_from_polygon(src.size, points, blur) part = src.copy() alpha = Image.composite(part.getchannel("A"), Image.new("L", src.size, 0), mask) part.putalpha(alpha) part.save(IMAGES / name) def composite_black(src: Image.Image, parts: list[str], dest: Path, width: int = 390, height: int = 600) -> None: canvas = Image.new("RGBA", (width, height), (16, 16, 18, 255)) scale = min(width * 0.89 / src.width, height * 1.12 / src.height) target = (int(src.width * scale), int(src.height * scale)) x = (width - target[0]) // 2 y = int(height - target[1] + 58) for part_name in parts: part = Image.open(IMAGES / part_name).convert("RGBA").resize(target, Image.Resampling.LANCZOS) canvas.alpha_composite(part, (x, y)) canvas.convert("RGB").save(dest) def save_preview(src: Image.Image) -> None: preview = Image.new("RGBA", (512, 768), (0, 0, 0, 0)) scale = min(512 * 0.82 / src.width, 768 * 1.04 / src.height) target = (int(src.width * scale), int(src.height * scale)) resized = src.resize(target, Image.Resampling.LANCZOS) preview.alpha_composite(resized, ((512 - target[0]) // 2, 768 - target[1] + 36)) preview.save(PREVIEW) def main() -> None: IMAGES.mkdir(parents=True, exist_ok=True) src = prepare_source() w, h = src.size (PUPPET / "leesori_v3_source.png").parent.mkdir(parents=True, exist_ok=True) src.save(PUPPET / "leesori_v3_source.png") src.save(IMAGES / "leesori_v3_base.png") # Tight masks: moving parts avoid the crop-top hem, waist skin, and pelvis. make_part(src, "leesori_v3_head.png", [(0.27, 0.00), (0.73, 0.00), (0.74, 0.19), (0.64, 0.225), (0.36, 0.225), (0.26, 0.19)], blur=0.45) make_part(src, "leesori_v3_chest.png", [(0.27, 0.18), (0.73, 0.18), (0.70, 0.315), (0.30, 0.315)], blur=0.25) make_part(src, "leesori_v3_pelvis.png", [(0.30, 0.38), (0.70, 0.38), (0.73, 0.56), (0.27, 0.56)], blur=0.8) make_part(src, "leesori_v3_upperarm_l.png", [(0.15, 0.23), (0.35, 0.22), (0.31, 0.44), (0.14, 0.48), (0.09, 0.36)], blur=0.9) make_part(src, "leesori_v3_forearm_l.png", [(0.12, 0.42), (0.31, 0.40), (0.24, 0.62), (0.05, 0.64)], blur=0.9) make_part(src, "leesori_v3_hand_l.png", [(0.03, 0.58), (0.25, 0.57), (0.22, 0.72), (0.00, 0.72)], blur=0.7) make_part(src, "leesori_v3_upperarm_r.png", [(0.65, 0.22), (0.85, 0.23), (0.91, 0.36), (0.86, 0.48), (0.69, 0.44)], blur=0.9) make_part(src, "leesori_v3_forearm_r.png", [(0.69, 0.40), (0.88, 0.42), (0.95, 0.64), (0.76, 0.62)], blur=0.9) make_part(src, "leesori_v3_hand_r.png", [(0.75, 0.57), (0.97, 0.58), (1.00, 0.72), (0.78, 0.72)], blur=0.7) make_part(src, "leesori_v3_thigh_l.png", [(0.27, 0.51), (0.50, 0.51), (0.48, 0.76), (0.27, 0.78)], blur=0.8) make_part(src, "leesori_v3_shin_l.png", [(0.27, 0.73), (0.48, 0.73), (0.47, 0.93), (0.30, 0.93)], blur=0.8) make_part(src, "leesori_v3_foot_l.png", [(0.27, 0.90), (0.50, 0.90), (0.49, 1.00), (0.24, 1.00)], blur=0.6) make_part(src, "leesori_v3_thigh_r.png", [(0.50, 0.51), (0.73, 0.51), (0.73, 0.78), (0.52, 0.76)], blur=0.8) make_part(src, "leesori_v3_shin_r.png", [(0.52, 0.73), (0.73, 0.73), (0.70, 0.93), (0.53, 0.93)], blur=0.8) make_part(src, "leesori_v3_foot_r.png", [(0.50, 0.90), (0.73, 0.90), (0.76, 1.00), (0.51, 1.00)], blur=0.6) rig = { "name": "LeeSoriV3", "status": "leesori_v3_new_art_full_rig_v1", "canvas": {"width": w, "height": h}, "imageBase": "./Images/", "note": "New generated source art with ordinary waist line and restored full limb rig. Moving masks are kept away from crop-top hem and waist skin.", "bones": [ {"name": "base", "parent": None, "pivot": [w * 0.50, h * 0.58], "z": 0, "image": "leesori_v3_base.png"}, {"name": "pelvis", "parent": "base", "pivot": [w * 0.50, h * 0.48], "z": 2, "image": "leesori_v3_pelvis.png"}, {"name": "chest", "parent": "base", "pivot": [w * 0.50, h * 0.30], "z": 3, "image": "leesori_v3_chest.png"}, {"name": "upperarm_l", "parent": "chest", "pivot": [w * 0.30, h * 0.25], "z": 4, "image": "leesori_v3_upperarm_l.png"}, {"name": "forearm_l", "parent": "upperarm_l", "pivot": [w * 0.20, h * 0.45], "z": 5, "image": "leesori_v3_forearm_l.png"}, {"name": "hand_l", "parent": "forearm_l", "pivot": [w * 0.13, h * 0.61], "z": 6, "image": "leesori_v3_hand_l.png"}, {"name": "upperarm_r", "parent": "chest", "pivot": [w * 0.70, h * 0.25], "z": 4, "image": "leesori_v3_upperarm_r.png"}, {"name": "forearm_r", "parent": "upperarm_r", "pivot": [w * 0.80, h * 0.45], "z": 5, "image": "leesori_v3_forearm_r.png"}, {"name": "hand_r", "parent": "forearm_r", "pivot": [w * 0.87, h * 0.61], "z": 6, "image": "leesori_v3_hand_r.png"}, {"name": "thigh_l", "parent": "pelvis", "pivot": [w * 0.40, h * 0.54], "z": 2, "image": "leesori_v3_thigh_l.png"}, {"name": "shin_l", "parent": "thigh_l", "pivot": [w * 0.39, h * 0.75], "z": 2, "image": "leesori_v3_shin_l.png"}, {"name": "foot_l", "parent": "shin_l", "pivot": [w * 0.39, h * 0.92], "z": 2, "image": "leesori_v3_foot_l.png"}, {"name": "thigh_r", "parent": "pelvis", "pivot": [w * 0.60, h * 0.54], "z": 2, "image": "leesori_v3_thigh_r.png"}, {"name": "shin_r", "parent": "thigh_r", "pivot": [w * 0.61, h * 0.75], "z": 2, "image": "leesori_v3_shin_r.png"}, {"name": "foot_r", "parent": "shin_r", "pivot": [w * 0.61, h * 0.92], "z": 2, "image": "leesori_v3_foot_r.png"}, {"name": "head", "parent": "chest", "pivot": [w * 0.50, h * 0.16], "z": 10, "image": "leesori_v3_head.png"}, ], } (PUPPET / "rig.json").write_text(json.dumps(rig, ensure_ascii=False, indent=2), encoding="utf-8") save_preview(src) composite_black(src, [bone["image"] for bone in rig["bones"]], PUPPET / "qa_view_390x600.png") black = Image.new("RGBA", src.size, (16, 16, 18, 255)) black.alpha_composite(src) black.convert("RGB").save(PUPPET / "qa_source_black.png") if __name__ == "__main__": main()