Giới thiệu thư viện OpenCV và module cv2

Thư viện OpenCV (Open Source Computer Vision Library) – trong Python được gọi và nhập qua module cv2 – là thư viện mã nguồn mở tiêu chuẩn, phổ biến nhất thế giới về thị giác máy tính và xử lý ảnh. Thư viện này ban đầu được phát triển bởi Intel (viết bằng C/C++), sau đó được tối ưu hóa và cung cấp interface cho Python, giúp xử lý ảnh với tốc độ thời gian thực (real-time).

Dưới đây là các điểm cốt lõi về bản chất, các thao tác cơ bản và ứng dụng thực tế của cv2.

1. Bản chất cốt lõi: Ảnh trong cv2 là một mảng NumPy

Khi bạn tải một bức ảnh vào Python bằng cv2, bản chất của bức ảnh đó là một mảng đa chiều (NumPy array) chứa các con số nguyên (thường là uint8, từ 0 đến 255):

  • Ảnh xám (Grayscale): Mảng 2 chiều có kích thước (Chiều cao, Chiều rộng). Mỗi phần tử là một số từ 0 (Đen) đến 255 (Trắng).

  • Ảnh màu (Color Image): Mảng 3 chiều có kích thước (Chiều cao, Chiều rộng, Số kênh màu).

⚠️ Lưu ý kinh điển của cv2: Thay vì thứ tự màu chuẩn RGB (Red - Green - Blue) như hầu hết các thư viện khác (Matplotlib, PIL), OpenCV mặc định đọc ảnh theo thứ tự kênh màu là BGR (Blue - Green - Red). Nếu vẽ ảnh OpenCV bằng Matplotlib mà không đổi hệ màu, ảnh sẽ bị ám xanh dương.

2. Các thao tác cơ bản thường dùng trong cv2

a. Đọc, hiển thị và lưu ảnh

import cv2

# Đọc ảnh (đọc dạng màu BGR mặc định)
img = cv2.imread("anh_dua.jpg")

# Kiểm tra kích thước: (Height, Width, Channels)
print(img.shape)

# Hiển thị cửa sổ xem ảnh
cv2.imshow("Cua so anh", img)
cv2.waitKey(0)  # Chờ bấm phím bất kỳ để đóng
cv2.destroyAllWindows()

# Lưu ảnh ra đĩa
cv2.imwrite("anh_da_luu.jpg", img)

b. Chuyển đổi không gian màu (Color Space Conversion)

  • Grayscale: Giảm từ 3 kênh xuống 1 kênh, giúp thuật toán chạy nhanh gấp 3 lần và giảm độ phức tạp khi không cần thông tin màu.

  • HSV: Rất quan trọng khi cần nhận diện, tách vật thể theo màu sắc (bởi vì hệ màu BGR/RGB bị ảnh hưởng nhiều bởi ánh sáng/bóng râm, còn hệ HSV tách riêng Hue - Sắc độ màu).

# Chuyển BGR sang Grayscale
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# Chuyển BGR sang HSV (Hue, Saturation, Value)
hsv_img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

c. Biến đổi hình học (Geometric Transformations)

  • Cắt ảnh (Crop) sử dụng cú pháp slicing chuẩn của NumPy: crop = img[y1:y2, x1:x2]

  • Thay đổi kích thước (Resize):

resized = cv2.resize(img, (640, 480))  # (Width, Height)

d. Lọc nhiễu và làm mờ (Blurring & Filtering)

Dùng để làm mịn ảnh, khử nhiễu camera trước khi nhận diện viền hoặc bóc tách vật thể:

# Làm mờ Gauss (Gaussian Blur)
blurred = cv2.GaussianBlur(gray_img, (5, 5), 0)

e. Phát hiện cạnh và viền (Edge Detection & Contours)

  • Canny Edge: Thuật toán kinh điển tìm đường bao viền vật thể.

  • Contours: Tìm đường cong nối liền các điểm biên liên tục có cùng màu/cường độ sáng (dùng để đếm số lượng vật thể, đo diện tích, chu vi).

# Tìm cạnh Canny
edges = cv2.Canny(blurred, threshold1=50, threshold2=150)

