☰
MediaPipe+KNN姿态识别实战:从人体关键点到健身计数
2026/10/5 7:24:35 网站建设 项目流程

简介:基于MediaPipe与KNN分类算法实现的健身计数源码包,面向需要完成引体向上、深蹲、俯卧撑等动作计数的Python开发者;项目不依赖骨骼角度计算,而是通过姿态关键点归一化编码与k-NN分类识别动作完成状态,切换运动只需调整输入选择,通用性强。源码基于Python 3.7和MediaPipe 0.8.10构建,囊括姿态编码、分类、结果平滑、计数、可视化以及训练集提取与校验等模块,各模块职责清晰,便于理解从关键点特征到动作计数的完整流程并二次开发。压缩包共61个文件,以10个Python脚本、6个CSV特征集、3个MP4示例视频为核心,另含XML配置、图片与说明文档,整体18.45MB,可在PyCharm中对照运行。附带的项目说明和示例数据可帮助读者快速上手,适合作为课程设计或健身应用开发参考;目前已有270人学习下载,值得姿态识别方向开发者收藏实践。

1. 用MediaPipe加KNN做健身计数器,到底解决了什么问题

引体向上数到一半忘了是第几个、深蹲深度不够但计数照加、俯卧撑做到后面肩膀借力动作变形——这类问题靠人眼能看出来,靠代码很难写规则。这个标题里的方案,就是用MediaPipe Pose把人体骨架抽成33个关键点,再把关键点换算成关节角度,交给KNN分类算法判断当前动作处在哪个相位,最后用状态机完成计数。它解决的痛点是:不需要训练深度学习模型,不需要GPU,一个普通笔记本就能跑,适合正在做人机交互课设、姿态识别入门,或者想给健身房做训练工具的人。我第一次跑通这条链路时最大的感受是,MediaPipe负责把“人”变成“数据”,KNN负责把“数据”变成“判断”,两者接口干净,调试起来非常顺手。

这套方案的核心逻辑不是“认出你在做引体向上”,而是“判断当前这一帧,你的身体是处于上升位还是下放位”。这两个相位一旦能稳定分类,计数就是一个状态切换的问题。下面按复现路径把原理、代码和坑一一道来。

2. 为什么选MediaPipe和KNN:相位分类比动作识别更靠谱

2.1 MediaPipe Pose把帧变成33个骨架点,计数就有了“数字底座”

MediaPipe Pose是谷歌开源的人体姿态估计方案,输入一帧RGB图像,输出33个关键点的归一化坐标。所谓归一化,就是x和y都除以了图像宽高,取值在0到1之间,z坐标表示关键点到相机的相对深度,量纲和x/y不一样。关键点索引按序号排列,比如11是左肩、12是右肩、13是左肘、14是右肘、15是左腕、16是右腕、23是左髋、24是右髋、25是左膝、26是右膝、27是左踝、28是右踝。做计数不关心眉毛眼睛,核心就躯干和四肢这十几个点。

为什么选它而不是OpenPose或者自训练的关键点模型?因为MediaPipe在CPU上就能跑到接近实时的帧率,代码集成简单,一个mp.solutions.pose就能把模型拉起来,对初学者极其友好。它支持三个模型档位:lite、full、heavy,分别在精度和速度之间做取舍。我一般用full,检测置信度设0.5,跟踪置信度设0.5,这两个阈值组合在多数室内环境下表现稳定。如果你在树莓派上跑,就需要降到lite。

MediaPipe真正厉害的地方不是单帧检测,而是相邻帧之间的跟踪。当人体在画面中连续运动时,它会复用上一帧的关键点位置做跟踪,速度明显快于每帧重新检测。但这也带来一个副作用:当人快速动作导致画面模糊或者肢体互相遮挡时,跟踪结果会短暂跳变,这一点在后面的避坑章节会重点讲。

2.2 KNN分类算法做的是“相位判断”,不是“动作识别”

很多人看到“KNN分类算法”会以为它负责识别引体向上、深蹲、俯卧撑这三个动作类别。实际上在这类健身计数器项目里,更常见的分工是:每个动作单独训练一个KNN二分类器,分类目标是当前帧处于“上升相位”还是“下降相位”,或者叫“高位”和“低位”。动作类型由人工切换或入口参数指定,不交给KNN判断。

