| | <!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> |