一、技術棧
1. OpenCV(Open Source Computer Vision Library)
-
性質:開源計算機視覺庫(Library)
-
主要功能:
-
圖像/視頻的基礎處理(讀取、裁剪、濾波、色彩轉換等)
-
特征檢測(邊緣、角點等)
-
攝像頭標定、目標跟蹤等
-
-
在項目中的作用:
-
負責視頻流的捕獲(
cv2.VideoCapture
) -
圖像格式轉換(
cv2.cvtColor
) -
最終結果的渲染顯示(
cv2.imshow
)
-
2. MediaPipe
-
性質:由Google開發的跨平臺機器學習框架(Framework)
-
主要功能:
-
提供預訓練的端到端模型(如手部關鍵點、人臉網格、姿態估計等)
-
專注于實時感知任務(低延遲、移動端優化)
-
-
在項目中的作用:
-
調用
mediapipe.solutions.hands
模型實現21個手部關鍵點檢測 -
輸出關鍵點坐標,并通過
mpDraw
可視化
-
二、手部關鍵點檢測
?(一)初始化
cap = cv2.VideoCapture(0) # 通過OpenCV調用攝像頭設備。參數0:默認攝像頭(筆記本內置攝像頭)。
mpHands = mp.solutions.hands # MediaPipe的手部關鍵點檢測模型(21個關鍵點)
hands = mpHands.Hands() # 創建模型實例
mpDraw = mp.solutions.drawing_utils # MediaPipe提供的繪圖工具,用于在圖像上繪制關鍵點和連線。
handLmsStyle = mpDraw.DrawingSpec(color=(0, 0, 255), thickness=5) # 點的樣式
handConStyle = mpDraw.DrawingSpec(color=(0, 255, 0), thickness=10) # 線的樣式
pTime = 0
cTime = 0
(二)關鍵點檢測
ret, img = cap.read() # 從攝像頭持續讀取視頻幀。OpenCV默認格式為BGR格式if ret:imgRGB = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 格式轉換,MediaPipe模型要求輸入為RGB格式# 手部關鍵點檢測result = hands.process(imgRGB)# print(result.multi_hand_landmarks)imgHeight = img.shape[0]imgWidth = img.shape[1]
?重要代碼解釋:
result = hands.process(imgRGB)
- 底層過程:
????????圖像輸入MediaPipe手部模型
????????模型輸出包含:
????????????????multi_hand_landmarks:21個關鍵點的歸一化坐標(0~1之間)
????????????????multi_handedness:左右手判斷
- result 數據結構:
????????類型:List(列表)
????????內容:每個元素代表一只手的21個關鍵點數據(因此result.multi_hand_landmarks最多? ? ? ? ? ? ? ? ? 有兩個元素)
????????層級關系:
result.multi_hand_landmarks[0] # 第1只手.landmark[0] # 第1個關鍵點.x # 歸一化x坐標 (0.0~1.0).y # 歸一化y坐標 (0.0~1.0).z # 相對深度(值越小越靠近攝像頭)
(三)可視化
# 關鍵點可視化if result.multi_hand_landmarks:for handLms in result.multi_hand_landmarks: # 遍歷每只檢測到的手mpDraw.draw_landmarks(img, handLms, mpHands.HAND_CONNECTIONS, handLmsStyle,handConStyle) # 繪制手部關鍵點和骨骼連線for i, lm in enumerate(handLms.landmark): # 遍歷21個關鍵點xPos = int(lm.x * imgWidth) # 將歸一化x坐標轉換為像素坐標yPos = int(lm.y * imgHeight) # 將歸一化y坐標轉換為像素坐標cv2.putText(img, str(i), (xPos - 25, yPos + 5), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 0, 255),2) # 在關鍵點旁標注索引數字if i == 4:cv2.circle(img, (xPos, yPos), 20, (166, 56, 56), cv2.FILLED)print(i, xPos, yPos) # 用深藍色實心圓高亮標記拇指尖
??重要代碼解釋:
mpDraw.draw_landmarks(img, handLms, mpHands.HAND_CONNECTIONS, handLmsStyle, handConStyle)
-
功能:繪制手部關鍵點和骨骼連線
-
參數詳解:
-
img
:目標圖像(OpenCV格式) -
handLms
:當前手的關鍵點數據 -
mpHands.HAND_CONNECTIONS
:預定義的關鍵點連接關系(如點0-1相連,點1-2相連等) -
handLmsStyle
:關鍵點繪制樣式(紅色圓點,厚度5) -
handConStyle
:連接線樣式(綠色線條,厚度10)
-
xPos = int(lm.x * imgWidth) # 將歸一化x坐標轉換為像素坐標
yPos = int(lm.y * imgHeight) # 將歸一化y坐標轉換為像素坐標
- 坐標轉換公式
像素坐標 = 歸一化坐標 × 圖像尺寸
????????示例:
????????????????若圖像寬度imgWidth=640,某點lm.x=0.5 → xPos=320
????????????????若圖像高度imgHeight=480,某點lm.y=0.25 → yPos=120
cv2.putText(img, str(i), (xPos - 25, yPos + 5), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 0, 255),2) # 在關鍵點旁標注索引數字
(四)完整代碼
import cv2
import mediapipe as mp
import timecap = cv2.VideoCapture(0) # 通過OpenCV調用攝像頭設備。參數0:默認攝像頭(筆記本內置攝像頭)。
mpHands = mp.solutions.hands # MediaPipe的手部關鍵點檢測模型(21個關鍵點)
hands = mpHands.Hands() # 創建模型實例
mpDraw = mp.solutions.drawing_utils # MediaPipe提供的繪圖工具,用于在圖像上繪制關鍵點和連線。
handLmsStyle = mpDraw.DrawingSpec(color=(0, 0, 255), thickness=5) # 點的樣式
handConStyle = mpDraw.DrawingSpec(color=(0, 255, 0), thickness=10) # 線的樣式
pTime = 0
cTime = 0while True:ret, img = cap.read() # 從攝像頭持續讀取視頻幀。OpenCV默認格式為BGR格式if ret:imgRGB = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 格式轉換,MediaPipe模型要求輸入為RGB格式# 手部關鍵點檢測result = hands.process(imgRGB)# print(result.multi_hand_landmarks)imgHeight = img.shape[0]imgWidth = img.shape[1]# 關鍵點可視化if result.multi_hand_landmarks:for handLms in result.multi_hand_landmarks: # 遍歷每只檢測到的手mpDraw.draw_landmarks(img, handLms, mpHands.HAND_CONNECTIONS, handLmsStyle,handConStyle) # 繪制手部關鍵點和骨骼連線for i, lm in enumerate(handLms.landmark): # 遍歷21個關鍵點xPos = int(lm.x * imgWidth) # 將歸一化x坐標轉換為像素坐標yPos = int(lm.y * imgHeight) # 將歸一化y坐標轉換為像素坐標cv2.putText(img, str(i), (xPos - 25, yPos + 5), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 0, 255),2) # 在關鍵點旁標注索引數字if i == 4:cv2.circle(img, (xPos, yPos), 20, (166, 56, 56), cv2.FILLED)print(i, xPos, yPos) # 用深藍色實心圓高亮標記拇指尖cTime = time.time()fps = 1 / (cTime - pTime)pTime = cTimecv2.putText(img, f"FPS:{int(fps)}", (30, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 0, 0), 3)cv2.imshow('img', img)if cv2.waitKey(1) == ord('q'):break
三、識別手指個數
(一)識別原理
1. 四指的判斷
# 處理食指到小指
for i in range(1, 5):if handLms.landmark[fingerTips[i]].y < handLms.landmark[fingerTips[i] - 2].y:fingerState.append(1) # 伸出else:fingerState.append(0) # 彎曲
關鍵點:當食指遠端指間關節(DIP,索引點8)在圖像坐標系中的垂直位置高于近端指間關節(PIP,索引點6)時,即滿足:y8<y6。
2. 拇指的判斷
# 鏡像翻轉修正左右手問題img = cv2.flip(img, 1)...# 處理拇指(默認掌心朝鏡頭)if handType == 'Right': # 對于右手if handLms.landmark[fingerTips[0]].x < handLms.landmark[fingerTips[0] - 1].x:fingerState.append(1) # 右手拇指伸出else:fingerState.append(0) # 右手拇指彎曲else: # 對于左手if handLms.landmark[fingerTips[0]].x > handLms.landmark[fingerTips[0] - 1].x:fingerState.append(1) # 左手拇指伸出else:fingerState.append(0) # 左手拇指彎曲
鏡像翻轉的必要性:
MediaPipe基于深度卷積神經網絡(CNN)架構,通過學習手部關鍵點的空間分布模式來區分左手和右手。因此會將拇指在圖像左側的手識別為物理右手。而攝像頭原始畫面中物理右手拇指實際位于右側,因此必須通過cv2.flip(img, 1)水平鏡像翻轉圖像,才能使MediaPipe正確識別手型。
坐標判斷的底層邏輯:
所有關鍵點坐標均基于鏡像翻轉后的圖像空間,物理右手在翻轉后的坐標系中表現為thumb_tip.x < thumb_ip.x。MediaPipe內部已自動處理坐標轉換,開發者直接使用檢測到的歸一化坐標即可,無需額外計算原始坐標。
(二)完整代碼
import cv2
import mediapipe as mp
import timecap = cv2.VideoCapture(0)
mpHands = mp.solutions.hands
hands = mpHands.Hands()
mpDraw = mp.solutions.drawing_utils
handLmsStyle = mpDraw.DrawingSpec(color=(0, 0, 255), thickness=5) # 關鍵點樣式
handConStyle = mpDraw.DrawingSpec(color=(0, 255, 0), thickness=10) # 連接線樣式
pTime = 0# 定義手指關鍵點
fingerTips = [4, 8, 12, 16, 20] # 拇指、食指、中指、無名指、小指的指尖關鍵點索引while True:ret, img = cap.read()if ret:# 鏡像翻轉修正左右手問題img = cv2.flip(img, 1)imgRGB = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)result = hands.process(imgRGB)imgHeight, imgWidth, _ = img.shapeif result.multi_hand_landmarks:for handLms, handInfo in zip(result.multi_hand_landmarks, result.multi_handedness):mpDraw.draw_landmarks(img, handLms, mpHands.HAND_CONNECTIONS, handLmsStyle, handConStyle)# 獲取手的類型:左手還是右手handType = handInfo.classification[0].labelhandLabel = "Right Hand" if handType == 'Right' else "Left Hand"# 手勢計數fingerState = [] # 記錄每根手指是否伸出# 處理食指到小指for i in range(1, 5):if handLms.landmark[fingerTips[i]].y < handLms.landmark[fingerTips[i] - 2].y:fingerState.append(1) # 伸出else:fingerState.append(0) # 彎曲# 處理拇指(默認掌心朝鏡頭)if handType == 'Right': # 對于右手if handLms.landmark[fingerTips[0]].x < handLms.landmark[fingerTips[0] - 1].x:fingerState.append(1) # 右手拇指伸出else:fingerState.append(0) # 右手拇指彎曲else: # 對于左手if handLms.landmark[fingerTips[0]].x > handLms.landmark[fingerTips[0] - 1].x:fingerState.append(1) # 左手拇指伸出else:fingerState.append(0) # 左手拇指彎曲# 計算伸出的手指數量fingerCount = sum(fingerState)# 在圖像上顯示手指數量cv2.putText(img, f"{handLabel}: {fingerCount} Fingers", (50, 100),cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 3)# 計算 FPScTime = time.time()fps = 1 / (cTime - pTime)pTime = cTimecv2.putText(img, f"FPS:{int(fps)}", (30, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 0, 0), 3)cv2.imshow('Hand Tracking', img)if cv2.waitKey(1) == ord('q'):breakcap.release()
cv2.destroyAllWindows()