prithivMLmods's picture
upload app
6930028 verified
raw
history blame
38.1 kB
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(
'<div class="bbox-hint">'
"<b>Draw mode:</b> Click & drag to create red rectangles. "
"<b>Select mode:</b> Click a box to select it → drag to <b>move</b>, "
"drag handles to <b>resize</b>. Use <b>Delete Selected</b> to remove one box."
"</div>"
)
gr.HTML(
"""
<div id="bbox-draw-wrap">
<canvas id="bbox-draw-canvas" width="512" height="400"></canvas>
<div id="bbox-status"></div>
<div id="bbox-count"></div>
</div>
"""
)
gr.HTML(
"""
<div class="bbox-toolbar-section">
<span class="toolbar-label">🛠 Tools:</span>
<button id="tb-draw" class="bbox-tb-draw active" title="Draw new boxes">✏️ Draw</button>
<button id="tb-select" class="bbox-tb-select" title="Select / move / resize">🔲 Select</button>
<div class="toolbar-divider"></div>
<span class="toolbar-label">Actions:</span>
<button id="tb-del" class="bbox-tb-del" title="Delete selected box">✕ Delete Selected</button>
<button id="tb-undo" class="bbox-tb-undo" title="Remove last box">↩ Undo</button>
<button id="tb-clear" class="bbox-tb-clear" title="Remove all boxes">🗑 Clear All</button>
</div>
"""
)
gr.HTML('<div id="bbox-debug-count">⬜ No boxes drawn yet</div>')
boxes_json = gr.Textbox(
value="[]",
visible=True,
interactive=True,
elem_id="boxes-json-input",
label="boxes-json",
)
prompt = gr.Textbox(
label="Prompt",
value=DEFAULT_PROMPT,
lines=2,
info="Edit the prompt if needed",
)
run_btn = gr.Button("🗑️ Remove Object", variant="primary", size="lg")
with gr.Column(scale=1):
result = gr.Image(label="Output Image", height=449)
preview = gr.Image(label="Input Sent to Model (with red boxes)", height=393)
with gr.Accordion("Advanced Settings", open=True):
seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0)
randomize_seed = gr.Checkbox(label="Randomize Seed", value=True)
with gr.Row():
guidance_scale = gr.Slider(label="Guidance Scale", minimum=1.0, maximum=10.0, step=0.1, value=1.0)
num_inference_steps = gr.Slider(label="Inference Steps", minimum=1, maximum=20, step=1, value=4)
with gr.Row():
height_slider = gr.Slider(label="Height", minimum=256, maximum=2048, step=8, value=1024)
width_slider = gr.Slider(label="Width", minimum=256, maximum=2048, step=8, value=1024)
with gr.Accordion("About", open=False):
gr.Markdown(
"*QIE-Object-Remover-Bbox* by "
"[prithivMLmods](https://huggingface.co/prithivMLmods). "
"Adapter: [QIE-2511-Object-Remover-v2]"
"(https://huggingface.co/prithivMLmods/QIE-2511-Object-Remover-v2). "
"More adapters → [Qwen-Image-Edit-LoRAs]"
"(https://huggingface.co/models?other=base_model:adapter:Qwen/Qwen-Image-Edit-2509)."
)
demo.load(fn=None, js=bbox_drawer_js)
run_btn.click(
fn=infer_object_removal,
inputs=[source_image, boxes_json, prompt, seed, randomize_seed,
guidance_scale, num_inference_steps, height_slider, width_slider],
outputs=[result, seed, preview],
js="""(src, bj, p, s, rs, gs, nis, h, w) => {
const boxes = window.__bboxBoxes || [];
const json = JSON.stringify(boxes);
console.log('[BBox] submitting', boxes.length, 'boxes:', json);
return [src, json, p, s, rs, gs, nis, h, w];
}""",
)
source_image.upload(
fn=update_dimensions_on_upload,
inputs=[source_image],
outputs=[width_slider, height_slider],
)
if __name__ == "__main__":
demo.launch(
css=css, theme=purple_theme,
mcp_server=True, ssr_mode=False, show_error=True
)