☰
Python视觉识别健身动作指导源码:深蹲角度计算与状态机判定
2026/10/2 14:19:13 网站建设 项目流程

简介:这份源码资源面向希望将计算机视觉落地到健身场景的Python开发者与健身科技爱好者,针对无教练指导下动作不规范、影响训练效果甚至造成损伤的痛点,提供一套可运行的健身动作识别与指导方案。压缩包共20个文件,约83KB,以8个py源码文件为核心,配合7个xml界面布局、2个txt说明、1个md文档及license、iml等配置,涵盖主程序入口、src模块、utils工具与data数据目录,结构清晰便于二次开发。已有88人学习下载。读者可从中获取人体姿态估计、动作识别算法、数据采集处理与实时反馈机制的完整实现思路,理解如何借助OpenCV与深度学习框架完成关键点比对与规范性判断,并参考模块化设计快速搭建自己的健身指导应用,适合作为课程设计、毕业项目或视觉识别练手案例。

1. 从一段手机拍摄的深蹲视频说起:这套 Python 视觉识别源码到底能干什么

健身房里最常见的场景:会员对着镜子做深蹲,膝盖内扣了自己看不见,教练在十米外带别的学员。等发现动作变形,一组已经做完,膝盖压力也吃满了。这套基于 Python 的视觉识别健身动作指导源码,解决的就是这个「没人盯动作」的缺口——用普通摄像头或手机录一段视频,程序把人体关键点抽出来,算出膝角、髋角、躯干倾角这些量化指标,再对照标准动作区间给出「膝盖内扣」「下蹲深度不足」这类判断。

它适合三类人:想入门计算机视觉但不想啃纯理论的 Python 学习者,手里有健身内容想做动作纠错产品的开发者,以及体育/康复方向想把动作评估数字化的从业者。整套代码是源码包形式,不是调个 API 就完事的黑匣子,关键点检测、角度计算、判定逻辑都能改。下面按「跑起来 → 看懂原理 → 改参数 → 避坑」的顺序拆,新手能照着复现,熟手能直接看到可扩展的边界在哪。

2. 环境搭起来:Python 版本、依赖与摄像头链路的选型理由

2.1 为什么是 Python + MediaPipe 这条技术路线

动作识别有两条主流路线:一条是端到端的动作分类模型(输入视频直接输出动作类别),另一条是先做人体姿态估计、再基于关键点做几何判定。这套源码走的是第二条。原因很实际——端到端分类模型要标注大量「标准深蹲/错误深蹲」视频,个人开发者根本凑不齐数据集;而姿态估计模型是预训练好的,你只需要在关键点坐标上写角度公式,几十行代码就能出判定结果。

姿态估计这一层,常见做法是用 MediaPipe Pose 或 OpenPose。MediaPipe 的优势是 CPU 上就能跑到实时,单帧 30ms 左右,装完即用,不需要配 CUDA;OpenPose 精度略高但依赖重、部署麻烦。源码包默认走 MediaPipe,这也是我推荐新手先跑通的原因——先把判定逻辑调对,再考虑换更重的模型提精度。

提示:如果你的机器没有独立显卡,别一上来就想着换 YOLO-Pose 或 HRNet,MediaPipe 在普通笔记本上跑 720p 视频已经够用,先把流程跑通比追求精度重要。

2.2 环境安装与依赖清单

Python 版本建议 3.8 到 3.10,MediaPipe 对 3.11 以上的支持在部分平台还不稳定,这是血泪经验——我见过有人在 3.12 上装 mediapipe 直接编译失败。用虚拟环境隔离,别污染系统 Python。

# 创建并激活虚拟环境,python 版本锁在 3.9 比较稳 python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate # 安装核心依赖 pip install mediapipe==0.10.9 pip install opencv-python==4.8.1.78 pip install numpy==1.24.3

