Initial Dansori EQ workspace
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image, ImageFilter
|
||||
|
||||
|
||||
def clean_alpha(src: Path, dst: Path) -> tuple[int, int]:
|
||||
img = Image.open(src).convert("RGBA")
|
||||
w, h = img.size
|
||||
px = img.load()
|
||||
|
||||
alpha = img.getchannel("A")
|
||||
solid = alpha.point(lambda a: 255 if a > 8 else 0)
|
||||
inner = solid.filter(ImageFilter.MinFilter(21))
|
||||
edge = Image.new("L", img.size, 0)
|
||||
edge.paste(solid)
|
||||
edge_px = edge.load()
|
||||
inner_px = inner.load()
|
||||
|
||||
removed = 0
|
||||
softened = 0
|
||||
for y in range(h):
|
||||
upper_zone = y < int(h * 0.55)
|
||||
for x in range(w):
|
||||
r, g, b, a = px[x, y]
|
||||
if a == 0:
|
||||
continue
|
||||
|
||||
near_edge = edge_px[x, y] and not inner_px[x, y]
|
||||
if not near_edge and a > 225:
|
||||
continue
|
||||
|
||||
bright = (r + g + b) / 3
|
||||
chroma = max(r, g, b) - min(r, g, b)
|
||||
semi = a < 245
|
||||
|
||||
neutral_matte = semi and bright > 118 and chroma < 82
|
||||
pink_or_cyan_matte = (
|
||||
semi
|
||||
and upper_zone
|
||||
and bright > 130
|
||||
and (
|
||||
(r > 135 and b > 130 and g > 80)
|
||||
or (g > 135 and b > 130 and r < 175)
|
||||
)
|
||||
and chroma < 135
|
||||
)
|
||||
|
||||
if near_edge and (neutral_matte or pink_or_cyan_matte):
|
||||
px[x, y] = (r, g, b, 0)
|
||||
removed += 1
|
||||
elif semi and neutral_matte:
|
||||
px[x, y] = (r, g, b, int(a * 0.2))
|
||||
softened += 1
|
||||
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
img.save(dst)
|
||||
return removed, softened
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("paths", nargs="+", type=Path)
|
||||
parser.add_argument("--in-place", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
for path in args.paths:
|
||||
dst = path if args.in_place else path.with_name(path.stem + "_clean.png")
|
||||
removed, softened = clean_alpha(path, dst)
|
||||
print(f"{path} -> {dst} removed={removed} softened={softened}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,72 @@
|
||||
"""
|
||||
Make the (opaque, near-white) background of the mascot cutouts transparent,
|
||||
WITHOUT punching holes in the character's white clothing.
|
||||
|
||||
Method: flag near-white pixels, then keep only the near-white regions that are
|
||||
CONNECTED TO THE IMAGE BORDER (that's the background). Interior whites (hoodie /
|
||||
tee), enclosed by the character's outline, are preserved. A tiny alpha blur
|
||||
feathers the edge to avoid a white halo.
|
||||
|
||||
Usage:
|
||||
python tools/make_transparent.py <file_or_dir> [<file_or_dir> ...]
|
||||
Originals are backed up to Characters/_opaque_backup/ (once) before overwrite.
|
||||
"""
|
||||
import sys, os, glob
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from scipy import ndimage
|
||||
|
||||
WHITE_THRESH = 236 # min channel value to be considered "near white"
|
||||
FEATHER = 0.8 # gaussian sigma for alpha edge feather (px)
|
||||
|
||||
BACKUP_ROOT = os.path.join("Characters", "_opaque_backup")
|
||||
|
||||
|
||||
def process(path: str):
|
||||
im = Image.open(path).convert("RGBA")
|
||||
arr = np.array(im)
|
||||
rgb = arr[:, :, :3].astype(np.int16)
|
||||
|
||||
# near-white mask: all channels high AND low chroma (avoids eating pale skin)
|
||||
mn = rgb.min(axis=2)
|
||||
mx = rgb.max(axis=2)
|
||||
near_white = (mn >= WHITE_THRESH) & ((mx - mn) <= 12)
|
||||
|
||||
# connected components of near-white; keep those touching the border = background
|
||||
labels, n = ndimage.label(near_white)
|
||||
border_labels = set(labels[0, :]) | set(labels[-1, :]) | set(labels[:, 0]) | set(labels[:, -1])
|
||||
border_labels.discard(0)
|
||||
bg = np.isin(labels, list(border_labels))
|
||||
|
||||
alpha = np.where(bg, 0, 255).astype(np.float32)
|
||||
if FEATHER > 0:
|
||||
alpha = ndimage.gaussian_filter(alpha, sigma=FEATHER)
|
||||
arr[:, :, 3] = np.clip(alpha, 0, 255).astype(np.uint8)
|
||||
|
||||
# backup once
|
||||
rel = os.path.relpath(path, "Characters") if path.replace("\\", "/").startswith("Characters") else os.path.basename(path)
|
||||
bpath = os.path.join(BACKUP_ROOT, rel)
|
||||
os.makedirs(os.path.dirname(bpath), exist_ok=True)
|
||||
if not os.path.exists(bpath):
|
||||
Image.open(path).save(bpath)
|
||||
|
||||
Image.fromarray(arr, "RGBA").save(path)
|
||||
covered = 100.0 * bg.mean()
|
||||
print(f" {os.path.basename(path)} bg removed {covered:4.1f}%")
|
||||
|
||||
|
||||
def collect(target):
|
||||
if os.path.isdir(target):
|
||||
return sorted(glob.glob(os.path.join(target, "*.png")))
|
||||
return [target]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
targets = sys.argv[1:] or ["Characters/sori", "Characters/dan", "Characters/duo"]
|
||||
for t in targets:
|
||||
files = collect(t)
|
||||
if files:
|
||||
print(t)
|
||||
for f in files:
|
||||
process(f)
|
||||
print("done.")
|
||||
Reference in New Issue
Block a user