KHÔNG GIAN MÀU HSV & BỘ LỌC MẶT NẠ (COLOR MASK)

PHẦN 1: TẠI SAO LẠI LÀ HSV MÀ KHÔNG PHẢI RGB/BGR?

Ở Buổi 1, con đã biết ảnh camera máy tính đọc theo chuẩn BGR. Nhưng trong bài toán lọc màu thực tế, BGR bộc lộ một nhược điểm chí mạng: Ánh sáng thay đổi làm toàn bộ giá trị B, G, R bị đảo lộn.

  • Vấn đề của BGR: Khi cầm một quả táo đỏ từ chỗ sáng vào bóng râm, màu đỏ thực chất không đổi, nhưng máy tính thấy giá trị $R$ sụt giảm mạnh, $B$ và $G$ cũng biến đổi $\rightarrow$ Máy tính tưởng là vật thể khác hoàn toàn!

  • Giải pháp: Không gian màu HSV (Hue - Saturation - Value)

    HSV tách bạch Màu sắc thực tế ra khỏi Độ sáng:

Plaintext
               H (Hue - Tông màu: 0 - 179)
             Đỏ -> Vàng -> Xanh lá -> Xanh dương -> Đỏ
                        [ 0° đến 360° ]
                             |
         +-------------------+-------------------+
         |                                       |
    S (Saturation - Độ đậm đặc)              V (Value - Độ sáng tối)
       0: Nhạt/Trắng bạch                       0: Tối đen như mực
     255: Màu đậm, rực rỡ                     255: Cực kỳ sáng

💡 Điểm nhấn cho bé:

  • Trong OpenCV, góc màu $H$ chỉ chạy từ $0$ đến $179$ (do 1 byte dữ liệu uint8 không chứa quá 255, nên OpenCV lấy $360^\circ / 2 = 180$ giá trị).

  • $S$ và $V$ chạy từ $0$ đến $255$.

  • Chỉ cần khóa chặt dải $H$ của quả bóng màu xanh, dù trời sáng hay tối (giá trị $V$ thay đổi), máy tính vẫn nhận ra đó là màu xanh!

Dưới đây là phần giải thích chi tiết, trực quan về Không gian màu HSV và Khái niệm Mask (Mặt nạ) để anh có thể chia sẻ và giải thích cho bé một cách dễ hiểu nhất.

1. KHÔNG GIAN MÀU HSV LÀ GÌ?

Trong khi RGB/BGR được thiết kế dựa trên cách hoạt động của phần cứng (màn hình hiển thị trộn các đèn LED đỏ, xanh lá, xanh dương), thì HSV được tạo ra để mô phỏng cách mắt người cảm nhận màu sắc.

Mô hình HSV thường được vẽ dưới dạng một hình trụ hoặc hình nón:

Plaintext
               H (Hue - Góc quay quanh trục tròn: 0° -> 360°)
                     [ Đỏ (0) ]
                   /            \
         [ Tím ]                    [ Vàng ]
            |                          |
         [ Lam ]                    [ Lục ]
                   \            /
                    [ Cyan ]
                    
        <----------------- S (Saturation) ----------------->
         Tâm trục (Nhạt/Trắng) -------> Vành ngoài (Rực rỡ/Đậm đà)

        | V (Value - Chiều dọc trục)
        | Sáng rực rỡ (255)
        |
        V Tối thui/Đen tuyền (0)

Ba thành phần của HSV:

  • H (Hue - Sắc thái màu):

    • Đây là "tên gọi" của màu (đỏ, cam, vàng, lục, lam, chàm, tím).

    • Về mặt hình học, nó là góc quay từ $0^\circ$ đến $360^\circ$ trên vòng tròn màu.

    • Trong OpenCV: Do kiểu dữ liệu 8-bit chỉ lưu số từ $0$ đến $255$, OpenCV chia đôi góc quay để nén về dải $0$ đến $179$ ($180^\circ$ tương ứng với trọn vòng tròn $360^\circ$).

  • S (Saturation - Độ bão hòa / Độ tinh khiết của màu):

    • Thể hiện màu đó "đậm đặc" hay bị "pha loãng" bởi ánh sáng trắng.

    • Giá trị từ $0$ đến $255$:

      • $S = 0$: Màu bị tẩy trắng hoàn toàn, trở thành màu trắng/xám (nằm ở tâm trục).

      • $S = 255$: Màu nguyên chất, rực rỡ nhất (nằm ở rìa ngoài của vòng tròn).

  • V (Value - Độ sáng):

    • Thể hiện lượng ánh sáng chiếu vào màu đó (tương tự như vặn chiết áp đèn).

    • Giá trị từ $0$ đến $255$:

      • $V = 0$: Không có ánh sáng, tất cả biến thành màu đen tuyền.

      • $V = 255$: Ánh sáng tối đa, màu hiển thị rõ nhất.

