prithivMLmods's picture
update app ✅
c0c4102 verified
import gradio as gr
import numpy as np
import random
import torch
import spaces
import base64
from io import BytesIO
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-2509-Object-Remover-Bbox",
"weights": "QIE-2509-Object-Remover-Bbox-5000.safetensors",
"adapter_name": "object-remover",
},
}
loaded = False
DEFAULT_PROMPT = "Remove the red highlighted object from the scene"
def b64_to_pil(b64_str: str) -> Image.Image | None:
"""Helper to decode base64 string from JS into a PIL Image"""
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: Image.Image, boxes_json_str: str) -> Image.Image:
"""Burn red outline-only rectangles onto the image (no fill)."""
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("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)
# Red outline only — no fill
draw.rectangle([lx, ty, rx, by_], outline=(255, 0, 0), width=bw)
return img
@spaces.GPU
def infer_object_removal(
b64_str: str,
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}'")
source_image = b64_to_pil(b64_str)
if source_image is None:
raise gr.Error("Please upload an image first using the Bbox editor area.")
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(b64_str: 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=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 dashed #C084FC;border-radius:12px;overflow:hidden;background:#1a1a1a;min-height:420px;transition: border-color 0.2s ease;}
#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}
/* Custom Uiverse.io Upload Prompt Component styling */
.upload-container {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
z-index: 20;
height: 300px;
width: 300px;
border-radius: 10px;
box-shadow: 4px 4px 30px rgba(168, 85, 247, 0.2);
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
padding: 10px;
gap: 5px;
background-color: rgba(250, 245, 255, 0.95);
backdrop-filter: blur(8px);
}
.dark .upload-container {
background-color: rgba(30, 30, 30, 0.95);
box-shadow: 4px 4px 30px rgba(0, 0, 0, 0.5);
}
.upload-header {
flex: 1;
width: 100%;
border: 2px dashed #A855F7;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
cursor: pointer;
transition: background-color 0.2s;
color: #A855F7;
}
.upload-header:hover {
background-color: rgba(168, 85, 247, 0.05);
}
.dark .upload-header:hover {
background-color: rgba(168, 85, 247, 0.15);
}
.upload-header svg {
height: 100px;
}
.upload-header p {
text-align: center;
color: #6B21A8;
font-family: 'Outfit', sans-serif;
font-weight: 600;
margin-top: 10px;
}
.dark .upload-header p {
color: #DAB2FF;
}
.upload-footer {
background-color: rgba(168, 85, 247, 0.08);
width: 100%;
height: 40px;
padding: 8px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: flex-end;
color: #6B21A8;
border: none;
box-sizing: border-box;
}
.dark .upload-footer {
background-color: rgba(168, 85, 247, 0.15);
color: #DAB2FF;
}
.upload-footer svg {
height: 130%;
fill: #A855F7;
background-color: rgba(255, 255, 255, 0.5);
border-radius: 50%;
padding: 2px;
cursor: pointer;
box-shadow: 0 2px 10px rgba(168, 85, 247, 0.2);
transition: transform 0.2s;
}
.dark .upload-footer svg {
background-color: rgba(0, 0, 0, 0.3);
}
.upload-footer svg:hover {
transform: scale(1.1);
}
.upload-footer p {
flex: 1;
text-align: center;
font-family: 'Outfit', sans-serif;
font-size: 0.9rem;
margin: 0;
}
.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-tb-change{background:#4b5563}
.bbox-tb-change:hover{background:#6b7280}
#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)}
.hidden-input {
display: none !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');
const btnChange = document.getElementById('tb-change-img');
const uploadPrompt = document.getElementById('upload-prompt');
const uploadHeader = document.getElementById('upload-header');
const fileInput = document.getElementById('custom-file-input');
if (!canvas || !wrap || !debugCount || !btnDraw || !fileInput) {
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;
const RED_STROKE = 'rgba(255,0,0,0.95)';
const RED_STROKE_WIDTH = 3;
const SEL_STROKE = 'rgba(0,120,255,0.95)';
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 syncToGradio() {
window.__bboxBoxes = boxes;
const jsonStr = JSON.stringify(boxes);
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';
}
const container = document.getElementById('boxes-json-input');
if (!container) return;
const targets = [
...container.querySelectorAll('textarea'),
...container.querySelectorAll('input:not([type="file"])')
];
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}));
}
});
}
function syncImageToGradio(dataUrl) {
const container = document.getElementById('hidden-image-b64');
if (!container) return;
const targets = [
...container.querySelectorAll('textarea'),
...container.querySelectorAll('input')
];
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, dataUrl);
el.dispatchEvent(new Event('input', {bubbles:true, composed:true}));
el.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
}
});
}
function redraw(tempRect) {
ctx.clearRect(0,0,dispW,dispH);
if (!baseImg) {
ctx.fillStyle='#1a1a1a'; 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;
/* RED OUTLINE ONLY — no fill */
if (i === selectedIdx) {
ctx.strokeStyle = SEL_STROKE;
ctx.lineWidth = RED_STROKE_WIDTH + 1;
ctx.setLineDash([6,3]);
} else {
ctx.strokeStyle = RED_STROKE;
ctx.lineWidth = RED_STROKE_WIDTH;
ctx.setLineDash([]);
}
ctx.strokeRect(lx, ty, w, h);
ctx.setLineDash([]);
/* label tag */
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);
});
/* temp drawing rect — outline only */
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([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'; }
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});
// --- File Upload Logic ---
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);
}
uploadHeader.addEventListener('click', () => fileInput.click());
btnChange.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', (e) => {
processFile(e.target.files[0]);
e.target.value = ''; // Reset input to allow re-upload of same file
});
wrap.addEventListener('dragover', (e) => {
e.preventDefault();
wrap.style.borderColor = '#A855F7';
wrap.style.boxShadow = '0 0 15px rgba(168,85,247,0.3)';
});
wrap.addEventListener('dragleave', (e) => {
e.preventDefault();
wrap.style.borderColor = '';
wrap.style.boxShadow = '';
});
wrap.addEventListener('drop', (e) => {
e.preventDefault();
wrap.style.borderColor = '';
wrap.style.boxShadow = '';
if (e.dataTransfer.files.length) {
processFile(e.dataTransfer.files[0]);
}
});
// --- Toolbar Logic ---
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;
window.__bboxBoxes = boxes;
selectedIdx = -1;
syncToGradio(); redraw(); hideStatus();
});
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-2509-Object-Remover-Bbox). "
"Upload an image directly into the bounding box editor area below, 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. Open on [GitHub](https://github.com/PRITHIVSAKTHIUR/QIE-Object-Remover-Bbox)",
elem_id="subtitle",
)
with gr.Row():
with gr.Column(scale=1):
hidden_image_b64 = gr.Textbox(
elem_id="hidden-image-b64",
elem_classes="hidden-input",
container=False
)
#gr.Markdown("### **Bbox Edit Controller**")
gr.HTML(
"""
<div id="bbox-draw-wrap">
<div id="upload-prompt" class="upload-container">
<div class="upload-header" id="upload-header">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 10V9C7 6.23858 9.23858 4 12 4C14.7614 4 17 6.23858 17 9V10C19.2091 10 21 11.7909 21 14C21 15.4806 20.1956 16.8084 19 17.5M7 10C4.79086 10 3 11.7909 3 14C3 15.4806 3.8044 16.8084 5 17.5M7 10C7.43285 10 7.84965 10.0688 8.24006 10.1959M12 12V21M12 12L15 15M12 12L9 15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
<p>Browse File to upload!</p>
</div>
<div class="upload-footer">
<svg fill="currentColor" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path d="M15.331 6H8.5v20h15V14.154h-8.169z"></path>
<path d="M18.153 6h-.009v5.342H23.5v-.002z"></path>
</svg>
<p>Not selected file</p>
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.16565 10.1534C5.07629 8.99181 5.99473 8 7.15975 8H16.8402C18.0053 8 18.9237 8.9918 18.8344 10.1534L18.142 19.1534C18.0619 20.1954 17.193 21 16.1479 21H7.85206C6.80699 21 5.93811 20.1954 5.85795 19.1534L5.16565 10.1534Z" stroke="currentColor" stroke-width="2"></path>
<path d="M19.5 5H4.5" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path>
<path d="M10 3C10 2.44772 10.4477 2 11 2H13C13.5523 2 14 2.44772 14 3V5H10V3Z" stroke="currentColor" stroke-width="2"></path>
</svg>
</div>
<input id="custom-file-input" type="file" accept="image/*" style="display:none;" />
</div>
<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</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 class="toolbar-divider"></div>
<button id="tb-change-img" class="bbox-tb-change" title="Upload a different image">📸 Change Image</button>
</div>
"""
)
gr.HTML('<div id="bbox-debug-count">\u2B1C No boxes drawn yet</div>')
boxes_json = gr.Textbox(
value="[]",
elem_id="boxes-json-input",
elem_classes="hidden-input",
container=False
)
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 \u2192 drag to <b>move</b>, "
"drag handles to <b>resize</b>. Use <b>Delete Selected</b> to remove one box."
"</div>"
)
prompt = gr.Textbox(
label="Prompt",
value=DEFAULT_PROMPT,
lines=1,
info="Edit the prompt if needed",
)
run_btn = gr.Button("\U0001F5D1\uFE0F Remove Object", variant="primary", size="lg")
with gr.Column(scale=1):
result = gr.Image(label="Output Image", height=475, format="png")
preview = gr.Image(label="Input Sent to Model (with red boxes)", height=415)
with gr.Accordion("Advanced Settings", open=False, visible=False):
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)
demo.load(fn=None, js=bbox_drawer_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);
console.log('[BBox] submitting', boxes.length, 'boxes:', json);
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(
css=css, theme=purple_theme,
mcp_server=True, ssr_mode=False, show_error=True
)