简介:这是一套面向八段锦健身场景的智能辅助训练系统,基于计算机视觉技术,采用MediaPipeHolistic模型实时检测人体的三十三个身体关键点与四十二个手部关键点,并通过自建测试数据集验证了八个标准动作的识别准确率达百分之九十二。资源适合对姿态识别、运动科学分析或居家健身指导感兴趣的开发者、研究者和健身爱好者,可作为从模型原理到实际部署的完整参考项目。压缩包共十个文件,大小约13.87MB,包含程序源码、说明文档、附赠资料、配置数据以及字体文件等,代码结构清晰,可直接运行与二次开发。其中文本说明便于快速上手,程序脚本与配置数据可帮助调整检测参数,字体文件用于界面显示。系统能够实时跟踪动作并依据关键点位置关系判断是否符合标准,提供评分反馈与练习历史记录,有效弥补无人指导时的训练盲区。目前已有140人学习浏览,对于想要了解姿态估计模型实际应用、构建动作识别数据集或优化识别算法的人群,是一份具有较高参考价值的实战资料。
1. 一套用关键点驱动动作识别的八段锦陪练系统,值不值得自己搭
看到“基于计算机视觉的八段锦智能辅助训练系统”这个标题,搞 CV 的人第一反应多半是:这不就是“MediaPipe + 分类模型”的标准组合吗?确实,这个方向的核心并不在算法多深,而在于把关键点检测、动作分割、时序特征和一套自建数据集串成闭环。标题里那个 92% 的准确率,说高不算高,说低也够用,真正值钱的是“用 33 个身体关键点和 42 个手部关键点,把 8 个标准动作识别出来”这条落地路径。适合谁做?适合正在做毕设或大作业的计算机视觉初学者,也适合想给体育教学/康复训练做辅助工具但不想上整套姿态估计硬件的人。读完这篇文章,你会知道从零搭这样一套系统要过哪些坎,哪些地方会翻车,以及怎样把准确率从“实验室自嗨”做到“换个人换个环境也能用”。
2. 关键点从哪来:MediaPipe Holistic 的 33+42 点检测与坐标约定
2.1 为什么选 Holistic 而不是 Pose + Hands 分开跑
常见做法是直接用 MediaPipe Pose 拿 33 个身体关键点,再单独跑 MediaPipe Hands 补手部。但分开跑有两个问题:一是两条管线各自独立,同一帧里身体和手部的坐标对齐要靠时间戳硬凑,画面里人稍微动一下就错位;二是手部检测在身体遮挡、手臂快速摆动时本来就容易丢,再叠加一次检测失败,整条数据就得丢弃。
Holistic 模型把 Pose、Hands、Face 统一到一个推理图里,虽然本质上是区域内分别检测,但它在同一帧上完成,并且手部关键点会优先基于身体姿态预估的手腕位置去搜索,稳定性明显好。标题里提到“33个身体关键点和42个手部关键点”,42 = 左手 21 点 + 右手 21 点,这正好是 Holistic 输出的标准结构。对八段锦这种需要关注“双手托天”“左右开弓”手型细节的动作,手部点缺失率直接决定特征能不能用,所以选 Holistic 是对的。
2.2 Holistic 输出的关键点结构、坐标归一化与时序帧处理
Holistic 的pose_landmarks是 33 个点,每点包含 x、y、z 和可见度 visibility;left_hand_landmarks和right_hand_landmarks各 21 点。x、y 是归一化到 [0,1] 的图像坐标,z 以髋部为中心点的相对深度,单位不是米。visibility 表示该点被遮挡或模糊的程度,手部点没有 visibility,这在实际使用里是个坑,后面避坑章会专门说。
八段锦每个动作持续 4~8 秒,按 30fps 采集就是 120~240 帧。直接把这些原始坐标塞给模型,第一是维度爆炸,第二是坐标受画面里人高矮胖瘦影响太大。所以通常只保留关节角度和少量相对距离作为特征。还有一点:MediaPipe 的坐标归一化是基于整张图,如果人站在画面左侧和站在中央,同一个动作的 x 坐标会整体平移。角度特征天然不受这个影响,这也是我坚持用角度的原因。
2.3 最小可视化管线:读视频帧、跑 Holistic、画出骨架
先把最基础的数据管线跑通,后面所有特征都得从这层拿。下面这段代码是最小实现,打开摄像头或视频文件,逐帧跑 Holistic,然后画出骨架。
import cv2 import mediapipe as mp mp_holistic = mp.solutions.holistic mp_drawing = mp.solutions.drawing_utils cap = cv2.VideoCapture(0) # 换成视频文件路径也行 with mp_holistic.Holistic( static_image_mode=False, model_complexity=1, min_detection_confidence=0.5, min_tracking_confidence=0.5) as holistic: while cap.isOpened(): ok, frame = cap.read() if not ok: break # BGR -> RGB,MediaPipe 只吃 RGB rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results = holistic.process(rgb) # 绘制身体和手部骨架 if results.pose_landmarks: mp_drawing.draw_landmarks( frame, results.pose_landmarks, mp_holistic.POSE_CONNECTIONS) if results.left_hand_landmarks: mp_drawing.draw_landmarks( frame, results.left_hand_landmarks, mp_holistic.HAND_CONNECTIONS) if results.right_hand_landmarks: mp_drawing.draw_landmarks( frame, results.right_hand_landmarks, mp_holistic.HAND_CONNECTIONS) cv2.imshow('holistic', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()逻辑说明:每一帧先做颜色空间转换,Holistic.process内部完成人体检测和关键点回归。绘制函数里,身体用POSE_CONNECTIONS连线,手用HAND_CONNECTIONS。跑通这一步之后,你才能确认 MediaPipe 在你机器上的推理速度和稳定性。
参数说明:min_detection_confidence控制人体检测的阈值,调低能捡回一些半遮挡画面,但会产生更多抖动;min_tracking_confidence控制帧间跟踪阈值,调高会频繁重新检测,调低会沿用上一帧结果。我一般设 0.5,如果目标设备是 CPU,把model_complexity降到 0 能明显提速,代价是手部关键点精度下降,后面特征里角度值会出现几度的抖动。
3. 8 个标准动作怎么变成特征:动作分割、角度序列与自建数据集
3.1 把一套八段锦拆成 8 个动作段:端点检测与滑动窗口
八段锦“两手托天理三焦”这类动作,是一段连续的视频流。要识别 8 个标准动作,先解决“从哪开始、到哪结束”的问题。常见做法是检测动作的起始和结束端点,而不是对每一帧单独分类。最简单可靠的端点是看手腕或重心在 y 方向上的极值。
比如“双手托天”,手掌从腰部上托到头顶,手腕 y 坐标会经历一个下降再上升的过程,取局部极小值作为起点,局部极小值再一次出现作为终点。但直接用原始坐标会有大量抖动,需要先做滑动窗口平滑。
import numpy as np def smooth_1d(data, window_size=5): kernel = np.ones(window_size) / window_size return np.convolve(data, kernel, mode='same') def find_action_bounds(wrists_y, min_gap=15): # wrists_y: 左右手腕在连续帧中的 y 坐标均值(图像坐标向下增大) smoothed = smooth_1d(wrists_y) # 找极小值点:左右都比当前点大的局部最小值 minima = [] for i in range(1, len(smoothed) - 1): if smoothed[i] < smoothed[i-1] and smoothed[i] < smoothed[i+1]: # 距离上一个端点太近就跳过,防止同一段的抖动产生多个端点 if len(minima) == 0 or i - minima[-1] > min_gap: minima.append(i) return minima逻辑说明:min_gap是两段动作之间的最小帧间隔,按 30fps 算,八段锦每段动作结束后会有短暂的定势停顿,15 帧约等于 0.5 秒,能有效滤掉动作过程中的小抖动。这个函数返回的是帧索引列表,相邻两个索引之间就是一段候选动作。
参数说明:window_size越大越平滑,但会让边界偏移,我通常取 5,也就是 5 帧平均。如果发现分割出来的动作段明显短于真实动作,八成是min_gap太大把开头砍掉了;反过来,如果一段动作被切碎,就调小min_gap或增大window_size。这个环节是整个系统里最玄学的部分,不同动作的节奏差异很大,最好针对 8 个动作分别测试端点位置。
3.2 关节角度特征:为什么不用原始坐标
原始坐标喂给模型也不是完全不行,但泛化能力很差。训练时人站在画面左侧,测试时站到右侧,模型就懵了。关节角度是旋转和平移不变的,所以对摄像头角度、人物横向位置不敏感。八段锦的动作识别主要靠大关节的相对位置变化,角度序列是最稳定的表征。
def compute_angle(a, b, c): # a, b, c 是三个关键点的 (x, y) 坐标,角度顶点为 b ab = (a[0] - b[0], a[1] - b[1]) cb = (c[0] - b[0], c[1] - b[1]) dot = ab[0] * cb[0] + ab[1] * cb[1] cross = ab[0] * cb[1] - ab[1] * cb[0] angle = np.arctan2(abs(cross), dot) return np.degrees(angle) # 示例:左肘角度,顶点是 elbow left_elbow_angle = compute_angle( (lshoulder_x, lshoulder_y), (lelbow_x, lelbow_y), (lwrist_x, lwrist_y) )逻辑说明:np.arctan2返回的是带符号的角度,取绝对值后得到 0~180 度的关节角。比np.dot加acos的方式更数值稳定,不会出现因 cos 值略微超出 [-1,1] 导致的角度跳变。对于八段锦,我一般取这些角度:左右肘、左右肩、左右膝、左右髋和躯干倾斜角,一共 9 个角度,再加两个手腕中心的距离作为手部配合特征。
参数说明:角度计算基于图像坐标,如果画面里人是正对摄像头的,左右关节角度会存在镜像对称差异。建议在采集时就规定人脸朝向一致,否则需要在预处理里做左右手翻转,这里有个大坑,后面避坑章会专门说。
3.3 自建测试数据集:采谁、采多少、动作标签怎么定
标题里的 92% 准确率来自“自建测试数据集”,这是整个项目最容易被低估的部分。数据采集如果偷懒——比如只找两个人,在同一块背景、同一台摄像头下录制——模型很快会过拟合到“这个人的动作习惯”而不是“八段锦这个动作”。
我一般按这个来设定采集方案:
- 至少 5 个不同身高体型的人,每人把 8 个动作各做 10 遍,得到 400 个动作片段。
- 每段动作从端点分割结果里截取,而不是手工裁剪,这样能保证训练数据和推理时的数据分布一致。
- 动作标签不是数字 1~8,而是记录成“动作名 + 人员编号 + 重复次数”,方便做分组验证。
import json samples = [] for person_id in range(5): for action_id in range(1, 9): for rep in range(10): # 假设样本已通过端点检测切好 sample = { "person": person_id, "action": action_id, "rep": rep, "video_path": f"data/p{person_id}_a{action_id}_r{rep}.mp4", "label": action_id } samples.append(sample) with open("dataset.json", "w", encoding="utf-8") as f: json.dump(samples, f, ensure_ascii=False, indent=2)逻辑说明:dataset.json只是索引文件,真正的关键点序列要离线用 Holistic 跑一遍视频后缓存成 numpy 数组。这样每次调模型不用重新跑推理,省下大量时间。缓存文件命名和video_path一一对应,方便排查是哪一段数据出了问题。
参数说明:重复次数 10 遍乍看不多,但换算成关键点序列就是 400 段 × 平均 60 帧 = 24000 帧,足够喂一个随机森林或小型 LSTM。如果条件允许把重复次数加到 20,准确率能再涨一点,但采集时长会翻倍,权衡之后我一般用 10~15。
4. 动作识别模型选型与 92% 准确率的复现路径
4.1 小数据集下模型对比:LSTM、1D-CNN 与随机森林
自建数据集规模不大,模型选型不用追新。标题能到 92%,用随机森林或 LSTM 都能做到。区别在于:
- 随机森林:训练快,特征解释性强,适合角度序列拼接成的固定长度向量。八段锦每段动作帧数不固定,需要先做插值或截断到固定长度。
- LSTM:天然处理变长序列,能学到动作节奏,但训练慢,数据量小的时候容易过拟合。
- 1D-CNN:可以局部提取动作特征,但需要配合 pooling 处理变长输入,调参比随机森林复杂。
我个人的建议是:先跑随机森林得到一个准确率基线,再决定要不要上 LSTM。很多项目最后发现随机森林够用,LSTM 只提升了 1~2 个百分点,却引入了大量训练不稳定因素。
4.2 训练/验证划分与评估口径:帧级还是动作级
这里有个隐藏的“准确率游戏”。如果你把每一帧都当作独立样本去划分训练集和测试集,同一段动作的前半段在训练集、后半段在测试集,模型会把相似帧识别得很准,准确率能轻松到 95% 以上,但实际用在完整动作上会翻车。
正确做法是按“动作片段”划分:一个完整的动作要么在训练集,要么在测试集,不能拆开。这样模型才是在识别动作的完整轨迹,而不是背帧。
from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import GroupShuffleSplit # X 是角度序列特征,长度固定为 90 帧的插值结果 # groups 是每个样本所属的动作片段 ID gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42) train_idx, test_idx = next(gss.split(X, y, groups=sample_ids)) X_train, X_test = X[train_idx], X[test_idx] y_train, y_test = y[train_idx], y[test_idx] clf = RandomForestClassifier(n_estimators=300, max_depth=12, random_state=42) clf.fit(X_train, y_train) print("test accuracy:", clf.score(X_test, y_test))逻辑说明:GroupShuffleSplit保证同一个sample_id的所有帧不会被拆散。这里sample_id就是前面dataset.json里的“动作片段 ID”,比如p0_a1_r0。如果不按 group 划分,测试准确率会虚高 5~10 个百分点。
参数说明:n_estimators=300对于这种中等规模数据集足够,再大收益很小且训练变慢;max_depth=12限制单棵树深度,防止小数据集过拟合。跑完看测试集准确率,能到 90% 以上就说明特征方向和模型选型没大问题。
4.3 参数表与推理时的多数投票策略:把帧级结果变成动作级判定
训练时用的是插值后的固定长度序列,但实际推理时动作是连续流动的,没法等整个动作结束再判断。常见做法是开一个 30 帧的滑动窗口,每 5 帧做一次预测,然后对窗口内的预测结果做多数投票。
| 参数 | 建议值 | 说明 |
|---|---|---|
| 滑动窗口 | 30 帧 | 约 1 秒,太短识别不出动作节奏 |
| 步长 | 5 帧 | 保证实时性,也避免输出频繁跳变 |
| 投票方式 | 多数投票 | 取窗口内出现次数最多的类别 |
| 置信度阈值 | 0.7 | 低于阈值输出“未识别”,不硬猜 |
from collections import Counter def predict_action(angle_window, clf): # angle_window: 一个动作段的全部角度帧 preds = [] step = 5 for start in range(0, len(angle_window) - 30 + 1, step): seg = angle_window[start:start+30] flat = seg.reshape(1, -1) preds.append(clf.predict(flat)[0]) if not preds: return -1 counter = Counter(preds) label, count = counter.most_common(1)[0] # 如果票数太少说明窗口内动作不确定,返回 -1 if count / len(preds) < 0.5: return -1 return label逻辑说明:窗口每 5 帧滑动一次,得到一个类别序列,Counter统计后取最高票。如果最高票占比不足一半,说明窗口跨越了两个动作的边界,这时候输出“未识别”比硬猜一个动作更安全。这个策略能把帧级准确率 92% 转换成动作级准确率,实际体验更稳。
参数说明:step=5在 30fps 下相当于每 0.17 秒刷一次结果,视觉上有连续感。如果觉得卡顿,把 step 改到 10,CPU 负载降低,但动作起始段会晚 3 帧左右响应。
5. 避坑指南:关键点丢失、动作错位、过拟合的 5 个典型问题
5.1 现象:手部关键点频繁消失,识别准确率掉到 70%
- 现象:视频里人物的手在某个角度突然没有骨架,尤其是手臂快速上举时,左/右手的关键点返回空列表。
- 原因:MediaPipe Holistic 的手部检测依赖先检测到手腕,如果手腕被身体挡住或运动模糊,手部模块会直接退出。八段锦里的“双手托天”动作手腕在头顶后方,很容易触发。
- 解决:不丢弃这些帧,而是把缺失的手部关键点用前一帧的值前向填充;如果连续缺失超过 5 帧,就用左右手对称关系镜像补点。另外把
min_tracking_confidence调低到 0.3,能明显减少手部丢失,代价是偶尔会跟错手。
5.2 现象:同一动作不同人做,识别结果不一致
- 现象:A 的“左右开弓似射雕”识别稳定,B 的总是被识别成“攒拳怒目增气力”。
- 原因:自建数据集里人员样本太少,模型学到的是“这个人的动作幅度”而非“动作的共性结构”。比如弓步下蹲深的人膝角变化大,下蹲浅的人膝角变化小,模型就把幅度差异当成类别差异。
- 解决:采集时要求每位受试者尽量按标准动作幅度做,同时在特征里加入相对位移的归一化。我后来把每个动作的起始帧作为基准,只保留相对角度变化量,不同人的幅度差异就显著减小了。
5.3 现象:训练集准确率 96%,测试集只有 80%
- 现象:训练时准确率几乎拉满,测试时明显下降,甚至某些动作完全分类错误。
- 原因:这是典型的小数据过拟合,随机森林的树记住了训练集里某个人独特的关节角度模式。另一个隐藏问题是前面说过的,划分数据集时没有按动作片段分组,导致同一段动作被拆进训练/测试。
- 解决:先把划分方式改成
GroupShuffleSplit,再看单棵树的max_depth是否过大。我一般把max_depth控制在 10~15,并且打开min_samples_leaf=5,让每片叶子至少有 5 个样本,过拟合会减轻很多。
5.4 现象:动作边界检测不准,两段动作被合成一段
- 现象:一些动作长度特别长,比如“双手托天”中间有一个短暂定势,被端点检测忽略,直接和下一个动作连到一起。
- 原因:
min_gap设得太大,把动作之间短暂的停顿跳过了。八段锦每段之间停顿约 1~2 秒,但有些初学者做得快,停顿不到 0.5 秒。 - 解决:把端点检测从“只找极小值”改成“找极小值 + 持续低速度”。具体做法是计算手腕 y 方向的速度绝对值,连续 10 帧速度都低于阈值才认为是动作边界,而不是只看一帧极值。这个改动能把误切率降一半。
5.5 现象:摄像头角度一变,整个系统失灵
- 现象:训练时摄像头正对受试者,换成斜 45 度或略俯视的教室摄像头后,准确率崩盘。
- 原因:关节角度虽然平移旋转不变,但斜视角下前景/背景比例变化会改变关键点深度和遮挡关系,手部区域有更多自遮挡。模型没见过这些角度。
- 解决:这是最难补的坑。最有效的办法是采集时让受试者站在角度可调的转盘上,或者在不同方位各采一遍。退而求其次,训练时对角度特征做高斯噪声增强——给每个角度加±5 度随机扰动,相当于模拟不同拍摄角度,模型会健壮不少。
6. 把 92% 再往上推:数据增强、帧采样与置信度过滤的实战调优
6.1 关键点坐标的随机仿射变换:让模型不怕高矮胖瘦
角度特征已经去掉了绝对值,但不同人的手臂长度、肩宽仍然会影响角度变化速率。我一般会在训练前对角度序列做随机缩放和偏移:
def augment_angles(angle_seq, noise_scale=3.0, offset_scale=2.0): noise = np.random.normal(0, noise_scale, angle_seq.shape) # 对每条序列整体加一个随机偏移,模拟不同人的站姿差异 offset = np.random.uniform(-offset_scale, offset_scale, size=(1, angle_seq.shape[1])) return angle_seq + noise + offset逻辑说明:noise_scale=3是对每帧每个角度加独立高斯噪声,offset_scale=2是对整个动作序列的所有帧加同一个偏移。前者模拟关键点检测抖动,后者模拟不同人的关节角度基准差异。这两种增强组合起来,比单纯复制训练数据有效得多。
参数说明:噪声太大会破坏动作结构,超过 8 度就基本看不出“双手托天”了。我建议噪声控制在 2~4 度,偏移控制在 1~3 度。数据增强只用在训练集,测试集保持原始数据,否则测试准确率没有参考意义。
6.2 帧采样策略:3 帧还是 5 帧,窗口步长怎么定
前面把动作段插值成固定 90 帧,这个数字不是随便定的。原始动作段最短可能只有 40 帧,最长 120 帧,强行拉伸到 90 帧会让慢动作“信息冗余”,让快动作“丢帧”。后来我把插值方式从“均匀采样”改成“动态时间规整下采样”:
- 动作段少于 90 帧:用线性插值补到 90 帧。
- 动作段多于 90 帧:先做 5 帧均值平滑,再等间隔抽到 90 帧。
这样做的好处是保留动作的起止节奏,而不是简单拉伸。窗口步长方面,训练时用 90 帧全序列,推理时用 30 帧窗口 + 5 帧步长,两者并不冲突——训练时模型看到的是完整动作,推理时通过投票模拟完整动作。
6.3 置信度过滤与关键点插值:最后一公里
前向填充能解决短时关键点丢失,但连续丢失超过 10 帧时,角度序列会出现明显台阶状跳变。我的习惯是:
- 对每个关键点的 x、y、visibility 做单独的三阶样条插值,而不是只填常数。
- 如果某帧的关键点 visibility 低于 0.8,该帧的角度值直接标记为缺失,不参与插值。
- 插值完成后,再做一次 5 帧滑动平均,去掉最后的毛刺。
这一步做完,92% 的准确率能稳定到 94% 上下,更重要的是系统不会再因为个别帧抖动而输出跳变的动作标签。我自己每调整一次特征或模型,都会把一段真实测试视频的预测结果按时间轴回放一遍,看动作标签是否在动作切换点附近干净地跳变。这个检查比盯着 test accuracy 有用得多——准确率是数字,回放是体验。别看 MediaPipe 关键点检测像个黑匣子,只要懂得看它输出的边界,这套八段锦辅助训练系统就能真正从“大作业”变成能放进教室或康复室当陪练的工具。希望帮到你。
本文还有配套的精品资源,点击获取