1. 项目缘起:从“数到崩溃”到“让机器看懂”
去年帮一个体育学院的朋友做体能测试,其中一项是记录学生在一分钟内完成标准俯卧撑的次数。我坐在旁边,一手拿秒表,一手拿记录板,眼睛死死盯着测试者的动作。前二十个还好,到后面,测试者动作开始变形,我的注意力也开始涣散,一个走神,可能就少数或多记了一个。更别提同时测试好几个人的混乱场面了。那次经历让我深刻体会到,依赖人工计数不仅效率低下、容易出错,而且无法提供动作质量的分析。
就在那时,“机器视觉”这个词跳进了我的脑子。我们能不能让摄像头代替人眼,让算法代替人脑,去自动、准确、公正地完成计数,甚至分析动作标准度?这个想法成了我毕业设计的核心。它不仅仅是一个学术课题,更是为了解决一个非常具体的实际问题:如何将计算机视觉技术落地到一个贴近生活的健身场景中,让科技服务于日常训练。
基于机器视觉的俯卧撑计数模型,其核心目标就是通过摄像头实时捕捉人体姿态,识别俯卧撑动作的关键阶段(如身体下降和撑起),并据此完成自动计数。它涉及的关键技术栈包括人体关键点检测、动作时序分析和计数逻辑设计。这个项目对于计算机视觉的初学者来说,是一个绝佳的练手项目:目标明确、应用场景清晰、技术链条完整,从数据采集、模型训练到部署应用都能走一遍。
2. 技术选型:为什么是MediaPipe而不是OpenPose?
实现人体姿态估计,主流开源方案主要有两个:OpenPose和Google的MediaPipe。在项目初期,我花了大量时间对比两者,最终选择了MediaPipe,原因基于以下几个非常实际的考量:
2.1 性能与效率的压倒性优势
OpenPose功能强大,精度高,但其模型复杂,对计算资源要求极高。在普通笔记本电脑甚至中端显卡上,很难达到实时(>30 FPS)的处理速度。这对于需要实时反馈的俯卧撑计数应用来说是致命的。MediaPipe则采用了轻量级模型和高度优化的流水线,在CPU上就能实现实时的全身姿态估计,这对于降低部署门槛、在移动端或边缘设备上运行至关重要。
2.2 开发与部署的便捷性
OpenPose的安装和配置过程堪称“劝退指南”,需要编译复杂的C++库,处理各种依赖冲突。MediaPipe提供了极其友好的Python API,通过pip即可安装其核心的姿势估计模块(mediapipe),几行代码就能初始化并运行检测器。这种“开箱即用”的特性,让我能把宝贵的时间集中在业务逻辑(计数算法)上,而不是和环境搏斗。
2.3 关键点定义的合理性
MediaPipe的人体姿态模型定义了33个三维关键点,涵盖了身体、面部和手部。对于俯卧撑计数,我们主要关注其中一部分:双肩(11, 12)、双髋(23, 24)、双膝(25, 26)、双踝(27, 28)以及鼻子(0)。这些点足以构建判断身体角度的向量。MediaPipe的关键点索引清晰、稳定,其提供的坐标(x, y, z)和可见度(visibility)信息非常实用。
注意:这里有一个初学者极易忽略的细节。MediaPipe返回的坐标(x, y)是归一化到[0, 1]区间的,分别表示相对于图像宽度和高度的比例。而z坐标是相对深度,值越小表示离摄像头越近。在计算真实世界角度时,必须先将x, y坐标乘以图像的宽和高,转换为像素坐标。
我的选择总结:对于毕业设计或快速原型开发,MediaPipe是更优解。它平衡了精度、速度和易用性。如果未来项目对精度有极端要求,且拥有强大的GPU服务器,可以再考虑优化版的OpenPose或其他学术模型。但在现阶段,“快速实现、快速验证”是第一要务。
3. 核心实现:拆解一个俯卧撑的完整周期
有了姿态估计工具,接下来就是核心逻辑:如何从一个动态的视频流中,识别出一次完整的俯卧撑?我将其分解为三个层次:单帧姿态解析、时序状态判断和计数逻辑触发。
3.1 单帧姿态解析:计算躯干倾斜角
计数的基础是判断身体是否处于“低位”(胸口接近地面)。最直观的指标是躯干与地面的夹角。我们可以利用肩部和髋部的关键点来近似代表躯干。
假设我们获得了左肩(shoulder_left)、右肩(shoulder_right)、左髋(hip_left)、右髋(hip_right)的像素坐标。首先计算肩部中点和髋部中点:
# 伪代码示例 shoulder_center = (shoulder_left + shoulder_right) / 2 hip_center = (hip_left + hip_right) / 2然后,构建从髋部中点指向肩部中点的向量torso_vector。在图像坐标系中,向量可以表示为(dx, dy) = (shoulder_center.x - hip_center.x, shoulder_center.y - hip_center.y)。
接着,我们需要一个参考水平向量。一个简单且有效的方法是,假设摄像头是基本水平放置的,那么图像的水平方向(x轴方向)就可以作为地面参考。我们定义水平向量horizontal_vector = (1, 0)。
最后,计算两个向量之间的夹角。这里使用向量点积公式:
cosθ = (torso_vector · horizontal_vector) / (|torso_vector| * |horizontal_vector|)
由于|horizontal_vector| = 1,公式简化为cosθ = dx / sqrt(dx^2 + dy^2)。通过反余弦函数arccos即可得到夹角 θ。这个夹角在0到180度之间,当身体直立时,θ接近90度;当身体水平时,θ接近0度或180度(取决于方向)。
3.2 时序状态判断:设计一个状态机
单帧的角度只是一个瞬时值。俯卧撑是一个连续动作,我们必须引入时间维度,设计一个有限状态机来跟踪整个运动过程。这是整个计数逻辑的大脑。
我定义了四个状态:
- 高位(UP):身体伸直,躯干角度大于某个阈值(如60度)。这是起始和结束位置。
- 下降中(GOING_DOWN):从高位进入,角度持续减小,且速度(角度变化率)为负。
- 低位(DOWN):身体降至最低点,角度小于某个阈值(如30度),并且速度接近零(处于短暂停顿)。
- 上升中(GOING_UP):从低位进入,角度持续增大,且速度为正。
状态之间的转换由角度值及其变化率(导数)触发。例如:
- UP -> GOING_DOWN:当角度小于“高位阈值”且角度变化率为负(正在变小)。
- GOING_DOWN -> DOWN:当角度小于“低位阈值”且角度变化率的绝对值很小(即将停止下降)。
- DOWN -> GOING_UP:当角度变化率为正(开始上升)。
- GOING_UP -> UP:当角度大于“高位阈值”且角度变化率很小(即将停止上升)。
3.3 计数逻辑触发:何时算完成一次?
计数触发点放在GOING_UP -> UP这个状态转换的时刻。为什么不是从DOWN状态直接计数?因为要防止在最低点抖动导致的误计数。只有当系统确认用户确实从最低点开始上升,并成功回到了高位,才认为完成了一次完整的、有效的俯卧撑。
这里有一个非常重要的防抖动机制:在状态转换条件中,我加入了“持续帧数”判断。例如,不能因为一帧的角度略高于阈值就立刻从GOING_UP跳转到UP,而需要连续N帧(比如5帧,约0.17秒)都满足高位条件,才进行状态转换和计数。这能有效过滤掉动作不稳定或检测噪声带来的抖动。
4. 从实验室到健身房:工程化落地中的五大挑战
把算法跑通Demo只是第一步,要让模型在真实场景中稳定工作,会遇到一系列在实验室里想不到的问题。这部分是我踩坑最多、也是收获最大的地方。
4.1 光照与背景干扰:预处理不是可有可无
最初我在宿舍均匀灯光下测试,效果很好。但一到健身房,问题就来了:屋顶射灯造成局部强光过曝,阴影区域又太暗;背景中走动的人和复杂器械形成了大量干扰。
解决方案:
- 动态ROI(感兴趣区域):不要处理整张图。我先用几帧图像通过背景减除或运动检测,粗略定位运动者的大致区域,后续的姿态估计和计算只在这个区域内进行,大幅减少了计算量和背景干扰。
- 直方图均衡化:对ROI区域进行CLAHE(限制对比度自适应直方图均衡化)处理。它能有效改善局部过曝或欠曝区域的对比度,让MediaPipe提取的关键点更稳定。这一步对精度提升非常明显。
- 背景简化建议:在项目演示或初期部署时,如果条件允许,尽量选择纯色、静止的背景墙。这能从根本上减少干扰。
4.2 摄像头视角与标定问题
“角度阈值”设定为30度和60度,这个值是基于摄像头正对测试者侧面这一理想视角。如果摄像头是从斜上方拍摄,计算出的躯干向量与水平面的夹角就会失真,导致阈值完全失效。
解决方案:
- 视角固定:这是最实用的方法。规定摄像头的安装位置和高度(例如,侧面,镜头中心与测试者髋部同高,距离1.5米)。在应用说明中明确要求用户按此设置。
- 简易标定:程序启动时,让用户先做一个“直立”和“平板支撑”姿势,程序记录下这两个姿势对应的角度值,作为动态的“高位基线”和“低位基线”。这样,阈值就变成了相对于用户自身基线的偏移量,而非固定值,适应性更强。
4.3 动作不标准与计数争议
这是最核心的业务逻辑挑战。什么是“有效”的俯卧撑?身体没完全下去算吗?屁股撅太高算吗?
解决方案:引入多角度联合判断。
- 躯干角度:主判断条件,如前所述。
- 膝关节角度:计算大腿(髋到膝)和小腿(膝到踝)的夹角。如果这个角度过大(比如>160度),说明腿没伸直,可以给出“膝盖未伸直”的提示,但不一定判定为无效,这取决于规则松紧。
- 躯干弯曲度:除了肩髋连线,还可以计算脊柱关键点(如肩、髋、膝)连线的弯曲程度,判断是否塌腰或过弓。
- 最终策略:我采用了一个“扣分制”模型。一次动作,躯干角度达标是得分的必要条件。膝角、脊柱曲度等作为附加检查项,如果不达标,则在计数结果旁显示“动作不标准:膝盖弯曲”,但依然计数。这样既保证了计数的连续性,又提供了质量反馈。
4.4 实时性与延迟优化
在树莓派4B上部署时,发现延迟高达2-3秒,完全无法用于实时计数。瓶颈分析发现,MediaPipe本身在CPU上运行尚可,但图像采集、显示和GUI渲染占用了大量资源。
解决方案:
- 降低处理分辨率:将输入图像从1920x1080下采样到640x480,MediaPipe的处理速度直接提升数倍,而对关键点检测精度影响微乎其微。
- 多线程流水线:设计一个生产者-消费者模型。一个线程专门负责从摄像头抓取帧(生产者),放入一个容量为1的队列;另一个线程从队列取帧,进行姿态估计和计数计算(消费者)。这样,抓取下一帧的操作不会被耗时的计算所阻塞,显著提升了帧率。
- 关闭非必要可视化:在最终部署版本中,可以只显示关键点连线和计数结果,甚至只输出数字和语音提示,关闭原始的、高分辨率的视频流显示。
4.5 数据收集与模型微调(进阶)
MediaPipe的通用模型在大多数情况下已经很好,但对于一些极端姿势(如穿着非常宽松的衣服、有部分遮挡)或特定人群(如儿童),效果可能会下降。
解决方案:建立自己的微调数据集。
- 数据收集:录制不同体型、不同着装、在不同光照和背景下做俯卧撑的视频。
- 关键点标注:使用Labelme等工具,手动标注出MediaPipe定义的33个关键点。这是一个费时但效果显著的工作。
- 模型微调:利用MediaPipe提供的模型训练框架(或使用PyTorch/TensorFlow重建类似轻量级模型),用自己的标注数据对模型进行微调。即使只用几百张标注图像,也能让模型在特定场景下的鲁棒性大幅提升。这一步属于进阶优化,但如果你想深入研究CV,这是必经之路。
5. 系统搭建与效果评估:一份可运行的实现清单
理论说了这么多,我们来点实际的。下面是一个基于Python,使用MediaPipe和OpenCV实现的简化版系统框架和评估方法。
5.1 核心代码结构
import cv2 import mediapipe as mp import numpy as np from enum import Enum class PoseState(Enum): UP = 1 GOING_DOWN = 2 DOWN = 3 GOING_UP = 4 # 初始化MediaPipe Pose mp_pose = mp.solutions.pose pose = mp_pose.Pose(static_image_mode=False, model_complexity=1, enable_segmentation=False, min_detection_confidence=0.5, min_tracking_confidence=0.5) mp_drawing = mp.solutions.drawing_utils # 状态机变量 current_state = PoseState.UP rep_count = 0 angle_buffer = [] # 用于平滑角度和计算变化率 UP_THRESH = 60 # 高位角度阈值 DOWN_THRESH = 30 # 低位角度阈值 CONFIRM_FRAMES = 5 # 状态确认所需连续帧数 def calculate_torso_angle(landmarks, image_shape): # 获取肩部和髋部关键点索引(MediaPipe索引) l_shoulder = landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER] r_shoulder = landmarks[mp_pose.PoseLandmark.RIGHT_SHOULDER] l_hip = landmarks[mp_pose.PoseLandmark.LEFT_HIP] r_hip = landmarks[mp_pose.PoseLandmark.RIGHT_HIP] # 转换为像素坐标并计算中点 h, w = image_shape[:2] shoulder_center = np.array([(l_shoulder.x + r_shoulder.x) * w / 2, (l_shoulder.y + r_shoulder.y) * h / 2]) hip_center = np.array([(l_hip.x + r_hip.x) * w / 2, (l_hip.y + r_hip.y) * h / 2]) # 计算向量和角度 torso_vec = shoulder_center - hip_center # 注意:图像y轴向下,所以角度计算需注意。这里我们主要关心角度大小变化趋势。 angle = np.degrees(np.arctan2(abs(torso_vec[1]), abs(torso_vec[0]))) # 一个简化的角度计算 return angle cap = cv2.VideoCapture(0) # 打开摄像头 while cap.isOpened(): success, image = cap.read() if not success: break # 转换为RGB并处理 image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) results = pose.process(image_rgb) if results.pose_landmarks: # 计算当前帧躯干角度 current_angle = calculate_torso_angle(results.pose_landmarks.landmark, image.shape) angle_buffer.append(current_angle) if len(angle_buffer) > 5: # 保留最近5帧用于计算平滑角度和速度 angle_buffer.pop(0) smoothed_angle = np.mean(angle_buffer) # 简单计算角度变化率(用最近两帧差值近似) if len(angle_buffer) >= 2: angle_velocity = angle_buffer[-1] - angle_buffer[-2] else: angle_velocity = 0 # 状态机逻辑(简化版,需完善防抖) if current_state == PoseState.UP: if smoothed_angle < UP_THRESH and angle_velocity < -0.5: current_state = PoseState.GOING_DOWN elif current_state == PoseState.GOING_DOWN: if smoothed_angle < DOWN_THRESH and abs(angle_velocity) < 0.5: current_state = PoseState.DOWN elif current_state == PoseState.DOWN: if angle_velocity > 0.5: current_state = PoseState.GOING_UP elif current_state == PoseState.GOING_UP: if smoothed_angle > UP_THRESH and abs(angle_velocity) < 0.5: current_state = PoseState.UP rep_count += 1 # 完成一次计数! # 在图像上绘制姿态和计数 mp_drawing.draw_landmarks(image, results.pose_landmarks, mp_pose.POSE_CONNECTIONS) cv2.putText(image, f'State: {current_state.name}', (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) cv2.putText(image, f'Count: {rep_count}', (10, 70), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) cv2.putText(image, f'Angle: {smoothed_angle:.1f}', (10, 110), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 0), 2) cv2.imshow('Push-up Counter', image) if cv2.waitKey(5) & 0xFF == 27: break cap.release() cv2.destroyAllWindows()5.2 如何评估你的模型?
不能光说“看起来挺准”,需要有量化的评估指标。我建议从两个层面进行:
计数准确率:录制一段包含已知次数(如20次)标准俯卧撑的视频。用你的程序跑一遍,记录计数结果。计算:
- 准确率= (程序计数 / 真实次数) * 100%。理想应接近100%。
- 误差分析:是漏计数(没检测到)还是多计数(误触发)?结合状态机日志,分析是哪个状态转换判断出了问题。
鲁棒性测试:
- 不同速度:快速俯卧撑和慢速俯卧撑。
- 不同幅度:半程俯卧撑和全程俯卧撑。
- 干扰测试:在背景中轻微走动;穿着不同颜色/款式的衣服。
- 边界测试:动作在摄像头边缘;光照突然变化。
记录下每种情况下的准确率。这个测试过程不仅能评估模型,更是发现算法缺陷、优化阈值和逻辑的最佳途径。
6. 超越计数:项目的延伸思考与优化方向
完成基本计数后,这个项目还有巨大的深化空间,可以从一个“计数器”进化为一套“体能训练辅助系统”。
6.1 从计数到质量评估
可以计算并反馈更多指标:
- 节奏分析:计算每次俯卧撑的下降和上升阶段分别用时,提示用户保持稳定节奏(如“下降过快”)。
- 不对称性检测:比较左右肩、左右髋的高度差,判断身体是否倾斜,预防肌肉力量不平衡导致的损伤。
- 耐力曲线:绘制每次俯卧撑到达最低点的时间(或速度)随次数变化的曲线,直观展示力竭过程。
6.2 多模态反馈与交互
- 语音提示:使用
pyttsx3或gTTS库,在计数时播报数字,在动作不标准时给出语音提醒(“请保持核心收紧”)。 - 可视化报告:训练结束后,生成一个简单的报告页面,展示总次数、平均速度、动作标准度曲线等。
- 云端存储与历史对比:将每次训练数据上传到数据库,用户可以看到自己的历史进步曲线。
6.3 部署到边缘设备
让模型脱离电脑,真正走进健身房:
- 树莓派+小型触摸屏:制作一个便携设备。优化代码后,在树莓派上可以达到不错的实时性。
- 安卓/iOS App:利用MediaPipe的移动端库,开发手机应用。用户将手机放在地面即可使用,最为便捷。
- Web应用:使用TensorFlow.js或ONNX Runtime Web,配合浏览器的WebRTC调用摄像头,无需安装任何软件,打开网页即可使用。
6.4 模型轻量化与加速
如果对延迟有极致要求,可以探索:
- 模型量化:将MediaPipe的浮点模型转换为INT8精度,在几乎不损失精度的情况下大幅提升推理速度。
- 模型剪枝:移除网络中不重要的连接或神经元,得到一个更小、更快的模型。
- 专用硬件:使用带有NPU(神经网络处理单元)的开发板,如华为昇腾Atlas 200 DK或英伟达Jetson Nano,能获得飞跃式的性能提升。
回过头看,这个毕业设计项目带给我的远不止一个计数程序。它是一次完整的“问题定义 -> 技术选型 -> 算法实现 -> 工程调优 -> 评估部署”的微型项目演练。最大的体会是:在机器视觉项目中,算法只占一半,另一半是工程。如何让算法在复杂、非受控的真实环境中稳定工作,如何设计人性化的交互和反馈,这些问题的挑战性和价值,丝毫不亚于模型本身的调优。如果你也在做类似的课题,希望我的这些踩坑经验和实现思路,能帮你少走些弯路,更快地做出一个既“炫酷”又“扎实”的作品。