Skip to content

Instantly share code, notes, and snippets.

@willwade
Created August 31, 2025 22:47
Show Gist options
  • Save willwade/e577345834482012df8d766108ffa0a3 to your computer and use it in GitHub Desktop.
Save willwade/e577345834482012df8d766108ffa0a3 to your computer and use it in GitHub Desktop.
full mediapipe face mesh test in js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MediaPipe FaceLandmarker Test</title>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@latest/vision_bundle.js" crossorigin="anonymous"></script>
<script>
// Wait for MediaPipe to load and expose globals
window.addEventListener('load', () => {
console.log('Available MediaPipe objects:', Object.keys(window).filter(k => k.toLowerCase().includes('mediapipe') || k.toLowerCase().includes('face') || k.toLowerCase().includes('fileset')));
});
</script>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.container {
display: flex;
gap: 20px;
}
.video-section {
flex: 1;
}
.debug-section {
flex: 1;
background: #f5f5f5;
padding: 15px;
border-radius: 8px;
max-height: 600px;
overflow-y: auto;
}
video {
width: 100%;
max-width: 480px;
border: 2px solid #ddd;
border-radius: 8px;
}
.debug-output {
font-family: monospace;
font-size: 12px;
white-space: pre-wrap;
background: white;
padding: 10px;
border-radius: 4px;
margin-top: 10px;
max-height: 400px;
overflow-y: auto;
}
.controls {
margin: 15px 0;
}
button {
padding: 10px 15px;
margin: 5px;
border: none;
border-radius: 4px;
background: #007bff;
color: white;
cursor: pointer;
}
button:hover {
background: #0056b3;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
.status {
padding: 10px;
margin: 10px 0;
border-radius: 4px;
font-weight: bold;
}
.status.success { background: #d4edda; color: #155724; }
.status.error { background: #f8d7da; color: #721c24; }
.status.info { background: #d1ecf1; color: #0c5460; }
</style>
</head>
<body>
<h1>MediaPipe FaceLandmarker API Test</h1>
<div class="container">
<div class="video-section">
<video id="video" autoplay muted playsinline></video>
<div class="controls">
<button id="startBtn">Start Camera & FaceLandmarker</button>
<button id="stopBtn" disabled>Stop</button>
<button id="clearBtn">Clear Debug Output</button>
</div>
<div id="status" class="status info">Ready to start</div>
</div>
<div class="debug-section">
<h3>Debug Output</h3>
<div>This will show exactly what MediaPipe returns:</div>
<div id="debugOutput" class="debug-output">Waiting for data...</div>
</div>
</div>
<script>
let faceLandmarker = null;
let video = null;
let stream = null;
let animationId = null;
let frameCount = 0;
let debugLogged = {
fullResult: false,
blendshapes: false,
matrix: false
};
const statusEl = document.getElementById('status');
const debugEl = document.getElementById('debugOutput');
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
const clearBtn = document.getElementById('clearBtn');
function updateStatus(message, type = 'info') {
statusEl.textContent = message;
statusEl.className = `status ${type}`;
}
function addDebugOutput(message) {
const timestamp = new Date().toLocaleTimeString();
debugEl.textContent += `[${timestamp}] ${message}\n`;
debugEl.scrollTop = debugEl.scrollHeight;
}
async function initializeFaceLandmarker() {
try {
updateStatus('Initializing MediaPipe...', 'info');
// Wait a bit for MediaPipe to fully load
await new Promise(resolve => setTimeout(resolve, 1000));
// Check what's available in the global scope
const availableKeys = Object.keys(window).filter(k =>
k.toLowerCase().includes('mediapipe') ||
k.toLowerCase().includes('face') ||
k.toLowerCase().includes('fileset') ||
k.includes('FilesetResolver') ||
k.includes('FaceLandmarker')
);
addDebugOutput(`Available globals: ${availableKeys.join(', ')}`);
addDebugOutput(`All window keys containing 'F': ${Object.keys(window).filter(k => k.includes('F')).slice(0, 10).join(', ')}`);
// Try to find the correct classes
const { FilesetResolver, FaceLandmarker } = await import('https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@latest/vision_bundle.js');
const vision = await FilesetResolver.forVisionTasks(
"https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@latest/wasm"
);
faceLandmarker = await FaceLandmarker.createFromOptions(vision, {
baseOptions: {
modelAssetPath: "https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task",
delegate: "GPU"
},
runningMode: "VIDEO",
numFaces: 1,
outputFaceBlendshapes: true,
outputFacialTransformationMatrices: true
});
addDebugOutput('✅ FaceLandmarker initialized successfully');
return true;
} catch (error) {
addDebugOutput(`❌ FaceLandmarker initialization failed: ${error.message}`);
addDebugOutput(`Error stack: ${error.stack}`);
updateStatus(`Initialization failed: ${error.message}`, 'error');
// Try fallback approach
try {
addDebugOutput('Trying fallback approach...');
// This might work if the script loaded correctly
if (typeof FilesetResolver !== 'undefined' && typeof FaceLandmarker !== 'undefined') {
const vision = await FilesetResolver.forVisionTasks(
"https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@latest/wasm"
);
faceLandmarker = await FaceLandmarker.createFromOptions(vision, {
baseOptions: {
modelAssetPath: "https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task",
delegate: "GPU"
},
runningMode: "VIDEO",
numFaces: 1,
outputFaceBlendshapes: true,
outputFacialTransformationMatrices: true
});
addDebugOutput('✅ FaceLandmarker initialized with fallback');
return true;
}
} catch (fallbackError) {
addDebugOutput(`❌ Fallback also failed: ${fallbackError.message}`);
}
return false;
}
}
async function startCamera() {
try {
updateStatus('Starting camera...', 'info');
stream = await navigator.mediaDevices.getUserMedia({
video: { width: 640, height: 480 }
});
video = document.getElementById('video');
video.srcObject = stream;
await new Promise((resolve) => {
video.onloadedmetadata = resolve;
});
addDebugOutput('✅ Camera started successfully');
return true;
} catch (error) {
addDebugOutput(`❌ Camera start failed: ${error.message}`);
updateStatus(`Camera failed: ${error.message}`, 'error');
return false;
}
}
function processFrame() {
if (!faceLandmarker || !video) return;
frameCount++;
const timestamp = performance.now();
try {
const result = faceLandmarker.detectForVideo(video, timestamp);
// Log basic result structure once
if (!debugLogged.fullResult) {
addDebugOutput(`MediaPipe Detection - Landmarks: ${result.faceLandmarks?.length || 0}, Blendshapes: ${result.faceBlendshapes?.length || 0}, Matrices: ${result.facialTransformationMatrixes?.length || 0}`);
// Debug: Check all properties of the result object
addDebugOutput(`Result object properties: ${Object.keys(result).join(', ')}`);
// Check for alternative matrix property names
const matrixProps = Object.keys(result).filter(key =>
key.toLowerCase().includes('matrix') ||
key.toLowerCase().includes('transform')
);
if (matrixProps.length > 0) {
addDebugOutput(`Matrix-related properties found: ${matrixProps.join(', ')}`);
}
if (result.facialTransformationMatrixes && result.facialTransformationMatrixes[0]) {
addDebugOutput(`Matrix available - Type: ${typeof result.facialTransformationMatrixes[0]}, Length: ${result.facialTransformationMatrixes[0].length}`);
}
debugLogged.fullResult = true;
}
// Log available blendshapes once
if (!debugLogged.blendshapes && result.faceBlendshapes && result.faceBlendshapes[0]) {
const categories = result.faceBlendshapes[0].categories || [];
const blendshapeNames = categories.map(c => c.categoryName);
addDebugOutput(`Available blendshapes (${blendshapeNames.length}): ${blendshapeNames.join(', ')}`);
debugLogged.blendshapes = true;
}
// Show live values and head pose every 30 frames
if (frameCount % 30 === 0) {
let output = '';
// Blendshape values
if (result.faceBlendshapes && result.faceBlendshapes[0]) {
const categories = result.faceBlendshapes[0].categories || [];
const eyeBlinkLeft = categories.find(c => c.categoryName === 'eyeBlinkLeft')?.score || 0;
const eyeBlinkRight = categories.find(c => c.categoryName === 'eyeBlinkRight')?.score || 0;
const jawOpen = categories.find(c => c.categoryName === 'jawOpen')?.score || 0;
const browInnerUp = categories.find(c => c.categoryName === 'browInnerUp')?.score || 0;
const cheekPuff = categories.find(c => c.categoryName === 'cheekPuff')?.score || 0;
const cheekSquintLeft = categories.find(c => c.categoryName === 'cheekSquintLeft')?.score || 0;
const cheekSquintRight = categories.find(c => c.categoryName === 'cheekSquintRight')?.score || 0;
output += `Blendshapes - Blink L: ${eyeBlinkLeft.toFixed(3)}, Blink R: ${eyeBlinkRight.toFixed(3)}, Jaw: ${jawOpen.toFixed(3)}, Brow: ${browInnerUp.toFixed(3)}`;
output += ` | Cheek - Puff: ${cheekPuff.toFixed(3)}, SquintL: ${cheekSquintLeft.toFixed(3)}, SquintR: ${cheekSquintRight.toFixed(3)}`;
}
// Head pose calculation using landmarks (improved method based on research)
if (result.faceLandmarks && result.faceLandmarks[0]) {
const landmarks = result.faceLandmarks[0];
// MediaPipe 468 face landmarks - key points:
const leftEyeOuter = landmarks[33]; // Left eye outer corner
const rightEyeOuter = landmarks[263]; // Right eye outer corner
const noseTip = landmarks[1]; // Nose tip
const noseBottom = landmarks[2]; // Nose bottom
const forehead = landmarks[10]; // Forehead center
const chin = landmarks[152]; // Chin bottom
const leftFace = landmarks[234]; // Left face edge
const rightFace = landmarks[454]; // Right face edge
// Helper function for angle calculation
const radians = (x1, y1, x2, y2) => Math.atan2(y2 - y1, x2 - x1);
// ROLL: Face lean left/right (eye line angle)
const roll = radians(leftEyeOuter.x, leftEyeOuter.y, rightEyeOuter.x, rightEyeOuter.y) * 180 / Math.PI;
// YAW: Face turn left/right (nose position relative to face center)
const faceCenter = (leftFace.x + rightFace.x) / 2;
const faceWidth = Math.abs(rightFace.x - leftFace.x);
const noseOffset = (noseTip.x - faceCenter) / faceWidth;
const yaw = noseOffset * 90; // Convert to degrees
// PITCH: Face up/down (adjusted baseline)
const faceHeight = Math.abs(forehead.y - chin.y);
const eyeLevel = (leftEyeOuter.y + rightEyeOuter.y) / 2;
const noseToEyeRatio = (noseTip.y - eyeLevel) / faceHeight;
// Adjust baseline - from measurements, neutral appears to be around 0.22
const neutralBaseline = 0.22;
const pitch = (noseToEyeRatio - neutralBaseline) * 200; // Convert to degrees with scaling
output += ` | Head Pose - Roll: ${roll.toFixed(1)}°, Pitch: ${pitch.toFixed(1)}°, Yaw: ${yaw.toFixed(1)}°`;
// Gesture detection with refined thresholds
const rollThreshold = 12;
const pitchThreshold = 15;
const yawThreshold = 0.15; // Using normalized ratio for yaw
let gestures = [];
// Flip directions to match user's perspective (mirror image)
if (roll > rollThreshold) gestures.push('TILT_LEFT');
if (roll < -rollThreshold) gestures.push('TILT_RIGHT');
if (pitch > pitchThreshold) gestures.push('HEAD_DOWN');
if (pitch < -pitchThreshold) gestures.push('HEAD_UP');
if (noseOffset > yawThreshold) gestures.push('HEAD_LEFT');
if (noseOffset < -yawThreshold) gestures.push('HEAD_RIGHT');
if (gestures.length > 0) {
output += ` | GESTURES: ${gestures.join(', ')}`;
}
// Debug key measurements occasionally
if (frameCount % 120 === 0) {
addDebugOutput(`Measurements - noseOffset: ${noseOffset.toFixed(3)}, faceHeight: ${faceHeight.toFixed(3)}, noseToEyeRatio: ${noseToEyeRatio.toFixed(3)}`);
}
} else if (result.facialTransformationMatrixes && result.facialTransformationMatrixes[0]) {
// Fallback to matrix method if available
const matrix = result.facialTransformationMatrixes[0];
// ... matrix calculation code would go here
output += ' | Head Pose - Using matrix data';
} else {
output += ' | Head Pose - No data available';
}
if (output) {
addDebugOutput(output);
}
}
updateStatus(`Processing frames... (${frameCount})`, 'success');
} catch (error) {
addDebugOutput(`❌ Frame processing error: ${error.message}`);
}
animationId = requestAnimationFrame(processFrame);
}
async function start() {
startBtn.disabled = true;
const faceLandmarkerReady = await initializeFaceLandmarker();
if (!faceLandmarkerReady) {
startBtn.disabled = false;
return;
}
const cameraReady = await startCamera();
if (!cameraReady) {
startBtn.disabled = false;
return;
}
updateStatus('Running - try facial expressions!', 'success');
stopBtn.disabled = false;
processFrame();
}
function stop() {
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
if (stream) {
stream.getTracks().forEach(track => track.stop());
stream = null;
}
if (video) {
video.srcObject = null;
}
startBtn.disabled = false;
stopBtn.disabled = true;
updateStatus('Stopped', 'info');
addDebugOutput('🛑 Stopped');
}
function clearDebug() {
debugEl.textContent = 'Debug output cleared...\n';
debugLogged = {
fullResult: false,
blendshapes: false,
matrix: false
};
frameCount = 0;
}
// Event listeners
startBtn.addEventListener('click', start);
stopBtn.addEventListener('click', stop);
clearBtn.addEventListener('click', clearDebug);
// Make objects available in console for debugging
window.faceLandmarkerTest = {
faceLandmarker,
video,
debugLogged,
frameCount
};
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment