Spaces:
Running
Running
Download pose.html from YOUSEF2434/Computer-Vision-Lab: direct link, hf CLI and curl.
- Browser
- Download file 13.7 kB
-
https://huggingface.co/spaces/YOUSEF2434/Computer-Vision-Lab/resolve/main/pose.html
- Command line
-
hf download hf://spaces/YOUSEF2434/Computer-Vision-Lab/pose.html
-
curl -L -o pose.html https://huggingface.co/spaces/YOUSEF2434/Computer-Vision-Lab/resolve/main/pose.html
13.7 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width,initial-scale=1" /> | |
| <title>MediaPipe Hands + FaceMesh (Bigger + Better)</title> | |
| <script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils@0.3.1675466862/camera_utils.js" crossorigin="anonymous"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands@0.4.1675469240/hands.js" crossorigin="anonymous"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh@0.4.1633559619/face_mesh.js" crossorigin="anonymous"></script> | |
| <style> | |
| :root { color-scheme: dark; } | |
| html, body { margin: 0; width: 100%; height: 100%; background: #000; overflow: hidden; } | |
| #video { position: absolute; left: 0; top: 0; width: 2px; height: 2px; opacity: 0; pointer-events: none; z-index: -1; } | |
| #canvas { position: fixed; inset: 0; width: 100vw; height: 100vh; display: block; background: #000; } | |
| #hud{ | |
| position:fixed; left:14px; top:14px; z-index:10; display:none; | |
| padding:10px 12px; border-radius:10px; | |
| background:rgba(18,18,18,0.72); border:1px solid rgba(255,255,255,0.10); | |
| backdrop-filter:blur(6px); -webkit-backdrop-filter:blur(6px); | |
| box-shadow:0 10px 24px rgba(0,0,0,0.45); | |
| font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; | |
| line-height:1.15; user-select:none; min-width: 190px; | |
| } | |
| .row{ display:flex; justify-content:space-between; gap:10px; } | |
| .k{ color:rgba(255,255,255,0.65); font-size:11px; letter-spacing:0.12em; text-transform:uppercase; } | |
| .v{ font-size:18px; font-weight:800; } | |
| #start{ | |
| position:fixed; inset:0; margin:auto; z-index:20; | |
| width:min(380px, calc(100vw - 36px)); height:56px; | |
| border:0; border-radius:999px; cursor:pointer; | |
| font-weight:800; letter-spacing:0.08em; text-transform:uppercase; | |
| color:#001114; | |
| background:linear-gradient(135deg, #00ffff 0%, #00d4ff 45%, #00ff9a 100%); | |
| box-shadow:0 0 0 1px rgba(0,255,255,0.18), 0 18px 44px rgba(0,255,255,0.18); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <video id="video" playsinline muted></video> | |
| <canvas id="canvas"></canvas> | |
| <div id="hud"> | |
| <div class="row"><div class="k">Render</div><div id="fpsR" class="v">0</div></div> | |
| <div class="row"><div class="k">Hands</div><div id="fpsH" class="v">0</div></div> | |
| <div class="row"><div class="k">Face</div><div id="fpsF" class="v">0</div></div> | |
| <div class="row"><div class="k">Face</div><div id="faceOn" class="v" style="font-size:14px;">OFF</div></div> | |
| </div> | |
| <button id="start">Start</button> | |
| <script> | |
| const HANDS_BASE = "https://cdn.jsdelivr.net/npm/@mediapipe/hands@0.4.1675469240/"; | |
| const FACE_BASE = "https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh@0.4.1633559619/"; | |
| // Camera input (keep low for speed) | |
| const CAM_W = 320, CAM_H = 240; | |
| // Make face/hands BIGGER (visual-only scaling around their own centers) | |
| // This does NOT change tracking, only how we draw it. | |
| const HAND_SCALE = 1.35; | |
| const FACE_SCALE = 1.25; | |
| // Visual thickness/size (also makes them look bigger/better) | |
| const HAND_LINE_W = 3.5; | |
| const HAND_POINT = 4; | |
| const FACE_POINT = 1.4; | |
| // Scheduler rates | |
| const HANDS_HZ = 24; | |
| const FACE_HZ = 10; | |
| // Model options | |
| const MAX_HANDS = 2; | |
| const HANDS_COMPLEXITY = 0; | |
| const FACE_REFINE = false; // set true for nicer eyes/lips (slower) | |
| const videoEl = document.getElementById('video'); | |
| const canvasEl = document.getElementById('canvas'); | |
| const ctx = canvasEl.getContext('2d', { alpha: false, desynchronized: true }); | |
| function resizeCanvas() { | |
| const dpr = Math.min(window.devicePixelRatio || 1, 1.5); | |
| canvasEl.width = Math.floor(window.innerWidth * dpr); | |
| canvasEl.height = Math.floor(window.innerHeight * dpr); | |
| canvasEl.style.width = window.innerWidth + 'px'; | |
| canvasEl.style.height = window.innerHeight + 'px'; | |
| ctx.setTransform(dpr, 0, 0, dpr, 0, 0); | |
| clearBlack(); | |
| } | |
| function clearBlack() { | |
| ctx.fillStyle = '#000000'; | |
| ctx.fillRect(0, 0, window.innerWidth, window.innerHeight); | |
| } | |
| window.addEventListener('resize', resizeCanvas, { passive: true }); | |
| resizeCanvas(); | |
| // Contain mapping (camera aspect to screen) | |
| function getViewRect() { | |
| const cw = window.innerWidth, ch = window.innerHeight; | |
| const camAR = CAM_W / CAM_H; | |
| const canvasAR = cw / ch; | |
| let vw, vh, vx, vy; | |
| if (canvasAR > camAR) { vh = ch; vw = vh * camAR; vx = (cw - vw) * 0.5; vy = 0; } | |
| else { vw = cw; vh = vw / camAR; vx = 0; vy = (ch - vh) * 0.5; } | |
| return { x: vx, y: vy, w: vw, h: vh }; | |
| } | |
| function mapLM(lm, view) { | |
| return { x: view.x + lm.x * view.w, y: view.y + lm.y * view.h }; | |
| } | |
| // Compute centroid in pixel space (for visual scaling) | |
| function centroidPx(landmarks, view) { | |
| let sx = 0, sy = 0; | |
| const n = landmarks.length; | |
| for (let i = 0; i < n; i++) { | |
| const p = mapLM(landmarks[i], view); | |
| sx += p.x; sy += p.y; | |
| } | |
| return { x: sx / n, y: sy / n }; | |
| } | |
| function drawPointsScaled(landmarks, color, sizePx, view, scale) { | |
| if (!landmarks || !landmarks.length) return; | |
| const c = centroidPx(landmarks, view); | |
| ctx.fillStyle = color; | |
| ctx.beginPath(); | |
| for (let i = 0, n = landmarks.length; i < n; i++) { | |
| const p = mapLM(landmarks[i], view); | |
| const x = c.x + (p.x - c.x) * scale; | |
| const y = c.y + (p.y - c.y) * scale; | |
| ctx.rect(x, y, sizePx, sizePx); | |
| } | |
| ctx.fill(); | |
| } | |
| function drawEdgesScaled(landmarks, edges, color, lineWidth, view, scale) { | |
| if (!landmarks || !landmarks.length) return; | |
| const c = centroidPx(landmarks, view); | |
| ctx.strokeStyle = color; | |
| ctx.lineWidth = lineWidth; | |
| ctx.beginPath(); | |
| for (let i = 0; i < edges.length; i++) { | |
| const a = edges[i][0], b = edges[i][1]; | |
| const p0 = mapLM(landmarks[a], view); | |
| const p1 = mapLM(landmarks[b], view); | |
| const x0 = c.x + (p0.x - c.x) * scale; | |
| const y0 = c.y + (p0.y - c.y) * scale; | |
| const x1 = c.x + (p1.x - c.x) * scale; | |
| const y1 = c.y + (p1.y - c.y) * scale; | |
| ctx.moveTo(x0, y0); | |
| ctx.lineTo(x1, y1); | |
| } | |
| ctx.stroke(); | |
| } | |
| // Hands skeleton (21 landmarks) | |
| const HAND_EDGES = [ | |
| [0,1],[1,2],[2,3],[3,4], | |
| [0,5],[5,6],[6,7],[7,8], | |
| [0,9],[9,10],[10,11],[11,12], | |
| [0,13],[13,14],[14,15],[15,16], | |
| [0,17],[17,18],[18,19],[19,20], | |
| [5,9],[9,13],[13,17] | |
| ]; | |
| // ===== Smoothing + interpolation (normalized coords) ===== | |
| const EMA_HANDS = 0.65; | |
| const EMA_FACE = 0.45; | |
| function cloneLms(lms) { | |
| if (!lms) return null; | |
| const out = new Array(lms.length); | |
| for (let i = 0; i < lms.length; i++) out[i] = { x: lms[i].x, y: lms[i].y, z: lms[i].z }; | |
| return out; | |
| } | |
| function emaUpdate(prev, next, alpha) { | |
| if (!next) return null; | |
| if (!prev || prev.length !== next.length) return cloneLms(next); | |
| const out = new Array(next.length); | |
| for (let i = 0; i < next.length; i++) { | |
| const p = prev[i], n = next[i]; | |
| out[i] = { x: p.x + (n.x - p.x) * alpha, y: p.y + (n.y - p.y) * alpha, z: 0 }; | |
| } | |
| return out; | |
| } | |
| function lerpFrame(a, b, t) { | |
| if (!a) return null; | |
| if (!b || a.length !== b.length) return a; | |
| const out = new Array(a.length); | |
| for (let i = 0; i < a.length; i++) { | |
| out[i] = { x: a[i].x + (b[i].x - a[i].x) * t, y: a[i].y + (b[i].y - a[i].y) * t, z: 0 }; | |
| } | |
| return out; | |
| } | |
| let prevLH=null, currLH=null, prevRH=null, currRH=null; | |
| let prevFace=null, currFace=null; | |
| let prevHandsTs=0, lastHandsTs=0; | |
| let prevFaceTs=0, lastFaceTs=0; | |
| // HUD FPS | |
| const hudEl = document.getElementById('hud'); | |
| const fpsREl = document.getElementById('fpsR'); | |
| const fpsHEl = document.getElementById('fpsH'); | |
| const fpsFEl = document.getElementById('fpsF'); | |
| const faceOnEl = document.getElementById('faceOn'); | |
| let rFrames = 0, rLastT = performance.now(); | |
| function tickRenderFps() { | |
| rFrames++; | |
| const now = performance.now(); | |
| const dt = now - rLastT; | |
| if (dt >= 500) { | |
| fpsREl.textContent = String(Math.round((rFrames * 1000) / dt)); | |
| rLastT = now; rFrames = 0; | |
| } | |
| } | |
| let hFrames = 0, hLastT = performance.now(); | |
| function tickHandsFps() { | |
| hFrames++; | |
| const now = performance.now(); | |
| const dt = now - hLastT; | |
| if (dt >= 900) { | |
| fpsHEl.textContent = String(Math.round((hFrames * 1000) / dt)); | |
| hLastT = now; hFrames = 0; | |
| } | |
| } | |
| let fFrames = 0, fLastT = performance.now(); | |
| function tickFaceFps() { | |
| fFrames++; | |
| const now = performance.now(); | |
| const dt = now - fLastT; | |
| if (dt >= 1100) { | |
| fpsFEl.textContent = String(Math.round((fFrames * 1000) / dt)); | |
| fLastT = now; fFrames = 0; | |
| } | |
| } | |
| function renderLoop() { | |
| clearBlack(); | |
| const view = getViewRect(); | |
| const now = performance.now(); | |
| const hdt = Math.max(1, lastHandsTs - prevHandsTs); | |
| const ht = Math.min(1, Math.max(0, (now - lastHandsTs) / hdt)); | |
| const drawLH = lerpFrame(prevLH, currLH, ht); | |
| const drawRH = lerpFrame(prevRH, currRH, ht); | |
| const fdt = Math.max(1, lastFaceTs - prevFaceTs); | |
| const ft = Math.min(1, Math.max(0, (now - lastFaceTs) / fdt)); | |
| const drawFace = lerpFrame(prevFace, currFace, ft); | |
| if (drawLH) { | |
| drawEdgesScaled(drawLH, HAND_EDGES, '#00ff00', HAND_LINE_W, view, HAND_SCALE); | |
| drawPointsScaled(drawLH, '#00ff00', HAND_POINT, view, HAND_SCALE); | |
| } | |
| if (drawRH) { | |
| drawEdgesScaled(drawRH, HAND_EDGES, '#ffff00', HAND_LINE_W, view, HAND_SCALE); | |
| drawPointsScaled(drawRH, '#ffff00', HAND_POINT, view, HAND_SCALE); | |
| } | |
| if (drawFace) { | |
| drawPointsScaled(drawFace, '#ff00ff', FACE_POINT, view, FACE_SCALE); | |
| } | |
| tickRenderFps(); | |
| requestAnimationFrame(renderLoop); | |
| } | |
| // ===== Models ===== | |
| const hands = new Hands({ locateFile: (f) => HANDS_BASE + f }); | |
| hands.setOptions({ | |
| maxNumHands: MAX_HANDS, | |
| modelComplexity: HANDS_COMPLEXITY, | |
| minDetectionConfidence: 0.5, | |
| minTrackingConfidence: 0.5, | |
| }); | |
| hands.onResults((res) => { | |
| prevHandsTs = lastHandsTs; | |
| lastHandsTs = performance.now(); | |
| tickHandsFps(); | |
| let left = null, right = null; | |
| const lms = res.multiHandLandmarks || []; | |
| const hd = res.multiHandedness || []; | |
| for (let i = 0; i < lms.length; i++) { | |
| const label = hd[i]?.label; | |
| if (label === 'Left') left = lms[i]; | |
| else if (label === 'Right') right = lms[i]; | |
| } | |
| prevLH = currLH; currLH = emaUpdate(currLH, left, EMA_HANDS); | |
| prevRH = currRH; currRH = emaUpdate(currRH, right, EMA_HANDS); | |
| }); | |
| const faceMesh = new FaceMesh({ locateFile: (f) => FACE_BASE + f }); | |
| faceMesh.setOptions({ | |
| maxNumFaces: 1, | |
| refineLandmarks: FACE_REFINE, | |
| minDetectionConfidence: 0.5, | |
| minTrackingConfidence: 0.5, | |
| }); | |
| faceMesh.onResults((res) => { | |
| prevFaceTs = lastFaceTs; | |
| lastFaceTs = performance.now(); | |
| tickFaceFps(); | |
| const face = res.multiFaceLandmarks?.[0] || null; | |
| prevFace = currFace; | |
| currFace = emaUpdate(currFace, face, EMA_FACE); | |
| faceOnEl.textContent = face ? "ON" : "OFF"; | |
| faceOnEl.style.color = face ? "#00ff80" : "#ff3b3b"; | |
| }); | |
| // ===== Deterministic scheduler ===== | |
| let busy = false; | |
| let lastHandsRun = 0, lastFaceRun = 0; | |
| let preferFaceNext = true; | |
| function due(now, lastT, hz) { return (now - lastT) >= (1000 / hz); } | |
| async function runScheduled(now) { | |
| if (busy) return; | |
| const handsDue = due(now, lastHandsRun, HANDS_HZ); | |
| const faceDue = due(now, lastFaceRun, FACE_HZ); | |
| let run = null; | |
| if (handsDue && !faceDue) run = 'hands'; | |
| else if (!handsDue && faceDue) run = 'face'; | |
| else if (handsDue && faceDue) run = (preferFaceNext ? 'face' : 'hands'); | |
| else return; | |
| busy = true; | |
| try { | |
| if (run === 'hands') { | |
| lastHandsRun = now; | |
| preferFaceNext = true; | |
| await hands.send({ image: videoEl }); | |
| } else { | |
| lastFaceRun = now; | |
| preferFaceNext = false; | |
| await faceMesh.send({ image: videoEl }); | |
| } | |
| } finally { | |
| busy = false; | |
| } | |
| } | |
| const camera = new Camera(videoEl, { | |
| width: CAM_W, | |
| height: CAM_H, | |
| onFrame: async () => { await runScheduled(performance.now()); } | |
| }); | |
| document.getElementById('start').addEventListener('click', async () => { | |
| document.getElementById('start').style.display = 'none'; | |
| hudEl.style.display = 'block'; | |
| clearBlack(); | |
| const t = performance.now(); | |
| prevHandsTs = lastHandsTs = t; | |
| prevFaceTs = lastFaceTs = t; | |
| lastHandsRun = t - 1000; | |
| lastFaceRun = t - 1000; | |
| await camera.start(); | |
| requestAnimationFrame(renderLoop); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |