Học AI & Lập trình

Optical flow với OpenCV: đo vận tốc băng tải, phát hiện chuyển động

Lucas-Kanade và Farneback trong OpenCV: đo dịch chuyển từng khung hình, quy đổi ra vận tốc băng tải theo m/s và tìm vùng đang chuyển động, có code kiểm chứng.

Optical flow với OpenCV: đo vận tốc băng tải, phát hiện chuyển động
Nội dung bài viết
  1. Giả định nền tảng của optical flow
  2. 1. Tạo hai khung hình
  3. 2. Lucas-Kanade: theo dõi các điểm đặc trưng
  4. 3. Farneback: dòng quang học dày
  5. 4. Quy đổi ra vận tốc thật
  6. 5. Phát hiện vùng đang chuyển động
  7. Optical flow trong nhà máy: khi nào nên dùng?

Làm sao biết băng tải đang chạy nhanh hay chậm mà không gắn encoder? Làm sao phát hiện một vật đang trượt trên máng, hay một cánh tay robot chuyển động bất thường? Câu trả lời là optical flow (dòng quang học): ước lượng mỗi điểm trong ảnh đã di chuyển bao nhiêu pixel giữa hai khung hình liên tiếp. Bài này giới thiệu hai thuật toán có sẵn trong OpenCV, Lucas-Kanade (thưa, theo dõi một số điểm) và Farneback (dày, tính cho mọi pixel), và áp dụng vào bài toán đo vận tốc băng tải.

Code chạy với OpenCV-Python và NumPy, dùng ảnh tạo bằng code với độ dịch chuyển biết trước để bạn kiểm chứng kết quả.

Giả định nền tảng của optical flow

  1. Độ sáng không đổi: một điểm trên vật giữ nguyên độ sáng khi di chuyển giữa hai khung.
  2. Chuyển động nhỏ: mỗi điểm chỉ dời vài pixel giữa hai khung (kim tự tháp ảnh giúp nới lỏng điều này).
  3. Lân cận chuyển động giống nhau: các pixel gần nhau di chuyển gần như cùng một vectơ.

Hệ quả quan trọng: vùng không có vân (một mảng màu đồng nhất) thì không đo được chuyển động, vì không có gì để "bám". Bề mặt băng tải cao su, vải, gỗ thường có đủ vân nhỏ để optical flow hoạt động tốt.

1. Tạo hai khung hình

Ta tạo một dải bề mặt có vân ngẫu nhiên rồi cắt hai khung hình lệch nhau đúng 4 px, giống băng tải chạy sang phải giữa hai lần chụp:

import cv2
import numpy as np

rng = np.random.default_rng(0)
# Bề mặt băng tải có vân ngẫu nhiên (lớn hơn khung hình để có thể "trượt")
be_mat = cv2.GaussianBlur(rng.integers(0, 256, (240, 600), dtype=np.uint8), (0, 0), 2)
be_mat = cv2.normalize(be_mat, None, 0, 255, cv2.NORM_MINMAX)

khung_1 = be_mat[:, 100:420].copy()          # khung hình 240x320
khung_2 = be_mat[:, 96:416].copy()           # băng tải chạy sang phải 4 px giữa 2 khung
print(khung_1.shape, khung_2.shape)
(240, 320) (240, 320)

2. Lucas-Kanade: theo dõi các điểm đặc trưng

Bước 1: chọn những điểm "dễ theo dõi" (góc, chỗ có vân hai chiều) bằng goodFeaturesToTrack. Bước 2: calcOpticalFlowPyrLK tìm vị trí mới của từng điểm ở khung sau.

diem = cv2.goodFeaturesToTrack(khung_1, maxCorners=200, qualityLevel=0.01, minDistance=8)
diem_moi, trang_thai, _ = cv2.calcOpticalFlowPyrLK(khung_1, khung_2, diem, None,
                                                   winSize=(21, 21), maxLevel=3)
