多目标跟踪DeepSort

先引入多目标跟踪DeepSort的论文地址及代码链接(Python版):

论文地址:https://arxiv.org/pdf/1703.07402.pdf

代码链接:https://github.com/nwojke/deep_sort

SORT是一种实用的多目标跟踪算法,然而由于现实中目标运动多变且遮挡频繁,该算法的身份转换(Identity switches)次数较高。DEEPSORT整合外观信息使得身份转换的数量减少了45%。DEEPSORT属于传统的单假设跟踪算法,采用递归卡尔曼滤波和逐帧数据关联。所提方案为:

(1)使用马氏距离和深度特征余弦距离两种度量

(2) 采用级联匹配,有限匹配距上次出现间隔短的目标;

(3)第一级关联以余弦距离作为成本函数,但设定马氏距离和余弦距离两个阈值约束;

(4)第二关联与SORT中相同,尝试关联未确认和年龄为n=1的不匹配轨迹;

(5)同样采用试用期甄别目标,但大幅提高轨迹寿命Amax=30.
论文解读

1.轨迹处理及状态估计(track handing and state estimation)

对于每条轨迹track都有一个阈值a用于记录轨迹从上一次成功匹配到当前时刻的时间,当该值大于设置的阈值Amax=30则认为该轨迹终止,直观上说就是长时间匹配不上的轨迹认为已经走出监控范围。

轨迹的三种状态:(在代码中类似枚举变量)tentative(初始默认状态),confirmed(确认的),deleted(删除的)

class TrackState:
    """
    Enumeration type for the single target track state. Newly created tracks are
    classified as `tentative` until enough evidence has been collected. Then,
    the track state is changed to `confirmed`. Tracks that are no longer alive
    are classified as `deleted` to mark them for removal from the set of active
    tracks.
    """
    Tentative = 1
    Confirmed = 2
    Deleted = 3

在匹配时,对于没有匹配成功的检测都认为可能产生新的轨迹。但由于这些检测结果可能是一些错误警告,所以,对这种新生成的轨迹标注状态tentative(初始默认状态);

然后判定在接下来的连续3帧中是否连续匹配成功,若成功,则由tentative修改为confirmed,认为是新轨迹产生;

否则,由tentative修改为deleted,删除。另外,超过预先设置的Amax=30的轨迹,也被认为离开场景,由confirmed修改为deleted,删除

    def mark_missed(self):
        """Mark this track as missed (no association at the current time step).
        """
        if self.state == TrackState.Tentative:
            self.state = TrackState.Deleted
        elif self.time_since_update > self._max_age:
            self.state = TrackState.Deleted

2、分配(匹配)问题(aaignment problem)

这里的匹配,是将过去的轨迹tracks与当前的检测detections之间的匹配。

