Ở buổi này, hai bố con sẽ chuyển toàn bộ những gì đã làm sang cấu trúc phần mềm chuyên nghiệp: tách biệt nhiệm vụ bằng tính đóng gói (Encapsulation) và đo đạc hiệu năng tính toán thời gian thực (FPS - Frames Per Second).
Thay vì dồn tất cả logic camera, xử lý ảnh và hiển thị vào một file lộn xộn, ta chia chương trình thành 2 Class độc lập:
ColorTracker (Bộ não xử lý):
Nhiệm vụ duy nhất: Nhận vào một bức ảnh thô $\rightarrow$ Trả về ảnh đã vẽ và tọa độ $(x, y)$.
Hoàn toàn không quan tâm ảnh đó đến từ webcam, video file hay qua mạng Socket.
FPSCounter (Đồng hồ đo hiệu năng):
Đo xem máy tính xử lý được bao nhiêu khung hình trong 1 giây.
Nếu FPS $\ge 30$: Chương trình mượt mà. Nếu FPS $< 15$: Cần tối ưu thuật toán.
Tạo file buoi4_pipeline_oop.py:
import cv2
import numpy as np
import time
from collections import deque
class FPSCounter:
"""Bộ đếm số khung hình trên giây (Frames Per Second)"""
def __init__(self):
self.prev_time = time.time()
self.fps = 0.0
def update(self):
current_time = time.time()
# Tránh chia cho 0 nếu thời gian trôi qua quá nhỏ
delta = current_time - self.prev_time
if delta > 0:
self.fps = 1.0 / delta
self.prev_time = current_time
return self.fps
class ColorTracker:
"""Module chuyên biệt: Lọc màu, tìm contour và lưu quỹ đạo"""
def __init__(self, lower_hsv, upper_hsv, trail_len=32, min_area=600):
self.lower = np.array(lower_hsv)
self.upper = np.array(upper_hsv)
self.min_area = min_area
self.trail = deque(maxlen=trail_len)
def process(self, frame):
"""Xử lý khung hình và trả về: (frame_ve, mask, center_point)"""
# 1. Làm mờ nhẹ khử nhiễu
blurred = cv2.GaussianBlur(frame, (11, 11), 0)
hsv = cv2.cvtColor(blurred, cv2.COLOR_BGR2HSV)
# 2. Tạo mặt nạ và làm sạch
mask = cv2.inRange(hsv, self.lower, self.upper)
mask = cv2.erode(mask, None, iterations=2)
mask = cv2.dilate(mask, None, iterations=2)
# 3. Tìm contours
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
center = None
if len(contours) > 0:
largest = max(contours, key=cv2.contourArea)
if cv2.contourArea(largest) > self.min_area:
((x, y), radius) = cv2.minEnclosingCircle(largest)
M = cv2.moments(largest)
if M["m00"] != 0:
center = (int(M["m10"] / M["m00"]), int(M["m01"] / M["m00"]))
# Vẽ vòng tròn và tâm
cv2.circle(frame, (int(x), int(y)), int(radius), (0, 255, 255), 2)
cv2.circle(frame, center, 5, (0, 0, 255), -1)
# 4. Cập nhật quỹ đạo vệt sáng
self.trail.appendleft(center)
self._draw_trail(frame)
return frame, mask, center
def _draw_trail(self, frame):
"""Phương thức nội bộ: vẽ vệt sáng nối các điểm"""
for i in range(1, len(self.trail)):
if self.trail[i - 1] is None or self.trail[i] is None:
continue
thickness = int(np.sqrt(len(self.trail) / float(i + 1)) * 2.5)
cv2.line(frame, self.trail[i - 1], self.trail[i], (0, 255, 255), thickness)
class MainApp:
"""Ứng dụng chính điều phối phần cứng và giao diện"""
def __init__(self, camera_index=0):
self.cap = cv2.VideoCapture(camera_index)
self.fps_counter = FPSCounter()
# Cấu hình màu HSV muốn theo dõi (đổi theo vật thực tế của hai bố con)
self.tracker = ColorTracker(lower_hsv=[5, 100, 100],
upper_hsv=[25, 255, 255])
def run(self):
if not self.cap.isOpened():
print("❌ Không mở được camera.")
return
print(" Chương trình đang chạy. Nhấn [Q] để dừng.")
while True:
ret, frame = self.cap.read()
if not ret:
break
# Lật gương
frame = cv2.flip(frame, 1)
# Xử lý hình ảnh qua Tracker
processed_frame, mask, center = self.tracker.process(frame)
# Tính và hiển thị FPS
current_fps = self.fps_counter.update()
cv2.putText(processed_frame, f"FPS: {int(current_fps)}", (20, 40),
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2)
# Hiển thị
cv2.imshow("Object Tracker OOP", processed_frame)
cv2.imshow("Mask View", mask)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
self.cap.release()
cv2.destroyAllWindows()
if __name__ == "__main__":
app = MainApp()
app.run()
Tính tái sử dụng (Reusability): Lớp ColorTracker giờ đây có thể được import vào bất kỳ dự án nào khác mà không cần copy paste lại cả đoạn code dài.
Quan sát FPS: Cho bé theo dõi chỉ số FPS trên màn hình:
Nếu tắt các bước vẽ hoặc giảm độ phân giải camera, FPS có tăng lên không?
Điều này giúp bé hiểu về khái niệm tối ưu hóa tài nguyên phần cứng trong kỹ thuật phần mềm.
Để hai bố con trực tiếp quan sát chỉ số FPS tăng vọt hay sụt giảm, chúng ta sẽ can thiệp vào hai yếu tố ảnh hưởng trực tiếp đến khối lượng tính toán của CPU/GPU: số lượng pixel cần xử lý (độ phân giải) và thời gian thực thi các hàm vẽ/xử lý ảnh.
Dưới đây là cơ chế bản chất và cách viết code cụ thể để bé thử nghiệm bật/tắt bằng phím bấm.
Ở độ phân giải Full HD (1920 $\times$ 1080), mỗi khung hình máy tính phải tính toán trên:
Với ảnh màu 3 kênh $(B, G, R)$, CPU phải duyệt qua hơn 6,2 triệu con số trong một lần lặp.
Nếu giảm xuống VGA (640 $\times$ 480), số pixel giảm xuống chỉ còn:
$\implies$ Khối lượng dữ liệu giảm gần 7 lần! CPU xử lý xong ma trận nhanh hơn rất nhiều, chu kỳ vòng lặp ngắn lại, đẩy chỉ số FPS lên cao.
Có 2 cách thực hiện:
Cách A: Yêu cầu phần cứng Camera hạ độ phân giải ngay từ đầu vào (Tiết kiệm băng thông USB/Bus):
# Đặt kích thước khung hình về 640x480 hoặc 320x240
self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
Cách B: Thu nhỏ ma trận ảnh bằng phần mềm bằng cv2.resize() (Dễ bật/tắt ngay trong lúc chạy):
# Thu nhỏ khung hình còn 50% kích thước gốc trước khi đưa vào bộ lọc màu
small_frame = cv2.resize(frame, (0, 0), fx=0.5, fy=0.5)
Các hàm vẽ hình như cv2.circle(), cv2.line(), cv2.putText() thực chất là các thuật toán can thiệp và ghi đè giá trị vào từng ô nhớ trong ma trận ảnh.
Đặc biệt, vòng lặp for vẽ vệt sáng quỹ đạo (32 đoạn thẳng nối tiếp) và hàm hiển thị cửa sổ cv2.imshow() ngốn một lượng thời gian đáng kể để đồng bộ với bộ nhớ đồ họa màn hình.
Khi ta dùng một biến cờ (flag dạng True/False) để bỏ qua (bypass) bước vẽ hoặc bước lọc nhiễu phức tạp (cv2.GaussianBlur, erode, dilate), CPU sẽ tiết kiệm được thời gian thực thi đáng kể.
Anh và bé có thể cập nhật phương thức run() trong lớp MainApp của file Buổi 4 như sau. Chúng ta gắn 2 phím tắt:
Phím D (Draw): Bật/Tắt chế độ vẽ vệt đuôi & vòng tròn.
Phím R (Resolution): Chuyển đổi giữa độ phân giải Gốc (100%) và Thu nhỏ (50%).
class MainApp:
def __init__(self, camera_index=0):
self.cap = cv2.VideoCapture(camera_index)
self.fps_counter = FPSCounter()
self.tracker = ColorTracker(lower_hsv=[5, 100, 100],
upper_hsv=[25, 255, 255])
# Các cờ (flags) điều khiển thử nghiệm hiệu năng
self.enable_drawing = True # Có vẽ vệt đuôi hay không
self.low_resolution = False # Có thu nhỏ độ phân giải hay không
def run(self):
if not self.cap.isOpened():
print("❌ Không mở được camera.")
return
print("--- THỬ NGHIỆM ĐO HIỆU NĂNG FPS ---")
print(" -> Phím [D]: Bật / Tắt vẽ vệt sáng & vòng tròn")
print(" -> Phím [R]: Bật / Tắt chế độ độ phân giải thấp (Resize 50%)")
print(" -> Phím [Q]: Thoát")
while True:
ret, frame = self.cap.read()
if not ret:
break
frame = cv2.flip(frame, 1)
# 1. THỬ NGHIỆM ĐỘ PHÂN GIẢI: Nếu bật low_resolution, thu nhỏ ảnh còn 50%
if self.low_resolution:
input_frame = cv2.resize(frame, (0, 0), fx=0.5, fy=0.5)
else:
input_frame = frame
# 2. Xử lý qua tracker
# Nếu tắt vẽ (self.enable_drawing = False), ta có thể truyền cờ này vào tracker
# hoặc đơn giản là đo FPS của việc tính toán thuần túy
processed_frame, mask, center = self.tracker.process(input_frame)
# Cập nhật FPS
current_fps = self.fps_counter.update()
# Hiển thị thông số cấu hình hiện tại lên màn hình
h, w = input_frame.shape[:2]
info_res = f"Res: {w}x{h} (Key R: {self.low_resolution})"
info_draw = f"Draw: {self.enable_drawing} (Key D)"
info_fps = f"FPS: {int(current_fps)}"
cv2.putText(processed_frame, info_fps, (20, 35),
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2)
cv2.putText(processed_frame, info_res, (20, 65),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 0), 1)
cv2.putText(processed_frame, info_draw, (20, 90),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 0), 1)
cv2.imshow("Object Tracker OOP", processed_frame)
# Bắt phím bấm điều khiển
key = cv2.waitKey(1) & 0xFF
if key == ord('q'):
break
elif key == ord('r'):
self.low_resolution = not self.low_resolution
print(f"🔄 Chế độ độ phân giải thấp: {self.low_resolution}")
elif key == ord('d'):
self.enable_drawing = not self.enable_drawing
# Bật/tắt việc xóa trail trong tracker
if not self.enable_drawing:
self.tracker.trail.clear()
print(f"🎨 Chế độ vẽ đồ họa: {self.enable_drawing}")
self.cap.release()
cv2.destroyAllWindows()
Khi bấm phím R để giảm độ phân giải: Ảnh có thể hơi vỡ nét hơn một chút, nhưng FPS thường sẽ nhảy vọt lên cao hơn (đặc biệt thấy rõ trên các máy tính cấu hình văn phòng hoặc chip yếu).
Khi chuyển sang làm các ứng dụng AI như MediaPipe hay YOLO ở các buổi sau: Người ta luôn thu nhỏ ảnh lại trước khi đưa vào mô hình AI để đạt tốc độ thời gian thực (Real-time), sau đó mới phóng to tọa độ kết quả lên màn hình hiển thị. Đây là chuẩn mực tối ưu hóa trong kỹ thuật Computer Vision.