Đếm vật thể cắt qua một vạch ảo (Virtual Tripwire)

Kỹ thuật đếm vật thể cắt qua một vạch ảo (Virtual Tripwire) là bài toán cốt lõi trong giám sát giao thông (đếm xe) và bán lẻ (đếm lượt khách ra vào).

Nguyên lý hoạt động gồm 3 bước:

  1. Xác định tâm vật thể (Centroid): Từ contour chuyển động, tính tọa độ trọng tâm $(c_x, c_y)$ của vật thể.

  2. Thiết lập dải va chạm (Detection Offset): Không bắt điểm cắt tuyệt đối tại một tọa độ pixel duy nhất (vì vật thể di chuyển có thể nhảy cóc qua vạch giữa 2 frame liên tiếp do FPS), mà tạo một vùng đệm quanh vạch (ví dụ: $\pm 6$ pixel).

  3. Cơ chế chống đếm lặp (State Management / Debounce): Khi tâm vật thể chạm dải va chạm, tăng biến đếm và đánh dấu trạng thái để không đếm lại vật thể đó ở frame kế tiếp.

Mã thực hành trên Google Colab: virtual_tripwire_counter.py

Kịch bản này sử dụng file video sample_video.mp4 sẵn có trong thư mục /content, thiết lập một vạch thẳng đứng (Line dọc tại $X = 320$) và đếm số lần vật thể cắt qua vạch từ trái sang phải.

import cv2
import numpy as np
from google.colab.patches import cv2_imshow

input_video = "sample_video.mp4"
output_video = "tripwire_counted.mp4"

cap = cv2.VideoCapture(input_video)

if not cap.isOpened():
    print(f"❌ Không thể mở file '{input_video}'. Vui lòng kiểm tra lại đường dẫn!")
