roicropper / web /app.py
fazrigading's picture
disable SSR mode for HF
3cd7d26 verified
Raw History Blame Contribute Delete
7.56 kB
"""ROICropper Web UI — Gradio interface for crop operations."""
from __future__ import annotations
import io
import zipfile
from pathlib import Path
from typing import Any
import cv2
import gradio as gr
import numpy as np
from roicropper.core import crop_to_aspect_ratio, crop_to_square
PRESETS = [("1:1", (1, 1)), ("4:3", (4, 3)), ("16:9", (16, 9)), ("3:2", (3, 2)), ("A4", (210, 297))]
def _process_crop(
image: Any,
crop_type: str,
preset: str,
custom_w: str,
custom_h: str,
slider_size: int,
slider_padding: int,
) -> tuple[np.ndarray | None, str]:
"""Core crop processing called by both Gradio callbacks."""
if image is None:
return None, "Please upload an image first."
if isinstance(image, dict) and "full_img" in image:
img_arr = image["full_img"]
elif isinstance(image, np.ndarray):
img_arr = image
else:
return None, "Unsupported input type."
# Save uploaded image to temp file for core module
tmp_dir = Path("/tmp/roicropper")
tmp_dir.mkdir(parents=True, exist_ok=True)
tmp_path = tmp_dir / f"upload_{id(image)}.png"
cv2.imwrite(str(tmp_path), img_arr)
try:
if crop_type == "Custom Ratio":
try:
ar = (int(custom_w.strip()), int(custom_h.strip()))
if ar[0] <= 0 or ar[1] <= 0:
return None, "Width and height must be positive integers."
except ValueError:
return None, "Invalid custom aspect ratio values."
result = crop_to_aspect_ratio(str(tmp_path), aspect_ratio=ar, max_size=slider_size, padding_pixels=slider_padding)
else:
result = crop_to_square(str(tmp_path), target_size=slider_size, padding_pixels=slider_padding)
finally:
tmp_path.unlink(missing_ok=True)
if result is None:
return None, "Could not process image. No object found or file unreadable."
return result, f"Success — cropped at {result.shape[1]}x{result.shape[0]}"
def _resize_for_preview(img: np.ndarray, max_preview: int = 400) -> np.ndarray:
"""Resize image for preview display while keeping aspect ratio."""
h, w = img.shape[:2]
if h > max_preview or w > max_preview:
scale = min(max_preview / h, max_preview / w)
return cv2.resize(img, (int(w * scale), (h * scale)))
return img
def ui_callback(*args: Any) -> tuple[np.ndarray | None, str]:
"""Gradio callback for the Process button."""
result, msg = _process_crop(*args)
preview = _resize_for_preview(result) if result is not None else None
return preview, msg
def handle_batch(
files: list[str],
crop_type: str,
preset: str,
custom_w: str,
custom_h: str,
slider_size: int,
slider_padding: int,
fmt: str,
) -> bytes | None:
"""Process multiple images and return ZIP archive."""
tmp_dir = Path("/tmp/roicropper/batch")
tmp_dir.mkdir(parents=True, exist_ok=True)
ar_w, ar_h = 1, 1
if crop_type == "Custom Ratio":
try:
ar_w, ar_h = int(custom_w.strip()), int(custom_h.strip())
except ValueError:
return None
results = []
for i, file_path in enumerate(files):
img = cv2.imread(str(file_path))
if img is None:
continue
if crop_type == "Custom Ratio":
result = crop_to_aspect_ratio(str(file_path), aspect_ratio=(ar_w, ar_h), max_size=slider_size, padding_pixels=slider_padding)
else:
result = crop_to_square(str(file_path), target_size=slider_size, padding_pixels=slider_padding)
if result is None:
continue
filename = f"cropped_{i+1}.{'jpg' if fmt == 'jpg' else 'png'}"
save_path = tmp_dir / filename
cv2.imwrite(str(save_path), result, [cv2.IMWRITE_JPEG_QUALITY, 95] if fmt == "jpg" else [])
results.append(filename)
if not results:
return None
zip_io = io.BytesIO()
with zipfile.ZipFile(zip_io, "w", zipfile.ZIP_DEFLATED) as zf:
for fname in results:
zf.write(tmp_dir / fname, fname)
# Cleanup
for fname in results:
(tmp_dir / fname).unlink(missing_ok=True)
tmp_dir.rmdir()
zip_io.seek(0)
return zip_io.getvalue()
def _get_theme() -> gr.Theme:
return gr.themes.Soft(primary_hue="indigo", neutral_hue="slate")
theme = _get_theme()
with gr.Blocks(title="ROICropper") as demo:
gr.Markdown("# ROICropper\nCrop image objects to any aspect ratio")
gr.Markdown("Built by [Fazri Gading](https://github.com/fazrigading/ROICropper)")
with gr.Row():
with gr.Column(scale=1):
with gr.Tab("Single"):
img_input = gr.Image(type="numpy", label="Upload Image", height=400)
crop_type = gr.Radio(["Square", "Custom Ratio"], value="Square", label="Crop Type")
preset = gr.Dropdown([p[0] for p in PRESETS], value="1:1", label="Aspect Ratio Preset")
with gr.Group(visible=False) as custom_group:
custom_w = gr.Number(label="Custom Width", value=16, precision=0)
custom_h = gr.Number(label="Custom Height", value=9, precision=0)
slider_size = gr.Slider(128, 4096, value=640, step=64, label="Size")
slider_padding = gr.Slider(0, 100, value=10, step=1, label="Padding")
btn_process = gr.Button("Process", variant="primary")
with gr.Tab("Batch"):
batch_input = gr.File(file_count="multiple", label="Upload Multiple Images")
same_controls = True
btn_process_batch = gr.Button("Process All & Download ZIP", variant="primary")
with gr.Tab("Info"):
gr.Markdown("### How It Works\n1. Upload an image with a white/light background\n2. Select crop type (square or custom aspect ratio)\n3. Adjust presets or custom dimensions\n4. Set size and padding\n5. Click Process\n\nThe tool detects the main object and crops to the requested ratio centered on it.")
gr.Markdown("### Supported Formats\nJPG, PNG, BMP, TIFF\n\n### Limits\nMax resolution handled depends on available RAM. Batch mode recommended for 50+ images.")
with gr.Column(scale=1):
img_output = gr.Image(type="numpy", label="Result")
status_text = gr.Textbox(label="Status", interactive=False)
# Custom ratio visibility toggle
def _show_custom(crop_type_val):
return gr.Group(visible=crop_type_val == "Custom Ratio")
crop_type.change(_show_custom, crop_type, custom_group)
# Preset values lookup
PRESET_MAP = {"1:1": (1, 1), "4:3": (4, 3), "16:9": (16, 9), "3:2": (3, 2), "A4": (210, 297)}
btn_process.click(
ui_callback,
inputs=[img_input, crop_type, preset, custom_w, custom_h, slider_size, slider_padding],
outputs=[img_output, status_text],
)
btn_process_batch.click(
handle_batch,
inputs=[batch_input, crop_type, preset, custom_w, custom_h, slider_size, slider_padding, gr.State("png")],
outputs=gr.File(label="Download Result"),
)
def launch(server_name: str = "0.0.0.0", server_port: int = 7860) -> None:
"""Launch the Gradio web UI."""
print(f"ROICropper web UI starting at http://{server_name}:{server_port}")
demo.launch(server_name=server_name, server_port=server_port, theme=theme, ssr_mode=False)
if __name__ == "__main__":
demo.launch(server_name="0.0.0.0", server_port=7860, theme=theme, ssr_mode=False)