import { MODEL_OPTIONS, detectObjects, getModelOption } from './detector.js'; const imageInput = document.querySelector('#image-input'); const modelSelect = document.querySelector('#model-select'); const modelNote = document.querySelector('#model-note'); const threshold = document.querySelector('#threshold'); const thresholdValue = document.querySelector('#threshold-value'); const runButton = document.querySelector('#run-button'); const statusEl = document.querySelector('#status'); const progressWrap = document.querySelector('#progress-wrap'); const progressBar = document.querySelector('#progress-bar'); const canvasWrap = document.querySelector('#canvas-wrap'); const canvas = document.querySelector('#result-canvas'); const ctx = canvas.getContext('2d'); const summary = document.querySelector('#summary'); const tbody = document.querySelector('#detections-body'); const runtimeBadge = document.querySelector('#runtime-badge'); const sampleGrid = document.querySelector('#sample-grid'); const shuffleButton = document.querySelector('#shuffle-examples'); const state = { image: null, dataUrl: null, }; const EXAMPLES = [ { src: './examples/000000010092.jpg', label: 'Bedroom furniture' }, { src: './examples/000000037777.jpg', label: 'Kitchen objects' }, { src: './examples/000000039769.jpg', label: 'Cats & remotes' }, { src: './examples/000000063965.jpg', label: 'Cake & person' }, { src: './examples/000000087038.jpg', label: 'Skate park' }, { src: './examples/000000090208.jpg', label: 'Double-decker buses' }, { src: './examples/000000097278.jpg', label: 'Snowboarder' }, { src: './examples/000000167898.jpg', label: 'Bathroom sink & toilet' }, { src: './examples/000000174482.jpg', label: 'Bike & traffic' }, { src: './examples/000000181666.jpg', label: 'Herd & person' }, { src: './examples/000000189806.jpg', label: 'Cat & dog at home' }, { src: './examples/000000199442.jpg', label: 'Surfer' }, { src: './examples/000000210520.jpg', label: 'Dining table' }, { src: './examples/000000252219.jpg', label: 'Street scene' }, { src: './examples/000000286458.jpg', label: 'Packed suitcase' }, { src: './examples/000000365886.jpg', label: 'Person & cell phone' }, { src: './examples/000000376310.jpg', label: 'Bathroom objects' }, { src: './examples/000000397133.jpg', label: 'Busy kitchen' }, { src: './examples/000000429281.jpg', label: 'Bananas at market' }, { src: './examples/000000431876.jpg', label: 'Horses & riders' }, { src: './examples/000000482978.jpg', label: 'Passenger train' }, { src: './examples/000000500270.jpg', label: 'Skateboarder' }, { src: './examples/000000517523.jpg', label: 'Airplane landing' }, { src: './examples/000000570448.jpg', label: 'Boat at the beach' }, { src: './examples/000000382122.jpg', label: 'COCO · 382122' }, { src: './examples/000000021879.jpg', label: 'COCO · 021879' }, { src: './examples/000000124975.jpg', label: 'COCO · 124975' }, { src: './examples/000000507975.jpg', label: 'COCO · 507975' }, { src: './examples/000000364297.jpg', label: 'COCO · 364297' }, { src: './examples/000000273711.jpg', label: 'COCO · 273711' }, { src: './examples/000000230819.jpg', label: 'COCO · 230819' }, { src: './examples/000000033854.jpg', label: 'COCO · 033854' }, { src: './examples/000000064462.jpg', label: 'COCO · 064462' }, { src: './examples/000000175443.jpg', label: 'COCO · 175443' }, { src: './examples/000000132408.jpg', label: 'COCO · 132408' }, { src: './examples/000000456394.jpg', label: 'COCO · 456394' }, { src: './examples/000000422670.jpg', label: 'COCO · 422670' }, { src: './examples/000000550797.jpg', label: 'COCO · 550797' }, { src: './examples/000000061333.jpg', label: 'COCO · 061333' }, { src: './examples/000000071877.jpg', label: 'COCO · 071877' }, { src: './examples/000000038210.jpg', label: 'COCO · 038210' }, { src: './examples/000000220764.jpg', label: 'COCO · 220764' }, { src: './examples/000000561679.jpg', label: 'COCO · 561679' }, { src: './examples/000000229216.jpg', label: 'COCO · 229216' }, { src: './examples/000000462614.jpg', label: 'COCO · 462614' }, { src: './examples/000000204871.jpg', label: 'COCO · 204871' }, { src: './examples/000000546823.jpg', label: 'COCO · 546823' }, { src: './examples/000000454067.jpg', label: 'COCO · 454067' }, { src: './examples/000000361571.jpg', label: 'COCO · 361571' }, { src: './examples/000000140556.jpg', label: 'COCO · 140556' }, { src: './examples/000000175438.jpg', label: 'COCO · 175438' }, { src: './examples/000000297578.jpg', label: 'COCO · 297578' }, { src: './examples/000000162732.jpg', label: 'COCO · 162732' }, { src: './examples/000000147415.jpg', label: 'COCO · 147415' }, { src: './examples/000000458702.jpg', label: 'COCO · 458702' }, { src: './examples/000000416745.jpg', label: 'COCO · 416745' }, { src: './examples/000000122166.jpg', label: 'COCO · 122166' }, { src: './examples/000000438862.jpg', label: 'COCO · 438862' }, { src: './examples/000000445658.jpg', label: 'COCO · 445658' }, { src: './examples/000000465129.jpg', label: 'COCO · 465129' }, { src: './examples/000000546626.jpg', label: 'COCO · 546626' }, { src: './examples/000000442009.jpg', label: 'COCO · 442009' }, { src: './examples/000000166277.jpg', label: 'COCO · 166277' }, { src: './examples/000000017379.jpg', label: 'COCO · 017379' }, { src: './examples/000000053994.jpg', label: 'COCO · 053994' }, { src: './examples/000000372203.jpg', label: 'COCO · 372203' }, { src: './examples/000000028809.jpg', label: 'COCO · 028809' }, { src: './examples/000000163290.jpg', label: 'COCO · 163290' }, { src: './examples/000000133819.jpg', label: 'COCO · 133819' }, { src: './examples/000000239318.jpg', label: 'COCO · 239318' }, { src: './examples/000000116208.jpg', label: 'COCO · 116208' }, { src: './examples/000000106235.jpg', label: 'COCO · 106235' }, { src: './examples/000000464522.jpg', label: 'COCO · 464522' }, { src: './examples/000000147205.jpg', label: 'COCO · 147205' }, { src: './examples/000000575500.jpg', label: 'COCO · 575500' }, { src: './examples/000000005600.jpg', label: 'COCO · 005600' }, { src: './examples/000000462629.jpg', label: 'COCO · 462629' }, { src: './examples/000000499109.jpg', label: 'COCO · 499109' }, { src: './examples/000000066523.jpg', label: 'COCO · 066523' }, { src: './examples/000000228144.jpg', label: 'COCO · 228144' }, { src: './examples/000000161397.jpg', label: 'COCO · 161397' }, { src: './examples/000000082688.jpg', label: 'COCO · 082688' }, { src: './examples/000000305343.jpg', label: 'COCO · 305343' }, { src: './examples/000000150649.jpg', label: 'COCO · 150649' }, { src: './examples/000000253002.jpg', label: 'COCO · 253002' }, { src: './examples/000000410735.jpg', label: 'COCO · 410735' }, { src: './examples/000000363666.jpg', label: 'COCO · 363666' }, { src: './examples/000000514540.jpg', label: 'COCO · 514540' }, { src: './examples/000000546964.jpg', label: 'COCO · 546964' }, { src: './examples/000000301718.jpg', label: 'COCO · 301718' }, { src: './examples/000000402473.jpg', label: 'COCO · 402473' }, { src: './examples/000000566524.jpg', label: 'COCO · 566524' }, { src: './examples/000000468332.jpg', label: 'COCO · 468332' }, { src: './examples/000000215259.jpg', label: 'COCO · 215259' }, { src: './examples/000000067315.jpg', label: 'COCO · 067315' }, { src: './examples/000000055002.jpg', label: 'COCO · 055002' }, { src: './examples/000000555412.jpg', label: 'COCO · 555412' }, { src: './examples/000000227187.jpg', label: 'COCO · 227187' }, { src: './examples/000000047571.jpg', label: 'COCO · 047571' }, { src: './examples/000000231580.jpg', label: 'COCO · 231580' }, ]; function chooseRandom(items, count) { return [...items] .map((item) => ({ item, sort: Math.random() })) .sort((a, b) => a.sort - b.sort) .slice(0, count) .map(({ item }) => item); } function resetResults() { summary.textContent = 'No detections yet.'; tbody.innerHTML = 'Run the detector to see details.'; } function setSelectedImage(dataUrl, readyMessage) { const image = new Image(); image.onload = () => { state.dataUrl = dataUrl; state.image = image; drawBaseImage(); runButton.disabled = false; statusEl.textContent = readyMessage; setBadge('Image ready'); resetResults(); }; image.onerror = () => { statusEl.textContent = 'Could not read that image.'; setBadge('Image error', 'error'); }; image.src = dataUrl; } function blobToDataUrl(blob) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = reject; reader.readAsDataURL(blob); }); } async function loadExample(example, button) { try { statusEl.textContent = `Loading COCO example: ${example.label}…`; setBadge('Loading sample', 'running'); const response = await fetch(example.src); if (!response.ok) throw new Error(`Could not load ${example.src}`); const dataUrl = await blobToDataUrl(await response.blob()); imageInput.value = ''; document.querySelectorAll('.sample-card').forEach((card) => card.classList.remove('selected')); button.classList.add('selected'); setSelectedImage(dataUrl, `COCO example ready: ${example.label}. Click Run AI Detection.`); } catch (error) { console.error(error); statusEl.textContent = 'Could not load that COCO example. Try another one.'; setBadge('Sample error', 'error'); } } function renderModelChoices() { for (const option of MODEL_OPTIONS) { const item = document.createElement('option'); item.value = option.key; item.textContent = option.label; modelSelect.appendChild(item); } modelSelect.value = MODEL_OPTIONS[0].key; modelNote.textContent = MODEL_OPTIONS[0].note; } function renderExampleChoices() { sampleGrid.innerHTML = ''; for (const example of chooseRandom(EXAMPLES, 4)) { const button = document.createElement('button'); button.type = 'button'; button.className = 'sample-card'; button.title = `Use sample: ${example.label}`; const image = document.createElement('img'); image.src = example.src; image.alt = example.label; image.loading = 'lazy'; const label = document.createElement('span'); label.textContent = example.label; const source = document.createElement('small'); const cocoId = example.src.match(/(\d{12})\.jpg$/)?.[1] || ''; source.textContent = cocoId ? `COCO val2017 · ${cocoId}` : 'COCO val2017'; button.append(image, label, source); button.addEventListener('click', () => loadExample(example, button)); sampleGrid.appendChild(button); } } function setBadge(text, mode = '') { runtimeBadge.textContent = text; runtimeBadge.className = `badge ${mode}`.trim(); } function setProgress(data) { let pct = null; if (typeof data?.progress === 'number') { pct = data.progress; } else if (data?.total && typeof data?.loaded === 'number') { pct = (data.loaded / data.total) * 100; } if (pct !== null && Number.isFinite(pct)) { progressWrap.hidden = false; progressBar.style.width = `${Math.max(0, Math.min(100, pct))}%`; } if (data?.status === 'progress' && data?.file) { statusEl.textContent = `Downloading model: ${data.file}`; } } function drawBaseImage() { if (!state.image) return; canvas.width = state.image.naturalWidth; canvas.height = state.image.naturalHeight; ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(state.image, 0, 0, canvas.width, canvas.height); canvasWrap.classList.remove('empty'); } function drawDetections(detections) { drawBaseImage(); if (!state.image) return; const scale = Math.max(1, Math.round(canvas.width / 900)); const fontSize = Math.max(14, Math.round(canvas.width / 55)); ctx.font = `700 ${fontSize}px system-ui, sans-serif`; ctx.textBaseline = 'top'; for (const item of detections) { const box = item.box; const x1 = Math.max(0, Number(box.xmin)); const y1 = Math.max(0, Number(box.ymin)); const x2 = Math.min(canvas.width, Number(box.xmax)); const y2 = Math.min(canvas.height, Number(box.ymax)); const label = `${item.label} ${(Number(item.score) * 100).toFixed(0)}%`; ctx.lineWidth = 3 * scale; ctx.strokeStyle = '#ffffff'; ctx.strokeRect(x1, y1, Math.max(0, x2 - x1), Math.max(0, y2 - y1)); const pad = 5 * scale; const metrics = ctx.measureText(label); const textHeight = fontSize + 2 * pad; const labelY = Math.max(0, y1 - textHeight); ctx.fillStyle = '#111827'; ctx.fillRect(x1, labelY, metrics.width + 2 * pad, textHeight); ctx.fillStyle = '#ffffff'; ctx.fillText(label, x1 + pad, labelY + pad); } } function renderDetections(detections) { tbody.innerHTML = ''; const option = getModelOption(modelSelect.value); if (!detections.length) { const row = document.createElement('tr'); row.className = 'empty-row'; const cell = document.createElement('td'); cell.colSpan = 3; cell.textContent = 'No objects passed the current confidence threshold.'; row.appendChild(cell); tbody.appendChild(row); summary.textContent = `${option.label}: no detections. Try a lower threshold or another image.`; return; } const counts = new Map(); for (const item of detections) { counts.set(item.label, (counts.get(item.label) || 0) + 1); const row = document.createElement('tr'); const box = item.box; const values = [ item.label, `${(Number(item.score) * 100).toFixed(1)}%`, `(${Math.round(box.xmin)}, ${Math.round(box.ymin)}) → (${Math.round(box.xmax)}, ${Math.round(box.ymax)})`, ]; for (const value of values) { const cell = document.createElement('td'); cell.textContent = value; row.appendChild(cell); } tbody.appendChild(row); } const pieces = [...counts.entries()] .sort(([a], [b]) => a.localeCompare(b)) .map(([label, count]) => `${label}: ${count}`); summary.textContent = `${option.label} found — ${pieces.join(', ')}`; } threshold.addEventListener('input', () => { thresholdValue.value = Number(threshold.value).toFixed(2); }); modelSelect.addEventListener('change', () => { const option = getModelOption(modelSelect.value); modelNote.textContent = option.note; setBadge(`Model: ${option.label}`); if (state.image) statusEl.textContent = `Model changed to ${option.label}. Run again to compare results.`; }); imageInput.addEventListener('change', () => { const file = imageInput.files?.[0]; if (!file) return; if (!file.type.startsWith('image/')) { statusEl.textContent = 'Please choose an image file.'; setBadge('Invalid file', 'error'); runButton.disabled = true; return; } document.querySelectorAll('.sample-card').forEach((card) => card.classList.remove('selected')); const reader = new FileReader(); reader.onload = () => setSelectedImage(reader.result, 'Your image is ready. Click Run AI Detection.'); reader.onerror = () => { statusEl.textContent = 'Could not read that image.'; setBadge('Image error', 'error'); }; reader.readAsDataURL(file); }); shuffleButton.addEventListener('click', () => { renderExampleChoices(); statusEl.textContent = state.image ? 'New COCO examples shown. Your current image is still selected.' : 'Choose a COCO example, or upload your own image.'; }); renderModelChoices(); renderExampleChoices(); runButton.addEventListener('click', async () => { if (!state.dataUrl || !state.image) return; const option = getModelOption(modelSelect.value); runButton.disabled = true; setBadge(`Loading ${option.label}`, 'running'); progressWrap.hidden = false; progressBar.style.width = '4%'; const started = performance.now(); try { statusEl.textContent = `Preparing ${option.label}. The first run may download model files.`; const detections = await detectObjects(state.dataUrl, { modelKey: option.key, threshold: Number(threshold.value), progressCallback: setProgress, }); drawDetections(detections); renderDetections(detections); const seconds = ((performance.now() - started) / 1000).toFixed(1); statusEl.textContent = `Done in ${seconds}s with ${option.label}. Change model or threshold to compare.`; setBadge(`${detections.length} detections`, 'ready'); } catch (error) { console.error(error); statusEl.textContent = `${option.label} failed to run. Check your connection or switch back to RT-DETR.`; setBadge('Error', 'error'); } finally { progressWrap.hidden = true; runButton.disabled = false; } });