main
adonis hero: flood-fill knockout (preserves surfaces the naive colour key destroyed) 8bc0a07 1mo ago
#!/usr/bin/env python3
"""Border-seeded flood-fill knockout (per transparent-hero-renders.md).
Numpy-vectorised BFS: same algorithm, fast. Never a global colour key."""
import sys, numpy as np
from PIL import Image

SRC, DOC, ICON = sys.argv[1], sys.argv[2], sys.argv[3]
FILL_TOL, T1 = 12, 30

im = Image.open(SRC).convert("RGB")
a = np.asarray(im).astype(np.int16)
h, w, _ = a.shape
corners = [tuple(a[2,2]), tuple(a[2,w-3]), tuple(a[h-3,2]), tuple(a[h-3,w-3])]
KEY = np.array(corners[0], dtype=np.int16)
print("corners:", corners, "| flat:", len(set(corners)) == 1)

near = (np.abs(a - KEY).max(axis=2) <= FILL_TOL)      # candidate background

# border-seeded flood fill, vectorised BFS over `near`
bg = np.zeros((h, w), bool)
seed = np.zeros((h, w), bool)
seed[0,:] = seed[-1,:] = True; seed[:,0] = seed[:,-1] = True
frontier = near & seed
bg |= frontier
while frontier.any():
    nxt = np.zeros((h, w), bool)
    nxt[1:,:]  |= frontier[:-1,:]
    nxt[:-1,:] |= frontier[1:,:]
    nxt[:,1:]  |= frontier[:,:-1]
    nxt[:,:-1] |= frontier[:,1:]
    frontier = nxt & near & ~bg
    bg |= frontier

# fringe = non-bg pixels 8-adjacent to bg
nb = np.zeros((h, w), bool)
for dy in (-1,0,1):
    for dx in (-1,0,1):
        if dy == dx == 0: continue
        nb[max(0,dy):h+min(0,dy), max(0,dx):w+min(0,dx)] |= bg[max(0,-dy):h+min(0,-dy), max(0,-dx):w+min(0,-dx)]
fringe = nb & ~bg

out = np.zeros((h, w, 4), np.uint8)
solid = ~bg & ~fringe
out[solid, :3] = a[solid]; out[solid, 3] = 255

d = np.abs(a - KEY).max(axis=2).astype(np.float32)
al = np.clip(d / T1, 0, 1)
af = np.maximum(al, 1e-3)[..., None]
un = KEY + (a - KEY) / af                              # un-premultiply the blend
un = np.clip(un, 0, 255)
out[fringe, :3] = un[fringe].astype(np.uint8)
out[fringe, 3]  = (al[fringe] * 255).round().astype(np.uint8)

img = Image.fromarray(out, "RGBA")
img.save(DOC)
img.crop(img.getbbox()).save(ICON)
ex = img.getchannel("A").getextrema()
print(f"bg px {int(bg.sum())} | fringe {int(fringe.sum())} | alpha {ex} | doc {img.size} | icon {img.crop(img.getbbox()).size}")