三个包各司其职:mediapipe负责从每帧图像里吐出 33 个人体关键点的归一化坐标;opencv-python负责读视频、画骨架、写输出;numpy负责角度计算里的向量运算。版本号不是随便写的,mediapipe 0.10.x 和 numpy 2.x 有兼容问题,锁在 1.24 能避开一堆np.float相关的报错。

装完验证一下:

import mediapipe as mp import cv2 import numpy as np print("mediapipe:", mp.__version__) print("opencv:", cv2.__version__) print("numpy:", np.__version__)

三行都打印出版本号,说明环境通了。如果 mediapipe 导入报AttributeError或ImportError,九成是 numpy 版本冲突,回退到 1.24.x 即可。

2.3 视频输入链路的两种接法

源码支持两种输入:本地视频文件和实时摄像头。本地文件适合调试判定逻辑,因为可以反复跑同一段视频对比结果;摄像头适合演示,但调试时动作不可复现,容易把自己绕进去。

import cv2 # 方式一:读本地视频,调试阶段首选 cap = cv2.VideoCapture("squat_demo.mp4") # 方式二:读摄像头,索引 0 通常是默认摄像头 # cap = cv2.VideoCapture(0) if not cap.isOpened(): raise RuntimeError("视频源打不开,检查路径或摄像头索引") fps = cap.get(cv2.CAP_PROP_FPS) w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) print(f"分辨率 {w}x{h}, 帧率 {fps}")

VideoCapture的参数就是路径或设备索引。调试阶段强烈建议用本地视频,把同一段深蹲视频跑十遍,看判定结果是否稳定,这比对着摄像头反复蹲要高效得多。分辨率建议 720p,1080p 在 CPU 上跑 MediaPipe 会掉帧,判定逻辑依赖连续帧,掉帧会让角度曲线出现跳变。

3. 关键点怎么变成判定结果:角度计算与动作状态机

3.1 从 33 个关键点到三个核心角度

MediaPipe Pose 每帧输出 33 个关键点,每个点有 x、y、z 和可见度。做深蹲判定,真正用到的只有几个:髋(23、24)、膝(25、26)、踝(27、28)、肩(11、12)。核心思路是把三个点连成两条向量,算夹角。

import numpy as np def calc_angle(a, b, c): """计算 b 点为顶点时,向量 ba 与 bc 的夹角,返回角度值""" a, b, c = np.array(a), np.array(b), np.array(c) ba = a - b bc = c - b # 点积公式求夹角,加 1e-6 防止除零 cosine = np.dot(ba, bc) / (np.linalg.norm(ba) * np.linalg.norm(bc) + 1e-6) # 数值裁剪到 [-1,1],避免浮点误差导致 arccos 报 nan cosine = np.clip(cosine, -1.0, 1.0) return np.degrees(np.arccos(cosine)) # 以左腿为例,关键点索引 # 髋 23,膝 25,踝 27 knee_angle = calc_angle(hip[23], knee[25], ankle[27]) hip_angle = calc_angle(shoulder[11], hip[23], knee[25])

calc_angle里三个参数是「点 A - 顶点 B - 点 C」的顺序,顶点放中间。膝角用髋-膝-踝,髋角用肩-髋-膝。np.clip那行是后悔药——浮点误差会让余弦值偶尔超出 [-1,1],arccos直接返回 nan,整个判定就崩了,加个裁剪能省掉大量排查时间。

3.2 用状态机判断「一次完整的深蹲」

单帧角度没有意义,你得知道动作进行到哪一步。深蹲的完整周期是:站立(膝角接近 180°)→ 下蹲(膝角减小)→ 最低点(膝角最小)→ 起身(膝角增大)→ 回到站立。用一个简单的状态机跟踪。

