這篇教學會延伸「Mediapipe 手掌偵測 ( hands )」文章的範例,當偵測到手指的節點後,運用公式計算出「手指角度」,再透過手指的角度進行手勢辨識 ( 辨識手勢 0~9、比 rock、比讚...等 )。
原文參考:Mediapipe 手勢辨識
因為程式使用 Jupyter 搭配 Tensorflow 進行開發,所以請先閱讀「使用 Anaconda」和「使用 MediaPipe」,安裝對應的套件,如果不要使用 Juputer,也可參考「使用 Python 虛擬環境」,建立虛擬環境進行實作。
因為整體程式碼較多,因此將詳細說明寫在程式碼的註解內,程式碼的重點如下:
- import math 函式庫,參考「靜態手勢 - 圖像二維方式約束參考代碼」文章,定義透過節點座標計算五隻手指角度的函式 ( vector_2d_angle 和 hand_angle )。
- 取得手指角度後 ( 串列格式 ),再定義另外一個函式 ( hand_pos ),由這個函式判斷角度範圍,回傳該角度所代表的文字。
- 參考「Mediapipe 手掌偵測 ( hands )」文章,啟用手掌偵測並將偵測到的節點座標,帶入 hand_angle 函式,將計算出的角度串列帶入 hand_pos 求出目前的手勢。
- 如果偵測到手指的角度如果小於 50 度,表示手指伸直,大於等於 50 度表示手指捲縮,可使用 print 先印出結果,再根據結果調整角度範圍。
import cv2
import mediapipe as mp
import math
mp_drawing = mp.solutions.drawing_utils
mp_drawing_styles = mp.solutions.drawing_styles
mp_hands = mp.solutions.hands
# 根據兩點的座標,計算角度
def vector_2d_angle(v1, v2):
v1_x = v1[0]
v1_y = v1[1]
v2_x = v2[0]
v2_y = v2[1]
try:
angle_= math.degrees(math.acos((v1_x*v2_x+v1_y*v2_y)/(((v1_x**2+v1_y**2)**0.5)*((v2_x**2+v2_y**2)**0.5))))
except:
angle_ = 180
return angle_
# 根據傳入的 21 個節點座標,得到該手指的角度
def hand_angle(hand_):
angle_list = []
# thumb 大拇指角度
angle_ = vector_2d_angle(
((int(hand_[0][0])- int(hand_[2][0])),(int(hand_[0][1])-int(hand_[2][1]))),
((int(hand_[3][0])- int(hand_[4][0])),(int(hand_[3][1])- int(hand_[4][1])))
)
angle_list.append(angle_)
# index 食指角度
angle_ = vector_2d_angle(
((int(hand_[0][0])-int(hand_[6][0])),(int(hand_[0][1])- int(hand_[6][1]))),
((int(hand_[7][0])- int(hand_[8][0])),(int(hand_[7][1])- int(hand_[8][1])))
)
angle_list.append(angle_)
# middle 中指角度
angle_ = vector_2d_angle(
((int(hand_[0][0])- int(hand_[10][0])),(int(hand_[0][1])- int(hand_[10][1]))),
((int(hand_[11][0])- int(hand_[12][0])),(int(hand_[11][1])- int(hand_[12][1])))
)
angle_list.append(angle_)
# ring 無名指角度
angle_ = vector_2d_angle(
((int(hand_[0][0])- int(hand_[14][0])),(int(hand_[0][1])- int(hand_[14][1]))),
((int(hand_[15][0])- int(hand_[16][0])),(int(hand_[15][1])- int(hand_[16][1])))
)
angle_list.append(angle_)
# pink 小拇指角度
angle_ = vector_2d_angle(
((int(hand_[0][0])- int(hand_[18][0])),(int(hand_[0][1])- int(hand_[18][1]))),
((int(hand_[19][0])- int(hand_[20][0])),(int(hand_[19][1])- int(hand_[20][1])))
)
angle_list.append(angle_)
return angle_list
# 根據手指角度的串列內容,返回對應的手勢名稱
def hand_pos(finger_angle):
f1 = finger_angle[0] # 大拇指角度
f2 = finger_angle[1] # 食指角度
f3 = finger_angle[2] # 中指角度
f4 = finger_angle[3] # 無名指角度
f5 = finger_angle[4] # 小拇指角度
# 小於 50 表示手指伸直,大於等於 50 表示手指捲縮
if f1<50 and f2>=50 and f3>=50 and f4>=50 and f5>=50:
return 'good'
elif f1>=50 and f2>=50 and f3<50 and f4>=50 and f5>=50:
return 'no!!!'
elif f1<50 and f2<50 and f3>=50 and f4>=50 and f5<50:
return 'ROCK!'
elif f1>=50 and f2>=50 and f3>=50 and f4>=50 and f5>=50:
return '0'
elif f1>=50 and f2>=50 and f3>=50 and f4>=50 and f5<50:
return 'pink'
elif f1>=50 and f2<50 and f3>=50 and f4>=50 and f5>=50:
return '1'
elif f1>=50 and f2<50 and f3<50 and f4>=50 and f5>=50:
return '2'
elif f1>=50 and f2>=50 and f3<50 and f4<50 and f5<50:
return 'ok'
elif f1<50 and f2>=50 and f3<50 and f4<50 and f5<50:
return 'ok'
elif f1>=50 and f2<50 and f3<50 and f4<50 and f5>50:
return '3'
elif f1>=50 and f2<50 and f3<50 and f4<50 and f5<50:
return '4'
elif f1<50 and f2<50 and f3<50 and f4<50 and f5<50:
return '5'
elif f1<50 and f2>=50 and f3>=50 and f4>=50 and f5<50:
return '6'
elif f1<50 and f2<50 and f3>=50 and f4>=50 and f5>=50:
return '7'
elif f1<50 and f2<50 and f3<50 and f4>=50 and f5>=50:
return '8'
elif f1<50 and f2<50 and f3<50 and f4<50 and f5>=50:
return '9'
else:
return ''
cap = cv2.VideoCapture(0) # 讀取攝影機
fontFace = cv2.FONT_HERSHEY_SIMPLEX # 印出文字的字型
lineType = cv2.LINE_AA # 印出文字的邊框
# mediapipe 啟用偵測手掌
with mp_hands.Hands(
model_complexity=0,
min_detection_confidence=0.5,
min_tracking_confidence=0.5) as hands:
if not cap.isOpened():
print("Cannot open camera")
exit()
w, h = 540, 310 # 影像尺寸
while True:
ret, img = cap.read()
img = cv2.resize(img, (w,h)) # 縮小尺寸,加快處理效率
if not ret:
print("Cannot receive frame")
break
img2 = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 轉換成 RGB 色彩
results = hands.process(img2) # 偵測手勢
if results.multi_hand_landmarks:
for hand_landmarks in results.multi_hand_landmarks:
finger_points = [] # 記錄手指節點座標的串列
for i in hand_landmarks.landmark:
# 將 21 個節點換算成座標,記錄到 finger_points
x = i.x*w
y = i.y*h
finger_points.append((x,y))
if finger_points:
finger_angle = hand_angle(finger_points) # 計算手指角度,回傳長度為 5 的串列
#print(finger_angle) # 印出角度 ( 有需要就開啟註解 )
text = hand_pos(finger_angle) # 取得手勢所回傳的內容
cv2.putText(img, text, (30,120), fontFace, 5, (255,255,255), 10, lineType) # 印出文字
cv2.imshow('oxxostudio', img)
if cv2.waitKey(5) == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
大家好,我是 OXXO,是個即將邁入中年的斜槓青年,我已經寫了超過 400 篇 Python 的教學,有興趣可以參考下方連結呦~ ^_^