本节内容是学习实现如何通过OpenCV实现质心跟踪,一个易于理解且高效的跟踪算法。
目标跟踪的过程:
此外,目标跟踪允许我们将唯一的ID应用于每个被跟踪的对象,从而使我们能够对视频中的唯一对象进行计数。目标跟踪对于建立人员计数器至关重要。
理想的目标跟踪算法将会:
对于任何计算机视觉和图像处理算法来说这都是一个很高的要求,并且我们可以发挥多种技巧来帮助提高我们的目标检测器。
但是在我们构建一个鲁棒的方法之前,我们首先需要研究目标跟踪的基础。
目标跟踪算法被叫做质心跟踪算法是因为它依赖于以下两者的欧式距离:
我将实现一个包含质心追踪算法的Python类,并且创建一个脚本去导入视频并运行目标追踪器。
最后,将运行目标检测器并检查结果,同时注意算法的优缺点。
质心跟踪算法
质心跟踪算法是一个多步骤过程。下面回顾每一个跟踪步骤。
步骤1: 接收边界框坐标并计算质心
质心跟踪算法假设我们正在传递每个单帧中每个检测到的对象的一组边界框(x,y)坐标。
这些边界框可以由您想要的任何类型的对象检测器生成(颜色阈值+轮廓提取,Haar级联,HOG +线性SVM,SSD,Faster R-CNN等),前提是要针对帧中的每个帧进行计算该视频。
有了边界框坐标后,我们必须计算“质心”或更简单地计算边界框的中心坐标(x, y)。上图演示了接收一组边界框坐标并计算质心。
由于这些是提供给我们算法的边界框的第一组初始集合,因此我们将为其分配唯一的ID。
步骤2 计算新边界框和存在目标的欧式距离
对于视频流中的每个后续帧,我们应用计算对象质心的步骤1。但是,我们首先需要确定是否可以将新的对象质心与旧的对象质心相关联,而不是为每个检测到的对象分配新的唯一ID(这会破坏对象跟踪的目的)。为了完成此过程,我们计算了每对现有对象质心和输入对象质心之间的欧式距离。
从上图中你可以看到我们在图像中检测到三个物体。靠近的两对是两个存在的目标。
然后,我们计算每对原始质心和新质心之间的欧式距离。但是,我们如何使用这些点之间的欧式距离来实际匹配它们并将它们关联起来呢?
步骤3:更新存在目标的(x, y)坐标
质心跟踪算法的主要假设是,给定的对象可能会在后续帧之间移动,但是帧的质心之间的距离将小于对象之间的所有其他距离。
因此,如果我们选择将质心与后续帧之间的最小距离相关联,则可以构建对象跟踪器。
上图中,你可以看到质心追踪算法如何选择将最小化欧式距离的质心关联起来。
但是左下角没有任何可以关联的点需要怎么办?
步骤4:注册新的目标
如果输入检测的数量多于被跟踪的现有对象,需要注册一个新目标。注册意味着需要添加一个新的对象到追踪列表中,通过:
现在回到步骤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
需要源码扫描下方二维码公众号内回复。