rmbg/src/rmbg_as_a_service/static/index.html
2026-05-16 18:08:59 -06:00

426 lines
17 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Background Removal &amp; Segmentation</title>
<style>
:root { color-scheme: dark; }
* { box-sizing: border-box; }
body {
margin: 0; font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
background: #15171c; color: #e8e8ea; padding: 24px;
}
h1 { font-size: 1.25rem; font-weight: 600; margin: 0 0 4px; }
.sub { color: #8a8f99; font-size: .85rem; margin-bottom: 16px; }
.wrap { max-width: 1100px; margin: 0 auto; }
.tabs { display: flex; gap: 4px; margin-bottom: 16px; border-bottom: 1px solid #2a2f3a; }
.tab { background: none; border: 0; color: #8a8f99; font-size: .9rem; font-weight: 600;
padding: 10px 16px; cursor: pointer; border-bottom: 2px solid transparent; }
.tab.active { color: #e8e8ea; border-bottom-color: #5b8cff; }
#drop {
border: 2px dashed #3a3f4b; border-radius: 12px; padding: 36px;
text-align: center; cursor: pointer; transition: border-color .15s, background .15s;
}
#drop.over { border-color: #5b8cff; background: #1c2230; }
#drop p { margin: 6px 0; color: #8a8f99; }
.controls { display: flex; gap: 12px; align-items: flex-end; margin: 14px 0; flex-wrap: wrap; }
.controls[hidden] { display: none; }
label.field { display: flex; flex-direction: column; gap: 4px; font-size: .72rem;
color: #8a8f99; text-transform: uppercase; letter-spacing: .04em; }
select, input[type=number], input[type=text] {
background: #2a2f3a; color: #e8e8ea; border: 1px solid #3a3f4b;
border-radius: 8px; padding: 8px 10px; font-size: .9rem;
}
input[type=number] { width: 78px; }
input[type=number]:disabled { opacity: .45; }
input[type=text]#prompt { width: 320px; }
.check { display: flex; align-items: center; gap: 6px; font-size: .85rem;
color: #e8e8ea; cursor: pointer; align-self: end; padding-bottom: 8px; }
.check input { width: 15px; height: 15px; accent-color: #5b8cff; cursor: pointer; }
/* help tooltips */
.help { display: inline-flex; align-items: center; justify-content: center;
width: 14px; height: 14px; margin-left: 5px; border-radius: 50%;
border: 1px solid #4a4f5b; color: #8a8f99; font-size: 9px; font-weight: 700;
font-style: normal; cursor: help; position: relative; vertical-align: middle; }
.help:hover { color: #e8e8ea; border-color: #5b8cff; }
.help:hover::after {
content: attr(data-tip); position: absolute; bottom: 150%; left: 50%;
transform: translateX(-50%); width: 220px; background: #0c0d11;
color: #d8d9dc; border: 1px solid #3a3f4b; border-radius: 6px;
padding: 7px 9px; font-size: .72rem; font-weight: 400; line-height: 1.4;
text-transform: none; letter-spacing: normal; white-space: normal;
z-index: 50; pointer-events: none; }
button.go {
background: #5b8cff; color: #fff; border: 0; border-radius: 8px;
padding: 10px 18px; font-size: .9rem; cursor: pointer; font-weight: 600;
}
button.go:disabled { background: #3a3f4b; cursor: not-allowed; }
button.ghost { background: #2a2f3a; color: #fff; border: 0; border-radius: 8px;
padding: 10px 18px; font-size: .9rem; cursor: pointer; font-weight: 600; }
.go-row { display: flex; gap: 12px; align-items: center; margin: 14px 0; flex-wrap: wrap; }
.status { color: #8a8f99; font-size: .85rem; }
.status.err { color: #ff6b6b; }
.hint { color: #6b7280; font-size: .78rem; margin: -4px 0 4px; }
.panels { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 16px; }
.panel { background: #1c1f27; border-radius: 12px; padding: 12px; }
.panel h2 { font-size: .8rem; font-weight: 600; color: #8a8f99; margin: 0 0 8px;
text-transform: uppercase; letter-spacing: .05em; }
.imgbox {
min-height: 260px; display: flex; align-items: center; justify-content: center;
border-radius: 8px; overflow: hidden;
}
.checker {
background-image:
linear-gradient(45deg, #2a2f3a 25%, transparent 25%),
linear-gradient(-45deg, #2a2f3a 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #2a2f3a 75%),
linear-gradient(-45deg, transparent 75%, #2a2f3a 75%);
background-size: 22px 22px;
background-position: 0 0, 0 11px, 11px -11px, -11px 0;
background-color: #20242d;
}
.imgbox img { max-width: 100%; max-height: 70vh; display: block; }
.imgbox img[src] { cursor: zoom-in; }
@media (max-width: 720px) { .panels { grid-template-columns: 1fr; } }
/* lightbox */
.lightbox { position: fixed; inset: 0; z-index: 100; background: rgba(12,13,17,.97);
display: flex; align-items: center; justify-content: center; }
.lightbox[hidden] { display: none; }
.lb-stage { width: 100vw; height: 100vh; overflow: hidden;
display: flex; align-items: center; justify-content: center; }
.lb-stage img { max-width: 100vw; max-height: 100vh; transform-origin: 0 0;
cursor: grab; user-select: none; -webkit-user-drag: none; will-change: transform; }
.lb-stage.grabbing img { cursor: grabbing; }
.lb-bar { position: fixed; top: 0; left: 0; right: 0; padding: 14px 20px;
z-index: 2; display: flex; justify-content: space-between; align-items: center;
color: #8a8f99; font-size: .8rem; pointer-events: none; }
.lb-close { pointer-events: auto; background: #2a2f3a; color: #e8e8ea;
border: 1px solid #3a3f4b; border-radius: 8px; width: 34px; height: 34px;
font-size: 1rem; line-height: 1; padding: 0; cursor: pointer;
display: flex; align-items: center; justify-content: center; }
</style>
</head>
<body>
<div class="wrap">
<h1>Background Removal &amp; Segmentation</h1>
<div class="sub">Automatic removal, or prompt-conditioned segmentation.</div>
<div class="tabs">
<button class="tab active" data-tab="auto">Auto remove</button>
<button class="tab" data-tab="prompt">Prompt segment</button>
</div>
<div id="drop">
<p><strong>Drop an image here</strong> or click to choose</p>
<p id="fname">No file selected</p>
<input id="file" type="file" accept="image/*" hidden />
</div>
<!-- Auto (BiRefNet / RMBG-2.0) controls -->
<div class="controls" id="ctl-auto">
<label class="field">Model
<select id="model">
<option value="general">general — clean single subjects (fast)</option>
<option value="HR" selected>HR — large / detailed scenes</option>
<option value="portrait">portrait — people</option>
<option value="matting">matting — soft edges / hair</option>
<option value="lite">lite — fastest</option>
<option value="rmbg2">rmbg2 — BRIA RMBG-2.0</option>
</select>
</label>
<label class="field">Resolution
<select id="resolution">
<option value="1024">1024</option>
<option value="1536">1536</option>
<option value="2048">2048</option>
<option value="2560" selected>2560</option>
</select>
</label>
</div>
<!-- Prompt (GroundingDINO + SAM) controls -->
<div class="controls" id="ctl-prompt" hidden>
<label class="field">Prompt — what to keep
<input type="text" id="prompt" placeholder="e.g. the dog · cow. person." />
</label>
<label class="field">
<span>Box threshold<span class="help" data-tip="Minimum confidence for GroundingDINO to keep a detected box. Lower finds more (and looser) objects; higher keeps only strong matches.">?</span></span>
<input type="number" id="boxThr" value="0.3" min="0" max="1" step="0.05" />
</label>
<label class="field">
<span>Text threshold<span class="help" data-tip="How strongly a detection must match your prompt words. Lower = looser word matching; higher = stricter.">?</span></span>
<input type="number" id="textThr" value="0.25" min="0" max="1" step="0.05" />
</label>
</div>
<!-- Shared output controls -->
<div class="controls">
<label class="field">Background
<select id="background">
<option value="alpha" selected>transparent</option>
<option value="white">white</option>
<option value="black">black</option>
<option value="gray">gray</option>
<option value="green">green</option>
<option value="blue">blue</option>
<option value="red">red</option>
</select>
</label>
<label class="field">
<span>Edge offset (px)<span class="help" data-tip="Grow (+) or shrink () the cutout edge by N pixels. A small negative value trims a leftover background-colored fringe around hair or fur.">?</span></span>
<input type="number" id="maskOffset" value="0" min="-20" max="20" step="1" />
</label>
<label class="field">
<span>Feather (px)<span class="help" data-tip="Gaussian blur applied to the mask edge, in pixels. Softens the cutout for smoother compositing onto a new background.">?</span></span>
<input type="number" id="maskBlur" value="0" min="0" max="64" step="1" />
</label>
<label class="check"><input type="checkbox" id="crop" checked /> Crop to subject</label>
<label class="field">Margin (in)
<input type="number" id="cropMargin" value="0" min="0" step="0.1" />
</label>
</div>
<div class="hint" id="hint"></div>
<div class="go-row">
<button class="go" id="go" disabled>Remove background</button>
<a id="dl" download="cutout.png"><button id="dlbtn" class="ghost" disabled>Download PNG</button></a>
<span id="status" class="status"></span>
</div>
<div class="panels">
<div class="panel">
<h2>Original</h2>
<div class="imgbox"><img id="src" alt="" /></div>
</div>
<div class="panel">
<h2>Result</h2>
<div class="imgbox checker"><img id="out" alt="" /></div>
</div>
</div>
</div>
<div id="lightbox" class="lightbox" hidden>
<div class="lb-bar">
<span>scroll to zoom · drag to pan · double-click resets · Esc closes</span>
<button class="lb-close" id="lbClose" title="Close"></button>
</div>
<div class="lb-stage" id="lbStage"><img id="lbImg" alt="" /></div>
</div>
<script>
const drop = document.getElementById('drop');
const fileInput = document.getElementById('file');
const fname = document.getElementById('fname');
const go = document.getElementById('go');
const dl = document.getElementById('dl');
const dlbtn = document.getElementById('dlbtn');
const statusEl = document.getElementById('status');
const srcImg = document.getElementById('src');
const outImg = document.getElementById('out');
const hint = document.getElementById('hint');
const modelSel = document.getElementById('model');
const resSel = document.getElementById('resolution');
const promptInput = document.getElementById('prompt');
const boxThr = document.getElementById('boxThr');
const textThr = document.getElementById('textThr');
const bgSel = document.getElementById('background');
const maskOffset = document.getElementById('maskOffset');
const maskBlur = document.getElementById('maskBlur');
const cropChk = document.getElementById('crop');
const cropMargin = document.getElementById('cropMargin');
const ctlAuto = document.getElementById('ctl-auto');
const ctlPrompt = document.getElementById('ctl-prompt');
let selectedFile = null;
let tab = 'auto';
const HINTS = {
auto: 'Large or busy scenes segment best with HR at 2048+. The general model expects a clear single subject at 1024.',
prompt: 'Type what to keep, e.g. "the dog" (or several: "cow. person."). Lower the box threshold to detect more / fainter objects.',
};
function setTab(name) {
tab = name;
document.querySelectorAll('.tab').forEach(t =>
t.classList.toggle('active', t.dataset.tab === name));
ctlAuto.hidden = name !== 'auto';
ctlPrompt.hidden = name !== 'prompt';
go.textContent = name === 'auto' ? 'Remove background' : 'Segment';
hint.textContent = HINTS[name];
}
document.querySelectorAll('.tab').forEach(t =>
t.addEventListener('click', () => setTab(t.dataset.tab)));
cropChk.addEventListener('change', () => { cropMargin.disabled = !cropChk.checked; });
function setStatus(msg, isErr) {
statusEl.textContent = msg;
statusEl.className = 'status' + (isErr ? ' err' : '');
}
function pickFile(file) {
if (!file || !file.type.startsWith('image/')) {
setStatus('Please choose an image file.', true);
return;
}
selectedFile = file;
fname.textContent = file.name + ' (' + Math.round(file.size / 1024) + ' KB)';
srcImg.src = URL.createObjectURL(file);
outImg.removeAttribute('src');
dlbtn.disabled = true;
go.disabled = false;
setStatus('');
}
drop.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', e => pickFile(e.target.files[0]));
['dragenter', 'dragover'].forEach(ev =>
drop.addEventListener(ev, e => { e.preventDefault(); drop.classList.add('over'); }));
['dragleave', 'drop'].forEach(ev =>
drop.addEventListener(ev, e => { e.preventDefault(); drop.classList.remove('over'); }));
drop.addEventListener('drop', e => pickFile(e.dataTransfer.files[0]));
function fileToBase64(file) {
return new Promise((resolve, reject) => {
const r = new FileReader();
r.onload = () => resolve(r.result.split(',')[1]); // strip data URL prefix
r.onerror = reject;
r.readAsDataURL(file);
});
}
// --- lightbox: click to inspect, scroll to zoom, drag to pan ---
const lightbox = document.getElementById('lightbox');
const lbStage = document.getElementById('lbStage');
const lbImg = document.getElementById('lbImg');
const lbClose = document.getElementById('lbClose');
let lbScale = 1, lbTx = 0, lbTy = 0, lbDrag = null;
function lbApply() {
lbImg.style.transform = `translate(${lbTx}px, ${lbTy}px) scale(${lbScale})`;
}
function lbReset() { lbScale = 1; lbTx = 0; lbTy = 0; lbApply(); }
function openLightbox(src, isResult) {
if (!src) return;
lbImg.src = src;
lbImg.classList.toggle('checker', !!isResult);
lbReset();
lightbox.hidden = false;
}
function closeLightbox() { lightbox.hidden = true; lbImg.removeAttribute('src'); }
srcImg.addEventListener('click', () => openLightbox(srcImg.getAttribute('src'), false));
outImg.addEventListener('click', () => openLightbox(outImg.getAttribute('src'), true));
lbClose.addEventListener('click', closeLightbox);
lightbox.addEventListener('mousedown', e => {
if (e.target === lightbox || e.target === lbStage) closeLightbox();
});
document.addEventListener('keydown', e => {
if (e.key === 'Escape' && !lightbox.hidden) closeLightbox();
});
lbStage.addEventListener('wheel', e => {
e.preventDefault();
const rect = lbImg.getBoundingClientRect();
const cx = e.clientX - rect.left, cy = e.clientY - rect.top;
const factor = e.deltaY < 0 ? 1.2 : 1 / 1.2;
const newScale = Math.min(8, Math.max(1, lbScale * factor));
const ratio = newScale / lbScale;
lbTx -= cx * (ratio - 1);
lbTy -= cy * (ratio - 1);
lbScale = newScale;
if (lbScale === 1) { lbTx = 0; lbTy = 0; }
lbApply();
}, { passive: false });
lbImg.addEventListener('mousedown', e => {
e.preventDefault();
lbDrag = { x: e.clientX, y: e.clientY, tx: lbTx, ty: lbTy };
lbStage.classList.add('grabbing');
});
window.addEventListener('mousemove', e => {
if (!lbDrag) return;
lbTx = lbDrag.tx + (e.clientX - lbDrag.x);
lbTy = lbDrag.ty + (e.clientY - lbDrag.y);
lbApply();
});
window.addEventListener('mouseup', () => {
lbDrag = null;
lbStage.classList.remove('grabbing');
});
lbImg.addEventListener('dblclick', e => { e.preventDefault(); lbReset(); });
go.addEventListener('click', async () => {
if (!selectedFile) return;
if (tab === 'prompt' && !promptInput.value.trim()) {
setStatus('Enter a prompt describing what to keep.', true);
return;
}
go.disabled = true;
dlbtn.disabled = true;
setStatus('Processing… (first use of a model downloads its weights)');
const t0 = performance.now();
try {
const b64 = await fileToBase64(selectedFile);
const shared = {
image: b64,
background: bgSel.value,
mask_offset: parseInt(maskOffset.value, 10) || 0,
mask_blur: parseInt(maskBlur.value, 10) || 0,
crop: cropChk.checked,
crop_margin: parseFloat(cropMargin.value) || 0,
};
let endpoint, body;
if (tab === 'auto') {
endpoint = '/predict';
body = { ...shared, model: modelSel.value, resolution: parseInt(resSel.value, 10) };
} else {
endpoint = '/segment';
body = { ...shared, prompt: promptInput.value.trim(),
box_threshold: parseFloat(boxThr.value) || 0.3,
text_threshold: parseFloat(textThr.value) || 0.25 };
}
const resp = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!resp.ok) throw new Error('HTTP ' + resp.status + ': ' + (await resp.text()));
const data = await resp.json();
const dataUrl = 'data:image/png;base64,' + data.image;
outImg.src = dataUrl;
dl.href = dataUrl;
dl.download = selectedFile.name.replace(/\.[^.]+$/, '') + '.png';
dlbtn.disabled = false;
const secs = ((performance.now() - t0) / 1000).toFixed(1);
if (tab === 'auto') {
setStatus(`Done — ${data.width}×${data.height} · ${data.model} @ ${data.resolution} · ${secs}s`);
} else {
const n = data.detections;
setStatus(`Done — ${n} object${n === 1 ? '' : 's'} matched "${data.prompt}" · ` +
`${data.width}×${data.height} · ${secs}s` +
(n === 0 ? ' (try a lower box threshold)' : ''));
}
} catch (err) {
setStatus(err.message || String(err), true);
} finally {
go.disabled = false;
}
});
setTab('auto');
</script>
</body>
</html>