|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>RF-DETR WebGPU</title> |
|
|
<link rel="stylesheet" href="style.css" /> |
|
|
</head> |
|
|
<body> |
|
|
|
|
|
<h1>RF-DETR WebGPU</h1> |
|
|
<div class="subtitle"> |
|
|
Real-Time Detection Transformers<br> |
|
|
running 100% locally in your browser. |
|
|
</div> |
|
|
|
|
|
<div class="container"> |
|
|
<div id="status"> |
|
|
<div class="spinner"></div> |
|
|
<div id="status-content"> |
|
|
<div id="status-text">Initializing...</div> |
|
|
<div id="status-sub">Please allow camera access</div> |
|
|
</div> |
|
|
</div> |
|
|
<div id="fps">FPS: 0.0</div> |
|
|
<video id="webcam" autoplay playsinline muted></video> |
|
|
<canvas id="overlay"></canvas> |
|
|
</div> |
|
|
|
|
|
<div class="controls"> |
|
|
<label class="control-label"> |
|
|
<span>Threshold</span> |
|
|
<input type="range" id="threshold" min="0" max="1" step="0.01" value="0.5"> |
|
|
<span id="thresh-val">0.50</span> |
|
|
</label> |
|
|
</div> |
|
|
|
|
|
<footer> |
|
|
Powered by <a href="https://github.com/huggingface/transformers.js" target="_blank">Transformers.js v4</a> |
|
|
</footer> |
|
|
|
|
|
<script type="module"> |
|
|
import { pipeline } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@next'; |
|
|
|
|
|
const video = document.getElementById('webcam'); |
|
|
const overlay = document.getElementById('overlay'); |
|
|
const status = document.getElementById('status'); |
|
|
const statusText = document.getElementById('status-text'); |
|
|
const statusSub = document.getElementById('status-sub'); |
|
|
const fpsElem = document.getElementById('fps'); |
|
|
const slider = document.getElementById('threshold'); |
|
|
const sliderVal = document.getElementById('thresh-val'); |
|
|
|
|
|
let detector; |
|
|
let lastTime = performance.now(); |
|
|
let threshold = 0.5; |
|
|
|
|
|
const inputCanvas = document.createElement('canvas'); |
|
|
const inputCtx = inputCanvas.getContext('2d', { willReadFrequently: true }); |
|
|
|
|
|
const COLORS = ['#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6', '#ec4899']; |
|
|
|
|
|
slider.addEventListener('input', (e) => { |
|
|
threshold = parseFloat(e.target.value); |
|
|
sliderVal.textContent = threshold.toFixed(2); |
|
|
}); |
|
|
|
|
|
|
|
|
function resizeOverlay() { |
|
|
const width = video.clientWidth; |
|
|
const height = video.clientHeight; |
|
|
const dpr = window.devicePixelRatio || 1; |
|
|
|
|
|
overlay.width = width * dpr; |
|
|
overlay.height = height * dpr; |
|
|
|
|
|
const ctx = overlay.getContext('2d'); |
|
|
ctx.scale(dpr, dpr); |
|
|
|
|
|
inputCanvas.width = video.videoWidth; |
|
|
inputCanvas.height = video.videoHeight; |
|
|
} |
|
|
|
|
|
window.addEventListener('resize', resizeOverlay); |
|
|
|
|
|
|
|
|
try { |
|
|
const stream = await navigator.mediaDevices.getUserMedia({ |
|
|
video: { |
|
|
facingMode: 'environment', |
|
|
width: { ideal: 640 }, |
|
|
height: { ideal: 480 } |
|
|
}, |
|
|
audio: false |
|
|
}); |
|
|
|
|
|
video.srcObject = stream; |
|
|
await new Promise(r => video.onloadedmetadata = r); |
|
|
video.play(); |
|
|
resizeOverlay(); |
|
|
|
|
|
} catch (e) { |
|
|
statusText.textContent = "Camera Error"; |
|
|
statusSub.textContent = e.message; |
|
|
document.querySelector('.spinner').style.display = 'none'; |
|
|
throw e; |
|
|
} |
|
|
|
|
|
|
|
|
statusText.textContent = "Loading Model..."; |
|
|
statusSub.textContent = "Downloading RF-DETR Nano (fp32)"; |
|
|
|
|
|
try { |
|
|
detector = await pipeline('object-detection', 'onnx-community/rfdetr_nano-ONNX', { |
|
|
device: 'webgpu', |
|
|
dtype: 'fp32', |
|
|
}); |
|
|
|
|
|
|
|
|
statusText.textContent = "Compiling Shaders..."; |
|
|
statusSub.textContent = "This may take a moment"; |
|
|
|
|
|
inputCtx.drawImage(video, 0, 0, inputCanvas.width, inputCanvas.height); |
|
|
await detector(inputCanvas, { threshold: 0.5, percentage: true }); |
|
|
|
|
|
status.style.opacity = '0'; |
|
|
setTimeout(() => status.style.display = 'none', 300); |
|
|
|
|
|
} catch (e) { |
|
|
statusText.textContent = "Model Error"; |
|
|
statusSub.textContent = e.message; |
|
|
document.querySelector('.spinner').style.display = 'none'; |
|
|
throw e; |
|
|
} |
|
|
|
|
|
|
|
|
async function loop() { |
|
|
const now = performance.now(); |
|
|
const dt = now - lastTime; |
|
|
lastTime = now; |
|
|
|
|
|
if (dt > 0) { |
|
|
fpsElem.textContent = `FPS: ${(1000 / dt).toFixed(1)}`; |
|
|
} |
|
|
|
|
|
inputCtx.drawImage(video, 0, 0, inputCanvas.width, inputCanvas.height); |
|
|
|
|
|
const results = await detector(inputCanvas, { |
|
|
threshold: threshold, |
|
|
percentage: true |
|
|
}); |
|
|
drawResults(results); |
|
|
|
|
|
requestAnimationFrame(loop); |
|
|
} |
|
|
|
|
|
function drawResults(results) { |
|
|
const ctx = overlay.getContext('2d'); |
|
|
const w = video.clientWidth; |
|
|
const h = video.clientHeight; |
|
|
|
|
|
|
|
|
ctx.save(); |
|
|
ctx.setTransform(1, 0, 0, 1, 0, 0); |
|
|
ctx.clearRect(0, 0, overlay.width, overlay.height); |
|
|
ctx.restore(); |
|
|
|
|
|
|
|
|
ctx.font = '600 13px system-ui'; |
|
|
ctx.lineWidth = 2.5; |
|
|
|
|
|
results.forEach((res, i) => { |
|
|
const { box, label, score } = res; |
|
|
const color = COLORS[i % COLORS.length]; |
|
|
|
|
|
const x1 = box.xmin * w; |
|
|
const y1 = box.ymin * h; |
|
|
const width = (box.xmax - box.xmin) * w; |
|
|
const height = (box.ymax - box.ymin) * h; |
|
|
|
|
|
|
|
|
ctx.strokeStyle = color; |
|
|
ctx.beginPath(); |
|
|
ctx.roundRect(x1, y1, width, height, 6); |
|
|
ctx.stroke(); |
|
|
|
|
|
|
|
|
ctx.fillStyle = color; |
|
|
const text = `${label} ${(score*100).toFixed(0)}%`; |
|
|
const textMetrics = ctx.measureText(text); |
|
|
const textWidth = textMetrics.width; |
|
|
const textHeight = 22; |
|
|
|
|
|
ctx.beginPath(); |
|
|
ctx.roundRect(x1, y1 - textHeight - 4, textWidth + 12, textHeight, 4); |
|
|
ctx.fill(); |
|
|
|
|
|
ctx.fillStyle = 'white'; |
|
|
ctx.fillText(text, x1 + 6, y1 - 9); |
|
|
}); |
|
|
} |
|
|
|
|
|
requestAnimationFrame(loop); |
|
|
|
|
|
</script> |
|
|
</body> |
|
|
</html> |