# Tìm contours (đường biên)
contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
print(f"Số lượng vật thể phát hiện: {len(contours)}")

f. Đọc dữ liệu từ Video hoặc Webcam thời gian thực

# 0 là webcam mặc định của máy tính, hoặc truyền đường dẫn file: "video.mp4"
cap = cv2.VideoCapture(0)

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

    # Xử lý frame ở đây (ví dụ: chuyển sang ảnh xám)
    gray_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

    cv2.imshow("Webcam Live", gray_frame)
    
    # Bấm phím 'q' để thoát vòng lặp
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()

3. Các ứng dụng thực tế trong Thị giác máy tính (Computer Vision)

cv2 đóng vai trò là "nền móng" trong hầu hết các pipeline thị giác máy tính:

  1. Tiền xử lý dữ liệu cho Deep Learning / AI:

    • Trước khi đưa ảnh vào các mô hình mạng nơ-ron như YOLO, ResNet, Vision Transformer (ViT), cv2 được dùng để resize chuẩn hóa kích thước, chuẩn hóa dải pixel, xoay lật tăng cường dữ liệu (Data Augmentation).

  2. Kiểm tra ngoại quan công nghiệp (Defect Inspection & QA):

    • Trong dây chuyền sản xuất/chế biến, dùng các phép lọc hình thái học (Morphology), bóc tách đường biên (findContours) để đo kích thước sản phẩm, phát hiện nứt vỡ, dị tật bề mặt, đếm sản phẩm chạy trên băng chuyền.

  3. Phân đoạn theo màu sắc (Color Segmentation):

    • Dùng dải màu HSV (cv2.inRange) để theo dõi các đối tượng có màu đặc trưng (như quả chín trên cây, nhận diện làn đường, bóng tennis).

  4. Nhận diện khuôn mặt và vật thể cổ điển:

    • cv2 tích hợp sẵn bộ phân loại Haar Cascade (cv2.CascadeClassifier), giúp nhận diện khuôn mặt, mắt, biển số xe nhanh chóng trên CPU mà không cần card đồ họa GPU rời.

  5. Đọc mã vạch / QR Code:

    • OpenCV có module tích hợp sẵn cv2.QRCodeDetector giúp định vị và giải mã chuỗi dữ liệu trong mã QR ngay từ luồng video trực tiếp.

  6. Ghép ảnh toàn cảnh (Image Stitching) & Căn chỉnh ảnh:

    • Sử dụng các thuật toán phát hiện điểm đặc trưng (SIFT, ORB) để tìm các điểm trùng lặp giữa hai bức ảnh và ghép thành ảnh góc rộng (Panorama).

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

Giải thích cách cv2 đóng vai trò tiền xử lý và hậu xử lý khi kết hợp với mô hình Object Detection như YOLO.

Trong một hệ thống phát hiện vật thể (Object Detection) thời gian thực sử dụng YOLO (You Only Look Once), mô hình Deep Learning chỉ đóng vai trò là "bộ não" tính toán tensor (ma trận số), còn cv2 (OpenCV) đóng vai trò là "tai mắt" và "đôi tay" đảm nhận toàn bộ khâu giao tiếp phần cứng, tiền xử lý đầu vào và hậu xử lý/hiển thị đầu ra.

Quy trình phối hợp chuẩn diễn ra theo chu trình 3 giai đoạn:

Plaintext
[Camera/Video Stream] ──(cv2)──> [Giai đoạn 1: Tiền xử lý] ──(cv2)──> [Mảng Tensor chuẩn]
                                                                            │
                                                                       (YOLO Model)
                                                                            ▼
[Màn hình / Lưu Video] <──(cv2)── [Giai đoạn 3: Hậu xử lý] <──(Tọa độ)── [Ma trận dự đoán]

Giai đoạn 1: Tiền xử lý (Pre-processing) với cv2