tot = trang_thai.ravel() == 1
dich = (diem_moi[tot] - diem[tot]).reshape(-1, 2)
print(f"Lucas-Kanade: {tot.sum()}/{len(diem)} điểm theo dõi được")
print(f"  dịch chuyển trung vị: dx = {np.median(dich[:, 0]):.2f}, dy = {np.median(dich[:, 1]):.2f} px")
Lucas-Kanade: 200/200 điểm theo dõi được
  dịch chuyển trung vị: dx = 4.00, dy = -0.00 px

Đúng 4 px theo phương ngang (giá trị −0,00 chỉ là số âm rất nhỏ, làm tròn về 0). Các tham số chính:

Tham sốÝ nghĩa
winSizeCửa sổ quanh mỗi điểm dùng để so khớp. Lớn hơn thì ổn định hơn với nhiễu nhưng kém chính xác ở chỗ chuyển động không đồng nhất
maxLevelSố tầng kim tự tháp. Mỗi tầng giảm nửa kích thước ảnh, nên với 3 tầng bắt được chuyển động lớn hơn nhiều so với cửa sổ
trang_thai (status)1 nếu tìm thấy điểm ở khung sau. Luôn lọc các điểm có status 0
Dùng trung vị, không dùng trung bìnhTrên dây chuyền thật, sẽ có điểm bị theo dõi sai (phản xạ, vật đi qua che khuất). Trung vị gần như không bị ảnh hưởng bởi vài điểm sai, trung bình thì có.

3. Farneback: dòng quang học dày

Farneback tính vectơ chuyển động cho mọi pixel. Kết quả là mảng (cao, rộng, 2): kênh 0 là dx, kênh 1 là dy.

dong = cv2.calcOpticalFlowFarneback(khung_1, khung_2, None, pyr_scale=0.5, levels=3,
                                    winsize=15, iterations=3, poly_n=5, poly_sigma=1.2, flags=0)
print("Farneback: kích thước trường dòng =", dong.shape)
vung_giua = dong[20:-20, 20:-20]                      # bỏ mép, nơi thiếu thông tin
print(f"  dịch chuyển trung vị: dx = {np.median(vung_giua[..., 0]):.2f}, dy = {np.median(vung_giua[..., 1]):.2f} px")
Farneback: kích thước trường dòng = (240, 320, 2)
  dịch chuyển trung vị: dx = 4.00, dy = -0.00 px

Ý nghĩa các tham số theo tài liệu OpenCV: pyr_scale=0.5 là kim tự tháp cổ điển (mỗi tầng nhỏ đi một nửa), levels là số tầng kể cả ảnh gốc, winsize là cửa sổ lấy trung bình (lớn thì chống nhiễu tốt và bắt chuyển động nhanh tốt hơn nhưng trường dòng mờ hơn), poly_n và poly_sigma điều khiển việc xấp xỉ đa thức quanh mỗi pixel. Mép ảnh bị bỏ khi tính trung vị vì ở đó có phần nội dung mới đi vào khung hình, không có tương ứng ở khung trước.

4. Quy đổi ra vận tốc thật

Có dịch chuyển theo pixel, chỉ cần thêm hai thông số: tỉ lệ mm/pixel (từ quy đổi pixel sang mm) và tốc độ khung hình.

MM_MOI_PX = 0.25        # từ hiệu chuẩn: 1 px = 0,25 mm trên mặt băng tải
FPS = 60                # tốc độ khung hình camera

dx_px = float(np.median(dich[:, 0]))
van_toc = dx_px * MM_MOI_PX * FPS / 1000          # m/s
print(f"Vận tốc băng tải ≈ {van_toc:.3f} m/s ({van_toc * 60:.1f} m/phút)")
Vận tốc băng tải ≈ 0.060 m/s (3.6 m/phút)

4 px × 0,25 mm × 60 khung/giây = 60 mm/s. Trong ứng dụng thật, hãy lấy thời điểm chụp (timestamp) từ camera thay vì giả định FPS cố định, vì camera công nghiệp có thể rớt khung hoặc chạy theo trigger không đều (xem trigger, encoder và cycle time).

