Computer-Vision-Lab / pose.html
YOUSEF2434's picture
Rename Pose.html to pose.html
bbd9c9c verified
<!doctype html>
<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>