Computer Vision

[Object Tracking] Simple vehicle counter with OpenCV

Xin chào, trong bài viết này chúng ta sẽ tìm hiểu về Object Tracking và giải thuật Centroid Tracking, qua đó áp dụng vào bài toán đếm số lượng xe đang lưu thông trên đường.


OBJECT TRACKING LÀ GÌ?

Nói đơn giản, Object Tracking là một giải thuật nhằm xác định vị trí mới của đối tượng đang chuyển động dựa trên các vị trí của nó trong quá khứ. Chúng ta đều biết rằng các thuật toán Object Detection có thể xác định được vị trí của đối tượng, vậy Object Tracking có lợi thế gì so với việc lặp đi lặp lại giải thuật Object Detection ở mỗi frame ảnh? Câu trả lời là: (1) Object Tracking đơn giản và nhanh hơn so với Object Detection; (2) Object Tracking có thể tiếp tục xử lý khi đối tượng đột ngột biến mất; (3) Object Tracking cho phép định danh các đối tượng đã được phát hiện trước đó.

Một giải thuật Object Tracking lý tưởng phải đáp ứng được các yêu cầu sau:
– Chỉ cần áp dụng Object Detection một lần duy nhất;
– Thời gian xử lý phải nhanh hơn nhiều so với áp dụng Object Detection;
– Có khả năng xử lý tiếp tục khi bị mất dấu đối tượng.

Vì những ưu điểm trên nên Object Tracking thường được áp dụng kèm với Object Detection hoặc Object Recognition nhằm tăng độ chính xác và cung cấp khả năng định danh cho  các đối tượng đã được phát hiện/nhận diện.


GIẢI THUẬT CENTROID TRACKING

Centroid Tracking là một giải thuật tracking đơn giản và rất hiệu quả, bên cạnh đó còn có Kernel-based và Correlation-based Tracking. 
Giải thuật Centroid Tracking hoạt động dựa trên khoảng cách Euclidean giữa trọng tâm của các đối tượng đã xuất hiện trước đó và trọng tâm của các đối tượng mới. 
Bước 1: Phát hiện các đối tượng có trong khung hình hiện tại và tìm trọng tâm của chúng, hình 1.

Hình 1 – Tìm trọng tâm của các đối tượng

Bước 2: Tính toán khoảng cách Euclidean giữa trọng tâm của các đối tượng cũ và các đối tượng có trong khung hình hiện tại. Với các khoảng cách này, chúng ta có thể xác định được mối liên hệ giữa các đối tượng có trong khung hình hiện tại và các đối tượng đã xuất hiện trước đó, từ đó có thể định danh chúng.
Bước 3: So sánh khoảng cách trọng tâm giữa các đối tượng cũ và các đối tượng có trong khung hình hiện tại. Chúng ta sẽ giả định rằng, sự khác biệt về khoảng cách của cùng một đối tượng giữa hai khung hình sẽ nhỏ hơn nhiều so với khoảng cách tới đối tượng mới xuất hiện. Xem hình 2, trong đó, điểm màu đỏ là các đối tượng đã xuất hiện trước đây; điểm màu xanh là các đối tượng có trong khung hình hiện tại. 

Hình 2 – Tính toán khoảng cách giữa các đối tượng

Bước 4: Cập nhật danh sách và vị trí của các đối tượng đang được theo dõi. Một đối tượng có trong khung hình hiện tại sẽ được coi là đã xuất hiện trước đây nếu nó có khoảng cách nhỏ nhất đến một đối tượng cũ, và sẽ được nhận ID của đối tượng đó; ngược lại nó sẽ được cung cấp một ID mới và thêm vào danh sách theo dõi.
Bước 5: Xóa các đối tượng đã biến mất được một thời gian nhất định.


BÀI TOÁN ĐẾM LƯU LƯỢNG XE

Vì mục tiêu chỉ là đếm lưu lượng và không cần phân loại nên chúng ta sẽ áp dụng Background Subtraction để phát hiện ra các phương tiện đang lưu thông trên đường cho đơn giản, các bạn có thể tham khảo bài viết này để hiểu rõ hơn về Background Subtraction.
Dưới đây là source code của class MotionDetector:
File: CarTracking.py

