1. 产品 Demo 效果展示
打开手机或电脑摄像头,站在镜头前做深蹲或俯卧撑,界面会实时显示骨骼关键点连线,并自动统计动作次数。动作不标准时,计数器不会增加——比如深蹲没蹲到90度,俯卧撑肘关节没弯曲到位。
这是我用 MediaPipe Pose + React 20 分钟搭出来的原型:
左侧是实时视频流叠加骨骼图,右侧显示动作名称和计数。核心代码不到 60 行,其中计数逻辑只有 15 行。
2. 技术选型
| 组件 | 选择 | 理由 |
|---|---|---|
| 姿态估计 | MediaPipe Pose | 浏览器端运行,纯JS,轻量,支持68个关键点,精度够用 |
| 前端框架 | React + Vite | 快速开发,HMR热更新,适合原型 |
| 摄像头 | navigator.mediaDevices | 无需后端,纯前端处理 |
| 部署 | Vercel | 免费,支持SPA,一键部署 |
为什么不选更重的模型(OpenPose、HRNet)?因为我们的目标是 能跑、够用。MediaPipe 在普通笔记本上稳定 30fps,移动端也能 15fps 以上,而 OpenPose 需要 GPU,部署成本太高。对 Demo 来说,用户要的是“卡”还是“流畅”?你猜答案。
3. 核心代码实现
3.1 初始化姿态检测
import { Pose } from '@mediapipe/pose';
import { Camera } from '@mediapipe/camera_utils';
const pose = new Pose({
locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/pose/${file}`
});
pose.setOptions({
modelComplexity: 1, // 0轻量 1标准 2高精度
smoothLandmarks: true,
minDetectionConfidence: 0.5,
minTrackingConfidence: 0.5
});
pose.onResults(onPoseResults);
const camera = new Camera(
document.getElementById('video'),
{ onFrame: async () => { await pose.send({ image: video }); } }
);
camera.start();
这段代码是通用的“摄像头→MediaPipe→回调”管道。注意 modelComplexity: 1 是性能和精度的平衡点。如果设备老旧,改为 0 能提帧率但会损失关节稳定性。
3.2 动作计数逻辑(核心 15 行)
以深蹲为例,关键角度是髋关节-膝关节-踝关节的夹角。当角度小于 110° 且当前未计数时,算一次深蹲;然后标记“已蹲”,等角度恢复 >160° 时重置标记,避免重复计数。
let squatCount = 0;
let isSquatting = false;
function countSquat(landmarks) {
// 取左髋、左膝、左踝关键点 (MediaPipe Pose索引: 23,25,27)
const hip = landmarks[23];
const knee = landmarks[25];
const ankle = landmarks[27];
// 计算膝关节角度(向量hip→knee 与 ankle→knee 的夹角)
const v1 = { x: hip.x - knee.x, y: hip.y - knee.y };
const v2 = { x: ankle.x - knee.x, y: ankle.y - knee.y };
const dot = v1.x * v2.x + v1.y * v2.y;
const mag = Math.hypot(v1.x, v1.y) * Math.hypot(v2.x, v2.y);
const angle = Math.acos(dot / mag) * (180 / Math.PI);
if (angle < 110 && !isSquatting) {
squatCount++;
isSquatting = true;
console.log(`深蹲次数: ${squatCount}`);
}
if (angle > 160) {
isSquatting = false;
}
}
这段代码的核心是状态机:isSquatting 防止在蹲下过程中反复触发。阈值 110° 和 160° 是我自己测试的标准深蹲阈值——如果你的用户是 56 岁的 Colman Domingo,可能需要放宽到 120° 和 150°,自己调。
3.3 完整 onPoseResults 回调
function onPoseResults(results) {
if (!results.poseLandmarks) return;
// 在canvas上绘制骨骼
drawCanvas(results);
// 执行动作计数
countSquat(results.poseLandmarks);
// pushUpCounting 同理,用肘关节角度
}
绘制骨骼的 drawCanvas 函数直接用 MediaPipe 提供的 drawConnectors 和 drawLandmarks,不用自己写,省力。
4. 项目结构和配置
文件结构简单到没朋友:
pose-fitness/
├── index.html
├── package.json
├── src/
│ ├── main.jsx
│ ├── App.jsx
│ └── poseModule.js
├── vercel.json
└── vite.config.js
package.json 关键依赖:
{
"dependencies": {
"react": "^18.0",
"react-dom": "^18.0",
"@mediapipe/pose": "^0.5.0",
"@mediapipe/camera_utils": "^0.3.0",
"@mediapipe/drawing_utils": "^0.3.0"
},
"devDependencies": {
"vite": "^5.0"
}
}
vercel.json 只需 { "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] },因为 Vite 构建是 SPA。
5. 上线要注意的坑
5.1 HTTPS 必需
浏览器摄像头 API 要求安全上下文(HTTPS 或 localhost)。部署到 Vercel 默认 HTTPS,但本地开发用 localhost。如果用 127.0.0.1 可能被某些浏览器阻止,统一用 localhost。
5.2 移动端兼容性
MediaPipe Pose 在 iOS Safari 上会遇到 WebGL 上下文丢失的问题。解决方案:在 onResults 回调中加 if (glContextLost) return;,但更稳定的做法是用 @mediapipe/tasks-vision(新一代 API),体积更小。
5.3 动作阈值要可配置
不同用户(年轻人 vs 56岁健身者)的动作幅度差异很大。Colman Domingo 的训练计划强调“长寿优先”(原文:training for longevity),关节活动度可能不如年轻人。因此我建议将阈值做成滑块 UI,让用户自己调。不过 Demo 阶段可以先写死,上线前加上配置。
5.4 隐私提示
用户对摄像头有天然的警惕。在页面显眼位置添加“视频仅在本地处理,不上传服务器”声明。这不仅是合规要求,也是用户信任的基础。用 onPoseResults 回调中直接处理 video 帧,代码里没有任何 fetch 上传,是事实。
5.5 性能优化
modelComplexity: 0在低端机上更友好- 去掉
smoothLandmarks: true可以省一点 CPU,但会抖动 minDetectionConfidence从 0.5 降到 0.7 可减少误报,但可能漏检- 不要每帧都执行
drawCanvas,改为每秒30帧渲染,计算逻辑保持60Hz监听——不过 Mediapipe 本身会控制帧率。
最后,说点个人看法
Colman Domingo 的健身故事其实给了我们一个启发:AI 产品不是越复杂越好。他56岁不再追求“大块头”,转而追求“保持运动能力”。我们的 AI 健身 Demo 也别追求“评测动作100%准确”——能做到80%的可用性,配合用户手动确认,就是一个好工具。下次如果有人让你做个“完美姿态纠正AI”,你可以反问:用户真的需要完美吗?还是先能跑起来?

完整项目代码已上传 GitHub(搜 pose-fitness-demo),欢迎 star 和 PR。