ĐÓNG GÓI OOP NÂNG CAO & ĐO TỐC ĐỘ XỬ LÝ (FPS)

Ở 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).

PHẦN 1: TƯ DUY KIẾN TRÚC PHẦN MỀM (SINGLE RESPONSIBILITY)

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:

  1. 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.

  2. 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.

PHẦN 2: THỰC HÀNH VIẾT MÃ NGUỒN HOÀN CHỈNH

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()

PHẦN 3: ĐIỂM CHỐT KIẾN THỨC CHO BÉ

  1. 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.

  2. 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.

1. Giảm độ phân giải Camera (Resolution)

Cơ chế bản chất:

  • Ở độ 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:

    $$1920 \times 1080 \approx 2.073.600 \text{ điểm ảnh (pixel)}$$

    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:

    $$640 \times 480 = 307.200 \text{ điểm ảnh}$$

    $\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ách thực hiện trong OpenCV:

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)

2. Tắt các bước vẽ và hiển thị (Drawing & Rendering Overhead)

Cơ chế bản chất:

  • 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ể.

3. Code thử nghiệm trực tiếp: Bật/Tắt tính năng bằng phím bấm

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()

Bài học rút ra cho bé khi bấm thử nghiệm:

  1. 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).

  2. 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.