| import re |
| from pathlib import Path |
| from PIL import Image |
|
|
|
|
| def natural_key(s: str): |
| """Natural sort key: '10.png' sorts after '2.png' correctly.""" |
| return [int(t) if t.isdigit() else t.lower() for t in re.split(r"(\d+)", s)] |
|
|
|
|
| def load_rgb_image(path: str | Path) -> Image.Image: |
| img = Image.open(path) |
| if img.mode != "RGB": |
| img = img.convert("RGB") |
| return img |
|
|
|
|
| def save_rgb_png(img: Image.Image, path: str | Path): |
| path = Path(path) |
| path.parent.mkdir(parents=True, exist_ok=True) |
| img.save(path, format="PNG") |
|
|
|
|
| def make_rgba_with_alpha(rgb_img: Image.Image, alpha_mask: Image.Image) -> Image.Image: |
| """ |
| rgb_img: RGB PIL |
| alpha_mask: L PIL, same size or will be resized |
| Returns RGBA PIL where alpha channel is the mask. |
| """ |
| if rgb_img.mode != "RGB": |
| rgb_img = rgb_img.convert("RGB") |
| if alpha_mask.mode != "L": |
| alpha_mask = alpha_mask.convert("L") |
| if alpha_mask.size != rgb_img.size: |
| alpha_mask = alpha_mask.resize(rgb_img.size, Image.NEAREST) |
| rgba = rgb_img.copy() |
| rgba.putalpha(alpha_mask) |
| return rgba |
|
|
|
|
| def save_rgba_png(img: Image.Image, path: str | Path): |
| path = Path(path) |
| path.parent.mkdir(parents=True, exist_ok=True) |
| img.save(path, format="PNG") |
|
|
|
|
| def remove_white_background_alpha(img: Image.Image, threshold: int = 240) -> Image.Image: |
| """ |
| Create an alpha mask by treating near-white pixels as background. |
| Pixels where R, G, B are all >= threshold are considered background (alpha=0). |
| All other pixels get alpha=255. |
| Returns L mode image (0=background, 255=foreground). |
| """ |
| import numpy as np |
|
|
| if img.mode != "RGB": |
| img = img.convert("RGB") |
| arr = np.array(img) |
| |
| is_white = (arr[:, :, 0] >= threshold) & (arr[:, :, 1] >= threshold) & (arr[:, :, 2] >= threshold) |
| alpha = np.where(is_white, 0, 255).astype(np.uint8) |
| return Image.fromarray(alpha, mode="L") |
|
|
|
|