Mediapipe和支持向量机实现网球击球动作检测
这个小项目是我的机器视觉课程设计的一部分,没有什么很好的创新点也比较粗糙,在这里记录一下整个实现流程,便于之后的学习,也是一个简单的分享。
感谢PDM和PKX两位同学对建立数据集的贡献。
简介
球场上单边运动员动作的检测:能够判断出当前球员正在进行的击球动作,共分为正手,反手以及发球三种,用于球路预测算法以及可能的运动员训练。
我和两位同学自行制作了三个击球动作类别的数据集。
每个数据集中的照片,使用Mediapipe进行单人人体骨骼提取并识别关键点,使用关键点坐标计算多个自定义的的角度,然后将这些角度作为特征使用支持向量机算法训练出多分类分类器。
对测试视频中的每一帧,用同样的方法计算出各特征角度后,用分类器进行分类并给出该帧运动员动作属于该类别的概率。
通过对数据集、特征角度和各种参数的调试,最终使分类器达到准确度的要求。
简单来说,这个识别的流程就是:
获取人体关键点-求取特征角度-进行分类-判断属于该类别的概率是否足够高
特征获取
特征获取除了在训练分类器之前要获取数据集图片的特征来生成data,在检测的过程中,对于每一帧都要重复这个过程
关键点提取-Mediapipe
Mediapipe是一个很好用的框架,对于单人来说效果也很好。
Mediapipe可以提取一共32个特征点,而我们并不需要识别这么多。因此只获取了左右肩部、手肘、手腕、胯、膝盖、脚踝关键点以及用于表示人物头部的鼻子关键点,记录下他们在图片上的x、y坐标。
部分代码如下:
landmarks = results.pose_landmarks.landmark
# 获取相应关键点的坐标
#左肩11
lshoulder = [landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].x,
landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].y]
#左手肘13
lelbow = [landmarks[mp_pose.PoseLandmark.LEFT_ELBOW.value].x,
landmarks[mp_pose.PoseLandmark.LEFT_ELBOW.value].y]
#左手腕15
lwrist = [landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].x,
landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].y]
#左胯23q
lhip = [landmarks[mp_pose.PoseLandmark.LEFT_HIP.value].x,
landmarks[mp_pose.PoseLandmark.LEFT_HIP.value].y]
# 左膝盖25
lknee = [landmarks[mp_pose.PoseLandmark.LEFT_KNEE.value].x,
landmarks[mp_pose.PoseLandmark.LEFT_KNEE.value].y]
#左脚踝27
lankle = [landmarks[mp_pose.PoseLandmark.LEFT_ANKLE.value].x,
landmarks[mp_pose.PoseLandmark.LEFT_ANKLE.value].y]
#右肩膀12
rshoulder = [landmarks[mp_pose.PoseLandmark.RIGHT_SHOULDER.value].x,
landmarks[mp_pose.PoseLandmark.RIGHT_SHOULDER.value].y]
#右手肘14
relbow = [landmarks[mp_pose.PoseLandmark.RIGHT_ELBOW.value].x,
landmarks[mp_pose.PoseLandmark.RIGHT_ELBOW.value].y]
#右手腕16
rwrist = [landmarks[mp_pose.PoseLandmark.RIGHT_WRIST.value].x,
landmarks[mp_pose.PoseLandmark.RIGHT_WRIST.value].y]
#右胯24
rhip = [landmarks[mp_pose.PoseLandmark.RIGHT_HIP.value].x,
landmarks[mp_pose.PoseLandmark.RIGHT_HIP.value].y]
# 右膝盖26
rknee = [landmarks[mp_pose.PoseLandmark.RIGHT_KNEE.value].x,
landmarks[mp_pose.PoseLandmark.RIGHT_KNEE.value].y]
#右脚踝28
rankle = [landmarks[mp_pose.PoseLandmark.RIGHT_ANKLE.value].x,
landmarks[mp_pose.PoseLandmark.RIGHT_ANKLE.value].y]
# 鼻子
nose = [landmarks[mp_pose.PoseLandmark.NOSE.value].x,
landmarks[mp_pose.PoseLandmark.NOSE.value].y]
特征角度
图片上运动员的大小、运动员的身材体格等都不一致,但同种动作的姿势相似。因而主要将运动员肢体各部分或是关节的角度作为识别不同种类的特征。
部分代码如下:
# 计算角度
langle = calculate_angle(lshoulder, lelbow, lwrist)#左胳膊角度
rangle = calculate_angle(rshoulder, relbow, rwrist)#右胳膊角度
lsangle = calculate_angle(lhip, lshoulder, lelbow)#左臂离身体角度
rsangle = calculate_angle(rhip, rshoulder, relbow)#右臂离身体角度
lhangle = calculate_angle(lshoulder, lhip, lknee) # 左肩膀-胯-膝盖角度
rhangle = calculate_angle(rshoulder, rhip, rknee)#右肩膀-胯-膝盖角度
lkangle = calculate_angle(lankle, lknee, lhip) # 左腿角度
rkangle = calculate_angle(rankle, rknee, rhip)#右腿角度
之后将这些角度数据作为每个动作分类的特征,生成数据即可
为了便于后续训练,在数据中将四种动作类别用0、1、2、3分别表示正手、反手、发球和准备姿势。
数据示例如下图:
使用支持向量机训练分类器
使用生成好的代码,调用sklearn中的svm进行支持向量机模型的训练,使用ovr(一对多)构造多分类分类器,在训练的过程中需要设置交叉验证,使其在之后的检测过程中可以输出当前动作属于某分类的概率为多少。在训练结束后,使用测试集对模型的准确度进行评估并输出评估的结果。Micro是使用总体样本的准召率直接计算f1-score。
最后训练集和测试集评估得分都是0.78。
部分代码如下:
ata = pd.read_csv(r'D:\tennis_pose\data.csv')
dx= data[['langle', 'rangle', 'lsangle', 'rsangle', 'lhangle', 'rhangle', 'lkangle','rkangle']]
x = np.array(dx)
dy=data[['class']]
y= np.array(dy)
y=np.transpose(y)[0]
x_train,x_test,y_train,y_test = train_test_split(x,y,random_state=20)
#print(x)
#print(y)
# 训练模型
model = svm.SVC(gamma='scale', C=1.2, decision_function_shape='ovr', kernel='rbf')#或poly ovo ovr:one vs rest
# 进行训练
model= svm.SVC(probability = True)#交叉验证,之后可输出概率
model.fit(x_train, y_train)
# 预测结果
result_train = model.predict(x_train)
result_test = model.predict(x_test)
# 进行评估
print("train-score: {0:.2f}".format(f1_score(result_train,y_train,average='micro')))
print("test-score: {0:.2f}".format(f1_score(result_test,y_test,average='micro')))
编写检测程序
读取视频,对每一帧提取人物姿态计算八个特征角度,调用训练好的分类器进行预测,记录分类结果和概率并输出。
da.append([langle, rangle, lsangle, rsangle,lhangle, rhangle, lkangle, rkangle])
result= model.predict(da)
prob = model.predict_proba(da)
result=result[0]
print(result,prob)
在输出分类结果时,需对情况进行判断。经过调试发现概率准确度阈值设为0.68较为合适。并且在实际情况中,运动员在打网球时,不可能上一帧在正手击球,下一帧马上就是反手。因而需记录当前帧前10帧的动作类别作为一个参考。若前10帧内有某动作时,当前帧识别出为其他动作则判断为错误识别,不输出预测分类。准备姿势的判断则不受前10帧动作的影响。
if max(prob[0])>=0.68:
if (result == 0) and (1 not in la_result) and(2 not in la_result):
stage = 'forehand'
la_result = add_last_result(la_result, result)
elif result == 1 and (0 not in la_result) and(2 not in la_result):
stage = "backhand"
la_result = add_last_result(la_result, result)
elif result == 2 and (1 not in la_result) and(0 not in la_result):
stage = "serve"
la_result = add_last_result(la_result, result)
elif result == 3 :
stage = "prepare"
la_result = add_last_result(la_result, result)
else:
stage = 'none'
counter = 0
result=4
la_result=add_last_result(la_result,result)
except:
pass
记录每一帧的前10帧类别的函数如下:
def add_last_result(la_result,result):
if len(la_result)==10:
for i in range(10):
if i<9:
la_result[i]=la_result[i+1]
else:
la_result[i]=result
else:
la_result.append(result)
return la_result
测试结果
测试时选取了在数据集中未曾使用过的视频进行测试以保证测试的可信度。基本能对三种击球动作进行识别,错误识别(将一种动作识别为另一种)的情况在测试中并未出现,但并不是在动作全过程中完整识别。在调整数据集和特征角度后识别的效果会有较大的不同。
正手:
发球:
反手:
准备姿势:
总结和反思
动作检测具有一定的准确度,但还不够稳定,不能对击球过程中的每一个动作进行准确的识别。此外,正手和反手的随挥动作的检测易出现错误。在本研究中,这个问题通过前10帧记录的方法进行了简单的规避。今后可以考虑结合时间序列对算法进行改进;还可以对击球动作进行深度分析,以调整特征和修改数据集。
本项目中,动作检测代码对于每一帧都进行了操作,在实际调用相机使用时,可以设计间隔多帧进行检测,以提升运行速度。
代码效果较为一般,如有需要可私信我。
本文仅做个人思路的记录和一点浅薄分享。欢迎大家的指正和建议。