简介这是一套面向八段锦健身场景的智能辅助训练系统基于计算机视觉技术采用MediaPipeHolistic模型实时检测人体的三十三个身体关键点与四十二个手部关键点并通过自建测试数据集验证了八个标准动作的识别准确率达百分之九十二。资源适合对姿态识别、运动科学分析或居家健身指导感兴趣的开发者、研究者和健身爱好者可作为从模型原理到实际部署的完整参考项目。压缩包共十个文件大小约13.87MB包含程序源码、说明文档、附赠资料、配置数据以及字体文件等代码结构清晰可直接运行与二次开发。其中文本说明便于快速上手程序脚本与配置数据可帮助调整检测参数字体文件用于界面显示。系统能够实时跟踪动作并依据关键点位置关系判断是否符合标准提供评分反馈与练习历史记录有效弥补无人指导时的训练盲区。目前已有140人学习浏览对于想要了解姿态估计模型实际应用、构建动作识别数据集或优化识别算法的人群是一份具有较高参考价值的实战资料。1. 一套用关键点驱动动作识别的八段锦陪练系统值不值得自己搭看到“基于计算机视觉的八段锦智能辅助训练系统”这个标题搞 CV 的人第一反应多半是这不就是“MediaPipe 分类模型”的标准组合吗确实这个方向的核心并不在算法多深而在于把关键点检测、动作分割、时序特征和一套自建数据集串成闭环。标题里那个 92% 的准确率说高不算高说低也够用真正值钱的是“用 33 个身体关键点和 42 个手部关键点把 8 个标准动作识别出来”这条落地路径。适合谁做适合正在做毕设或大作业的计算机视觉初学者也适合想给体育教学/康复训练做辅助工具但不想上整套姿态估计硬件的人。读完这篇文章你会知道从零搭这样一套系统要过哪些坎哪些地方会翻车以及怎样把准确率从“实验室自嗨”做到“换个人换个环境也能用”。2. 关键点从哪来MediaPipe Holistic 的 3342 点检测与坐标约定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 和可见度 visibilityleft_hand_landmarks和right_hand_landmarks各 21 点。x、y 是归一化到 [0,1] 的图像坐标z 以髋部为中心点的相对深度单位不是米。visibility 表示该点被遮挡或模糊的程度手部点没有 visibility这在实际使用里是个坑后面避坑章会专门说。八段锦每个动作持续 48 秒按 30fps 采集就是 120240 帧。直接把这些原始坐标塞给模型第一是维度爆炸第二是坐标受画面里人高矮胖瘦影响太大。所以通常只保留关节角度和少量相对距离作为特征。还有一点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_modeFalse, model_complexity1, min_detection_confidence0.5, min_tracking_confidence0.5) as holistic: while cap.isOpened(): ok, frame cap.read() if not ok: break # BGR - RGBMediaPipe 只吃 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_size5): kernel np.ones(window_size) / window_size return np.convolve(data, kernel, modesame) def find_action_bounds(wrists_y, min_gap15): # 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[i1]: # 距离上一个端点太近就跳过防止同一段的抖动产生多个端点 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: fdata/p{person_id}_a{action_id}_r{rep}.mp4, label: action_id } samples.append(sample) with open(dataset.json, w, encodingutf-8) as f: json.dump(samples, f, ensure_asciiFalse, indent2)逻辑说明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_splits1, test_size0.2, random_state42) train_idx, test_idx next(gss.split(X, y, groupssample_ids)) X_train, X_test X[train_idx], X[test_idx] y_train, y_test y[train_idx], y[test_idx] clf RandomForestClassifier(n_estimators300, max_depth12, random_state42) 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_estimators300对于这种中等规模数据集足够再大收益很小且训练变慢max_depth12限制单棵树深度防止小数据集过拟合。跑完看测试集准确率能到 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:start30] 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% 转换成动作级准确率实际体验更稳。参数说明step5在 30fps 下相当于每 0.17 秒刷一次结果视觉上有连续感。如果觉得卡顿把 step 改到 10CPU 负载降低但动作起始段会晚 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_leaf5让每片叶子至少有 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_scale3.0, offset_scale2.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_scale3是对每帧每个角度加独立高斯噪声offset_scale2是对整个动作序列的所有帧加同一个偏移。前者模拟关键点检测抖动后者模拟不同人的关节角度基准差异。这两种增强组合起来比单纯复制训练数据有效得多。参数说明噪声太大会破坏动作结构超过 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 关键点检测像个黑匣子只要懂得看它输出的边界这套八段锦辅助训练系统就能真正从“大作业”变成能放进教室或康复室当陪练的工具。希望帮到你。本文还有配套的精品资源点击获取