import cv2
import numpy as np
from collections import OrderedDict
from scipy.spatial import distance as dist

class MotionDetector:
    def __init__(self, accumWeight=0.3, minArea=1000):
        self.accumWeight = accumWeight
        self.bg = None
        self.minArea = minArea
 
    def update(self, frame):
        if self.bg is None:
            self.bg = frame.copy().astype("float")
            return
            
        cv2.accumulateWeighted(frame, self.bg, self.accumWeight)
 
    def detect(self, frame, barrier, tVal=25):
        delta = cv2.absdiff(self.bg.astype("uint8"), frame)
        thresh = cv2.threshold(delta, tVal, 255, cv2.THRESH_BINARY)[1]
        
        thresh = cv2.erode(thresh, None, iterations=2)        
        thresh = cv2.dilate(thresh, None, iterations=7)
        
        cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[1]
        
        boundingRects = []
 
        if len(cnts) == 0:
            return None
            
        for c in cnts: 
            if (cv2.contourArea(c) < self.minArea):
                continue
            
            boundingRects.append(cv2.boundingRect(c))
            
        return (thresh, boundingRects)

Tiếp theo chúng ta sẽ thực hiện giải thuật Centroid Tracking với class CentroidTracker:

class CentroidTracker:
    def __init__(self, maxDisappeared=50):
        self.nextObjectID = 0
        self.objects = OrderedDict()
        self.disappeared = OrderedDict()
        
        self.maxDisappeared = maxDisappeared
    
    def register(self, centroid, state):
        self.objects[self.nextObjectID] = {"centroid":centroid, "state":state}
        self.disappeared[self.nextObjectID] = 0
        self.nextObjectID += 1
        
    def deregister(self, objectID):
        del self.objects[objectID]
        del self.disappeared[objectID]
        
    def update(self, rects):
        maxDistance = 50
        
        # if there are not objects in current frame
        # then increase disappeared counter
        if len(rects) == 0:
            for objectID in self.disappeared.keys():
                self.disappeared[objectID] += 1
                
                if self.disappeared[objectID] > self.maxDisappeared:
                    self.deregister(objectID)
                    
            return self.objects
            
        inputCentroids = np.zeros((len(rects), 2), dtype="int")
        
        # find centroid of new objects
        for (i, (x,y,w,h)) in enumerate(rects):
            cX = x + w//2
            cY = y + h//2
            inputCentroids[i] = (cX, cY)
        
        if len(self.objects) == 0:
            for i in range(0, len(inputCentroids)):
                self.register(inputCentroids[i], False)
              
        else:
            objectIDs = list(self.objects.keys())
            objectCentroids = [x["centroid"] for x in self.objects.values()]
            
            # calculate distance between centroids
            D = dist.cdist(np.array(objectCentroids), inputCentroids)
            
            # find the smallest distances
            rows = D.min(axis=1).argsort()
            cols = D.argmin(axis=1)[rows]
            
            usedRows = set()
            usedCols = set()
            
            for (row, col) in zip(rows, cols):
                if row in usedRows or col in usedCols:
                    continue
                
                objectID = objectIDs[row]
                
                # check distance between old and new object
                # if distance < threshold value, then update its position
                if D[row,col] < maxDistance:
                    self.objects[objectID]["centroid"] = inputCentroids[col]
                    self.disappeared[objectID] = 0
                
                    usedRows.add(row)
                    usedCols.add(col)
                
            unusedRows = set(range(0, D.shape[0])).difference(usedRows)
            unusedCols = set(range(0, D.shape[1])).difference(usedCols)
            
            # some objects disappeared
            for row in unusedRows:
                objectID = objectIDs[row]

                self.disappeared[objectID] += 1
                        
                if self.disappeared[objectID] > self.maxDisappeared:
                    self.deregister(objectID)
            
            # new objects are registered        
            for col in unusedCols:
                self.register(inputCentroids[col], False)
                
        return self.objects

