test / app.js
miyuki17's picture
Expand COCO classroom example bank to 100 images
330fbdb verified
Raw History Blame Contribute Delete
16.9 kB
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 = '<tr class="empty-row"><td colspan="3">Run the detector to see details.</td></tr>';
}
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;
}
});