基于计算机视觉(opencv)的运动计数系统
Notice:本文的源码均为开源代码,(由于文章篇幅有限,文章中的源码均是伪代码),需要源码的小伙伴可以先关注我,私信或评论与我联系,大家一起学习
1、项目简介
1.1研究背景及意义
随着全民健身热潮的兴起,越来越多的人积极参加健身锻炼,但由于缺乏科学的运动指导,使健身难以取得相应的效果。据市场调查显示,没有产品可以自动分析健身运动并提供指导。而近年深度神经网络在人体姿态识别上已经取得了巨大的成功,针对这个现象,本课程设计制作了一个基于MediaPipe的运动计数系统。该系统主要内容包括对于单人人体关键点的检测,关键点的连接,以及运动健身关键点的角度变化来实现对俯卧撑和深蹲的计数。
1.2项目设计方案
系统框架如下图所示,本系统将视频或者摄像头实时捕捉的俯卧撑或者深蹲的视频一步步进行处理,首先我们获得俯卧撑和深蹲的训练样本,之后将训练样本转换为k-NN分类训练集,将预测的landmarks转移储存至CSV文件中,之后进行KNN算法分类。最后实现重复计数将结果显示在面板中。
具体方案如下:
(1)收集目标练习的图像样本并对其进行姿势预测
(2)将获得的姿态标志转换为适合 k-NN 分类器的数据,并使用这些数据形成训练集
(3)执行分类本身,然后进行重复计数
(4)面板显示结果
2、程序设计
2.1 程序设计方案
2.1.1 建立训练样本
为了建立一个好的分类器,应该为训练集收集适当的样本:理论上每个练习的每个最终状态大约有几百个样本(例如,俯卧撑和深蹲的“向上”和“向下”位置),收集的样本涵盖不同的摄像机角度、环境条件、身体形状和运动变化,这一点很重要,能做到这样最好。但实际中如果嫌麻烦的话每种状态15-25张左右都可以,然后拍摄角度要注意多样化,最好每隔15度拍摄一张。
具体代码如下:
训练样本图片格式 Required structure of the images_in_folder: fitness_poses_images_in/ squat_up/ image_001.jpg image_002.jpg ... squat_down/ image_001.jpg image_002.jpg
2.1.2 获取归一化landmarks
要将样本转换为 k-NN 分类器训练集,在给定图像上运行 BlazePose 模型,并将预测的landmarks转储到 CSV 文件中。此外,Pose Classification Colab (Extended)通过针对整个训练集对每个样本进行分类,提供了有用的工具来查找异常值(例如,错误预测的姿势)和代表性不足的类别(例如,不覆盖所有摄像机角度)。之后,能够在任意视频上测试该分类器。fitness_poses_csvs_out文件夹里面的csv文件就是使用拍摄的训练样本提取出来的深蹲和俯卧撑的训练集文件,有了该文件后就可以直接运行本项目。生成的CSV文件如图:
具体代码如下:
class FullBodyPoseEmbedder(object): Converts 3D pose landmarks into 3D embedding.""" def __init__(self, torso_size_multiplier): 乘数应用于躯干以获得最小的身体尺寸 def __call__(self, landmarks): Normalizes pose landmarks and converts to embedding 获取 landmarks.归一化姿势landmarks并转换为embedding 具有形状 (M, 3) 的姿势embedding的 Numpy 数组,其中“M”是“_get_pose_distance_embedding”中定义的成对距离的数量。 return embedding def _normalize_pose_landmarks(self, landmarks): Normalizes landmarks translation and scale.归一化landmarks的平移和缩放 return landmarks def _get_pose_center(self, landmarks): Calculates pose center as point between hips.将姿势中心计算为臀部之间的点。 return center def _get_pose_size(self, landmarks, torso_size_multiplier): Calculates pose size.计算姿势大小。 它是下面两个值的最大值: * 躯干大小乘以`torso_size_multiplier` * 从姿势中心到任何姿势地标的最大距离 这种方法仅使用 2D landmarks来计算姿势大小分别计算 臀部中心、两肩中心、躯干尺寸作为最小的身体尺寸到姿势中心的最大距离. return max(torso_size * torso_size_multiplier, max_dist) def _get_pose_distance_embedding(self, landmarks): Converts pose landmarks into 3D embedding. 将姿势landmarks转换为 3D embedding. 我们使用几个成对的 3D 距离来形成姿势embedding。 所有距离都包括带符号的 X 和 Y 分量。 我们使用不同类型的对来覆盖不同的姿势类别。 Feel free to remove some or add new.
2.1.3 使用KNN算法分类
用于姿势分类的 k-NN 算法需要每个样本的特征向量表示和一个度量来计算两个这样的向量之间的距离,以找到最接近目标的姿势样本。
为了将姿势标志转换为特征向量,使用预定义的姿势关节列表之间的成对距离,例如手腕和肩膀、脚踝和臀部以及两个手腕之间的距离。由于该算法依赖于距离,因此在转换之前所有姿势都被归一化以具有相同的躯干尺寸和垂直躯干方向。
可以根据运动的特点选择所要计算的距离对(例如,引体向上可能更加关注上半身的距离对)。
为了获得更好的分类结果,使用不同的距离度量调用了两次 KNN 搜索:
首先,为了过滤掉与目标样本几乎相同但在特征向量中只有几个不同值的样本(这意味着不同的弯曲关节和其他姿势类),使用最小坐标距离作为距离度量,然后使用平均坐标距离在第一次搜索中找到最近的姿势簇。
最后,应用指数移动平均(EMA) 平滑来平衡来自姿势预测或分类的任何噪声。不仅搜索最近的姿势簇,而且计算每个姿势簇的概率,并将其用于随着时间的推移进行平滑处理。
class PoseSample(object): 获取捕捉到的图像landmarks class PoseClassifier(object): 根据捕捉到图像的landmarks,对图像进行分类 *根据提供的训练图片,将训练的图片分类,例如蹲起或者俯卧撑,分为两类,放入不同文件夹 *根据不同类别的图片分别生成.csv的文件 训练图片的文件结构: neutral_standing.csv pushups_down.csv pushups_up.csv squats_down.csv ... csv文件结构: sample_00001,x1,y1,z1,x2,y2,z2,.... sample_00002,x1,y1,z1,x2,y2,z2,.... return pose_samples def find_pose_sample_outliers(self): 针对整个数据库对每个样本进行分类 *找出目标姿势中的异常值 *为目标找到最近的姿势 *如果最近的姿势具有不同的类别或多个姿势类别被检测为最近,则样本是异常值 return outliers def __call__(self, pose_landmarks): 对给定的姿势进行分类。 分类分两个阶段完成: *按 MAX 距离选取前 N 个样本。 它允许删除与给定姿势几乎相同但有一些关节在向一个方向弯曲的样本。 *按平均距离选择前 N 个样本。 在上一步移除异常值后, 我们可以选择在平均值上接近的样本。 *按平均距离过滤,去除异常值后,我们可以通过平均距离找到最近的姿势 return result
2.1.4 计数实现
为了计算重复次数,该算法监控目标姿势类别的概率。深蹲的“向上”和“向下”终端状态:
当“下”位姿类的概率第一次通过某个阈值时,算法标记进入“下”位姿类。
一旦概率下降到阈值以下(即起身超过一定高度),算法就会标记“向下”姿势类别,退出并增加计数器。
为了避免概率在阈值附近波动(例如,当用户在“向上”和“向下”状态之间暂停时)导致幻像计数的情况,用于检测何时退出状态的阈值实际上略低于用于检测状态退出的阈值。
动作计数器 class RepetitionCounter(object): # 计算给定目标姿势类的重复次数 def __init__(self, class_name, enter_threshold=6, exit_threshold=4): self._class_name = class_name # 如果姿势通过了给定的阈值,那么我们就进入该动作的计数 self._enter_threshold = enter_threshold self._exit_threshold = exit_threshold # 是否处于给定的姿势 self._pose_entered = False # 退出姿势的次数 self._n_repeats = 0 @property def n_repeats(self): return self._n_repeats def __call__(self, pose_classification): # 计算给定帧之前发生的重复次数 # 我们使用两个阈值。首先,您需要从较高的位置上方进入姿势,然后您需要从较低的位置下方退出。 # 阈值之间的差异使其对预测抖动稳定(如果只有一个阈值,则会导致错误计数)。 # 参数: # pose_classification:当前帧上的姿势分类字典 # Sample: # { # 'squat_down': 8.3, # 'squat_up': 1.7, # } # 获取姿势的置信度. pose_confidence = 0.0 if self._class_name in pose_classification: pose_confidence = pose_classification[self._class_name] # On the very first frame or if we were out of the pose, just check if we # entered it on this frame and update the state. # 在第一帧或者如果我们不处于姿势中,只需检查我们是否在这一帧上进入该姿势并更新状态 if not self._pose_entered: self._pose_entered = pose_confidence > self._enter_threshold return self._n_repeats # 如果我们处于姿势并且正在退出它,则增加计数器并更新状态 if pose_confidence < self._exit_threshold: self._n_repeats += 1 self._pose_entered = False return self._n_repeats
2.1.5 界面实现
利用Python的QtDesinger来进行设计界面,添加摄像头捕捉区域,以及捕捉到的图像的最高点和最低点的变化曲线区域,和计算运动次数并显示的区域。整体采用了水平的布局,添加按钮以及界面关闭提示。摄像头则采用英文状态下,输入“q”来进行退出,方法很简单。具体运行结果如下图:
class Ui_MainWindow(object): def __init__(self): self.RowLength = 0 def setupUi(self, MainWindow): MainWindow.setObjectName("MainWindow") MainWindow.resize(1213, 680) 设置窗体固定大小 MainWindow.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly) 图片区域 self.centralwidget.setObjectName("centralwidget") 设置窗口大小 self.tableWidget = QtWidgets.QTableWidget(self.scrollAreaWidgetContents_1) 设置布局 设置窗口大小及颜色 self.tableWidget.setObjectName("tableWidget") self.tableWidget.setColumnCount(6) 设置1列的宽度 设置2列的宽度 设置3列的宽度 设置4列的宽度 设置5列的宽度 设置6列的宽度 self.tableWidget.setRowCount(self.RowLength) 隐藏垂直表头 self.tableWidget.setEditTriggers(QAbstractItemView.NoEditTriggers) self.tableWidget.raise_() self.scrollArea_4.setWidget(self.scrollAreaWidgetContents_4) MainWindow.setCentralWidget(self.centralwidget) self.statusbar = QtWidgets.QStatusBar(MainWindow) self.statusbar.setObjectName("statusbar") MainWindow.setStatusBar(self.statusbar) self.retranslateUi(MainWindow) QtCore.QMetaObject.connectSlotsByName(MainWindow) 获取当前工程文件位置 def retranslateUi(self, MainWindow): _translate = QtCore.QCoreApplication.translate 设置UI界面标题为:基于姿态估计的运动计数系统 设置左侧UI界面横坐标为:最高点与最低点高度变化曲线 设置左侧UI界面标题为:摄像头捕捉 设置右侧UI界面标题为:计数区域 self.scrollAreaWidgetContents_1.show()
2.1.6 入口函数main
有两种选择模式,可以从本地上传视频进行检测,也可以调用摄像头进行实时检测。具体的模式选择可以通过输入选择的数字进行选择:
if __name__ == "__main__": app = QtWidgets.QApplication(sys.argv) 调用主窗口函数 调用UI界面 while True: menu = int(input("请输入检测模式(数字):1. 从本地导入视频检测\t2. 调用摄像头检测\n")) if 输入数字为1: flag = int(input("请输入检测的运动类型(数字):1. 俯卧撑\t2. 深蹲\n")) video_path = input("请输入视频路径:") tp.trainset_process(flag) vp.video_process(video_path, flag) continue elif 输入数字为2: flag = int(input("请输入检测的运动类型(数字):1. 俯卧撑\t2. 深蹲\n")) print("\n按英文状态下的q或esc退出摄像头采集") tp.trainset_process(flag) vc.process(flag) continue elif 输入数字为3: break else: print("输入错误,请重新输入!") continue sys.exit(app.exec_())
3、成果展示
由我们组成员分别对系统的镜头采集功能和视频采集功能进行了实验,当选择镜头采集功能时,利用opencv调用电脑摄像头进行采集画面。
如图所示为打开摄像头界面,左边是摄像头的捕捉区域,并附带检测屏幕中人体最高点和最低点的曲线变化图;当曲线发生阶跃性突变,且达到一定的峰值,摄像头中的右上方的数字就会进行计数。上图选择的模式为蹲起计数,同时我们也添加了俯卧撑计数的模式,只需在模式选择下进行调整即可。模型会根据选择的模式,自动进行分类。
上图可以清晰的看出测试者在经过蹲起测试之后截下的图片,一共测试了16次蹲起,曲线形象的记录下了每一次的过程,蓝色表示最高点的变化,黄色表示最低点的变化。
上图采用的是对视频中的运动进行计数,与摄像头调用是类似的方法,此方法可以直接生成结果视频,可视化的效果对计数结果进行验证。
上图为视频中计算的蹲起的计数
4、结论及展望
本设计实现的人体姿态识别网络模型虽然可以在一般设备上运行,并且基本达到了实时检测的效果,但总体来说检测速度还是偏慢。目前所达到的水平还有待提升,需要在保证精度的情况下,进一步进行轻量化。
本文中的人体姿态识别的过程中摄像头不移动并且固定在一个位置,因此只能检测到特定区域的人体动作姿态,如果人体所在位置超出该摄像头的覆盖范围,那么系统就无法识别人体的动作姿态。所以下一步可以考虑将摄像机安装在机器人上,通过激光雷达和目标跟踪等技术控制机器人对人体进行实时跟踪,通过移动的摄像头实时捕捉人体的动作姿态,可以在更大范围内更加有效的识别人体动作姿态。
至此,文章主要内容介绍完毕,项目除上述主要模块以外,还包含训练数据集等文件,需要源码的朋友关注再私信与我联系,本人看到一定回复!!!!