| """
|
| Gradio Space: remove background with rembg, crop to subject, normalize to 256×256.
|
| """
|
|
|
| from __future__ import annotations
|
|
|
| import os
|
| import traceback
|
| from typing import Optional, Tuple
|
|
|
| import gradio as gr
|
| import numpy as np
|
| from PIL import Image, ImageEnhance, ImageOps
|
| from rembg import remove
|
|
|
|
|
| OUTPUT_SIZE = 256
|
|
|
| CONTRAST_FACTOR = 1.08
|
| SHARPNESS_FACTOR = 1.12
|
| ALPHA_THRESHOLD = 8
|
|
|
|
|
| def _alpha_bounding_box_rgba(img: Image.Image) -> Optional[Tuple[int, int, int, int]]:
|
| """Return (left, upper, right, lower) of non-transparent pixels, or None if empty."""
|
| if img.mode != "RGBA":
|
| img = img.convert("RGBA")
|
| alpha = np.array(img.split()[3])
|
| mask = alpha > ALPHA_THRESHOLD
|
| rows = np.any(mask, axis=1)
|
| cols = np.any(mask, axis=0)
|
| if not (np.any(rows) and np.any(cols)):
|
| return None
|
| y_indices = np.where(rows)[0]
|
| x_indices = np.where(cols)[0]
|
| return (
|
| int(x_indices[0]),
|
| int(y_indices[0]),
|
| int(x_indices[-1] + 1),
|
| int(y_indices[-1] + 1),
|
| )
|
|
|
|
|
| def _resize_pad_square_rgba(img: Image.Image, size: int) -> Image.Image:
|
| """Scale uniformly to fit inside size×size, center on transparent canvas."""
|
| img = img.convert("RGBA")
|
| w, h = img.size
|
| if w == 0 or h == 0:
|
| return Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
| scale = min(size / w, size / h)
|
| new_w = max(1, int(round(w * scale)))
|
| new_h = max(1, int(round(h * scale)))
|
| resized = img.resize((new_w, new_h), Image.Resampling.LANCZOS)
|
| canvas = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
| ox = (size - new_w) // 2
|
| oy = (size - new_h) // 2
|
| canvas.paste(resized, (ox, oy), resized)
|
| return canvas
|
|
|
|
|
| def _enhance_rgba(img: Image.Image, contrast: float, sharpness: float) -> Image.Image:
|
| """Apply contrast/sharpness to RGB channels only; preserve alpha."""
|
| img = img.convert("RGBA")
|
| r, g, b, a = img.split()
|
| rgb = Image.merge("RGB", (r, g, b))
|
| rgb = ImageEnhance.Contrast(rgb).enhance(contrast)
|
| rgb = ImageEnhance.Sharpness(rgb).enhance(sharpness)
|
| r2, g2, b2 = rgb.split()
|
| return Image.merge("RGBA", (r2, g2, b2, a))
|
|
|
|
|
| def process_image(input_image: Optional[Image.Image]) -> Tuple[Optional[Image.Image], str]:
|
| """
|
| Pipeline: rembg → alpha bbox crop → 256×256 pad → mild enhance → PNG-ready RGBA.
|
| Returns (result_image, status_message).
|
| """
|
| if input_image is None:
|
| return None, "Please upload an image."
|
|
|
| try:
|
|
|
| pil = ImageOps.exif_transpose(input_image)
|
| if pil.mode not in ("RGB", "RGBA"):
|
| pil = pil.convert("RGBA") if pil.mode in ("P", "LA") else pil.convert("RGB")
|
|
|
|
|
| if pil.mode == "RGBA":
|
| rgb = Image.new("RGB", pil.size, (255, 255, 255))
|
| rgb.paste(pil, mask=pil.split()[3])
|
| pil_rgb = rgb
|
| else:
|
| pil_rgb = pil.convert("RGB")
|
|
|
| cutout = remove(pil_rgb)
|
| if cutout.mode != "RGBA":
|
| cutout = cutout.convert("RGBA")
|
|
|
| bbox = _alpha_bounding_box_rgba(cutout)
|
| if bbox is None:
|
| return None, (
|
| "Could not detect a subject (empty alpha mask after background removal). "
|
| "Try another photo with a clearer foreground."
|
| )
|
|
|
| cropped = cutout.crop(bbox)
|
| out = _resize_pad_square_rgba(cropped, OUTPUT_SIZE)
|
| out = _enhance_rgba(out, CONTRAST_FACTOR, SHARPNESS_FACTOR)
|
|
|
| return out, "Done. Download the PNG below."
|
|
|
| except Exception as e:
|
| err = f"{type(e).__name__}: {e}"
|
| tb = traceback.format_exc()
|
|
|
| print(tb)
|
| return None, f"Processing failed: {err}. Check the image format and try again."
|
|
|
|
|
| def build_demo() -> gr.Blocks:
|
| theme = gr.themes.Soft(
|
| primary_hue="teal",
|
| secondary_hue="slate",
|
| font=[gr.themes.GoogleFont("Source Sans 3"), "ui-sans-serif", "system-ui", "sans-serif"],
|
| )
|
|
|
| with gr.Blocks(theme=theme, title="Background remover — 256×256 cutout") as demo:
|
| gr.Markdown(
|
| """
|
| # Subject cutout (256×256)
|
| Upload an image. The app removes the background with **rembg**, crops to the **alpha bounding box**,
|
| fits and **pads** to **256×256**, then applies a **light** contrast and sharpness boost.
|
| Output is a **PNG** with transparency, ready to download.
|
| """
|
| )
|
|
|
| with gr.Row():
|
| with gr.Column(scale=1):
|
| inp = gr.Image(
|
| label="Upload image",
|
| type="pil",
|
| image_mode="RGB",
|
| sources=["upload", "clipboard"],
|
| height=400,
|
| )
|
| run_btn = gr.Button("Process", variant="primary")
|
|
|
| with gr.Column(scale=1):
|
| out_img = gr.Image(
|
| label="Result (256×256 PNG)",
|
| type="pil",
|
| image_mode="RGBA",
|
| format="png",
|
| height=400,
|
| buttons=["download", "fullscreen", "share"],
|
| )
|
| status = gr.Textbox(label="Status", interactive=False, lines=3)
|
|
|
| gr.Markdown(
|
| "*Large images may take a few seconds on first run while models load.*"
|
| )
|
|
|
| run_btn.click(fn=process_image, inputs=[inp], outputs=[out_img, status])
|
|
|
| return demo
|
|
|
|
|
| if __name__ == "__main__":
|
| port = int(os.environ.get("PORT", 7860))
|
| demo = build_demo()
|
| demo.queue()
|
| demo.launch(server_name="0.0.0.0", server_port=port)
|
|
|