const video = document.getElementById('webcam'); const canvas = document.getElementById('output'); const ctx = canvas.getContext('2d'); let detector, rafId; async function setupCamera() { if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { try { const stream = await navigator.mediaDevices.getUserMedia({ video: true }); video.srcObject = stream; return new Promise((resolve) => { video.onloadedmetadata = () => { video.play(); // 确保视频播放才能获取到正确的宽度和高度 video.onplaying = () => { // 视频真正开始播放时 canvas.width = video.videoWidth; canvas.height = video.videoHeight; resolve(); }; }; }); } catch (error) { console.error('Error accessing webcam:', error); alert('Cannot access webcam. Please check permissions.'); } } else { alert('Your browser does not support webcam access.'); } } async function loadModel() { const model = handPoseDetection.SupportedModels.MediaPipeHands; const detectorConfig = { runtime: 'mediapipe', // 或者 'tfjs' solutionPath: 'https://cdn.jsdelivr.net/npm/@mediapipe/hands' // MediaPipe solution files path }; detector = await handPoseDetection.createDetector(model, detectorConfig); console.log('Hand Pose Detector loaded.'); } const handSkeleton = { 'thumb': [0, 1, 2, 3, 4], 'indexFinger': [0, 5, 6, 7, 8], 'middleFinger': [0, 9, 10, 11, 12], 'ringFinger': [0, 13, 14, 15, 16], 'pinky': [0, 17, 18, 19, 20], // 腕部和手掌基部的连接,确保0号点连接到所有手指的起始关节 'palmBase': [0, 1, 5, 9, 13, 17, 0] // 形成手掌的近似轮廓 }; async function detectHands() { if (detector && video.readyState === 4) { const hands = await detector.estimateHands(video, { flipHorizontal: false }); ctx.clearRect(0, 0, canvas.width, canvas.height); // 清空画布 if (hands.length > 0) { for (const hand of hands) { // 绘制手部骨架连接线 // MediaPipe Hands 模型通常有21个关键点,这些关键点有固定的索引 // 这些连接关系是标准的,可以通过硬编码定义或查找库的定义 const keypoints = hand.keypoints; // 简化访问 // 直接根据索引绘制连接线 const connectionsToDraw = [ // Thumb (拇指) [0, 1], [1, 2], [2, 3], [3, 4], // Index finger (食指) [0, 5], [5, 6], [6, 7], [7, 8], // Middle finger (中指) [0, 9], [9, 10], [10, 11], [11, 12], // Ring finger (无名指) [0, 13], [13, 14], [14, 15], [15, 16], // Pinky finger (小指) [0, 17], [17, 18], [18, 19], [19, 20], // Palm base connections (手掌基部连接) [0, 5], [5, 9], [9, 13], [13, 17], [17, 0] // Connect wrist to finger bases and form a loop ]; ctx.strokeStyle = '#00FFFF'; // 青色 ctx.lineWidth = 2; // 线宽 for (const connection of connectionsToDraw) { const start = keypoints[connection[0]]; const end = keypoints[connection[1]]; if (start && end) { // 确保关键点存在 ctx.beginPath(); ctx.moveTo(start.x, start.y); ctx.lineTo(end.x, end.y); ctx.stroke(); } } // 绘制关键点 ctx.fillStyle = '#FF0000'; // 红色 for (const keypoint of keypoints) { // keypoint.x, keypoint.y 是像素坐标 // keypoint.z 是深度信息 (相对坐标),通常不用于2D绘制 ctx.beginPath(); ctx.arc(keypoint.x, keypoint.y, 4, 0, 2 * Math.PI); // 绘制半径为4的圆 ctx.fill(); } // 绘制手部关键点标记(如果仍想使用util函数绘制) // handPoseDetection.util.drawLandmarks(ctx, hand.keypoints, {color: '#FF0000', radius: 4}); // 由于我们已经手动绘制了,这行可以注释掉或移除,避免重复绘制。 } } } rafId = requestAnimationFrame(detectHands); } async function app() { await setupCamera(); await loadModel(); detectHands(); } app();