class SquatFSM: def __init__(self): self.state = "STAND" # 初始站立 self.min_knee_angle = 180 # 记录本次下蹲的最小膝角 self.rep_count = 0 # 完成次数 def update(self, knee_angle): if self.state == "STAND" and knee_angle < 150: # 从站立进入下蹲 self.state = "DOWN" self.min_knee_angle = knee_angle elif self.state == "DOWN": # 持续记录最低点 self.min_knee_angle = min(self.min_knee_angle, knee_angle) if knee_angle > 160: # 起身回到站立,完成一次 self.state = "STAND" self.rep_count += 1 return self.judge(self.min_knee_angle) return None def judge(self, min_angle): # 最低点膝角大于 100 度,判定下蹲深度不足 if min_angle > 100: return "下蹲深度不足" return "动作达标"

状态机的两个阈值 150 和 160 是滞回区间,防止角度在临界值附近抖动导致状态反复横跳。min_knee_angle记录整个下蹲过程的最小值,用它来判断深度,比用某一帧的角度可靠得多。判定逻辑集中在judge方法里,想加「膝盖内扣」检测,就在这个方法里补膝-踝连线与垂直方向的夹角判断。

3.3 把骨架和判定结果画回视频

光有数字不够直观,得把关键点、骨架、角度值、判定文字叠加到画面上,输出成可回看的视频。

