import DeviceDetector from "https://cdn.skypack.dev/device-detector-js@2.2.10"; // 用法: testSupport({client?: string, os?: string}[]) // Client 和 os 是正则表达式。 // 参见: https://cdn.jsdelivr.net/npm/device-detector-js@2.2.10/README.md // 了解 client 和 os 的合法值 // 导入必要的库 // 初始化速度和加速度图表 let speedChart, accelerationChart; let previousPoseData = null; let lastTimestamp = 0; testSupport([ { client: 'Chrome' }, ]); function initCharts() { const speedChartCtx = document.getElementById('speedChart').getContext('2d'); const accelerationChartCtx = document.getElementById('accelerationChart').getContext('2d'); speedChart = new Chart(speedChartCtx, { type: 'line', data: { labels: [], datasets: [{ label: 'Movement Speed (px/s)', data: [], borderColor: 'rgba(75, 192, 192, 1)', tension: 0.4, borderWidth: 2, fill: false }] }, options: { responsive: true, maintainAspectRatio: false, animation: { duration: 0 }, scales: { y: { beginAtZero: true, title: { display: true, text: 'Speed' } }, x: { title: { display: true, text: 'Time' } } } } }); accelerationChart = new Chart(accelerationChartCtx, { type: 'line', data: { labels: [], datasets: [{ label: 'Acceleration (px/s²)', data: [], borderColor: 'rgba(255, 99, 132, 1)', tension: 0.4, borderWidth: 2, fill: false }] }, options: { responsive: true, maintainAspectRatio: false, animation: { duration: 0 }, scales: { y: { beginAtZero: true, title: { display: true, text: 'Acceleration' } }, x: { title: { display: true, text: 'Time' } } } } }); } // 计算姿态变化的速度和加速度 function calculateMotionMetrics(currentPose, timestamp) { if (!previousPoseData || !currentPose.poseLandmarks) { previousPoseData = currentPose; lastTimestamp = timestamp; return { speed: 0, acceleration: 0 }; } const deltaTime = (timestamp - lastTimestamp) / 1000; // 转换为秒 if (deltaTime === 0) return { speed: 0, acceleration: 0 }; // 计算关键点的平均位移 let totalDisplacement = 0; let validPoints = 0; currentPose.poseLandmarks.forEach((landmark, index) => { if (previousPoseData.poseLandmarks[index]) { const dx = landmark.x - previousPoseData.poseLandmarks[index].x; const dy = landmark.y - previousPoseData.poseLandmarks[index].y; const displacement = Math.sqrt(dx * dx + dy * dy); totalDisplacement += displacement; validPoints++; } }); const averageDisplacement = validPoints > 0 ? totalDisplacement / validPoints : 0; const currentSpeed = averageDisplacement / deltaTime; // 计算加速度 const previousSpeed = speedChart.data.datasets[0].data[speedChart.data.datasets[0].data.length - 1] || 0; const acceleration = (currentSpeed - previousSpeed) / deltaTime; // 更新先前数据 previousPoseData = currentPose; lastTimestamp = timestamp; return { speed: currentSpeed, acceleration: acceleration }; } // 更新图表数据 function updateCharts(metrics) { const timestamp = new Date().toLocaleTimeString(); const maxDataPoints = 50; // 限制数据点数量 // 更新速度图表 speedChart.data.labels.push(timestamp); speedChart.data.datasets[0].data.push(metrics.speed); if (speedChart.data.labels.length > maxDataPoints) { speedChart.data.labels.shift(); speedChart.data.datasets[0].data.shift(); } speedChart.update('none'); // 更新加速度图表 accelerationChart.data.labels.push(timestamp); accelerationChart.data.datasets[0].data.push(metrics.acceleration); if (accelerationChart.data.labels.length > maxDataPoints) { accelerationChart.data.labels.shift(); accelerationChart.data.datasets[0].data.shift(); } accelerationChart.update('none'); } function testSupport(supportedDevices) { const deviceDetector = new DeviceDetector(); const detectedDevice = deviceDetector.parse(navigator.userAgent); let isSupported = false; for (const device of supportedDevices) { if (device.client !== undefined) { const re = new RegExp(`^${device.client}$`); if (!re.test(detectedDevice.client.name)) { continue; } } if (device.os !== undefined) { const re = new RegExp(`^${device.os}$`); if (!re.test(detectedDevice.os.name)) { continue; } } isSupported = true; break; } if (!isSupported) { alert(`此演示在 ${detectedDevice.client.name}/${detectedDevice.os.name} 上运行时 ` + `目前不能很好地支持,继续使用需自担风险。`); } } const controls = window; const mpHolistic = window; const drawingUtils = window; const config = { locateFile: (file) => { return `https://cdn.jsdelivr.net/npm/@mediapipe/holistic@` + `${mpHolistic.VERSION}/${file}`; } }; // 我们的输入帧将来自这里。 const videoElement = document.getElementsByClassName('input_video')[0]; const canvasElement = document.getElementsByClassName('output_canvas')[0]; const controlsElement = document.getElementsByClassName('control-panel')[0]; const canvasCtx = canvasElement.getContext('2d'); // 我们稍后会将这个添加到控制面板中,但我们会在这里保存它, // 以便每次图形运行时都可以调用 tick()。 const fpsControl = new controls.FPS(); // 优化:在隐藏动画完成后关闭动画旋转器。 const spinner = document.querySelector('.loading'); spinner.ontransitionend = () => { spinner.style.display = 'none'; }; function removeElements(landmarks, elements) { for (const element of elements) { delete landmarks[element]; } } function removeLandmarks(results) { if (results.poseLandmarks) { removeElements(results.poseLandmarks, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 16, 17, 18, 19, 20, 21, 22]); } } function connect(ctx, connectors) { const canvas = ctx.canvas; for (const connector of connectors) { const from = connector[0]; const to = connector[1]; if (from && to) { if (from.visibility && to.visibility && (from.visibility < 0.1 || to.visibility < 0.1)) { continue; } ctx.beginPath(); ctx.moveTo(from.x * canvas.width, from.y * canvas.height); ctx.lineTo(to.x * canvas.width, to.y * canvas.height); ctx.stroke(); } } } let activeEffect = 'mask'; function onResults(results) { // 隐藏旋转器。 document.body.classList.add('loaded'); // 移除我们不想绘制的关键点。 removeLandmarks(results); // 更新帧率。 fpsControl.tick(); // 绘制叠加层。 canvasCtx.save(); canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height); if (results.segmentationMask) { canvasCtx.drawImage(results.segmentationMask, 0, 0, canvasElement.width, canvasElement.height); // 仅覆盖现有像素。 if (activeEffect === 'mask' || activeEffect === 'both') { canvasCtx.globalCompositeOperation = 'source-in'; // 这可以是颜色、纹理或其他... canvasCtx.fillStyle = '#00FF007F'; canvasCtx.fillRect(0, 0, canvasElement.width, canvasElement.height); } else { canvasCtx.globalCompositeOperation = 'source-out'; canvasCtx.fillStyle = '#0000FF7F'; canvasCtx.fillRect(0, 0, canvasElement.width, canvasElement.height); } // 仅覆盖缺失的像素。 canvasCtx.globalCompositeOperation = 'destination-atop'; canvasCtx.drawImage(results.image, 0, 0, canvasElement.width, canvasElement.height); canvasCtx.globalCompositeOperation = 'source-over'; } else { canvasCtx.drawImage(results.image, 0, 0, canvasElement.width, canvasElement.height); } // 连接肘部到手部。首先做这个,这样其他图形将绘制在这些标记之上。 canvasCtx.lineWidth = 5; if (results.poseLandmarks) { if (results.rightHandLandmarks) { canvasCtx.strokeStyle = 'white'; connect(canvasCtx, [[ results.poseLandmarks[mpHolistic.POSE_LANDMARKS.RIGHT_ELBOW], results.rightHandLandmarks[0] ]]); } if (results.leftHandLandmarks) { canvasCtx.strokeStyle = 'white'; connect(canvasCtx, [[ results.poseLandmarks[mpHolistic.POSE_LANDMARKS.LEFT_ELBOW], results.leftHandLandmarks[0] ]]); } } // 计算并更新动作指标 const metrics = calculateMotionMetrics(results, performance.now()); updateCharts(metrics); // 姿势... drawingUtils.drawConnectors(canvasCtx, results.poseLandmarks, mpHolistic.POSE_CONNECTIONS, { color: 'white' }); drawingUtils.drawLandmarks(canvasCtx, Object.values(mpHolistic.POSE_LANDMARKS_LEFT) .map(index => results.poseLandmarks[index]), { visibilityMin: 0.65, color: 'white', fillColor: 'rgb(255,138,0)' }); drawingUtils.drawLandmarks(canvasCtx, Object.values(mpHolistic.POSE_LANDMARKS_RIGHT) .map(index => results.poseLandmarks[index]), { visibilityMin: 0.65, color: 'white', fillColor: 'rgb(0,217,231)' }); // 手... drawingUtils.drawConnectors(canvasCtx, results.rightHandLandmarks, mpHolistic.HAND_CONNECTIONS, { color: 'white' }); drawingUtils.drawLandmarks(canvasCtx, results.rightHandLandmarks, { color: 'white', fillColor: 'rgb(0,217,231)', lineWidth: 2, radius: (data) => { return drawingUtils.lerp(data.from.z, -0.15, .1, 10, 1); } }); drawingUtils.drawConnectors(canvasCtx, results.leftHandLandmarks, mpHolistic.HAND_CONNECTIONS, { color: 'white' }); drawingUtils.drawLandmarks(canvasCtx, results.leftHandLandmarks, { color: 'white', fillColor: 'rgb(255,138,0)', lineWidth: 2, radius: (data) => { return drawingUtils.lerp(data.from.z, -0.15, .1, 10, 1); } }); // 面部... drawingUtils.drawConnectors(canvasCtx, results.faceLandmarks, mpHolistic.FACEMESH_TESSELATION, { color: '#C0C0C070', lineWidth: 1 }); drawingUtils.drawConnectors(canvasCtx, results.faceLandmarks, mpHolistic.FACEMESH_RIGHT_EYE, { color: 'rgb(0,217,231)' }); drawingUtils.drawConnectors(canvasCtx, results.faceLandmarks, mpHolistic.FACEMESH_RIGHT_EYEBROW, { color: 'rgb(0,217,231)' }); drawingUtils.drawConnectors(canvasCtx, results.faceLandmarks, mpHolistic.FACEMESH_LEFT_EYE, { color: 'rgb(255,138,0)' }); drawingUtils.drawConnectors(canvasCtx, results.faceLandmarks, mpHolistic.FACEMESH_LEFT_EYEBROW, { color: 'rgb(255,138,0)' }); drawingUtils.drawConnectors(canvasCtx, results.faceLandmarks, mpHolistic.FACEMESH_FACE_OVAL, { color: '#E0E0E0', lineWidth: 5 }); drawingUtils.drawConnectors(canvasCtx, results.faceLandmarks, mpHolistic.FACEMESH_LIPS, { color: '#E0E0E0', lineWidth: 5 }); canvasCtx.restore(); } // 视频上传处理 function handleVideoUpload(file) { const formData = new FormData(); formData.append('video', file); // 重置图表数据 speedChart.data.labels = []; speedChart.data.datasets[0].data = []; accelerationChart.data.labels = []; accelerationChart.data.datasets[0].data = []; previousPoseData = null; lastTimestamp = 0; // 发送视频到服务器 fetch('/upload_video', { method: 'POST', body: formData }) .then(response => response.json()) .then(data => { if (data.success) { // 更新视频源 videoElement.src = URL.createObjectURL(file); videoElement.play(); } else { console.error('Video upload failed:', data.error); } }) .catch(error => { console.error('Error uploading video:', error); }); } const holistic = new mpHolistic.Holistic(config); holistic.onResults(onResults); // 呈现一个控制面板,用户可以通过它操作解决方案选项。 new controls .ControlPanel(controlsElement, { selfieMode: true, modelComplexity: 1, smoothLandmarks: true, enableSegmentation: false, smoothSegmentation: true, minDetectionConfidence: 0.5, minTrackingConfidence: 0.5, effect: 'background', }) .add([ new controls.StaticText({ title: 'MediaPipe 全身姿态检测' }), fpsControl, new controls.Toggle({ title: '自拍模式', field: 'selfieMode' }), new controls.SourcePicker({ onSourceChanged: () => { // 重置,因为在源更改之间重置时,姿势会给出更好的结果。 holistic.reset(); }, onFrame: async (input, size) => { const aspect = size.height / size.width; let width, height; if (window.innerWidth > window.innerHeight) { height = window.innerHeight; width = height / aspect; } else { width = window.innerWidth; height = width * aspect; } canvasElement.width = width; canvasElement.height = height; await holistic.send({ image: input }); }, }), new controls.Slider({ title: '模型复杂度', field: 'modelComplexity', discrete: ['轻量', '完整', '重度'], }), new controls.Toggle({ title: '平滑关键点', field: 'smoothLandmarks' }), new controls.Toggle({ title: '启用分割', field: 'enableSegmentation' }), new controls.Toggle({ title: '平滑分割', field: 'smoothSegmentation' }), new controls.Slider({ title: '最小检测置信度', field: 'minDetectionConfidence', range: [0, 1], step: 0.01 }), new controls.Slider({ title: '最小跟踪置信度', field: 'minTrackingConfidence', range: [0, 1], step: 0.01 }), new controls.Slider({ title: '效果', field: 'effect', discrete: { 'background': '背景', 'mask': '前景' }, }), ]) .on(x => { const options = x; videoElement.classList.toggle('selfie', options.selfieMode); activeEffect = x['effect']; holistic.setOptions(options); }); // 初始化函数 function initialize() { // 初始化图表 initCharts(); // 设置视频上传处理 const videoUploadInput = document.querySelector('#video-upload'); if (videoUploadInput) { videoUploadInput.addEventListener('change', (e) => { if (e.target.files.length > 0) { handleVideoUpload(e.target.files[0]); } }); } // 初始化姿态检测 const holistic = new mpHolistic.Holistic(config); holistic.onResults(onResults); // ... 保持其他原有的初始化逻辑 ... } // 启动应用 window.addEventListener('load', initialize); // 保持原有的窗口大小调整逻辑 window.addEventListener('resize', () => { const aspect = videoElement.videoHeight / videoElement.videoWidth; let width, height; if (window.innerWidth > window.innerHeight) { height = window.innerHeight; width = height / aspect; } else { width = window.innerWidth; height = width * aspect; } canvasElement.width = width; canvasElement.height = height; // 重新调整图表大小 speedChart.resize(); accelerationChart.resize(); });