Class CentroidTracker nhận một tham số duy nhất là thời gian tồn tại tối đa của đối tượng tính từ lần cuối cùng xuất hiện (tính bằng số lượng khung hình). Sau khi được phát hiện, mỗi đối tượng sẽ được cập nhật vào kho dữ liệu đã có cùng với tọa độ trọng tâm (centroid) và trạng thái (state – đã được đếm hay chưa) của nó.
Trong chương trình chính, MotionDetector sẽ phát hiện chuyển động và tìm ra vị trí hiện tại của phương tiện giao thông, sau đó thông tin này sẽ được đưa vào CentroidTracker để định danh và theo dõi.
File: test.py

import cv2
import numpy as np
from CarTracking import MotionDetector
from CarTracking import CentroidTracker

detector = MotionDetector(accumWeight=0.3, minArea=2000)
tracker = CentroidTracker(maxDisappeared=30)

video = cv2.VideoCapture("trafficvideo.mp4")

totalFrame = 0
counter = 0
barrier = int(video.get(4)) // 2

while True:
 
    ret, frame = video.read()
    if not ret:
        print "Not captured"
        break

    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    gray = cv2.GaussianBlur(gray, (5,5), 0)
    cv2.line(frame, (0, barrier), (frame.shape[1], barrier), (0,255,0), 2)
    
    if totalFrame > 32:
        motion = detector.detect(gray, barrier)
 
        if motion is not None:
            (thresh, boundingRects) = motion
            
            # choose only object that stay before the barrier
            boundingRects = filter(lambda x: x[1]+x[3]//2 < barrier, boundingRects)
            
            objects = tracker.update(boundingRects)

            cv2.imshow("masked", thresh)
            
            for (x,y,w,h) in boundingRects:
                cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2)
            
            for (objectID, object) in objects.items():
                centroid = object["centroid"]
                state = object["state"]
            
                cv2.putText(frame, "{}".format(objectID+1), (centroid[0]-10, centroid[1]-10),
                        cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0,255,0), 2)
                        
                if (centroid[1] >= barrier-50) and (state == False):
                    tracker.objects[objectID]["state"] = True
                    counter += 1

                cv2.circle(frame, (centroid[0], centroid[1]), 4, (0, 255*(1-state), 255*state), -1)
            
    detector.update(gray)
    totalFrame += 1
    cv2.putText(frame, "Count: {}".format(counter), (10, 30),
            cv2.FONT_HERSHEY_SIMPLEX, 1, (0,255,0), 2)
           
    cv2.imshow("Frame", frame)
    key = cv2.waitKey(1) & 0xFF
 
    if key == ord("q"):
        break
 
video.release()
cv2.destroyAllWindows()

Bộ đếm sẽ tăng lên khi có một phương tiện vượt qua đường giới hạn màu xanh lá. Bất cứ phương tiện nào xuất hiện phía sau đường giới hạn sẽ không cần được theo dõi.

Demo:

Đánh giá:
– Ưu điểm: giải thuật đơn giản, thời gian tính toán nhanh.
– Nhược điểm: vì Detector sử dụng Background Subtraction nên độ chính xác không cao, dễ bị nhiễu; phải thực hiện Object Detection ở từng frame ảnh.

Vì những khuyết điểm trên nên bộ Vehicle Counter này chỉ nhằm mục đích áp dụng lý thuyết và demo cho vui chứ còn lâu mới ứng dụng vào thực tế được. 


KẾT LUẬN

Qua bài viết này chúng ta đã có cái nhìn tổng quát về Object Tracking và các yêu cầu cần đảm bảo đối với hệ thống tracking. Phần ứng dụng “bài toán đếm lưu lượng xe” đã cho phép chúng ta áp dụng các thuật toán vào vấn đề thực tế, tuy nhiên vì đây là bộ Vehicle Counter cực kỳ đơn giản nên hiệu quả không cao và không thể áp dụng vào thực tế được.
Cảm ơn các bạn đã theo dõi bài viết.
Thân ái và quyết thắng.

Reference:
[1] A Closer Look at Object Detection, Recognition and Tracking.
[2] Object Tracking using OpenCV (C++/Python)
[3] Simple object tracking with OpenCV.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s