5. Phát hiện vùng đang chuyển động

Trường dòng dày cũng cho biết vùng nào đang chuyển động. Ví dụ: băng tải đứng yên, một hộp trượt xuống 5 px:

# Khung 3: băng tải đứng yên, nhưng có một hộp di chuyển xuống dưới 5 px
khung_3 = khung_1.copy()
hop = khung_1[60:120, 140:200].copy()
cv2.rectangle(khung_1, (140, 60), (199, 119), 255, 2)          # viền hộp ở khung 1
khung_3[65:125, 140:200] = hop
cv2.rectangle(khung_3, (140, 65), (199, 124), 255, 2)          # hộp ở khung 3, thấp hơn 5 px

dong = cv2.calcOpticalFlowFarneback(khung_1, khung_3, None, 0.5, 3, 15, 3, 5, 1.2, 0)
do_lon = np.linalg.norm(dong, axis=2)
mat_na = (do_lon > 2).astype(np.uint8)
x, y, w, h = cv2.boundingRect(mat_na)
print(f"Vùng chuyển động: x={x}, y={y}, w={w}, h={h}")
print(f"  dịch chuyển trung vị trong vùng: dy = {np.median(dong[..., 1][mat_na == 1]):.1f} px")
Vùng chuyển động: x=133, y=49, w=74, h=82
  dịch chuyển trung vị trong vùng: dy = 5.0 px

Thuật toán tìm đúng hộp và đo đúng dịch chuyển 5 px. Khung bao (74×82) rộng hơn hộp thật (60×65) vì Farneback lấy trung bình trong cửa sổ, nên chuyển động "lan" ra vài pixel quanh biên vật. Nếu cần đường biên chính xác, hãy dùng optical flow để định vị sơ bộ rồi phân đoạn vật bằng phương pháp khác.

Optical flow trong nhà máy: khi nào nên dùng?

Bài toánGợi ý
Đo vận tốc băng tải, phát hiện trượt, kẹtLucas-Kanade trên một ROI mặt băng tải, lấy trung vị. Nhanh, chạy tốt trên CPU
Phát hiện vật rơi, chuyển động bất thường trong vùng an toànFarneback trên ảnh thu nhỏ, ngưỡng độ lớn vectơ
Ổn định ảnh khi camera rungLucas-Kanade + cv2.estimateAffinePartial2D để tìm chuyển động của cả khung hình
Đếm, theo dõi từng sản phẩmThường đơn giản hơn với phát hiện + theo dõi, như dự án đếm sản phẩm trên băng tải
Cần độ chính xác vị trí tuyệt đối caoEncoder vẫn là chuẩn mực; optical flow phù hợp để giám sát hoặc khi không lắp được encoder
Những thứ phá vỡ optical flowÁnh sáng nhấp nháy (đèn không đồng bộ với camera), bề mặt bóng phản xạ, motion blur do thời gian phơi sáng quá dài, và bề mặt không có vân. Hãy giải quyết từ phần cứng trước: đèn ổn định, thời gian phơi sáng ngắn (xem đèn strobe và đồng bộ trigger).

Bài tập

Thêm nhiễu Gauss (độ lệch chuẩn 10) vào khung_2 và một độ lệch dọc 1 px (dùng be_mat[1:, 96:416] và cắt khung_1 tương ứng cho cùng kích thước). Lucas-Kanade với winSize=(21, 21) và (7, 7) cho kết quả trung vị khác nhau thế nào? Thử thêm độ dịch 30 px với maxLevel=0 và maxLevel=3.

Nguồn tham khảo: OpenCV Tutorials: Optical Flow, OpenCV: calcOpticalFlowPyrLK, calcOpticalFlowFarneback.

Ban biên tập HọcFree

Chúng tôi viết những bài hướng dẫn dễ hiểu, thực tế để ai cũng có thể tự học lập trình và ngoại ngữ, hoàn toàn miễn phí.