RF-DETR-WebGPU / index.html
Xenova's picture
Xenova HF Staff
Update index.html
1f97e39 verified
<!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);
});
// Handle high DPI displays
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);
// 1. Start Camera
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;
}
// 2. Load Model
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',
});
// 3. Warmup
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;
}
// 4. Render Loop
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;
// Clear canvas
ctx.save();
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, overlay.width, overlay.height);
ctx.restore();
// Set styles common to all results
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;
// Box
ctx.strokeStyle = color;
ctx.beginPath();
ctx.roundRect(x1, y1, width, height, 6);
ctx.stroke();
// Label
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>