本節內容是學習實現如何通過OpenCV實現質心跟蹤,一個易於理解且高效的跟蹤算法。
目標跟蹤的過程:
- 進行一組初始的對象檢測(例如邊界框座標的輸入集)
- 爲每一個初始檢測對象創建一個唯一ID
- 然後跟蹤每個對象在視頻中的幀中移動時的情況,並保持唯一ID的分配
此外,目標跟蹤允許我們將唯一的ID應用於每個被跟蹤的對象,從而使我們能夠對視頻中的唯一對象進行計數。目標跟蹤對於建立人員計數器至關重要。
理想的目標跟蹤算法將會:
- 僅僅需要請求一次目標檢測階段
- 將非常快-比運行實際的對象檢測器本身快得多
- 能夠處理當被跟蹤對象“消失”或移動到視頻幀的邊界之外
- 抗遮擋能力強
- 能夠拾取幀之間丟失的目標
對於任何計算機視覺和圖像處理算法來說這都是一個很高的要求,並且我們可以發揮多種技巧來幫助提高我們的目標檢測器。
但是在我們構建一個魯棒的方法之前,我們首先需要研究目標跟蹤的基礎。
目標跟蹤算法被叫做質心跟蹤算法是因爲它依賴於以下兩者的歐式距離:
- 已經存在的目標質心
- 視頻後續幀中的新目標質心
我將實現一個包含質心追蹤算法的Python類,並且創建一個腳本去導入視頻並運行目標追蹤器。
最後,將運行目標檢測器並檢查結果,同時注意算法的優缺點。
質心跟蹤算法
質心跟蹤算法是一個多步驟過程。下面回顧每一個跟蹤步驟。
步驟1: 接收邊界框座標並計算質心
質心跟蹤算法假設我們正在傳遞每個單幀中每個檢測到的對象的一組邊界框(x,y)座標。
這些邊界框可以由您想要的任何類型的對象檢測器生成(顏色閾值+輪廓提取,Haar級聯,HOG +線性SVM,SSD,Faster R-CNN等),前提是要針對幀中的每個幀進行計算該視頻。
有了邊界框座標後,我們必須計算“質心”或更簡單地計算邊界框的中心座標(x, y)。上圖演示了接收一組邊界框座標並計算質心。
由於這些是提供給我們算法的邊界框的第一組初始集合,因此我們將爲其分配唯一的ID。
步驟2 計算新邊界框和存在目標的歐式距離
對於視頻流中的每個後續幀,我們應用計算對象質心的步驟1。但是,我們首先需要確定是否可以將新的對象質心與舊的對象質心相關聯,而不是爲每個檢測到的對象分配新的唯一ID(這會破壞對象跟蹤的目的)。爲了完成此過程,我們計算了每對現有對象質心和輸入對象質心之間的歐式距離。
從上圖中你可以看到我們在圖像中檢測到三個物體。靠近的兩對是兩個存在的目標。
然後,我們計算每對原始質心和新質心之間的歐式距離。但是,我們如何使用這些點之間的歐式距離來實際匹配它們並將它們關聯起來呢?
步驟3:更新存在目標的(x, y)座標
質心跟蹤算法的主要假設是,給定的對象可能會在後續幀之間移動,但是幀的質心之間的距離將小於對象之間的所有其他距離。
因此,如果我們選擇將質心與後續幀之間的最小距離相關聯,則可以構建對象跟蹤器。
上圖中,你可以看到質心追蹤算法如何選擇將最小化歐式距離的質心關聯起來。
但是左下角沒有任何可以關聯的點需要怎麼辦?
步驟4:註冊新的目標
如果輸入檢測的數量多於被跟蹤的現有對象,需要註冊一個新目標。註冊意味着需要添加一個新的對象到追蹤列表中,通過:
- 給它分配一個新的對象ID
- 存儲該對象的邊界框座標的質心
現在回到步驟2對視頻流中的每一幀重複步驟。
上圖演示了使用最小歐式距離來關聯現有對象ID,然後註冊一個新對象的過程。
步驟5:註銷舊的目標
任何合理的對象跟蹤算法都必須能夠處理對象丟失,消失或離開視野時的情況。實際情況下,如何處理這些情況實際上取決於對象跟蹤器的部署位置,但是對於此實現,我們將在舊對象無法與任何現有對象匹配(總共N個後續幀)時註銷舊對象。
目標跟蹤項目結構
# import the necessary packages
from scipy.spatial import distance as dist
from collections import OrderedDict
import numpy as np
class CentroidTracker:
def __init__(self, maxDisappeared=50, maxDistance=50):
# initialize the next unique object ID along with two ordered
# dictionaries used to keep track of mapping a given object
# ID to its centroid and number of consecutive frames it has
# been marked as "disappeared", respectively
self.nextObjectID = 0
self.objects = OrderedDict()
self.disappeared = OrderedDict()
# store the number of maximum consecutive frames a given
# object is allowed to be marked as "disappeared" until we
# need to deregister the object from tracking
self.maxDisappeared = maxDisappeared
# store the maximum distance between centroids to associate
# an object -- if the distance is larger than this maximum
# distance we'll start to mark the object as "disappeared"
self.maxDistance = maxDistance
def register(self, centroid):
# when registering an object we use the next available object
# ID to store the centroid
self.objects[self.nextObjectID] = centroid
self.disappeared[self.nextObjectID] = 0
self.nextObjectID += 1
def deregister(self, objectID):
# to deregister an object ID we delete the object ID from
# both of our respective dictionaries
del self.objects[objectID]
del self.disappeared[objectID]
def update(self, rects):
# check to see if the list of input bounding box rectangles
# is empty
if len(rects) == 0:
# loop over any existing tracked objects and mark them
# as disappeared
for objectID in list(self.disappeared.keys()):
self.disappeared[objectID] += 1
# if we have reached a maximum number of consecutive
# frames where a given object has been marked as
# missing, deregister it
if self.disappeared[objectID] > self.maxDisappeared:
self.deregister(objectID)
# return early as there are no centroids or tracking info
# to update
return self.objects
# initialize an array of input centroids for the current frame
inputCentroids = np.zeros((len(rects), 2), dtype="int")
# loop over the bounding box rectangles
for (i, (startX, startY, endX, endY)) in enumerate(rects):
# use the bounding box coordinates to derive the centroid
cX = int((startX + endX) / 2.0)
cY = int((startY + endY) / 2.0)
inputCentroids[i] = (cX, cY)
# if we are currently not tracking any objects take the input
# centroids and register each of them
if len(self.objects) == 0:
for i in range(0, len(inputCentroids)):
self.register(inputCentroids[i])
# otherwise, are are currently tracking objects so we need to
# try to match the input centroids to existing object
# centroids
else:
# grab the set of object IDs and corresponding centroids
objectIDs = list(self.objects.keys())
objectCentroids = list(self.objects.values())
# compute the distance between each pair of object
# centroids and input centroids, respectively -- our
# goal will be to match an input centroid to an existing
# object centroid
D = dist.cdist(np.array(objectCentroids), inputCentroids)
# in order to perform this matching we must (1) find the
# smallest value in each row and then (2) sort the row
# indexes based on their minimum values so that the row
# with the smallest value as at the *front* of the index
# list
rows = D.min(axis=1).argsort()
# next, we perform a similar process on the columns by
# finding the smallest value in each column and then
# sorting using the previously computed row index list
cols = D.argmin(axis=1)[rows]
# in order to determine if we need to update, register,
# or deregister an object we need to keep track of which
# of the rows and column indexes we have already examined
usedRows = set()
usedCols = set()
# loop over the combination of the (row, column) index
# tuples
for (row, col) in zip(rows, cols):
# if we have already examined either the row or
# column value before, ignore it
if row in usedRows or col in usedCols:
continue
# if the distance between centroids is greater than
# the maximum distance, do not associate the two
# centroids to the same object
if D[row, col] > self.maxDistance:
continue
# otherwise, grab the object ID for the current row,
# set its new centroid, and reset the disappeared
# counter
objectID = objectIDs[row]
self.objects[objectID] = inputCentroids[col]
self.disappeared[objectID] = 0
# indicate that we have examined each of the row and
# column indexes, respectively
usedRows.add(row)
usedCols.add(col)
# compute both the row and column index we have NOT yet
# examined
unusedRows = set(range(0, D.shape[0])).difference(usedRows)
unusedCols = set(range(0, D.shape[1])).difference(usedCols)
# in the event that the number of object centroids is
# equal or greater than the number of input centroids
# we need to check and see if some of these objects have
# potentially disappeared
if D.shape[0] >= D.shape[1]:
# loop over the unused row indexes
for row in unusedRows:
# grab the object ID for the corresponding row
# index and increment the disappeared counter
objectID = objectIDs[row]
self.disappeared[objectID] += 1
# check to see if the number of consecutive
# frames the object has been marked "disappeared"
# for warrants deregistering the object
if self.disappeared[objectID] > self.maxDisappeared:
self.deregister(objectID)
# otherwise, if the number of input centroids is greater
# than the number of existing object centroids we need to
# register each new input centroid as a trackable object
else:
for col in unusedCols:
self.register(inputCentroids[col])
# return the set of trackable objects
return self.objects
需要源碼掃描下方二維碼公衆號內回覆。