Spaces:
Running on Zero
Running on Zero
Update app.py
Browse files
app.py
CHANGED
|
@@ -3,8 +3,6 @@ import numpy as np
|
|
| 3 |
import random
|
| 4 |
import torch
|
| 5 |
import spaces
|
| 6 |
-
import base64
|
| 7 |
-
from io import BytesIO
|
| 8 |
from typing import Iterable
|
| 9 |
from PIL import Image, ImageDraw
|
| 10 |
from diffusers import FlowMatchEulerDiscreteScheduler
|
|
@@ -116,19 +114,6 @@ loaded = False
|
|
| 116 |
DEFAULT_PROMPT = "Remove the red highlighted object from the scene"
|
| 117 |
|
| 118 |
|
| 119 |
-
def b64_to_pil(b64_str: str) -> Image.Image | None:
|
| 120 |
-
"""Helper to decode base64 string from JS into a PIL Image"""
|
| 121 |
-
if not b64_str or not b64_str.startswith("data:image"):
|
| 122 |
-
return None
|
| 123 |
-
try:
|
| 124 |
-
_, data = b64_str.split(',', 1)
|
| 125 |
-
image_data = base64.b64decode(data)
|
| 126 |
-
return Image.open(BytesIO(image_data)).convert("RGB")
|
| 127 |
-
except Exception as e:
|
| 128 |
-
print(f"Error decoding image: {e}")
|
| 129 |
-
return None
|
| 130 |
-
|
| 131 |
-
|
| 132 |
def burn_boxes_onto_image(pil_image: Image.Image, boxes_json_str: str) -> Image.Image:
|
| 133 |
"""Burn red outline-only rectangles onto the image (no fill)."""
|
| 134 |
import json
|
|
@@ -153,7 +138,6 @@ def burn_boxes_onto_image(pil_image: Image.Image, boxes_json_str: str) -> Image.
|
|
| 153 |
y2 = int(b["y2"] * h)
|
| 154 |
lx, rx = min(x1, x2), max(x1, x2)
|
| 155 |
ty, by_ = min(y1, y2), max(y1, y2)
|
| 156 |
-
# Red outline only β no fill
|
| 157 |
draw.rectangle([lx, ty, rx, by_], outline=(255, 0, 0), width=bw)
|
| 158 |
|
| 159 |
return img
|
|
@@ -161,7 +145,7 @@ def burn_boxes_onto_image(pil_image: Image.Image, boxes_json_str: str) -> Image.
|
|
| 161 |
|
| 162 |
@spaces.GPU
|
| 163 |
def infer_object_removal(
|
| 164 |
-
|
| 165 |
boxes_json: str,
|
| 166 |
prompt: str,
|
| 167 |
seed: int = 0,
|
|
@@ -190,9 +174,8 @@ def infer_object_removal(
|
|
| 190 |
print(f"Prompt: {prompt}")
|
| 191 |
print(f"Boxes JSON received: '{boxes_json}'")
|
| 192 |
|
| 193 |
-
source_image = b64_to_pil(b64_str)
|
| 194 |
if source_image is None:
|
| 195 |
-
raise gr.Error("Please upload an image first
|
| 196 |
|
| 197 |
import json
|
| 198 |
try:
|
|
@@ -227,8 +210,7 @@ def infer_object_removal(
|
|
| 227 |
return result, seed, marked
|
| 228 |
|
| 229 |
|
| 230 |
-
def update_dimensions_on_upload(
|
| 231 |
-
image = b64_to_pil(b64_str)
|
| 232 |
if image is None:
|
| 233 |
return 1024, 1024
|
| 234 |
original_width, original_height = image.size
|
|
@@ -276,65 +258,12 @@ label{font-weight:600!important;color:#333!important}
|
|
| 276 |
::-webkit-scrollbar-thumb{background:linear-gradient(135deg,#A855F7,#C084FC);border-radius:4px}
|
| 277 |
::-webkit-scrollbar-thumb:hover{background:linear-gradient(135deg,#9333EA,#A855F7)}
|
| 278 |
|
| 279 |
-
#bbox-draw-wrap{position:relative;border:2px
|
| 280 |
#bbox-draw-wrap:hover{border-color:#A855F7}
|
| 281 |
#bbox-draw-canvas{cursor:crosshair;display:block;margin:0 auto}
|
| 282 |
.bbox-hint{background:rgba(168,85,247,.08);border:1px solid #E9D5FF;border-radius:8px;padding:10px 16px;margin:8px 0;font-size:.9rem;color:#6B21A8}
|
| 283 |
.dark .bbox-hint{background:rgba(168,85,247,.15);border-color:rgba(168,85,247,.3);color:#C084FC}
|
| 284 |
|
| 285 |
-
/* Custom Upload Prompt overlay */
|
| 286 |
-
#upload-prompt {
|
| 287 |
-
position: absolute;
|
| 288 |
-
top: 50%; left: 50%;
|
| 289 |
-
transform: translate(-50%, -50%);
|
| 290 |
-
text-align: center;
|
| 291 |
-
z-index: 20;
|
| 292 |
-
background: rgba(255, 255, 255, 0.95);
|
| 293 |
-
padding: 40px;
|
| 294 |
-
border-radius: 12px;
|
| 295 |
-
box-shadow: 0 8px 32px rgba(168,85,247,0.15);
|
| 296 |
-
backdrop-filter: blur(8px);
|
| 297 |
-
width: 80%;
|
| 298 |
-
max-width: 400px;
|
| 299 |
-
cursor: pointer;
|
| 300 |
-
border: 2px dashed #C084FC;
|
| 301 |
-
transition: all 0.2s ease;
|
| 302 |
-
}
|
| 303 |
-
#upload-prompt:hover {
|
| 304 |
-
border-color: #A855F7;
|
| 305 |
-
background: rgba(250, 245, 255, 0.95);
|
| 306 |
-
transform: translate(-50%, -52%);
|
| 307 |
-
box-shadow: 0 12px 36px rgba(168,85,247,0.25);
|
| 308 |
-
}
|
| 309 |
-
.dark #upload-prompt {
|
| 310 |
-
background: rgba(30, 30, 30, 0.95);
|
| 311 |
-
border-color: rgba(168,85,247,0.5);
|
| 312 |
-
}
|
| 313 |
-
.dark #upload-prompt:hover {
|
| 314 |
-
background: rgba(40, 40, 40, 0.95);
|
| 315 |
-
border-color: #C084FC;
|
| 316 |
-
}
|
| 317 |
-
.upload-icon {
|
| 318 |
-
width: 54px;
|
| 319 |
-
height: 54px;
|
| 320 |
-
color: #A855F7;
|
| 321 |
-
margin-bottom: 12px;
|
| 322 |
-
}
|
| 323 |
-
#upload-prompt p {
|
| 324 |
-
margin: 0 0 6px 0;
|
| 325 |
-
font-family: 'Outfit', sans-serif;
|
| 326 |
-
color: #6B21A8;
|
| 327 |
-
font-weight: 700;
|
| 328 |
-
font-size: 1.5rem;
|
| 329 |
-
}
|
| 330 |
-
.dark #upload-prompt p { color: #DAB2FF; }
|
| 331 |
-
.upload-subtext {
|
| 332 |
-
font-size: 0.95rem;
|
| 333 |
-
color: #888;
|
| 334 |
-
font-family: 'Outfit', sans-serif;
|
| 335 |
-
}
|
| 336 |
-
.dark .upload-subtext { color: #aaa; }
|
| 337 |
-
|
| 338 |
.bbox-toolbar-section{
|
| 339 |
display:flex;
|
| 340 |
gap:8px;
|
|
@@ -388,14 +317,16 @@ label{font-weight:600!important;color:#333!important}
|
|
| 388 |
.bbox-tb-select{background:#6366f1}
|
| 389 |
.bbox-tb-select:hover{background:#818cf8}
|
| 390 |
.bbox-tb-select.active{background:#22c55e;box-shadow:0 0 8px rgba(34,197,94,.5)}
|
|
|
|
|
|
|
| 391 |
.bbox-tb-del{background:#dc2626}
|
| 392 |
.bbox-tb-del:hover{background:#ef4444}
|
| 393 |
.bbox-tb-undo{background:#7E22CE}
|
| 394 |
.bbox-tb-undo:hover{background:#9333EA}
|
| 395 |
.bbox-tb-clear{background:#be123c}
|
| 396 |
.bbox-tb-clear:hover{background:#e11d48}
|
| 397 |
-
.bbox-tb-
|
| 398 |
-
.bbox-tb-
|
| 399 |
|
| 400 |
#bbox-status{position:absolute;top:10px;left:10px;background:rgba(0,0,0,.75);color:#00ff88;padding:5px 10px;border-radius:6px;font-family:'IBM Plex Mono',monospace;font-size:11px;z-index:10;display:none;pointer-events:none}
|
| 401 |
#bbox-count{position:absolute;top:10px;right:10px;background:rgba(147,51,234,.85);color:#fff;padding:4px 10px;border-radius:6px;font-family:'IBM Plex Mono',monospace;font-size:11px;z-index:10;display:none}
|
|
@@ -413,9 +344,70 @@ label{font-weight:600!important;color:#333!important}
|
|
| 413 |
}
|
| 414 |
.dark #bbox-debug-count{color:#C084FC;background:rgba(168,85,247,.12)}
|
| 415 |
|
| 416 |
-
|
| 417 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 418 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 419 |
"""
|
| 420 |
|
| 421 |
bbox_drawer_js = r"""
|
|
@@ -428,18 +420,19 @@ bbox_drawer_js = r"""
|
|
| 428 |
const status = document.getElementById('bbox-status');
|
| 429 |
const badge = document.getElementById('bbox-count');
|
| 430 |
const debugCount = document.getElementById('bbox-debug-count');
|
| 431 |
-
|
| 432 |
-
const
|
| 433 |
-
|
| 434 |
-
const
|
| 435 |
-
const
|
| 436 |
-
const
|
| 437 |
-
const
|
| 438 |
-
|
| 439 |
-
const
|
| 440 |
-
const
|
| 441 |
-
|
| 442 |
-
|
|
|
|
| 443 |
console.log('[BBox] waiting for DOM...');
|
| 444 |
setTimeout(initCanvasBbox, 250);
|
| 445 |
return;
|
|
@@ -453,7 +446,8 @@ bbox_drawer_js = r"""
|
|
| 453 |
window.__bboxBoxes = boxes;
|
| 454 |
|
| 455 |
let baseImg = null;
|
| 456 |
-
let
|
|
|
|
| 457 |
let selectedIdx = -1;
|
| 458 |
let mode = 'draw';
|
| 459 |
|
|
@@ -466,7 +460,9 @@ bbox_drawer_js = r"""
|
|
| 466 |
const RED_STROKE_WIDTH = 3;
|
| 467 |
const SEL_STROKE = 'rgba(0,120,255,0.95)';
|
| 468 |
|
| 469 |
-
function n2px(b) {
|
|
|
|
|
|
|
| 470 |
function px2n(x1,y1,x2,y2) {
|
| 471 |
return {
|
| 472 |
x1: Math.min(x1,x2)/dispW, y1: Math.min(y1,y2)/dispH,
|
|
@@ -475,7 +471,7 @@ bbox_drawer_js = r"""
|
|
| 475 |
}
|
| 476 |
function clamp01(v){return Math.max(0,Math.min(1,v));}
|
| 477 |
function fitSize(nw, nh) {
|
| 478 |
-
const mw = wrap.clientWidth || 512, mh =
|
| 479 |
const r = Math.min(mw/nw, mh/nh, 1);
|
| 480 |
dispW = Math.round(nw*r); dispH = Math.round(nh*r);
|
| 481 |
canvas.width = dispW; canvas.height = dispH;
|
|
@@ -503,53 +499,79 @@ bbox_drawer_js = r"""
|
|
| 503 |
}
|
| 504 |
|
| 505 |
const container = document.getElementById('boxes-json-input');
|
| 506 |
-
if (!container)
|
|
|
|
|
|
|
|
|
|
| 507 |
const targets = [
|
| 508 |
...container.querySelectorAll('textarea'),
|
| 509 |
-
...container.querySelectorAll('input
|
|
|
|
| 510 |
];
|
| 511 |
targets.forEach(el => {
|
| 512 |
-
const proto = el.tagName === 'TEXTAREA'
|
|
|
|
|
|
|
| 513 |
const ns = Object.getOwnPropertyDescriptor(proto, 'value');
|
| 514 |
if (ns && ns.set) {
|
| 515 |
ns.set.call(el, jsonStr);
|
| 516 |
el.dispatchEvent(new Event('input', {bubbles:true, composed:true}));
|
| 517 |
el.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
|
|
|
|
| 518 |
}
|
| 519 |
});
|
| 520 |
}
|
| 521 |
|
| 522 |
-
|
| 523 |
-
|
|
|
|
| 524 |
if (!container) return;
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 538 |
}
|
| 539 |
|
| 540 |
function redraw(tempRect) {
|
| 541 |
ctx.clearRect(0,0,dispW,dispH);
|
| 542 |
-
if (!baseImg) {
|
| 543 |
-
ctx.fillStyle='#1a1a1a'; ctx.fillRect(0,0,dispW,dispH);
|
| 544 |
-
updateBadge(); return;
|
| 545 |
-
}
|
| 546 |
ctx.drawImage(baseImg, 0, 0, dispW, dispH);
|
| 547 |
|
| 548 |
boxes.forEach((b,i) => {
|
| 549 |
const p = n2px(b);
|
| 550 |
const lx=p.x1, ty=p.y1, w=p.x2-p.x1, h=p.y2-p.y1;
|
| 551 |
|
| 552 |
-
/* RED OUTLINE ONLY β no fill */
|
| 553 |
if (i === selectedIdx) {
|
| 554 |
ctx.strokeStyle = SEL_STROKE;
|
| 555 |
ctx.lineWidth = RED_STROKE_WIDTH + 1;
|
|
@@ -562,7 +584,6 @@ bbox_drawer_js = r"""
|
|
| 562 |
ctx.strokeRect(lx, ty, w, h);
|
| 563 |
ctx.setLineDash([]);
|
| 564 |
|
| 565 |
-
/* label tag */
|
| 566 |
ctx.fillStyle = i===selectedIdx ? 'rgba(0,120,255,0.85)' : 'rgba(255,0,0,0.85)';
|
| 567 |
ctx.font = 'bold 11px IBM Plex Mono,monospace';
|
| 568 |
ctx.textAlign = 'left'; ctx.textBaseline = 'top';
|
|
@@ -575,7 +596,6 @@ bbox_drawer_js = r"""
|
|
| 575 |
if (i === selectedIdx) drawHandles(p);
|
| 576 |
});
|
| 577 |
|
| 578 |
-
/* temp drawing rect β outline only */
|
| 579 |
if (tempRect) {
|
| 580 |
const rx = Math.min(tempRect.x1,tempRect.x2);
|
| 581 |
const ry = Math.min(tempRect.y1,tempRect.y2);
|
|
@@ -651,6 +671,121 @@ bbox_drawer_js = r"""
|
|
| 651 |
}
|
| 652 |
function hideStatus() { status.style.display = 'none'; }
|
| 653 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 654 |
function onDown(e) {
|
| 655 |
if (!baseImg) return;
|
| 656 |
e.preventDefault();
|
|
@@ -779,59 +914,6 @@ bbox_drawer_js = r"""
|
|
| 779 |
canvas.addEventListener('touchend', onUp, {passive:false});
|
| 780 |
canvas.addEventListener('touchcancel',(e)=>{e.preventDefault();dragging=false;redraw();},{passive:false});
|
| 781 |
|
| 782 |
-
// --- File Upload Logic ---
|
| 783 |
-
function processFile(file) {
|
| 784 |
-
if (!file || !file.type.startsWith('image/')) return;
|
| 785 |
-
const reader = new FileReader();
|
| 786 |
-
reader.onload = (event) => {
|
| 787 |
-
const dataUrl = event.target.result;
|
| 788 |
-
const img = new window.Image();
|
| 789 |
-
img.crossOrigin = 'anonymous';
|
| 790 |
-
img.onload = () => {
|
| 791 |
-
baseImg = img;
|
| 792 |
-
boxes.length = 0;
|
| 793 |
-
window.__bboxBoxes = boxes;
|
| 794 |
-
selectedIdx = -1;
|
| 795 |
-
fitSize(img.naturalWidth, img.naturalHeight);
|
| 796 |
-
syncToGradio(); redraw(); hideStatus();
|
| 797 |
-
uploadPrompt.style.display = 'none';
|
| 798 |
-
syncImageToGradio(dataUrl);
|
| 799 |
-
};
|
| 800 |
-
img.src = dataUrl;
|
| 801 |
-
};
|
| 802 |
-
reader.readAsDataURL(file);
|
| 803 |
-
}
|
| 804 |
-
|
| 805 |
-
uploadPrompt.addEventListener('click', (e) => {
|
| 806 |
-
if(e.target !== fileInput) fileInput.click();
|
| 807 |
-
});
|
| 808 |
-
btnChange.addEventListener('click', () => fileInput.click());
|
| 809 |
-
|
| 810 |
-
fileInput.addEventListener('change', (e) => {
|
| 811 |
-
processFile(e.target.files[0]);
|
| 812 |
-
e.target.value = ''; // Reset input to allow re-upload of same file
|
| 813 |
-
});
|
| 814 |
-
|
| 815 |
-
wrap.addEventListener('dragover', (e) => {
|
| 816 |
-
e.preventDefault();
|
| 817 |
-
wrap.style.borderColor = '#A855F7';
|
| 818 |
-
wrap.style.boxShadow = '0 0 15px rgba(168,85,247,0.3)';
|
| 819 |
-
});
|
| 820 |
-
wrap.addEventListener('dragleave', (e) => {
|
| 821 |
-
e.preventDefault();
|
| 822 |
-
wrap.style.borderColor = '';
|
| 823 |
-
wrap.style.boxShadow = '';
|
| 824 |
-
});
|
| 825 |
-
wrap.addEventListener('drop', (e) => {
|
| 826 |
-
e.preventDefault();
|
| 827 |
-
wrap.style.borderColor = '';
|
| 828 |
-
wrap.style.boxShadow = '';
|
| 829 |
-
if (e.dataTransfer.files.length) {
|
| 830 |
-
processFile(e.dataTransfer.files[0]);
|
| 831 |
-
}
|
| 832 |
-
});
|
| 833 |
-
|
| 834 |
-
// --- Toolbar Logic ---
|
| 835 |
btnDraw.addEventListener('click', ()=>setMode('draw'));
|
| 836 |
btnSelect.addEventListener('click', ()=>setMode('select'));
|
| 837 |
|
|
@@ -865,12 +947,15 @@ bbox_drawer_js = r"""
|
|
| 865 |
syncToGradio(); redraw(); hideStatus();
|
| 866 |
});
|
| 867 |
|
|
|
|
|
|
|
|
|
|
| 868 |
new ResizeObserver(() => {
|
| 869 |
if (baseImg) { fitSize(baseImg.naturalWidth, baseImg.naturalHeight); redraw(); }
|
| 870 |
}).observe(wrap);
|
| 871 |
|
| 872 |
setMode('draw');
|
| 873 |
-
fitSize(512,
|
| 874 |
syncToGradio();
|
| 875 |
}
|
| 876 |
|
|
@@ -883,74 +968,72 @@ with gr.Blocks(css=css, theme=purple_theme) as demo:
|
|
| 883 |
gr.Markdown("# **QIE-Object-Remover-Bbox**", elem_id="main-title")
|
| 884 |
gr.Markdown(
|
| 885 |
"Perform diverse image edits using a specialized [LoRA](https://huggingface.co/prithivMLmods/QIE-2511-Object-Remover-v2). "
|
| 886 |
-
"Upload an image directly into the
|
| 887 |
"Multiple boxes supported. Select, move, resize or delete individual boxes.",
|
| 888 |
elem_id="subtitle",
|
| 889 |
)
|
| 890 |
|
| 891 |
with gr.Row():
|
| 892 |
with gr.Column(scale=1):
|
| 893 |
-
|
| 894 |
-
|
| 895 |
-
|
| 896 |
-
|
| 897 |
-
|
|
|
|
|
|
|
| 898 |
)
|
| 899 |
|
| 900 |
gr.Markdown("# **Bbox Edit Controller**")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 901 |
|
| 902 |
gr.HTML(
|
| 903 |
"""
|
| 904 |
<div id="bbox-draw-wrap">
|
| 905 |
-
<
|
| 906 |
-
<svg class="upload-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 907 |
-
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
| 908 |
-
<polyline points="17 8 12 3 7 8"></polyline>
|
| 909 |
-
<line x1="12" y1="3" x2="12" y2="15"></line>
|
| 910 |
-
</svg>
|
| 911 |
-
<p>Upload Image</p>
|
| 912 |
-
<span class="upload-subtext">Drag and drop or click to browse</span>
|
| 913 |
-
<input type="file" id="custom-file-input" accept="image/*" style="display:none;" />
|
| 914 |
-
</div>
|
| 915 |
-
<canvas id="bbox-draw-canvas" width="512" height="400"></canvas>
|
| 916 |
<div id="bbox-status"></div>
|
| 917 |
<div id="bbox-count"></div>
|
|
|
|
| 918 |
</div>
|
|
|
|
| 919 |
"""
|
| 920 |
)
|
| 921 |
|
| 922 |
gr.HTML(
|
| 923 |
"""
|
| 924 |
<div class="bbox-toolbar-section">
|
|
|
|
|
|
|
|
|
|
|
|
|
| 925 |
<span class="toolbar-label">π Tools:</span>
|
| 926 |
<button id="tb-draw" class="bbox-tb-draw active" title="Draw new boxes">βοΈ Draw</button>
|
| 927 |
<button id="tb-select" class="bbox-tb-select" title="Select / move / resize">π² Select</button>
|
| 928 |
<div class="toolbar-divider"></div>
|
| 929 |
<span class="toolbar-label">Actions:</span>
|
| 930 |
-
<button id="tb-del" class="bbox-tb-del" title="Delete selected box">β Delete</button>
|
| 931 |
<button id="tb-undo" class="bbox-tb-undo" title="Remove last box">β© Undo</button>
|
| 932 |
<button id="tb-clear" class="bbox-tb-clear" title="Remove all boxes">π Clear All</button>
|
| 933 |
-
<div class="toolbar-divider"></div>
|
| 934 |
-
<button id="tb-change-img" class="bbox-tb-change" title="Upload a different image">πΈ Change Image</button>
|
| 935 |
</div>
|
| 936 |
"""
|
| 937 |
)
|
| 938 |
-
|
| 939 |
-
gr.HTML(
|
| 940 |
-
'<div class="bbox-hint">'
|
| 941 |
-
"<b>Draw mode:</b> Click & drag to create red rectangles. "
|
| 942 |
-
"<b>Select mode:</b> Click a box to select it \u2192 drag to <b>move</b>, "
|
| 943 |
-
"drag handles to <b>resize</b>. Use <b>Delete Selected</b> to remove one box."
|
| 944 |
-
"</div>"
|
| 945 |
-
)
|
| 946 |
-
|
| 947 |
gr.HTML('<div id="bbox-debug-count">\u2B1C No boxes drawn yet</div>')
|
| 948 |
|
| 949 |
boxes_json = gr.Textbox(
|
| 950 |
value="[]",
|
|
|
|
|
|
|
| 951 |
elem_id="boxes-json-input",
|
| 952 |
-
|
| 953 |
-
container=False
|
| 954 |
)
|
| 955 |
|
| 956 |
prompt = gr.Textbox(
|
|
@@ -982,7 +1065,7 @@ with gr.Blocks(css=css, theme=purple_theme) as demo:
|
|
| 982 |
"[prithivMLmods](https://huggingface.co/prithivMLmods). "
|
| 983 |
"Adapter: [QIE-2511-Object-Remover-v2]"
|
| 984 |
"(https://huggingface.co/prithivMLmods/QIE-2511-Object-Remover-v2). "
|
| 985 |
-
"More adapters
|
| 986 |
"(https://huggingface.co/models?other=base_model:adapter:Qwen/Qwen-Image-Edit-2509)."
|
| 987 |
)
|
| 988 |
|
|
@@ -990,23 +1073,24 @@ with gr.Blocks(css=css, theme=purple_theme) as demo:
|
|
| 990 |
|
| 991 |
run_btn.click(
|
| 992 |
fn=infer_object_removal,
|
| 993 |
-
inputs=[
|
| 994 |
guidance_scale, num_inference_steps, height_slider, width_slider],
|
| 995 |
outputs=[result, seed, preview],
|
| 996 |
-
js="""(
|
| 997 |
const boxes = window.__bboxBoxes || [];
|
| 998 |
const json = JSON.stringify(boxes);
|
| 999 |
console.log('[BBox] submitting', boxes.length, 'boxes:', json);
|
| 1000 |
-
return [
|
| 1001 |
}""",
|
| 1002 |
)
|
| 1003 |
|
| 1004 |
-
|
| 1005 |
fn=update_dimensions_on_upload,
|
| 1006 |
-
inputs=[
|
| 1007 |
outputs=[width_slider, height_slider],
|
| 1008 |
)
|
| 1009 |
|
|
|
|
| 1010 |
if __name__ == "__main__":
|
| 1011 |
demo.launch(
|
| 1012 |
css=css, theme=purple_theme,
|
|
|
|
| 3 |
import random
|
| 4 |
import torch
|
| 5 |
import spaces
|
|
|
|
|
|
|
| 6 |
from typing import Iterable
|
| 7 |
from PIL import Image, ImageDraw
|
| 8 |
from diffusers import FlowMatchEulerDiscreteScheduler
|
|
|
|
| 114 |
DEFAULT_PROMPT = "Remove the red highlighted object from the scene"
|
| 115 |
|
| 116 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
def burn_boxes_onto_image(pil_image: Image.Image, boxes_json_str: str) -> Image.Image:
|
| 118 |
"""Burn red outline-only rectangles onto the image (no fill)."""
|
| 119 |
import json
|
|
|
|
| 138 |
y2 = int(b["y2"] * h)
|
| 139 |
lx, rx = min(x1, x2), max(x1, x2)
|
| 140 |
ty, by_ = min(y1, y2), max(y1, y2)
|
|
|
|
| 141 |
draw.rectangle([lx, ty, rx, by_], outline=(255, 0, 0), width=bw)
|
| 142 |
|
| 143 |
return img
|
|
|
|
| 145 |
|
| 146 |
@spaces.GPU
|
| 147 |
def infer_object_removal(
|
| 148 |
+
source_image: Image.Image,
|
| 149 |
boxes_json: str,
|
| 150 |
prompt: str,
|
| 151 |
seed: int = 0,
|
|
|
|
| 174 |
print(f"Prompt: {prompt}")
|
| 175 |
print(f"Boxes JSON received: '{boxes_json}'")
|
| 176 |
|
|
|
|
| 177 |
if source_image is None:
|
| 178 |
+
raise gr.Error("Please upload an image first.")
|
| 179 |
|
| 180 |
import json
|
| 181 |
try:
|
|
|
|
| 210 |
return result, seed, marked
|
| 211 |
|
| 212 |
|
| 213 |
+
def update_dimensions_on_upload(image):
|
|
|
|
| 214 |
if image is None:
|
| 215 |
return 1024, 1024
|
| 216 |
original_width, original_height = image.size
|
|
|
|
| 258 |
::-webkit-scrollbar-thumb{background:linear-gradient(135deg,#A855F7,#C084FC);border-radius:4px}
|
| 259 |
::-webkit-scrollbar-thumb:hover{background:linear-gradient(135deg,#9333EA,#A855F7)}
|
| 260 |
|
| 261 |
+
#bbox-draw-wrap{position:relative;border:2px solid #C084FC;border-radius:12px;overflow:hidden;background:#1a1a1a;min-height:420px}
|
| 262 |
#bbox-draw-wrap:hover{border-color:#A855F7}
|
| 263 |
#bbox-draw-canvas{cursor:crosshair;display:block;margin:0 auto}
|
| 264 |
.bbox-hint{background:rgba(168,85,247,.08);border:1px solid #E9D5FF;border-radius:8px;padding:10px 16px;margin:8px 0;font-size:.9rem;color:#6B21A8}
|
| 265 |
.dark .bbox-hint{background:rgba(168,85,247,.15);border-color:rgba(168,85,247,.3);color:#C084FC}
|
| 266 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
.bbox-toolbar-section{
|
| 268 |
display:flex;
|
| 269 |
gap:8px;
|
|
|
|
| 317 |
.bbox-tb-select{background:#6366f1}
|
| 318 |
.bbox-tb-select:hover{background:#818cf8}
|
| 319 |
.bbox-tb-select.active{background:#22c55e;box-shadow:0 0 8px rgba(34,197,94,.5)}
|
| 320 |
+
.bbox-tb-upload{background:#0891b2}
|
| 321 |
+
.bbox-tb-upload:hover{background:#06b6d4}
|
| 322 |
.bbox-tb-del{background:#dc2626}
|
| 323 |
.bbox-tb-del:hover{background:#ef4444}
|
| 324 |
.bbox-tb-undo{background:#7E22CE}
|
| 325 |
.bbox-tb-undo:hover{background:#9333EA}
|
| 326 |
.bbox-tb-clear{background:#be123c}
|
| 327 |
.bbox-tb-clear:hover{background:#e11d48}
|
| 328 |
+
.bbox-tb-remove-img{background:#64748b}
|
| 329 |
+
.bbox-tb-remove-img:hover{background:#94a3b8}
|
| 330 |
|
| 331 |
#bbox-status{position:absolute;top:10px;left:10px;background:rgba(0,0,0,.75);color:#00ff88;padding:5px 10px;border-radius:6px;font-family:'IBM Plex Mono',monospace;font-size:11px;z-index:10;display:none;pointer-events:none}
|
| 332 |
#bbox-count{position:absolute;top:10px;right:10px;background:rgba(147,51,234,.85);color:#fff;padding:4px 10px;border-radius:6px;font-family:'IBM Plex Mono',monospace;font-size:11px;z-index:10;display:none}
|
|
|
|
| 344 |
}
|
| 345 |
.dark #bbox-debug-count{color:#C084FC;background:rgba(168,85,247,.12)}
|
| 346 |
|
| 347 |
+
#boxes-json-input{
|
| 348 |
+
max-height:0!important;
|
| 349 |
+
overflow:hidden!important;
|
| 350 |
+
margin:0!important;
|
| 351 |
+
padding:0!important;
|
| 352 |
+
opacity:0!important;
|
| 353 |
+
pointer-events:none!important;
|
| 354 |
+
position:absolute!important;
|
| 355 |
+
z-index:-1!important;
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
#source-image-component{
|
| 359 |
+
max-height:0!important;
|
| 360 |
+
overflow:hidden!important;
|
| 361 |
+
margin:0!important;
|
| 362 |
+
padding:0!important;
|
| 363 |
+
opacity:0!important;
|
| 364 |
+
pointer-events:none!important;
|
| 365 |
+
position:absolute!important;
|
| 366 |
+
z-index:-1!important;
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
#bbox-file-input{
|
| 370 |
+
display:none;
|
| 371 |
}
|
| 372 |
+
|
| 373 |
+
/* Drop zone overlay */
|
| 374 |
+
#bbox-drop-overlay{
|
| 375 |
+
position:absolute;
|
| 376 |
+
top:0;left:0;right:0;bottom:0;
|
| 377 |
+
background:rgba(147,51,234,.3);
|
| 378 |
+
border:3px dashed #A855F7;
|
| 379 |
+
border-radius:12px;
|
| 380 |
+
display:none;
|
| 381 |
+
align-items:center;
|
| 382 |
+
justify-content:center;
|
| 383 |
+
z-index:20;
|
| 384 |
+
pointer-events:none;
|
| 385 |
+
}
|
| 386 |
+
#bbox-drop-overlay.active{display:flex}
|
| 387 |
+
#bbox-drop-overlay span{
|
| 388 |
+
font-family:'Outfit',sans-serif;
|
| 389 |
+
font-size:18px;
|
| 390 |
+
font-weight:700;
|
| 391 |
+
color:#fff;
|
| 392 |
+
background:rgba(147,51,234,.8);
|
| 393 |
+
padding:12px 24px;
|
| 394 |
+
border-radius:10px;
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
/* Image info bar */
|
| 398 |
+
#bbox-image-info{
|
| 399 |
+
text-align:center;
|
| 400 |
+
padding:4px 12px;
|
| 401 |
+
margin-top:4px;
|
| 402 |
+
font-family:'IBM Plex Mono',monospace;
|
| 403 |
+
font-size:11px;
|
| 404 |
+
color:#9333EA;
|
| 405 |
+
background:rgba(168,85,247,.06);
|
| 406 |
+
border:1px solid #E9D5FF;
|
| 407 |
+
border-radius:6px;
|
| 408 |
+
display:none;
|
| 409 |
+
}
|
| 410 |
+
.dark #bbox-image-info{color:#C084FC;background:rgba(168,85,247,.12);border-color:rgba(168,85,247,.3)}
|
| 411 |
"""
|
| 412 |
|
| 413 |
bbox_drawer_js = r"""
|
|
|
|
| 420 |
const status = document.getElementById('bbox-status');
|
| 421 |
const badge = document.getElementById('bbox-count');
|
| 422 |
const debugCount = document.getElementById('bbox-debug-count');
|
| 423 |
+
const dropOverlay= document.getElementById('bbox-drop-overlay');
|
| 424 |
+
const imageInfo = document.getElementById('bbox-image-info');
|
| 425 |
+
|
| 426 |
+
const btnDraw = document.getElementById('tb-draw');
|
| 427 |
+
const btnSelect = document.getElementById('tb-select');
|
| 428 |
+
const btnUpload = document.getElementById('tb-upload');
|
| 429 |
+
const btnDel = document.getElementById('tb-del');
|
| 430 |
+
const btnUndo = document.getElementById('tb-undo');
|
| 431 |
+
const btnClear = document.getElementById('tb-clear');
|
| 432 |
+
const btnRemoveImg= document.getElementById('tb-remove-img');
|
| 433 |
+
const fileInput = document.getElementById('bbox-file-input');
|
| 434 |
+
|
| 435 |
+
if (!canvas || !wrap || !debugCount || !btnDraw) {
|
| 436 |
console.log('[BBox] waiting for DOM...');
|
| 437 |
setTimeout(initCanvasBbox, 250);
|
| 438 |
return;
|
|
|
|
| 446 |
window.__bboxBoxes = boxes;
|
| 447 |
|
| 448 |
let baseImg = null;
|
| 449 |
+
let originalFile = null;
|
| 450 |
+
let dispW = 512, dispH = 450;
|
| 451 |
let selectedIdx = -1;
|
| 452 |
let mode = 'draw';
|
| 453 |
|
|
|
|
| 460 |
const RED_STROKE_WIDTH = 3;
|
| 461 |
const SEL_STROKE = 'rgba(0,120,255,0.95)';
|
| 462 |
|
| 463 |
+
function n2px(b) {
|
| 464 |
+
return {x1:b.x1*dispW, y1:b.y1*dispH, x2:b.x2*dispW, y2:b.y2*dispH};
|
| 465 |
+
}
|
| 466 |
function px2n(x1,y1,x2,y2) {
|
| 467 |
return {
|
| 468 |
x1: Math.min(x1,x2)/dispW, y1: Math.min(y1,y2)/dispH,
|
|
|
|
| 471 |
}
|
| 472 |
function clamp01(v){return Math.max(0,Math.min(1,v));}
|
| 473 |
function fitSize(nw, nh) {
|
| 474 |
+
const mw = wrap.clientWidth || 512, mh = 550;
|
| 475 |
const r = Math.min(mw/nw, mh/nh, 1);
|
| 476 |
dispW = Math.round(nw*r); dispH = Math.round(nh*r);
|
| 477 |
canvas.width = dispW; canvas.height = dispH;
|
|
|
|
| 499 |
}
|
| 500 |
|
| 501 |
const container = document.getElementById('boxes-json-input');
|
| 502 |
+
if (!container) {
|
| 503 |
+
console.warn('[BBox] #boxes-json-input not in DOM');
|
| 504 |
+
return;
|
| 505 |
+
}
|
| 506 |
const targets = [
|
| 507 |
...container.querySelectorAll('textarea'),
|
| 508 |
+
...container.querySelectorAll('input[type="text"]'),
|
| 509 |
+
...container.querySelectorAll('input:not([type])')
|
| 510 |
];
|
| 511 |
targets.forEach(el => {
|
| 512 |
+
const proto = el.tagName === 'TEXTAREA'
|
| 513 |
+
? HTMLTextAreaElement.prototype
|
| 514 |
+
: HTMLInputElement.prototype;
|
| 515 |
const ns = Object.getOwnPropertyDescriptor(proto, 'value');
|
| 516 |
if (ns && ns.set) {
|
| 517 |
ns.set.call(el, jsonStr);
|
| 518 |
el.dispatchEvent(new Event('input', {bubbles:true, composed:true}));
|
| 519 |
el.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
|
| 520 |
+
el.dispatchEvent(new Event('blur', {bubbles:true, composed:true}));
|
| 521 |
}
|
| 522 |
});
|
| 523 |
}
|
| 524 |
|
| 525 |
+
/* Push image data to hidden Gradio Image component */
|
| 526 |
+
function pushImageToGradio(dataURL) {
|
| 527 |
+
const container = document.getElementById('source-image-component');
|
| 528 |
if (!container) return;
|
| 529 |
+
|
| 530 |
+
/* Convert data URL to File and use Gradio's upload mechanism */
|
| 531 |
+
fetch(dataURL)
|
| 532 |
+
.then(res => res.blob())
|
| 533 |
+
.then(blob => {
|
| 534 |
+
const file = new File([blob], 'canvas-image.png', {type: 'image/png'});
|
| 535 |
+
const dt = new DataTransfer();
|
| 536 |
+
dt.items.add(file);
|
| 537 |
+
|
| 538 |
+
const inp = container.querySelector('input[type="file"]');
|
| 539 |
+
if (inp) {
|
| 540 |
+
inp.files = dt.files;
|
| 541 |
+
inp.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
|
| 542 |
+
console.log('[BBox] pushed image to Gradio component');
|
| 543 |
+
} else {
|
| 544 |
+
/* Fallback: try drop event */
|
| 545 |
+
const dropTarget = container.querySelector('.upload-container, .image-container, [data-testid]') || container;
|
| 546 |
+
const dropEvt = new DragEvent('drop', {bubbles:true, composed:true, dataTransfer: dt});
|
| 547 |
+
dropTarget.dispatchEvent(dropEvt);
|
| 548 |
+
console.log('[BBox] pushed image via drop event');
|
| 549 |
+
}
|
| 550 |
+
});
|
| 551 |
+
}
|
| 552 |
+
|
| 553 |
+
function placeholder() {
|
| 554 |
+
ctx.fillStyle='#2a2a2a'; ctx.fillRect(0,0,dispW,dispH);
|
| 555 |
+
ctx.strokeStyle='#7E22CE'; ctx.lineWidth=2; ctx.setLineDash([8,4]);
|
| 556 |
+
ctx.strokeRect(20,20,dispW-40,dispH-40); ctx.setLineDash([]);
|
| 557 |
+
ctx.fillStyle='#A855F7'; ctx.font='bold 18px Outfit,sans-serif';
|
| 558 |
+
ctx.textAlign='center'; ctx.textBaseline='middle';
|
| 559 |
+
ctx.fillText('π· Drop image here or click Upload',dispW/2,dispH/2-20);
|
| 560 |
+
ctx.font='14px Outfit'; ctx.fillStyle='#888';
|
| 561 |
+
ctx.fillText('Supports PNG, JPG, WEBP',dispW/2,dispH/2+10);
|
| 562 |
+
ctx.font='12px Outfit'; ctx.fillStyle='#666';
|
| 563 |
+
ctx.fillText('Then draw red boxes on objects to remove',dispW/2,dispH/2+32);
|
| 564 |
}
|
| 565 |
|
| 566 |
function redraw(tempRect) {
|
| 567 |
ctx.clearRect(0,0,dispW,dispH);
|
| 568 |
+
if (!baseImg) { placeholder(); updateBadge(); return; }
|
|
|
|
|
|
|
|
|
|
| 569 |
ctx.drawImage(baseImg, 0, 0, dispW, dispH);
|
| 570 |
|
| 571 |
boxes.forEach((b,i) => {
|
| 572 |
const p = n2px(b);
|
| 573 |
const lx=p.x1, ty=p.y1, w=p.x2-p.x1, h=p.y2-p.y1;
|
| 574 |
|
|
|
|
| 575 |
if (i === selectedIdx) {
|
| 576 |
ctx.strokeStyle = SEL_STROKE;
|
| 577 |
ctx.lineWidth = RED_STROKE_WIDTH + 1;
|
|
|
|
| 584 |
ctx.strokeRect(lx, ty, w, h);
|
| 585 |
ctx.setLineDash([]);
|
| 586 |
|
|
|
|
| 587 |
ctx.fillStyle = i===selectedIdx ? 'rgba(0,120,255,0.85)' : 'rgba(255,0,0,0.85)';
|
| 588 |
ctx.font = 'bold 11px IBM Plex Mono,monospace';
|
| 589 |
ctx.textAlign = 'left'; ctx.textBaseline = 'top';
|
|
|
|
| 596 |
if (i === selectedIdx) drawHandles(p);
|
| 597 |
});
|
| 598 |
|
|
|
|
| 599 |
if (tempRect) {
|
| 600 |
const rx = Math.min(tempRect.x1,tempRect.x2);
|
| 601 |
const ry = Math.min(tempRect.y1,tempRect.y2);
|
|
|
|
| 671 |
}
|
| 672 |
function hideStatus() { status.style.display = 'none'; }
|
| 673 |
|
| 674 |
+
function updateImageInfo() {
|
| 675 |
+
if (baseImg && imageInfo) {
|
| 676 |
+
imageInfo.style.display = 'block';
|
| 677 |
+
imageInfo.textContent = 'πΌ Image: ' + baseImg.naturalWidth + 'Γ' + baseImg.naturalHeight + ' px';
|
| 678 |
+
} else if (imageInfo) {
|
| 679 |
+
imageInfo.style.display = 'none';
|
| 680 |
+
}
|
| 681 |
+
}
|
| 682 |
+
|
| 683 |
+
/* ---- Image loading ---- */
|
| 684 |
+
function loadImageFromSource(src) {
|
| 685 |
+
const img = new window.Image();
|
| 686 |
+
img.crossOrigin = 'anonymous';
|
| 687 |
+
img.onload = () => {
|
| 688 |
+
baseImg = img;
|
| 689 |
+
boxes.length = 0;
|
| 690 |
+
window.__bboxBoxes = boxes;
|
| 691 |
+
selectedIdx = -1;
|
| 692 |
+
fitSize(img.naturalWidth, img.naturalHeight);
|
| 693 |
+
syncToGradio(); redraw(); hideStatus();
|
| 694 |
+
updateImageInfo();
|
| 695 |
+
showStatus('Image loaded β draw boxes now!');
|
| 696 |
+
setTimeout(hideStatus, 2000);
|
| 697 |
+
console.log('[BBox] loaded image', img.naturalWidth, 'x', img.naturalHeight);
|
| 698 |
+
|
| 699 |
+
/* Push to hidden Gradio component */
|
| 700 |
+
const tmpCanvas = document.createElement('canvas');
|
| 701 |
+
tmpCanvas.width = img.naturalWidth;
|
| 702 |
+
tmpCanvas.height = img.naturalHeight;
|
| 703 |
+
const tmpCtx = tmpCanvas.getContext('2d');
|
| 704 |
+
tmpCtx.drawImage(img, 0, 0);
|
| 705 |
+
const dataURL = tmpCanvas.toDataURL('image/png');
|
| 706 |
+
pushImageToGradio(dataURL);
|
| 707 |
+
};
|
| 708 |
+
img.onerror = () => {
|
| 709 |
+
console.warn('[BBox] image load failed');
|
| 710 |
+
showStatus('Failed to load image');
|
| 711 |
+
};
|
| 712 |
+
img.src = src;
|
| 713 |
+
}
|
| 714 |
+
|
| 715 |
+
function loadImageFromFile(file) {
|
| 716 |
+
if (!file || !file.type.startsWith('image/')) return;
|
| 717 |
+
originalFile = file;
|
| 718 |
+
const reader = new FileReader();
|
| 719 |
+
reader.onload = (ev) => {
|
| 720 |
+
loadImageFromSource(ev.target.result);
|
| 721 |
+
};
|
| 722 |
+
reader.readAsDataURL(file);
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
function removeImage() {
|
| 726 |
+
baseImg = null;
|
| 727 |
+
originalFile = null;
|
| 728 |
+
boxes.length = 0;
|
| 729 |
+
window.__bboxBoxes = boxes;
|
| 730 |
+
selectedIdx = -1;
|
| 731 |
+
fitSize(512, 450);
|
| 732 |
+
syncToGradio(); redraw(); hideStatus();
|
| 733 |
+
updateImageInfo();
|
| 734 |
+
|
| 735 |
+
/* Clear hidden Gradio component */
|
| 736 |
+
const container = document.getElementById('source-image-component');
|
| 737 |
+
if (container) {
|
| 738 |
+
const clearBtn = container.querySelector('button[aria-label="Clear"], button.clear-btn, [data-testid="clear-btn"]');
|
| 739 |
+
if (clearBtn) clearBtn.click();
|
| 740 |
+
}
|
| 741 |
+
}
|
| 742 |
+
|
| 743 |
+
/* ---- File input button ---- */
|
| 744 |
+
btnUpload.addEventListener('click', () => {
|
| 745 |
+
fileInput.click();
|
| 746 |
+
});
|
| 747 |
+
fileInput.addEventListener('change', (e) => {
|
| 748 |
+
if (e.target.files && e.target.files[0]) {
|
| 749 |
+
loadImageFromFile(e.target.files[0]);
|
| 750 |
+
}
|
| 751 |
+
e.target.value = '';
|
| 752 |
+
});
|
| 753 |
+
|
| 754 |
+
/* ---- Remove image button ---- */
|
| 755 |
+
btnRemoveImg.addEventListener('click', removeImage);
|
| 756 |
+
|
| 757 |
+
/* ---- Drag & Drop on canvas ---- */
|
| 758 |
+
wrap.addEventListener('dragenter', (e) => {
|
| 759 |
+
e.preventDefault(); e.stopPropagation();
|
| 760 |
+
dropOverlay.classList.add('active');
|
| 761 |
+
});
|
| 762 |
+
wrap.addEventListener('dragover', (e) => {
|
| 763 |
+
e.preventDefault(); e.stopPropagation();
|
| 764 |
+
dropOverlay.classList.add('active');
|
| 765 |
+
});
|
| 766 |
+
wrap.addEventListener('dragleave', (e) => {
|
| 767 |
+
e.preventDefault(); e.stopPropagation();
|
| 768 |
+
if (!wrap.contains(e.relatedTarget)) {
|
| 769 |
+
dropOverlay.classList.remove('active');
|
| 770 |
+
}
|
| 771 |
+
});
|
| 772 |
+
wrap.addEventListener('drop', (e) => {
|
| 773 |
+
e.preventDefault(); e.stopPropagation();
|
| 774 |
+
dropOverlay.classList.remove('active');
|
| 775 |
+
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
|
| 776 |
+
loadImageFromFile(e.dataTransfer.files[0]);
|
| 777 |
+
}
|
| 778 |
+
});
|
| 779 |
+
|
| 780 |
+
/* ---- Also allow click on canvas to upload when no image ---- */
|
| 781 |
+
canvas.addEventListener('dblclick', (e) => {
|
| 782 |
+
if (!baseImg) {
|
| 783 |
+
e.preventDefault();
|
| 784 |
+
fileInput.click();
|
| 785 |
+
}
|
| 786 |
+
});
|
| 787 |
+
|
| 788 |
+
/* ---- Drawing handlers ---- */
|
| 789 |
function onDown(e) {
|
| 790 |
if (!baseImg) return;
|
| 791 |
e.preventDefault();
|
|
|
|
| 914 |
canvas.addEventListener('touchend', onUp, {passive:false});
|
| 915 |
canvas.addEventListener('touchcancel',(e)=>{e.preventDefault();dragging=false;redraw();},{passive:false});
|
| 916 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 917 |
btnDraw.addEventListener('click', ()=>setMode('draw'));
|
| 918 |
btnSelect.addEventListener('click', ()=>setMode('select'));
|
| 919 |
|
|
|
|
| 947 |
syncToGradio(); redraw(); hideStatus();
|
| 948 |
});
|
| 949 |
|
| 950 |
+
/* Periodic sync */
|
| 951 |
+
setInterval(() => { syncToGradio(); }, 500);
|
| 952 |
+
|
| 953 |
new ResizeObserver(() => {
|
| 954 |
if (baseImg) { fitSize(baseImg.naturalWidth, baseImg.naturalHeight); redraw(); }
|
| 955 |
}).observe(wrap);
|
| 956 |
|
| 957 |
setMode('draw');
|
| 958 |
+
fitSize(512,450); redraw();
|
| 959 |
syncToGradio();
|
| 960 |
}
|
| 961 |
|
|
|
|
| 968 |
gr.Markdown("# **QIE-Object-Remover-Bbox**", elem_id="main-title")
|
| 969 |
gr.Markdown(
|
| 970 |
"Perform diverse image edits using a specialized [LoRA](https://huggingface.co/prithivMLmods/QIE-2511-Object-Remover-v2). "
|
| 971 |
+
"Upload an image directly into the canvas below, draw red bounding boxes over the objects you want to remove, and click Remove Object. "
|
| 972 |
"Multiple boxes supported. Select, move, resize or delete individual boxes.",
|
| 973 |
elem_id="subtitle",
|
| 974 |
)
|
| 975 |
|
| 976 |
with gr.Row():
|
| 977 |
with gr.Column(scale=1):
|
| 978 |
+
# Hidden source image component (receives image data from canvas JS)
|
| 979 |
+
source_image = gr.Image(
|
| 980 |
+
label="Upload Image",
|
| 981 |
+
type="pil",
|
| 982 |
+
height=350,
|
| 983 |
+
elem_id="source-image-component",
|
| 984 |
+
visible=False,
|
| 985 |
)
|
| 986 |
|
| 987 |
gr.Markdown("# **Bbox Edit Controller**")
|
| 988 |
+
gr.HTML(
|
| 989 |
+
'<div class="bbox-hint">'
|
| 990 |
+
"π· <b>Upload:</b> Click <b>Upload</b> button, drag & drop onto the canvas, or <b>double-click</b> the canvas. "
|
| 991 |
+
"<b>Draw mode:</b> Click & drag to create red rectangles. "
|
| 992 |
+
"<b>Select mode:</b> Click a box to select it β drag to <b>move</b>, "
|
| 993 |
+
"drag handles to <b>resize</b>. Use <b>Delete Selected</b> to remove one box."
|
| 994 |
+
"</div>"
|
| 995 |
+
)
|
| 996 |
|
| 997 |
gr.HTML(
|
| 998 |
"""
|
| 999 |
<div id="bbox-draw-wrap">
|
| 1000 |
+
<canvas id="bbox-draw-canvas" width="512" height="450"></canvas>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1001 |
<div id="bbox-status"></div>
|
| 1002 |
<div id="bbox-count"></div>
|
| 1003 |
+
<div id="bbox-drop-overlay"><span>π· Drop image here</span></div>
|
| 1004 |
</div>
|
| 1005 |
+
<input type="file" id="bbox-file-input" accept="image/*">
|
| 1006 |
"""
|
| 1007 |
)
|
| 1008 |
|
| 1009 |
gr.HTML(
|
| 1010 |
"""
|
| 1011 |
<div class="bbox-toolbar-section">
|
| 1012 |
+
<span class="toolbar-label">π· Image:</span>
|
| 1013 |
+
<button id="tb-upload" class="bbox-tb-upload" title="Upload an image">π Upload</button>
|
| 1014 |
+
<button id="tb-remove-img" class="bbox-tb-remove-img" title="Remove current image">π Remove Image</button>
|
| 1015 |
+
<div class="toolbar-divider"></div>
|
| 1016 |
<span class="toolbar-label">π Tools:</span>
|
| 1017 |
<button id="tb-draw" class="bbox-tb-draw active" title="Draw new boxes">βοΈ Draw</button>
|
| 1018 |
<button id="tb-select" class="bbox-tb-select" title="Select / move / resize">π² Select</button>
|
| 1019 |
<div class="toolbar-divider"></div>
|
| 1020 |
<span class="toolbar-label">Actions:</span>
|
| 1021 |
+
<button id="tb-del" class="bbox-tb-del" title="Delete selected box">β Delete Selected</button>
|
| 1022 |
<button id="tb-undo" class="bbox-tb-undo" title="Remove last box">β© Undo</button>
|
| 1023 |
<button id="tb-clear" class="bbox-tb-clear" title="Remove all boxes">π Clear All</button>
|
|
|
|
|
|
|
| 1024 |
</div>
|
| 1025 |
"""
|
| 1026 |
)
|
| 1027 |
+
|
| 1028 |
+
gr.HTML('<div id="bbox-image-info"></div>')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1029 |
gr.HTML('<div id="bbox-debug-count">\u2B1C No boxes drawn yet</div>')
|
| 1030 |
|
| 1031 |
boxes_json = gr.Textbox(
|
| 1032 |
value="[]",
|
| 1033 |
+
visible=True,
|
| 1034 |
+
interactive=True,
|
| 1035 |
elem_id="boxes-json-input",
|
| 1036 |
+
label="boxes-json",
|
|
|
|
| 1037 |
)
|
| 1038 |
|
| 1039 |
prompt = gr.Textbox(
|
|
|
|
| 1065 |
"[prithivMLmods](https://huggingface.co/prithivMLmods). "
|
| 1066 |
"Adapter: [QIE-2511-Object-Remover-v2]"
|
| 1067 |
"(https://huggingface.co/prithivMLmods/QIE-2511-Object-Remover-v2). "
|
| 1068 |
+
"More adapters β [Qwen-Image-Edit-LoRAs]"
|
| 1069 |
"(https://huggingface.co/models?other=base_model:adapter:Qwen/Qwen-Image-Edit-2509)."
|
| 1070 |
)
|
| 1071 |
|
|
|
|
| 1073 |
|
| 1074 |
run_btn.click(
|
| 1075 |
fn=infer_object_removal,
|
| 1076 |
+
inputs=[source_image, boxes_json, prompt, seed, randomize_seed,
|
| 1077 |
guidance_scale, num_inference_steps, height_slider, width_slider],
|
| 1078 |
outputs=[result, seed, preview],
|
| 1079 |
+
js="""(src, bj, p, s, rs, gs, nis, h, w) => {
|
| 1080 |
const boxes = window.__bboxBoxes || [];
|
| 1081 |
const json = JSON.stringify(boxes);
|
| 1082 |
console.log('[BBox] submitting', boxes.length, 'boxes:', json);
|
| 1083 |
+
return [src, json, p, s, rs, gs, nis, h, w];
|
| 1084 |
}""",
|
| 1085 |
)
|
| 1086 |
|
| 1087 |
+
source_image.upload(
|
| 1088 |
fn=update_dimensions_on_upload,
|
| 1089 |
+
inputs=[source_image],
|
| 1090 |
outputs=[width_slider, height_slider],
|
| 1091 |
)
|
| 1092 |
|
| 1093 |
+
|
| 1094 |
if __name__ == "__main__":
|
| 1095 |
demo.launch(
|
| 1096 |
css=css, theme=purple_theme,
|