Initial Dansori character workspace
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFilter
|
||||
|
||||
|
||||
ROOT = Path(r"D:\Work_AI\Dansori")
|
||||
SHEET = ROOT / "Characters_Build_Docs" / "LeeSori_Profile" / "03_Assets" / "Reference" / "sori_sheet.png"
|
||||
APP_ASSETS = ROOT / "DansoriEQ" / "src" / "DansoriEQ.App" / "Assets"
|
||||
PUPPET = APP_ASSETS / "Characters" / "Puppets" / "LeeSoriV2"
|
||||
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 clean_alpha(im: Image.Image) -> Image.Image:
|
||||
im = im.convert("RGBA")
|
||||
r, g, b, a = im.split()
|
||||
# Remove very dark residual antialias pixels from the black sheet background.
|
||||
pixels = im.load()
|
||||
for y in range(im.height):
|
||||
for x in range(im.width):
|
||||
rr, gg, bb, aa = pixels[x, y]
|
||||
if aa == 0:
|
||||
continue
|
||||
if max(rr, gg, bb) < 18:
|
||||
pixels[x, y] = (0, 0, 0, 0)
|
||||
elif max(rr, gg, bb) < 44 and aa < 255:
|
||||
pixels[x, y] = (rr, gg, bb, max(0, aa - 80))
|
||||
return im
|
||||
|
||||
|
||||
def crop_front_pose(sheet: Image.Image) -> Image.Image:
|
||||
# The new sheet's front full-body pose is the leftmost character.
|
||||
crop = sheet.crop((30, 30, 365, 1038))
|
||||
crop = clean_alpha(crop)
|
||||
crop = crop.crop(alpha_bbox(crop))
|
||||
|
||||
margin = 36
|
||||
out = Image.new("RGBA", (crop.width + margin * 2, crop.height + margin * 2), (0, 0, 0, 0))
|
||||
out.alpha_composite(crop, (margin, margin))
|
||||
return out
|
||||
|
||||
|
||||
def mask_from_polygon(size: tuple[int, int], points: list[tuple[float, float]], blur: float = 3.5) -> 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 = 3.5) -> None:
|
||||
part = Image.new("RGBA", src.size, (0, 0, 0, 0))
|
||||
mask = mask_from_polygon(src.size, points, blur)
|
||||
part.alpha_composite(src)
|
||||
alpha = Image.composite(part.getchannel("A"), Image.new("L", src.size, 0), mask)
|
||||
part.putalpha(alpha)
|
||||
part.save(IMAGES / name)
|
||||
|
||||
|
||||
def composite_black(parts: list[str], dest: Path, width: int = 390, height: int = 600) -> None:
|
||||
canvas = Image.new("RGBA", (width, height), (16, 16, 18, 255))
|
||||
src_size = Image.open(IMAGES / parts[0]).size
|
||||
scale = min(width * 0.83 / src_size[0], height * 1.08 / src_size[1])
|
||||
target = (int(src_size[0] * scale), int(src_size[1] * scale))
|
||||
x = (width - target[0]) // 2 + 4
|
||||
y = int(height - target[1] + 78)
|
||||
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 main() -> None:
|
||||
IMAGES.mkdir(parents=True, exist_ok=True)
|
||||
sheet = Image.open(SHEET).convert("RGBA")
|
||||
source_path = PUPPET / "leesori_v2_source.png"
|
||||
if source_path.exists():
|
||||
src = Image.open(source_path).convert("RGBA")
|
||||
else:
|
||||
src = crop_front_pose(sheet)
|
||||
src.save(source_path)
|
||||
|
||||
w, h = src.size
|
||||
# Full source is retained underneath; overlays are intentionally separated
|
||||
# enough for visible idle motion while staying close to the sheet artwork.
|
||||
src.save(IMAGES / "leesori_v2_v3_base.png")
|
||||
|
||||
# LeeSoriV2 uses the full base for torso/waist. A moving chest overlay
|
||||
# creates green/crop-hem ghosting around the exposed waist, so the active
|
||||
# rig below omits chest and keeps only head/right-arm motion.
|
||||
make_part(src, "leesori_v2_v3_arm_r.png", [(0.65, 0.24), (0.86, 0.24), (0.84, 0.49), (0.69, 0.49), (0.61, 0.34)], blur=2.0)
|
||||
make_part(src, "leesori_v2_v3_hand_r.png", [(0.70, 0.47), (0.88, 0.47), (0.87, 0.61), (0.67, 0.60)], blur=1.5)
|
||||
make_part(src, "leesori_v2_v3_head.png", [(0.19, 0.00), (0.81, 0.00), (0.82, 0.27), (0.65, 0.36), (0.35, 0.36), (0.18, 0.27)])
|
||||
|
||||
rig = {
|
||||
"name": "LeeSoriV2",
|
||||
"status": "new_sheet_overlap_puppet_v4_static_torso_clean_waist",
|
||||
"canvas": {"width": w, "height": h},
|
||||
"imageBase": "./Images/",
|
||||
"note": "New LeeSori sheet-based overlap puppet. Torso, waist, lower body, and pocketed left hand stay in the base image to avoid doubled outlines, green waist ghosting, and awkward hand motion.",
|
||||
"bones": [
|
||||
{"name": "base", "parent": None, "pivot": [w * 0.50, h * 0.62], "z": 0, "image": "leesori_v2_v3_base.png"},
|
||||
{"name": "upperarm_r", "parent": "base", "pivot": [w * 0.74, h * 0.28], "z": 4, "image": "leesori_v2_v3_arm_r.png"},
|
||||
{"name": "hand_r", "parent": "upperarm_r", "pivot": [w * 0.78, h * 0.55], "z": 8, "image": "leesori_v2_v3_hand_r.png"},
|
||||
{"name": "head", "parent": "base", "pivot": [w * 0.50, h * 0.20], "z": 10, "image": "leesori_v2_v3_head.png"},
|
||||
],
|
||||
}
|
||||
(PUPPET / "rig.json").write_text(json.dumps(rig, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
|
||||
preview = Image.new("RGBA", (512, 768), (0, 0, 0, 0))
|
||||
scale = min(512 * 0.76 / w, 768 * 1.02 / h)
|
||||
target = (int(w * scale), int(h * scale))
|
||||
resized = src.resize(target, Image.Resampling.LANCZOS)
|
||||
preview.alpha_composite(resized, ((512 - target[0]) // 2, 768 - target[1] + 44))
|
||||
preview.save(PREVIEW)
|
||||
|
||||
parts = [
|
||||
"leesori_v2_v3_base.png",
|
||||
"leesori_v2_v3_arm_r.png",
|
||||
"leesori_v2_v3_hand_r.png",
|
||||
"leesori_v2_v3_head.png",
|
||||
]
|
||||
composite_black(parts, PUPPET / "qa_view_390x600_upper_bias.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()
|
||||
Reference in New Issue
Block a user