一、前言
本文基于Google的Mediapipe框架,利用其自身返回的坐标,计算手指的弯曲角度。效果图如下。
二、实现过程
我在我的文章Mediapipe入门——搭建姿态检测模型并实时输出人体关节点3d坐标里讲述了如何搭建mediapipe姿态检测模型以及如何获得坐标,这里不多赘述。
依据官网的说明,mediapipe处理视频流的函数能够返回人体标注节点的三维坐标。如下图所示,一共33个。
再看看手部的节点,每只手有21个。
让我举个例子,上面“手掌”的部分节点序号标在手上是这样的。
现在知道了坐标,也知道了点在哪,开始计算手指运动过程中的夹角。以食指为例,计算∠876。推导过程比较简单,因为知道三个点的xy坐标,借助三角函数弧度rad=arctan(y/x)分别求出两条直线的正切角,再相减即可。
Python里该如何实现呢,以右手掌为例。首先,视频流处理函数返回右手地标right_hand_landmarks,该地标包含坐标xyz与可见度visibility两个信息,只需提取坐标landmark。而landmark就像是数组,只要知道对应节点的序号(索引)就能提取到坐标。
#以提取右手食指指尖坐标为例,由前述可知指尖序号为8
results = holistic.process(image)
if results.right_hand_landmarks:
RHL = results.right_hand_landmarks
coord_8=[RHL.landmark[8].x, RHL.landmark[8].y]
#coord_8便是指尖的xy坐标
接下来借助for循环来批量操作。
#食指、中指、无名指、小手指
joint_list = [[8, 7, 6], [12, 11, 10], [16, 15, 14], [20, 19, 18]] # 手指关节序列
if results.right_hand_landmarks:
RHL = results.right_hand_landmarks
for joint in joint_list:
a = np.array([RHL.landmark[joint[0]].x, RHL.landmark[joint[0]].y])
b = np.array([RHL.landmark[joint[1]].x, RHL.landmark[joint[1]].y])
c = np.array([RHL.landmark[joint[2]].x, RHL.landmark[joint[2]].y])
这样,a, b, c就是三个点的xy坐标。再借助np.arctan2函数计算反正切,然后将计算出来的弧度转为角度。
# 计算弧度
radians_fingers = np.arctan2(c[1] - b[1], c[0] - b[0]) - np.arctan2(a[1] - b[1], a[0] - b[0])
angle = np.abs(radians_fingers * 180.0 / np.pi) # 弧度转角度
有必要说一下,arctan2函数和arctan函数有所不同。arctan的值域是[-π/2, π/2],arctan2的值域是[-π, π]。毕竟手指伸直可以达到180°,还是用arctan2函数好。
有了角度,再利用cv2.putText()函数把角度数据实时渲染在手指旁边。
cv2.putText(image, str(round(angle, 2)), tuple(np.multiply(b, [640, 480]).astype(int)),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2, cv2.LINE_AA)
#渲染的位置在三个点的中间也就是坐标b的位置
三、演示
直接运行
检测速度很快。其实基于此,还可以计算手肘角度、腋下角度、膝盖弯曲角度等,原理相同,不多说。
四、完整代码
import cv2
import numpy as np
import mediapipe as mp
mp_drawing = mp.solutions.drawing_utils
mp_drawing_styles = mp.solutions.drawing_styles
mp_holistic = mp.solutions.holistic
joint_list = [[8, 7, 6], [12, 11, 10], [16, 15, 14], [20, 19, 18]] # 手指关节序列
cap = cv2.VideoCapture(0)
with mp_holistic.Holistic(
min_detection_confidence=0.5,
min_tracking_confidence=0.5) as holistic:
while cap.isOpened():
success, image = cap.read()
if not success:
print("Ignoring empty camera frame.")
break
image.flags.writeable = False
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
results = holistic.process(image)
image.flags.writeable = True
image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
# 渲染
mp_drawing.draw_landmarks(
image,
results.face_landmarks,
mp_holistic.FACEMESH_CONTOURS,
landmark_drawing_spec=None,
connection_drawing_spec=mp_drawing_styles
.get_default_face_mesh_tesselation_style())
mp_drawing.draw_landmarks(
image,
results.pose_landmarks,
mp_holistic.POSE_CONNECTIONS, landmark_drawing_spec=mp_drawing_styles.get_default_pose_landmarks_style())
mp_drawing.draw_landmarks(image, results.left_hand_landmarks, mp_holistic.HAND_CONNECTIONS,
landmark_drawing_spec=mp_drawing_styles.get_default_hand_landmarks_style())
mp_drawing.draw_landmarks(image, results.right_hand_landmarks, mp_holistic.HAND_CONNECTIONS,
landmark_drawing_spec=mp_drawing_styles.get_default_hand_landmarks_style())
# 监测到右手,执行
if results.right_hand_landmarks:
RHL = results.right_hand_landmarks
# 计算角度
for joint in joint_list:
a = np.array([RHL.landmark[joint[0]].x, RHL.landmark[joint[0]].y])
b = np.array([RHL.landmark[joint[1]].x, RHL.landmark[joint[1]].y])
c = np.array([RHL.landmark[joint[2]].x, RHL.landmark[joint[2]].y])
# 计算弧度
radians_fingers = np.arctan2(c[1] - b[1], c[0] - b[0]) - np.arctan2(a[1] - b[1], a[0] - b[0])
angle = np.abs(radians_fingers * 180.0 / np.pi) # 弧度转角度
if angle > 180.0:
angle = 360 - angle
cv2.putText(image, str(round(angle, 2)), tuple(np.multiply(b, [640, 480]).astype(int)),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2, cv2.LINE_AA)
# cv2.imshow('MediaPipe Holistic', cv2.flip(image, 1))
cv2.imshow('Mediapipe Holistic', image) # 取消镜面翻转
if cv2.waitKey(5) == ord('q'):
break
cap.release()
五、若有错误请指正,欢迎讨论赐教。