Hai bố con mở Terminal và chạy lệnh sau để cài đặt mediapipe:
pip install mediapipe
Khi phát hiện bàn tay, MediaPipe trả về 21 điểm mốc tọa độ (từ 0 đến 20) biểu diễn các đốt ngón tay:
8 (Đầu ngón trỏ) 12 (Đầu ngón giữa) 16 (Đầu áp út) 20 (Đầu ngón út)
| | | |
7 11 15 19
| | | |
6 10 14 18
| | | |
5 9 13 17
\ | / /
4 (Ngón cái) \ | / /
\ \--------------+-----------------+----------------
3 |
| | (Lòng bàn tay)
2 |
\ |
1 |
\ /
0 (Cổ tay - Wrist) --+
Gốc cổ tay: Điểm 0
4 đầu ngón dài: Ngón trỏ (8), Ngón giữa (12), Ngón áp út (16), Ngón út (20).
Các khớp đốt thứ hai: Ngón trỏ (6), Ngón giữa (10), Ngón áp út (14), Ngón út (18).
Đầu ngón cái: Điểm 4 (và khớp dưới là 3 hoặc 2).
Hệ tọa độ ảnh có trục $Y$ hướng từ trên xuống dưới (gốc $(0,0)$ ở góc trên cùng bên trái). Nghĩa là: Càng lên cao thì $Y$ càng NHỎ.
Với 4 ngón dài (Trỏ, Giữa, Áp út, Út):
Ngón tay được xem là ĐANG MỞ (Giơ lên) nếu Đầu ngón tay cao hơn Khớp đốt thứ hai:
(Ví dụ ngón trỏ: $Y_8 < Y_6 \implies$ Ngón trỏ đang giơ lên).
Ngược lại ($Y_{\text{đầu ngón}} \ge Y_{\text{khớp}}$) $\implies$ Ngón tay đang gập lại.
Với Ngón cái:
Vì ngón cái co duỗi theo chiều ngang (trục $X$) chứ không co duỗi theo chiều dọc như 4 ngón kia:
Nếu úp mu bàn tay về phía camera: Ngón cái xòe ra khi $X_{\text{đầu ngón cái}} < X_{\text{khớp dưới}}$ (hoặc ngược lại tùy tay trái hay tay phải).
Tạo file buoi5_hand_tracking.py:
import os
import urllib.request
import cv2
import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python import vision
class HandDetector:
"""Module AI nhận diện bàn tay và phát hiện ngón mở/gập (tương thích MediaPipe Tasks API mới)"""
def __init__(self, mode=False, max_hands=1, detection_con=0.7, track_con=0.5):
# Đường dẫn file model hand_landmarker.task
script_dir = os.path.dirname(os.path.abspath(__file__))
self.model_path = os.path.join(script_dir, "hand_landmarker.task")
# Tự động tải model nếu chưa có sẵn
if not os.path.exists(self.model_path):
print("⏳ Đang tự động tải mô hình MediaPipe Hand Landmarker (~7.8MB)...")
url = "https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task"
urllib.request.urlretrieve(url, self.model_path)
print("✅ Tải mô hình hoàn tất!")
# Khởi tạo MediaPipe Tasks HandLandmarker
base_options = python.BaseOptions(model_asset_path=self.model_path)
options = vision.HandLandmarkerOptions(
base_options=base_options,
num_hands=max_hands,
min_hand_detection_confidence=detection_con,
min_tracking_confidence=track_con
)
self.detector = vision.HandLandmarker.create_from_options(options)
self.results = None
# 21 điểm mốc nối xương bàn tay chuẩn MediaPipe
self.connections = [
(0, 1), (1, 2), (2, 3), (3, 4), # Ngón cái
(0, 5), (5, 6), (6, 7), (7, 8), # Ngón trỏ
(5, 9), (9, 10), (10, 11), (11, 12), # Ngón giữa
(9, 13), (13, 14), (14, 15), (15, 16), # Ngón áp út
(13, 17), (17, 18), (18, 19), (19, 20), # Ngón út
(0, 17) # Lòng bàn tay
]
# Chỉ số đầu ngón và khớp ngón tương ứng (4 ngón dài)
# Cấu trúc: [ID_Đầu_ngón, ID_Khớp_dưới]
self.tip_ids = [8, 12, 16, 20]
self.pip_ids = [6, 10, 14, 18]
def find_hands(self, frame, draw=True):
"""Phát hiện bàn tay và vẽ khung xương lên ảnh"""
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=rgb_frame)
self.results = self.detector.detect(mp_image)
if draw and self.results and self.results.hand_landmarks:
h, w, _ = frame.shape
for hand_landmarks in self.results.hand_landmarks:
# 1. Vẽ các đường nối xương (màu xanh lá tươi)
for start_idx, end_idx in self.connections:
p1 = hand_landmarks[start_idx]
p2 = hand_landmarks[end_idx]
pt1 = (int(p1.x * w), int(p1.y * h))
pt2 = (int(p2.x * w), int(p2.y * h))
cv2.line(frame, pt1, pt2, (0, 255, 0), 2)
# 2. Vẽ 21 khớp tròn (màu đỏ viền trắng)
for lm in hand_landmarks:
cx, cy = int(lm.x * w), int(lm.y * h)
cv2.circle(frame, (cx, cy), 5, (0, 0, 255), cv2.FILLED)
cv2.circle(frame, (cx, cy), 5, (255, 255, 255), 1)
return frame
def find_positions(self, frame):
"""Trích xuất danh sách tọa độ (id, x, y) tính bằng đơn vị Pixel"""
lm_list = []
if self.results and self.results.hand_landmarks and len(self.results.hand_landmarks) > 0:
my_hand = self.results.hand_landmarks[0]
h, w, _ = frame.shape
for id, lm in enumerate(my_hand):
# Tọa độ lm.x, lm.y được chuẩn hóa từ 0.0 đến 1.0 -> nhân với W, H để ra pixel
cx, cy = int(lm.x * w), int(lm.y * h)
lm_list.append((id, cx, cy))
return lm_list
def count_fingers(self, lm_list):
"""Đếm số ngón tay đang giơ lên"""
if len(lm_list) == 0:
return 0
fingers = []
# 1. Kiểm tra Ngón Cái (Trục X)
# Khi soi gương (cv2.flip = 1), tay phải giơ lên ngón cái hướng về bên trái (X đầu ngón < X khớp)
if lm_list[4][1] < lm_list[3][1]:
fingers.append(1) # Đang mở
else:
fingers.append(0) # Đang gập
# 2. Kiểm tra 4 ngón dài (Trục Y: Điểm ngọn có Y nhỏ hơn điểm gốc không?)
for tip, pip in zip(self.tip_ids, self.pip_ids):
if lm_list[tip][2] < lm_list[pip][2]:
fingers.append(1) # Giơ lên
else:
fingers.append(0) # Gập xuống
# Trả về tổng số ngón tay đang mở (từ 0 đến 5)
return fingers.count(1)
class FingerCounterApp:
def __init__(self):
self.cap = cv2.VideoCapture(0)
self.detector = HandDetector()
def run(self):
if not self.cap.isOpened():
print("❌ Không thể kết nối Webcam.")
return
print(" Chương trình đếm ngón tay AI đang chạy...")
print(" -> Nhấn [Q] để thoát")
while True:
ret, frame = self.cap.read()
if not ret:
break
# Lật gương để phản xạ tự nhiên như soi gương
frame = cv2.flip(frame, 1)
# 1. Phát hiện bàn tay và vẽ khung xương
frame = self.detector.find_hands(frame, draw=True)
# 2. Lấy tọa độ 21 điểm mốc
lm_list = self.detector.find_positions(frame)
# 3. Đếm ngón tay và hiển thị số lượng
finger_count = self.detector.count_fingers(lm_list)
# Vẽ ô hiển thị kết quả màu tím nổi bật
cv2.rectangle(frame, (20, 20), (180, 140), (255, 0, 128), cv2.FILLED)
cv2.putText(frame, str(finger_count), (70, 110),
cv2.FONT_HERSHEY_PLAIN, 6, (255, 255, 255), 6)
cv2.imshow("Buoi 5: MediaPipe Finger Counter", frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
self.cap.release()
cv2.destroyAllWindows()
if __name__ == "__main__":
app = FingerCounterApp()
app.run()
app.run()
Sau khi chạy thử chương trình, giơ từ $1, 2, 3, 4, 5$ ngón tay trước camera và thấy số nhảy chính xác, hãy hỏi bé:
Thử thách 1 (Bắt bài ngón cái):
Cho bé thử đổi giữa Tay Phải và Tay Trái.
Bé sẽ phát hiện ra: Ngón cái của một trong hai tay đếm rất chuẩn, nhưng đổi sang tay kia thì ngón cái bị tính ngược lại!
Câu hỏi cho bé: Tại sao lại như vậy? (Vì khi đổi tay, ngón cái xòe sang hướng đối diện của trục $X$). MediaPipe có thuộc tính nào cho biết bàn tay đang nhận là Tay Trái (Left) hay Tay Phải (Right) không?
Thử thách 2 (Ký hiệu chiến thắng - Victory sign):
Làm sao để viết một điều kiện kiểm tra xem người dùng có đang làm cử chỉ Peace/Victory (chỉ giơ 2 ngón: ngón trỏ và ngón giữa, 3 ngón còn lại gập) hay không?
Dưới đây là lời giải chi tiết cho 2 thử thách của Buổi 5 trước khi chúng ta bước tiếp sang Buổi 6.
Nguyên nhân hiện tượng:
Trục $X$ chạy từ trái sang phải ($0 \rightarrow W$).
Khi ngửa bàn tay hướng về phía camera:
Với tay phải: Ngón cái xòe sang bên trái màn hình $\implies X_{\text{đầu ngón cái (4)}} < X_{\text{khớp (3)}}$.
Với tay trái: Ngón cái xòe sang bên phải màn hình $\implies X_{\text{đầu ngón cái (4)}} > X_{\text{khớp (3)}}$.
Vì vậy, nếu chỉ dùng 1 điều kiện so sánh cố định, ngón cái của một trong hai tay chắc chắn sẽ bị tính ngược.
Giải pháp với MediaPipe:
MediaPipe cung cấp thuộc tính results.multi_handedness chứa thông tin nhãn bàn tay (Left hoặc Right).
def get_hand_type(self):
"""Xác định bàn tay đang nhận diện là Tay Trái hay Tay Phải"""
if self.results.multi_handedness:
# Lấy nhãn của bàn tay đầu tiên
hand_type = self.results.multi_handedness[0].classification[0].label
return hand_type # Trả về chuỗi 'Left' hoặc 'Right'
return None
Khi kết hợp với cv2.flip(frame, 1) (chế độ soi gương), nhãn Left/Right của MediaPipe đã tương ứng trực quan với góc nhìn của bé:
Nếu là Tay Phải (Right): Ngón cái mở khi lm_list[4][1] < lm_list[3][1].
Nếu là Tay Trái (Left): Ngón cái mở khi lm_list[4][1] > lm_list[3][1].
Điều kiện logic:
Ngón trỏ (Tip 8) MỞ: $Y_8 < Y_6$
Ngón giữa (Tip 12) MỞ: $Y_{12} < Y_{10}$
Ngón áp út (Tip 16) GẬP: $Y_{16} > Y_{14}$
Ngón út (Tip 20) GẬP: $Y_{20} > Y_{18}$
Ngón cái GẬP vào lòng bàn tay.
Khi thỏa mãn đồng thời, ta có thể hiển thị chữ "VICTORY / PEACE" màu xanh lá nổi bật trên màn hình.