Mô hình YOLO đòi hỏi dữ liệu đầu vào rất khắt khe: ma trận ảnh phải đúng kích thước (thường là $640 \times 640$), đúng thứ tự kênh màu RGB, giá trị điểm ảnh chuẩn hóa về khoảng $[0.0, 1.0]$. cv2 xử lý các bước này như sau:

  1. Thu nhận luồng hình ảnh (Image Acquisition):

    • Sử dụng cv2.VideoCapture để bắt từng khung hình (frame) từ camera USB, camera công nghiệp RTSP hoặc file video.

  2. Kỹ thuật Letterbox (Resize giữ nguyên tỉ lệ):

    • Nếu dùng cv2.resize đơn thuần ép ảnh từ $1920 \times 1080$ về $640 \times 640$, vật thể sẽ bị méo mó (bẹp/dẹt), làm giảm độ chính xác nhận diện.

    • cv2 thực hiện resize tỉ lệ đồng dạng và đắp thêm viền xám vào hai cạnh thừa (padding) bằng hàm cv2.copyMakeBorder.

  3. Chuyển đổi không gian màu (BGR $\rightarrow$ RGB):

    • Camera qua cv2 đọc ảnh theo hệ BGR, trong khi YOLO được huấn luyện trên hệ RGB. cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) đảo lại đúng thứ tự kênh.

  4. Đóng gói định dạng (Blob format):

    • Sử dụng hàm tích hợp sẵn cv2.dnn.blobFromImage để thực hiện cùng lúc: chuẩn hóa pixel từ $[0, 255]$ sang $[0.0, 1.0]$ và đổi chiều ma trận từ HWC (Height, Width, Channel) sang CHW mà mạng nơ-ron yêu cầu.

Giai đoạn 2: Suy luận mô hình (Model Inference - YOLO)

Tại bước này, YOLO nhận ma trận số từ cv2, chạy qua các lớp tích chập (Convolutional Layers), và trả về một mảng chứa hàng nghìn hộp dự đoán (Bounding Boxes) gồm:

  • Tọa độ tâm, chiều rộng, chiều cao: $(x_{center}, y_{center}, w, h)$.

  • Độ tin cậy (Confidence score): ví dụ $0.85$.

  • Lớp đối tượng (Class ID): ví dụ 0 là "người", 1 là "xe máy".

Giai đoạn 3: Hậu xử lý & Trực quan hóa (Post-processing) với cv2

Kết quả từ YOLO trả về là những con số thô. cv2 tiếp nhận và biến chúng thành thông tin trực quan:

  1. Khử các hộp trùng lặp (Non-Maximum Suppression - NMS):

    • YOLO có thể phát hiện 1 vật thể bằng 5-10 khung hình bao quanh chồng chéo nhau. Hàm cv2.dnn.NMSBoxes lọc và chỉ giữ lại khung hình có độ tin cậy cao nhất và loại bỏ các khung trùng lặp dựa trên chỉ số IoU (Intersection over Union).

  2. Quy đổi tọa độ về kích thước gốc (Coordinate Mapping):

    • Tọa độ của YOLO nằm trên ảnh $640 \times 640$. Cần nhân tỉ lệ nghịch đảo để đưa tọa độ về đúng độ phân giải gốc của camera (ví dụ $1920 \times 1080$).

  3. Vẽ hộp nhận diện (Bounding Box Visualization):

    • Sử dụng cv2.rectangle để vẽ khung chữ nhật bao quanh vật thể.

    • Sử dụng cv2.putText để gắn nhãn tên lớp và tỷ lệ phần trăm tin cậy ngay phía trên hộp.

  4. Đo đạc và tính toán tốc độ khung hình (FPS Counter):

    • Dùng hàm cv2.getTickCount để đo thời gian xử lý từng frame, tính toán FPS và in lên góc màn hình giúp kiểm tra độ trễ thời gian thực.

  5. Hiển thị hoặc truyền phát (Streaming / Storage):

    • Dùng cv2.imshow hiển thị lên màn hình, hoặc cv2.VideoWriter để ghi video đã đóng khung ra ổ cứng, hoặc mã hóa thành JPEG byte stream (cv2.imencode) để truyền qua giao thức HTTP/WebSocket lên web browser.

Code Python minh họa toàn diện pipeline cv2 + YOLO

Đoạn mã sau sử dụng module cv2.dnn (OpenCV Deep Neural Network) để nạp và chạy trực tiếp file mô hình YOLO định dạng ONNX mà không cần cài đặt thêm framework nặng như PyTorch:

