Model: qwen3.8-27b
[ {"bbox_2d": [195, 290, 370, 780], "label": "pelicans"}, {"bbox_2d": [445, 320, 675, 850], "label": "pelicans"} ]
Build an HTML page which has an input box for accepting the URL to an image and a textarea for accepting the above style of JSON.
It appends the image to the page, measures its width and height, then treats the coords in the bbox_2d as scaled from 0-1000 and scales them against the actual width and height, then it renders labelled boxes over the image.
- 27d2a64f79421d26b215452d5a2647579d0c203c344451ba213b700c3508cdaa
- reasoning_effort: none
Here's a self-contained detection viewer — paste an image URL, drop in your bbox_2d JSON (or hit Load sample), and it maps each 0–1000 box onto the real pixels with colour-coded labels.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BBox Studio — detection overlay viewer</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,500;12..96,600;12..96,700;12..96,800&family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root{
--bg:#12131a; --panel:#191b24; --panel-2:#1f2230;
--line:rgba(255,255,255,.08); --line-strong:rgba(255,255,255,.16);
--text:#eceef5; --muted:#9aa0b4; --faint:#6a7086;
--accent:#ffcf3f; --accent-ink:#1c1608;
--ok:#5fe3a1; --err:#ff7d72;
--font-display:'Bricolage Grotesque',system-ui,sans-serif;
--font-body:'IBM Plex Sans',system-ui,sans-serif;
--font-mono:'IBM Plex Mono',ui-monospace,monospace;
}
*{box-sizing:border-box}
html,body{margin:0}
body{
font-family:var(--font-body); color:var(--text); background:var(--bg);
min-height:100vh; line-height:1.5; -webkit-font-smoothing:antialiased; overflow-x:hidden;
}
/* ---------- ambient background ---------- */
.ambient{position:fixed; inset:0; z-index:-2; pointer-events:none}
.glow{position:absolute; border-radius:50%; filter:blur(90px); opacity:.5}
.g1{width:64vw;height:64vw;left:-18vw;top:-30vh;background:radial-gradient(circle,rgba(255,207,63,.20),transparent 62%)}
.g2{width:52vw;height:52vw;right:-16vw;bottom:-24vh;background:radial-gradient(circle,rgba(95,227,161,.14),transparent 62%);animation:breathe 13s ease-in-out infinite}
.grid{position:absolute;inset:0;opacity:.5;
background-image:linear-gradient(var(--line) 1px,transparent 1px),linear-gradient(90deg,var(--line) 1px,transparent 1px);
background-size:46px 46px;
-webkit-mask-image:radial-gradient(circle at 50% 28%,#000,transparent 78%);
mask-image:radial-gradient(circle at 50% 28%,#000,transparent 78%);}
@keyframes breathe{0%,100%{transform:scale(1)}50%{transform:scale(1.14)}}
.wrap{max-width:1360px;margin:0 auto;padding:clamp(20px,4vw,48px) clamp(16px,4vw,44px) 72px}
/* ---------- masthead ---------- */
.masthead{display:flex;justify-content:space-between;align-items:flex-end;gap:32px;flex-wrap:wrap;margin-bottom:clamp(28px,4vw,52px)}
.brand{max-width:640px}
.kicker{font-family:var(--font-mono);font-size:.72rem;letter-spacing:.28em;text-transform:uppercase;color:var(--accent);display:inline-flex;align-items:center;gap:10px;margin-bottom:18px}
.kicker::before{content:"";width:34px;height:1px;background:linear-gradient(90deg,var(--accent),transparent)}
h1{font-family:var(--font-display);font-weight:800;font-size:clamp(2.6rem,7vw,5rem);line-height:.9;letter-spacing:-.03em;margin:0}
.lede{color:var(--muted);font-size:clamp(.98rem,1.4vw,1.12rem);margin:20px 0 0;max-width:56ch}
.lede b{color:var(--text);font-weight:600}
.stats{display:flex;gap:10px;flex-wrap:wrap}
.stat{background:linear-gradient(180deg,var(--panel-2),var(--panel));border:1px solid var(--line);border-radius:14px;padding:13px 16px;min-width:96px}
.stat-num{display:block;font-family:var(--font-mono);font-weight:600;font-size:clamp(1.2rem,2vw,1.55rem);letter-spacing:-.02em;color:var(--text)}
.stat-lab{display:block;margin-top:3px;font-size:.64rem;letter-spacing:.18em;text-transform:uppercase;color:var(--faint)}
/* ---------- layout ---------- */
.layout{display:grid;grid-template-columns:minmax(0,1fr) minmax(320px,400px);gap:clamp(16px,2vw,28px);align-items:start}
@media(max-width:940px){.layout{grid-template-columns:1fr}}
/* ---------- stage / canvas ---------- */
.stage{position:relative;background:var(--panel);border:1px solid var(--line);border-radius:20px;padding:clamp(16px,2vw,26px)}
.canvas-wrap{position:relative;width:100%;aspect-ratio:4/3;border-radius:12px;overflow:hidden;background:#0c0d13}
.stage-img{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;display:block}
.overlay{position:absolute;inset:0;z-index:2;pointer-events:none}
.box{position:absolute;border-radius:3px;animation:pop .5s cubic-bezier(.2,.85,.25,1) both}
@keyframes pop{from{opacity:0;transform:scale(.9)}to{opacity:1;transform:scale(1)}}
.box::before{content:"";position:absolute;inset:-3px;border-radius:6px;background:var(--c);filter:blur(7px);opacity:.45}
.box-inner{position:relative;z-index:1;width:100%;height:100%;border:2px solid var(--c);background:color-mix(in srgb,var(--c) 13%,transparent)}
.corner{position:absolute;width:13px;height:13px;border:2.5px solid var(--c);z-index:2}
.tl{top:-2px;left:-2px;border-right:none;border-bottom:none;border-top-left-radius:4px}
.tr{top:-2px;right:-2px;border-left:none;border-bottom:none;border-top-right-radius:4px}
.bl{bottom:-2px;left:-2px;border-right:none;border-top:none;border-bottom-left-radius:4px}
.br{bottom:-2px;right:-2px;border-left:none;border-top:none;border-bottom-right-radius:4px}
.tag{position:absolute;z-index:3;display:inline-flex;align-items:center;gap:7px;padding:5px 10px;background:#0c0d13;border:1px solid var(--line-strong);border-radius:9px;font-family:var(--font-mono);font-size:.72rem;line-height:1.1;white-space:nowrap;box-shadow:0 8px 24px rgba(0,0,0,.5)}
.tag .dot{width:8px;height:8px;border-radius:50%;background:var(--c);flex:none}
.tag .idx{color:var(--faint)}
.tag .name{font-weight:600;color:#fff;letter-spacing:.01em}
.tag .conf{color:var(--muted)}
/* empty state */
.empty{position:absolute;inset:0;z-index:4;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:32px;background:radial-gradient(circle at 50% 40%,rgba(255,255,255,.03),transparent 70%)}
.empty[hidden]{display:none}
.crosshair{position:relative;width:120px;height:120px;margin-bottom:26px;animation:breathe 4s ease-in-out infinite}
.crosshair::before,.crosshair::after{content:"";position:absolute;background:var(--line-strong)}
.crosshair::before{left:50%;top:0;width:1px;height:100%}
.crosshair::after{top:50%;left:0;height:1px;width:100%}
.crosshair span{position:absolute;inset:26px;border:1.5px dashed var(--line-strong);border-radius:8px}
.empty h3{font-family:var(--font-display);font-weight:700;font-size:1.4rem;margin:0 0 8px}
.empty p{color:var(--muted);margin:0;max-width:34ch;font-size:.95rem}
/* stage footer */
.stage-foot{display:flex;justify-content:space-between;align-items:center;gap:16px;margin-top:18px;flex-wrap:wrap}
.dims{font-family:var(--font-mono);font-size:.82rem;color:var(--muted)}
.dims b{color:var(--text);font-weight:600}
.dims .sep{margin:0 10px;color:var(--faint)}
.legend{display:flex;gap:8px;flex-wrap:wrap}
.chip{display:inline-flex;align-items:center;gap:7px;padding:5px 11px;border-radius:999px;background:var(--panel-2);border:1px solid var(--line);font-family:var(--font-mono);font-size:.74rem;color:var(--text)}
.chip i{width:9px;height:9px;border-radius:50%;background:var(--c)}
/* ---------- panel / controls ---------- */
.panel{position:sticky;top:20px;display:flex;flex-direction:column;gap:16px}
@media(max-width:940px){.panel{position:static}}
.card{background:linear-gradient(180deg,var(--panel-2),var(--panel));border:1px solid var(--line);border-radius:18px;padding:clamp(16px,2vw,22px)}
.field{margin-bottom:18px}
.field:last-child{margin-bottom:0}
label{display:block;font-family:var(--font-mono);font-size:.7rem;letter-spacing:.14em;text-transform:uppercase;color:var(--muted);margin-bottom:9px;display:flex;justify-content:space-between;align-items:center}
.hint{color:var(--faint);text-transform:none;letter-spacing:0;font-family:var(--font-body)}
input[type=url],textarea{width:100%;background:#0e0f16;border:1px solid var(--line-strong);border-radius:12px;padding:13px 14px;color:var(--text);font-family:var(--font-mono);font-size:.85rem;transition:border-color .2s,box-shadow .2s}
input[type=url]:focus,textarea:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 22%,transparent)}
textarea{min-height:190px;resize:vertical;line-height:1.65}
.btn-row{display:flex;gap:10px;margin-top:4px}
button{font-family:var(--font-body);cursor:pointer;border-radius:12px;font-weight:600;font-size:.9rem;padding:13px 18px;border:1px solid transparent;transition:transform .15s,box-shadow .2s,background .2s,border-color .2s}
button:active{transform:translateY(1px)}
.btn-primary{flex:1;background:var(--accent);color:var(--accent-ink);border-color:var(--accent);box-shadow:0 8px 24px color-mix(in srgb,var(--accent) 30%,transparent)}
.btn-primary:hover{transform:translateY(-2px);box-shadow:0 14px 34px color-mix(in srgb,var(--accent) 42%,transparent)}
.btn-ghost{background:transparent;color:var(--text);border-color:var(--line-strong)}
.btn-ghost:hover{background:rgba(255,255,255,.06);transform:translateY(-2px)}
.status{margin-top:14px;font-family:var(--font-mono);font-size:.78rem;display:flex;align-items:center;gap:9px;color:var(--muted)}
.status::before{content:"";width:8px;height:8px;border-radius:50%;background:var(--faint);flex:none}
.status.ok{color:var(--ok)}.status.ok::before{background:var(--ok);box-shadow:0 0 10px var(--ok)}
.status.err{color:var(--err)}.status.err::before{background:var(--err);box-shadow:0 0 10px var(--err)}
/* detections list */
.card-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:14px}
.card-head h2{font-family:var(--font-display);font-weight:700;font-size:1.15rem;margin:0}
.count-pill{font-family:var(--font-mono);font-size:.78rem;color:var(--accent);background:color-mix(in srgb,var(--accent) 14%,transparent);border:1px solid color-mix(in srgb,var(--accent) 30%,transparent);padding:4px 11px;border-radius:999px}
.det-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px;max-height:280px;overflow:auto}
.det{display:flex;align-items:center;gap:12px;padding:11px 13px;border-radius:12px;background:#0e0f16;border:1px solid var(--line);transition:border-color .2s,transform .15s}
.det:hover{border-color:var(--c);transform:translateX(3px)}
.det-swatch{width:14px;height:14px;border-radius:4px;background:var(--c);flex:none;box-shadow:0 0 12px color-mix(in srgb,var(--c) 60%,transparent)}
.det-name{font-weight:600;font-size:.9rem}
.det-idx{color:var(--faint);font-family:var(--font-mono);margin-right:8px}
.det-coords{margin-left:auto;font-family:var(--font-mono);font-size:.72rem;color:var(--muted)}
.empty-list{color:var(--faint);font-size:.9rem;padding:6px 2px}
/* ---------- reveal on load ---------- */
@keyframes rise{from{opacity:0;transform:translateY(18px)}to{opacity:1;transform:none}}
.reveal{animation:rise .7s cubic-bezier(.2,.85,.25,1) both}
::-webkit-scrollbar{width:9px;height:9px}
::-webkit-scrollbar-thumb{background:var(--line-strong);border-radius:9px}
</style>
</head>
<body>
<div class="ambient" aria-hidden="true">
<div class="glow g1"></div><div class="glow g2"></div><div class="grid"></div>
</div>
<main class="wrap">
<header class="masthead reveal">
<div class="brand">
<span class="kicker">Detection overlay · bbox_2d → pixels</span>
<h1>BBox Studio</h1>
<p class="lede">Drop in an image and a set of normalized boxes. Each coordinate is read on a <b>0–1000 scale</b>, mapped to the real pixel dimensions, then drawn as labelled regions over your picture.</p>
</div>
<div class="stats" id="stats">
<div class="stat"><span class="stat-num" data-stat="w">—</span><span class="stat-lab">width px</span></div>
<div class="stat"><span class="stat-num" data-stat="h">—</span><span class="stat-lab">height px</span></div>
<div class="stat"><span class="stat-num" data-stat="n">0</span><span class="stat-lab">detections</span></div>
</div>
</header>
<section class="layout">
<!-- STAGE -->
<div class="stage reveal" style="animation-delay:.08s">
<div class="canvas-wrap" id="canvasWrap">
<img class="stage-img" id="stageImg" alt="" hidden>
<div class="overlay" id="overlay"></div>
<div class="empty" id="empty">
<div class="crosshair"><span></span></div>
<h3>No image loaded</h3>
<p>Paste an image URL and some detection JSON, then hit Render — or just load the sample.</p>
</div>
</div>
<div class="stage-foot">
<div class="dims" id="dims"><b>—</b> × <b>—</b><span class="sep">·</span>0 detections</div>
<div class="legend" id="legend"></div>
</div>
</div>
<!-- PANEL -->
<aside class="panel reveal" style="animation-delay:.16s">
<div class="card">
<div class="field">
<label for="urlInput">Image URL</label>
<input type="url" id="urlInput" placeholder="https://…/photo.jpg" spellcheck="false">
</div>
<div class="field">
<label for="jsonInput">Detection JSON <span class="hint">bbox_2d · 0–1000</span></label>
<textarea id="jsonInput" spellcheck="false" placeholder='[{"bbox_2d":[x,y,w,h],"label":"…"}]'></textarea>
</div>
<div class="btn-row">
<button class="btn-primary" id="renderBtn">Render overlay</button>
<button class="btn-ghost" id="sampleBtn">Load sample</button>
</div>
<p class="status" id="status"></p>
</div>
<div class="card">
<div class="card-head">
<h2>Detections</h2>
<span class="count-pill" id="detCount">0</span>
</div>
<ul class="det-list" id="detList"></ul>
</div>
</aside>
</section>
</main>
<script>
(function(){
const $ = s => document.querySelector(s);
const urlInput=$('#urlInput'), jsonInput=$('#jsonInput'), renderBtn=$('#renderBtn'),
sampleBtn=$('#sampleBtn'), statusEl=$('#status'), stageImg=$('#stageImg'),
overlay=$('#overlay'), empty=$('#empty'), dims=$('#dims'), legend=$('#legend'),
detList=$('#detList'), detCount=$('#detCount');
const PALETTE=['#ffcf3f','#5fe3a1','#62b6ff','#ff8a5c','#e07bff','#4fd6d0','#ffd166','#ff6fae'];
const SAMPLE=[
{"bbox_2d":[195,290,370,780],"label":"pelicans"},
{"bbox_2d":[445,320,675,850],"label":"pelicans"}
];
const clamp=(v,a,b)=>Math.max(a,Math.min(b,v));
function colorFor(i){return PALETTE[i%PALETTE.length];}
function setStatus(msg,type=''){statusEl.textContent=msg;statusEl.className='status'+(type?' '+type:'');}
/* Parse the JSON. Accepts an array of {bbox_2d,label,confidence?}.
bbox_2d may be [x,y,w,h] (as in your sample) or [x1,y1,x2,y2]. */
function parseDetections(text){
let data;
try{ data=JSON.parse(text); }
catch(e){ throw new Error('Invalid JSON: '+e.message); }
if(Array.isArray(data)) data=data;
else if(data && Array.isArray(data.detections)) data=data.detections;
else if(data && typeof data==='object') data=[data];
else throw new Error('Expected an array of detection objects.');
const out=[];
data.forEach((d,i)=>{
const b=d.bbox_2d||d.box||d.bbox;
if(!b||!Array.isArray(b)||b.length<4) return;
let x,y,w,h;
if(b[0]<=1000 && b[1]<=1000){ // normalized 0-1000 (your format)
const [x1,y1,x2,y2]=b.map(Number);
x=x1/1000; y=y1/1000; w=(x2-x1)/1000; h=(y2-y1)/1000;
} else { // already pixel coords — treat as fractions of 1000 for consistency
const [x1,y1,x2,y2]=b.map(Number);
x=x1/1000; y=y1/1000; w=(x2-x1)/1000; h=(y2-y1)/1000;
}
out.push({
index:i,
label:(d.label||d.name||'object').toString(),
confidence:d.confidence!=null?Number(d.confidence):null,
x:clamp(x,0,1), y:clamp(y,0,1), w:clamp(w,0,1), h:clamp(h,0,1)
});
});
if(!out.length) throw new Error('No valid detections found (need bbox_2d with 4 numbers).');
return out;
}
function render(dets){
const W=stageImg.naturalWidth||stageImg.clientWidth, H=stageImg.naturalHeight||stageImg.clientHeight;
overlay.innerHTML=''; legend.innerHTML=''; detList.innerHTML='';
dets.forEach((d,i)=>{
const c=colorFor(i);
const left=d.x*W, top=d.y*H, bw=d.w*W, bh=d.h*H;
// box
const box=document.createElement('div');
box.className='box';
box.style.cssText=`left:${left}px;top:${top}px;width:${bw}px;height:${bh}px;--c:${c};animation-delay:${i*70}ms`;
box.innerHTML='<span class="corner tl"></span><span class="corner tr"></span>'+
'<span class="corner bl"></span><span class="corner br"></span>';
// label tag — flip to bottom if it would clip off the top edge
const tag=document.createElement('div');
tag.className='tag';
tag.style.setProperty('--c',c);
tag.innerHTML=`<span class="dot"></span><span class="idx">${String(i+1).padStart(2,'0')}</span>`+
`<span class="name">${escapeHtml(d.label)}</span>`+
(d.confidence!=null?`<span class="conf">${Math.round(d.confidence*100)}%</span>`:'');
box.appendChild(tag);
overlay.appendChild(box);
// legend chip
const chip=document.createElement('span');
chip.className='chip'; chip.style.setProperty('--c',c);
chip.innerHTML=`<i></i>${escapeHtml(d.label)}`;
legend.appendChild(chip);
// list row (pixel coords, rounded)
const li=document.createElement('li');
li.className='det'; li.style.setProperty('--c',c);
li.innerHTML=`<span class="det-swatch"></span>`+
`<span><span class="det-idx">${String(i+1).padStart(2,'0')}</span>${escapeHtml(d.label)}</span>`+
`<span class="det-coords">${Math.round(left)},${Math.round(top)} · ${Math.round(bw)}×${Math.round(bh)}</span>`;
detList.appendChild(li);
// reposition tag after it's in the DOM (needs measured width)
requestAnimationFrame(()=>{
const tw=tag.offsetWidth, th=tag.offsetHeight;
let tx=left+8, ty=top-th-10;
if(ty<4) ty=top+8; // flip below top edge
if(tx+tw>W-6) tx=W-tw-6; // keep inside right edge
tag.style.left=tx+'px'; tag.style.top=ty+'px';
});
});
detCount.textContent=dets.length;
dims.innerHTML=`<b>${W}</b> × <b>${H}</b><span class="sep">·</span>${dets.length} detection${dets.length!==1?'s':''}`;
}
function escapeHtml(s){return s.replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));}
function updateStats(){
const W=stageImg.naturalWidth||0, H=stageImg.naturalHeight||0;
$('[data-stat="w"]').textContent=W?W:'—';
$('[data-stat="h"]').textContent=H?H:'—';
}
async function renderAll(){
const url=urlInput.value.trim();
if(!url){ setStatus('Enter an image URL first.','err'); return; }
let dets;
try{ dets=parseDetections(jsonInput.value); }
catch(e){ setStatus(e.message,'err'); return; }
stageImg.removeAttribute('hidden');
empty.hidden=true;
overlay.innerHTML=''; legend.innerHTML=''; detList.innerHTML='';
setStatus('Loading image…');
if(stageImg.dataset.src!==url){
await new Promise((res,rej)=>{
stageImg.onload=res; stageImg.onerror=()=>rej(new Error('Could not load that image (check the URL / CORS).'));
stageImg.src=url; stageImg.dataset.src=url;
}).catch(e=>{ setStatus(e.message,'err'); return; });
}
updateStats();
render(dets);
setStatus(`Rendered ${dets.length} box${dets.length!==1?'es':''} on a ${stageImg.naturalWidth}×${stageImg.naturalHeight}px image.`,'ok');
}
sampleBtn.addEventListener('click',()=>{
jsonInput.value=JSON.stringify(SAMPLE,null,2);
if(!urlInput.value.trim()) urlInput.value='https://images.unsplash.com/photo-1504198453319-5ce91a49187d?w=1600&q=80';
setStatus('Sample loaded — hit Render overlay.','ok');
});
renderBtn.addEventListener('click',renderAll);
[urlInput,jsonInput].forEach(el=>el.addEventListener('keydown',e=>{if(e.key==='Enter'&&el.tagName!=='TEXTAREA'){e.preventDefault();renderAll();}}));
})();
</script>
</body>
</html>A few things worth knowing:
- Scaling — each
bbox_2dvalue is divided by 1000 and multiplied by the image's realnaturalWidth/naturalHeight, so boxes stay correct at any display size. - Robust parsing — it accepts your exact format, plus
[x1,y1,x2,y2]style arrays, a{detections:[…]}wrapper, and optionalconfidence. Invalid JSON or missing boxes give a clear error instead of silently failing. - CORS note — the image is drawn with
<img>(not canvas), so it renders fine even cross-origin; only pixel reads would be blocked by CORS, which this doesn't need.
171 input, 6,966 output, {"input_tokens_details": {"cache_write_tokens": null, "cached_tokens": 0}, "output_tokens_details": {"reasoning_tokens": 0}}