73 lines
2.5 KiB
Python
73 lines
2.5 KiB
Python
"""
|
|
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.")
|