import mediapipe as mp import cv2 mp_pose = mp.solutions.pose mp_draw = mp.solutions.drawing_utils pose = mp_pose.Pose( static_image_mode=False, # 视频流用 False,会做帧间跟踪 model_complexity=1, # 0 最快 1 均衡 2 最准 min_detection_confidence=0.5, min_tracking_confidence=0.5 ) out = cv2.VideoWriter("output.mp4", cv2.VideoWriter_fourcc(*"mp4v"), fps, (w, h)) while cap.isOpened(): ret, frame = cap.read() if not ret: break rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) result = pose.process(rgb) if result.pose_landmarks: mp_draw.draw_landmarks(frame, result.pose_landmarks, mp_pose.POSE_CONNECTIONS) # 取关键点像素坐标,后续算角度、更新状态机 h_, w_ = frame.shape[:2] lm = result.pose_landmarks.landmark knee = (lm[25].x * w_, lm[25].y * h_) # ... 省略其余点提取 out.write(frame) cap.release() out.release()

static_image_mode=False是关键,视频流下 MediaPipe 会用上一帧的结果做跟踪,速度和稳定性都更好;设成 True 每帧独立检测,又慢又抖。model_complexity从 0 到 2,精度和速度的权衡,CPU 上跑建议 1。min_detection_confidence调到 0.5 是平衡值,调太高会漏检,调太低会误检背景里的杂物。

4. 避坑与排查:跑不通、判不准、卡成 PPT 的常见原因

4.1 关键点抖动导致判定结果乱跳

现象:同一段视频跑两遍,一次说达标一次说深度不足,角度曲线像锯齿。

原因:MediaPipe 逐帧检测有随机误差,尤其脚踝和膝在快速下蹲时可见度低,坐标会跳。单帧角度直接拿去判定,结果自然不稳。

解决:对角度做滑动平均或指数平滑。常见做法是维护一个长度 5 的队列,取均值再送进状态机。别用太长的窗口,否则最低点会被抹平,深度判定反而失准。

from collections import deque angle_buffer = deque(maxlen=5) def smooth_angle(new_angle): angle_buffer.append(new_angle) return sum(angle_buffer) / len(angle_buffer)

4.2 摄像头打不开或读到黑屏

现象:cap.isOpened()返回 False,或者读出来的帧全黑。

原因:设备索引不对,或者摄像头被其他程序占用(视频会议软件最常见),或者 Linux 下没有访问权限。

解决:先换索引试 0、1、2;关掉占用摄像头的程序;Linux 下把用户加进 video 组。本地视频黑屏则多半是编码格式问题,换 H.264 编码的 mp4 重试。

4.3 帧率太低,判定跟不上动作

现象:视频播放卡顿,一次深蹲只抓到三四帧,状态机来不及记录最低点。

原因:1080p 以上分辨率在 CPU 上跑 MediaPipe 会掉到 10fps 以下,或者model_complexity设成了 2。

解决:把输入分辨率降到 720p 甚至 480p,model_complexity设 0 或 1。判定逻辑依赖帧数,宁可画质差一点也要保证帧率。如果必须高分辨率,先把视频抽帧存成图片,离线批量处理,不追求实时。

4.4 角度算出来是 nan 或明显离谱

现象:打印角度出现 nan,或者算出 300 多度这种不可能的值。

原因:关键点可见度太低时坐标是无效的,或者calc_angle里没做除零保护和数值裁剪。

解决:算角度前先检查三个点的visibility,低于 0.5 就跳过这一帧,沿用上一帧的角度。calc_angle里的+1e-6和np.clip必须保留,这是防 nan 的底线。

4.5 判定阈值照搬别人的,自己视频全判错

现象:代码逻辑没问题,但所有动作都被判成不达标。

原因:不同身高、拍摄角度、镜头焦距下,同样的动作角度分布不一样。别人视频里膝角 90° 是标准,你的机位拍出来可能是 110°。

解决:先跑一段自己的标准动作视频,把每帧角度打印出来,看最低点实际落在哪个区间,再据此调judge里的阈值。别迷信默认值,阈值是标定出来的,不是抄来的。

5. 进阶玩法:把单动作判定扩成多动作与实时反馈

跑通深蹲之后,这套源码的骨架可以往两个方向扩。一是多动作支持,二是实时反馈。多动作的核心是把「角度组合」抽象成动作模板:深蹲看膝角和髋角,俯卧撑看肘角和躯干倾角,硬拉看髋角和脊柱曲度。每个动作写一个判定类,共用同一套关键点提取和平滑逻辑,主循环里根据用户选择切换判定器。

class PushupJudge: """俯卧撑判定:肘角 + 躯干是否成直线""" def __init__(self): self.state = "UP" self.min_elbow = 180 def update(self, elbow_angle, body_angle): # body_angle 是肩-髋-踝夹角,接近 180 表示躯干挺直 if self.state == "UP" and elbow_angle < 120: self.state = "DOWN" self.min_elbow = elbow_angle elif self.state == "DOWN": self.min_elbow = min(self.min_elbow, elbow_angle) if elbow_angle > 150: self.state = "UP" if body_angle < 160: return "塌腰,躯干没挺直" if self.min_elbow > 100: return "下放深度不足" return "动作达标" return None

body_angle用肩-髋-踝三点算,接近 180° 说明身体成一条直线,小于 160° 就是塌腰或撅臀。这个思路可以复制到任何「关节角度 + 身体姿态」组合的动作上。

实时反馈则是把判定结果从「写进视频」改成「即时显示 + 语音提示」。用 OpenCV 的putText在画面上叠加文字,配合pyttsx3做本地语音播报,延迟能控制在可接受范围。注意语音播报别每帧都触发,用状态机的返回值做触发条件,一次动作只播一次。

动作核心角度判定阈值参考常见错误
深蹲膝角、髋角最低点膝角 70-100°深度不足、膝盖内扣
俯卧撑肘角、躯干角最低点肘角 80-100°塌腰、下放不足
硬拉髋角、脊柱角起始髋角 45-60°弓背、髋膝不同步
弓步蹲前膝角、后膝角前膝 90° 左右前膝过脚尖、重心不稳

阈值这一列是参考区间,实际用之前一定按 4.5 节的方法在自己的视频上标定一遍。我现在的习惯是:每换一个拍摄机位或换一批测试者,先跑十次标准动作把角度分布打出来,确认阈值落在合理区间再开判定。这套源码的价值不在于它内置的判定有多准,而在于它把「关键点提取 → 角度计算 → 状态机判定 → 可视化输出」这条链路完整摊开给你,每一环都能按自己的场景替换。希望帮到你。

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

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

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

立即咨询