webml-community/RF-DETR-Nano-WebGPU
5
1<!DOCTYPE html>2<html lang="en">3<head>4 <meta charset="UTF-8">5 <meta name="viewport" content="width=device-width, initial-scale=1.0">6 <title>RF-DETR WebGPU</title>7 <link rel="stylesheet" href="style.css" />8</head>9<body>10 11 <h1>RF-DETR WebGPU</h1>12 <div class="subtitle">13 Real-Time Detection Transformers<br>14 running 100% locally in your browser.15 </div>16 17 <div class="container">18 <div id="status">19 <div class="spinner"></div>20 <div id="status-content">21 <div id="status-text">Initializing...</div>22 <div id="status-sub">Please allow camera access</div>23 </div>24 </div>25 <div id="fps">FPS: 0.0</div>26 <video id="webcam" autoplay playsinline muted></video>27 <canvas id="overlay"></canvas>28 </div>29 30 <div class="controls">31 <label class="control-label">32 <span>Threshold</span>33 <input type="range" id="threshold" min="0" max="1" step="0.01" value="0.5">34 <span id="thresh-val">0.50</span>35 </label>36 </div>37 38 <footer>39 Powered by <a href="https://github.com/huggingface/transformers.js" target="_blank">Transformers.js v4</a>40 </footer>41 42 <script type="module">43 import { pipeline } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@next';44 45 const video = document.getElementById('webcam');46 const overlay = document.getElementById('overlay');47 const status = document.getElementById('status');48 const statusText = document.getElementById('status-text');49 const statusSub = document.getElementById('status-sub');50 const fpsElem = document.getElementById('fps');51 const slider = document.getElementById('threshold');52 const sliderVal = document.getElementById('thresh-val');53 54 let detector;55 let lastTime = performance.now();56 let threshold = 0.5;57 58 const inputCanvas = document.createElement('canvas');59 const inputCtx = inputCanvas.getContext('2d', { willReadFrequently: true });60 61 const COLORS = ['#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6', '#ec4899'];62 63 slider.addEventListener('input', (e) => {64 threshold = parseFloat(e.target.value);65 sliderVal.textContent = threshold.toFixed(2);66 });67 68 // Handle high DPI displays69 function resizeOverlay() {70 const width = video.clientWidth;71 const height = video.clientHeight;72 const dpr = window.devicePixelRatio || 1;73 74 overlay.width = width * dpr;75 overlay.height = height * dpr;76 77 const ctx = overlay.getContext('2d');78 ctx.scale(dpr, dpr);79 80 inputCanvas.width = video.videoWidth;81 inputCanvas.height = video.videoHeight;82 }83 84 window.addEventListener('resize', resizeOverlay);85 86 // 1. Start Camera87 try {88 const stream = await navigator.mediaDevices.getUserMedia({89 video: {90 facingMode: 'environment',91 width: { ideal: 640 },92 height: { ideal: 480 }93 },94 audio: false95 });96 97 video.srcObject = stream;98 await new Promise(r => video.onloadedmetadata = r);99 video.play();100 resizeOverlay();101 102 } catch (e) {103 statusText.textContent = "Camera Error";104 statusSub.textContent = e.message;105 document.querySelector('.spinner').style.display = 'none';106 throw e;107 }108 109 // 2. Load Model110 statusText.textContent = "Loading Model...";111 statusSub.textContent = "Downloading RF-DETR Nano (fp32)";112 113 try {114 detector = await pipeline('object-detection', 'onnx-community/rfdetr_nano-ONNX', {115 device: 'webgpu',116 dtype: 'fp32',117 });118 119 // 3. Warmup120 statusText.textContent = "Compiling Shaders...";121 statusSub.textContent = "This may take a moment";122 123 inputCtx.drawImage(video, 0, 0, inputCanvas.width, inputCanvas.height);124 await detector(inputCanvas, { threshold: 0.5, percentage: true });125 126 status.style.opacity = '0';127 setTimeout(() => status.style.display = 'none', 300);128 129 } catch (e) {130 statusText.textContent = "Model Error";131 statusSub.textContent = e.message;132 document.querySelector('.spinner').style.display = 'none';133 throw e;134 }135 136 // 4. Render Loop137 async function loop() {138 const now = performance.now();139 const dt = now - lastTime;140 lastTime = now;141 142 if (dt > 0) {143 fpsElem.textContent = `FPS: ${(1000 / dt).toFixed(1)}`;144 }145 146 inputCtx.drawImage(video, 0, 0, inputCanvas.width, inputCanvas.height);147 148 const results = await detector(inputCanvas, {149 threshold: threshold,150 percentage: true151 });152 drawResults(results);153 154 requestAnimationFrame(loop);155 }156 157 function drawResults(results) {158 const ctx = overlay.getContext('2d');159 const w = video.clientWidth;160 const h = video.clientHeight;161 162 // Clear canvas163 ctx.save();164 ctx.setTransform(1, 0, 0, 1, 0, 0);165 ctx.clearRect(0, 0, overlay.width, overlay.height);166 ctx.restore();167 168 // Set styles common to all results169 ctx.font = '600 13px system-ui';170 ctx.lineWidth = 2.5;171 172 results.forEach((res, i) => {173 const { box, label, score } = res;174 const color = COLORS[i % COLORS.length];175 176 const x1 = box.xmin * w;177 const y1 = box.ymin * h;178 const width = (box.xmax - box.xmin) * w;179 const height = (box.ymax - box.ymin) * h;180 181 // Box182 ctx.strokeStyle = color;183 ctx.beginPath();184 ctx.roundRect(x1, y1, width, height, 6);185 ctx.stroke();186 187 // Label188 ctx.fillStyle = color;189 const text = `${label} ${(score*100).toFixed(0)}%`;190 const textMetrics = ctx.measureText(text);191 const textWidth = textMetrics.width;192 const textHeight = 22;193 194 ctx.beginPath();195 ctx.roundRect(x1, y1 - textHeight - 4, textWidth + 12, textHeight, 4);196 ctx.fill();197 198 ctx.fillStyle = 'white';199 ctx.fillText(text, x1 + 6, y1 - 9);200 });201 }202 203 requestAnimationFrame(loop);204 205 </script>206</body>207</html>