基于MediaPipe Holistic的八段锦动作识别与质量评估实战
简介这份资源面向计算机视觉与智能健身方向的开发者、学生及八段锦练习者提供一套基于MediaPipeHolistic的八段锦智能辅助训练系统实现方案。系统通过摄像头实时检测人体33个身体关键点与42个手部关键点结合自建测试数据集对8个标准动作进行识别准确率达92%可用于居家动作纠正、训练评分与练习历史跟踪。压缩包共10个文件约13.87MB包含Python主程序、JSON配置、Markdown说明文档、TXT依赖清单、DOCX附赠资料及TTF字体文件覆盖从环境搭建到动作分析的核心环节。目前已有142人学习。读者可获取完整项目代码与运行说明理解关键点检测、动作特征比对与评分反馈的实现思路并借助字体与配置文件快速复现系统适合作为课程设计、毕业项目或计算机视觉落地健身场景的参考案例。1. 从「对着视频练八段锦」说起MediaPipe Holistic 能解决什么八段锦看起来慢但真到自己练问题一点都不少手抬多高算「标准」、开弓时两臂是不是一条线、摇头摆尾的幅度够不够、上举时掌心到底朝哪。跟着视频练最大的问题是没人给你反馈练了半年可能动作一直是错的。我一开始想用姿态估计做个「AI 教练」试过 OpenPose、试过纯 Pose 模型最后落到 MediaPipe Holistic 上原因很直接它一次前向就能同时给出 33 个身体关键点、468 个面部关键点和 21×2 个手部关键点而八段锦恰恰是「身体姿态 手型 头部朝向」三者耦合的动作缺一个维度都判不准。这套方案要解决的核心问题是把「练得对不对」从主观感受变成可量化的关键点坐标再用规则或分类器映射到 8 个标准动作上。它适合两类人一类是想做计算机视觉大作业、又不想从零训 backbone 的学生另一类是真想给自己或家人做个居家训练反馈工具的开发者。整条链路不依赖昂贵设备普通摄像头 一台能跑 MediaPipe 的机器就能起步这也是我愿意把它写成可复现笔记的原因。2. MediaPipe Holistic 的关键点体系与选型理由2.1 33 42 个关键点分别对应什么先把坐标系和索引搞清楚不然后面写规则全是玄学。MediaPipe Holistic 输出的是一组归一化坐标x、y 在 0~1z 是相对深度身体部分沿用 BlazePose 的 33 点拓扑手部是每只手 21 点。做八段锦真正高频用到的是身体点里的肩、肘、腕、髋、膝、踝以及手部的指尖和掌指关节。部位关键点索引身体八段锦里的用途肩11 左肩 / 12 右肩判断开弓、上举时两肩是否水平肘13 左肘 / 14 右肘计算手臂弯曲角度腕15 左腕 / 16 右腕配合手部点判断掌型髋23 左髋 / 24 右髋判断马步下沉、重心偏移膝25 左膝 / 26 右膝判断屈膝角度是否到位踝27 左踝 / 28 右踝站姿稳定性、双脚开度手部 21 点里我一般只用 4 个指尖索引 4、8、12、16加掌心点索引 0、9来区分「握拳」「立掌」「八字掌」这几种八段锦常见手型。面部 468 点在这套系统里基本不用除非你要做「摇头摆尾」时头部朝向的辅助判断否则直接忽略能省不少算力。2.2 为什么是 Holistic 而不是单独 Pose 或 Hands单独用 Pose 模型手部只有腕关节一个点你根本分不清「立掌」和「握拳」而八段锦里「攒拳怒目」「双手托天」的手型差异恰恰是判分关键。单独用 Hands 模型又拿不到躯干和下肢马步、弓步全废。Holistic 的价值就在于它把两条流水线在同一个图里跑共享一次人体检测手部区域是从身体关键点裁剪出来的 ROI所以手和身体的坐标天然对齐不需要你手动做坐标变换。代价也要说清楚Holistic 比纯 Pose 慢模型体积也大。实测在普通 CPU 上单帧大概几十毫秒量级做实时预览够用但如果你要跑高帧率视频批处理建议降采样到 15~20 FPS 再喂进去。选型上我的判断是只要你的动作识别依赖手型Holistic 就是当前最省事的方案没有之一。2.3 最小可运行代码把关键点抽出来先跑通「一帧图 → 关键点」再谈识别。下面这段是抽帧检测的最小骨架重点是理解results里各字段的结构。import cv2 import mediapipe as mp mp_holistic mp.solutions.holistic # static_image_modeFalse 走视频流模式内部会做关键点跟踪更快 # model_complexity1 是精度和速度的折中0 最快、2 最准 with mp_holistic.Holistic( static_image_modeFalse, model_complexity1, smooth_landmarksTrue, min_detection_confidence0.5, min_tracking_confidence0.5, ) as holistic: cap cv2.VideoCapture(baduanjin.mp4) while cap.isOpened(): ok, frame cap.read() if not ok: break rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results holistic.process(rgb) # pose_landmarks 是 33 点hand_landmarks 是单只手的 21 点 if results.pose_landmarks: for idx, lm in enumerate(results.pose_landmarks.landmark): print(idx, round(lm.x, 3), round(lm.y, 3), round(lm.z, 3)) if results.left_hand_landmarks: print(left hand points:, len(results.left_hand_landmarks.landmark)) if results.right_hand_landmarks: print(right hand points:, len(results.right_hand_landmarks.landmark)) cap.release()逻辑说明Holistic对象要复用不要每帧新建否则跟踪状态丢失、速度暴跌。smooth_landmarksTrue会让关键点在时间轴上做平滑抖动明显减小但代价是动作切换瞬间会有轻微延迟做快速动作识别时可以考虑关掉。参数说明min_detection_confidence控制首次检测阈值光线差就调低到 0.3~0.4min_tracking_confidence控制跟踪阈值画面里人一直没出画就保持 0.5 即可。跑通这一步你手里就有了一串随时间变化的关键点序列后面所有识别都是在这串序列上做文章。3. 自建测试数据集8 个动作怎么采、怎么标3.1 采集方案与动作切分公开数据集里没有八段锦这是绕不过去的坎只能自建。我的做法是找 3~5 个会打八段锦的人每人每个动作重复 10 遍正面和侧面各录一遍用手机 1080p、30 FPS 就够。8 个动作分别是双手托天理三焦、左右开弓似射雕、调理脾胃须单举、五劳七伤往后瞧、摇头摆尾去心火、两手攀足固肾腰、攒拳怒目增气力、背后七颠百病消。关键不是录多少而是怎么切。八段锦是连续套路你不能把整段视频当一个样本。我一般用「动作起始帧 结束帧」手动标一遍或者用关键点速度的过零点做半自动切分再人工修正。每个动作片段控制在 3~6 秒太短关键点序列不够太长会混入过渡动作。3.2 特征工程从关键点序列到可分类向量直接把原始坐标丢给分类器效果一般因为坐标受拍摄距离和站位影响。我一般做三层特征第一层是归一化坐标以髋中心为原点用肩宽做尺度归一化消除身高和距离差异。第二层是关节角度比如肘角、膝角、两臂夹角这些是尺度无关的对八段锦这种讲究「角度到位」的动作特别有效。第三层是时序统计量对每个角度在动作窗口内取均值、最大值、标准差把变长序列压成定长向量。import numpy as np def angle(a, b, c): # 计算 b 为顶点时 a-b-c 的夹角输入是 (x, y) 或 (x, y, z) a, b, c np.array(a), np.array(b), np.array(c) ba, bc a - b, c - b cos np.dot(ba, bc) / (np.linalg.norm(ba) * np.linalg.norm(bc) 1e-6) return np.degrees(np.arccos(np.clip(cos, -1.0, 1.0))) def extract_features(pose_seq): # pose_seq: (T, 33, 3) 一段动作的关键点序列 feats [] for t in range(pose_seq.shape[0]): p pose_seq[t] feats.append([ angle(p[11], p[13], p[15]), # 左肘角 angle(p[12], p[14], p[16]), # 右肘角 angle(p[23], p[25], p[27]), # 左膝角 angle(p[24], p[26], p[28]), # 右膝角 angle(p[13], p[11], p[12]), # 左肩-肩-右肩判断开弓 ]) feats np.array(feats) # (T, 5) # 时序聚合压成定长 return np.concatenate([feats.mean(0), feats.max(0), feats.std(0)])逻辑说明angle用余弦定理算夹角加1e-6防止除零。extract_features先逐帧算角度再对时间轴做 mean/max/std 聚合得到一个 15 维向量。参数说明如果你要区分「摇头摆尾」这种躯干旋转明显的动作再加一个两髋连线与两肩连线的夹角如果手型重要把手部指尖到掌心的距离也加进去。这套特征维度低、可解释小样本下比直接上深度模型稳。3.3 数据集划分与标注格式标注我用最简单的 CSV一行一个样本前 15 列是特征最后一列是动作标签0~7。划分上按「人」划分不要按样本随机划分否则同一个人同一动作的片段会同时出现在训练和测试集里准确率虚高。我一般 3 人训练、1 人验证、1 人测试这样测出来的 92% 才是真的能泛化到新人的数字。提示采集时让每个人站位略有不同、光线略有差异数据集才有鲁棒性。全在同一个位置录模型学到的可能是背景而不是动作。4. 8 个动作的识别实现与准确率验证4.1 分类器选型为什么小样本别急着上深度网络自建数据集通常只有几百到一两千个样本这个量级上 SVM 或随机森林往往比 LSTM 更稳训练快、调参少、不容易过拟合。我一般先用随机森林打底看混淆矩阵哪个动作老混就针对性地补特征。等样本量上到几千、且你有 GPU 资源再考虑 1D-CNN 或 LSTM 吃原始序列。from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import cross_val_score import numpy as np X np.load(features.npy) # (N, 15) y np.load(labels.npy) # (N,) clf RandomForestClassifier( n_estimators300, # 树够多方差小 max_depth12, # 限制深度防过拟合 min_samples_leaf2, random_state42, ) scores cross_val_score(clf, X, y, cv5, scoringaccuracy) print(cv acc:, scores.mean(), scores.std()) clf.fit(X, y)逻辑说明cross_val_score先给你一个不依赖单次划分的准确率估计std大说明数据分布不均要回去查是不是某个动作样本太少。参数说明max_depth是防过拟合的关键样本少就压到 8~10n_estimators300 基本够再多收益递减。跑完看混淆矩阵八段锦里最容易混的是「调理脾胃须单举」和「双手托天」因为都是单臂上举区别在另一只手的位置这时候就得补一个「另一只手腕相对髋的高度」特征。4.2 从单帧分类到动作片段判定真实使用中你不可能手动切好片段再喂给分类器得让系统自己判断「一个动作开始了、结束了」。我的做法是滑窗维护一个 1.5 秒的缓冲队列每来一帧就更新特征用分类器预测当前窗口的标签连续 N 帧预测一致才确认动作成立。这样能过滤掉过渡帧的抖动。from collections import deque, Counter window deque(maxlen45) # 30FPS 下约 1.5 秒 confirm deque(maxlen10) # 连续 10 帧一致才确认 def on_frame(feature_vec): window.append(feature_vec) if len(window) 30: return None agg np.concatenate([np.mean(window, 0), np.max(window, 0), np.std(window, 0)]) pred clf.predict([agg])[0] confirm.append(pred) if len(confirm) 10 and len(set(confirm)) 1: return pred # 确认一个动作 return None逻辑说明window存最近 45 帧的逐帧特征聚合方式和训练时保持一致这点非常重要训练和推理的特征口径不一致是翻车重灾区。confirm做多数确认避免单帧误判触发。参数说明窗口长度按你的帧率换算30 FPS 用 4515 FPS 就用 22 左右确认帧数 10 是经验值要求响应快就降到 5要求稳就加到 15。4.3 92% 准确率是怎么测出来的这个数字必须说清楚测法否则没有意义。我的口径是按人划分测试集测试集里的人不参与训练每个动作取完整片段做滑窗判定动作级准确率 正确确认的动作数 / 总动作数。不是逐帧准确率逐帧准确率会因为大量静止帧而虚高到 97% 以上没有参考价值。8 个动作里通常「摇头摆尾」和「两手攀足」准确率偏低前者因为躯干旋转在正面视角下关键点重叠后者因为弯腰时手部容易被身体遮挡。这两个动作建议补侧面视角数据。5. 避坑与排查这套系统最容易翻车的 5 个地方5.1 关键点抖动导致动作误触发现象人站着不动系统却偶尔报出一个动作。原因MediaPipe 在低光照或背景杂乱时关键点会跳变逐帧角度特征随之抖动滑窗聚合后仍可能越过分类边界。解决开smooth_landmarksTrue并在特征层对角度做滑动平均确认帧数从 10 提到 15光照不足就补一盏正面灯这是最省事的办法。5.2 训练和推理特征口径不一致现象离线交叉验证 92%一上实时就掉到 60% 多。原因训练时用的是手动切好的完整片段做聚合推理时用的是滑窗窗口长度和聚合范围对不上。解决训练阶段就用滑窗方式生成特征让训练和推理走同一套extract_features逻辑别写两份代码。5.3 手部关键点频繁丢失现象left_hand_landmarks经常是 None手型判断时有时无。原因手离镜头太远、被身体遮挡或手部 ROI 裁剪失败。解决把手部判断做成「有则用、无则降级」丢失时只用身体特征判动作大类不判手型同时让使用者站得离镜头近一点手部占画面比例大一些。5.4 按样本随机划分导致准确率虚高现象报告里写 98%换个人测直接崩。原因同一个人同一动作的相邻片段高度相似随机划分会让训练集和测试集泄漏。解决严格按人划分测试集的人一个片段都不能进训练集。这是血泪经验评审或答辩时被问到划分方式答错直接扣分。5.5 不同身高体型导致角度阈值失效现象高个子做「双手托天」被判成没到位。原因特征没做尺度归一化或者规则里写死了像素阈值。解决所有坐标以髋中心为原点、肩宽为尺度归一化判断「到位」用角度而不是绝对高度角度是尺度无关的。6. 进阶把规则和模型叠起来让判分更接近真人教练纯分类器只能告诉你「这是第几个动作」但真人教练还会说「你这个开弓手臂没拉直」。要做到这一步得在动作识别之上再叠一层质量评估。我的做法是动作确认后取该动作窗口内的关键帧对几个核心角度设理想区间算偏离度。比如「左右开弓似射雕」理想状态下拉弓臂的肘角接近 170 度、两肩连线与拉弓方向垂直偏离超过阈值就给出具体提示。# 动作确认后做质量打分 def score_kai_gong(pose_seq): scores {} elbow [angle(p[11], p[13], p[15]) for p in pose_seq] # 左肘角序列 max_elbow max(elbow) # 理想接近 170 度偏离越多扣分越多 scores[arm_straight] max(0, 100 - abs(170 - max_elbow) * 2) shoulder_tilt [abs(p[11][1] - p[12][1]) for p in pose_seq] scores[shoulder_level] max(0, 100 - max(shoulder_tilt) * 500) return scores逻辑说明score_kai_gong在动作窗口内取肘角最大值和肩部高度差最大值映射成 0~100 的分数。参数说明170和500这两个系数要在你的数据集上标定不同拍摄角度下数值会变别照抄。这套打分的价值在于它可解释用户知道错在哪而不是只看到一个冷冰冰的类别。验证方法上我一般会做两件事一是找几个真人教练对同一批动作打分和系统分数做相关性分析相关性到 0.7 以上才算能用二是做消融把质量评估关掉只留动作识别看用户反馈差多少确认这层不是白加的。最后说个习惯我做完任何关键点项目都会先把原始关键点序列可视化出来看一遍抖动、丢失、坐标跳变全在图上比看准确率数字有用得多。这套八段锦系统真正花时间的不是模型是数据采集和特征口径对齐模型本身反而是最简单的一环。希望帮到你。本文还有配套的精品资源点击获取