|
|
|
|
|
|
|
|
|
|
|
import os |
|
|
import time |
|
|
from pathlib import Path |
|
|
from typing import List, Tuple, Optional |
|
|
|
|
|
import gradio as gr |
|
|
from PIL import Image, ImageOps |
|
|
|
|
|
|
|
|
from unibox.utils.image_utils import ( |
|
|
concatenate_images_horizontally, |
|
|
add_annotation, |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
def _norm_path(f) -> Optional[str]: |
|
|
if f is None: |
|
|
return None |
|
|
if isinstance(f, (str, Path)): |
|
|
return str(f) |
|
|
if hasattr(f, "name"): |
|
|
return str(getattr(f, "name")) |
|
|
if isinstance(f, dict): |
|
|
return str(f.get("name") or f.get("path") or "") |
|
|
return str(f) |
|
|
|
|
|
def _load_images(files) -> List[Tuple[Image.Image, str]]: |
|
|
out: List[Tuple[Image.Image, str]] = [] |
|
|
for f in (files or []): |
|
|
p = _norm_path(f) |
|
|
if not p: |
|
|
continue |
|
|
im = Image.open(p) |
|
|
|
|
|
im = ImageOps.exif_transpose(im).convert("RGB") |
|
|
out.append((im, os.path.basename(p))) |
|
|
return out |
|
|
|
|
|
def _parse_descriptions(text: str, n: int): |
|
|
lines = (text or "").splitlines() |
|
|
if len(lines) > n: |
|
|
return None, f"Too many description lines ({len(lines)}) for {n} image(s). Provide ≤ one per image." |
|
|
lines = lines + [""] * (max(0, n - len(lines))) |
|
|
return lines[:n], None |
|
|
|
|
|
def _build_stats(files, desc_text: str) -> dict: |
|
|
pairs = _load_images(files) |
|
|
n = len(pairs) |
|
|
lines, err = _parse_descriptions(desc_text, n) if n > 0 else ((desc_text or "").splitlines(), None) |
|
|
mapping = {} |
|
|
for i, (_, fname) in enumerate(pairs): |
|
|
mapping[fname] = (lines[i] if isinstance(lines, list) and i < len(lines) else "") |
|
|
return { |
|
|
"num_images": n, |
|
|
"num_descriptions": len((desc_text or "").splitlines()), |
|
|
"mapping": mapping, |
|
|
**({"error": err} if err else {}), |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
def concatenate_with_annotations( |
|
|
files, |
|
|
desc_text: str, |
|
|
max_height: int, |
|
|
position: str, |
|
|
alignment: str, |
|
|
size_adj: str, |
|
|
): |
|
|
logs = [] |
|
|
out_img = None |
|
|
out_file = None |
|
|
|
|
|
pairs = _load_images(files) |
|
|
if not pairs: |
|
|
logs.append("ERROR: Please upload at least one image.") |
|
|
return out_img, out_file, "\n".join(logs), _build_stats(files, desc_text) |
|
|
|
|
|
lines, err = _parse_descriptions(desc_text, len(pairs)) |
|
|
if err: |
|
|
logs.append(f"ERROR: {err}") |
|
|
return out_img, out_file, "\n".join(logs), _build_stats(files, desc_text) |
|
|
|
|
|
|
|
|
if position in ("left", "right"): |
|
|
alignment = "center" |
|
|
|
|
|
annotated = [] |
|
|
for (im, fname), line in zip(pairs, lines): |
|
|
if line.strip(): |
|
|
im2 = add_annotation( |
|
|
pil_image=im, |
|
|
annotation=line, |
|
|
position=position, |
|
|
alignment=alignment, |
|
|
size=size_adj, |
|
|
) |
|
|
annotated.append(im2) |
|
|
logs.append(f"Annotated: {fname}") |
|
|
else: |
|
|
annotated.append(im) |
|
|
logs.append(f"Skipped (no description): {fname}") |
|
|
|
|
|
started = time.time() |
|
|
merged = concatenate_images_horizontally(annotated, max_height=max_height) |
|
|
if merged is None: |
|
|
logs.append("ERROR: Concatenation produced no result.") |
|
|
return None, None, "\n".join(logs), _build_stats(files, desc_text) |
|
|
|
|
|
|
|
|
out_dir = Path("outputs") |
|
|
out_dir.mkdir(parents=True, exist_ok=True) |
|
|
stamp = time.strftime("%Y%m%d_%H%M%S") |
|
|
out_name = f"concatenate_{stamp}.jpg" |
|
|
out_path = out_dir / out_name |
|
|
merged.save(str(out_path), format="JPEG", quality=95, optimize=True) |
|
|
|
|
|
w, h = merged.size |
|
|
size_bytes = out_path.stat().st_size |
|
|
latency = time.time() - started |
|
|
logs.append(f"Output: {out_name} — {w}×{h}px — {size_bytes} bytes — {latency:.3f}s") |
|
|
|
|
|
return merged, str(out_path), "\n".join(logs), _build_stats(files, desc_text) |
|
|
|
|
|
def check_stats_only(files, desc_text: str, *_): |
|
|
stats = _build_stats(files, desc_text) |
|
|
log = f"Images: {stats.get('num_images', 0)}; Description lines: {stats.get('num_descriptions', 0)}" |
|
|
if "error" in stats: |
|
|
log += f"\nERROR: {stats['error']}" |
|
|
return None, None, log, stats |
|
|
|
|
|
|
|
|
|
|
|
theme = gr.themes.Monochrome(primary_hue="slate", radius_size="sm") |
|
|
|
|
|
with gr.Blocks( |
|
|
title="Annotated Concatenation — Demo", |
|
|
theme=theme, |
|
|
analytics_enabled=False, |
|
|
) as demo: |
|
|
|
|
|
gr.Markdown("# Annotate & Concatenate Images") |
|
|
gr.Markdown( |
|
|
"Upload images (PNG/JPEG/WebP…), add one description per line (blank = skip), " |
|
|
"and concatenate horizontally. The output JPEG is named `concatenate_{timestamp}.jpg`." |
|
|
) |
|
|
|
|
|
with gr.Row(variant="panel"): |
|
|
with gr.Column(scale=2): |
|
|
files_in = gr.Files( |
|
|
label="Image files", |
|
|
|
|
|
file_types=[ |
|
|
".png", ".jpg", ".jpeg", ".webp", ".gif", |
|
|
".bmp", ".tif", ".tiff", ".jfif" |
|
|
], |
|
|
type="filepath", |
|
|
interactive=True, |
|
|
) |
|
|
desc_in = gr.Textbox( |
|
|
label="Descriptions (one per line; blank lines allowed to skip)", |
|
|
placeholder="e.g.\nLeft image label\n\nRight image label", |
|
|
lines=8, |
|
|
) |
|
|
max_h = gr.Number( |
|
|
label="Max height (px) for concatenated image", |
|
|
value=1024, |
|
|
precision=0, |
|
|
minimum=64, |
|
|
interactive=True, |
|
|
) |
|
|
|
|
|
|
|
|
with gr.Accordion("Annotation settings", open=False): |
|
|
pos = gr.Dropdown( |
|
|
label="Position", |
|
|
choices=["top", "bottom", "left", "right"], |
|
|
value="bottom", |
|
|
) |
|
|
align = gr.Radio( |
|
|
label="Alignment (applies to top/bottom)", |
|
|
choices=["left", "center", "right"], |
|
|
value="center", |
|
|
) |
|
|
size_adj = gr.Radio( |
|
|
label="Text size", |
|
|
choices=["default", "larger", "smaller", "smallest", "largest"], |
|
|
value="default", |
|
|
) |
|
|
|
|
|
def _toggle_align(p): |
|
|
return gr.update(value="center", interactive=False) if p in ("left", "right") else gr.update(interactive=True) |
|
|
|
|
|
pos.change(_toggle_align, inputs=[pos], outputs=[align]) |
|
|
|
|
|
with gr.Row(): |
|
|
concat_btn = gr.Button("Concatenate image", variant="primary") |
|
|
stats_btn = gr.Button("Check stats") |
|
|
|
|
|
with gr.Column(scale=3): |
|
|
out_img = gr.Image( |
|
|
label="Concatenated image (preview)", |
|
|
interactive=False, |
|
|
format="jpeg", |
|
|
show_download_button=False, |
|
|
) |
|
|
download_file = gr.File( |
|
|
label="Download JPEG (named as saved)", |
|
|
interactive=False, |
|
|
height=72, |
|
|
) |
|
|
|
|
|
with gr.Accordion("Logs", open=False): |
|
|
logs_out = gr.Textbox( |
|
|
label="Info / Errors", |
|
|
lines=10, |
|
|
interactive=False, |
|
|
) |
|
|
|
|
|
with gr.Accordion("Stats", open=False): |
|
|
stats_out = gr.JSON(label="Counts and current filename→description mapping") |
|
|
|
|
|
concat_btn.click( |
|
|
concatenate_with_annotations, |
|
|
inputs=[files_in, desc_in, max_h, pos, align, size_adj], |
|
|
outputs=[out_img, download_file, logs_out, stats_out], |
|
|
api_name="concatenate", |
|
|
) |
|
|
|
|
|
stats_btn.click( |
|
|
check_stats_only, |
|
|
inputs=[files_in, desc_in, max_h, pos, align, size_adj], |
|
|
outputs=[out_img, download_file, logs_out, stats_out], |
|
|
api_name="check_stats", |
|
|
) |
|
|
|
|
|
if __name__ == "__main__": |
|
|
demo.queue(max_size=8).launch() |