news 2026/9/16 6:36:28

基于BlazePose和KNN的轻量级健身动作计数方法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于BlazePose和KNN的轻量级健身动作计数方法

简介:基于BlazePose与KNN算法实现的人体姿态健身计数项目,Python源码配套项目说明,面向计算机视觉与AI健身方向的学习者、开发者以及相关课程设计场景。项目借助MediaPipe进行人体关键点提取,并利用KNN分类器完成俯卧撑、深蹲、引体向上、仰卧起坐等常见健身动作的识别与自动计数,同时提供了自定义动作的训练流程,便于扩展新动作。压缩包共26个文件,包含10个Python脚本、8个CSV关键点数据文件、5张示例图像,另附字体、图标及DOCX操作手册,整体大小3.9MB。代码模块覆盖姿态嵌入、训练集构建、实时视频计数、结果平滑与可视化等关键环节,有助于快速理解人体姿态识别与分类的完整链路,也可作为健身应用算法原型的搭建参考。项目结构清晰,文档对训练新动作的流程说明细致,适合用来完成课程项目或竞赛实践。目前已有221人学习,具备较好的参考价值。

1. BlazePose+KNN在健身计数里的定位:为什么姿态估计搭配KNN够用

BlazePose把任意视频帧压成33个关键点坐标,KNN再在这套坐标空间里对动作相位做近邻投票,看第一眼这是个相当“复古”的组合——市面上讲人体姿态计数的文章,要么堆LSTM,要么上ST-GCN,连OpenPose都嫌老。但健身计数这个任务的数据形态恰好把问题变了:一个课程设计或实际原型,有效训练数据往往只有几百到几千个标注帧,在这个量级上端到端时空模型几乎必然过拟合,而KNN作为惰性学习算法,在低维结构化输入上反而没有任何拟合压力。BlazePose负责把高维图像里的尺度、光照和背景干扰全部剥离掉,输出一个只有33个点的骨架;KNN负责在这个干净的欧氏空间里做最近邻检索,两件事各司其职,模型小、可解释、能在纯CPU的摄像头视频流上跑实时。这套方案锁定的落地对象是俯卧撑、深蹲、引体向上、卷腹这类相位清晰的动作计次,适合做课程设计、毕业设计,也适合快速验证一个健身App原型。它的边界同样明确:对动作幅度微小、相位模糊的复合动作效果有限,这类场景才需要引入时间序列模型,但特征工程和计数状态机一样能复用。

2. BlazePose关键点提取与角度特征设计

2.1 BlazePose的33个关键点与坐标语义

MediaPipe中的BlazePose姿态估计模型输出33个关键点,每个关键点包含归一化的x、y坐标、以臀部中心为原点的相对深度z,以及0到1之间的可见性visibility值。和OpenPose的COCO 18点相比,BlazePose多了面部关键点和更密的躯干/手部点,但对健身计数来说真正有价值的通常只有肩、肘、腕、髋、膝、踝这几处,共14到18个点。数据构建的顺序是:视频帧 → 关键点坐标 → 几何特征 → 窗口向量 → 标签,这个链条里最容易出错的一步就是“直接把坐标当特征”。

直接拿原始坐标喂给KNN有两个问题。第一是尺度问题:人离摄像头近一点,肩和髋在画面里的像素距离就差出几倍,欧氏距离直接被拉伸;第二是体型差异:1.8米和1.6米的两个人,就算动作完全一样,关节坐标向量的差异也远大于“深蹲”和“站立”这两个真实类别的类间距离。原始坐标必须先做几何变换,否则KNN会拿身高当动作特征。

2.2 从原始坐标到角度特征的计算

角度特征能同时绕开尺度、平移和部分旋转干扰,而且物理含义直观。以下代码从MediaPipe的landmark列表里提取6个关节角,单位是度。

