Initial Dansori character workspace
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import deque
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image, ImageFilter
|
||||
|
||||
|
||||
def connected_components(mask: np.ndarray) -> np.ndarray:
|
||||
h, w = mask.shape
|
||||
labels = np.zeros((h, w), dtype=np.int32)
|
||||
current = 0
|
||||
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
if not mask[y, x] or labels[y, x] != 0:
|
||||
continue
|
||||
current += 1
|
||||
queue: deque[tuple[int, int]] = deque([(x, y)])
|
||||
labels[y, x] = current
|
||||
while queue:
|
||||
cx, cy = queue.popleft()
|
||||
for nx, ny in (
|
||||
(cx + 1, cy),
|
||||
(cx - 1, cy),
|
||||
(cx, cy + 1),
|
||||
(cx, cy - 1),
|
||||
):
|
||||
if nx < 0 or ny < 0 or nx >= w or ny >= h:
|
||||
continue
|
||||
if mask[ny, nx] and labels[ny, nx] == 0:
|
||||
labels[ny, nx] = current
|
||||
queue.append((nx, ny))
|
||||
return labels
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if len(sys.argv) != 3:
|
||||
raise SystemExit("usage: make_hairmask.py <input-head.png> <output-mask.png>")
|
||||
|
||||
src = Path(sys.argv[1])
|
||||
out = Path(sys.argv[2])
|
||||
|
||||
image = Image.open(src).convert("RGBA")
|
||||
arr = np.asarray(image)
|
||||
rgb = arr[:, :, :3].astype(np.int16)
|
||||
alpha = arr[:, :, 3]
|
||||
r, g, b = rgb[:, :, 0], rgb[:, :, 1], rgb[:, :, 2]
|
||||
|
||||
opaque = alpha > 32
|
||||
brown_hair = (
|
||||
opaque
|
||||
& (r > 34)
|
||||
& (g > 18)
|
||||
& (r >= g - 8)
|
||||
& (g >= b - 4)
|
||||
& ((r - b) > 22)
|
||||
& ((r < 205) | (g < 138) | (b < 105))
|
||||
)
|
||||
dark_hair_lines = opaque & (r > 18) & (r < 115) & (g < 100) & (b < 85) & ((r - b) > 8)
|
||||
mask = brown_hair | dark_hair_lines
|
||||
|
||||
labels = connected_components(mask)
|
||||
keep = np.zeros_like(mask)
|
||||
for label in range(1, labels.max() + 1):
|
||||
ys, xs = np.where(labels == label)
|
||||
if xs.size == 0:
|
||||
continue
|
||||
area = xs.size
|
||||
width = int(xs.max() - xs.min() + 1)
|
||||
height = int(ys.max() - ys.min() + 1)
|
||||
left = int(xs.min())
|
||||
right = int(xs.max())
|
||||
top = int(ys.min())
|
||||
bottom = int(ys.max())
|
||||
h, w = arr.shape[:2]
|
||||
touches_hair_zone = (
|
||||
left < w * 0.25
|
||||
or right > w * 0.75
|
||||
or top < h * 0.32
|
||||
or bottom > h * 0.78
|
||||
)
|
||||
if area >= 5000 or (
|
||||
area >= 1200
|
||||
and touches_hair_zone
|
||||
and (width >= 45 or height >= 45)
|
||||
):
|
||||
keep[ys, xs] = True
|
||||
|
||||
mask_img = Image.fromarray((keep.astype(np.uint8) * 255), "L")
|
||||
mask_img = mask_img.filter(ImageFilter.MaxFilter(5))
|
||||
mask_arr = np.asarray(mask_img) > 0
|
||||
mask_arr &= opaque
|
||||
|
||||
out_arr = np.zeros_like(arr)
|
||||
out_arr[:, :, :3] = 255
|
||||
out_arr[:, :, 3] = (mask_arr.astype(np.uint8) * 255)
|
||||
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
Image.fromarray(out_arr, "RGBA").save(out)
|
||||
print(f"Wrote {out}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user