💡 Ví dụ trực quan cho bé: Hãy tưởng tượng bé cầm một quả cam đi từ ngoài sân nắng vào phòng khách, rồi vào một góc phòng hơi tối:

  • Trong hệ BGR: Màu quả cam thay đổi hoàn toàn ở cả 3 chỉ số $B, G, R$, khiến máy tính tưởng là một vật khác.

  • Trong hệ HSV: Chỉ số H (màu cam) hầu như không đổi, chỉ có V (độ sáng) bị giảm đi. Nhờ đó máy tính vẫn nhận diện quả cam chính xác dù điều kiện sáng thay đổi!

PHẦN 2: KHÁI NIỆM "MẶT NẠ" (MASK)

Để tách riêng vật thể màu mong muốn, ta dùng kỹ thuật tạo Mặt nạ nhị phân (Binary Mask):

  • Máy tính quét qua từng điểm ảnh:

    • Điểm nào có màu nằm trong khoảng quy định: Gán thành số $255$ (Màu trắng).

    • Điểm nào nằm ngoài khoảng: Gán thành số $0$ (Màu đen).

  • Hàm thực hiện việc này trong OpenCV là:

    Python
    mask = cv2.inRange(hsv_frame, lower_bound, upper_bound)
    

 

KHÁI NIỆM "MASK" (MẶT NẠ) TRONG XỬ LÝ ẢNH

Trong xử lý ảnh, Mask (Mặt nạ) là một bức ảnh nhị phân đặc biệt (chỉ gồm 2 giá trị: 0 hoặc 255), dùng để chọn lọc hoặc che đi những phần ta không quan tâm.

Ẩn dụ thực tế: Tấm bìa khoét lỗ

Hãy tưởng tượng anh và bé dùng sơn xịt để vẽ chữ lên áo:

  1. Hai bố con lấy một tấm bìa carton, dùng dao rọc giấy khoét rỗng theo hình chữ mong muốn.

  2. Đặt tấm bìa đó đè lên mặt áo.

  3. Cầm bình sơn xịt phủ lên toàn bộ tấm bìa:

    • Phần bìa cứng sẽ chặn nước sơn lại (phần che giấu).

    • Phần lỗ bị khoét rỗng sẽ cho nước sơn đi xuyên qua thấm vào áo (phần hiển thị).

      Tấm bìa carton đó chính là một Mask.

Plaintext
  [ Khung hình gốc (BGR) ]        [ Tấm Mask nhị phân ]        [ Kết quả (Bitwise AND) ]
      (Mọi màu sắc)               (Đen: 0 | Trắng: 255)            (Chỉ giữ lại vật)
  +----------------------+      +----------------------+      +----------------------+
  |          Phông nền   |      | 00000000000000000000 |      |          Đen         |
  |      [Quả bóng cam]  |  X   |      [ 255 255 ]     |  =   |      [Quả bóng cam]  |
  |     Mặt bàn, tường   |      | 00000000000000000000 |      |          Đen         |
  +----------------------+      +----------------------+      +----------------------+

Cơ chế toán học trong máy tính:

  1. Lệnh cv2.inRange(hsv, lower, upper) tạo ra Mask:

    • Máy tính duyệt qua từng điểm ảnh (pixel).

    • Nếu pixel đó có giá trị HSV nằm trong khoảng $[lower, upper]$, máy tính gán cho pixel tương ứng trên Mask giá trị 255 (Hiển thị màu Trắng).

    • Nếu nằm ngoài khoảng, máy tính gán giá trị 0 (Hiển thị màu Đen).

  2. Lệnh cv2.bitwise_and(frame, frame, mask=mask) trích xuất màu:

    • Máy tính thực hiện phép toán logic AND từng bit giữa bức ảnh gốc và tấm Mask:

      $$\text{Điểm ảnh gốc} \text{ AND } 255 (\text{tất cả bit 1}) = \text{Giữ nguyên màu gốc}$$
      $$\text{Điểm ảnh gốc} \text{ AND } 0 (\text{tất cả bit 0}) = 0 (\text{Màu đen})$$
    • Kết quả thu được là một bức ảnh nền đen hoàn toàn, chỉ duy nhất vật thể lọt qua khe mặt nạ được giữ lại màu sắc nguyên bản.

