const { FaceLandmarker, HandLandmarker, PoseLandmarker, FilesetResolver, DrawingUtils } = require('@mediapipe/tasks-vision'); const videoBlendShapes = document.getElementById('video-blend-shapes'); const videoSelect = document.querySelector('select#videoSource'); const videoElement = document.getElementById('video'); const canvasElement = document.getElementsByClassName('output_canvas')[0]; const canvasCtx = canvasElement.getContext('2d'); let cameraRunning = false; let cameraVisible = true; let cameraFPS = 30; let cameraWidth = 640; let cameraHeight = 480; let lastVideoTime = -1; const drawingUtils = new DrawingUtils(canvasCtx); getDevices().then(gotDevices); let detections = []; document.body.querySelector('#cameraVisible').addEventListener('click', async () => { cameraVisible = !cameraVisible; if (cameraVisible) { document.body.querySelector('#cameraVisible').innerHTML = 'Hide Camera'; document.body.querySelector('#video').style.display = ''; } else { document.body.querySelector('#cameraVisible').innerHTML = 'Show Camera'; document.body.querySelector('#video').style.display = 'none'; } }); document.body.querySelector('#cameraRunning').addEventListener('click', async () => { cameraRunning = !cameraRunning; if (cameraRunning) { getStream(); document.body.querySelector('#cameraRunning').innerHTML = 'Stop Camera'; } else { if (window.stream) { window.stream.getTracks().forEach(track => { track.stop(); }); } document.body.querySelector('#cameraRunning').innerHTML = 'Start Camera'; } }); document.body.querySelector('#cameraResolution').addEventListener('change', async e => { cameraWidth = e.target.options[e.target.selectedIndex].getAttribute('width'); cameraHeight = e.target.options[e.target.selectedIndex].getAttribute('height'); }); document.body.querySelector('#cameraFPS').addEventListener('change', async () => { cameraFPS = document.body.querySelector('#cameraFPS').value; }); // -------------- Face Detection -------------- let faceDetectionDelegate = 'GPU'; let minFaceDetectionConfidence = 0.5; document.getElementById('minFaceDetectionConfidenceValue').innerHTML = minFaceDetectionConfidence; let minFacePresenceConfidence = 0.5; document.getElementById('minFacePresenceConfidenceValue').innerHTML = minFacePresenceConfidence; let minFaceTrackingConfidence = 0.5; document.getElementById('minFaceTrackingConfidenceValue').innerHTML = minFaceTrackingConfidence; let faceTrackingEnabled = false; let hideFace = false; let faceLandmarker; let resultsFace; document.body.querySelector('#face').addEventListener('click', async () => { faceTrackingEnabled = !faceTrackingEnabled; if (faceTrackingEnabled) { createFaceLandmarker(); document.body.querySelector('#face').innerHTML = 'Disable face detection'; } else { faceLandmarker = null; document.body.querySelector('#face').innerHTML = 'Enable face detection'; } }); document.body.querySelector('#hideFace').addEventListener('click', async () => { hideFace = !hideFace; if (hideFace) { document.body.querySelector('#hideFace').innerHTML = 'Show face detection'; } else { document.body.querySelector('#hideFace').innerHTML = 'Hide face detection'; } }); document.body.querySelector('#faceDetectionDelegate').addEventListener('change', async () => { faceDetectionDelegate = document.getElementById('faceDetectionDelegate').value; if (faceTrackingEnabled) { createFaceLandmarker(); } }); document.body.querySelector('#minFaceDetectionConfidence').addEventListener('change', async () => { minFaceDetectionConfidence = parseInt(document.getElementById('minFaceDetectionConfidence').value); document.getElementById('minFaceDetectionConfidenceValue').innerHTML = minFaceDetectionConfidence; if (faceTrackingEnabled) { createFaceLandmarker(); } }); document.body.querySelector('#minFacePresenceConfidence').addEventListener('change', async () => { minFacePresenceConfidence = parseInt(document.getElementById('minFacePresenceConfidence').value); document.getElementById('minFacePresenceConfidenceValue').innerHTML = minFacePresenceConfidence; if (faceTrackingEnabled) { createFaceLandmarker(); } }); document.body.querySelector('#minFaceTrackingConfidence').addEventListener('change', async () => { minFaceTrackingConfidence = parseInt(document.getElementById('minFaceTrackingConfidence').value); document.getElementById('minFaceTrackingConfidenceValue').innerHTML = minFaceTrackingConfidence; if (faceTrackingEnabled) { createFaceLandmarker(); } }); // -------------- Hand Detection -------------- let handDetectionDelegate = 'GPU'; let minHandDetectionConfidence = 0.5; document.getElementById('minHandDetectionConfidenceValue').innerHTML = minHandDetectionConfidence; let minHandPresenceConfidence = 0.5; document.getElementById('minHandPresenceConfidenceValue').innerHTML = minHandPresenceConfidence; let minHandTrackingConfidence = 0.5; document.getElementById('minHandTrackingConfidenceValue').innerHTML = minHandTrackingConfidence; let handTrackingEnabled = false; let hideHand = false; let handLandmarker; let resultsHands; document.body.querySelector('#hand').addEventListener('click', async () => { handTrackingEnabled = !handTrackingEnabled; if (handTrackingEnabled) { createHandLandmarker(); document.body.querySelector('#hand').innerHTML = 'Disable hand detection'; } else { handLandmarker = null; document.body.querySelector('#hand').innerHTML = 'Enable hand detection'; } }); document.body.querySelector('#hideHand').addEventListener('click', async () => { hideHand = !hideHand; if (hideFace) { document.body.querySelector('#hideHand').innerHTML = 'Show hand detection'; } else { document.body.querySelector('#hideHand').innerHTML = 'Hide hand detection'; } }); document.body.querySelector('#handDetectionDelegate').addEventListener('change', async () => { handDetectionDelegate = document.getElementById('handDetectionDelegate').value; if (handTrackingEnabled) { createHandLandmarker(); } }); document.body.querySelector('#minHandDetectionConfidence').addEventListener('change', async () => { minHandDetectionConfidence = parseInt(document.getElementById('minHandDetectionConfidence').value); document.getElementById('minHandDetectionConfidenceValue').innerHTML = minHandDetectionConfidence; if (handTrackingEnabled) { createHandLandmarker(); } }); document.body.querySelector('#minHandPresenceConfidence').addEventListener('change', async () => { minHandPresenceConfidence = parseInt(document.getElementById('minHandPresenceConfidence').value); document.getElementById('minHandPresenceConfidenceValue').innerHTML = minHandPresenceConfidence; if (handTrackingEnabled) { createHandLandmarker(); } }); document.body.querySelector('#minHandTrackingConfidence').addEventListener('change', async () => { minHandTrackingConfidence = parseInt(document.getElementById('minHandTrackingConfidence').value); document.getElementById('minHandTrackingConfidenceValue').innerHTML = minHandTrackingConfidence; if (handTrackingEnabled) { createHandLandmarker(); } }); // -------------- Pose Detection -------------- let poseDetectionDelegate = 'GPU'; let minPoseDetectionConfidence = 0.5; document.getElementById('minPoseDetectionConfidenceValue').innerHTML = minPoseDetectionConfidence; let minPosePresenceConfidence = 0.5; document.getElementById('minPosePresenceConfidenceValue').innerHTML = minPosePresenceConfidence; let minPoseTrackingConfidence = 0.5; document.getElementById('minPoseTrackingConfidenceValue').innerHTML = minPoseTrackingConfidence; let poseTrackingEnabled = false; let hidepose = false; let poseLandmarker; let resultsPose; document.body.querySelector('#pose').addEventListener('click', async () => { poseTrackingEnabled = !poseTrackingEnabled; if (poseTrackingEnabled) { createPoseLandmarker(); document.body.querySelector('#pose').innerHTML = 'Disable pose detection'; } else { poseLandmarker = null; document.body.querySelector('#pose').innerHTML = 'Enable pose detection'; } }); document.body.querySelector('#hidePose').addEventListener('click', async () => { hidepose = !hidepose; if (hideFace) { document.body.querySelector('#hidePose').innerHTML = 'Show pose detection'; } else { document.body.querySelector('#hidePose').innerHTML = 'Hide pose detection'; } }); document.body.querySelector('#poseDetectionDelegate').addEventListener('change', async () => { poseDetectionDelegate = document.getElementById('faceDetectionDelegate').value; if (poseTrackingEnabled) { createPoseLandmarker(); } }); document.body.querySelector('#minPoseDetectionConfidence').addEventListener('change', async () => { minPoseDetectionConfidence = parseInt(document.getElementById('minPoseDetectionConfidence').value); document.getElementById('minPoseDetectionConfidenceValue').innerHTML = minPoseDetectionConfidence; if (poseTrackingEnabled) { createPoseLandmarker(); } }); document.body.querySelector('#minPosePresenceConfidence').addEventListener('change', async () => { minPosePresenceConfidence = parseInt(document.getElementById('minPosePresenceConfidence').value); document.getElementById('minPosePresenceConfidenceValue').innerHTML = minPosePresenceConfidence; if (poseTrackingEnabled) { createPoseLandmarker(); } }); document.body.querySelector('#minPoseTrackingConfidence').addEventListener('change', async () => { minPoseTrackingConfidence = parseInt(document.getElementById('minPoseTrackingConfidence').value); document.getElementById('minPoseTrackingConfidenceValue').innerHTML = minPoseTrackingConfidence; if (poseTrackingEnabled) { createPoseLandmarker(); } }); // -------------- Camera -------------- function getDevices() { // AFAICT in Safari this only gets default devices until gUM is called :/ return navigator.mediaDevices.enumerateDevices(); } function gotDevices(deviceInfos) { window.deviceInfos = deviceInfos; // make available to console for (const deviceInfo of deviceInfos) { const option = document.createElement('option'); option.value = deviceInfo.deviceId; if (deviceInfo.kind === 'videoinput') { option.text = deviceInfo.label || `Camera ${videoSelect.length + 1}`; videoSelect.appendChild(option); } } } function getStream() { if (window.stream) { window.stream.getTracks().forEach(track => { track.stop(); }); } const videoSource = videoSelect.value; const constraints = { video: { deviceId: videoSource ? { exact: videoSource } : undefined, frameRate: { min: 30, ideal: cameraFPS, max: 60 }, width: cameraWidth, height: cameraHeight } }; return navigator.mediaDevices.getUserMedia(constraints).then(gotStream).catch(handleError); } function gotStream(stream) { window.stream = stream; // make stream available to console videoSelect.selectedIndex = [...videoSelect.options].findIndex(option => option.text === stream.getVideoTracks()[0].label); videoElement.srcObject = stream; videoElement.addEventListener('loadeddata', predictWebcam); } function handleError(error) { console.error('Error: ', error); } // -------------- Tracking -------------- async function createFaceLandmarker() { const filesetResolver = await FilesetResolver.forVisionTasks('https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision/wasm'); faceLandmarker = await FaceLandmarker.createFromOptions(filesetResolver, { baseOptions: { modelAssetPath: 'face_landmarker_model/face_landmarker.task', delegate: faceDetectionDelegate }, minFaceDetectionConfidence: minFaceDetectionConfidence, minFacePresenceConfidence: minFacePresenceConfidence, minTrackingConfidence: minFaceTrackingConfidence, outputFaceBlendshapes: true, runningMode: 'VIDEO', numFaces: 1 }); window.requestAnimationFrame(predictWebcam); } const createHandLandmarker = async () => { const vision = await FilesetResolver.forVisionTasks('https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision/wasm'); handLandmarker = await HandLandmarker.createFromOptions(vision, { baseOptions: { modelAssetPath: 'hand_landmarker_model/hand_landmarker.task', delegate: handDetectionDelegate }, minHandDetectionConfidence: minHandDetectionConfidence, minHandPresenceConfidence: minHandPresenceConfidence, minTrackingConfidence: minHandTrackingConfidence, runningMode: 'VIDEO', numHands: 2 }); window.requestAnimationFrame(predictWebcam); }; const createPoseLandmarker = async () => { const vision = await FilesetResolver.forVisionTasks('https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision/wasm'); poseLandmarker = await PoseLandmarker.createFromOptions(vision, { baseOptions: { modelAssetPath: 'pose_landmarker_model/pose_landmarker_heavy.task', delegate: poseDetectionDelegate }, minPoseDetectionConfidence: minPoseDetectionConfidence, minPosePresenceConfidence: minPosePresenceConfidence, minTrackingConfidence: minPoseTrackingConfidence, runningMode: 'VIDEO', numPoses: 1 }); window.requestAnimationFrame(predictWebcam); }; // -------------- Detection -------------- function mediapipeRunning() { if (faceLandmarker || handLandmarker || poseLandmarker) { return true; } return false; } function predictWebcam() { if (!mediapipeRunning()) { canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height); return; } if (!hideFace || !hidepose || !hideHand) { canvasElement.width = videoElement.videoWidth; canvasElement.height = videoElement.videoHeight; } else { canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height); } // Now let's start detecting the stream. const startTimeMs = performance.now(); if (lastVideoTime !== videoElement.currentTime) { lastVideoTime = videoElement.currentTime; resultsFace = faceLandmarker ? faceLandmarker.detectForVideo(videoElement, startTimeMs) : null; // resultsHands = handLandmarker ? handLandmarker.detectForVideo(videoElement, startTimeMs) : null; // resultsPose = poseLandmarker ? poseLandmarker.detectForVideo(videoElement, startTimeMs) : null; } if (faceTrackingEnabled && resultsFace && resultsFace.faceLandmarks) { // console.log(resultsFace); drawDebugFace(resultsFace); } if (handTrackingEnabled && resultsHands && resultsHands.landmarks) { // drawDebugHands(resultsHands); } if (poseTrackingEnabled && resultsPose && resultsPose.landmarks) { // drawDebugPose(resultsPose); } // drawBlendShapes(videoBlendShapes, resultsFace.faceBlendshapes); // canvasCtx.restore(); // Call this function again to keep predicting when the browser is ready. if (cameraRunning === true) { window.requestAnimationFrame(predictWebcam); } } function drawDebugFace(results) { if (results) { detections = results; } if (hideFace) { return; } for (const landmarks of results.faceLandmarks) { drawingUtils.drawConnectors(landmarks, FaceLandmarker.FACE_LANDMARKS_TESSELATION, { color: '#C0C0C070', lineWidth: 1 }); drawingUtils.drawConnectors(landmarks, FaceLandmarker.FACE_LANDMARKS_RIGHT_EYE, { color: '#FF3030', lineWidth: 1 }); drawingUtils.drawConnectors(landmarks, FaceLandmarker.FACE_LANDMARKS_RIGHT_EYEBROW, { color: '#FF3030', lineWidth: 1 }); drawingUtils.drawConnectors(landmarks, FaceLandmarker.FACE_LANDMARKS_LEFT_EYE, { color: '#30FF30', lineWidth: 1 }); drawingUtils.drawConnectors(landmarks, FaceLandmarker.FACE_LANDMARKS_LEFT_EYEBROW, { color: '#30FF30', lineWidth: 1 }); drawingUtils.drawConnectors(landmarks, FaceLandmarker.FACE_LANDMARKS_FACE_OVAL, { color: '#E0E0E0', lineWidth: 1 }); drawingUtils.drawConnectors(landmarks, FaceLandmarker.FACE_LANDMARKS_LIPS, { color: '#E0E0E0', lineWidth: 1 }); drawingUtils.drawConnectors(landmarks, FaceLandmarker.FACE_LANDMARKS_RIGHT_IRIS, { color: '#FF3030', lineWidth: 1 }); drawingUtils.drawConnectors(landmarks, FaceLandmarker.FACE_LANDMARKS_LEFT_IRIS, { color: '#30FF30', lineWidth: 1 }); } } function drawDebugHands(results) { if (hideHand) { return; } for (const landmarks of results.landmarks) { drawingUtils.drawConnectors(landmarks, HandLandmarker.HAND_CONNECTIONS, { color: '#E0E0E0', lineWidth: 1 }); drawingUtils.drawLandmarks(landmarks, { color: '#E0E0E0', lineWidth: 1 }); } } function drawDebugPose(results) { if (hidePose) { return; } for (const landmark of results.landmarks) { drawingUtils.drawLandmarks(landmark, { radius: data => DrawingUtils.lerp(data.from.z, -0.15, 0.1, 5, 1) }); drawingUtils.drawConnectors(landmark, PoseLandmarker.POSE_CONNECTIONS, { color: '#E0E0E0', lineWidth: 1 }); } } function drawBlendShapes(el, blendShapes) { if (!blendShapes.length) { return; } // console.log(blendShapes[0]); let htmlMaker = ''; blendShapes[0].categories.map(shape => { htmlMaker += `
  • ${shape.displayName || shape.categoryName} ${(+shape.score).toFixed(4)}
  • `; }); el.innerHTML = htmlMaker; }