COCO / app.js
Varun95's picture
Update app.js
fe6889f verified
Raw History Blame Contribute Delete
18 kB
/*
YOLO VISION
Browser controller
Responsibilities:
1. User selects ONNX model
2. Read model locally
3. Send model to Web Worker
4. Access webcam
5. Send camera frames to Worker
6. Receive detections
7. Draw boxes + segmentation masks
*/
const INPUT_SIZE = 640;
/* =========================================================
STATE
========================================================= */
let worker = null;
let stream = null;
let running = false;
let frameBusy = false;
let confidenceThreshold = 0.25;
let fps = 0;
let lastFrameTime = 0;
/* =========================================================
DOM
========================================================= */
const modelFile =
document.getElementById(
"modelFile"
);
const modelStatus =
document.getElementById(
"modelStatus"
);
const modelFileName =
document.getElementById(
"modelFileName"
);
const loadingStatus =
document.getElementById(
"loadingStatus"
);
const progressBar =
document.getElementById(
"progressBar"
);
const progressPercent =
document.getElementById(
"progressPercent"
);
const status =
document.getElementById(
"status"
);
const backend =
document.getElementById(
"backend"
);
const camera =
document.getElementById(
"camera"
);
const output =
document.getElementById(
"output"
);
const outputCtx =
output.getContext(
"2d"
);
const cameraPlaceholder =
document.getElementById(
"cameraPlaceholder"
);
const outputPlaceholder =
document.getElementById(
"outputPlaceholder"
);
const startButton =
document.getElementById(
"start"
);
const stopButton =
document.getElementById(
"stop"
);
const confidence =
document.getElementById(
"confidence"
);
const confidenceText =
document.getElementById(
"confidenceText"
);
const objectsText =
document.getElementById(
"objects"
);
/* =========================================================
PROGRESS
========================================================= */
function updateProgress(
percent,
message
) {
percent =
Math.max(
0,
Math.min(
100,
percent
)
);
progressBar.style.width =
`${percent}%`;
progressPercent.textContent =
`${Math.round(percent)}%`;
loadingStatus.textContent =
message;
}
/* =========================================================
FORMAT FILE SIZE
========================================================= */
function formatBytes(bytes) {
if (bytes < 1024) {
return `${bytes} B`;
}
if (
bytes <
1024 * 1024
) {
return (
bytes /
1024
).toFixed(1)
+ " KB";
}
return (
bytes /
1024 /
1024
).toFixed(2)
+ " MB";
}
/* =========================================================
CREATE WORKER
========================================================= */
function createWorker() {
if (worker) {
worker.terminate();
}
worker =
new Worker(
"worker.js",
{
type: "module"
}
);
worker.onmessage =
handleWorkerMessage;
worker.onerror =
function(event) {
console.error(
"Worker error:",
event
);
modelStatus.textContent =
"πŸ”΄ Worker error";
status.textContent =
"πŸ”΄ Worker error";
updateProgress(
0,
"Worker error. Check browser console."
);
};
}
/* =========================================================
MODEL FILE SELECTION
========================================================= */
modelFile.addEventListener(
"change",
async function(event) {
const file =
event.target.files[0];
if (!file) {
return;
}
console.log(
"Selected:",
file.name
);
/*
Basic validation
*/
if (
!file.name
.toLowerCase()
.endsWith(".onnx")
) {
alert(
"Please select an ONNX (.onnx) model."
);
return;
}
modelFileName.textContent =
`${file.name} β€” ${formatBytes(file.size)}`;
modelStatus.textContent =
"🟑 Reading model...";
status.textContent =
"🟑 Loading YOLO model";
updateProgress(
10,
"Reading selected ONNX model..."
);
try {
/*
Read model into memory
*/
const buffer =
await file.arrayBuffer();
updateProgress(
45,
"Model loaded into browser memory..."
);
/*
Create worker
*/
createWorker();
updateProgress(
65,
"Sending model to AI worker..."
);
/*
Transfer ArrayBuffer.
Transfer means the browser does not
make another copy of the model.
*/
worker.postMessage(
{
type: "init",
modelBytes:
buffer,
inputSize:
INPUT_SIZE
},
[
buffer
]
);
}
catch(error) {
console.error(
error
);
modelStatus.textContent =
"πŸ”΄ Model loading failed";
status.textContent =
"πŸ”΄ Model Error";
updateProgress(
0,
"❌ " + error.message
);
}
}
);
/* =========================================================
WORKER MESSAGE HANDLER
========================================================= */
function handleWorkerMessage(event) {
const data =
event.data;
/*
Worker setup progress
*/
if (
data.type ===
"loading"
) {
updateProgress(
data.progress,
data.message
);
return;
}
/*
Model ready
*/
if (
data.type ===
"ready"
) {
updateProgress(
100,
"YOLO model ready!"
);
modelStatus.textContent =
"🟒 YOLO11n-Seg Ready";
status.textContent =
"🟒 YOLO Ready";
backend.textContent =
data.backend;
startButton.disabled =
false;
return;
}
/*
Error
*/
if (
data.type ===
"error"
) {
console.error(
data.message
);
modelStatus.textContent =
"πŸ”΄ Model error";
status.textContent =
"πŸ”΄ AI Error";
updateProgress(
0,
"❌ " + data.message
);
return;
}
/*
Detection result
*/
if (
data.type ===
"result"
) {
frameBusy =
false;
drawResult(
data
);
}
}
/* =========================================================
START CAMERA
========================================================= */
async function startCamera() {
if (!worker) {
alert(
"Please select the YOLO ONNX model first."
);
return;
}
try {
stream =
await navigator
.mediaDevices
.getUserMedia(
{
video: {
facingMode:
"environment"
},
audio: false
}
);
camera.srcObject =
stream;
await camera.play();
running =
true;
startButton.disabled =
true;
stopButton.disabled =
false;
cameraPlaceholder.style.display =
"none";
outputPlaceholder.style.display =
"none";
status.textContent =
"🟒 Camera Running";
processFrame();
}
catch(error) {
console.error(
error
);
status.textContent =
"πŸ”΄ Camera Error";
alert(
"Camera access failed:\n" +
error.message
);
}
}
/* =========================================================
STOP CAMERA
========================================================= */
function stopCamera() {
running =
false;
if (stream) {
stream
.getTracks()
.forEach(
track =>
track.stop()
);
stream =
null;
}
camera.srcObject =
null;
startButton.disabled =
false;
stopButton.disabled =
true;
cameraPlaceholder.style.display =
"block";
status.textContent =
"🟑 Camera Stopped";
}
/* =========================================================
PROCESS CAMERA FRAME
========================================================= */
function processFrame() {
if (!running) {
return;
}
/*
Don't send another frame while
the previous one is being processed.
*/
if (!frameBusy) {
if (
camera.readyState >= 2 &&
camera.videoWidth > 0
) {
sendFrame();
}
}
requestAnimationFrame(
processFrame
);
}
/* =========================================================
SEND FRAME
========================================================= */
function sendFrame() {
frameBusy =
true;
const width =
camera.videoWidth;
const height =
camera.videoHeight;
/*
Temporary canvas
Using the camera's actual resolution.
*/
const canvas =
document.createElement(
"canvas"
);
canvas.width =
width;
canvas.height =
height;
const ctx =
canvas.getContext(
"2d",
{
willReadFrequently:
true
}
);
ctx.drawImage(
camera,
0,
0,
width,
height
);
const imageData =
ctx.getImageData(
0,
0,
width,
height
);
/*
Copy RGBA data into an ArrayBuffer.
*/
const rgba =
new Uint8ClampedArray(
imageData.data
);
worker.postMessage(
{
type:
"frame",
rgba:
rgba.buffer,
width:
width,
height:
height,
confidence:
confidenceThreshold
},
[
rgba.buffer
]
);
}
/* =========================================================
DRAW RESULT
========================================================= */
function drawResult(data) {
const width =
data.width;
const height =
data.height;
if (
!width ||
!height
) {
return;
}
output.width =
width;
output.height =
height;
/*
Draw original frame
*/
if (data.frame) {
const frameBytes =
new Uint8ClampedArray(
data.frame
);
const frameImage =
new ImageData(
frameBytes,
width,
height
);
outputCtx.putImageData(
frameImage,
0,
0
);
}
/*
Segmentation overlays
*/
if (
data.detections &&
data.detections.length
) {
for (
const detection
of data.detections
) {
drawMask(
detection,
width,
height
);
}
}
/*
Draw bounding boxes + labels
*/
if (
data.detections
) {
for (
const detection
of data.detections
) {
drawDetection(
detection
);
}
}
objectsText.textContent =
data.detections
? data.detections.length
: "0";
/*
FPS
*/
const now =
performance.now();
if (lastFrameTime > 0) {
const delta =
now -
lastFrameTime;
const currentFPS =
1000 /
delta;
fps =
fps * 0.8 +
currentFPS * 0.2;
const fpsElement =
document.getElementById(
"fps"
);
if (fpsElement) {
fpsElement.textContent =
fps.toFixed(1);
}
}
lastFrameTime =
now;
}
/* =========================================================
DRAW SEGMENTATION MASK
========================================================= */
function drawMask(
detection,
width,
height
) {
if (
!detection.mask
) {
return;
}
const mask =
new Uint8Array(
detection.mask
);
const maskWidth =
detection.maskWidth;
const maskHeight =
detection.maskHeight;
if (
!maskWidth ||
!maskHeight
) {
return;
}
/*
Draw mask directly onto output.
The worker returns a mask already
mapped to the original camera image.
*/
const image =
outputCtx.getImageData(
0,
0,
width,
height
);
const pixels =
image.data;
const limit =
Math.min(
mask.length,
width * height
);
for (
let i = 0;
i < limit;
i++
) {
if (
mask[i] > 0
) {
const p =
i * 4;
/*
Semi-transparent cyan/green
segmentation overlay.
*/
pixels[p] =
Math.min(
255,
pixels[p] * 0.45 +
40 * 0.55
);
pixels[p + 1] =
Math.min(
255,
pixels[p + 1] * 0.45 +
220 * 0.55
);
pixels[p + 2] =
Math.min(
255,
pixels[p + 2] * 0.45 +
120 * 0.55
);
}
}
outputCtx.putImageData(
image,
0,
0
);
}
/* =========================================================
DRAW BOX + LABEL
========================================================= */
function drawDetection(
detection
) {
const x1 =
detection.x1;
const y1 =
detection.y1;
const x2 =
detection.x2;
const y2 =
detection.y2;
const width =
x2 - x1;
const height =
y2 - y1;
outputCtx.strokeStyle =
"#22c55e";
outputCtx.lineWidth =
3;
outputCtx.strokeRect(
x1,
y1,
width,
height
);
const confidence =
Number(
detection.confidence
).toFixed(2);
const label =
`${detection.name} ${confidence}`;
outputCtx.font =
"bold 15px Arial";
const textWidth =
outputCtx.measureText(
label
).width;
const labelHeight =
24;
outputCtx.fillStyle =
"#22c55e";
outputCtx.fillRect(
x1,
Math.max(
0,
y1 - labelHeight
),
textWidth + 12,
labelHeight
);
outputCtx.fillStyle =
"#020617";
outputCtx.fillText(
label,
x1 + 6,
Math.max(
17,
y1 - 7
)
);
}
/* =========================================================
CONFIDENCE SLIDER
========================================================= */
confidence.addEventListener(
"input",
function() {
confidenceThreshold =
Number(
confidence.value
);
confidenceText.textContent =
confidenceThreshold.toFixed(
2
);
}
);
/* =========================================================
BUTTONS
========================================================= */
startButton.addEventListener(
"click",
startCamera
);
stopButton.addEventListener(
"click",
stopCamera
);
/* =========================================================
INITIAL STATE
========================================================= */
startButton.disabled =
true;
stopButton.disabled =
true;
updateProgress(
0,
"Download the model, then select the .onnx file."
);