PHẦN 3: VIẾT CODE - TẠO CÔNG CỤ DÒ DẢI MÀU BẰNG THANH TRƯỢT (TRACKBAR)

Trước khi theo dõi đồ vật, ta cần một công cụ trực quan để 2 bố con cùng kéo thanh trượt, tìm ra thông số màu chuẩn của món đồ đang cầm trên tay.

Ta sẽ thiết kế một Class tên là ColorDetectorApp trong file buoi2_color_tracker.py:

import cv2
import numpy as np

class ColorDetectorApp:
    def __init__(self, camera_index=0):
        self.cap = cv2.VideoCapture(camera_index)
        self.window_name = "Dieu Chinh Mau HSV"
        cv2.namedWindow(self.window_name)

        # Hàm rỗng dùng làm callback cho trackbar
        def nothing(x):
            pass

        # Tạo 6 thanh trượt (Trackbars) tương ứng với Min-Max của H, S, V
        # Tham số: (Tên thanh trượt, Tên cửa sổ, Giá trị ban đầu, Giá trị tối đa, Hàm callback)
        cv2.createTrackbar("H Min", self.window_name, 35, 179, nothing)
        cv2.createTrackbar("H Max", self.window_name, 85, 179, nothing)
        cv2.createTrackbar("S Min", self.window_name, 80, 255, nothing)
        cv2.createTrackbar("S Max", self.window_name, 255, 255, nothing)
        cv2.createTrackbar("V Min", self.window_name, 80, 255, nothing)
        cv2.createTrackbar("V Max", self.window_name, 255, 255, nothing)

    def get_trackbar_values(self):
        """Lấy giá trị hiện tại của 6 thanh trượt"""
        h_min = cv2.getTrackbarPos("H Min", self.window_name)
        h_max = cv2.getTrackbarPos("H Max", self.window_name)
        s_min = cv2.getTrackbarPos("S Min", self.window_name)
        s_max = cv2.getTrackbarPos("S Max", self.window_name)
        v_min = cv2.getTrackbarPos("V Min", self.window_name)
        v_max = cv2.getTrackbarPos("V Max", self.window_name)

        lower_bound = np.array([h_min, s_min, v_min])
        upper_bound = np.array([h_max, s_max, v_max])
        return lower_bound, upper_bound

    def run(self):
        if not self.cap.isOpened():
            print("❌ Không mở được Camera.")
            return

        print(" Đang chạy bộ dò màu...")
        print(" -> Cầm đồ vật trước camera và kéo các thanh trượt")
        print(" -> Nhấn [P] để in thông số dải màu ra terminal")
        print(" -> Nhấn [Q] để thoát")

        while True:
            ret, frame = self.cap.read()
            if not ret:
                break

            # Lật gương cho tự nhiên
            frame = cv2.flip(frame, 1)

            # 1. Chuyển đổi khung hình sang không gian HSV
            hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)

            # 2. Lấy giới hạn màu từ Trackbars
            lower_bound, upper_bound = self.get_trackbar_values()

            # 3. Tạo mặt nạ lọc màu (Mask: Trắng = Trúng màu, Đen = Bỏ qua)
            mask = cv2.inRange(hsv, lower_bound, upper_bound)

            # 4. Trích xuất màu gốc bằng phép toán Bitwise AND (Phép nhân logic)
            result = cv2.bitwise_and(frame, frame, mask=mask)

            # 5. Ghép các khung hình để quan sát song song: Gốc | Mặt nạ | Kết quả
            # Giảm kích thước mỗi khung một nửa để hiển thị vừa màn hình
            small_frame = cv2.resize(frame, (320, 240))
            small_mask = cv2.resize(cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR), (320, 240))
            small_result = cv2.resize(result, (320, 240))

            combined_view = np.hstack([small_frame, small_mask, small_result])

            # 6. Hiển thị
            cv2.imshow(self.window_name, combined_view)

            key = cv2.waitKey(1) & 0xFF
            if key == ord('q'):
                break
            elif key == ord('p'):
                print(f"📊 Dải màu hiện tại:")
                print(f"   lower = np.array({list(lower_bound)})")
                print(f"   upper = np.array({list(upper_bound)})")

        self.cap.release()
        cv2.destroyAllWindows()

if __name__ == "__main__":
    app = ColorDetectorApp(camera_index=0)
    app.run()

