Slacky / app.py
Hussam-x's picture
Upload 3 files
9a34a98 verified
Raw
History Blame Contribute Delete
6.05 kB
"""
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
# Subtle enhancement — tweak without blowing out edges on small subjects
CONTRAST_FACTOR = 1.08
SHARPNESS_FACTOR = 1.12
ALPHA_THRESHOLD = 8 # ignore nearly-transparent noise
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:
# Normalize input (handles EXIF orientation, mode)
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")
# rembg expects RGB or path/bytes; PIL RGB is fine
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()
# Full traceback in console for Space logs; short message in UI
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)