Ở buổi này, chúng ta sẽ nâng cấp từ việc "đếm số ngón tay tĩnh" sang tương tác động theo thời gian thực: xây dựng một Bảng vẽ ma thuật (Virtual Painter).
Làm thế nào để phân biệt khi nào người dùng muốn Di chuyển con trỏ (chọn màu) và khi nào muốn Đặt bút vẽ?
Ta thiết lập hệ thống 2 chế độ (Mode):
Chế độ Chọn Màu / Tẩy (Selection Mode):
Quy ước: Giơ đồng thời cả Ngón Trỏ và Ngón Giữa ($2$ ngón).
Hành vi: Không vẽ gì lên màn hình. Đầu ngón trỏ sẽ là con trỏ để di chuột lên bảng chọn màu ở mép trên cùng màn hình (chọn màu Đỏ, Xanh lá, Xanh dương hoặc Cục tẩy).
Chế độ Vẽ (Drawing Mode):
Quy ước: Chỉ giơ duy nhất Ngón Trỏ (ngón giữa gập lại).
Hành vi: Tọa độ đầu ngón trỏ (Landmark 8) bắt đầu kẻ các đoạn thẳng liên tiếp theo nét tay di chuyển.
Nếu vẽ trực tiếp các nét mực lên bức ảnh camera frame, mỗi khung hình mới tải từ camera sẽ xóa mất nét vẽ trước đó!
Giải pháp chuẩn:
Tạo một tấm bảng đen phụ bằng NumPy: img_canvas = np.zeros_like(frame).
Khi di ngón tay, ta vẽ các đường mực cv2.line() lên img_canvas. Bảng img_canvas này sẽ giữ nguyên toàn bộ các nét vẽ từ đầu đến cuối.
Cuối cùng, dùng phép trộn ảnh hòa trộn frame (ảnh người) và img_canvas (các nét vẽ) lại với nhau.
buoi6_virtual_painter.pyimport cv2
import numpy as np
from mediapipe.python.solutions import hands as mp_hands
from mediapipe.python.solutions import drawing_utils as mp_draw
class HandTracker:
def __init__(self):
self.hands = mp_hands.Hands(
max_num_hands=1,
min_detection_confidence=0.8,
min_tracking_confidence=0.7
)
def process(self, frame):
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
res = self.hands.process(rgb)
lm_list = []
if res.multi_hand_landmarks:
h, w, _ = frame.shape
for lm in res.multi_hand_landmarks[0].landmark:
lm_list.append((int(lm.x * w), int(lm.y * h)))
return lm_list
class VirtualPainterApp:
def __init__(self):
self.cap = cv2.VideoCapture(0)
self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
self.tracker = HandTracker()
self.canvas = None # Sẽ khởi tạo theo kích thước camera
# Cấu hình màu sắc mặc định: (B, G, R)
self.draw_color = (0, 0, 255) # Mặc định là màu Đỏ
self.brush_thickness = 12
self.eraser_thickness = 45
# Tọa độ điểm vẽ trước đó (để nối đoạn thẳng)
self.xp, self.yp = 0, 0
def draw_header_palette(self, frame):
"""Vẽ thanh công cụ chọn màu ở cạnh trên cùng"""
# Hộp màu Đỏ (100 -> 350)
cv2.rectangle(frame, (100, 10), (350, 90), (0, 0, 255), cv2.FILLED)
cv2.putText(frame, "RED", (190, 60), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
# Hộp màu Xanh Lá (400 -> 650)
cv2.rectangle(frame, (400, 10), (650, 90), (0, 255, 0), cv2.FILLED)
cv2.putText(frame, "GREEN", (480, 60), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
# Hộp màu Xanh Dương (700 -> 950)
cv2.rectangle(frame, (700, 10), (950, 90), (255, 0, 0), cv2.FILLED)
cv2.putText(frame, "BLUE", (790, 60), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
# Hộp Cục Tẩy (1000 -> 1200)
cv2.rectangle(frame, (1000, 10), (1200, 90), (50, 50, 50), cv2.FILLED)
cv2.putText(frame, "ERASER", (1030, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (255, 255, 255), 2)
def run(self):
if not self.cap.isOpened():
print("❌ Không mở được camera.")
return
print("--- BẢNG VẼ MA THUẬT (VIRTUAL PAINTER) ---")
print(" -> Giơ 2 ngón (Trỏ + Giữa): Chế độ CHỌN MÀU ở menu trên cùng")
print(" -> Giơ 1 ngón (Chỉ ngón Trỏ): Chế độ VẼ NÉT")
print(" -> Nhấn [C]: Xóa sạch bảng vẽ")
print(" -> Nhấn [Q]: Thoát")
while True:
ret, frame = self.cap.read()
if not ret:
break
frame = cv2.flip(frame, 1)
# Khởi tạo bảng vẽ phụ nếu chưa có
if self.canvas is None:
self.canvas = np.zeros_like(frame)
# Lấy tọa độ bàn tay
lm_list = self.tracker.process(frame)
if len(lm_list) != 0:
# Tọa độ đầu ngón trỏ (8) và ngón giữa (12)
x1, y1 = lm_list[8]
x2, y2 = lm_list[12]
# Kiểm tra ngón nào đang giơ: Y đỉnh < Y đốt dưới
index_up = lm_list[8][1] < lm_list[6][1]
middle_up = lm_list[12][1] < lm_list[10][1]
# TRƯỜNG HỢP 1: Giơ cả 2 ngón -> CHỌN MÀU
if index_up and middle_up:
self.xp, self.yp = 0, 0 # Reset điểm vẽ trước
cv2.rectangle(frame, (x1 - 15, y1 - 15), (x2 + 15, y2 + 15),
self.draw_color, 2)
# Nếu ngón tay chạm vào thanh Menu (Y < 100)
if y1 < 100:
if 100 < x1 < 350:
self.draw_color = (0, 0, 255) # Đỏ
elif 400 < x1 < 650:
self.draw_color = (0, 255, 0) # Xanh lá
elif 700 < x1 < 950:
self.draw_color = (255, 0, 0) # Xanh dương
elif 1000 < x1 < 1200:
self.draw_color = (0, 0, 0) # Tẩy (màu đen)
# TRƯỜNG HỢP 2: Chỉ giơ 1 ngón trỏ -> VẼ NÉT
elif index_up and not middle_up:
cv2.circle(frame, (x1, y1), 10, self.draw_color, cv2.FILLED)
if self.xp == 0 and self.yp == 0:
self.xp, self.yp = x1, y1
thick = self.eraser_thickness if self.draw_color == (0, 0, 0) else self.brush_thickness
cv2.line(self.canvas, (self.xp, self.yp), (x1, y1), self.draw_color, thick)
self.xp, self.yp = x1, y1
else:
self.xp, self.yp = 0, 0
# Vẽ thanh Menu lên trên cùng của khung hình
self.draw_header_palette(frame)
# Hòa trộn nét vẽ từ Canvas vào khung hình Camera
img_gray = cv2.cvtColor(self.canvas, cv2.COLOR_BGR2GRAY)
_, img_inv = cv2.threshold(img_gray, 20, 255, cv2.THRESH_BINARY_INV)
frame_bg = cv2.bitwise_and(frame, frame, mask=img_inv)
final_frame = cv2.bitwise_or(frame_bg, self.canvas)
cv2.imshow("Virtual Painter", final_frame)
key = cv2.waitKey(1) & 0xFF
if key == ord('q'):
break
elif key == ord('c'):
self.canvas = np.zeros_like(frame) # Xóa sạch bảng
self.cap.release()
cv2.destroyAllWindows()
if __name__ == "__main__":
app = VirtualPainterApp()
app.run()
Hãy cùng chạy thử và trải nghiệm bảng vẽ tương tác này nhé! C