拓冰建站拓冰建站
首页 / 资讯中心 / 正文

基于MediaPipe与KNN的健身动作计数实现

简介基于mediapipe与KNN分类算法的通用健身计数项目支持引体向上、深蹲、俯卧撑等动作计数。与依赖各运动骨架角度阈值的传统方案不同项目对mediapipe提取的人体关键点做归一化编码后交由KNN分类器判断动作完成状态因此只需在启动时选择输入视频或摄像头以及对应运动类型核心计数代码基本无需改动适合想快速搭建姿态识别计数应用或学习KNN动作分类的Python开发者参考。压缩包共61个文件主要包含10个py源码模块、6个csv训练特征集、3个mp4示例视频、xml配置文件及README说明等整体大小18.45MB模块覆盖关键点编码、KNN姿态分类、结果平滑、运动计数、可视化、训练集提取与检验校正等多个环节可直接运行视频处理入口验证效果目录结构清晰。已有270人学习下载。从训练集生成到计数输出均有对应模块和示例样本能完整还原机器学习动作识别的落地流程适合计算机视觉与运动分析方向的入门实践。1. 引体向上数到一半就乱健身计数器真正要解决的是“判不到位”引体向上做到第12个自己数的和同伴数的差了3个——不是谁记错而是半程、借力、没下到底这些情况都被算成了“一次”。做一套基于MediaPipe和KNN分类算法的健身计数器正是解决这类“数不准、判不到位”的诉求摄像头采集人体关键点KNN把每一帧姿态分成高位/低位两个阶段再用状态机决定什么时候计一次。它不识别“动作”它分类“姿态”这个思路适合没有GPU的学生、想给个人训练做打点工具的开发者也适合拿它当课程设计的入门者——代码量可控效果直观坑都踩得明白。2. 用MediaPipe Pose提取关键点坐标会骗人角度才是稳定特征2.1 为什么选MediaPipe Pose而不是目标检测或OpenPose先想清楚一个问题计数器需要什么输入不是“人”的边框而是人的关节位置。目标检测给你一个矩形框框里看不出手臂是否伸直、膝盖是否弯曲OpenPose能出关键点但模型体积大、CPU上跑不动实时MediaPipe Pose在模型大小、推理速度和关键点数量之间取了平衡一套模型输出33个人体关键点包含鼻子、肩膀、手肘、手腕、髋、膝、踝这些计数必须的节点。这套方案里MediaPipe只是“眼睛”它把每一帧画面变成一组坐标。常见做法是把mp.solutions.pose在视频流模式下运行关键点是landmark列表每个节点带x、y、z和visibility四个值。x、y是归一化到 0~1 的坐标z是深度估计visibility标识这个点被遮挡的概率值越接近1越可信。如果你看过MediaPipe Model Maker的文档会发现它也能做自定义姿态分类。但在这个项目里不必要KNN处理10~20维的角度特征分类质量足够还省去了训练模型的时间。2.2 摄像头采集与姿态检测的最小骨架最小可运行的程序不长直接看代码import cv2 import mediapipe as mp mp_pose mp.solutions.pose pose mp_pose.Pose( static_image_modeFalse, # 视频流模式会启用帧间跟踪 model_complexity1, # 0轻量 / 1平衡 / 2高精度CPU选1 min_detection_confidence0.5, # 首次检测到人的最低置信度 min_tracking_confidence0.5, # 帧间跟踪的最低置信度 ) cap cv2.VideoCapture(0) while cap.isOpened(): ok, frame cap.read() if not ok: break rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results pose.process(rgb) if results.pose_landmarks: lm results.pose_landmarks.landmark # 典型节点编号 # 0鼻子 11/12肩膀 13/14手肘 15/16手腕 # 23/24髋 25/26膝盖 27/28脚踝 nose_y lm[0].y # 之后所有特征提取都在这里扩展 cap.release() cv2.destroyAllWindows()这段代码里static_image_modeFalse是性能关键——它让MediaPipe在连续帧之间复用跟踪结果CPU上单帧推理可以从几十毫秒降到十几毫秒级别。model_complexity1是平衡档0在动作幅度大时容易丢点2在笔记本CPU上会掉到10帧以下。results.pose_landmarks为空就说明这一帧没检测到人这个分支必须处理否则.landmark访问会直接抛异常。2.3 特征设计不用坐标用三点夹角原始坐标不能直接喂给KNN。原因很简单人离摄像头近髋膝踝的坐标整体变大人站得偏左所有x坐标偏移。KNN靠距离判断相似坐标一旦受位置影响同一个人同一个动作会被算成两个完全不同的样本。解决办法是把坐标转成角度。三个点确定一个夹角角度不受人物在画面中的位置、缩放影响。计算夹角用向量点积import math def calc_angle(a, b, c): 计算点abc形成的夹角b为顶点返回角度制数值 v1 (a[0] - b[0], a[1] - b[1]) v2 (c[0] - b[0], c[1] - b[1]) dot v1[0] * v2[0] v1[1] * v2[1] n1 math.hypot(v1[0], v1[1]) n2 math.hypot(v2[0], v2[1]) if n1 0 or n2 0: return 0.0 cos_val max(-1.0, min(1.0, dot / (n1 * n2))) return math.degrees(math.acos(cos_val))有了这个函数特征向量就按固定顺序拼出来def extract_features(lm): # 取关键点坐标顺序不能乱训练和推理必须一致 left_shoulder (lm[11].x, lm[11].y) left_elbow (lm[13].x, lm[13].y) left_wrist (lm[15].x, lm[15].y) right_shoulder (lm[12].x, lm[12].y) right_elbow (lm[14].x, lm[14].y) right_wrist (lm[16].x, lm[16].y) left_hip (lm[23].x, lm[23].y) left_knee (lm[25].x, lm[25].y) left_ankle (lm[27].x, lm[27].y) right_hip (lm[24].x, lm[24].y) right_knee (lm[26].x, lm[26].y) right_ankle (lm[28].x, lm[28].y) return [ calc_angle(left_shoulder, left_elbow, left_wrist), calc_angle(right_shoulder, right_elbow, right_wrist), calc_angle(left_hip, left_knee, left_ankle), calc_angle(right_hip, right_knee, right_ankle), calc_angle(left_shoulder, left_hip, left_knee), calc_angle(right_shoulder, right_hip, right_knee), calc_angle(left_hip, left_shoulder, left_elbow), calc_angle(right_hip, right_shoulder, right_elbow), ]引体向上、深蹲、俯卧撑三个动作的形态差异主要体现在这几个角度上肘关节角区分手臂弯曲程度膝关节角区分深蹲深度肩髋连线与竖直方向夹角区分躯干是否直立。特征是8维就够用别贪多多一个噪声特征KNN的边界就乱一分。3. KNN分类器样本量不大时它是比深度学习更先落地的选择3.1 这个任务为什么用KNN而不用LSTM或CNNKNN在量化分析的行情状态识别、室内定位的WiFi指纹匹配里也常被用作基线算法本质思路一致找空间中最近的K个已知样本投票决定新样本属于哪一类。应用到健身计数关键洞察是——每一帧的姿态分类是独立的不需要上下文记忆。计数器缺的不是“时间序列理解”而是“当前帧处于动作的哪个阶段”。KNN天然适合这种静态分类。三个现实理由第一训练成本极低不需要GPU几十秒就能 fit 完第二可解释性强分类错了你能打印出最近的几个邻居看到底是哪条样本拉偏了第三需求决定复杂度一个二分类任务犯不上上LSTM。它也有边界——如果动作极快导致运动模糊或者摄像头前有多人同时运动KNN就力不从心了那种场景需要换检测跟踪架构。3.2 采集训练数据边做动作边按键盘存帧KNN质量的80%取决于训练样本。常见做法是写一个采集脚本每按一次数字键就把当前帧的特征向量连同一个标签写进CSV。标签建议直接用状态阶段而不是动作名称比如引体向上标0代表下垂位、1代表下巴过杠位深蹲标0站直、1蹲下俯卧撑标0撑起、1下放。import csv import cv2 import mediapipe as mp # ... 初始化 pose、cap、calc_angle、extract_features 同上 ... csv_file open(squat_data.csv, a, newline) writer csv.writer(csv_file) while cap.isOpened(): ok, frame cap.read() if not ok: break rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results pose.process(rgb) if results.pose_landmarks: feats extract_features(results.pose_landmarks.landmark) key cv2.waitKey(1) 0xFF if key ord(1): writer.writerow(feats [0]) # 站直 print(saved class 0) elif key ord(2): writer.writerow(feats [1]) # 蹲下 print(saved class 1) cap.release() csv_file.close()采集时注意三点一是动作要慢让每一帧对应到确定的姿态快速蹲起会让中间过渡帧的标签定义模糊二是类间要录均匀每个类别至少300帧数据量少时KNN的分类边界会被单条离群样本带偏三是换个位置和角度各录一遍不然模型只认你坐的位置。CSV表头可以不加训练时靠行号位置对应。3.3 参数怎么定K值、距离度量、特征标准化KNN的默认参数能跑但不调一下会出现两类典型问题K太小分类结果帧间抖动得像抽风K太大动作做到一半就被归到另一类。实操中我一般这样配from sklearn.neighbors import KNeighborsClassifier from sklearn.preprocessing import StandardScaler import joblib # X_train: 二维数组每行一个样本的特征向量 # y_train: 对应的标签列表0 或 1 scaler StandardScaler() X_scaled scaler.fit_transform(X_train) model KNeighborsClassifier( n_neighbors5, # K值取奇数5是平衡点 metriceuclidean, # 欧氏距离角度特征下表现稳定 weightsdistance, # 近邻权重更大降低远处样本干扰 ) model.fit(X_scaled, y_train) joblib.dump(model, counter_model.pkl) joblib.dump(scaler, scaler.pkl)n_neighbors5不是拍脑袋定的3太敏感单人单动作场景下任一两帧的噪声都可能改变投票结果7以上又会把“蹲得很浅”和“蹲到底”的边界磨平。weightsdistance是容易忽略却很有用的一行——距离近的邻居更有资格说话能让分类结果稳定不少。标准化必须做角度虽然天然在同一个量纲但不同角度数值的方差差异很大不缩放时KNN的距离计算会被方差最大的那个角度主导。模型和scaler都要保存推理阶段先scaler.transform再predict少一个都会让结果偏离训练时的分布。4. 计数逻辑KNN回答“现在是什么姿态”状态机回答“做完几次”4.1 状态机为什么能防止乱计数KNN每帧输出一个类别但类别不等于次数。如果直接数“从0变成1的次数”动作在半途抖动、身体轻微晃动、检测丢帧都会造成同一动作被反复计数。状态机的思路是把每种姿态当成一个稳定状态只有当状态从低位切换到高位且新状态持续了N帧才认为完成一次。class RepCounter: def __init__(self, min_frames5): self.state None # 当前状态: 0低位 / 1高位 self.candidate None # 候选状态 self.candidate_frames 0 # 候选状态持续帧数 self.reps 0 def update(self, cls): if cls self.state: # 状态没变清空候选计数 self.candidate_frames 0 return self.reps # 状态变了进入候选期 if cls self.candidate: self.candidate_frames 1 else: self.candidate cls self.candidate_frames 1 # 候选状态持续足够帧数确认切换 if self.candidate_frames self.min_frames: if self.state 0 and self.candidate 1: self.reps 1 self.state self.candidate self.candidate_frames 0 return self.repsmin_frames5的实际意义是假设摄像头30帧每秒5帧约等于166毫秒低于这个持续时间的类别跳变会被当作噪声丢弃。这个参数不要超过10否则快速动作会被吞掉——一个爆发式引体向上从下垂到过杠可能只有0.3秒9帧刚好卡在边界上。4.2 引体向上的关键识别“借力摆荡”引体向上计数最常见的争议是摆荡借力拉上去算不算。判断标准有两个维度。下巴过杠看鼻子关键点的y坐标是否高于手肘关键点或高于你设定的参考线这是“有没有拉到位”身体是否笔直看肩-髋-踝的夹角引体向上标准姿态躯干接近一条直线摆荡时躯干会前倾后仰这个角度会明显偏离180度。深蹲和俯卧撑的姿势可以仿照同样的逻辑用一个角度阈值辅助KNN结果KNN判低位同时髋膝踝夹角小于100度才真正进入“蹲下”状态KNN判高位但膝盖还没伸直就继续留在蹲下状态。这个双保险在动作不到位时尤其有效。4.3 三个动作的参数配置对照动作低位判定特征高位判定特征建议阈值防作弊规则引体向上下巴低于手肘、肘角150°下巴高于手肘、肘角90°躯干角偏离180°20°判摆荡摆荡帧不推进状态深蹲膝角100°髋角明显折叠膝角160°髋关节接近站直膝角90°为中间线膝盖内扣时短暂不更新状态俯卧撑肘角90°躯干保持直线肘角150°躯干仍保持直线躯干角允许±10°偏差塌腰或撅臀暂停计数表格里的阈值不是固定死的每个人的柔韧性和动作标准不同。落地的做法是先采集几十帧自己“标准动作”的数据打印出膝角和肘角的分布区间取区间中值作为阈值。我在实测中发现深蹲膝角100度是个合理的分界线低于这个角度膝盖压力明显增大也说明蹲得够深计数结果和教练人工判定基本一致。5. 避坑清单从环境安装到计数虚报五个最常见的坑5.1 pip install mediapipe装不上卡在依赖编译上现象pip install mediapipe报错或者长时间卡住有的还会在装完opencv后出现cv2导入版本冲突。原因绝大多数是Python版本太新。MediaPipe对Python版本挑剔很多新手照着网上教程装了最新的Python 3.12但库的预编译轮子只覆盖到3.8~3.11装不上或者装上了也是从源码编译慢且容易失败。解决换到3.9或3.10用虚拟环境隔离。安装时指定镜像源比如清华源能避免因为官方源下载超时导致的中断。装完先跑一句import mediapipe as mp; print(mp.__version__)能打印版本号再继续别急着写业务代码。5.2 人离得忽远忽近分类结果跟着乱跳现象人往摄像头前走一步明明没做动作KNN分类却在高低位之间来回切换。原因特征向量里混入了坐标值。坐标受画面中人物大小和位置影响人一靠近所有关键点坐标整体变化距离计算被坐标分量主导角度特征反而被淹没。解决删掉特征里的原始坐标只保留上一章说的角度特征。血泪经验是特征向量里每多一个坐标分量KNN的稳定性就差一截。如果一定要加平移不变特征可以加“左右肩连线与水平线的夹角”这类相对量不要加绝对x、y。5.3 摄像头镜像导致左右关节弄反现象右手举起来程序判定左手在上动作计数完全乱套。原因摄像头对着自己时画面相当于镜像。MediaPipe返回的landmark坐标基于图像坐标不是真实空间的左右——图像里你的左边其实是你身体的右边。解决别想着在代码里做复杂的坐标变换。直接看检测结果在画面上是否自然把你的右手举起来画面里右肩关键点编号12如果跟着动那说明模型定位没有错你只需要习惯“代码里叫 right 的是图像右侧”。真正需要翻转的是显示层用cv2.flip(frame, 1)翻转画面给你看关键点坐标不反转因为特征用的是角度左右对称动作算出来的角度相同不影响分类。5.4 训练样本不均衡模型永远只判一类现象深蹲练了20个计数器只加了2个检查发现KNN几乎把所有帧都判成“蹲下”或“站直”中的一类。原因采集时按下键的节奏不均一个类别录了600帧另一类只录了100帧。KNN的投票基于近邻不是基于概率但样本密度会让决策边界偏移——某一类的样本越密它占据的“领地主权”范围越大。解决采集时监控每类样本数量差距超过50%就补录少的那一类。真到了训练时才发现不均衡可以临时把n_neighbors调小到3能缓解但不根治。最靠谱的办法是重新录别偷懒。5.5 动作只做一半也被计数虚报次数现象深蹲只蹲到一半系统就计了1次引体向上下巴还没过杠系统也提示完成。原因这不是KNN分类错而是状态机的切换条件太松。min_frames太小或者低位和高位的特征差距在你这套动作里本来就模糊——蹲一半时膝关节角度在130度左右既像从高位开始下蹲的过渡帧又像浅蹲的结束帧KNN在两者之间摇摆。解决两个手段一起用。把min_frames从5提到8过滤短促的过渡帧再给低位状态加一个“深度确认”阈值比如膝角必须小于100度才确认蹲下。阈值用前面表格里的参考值配合自己录的样本微调。玄学的地方在于阈值卡得太死会漏计真实动作卡得太松会虚报最少要调三轮视频回放才能找到平衡点。6. 把计数器做出可信度离线验证精度与扩展新动作6.1 离线验证别盯着实时画面凭感觉判断实时跑起来感觉“挺准”不够要量化。录一段你已经知道次数的视频离线跑分类和计数把实际次数和模型输出的次数做对比。这样能算出精确率和召回率精确率是“模型报的次数里真正完成的有多少”召回率是“真做完的里面模型报出了多少”。# 假设你手动标好了真值 ground_truth模型输出 model_reps precision correct_reps / model_reps if model_reps else 0 recall correct_reps / total_actual_reps if total_actual_reps else 0 print(fprecision{precision:.2f}, recall{recall:.2f})验证时只用一段没参与过调参的视频否则结果偏向乐观。测试至少测三段正常速度一组、疲劳后动作变形一组、故意借力一组。假如精确率低于0.8优先检查低位阈值而不是K参数召回率低于0.8优先加训练样本和调低min_frames。6.2 扩展新动作的套路想加个卷腹计数器不用改架构按四步走定义两个姿态阶段躺下/起来采集两类的特征帧并打标签重新训练KNN配置新的状态机。特征选择时先问自己这个动作的两个阶段在关节角度上差别明显吗卷腹的差别主要体现在肩关节和髋关节折叠角度加这两个角度进特征向量如果差别不明显——比如滑步训练——就得考虑加更多关键点或换算法这已经是另一个项目了。我第一次把深蹲计数器跑通之后兴冲冲地做了100个深蹲结果右膝内扣时计数器漏判了十几次。当时第一反应是调K值折腾半天没用最后发现是特征里没加入膝盖横向偏移信息。这个坑让我记住一个道理特征集设计永远排在调参前面。计数器这个项目值不值得投入就看你能不能把姿态差异转成清晰的角度特征——这一步做好了后面的代码只是把你已经定义出的规律准确执行一遍。希望帮到你。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门