ai-puzzle-maker / sprites.py
munish0838's picture
AI Puzzle Maker — FLUX jigsaws + MiniCPM mascot commentary
019630f verified
Raw
History Blame Contribute Delete
3.12 kB
"""
Image post-processing for the puzzle maker.
* Mascot sprite: magenta chroma-key -> autocrop -> downscale -> quantize
(same hue/dominance keying as the runner — removes #FF00FF AND the darkened
magenta drop shadow Flux likes to add, while sparing real colors).
* Puzzle artwork: resize to the canonical 1024x768 board and emit a compact
JPEG data-URL (PNG at this size would blow the community-pack budget).
* Thumbnails for the gallery cards.
"""
import base64
import io
import numpy as np
from PIL import Image
PUZZLE_W, PUZZLE_H = 1024, 768
def _open(data):
if isinstance(data, (bytes, bytearray)):
return Image.open(io.BytesIO(data))
return Image.open(data)
def _key_magenta(img, thr=38):
"""Set alpha=0 wherever a pixel is 'magenta-dominant' (R and B both clearly
exceed G). Catches #FF00FF and its purple shadow; leaves real colors alone."""
img = img.convert("RGBA")
a = np.asarray(img).astype(np.int16)
r, g, b = a[..., 0], a[..., 1], a[..., 2]
mag = (r - g > thr) & (b - g > thr) & (np.minimum(r, b) - g > thr)
# despill the fringe: pixels slightly magenta (anti-aliased edges) get their
# red+blue pulled toward green so there's no pink halo after keying.
fringe = (~mag) & (r - g > 8) & (b - g > 8)
a[..., 0][fringe] = (r[fringe] + g[fringe]) // 2
a[..., 2][fringe] = (b[fringe] + g[fringe]) // 2
a[..., 3][mag] = 0
return Image.fromarray(a.astype(np.uint8), "RGBA")
def _resize_h(img, target_h):
w, h = img.size
nw = max(1, round(w * target_h / h))
return img.resize((nw, target_h), Image.LANCZOS)
def _crunch(img, colors):
"""Hard-edge the alpha (pixel-art look) and quantize the palette."""
alpha = img.getchannel("A").point(lambda v: 255 if v > 110 else 0)
rgb = img.convert("RGB").quantize(colors=colors, method=Image.FASTOCTREE).convert("RGB")
out = rgb.convert("RGBA")
out.putalpha(alpha)
return out
def process_mascot(data, target_h=160, colors=32):
img = _key_magenta(_open(data))
bbox = img.getchannel("A").getbbox() # crop to the keyed subject only
if bbox:
img = img.crop(bbox)
return _crunch(_resize_h(img, target_h), colors)
def process_artwork(data):
"""Whatever Flux returns -> exactly PUZZLE_W x PUZZLE_H, center-cropped."""
img = _open(data).convert("RGB")
w, h = img.size
scale = max(PUZZLE_W / w, PUZZLE_H / h)
img = img.resize((round(w * scale), round(h * scale)), Image.LANCZOS)
w, h = img.size
x, y = (w - PUZZLE_W) // 2, (h - PUZZLE_H) // 2
return img.crop((x, y, x + PUZZLE_W, y + PUZZLE_H))
def thumb(img, size=256):
t = img.copy().convert("RGB")
t.thumbnail((size, size))
return t
def to_data_url(img, fmt="PNG"):
buf = io.BytesIO()
img.save(buf, fmt)
return f"data:image/{fmt.lower()};base64," + base64.b64encode(buf.getvalue()).decode()
def to_jpeg_data_url(img, quality=85):
buf = io.BytesIO()
img.convert("RGB").save(buf, "JPEG", quality=quality, optimize=True)
return "data:image/jpeg;base64," + base64.b64encode(buf.getvalue()).decode()