PHẦN 4: THỰC HÀNH TƯƠNG TÁC CHO BÉ

  1. Chuẩn bị đạo cụ: Anh nhờ bé tìm một đồ vật có màu sắc rực rỡ và đồng nhất trong nhà (ví dụ: quả bóng tennis màu vàng chanh, nắp chai nước ngọt màu xanh dương, hoặc vỏ bút dạ quang).

  2. Thao tác kéo thanh trượt:

    • Ban đầu để S Min = 100, V Min = 100 (để lọc bỏ các vùng mờ đục hoặc bóng tối).

    • Kéo thanh H Min và H Max quanh vùng màu của vật thể:

      • Màu Xanh lá cây (Green): $H$ thường rơi vào khoảng $35 \rightarrow 85$.

      • Màu Xanh da trời (Blue): $H$ thường rơi vào khoảng $90 \rightarrow 130$.

      • Màu Vàng (Yellow): $H$ thường rơi vào khoảng $20 \rightarrow 35$.

  3. Mục tiêu: Kéo sao cho trên ô màn hình ở giữa (small_mask), vật thể của con hiện lên thành một khối màu trắng trọn vẹn, còn toàn bộ hậu cảnh xung quanh trở thành màu đen tuyền.

  4. Nhấn phím P để in và ghi lại bộ thông số lower và upper đó.

--------------------------------------------------------------------------------------------------------------

Để phân biệt rõ ràng giữa Saturation (Độ bão hòa) và Value (Độ sáng), cách dễ nhất là liên tưởng đến hai hành động thực tế rất quen thuộc: pha màu vẽ và bật/tắt bóng đèn.

1. Phân biệt bằng ví dụ đời thực

  • Saturation ($S$ - Độ bão hòa): Giống như hành động "Pha thêm nước hoặc sơn trắng vào màu vẽ".

    • Đại diện cho độ tinh khiết, độ đậm đặc của màu sắc.

    • Khi $S$ ở mức cao nhất ($255$): Màu sắc nguyên chất, cực kỳ rực rỡ và tươi tắn.

    • Khi $S$ giảm dần về $0$: Màu bị "pha loãng", nhạt dần rồi biến thành màu trắng hoặc xám nhạt, mất hẳn màu gốc ban đầu.

  • Value ($V$ - Độ sáng): Giống như hành động "Vặn núm chiết áp để chỉnh bóng đèn sáng hay tối".

    • Đại diện cho lượng ánh sáng chiếu vào vật thể.

    • Khi $V$ ở mức cao nhất ($255$): Đèn bật sáng tối đa, vật thể hiện rõ màu nhất.

    • Khi $V$ giảm dần về $0$: Căn phòng tối dần, bất kể màu gì ban đầu cũng sẽ chìm vào bóng tối và biến thành màu đen tuyền.

2. Bảng so sánh trực quan (Thử nghiệm với Màu Đỏ: $H \approx 0$)

Trạng thái Thay đổi giá trị nào? Màu sắc mắt nhìn thấy Hiện tượng thực tế
Gốc ban đầu $S = 255, V = 255$ Đỏ tươi rực rỡ Quả táo đỏ đặt dưới ánh đèn đầy đủ
Giảm Saturation $S$ giảm về $0$, giữ nguyên $V = 255$ Đỏ $\rightarrow$ Hồng nhạt $\rightarrow$ Trắng tinh Áo đỏ giặt nhiều lần bị bạc màu, phai màu thành trắng
Giảm Value $V$ giảm về $0$, giữ nguyên $S = 255$ Đỏ $\rightarrow$ Đỏ bầm $\rightarrow$ Nâu đất $\rightarrow$ Đen Cầm quả táo đỏ bước vào phòng tắt dần đèn

3. Tóm tắt quy tắc cốt lõi

  • Hạ Saturation ($S \rightarrow 0$): Rút kiệt màu sắc, đưa vật về màu trắng / xám.

  • Hạ Value ($V \rightarrow 0$): Rút kiệt ánh sáng, đưa vật về màu đen.

Trong bài toán lọc màu bằng camera, việc hiểu rõ hai thông số này giúp bé xử lý nhiễu rất nhanh:

  • Nếu bị lọt các mảng tường trắng, đồ vật màu xám xung quanh $\implies$ Tăng $S_{min}$ lên để loại bỏ những vật nhạt màu.

  • Nếu bị lọt các góc tối, bóng râm đen dưới bàn $\implies$ Tăng $V_{min}$ lên để loại bỏ những điểm thiếu sáng.