根据tracks与detections之间的相似程度(这个相似程度是根据外观特征iou计算得出的,使用匈牙利算法对相似程度进行筛选,得到tracks与detections之间的匹配对;

匈牙利算法:

  可以告诉我们当前帧的某个检测目标detection,是否与前一帧(或者更早帧)的某个目标track是同一个物体。

首先,先介绍一下什么是分配问题(Assignment problem):假设有N个人和N个任务,每个任务可以任意分配给不同的人,已知每个人完成每个任务要花费的代价不尽相同,那么如何分配可以使得总的代价最小?

举个例子,假设现在有3个任务,要分别分配给3个人,每个人完成各个任务所需代价矩阵(cost matrix)如下所示,这个代价可以是金钱,时间等等:

多目标跟踪DeepSort_第1张图片

怎样才能找到一个最优分配,使得完成所有任务花费的代价最小呢?

匈牙利算法(又叫KM算法)就是用来解决分配问题的一种方法,它基于定理:

如果代价矩阵的某一行或某一列同时加上或减去某个数,则这个新的代价矩阵的最优分配仍然是原代价矩阵的最优分配。

算法步骤(假设矩阵为N×N方阵):

1、对于矩阵的每一行,减去其中最小的元素

2、对于矩阵的每一列,减去其中最小的元素

3、用最少的水平线或垂直线覆盖矩阵中所有的0

4、如果线的数量=N,则找到了最优分配,算法结束,否则进入5

5、找到没有被任何线覆盖的最小元素,每个没被线覆盖的行减去这个元素,每个被线覆盖的列加上这个元素,返回步骤3

继续拿上面的例子做演示:

step1 每一行最小的元素分别为15、20、20,减去得到:

多目标跟踪DeepSort_第2张图片

step2 每一列最小的元素分别为0、20、5,减去得到:

多目标跟踪DeepSort_第3张图片

step3 用最少的水平线或垂直线覆盖所有的0,得到:

多目标跟踪DeepSort_第4张图片

step4 线的数量为2,小于3,进入下一步;

step5 现在没被覆盖的最小元素是5,没被覆盖的行(第一和第二行)减去5,得到:

多目标跟踪DeepSort_第5张图片

被覆盖的列(第一列)加上5,得到:

多目标跟踪DeepSort_第6张图片

跳转到step3,用最少的水平线或垂直线覆盖所有的0,得到:

多目标跟踪DeepSort_第7张图片

step4:线的数量为3,满足条件,算法结束。显然,将任务2分配给第1个人、任务1分配给第2个人、任务3分配给第3个人时,总的代价最小(0+0+0=0):

多目标跟踪DeepSort_第8张图片

所以原矩阵的最小总代价为(40+20+25=85):

多目标跟踪DeepSort_第9张图片

sklearn里的linear_assignment()函数以及scipy里的linear_sum_assignment()函数都实现了匈牙利算法,两者的返回值的形式不同:

import numpy as np 
from sklearn.utils.linear_assignment_ import linear_assignment
from scipy.optimize import linear_sum_assignment
 

cost_matrix = np.array([
    [15,40,45],
    [20,60,35],
    [20,40,25]
])
 
matches = linear_assignment(cost_matrix)
print('sklearn API result:\n', matches)
matches = linear_sum_assignment(cost_matrix)
print('scipy API result:\n', matches)
 

"""Outputs
sklearn API result:
 [[0 1]
  [1 0]
  [2 2]]
scipy API result:
 (array([0, 1, 2], dtype=int64), array([1, 0, 2], dtype=int64))
"""

在DeepSORT中,先基于外观信息(appearance information)或者IOU计算出成本矩阵矩阵cost_matrix。再用匈牙利算法对成本矩阵cost_matrix计算匹配对:

linear_assignment.py
def min_cost_matching(
        distance_metric, max_distance, tracks, detections, track_indices=None,
        detection_indices=None):
    ...
    cost_matrix = distance_metric(#由距离度量指标计算成本矩阵
        tracks, detections, track_indices, detection_indices)
    ...    
    indices = linear_assignment(cost_matrix)#调用了sklearn包的函数执行匈牙利算法,得到匹配成功的索引对,行索引为tracks的索引,列索引为detections的索引,关联检测框
    #[[0 1]
    # [1 0]
    # [2 2]]

上述代码中的distance_metric()是一个函数,会在min_cost_matching()被调用时传入,有时传入的是余弦距离,有时是iou距离。

轨迹track处理:

      这个主要说轨迹track什么时候终止,什么时候产生新的轨迹。首先对于每条轨迹都有一个阈值a用于记录轨迹从上一次成功匹配到当前时刻的时间。当该值大于提前设定的阈值(_max_age=30,从时间来讲大概不到2秒)则认为该轨迹终止(就是人已经走出了监控范围了),直观上说就是长时间匹配不上的轨迹认为已经结束。

track.py
    def mark_missed(self):
       ...
        elif self.time_since_update > self._max_age:#30,上次匹配成功到当前时刻的时间,就是有30次都没有再更新了,就把这个track标记为deleted
            self.state = TrackState.Deleted

然后在匹配时,对于没有匹配成功的检测目标detections都认为是新的track.但由于这些detections可能是一些false alarms,所以对这种情形新生成的轨迹标注状态为tentative,

tracker.py
   def update(self,detections):
        ...
        for detection_idx in unmatched_detections:#对于未匹配成功的detectciton,初始化为新的track,
            #对于第一帧图片,暂时只有检测目标,比如有3个检测目标,则全部为unmatched_detections,则将这3个检测目标初始化
            self._initiate_track(detections[detection_idx])

    def _initiate_track(self, detection):#这里传入来的detection,本质上是一个类class,定义在detection.py,print(detection)是一个地址
        mean, covariance = self.kf.initiate(detection.to_xyah())#由检测目标detection的坐标((center x, center y, aspect ratio,
       # height)构建均值向量(1×8的向量,初始化为[cenx,ceny,w/h,h,0,0,0,0]与协方差矩阵(8*8矩阵,根据目标的高度构造的一个对角矩阵),
        self.tracks.append(Track(
            mean, covariance, self._next_id, self.n_init, self.max_age,
            detection.feature))#Track()本身是一个类,这里把每个新目标用一个类来刻画,放入tracks列表
        self._next_id += 1

track.py
class Track:   
    def __init__(self, mean, covariance, track_id, n_init, max_age,
                 feature=None):
       ...
        self.state = TrackState.Tentative#所有的track初始状态都是tentative
       ...

然后观测这些新的track,

case 1: 若在下一帧中能匹配成功, 且能够连续3次匹配成功的话,是的话则认为是新轨迹产生,状态修改为confirmed  ,否则则认为是假性轨迹,标注为deleted.

tracker.py
 def update(self, detections):#执行测量更新和跟踪管理,入参detections本质是一个类,类中的各个属性和方法由一对对的box与对应的128特征向量刻画
      ...
     for track_idx, detection_idx in matches:#对于匹配好的track和detection           
            self.tracks[track_idx].update(self.kf, detections[detection_idx])
 #对能够匹配成功的track,调用track.py中的update对这个track的状态进行更新
track.py
    def update(self, kf, detection):       
        ...       
        self.hits += 1#标志成功匹配次数,主要是对新出现的track有意义,若这个新的track能够被连续匹配成功3次,        
        if self.state == TrackState.Tentative and self.hits >= self._n_init:#对与一个新出现的检测目标,若连续3帧都匹配成功,就改变其状态为confirmed
            self.state = TrackState.Confirmed

case 2: 若一个新的track,在下帧中没有匹配成功,且其状态还是tentative时,就标记其为Deleted,

注记:若是一个新的track,即使其被匹配成功过1次或2次,在没满3次之前,都会被删除

tracker.py
def update(self, detections):#执行测量更新和跟踪管理,入参detections本质是一个类,类中的各个属性和方法由一对对的box与对应的128特征向量刻画
    for track_idx in unmatched_tracks:#对于未匹配成功的track,将其状态进行改变          
       self.tracks[track_idx].mark_missed()#调用track.py中的函数对这个track状态进行管理

track.py
    def mark_missed(self):        
        if self.state == TrackState.Tentative:#若原来是实验性的,即是上一帧中出现的新的检测目标,在这一帧没有被匹配上,则改变其状态为deleted
            self.state = TrackState.Deleted
   

源码解析:

 

1.初始化余弦距离测量,跟踪器

main.py
metric = nn_matching.NearestNeighborDistanceMetric("cosine", max_cosine_distance, nn_budget)#这里初始化一个类,是余弦距离的实现
tracker = Tracker(metric)#Tracker是一个类,这里初始化一个跟踪器

2.用某一检测器如yolo或其他来检测出目标位置box,形如[xmin,ymin,xmax,ymax],用某一特征提取器提取出128维的特征feature

3.根据目标位置box,目标特征feature实例化检测目标detection(是一个类)

main.py
# image = Image.fromarray(frame)
        image = Image.fromarray(frame[...,::-1]) #bgr to rgb
        boxs,class_names = yolo.detect_image(image)#返回检测到的目标的位置box和类别,这里已经执行过NMS
        #boxs [[1568, 646, 74, 217], [1570, 504, 66, 158], [1320, 460, 56, 170]]
        #class_names [['person'], ['person'], ['person']]
        features = encoder(frame,boxs)#对每个图片,提取128维度的特征向量,这里的用法比较特殊,函数encoder的参数在上面已经传好了,
        #但encoder函数本身嵌套了一个子函数,这里给子函数传进去了参数frame,boxs,函数返回值为m*128,m具体多大,要看在当前视频帧上检测出了多少个目标
        # score to 1.0 here).
         #Detection本身是一个类,代表每一个检测目标的属性       
        detections = [Detection(bbox, 1.0, feature) for bbox, feature in zip(boxs, features)]#zip之后,将每个box和其对应的特征feature组合在一起
       

4.将这个detection传入tracker.py-->update(self,detections)执行测量更新和跟踪管理 

main.py
...
# Call the tracker
tracker.predict()#将跟踪状态分布向前传播一步。
tracker.update(detections)#执行测量更新和跟踪管理。

  4.1  先对所有的检测目标进行级联匹配,检测目就是把过去的目标与新检测出的目标配对,能配对在一起的matches认为是同一个目标(同一人),匹配未成功的过去目标就是unmatched_tracks,匹配未成功的新建测目标就是unmatched_detections

tracker.py
 def update(self, detections):#执行测量更新和跟踪管理,入参detections本质是一个类,类中的各个属性和方法由一对对的box与对应的128特征向量刻画       
        # Run matching cascade.得到匹配对,未匹配的tracks,未匹配的detections
        matches, unmatched_tracks, unmatched_detections = \
            self._match(detections)#调用_match进行级联匹配,对于第一帧图片,暂时只有检测目标,比如有3个检测目标,此时匹配结果为

     4.1.1对于第一帧图片,比如检测到了3个目标,因为现在没有过去的跟踪信息,这3个目标没有可匹配对象,自然全是unmatched_detections, 则会将这3个检测目标初始化为新的track,append到跟踪目标集合中self.tracks中

tracker.py
        for detection_idx in unmatched_detections:#对于未匹配成功的detectciton,初始化为新的track,
            #对于第一帧图片,暂时只有检测目标,比如有3个检测目标,则全部为unmatched_detections,则将这3个检测目标初始化
            self._initiate_track(detections[detection_idx])

    def _initiate_track(self, detection):#这里传入来的detection,本质上是一个类class,定义在detection.py,print(detection)是一个地址
        mean, covariance = self.kf.initiate(detection.to_xyah())#由检测目标detection的坐标((center x, center y, aspect ratio,
       # height)构建均值向量(1×8的向量,初始化为[cenx,ceny,w/h,h,0,0,0,0]与协方差矩阵(8*8矩阵,根据目标的高度构造的一个对角矩阵),
        self.tracks.append(Track(
            mean, covariance, self._next_id, self.n_init, self.max_age,
            detection.feature))#Track()本身是一个类,这里把每个新目标用一个类来刻画,放入tracks列表
        self._next_id += 1

     4.1.2 对于第二帧图片,仍然先检测目标比如3个,检测这3个目标的特征,然后执行tracker.py中的predict和update

      注意,第二帧时,因为第一帧图片有检测目标,已经有了几个轨迹tracks,所以现在要调用级联匹配,确定下第一帧的检测目标与第二帧的检测目标是否为同一物体。并且因为第一帧的目标的状态都是tentative,所以这里执行的是iou距离,而不会调用余弦距离来计算两帧目标之间的相似度。

tracker.py
 def update(self, detections):#执行测量更新和跟踪管理,入参detections本质是一个类,类中的各个属性和方法由一对对的box与对应的128特征向量刻画        
        matches, unmatched_tracks, unmatched_detections = \
            self._match(detections)#调用_match进行级联匹配
def _match(self, detections):#入参detections为当前帧检测到的所有目标,实现了论文2.3matching cascade级联匹配的内容
...
 unconfirmed_tracks = [i for i, t in enumerate(self.tracks) if not t.is_confirmed()]#[5, 6, 7]
...
# 现在处理上面未参与外观匹配的新的轨迹,即uncofirmed tracks,同时把那些虽然是confirmed track,但外观匹配仅在上一帧没有成功的track放进来
#那些长时间没有被匹配成功的track不用iou匹配,因为长时间没有匹配到,人早就走掉了,iou重合率也会非常低,iou比较高的也不太可能是同一个人
iou_track_candidates = unconfirmed_tracks + [k for k in unmatched_tracks_a if
            self.tracks[k].time_since_update == 1]
#根据track与detection之间的iou,使用匈牙利算法解决两者之间的匹配问题,对于第一帧图片,暂时只有检测目标,比如有3个检测目标,此时返回值为
# matches_b, unmatched_tracks_b, unmatched_detections [] [] [0, 1, 2]
#对于用外观特征匹配失败的的tracks和检测目标,用iou来匹配一下,实践证明,iou匹配的效果也很好
matches_b, unmatched_tracks_b, unmatched_detections =\           linear_assignment.min_cost_matching(iou_matching.iou_cost, self.max_iou_distance, self.tracks,detections, iou_track_candidates, unmatched_detections)

细节解析:

1 运动匹配

         用Mahalanobis距离(马氏距离)来表示第j个检测和第i条轨迹之间的运动匹配程度,公式如下图所示:

d^{(1)}(i,j)=(d_j-y_i)^TS_i^{-1}(d_j-y_i)

其中,

dj表示第j个检测的状态track_j;

yi是轨迹在当前时刻的预测值detection_i;

si是由kalman滤波器预测得到的矩阵;

通过该马氏距离对检测框进行筛选,使用卡方分布的0.95分位点作为阈值。

2外观匹配

    在实际中,比如相机运动,都会导致马氏距离匹配失效,因此引入余弦距离(第i次跟踪和第j次检测的最小余弦距离)来进行外观匹配,该匹配对常时间遮挡后恢复尤其有用,公式如下:

d^{(2)}(i,j)=min \{ 1-r_j^Tr_k^{(i)}|r_k^{(i)}\in R_i \}

最后,利用加权的方式对这两个距离进行融合。关联度量的总公式如下所示:

c_{i,j}=\lambda d^{(1)}(i,j)+(1-\lambda) d^{(2)}(i,j)

外观匹配和基于卡尔曼滤波,马氏距离的融合见上面代码中linear_assigenment.py-->gate_cost_matrix()中对成本矩阵

cost_matrix的修正。

3、级联匹配(matching cascade)

     当一个目标被遮挡很长时间,kalman滤波的不确定性就会大大增加,为了解决该问题,论文采用级联匹配的策略来提高匹配精度。文中算法为

多目标跟踪DeepSort_第10张图片

其中,T表示目标跟踪集合

D表示目标检测集合

C矩阵存放所有目标跟踪与目标检测之间距离的计算结果

B矩阵存放所有目标跟踪与目标检测之间是否关联的判断(0或者1)

M,U为返回值,分别表示匹配集合和非匹配集合。

深度表观特征(deep appearance descriptor)

   论文中,作者使用一个深度卷积神经网络去提取目标的特征信息,论文中的预训练网络是在一个ReID的大数据集上训练得到的,包含1261个人的11,000,000幅图像,非常适合对人物目标的跟踪。

网络结构如下

多目标跟踪DeepSort_第11张图片

该网络有2800864个参数和32个目标框,在NVIDIA GTX1050上需要30ms.

程序算法

算法实体为Tracker,kalmanFilter, Track, NearestNeighborDistanceMetric和Detection.KalmanFilter中自己定义了马氏距离的计算,

NearestNeighborDistanceMetric能够计算特征相似度,对于每个目标,返回到目前为止已观察到的任何样本的最近距离(欧式或余弦),由距离度量方法构造一个tracker.

main.py
metric = nn_matching.NearestNeighborDistanceMetric("cosine", max_cosine_distance, nn_budget)
tracker = Tracker(metric)

create_detections从原始检测矩阵创建给定帧索引的检测

non_max_suppression,NMS最大值抑制重叠检测

linear_assignment.py中定义了阈值和匹配函数

tracker.predict将跟踪状态分布向前传播一步

tracker.update执行测量更新和跟踪管理。

匹配:首先对基于外观信息的马氏距离计算tracks和detections的代价矩阵,然后相继进行级联匹配和iou匹配,最后得到当前帧的匹配对,未匹配的tracks以及未匹配的detections:

先来看下余弦距离(或称为余弦相似度):

余弦相似度用向量空间中两个向量夹角的余弦值作为衡量两个个体间差异的大小,相比距离度量,余弦相似度更加注重两个向量在方向上的差异,而非距离或长度上.余弦相似度计算公式如下:

cos\theta =\frac{\vec{x}.\vec{y}}{||x||.||y||}

余弦距离就是用1减去余弦相似度获得的:

d(x,y)=1-cos\theta = 1-\frac{\vec{x}.\vec{y}}{||x||.||y||}

代码实现

nn_matching.py
def _cosine_distance(a, b, data_is_normalized=False):#计算两个向量的余弦距离,刻画两个向量在各个维度上的相似性
    """Compute pair-wise cosine distance between points in `a` and `b`.

    Parameters
    ----------
    a : array_like
        An NxM matrix of N samples of dimensionality M.
    b : array_like
        An LxM matrix of L samples of dimensionality M.
    data_is_normalized : Optional[bool]
        If True, assumes rows in a and b are unit length vectors.
        Otherwise, a and b are explicitly normalized to lenght 1.

    Returns
    -------
    ndarray
        Returns a matrix of size len(a), len(b) such that eleement (i, j)
        contains the squared distance between `a[i]` and `b[j]`.

    """
    if not data_is_normalized:
        a = np.asarray(a) / np.linalg.norm(a, axis=1, keepdims=True)#linear(线性)+algebra(代数),norm则表示范数。求整个矩阵元素平方和再开根号
        b = np.asarray(b) / np.linalg.norm(b, axis=1, keepdims=True)
    return 1. - np.dot(a, b.T)#np.dot就是矩阵点乘,结果仍然是一个矩阵,余弦距离公式 ab/||a||||b||,余弦距离越小,说明两个向量相似性越强

那这个余弦距离是怎么算的呢?如何跟外观相似度联合在了一起呢?

step1:在main.py中首先初始化了两个类

main.py
 metric = nn_matching.NearestNeighborDistanceMetric("cosine", max_cosine_distance, nn_budget)#这里初始化一个类
tracker = Tracker(metric)#Tracker是一个类,这里初始化一个跟踪器

nn_matching.py
class NearestNeighborDistanceMetric(object):    

    def __init__(self, metric, matching_threshold, budget=None):

        if metric == "euclidean":
            self._metric = _nn_euclidean_distance
        elif metric == "cosine":
            self._metric = _nn_cosine_distance
        else:
            raise ValueError(
                "Invalid metric; must be either 'euclidean' or 'cosine'")
        self.matching_threshold = matching_threshold
        self.budget = budget
        self.samples = {}

tracker.py
class Tracker:
    def __init__(self, metric, max_iou_distance=0.7, max_age=30, n_init=3):
        self.metric = metric #入参metric为一个类nn_matching.NearestNeighborDistanceMetric("cosine", max_cosine_distance, nn_budget)
        self.max_iou_distance = max_iou_distance
        self.max_age = max_age
        self.n_init = n_init

        self.kf = kalman_filter.KalmanFilter()#卡尔曼滤波
        self.tracks = []

        self._next_id = 1

   也就是说,tracker中是以"余弦距离“度量来初始化的

step2:在main.py中对图片用yolo检测器检测目标的box,然后再选用一个检测器对每一个box计算其128维的特征向量feature

step3:调用tracker.py中 tracker.update(self,detections)-->_match(self,detections),重点在这个_match函数

     step 3.1 先把tracks中的所有目标分为确认confirmed的和未确认的(tentative, deleted):

确认的和未确认的区分标准:对于一个新track其初始状态为tentative,如果在后面的连续3帧中都有新的检测目标与其匹配的上,其状态就称为confirmed,若在后面的连续3帧中,有任何一帧没有被匹配上,就会变成deleted被删除

然后调用linear_assignment.py-->matching_cascade()对新检测出的目标detections和过去的目标tracks进行匹配

tracker.py
    def _match(self, detections):#实现了论文2.3matching cascade级联匹配的内容

        def gated_metric(tracks, dets, track_indices, detection_indices):#内部嵌套定义函数,由特征距离构建门矩阵
            '''
            基于外观信息和马氏距离,计算卡尔曼滤波预测的tracks和当前检测到的detections的成本矩阵
            :param tracks:
            :param dets:
            :param track_indices:
            :param detection_indices:
            :return:
            '''
            features = np.array([dets[i].feature for i in detection_indices])
            targets = np.array([tracks[i].track_id for i in track_indices])
            print('targets<<<,',targets)
            cost_matrix = self.metric.distance(features, targets)#用余弦距离计算d^2(i,j)=min{1-r_j^Tr_k^(i)||r_k^(i)\in R_i}
            cost_matrix = linear_assignment.gate_cost_matrix(
                self.kf, cost_matrix, tracks, dets, track_indices,
                detection_indices)#问题,这里不是把上面的距离覆盖掉了吗?第一个余弦距离等于白算了?

            return cost_matrix

        # Split track set into confirmed and unconfirmed tracks.#把跟踪集合区分为确认的和未确认的
        confirmed_tracks = [
            i for i, t in enumerate(self.tracks) if t.is_confirmed()]#将轨迹集合拆分为已确认的和未确认的,得到两个集合的索引[0, 1, 2]
        unconfirmed_tracks = [
            i for i, t in enumerate(self.tracks) if not t.is_confirmed()]#[0, 1, 2]
        # Associate confirmed tracks using appearance features.使用外观特征关联已确认的轨迹。
        #对confirmed tracks进行级联匹配,对于第一帧图片,暂时只有检测目标,比如有3个检测目标,此时返回值为
        #matches_a, unmatched_tracks_a, unmatched_detections [] [] [0, 1, 2]
       
        matches_a, unmatched_tracks_a, unmatched_detections = \
            linear_assignment.matching_cascade(
                gated_metric, self.metric.matching_threshold, self.max_age,
                self.tracks, detections, confirmed_tracks)#根据128维特征detections将检测框匹配到确认的轨迹,问题,这里传入的gated_metric是什么?

linear_assignment.py
def matching_cascade(
        distance_metric, max_distance, cascade_depth, tracks, detections,
        track_indices=None, detection_indices=None):#track_indices里面是数字,比如[1,2,3]代表一些被确认的track的id

可以看出调用matching_cascade时,传入的distance_metric本质是tracker.py中的gated_metric,而gated_metric计算成本矩阵时调用的是self.metric.distance=nn_matching.py-->distance(self,features,targets)

matches_a, unmatched_tracks_a, unmatched_detections = \
            linear_assignment.matching_cascade(
                gated_metric, self.metric.matching_threshold, self.max_age,
                self.tracks, detections, confirmed_tracks)#根据128维特征detections将检测框匹配到确认的轨迹,问题,这里传入的gated_metric是什么?

 

nn_matching.py
    def distance(self, features, targets):
        """Compute distance between features and targets.

        Parameters
        ----------
        features : ndarray
            An NxM matrix of N features of dimensionality M.
        targets : List[int]
            A list of targets to match the given `features` against.

        Returns
        -------
        ndarray
            Returns a cost matrix of shape len(targets), len(features), where
            element (i, j) contains the closest squared distance between
            `targets[i]` and `features[j]`.

        """
          cost_matrix = np.zeros((len(targets), len(features)))
        for i, target in enumerate(targets):
            cost_matrix[i, :] = self._metric(self.samples[target], features)#计算成本矩阵,具体用哪种算法,要根据self._metric来决定,
            #因为nn_matching在main.py中初始化self._metric=‘cosine,所以这里是用的_nn_cosine_distance即余弦距离       
        return cost_matrix

step4 基于128维度的特征向量,根据余弦距离,算出特征矩阵后,在linear_assignment.py-->min_cost_matching()函数中根据匈牙利算法,就可以得出过去的检测目标与新检测目标的匹配对

linear_assignment.py
cost_matrix = distance_metric(
        tracks, detections, track_indices, detection_indices)

    cost_matrix[cost_matrix > max_distance] = max_distance + 1e-5#设置超过阈值max_distance=0.7的成本为固定值,消除差异
    indices = linear_assignment(cost_matrix)#调用了sklearn包的函数执行匈牙利算法,得到匹配成功的索引对,行索引为tracks的索引,列索引为detections的索引,关联检测框
    #[[0 1]
    # [1 0]
    # [2 2]]

 

tracker.py
    def _match(self, detections):#实现了论文2.3matching cascade级联匹配的内容

        def gated_metric(tracks, dets, track_indices, detection_indices):#内部嵌套定义函数,由特征距离构建门矩阵
            '''
            基于外观信息和马氏距离,计算卡尔曼滤波预测的tracks和当前检测到的detections的成本矩阵
            :param tracks:
            :param dets:
            :param track_indices:
            :param detection_indices:
            :return:
            '''
            features = np.array([dets[i].feature for i in detection_indices])
            targets = np.array([tracks[i].track_id for i in track_indices])
            cost_matrix = self.metric.distance(features, targets)#用余弦距离计算d^2(i,j)=min{1-r_j^Tr_k^(i)||r_k^(i)\in R_i}
            cost_matrix = linear_assignment.gate_cost_matrix(
                self.kf, cost_matrix, tracks, dets, track_indices,
                detection_indices)#问题,这里不是把上面的距离覆盖掉了吗?第一个余弦距离等于白算了?

            return cost_matrix

基于匈牙利算法的级联匹配

linear_assignment.py
def matching_cascade(
        distance_metric, max_distance, cascade_depth, tracks, detections,
        track_indices=None, detection_indices=None):    
    if track_indices is None:
        track_indices = list(range(len(tracks)))
    if detection_indices is None:
        detection_indices = list(range(len(detections)))

    unmatched_detections = detection_indices
    matches = []
    for level in range(cascade_depth):
        if len(unmatched_detections) == 0:  # No detections left
            break

        track_indices_l = [
            k for k in track_indices
            if tracks[k].time_since_update == 1 + level
        ]
        if len(track_indices_l) == 0:  # Nothing to match at this level
            continue
        # 匈牙利算法
        matches_l, _, unmatched_detections = \ 
            min_cost_matching(
                distance_metric, max_distance, tracks, detections,
                track_indices_l, unmatched_detections)
        matches += matches_l
    unmatched_tracks = list(set(track_indices) - set(k for k, _ in matches))
    return matches, unmatched_tracks, unmatched_detections

 

特征features的更新

step1: 在main.py中,对每一帧的检测目标,都会计算其128维度特征。并且调用detection.py,对每个检测目标,都会用一个类实例化其属性

main.py
features = encoder(frame,boxs)#对每个图片,提取128维度的特征向量,这里的用法比较特殊,函数encoder的参数在上面已经传好了,
        #但encoder函数本身嵌套了一个子函数,这里给子函数传进去了参数frame,boxs,函数返回值为m*128,m具体多大,要看在当前视频帧上检测出了多少个目标
        # score to 1.0 here).
         #Detection本身是一个类,代表每一个检测目标的属性
        detections = [Detection(bbox, 1.0, feature) for bbox, feature in zip(boxs, features)]#zip之后,将每个box和其对应的特征feature组合在一起

detection.py
class Detection(object):#刻画单个检测目标的各种属性,比如box坐标,128维度特征
    def __init__(self, tlwh, confidence, feature): #bbox, 1.0, feature
        self.tlwh = np.asarray(tlwh, dtype=np.float)#原始视频帧中检测出的目标位置,(x, y, w, h)`.左上坐标,宽高
        self.confidence = float(confidence)#检测目标的置信度
        self.feature = np.asarray(feature, dtype=np.float32)#这个是检测出的目标的128维特征

step2: 将当前帧的检测目标detections与原来的track进行匹配

    case1:若track与detection匹配成功,则会将匹配的detection的特征feature添加到对应的track的feature

tracker.py
def update(self, detections):#执行测量更新和跟踪管理,入参detections本质是一个类,类中的各个属性和方法由一对对的box与对应的128特征向量刻画
 for track_idx, detection_idx in matches:#对于匹配好的track和detection
            print('track_idx, detection_idx',track_idx, detection_idx)
            self.tracks[track_idx].update(self.kf, detections[detection_idx])#只有当这个track被批够了3帧以上,其状态才会变成confirmed

tracke.py
    def update(self, kf, detection):
         ... 
        self.features.append(detection.feature)#用新检测出的匹配对detection的特征,添加匹配对track的feature

case 2,若匹配不成功,新的detection初始化为track,track的特征就是detection的特征

step3: 对所有的confirmed track,将其特征append到一起构成features,track的身份id再append到一起,构成targets,然后将每个target的feature清空

tracker.py
def update(self,detections)
 for track in self.tracks:            
     if not track.is_confirmed():#第一帧时,所有的目标状态都是tentative,
       continue         
       #只有当一个track的状态为confirmed时,才会继续向下执行
       features += track.features
       targets += [track.track_id for _ in track.features]
        track.features = []#清空confirmed track的特征值

step4 组合的features有什么用呢?这些特征与其身份id一一对应起来,存入了self.samples,而且,对于一个track,其特征会随着时间的累积,有很多个,这样,对一个track,我们可以构造一个m*128的特征矩阵,m的值理论上来讲,越大越好,因为这个矩阵表征了同一个物体的各种特征,比如坐的,立的,蹲的等,然后将这个m*128的特征矩阵与当前帧的所有检测目标的特征进行对比,看这个target与哪个检测目标detection最相似。

tracker.py
def update(self,detections)
 self.metric.partial_fit(np.asarray(features), np.asarray(targets), active_targets)#不知道这个有什么作用?构建了一个字典,将每个track与128特征对应

nn_matching.py
    def partial_fit(self, features, targets, active_targets):        
        for feature, target in zip(features, targets):#将每个track与其特征feature对应起来
            self.samples.setdefault(target, []).append(feature)#dict.setdefault(key, default=None)            
            # 如果字典中包含有给定键,则返回该键对应的值,否则返回为该键设置的值。这里若samples存在target,就把feature放进去        
            if self.budget is not None:#好像这个一直都是none,
                self.samples[target] = self.samples[target][-self.budget:]

        #self.samples记录了每个target与其对应的128维特征,key为每个track的身份id,value是每个id在每帧中的128特征,并且这个特征会随着时间的累计,越来越大,
        #问题,积累这么多特征有用吗?是否太占内存?当然会占内存,但积累的越多,同一个track的特征信息越丰富,越容易跟最新的检测目标匹配的好,不容易跟丢       
        self.samples = {k: self.samples[k] for k in active_targets}#第一帧中,self.samples={},active_targets=[

你可能感兴趣的:(多目标跟踪DeepSort)