import numpy as np def calc_angle(a, b, c): """根据三个关键点坐标计算中间点的角度,返回度数""" ba = a - b bc = c - b # 点积 / 模长 = cosine,加1e-9防止除零 cosine = np.dot(ba, bc) / (np.linalg.norm(ba) * np.linalg.norm(bc) + 1e-9) cosine = np.clip(cosine, -1.0, 1.0) return np.degrees(np.arccos(cosine)) def extract_joint_angles(landmarks): """从 BlazePose 的 33 个关键点中提取 6 个关节角度 landmarks: mediapipe.framework.formats.landmark_pb2.NormalizedLandmarkList """ lm = np.array([[p.x, p.y, p.z] for p in landmarks]) # (33, 3) left_elbow = calc_angle(lm[11], lm[13], lm[15]) # 左肩-左肘-左腕 right_elbow = calc_angle(lm[12], lm[14], lm[16]) # 右肩-右肘-右腕 left_knee = calc_angle(lm[23], lm[25], lm[27]) # 左髋-左膝-左踝 right_knee = calc_angle(lm[24], lm[26], lm[28]) # 右髋-右膝-右踝 left_shoulder = calc_angle(lm[13], lm[11], lm[23]) right_shoulder = calc_angle(lm[14], lm[12], lm[24]) return np.array([left_elbow, right_elbow, left_knee, right_knee, left_shoulder, right_shoulder])

代码里的11、13、15是BlazePose的固定索引:肩11/12、肘13/14、腕15/16、髋23/24、膝25/26、踝27/28。calc_angle先算两个向量ba和bc,再用点积反余弦得角度,加1e-9是防止两个关键点重叠时模长为0。第5、6个角度——肘-肩-髋,用来区分躯干前倾角,对硬拉和深蹲这类需要看髋角位移的动作很关键。

2.2.1 为什么跳过原始坐标直接算角度

平移、旋转和缩放这三类几何变换对关节角度没有影响。人在画面里往左挪两步、转个15度、摄像头拉近拉远,坐标全变了,但手臂夹角不变。KNN的距离计算对输入向量的每个分量都敏感,与其在坐标层面做复杂的归一化或者对齐,不如直接用角度让特征本身对这些变化免疫。这个取舍在样本量只有几百条的项目里尤其划算,等于免费去掉了数据增强的需求。

2.3 带时间窗口的样本构造

单帧角度在动作过程中会剧烈抖动,而且“下蹲到一半”和“下蹲到底”在数学上都是膝盖角度变小,单帧根本分不清是正在进行还是已经结束。健身计数真正要预测的是“动作相位”——是正在下探,还是正在还原,所以样本必须以帧序列为单位。

def build_samples(frame_angles, labels, window_size=30): """把角度序列切成长度为 window_size 的滑动窗口样本 frame_angles: shape (T, 6) 的连续帧角度 labels: shape (T,) 的相位标签, 取值 {0: up, 1: down} """ samples, targets = [], [] for i in range(len(labels) - window_size): samples.append(frame_angles[i:i + window_size].flatten()) targets.append(labels[i + window_size]) # 用窗口最后一帧的相位作标签 return np.array(samples), np.array(targets)

窗口大小按动作周期来定:一个俯卧撑用2秒完成,30fps就是60帧,窗口取30帧能看到“up→down→up”的半个周期,信息量刚好;窗口如果覆盖整个完整周期,模型会学到一种循环重复的状态,反而不利于区分动作边界。标签取窗口最后一帧而不是中间帧,是因为在视频流推理时,我们想知道的是“当前时刻”的相位,而不是历史平均相位,这样可以减少预测结果在时间轴上的滞后感。数据标注阶段,对每个动作视频按相位分段时间点打上标签,再调用build_samples切窗,就得到标准的KNN训练数据集。

3. KNN分类器:特征标准化、近邻数选择与动作帧判定

3.1 特征标准化与KNN的距离度量

