bài học này đưa bạn đến bài toán kinh điển trong giám sát camera an ninh: Làm sao để hệ thống nhận biết có chuyển động và khoanh vùng chính xác đối tượng đang di chuyển mà không cần mô hình AI nặng nề?
Mắt người nhận biết chuyển động khi vị trí của vật thể thay đổi theo thời gian. Máy tính mô phỏng điều này qua phép trừ ma trận:
Các điểm ảnh tĩnh (nền tường, sàn nhà, bàn ghế) có giá trị màu không đổi qua từng khung hình $\rightarrow$ Hiệu số xấp xỉ bằng $0$ (màu đen).
Vật thể di chuyển sẽ tạo ra sự chênh lệch độ sáng lớn giữa hai khung hình liên tiếp $\rightarrow$ Hiệu số $> 0$ (màu trắng).
Đọc 2 frame liên tiếp: $F_1$ và $F_2$.
Chuyển sang ảnh xám (Grayscale) và làm mờ (cv2.GaussianBlur) để khử nhiễu cảm biến camera.
Tính sai phân tuyệt đối: cv2.absdiff(F1, F2).
Phân ngưỡng nhị phân (cv2.threshold): Biến mọi biến đổi lớn hơn ngưỡng thành màu trắng ($255$).
Giãn nở hình thái học (cv2.dilate): Hàn kín các mảng chuyển động rời rạc.
Tìm contours và vẽ khung chữ nhật bao quanh đối tượng di chuyển.
Chúng ta sẽ sử dụng trực tiếp file video mẫu sample_video.mp4 đã tạo ở bài trước (chứa hình tròn xanh di chuyển ngang màn hình). Chúng ta sẽ viết thuật toán phát hiện chuyển động, tự động đóng khung đối tượng di chuyển và xuất ra video giám sát hoàn chỉnh.
Chạy khối mã sau trong một cell mới trên Google Colab:
import cv2
import numpy as np
from google.colab.patches import cv2_imshow
video_input = "sample_video.mp4"
video_output = "motion_detected_output.mp4"
cap = cv2.VideoCapture(video_input)
if not cap.isOpened():
print(f"❌ Không thể mở file '{video_input}'. Hãy chắc chắn file tồn tại!")
else:
# 1. Lấy thông số video
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')
# Khởi tạo VideoWriter (isColor=True vì chúng ta vẽ khung xanh/đỏ)
out = cv2.VideoWriter(video_output, fourcc, fps, (w, h), isColor=True)
# Đọc frame đầu tiên
ret, frame1 = cap.read()
# Đọc frame thứ hai kế tiếp
ret, frame2 = cap.read()
processed_frames = 0
motion_event_count = 0
print("🚨 Bắt đầu quét phát hiện chuyển động qua từng khung hình...")
while ret:
# A. Tính toán sai phân giữa frame1 và frame2
# Bước 1: Tính hiệu số tuyệt đối giữa 2 khung hình màu
diff = cv2.absdiff(frame1, frame2)
# Bước 2: Chuyển về ảnh xám để xử lý 1 kênh
gray = cv2.cvtColor(diff, cv2.COLOR_BGR2GRAY)
# Bước 3: Làm mờ nhẹ khử nhiễu hạt bụi li ti
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
# Bước 4: Phân ngưỡng nhị phân (Thresholding)
# Các pixel lệch độ sáng > 20 sẽ biến thành 255 (trắng)
_, thresh = cv2.threshold(blurred, 20, 255, cv2.THRESH_BINARY)
# Bước 5: Giãn nở hình thái học để nối liền các mảnh vỡ chuyển động
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
dilated = cv2.dilate(thresh, kernel, iterations=3)
# Bước 6: Tìm đường bao chuyển động (Contours)
contours, _ = cv2.findContours(dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# Tạo bản sao frame để vẽ cảnh báo
annotated_frame = frame2.copy()
has_motion = False
for cnt in contours:
# Lọc bỏ nhiễu có diện tích nhỏ hơn 400 pixel vuông
if cv2.contourArea(cnt) < 400:
continue
has_motion = True
# Lấy bounding box của đối tượng chuyển động
x, y, bw, bh = cv2.boundingRect(cnt)
# Vẽ khung cảnh báo màu đỏ
cv2.rectangle(annotated_frame, (x, y), (x + bw, y + bh), (0, 0, 255), 2)
cv2.putText(annotated_frame, "MOTION DETECTED", (x, y - 8),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2)
# B. Vẽ HUD trạng thái giám sát lên góc trên
if has_motion:
motion_event_count += 1
cv2.circle(annotated_frame, (30, 30), 10, (0, 0, 255), -1) # Đèn đỏ nhấp nháy
cv2.putText(annotated_frame, "STATUS: ALERT!", (50, 35),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
else:
cv2.circle(annotated_frame, (30, 30), 10, (0, 255, 0), -1) # Đèn xanh bình thường
cv2.putText(annotated_frame, "STATUS: CLEAR", (50, 35),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)
# Ghi frame đã vẽ vào video đầu ra
out.write(annotated_frame)
processed_frames += 1
# C. Tịnh tiến khung hình: Gán frame2 cho frame1, và đọc frame mới vào frame2
frame1 = frame2
ret, frame2 = cap.read()
cap.release()
out.release()
print(f"✅ Hoàn tất! Đã ghi {processed_frames} frames vào '{video_output}'.")
print(f"📊 Số khung hình ghi nhận chuyển động: {motion_event_count}/{processed_frames}")
Chạy đoạn mã kiểm tra sau để trích xuất 1 frame có chuyển động và xem ngay:
# Mở lại video vừa xuất để kiểm tra chất lượng
check_cap = cv2.VideoCapture("motion_detected_output.mp4")
check_cap.set(cv2.CAP_PROP_POS_FRAMES, 50) # Lấy frame thứ 50
ret_chk, sample_frame = check_cap.read()
check_cap.release()
if ret_chk:
print("Ảnh chụp một frame phát hiện chuyển động:")
cv2_imshow(sample_frame)
Cơ chế dịch chuyển khung (frame1 = frame2):
Trong vòng lặp video, ta không cần giữ danh sách khung hình. Cửa sổ trượt chỉ gồm 2 khung hình liên tiếp được tráo đổi con trỏ liên tục, giúp thuật toán chạy với mức tiêu tốn RAM chỉ vài Megabytes.
Kỹ thuật lọc ngưỡng diện tích (cv2.contourArea(cnt) < 400):
Camera thực tế luôn có rung lắc nhẹ của cành cây hoặc hạt mưa. Việc đặt ngưỡng diện tích tối thiểu loại bỏ hoàn toàn các báo động giả (False Alarms).
Giãn nở (cv2.dilate):
Khi người hoặc xe di chuyển, phần thân giữa áo/áo khoác có thể đồng màu khiến ma trận trừ sai phân bị rỗng ở giữa. Hàm dilate giúp nối các mảng biên rời rạc thành một khối thống nhất để vẽ đúng 1 bounding box lớn bao quanh toàn bộ vật thể.