import gradio as gr import numpy as np import random import torch import spaces import base64 import json from io import BytesIO 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 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-2509-Object-Remover-Bbox-v3", "weights": "QIE-2509-Object-Remover-Bbox-v3-10000.safetensors", "adapter_name": "object-remover", }, } loaded = False DEFAULT_PROMPT = "Remove the red highlighted object from the scene" def b64_to_pil(b64_str): if not b64_str or not b64_str.startswith("data:image"): return None try: _, data = b64_str.split(',', 1) image_data = base64.b64decode(data) return Image.open(BytesIO(image_data)).convert("RGB") except Exception as e: print(f"Error decoding image: {e}") return None def burn_boxes_onto_image(pil_image, boxes_json_str): 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("RGB") w, h = img.size draw = ImageDraw.Draw(img) 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_], outline=(255, 0, 0), width=bw) return img @spaces.GPU def infer_object_removal( b64_str, boxes_json, prompt, seed=0, randomize_seed=True, guidance_scale=1.0, num_inference_steps=4, height=1024, width=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() == "": raise gr.Error("Please enter an edit prompt.") source_image = b64_to_pil(b64_str) if source_image is None: raise gr.Error("Please upload an image first using the canvas area.") try: boxes = json.loads(boxes_json) if boxes_json and boxes_json.strip() else [] except Exception: 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(b64_str): image = b64_to_pil(b64_str) 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=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap'); *{box-sizing:border-box;margin:0;padding:0} body,.gradio-container{ background:#0f0f13!important; font-family:'Inter',system-ui,-apple-system,sans-serif!important; font-size:14px!important; color:#e4e4e7!important; min-height:100vh; } .dark body,.dark .gradio-container{ background:#0f0f13!important; color:#e4e4e7!important; } footer{display:none!important} .hidden-input{ display:none!important; height:0!important; overflow:hidden!important; margin:0!important; padding:0!important; } /* ── Main Container ── */ .app-shell{ background:#18181b; border:1px solid #27272a; border-radius:16px; margin:12px auto; max-width:1400px; overflow:hidden; box-shadow:0 25px 50px -12px rgba(0,0,0,.6), 0 0 0 1px rgba(255,255,255,.03); } /* ── Header Bar ── */ .app-header{ background:linear-gradient(135deg,#18181b 0%,#1e1e24 100%); border-bottom:1px solid #27272a; padding:14px 24px; display:flex; align-items:center; justify-content:space-between; } .app-header-left{ display:flex; align-items:center; gap:12px; } .app-logo{ width:36px;height:36px; background:linear-gradient(135deg,#6366f1,#8b5cf6,#a78bfa); border-radius:10px; display:flex;align-items:center;justify-content:center; font-size:18px;font-weight:800;color:#fff; box-shadow:0 4px 12px rgba(99,102,241,.35); } .app-title{ font-size:18px;font-weight:700; background:linear-gradient(135deg,#e4e4e7,#a1a1aa); -webkit-background-clip:text; -webkit-text-fill-color:transparent; letter-spacing:-.3px; } .app-badge{ font-size:11px; font-weight:600; padding:3px 10px; border-radius:20px; background:rgba(99,102,241,.15); color:#818cf8; border:1px solid rgba(99,102,241,.25); letter-spacing:.3px; } /* ── Toolbar ── */ .app-toolbar{ background:#18181b; border-bottom:1px solid #27272a; padding:8px 16px; display:flex; gap:4px; align-items:center; flex-wrap:wrap; } .tb-sep{ width:1px;height:28px; background:#27272a; margin:0 8px; } .modern-tb-btn{ display:inline-flex;align-items:center;justify-content:center; gap:6px; min-width:32px;height:34px; background:transparent; border:1px solid transparent; border-radius:8px; cursor:pointer; font-size:13px; font-weight:600; padding:0 12px; font-family:'Inter',sans-serif; color:#ffffff!important; transition:all .15s ease; } .modern-tb-btn:hover{ background:rgba(99,102,241,.15); color:#ffffff!important; border-color:rgba(99,102,241,.3); } .modern-tb-btn:active,.modern-tb-btn.active{ background:rgba(99,102,241,.25); color:#ffffff!important; border-color:rgba(99,102,241,.45); } .modern-tb-btn .tb-icon{ font-size:15px; line-height:1; color:#ffffff!important; } .modern-tb-btn .tb-label{ font-size:13px; color:#ffffff!important; font-weight:600; } /* ── Main Layout ── */ .app-main-row{ display:flex; gap:0; flex:1; overflow:hidden; } .app-main-left{ flex:1; display:flex; flex-direction:column; min-width:0; border-right:1px solid #27272a; } .app-main-right{ width:420px; display:flex; flex-direction:column; flex-shrink:0; background:#18181b; } /* ── Canvas Area ── */ #bbox-draw-wrap{ position:relative; background:#09090b; margin:0; min-height:440px; overflow:hidden; cursor:crosshair; } #bbox-draw-canvas{display:block;margin:0 auto} #bbox-status{ position:absolute;top:12px;left:12px; background:rgba(99,102,241,.9); color:#fff; padding:4px 12px; font-family:'JetBrains Mono',monospace; font-size:12px; font-weight:500; border-radius:6px; z-index:10;display:none; pointer-events:none; backdrop-filter:blur(8px); } #bbox-count{ position:absolute;top:12px;right:12px; background:rgba(24,24,27,.9); color:#a78bfa; padding:4px 12px; font-family:'JetBrains Mono',monospace; font-size:12px; font-weight:600; border-radius:6px; border:1px solid rgba(99,102,241,.3); z-index:10;display:none; backdrop-filter:blur(8px); } /* ── Upload Prompt (icon only) ── */ .upload-prompt-modern{ position:absolute; top:50%;left:50%; transform:translate(-50%,-50%); z-index:20; } .upload-click-area{ display:flex; flex-direction:column; align-items:center; justify-content:center; cursor:pointer; padding:36px 44px; border:2px dashed #3f3f46; border-radius:16px; background:rgba(99,102,241,.03); transition:all .2s ease; } .upload-click-area:hover{ background:rgba(99,102,241,.08); border-color:#6366f1; transform:scale(1.03); } .upload-click-area:active{ background:rgba(99,102,241,.12); transform:scale(.98); } .upload-click-area svg{ width:80px;height:80px; } /* ── Hint Bar ── */ .hint-bar{ background:rgba(99,102,241,.06); border-top:1px solid #27272a; border-bottom:1px solid #27272a; padding:10px 20px; font-size:13px; color:#a1a1aa; line-height:1.7; } .hint-bar b{color:#c7d2fe;font-weight:600} .hint-bar kbd{ display:inline-block; padding:1px 6px; background:#27272a; border:1px solid #3f3f46; border-radius:4px; font-family:'JetBrains Mono',monospace; font-size:11px; color:#a1a1aa; } /* ── JSON Panel ── */ .json-panel{ background:#18181b; border-top:1px solid #27272a; display:flex; flex-direction:column; height:160px; max-height:160px; min-height:160px; } .json-panel-title{ padding:8px 16px; font-size:12px; font-weight:600; color:#71717a; text-transform:uppercase; letter-spacing:.8px; border-bottom:1px solid #27272a; display:flex; align-items:center; gap:8px; flex-shrink:0; } .json-panel-title::before{ content:'{ }'; font-family:'JetBrains Mono',monospace; font-size:11px; color:#6366f1; background:rgba(99,102,241,.12); padding:2px 6px; border-radius:4px; } .json-panel-content{ background:#09090b; margin:0; padding:12px 16px; font-family:'JetBrains Mono',monospace; font-size:12px; color:#a1a1aa; flex:1; overflow-y:auto; overflow-x:hidden; word-break:break-all; white-space:pre-wrap; line-height:1.6; } .json-panel-content::-webkit-scrollbar{width:8px} .json-panel-content::-webkit-scrollbar-track{background:#09090b} .json-panel-content::-webkit-scrollbar-thumb{ background:#27272a; border-radius:4px; } .json-panel-content::-webkit-scrollbar-thumb:hover{background:#3f3f46} /* ── Right Panel Cards ── */ .panel-card{ border-bottom:1px solid #27272a; } .panel-card-title{ padding:12px 20px; font-size:12px; font-weight:600; color:#71717a; text-transform:uppercase; letter-spacing:.8px; border-bottom:1px solid rgba(39,39,42,.6); } .panel-card-body{ padding:16px 20px; display:flex; flex-direction:column; gap:8px; } .modern-label{ font-size:13px;font-weight:500;color:#a1a1aa;margin-bottom:4px;display:block; } .modern-textarea{ width:100%; background:#09090b; border:1px solid #27272a; border-radius:8px; padding:10px 14px; font-family:'Inter',sans-serif; font-size:14px; color:#e4e4e7; resize:vertical; outline:none; min-height:42px; transition:border-color .2s; } .modern-textarea:focus{ border-color:#6366f1; box-shadow:0 0 0 3px rgba(99,102,241,.15); } .modern-textarea::placeholder{color:#3f3f46} .modern-textarea.error-flash{ border-color:#ef4444!important; box-shadow:0 0 0 3px rgba(239,68,68,.2)!important; animation:shake .4s ease; } @keyframes shake{ 0%,100%{transform:translateX(0)} 20%,60%{transform:translateX(-4px)} 40%,80%{transform:translateX(4px)} } /* ── Toast Notification ── */ .toast-notification{ position:fixed; top:24px; left:50%; transform:translateX(-50%) translateY(-120%); z-index:9999; padding:10px 24px; border-radius:10px; font-family:'Inter',sans-serif; font-size:14px; font-weight:600; display:flex; align-items:center; gap:8px; box-shadow:0 8px 24px rgba(0,0,0,.5); transition:transform .35s cubic-bezier(.34,1.56,.64,1),opacity .35s ease; opacity:0; pointer-events:none; } .toast-notification.visible{ transform:translateX(-50%) translateY(0); opacity:1; pointer-events:auto; } .toast-notification.error{ background:linear-gradient(135deg,#dc2626,#b91c1c); color:#fff; border:1px solid rgba(255,255,255,.15); } .toast-notification.warning{ background:linear-gradient(135deg,#d97706,#b45309); color:#fff; border:1px solid rgba(255,255,255,.15); } .toast-notification.info{ background:linear-gradient(135deg,#2563eb,#1d4ed8); color:#fff; border:1px solid rgba(255,255,255,.15); } .toast-notification .toast-icon{font-size:16px;line-height:1} .toast-notification .toast-text{line-height:1.3} /* ── Primary Button ── */ .btn-run{ display:flex;align-items:center;justify-content:center;gap:8px; width:100%; background:linear-gradient(135deg,#6366f1,#7c3aed); border:none; border-radius:10px; padding:12px 24px; cursor:pointer; font-size:15px; font-weight:600; font-family:'Inter',sans-serif; color:#fff; transition:all .2s ease; box-shadow:0 4px 16px rgba(99,102,241,.3), inset 0 1px 0 rgba(255,255,255,.1); letter-spacing:-.2px; } .btn-run:hover{ background:linear-gradient(135deg,#7c7cf5,#8b5cf6); box-shadow:0 6px 24px rgba(99,102,241,.45), inset 0 1px 0 rgba(255,255,255,.15); transform:translateY(-1px); } .btn-run:active{ transform:translateY(0); box-shadow:0 2px 8px rgba(99,102,241,.3); } .btn-run svg{width:18px;height:18px;fill:#fff} /* ── Output Frames ── */ .output-frame{ border-bottom:1px solid #27272a; display:flex; flex-direction:column; position:relative; } .output-frame .out-title{ padding:10px 20px; font-size:13px; font-weight:700; color:#ffffff!important; text-transform:uppercase; letter-spacing:.8px; border-bottom:1px solid rgba(39,39,42,.6); display:flex; align-items:center; justify-content:space-between; } .output-frame .out-title span{ color:#ffffff!important; } .output-frame .out-body{ flex:1; background:#09090b; display:flex; align-items:center; justify-content:center; overflow:hidden; min-height:180px; position:relative; } .output-frame .out-body img{ max-width:100%;max-height:460px; image-rendering:auto; } .output-frame .out-placeholder{ color:#3f3f46; font-size:13px; text-align:center; padding:20px; } .out-download-btn{ display:none; align-items:center; justify-content:center; background:rgba(99,102,241,.1); border:1px solid rgba(99,102,241,.2); border-radius:6px; cursor:pointer; padding:3px 10px; font-size:11px; font-weight:500; color:#c7d2fe!important; gap:4px; height:24px; transition:all .15s; } .out-download-btn:hover{ background:rgba(99,102,241,.2); border-color:rgba(99,102,241,.35); color:#ffffff!important; } .out-download-btn.visible{display:inline-flex} .out-download-btn svg{width:12px;height:12px;fill:#c7d2fe} /* ── Loader ── */ .modern-loader{ display:none; position:absolute; top:0;left:0;right:0;bottom:0; background:rgba(9,9,11,.92); z-index:15; flex-direction:column; align-items:center; justify-content:center; gap:16px; backdrop-filter:blur(4px); } .modern-loader.active{display:flex} .modern-loader .loader-spinner{ width:36px;height:36px; border:3px solid #27272a; border-top-color:#6366f1; border-radius:50%; animation:spin .8s linear infinite; } @keyframes spin{to{transform:rotate(360deg)}} .modern-loader .loader-text{ font-size:13px; color:#a1a1aa; font-weight:500; } .loader-bar-track{ width:200px;height:4px; background:#27272a; border-radius:2px; overflow:hidden; } .loader-bar-fill{ height:100%; background:linear-gradient(90deg,#6366f1,#8b5cf6,#6366f1); background-size:200% 100%; animation:shimmer 1.5s ease-in-out infinite; border-radius:2px; } @keyframes shimmer{ 0%{background-position:200% 0} 100%{background-position:-200% 0} } /* ── Settings ── */ .settings-group{ border:1px solid #27272a; border-radius:10px; margin:12px 16px; padding:0; overflow:hidden; } .settings-group-title{ font-size:12px; font-weight:600; color:#71717a; text-transform:uppercase; letter-spacing:.8px; padding:10px 16px; border-bottom:1px solid #27272a; background:rgba(24,24,27,.5); } .settings-group-body{ padding:14px 16px; display:flex; flex-direction:column; gap:12px; } .slider-row{ display:flex; align-items:center; gap:10px; min-height:28px; } .slider-row label,.slider-row .dim-label{ font-size:13px; font-weight:500; color:#a1a1aa; min-width:72px; flex-shrink:0; } .slider-row input[type="range"]{ flex:1; -webkit-appearance:none; appearance:none; height:6px; background:#27272a; border-radius:3px; outline:none; min-width:0; } .slider-row input[type="range"]::-webkit-slider-thumb{ -webkit-appearance:none; appearance:none; width:16px;height:16px; background:linear-gradient(135deg,#6366f1,#7c3aed); border-radius:50%; cursor:pointer; box-shadow:0 2px 6px rgba(99,102,241,.4); transition:transform .15s; } .slider-row input[type="range"]::-webkit-slider-thumb:hover{ transform:scale(1.2); } .slider-row input[type="range"]::-moz-range-thumb{ width:16px;height:16px; background:linear-gradient(135deg,#6366f1,#7c3aed); border-radius:50%; cursor:pointer; border:none; box-shadow:0 2px 6px rgba(99,102,241,.4); } .slider-row .slider-val{ min-width:52px;text-align:right; font-family:'JetBrains Mono',monospace; font-size:12px; font-weight:500; padding:3px 8px; background:#09090b; border:1px solid #27272a; border-radius:6px; color:#a1a1aa; flex-shrink:0; } .checkbox-row{ display:flex;align-items:center;gap:8px; font-size:13px;cursor:default; color:#a1a1aa; } .checkbox-row input[type="checkbox"]{ accent-color:#6366f1; width:16px;height:16px; cursor:pointer; } .checkbox-row label{ color:#a1a1aa;font-size:13px;cursor:pointer; } /* ── Status Bar ── */ .app-statusbar{ background:#18181b; border-top:1px solid #27272a; padding:6px 20px; display:flex; gap:12px; height:34px; align-items:center; font-size:12px; } .app-statusbar .sb-section{ padding:0 12px; flex:1; display:flex;align-items:center; font-family:'JetBrains Mono',monospace; font-size:12px; color:#52525b; overflow:hidden; white-space:nowrap; } .app-statusbar .sb-section.sb-fixed{ flex:0 0 auto; min-width:90px; text-align:center; justify-content:center; padding:3px 12px; background:rgba(99,102,241,.08); border-radius:6px; color:#818cf8; font-weight:500; } #gradio-run-btn{ position:absolute; left:-9999px; top:-9999px; width:1px; height:1px; opacity:0.01; pointer-events:none; overflow:hidden; } #bbox-debug-count{ font-family:'JetBrains Mono',monospace; font-size:12px; color:#52525b; } /* ── Global scrollbar ── */ ::-webkit-scrollbar{width:8px;height:8px} ::-webkit-scrollbar-track{background:#09090b} ::-webkit-scrollbar-thumb{background:#27272a;border-radius:4px} ::-webkit-scrollbar-thumb:hover{background:#3f3f46} /* ── Dark mode force-overrides ── */ .dark .app-shell{background:#18181b} .dark .upload-prompt-modern{background:transparent} .dark .panel-card{background:#18181b} .dark .settings-group{background:#18181b} .dark .modern-tb-btn{color:#ffffff!important} .dark .modern-tb-btn .tb-icon{color:#ffffff!important} .dark .modern-tb-btn .tb-label{color:#ffffff!important} .dark .modern-tb-btn:hover{color:#ffffff!important} .dark .modern-tb-btn:active,.dark .modern-tb-btn.active{color:#ffffff!important} .dark .output-frame .out-title{color:#ffffff!important} .dark .output-frame .out-title span{color:#ffffff!important} .dark .out-download-btn{color:#c7d2fe!important} .dark .out-download-btn:hover{color:#ffffff!important} @media(max-width:840px){ .app-main-row{flex-direction:column} .app-main-right{width:100%} .app-main-left{border-right:none;border-bottom:1px solid #27272a} } """ 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 jsonDisplay = document.getElementById('bbox-json-content'); const btnDraw = document.getElementById('tb-draw'); const btnSelect = document.getElementById('tb-select'); const btnReset = document.getElementById('tb-reset'); const btnDel = document.getElementById('tb-del'); const btnUndo = document.getElementById('tb-undo'); const btnClear = document.getElementById('tb-clear'); const btnChange = document.getElementById('tb-change-img'); const uploadPrompt = document.getElementById('upload-prompt'); const uploadClickArea = document.getElementById('upload-click-area'); const fileInput = document.getElementById('custom-file-input'); const promptInput = document.getElementById('custom-prompt-input'); if (!canvas || !wrap || !debugCount || !btnDraw || !fileInput) { setTimeout(initCanvasBbox, 250); return; } window.__bboxInitDone = true; 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 = 6; const RED_STROKE = 'rgba(239,68,68,0.95)'; const RED_STROKE_WIDTH = 2; const SEL_STROKE = 'rgba(99,102,241,0.95)'; let toastTimer = null; function showToast(message, type) { let toast = document.getElementById('app-toast'); if (!toast) { toast = document.createElement('div'); toast.id = 'app-toast'; toast.className = 'toast-notification'; toast.innerHTML = ''; document.body.appendChild(toast); } const icon = toast.querySelector('.toast-icon'); const text = toast.querySelector('.toast-text'); toast.className = 'toast-notification ' + (type || 'error'); if (type === 'warning') icon.textContent = '\u26A0'; else if (type === 'info') icon.textContent = '\u2139'; else icon.textContent = '\u2717'; text.textContent = message; if (toastTimer) clearTimeout(toastTimer); void toast.offsetWidth; toast.classList.add('visible'); toastTimer = setTimeout(() => toast.classList.remove('visible'), 3500); } window.__showToast = showToast; function flashPromptError() { if (!promptInput) return; promptInput.classList.add('error-flash'); promptInput.focus(); setTimeout(() => promptInput.classList.remove('error-flash'), 800); } 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))}; } function setGradioValue(containerId, value) { const container = document.getElementById(containerId); if (!container) return; const allInputs = container.querySelectorAll('input, textarea'); allInputs.forEach(el => { if (el.type === 'file' || el.type === 'range' || el.type === 'checkbox') return; const proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype; const ns = Object.getOwnPropertyDescriptor(proto, 'value'); if (ns && ns.set) { ns.set.call(el, value); el.dispatchEvent(new Event('input', {bubbles:true, composed:true})); el.dispatchEvent(new Event('change', {bubbles:true, composed:true})); } }); } function formatJsonPretty(boxes) { if (!boxes || boxes.length === 0) return '[\n // No bounding boxes defined\n]'; let lines = '[\n'; boxes.forEach((b, i) => { lines += ' {\n'; lines += ' "x1": ' + b.x1.toFixed(4) + ',\n'; lines += ' "y1": ' + b.y1.toFixed(4) + ',\n'; lines += ' "x2": ' + b.x2.toFixed(4) + ',\n'; lines += ' "y2": ' + b.y2.toFixed(4) + '\n'; lines += ' }'; if (i < boxes.length - 1) lines += ','; lines += '\n'; }); lines += ']'; return lines; } function syncToGradio() { window.__bboxBoxes = boxes; const jsonStr = JSON.stringify(boxes); if (debugCount) { debugCount.textContent = boxes.length > 0 ? boxes.length + ' box' + (boxes.length > 1 ? 'es' : '') + ' drawn' : 'No boxes drawn'; } if (jsonDisplay) { jsonDisplay.textContent = formatJsonPretty(boxes); jsonDisplay.scrollTop = jsonDisplay.scrollHeight; } setGradioValue('boxes-json-input', jsonStr); } function syncImageToGradio(dataUrl) { setGradioValue('hidden-image-b64', dataUrl); } function syncPromptToGradio() { if (promptInput) { setGradioValue('prompt-gradio-input', promptInput.value); } } function resetCanvas() { baseImg = null; boxes.length = 0; window.__bboxBoxes = boxes; selectedIdx = -1; dragging = false; dragType = null; dragOrig = null; fitSize(512, 400); syncToGradio(); syncImageToGradio(''); redraw(); hideStatus(); uploadPrompt.style.display = ''; showStatus('Image removed'); setTimeout(hideStatus, 1500); } function redraw(tempRect) { ctx.clearRect(0,0,dispW,dispH); if (!baseImg) { ctx.fillStyle='#09090b'; ctx.fillRect(0,0,dispW,dispH); 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; if (i === selectedIdx) { ctx.strokeStyle = SEL_STROKE; ctx.lineWidth = RED_STROKE_WIDTH + 1; ctx.setLineDash([4,3]); } else { ctx.strokeStyle = RED_STROKE; ctx.lineWidth = RED_STROKE_WIDTH; ctx.setLineDash([]); } ctx.strokeRect(lx, ty, w, h); ctx.setLineDash([]); ctx.fillStyle = i===selectedIdx ? '#6366f1' : '#ef4444'; ctx.font = 'bold 11px Inter,system-ui,sans-serif'; ctx.textAlign = 'left'; ctx.textBaseline = 'top'; const label = '#'+(i+1); const tw = ctx.measureText(label).width; const rx = lx, ry = ty - 18; const rw = tw + 10, rh = 18; ctx.beginPath(); if (ctx.roundRect) { ctx.roundRect(rx, ry, rw, rh, 3); } else { ctx.rect(rx, ry, rw, rh); } ctx.fill(); ctx.fillStyle = '#fff'; ctx.fillText(label, lx+5, ty-15); 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.strokeStyle = RED_STROKE; ctx.lineWidth = RED_STROKE_WIDTH; ctx.setLineDash([4,3]); ctx.strokeRect(rx, ry, rw, rh); ctx.setLineDash([]); } updateBadge(); } function drawHandles(p) { const pts = handlePoints(p); for (const k in pts) { const h = pts[k]; ctx.fillStyle = '#6366f1'; ctx.beginPath(); ctx.arc(h.x, h.y, HANDLE, 0, Math.PI*2); ctx.fill(); ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(h.x, h.y, HANDLE, 0, Math.PI*2); ctx.stroke(); } } 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'; } 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 #'+(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 #'+(selectedIdx+1)); redraw(); return; } dragging = true; dragType = 'move'; dragStart = {x, y}; dragOrig = {...boxes[selectedIdx]}; showStatus('Moving #'+(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; 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}); function processFile(file) { if (!file || !file.type.startsWith('image/')) return; const reader = new FileReader(); reader.onload = (event) => { const dataUrl = event.target.result; 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(); uploadPrompt.style.display = 'none'; syncImageToGradio(dataUrl); }; img.src = dataUrl; }; reader.readAsDataURL(file); } function openFilePicker() { fileInput.click(); } uploadClickArea.addEventListener('click', openFilePicker); btnChange.addEventListener('click', openFilePicker); fileInput.addEventListener('change', (e) => { processFile(e.target.files[0]); e.target.value = ''; }); wrap.addEventListener('dragover', (e) => { e.preventDefault(); wrap.style.outline = '2px solid #6366f1'; wrap.style.outlineOffset = '-2px'; }); wrap.addEventListener('dragleave', (e) => { e.preventDefault(); wrap.style.outline = ''; }); wrap.addEventListener('drop', (e) => { e.preventDefault(); wrap.style.outline = ''; if (e.dataTransfer.files.length) { processFile(e.dataTransfer.files[0]); } }); btnDraw.addEventListener('click', ()=>setMode('draw')); btnSelect.addEventListener('click', ()=>setMode('select')); btnReset.addEventListener('click', () => { resetCanvas(); }); 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('Select a box first'); } }); 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; window.__bboxBoxes = boxes; selectedIdx = -1; syncToGradio(); redraw(); hideStatus(); }); if (promptInput) { promptInput.addEventListener('input', () => { syncPromptToGradio(); }); } function syncSlider(customId, gradioId) { const slider = document.getElementById(customId); const valSpan = document.getElementById(customId + '-val'); if (!slider) return; slider.addEventListener('input', () => { if (valSpan) valSpan.textContent = slider.value; const container = document.getElementById(gradioId); if (!container) return; const targets = [ ...container.querySelectorAll('input[type="range"]'), ...container.querySelectorAll('input[type="number"]') ]; targets.forEach(el => { const ns = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value'); if (ns && ns.set) { ns.set.call(el, slider.value); el.dispatchEvent(new Event('input', {bubbles:true, composed:true})); el.dispatchEvent(new Event('change', {bubbles:true, composed:true})); } }); }); } syncSlider('custom-seed', 'gradio-seed'); syncSlider('custom-guidance', 'gradio-guidance'); syncSlider('custom-steps', 'gradio-steps'); syncSlider('custom-height', 'gradio-height'); syncSlider('custom-width', 'gradio-width'); const randCheck = document.getElementById('custom-randomize'); if (randCheck) { randCheck.addEventListener('change', () => { const container = document.getElementById('gradio-randomize'); if (!container) return; const cb = container.querySelector('input[type="checkbox"]'); if (cb && cb.checked !== randCheck.checked) { cb.click(); } }); } function showLoaders() { const l1 = document.getElementById('output-loader'); const l2 = document.getElementById('preview-loader'); if (l1) l1.classList.add('active'); if (l2) l2.classList.add('active'); const sb = document.querySelector('.app-statusbar .sb-fixed'); if (sb) sb.textContent = 'Processing...'; } function hideLoaders() { const l1 = document.getElementById('output-loader'); const l2 = document.getElementById('preview-loader'); if (l1) l1.classList.remove('active'); if (l2) l2.classList.remove('active'); const sb = document.querySelector('.app-statusbar .sb-fixed'); if (sb) sb.textContent = 'Done'; } window.__showLoaders = showLoaders; window.__hideLoaders = hideLoaders; function validateBeforeRun() { const promptVal = promptInput ? promptInput.value.trim() : ''; const hasImage = !!baseImg; const hasBoxes = boxes.length > 0; if (!hasImage && !promptVal && !hasBoxes) { showToast('Please upload an image, draw boxes, and enter a prompt', 'error'); flashPromptError(); return false; } if (!hasImage) { showToast('Please upload an image first', 'error'); return false; } if (!hasBoxes) { showToast('Please draw at least one bounding box on the image', 'error'); return false; } if (!promptVal) { showToast('Please enter an edit prompt', 'warning'); flashPromptError(); return false; } return true; } window.__clickGradioRunBtn = function() { if (!validateBeforeRun()) return; syncPromptToGradio(); syncToGradio(); showLoaders(); setTimeout(() => { const gradioBtn = document.getElementById('gradio-run-btn'); if (!gradioBtn) return; const btn = gradioBtn.querySelector('button'); if (btn) { btn.click(); } else { gradioBtn.click(); } }, 200); }; const customRunBtn = document.getElementById('custom-run-btn'); if (customRunBtn) { customRunBtn.addEventListener('click', () => { window.__clickGradioRunBtn(); }); } new ResizeObserver(() => { if (baseImg) { fitSize(baseImg.naturalWidth, baseImg.naturalHeight); redraw(); } }).observe(wrap); setMode('draw'); fitSize(512,400); redraw(); syncToGradio(); } initCanvasBbox(); } """ wire_outputs_js = r""" () => { function downloadImage(imgSrc, filename) { const a = document.createElement('a'); a.href = imgSrc; a.download = filename || 'image.png'; document.body.appendChild(a); a.click(); document.body.removeChild(a); } function watchOutputs() { const resultContainer = document.getElementById('gradio-result'); const previewContainer = document.getElementById('gradio-preview'); const outBody = document.getElementById('output-image-container'); const prevBody = document.getElementById('preview-image-container'); const outPh = document.getElementById('output-placeholder'); const prevPh = document.getElementById('preview-placeholder'); const dlBtnOut = document.getElementById('dl-btn-output'); const dlBtnPrev = document.getElementById('dl-btn-preview'); if (!resultContainer || !previewContainer || !outBody || !prevBody) { setTimeout(watchOutputs, 500); return; } if (dlBtnOut) { dlBtnOut.addEventListener('click', (e) => { e.stopPropagation(); const img = outBody.querySelector('img.modern-out-img'); if (img && img.src) downloadImage(img.src, 'output_result.png'); }); } if (dlBtnPrev) { dlBtnPrev.addEventListener('click', (e) => { e.stopPropagation(); const img = prevBody.querySelector('img.modern-out-img'); if (img && img.src) downloadImage(img.src, 'input_preview.png'); }); } function syncImages() { const resultImg = resultContainer.querySelector('img'); if (resultImg && resultImg.src) { if (outPh) outPh.style.display = 'none'; let existing = outBody.querySelector('img.modern-out-img'); if (!existing) { existing = document.createElement('img'); existing.className = 'modern-out-img'; outBody.appendChild(existing); } if (existing.src !== resultImg.src) { existing.src = resultImg.src; if (dlBtnOut) dlBtnOut.classList.add('visible'); if (window.__hideLoaders) window.__hideLoaders(); } } const previewImg = previewContainer.querySelector('img'); if (previewImg && previewImg.src) { if (prevPh) prevPh.style.display = 'none'; let existing2 = prevBody.querySelector('img.modern-out-img'); if (!existing2) { existing2 = document.createElement('img'); existing2.className = 'modern-out-img'; prevBody.appendChild(existing2); } if (existing2.src !== previewImg.src) { existing2.src = previewImg.src; if (dlBtnPrev) dlBtnPrev.classList.add('visible'); } } } const observer = new MutationObserver(syncImages); observer.observe(resultContainer, {childList:true, subtree:true, attributes:true, attributeFilter:['src']}); observer.observe(previewContainer, {childList:true, subtree:true, attributes:true, attributeFilter:['src']}); setInterval(syncImages, 800); } watchOutputs(); function watchDimensions() { const wContainer = document.getElementById('gradio-width'); const hContainer = document.getElementById('gradio-height'); const wSlider = document.getElementById('custom-width'); const hSlider = document.getElementById('custom-height'); const wVal = document.getElementById('custom-width-val'); const hVal = document.getElementById('custom-height-val'); if (!wContainer || !hContainer || !wSlider || !hSlider) { setTimeout(watchDimensions, 500); return; } function syncDims() { const wInput = wContainer.querySelector('input[type="range"],input[type="number"]'); const hInput = hContainer.querySelector('input[type="range"],input[type="number"]'); if (wInput && wInput.value) { wSlider.value = wInput.value; if(wVal) wVal.textContent = wInput.value; } if (hInput && hInput.value) { hSlider.value = hInput.value; if(hVal) hVal.textContent = hInput.value; } } const obs = new MutationObserver(syncDims); obs.observe(wContainer, {childList:true, subtree:true, attributes:true, attributeFilter:['value']}); obs.observe(hContainer, {childList:true, subtree:true, attributes:true, attributeFilter:['value']}); setInterval(syncDims, 1000); } watchDimensions(); } """ DOWNLOAD_SVG = '' with gr.Blocks(css=css) as demo: hidden_image_b64 = gr.Textbox( elem_id="hidden-image-b64", elem_classes="hidden-input", container=False ) boxes_json = gr.Textbox( value="[]", elem_id="boxes-json-input", elem_classes="hidden-input", container=False ) prompt = gr.Textbox( value=DEFAULT_PROMPT, elem_id="prompt-gradio-input", elem_classes="hidden-input", container=False ) seed = gr.Slider( minimum=0, maximum=MAX_SEED, step=1, value=0, elem_id="gradio-seed", elem_classes="hidden-input", container=False ) randomize_seed = gr.Checkbox( value=True, elem_id="gradio-randomize", elem_classes="hidden-input", container=False ) guidance_scale = gr.Slider( minimum=1.0, maximum=10.0, step=0.1, value=1.0, elem_id="gradio-guidance", elem_classes="hidden-input", container=False ) num_inference_steps = gr.Slider( minimum=1, maximum=20, step=1, value=4, elem_id="gradio-steps", elem_classes="hidden-input", container=False ) height_slider = gr.Slider( minimum=256, maximum=2048, step=8, value=1024, elem_id="gradio-height", elem_classes="hidden-input", container=False ) width_slider = gr.Slider( minimum=256, maximum=2048, step=8, value=1024, elem_id="gradio-width", elem_classes="hidden-input", container=False ) result = gr.Image( elem_id="gradio-result", elem_classes="hidden-input", container=False, format="png" ) preview = gr.Image( elem_id="gradio-preview", elem_classes="hidden-input", container=False ) gr.HTML(f"""
QIE Object Remover Bbox
Draw: Click & drag to create selection boxes  ·  Select: Click a box to move or resize  ·  Delete removes selected  ·  Clear removes all  ·  Reset removes image
Bounding Boxes
[ // No bounding boxes defined ]
Edit Instruction
Output {DOWNLOAD_SVG} Save
Processing image…
Result will appear here
Input Preview {DOWNLOAD_SVG} Save
Preparing input…
Preview will appear here
Advanced Settings
0
1.0
4
Width 1024
Height 1024
No boxes drawn
Ready
""") run_btn = gr.Button("Run", elem_id="gradio-run-btn") demo.load(fn=None, js=bbox_drawer_js) demo.load(fn=None, js=wire_outputs_js) run_btn.click( fn=infer_object_removal, inputs=[hidden_image_b64, boxes_json, prompt, seed, randomize_seed, guidance_scale, num_inference_steps, height_slider, width_slider], outputs=[result, seed, preview], js="""(b64, bj, p, s, rs, gs, nis, h, w) => { const boxes = window.__bboxBoxes || []; const json = JSON.stringify(boxes); return [b64, json, p, s, rs, gs, nis, h, w]; }""", ) hidden_image_b64.change( fn=update_dimensions_on_upload, inputs=[hidden_image_b64], outputs=[width_slider, height_slider], ) if __name__ == "__main__": demo.launch( mcp_server=True, ssr_mode=False, show_error=True )