KNN的逻辑是“和训练集里哪些样本最像,就把它分成哪类”,这里的“像”由距离度量定义。第2章得到的窗口向量维度是30帧 x 6角度 = 180维,其中肩角度和肘角度在数值范围上天然不同,如果不做标准化,欧氏距离会被数值大的维度主导,等于是让肩关节角度一个人说了算。工程上一律先标准化再算距离,这一步直接决定KNN的上限。

from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline from sklearn.neighbors import KNeighborsClassifier clf = Pipeline([ ("scaler", StandardScaler()), # 每列去均值、除标准差 ("knn", KNeighborsClassifier( n_neighbors=5, # 近邻数,先按经验设奇数 weights="distance", # 距离越近权重越大 metric="euclidean", # 标准化后用欧氏距离 )), ]) clf.fit(X_train, y_train)

StandardScaler的fit只能放在训练数据上,不能对整个数据集fit后再切分,否则等于把测试集的均值信息泄漏给训练过程。weights="distance"很有用:姿态数据里不同人体同一动作的角度常常是“中心密、边缘疏”,均匀投票会让少量离群的远端帧拉偏结果,距离加权后每个近邻按距离倒数贡献,能压住离群点的影响。样本总量只有几百条时,可以把metric换成manhattan,它对个别关键点的跳变没有那么敏感,不容易被一个异常关节角的偏差带跑。

3.2 近邻数、weights与metric的取舍

这是knn算法最常见的调参路径,三个参数相互影响,经验值如下表:

参数常用值对结果的影响本项目建议
n_neighbors3~15太小时易受帧级噪声干扰,太大时动作边界样本被多数类吞掉默认5或7,交叉验证后取最优
weightsuniform / distanceuniform简单、distance对样本密度不均更稳摄像头视频流优先distance
metriceuclidean / manhattan决定距离定义标准化后用euclidean足够

近邻数不要取偶数,二分类时偶数的平票问题会让相位判定反复横跳,这会直接传导到计数层导致重复计数。实际项目中样本量一般在几百到几千条,n_neighbors=5是个相当稳健的起点,真正能拉开差距的是特征标准化和窗口大小,而不是K值本身。调参时建议盯着macro-F1而不是accuracy,up/down两类帧的数量往往不对等,准确率会被多数类抬高。

3.3 用分组交叉验证找最优近邻数

调参要有验证集,否则只是把训练误差越压越低。姿态数据有个天然陷阱:同一个人的相邻帧几乎相同,如果随机切分训练/测试,分类器等于看着答案考试。正确做法是按“人”分组,一个人一帧都不允许同时出现在训练集和测试集:

from sklearn.model_selection import GridSearchCV, GroupKFold param_grid = { "knn__n_neighbors": [3, 5, 7, 9, 11], "knn__weights": ["uniform", "distance"], } gkf = GroupKFold(n_splits=5) grid = GridSearchCV(clf, param_grid, cv=gkf, scoring="f1_macro") grid.fit(X_train, y_train, groups=user_ids) print("best params:", grid.best_params_) print("best macro-f1:", grid.best_score_)

user_ids这块是重点:GroupKFoldgroups参数对应每个样本所属的人,切分时同一个人只能落在同一折里。如果省略groupsGroupKFold会按样本顺序切分,结果和随机切分没区别。网格搜索迭代了10个参数组合、5折交叉验证,总共50次训练,样本量不大时几秒就能跑完。搜索出的最优参数再对整个训练集fit一次,作为部署模型使用。

4. 基于BlazePose+KNN的计数逻辑:状态机、峰值检测与防抖窗口

4.1 为什么逐帧标签不能直接计次

KNN输出的是一帧的相位:俯卧撑里“手臂伸直”是up,“胸部贴地”是down。如果拿到down标签就加1,一次2秒的俯卧撑里连续15帧的down会被数出15次。反过来只在相位切换时计数,又会在“up→down”和“down→up”两个方向各计一次,变成双倍计数。计数的本质是回答“一个完整动作周期是否完成”,而不是“这一帧属于哪个相位”,所以必须在KNN之上再加一层状态逻辑。

4.2 有限状态机实现

最省事的计数方案是把相位视为状态,只在“结束态→起始态”这个边界上加1。以下实现提供一个冷却窗口,防止单帧抖动造成的重复计数:

class PhaseCounter: """基于状态机的动作相位计数器 约定: state=0 表示 up (动作起始), state=1 表示 down (动作发力/下探) cooldown: 相位跳变后锁定的帧数, 用于过滤单帧抖动 """ def __init__(self, cooldown=10): self.state = 0 self.count = 0 self.cooldown = cooldown self._lock = 0 def update(self, phase_label: int): # 锁定期内不改变状态也不计数,等待抖动过去 if self._lock > 0: self._lock -= 1 return self.count if phase_label == self.state: return self.count # 相位发生了一次真实切换 if self.state == 1 and phase_label == 0: self.count += 1 # 只有 down -> up 才计一次 self.state = phase_label self._lock = self.cooldown return self.count

cooldown的单位是帧。30fps相机下cooldown=10,意味着相位变化后0.33秒内不允许再次翻转,这个数值能滤掉大部分手臂抖动造成的瞬时误判;但如果是快速波比跳,cooldown要降到5以下,否则真实切换也会被吞掉。计数加一放在down→up边界而不是up→down,是因为“回到起始位置”才代表一次完整动作回到原点,和人工数数的直觉一致。

4.3 峰值检测作为备选方案

状态机适合相位分明的动作,但深蹲、硬拉这类角度连续变化的动作,另一种通用做法是直接找角度曲线的局部极值。膝盖角度在深蹲时先降后升,曲线最低点就是“蹲到底”的瞬间。

from scipy.signal import find_peaks angles = smoothed_knee_angle # 左右膝角度均值,一维数组 peaks, _ = find_peaks(-angles, # 取负号变为找局部极小值 distance=10, # 两个波谷之间至少隔10帧 prominence=5) # 波谷相对两侧至少突出5度 count = len(peaks)

distanceprominence是两个必须细调的参数:distance防止同一个下蹲过程被相邻的两条谷底重复计数,prominence防止小幅抖动也被当成一次完整动作。这个方法比状态机更直观,也没有状态迁移的延迟,缺点是对平滑参数更敏感,曲线不干净时一个动作会被拆成两个峰。

计数方案适用动作优势主要风险
状态机引体向上、俯卧撑、卷腹逻辑直观、不易重复计数快速动作下cooldown难配
峰值检测深蹲、屈膝抬腿、硬拉不依赖相位分割阈值敏感、对抖动易碎

两个方案可以结合使用:状态机算主计数,峰值检测作为校验,两者结果偏差超过1次时以置信度更高的一个为准。实践里,把KNN的分类概率小于0.6的帧直接延续上一帧相位,能显著减少相位跳变的次数,这个技巧在实时推理中比调大cooldown更有效。

5. 实时推理优化:帧率、关键点抖动与KNN预测瓶颈

5.1 推理循环怎么组织

实时计数的整体循环是:读帧 → 缩放 → BlazePose检测 → 提取角度 → 更新窗口 → KNN预测 → 状态机更新 → 渲染。关键技术瓶颈在BlazePose的目标检测,而不是KNN——当训练样本只有几千条时,sklearn的KNN预测耗时只有微秒级,但BlazePose在CPU上处理一张640x480图像大约需要20到40毫秒,所以优化的重心全在检测器侧。

cap = cv2.VideoCapture(0) pose = mp.solutions.pose.Pose( model_complexity=1, # 0/1/2,越大越准也越慢 min_detection_confidence=0.5, min_tracking_confidence=0.5) window = [] # 维护最近 WINDOW 帧的角度 while cap.isOpened(): ok, frame = cap.read() if not ok: break frame = cv2.resize(frame, (640, 480)) results = pose.process(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) if results.pose_landmarks is None: continue # 人不在画面里,直接跳过 angles = extract_joint_angles(results.pose_landmarks.landmark) window.append(angles) if len(window) < WINDOW: continue window.pop(0) x_feat = np.array(window).flatten().reshape(1, -1) label = clf.predict(x_feat)[0] count = counter.update(label) # 渲染计数结果到画面 cv2.putText(frame, f"count: {count}", (30, 50), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 255, 0), 2)

model_complexity在视频流上设1是平衡点:0是CPU最快但关键点抖动肉眼可见,2最准但要压到15fps以下。min_detection_confidence建议0.5,低于这个值人体区域没有被检测到就不会输出landmark,与其用一个错位骨架去干扰计数,不如直接跳过这一帧。KNN的predict之前不用再单独调scaler,因为Pipelinetransform已经包含其中,这也是用Pipeline而不是手动拼接的好处。

5.2 帧率与检测频率的取舍

健身动作大部分持续1到3秒,不需要每帧都做姿态估计,降低检测频率是性价比最高的优化手段:

手段典型设置收益副作用
降输入分辨率1280x720 → 640x480耗时减少约一半小目标关键点会丢
跳帧检测每2帧检测一次吞吐翻倍快速动作相位边界变模糊
复用未检测帧的角度检测到的人体区域做ROI跟踪混合方案效果最好遮挡时输出陈旧结果

跳帧时窗口的滑动仍然按“被检测的帧”来计,而不是按视频帧时间计。如果视频是30fps、窗口大小为30,原本表示1秒历史;跳帧后同样30帧窗口对应2秒历史,时间尺度失真会影响KNN对动作相位的判断。这个坑容易踩,建议在窗口滑动处打印时间戳做校验。

5.3 关键点抖动的EMA平滑与置信度过滤

BlazePose在快速动作的瞬间输出会抖,尤其是手腕和脚踝。用指数移动平均(EMA)给角度做低通滤波,比直接在坐标上滤波更直观:

alpha = 0.5 # 0 < alpha <= 1,越大越跟随原始值 smoothed = alpha * angles_now + (1 - alpha) * smoothed_prev

alpha=0.5意味着当前帧只占50%权重,抖动幅度减半,延迟1到2帧;调到0.2后平滑效果更强,但深蹲底部的相位会晚4到6帧才体现出来。计数任务里漏计比延迟严重,所以alpha不要低于0.3。另一个容易忽略的过滤器是visibility:肩膀、髋部这类大关键点基本稳定,但手腕朝向侧面时visibility常常低于0.5,这一帧的角度就不该进窗口。用低置信度替换成上一帧的值,比塞一个噪声值进窗口更安全:

vis = [p.visibility for p in landmarks] if min(vis[11], vis[12], vis[23], vis[24]) < 0.5: continue # 躯干主干关键点不可信,整帧跳过

训练阶段和推理阶段必须使用同一套过滤逻辑,否则离线评估的准确率和线上表现对不上。

6. 多动作扩展与KNN验证:从重复计数到分组留一法

6.1 多动作标签设计

样本充足时可以把标签从“相位”扩展成“动作+相位”,例如squat_downsquat_uppushup_downpushup_up四类。类别数增加后第一批遇到的问题就是类别不平衡:俯卧撑的down相位只有10多帧,而up相位有30多帧,KNN的多数类偏好会把少数类的边界推得很偏。解决办法是训练时用weights="distance"配合欠采样,把多数类压到和少数类差不多量级,或者给少数类样本加权。动作之间的混淆也有规律:深蹲和半蹲只在膝盖角度上差10度左右,最容易互相串;跪姿俯卧撑和标准俯卧撑因为肩髋角度接近,也常被误判。这类问题只有多动作的混淆矩阵才能暴露。

6.2 分组留一法验证

随机切分训练和测试集时,同一个人的相邻帧同时出现在两边,评估结果会虚高。按人分组做留一交叉验证,才是真正检验泛化能力的方式。一个典型的对比结果:

GroupKFold accuracy : 0.92 (std 0.04) RandomSplit accuracy: 0.97 (std 0.02)

差出来的5个百分点全部来自数据泄漏。分组留一法用GroupKFold,把groups设成人的ID,最后画混淆矩阵观察哪个相位互相误判。如果分组和随机切分的准确率差距小于1%,说明模型没有大量记忆个人特征,这时的准确率才是能写进项目说明的最终数字。

6.3 训练集常见误用和裁剪技巧

最后一个实操技巧是裁剪冗余样本。KNN的预测耗时随训练样本量线性增长:几千条时微秒级,膨胀到几万条时延迟开始明显。先用当前最优K值在训练集上自预测,把被误分类的样本单独挑出来放入“难例集”,KNN的最终邻居从难例集里检索。这个思路等价于对决策边界做压缩,比随机抽稀保留的边界信息更多,在数据集膨胀到单动作上千样本时能把KNN的预测延迟控制住,而不用急着换深度模型。至此,BlazePose做特征提取、KNN做帧级分类、状态机做序列修正、分组验证做性能评估,这一条链路的每个环节都能独立验证,也可以单独替换。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 6:35:06

STM32 Modbus无线网关设计:从RS485到ESP8266的完整方案

简介&#xff1a;一份基于stm32单片机modbus无线网关系统的完整设计资料&#xff0c;面向嵌入式开发者、电子爱好者及毕业设计学生&#xff0c;解决无线温湿度采集与modbus协议网关搭建的实际需求。系统采用stm32作为核心控制&#xff0c;采集端搭配温湿度传感器与Lora无线模块…

作者头像 李华
网站建设 2026/9/16 6:34:59

Swing+MySQL教务管理系统:从MVC分层到并发选课实战

简介&#xff1a;一套基于Java Swing和MySQL的学校教务管理系统完整项目&#xff0c;面向Java SE学习者、高校课程设计与毕业设计学生&#xff0c;以及需要借鉴桌面端管理类系统架构的开发者。系统涵盖用户登录与权限控制、课程管理、学生与教师信息维护、选课排课、考勤记录、…

作者头像 李华
网站建设 2026/9/16 6:34:29

LLM系统提示词泄露风险与全链路防护指南

1. 项目概述&#xff1a;为什么“system_prompts_leaks”突然成了技术圈的高频词最近两周&#xff0c;无论是在GitHub Trending榜单、Hugging Face社区讨论区&#xff0c;还是国内几个主流AI开发者微信群里&#xff0c;“system_prompts_leaks”这个短语出现频率陡增——不是作…

作者头像 李华
网站建设 2026/9/16 6:33:56

MCP协议安全风险解析与AI模型通信防护实践

1. 项目概述&#xff1a;MCP协议在AI生态中的关键作用在当今AI技术快速发展的背景下&#xff0c;各种AI系统间的互联互通变得尤为重要。MCP&#xff08;Model Communication Protocol&#xff09;协议作为AI模型间通信的标准化接口&#xff0c;正逐渐成为AI生态系统中不可或缺的…

作者头像 李华
网站建设 2026/9/16 6:33:21

双馈风机虚拟同步控制技术解析与应用

1. 双馈风机虚拟同步控制技术背景解析在新能源发电领域&#xff0c;双馈感应发电机&#xff08;DFIG&#xff09;因其优异的变速恒频特性已成为主流机型。但传统矢量控制方案存在惯性缺失、抗扰动能力弱等问题&#xff0c;而虚拟同步发电机&#xff08;VSG&#xff09;技术通过…

作者头像 李华
网站建设 2026/9/16 6:32:53

安检X光危险品检测:目标检测模型选型与PyTorch实现

简介&#xff1a;面向机场安检场景的深度学习危险品自动识别系统源码包&#xff0c;适合计算机、人工智能等专业学生用于课程设计、毕业设计或期末大作业&#xff0c;也可供企业开发者二次拓展&#xff0c;是计算机视觉与智能安检方向不错的入门进阶参考。压缩包共179个文件&am…

作者头像 李华