else:
    # 1. Lấy thông số khung hình
    w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
    h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
    fps = cap.get(cv2.CAP_PROP_FPS) or 30.0
    fourcc = cv2.VideoWriter_fourcc(*'mp4v')
    out = cv2.VideoWriter(output_video, fourcc, fps, (w, h), isColor=True)

    # 2. Thiết lập vạch ảo (Virtual Tripwire)
    # Ở đây dùng vạch dọc ở giữa màn hình (X = 320)
    LINE_X = w // 2
    OFFSET = 8  # Dải sai số bắt va chạm (+- 8 pixels quanh LINE_X)

    # Đọc 2 frame khởi tạo luồng trừ nền
    ret, frame1 = cap.read()
    ret, frame2 = cap.read()

    total_count = 0
    # Biến cờ (flag) ghi nhận trạng thái vật thể đang nằm trong dải vạch kẻ
    object_in_trigger_zone = False

    print("🚦 Bắt đầu giám sát và đếm vật thể qua vạch ảo...")

    while ret:
        # A. Phép trừ nền phát hiện chuyển động
        diff = cv2.absdiff(frame1, frame2)
        gray = cv2.cvtColor(diff, cv2.COLOR_BGR2GRAY)
        blur = cv2.GaussianBlur(gray, (5, 5), 0)
        _, thresh = cv2.threshold(blur, 20, 255, cv2.THRESH_BINARY)
        dilated = cv2.dilate(thresh, None, iterations=3)
        contours, _ = cv2.findContours(dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

        annotated_frame = frame2.copy()

        # B. Vẽ vạch ảo (Tripwire) và dải cảm ứng
        # Vẽ vạch chính (Màu đỏ: (0, 0, 255))
        cv2.line(annotated_frame, (LINE_X, 0), (LINE_X, h), (0, 0, 255), 2)
        # Vẽ 2 đường biên dải sai số (Màu xám đứt đoạn mô phỏng)
        cv2.line(annotated_frame, (LINE_X - OFFSET, 0), (LINE_X - OFFSET, h), (70, 70, 70), 1)
        cv2.line(annotated_frame, (LINE_X + OFFSET, 0), (LINE_X + OFFSET, h), (70, 70, 70), 1)

        detected_in_this_frame = False

        for cnt in contours:
            # Lọc bỏ nhiễu nhỏ
            if cv2.contourArea(cnt) < 500:
                continue

            # Tính Bounding Box
            x, y, bw, bh = cv2.boundingRect(cnt)

            # Tính tọa độ trọng tâm (Centroid) của vật thể
            cx = x + bw // 2
            cy = y + bh // 2

            # Vẽ bounding box và chấm tâm vật thể
            cv2.rectangle(annotated_frame, (x, y), (x + bw, y + bh), (0, 255, 0), 2)
            cv2.circle(annotated_frame, (cx, cy), 5, (255, 0, 0), -1)

            # C. Kiểm tra va chạm với dải vạch ảo
            if (LINE_X - OFFSET) <= cx <= (LINE_X + OFFSET):
                detected_in_this_frame = True
                
                # Nếu vật thể mới chạm vào dải (chưa được tính trước đó)
                if not object_in_trigger_zone:
                    total_count += 1
                    object_in_trigger_zone = True
                    print(f"🎯 [BẮT ĐƯỢC] Vật thể vừa cắt qua vạch! Tổng số lượt: {total_count}")

                # Đổi màu vạch sang màu vàng/xanh để biểu thị trạng thái đang chạm vạch
                cv2.line(annotated_frame, (LINE_X, 0), (LINE_X, h), (0, 255, 255), 3)
                cv2.circle(annotated_frame, (cx, cy), 8, (0, 0, 255), -1)

        # Nếu không còn vật thể nào trong dải va chạm, reset cờ để sẵn sàng đếm lượt tiếp theo
        if not detected_in_this_frame:
            object_in_trigger_zone = False

        # D. Hiển thị bảng điều khiển số liệu (HUD Counter)
        # Vẽ khung nền đen góc trái trên
        cv2.rectangle(annotated_frame, (20, 20), (280, 80), (0, 0, 0), -1)
        cv2.putText(annotated_frame, f"PASSED: {total_count}", (35, 62),
                    cv2.FONT_HERSHEY_SIMPLEX, 1.1, (0, 255, 0), 3, cv2.LINE_AA)

        out.write(annotated_frame)

        frame1 = frame2
        ret, frame2 = cap.read()

    cap.release()
    out.release()
    print(f"\n🎉 Hoàn thành xử lý! Tổng cộng có {total_count} lượt cắt qua vạch.")

Kiểm tra frame có sự kiện va chạm ngay trên Colab

Chạy cell sau để trích xuất frame tại thời điểm vật thể đang chạm vạch:

# Kiểm tra frame thứ 60 (thời điểm vật thể chạm vạch kẻ giữa)
cap_check = cv2.VideoCapture("tripwire_counted.mp4")
cap_check.set(cv2.CAP_PROP_POS_FRAMES, 60)
ret, f = cap_check.read()
cap_check.release()

if ret:
    print("Ảnh chụp khoảnh khắc vật thể chạm vạch ảo:")
    cv2_imshow(f)

Các lưu ý kỹ thuật khi áp dụng vào thực tế

  • Vạch ngang (Horizontal Tripwire): Nếu camera nhìn từ trên xuống và xe chạy dọc màn hình, đổi trục kiểm tra từ $X$ sang $Y$: LINE_Y = h // 2 và kiểm tra (LINE_Y - OFFSET) <= cy <= (LINE_Y + OFFSET).

  • Phân biệt hướng di chuyển (In vs Out): Trong hệ thống thực tế có nhiều người qua lại cùng lúc, giải pháp chuẩn là lưu vết tọa độ $c_{x}^{(t-1)}$ và so sánh với $c_{x}^{(t)}$:

    • Nếu $c_{x}^{(t-1)} < \text{LINE\_X}$ và $c_{x}^{(t)} > \text{LINE\_X}$ $\rightarrow$ Hướng đi vào (IN).

    • Nếu $c_{x}^{(t-1)} > \text{LINE\_X}$ và $c_{x}^{(t)} < \text{LINE\_X}$ $\rightarrow$ Hướng đi ra (OUT).

  • Nhiều vật thể cùng lúc: Với bài toán đông người hoặc dòng xe cộ, cần tích hợp thêm thuật toán theo dõi định danh (như Centroid Tracker hoặc SORT/DeepSORT) để gắn ID duy nhất cho từng vật thể trước khi xét va chạm.