import gradio as gr import numpy as np import random import torch import spaces from typing import Iterable from PIL import Image, ImageDraw from diffusers import FlowMatchEulerDiscreteScheduler from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3 from gradio.themes import Soft from gradio.themes.utils import colors, fonts, sizes colors.purple = colors.Color( name="purple", c50="#FAF5FF", c100="#F3E8FF", c200="#E9D5FF", c300="#DAB2FF", c400="#C084FC", c500="#A855F7", c600="#9333EA", c700="#7E22CE", c800="#6B21A8", c900="#581C87", c950="#3B0764", ) class PurpleTheme(Soft): def __init__( self, *, primary_hue: colors.Color | str = colors.gray, secondary_hue: colors.Color | str = colors.purple, neutral_hue: colors.Color | str = colors.slate, text_size: sizes.Size | str = sizes.text_lg, font: fonts.Font | str | Iterable[fonts.Font | str] = ( fonts.GoogleFont("Outfit"), "Arial", "sans-serif", ), font_mono: fonts.Font | str | Iterable[fonts.Font | str] = ( fonts.GoogleFont("IBM Plex Mono"), "ui-monospace", "monospace", ), ): super().__init__( primary_hue=primary_hue, secondary_hue=secondary_hue, neutral_hue=neutral_hue, text_size=text_size, font=font, font_mono=font_mono, ) super().set( background_fill_primary="*primary_50", background_fill_primary_dark="*primary_900", body_background_fill="linear-gradient(135deg, *primary_200, *primary_100)", body_background_fill_dark="linear-gradient(135deg, *primary_900, *primary_800)", button_primary_text_color="white", button_primary_text_color_hover="white", button_primary_background_fill="linear-gradient(90deg, *secondary_500, *secondary_600)", button_primary_background_fill_hover="linear-gradient(90deg, *secondary_600, *secondary_700)", button_primary_background_fill_dark="linear-gradient(90deg, *secondary_600, *secondary_700)", button_primary_background_fill_hover_dark="linear-gradient(90deg, *secondary_500, *secondary_600)", button_secondary_text_color="black", button_secondary_text_color_hover="white", button_secondary_background_fill="linear-gradient(90deg, *primary_300, *primary_300)", button_secondary_background_fill_hover="linear-gradient(90deg, *primary_400, *primary_400)", button_secondary_background_fill_dark="linear-gradient(90deg, *primary_500, *primary_600)", button_secondary_background_fill_hover_dark="linear-gradient(90deg, *primary_500, *primary_500)", slider_color="*secondary_500", slider_color_dark="*secondary_600", block_title_text_weight="600", block_border_width="3px", block_shadow="*shadow_drop_lg", button_primary_shadow="*shadow_drop_lg", button_large_padding="11px", color_accent_soft="*primary_100", block_label_background_fill="*primary_200", ) purple_theme = PurpleTheme() MAX_SEED = np.iinfo(np.int32).max dtype = torch.bfloat16 device = "cuda" if torch.cuda.is_available() else "cpu" pipe = QwenImageEditPlusPipeline.from_pretrained( "Qwen/Qwen-Image-Edit-2509", transformer=QwenImageTransformer2DModel.from_pretrained( "prithivMLmods/Qwen-Image-Edit-Rapid-AIO-V4", torch_dtype=dtype, device_map="cuda", ), torch_dtype=dtype, ).to(device) try: pipe.transformer.set_attn_processor(QwenDoubleStreamAttnProcessorFA3()) print("Flash Attention 3 Processor set successfully.") except Exception as e: print(f"Warning: Could not set FA3 processor: {e}") ADAPTER_SPECS = { "Object-Remover": { "repo": "prithivMLmods/QIE-2511-Object-Remover-v2", "weights": "Qwen-Image-Edit-2511-Object-Remover-v2-9200.safetensors", "adapter_name": "object-remover", }, } loaded = False DEFAULT_PROMPT = "Remove the red highlighted object from the scene" def burn_boxes_onto_image(pil_image: Image.Image, boxes_json_str: str) -> Image.Image: import json if not pil_image: return pil_image try: boxes = json.loads(boxes_json_str) if boxes_json_str and boxes_json_str.strip() else [] except Exception: boxes = [] if not boxes: return pil_image img = pil_image.copy().convert("RGBA") w, h = img.size overlay = Image.new("RGBA", img.size, (0, 0, 0, 0)) draw = ImageDraw.Draw(overlay) bw = max(3, w // 250) for b in boxes: x1 = int(b["x1"] * w) y1 = int(b["y1"] * h) x2 = int(b["x2"] * w) y2 = int(b["y2"] * h) lx, rx = min(x1, x2), max(x1, x2) ty, by_ = min(y1, y2), max(y1, y2) draw.rectangle([lx, ty, rx, by_], fill=(255, 0, 0, 90)) draw.rectangle([lx, ty, rx, by_], outline=(255, 0, 0, 255), width=bw) return Image.alpha_composite(img, overlay).convert("RGB") @spaces.GPU def infer_object_removal( source_image: Image.Image, boxes_json: str, prompt: str, seed: int = 0, randomize_seed: bool = True, guidance_scale: float = 1.0, num_inference_steps: int = 4, height: int = 1024, width: int = 1024, ): global loaded progress = gr.Progress(track_tqdm=True) if not loaded: pipe.load_lora_weights( ADAPTER_SPECS["Object-Remover"]["repo"], weight_name=ADAPTER_SPECS["Object-Remover"]["weights"], adapter_name=ADAPTER_SPECS["Object-Remover"]["adapter_name"], ) pipe.set_adapters( [ADAPTER_SPECS["Object-Remover"]["adapter_name"]], adapter_weights=[1.0] ) loaded = True if not prompt or prompt.strip() == "": prompt = DEFAULT_PROMPT print(f"Prompt: {prompt}") print(f"Boxes JSON received: '{boxes_json}'") if source_image is None: raise gr.Error("Please upload an image first.") import json try: boxes = json.loads(boxes_json) if boxes_json and boxes_json.strip() else [] except Exception as e: print(f"JSON parse error: {e}") boxes = [] if not boxes: raise gr.Error("Please draw at least one bounding box on the image.") progress(0.3, desc="Burning red boxes onto image...") marked = burn_boxes_onto_image(source_image, boxes_json) progress(0.5, desc="Running object removal inference...") if randomize_seed: seed = random.randint(0, MAX_SEED) generator = torch.Generator(device=device).manual_seed(seed) result = pipe( image=[marked], prompt=prompt, height=height if height != 0 else None, width=width if width != 0 else None, num_inference_steps=num_inference_steps, generator=generator, guidance_scale=guidance_scale, num_images_per_prompt=1, ).images[0] return result, seed, marked def update_dimensions_on_upload(image): if image is None: return 1024, 1024 original_width, original_height = image.size if original_width > original_height: new_width = 1024 aspect_ratio = original_height / original_width new_height = int(new_width * aspect_ratio) else: new_height = 1024 aspect_ratio = original_width / original_height new_width = int(new_height * aspect_ratio) new_width = (new_width // 8) * 8 new_height = (new_height // 8) * 8 return new_width, new_height css = r""" @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap'); body,.gradio-container{background-color:#FAF5FF!important;background-image:linear-gradient(#E9D5FF 1px,transparent 1px),linear-gradient(90deg,#E9D5FF 1px,transparent 1px)!important;background-size:40px 40px!important;font-family:'Outfit',sans-serif!important} .dark body,.dark .gradio-container{background-color:#1a1a1a!important;background-image:linear-gradient(rgba(168,85,247,.1) 1px,transparent 1px),linear-gradient(90deg,rgba(168,85,247,.1) 1px,transparent 1px)!important;background-size:40px 40px!important} #col-container{margin:0 auto;max-width:1200px} #main-title{text-align:center!important;padding:1rem 0 .5rem 0} #main-title h1{font-size:2.4em!important;font-weight:700!important;background:linear-gradient(135deg,#A855F7 0%,#C084FC 50%,#9333EA 100%);background-size:200% 200%;-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;animation:gradient-shift 4s ease infinite;letter-spacing:-.02em} @keyframes gradient-shift{0%,100%{background-position:0% 50%}50%{background-position:100% 50%}} #subtitle{text-align:center!important;margin-bottom:1.5rem} #subtitle p{margin:0 auto;color:#666;font-size:1rem;text-align:center!important} #subtitle a{color:#A855F7!important;text-decoration:none;font-weight:500} #subtitle a:hover{text-decoration:underline} .gradio-group{background:rgba(255,255,255,.9)!important;border:2px solid #E9D5FF!important;border-radius:12px!important;box-shadow:0 4px 24px rgba(168,85,247,.08)!important;backdrop-filter:blur(10px);transition:all .3s ease} .gradio-group:hover{box-shadow:0 8px 32px rgba(168,85,247,.12)!important;border-color:#C084FC!important} .dark .gradio-group{background:rgba(30,30,30,.9)!important;border-color:rgba(168,85,247,.3)!important} .primary{border-radius:8px!important;font-weight:600!important;letter-spacing:.02em!important;transition:all .3s ease!important} .primary:hover{transform:translateY(-2px)!important} .gradio-textbox textarea{font-family:'IBM Plex Mono',monospace!important;font-size:.95rem!important;line-height:1.7!important;background:rgba(255,255,255,.95)!important;border:1px solid #E9D5FF!important;border-radius:8px!important} .gradio-accordion{border-radius:10px!important;border:1px solid #E9D5FF!important} .gradio-accordion>.label-wrap{background:rgba(168,85,247,.03)!important;border-radius:10px!important} footer{display:none!important} @keyframes fadeIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}} .gradio-row{animation:fadeIn .4s ease-out} label{font-weight:600!important;color:#333!important} .dark label{color:#eee!important} .gradio-slider input[type="range"]{accent-color:#A855F7!important} ::-webkit-scrollbar{width:8px;height:8px} ::-webkit-scrollbar-track{background:rgba(168,85,247,.05);border-radius:4px} ::-webkit-scrollbar-thumb{background:linear-gradient(135deg,#A855F7,#C084FC);border-radius:4px} ::-webkit-scrollbar-thumb:hover{background:linear-gradient(135deg,#9333EA,#A855F7)} #bbox-draw-wrap{position:relative;border:2px solid #C084FC;border-radius:12px;overflow:hidden;background:#1a1a1a;min-height:420px} #bbox-draw-wrap:hover{border-color:#A855F7} #bbox-draw-canvas{cursor:crosshair;display:block;margin:0 auto} .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} .dark .bbox-hint{background:rgba(168,85,247,.15);border-color:rgba(168,85,247,.3);color:#C084FC} .bbox-toolbar-section{ display:flex; gap:8px; flex-wrap:wrap; justify-content:center; align-items:center; padding:12px 16px; margin-top:10px; background:rgba(255,255,255,.92); border:2px solid #E9D5FF; border-radius:10px; box-shadow:0 2px 12px rgba(168,85,247,.08); } .dark .bbox-toolbar-section{ background:rgba(30,30,30,.9); border-color:rgba(168,85,247,.3); } .bbox-toolbar-section .toolbar-label{ font-family:'Outfit',sans-serif; font-weight:600; font-size:13px; color:#6B21A8; margin-right:6px; user-select:none; } .dark .bbox-toolbar-section .toolbar-label{color:#C084FC} .bbox-toolbar-section .toolbar-divider{ width:1px; height:28px; background:#E9D5FF; margin:0 4px; } .dark .bbox-toolbar-section .toolbar-divider{background:rgba(168,85,247,.3)} .bbox-toolbar-section button{ color:#fff; border:none; padding:7px 15px; border-radius:7px; cursor:pointer; font-family:'Outfit',sans-serif; font-weight:600; font-size:13px; box-shadow:0 2px 5px rgba(0,0,0,.15); transition:background .2s,transform .15s,box-shadow .2s; } .bbox-toolbar-section button:hover{transform:translateY(-1px);box-shadow:0 4px 10px rgba(0,0,0,.2)} .bbox-toolbar-section button:active{transform:translateY(0)} .bbox-tb-draw{background:#9333EA} .bbox-tb-draw:hover{background:#A855F7} .bbox-tb-draw.active{background:#22c55e;box-shadow:0 0 8px rgba(34,197,94,.5)} .bbox-tb-select{background:#6366f1} .bbox-tb-select:hover{background:#818cf8} .bbox-tb-select.active{background:#22c55e;box-shadow:0 0 8px rgba(34,197,94,.5)} .bbox-tb-del{background:#dc2626} .bbox-tb-del:hover{background:#ef4444} .bbox-tb-undo{background:#7E22CE} .bbox-tb-undo:hover{background:#9333EA} .bbox-tb-clear{background:#be123c} .bbox-tb-clear:hover{background:#e11d48} #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} #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} #bbox-debug-count{ text-align:center; padding:6px 12px; margin-top:6px; font-family:'IBM Plex Mono',monospace; font-size:12px; color:#6B21A8; background:rgba(168,85,247,.06); border:1px dashed #C084FC; border-radius:6px; } .dark #bbox-debug-count{color:#C084FC;background:rgba(168,85,247,.12)} /* ===== FIX: hide the sync textbox visually but keep it in the DOM ===== */ #boxes-json-input{ max-height:0!important; overflow:hidden!important; margin:0!important; padding:0!important; opacity:0!important; pointer-events:none!important; position:absolute!important; z-index:-1!important; } """ bbox_drawer_js = r""" () => { function initCanvasBbox() { if (window.__bboxInitDone) return; const canvas = document.getElementById('bbox-draw-canvas'); const wrap = document.getElementById('bbox-draw-wrap'); const status = document.getElementById('bbox-status'); const badge = document.getElementById('bbox-count'); const debugCount = document.getElementById('bbox-debug-count'); const btnDraw = document.getElementById('tb-draw'); const btnSelect = document.getElementById('tb-select'); const btnDel = document.getElementById('tb-del'); const btnUndo = document.getElementById('tb-undo'); const btnClear = document.getElementById('tb-clear'); if (!canvas || !wrap || !debugCount || !btnDraw) { console.log('[BBox] waiting for DOM …'); setTimeout(initCanvasBbox, 250); return; } window.__bboxInitDone = true; console.log('[BBox] canvas init OK'); const ctx = canvas.getContext('2d'); let boxes = []; window.__bboxBoxes = boxes; let baseImg = null; let dispW = 512, dispH = 400; let selectedIdx = -1; let mode = 'draw'; let dragging = false; let dragType = null; let dragStart = {x:0, y:0}; let dragOrig = null; const HANDLE = 7; /* ── helpers ── */ function n2px(b) { return {x1:b.x1*dispW, y1:b.y1*dispH, x2:b.x2*dispW, y2:b.y2*dispH}; } function px2n(x1,y1,x2,y2) { return { x1: Math.min(x1,x2)/dispW, y1: Math.min(y1,y2)/dispH, x2: Math.max(x1,x2)/dispW, y2: Math.max(y1,y2)/dispH }; } function clamp01(v){return Math.max(0,Math.min(1,v));} function fitSize(nw, nh) { const mw = wrap.clientWidth || 512, mh = 500; const r = Math.min(mw/nw, mh/nh, 1); dispW = Math.round(nw*r); dispH = Math.round(nh*r); canvas.width = dispW; canvas.height = dispH; canvas.style.width = dispW+'px'; canvas.style.height = dispH+'px'; } function canvasXY(e) { const r = canvas.getBoundingClientRect(); const cx = e.touches ? e.touches[0].clientX : e.clientX; const cy = e.touches ? e.touches[0].clientY : e.clientY; return {x: Math.max(0,Math.min(dispW, cx-r.left)), y: Math.max(0,Math.min(dispH, cy-r.top))}; } /* ── FIX: always update debug + badge, then try textbox ── */ function syncToGradio() { window.__bboxBoxes = boxes; const jsonStr = JSON.stringify(boxes); /* 1) update on-screen debug no matter what */ if (debugCount) { debugCount.textContent = boxes.length > 0 ? '\u2705 ' + boxes.length + ' box' + (boxes.length > 1 ? 'es' : '') + ' ready | JSON: ' + jsonStr.substring(0,80) + (jsonStr.length > 80 ? '\u2026' : '') : '\u2B1C No boxes drawn yet'; } /* 2) try to push into the Gradio Textbox (best-effort) */ const container = document.getElementById('boxes-json-input'); if (!container) { console.warn('[BBox] #boxes-json-input not in DOM (hidden?)'); return; } const targets = [ ...container.querySelectorAll('textarea'), ...container.querySelectorAll('input[type="text"]'), ...container.querySelectorAll('input:not([type])') ]; targets.forEach(el => { const proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype; const ns = Object.getOwnPropertyDescriptor(proto, 'value'); if (ns && ns.set) { ns.set.call(el, jsonStr); el.dispatchEvent(new Event('input', {bubbles:true, composed:true})); el.dispatchEvent(new Event('change', {bubbles:true, composed:true})); el.dispatchEvent(new Event('blur', {bubbles:true, composed:true})); } }); } /* ── draw ── */ function placeholder() { ctx.fillStyle='#2a2a2a'; ctx.fillRect(0,0,dispW,dispH); ctx.strokeStyle='#444'; ctx.lineWidth=2; ctx.setLineDash([8,4]); ctx.strokeRect(20,20,dispW-40,dispH-40); ctx.setLineDash([]); ctx.fillStyle='#888'; ctx.font='16px Outfit,sans-serif'; ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.fillText('Upload an image above first',dispW/2,dispH/2-10); ctx.font='13px Outfit'; ctx.fillStyle='#666'; ctx.fillText('Then draw red boxes on objects to remove',dispW/2,dispH/2+14); } function redraw(tempRect) { ctx.clearRect(0,0,dispW,dispH); if (!baseImg) { placeholder(); updateBadge(); return; } ctx.drawImage(baseImg, 0, 0, dispW, dispH); boxes.forEach((b,i) => { const p = n2px(b); const lx=p.x1, ty=p.y1, w=p.x2-p.x1, h=p.y2-p.y1; ctx.fillStyle = 'rgba(255,0,0,0.25)'; ctx.fillRect(lx,ty,w,h); if (i === selectedIdx) { ctx.strokeStyle = 'rgba(0,120,255,0.95)'; ctx.lineWidth = 2.5; ctx.setLineDash([6,3]); } else { ctx.strokeStyle = 'rgba(255,0,0,0.9)'; ctx.lineWidth = 2.5; ctx.setLineDash([]); } ctx.strokeRect(lx,ty,w,h); ctx.setLineDash([]); ctx.fillStyle = i===selectedIdx ? 'rgba(0,120,255,0.85)' : 'rgba(255,0,0,0.85)'; ctx.font = 'bold 11px IBM Plex Mono,monospace'; ctx.textAlign = 'left'; ctx.textBaseline = 'top'; const label = '#'+(i+1); const tw = ctx.measureText(label).width; ctx.fillRect(lx, ty-16, tw+6, 16); ctx.fillStyle = '#fff'; ctx.fillText(label, lx+3, ty-14); if (i === selectedIdx) drawHandles(p); }); if (tempRect) { const rx = Math.min(tempRect.x1,tempRect.x2); const ry = Math.min(tempRect.y1,tempRect.y2); const rw = Math.abs(tempRect.x2-tempRect.x1); const rh = Math.abs(tempRect.y2-tempRect.y1); ctx.fillStyle='rgba(255,0,0,0.12)'; ctx.fillRect(rx,ry,rw,rh); ctx.strokeStyle='rgba(255,0,0,0.7)'; ctx.lineWidth=2; ctx.setLineDash([6,3]); ctx.strokeRect(rx,ry,rw,rh); ctx.setLineDash([]); } updateBadge(); } function drawHandles(p) { const pts = handlePoints(p); ctx.fillStyle = 'rgba(0,120,255,0.9)'; ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.5; for (const k in pts) { const h = pts[k]; ctx.fillRect(h.x-HANDLE, h.y-HANDLE, HANDLE*2, HANDLE*2); ctx.strokeRect(h.x-HANDLE, h.y-HANDLE, HANDLE*2, HANDLE*2); } } function handlePoints(p) { const mx = (p.x1+p.x2)/2, my = (p.y1+p.y2)/2; return { tl:{x:p.x1,y:p.y1}, tc:{x:mx,y:p.y1}, tr:{x:p.x2,y:p.y1}, ml:{x:p.x1,y:my}, mr:{x:p.x2,y:my}, bl:{x:p.x1,y:p.y2}, bc:{x:mx,y:p.y2}, br:{x:p.x2,y:p.y2} }; } function hitHandle(px, py, boxIdx) { if (boxIdx < 0) return null; const p = n2px(boxes[boxIdx]); const pts = handlePoints(p); for (const k in pts) { if (Math.abs(px-pts[k].x) <= HANDLE+2 && Math.abs(py-pts[k].y) <= HANDLE+2) return k; } return null; } function hitBox(px, py) { for (let i = boxes.length-1; i >= 0; i--) { const p = n2px(boxes[i]); if (px >= p.x1 && px <= p.x2 && py >= p.y1 && py <= p.y2) return i; } return -1; } function updateBadge() { if (boxes.length > 0) { badge.style.display = 'block'; badge.textContent = boxes.length + ' box' + (boxes.length>1?'es':''); } else { badge.style.display = 'none'; } } function setMode(m) { mode = m; btnDraw.classList.toggle('active', m==='draw'); btnSelect.classList.toggle('active', m==='select'); canvas.style.cursor = m==='draw' ? 'crosshair' : 'default'; if (m==='draw') selectedIdx = -1; redraw(); } function showStatus(txt) { status.textContent = txt; status.style.display = 'block'; } function hideStatus() { status.style.display = 'none'; } /* ── pointer events ── */ function onDown(e) { if (!baseImg) return; e.preventDefault(); const {x, y} = canvasXY(e); if (mode === 'draw') { dragging = true; dragType = 'new'; dragStart = {x, y}; selectedIdx = -1; } else { if (selectedIdx >= 0) { const h = hitHandle(x, y, selectedIdx); if (h) { dragging = true; dragType = h; dragStart = {x, y}; dragOrig = {...boxes[selectedIdx]}; showStatus('Resizing box #'+(selectedIdx+1)); return; } } const hi = hitBox(x, y); if (hi >= 0) { selectedIdx = hi; const h2 = hitHandle(x, y, selectedIdx); if (h2) { dragging = true; dragType = h2; dragStart = {x, y}; dragOrig = {...boxes[selectedIdx]}; showStatus('Resizing box #'+(selectedIdx+1)); redraw(); return; } dragging = true; dragType = 'move'; dragStart = {x, y}; dragOrig = {...boxes[selectedIdx]}; showStatus('Moving box #'+(selectedIdx+1)); } else { selectedIdx = -1; hideStatus(); } redraw(); } } function onMove(e) { if (!baseImg) return; e.preventDefault(); const {x, y} = canvasXY(e); if (!dragging) { if (mode === 'select') { if (selectedIdx >= 0 && hitHandle(x,y,selectedIdx)) { const h = hitHandle(x,y,selectedIdx); const curs = {tl:'nwse-resize',tr:'nesw-resize',bl:'nesw-resize',br:'nwse-resize', tc:'ns-resize',bc:'ns-resize',ml:'ew-resize',mr:'ew-resize'}; canvas.style.cursor = curs[h] || 'move'; } else if (hitBox(x,y) >= 0) { canvas.style.cursor = 'move'; } else { canvas.style.cursor = 'default'; } } return; } if (dragType === 'new') { redraw({x1:dragStart.x, y1:dragStart.y, x2:x, y2:y}); showStatus(Math.abs(x-dragStart.x).toFixed(0)+'\u00d7'+Math.abs(y-dragStart.y).toFixed(0)+' px'); return; } const dx = (x - dragStart.x) / dispW; const dy = (y - dragStart.y) / dispH; const b = boxes[selectedIdx]; const o = dragOrig; if (dragType === 'move') { const bw = o.x2-o.x1, bh = o.y2-o.y1; let nx1 = o.x1+dx, ny1 = o.y1+dy; nx1 = clamp01(nx1); ny1 = clamp01(ny1); if (nx1+bw > 1) nx1 = 1-bw; if (ny1+bh > 1) ny1 = 1-bh; b.x1=nx1; b.y1=ny1; b.x2=nx1+bw; b.y2=ny1+bh; } else { const t = dragType; if (t.includes('l')) b.x1 = clamp01(o.x1 + dx); if (t.includes('r')) b.x2 = clamp01(o.x2 + dx); if (t.includes('t')) b.y1 = clamp01(o.y1 + dy); if (t.includes('b')) b.y2 = clamp01(o.y2 + dy); if (Math.abs(b.x2-b.x1) < 0.01) { b.x1=o.x1; b.x2=o.x2; } if (Math.abs(b.y2-b.y1) < 0.01) { b.y1=o.y1; b.y2=o.y2; } if (b.x1 > b.x2) { const t2=b.x1; b.x1=b.x2; b.x2=t2; } if (b.y1 > b.y2) { const t2=b.y1; b.y1=b.y2; b.y2=t2; } } redraw(); } function onUp(e) { if (!dragging) return; if (e) e.preventDefault(); dragging = false; if (dragType === 'new') { const pt = e ? canvasXY(e) : {x:dragStart.x, y:dragStart.y}; if (Math.abs(pt.x-dragStart.x) > 4 && Math.abs(pt.y-dragStart.y) > 4) { const nb = px2n(dragStart.x, dragStart.y, pt.x, pt.y); boxes.push(nb); window.__bboxBoxes = boxes; selectedIdx = boxes.length - 1; console.log('[BBox] created box #'+boxes.length, nb); showStatus('Box #'+boxes.length+' created'); } else { hideStatus(); } } else { showStatus('Box #'+(selectedIdx+1)+' updated'); } dragType = null; dragOrig = null; syncToGradio(); redraw(); } canvas.addEventListener('mousedown', onDown); canvas.addEventListener('mousemove', onMove); canvas.addEventListener('mouseup', onUp); canvas.addEventListener('mouseleave', (e)=>{if(dragging)onUp(e);}); canvas.addEventListener('touchstart', onDown, {passive:false}); canvas.addEventListener('touchmove', onMove, {passive:false}); canvas.addEventListener('touchend', onUp, {passive:false}); canvas.addEventListener('touchcancel',(e)=>{e.preventDefault();dragging=false;redraw();},{passive:false}); /* ── toolbar ── */ btnDraw.addEventListener('click', ()=>setMode('draw')); btnSelect.addEventListener('click', ()=>setMode('select')); btnDel.addEventListener('click', () => { if (selectedIdx >= 0 && selectedIdx < boxes.length) { const removed = selectedIdx + 1; boxes.splice(selectedIdx, 1); window.__bboxBoxes = boxes; selectedIdx = -1; syncToGradio(); redraw(); showStatus('Box #'+removed+' deleted'); } else { showStatus('No box selected'); } }); btnUndo.addEventListener('click', () => { if (boxes.length > 0) { boxes.pop(); window.__bboxBoxes = boxes; selectedIdx = -1; syncToGradio(); redraw(); showStatus('Last box removed'); } }); btnClear.addEventListener('click', () => { boxes.length = 0; /* clear in-place */ window.__bboxBoxes = boxes; selectedIdx = -1; syncToGradio(); redraw(); hideStatus(); }); /* ── FIX: robust image polling with multiple selectors ── */ let lastSrc = null; setInterval(() => { /* try several selectors Gradio might use */ const imgs = document.querySelectorAll('#source-image-component img'); let el = null; for (const img of imgs) { if (img.src && img.src.length > 30 && (img.src.startsWith('data:') || img.src.startsWith('blob:') || img.src.includes('/file=') || img.src.includes('/upload') || img.src.includes('localhost') || img.src.includes('127.0.0.1') || img.src.startsWith('http'))) { el = img; break; } } if (el && el.src && el.src !== lastSrc) { lastSrc = el.src; const img = new window.Image(); img.crossOrigin = 'anonymous'; img.onload = () => { baseImg = img; boxes.length = 0; window.__bboxBoxes = boxes; selectedIdx = -1; fitSize(img.naturalWidth, img.naturalHeight); syncToGradio(); redraw(); hideStatus(); console.log('[BBox] loaded image', img.naturalWidth, 'x', img.naturalHeight); }; img.onerror = () => { console.warn('[BBox] image load failed for', el.src.substring(0,60)); }; img.src = el.src; } else if (!el || !el.src) { if (baseImg) { baseImg = null; boxes.length = 0; window.__bboxBoxes = boxes; selectedIdx = -1; fitSize(512,400); syncToGradio(); redraw(); hideStatus(); } lastSrc = null; } }, 300); /* periodic re-sync every 500 ms */ setInterval(() => { syncToGradio(); }, 500); new ResizeObserver(() => { if (baseImg) { fitSize(baseImg.naturalWidth, baseImg.naturalHeight); redraw(); } }).observe(wrap); setMode('draw'); fitSize(512,400); redraw(); syncToGradio(); } initCanvasBbox(); } """ with gr.Blocks() as demo: gr.Markdown("# **QIE-Object-Remover-Bbox**", elem_id="main-title") gr.Markdown( "Perform diverse image edits using a specialized [LoRA](https://huggingface.co/prithivMLmods/QIE-2511-Object-Remover-v2). Upload an image, draw red bounding boxes over the objects you want to remove, and click Remove Object." "Multiple boxes supported. Select, move, resize or delete individual boxes.", elem_id="subtitle", ) with gr.Row(): with gr.Column(scale=1): source_image = gr.Image( label="Upload Image", type="pil", height=350, elem_id="source-image-component", ) gr.Markdown("# **Bbox Edit Controller**") gr.HTML( '