import cv2
import numpy as np
import time

# 1. KHỞI TẠO MODEL VÀ DANH SÁCH LỚP
CLASSES = ["person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck"]
net = cv2.dnn.readNetFromONNX("yolov8n.onnx")  # Nạp model ONNX trực tiếp qua cv2

cap = cv2.VideoCapture(0)  # Mở webcam

prev_time = 0

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

    h_orig, w_orig = frame.shape[:2]

    # =================================================================
    # BƯỚC 1: TIỀN XỬ LÝ VỚI CV2 (PRE-PROCESSING)
    # =================================================================
    # blobFromImage: Resize về 640x640, scale [0, 255] -> [0.0, 1.0], BGR -> RGB
    blob = cv2.dnn.blobFromImage(frame, scalefactor=1/255.0, size=(640, 640), swapRB=True, crop=False)
    net.setInput(blob)

    # =================================================================
    # BƯỚC 2: INFERENCE MODEL (SUY LUẬN)
    # =================================================================
    start_infer = time.time()
    outputs = net.forward()  # Tensor đầu ra từ YOLO
    infer_time = time.time() - start_infer

    # =================================================================
    # BƯỚC 3: HẬU XỬ LÝ VỚI CV2 (POST-PROCESSING)
    # =================================================================
    # Chuẩn hóa ma trận đầu ra YOLOv8: shape (1, 84, 8400) -> transpose thành (8400, 84)
    rows = outputs[0].shape[1]
    boxes, confidences, class_ids = [], [], []

    x_factor = w_orig / 640.0
    y_factor = h_orig / 640.0

    # Bóc tách tọa độ và điểm số
    for i in range(rows):
        classes_scores = outputs[0][4:, i]
        _, max_score, _, class_id = cv2.minMaxLoc(classes_scores)
        
        # Ngưỡng tin cậy (Confidence Threshold) > 0.5
        if max_score > 0.5:
            # Tọa độ tâm x, y, chiều rộng w, chiều cao h
            x, y, w, h = outputs[0][0:4, i]
            
            # Quy đổi về tọa độ góc trên bên trái của ảnh gốc
            left = int((x - 0.5 * w) * x_factor)
            top = int((y - 0.5 * h) * y_factor)
            width = int(w * x_factor)
            height = int(h * y_factor)

            boxes.append([left, top, width, height])
            confidences.append(float(max_score))
            class_ids.append(class_id[1])

    # Khử các hộp trùng lặp bằng Non-Maximum Suppression (NMS)
    indices = cv2.dnn.NMSBoxes(boxes, confidences, score_threshold=0.5, nms_threshold=0.4)

    # Vẽ bounding box và nhãn lên frame ảnh gốc
    for idx in indices:
        box = boxes[idx]
        left, top, width, height = box[0], box[1], box[2], box[3]
        confidence = confidences[idx]
        label = f"{CLASSES[class_ids[idx]]}: {confidence:.2f}"

        # Vẽ khung viền
        cv2.rectangle(frame, (left, top), (left + width, top + height), (0, 255, 0), 2)
        
        # Vẽ nền nhãn và chữ
        cv2.rectangle(frame, (left, top - 20), (left + width, top), (0, 255, 0), -1)
        cv2.putText(frame, label, (left, top - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)

    # Tính và in FPS lên góc màn hình
    curr_time = time.time()
    fps = 1 / (curr_time - prev_time) if prev_time != 0 else 0
    prev_time = curr_time
    cv2.putText(frame, f"FPS: {int(fps)} | Infer: {int(infer_time*1000)}ms", (10, 30), 
                cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)

    # Hiển thị frame hoàn chỉnh ra cửa sổ
    cv2.imshow("OpenCV + YOLO Object Detection Pipeline", frame)

    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()

Sự phân công này mang lại hiệu quả cao: các phép toán ma trận nặng được tối ưu trên engine của mô hình (GPU/NPU), trong khi toàn bộ phần I/O, format dữ liệu và đồ họa giao diện do C++ core của cv2 gánh vác mà không tạo điểm nghẽn (bottleneck).