Spaces:
Paused
Paused
Download background.py from Cnass/sprite: direct link, hf CLI and curl.
- Browser
- Download file 4.83 kB
-
https://huggingface.co/spaces/Cnass/sprite/resolve/main/background.py
- Command line
-
hf download hf://spaces/Cnass/sprite/background.py
-
curl -L -o background.py https://huggingface.co/spaces/Cnass/sprite/resolve/main/background.py
4.83 kB
| """ | |
| background.py | |
| -------------- | |
| Border-connected background detection. | |
| The critical rule from the spec: we must NOT treat "detected the background | |
| color" as "make every pixel of that color transparent". A sprite can | |
| legitimately contain the same color as the background inside its own body | |
| (e.g. a black outline on a black-background sheet). Only background pixels | |
| that are *reachable from the sheet's border* through same-colored | |
| (tolerance-aware) neighbours are background. A black pixel enclosed inside | |
| a sprite's silhouette is never touched. | |
| Algorithm | |
| --------- | |
| 1. Sample border pixels to estimate the background color (mode of the | |
| border ring, alpha-aware). | |
| 2. Flood-fill (BFS, 4-connected) starting from every border pixel that | |
| matches the background color within tolerance, expanding only through | |
| neighbours that also match. This produces a boolean "is background" | |
| mask that is by construction connected to the border. | |
| 3. Anything not reached by the flood fill is foreground, even if it is the | |
| exact same color as the background. | |
| """ | |
| from __future__ import annotations | |
| from collections import deque | |
| from dataclasses import dataclass | |
| import numpy as np | |
| from config import BACKGROUND_COLOR_TOLERANCE | |
| class BackgroundInfo: | |
| color: tuple[int, int, int, int] | |
| is_transparent: bool # sheet already had a real alpha channel background | |
| tolerance: int | |
| mask: np.ndarray # HxW bool, True = background | |
| def _estimate_border_color(rgb: np.ndarray, alpha: np.ndarray) -> tuple[int, int, int, int]: | |
| """Mode color of the outermost ring of pixels, alpha-aware.""" | |
| h, w = alpha.shape | |
| ring_mask = np.zeros((h, w), dtype=bool) | |
| ring_mask[0, :] = True | |
| ring_mask[-1, :] = True | |
| ring_mask[:, 0] = True | |
| ring_mask[:, -1] = True | |
| ring_rgb = rgb[ring_mask] | |
| ring_alpha = alpha[ring_mask] | |
| # If the border is overwhelmingly transparent, background = transparent. | |
| if (ring_alpha < 8).mean() > 0.6: | |
| return (0, 0, 0, 0) | |
| # Otherwise take the most common RGB triple on the border. | |
| colors, counts = np.unique(ring_rgb.reshape(-1, 3), axis=0, return_counts=True) | |
| mode_color = colors[np.argmax(counts)] | |
| return (int(mode_color[0]), int(mode_color[1]), int(mode_color[2]), 255) | |
| def detect_background(rgba: np.ndarray, tolerance: int = BACKGROUND_COLOR_TOLERANCE) -> BackgroundInfo: | |
| """ | |
| Detect the background of a sprite sheet using the border-connected | |
| flood-fill approach described above. | |
| """ | |
| h, w = rgba.shape[:2] | |
| rgb = rgba[:, :, :3].astype(np.int16) | |
| alpha = rgba[:, :, 3] | |
| bg_color = _estimate_border_color(rgb, alpha) | |
| is_transparent_bg = bg_color[3] == 0 | |
| if is_transparent_bg: | |
| # Background = fully (or near-fully) transparent pixels. | |
| candidate = alpha < 8 | |
| else: | |
| bg_rgb = np.array(bg_color[:3], dtype=np.int16) | |
| # Per-pixel color distance (max-channel-diff is cheap and works well | |
| # for the flat, dithering-free colors typical of sprite sheet mattes). | |
| diff = np.abs(rgb - bg_rgb).max(axis=2) | |
| candidate = (diff <= tolerance) & (alpha > 0) | |
| # Also treat fully transparent pixels as background even on an | |
| # otherwise opaque-background sheet (mixed sheets do happen). | |
| candidate |= alpha < 8 | |
| # Border-connected flood fill (BFS) restricted to `candidate` pixels. | |
| visited = np.zeros((h, w), dtype=bool) | |
| dq: deque[tuple[int, int]] = deque() | |
| border_coords = [] | |
| border_coords.extend((0, x) for x in range(w)) | |
| border_coords.extend((h - 1, x) for x in range(w)) | |
| border_coords.extend((y, 0) for y in range(h)) | |
| border_coords.extend((y, w - 1) for y in range(h)) | |
| for y, x in border_coords: | |
| if candidate[y, x] and not visited[y, x]: | |
| visited[y, x] = True | |
| dq.append((y, x)) | |
| while dq: | |
| cy, cx = dq.popleft() | |
| for ny, nx in ((cy - 1, cx), (cy + 1, cx), (cy, cx - 1), (cy, cx + 1)): | |
| if 0 <= ny < h and 0 <= nx < w and not visited[ny, nx] and candidate[ny, nx]: | |
| visited[ny, nx] = True | |
| dq.append((ny, nx)) | |
| return BackgroundInfo( | |
| color=bg_color, | |
| is_transparent=is_transparent_bg, | |
| tolerance=tolerance, | |
| mask=visited, | |
| ) | |
| def foreground_mask(rgba: np.ndarray, bg_info: BackgroundInfo) -> np.ndarray: | |
| """Convenience: inverse of the background mask.""" | |
| return ~bg_info.mask | |
| def apply_transparency(rgba: np.ndarray, bg_info: BackgroundInfo) -> np.ndarray: | |
| """ | |
| Return a copy of `rgba` with ONLY the border-connected background | |
| pixels made transparent. Pixels inside sprites that merely share the | |
| background color are left completely untouched (both RGB and alpha). | |
| """ | |
| out = rgba.copy() | |
| out[bg_info.mask, 3] = 0 | |
| return out | |