一、前言
继续上篇文章叙述,上一篇主要讲解了动作识别的基础知识和研究内容。如果没有了解,可浏览上一篇文章基于动作识别的健身动作识别记录系统
本文主要解释系统的具体实现,其中包括主要实现模块和方法,以及相关的主要代码。
二、Mediapipe框架
Mediapipe是谷歌公司开发的一款开源框架,是一种基于机器视觉的数据处理方式 ,MediaPipe 可以用于处理多种形式的数据输入 包括但不限于图片、视频、音频、文本。MediaPipe显著优势是可移植性高,支持多平台部署, 同时可以部署在GPU和CPU。
通过MediaPipe,输入数据可以被分解成由图形模块表示的多个数据流管道 , 通过人体姿态估计检测器和姿态跟踪网络(如下图)。跟踪网络预测关键点坐标和判断当前帧的任务是否存在。当跟踪器反馈没有人时时,会在下一帧重新运行检测器网络。
2.1、人体检测
多数物体检测的解决方案都通过非最大抑制(NMS)算法进行最后的处理。这对于自由度较小的检测显然是很实用的。但是,NMS会分解成包含高度清晰的人体姿势的帧,例如握手、深蹲。这是因为多个不确定的框值满足NMS算法的交集并集(IoU)阈值。
Mediapipe通过通过检测人体相对不变或相对刚性的人体部位(如人脸和脊柱),同时通过人脸检测器可预测其他特定于人体的对齐参数这一特性,作为人体检测的代理,计算臀部之间的中间点、环绕整个人的圆圈的大小和倾斜度(连接两个中肩和中臀部点的线之间的角度),从结果看这是有效的。
2.2、实现方法
人体姿态评估中,常见的方法是为每个骨关节点生成热图,同时通过深度学习或者卷积神经网络优化骨关节点坐标的偏移量。这种热图可以在资源使用尽可能小的情况下实现多人检测,但是对于单人的预测却是有过多的资源浪费,模型也要更大。Mediapipe实验中显示,对于这种特例的预测,并通过模型的显着加速,几乎没有质量的下降。
与基于热图的技术相比,基于回归的方法虽然计算要求较低且更具可扩展性,但通过预测平均值坐标,无法避免潜在的数据模糊,精度和可靠性远远不够。即使参数数量较少,堆叠沙漏架构也能显著提高预测质量。Mediapipe使用编码器-解码器网络架构来预测所有关节的热图,并通过另一个编码器降低热图数量,该编码器直接回归到所有骨关节的坐标。简而言之,就是热图分支可以在推理过程中被丢弃,这样推理模型可以足够轻便,在移动设备可快速运行。它的梯度停止连接如下图所示。
三、系统实现
本章节主要通过四个方面实现:采集骨骼关键点的信息、获取骨骼角度并设定阈值、定义运动状态、系统界面实现。下面依次论述讲解。
3.1、骨骼关键点信息采集
Mediapipe是由谷歌公司开发的开源机器学习应用框架,可以直接采用其预训练好的模型或者自己提供数据进行训练自定义模型。Mediapipe可以实现物体检测、图像分类、手势识别、人体坐标等功能。本文使用的是Mediapipe中对人体骨骼关键点采集的Pose模块实现数据信息采集,谷歌有自己的在线平台,通过在线获取摄像头图像进行识别演示。如果感兴趣可以点击骨骼关键点识别在线演示自己尝试一下。
因为我是提取特定关键点坐标,所有详细的关键点坐标获取可以参考骨骼坐标获取这位博主的文章。
1)、提取特定坐标
# 从姿势关键点数据中提取特定关键点的坐标
def get_landmark_features(kp_results, dict_features, feature, frame_width, frame_height):
# 参数:包含姿势关键点信息的数据结构、特征关键点的名称和对应的关键点索引号字典、要提取的特征名称、帧的宽度和高度
if feature == 'nose':
return get_landmark_array(kp_results, dict_features[feature], frame_width, frame_height)
# 提取鼻子的特征点及其坐标
elif feature == 'left' or feature == 'right':
shldr_coord = get_landmark_array(kp_results, dict_features[feature]['shoulder'], frame_width, frame_height)
elbow_coord = get_landmark_array(kp_results, dict_features[feature]['elbow'], frame_width, frame_height)
wrist_coord = get_landmark_array(kp_results, dict_features[feature]['wrist'], frame_width, frame_height)
hip_coord = get_landmark_array(kp_results, dict_features[feature]['hip'], frame_width, frame_height)
knee_coord = get_landmark_array(kp_results, dict_features[feature]['knee'], frame_width, frame_height)
ankle_coord = get_landmark_array(kp_results, dict_features[feature]['ankle'], frame_width, frame_height)
foot_coord = get_landmark_array(kp_results, dict_features[feature]['foot'], frame_width, frame_height)
# 获得肩膀、肘部、手腕、臀部、膝盖、脚踝、脚的坐标
return shldr_coord, elbow_coord, wrist_coord, hip_coord, knee_coord, ankle_coord, foot_coord
else:
raise ValueError("feature needs to be either 'nose', 'left' or 'right")
2)、坐标转换
# 把姿势关键点坐标(以相对于帧尺寸的百分比表示)转换为绝对坐标值
def get_landmark_array(pose_landmark, key, frame_width, frame_height):
# 接受关键点坐标信息、关键点索引号、帧的宽度和高度
denorm_x = int(pose_landmark[key].x * frame_width)
denorm_y = int(pose_landmark[key].y * frame_height)
# 把相对于帧尺寸的百分比坐标乘以帧的宽度和高度,从而得到姿势关键点的绝对水平和垂直坐标值
return np.array([denorm_x, denorm_y])
3)、创建模型对象
# 创建一个 MediaPipe Pose 模型对象
def get_mediapipe_pose(
static_image_mode=False,
model_complexity=1,
smooth_landmarks=True,
min_detection_confidence=0.5,
min_tracking_confidence=0.5
):
# 参数:是否为静态img、模型复杂度、是否对关键点平滑处理、确定姿势检测和跟踪的最小置信度阈值
pose = mp.solutions.pose.Pose(
static_image_mode=static_image_mode,
model_complexity=model_complexity,
smooth_landmarks=smooth_landmarks,
min_detection_confidence=min_detection_confidence,
min_tracking_confidence=min_tracking_confidence
)
# 从 MediaPipe 库导入mp.solutions.pose.Pose这个类,并将创建的模型对象存储在pose里,实现姿势估计。
return pose
3.2、角度获取并设计阈值
通过前期准备工作,我们获取了相应骨骼关键点相应信息。在体育锻炼时,人体骨骼会形成特定的角度。我们可以通过计算得到相应角度来判断人体所处状态和做出的动作,例如在人体摔倒检测报警应用程序中,当人体摔倒后可以通过肩膀和髋关节连线与垂直方向夹角变化来进行判断预警。在体育锻炼中我们也是采用这种方法,相对于关键点的垂线进行角度计算。
通过前面的角度设计和计算,我们已经具备阈值设计相关基础,为此我们需要对体育锻炼动作阈值进行设计和划分,同时为下一步动作状态提供基础。
深蹲动作的阈值划分我们设计选取的是三个角度(如下图),分别是髋关节、膝关节、踝关节的角度。
1)、计算角度的函数
# 计算两个向量之间的夹角最后转成度数
def find_angle(p1, p2, ref_pt=np.array([0, 0])):
p1_ref = p1 - ref_pt
p2_ref = p2 - ref_pt
# find_angle函数有p1和p2两个向量和ref_pt原点向量坐标
cos_theta = (np.dot(p1_ref, p2_ref)) / (1.0 * np.linalg.norm(p1_ref) * np.linalg.norm(p2_ref))
# 通过向量的点积来计算两个向量夹角的余弦值
theta = np.arccos(np.clip(cos_theta, -1.0, 1.0))
# 返回cos值,并确保其在-1到1之间
degree = int(180 / np.pi) * theta
# 把弧度值转换成度数并取整
return int(degree)
2)、各个关节角度计算
# 使用 find_angle 函数计算各部分的垂直角度。
# 这个角度是通过肩膀坐标、一个假想的垂直点(与髋部同x坐标,y坐标设为0),以及髋部坐标来计算的
hip_vertical_angle = find_angle(shldr_coord, np.array([hip_coord[0], 0]), hip_coord)
# 在髋部坐标处绘制一个椭圆形,用于表示髋部的垂直角度。multiplier 根据是左侧还是右侧的身体关键点,调整角度的正负,以适应不同的视角
cv2.ellipse(frame, hip_coord, (30, 30),
angle=0, startAngle=-90, endAngle=-90 + multiplier * hip_vertical_angle,
color=self.COLORS['white'], thickness=3, lineType=self.linetype)
# 在髋部位置绘制一条垂直的虚线,让角度更直接可视化
draw_dotted_line(frame, hip_coord, start=hip_coord[1] - 80, end=hip_coord[1] + 20,
line_color=self.COLORS['blue'])
# 这是髋关节和膝关节连线与垂直方向夹角实现
knee_vertical_angle = find_angle(hip_coord, np.array([knee_coord[0], 0]), knee_coord)
cv2.ellipse(frame, knee_coord, (20, 20),
angle=0, startAngle=-90, endAngle=-90 - multiplier * knee_vertical_angle,
color=self.COLORS['white'], thickness=3, lineType=self.linetype)
draw_dotted_line(frame, knee_coord, start=knee_coord[1] - 50, end=knee_coord[1] + 20,
line_color=self.COLORS['blue'])
# # 这是膝关节和踝关节连线与垂直方向夹角实现
ankle_vertical_angle = find_angle(knee_coord, np.array([ankle_coord[0], 0]), ankle_coord)
cv2.ellipse(frame, ankle_coord, (30, 30),
angle=0, startAngle=-90, endAngle=-90 + multiplier * ankle_vertical_angle,
color=self.COLORS['white'], thickness=3, lineType=self.linetype)
draw_dotted_line(frame, ankle_coord, start=ankle_coord[1] - 50, end=ankle_coord[1] + 20,
line_color=self.COLORS['blue'])
3)、阈值设计
def get_thresholds_beginner():
_ANGLE_HIP_KNEE_VERT = {
'NORMAL': (0, 32),
'TRANS': (35, 65),
'PASS': (70, 95)
}
# 字典有髋关节和膝盖关键点连线与垂直方向的角度,三种状态阈值范围:s1正常、s2过渡、s3通过
thresholds = {
# 关联到髋关节和膝盖
'HIP_KNEE_VERT': _ANGLE_HIP_KNEE_VERT,
# 髋关节角度阈值范围
'HIP_THRESH': [10, 50],
# 踝关节角度阈值
'ANKLE_THRESH': 45,
# 膝盖角度的阈值范围
'KNEE_THRESH': [50, 70, 95],
# 偏移阈值
'OFFSET_THRESH': 35.0,
# 不活动阈值
'INACTIVE_THRESH': 15.0,
# 帧计数阈值
'CNT_FRAME_THRESH': 50
}
return thresholds
3.3、定义运动状态
体育锻炼是一个多个连贯动作组成的过程,这个过程大部分是连贯且具有循环往复的特点。为此我们为深蹲和仰卧起坐都定义了三个状态,完成一个循环记作一个体育锻炼动作的完成。下面详细展开论述状态的定义和如何判断是否正确完成一个动作并计数。
我们为深蹲和仰卧起坐都划分了三个状态,分别是初始、过渡、通过。在进行深蹲动作时我们发现,大腿是主要发力点和变化点,为此我们通过膝关节角度定义状态阈值。同样的,在进行仰卧起坐动作时,腹部是主要发力部位,所有我们通过髋关节角度定义三个状态阈值。
1)、计数实现
程序初始化设定错误姿势为False,如果状态列表长度为3(列表正确添加完毕三个状态)并且没有错误姿势,我们的正确深蹲计数器就会增加一个计数;如果状态列表长度为1,并且出现过渡状态,说明动作序列不正确,错误深蹲计数器增加一个计数,同时也会出现某个状态时错误的姿势导致错误深蹲计数器增加的情况,例如下文会说到的膝盖没有落在脚上和下蹲过深。
在深蹲动作中,由于我们的状态判断是依据膝关节角度判断,但是髋关节角度和踝关节角度依旧会影响深蹲动作的正确性,所以我们增加了更严格的判断条件。通过研究设计,我们设定了五个反馈信息:向前弯腰、向后弯腰、下蹲过渡、降低臀部、保持膝盖落在脚上。
# 如果当前状态为s1
if current_state == 's1':
# 如果三个动作序列全完成且没有错误动作,说明完成了一个正确深蹲动作
if len(self.state_tracker['state_seq']) == 3 and not self.state_tracker['INCORRECT_POSTURE']:
# 深蹲计数器+1
self.state_tracker['SQUAT_COUNT'] += 1
# 准备播放的声音设置为当前深蹲的计数
play_sound = str(self.state_tracker['SQUAT_COUNT'])
# 检查state_seq列表中是否包含s2状态,并且列表的长度为1。如果为真,表示动作序列不正确
elif 's2' in self.state_tracker['state_seq'] and len(self.state_tracker['state_seq']) == 1:
# 错误深蹲计数器+1
self.state_tracker['IMPROPER_SQUAT'] += 1
# 准备播放表示不正确的声音
play_sound = 'incorrect'
# 检查是否存在不正确的姿势。如果为真,也将不正确的深蹲次数加1,并准备播放不正确的声音
elif self.state_tracker['INCORRECT_POSTURE']:
self.state_tracker['IMPROPER_SQUAT'] += 1
play_sound = 'incorrect'
# 最后重置深蹲状态序列,假设下一个动作为正确动作
self.state_tracker['state_seq'] = []
self.state_tracker['INCORRECT_POSTURE'] = False
3.4、系统界面实现
对于一个程序而言,一个简洁便利的界面可以带给使用者更好的体验,同时也可以让程序功能实现可视化。为此,我们采取Streamlit框架。Streamlit是一个开源的Python框架,能够通过少量代码快速创建和部署动态的数据应用程序。使用此框架,可以在较少时间内搭建出功能强大的数据应用程序。
import streamlit as st
st.title('AI健身教练:深蹲分析')
st.header('更多功能请点击左侧')
st.text('实时视频-->Live 上传视频-->Upload')
# 通过路径把视频传到st.empty()里
recorded_file = 'demo_video.mp4'
sample_vid = st.empty()
sample_vid.video(recorded_file)
四、总结
对于本文的基于骨骼关键点体育运动的动作识别,实现了深蹲和仰卧起坐的识别和记录。主要成果如下:
- 基于Mediapipe框架,在人体动作识别中采用单侧的人体骨骼关键点识别。实现了深蹲和仰卧起坐体育运动的识别和记录,并将相关纠正信息反馈给使用者。一方面,降低了设备的运行功耗;另一方面,提高对于设备反应速度。
- 通过骨骼关键点为原点坐标的计算方式,简化了骨骼角度阈值的计算和提高了动作识别的准确性。
- 对体育锻炼动作的三个状态划分具有普适性,通过关键骨骼的角度阈值设定,实现了体育动作的识别和计数。在拓展其他体育运动时可以更好更快的进行状态识别和记录相应运动量。
- 基于Streamlit实现了云端部署,可以让使用者在PC网络端进行访问和使用。