这么设计是有道理的。引体向上的高位和低位,在关节角度特征上的差异非常清晰:低位时肘关节接近伸直,高位时肘关节大幅弯曲。深蹲的高低位则体现在膝关节和髋关节角度上。俯卧撑更特殊,肘角变化是主特征,但身体直线度也参与判别。这些特征在“同一个动作内部”做区分,类别边界简单、线性可分度好,KNN这种惰性学习分类器完全够用,不需要上SVM或者神经网络。

KNN的原理一句话说透:把训练样本看成高维空间中的点,预测一个新样本时,找到离它最近的k个点,让这k个点投票决定类别。它有三个特性直接影响工程实践。第一,没有显式训练过程,训练就是存样本,所以新增数据很灵活。第二,特征空间的距离由所有维度共同决定,特征必须做标准化,否则量纲大的维度会完全压过量纲小的维度。第三,k值太小会过拟合,k值太大会把边界抹平,一般从3到7之间试。

2.3 一帧数据走完的完整流水线:关键点→特征→分类→状态机

整个计数程序在一帧内的处理流程是固定的,理解这条流水线比看懂任何一段代码都重要。第一步,从摄像头或视频文件读取帧,转为RGB格式交给MediaPipe。第二步,MediaPipe输出33个关键点,如果画面里没有人,检测结果为空,这一步就要跳过后续处理。第三步,从关键点坐标计算当前动作对应的关节角度,得到一个形状为(5,)或(7,)的特征向量。第四步,用StandardScaler标准化特征,喂给KNN分类器,得到相位预测。第五步,把预测结果放进滑动窗口做多数表决,得到平滑后的相位。第六步,把相位喂给状态机,状态机只在检测到“低位→高位”的切换时把计数器加一。

这条链路里最容易出错的位置不是KNN,而是第三步的特征提取。角度计算依赖三点坐标,任何一个关键点跳变都会让角度发生几十度的突变,直接影响KNN的决策。所以我在实际开发时习惯先把角度特征实时可视化出来,看到波形稳定了再往下做分类,而不是一上来就调KNN参数。特征波形断断续续,KNN调得再花哨也救不回来。

3. 复现这套Python源码:从环境搭建到特征采集

3.1 依赖安装与Python版本选择:一个墙角坑提前说明

这个项目依赖四个核心库:mediapipe、opencv-python、scikit-learn、numpy。安装命令如下。

pip install mediapipe opencv-python scikit-learn numpy

如果你用的是3.12或更高版本的Python,mediapipe大概率没有预编译的wheel包,pip会尝试从源码编译,在普通机器上几乎必失败。我建议直接装Python 3.8到3.10之间的版本,这几个版本都有现成的二进制的wheel包,装完就能用。装完验证一下。

python -c "import mediapipe as mp; print(mp.__version__)"

能输出版本号就说明环境没问题。如果import时报错提示找不到mediapipe.solutions,常见原因是mediapipe版本太旧,和numpy版本冲突。解决办法是先升级numpy再重装mediapipe,或者反过来固定numpy版本到1.24.x再装最新mediapipe。这类兼容性问题属于生态常态,换版本组合是常规手段。

opencv-python和mediapipe的配合也要注意。MediaPipe的Pose.process()要求输入RGB图像,而OpenCV的VideoCapture读出来是BGR格式,必须先用cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)转换。漏了这一步图像看起来只是颜色不对,但关键点检测精度会明显下降,因为模型是在RGB分布上训练的。别问我怎么知道的,第一次跑通项目时检测总是不准,排查了半天才发现是这种低级问题。

3.2 源码项目的常规目录结构:采集、训练、推理三个模块分开

这类健身计数器源码,一套合理的目录结构会把采集、训练、推理分成三个独立模块。通常长下面这样。因为我没有看过你手上zip包的具体内容,只能按这类项目最常见的做法来拆。

project/ ├── collect_data.py # 数据采集:录一段动作视频,生成特征CSV ├── train_knn.py # 模型训练:读取CSV,训练KNN并保存模型 ├── counter.py # 实时计数:读取摄像头,加载模型,跑状态机 ├── features.py # 共同的特征提取函数 ├── models/ │ ├── pullup_knn.pkl # 引体向上模型 │ ├── squat_knn.pkl # 深蹲模型 │ └── pushup_knn.pkl # 俯卧撑模型 └── data/ ├── pullup_up.csv ├── pullup_down.csv └── ...

