grid2frames / app.py
crownemmanuel's picture
Add ZIP download button
52d4309
Raw
History Blame Contribute Delete
8.25 kB
from __future__ import annotations
import shutil
import tempfile
import zipfile
from pathlib import Path
import cv2
import gradio as gr
from grid2frame.extractor import annotate_regions, detect_grid, extract_frames, read_image
WORK_ROOT = Path(tempfile.gettempdir()) / "grid2frame"
WORK_ROOT.mkdir(parents=True, exist_ok=True)
def process_image(
image_path: str | None,
sensitivity: int,
min_frame_size: int,
separator_mode: str,
output_format: str,
):
if not image_path:
return None, "<span>No image loaded</span>", [], None, None
run_dir = Path(tempfile.mkdtemp(prefix="run-", dir=WORK_ROOT))
image = read_image(image_path)
result = detect_grid(
image,
min_frame_size=min_frame_size,
sensitivity=sensitivity,
separator_mode=separator_mode.lower(),
)
if not result.regions:
return None, "<span>No frames detected</span>", [], None, None
annotated = annotate_regions(image, result.regions)
annotated_path = run_dir / "detected-grid.jpg"
cv2.imwrite(str(annotated_path), annotated, [cv2.IMWRITE_JPEG_QUALITY, 92])
extension = extension_for_format(output_format)
image_params = encoding_params(extension)
frame_paths: list[str] = []
zip_path = run_dir / "grid2frame-frames.zip"
with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as archive:
for index, (region, frame) in enumerate(
zip(result.regions, extract_frames(image, result.regions)),
start=1,
):
frame_name = (
f"frame-{index:03d}-r{region.row + 1:02d}-c{region.col + 1:02d}"
f".{extension}"
)
frame_path = run_dir / frame_name
cv2.imwrite(str(frame_path), frame, image_params)
frame_paths.append(str(frame_path))
archive.write(frame_path, arcname=frame_name)
status = (
f"<span>{len(result.regions)} frames extracted from "
f"{result.columns} columns x {result.rows} rows</span>"
)
return str(annotated_path), status, frame_paths, str(zip_path), str(zip_path)
def clear_outputs():
return None, "<span>No image loaded</span>", [], None, None
def extension_for_format(output_format: str) -> str:
if output_format == "PNG":
return "png"
if output_format == "WebP":
return "webp"
return "jpg"
def encoding_params(extension: str) -> list[int]:
if extension == "jpg":
return [cv2.IMWRITE_JPEG_QUALITY, 94]
if extension == "webp":
return [cv2.IMWRITE_WEBP_QUALITY, 94]
return []
def build_app() -> gr.Blocks:
with gr.Blocks(
title="Grid2Frame",
) as demo:
gr.HTML(
"""
<section class="hero-band">
<div>
<p class="eyebrow">Grid2Frame</p>
<h1>Extract every frame from a grid image.</h1>
</div>
</section>
"""
)
with gr.Row(elem_classes="workspace"):
with gr.Column(scale=8, elem_classes="preview-panel"):
input_image = gr.Image(
label="Upload grid",
sources=["upload"],
type="filepath",
height=420,
elem_classes="upload-box",
)
annotated_image = gr.Image(
label="Detected frames",
type="filepath",
height=420,
elem_classes="detected-box",
)
with gr.Column(scale=3, elem_classes="control-panel"):
status = gr.HTML("<span>No image loaded</span>", elem_classes="status-line")
sensitivity = gr.Slider(
minimum=0,
maximum=100,
value=58,
step=1,
label="Sensitivity",
)
min_frame_size = gr.Slider(
minimum=16,
maximum=600,
value=80,
step=4,
label="Minimum frame",
)
separator_mode = gr.Radio(
["Auto", "Dark", "Light"],
value="Auto",
label="Separator",
)
output_format = gr.Radio(
["JPG", "PNG", "WebP"],
value="JPG",
label="Output",
)
extract_button = gr.Button("Extract", variant="primary")
zip_file = gr.File(label="Download ZIP", elem_classes="zip-download")
zip_button = gr.DownloadButton(
"Download all frames",
variant="primary",
elem_classes="zip-button",
)
with gr.Column(elem_classes="frames-section"):
gr.HTML("<div class='section-head'><h2>Extracted frames</h2></div>")
gallery = gr.Gallery(
label="",
columns=6,
rows=2,
height=520,
object_fit="cover",
elem_classes="frame-gallery",
)
extract_inputs = [
input_image,
sensitivity,
min_frame_size,
separator_mode,
output_format,
]
extract_outputs = [annotated_image, status, gallery, zip_file, zip_button]
input_image.upload(process_image, extract_inputs, extract_outputs)
extract_button.click(process_image, extract_inputs, extract_outputs)
input_image.clear(clear_outputs, None, extract_outputs)
return demo
def launch_app() -> None:
build_app().launch(
css=APP_CSS,
theme=gr.themes.Base(
primary_hue="teal",
neutral_hue="slate",
font=["Arial", "Helvetica", "sans-serif"],
),
)
APP_CSS = """
:root {
--background: #f6f3ec;
--surface: #ffffff;
--surface-muted: #e9edf0;
--ink: #171c1f;
--muted: #66727a;
--line: #c8d0d4;
--accent: #0f766e;
--accent-strong: #0b4f49;
}
body,
.gradio-container {
background: linear-gradient(180deg, #f6f3ec 0, #eef3f1 42%, #f6f3ec 100%) !important;
color: var(--ink) !important;
}
.gradio-container {
max-width: none !important;
padding: 28px !important;
}
.hero-band {
align-items: end;
display: flex;
justify-content: space-between;
margin: 0 auto 24px;
max-width: 1440px;
}
.eyebrow {
color: var(--accent-strong);
font-size: 0.76rem;
font-weight: 800;
letter-spacing: 0.12em;
margin: 0 0 8px;
text-transform: uppercase;
}
h1 {
color: var(--ink);
font-size: clamp(2rem, 4vw, 4.6rem);
line-height: 0.98;
margin: 0;
max-width: 820px;
}
.workspace {
gap: 18px !important;
margin: 0 auto !important;
max-width: 1440px !important;
}
.preview-panel {
background: #111619;
border: 1px solid rgba(23, 28, 31, 0.18);
border-radius: 8px;
box-shadow: 0 22px 50px rgba(23, 28, 31, 0.12);
min-height: 58vh;
overflow: hidden;
padding: 18px;
}
.preview-panel .block,
.control-panel .block {
border-radius: 8px !important;
}
.upload-box,
.detected-box {
background: #111619 !important;
border-color: rgba(255, 255, 255, 0.08) !important;
}
.control-panel {
background: var(--surface);
border: 1px solid var(--line);
border-radius: 8px;
box-shadow: 0 22px 50px rgba(23, 28, 31, 0.12);
gap: 16px;
padding: 18px;
}
.status-line {
color: var(--muted);
font-weight: 700;
}
button.primary,
.primary {
background: var(--accent) !important;
border-color: var(--accent) !important;
color: #ffffff !important;
}
.zip-download {
background: #d7efea !important;
}
.zip-button {
width: 100% !important;
}
.frames-section {
margin: 28px auto 0 !important;
max-width: 1440px !important;
}
.section-head h2 {
color: var(--ink);
font-size: 1.35rem;
margin: 0 0 14px;
}
.frame-gallery {
background: transparent !important;
border: 0 !important;
}
@media (max-width: 980px) {
.gradio-container {
padding: 18px !important;
}
}
"""
if __name__ == "__main__":
launch_app()