如果你正在开发健身、舞蹈教学、体感互动类产品,或者只是想在毕业设计里做一个有趣的 AI Demo,大概率会遇到这样一个需求:用户点一个“一键跟跳”按钮,程序就开始播放标准动作视频,同时打开摄像头捕捉用户动作,实时判断用户跟得准不准,最后给出一个分数。
这个需求听起来不复杂,但真正落地时涉及视频播放、姿态检测、动作角度计算、实时反馈等多个环节。网上资料大多只讲 MediaPipe 怎么检测关键点,很少有人把“检测 → 评分 → 交互 → 完整演示”串成一套可以跑的代码。本文就围绕“一键跟跳”这个功能,从零搭建一个基于 Python + OpenCV + MediaPipe 的实时跟跳评分工具,包含完整代码、运行效果、常见问题排查和工程优化建议。
文章适合以下读者:
- 有 Python 基础,但没做过姿态估计项目的开发者;
- 想做健身类 App 原型的初级/中级程序员;
- 想拿姿态识别做课程设计或毕设的学生;
- 对 MediaPipe 感兴趣,想快速体验完整流程的同学。
学完本文后,你将掌握:MediaPipe POSE 关键点提取、关节角度计算原理、动作相似度评分设计,以及一套可直接运行的一键跟跳示例代码。
1. 什么是“一键跟跳”:需求拆解与技术选型
1.1 功能需求拆解
把“一键跟跳”拆开来看,其实由几个独立模块组成:
| 模块 | 作用 |
|---|---|
| 视频播放模块 | 播放标准动作示范视频,用户可以一边看一边跟着跳 |
| 摄像头采集模块 | 实时读取用户画面 |
| 姿态检测模块 | 从画面中识别人的骨骼关键点 |
| 动作对比模块 | 将用户动作与示范动作进行相似度比对 |
| 反馈模块 | 实时显示评分,并给出整体分数 |
其中视频播放和摄像头采集属于 OpenCV 的基础能力,姿态检测和动作对比才是核心技术难点。本文会把重心放在姿态检测和动作对比上,同时保证整个流程完整可运行。
1.2 技术选型:为什么用 MediaPipe + OpenCV + NumPy
实现人体姿态检测,可以自己训练深度学习模型,也可以使用现成的开源方案。作为教程,推荐使用 MediaPipe。
MediaPipe 是 Google 开源的跨平台多媒体处理框架,其中的 Pose 模块可以在普通 CPU 上实时检测人体 33 个关键点,不需要 GPU,也不用手写网络结构,非常适合快速验证和教学使用。OpenCV 负责视频读取、摄像头采集和画面绘制,NumPy 或标准数学库负责角度计算与评分。
这里不选择自训练模型的理由是:训练姿态估计模型需要大量标注数据、GPU 资源和标注工具,周期长且门槛高。对于“一键跟跳”这种对精度要求不是极高、但对实时性要求很高的场景,MediaPipe 的 Pose 方案是性价比最高的选择。
1.3 核心产物:人体 33 个关键点
MediaPipe Pose 会输出人体 33 个关键点,包括鼻子、左右肩膀、左右手肘、左右手腕、左右髋部、左右膝盖、左右脚踝等。每一个关键点都有 x、y、z、visibility(可见度)四个值:
- x、y 是归一化坐标,取值在 0~1 之间;
- z 表示关键点相对臀部中心的深度,数值越大离摄像头越远;
- visibility 表示该点被检测到的置信度,取值 0~1。
在跟跳场景中,最常用的是上半身和下半身的 8 个角度:左右手肘、左右肩膀、左右髋部、左右膝盖。这些角度能较好地描述一个动作的姿态,而且不受人的身高、胖瘦、离镜头远近影响。
33 个关键点中,常用索引如下: 0 = 鼻子 11 = 左肩 12 = 右肩 13 = 左手肘 14 = 右手肘 15 = 左手腕 16 = 右手腕 23 = 左髋 24 = 右髋 25 = 左膝 26 = 右膝 27 = 左踝 28 = 右踝2. 环境准备与工程结构
2.1 环境要求
本文示例以常见 Python 环境为例,重点演示实现思路,具体版本需要根据你的实际情况调整。
操作系统方面,Windows、macOS、Linux 均可以运行。需要提前装好 Python 3.8 以上版本,并确保 pip 命令可用。
建议使用虚拟环境安装依赖:
python -m venv jump_env source jump_env/bin/activate # Windows 下使用 jump_env\Scripts\activate安装核心依赖:
pip install opencv-python mediapipe numpy安装完成后,可以验证一下版本:
python -c "import cv2; print(cv2.__version__)" python -c "import mediapipe as mp; print(mp.__version__)" python -c "import numpy as np; print(np.__version__)"如果安装较慢,可以换用国内镜像源,例如:
pip install opencv-python mediapipe numpy -i https://pypi.tuna.tsinghua.edu.cn/simple2.2 项目结构
本文代码使用以下工程结构:
jump-coach/ ├── assets/ │ └── demo.mp4 # 跟跳示范视频,自行准备 ├── config.py # 配置文件 ├── pose_utils.py # 角度计算工具 ├── scorer.py # 评分模块 └── main.py # 一键跟跳主程序其中assets/demo.mp4需要你自行准备一段动作示范视频。可以用自己录制的健身操视频,也可以在开源素材网站寻找。注意使用有版权授权的视频,避免商用风险。
2.3 视频准备建议
示范视频的质量直接影响跟跳效果。建议选择:
- 人物全身可见、正对镜头;
- 动作幅度清晰、连贯;
- 画面光线均匀;
- 视频分辨率不需要太高,720p 足够。
如果你暂时没有合适的视频,可以直接用手机横屏录制一段 10 秒左右的“原地抬臂 + 下蹲”动作作为测试素材。
3. 核心原理拆解:关键点、角度与评分
在编写完整代码之前,先理解三个核心原理:如何计算关节角度、为什么用角度而不是坐标、如何设计评分逻辑。
3.1 关节角度计算:三点定角
要计算一个关节的角度,比如左手肘的角度,只需要找到三个关键点:左肩、左手肘、左手腕。其中手肘是顶点,肩和腕是两条边的端点。
计算原理使用反三角函数atan2,公式为:
angle = atan2(c.y - b.y, c.x - b.x) - atan2(a.y - b.y, a.x - b.x)其中 b 是顶点,a、b、c 分别是肩、肘、腕。atan2能自动处理象限问题,比直接用余弦定理更稳定。
下面给出一个最简示例:
import math def calculate_angle(a, b, c): """ 计算三个点构成的角度,单位是度。 b 是角度的顶点,例如手肘。 """ radians = math.atan2(c.y - b.y, c.x - b.x) - math.atan2(a.y - b.y, a.x - b.x) angle = math.degrees(radians) if angle < 0: angle = abs(angle) if angle > 180.0: angle = 360.0 - angle return angle这里注意:MediaPipe 的关键点对象包含 x、y、z 属性,但角度计算只用了 x、y,也就是忽略了深度方向。这在正面跟跳场景中是合理的近似,因为用户和示范动作基本都是面向镜头。如果需要更精细的 3D 角度,可以把 z 轴也引入计算,但代码复杂度会增加。
3.2 为什么用角度而不是坐标
MediaPipe 输出的 x、y 坐标是归一化坐标,但不同人的臂长、身高、摄像头距离都会让坐标值产生差异。两个人摆出同样的动作,如果身高不同,肩、肘、腕的坐标位置差距会很大,直接比较坐标会导致误判。
角度则不同。只要姿态相似,即使身高不同,对应关节角度也基本一致。所以,跟跳评分选用角度特征是最稳妥的方案。
本文使用 8 个角度构成一个动作特征向量:
特征向量 = [左手肘, 右手肘, 左肩, 右肩, 左髋, 右髋, 左膝, 右膝]使用这 8 个角度,基本可以描述一套上下肢动作。
3.3 评分逻辑设计:加权误差评分
实时跟跳评分的核心是比较用户当前角度的特征向量和示范视频帧的角度特征向量。
最简单的评分公式是加权绝对误差:
误差率 = |用户角度 - 示范角度| / 180 总分 = max(0, 100 - 加权平均误差率 * 100)这种评分方式直观、计算量小,适合实时运行。如果需要多种特征,也可以使用余弦相似度,但角度特征向量较短,余弦相似度区分度不如直接误差评分直观,因此本文采用加权误差评分。
每种角度的权重可以调整。例如手肘角度可以权重低一些,肩髋角度权重高一些,因为躯干姿态对“动作是否标准”影响更大。后面实战部分会给出完整实现。
4. 完整实战:实现一键跟跳工具
接下来进入核心环节,实现一个可以运行的“一键跟跳”工具。
4.1 配置文件 config.py
新建config.py,把摄像头编号、视频路径、画面尺寸等参数集中管理:
# config.py CONFIG = { "camera_id": 0, # 摄像头编号,0 表示默认摄像头 "video_path": "assets/demo.mp4", # 跟跳示范视频路径 "frame_width": 720, # 显示画面缩放宽度 "model_complexity": 1, # MediaPipe 模型复杂度,0/1/2,越高级越慢 "min_detection_confidence": 0.5, # 人体检测置信度阈值 "min_tracking_confidence": 0.5, # 关键点跟踪置信度阈值 }把参数集中到配置文件里,以后调整代码时不需要修改业务逻辑,直接改这些参数即可。
4.2 姿态工具函数 pose_utils.py
新建pose_utils.py,定义关键点索引、角度定义和角度提取函数:
# pose_utils.py import math # 本文用到的关键点索引 KEYPOINT_INDEX = { "left_shoulder": 11, "right_shoulder": 12, "left_elbow": 13, "right_elbow": 14, "left_wrist": 15, "right_wrist": 16, "left_hip": 23, "right_hip": 24, "left_knee": 25, "right_knee": 26, "left_ankle": 27, "right_ankle": 28, } # 角度定义:名称, 顶点索引, 左端点索引, 右端点索引 ANGLE_DEFS = [ ("left_elbow", 13, 11, 15), ("right_elbow", 14, 12, 16), ("left_shoulder", 11, 13, 23), ("right_shoulder", 12, 14, 24), ("left_hip", 23, 11, 25), ("right_hip", 24, 12, 26), ("left_knee", 25, 23, 27), ("right_knee", 26, 24, 28), ] def calculate_angle(a, b, c): """ 计算三个关键点构成的角度,b 为顶点。 返回值为角度制,范围 0~180。 """ radians = math.atan2(c.y - b.y, c.x - b.x) - math.atan2(a.y - b.y, a.x - b.x) angle = abs(math.degrees(radians)) if angle > 180.0: angle = 360.0 - angle return angle def get_angle_dict(landmarks): """ 从 MediaPipe 输出的关键点列表中提取角度字典。 landmarks 是 NormalizedLandmarkList.landmark 列表。 如果关键点为空,返回 None。 """ if landmarks is None: return None angles = {} for name, vertex, left, right in ANGLE_DEFS: a = landmarks[left] b = landmarks[vertex] c = landmarks[right] angles[name] = calculate_angle(a, b, c) return angles这里的get_angle_dict会返回一个字典,例如:
{ "left_elbow": 168.2, "right_elbow": 120.5, "left_shoulder": 45.1, ... }这个字典就是接下来评分模块的输入。
4.3 评分模块 scorer.py
新建scorer.py,定义各角度权重和评分函数:
# scorer.py # 每种角度的权重,权重越大越重要,可以根据项目实际情况调整 WEIGHTS = { "left_elbow": 1.0, "right_elbow": 1.0, "left_shoulder": 1.2, "right_shoulder": 1.2, "left_hip": 1.0, "right_hip": 1.0, "left_knee": 1.0, "right_knee": 1.0, } def compute_score(user_angles, ref_angles): """ 计算用户动作和示范动作的相似度评分,满分 100。 采用加权误差评分,误差越大,得分越低。 """ if user_angles is None or ref_angles is None: return 0.0 total_error = 0.0 total_weight = 0.0 for name, weight in WEIGHTS.items(): if name not in user_angles or name not in ref_angles: continue diff = abs(user_angles[name] - ref_angles[name]) error_rate = diff / 180.0 total_error += error_rate * weight total_weight += weight if total_weight == 0: return 0.0 score = max(0.0, 100.0 - (total_error / total_weight) * 100.0) return round(score, 1)核心思路是把 8 个角度的误差率做加权平均,再换算成百分制得分。误差率为 0 时得 100 分,误差率为 100% 时得 0 分。
4.4 主程序 main.py
新建main.py,实现“待机 → 跟跳 → 结束”的状态机。
# main.py import cv2 import mediapipe as mp from config import CONFIG from pose_utils import get_angle_dict from scorer import compute_score mp_pose = mp.solutions.pose mp_drawing = mp.solutions.drawing_utils mp_drawing_styles = mp.solutions.drawing_styles class JumpCoach: """一键跟跳主程序""" def __init__(self): # 打开摄像头 self.cam = cv2.VideoCapture(CONFIG["camera_id"]) if not self.cam.isOpened(): raise RuntimeError("摄像头未打开,请检查摄像头编号和系统权限") # 打开示范视频 self.demo = cv2.VideoCapture(CONFIG["video_path"]) if not self.demo.isOpened(): raise RuntimeError("示范视频未找到,请检查 video_path 路径") # 初始化 MediaPipe Pose self.pose = mp_pose.Pose( static_image_mode=False, model_complexity=CONFIG["model_complexity"], min_detection_confidence=CONFIG["min_detection_confidence"], min_tracking_confidence=CONFIG["min_tracking_confidence"], ) # 状态:WAIT 待机 / JUMPING 跟跳中 / FINISH 本局结束 self.state = "WAIT" self.scores = [] self.frame_count = 0 def _resize_frame(self, frame): """按配置宽度缩放画面,降低处理耗时""" h, w = frame.shape[:2] new_w = CONFIG["frame_width"] new_h = int(h * new_w / w) return cv2.resize(frame, (new_w, new_h), interpolation=cv2.INTER_LINEAR) def _draw_pose(self, frame, landmarks): """绘制人体骨架和关键点""" if landmarks is None: return frame mp_drawing.draw_landmarks( frame, landmarks, mp_pose.POSE_CONNECTIONS, landmark_drawing_spec=mp_drawing_styles.get_default_pose_landmarks_style(), ) return frame def _draw_score(self, frame, score): """在画面顶部绘制实时评分条和数值""" overlay = frame.copy() cv2.rectangle(overlay, (20, 20), (460, 60), (0, 0, 0), -1) frame = cv2.addWeighted(overlay, 0.6, frame, 0.4, 0) bar_width = int(score * 4) if score >= 80: color = (0, 255, 0) elif score >= 60: color = (0, 255, 255) else: color = (0, 0, 255) cv2.rectangle(frame, (30, 30), (30 + bar_width, 50), color, -1) cv2.putText( frame, f"Score: {score}", (30, 80), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 255, 255), 2, ) return frame def run(self): print("按 空格 开始/暂停跟跳,按 Q 退出") while True: ret_cam, cam_frame = self.cam.read() if not ret_cam: print("读取摄像头帧失败") break cam_frame = self._resize_frame(cam_frame) if self.state == "WAIT": # 待机状态:提示按下空格开始 self.scores = [] self.frame_count = 0 cv2.putText( cam_frame, "Press SPACE to start", (50, 200), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 255, 0), 2, ) elif self.state == "JUMPING": # 读取示范视频下一帧 ret_demo, demo_frame = self.demo.read() if not ret_demo: self.state = "FINISH" continue demo_frame = self._resize_frame(demo_frame) # 提取示范视频角度特征 demo_rgb = cv2.cvtColor(demo_frame, cv2.COLOR_BGR2RGB) demo_result = self.pose.process(demo_rgb) demo_landmarks = ( demo_result.pose_landmarks.landmark if demo_result.pose_landmarks else None ) ref_angles = get_angle_dict(demo_landmarks) # 提取用户角度特征 cam_rgb = cv2.cvtColor(cam_frame, cv2.COLOR_BGR2RGB) cam_result = self.pose.process(cam_rgb) cam_landmarks = ( cam_result.pose_landmarks.landmark if cam_result.pose_landmarks else None ) user_angles = get_angle_dict(cam_landmarks) # 绘制骨架 demo_frame = self._draw_pose( demo_frame, demo_result.pose_landmarks ) cam_frame = self._draw_pose( cam_frame, cam_result.pose_landmarks ) # 计算评分 if ref_angles is not None and user_angles is not None: score = compute_score(user_angles, ref_angles) self.scores.append(score) self.frame_count += 1 else: score = 0.0 cv2.putText( cam_frame, "Tracking lost...", (30, 150), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2, ) cam_frame = self._draw_score(cam_frame, score) cv2.putText( cam_frame, f"Frame: {self.frame_count}", (30, 120), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2, ) cv2.imshow("Demo", demo_frame) elif self.state == "FINISH": # 视频播放完毕,显示本局平均分 if self.scores: avg_score = sum(self.scores) / len(self.scores) msg = f"Finish! Avg score: {avg_score:.1f} Press SPACE to restart" else: msg = "No valid frames. Press SPACE to restart" cv2.putText( cam_frame, msg, (50, 200), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 255), 2, ) cv2.imshow("User", cam_frame) key = cv2.waitKey(1) & 0xFF if key == ord("q"): break if key == ord(" "): if self.state in ("WAIT", "FINISH"): # 重置视频到第一帧并开始跟跳 self.demo.set(cv2.CAP_PROP_POS_FRAMES, 0) self.scores = [] self.frame_count = 0 self.state = "JUMPING" elif self.state == "JUMPING": self.state = "WAIT" self.cam.release() self.demo.release() cv2.destroyAllWindows() self.pose.close() if __name__ == "__main__": coach = JumpCoach() coach.run()主程序的核心逻辑不复杂,重点在于状态机设计。程序始终处于三种状态之一:等待开始、正在跟跳、本局结束。用户按下空格后进入跟跳,视频播放完毕回到结束状态,再按空格可以重新开始。
4.5 运行与验证
在项目根目录执行:
python main.py运行后预期效果:
- 打开两个窗口,窗口标题分别为
Demo和User; Demo窗口播放示范视频,并绘制人体骨架;User窗口显示摄像头画面,同步绘制用户骨架;- 用户跟着视频动作,
User窗口顶部实时显示评分; - 视频播放完毕后,画面显示本局平均分;
- 按
Q退出程序。
如果你使用本文前面提到的“原地抬臂 + 下蹲”测试视频,可以观察到:当用户动作和示范动作接近时,评分会在 80 分以上;当动作偏差较大时,评分会明显下降。说明评分模块已经生效。
5. 常见问题与排查思路
实操过程中最容易遇到的问题集中在依赖安装、摄像头、检测稳定性和运行性能几个方面。下面整理一份排查清单:
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
ModuleNotFoundError: No module named 'mediapipe' | 依赖未安装 | 执行pip install mediapipe,或使用镜像源 |
AttributeError: module 'mediapipe' has no attribute 'solutions' | mediapipe 版本异常或安装不完整 | 卸载重装,回退到较稳定的 0.10.x 版本 |
摄像头窗口黑屏或cap.isOpened()返回 False | 摄像头编号错误、被占用或权限未开启 | 修改 camera_id;关闭会议软件;检查系统相机权限 |
| 画面卡顿严重 | 每帧执行两次姿态检测,CPU 压力大 | 降低 frame_width、设置 model_complexity=0、跳帧处理 |
| 一直显示 Tracking lost | 光线暗、人离镜头太远或没有完整入镜 | 保证光线充足;身体完整出现在画面中;正对镜头 |
| 评分一直为 0 | 关键点未被检测到,或评分逻辑匹配不到角度 | 检查get_angle_dict返回的字典是否包含全部角度名 |
| 视频播放结束但无法重新开始 | 视频读取位置未重置 | 检查是否执行了demo.set(cv2.CAP_PROP_POS_FRAMES, 0) |
下面针对高频问题展开说明。
5.1 MediaPipe 安装或版本问题
MediaPipe 对 Python 版本有一定要求,某些较新的 Python 版本可能没有对应的预编译包。如果安装失败,可以考虑使用 Python 3.9 或 3.10 建立虚拟环境。
如果遇到solutions属性不存在,通常是安装到了非官方的同名包或者安装过程被中断。执行以下命令重装:
pip uninstall mediapipe -y pip install mediapipe==0.10.9需要注意,版本号要根据自己的 Python 环境调整,不要盲目追求最新版本。
5.2 摄像头打不开
摄像头打不开有一个很容易被忽视的原因:系统的摄像头权限。Windows 系统需要在“设置 → 隐私 → 相机”中允许桌面应用访问摄像头;macOS 需要在“系统设置 → 隐私与安全性 → 摄像头”中授权终端或 IDE 访问。
另外,如果开启了腾讯会议、钉钉、OBS 等软件,它们会独占摄像头,导致 OpenCV 无法读取。关闭这些软件后再试。
5.3 检测不稳定或评分为 0
MediaPipe 是依赖视觉特征进行检测的模型,光线、背景和人的大小都会影响结果。建议:
- 正对镜头,不要侧身;
- 保证上半身和下半身尽量完整入镜;
- 避免逆光;
- 背景不要太杂乱。
如果“Tracking lost”频繁出现,说明关键点检测失败,这时的评分帧不会计入总分,所以最终平均分会更真实。
6. 最佳实践与工程建议
本文的示例代码可以作为 Demo 运行,但距离一个正式的跟跳功能还有一定距离。下面这些工程建议可以帮助你把“一键跟跳”做得更健壮、更可用。
6.1 隐私与合规是前提
摄像头涉及用户隐私。在真实项目中,必须先获得用户明确授权,再打开摄像头,建议在 UI 中增加明显的授权说明。处理过程中,用户的视频流应保持在本地内存中,不做无意义的上传。如果确实需要保存动作数据,应进行脱敏处理,并设置明确的保留期限。
对开发者自己来说,也要注意不要在调试过程中把含有人脸的录像随意发布到公开网络。
6.2 关键点可见性过滤
上文代码没有对visibility做过滤,但生产环境必须处理。当某个关键点的visibility低于 0.5 时,应该认为该关键点不可靠,跳过该帧计算,而不是带着错误数据进入评分。
def is_visible(landmark, threshold=0.5): return landmark.visibility >= threshold增加这个过滤后,评分结果会明显稳定。
6.3 动作模板拆段管理
一套完整的跟跳视频通常包含热身、主动作、放松等多段动作。直接对整段视频做平均评分,很难定位用户在哪个动作上做得不好。
更合理的做法是把动作模板按时间段拆成多个片段,每个片段有独立的名称、起止帧和角度权重,例如:
{ "name": "dance_demo", "segments": [ { "name": "warmup", "start_frame": 0, "end_frame": 120, "weights": { "left_elbow": 1.0, "right_elbow": 1.0, "left_shoulder": 1.2, "right_shoulder": 1.2 } }, { "name": "main_move", "start_frame": 121, "end_frame": 300, "weights": { "left_knee": 1.5, "right_knee": 1.5, "left_hip": 1.2, "right_hip": 1.2 } } ] }按片段统计分数,用户可以清楚地知道自己在哪个环节需要改进。
6.4 帧率与耗时的取舍
本文代码中,每一帧都会对示范视频和摄像头画面各做一次姿态检测,CPU 压力较大。工程优化可以从几个方向入手:
- 降低摄像头采集分辨率和显示分辨率;
- 跳帧检测,例如每处理 2 帧检测 1 次;
- 将摄像头读取和模型推理放到两个线程;
- 在移动端使用 MediaPipe Tasks 的 TFLite 模型,性能和体积更优。
对实时性要求高的场景,宁可牺牲少量精度,也要保证画面流畅。
6.5 不要忽略声音和节拍反馈
跟跳类应用的核心体验不只是“显示分数”,还包括引导用户跟上节奏。建议在示范视频中加入音频节拍,或者在用户评分低于阈值时给出语音提示。当前的视觉评分只解决“动作像不像”的问题,“动作跟没跟上节奏”是另一个维度,需要引入节拍对齐算法,可以作为后续优化方向。
6.6 日志与异常处理
真实项目中,摄像头可能突然被其他应用占用,视频文件可能损坏。代码中应当对异常情况做捕获并记录日志,而不是让程序直接崩溃。例如,摄像头读取失败时,可以提示用户检查设备并自动重试。
import logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger("jump-coach") try: cap = cv2.VideoCapture(0) if not cap.isOpened(): logger.warning("camera open failed, retry in 2s") except Exception as exc: logger.error("unexpected error: %s", exc)7. 总结与下一步学习路线
回顾本文,我们完成了一个完整的“一键跟跳”工具,核心流程是:摄像头采集 → MediaPipe 姿态检测 → 关节角度提取 → 与示范视频角度计算相似度 → 实时分数展示。代码中包含了配置文件、姿态工具函数、评分模块和主程序,可以直接复制运行。
以下几个知识点是本文的关键收获:
- MediaPipe Pose 输出人体 33 个关键点,坐标是归一化的,但直接用坐标做对比不够鲁棒;
- 关节角度不受身高和摄像头距离影响,是动作对比的合理特征;
- 评分函数采用加权误差方案,权重可以根据运动类型调整;
- 状态机设计能让程序在待机、跟跳、结束三种状态之间平滑切换。
如果你想把这个 Demo 做成正式功能,下一步可以从这些方向继续深入:
- 将程序改造成 Web 应用,前端用浏览器调用摄像头,后端复用本文的评分逻辑;
- 把 MediaPipe Pose 换成 TFLite 模型,部署到 Android 或嵌入式设备;
- 增加动作类型识别,判断用户当前是在抬手还是下蹲,而不是只算相似度;
- 引入节拍对齐,让评分同时考虑“跟没跟准”和“跟没跟上”。
如果你打算把“一键跟跳”落地到真实项目,我建议先把手头的视频素材整理清楚,确定动作模板拆到多细,然后按“检测稳定 → 评分合理 → 交互流畅”的顺序逐步完善。技术本身并不复杂,复杂的是让用户真正跟着视频动起来、坚持下去的体验设计。希望本文能给你一个扎实的起点,动手跑起来再说。