采集、训练、推理分开的价值在于,你可以单独优化每一环。比如先录一段视频,离线跑特征提取,看特征波形是否正确;确认没问题之后再训练模型;最后才接实时视频流。不然三个环节的错误混在一起,出了bug很难定位。我自己的习惯是先写features.py,因为所有环节都依赖它,把它调试稳定了,后面就是流水线作业。

3.3 最小的关键点特征采集脚本:一帧落到CSV一行

采集脚本是训练KNN的第一步。它的任务是:读入你的动作视频或摄像头画面,用MediaPipe提取关键点,计算特征向量,然后把特征和标签写入CSV。下面是一段最小可用的采集脚本,重点看特征如何组织、标签如何写入。

import cv2 import mediapipe as mp import csv import os mp_pose = mp.solutions.pose pose = mp_pose.Pose( min_detection_confidence=0.5, min_tracking_confidence=0.5, model_complexity=1 # 0=lite, 1=full, 2=heavy ) cap = cv2.VideoCapture(0) csv_path = "data/pullup_down.csv" os.makedirs("data", exist_ok=True) def extract_features(lm): # lm是33个关键点对象列表 # 引体向上特征:左右肘角、左右肩角 def angle(a, b, c): import math v1 = (a.x - b.x, a.y - b.y, a.z - b.z) v2 = (c.x - b.x, c.y - b.y, c.z - b.z) dot = v1[0]*v2[0] + v1[1]*v2[1] + v1[2]*v2[2] n1 = math.sqrt(sum(v**2 for v in v1)) n2 = math.sqrt(sum(v**2 for v in v2)) if n1 == 0 or n2 == 0: return 0.0 cos = max(-1.0, min(1.0, dot / (n1 * n2))) return math.degrees(math.acos(cos)) left_elbow = angle(lm[11], lm[13], lm[15]) # 左肩-左肘-左腕 right_elbow = angle(lm[12], lm[14], lm[16]) # 右肩-右肘-右腕 left_shoulder = angle(lm[23], lm[11], lm[13]) # 左髋-左肩-左肘 right_shoulder = angle(lm[24], lm[12], lm[14]) # 右髋-右肩-右肘 return [left_elbow, right_elbow, left_shoulder, right_shoulder] print("按 s 保存当前帧特征到CSV,按 q 退出") while True: ret, frame = cap.read() if not ret: break rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) result = pose.process(rgb) if result.pose_landmarks: feats = extract_features(result.pose_landmarks.landmark) cv2.putText(frame, "detected", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) cv2.imshow("collect", frame) key = cv2.waitKey(1) & 0xFF if key == ord('s') and result.pose_landmarks: with open(csv_path, 'a', newline='') as f: writer = csv.writer(f) writer.writerow(feats + ["down"]) print("saved:", feats) elif key == ord('q'): break cap.release() cv2.destroyAllWindows()

这段代码的逻辑不复杂:每帧调用MediaPipe检测,检测到人体就计算四个肘肩角度,按s键把特征和标签追加写入CSV。这里我把标签硬编码为“down”,因为这段代码的目标是采集引体向上低位样本。要采高位样本,把标签改成“up”再跑一遍就行。

代码里有三个参数值得说。model_complexity=1选择完整模型,精度和速度的折中;如果你机器性能弱,设为0可以减少一半以上的耗时。min_detection_confidence=0.5是检测阶段的最低置信度,调高到0.7会让误检变少,但也会让弱光环境下的漏检变多。按s保存而不是自动保存,是为了让你能在动作的最低点或者最高点精确暂停,只保存最典型的那几帧,避免过渡帧污染训练数据。

实际采集时有个重要原则:标签必须是“明确的一帧”。不要在动作中途按保存键,那种半上不下的过渡帧会让KNN的类别边界模糊。我一般每个动作的每个相位采集200到300帧,也就是从视频里挑出几十个稳定帧,数据量不需要太大。

4. 核心代码拆解:KNN训练参数与计数状态机的配合

4.1 特征向量怎么定义:三种动作分别取哪几个关节角

特征工程是整个项目的灵魂。同一个MediaPipe关键点数据,特征选得好,KNN分类精度轻松上95%;特征选得糙,调参调到头也过不了80%。三个动作的特征选取逻辑完全不同,我按经验整理了一张表。

动作特征维度具体特征关键点索引组合
引体向上4维左肘角、右肘角、左肩角、右肩角(11,13,15)、(12,14,16)、(23,11,13)、(24,12,14)
深蹲4维左膝角、右膝角、左髋角、右髋角(23,25,27)、(24,26,28)、(11,23,25)、(12,24,26)
俯卧撑5维左肘角、右肘角、左肩髋踝直线度、右肩髋踝直线度、肩髋连线倾角(11,13,15)、(12,14,16)、(11,23,27)、(12,24,28)、(11,23)两点向量与垂直方向夹角

为什么引体向上取肩角和肘角而不取腕角?因为引体向上握杠时手腕角度基本不变,肘角和肩角才是驱动相位变化的核心。深蹲取膝角和髋角,道理一样:下蹲时膝盖弯曲、髋部折叠,这两个角度从接近180度一路缩到90度以下,变化范围大且稳定。

俯卧撑最特殊。它的相位区分主要靠肘角,但“身体是否成一条直线”是判别动作是否标准的关键。肩、髋、踝三点连线的角度越接近180度,说明身体越直。如果塌腰或者撅屁股,这个角度会明显变小。我额外加了肩髋连线倾角,因为它能区分你是“标准俯卧撑”还是“跪姿俯卧撑”,后者躯干倾斜角度完全不同。特征维度宁缺毋滥,维度太多反而让KNN在小样本下过拟合。

4.2 KNN训练代码与参数讲解:k值、距离权重与标准化

采集完特征CSV之后,训练代码就非常短了。但短代码里藏着两个关键操作:标准化和k值选择。

import pandas as pd import joblib from sklearn.neighbors import KNeighborsClassifier from sklearn.preprocessing import StandardScaler df = pd.read_csv("data/pullup.csv") # 最后一列是标签列 X = df.iloc[:, :-1].values.astype(float) y = df.iloc[:, -1].values # 标准化:KNN基于距离,特征量纲不一致会把距离计算带偏 scaler = StandardScaler() X_scaled = scaler.fit_transform(X) # k=5,距离加权投票,距离越近的样本话语权越大 knn = KNeighborsClassifier( n_neighbors=5, weights='distance', metric='euclidean' ) knn.fit(X_scaled, y) # 保存模型和scaler,推理时两者必须一起加载 joblib.dump(knn, "models/pullup_knn.pkl") joblib.dump(scaler, "models/pullup_scaler.pkl") print("训练完成,样本数:", len(X), "类别:", knn.classes_)

这五行代码里有三个参数决定成败。StandardScaler是必须的,它把每个特征列的均值变成0、方差变成1。不加这一步,肘角特征取值范围0到180,肩角也是0到180,两个量纲一致还好,但如果你后面加入坐标距离类特征,取值范围只有0到0.5,就会被角度特征完全淹没,等于白加。n_neighbors=5是最常用的起点值,样本量300左右时5是个安全选择。太小的k比如1,会把噪声样本直接变成决策边界;太大的k比如20,会把高低位的边界抹平。weights='distance'意味着距离新的测试点更近的训练样本拥有更大的投票权重,这比等权投票更抗噪声。

训练完成后一定要打印看一眼classes_数组,确认类别标签是['down', 'up']而不是只学到一个类别。这种“一点通”的翻车经常发生:采集数据时按保存键太早,所有样本都落在了同一相位,模型就变成了只会输出一个类别的废物。

训练集划分也不该省。用train_test_split切出20%做验证,看分类报告里的F1分数而不是只看准确率。相位分类如果两类样本数量严重不均衡,比如down有300个、up只有30个,准确率会虚高,F1才能暴露问题。

4.3 计数状态机防止重复计数的实现

模型训练好之后的实时计数逻辑,我用下面这段来展示。重点不在KNN预测,而在状态机的切换条件。

import cv2 import mediapipe as mp import numpy as np import joblib knn = joblib.load("models/pullup_knn.pkl") scaler = joblib.load("models/pullup_scaler.pkl") mp_pose = mp.solutions.pose pose = mp_pose.Pose(min_detection_confidence=0.5, min_tracking_confidence=0.5) cap = cv2.VideoCapture(0) counter = 0 state = "none" # 状态变量:none / up / down history = [] # 滑动窗口最多存5帧预测结果 def extract_features(lm): # 和采集脚本完全一致的特征提取逻辑 ... while True: ret, frame = cap.read() if not ret: break rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) result = pose.process(rgb) if not result.pose_landmarks: cv2.imshow("counter", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break continue feats = extract_features(result.pose_landmarks.landmark) feats_scaled = scaler.transform([feats]) pred = knn.predict(feats_scaled)[0] # 5帧多数表决,过滤单帧抖动 history.append(pred) if len(history) > 5: history.pop(0) pred_smooth = max(set(history), key=history.count) # 状态机:只有 down -> up 的切换才计数 if state == "none": state = pred_smooth elif state == "up" and pred_smooth == "down": state = "down" elif state == "down" and pred_smooth == "up": state = "up" counter += 1 print("count:", counter) cv2.putText(frame, f"count: {counter} state: {state}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) cv2.imshow("counter", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()

状态机是这个项目的核心工程逻辑。它的本质是一个只能按“up到down、down到up”顺序迁移的有限状态机。为什么必须这样?因为KNN对每一帧独立分类,在动作过渡到一半时,相位预测会在up和down之间来回切换。如果没有状态机,你可能在同一个引体向上动作里看到3到4次“up→down→up”的抖动,计数器就加了三四次。

状态机的关键设计是:up→down只切换不计数,down→up才计数。这意味着一个完整动作必须经过“从高到低、再从低到高”两个阶段,任何一次重复预测都不会导致重复计数。我第一次自己写的时候偷懒,用“检测到up就计数”,结果半程动作都被算了两次,后来老老实实补上状态机才稳定。

滑动窗口的5帧多数表决和状态机是双保险。多数表决能过滤掉KNN的单帧误判,状态机保证计数逻辑的严谨。你会发现窗口大小设5是个甜点值,设3在快速动作时过滤能力不够,设7会让计数反应变迟钝,等你看到计数跳变时,实际动作已经过去两三秒了。

5. 复现翻车避坑:骨架抖动、镜像翻转与计数错乱的排查

5.1 画面里没有人或半遮挡时骨架点全None,程序直接报错

现象是:程序跑着跑着突然抛异常,报错指向result.pose_landmarks.landmark那一行,提示NoneType没有landmark属性。

原因是:MediaPipe检测不到人体时,pose_landmarks字段是None,程序却照常尝试提取特征。站在摄像头前时没事,人一离开画面或者做动作时身体被扶手挡住大半,就会触发。

解决:处理每一帧前必须做空值判断。上面计数代码里我已经写了if not result.pose_landmarks: continue,这个判断必须放在特征提取之前。采集脚本里也要同样处理,否则录数据时人偶尔出画,程序就崩了。顺便说一句,即使检测到人体,部分关键点也可能置信度极低导致坐标为0,如果特征里出现突然的角度跳变,大概率就是这种情况。

5.2 背对镜头和左右镜像让MediaPipe标点翻转,相位分类乱掉

现象是:正面朝摄像头时计数正常,转身背对摄像头之后,KNN预测结果开始乱跳,引体向上下降和上升完全反了。

原因是:MediaPipe默认按“镜面视角”输出关键点,也就是画面中你看到的人的左臂对应关键点索引11,但当你背对摄像头时,身体映射关系会让人困惑——实际物理世界中的左臂在画面里变成了右边。MediaPipe在这个场景下的点标记可能会出现左右翻转,或者检测到的左右肩、左右肘的坐标关系反转。

解决:一是训练和推理时保持同一个朝向,不要正面朝向采集训练数据、背面朝向做推理。二是如果需要支持背面,训练数据必须也包含背面样本,让KNN学到镜像后的特征分布。实际上最省事的做法是全程正面朝向摄像头,引体向上面对杠、深蹲面对摄像头、俯卧撑头朝摄像头方向,这样关键点的左右关系永远一致。这个坑最隐蔽,因为它不报错,只是计数结果莫名其妙地错。

5.3 特征量纲不统一,KNN距离被肘关节角主导

现象是:模型训练完验证准确率95%,一接实时视频,预测结果在up和down之间疯狂横跳,而且对动作幅度的大小特别敏感。

原因是:特征里肘角变化范围0到180度,而俯卧撑的肩髋踝直线度特征取值范围可能只有150到180度,变化范围不足30度。KNN用欧氏距离衡量样本相似度时,角度绝对值大的特征天然拥有更大的“距离话语权”,小范围变化的特征被完全压制。

解决:用StandardScaler对所有特征做标准化,让每个维度均值0、方差1。上面训练代码里已经包含了这一步,但很多人容易在推理时漏掉——只加载KNN模型而忘记加载scaler,推理前不做标准化就喂给模型。scaler和模型必须成对保存成对加载,这是血的教训。另外,如果你的特征里混合了角度值和归一化距离值,标准化就更重要了。

5.4 KNN预测结果在临界帧反复横跳,计数重复加一

现象是:明明只做了12个引体向上,计数器最后显示15个。回看打印的预测状态日志,发现某个动作中出现了多次up到down的切换。

原因是:KNN对连续帧的预测是独立事件,动作在临界位置时,关节角度特征恰好落在两个类别边界附近,相邻几帧预测结果不连续。比如下放到最低点时,肘角可能有时判成up,有时判成down,状态机就被反复触发。

解决:靠状态机本身只能保证“一个完整周期计一次”,不能阻止边界抖动带来的虚切换。必须加滑动窗口多数表决,让当前帧的预测结果参考前四帧的投票。窗口大小的选择看动作速度,慢动作用7帧,正常速度用5帧,快动作用3帧。同时还可以给状态机加“最小停留帧数”条件:只有预测结果稳定保持某个相位超过3帧,才切换状态。这个条件加上之后,重复计数的问题基本根除。

5.5 训练样本太少且左右不均衡,模型泛化不了

现象是:自己录的数据自己测很准,换个场地、换个人测就开始漏。

原因是:KNN是记忆型分类器,它的泛化能力完全取决于训练样本覆盖了多少种情况。如果你只在固定光线、固定背景、固定身高下录了100帧,KNN记住的就是那个特定环境下的特征分布。新手最容易犯的错是“一个动作录一段视频就完事”,样本没有覆盖动作幅度的连续变化。

解决:采集数据时要刻意覆盖动作的各种幅度。引体向上要录半程的和全程的、深蹲要录浅蹲和全蹲、不同的人左右手发力习惯不同也要分别录。每种相位的样本不少于150帧,两类样本数量差距不要超过2倍。如果左右侧数据不均衡,比如右侧角度特征参与训练多、左侧参与少,到了实际推理时左侧动作的计数准确率就会明显更差。

6. 让计数器更稳的后续手段:增强、缓冲与半帧验证

6.1 训练数据增强的思路

KNN没有训练过程,所以“数据增强”不是对数据集做变换迭代,而是在采集阶段就制造多样性。做法很简单:录数据时不要只在一个固定位置录,前后左右各挪半米,把光线条件也换一遍。另一个有效手段是“角度微扰”,把已采集特征向量的每个维度随机加减2到5度,生成大量合成样本。这样操作的前提是原始数据质量足够高,不然就是把噪声放大。

我用这个思路给俯卧撑数据做过一次增强:原始样本200帧,按每维加减0到3度的均匀扰动扩充到800帧,KNN在验证集上的F1从0.86升到了0.92。增强之后记得重新标准化,因为均值可能变了。

6.2 时序平滑的参数建议

时序平滑除了滑动窗口多数表决,还可以用指数移动平均来处理KNN的类别概率输出。KNeighborsClassifier.predict_proba()能输出每个类别的概率,把这个概率做EMA,超过0.6才判定为当前相位。这个阈值比多数表决更细腻,适合动作速度较慢的深蹲和引体向上。快速动作比如俯卧撑,EMA延迟偏大,建议只用窗口表决。

6.3 离线验证与误计数的检查方法

实时计数跑得再欢,最终还是要离线验证才敢信。我的习惯是:录一段包含10个标准动作的视频,记下真实次数,然后跑一遍离线推理程序,比较程序计数和真实计数的差值。如果差值为0,说明状态机和KNN配合正常;如果差值在1到2之间,先看是漏计还是重复计,漏计说明状态机某次没完成完整的down到up切换,重复计说明边界抖动没过滤干净,据此针对性调整。

这个方案的下一步方向因人而异。可以往动作质量评估走,给肘角阈值加规则,判断动作是否标准;也可以把KNN换成一个5层的MLP,用同样的特征向量做分类,精度会上一个台阶。

我做这类项目养成的一个习惯是:每调一次参数,就把当时的模型精度和踩到的坑记在项目说明文档末尾。每次翻车的记录,下次都能省掉大量排查时间。希望这份基于MediaPipe和KNN的健身计数器拆解能帮到你,从搭建环境到跑通计数,最顺的路就是先采集数据,再训练模型,最后调状态机,一步都不要跳。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询