Spaces:
Runtime error
Runtime error
Upload 2 files
Browse filesAdded functionality to cropping tab
- app.py +66 -0
- image_utils.py +89 -1
app.py
CHANGED
|
@@ -10,6 +10,7 @@ import threading
|
|
| 10 |
import gradio as gr
|
| 11 |
import spaces
|
| 12 |
import torch
|
|
|
|
| 13 |
from PIL import Image
|
| 14 |
|
| 15 |
# ββ Local modules β single source of truth for each concern βββββββββββββββββ
|
|
@@ -48,6 +49,8 @@ from image_utils import (
|
|
| 48 |
build_pnginfo,
|
| 49 |
push_pil_to_base,
|
| 50 |
push_pil_to_reference,
|
|
|
|
|
|
|
| 51 |
)
|
| 52 |
from control_tools import (
|
| 53 |
generate_depthmap,
|
|
@@ -639,6 +642,33 @@ with gr.Blocks() as demo:
|
|
| 639 |
send_to_base_btn = gr.Button("β Send to Base Image", variant="primary")
|
| 640 |
send_to_ref_btn = gr.Button("β Add to Reference Images")
|
| 641 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 642 |
# ββ Bulk processing tab ββββββββββββββββββββββββββββββββββββββββββ
|
| 643 |
with gr.Tab("π¦ Bulk Process", id="tab_bulk"):
|
| 644 |
gr.Markdown(
|
|
@@ -815,6 +845,42 @@ with gr.Blocks() as demo:
|
|
| 815 |
outputs=[reference_images],
|
| 816 |
).then(fn=on_reference_change, inputs=[reference_images], outputs=[reference_info])
|
| 817 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 818 |
# ββ Bulk tab wiring ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 819 |
bulk_event = bulk_run_btn.click(
|
| 820 |
fn=bulk_infer,
|
|
|
|
| 10 |
import gradio as gr
|
| 11 |
import spaces
|
| 12 |
import torch
|
| 13 |
+
import numpy as np
|
| 14 |
from PIL import Image
|
| 15 |
|
| 16 |
# ββ Local modules β single source of truth for each concern βββββββββββββββββ
|
|
|
|
| 49 |
build_pnginfo,
|
| 50 |
push_pil_to_base,
|
| 51 |
push_pil_to_reference,
|
| 52 |
+
extend_editor_canvas,
|
| 53 |
+
render_extend_schematic,
|
| 54 |
)
|
| 55 |
from control_tools import (
|
| 56 |
generate_depthmap,
|
|
|
|
| 642 |
send_to_base_btn = gr.Button("β Send to Base Image", variant="primary")
|
| 643 |
send_to_ref_btn = gr.Button("β Add to Reference Images")
|
| 644 |
|
| 645 |
+
# ββ Extend canvas section ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 646 |
+
# Uses the editor's current composite as the source so cropping + painting
|
| 647 |
+
# happen first, then we grow the canvas around the result. Output is
|
| 648 |
+
# loaded back into the same editor β Send β Base / Reference from there.
|
| 649 |
+
with gr.Accordion("π Extend canvas (add padding around image)", open=True):
|
| 650 |
+
gr.Markdown(
|
| 651 |
+
"Grow the editor image's canvas by a percentage in any combination "
|
| 652 |
+
"of directions. Percentages are relative to the *current* image "
|
| 653 |
+
"size β `Down = 100` doubles the height with the image on top. "
|
| 654 |
+
"The result replaces the editor contents so you can crop again or "
|
| 655 |
+
"send it to Base / Reference with the buttons above."
|
| 656 |
+
)
|
| 657 |
+
with gr.Row():
|
| 658 |
+
ext_up = gr.Number(label="Up %", value=0, minimum=0, precision=2)
|
| 659 |
+
ext_down = gr.Number(label="Down %", value=0, minimum=0, precision=2)
|
| 660 |
+
ext_left = gr.Number(label="Left %", value=0, minimum=0, precision=2)
|
| 661 |
+
ext_right = gr.Number(label="Right %", value=0, minimum=0, precision=2)
|
| 662 |
+
with gr.Row():
|
| 663 |
+
ext_fill = gr.ColorPicker(label="Fill colour", value="#000000")
|
| 664 |
+
extend_btn = gr.Button("π Extend canvas", variant="primary")
|
| 665 |
+
with gr.Row():
|
| 666 |
+
ext_schematic = gr.Image(
|
| 667 |
+
label="Layout preview (red outline = current image)",
|
| 668 |
+
type="pil", interactive=False, height=220,
|
| 669 |
+
)
|
| 670 |
+
ext_info = gr.Markdown("*Upload something into the editor first.*")
|
| 671 |
+
|
| 672 |
# ββ Bulk processing tab ββββββββββββββββββββββββββββββββββββββββββ
|
| 673 |
with gr.Tab("π¦ Bulk Process", id="tab_bulk"):
|
| 674 |
gr.Markdown(
|
|
|
|
| 845 |
outputs=[reference_images],
|
| 846 |
).then(fn=on_reference_change, inputs=[reference_images], outputs=[reference_info])
|
| 847 |
|
| 848 |
+
# Extend-canvas wiring
|
| 849 |
+
# The schematic previews the *current editor composite* so users see live
|
| 850 |
+
# feedback as they nudge the percentages / fill colour.
|
| 851 |
+
def _editor_source_for_preview(editor_value):
|
| 852 |
+
if not editor_value or editor_value.get("composite") is None:
|
| 853 |
+
return None
|
| 854 |
+
comp = editor_value["composite"]
|
| 855 |
+
if isinstance(comp, np.ndarray):
|
| 856 |
+
from PIL import Image as _Image
|
| 857 |
+
comp = _Image.fromarray(comp)
|
| 858 |
+
return comp
|
| 859 |
+
|
| 860 |
+
def _update_extend_preview(editor_value, up, down, left, right, fill):
|
| 861 |
+
return render_extend_schematic(
|
| 862 |
+
_editor_source_for_preview(editor_value), up, down, left, right, fill,
|
| 863 |
+
)
|
| 864 |
+
|
| 865 |
+
_extend_preview_inputs = [editor, ext_up, ext_down, ext_left, ext_right, ext_fill]
|
| 866 |
+
_extend_preview_outputs = [ext_schematic, ext_info]
|
| 867 |
+
for _c in (ext_up, ext_down, ext_left, ext_right, ext_fill):
|
| 868 |
+
_c.change(fn=_update_extend_preview,
|
| 869 |
+
inputs=_extend_preview_inputs, outputs=_extend_preview_outputs)
|
| 870 |
+
# Also refresh whenever the editor content itself changes (upload, HEIC load,
|
| 871 |
+
# a fresh crop) β that way the schematic scale updates too.
|
| 872 |
+
editor.change(fn=_update_extend_preview,
|
| 873 |
+
inputs=_extend_preview_inputs, outputs=_extend_preview_outputs)
|
| 874 |
+
|
| 875 |
+
# Run: extend, then hand the new PIL back to the editor. `render_extend_
|
| 876 |
+
# schematic` re-runs via editor.change once the new image lands, so no
|
| 877 |
+
# extra .then() is needed for the preview.
|
| 878 |
+
extend_btn.click(
|
| 879 |
+
fn=extend_editor_canvas,
|
| 880 |
+
inputs=[editor, ext_up, ext_down, ext_left, ext_right, ext_fill],
|
| 881 |
+
outputs=[editor],
|
| 882 |
+
)
|
| 883 |
+
|
| 884 |
# ββ Bulk tab wiring ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 885 |
bulk_event = bulk_run_btn.click(
|
| 886 |
fn=bulk_infer,
|
image_utils.py
CHANGED
|
@@ -10,7 +10,7 @@ import tempfile
|
|
| 10 |
from typing import Any
|
| 11 |
|
| 12 |
import numpy as np
|
| 13 |
-
from PIL import Image, ImageOps, ImageFilter
|
| 14 |
from PIL.PngImagePlugin import PngInfo
|
| 15 |
import gradio as gr
|
| 16 |
|
|
@@ -316,3 +316,91 @@ def push_pil_to_reference(img, current_gallery):
|
|
| 316 |
current.append(img)
|
| 317 |
gr.Info("Added to Reference Images.")
|
| 318 |
return current
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
from typing import Any
|
| 11 |
|
| 12 |
import numpy as np
|
| 13 |
+
from PIL import Image, ImageOps, ImageFilter, ImageDraw, ImageColor
|
| 14 |
from PIL.PngImagePlugin import PngInfo
|
| 15 |
import gradio as gr
|
| 16 |
|
|
|
|
| 316 |
current.append(img)
|
| 317 |
gr.Info("Added to Reference Images.")
|
| 318 |
return current
|
| 319 |
+
|
| 320 |
+
# ββ Canvas extension (used by the Crop / Fix tab) βββββββββββββββββββββββββββ
|
| 321 |
+
# Percentages are relative to the *original* dimensions. Padding is filled
|
| 322 |
+
# with a flat colour. Returns a new PIL image; does NOT save to disk β the
|
| 323 |
+
# result is loaded straight back into the ImageEditor so the existing
|
| 324 |
+
# Send β Base / Send β Reference buttons handle export.
|
| 325 |
+
|
| 326 |
+
from PIL import ImageColor # add near the other PIL imports at top of file
|
| 327 |
+
|
| 328 |
+
|
| 329 |
+
def _parse_fill(color_str: str, mode: str):
|
| 330 |
+
rgba = ImageColor.getcolor(color_str or "#000000", "RGBA")
|
| 331 |
+
if mode == "RGB":
|
| 332 |
+
return rgba[:3]
|
| 333 |
+
if mode == "L":
|
| 334 |
+
r, g, b, _ = rgba
|
| 335 |
+
return int(0.299 * r + 0.587 * g + 0.114 * b)
|
| 336 |
+
return rgba # RGBA
|
| 337 |
+
|
| 338 |
+
|
| 339 |
+
def compute_extend_padding(w: int, h: int, up, down, left, right) -> tuple[int, int, int, int]:
|
| 340 |
+
"""Return (pad_left, pad_top, pad_right, pad_bottom) in pixels for the
|
| 341 |
+
given percentage inputs. Percentages are relative to original W/H so
|
| 342 |
+
Down=100 on 960Γ960 β 960Γ1920 with the source at the top."""
|
| 343 |
+
pl = int(round(w * (float(left or 0) / 100.0)))
|
| 344 |
+
pr = int(round(w * (float(right or 0) / 100.0)))
|
| 345 |
+
pt = int(round(h * (float(up or 0) / 100.0)))
|
| 346 |
+
pb = int(round(h * (float(down or 0) / 100.0)))
|
| 347 |
+
return pl, pt, pr, pb
|
| 348 |
+
|
| 349 |
+
|
| 350 |
+
def extend_canvas(img: Image.Image, up, down, left, right, fill: str) -> Image.Image:
|
| 351 |
+
"""Extend `img`'s canvas by the given per-side percentages, filling the
|
| 352 |
+
new area with `fill`. Original image mode is preserved so RGBA stays
|
| 353 |
+
RGBA (no transparency loss)."""
|
| 354 |
+
if img is None:
|
| 355 |
+
raise gr.Error("Nothing to extend β upload an image into the editor first.")
|
| 356 |
+
if img.mode not in ("RGB", "RGBA", "L"):
|
| 357 |
+
img = img.convert("RGBA")
|
| 358 |
+
for name, v in (("Up", up), ("Down", down), ("Left", left), ("Right", right)):
|
| 359 |
+
if v is None or float(v) < 0:
|
| 360 |
+
raise gr.Error(f"'{name} %' must be β₯ 0.")
|
| 361 |
+
w, h = img.size
|
| 362 |
+
pl, pt, pr, pb = compute_extend_padding(w, h, up, down, left, right)
|
| 363 |
+
if pl == pt == pr == pb == 0:
|
| 364 |
+
gr.Info("All percentages are 0 β image unchanged.")
|
| 365 |
+
return img
|
| 366 |
+
new_size = (w + pl + pr, h + pt + pb)
|
| 367 |
+
canvas = Image.new(img.mode, new_size, _parse_fill(fill, img.mode))
|
| 368 |
+
canvas.paste(img, (pl, pt))
|
| 369 |
+
return canvas
|
| 370 |
+
|
| 371 |
+
|
| 372 |
+
def render_extend_schematic(
|
| 373 |
+
img: Image.Image | None, up, down, left, right, fill: str,
|
| 374 |
+
max_dim: int = 320,
|
| 375 |
+
) -> tuple[Image.Image | None, str]:
|
| 376 |
+
"""Live, to-scale preview of what extend_canvas() will produce, without
|
| 377 |
+
doing the full render. Returns (schematic PIL, info markdown)."""
|
| 378 |
+
if img is None:
|
| 379 |
+
return None, "*Upload something into the editor first.*"
|
| 380 |
+
w, h = img.size
|
| 381 |
+
pl, pt, pr, pb = compute_extend_padding(w, h, up, down, left, right)
|
| 382 |
+
nw, nh = w + pl + pr, h + pt + pb
|
| 383 |
+
scale = min(max_dim / nw, max_dim / nh, 1.0)
|
| 384 |
+
sw, sh = max(1, int(nw * scale)), max(1, int(nh * scale))
|
| 385 |
+
spl, spt = int(pl * scale), int(pt * scale)
|
| 386 |
+
sow, soh = max(1, int(w * scale)), max(1, int(h * scale))
|
| 387 |
+
|
| 388 |
+
schem = Image.new("RGB", (sw, sh), _parse_fill(fill, "RGB"))
|
| 389 |
+
d = ImageDraw.Draw(schem) # ImageDraw already imported at top of module
|
| 390 |
+
d.rectangle([spl, spt, spl + sow - 1, spt + soh - 1],
|
| 391 |
+
fill=(200, 200, 200), outline=(255, 0, 0), width=2)
|
| 392 |
+
info = (f"**Original:** {w} Γ {h} \n"
|
| 393 |
+
f"**Padding (L, T, R, B):** {pl}, {pt}, {pr}, {pb} \n"
|
| 394 |
+
f"**Final canvas:** {nw} Γ {nh}")
|
| 395 |
+
return schem, info
|
| 396 |
+
|
| 397 |
+
|
| 398 |
+
def extend_editor_canvas(editor_value, up, down, left, right, fill) -> Image.Image:
|
| 399 |
+
"""Take the current editor composite, extend it, and return the result so
|
| 400 |
+
it can be loaded straight back into the same ImageEditor. Any crop marks
|
| 401 |
+
the user had placed are baked in before extending (that's what
|
| 402 |
+
`_editor_composite` already does)."""
|
| 403 |
+
composite = fix_orientation(_editor_composite(editor_value))
|
| 404 |
+
out = extend_canvas(composite, up, down, left, right, fill)
|
| 405 |
+
gr.Info(f"Canvas extended to {out.size[0]} Γ {out.size[1]}.")
|
| 406 |
+
return out
|