Initial Dansori character workspace
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import deque
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image, ImageFilter
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
REFERENCE = ROOT / "03_Assets" / "Reference"
|
||||
PARTS = ROOT / "03_Assets" / "Parts" / "Images"
|
||||
RAW = REFERENCE / "sori_generated_apose_raw.png"
|
||||
|
||||
SRC_W, SRC_H = 520, 900
|
||||
|
||||
|
||||
def load_source() -> Image.Image:
|
||||
return Image.open(RAW).convert("RGBA")
|
||||
|
||||
|
||||
def checker_background_mask(img: Image.Image) -> np.ndarray:
|
||||
arr = np.array(img.convert("RGBA"))
|
||||
rgb = arr[:, :, :3].astype(np.int16)
|
||||
alpha = arr[:, :, 3]
|
||||
light_gray = (rgb.min(axis=2) > 218) & ((rgb.max(axis=2) - rgb.min(axis=2)) < 18)
|
||||
transparent = alpha < 8
|
||||
candidate = light_gray | transparent
|
||||
|
||||
h, w = candidate.shape
|
||||
seen = np.zeros_like(candidate, dtype=bool)
|
||||
q: deque[tuple[int, int]] = deque()
|
||||
for x in range(w):
|
||||
if candidate[0, x]:
|
||||
q.append((x, 0))
|
||||
if candidate[h - 1, x]:
|
||||
q.append((x, h - 1))
|
||||
for y in range(h):
|
||||
if candidate[y, 0]:
|
||||
q.append((0, y))
|
||||
if candidate[y, w - 1]:
|
||||
q.append((w - 1, y))
|
||||
|
||||
while q:
|
||||
x, y = q.popleft()
|
||||
if seen[y, x] or not candidate[y, x]:
|
||||
continue
|
||||
seen[y, x] = True
|
||||
if x > 0:
|
||||
q.append((x - 1, y))
|
||||
if x < w - 1:
|
||||
q.append((x + 1, y))
|
||||
if y > 0:
|
||||
q.append((x, y - 1))
|
||||
if y < h - 1:
|
||||
q.append((x, y + 1))
|
||||
return seen
|
||||
|
||||
|
||||
def remove_checker_background(img: Image.Image) -> Image.Image:
|
||||
bg = checker_background_mask(img)
|
||||
alpha = Image.fromarray((~bg).astype(np.uint8) * 255, "L")
|
||||
alpha = alpha.filter(ImageFilter.GaussianBlur(0.65)).point(lambda p: 0 if p < 28 else min(255, int(p * 1.18)))
|
||||
out = img.copy()
|
||||
out.putalpha(alpha)
|
||||
return out
|
||||
|
||||
|
||||
def normalize_canvas(img: Image.Image) -> Image.Image:
|
||||
bbox = img.getbbox()
|
||||
if bbox is None:
|
||||
raise RuntimeError("source image has no visible pixels")
|
||||
crop = img.crop(bbox)
|
||||
ratio = min(430 / crop.width, 850 / crop.height)
|
||||
resized = crop.resize((round(crop.width * ratio), round(crop.height * ratio)), Image.Resampling.LANCZOS)
|
||||
canvas = Image.new("RGBA", (SRC_W, SRC_H), (0, 0, 0, 0))
|
||||
canvas.alpha_composite(resized, ((SRC_W - resized.width) // 2, 24))
|
||||
return canvas
|
||||
|
||||
|
||||
def save_part(master: Image.Image, name: str, box: tuple[int, int, int, int]) -> None:
|
||||
mask = Image.new("L", master.size, 0)
|
||||
part_alpha = master.getchannel("A").crop(box)
|
||||
mask.paste(part_alpha, box)
|
||||
out = master.copy()
|
||||
out.putalpha(mask.filter(ImageFilter.GaussianBlur(0.25)))
|
||||
out.save(PARTS / name)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
PARTS.mkdir(parents=True, exist_ok=True)
|
||||
master = normalize_canvas(remove_checker_background(load_source()))
|
||||
master.save(PARTS / "sori_part_master_apose.png")
|
||||
|
||||
# Coarse Live2D source parts. Boxes are in the normalized 520x900 master coordinate system.
|
||||
boxes = {
|
||||
"sori_part_head.png": (135, 8, 385, 255),
|
||||
"sori_part_neck.png": (215, 220, 305, 295),
|
||||
"sori_part_chest.png": (112, 212, 408, 435),
|
||||
"sori_part_pelvis.png": (145, 385, 375, 505),
|
||||
"sori_part_upperarm_l.png": (342, 250, 465, 452),
|
||||
"sori_part_forearm_l.png": (370, 360, 500, 535),
|
||||
"sori_part_hand_l.png": (382, 428, 520, 590),
|
||||
"sori_part_upperarm_r.png": (55, 250, 178, 452),
|
||||
"sori_part_forearm_r.png": (20, 360, 150, 535),
|
||||
"sori_part_hand_r.png": (0, 428, 138, 590),
|
||||
"sori_part_thigh_l.png": (260, 438, 385, 675),
|
||||
"sori_part_thigh_r.png": (135, 438, 260, 675),
|
||||
"sori_part_shin_l.png": (262, 640, 370, 825),
|
||||
"sori_part_shin_r.png": (150, 640, 258, 825),
|
||||
"sori_part_foot_l.png": (258, 780, 375, 900),
|
||||
"sori_part_foot_r.png": (145, 780, 262, 900),
|
||||
}
|
||||
for name, box in boxes.items():
|
||||
save_part(master, name, box)
|
||||
print(PARTS / "sori_part_master_apose.png")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user