精华内容
下载资源
问答
  • 视频演示算法包括: 1. 静态背景下的背景预测法目标检测 2. 静态背景下帧间差分法目标检测...4. 重心多目标跟踪方法 该框架支持的视频只限于RGB非压缩Windows AVI格式,可以通过“文件”菜单下打开视频来打开视频文件。
  • 总览 1、训练 (1)数据 (2)损失函数 (3)跟踪 ...首先__getitem__方法中读取图片和对应的标签,做简单的数据增强,统计所有数据集的ID最大值,也就是最多有多少个需要跟踪的对象。由于FairMOT的...

    总览 

    1、训练检测网络

    (1)数据

    (2)损失函数

    2、测试

    (1)检测后处理及跟踪


    1、训练

    (1)数据

    数据集类JointDataset在src\lib\datasets\dataset\jde.py文件中:

    首先__getitem__方法中读取图片和对应的标签,做简单的数据增强,统计所有数据集的ID最大值,也就是最多有多少个需要跟踪的对象。由于FairMOT的检测网络有三个分支:目标类别分类,目标位置和大小回归,还有区分目标的特征提取。所以训练数据也要做对应的label。FairMOT使用的是centernet类型的点回归方式来检测目标的中心点以及确定目标的宽高。主干的最后加了三个网络构造来输出预测值,默认是C个类(hm)、2个预测的宽高值(wh)、2个中心点的偏置(xy)。

    类别(hm):而这C个类别是通过C通道的heatmap来预测的,heatmap的值是在0~1之间的概率值。一个通道表示一类,heatmap的大小是原图下采样4倍之后的大小,也就是说,网络在经过4次下采样之后的特征图上做预测。如果图像中有某个类别的目标,那么这个目标的中心点在heatmap上的概率值为1,其余周围概率成高斯分布逐渐衰减,如图所示:

    对应的heatmap:

    两个红色的点,表示两个目标的中心点的概率值最大,在其周围有渐变的一个小圆圈,这个圆圈成高斯分布,其半径和目标的大小有关系,越靠近圆心的位置,是目标中心点的概率就越大。

    目标宽高(wh),目标中心偏移(xy):通过回归得到。

    目标ID(id): 交叉熵分类得到。

    这部分label设置代码如下:

    (2)损失函数

    通过(1)中的分析,损失包括:heatmap分类loss,宽高回归loss,目标中心点偏移回归loss,目标ID分类loss。

    MOTloss类定义了这几个loss,在src\lib\trains\mot.py文件中

    class MotLoss(torch.nn.Module):
        def __init__(self, opt):
            super(MotLoss, self).__init__()
            self.crit = torch.nn.MSELoss() if opt.mse_loss else FocalLoss()         #分类loss
            self.crit_reg = RegL1Loss() if opt.reg_loss == 'l1' else \
                RegLoss() if opt.reg_loss == 'sl1' else None                        #中心点x,y的offset
            self.crit_wh = torch.nn.L1Loss(reduction='sum') if opt.dense_wh else \
                NormRegL1Loss() if opt.norm_wh else \
                    RegWeightedL1Loss() if opt.cat_spec_wh else self.crit_reg       #宽高回归loss
            self.opt = opt
            self.emb_dim = opt.reid_dim #reid特征长度
            self.nID = opt.nID          #所有目标的ID数
            self.classifier = nn.Linear(self.emb_dim, self.nID)
            self.IDLoss = nn.CrossEntropyLoss(ignore_index=-1)  #ID loss
            #self.TriLoss = TripletLoss()
            self.emb_scale = math.sqrt(2) * math.log(self.nID - 1)   
            self.s_det = nn.Parameter(-1.85 * torch.ones(1))
            self.s_id = nn.Parameter(-1.05 * torch.ones(1))
    
        def forward(self, outputs, batch):
            opt = self.opt
            hm_loss, wh_loss, off_loss, id_loss = 0, 0, 0, 0
            for s in range(opt.num_stacks):
                output = outputs[s]
                if not opt.mse_loss:
                    output['hm'] = _sigmoid(output['hm'])
    
                hm_loss += self.crit(output['hm'], batch['hm']) / opt.num_stacks    #修改过后的focal loss
                #wh_loss
                if opt.wh_weight > 0:
                    if opt.dense_wh:
                        mask_weight = batch['dense_wh_mask'].sum() + 1e-4
                        wh_loss += (
                                       self.crit_wh(output['wh'] * batch['dense_wh_mask'],
                                                    batch['dense_wh'] * batch['dense_wh_mask']) /
                                       mask_weight) / opt.num_stacks
                    else:
                        wh_loss += self.crit_reg(
                            output['wh'], batch['reg_mask'],
                            batch['ind'], batch['wh']) / opt.num_stacks
                #xyoffset_loss
                if opt.reg_offset and opt.off_weight > 0:
                    off_loss += self.crit_reg(output['reg'], batch['reg_mask'],
                                              batch['ind'], batch['reg']) / opt.num_stacks
                #id loss
                if opt.id_weight > 0:
                    id_head = _tranpose_and_gather_feat(output['id'], batch['ind'])
                    id_head = id_head[batch['reg_mask'] > 0].contiguous()
                    id_head = self.emb_scale * F.normalize(id_head)
                    id_target = batch['ids'][batch['reg_mask'] > 0]
                    id_output = self.classifier(id_head).contiguous()
                    id_loss += self.IDLoss(id_output, id_target)
                    #id_loss += self.IDLoss(id_output, id_target) + self.TriLoss(id_head, id_target)
    
            #loss = opt.hm_weight * hm_loss + opt.wh_weight * wh_loss + opt.off_weight * off_loss + opt.id_weight * id_loss
    
            det_loss = opt.hm_weight * hm_loss + opt.wh_weight * wh_loss + opt.off_weight * off_loss  
    
            loss = torch.exp(-self.s_det) * det_loss + torch.exp(-self.s_id) * id_loss + (self.s_det + self.s_id)
            loss *= 0.5
    
            #print(loss, hm_loss, wh_loss, off_loss, id_loss)
    
            loss_stats = {'loss': loss, 'hm_loss': hm_loss,
                          'wh_loss': wh_loss, 'off_loss': off_loss, 'id_loss': id_loss}
            return loss, loss_stats

    有了数据和loss,就可以训练了。

    2、测试

    (1)检测后处理及跟踪

      检测每一帧输入的图像,src\demo.py

    src\track.py

     src\lib\tracker\multitracker.py————> tracker.update ()

    接下来就是类似于Deepsort的跟踪流程了,初始化跟踪器,根据特征距离和马氏距离计算匹配矩阵,匈牙利匹配,处理匹配和未匹配的检测框和跟踪器等。

    if len(dets) > 0:
                '''Detections'''
                detections = [STrack(STrack.tlbr_to_tlwh(tlbrs[:4]), tlbrs[4], f, 30) for
                              (tlbrs, f) in zip(dets[:, :5], id_feature)]     #将特征保存在每一个跟踪目标中
            else:
                detections = []
    
            ''' Add newly detected tracklets to tracked_stracks'''
            unconfirmed = []
            tracked_stracks = []  # type: list[STrack]
            for track in self.tracked_stracks:
                if not track.is_activated:
                    unconfirmed.append(track)
                else:
                    tracked_stracks.append(track)
    
            ''' Step 2: First association, with embedding'''
            strack_pool = joint_stracks(tracked_stracks, self.lost_stracks)
            # Predict the current location with KF
            #for strack in strack_pool:
                #strack.predict()
            STrack.multi_predict(strack_pool)  #卡尔曼滤波预测新位置
            dists = matching.embedding_distance(strack_pool, detections)  #计算跟踪框和检测框的特征余弦距离矩阵
            #dists = matching.gate_cost_matrix(self.kalman_filter, dists, strack_pool, detections)
            dists = matching.fuse_motion(self.kalman_filter, dists, strack_pool, detections)  #计算马氏距离矩阵
            matches, u_track, u_detection = matching.linear_assignment(dists, thresh=0.7)     #匈牙利匹配  
    
            for itracked, idet in matches:
                track = strack_pool[itracked]
                det = detections[idet]
                if track.state == TrackState.Tracked:
                    track.update(detections[idet], self.frame_id)
                    activated_starcks.append(track)
                else:
                    track.re_activate(det, self.frame_id, new_id=False)
                    refind_stracks.append(track)
    
            ''' Step 3: Second association, with IOU'''
            detections = [detections[i] for i in u_detection]
            r_tracked_stracks = [strack_pool[i] for i in u_track if strack_pool[i].state == TrackState.Tracked]
            dists = matching.iou_distance(r_tracked_stracks, detections)
            matches, u_track, u_detection = matching.linear_assignment(dists, thresh=0.5)
    
            for itracked, idet in matches:
                track = r_tracked_stracks[itracked]
                det = detections[idet]
                if track.state == TrackState.Tracked:
                    track.update(det, self.frame_id)
                    activated_starcks.append(track)
                else:
                    track.re_activate(det, self.frame_id, new_id=False)
                    refind_stracks.append(track)
    
            for it in u_track:
                track = r_tracked_stracks[it]
                if not track.state == TrackState.Lost:
                    track.mark_lost()
                    lost_stracks.append(track)
    
            '''Deal with unconfirmed tracks, usually tracks with only one beginning frame'''
            detections = [detections[i] for i in u_detection]
            dists = matching.iou_distance(unconfirmed, detections)
            matches, u_unconfirmed, u_detection = matching.linear_assignment(dists, thresh=0.7)
            for itracked, idet in matches:
                unconfirmed[itracked].update(detections[idet], self.frame_id)
                activated_starcks.append(unconfirmed[itracked])
            for it in u_unconfirmed:
                track = unconfirmed[it]
                track.mark_removed()
                removed_stracks.append(track)
    
            """ Step 4: Init new stracks"""
            for inew in u_detection:
                track = detections[inew]
                if track.score < self.det_thresh:
                    continue
                track.activate(self.kalman_filter, self.frame_id)
                activated_starcks.append(track)
            """ Step 5: Update state"""
            for track in self.lost_stracks:
                if self.frame_id - track.end_frame > self.max_time_lost:
                    track.mark_removed()
                    removed_stracks.append(track)
    

    总结

    FairMOT的特色主要在于检测算法和reid特征的融合在一起,检测目标的同时,输出reid特征,而且用了anchor-free的centerNet检测算法,相比于anchor类的方法有很多优势。而真正跟踪流程还是DeepSort那一套。话说anchor-free类算法还是很有前景的。

    展开全文
  • 多目标跟踪算法解读

    千次阅读 2020-09-28 14:29:43
    一、多目标跟踪背景介绍 1.问题定义 2.难点 3.应用场景 二、相关方法 1.Model free 方法 2. Tracking by detection 方法 1)离线方法 2)在线方法 三、基准 多目标跟踪背景介绍 问题定义 多目标跟踪是将视频...
    目录
    一、多目标跟踪背景介绍
            1.问题定义
            2.难点
            3.应用场景
    二、相关方法
            1.Model free 方法
            2. Tracking by detection 方法
                 1)离线方法
                  2)在线方法
    三、基准
    

    多目标跟踪背景介绍

    问题定义

    多目标跟踪是将视频中每一帧的物体都赋予一个id,并且得到每个id的行为轨迹,如图1所示。
    从代码的层次考虑,其输入为视频序列,输出为物体的行为轨迹和id。从是否有模型上考虑,可以分为model free的方法和tracking by detection的方法,其中后者又可以分为离线学习的方法和在线学习的方法。
    有多目标跟踪算法就有单目标跟踪方法,不同之处就在于匹配问题,举例说明,如果在前一帧检测到两个物体a,b,在后一帧检测到两个物体c,d,怎么知道前一帧的a在后一帧所对应的是c或者d的哪一个物体呢?匹配问题在单目标跟踪领域就不存在,因为前后两帧只存在一个目标。
    在这里插入图片描述

    难点

    多目标跟踪的难点在于ID switch, 如图2所示,当骑自行车的人与步行的人相遇的时候,步行的人发生了遮挡,这是对于电脑来说,认为是此id的跟踪已经结束,过了一段时间,步行的人重新出现在了视野,但是电脑会认为是一个新的物体出现,因此赋予一个新的id,这种情况下,就发生了id交换,同理当物体被其他物体例如电线杆遮挡时也是一样。因此针对这种id交换的情况,学术界提出了很多的解决方案,这个我们后面再看。
    在这里插入图片描述

    应用场景

    多目标跟踪是一种通用的算法,几乎应用在视觉领域的各个方面,例如安防领域、自动驾驶领域以及医疗领域。如图3,4,5所示,在安防领域,我们通常需要跟踪来得到在特定区域的人数统计。在自动驾驶领域,我们通常需要跟踪来预估行人或者车辆的轨迹。在医疗领域,我们通常需要跟踪来得到细胞的运动情况。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    相关方法

    多目标的跟踪方法在这里我们分为两类,model-free的方法和tracking by detection的方法。

    model-free的方法

    model-free的方法核心思想是在第一帧给定初始的检测框,在后续帧中跟踪这些框,这种方法是早期的算法,缺点是如果有些目标在后续帧中才出现,这些目标并不会被跟踪。如图6所示。
    在这里插入图片描述

    tracking by detection的方法

    离线学习

    离线学习的方法缺点在于无法实时处理视频,通过比较相邻帧之间的相似度生成短的轨迹,之后再将短的轨迹合成长的轨迹。如图7所示。
    在这里插入图片描述

    在线学习

    Learning to Track: Online Multi-Object Tracking by Decision Making (2015,ICCV)
    该论文第一次将强化学习加入到跟踪这个领域,将目标的跟踪周期建模成马尔可夫过程进行优化。如图8所示。当物体被检测到之后,被认为是active的状态。在active的状态下如果下一帧也检测到了,状态由active转换为tracked,如果没有检测到,状态转换为inactive状态。在tracked状态下如果如果继续检测到该目标,则持续为tracked状态,如果没检测到,则状态转换为lost。在lost状态下如果被检测到了,则状态恢复为tracked,如果持续没检测到,则还保持lost状态,当lost的时间超过了一定的阈值后,状态切换为inactive,inactive状态下的目标会保持inactive直到视频结束。
    在这里插入图片描述
    Online Multi-Object Tracking via Structural Constraint Event Aggregation (2016,CVPR)
    该论文将目标跟踪中的匹配问题加入了结构约束,举个例子,前一帧如果检测到的目标连结起来呈现的是三角形,那么通过速度预测每个目标的位置,从而调整三角形的形态,并且认为后面的帧就是呈现这样的结构。如图9所示
    在这里插入图片描述
    Tracking The Untrackable: Learning to Track Multiple Cues with Long-Term Dependencies (2017, ICCV)
    该论文首次将深度学习加入到跟踪中,通过神经网络提取三种特征,外表特征、运动特征、交互特征,将三种特征连接到一起,再经过全连接层,得到相似度分数。相邻帧得到相似分数后,利用匈牙利匹配得到最大匹配。匈牙利匹配的概念见《带你入门多目标跟踪(三)匈牙利算法&KM算法》
    在这里插入图片描述
    Simple Online and Real-time Tracking with a D eep Association Metric
    这个系列有两个算法,sort和deep sort,这里先介绍sort。sort的思路很简单,首先通过检测器例如faster rcnn把每一帧的物体检测出来,之后通过卡尔曼滤波器预测物体在下一帧的位置,将预测的位置与下一帧实际检测到的位置做IOU的计算,得到相邻两帧物体的相似度,最后利用匈牙利匹配得到相邻帧的对应id。由于这里只是计算框的重叠面积,因此如果两个物体发生遮挡,会出现id交换的情况,所以作者为了降低id交换,提出了deep sort算法。deep sort之所以可以把id交换降低45%,是因为它将面积匹配修改为了特征匹配。在匹配之前作者利用resnet50先将特征里取出来,由于一般情况下很少会遇到外观特征完全相同的两个物体,因此这种改进是有效的。deep dort由于很简单,因此没有在顶会上发表成功,但是由于其可以保证在高速度的情况下的高准确度,所以广泛应用在了工程实践的过程中。相关的demo
    在这里插入图片描述
    Chained-Tracker: Chaining Paired Attentive Regression Results for End-to-End Joint Multiple-Object Detection and Tracking
    论文有两个贡献1)提出了一种链式跟踪算法,业内首创将相邻两帧输入网络,可以将目标检测、特征提取、数据关联三个模块集成到单个网络中,实现端到端联合检测跟踪。2)相邻帧检测框对回归。论文将数据关联问题转换为相邻帧中检测框对的回归问题。这两个贡献主要是针对现存的两个问题:1)算法不能端到端的优化。2)过度依赖于检测的质量

    在这里插入图片描述

    基准

    目前在MOT7数据集上所能达到的效果如下图所示。MOTbenchmark
    在这里插入图片描述
    参考:高旭 《多目标跟踪》

    展开全文
  • DeepSORT 多目标跟踪算法笔记

    千次阅读 多人点赞 2019-05-11 16:54:58
    SORT 是一种实用的多目标跟踪算法,然而由于现实中目标运动多变且遮挡频繁,该算法的身份转换(Identity Switches)次数较高。DeepSORT 整合外观信息使得身份转换的数量减少了45%。所提方案为: 使用马氏距离和...

    SORT 是一种实用的多目标跟踪算法,然而由于现实中目标运动多变且遮挡频繁,该算法的身份转换(Identity Switches)次数较高。DeepSORT 整合外观信息使得身份转换的数量减少了45%。所提方案为:

    • 使用马氏距离和深度特征余弦距离两种度量;
    • 采取级联匹配,优先匹配距上次出现间隔短的目标;
    • 第一级关联以余弦距离作为成本函数,但设定马氏距离和余弦距离两个阈值约束;
    • 第二级关联与 SORT 中相同,尝试关联未确认和年龄为 n=1n=1 的不匹配轨迹;
    • 同样采用试用期甄别目标,但大幅提高轨迹寿命 Amax=30A_{\mathrm{max}}=30

    带有深度关联度量的 SORT 算法

    DeepSORT 属于传统的单假设跟踪方法,采用递归卡尔曼滤波和逐帧数据关联。

    轨迹处理和状态估计

    多目标跟踪问题的一个普遍场景为:摄像机未校准且没有自身运动信息可用。这也是多目标跟踪基准测试中最常见的设置(MOT16)。因此,DeepSORT

    • 将跟踪场景定义在八维状态空间——(u,v,γ,h,x˙,y˙,γ˙,h˙)(u, v, \gamma, h, \dot{x}, \dot{y}, \dot{\gamma}, \dot{h}),包含边界框中心位置 (u,v)(u, v)、纵横比 γ\gamma、高度 hh 以及它们各自在图像坐标中的速度。
    • 使用具有等速运动和线性观测模型的标准卡尔曼滤波器,并把边界坐标 (u,v,γ,h)(u, v, \gamma, h) 作为物体状态的直接观测。
    • 轨迹处理和卡尔曼滤波框架与 SORT 中的原始公式基本相同。

    每个轨迹 KK 内部记录自上次成功关联到测量 aka_k 以来的帧数。该计数器在卡尔曼滤波器预测期间递增,并且当轨迹与测量相关联时重置为0。

    • 将超过预定义最大寿命 AmaxA_{\rm max} 的轨迹视为已离开场景并从轨迹集中删除。
    • 由无法与现有轨迹相关联的检测创建新的轨迹假设。这些新轨迹在前三帧中被列为暂定轨迹。在此期间,每次检测后如不能关联到则删除该轨迹。

    关联问题

    关联预测卡尔曼状态与新到达测量值的常规方法是将其看作分配问题,利用匈牙利算法求解。DeepSORT 通过结合目标框马氏距离和特征余弦距离两个度量来整合运动和外观信息。一方面,马式距离基于运动提供了有关物体可能位置的信息,这对短期预测特别有用。另一方面,余弦距离考虑外观信息,这对于在长期遮挡之后找回身份特别有用,此时运动不那么具有辨别力。

    预测卡尔曼状态和新到测量值之间的(平方)马氏距离(Mahalanobis 距离):

    d(1)(i,j)=(djyi)Si1(djyi), \begin{aligned} d^{(1)}(i,j) = (d_j - y_i)^\top S^{-1}_i (d_j - y_i), \end{aligned}
    其中,(yi,Si)(y_i, S_i) 表示第 ii 个轨迹分布到测量空间的投影,djd_j 表示第 jj 个检测边界框。

    马氏距离通过测算检测与平均轨迹位置的距离超过多少标准差来考虑状态估计的不确定性。此外,可以通过从逆 χ2\chi^2 分布计算 95%95\% 置信区间的阈值,排除可能性小的关联。四维测量空间对应的马氏阈值为 t(1)=9.4877t^{(1)} = 9.4877。如果第 ii 条轨迹和第 jj 个检测之间的关联是可采纳的,则:
    bi,j(1)=1[d(1)(i,j)t(1)] \begin{aligned} b_{i,j}^{(1)} = \mathbb{1}[d^{(1)}(i, j) \leq t^{(1)}] \end{aligned}

    当运动不确定性较低时,马式距离是一个合适的关联度量。但在跟踪的图像空间问题公式中,卡尔曼滤波框架仅提供目标位置的粗略估计。尤其是,未考虑的摄像机运动会在图像平面中引入快速位移,使得在遮挡情况下跟踪时马式距离度量相当不精确。因此,DeepSORT

    • 对于每个检测边界框 djd_j 计算一个外观描述符 rjr_j,满足 rj=1\lVert r_j\rVert = 1
    • 每个轨迹保留最后 Lk=100L_k=100 个关联外观描述符的原型库(gallery) Rk={rk(i)}k=1Lk\mathcal{R}_k = \{\vec{r}_k^{(i)}\}_{k=1}^{L_k}
    • 外观空间中第 ii 个轨迹和第 jj 个检测之间的最小余弦距离为:
      d(2)(i,j)=min{1rjrk(i)rk(i)Ri} \begin{aligned} d^{(2)}(i, j) = \min\{1 - r^\top_j r^{(i)}_k \| r^{(i)}_k\in \mathcal{R}_i\} \end{aligned}
    • 引入一个二进制变量来指示根据此度量标准是否允许关联:
      bi,j(2)=1[d(2)(i,j)t(2)] \begin{aligned} b_{i,j}^{(2)} = \mathbb{1}[d^{(2)}(i, j) \leq t^{(2)}] \end{aligned}

    算法在一个独立训练数据集上找到该指标的合适阈值。在实践中,DeepSORT 应用一个预训练的 CNN 来计算边界框外观描述符。

    关联问题的成本函数为以上两个指标的加权和:
    ci,j=λ&ThinSpace;d(1)(i,j)+(1λ)d(2)(i,j) \begin{aligned} c_{i,j} = \lambda \, d^{(1)}(i, j) + (1 - \lambda) d^{(2)}(i, j) \end{aligned}
    超参数 λ\lambda 控制每个度量对组合关联成本的影响。在实验中,作者发现当有大量的相机运动时,设置 λ=0\lambda=0 是一个合理的选择。此时,关联成本中仅使用外观信息。然而,关联结果仍受两方面的约束。仅当关联在两个度量的选通区域内时,称其为可接受关联:

    bi,j=m=12bi,j(m). \begin{aligned} b_{i,j} = \prod_{m=1}^{2} b_{i, j}^{(m)}. \end{aligned}

    级联匹配

    当目标被遮挡一段较长的时间后,随后的卡尔曼滤波预测会增加与目标位置相关的不确定性。因此,概率质量在状态空间中扩散,观测概率变得不那么尖峰。直观地说,关联度量应该通过增加测量跟踪距离来解释概率质量的这种扩散。与直觉相反,当两条轨迹竞争同一检测时,马式距离倾向于更大的不确定性,因为它有效地减少了检测的标准偏差到投影轨迹平均值的距离。这不是我们所期望的,因为它可能导致轨迹碎片增加和轨迹不稳定。因此,DeepSORT
    引入级联匹配,优先考虑更常见的目标,以编码关联似然中概率扩散的概念。

    在这里插入图片描述

    在最后的匹配阶段,使用 SORT 算法中提出的 IoU 度量方法尝试关联未确认和年龄为 n=1n=1 的不匹配轨迹。 这有助于解决外观的突然变化,如静态场景几何体的部分遮挡,并且增加了针对错误初始化的鲁棒性。

    Deep Appearance Descriptor

    以上方法的成功应用需要提前离线训练区分度高的特征嵌入。为此,DeepSORT 采用了一个在大规模行人重新识别数据集(MARS)上训练的 CNN,其中包含1261个行人的超过110万张图像,这使得它非常适合行人跟踪中的深度度量学习。

    如下表所示,模型结构为宽残差网络(WRNS),其中有两个卷积层,后面是六个残差块。维度 128128 的全局特征映射在 “Dense 10”层中计算。最终 BN 和 2\ell_2 规范化投影特征到单元超球面上从而与余弦外观度量兼容。网络参数量为2.67M,在 Nvidia GeForce GTX 1050移动 GPU 上,32个边界框的一次前向花费大约 30&ThinSpace;ms30\,\textrm{ms}。因此,只要有现代 GPU,该网络就非常适合在线跟踪。作者在 GitHub 仓库中提供了预先训练的模型以及可用于生成特征的脚本。

    cnn

    程序分为两部分:运行框架(application_util)和算法(deep_sort)。程序运行时由 Visualization 或者 NoVisualization 进行管理。算法主要实体为 TrackerKalmanFilterTrackNearestNeighborDistanceMetricDetectionKalmanFilter 中自己定义了马氏距离的计算,NearestNeighborDistanceMetric 能够计算特征相似度。linear_assignment.py 中定义了阈值选通和匹配函数。

    deep_sort_app.py

    main
    parse_args
    run
        args = parse_args()
        run(
            args.sequence_dir, args.detection_file, args.output_file,
            args.min_confidence, args.nms_max_overlap, args.min_detection_height,
            args.max_cosine_distance, args.nn_budget, args.display)
    

    parse_args()

    解析命令行参数。

        parser = argparse.ArgumentParser(description="Deep SORT")
        parser.add_argument(
            "--sequence_dir", help="Path to MOTChallenge sequence directory",
            default=None, required=True)
        parser.add_argument(
            "--detection_file", help="Path to custom detections.", default=None,
            required=True)
        parser.add_argument(
            "--output_file", help="Path to the tracking output file. This file will"
            " contain the tracking results on completion.",
            default="/tmp/hypotheses.txt")
        parser.add_argument(
            "--min_confidence", help="Detection confidence threshold. Disregard "
            "all detections that have a confidence lower than this value.",
            default=0.8, type=float)
        parser.add_argument(
            "--min_detection_height", help="Threshold on the detection bounding "
            "box height. Detections with height smaller than this value are "
            "disregarded", default=0, type=int)
        parser.add_argument(
            "--nms_max_overlap",  help="Non-maxima suppression threshold: Maximum "
            "detection overlap.", default=1.0, type=float)
        parser.add_argument(
            "--max_cosine_distance", help="Gating threshold for cosine distance "
            "metric (object appearance).", type=float, default=0.2)
        parser.add_argument(
            "--nn_budget", help="Maximum size of the appearance descriptors "
            "gallery. If None, no budget is enforced.", type=int, default=None)
        parser.add_argument(
            "--display", help="Show intermediate tracking results",
            default=True, type=bool_string)
        return parser.parse_args()
    

    run

    gather_sequence_info 收集序列信息,例如图像文件名、检测、标注(如果有的话)。
    NearestNeighborDistanceMetric 最近邻距离度量,对于每个目标,返回到目前为止已观察到的任何样本的最近距离(欧式或余弦)。
    由距离度量方法构造一个 Tracker

        seq_info = gather_sequence_info(sequence_dir, detection_file)
        metric = nn_matching.NearestNeighborDistanceMetric(
            "cosine", max_cosine_distance, nn_budget)
        tracker = Tracker(metric)
        results = []
    

    嵌套定义回调函数,过滤检测结果,预测目标并进行更新。
    create_detections 从原始检测矩阵创建给定帧索引的检测。
    non_max_suppression 抑制重叠的检测。

        def frame_callback(vis, frame_idx):
            print("Processing frame %05d" % frame_idx)
    
            # Load image and generate detections.
            detections = create_detections(
                seq_info["detections"], frame_idx, min_detection_height)
            detections = [d for d in detections if d.confidence >= min_confidence]
    
            # Run non-maxima suppression.
            boxes = np.array([d.tlwh for d in detections])
            scores = np.array([d.confidence for d in detections])
            indices = preprocessing.non_max_suppression(
                boxes, nms_max_overlap, scores)
            detections = [detections[i] for i in indices]
    

    Tracker.predict 将跟踪状态分布向前传播一步。
    Tracker.update 执行测量更新和跟踪管理。

            # Update tracker.
            tracker.predict()
            tracker.update(detections)
    

    visVisualization 或者 NoVisualization
    Visualization.set_image 设置 ImageViewer
    Visualization.draw_detections 绘制检测框。
    Visualization.draw_trackers 绘制跟踪框。

            # Update visualization.
            if display:
                image = cv2.imread(
                    seq_info["image_filenames"][frame_idx], cv2.IMREAD_COLOR)
                vis.set_image(image.copy())
                vis.draw_detections(detections)
                vis.draw_trackers(tracker.tracks)
    

    Track.is_confirmed 检查该轨迹是否确认过。
    Track.to_tlwh[x, y, width, height]边界框格式获取当前位置。

            # Store results.
            for track in tracker.tracks:
                if not track.is_confirmed() or track.time_since_update > 1:
                    continue
                bbox = track.to_tlwh()
                results.append([
                    frame_idx, track.track_id, bbox[0], bbox[1], bbox[2], bbox[3]])
    

    根据序列信息创建一个 Visualization 或者 NoVisualization 对象。由其运行跟踪器。
    update_msImageViewer 刷新显示的最小间隔(包含了跟踪处理时间)。

        # Run tracker.
        if display:
            visualizer = visualization.Visualization(seq_info, update_ms=5)
        else:
            visualizer = visualization.NoVisualization(seq_info)
        visualizer.run(frame_callback)
    
        # Store results.
        f = open(output_file, 'w')
        for row in results:
            print('%d,%d,%.2f,%.2f,%.2f,%.2f,1,-1,-1,-1' % (
                row[0], row[1], row[2], row[3], row[4], row[5]),file=f)
    

    Visualization

    显示 OpenCV 图像查看器中的跟踪输出。
    seq_info主要包含图片大小和帧起止索引。

        def __init__(self, seq_info, update_ms):
            image_shape = seq_info["image_size"][::-1]
            aspect_ratio = float(image_shape[1]) / image_shape[0]
            image_shape = 1024, int(aspect_ratio * 1024)
            self.viewer = ImageViewer(
                update_ms, image_shape, "Figure %s" % seq_info["sequence_name"])
            self.viewer.thickness = 2
            self.frame_idx = seq_info["min_frame_idx"]
            self.last_idx = seq_info["max_frame_idx"]
    

    run

    run
    ImageViewer.run
    _update_fun
            self.viewer.run(lambda: self._update_fun(frame_callback))
    

    _update_fun

    _update_fun
    frame_callback

    _update_funframe_callback 进行封装。根据帧索引判断是否终止,调用 frame_callback 进行处理。

            if self.frame_idx > self.last_idx:
                return False  # Terminate
            frame_callback(self, self.frame_idx)
            self.frame_idx += 1
            return True
    

    set_image

            self.viewer.image = image
    

    draw_groundtruth

    create_unique_color_uchar 为给定的轨迹 ID(标签)创建唯一的 RGB 颜色代码。

    draw_groundtruth
    create_unique_color_uchar
    rectangle
            self.viewer.thickness = 2
            for track_id, box in zip(track_ids, boxes):
                self.viewer.color = create_unique_color_uchar(track_id)
                self.viewer.rectangle(*box.astype(np.int), label=str(track_id))
    

    draw_detections

    绘制红色检测框。

            self.viewer.thickness = 2
            self.viewer.color = 0, 0, 255
            for i, detection in enumerate(detections):
                self.viewer.rectangle(*detection.tlwh)
    

    draw_trackers

    绘制目标轨迹,跳过未确认或者本次未检到的目标。

            self.viewer.thickness = 2
            for track in tracks:
                if not track.is_confirmed() or track.time_since_update > 0:
                    continue
                self.viewer.color = create_unique_color_uchar(track.track_id)
                self.viewer.rectangle(
                    *track.to_tlwh().astype(np.int), label=str(track.track_id))
                # self.viewer.gaussian(track.mean[:2], track.covariance[:2, :2],
                #                      label="%d" % track.track_id)
    
    

    Tracker

    参数:

    • metricNearestNeighborDistanceMetric 测量与轨迹关联的距离度量。
    • max_age:int,删除轨迹前的最大未命中数 AmaxA_{\mathrm{max}}
    • n_init:int,确认轨迹前的连续检测次数。如果前n_init帧内发生未命中,则将轨迹状态设置为Deleted
    
        def __init__(self, metric, max_iou_distance=0.7, max_age=30, n_init=3):
            self.metric = metric
            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
    

    predict

    Tracker.predict
    Track.predict
    KalmanFilter.predict

    对于每个轨迹,由一个 KalmanFilter 预测状态分布。每个轨迹记录自己的均值和方差作为滤波器输入。

            for track in self.tracks:
                track.predict(self.kf)
    

    update

    调用 _match 进行级联匹配。

            """Perform measurement update and track management.
            Parameters
            ----------
            detections : List[deep_sort.detection.Detection]
                A list of detections at the current time step.
            """
            # Run matching cascade.
            matches, unmatched_tracks, unmatched_detections = \
                self._match(detections)
    

    根据匹配结果更新轨迹集合。

            # Update track set.
            for track_idx, detection_idx in matches:
                self.tracks[track_idx].update(
                    self.kf, detections[detection_idx])
            for track_idx in unmatched_tracks:
                self.tracks[track_idx].mark_missed()
            for detection_idx in unmatched_detections:
                self._initiate_track(detections[detection_idx])
            self.tracks = [t for t in self.tracks if not t.is_deleted()]
    

    传入特征列表及其对应 id,NearestNeighborDistanceMetric.partial_fit 构造一个活跃目标的特征字典。

            # Update distance metric.
            active_targets = [t.track_id for t in self.tracks if t.is_confirmed()]
            features, targets = [], []
            for track in self.tracks:
                if not track.is_confirmed():
                    continue
                features += track.features
                targets += [track.track_id for _ in track.features]
                track.features = []
            self.metric.partial_fit(
                np.asarray(features), np.asarray(targets), active_targets)
    
    

    _match

    _match 实现了论文2.3. Matching Cascade 的内容。

    内部嵌套定义 gated_metric 函数,由特征距离构建门矩阵。

    gated_metric
    NearestNeighborDistanceMetric.distance
    linear_assignment.gate_cost_matrix

    d(2)(i,j)=min{1rjrk(i)rk(i)Ri}bi,j(2)=1[d(2)(i,j)t(2)] \begin{aligned} d^{(2)}(i, j) &amp;= \min\{1 - r^\top_j r^{(i)}_k \| r^{(i)}_k\in \mathcal{R}_i\}\\ b_{i,j}^{(2)} &amp;= \mathbb{1}[d^{(2)}(i, j) \leq t^{(2)}] \end{aligned}
    NearestNeighborDistanceMetric.distance 计算 d(2)(i,j)d^{(2)}(i, j)

            def gated_metric(tracks, dets, track_indices, detection_indices):
                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)
                cost_matrix = linear_assignment.gate_cost_matrix(
                    self.kf, cost_matrix, tracks, dets, track_indices,
                    detection_indices)
    
                return cost_matrix
    

    将轨迹集合拆分为已确认和未确认的,得到两个集合的索引。
    Track.is_confirmed 查询轨迹的状态。

            # Split track set into confirmed and unconfirmed tracks.
            confirmed_tracks = [
                i for i, t in enumerate(self.tracks) if t.is_confirmed()]
            unconfirmed_tracks = [
                i for i, t in enumerate(self.tracks) if not t.is_confirmed()]
    

    matching_cascade 根据特征将检测框匹配到确认的轨迹。
    传入门矩阵 B=[bi,j]\mathit{B} = [b_{i,j}] 而不是成本矩阵 C=[ci,j]\mathit{C} = [c_{i,j}]

            # Associate confirmed tracks using appearance features.
            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)
    

    min_cost_matching 使用匈牙利算法解决线性分配问题。
    传入 iou_cost,尝试关联剩余的轨迹与未确认的轨迹。

            # Associate remaining tracks together with unconfirmed tracks using IOU.
            iou_track_candidates = unconfirmed_tracks + [
                k for k in unmatched_tracks_a if
                self.tracks[k].time_since_update == 1]
            unmatched_tracks_a = [
                k for k in unmatched_tracks_a if
                self.tracks[k].time_since_update != 1]
            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)
    
            matches = matches_a + matches_b
            unmatched_tracks = list(set(unmatched_tracks_a + unmatched_tracks_b))
            return matches, unmatched_tracks, unmatched_detections
    

    _initiate_track

    _initiate_track
    KalmanFilter.initiate
    Track

    KalmanFilter.initiate 由检测目标构建均值向量与协方差矩阵。

            mean, covariance = self.kf.initiate(detection.to_xyah())
            self.tracks.append(Track(
                mean, covariance, self._next_id, self.n_init, self.max_age,
                detection.feature))
            self._next_id += 1
    

    min_cost_matching

    min_cost_matching
    distance_metric
    linear_assignment

    解决线性分配问题。
    参数:

    • distance_metricCallable[List[Track], List[Detection], List[int], List[int]) -> ndarray。距离度量给出了轨迹和检测的列表以及 N 个轨迹索引和 M 个检测索引的列表。度量应该返回 NxM 维度成本矩阵,其中元素(i,j)是给定轨迹索引中的第 i 个轨迹与给定的检测索引中的第 j 个检测之间的关联成本。
    • max_distance:门控阈值,float。忽略成本大于此值的关联。
    • tracks:列表[track.Track],当前时间步骤的预测轨迹列表。
    • detections:列表[detection.Detection]当前时间步骤的检测列表。
    • track_indices:int 型列表。将cost_matrix中的行映射到轨迹的轨迹索引列表track(见上面的描述)。
    • detection_indices:int 型列表。 将cost_matrix中的列映射到的检测索引列表detections 中的检测(见上面的描述)。

    返回值:
    (List[(int, int)], List[int], List[int])
    返回包含以下三个条目的元组:

    • 匹配的跟踪和检测索引列表。
    • 不匹配的轨迹索引列表。
    • 未匹配的检测索引列表。
        if track_indices is None:
            track_indices = np.arange(len(tracks))
        if detection_indices is None:
            detection_indices = np.arange(len(detections))
    
        if len(detection_indices) == 0 or len(track_indices) == 0:
            return [], track_indices, detection_indices  # Nothing to match.
    

    由距离度量指标计算成本矩阵。设置超过阈值max_distance的成本为固定值,消除差异。
    linear_assignment 关联检测框。

        cost_matrix = distance_metric(
            tracks, detections, track_indices, detection_indices)
        cost_matrix[cost_matrix > max_distance] = max_distance + 1e-5
        indices = linear_assignment(cost_matrix)
    
        matches, unmatched_tracks, unmatched_detections = [], [], []
        for col, detection_idx in enumerate(detection_indices):
            if col not in indices[:, 1]:
                unmatched_detections.append(detection_idx)
        for row, track_idx in enumerate(track_indices):
            if row not in indices[:, 0]:
                unmatched_tracks.append(track_idx)
        for row, col in indices:
            track_idx = track_indices[row]
            detection_idx = detection_indices[col]
            if cost_matrix[row, col] > max_distance:
                unmatched_tracks.append(track_idx)
                unmatched_detections.append(detection_idx)
            else:
                matches.append((track_idx, detection_idx))
        return matches, unmatched_tracks, unmatched_detections
    

    matching_cascade

        if track_indices is None:
            track_indices = list(range(len(tracks)))
        if detection_indices is None:
            detection_indices = list(range(len(detections)))
    

    初始化匹配集matches MM \gets \emptyset
    未匹配检测集unmatched_detections UDU \gets D

        unmatched_detections = detection_indices
        matches = []
    

    for&ThinSpace;n{1,,Amax}&ThinSpace;doSelect tracks by age Tn{iTai=n} \begin{aligned} \mathbf{for}&amp; \, n\in\{1,\dots,A_{\rm max}\}\, \mathbf{do}\\ &amp;\text{Select tracks by age } T_n \gets \{i \in T \mid a_i = n\} \end{aligned}

        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
    

    [xi,j]min_cost_matching(C,Tn,U)MM{(i,j)bi,jxi,j&gt;0}UU{jibi,jxi,j&gt;0} [x_{i,j}] \gets \text{min\_cost\_matching}(\mathit{C}, \mathcal{T}_n, \mathcal{U})\\ \mathcal{M} \gets \mathcal{M} \cup \{(i, j) \mid b_{i,j}\cdot x_{i,j} &gt; 0 \}\\ \mathcal{U} \gets \mathcal{U} \setminus \{j \mid \sum_i b_{i,j}\cdot x_{i,j} &gt; 0\}
    min_cost_matching 输出的匹配直接满足 bi,j&gt;0b_{i,j}&gt;0

            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
    

    gate_cost_matrix

    基于卡尔曼滤波获得的状态分布,使成本矩阵中的不可行条目无效。
    参数:

    • kf:卡尔曼滤波器。
    • cost_matrix:ndarray NxM 维度成本矩阵,其中 N 是轨迹索引的数量,M 是检测索引的数量,使得条目(i, j)是track[track_indices [i]]detections[detection_indices[j]]关联成本。
    • tracks:列表 [track.Track],当前时间点的预测轨迹列表。
    • detections:列表 [detection.Detection],当前时间步骤的检测列表。
    • track_indices:List [int] 将cost_matrix中的行映射到轨道的轨道索引列表track(见上面的描述)。
    • detection_indices:List [int] 将“cost_matrix”中的列映射到“检测”中的检测的检测索引列表(参见上面的描述)。
    • gated_cost:可选[float] 与不可行关联对应的成本矩阵中的条目设置为此值。默认为非常大的值。
    • only_position:可选[bool] 如果为True,则在门控期间仅考虑状态分布的x,y位置。默认为False。

    返回值:

    • ndarray,返回修改后的成本矩阵。

    chi2inv95 具有N个自由度的卡方分布的0.95分位数的表(包含N=1, ..., 9的值)。 取自 MATLAB/Octave 的 chi2inv 函数并用作 Mahalanobis 门控阈值。
    KalmanFilter.gating_distance 计算状态分布和测量之间的选通距离。

        gating_dim = 2 if only_position else 4
        gating_threshold = kalman_filter.chi2inv95[gating_dim]
        measurements = np.asarray(
            [detections[i].to_xyah() for i in detection_indices])
        for row, track_idx in enumerate(track_indices):
            track = tracks[track_idx]
            gating_distance = kf.gating_distance(
                track.mean, track.covariance, measurements, only_position)
            cost_matrix[row, gating_distance > gating_threshold] = gated_cost
        return cost_matrix
    

    NearestNeighborDistanceMetric

    最近邻距离度量。对于每个目标,返回到目前为止已观察到的所有样本的最近距离。
    参数:

    • metric:str 类型,“euclidean"或者是"cosine”。

    • matching_threshold:float 型,匹配阈值。将距离较大的样本视为无效匹配。

    • budget:int 型(可选),如果不是 None,则最多每个类抽样到为此数字。达到预算时删除最旧的样本。
      属性:

    • samples:Dict [int - > List [ndarray]],从目标身份映射到目前已观察到的样本列表的字典。

     
        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 = {}
    

    partial_fit

    使用新数据更新距离指标。
    参数:

    • features:ndarray 类型,具有维数 M 的 N 个特征的 N×M 矩阵。
    • targets:ndarray 类型,关联目标标识的整数数组。
    • active_targets:int 型列表,场景中当前存在的目标列表。

    setdefault 如果字典存在键key,返回它的值。如果不存在,插入值为default的键key,并返回defaultdefault默认为None

    由目标及对应特征构造样本字典self.samples并剔除其中不活跃的。

            for feature, target in zip(features, targets):
                self.samples.setdefault(target, []).append(feature)
                if self.budget is not None:
                    self.samples[target] = self.samples[target][-self.budget:]
            self.samples = {k: self.samples[k] for k in active_targets}
    

    ImageViewer

    具有绘图程序和视频捕获功能的图像查看器。

        def __init__(self, update_ms, window_shape=(640, 480), caption="Figure 1"):
            self._window_shape = window_shape
            self._caption = caption
            self._update_ms = update_ms
            self._video_writer = None
            self._user_fun = lambda: None
            self._terminate = False
    
            self.image = np.zeros(self._window_shape + (3, ), dtype=np.uint8)
            self._color = (0, 0, 0)
            self.text_color = (255, 255, 255)
            self.thickness = 1
    
        @property
        def color(self):
            return self._color
    
        @color.setter
        def color(self, value):
            if len(value) != 3:
                raise ValueError("color must be tuple of 3")
            self._color = tuple(int(c) for c in value)
    

    rectangle

    绘制一个矩形。输入矩形参数格式为[x, y, w, h],在矩形左上角放置文本标签。

            pt1 = int(x), int(y)
            pt2 = int(x + w), int(y + h)
            cv2.rectangle(self.image, pt1, pt2, self._color, self.thickness)
    
            if label is not None:
                text_size = cv2.getTextSize(
                    label, cv2.FONT_HERSHEY_PLAIN, 1, self.thickness)
    
                center = pt1[0] + 5, pt1[1] + 5 + text_size[0][1]
                pt2 = pt1[0] + 10 + text_size[0][0], pt1[1] + 10 + \
                    text_size[0][1]
                cv2.rectangle(self.image, pt1, pt2, self._color, -1)
                cv2.putText(self.image, label, center, cv2.FONT_HERSHEY_PLAIN,
                            1, (255, 255, 255), self.thickness)
    

    circle

    绘制圆圈。

            image_size = int(radius + self.thickness + 1.5)  # actually half size
            roi = int(x - image_size), int(y - image_size), \
                int(2 * image_size), int(2 * image_size)
            if not is_in_bounds(self.image, roi):
                return
    
            image = view_roi(self.image, roi)
            center = image.shape[1] // 2, image.shape[0] // 2
            cv2.circle(
                image, center, int(radius + .5), self._color, self.thickness)
            if label is not None:
                cv2.putText(
                    self.image, label, center, cv2.FONT_HERSHEY_PLAIN,
                    2, self.text_color, 2)
    
    

    gaussian

    绘制二维高斯分布的95%置信椭圆。

            # chi2inv(0.95, 2) = 5.9915
            vals, vecs = np.linalg.eigh(5.9915 * covariance)
            indices = vals.argsort()[::-1]
            vals, vecs = np.sqrt(vals[indices]), vecs[:, indices]
    
            center = int(mean[0] + .5), int(mean[1] + .5)
            axes = int(vals[0] + .5), int(vals[1] + .5)
            angle = int(180. * np.arctan2(vecs[1, 0], vecs[0, 0]) / np.pi)
            cv2.ellipse(
                self.image, center, axes, angle, 0, 360, self._color, 2)
            if label is not None:
                cv2.putText(self.image, label, center, cv2.FONT_HERSHEY_PLAIN,
                            2, self.text_color, 2)
    

    annotate

            cv2.putText(self.image, text, (int(x), int(y)), cv2.FONT_HERSHEY_PLAIN,
                        2, self.text_color, 2)
    
    

    colored_points

            if not skip_index_check:
                cond1, cond2 = points[:, 0] >= 0, points[:, 0] < 480
                cond3, cond4 = points[:, 1] >= 0, points[:, 1] < 640
                indices = np.logical_and.reduce((cond1, cond2, cond3, cond4))
                points = points[indices, :]
            if colors is None:
                colors = np.repeat(
                    self._color, len(points)).reshape(3, len(points)).T
            indices = (points + .5).astype(np.int)
            self.image[indices[:, 1], indices[:, 0], :] = colors
    

    enable_videowriter

            fourcc = cv2.VideoWriter_fourcc(*fourcc_string)
            if fps is None:
                fps = int(1000. / self._update_ms)
            self._video_writer = cv2.VideoWriter(
                output_filename, fourcc, fps, self._window_shape)
    
        def disable_videowriter(self):
            """ Disable writing videos.
            """
            self._video_writer = None
    
    

    run

    启动图像查看器。此方法将阻塞,直到用户请求关闭窗口。
    运行传入的函数,保存视频并显示。enable_videowriter 函数会创建 VideoWriter

            if update_fun is not None:
                self._user_fun = update_fun
    
            self._terminate, is_paused = False, False
            # print("ImageViewer is paused, press space to start.")
            while not self._terminate:
                t0 = time.time()
                if not is_paused:
                    self._terminate = not self._user_fun()
                    if self._video_writer is not None:
                        self._video_writer.write(
                            cv2.resize(self.image, self._window_shape))
                t1 = time.time()
                remaining_time = max(1, int(self._update_ms - 1e3*(t1-t0)))
                cv2.imshow(
                    self._caption, cv2.resize(self.image, self._window_shape[:2]))
                key = cv2.waitKey(remaining_time)
                if key & 255 == 27:  # ESC
                    print("terminating")
                    self._terminate = True
                elif key & 255 == 32:  # ' '
                    print("toggeling pause: " + str(not is_paused))
                    is_paused = not is_paused
                elif key & 255 == 115:  # 's'
                    print("stepping")
                    self._terminate = not self._user_fun()
                    is_paused = True
    

    销毁窗口后重新调用imshow

            # Due to a bug in OpenCV we must call imshow after destroying the
            # window. This will make the window appear again as soon as waitKey
            # is called.
            #
            # see https://github.com/Itseez/opencv/issues/4535
            self.image[:] = 0
            cv2.destroyWindow(self._caption)
            cv2.waitKey(1)
            cv2.imshow(self._caption, self.image)
    

    stop

            self._terminate = True
    

    KalmanFilter

    一种简单的卡尔曼滤波器,用于跟踪图像空间中的边界框。8维状态空间[x, y, a, h, vx, vy, va, vh]包含边界框中心位置(x, y),纵横比a,高度h和它们各自的速度。物体运动遵循等速模型。 边界框位置(x, y, a, h)被视为状态空间的直接观察(线性观察模型)。

    创建卡尔曼滤波器模型矩阵self._motion_matself._update_mat

        def __init__(self):
            ndim, dt = 4, 1.
    
            # Create Kalman filter model matrices.
            self._motion_mat = np.eye(2 * ndim, 2 * ndim)
            for i in range(ndim):
                self._motion_mat[i, ndim + i] = dt
            self._update_mat = np.eye(ndim, 2 * ndim)
    

    依据当前状态估计(高度)选择运动和观测不确定性。这些权重控制模型中的不确定性。这有点 hacky。

            # Motion and observation uncertainty are chosen relative to the current
            # state estimate. These weights control the amount of uncertainty in
            # the model. This is a bit hacky.
            self._std_weight_position = 1. / 20
            self._std_weight_velocity = 1. / 160
    

    initiate

    由测量初始化均值向量(8维)和协方差矩阵(8x8维)。
    numpy.r_ 沿第一轴连接切片对象。

            mean_pos = measurement
            mean_vel = np.zeros_like(mean_pos)
            mean = np.r_[mean_pos, mean_vel]
    
            std = [
                2 * self._std_weight_position * measurement[3],
                2 * self._std_weight_position * measurement[3],
                1e-2,
                2 * self._std_weight_position * measurement[3],
                10 * self._std_weight_velocity * measurement[3],
                10 * self._std_weight_velocity * measurement[3],
                1e-5,
                10 * self._std_weight_velocity * measurement[3]]
            covariance = np.diag(np.square(std))
            return mean, covariance
    

    predict

    卡尔曼滤波器由目标上一时刻的均值和协方差进行预测。

    motion_cov是过程噪声 WkW_k 协方差矩阵 QkQ_k

            std_pos = [
                self._std_weight_position * mean[3],
                self._std_weight_position * mean[3],
                1e-2,
                self._std_weight_position * mean[3]]
            std_vel = [
                self._std_weight_velocity * mean[3],
                self._std_weight_velocity * mean[3],
                1e-5,
                self._std_weight_velocity * mean[3]]
            motion_cov = np.diag(np.square(np.r_[std_pos, std_vel]))
    

    x^kk1=Fkx^k1k1+BkukPkk1=FkPk1k1Fk+Qk \begin{aligned} \hat{\mathrm{x}}_{k|k-1} &amp;= F_k\hat{\mathrm{x}}_{k-1|k-1}+B_k u_k\\ P_{k|k-1}&amp;= F_k P_{k-1|k-1}F^\top_k+Q_k \end{aligned}
    self._motion_matFkF_k 是作用在 xk1\mathrm{x}_{k-1} 上的状态变换模型(/矩阵/矢量)。
    BkB_k 是作用在控制器向量 uku_k 上的输入-控制模型。
    covariancePkkP_{k|k},后验估计误差协方差矩阵,度量估计值的精确程度。

            mean = np.dot(self._motion_mat, mean)
            covariance = np.linalg.multi_dot((
                self._motion_mat, covariance, self._motion_mat.T)) + motion_cov
    
            return mean, covariance
    

    project

    投影状态分布到测量空间。
    参数:

    • mean:ndarray,状态的平均向量(8维数组)。
    • covariance:ndarray,状态的协方差矩阵(8x8维)。

    返回(ndarray,ndarray),返回给定状态估计的预计平均值和协方差矩阵。
    numpy.linalg.multi_dot 在单个函数调用中计算两个或多个数组的点积,同时自动选择最快的求值顺序。

    
            std = [
                self._std_weight_position * mean[3],
                self._std_weight_position * mean[3],
                1e-1,
                self._std_weight_position * mean[3]]
            innovation_cov = np.diag(np.square(std))
    
            mean = np.dot(self._update_mat, mean)
            covariance = np.linalg.multi_dot((
                self._update_mat, covariance, self._update_mat.T))
            return mean, covariance + innovation_cov
    

    update

            projected_mean, projected_cov = self.project(mean, covariance)
    
            chol_factor, lower = scipy.linalg.cho_factor(
                projected_cov, lower=True, check_finite=False)
            kalman_gain = scipy.linalg.cho_solve(
                (chol_factor, lower), np.dot(covariance, self._update_mat.T).T,
                check_finite=False).T
            innovation = measurement - projected_mean
    

    x^kk=x^kk1+Kky~kPkk=(IKkHk)Pkk1 \begin{aligned} \hat{\mathrm{x}}_{k|k} &amp;= \hat{\mathrm{x}}_{k|k-1}+K_k \tilde{\mathrm{y}}_k\\ P_{k|k}&amp;= (I- K_{k}H_k)P_{k|k-1} \end{aligned}

            new_mean = mean + np.dot(innovation, kalman_gain.T)
            new_covariance = covariance - np.linalg.multi_dot((
                kalman_gain, projected_cov, kalman_gain.T))
            return new_mean, new_covariance
    

    gating_distance

    计算状态分布和测量之间的选通距离。可以从 chi2inv95 获得合适的距离阈值。如果only_position为 False,则卡方分布具有4个自由度,否则为2。
    参数:

    • mean:ndarray,状态分布上的平均向量(8维)。
    • covariance:ndarray,状态分布的协方差(8x8维)。
    • measurements:ndarray,N 个测量的 N×4维矩阵,每个矩阵的格式为(x,y,a,h),其中(x,y)是边界框中心位置,纵横比和h高度。
    • only_position:可选[bool],如果为True,则相对于边界进行距离计算盒子中心位置。

    返回,ndarray,返回一个长度为N的数组,其中第i个元素包含(mean,covariance)和measurements [i]之间的平方Mahalanobis距离。

    numpy.linalg.cholesky Cholesky 分解。返回方阵a的 Cholesky 分解 LL.HL * L.H,其中 LL 是下三角形,.H.H 是共轭转置算子(如果a是实值则是普通转置)。 a必须是 Hermitian(对称的,如果是实值的)和正定的。实际只返回 LL
    scipy.linalg.solve_triangular 假设a是三角阵,求解x的等式a x = b

    
            mean, covariance = self.project(mean, covariance)
            if only_position:
                mean, covariance = mean[:2], covariance[:2, :2]
                measurements = measurements[:, :2]
    
            cholesky_factor = np.linalg.cholesky(covariance)
            d = measurements - mean
            z = scipy.linalg.solve_triangular(
                cholesky_factor, d.T, lower=True, check_finite=False,
                overwrite_b=True)
            squared_maha = np.sum(z * z, axis=0)
            return squared_maha
    

    改进思路

    Challenges on Large Scale Surveillance Video Analysis 跟踪与 DeepSORT 类似,但使用 Re-ranking Person Re-identification with k-reciprocal Encoding 方法。

    参考资料:

    展开全文
  • 目标跟踪概念、多目标跟踪算法SORT和deep SORT原理

    千次阅读 多人点赞 2019-05-29 19:56:45
    文章目录目标跟踪、单目标跟踪多目标跟踪的概念在线多目标跟踪sort算法原理SORT算法过程简述估计模型(卡尔曼滤波跟踪器) 目标跟踪、单目标跟踪多目标跟踪的概念 目标跟踪分为静态背景下的目标跟踪和动态背景下...

    目标跟踪、单目标跟踪、多目标跟踪的概念

    目标跟踪分为静态背景下的目标跟踪和动态背景下的目标跟踪。
    静态背景下的目标跟踪:
    静态背景下的目标跟踪指摄像头是固定的,其采集的视野中背景是静止的,如在十字路口的固定摄像头。
    动态背景下的目标跟踪:
    摄像头采集的视野中背景和目标都是在变化的。

    目标跟踪又分为单目标跟踪和多目标跟踪。
    单目标跟踪:
    在视频的初始帧画面上框出单个目标,预测后续帧中该目标的大小与位置。典型算法有Mean shift(用卡尔曼滤波、粒子滤波进行状态预测)、TLD(基于在线学习的跟踪)、KCF(基于相关性滤波)等。
    多目标追踪:
    不像单目标追踪一样先在初始帧上框出单个目标,而是追踪多个目标的大小和位置,且每一帧中目标的数量和位置都可能变化。此外,多目标的追踪中还存在下列问题:
    处理新目标的出现和老目标的消失;
    跟踪目标的运动预测和相似度判别,即上一帧与下一帧目标的匹配;
    跟踪目标之间的重叠和遮挡处理;
    跟踪目标丢失一段时间后再重新出现的再识别。

    欧氏距离、马氏距离、余弦距离

    欧氏距离

    在数学中,欧几里得距离或欧几里得度量是欧几里得空间中两点间“普通”(即直线)距离。
    假设二维空间中有点(x1,y1)和(x2,y2),则这两点的欧氏距离为:
    ρ=(x2x1)2+(y2y1)2 \rho=\sqrt{\left(x_{2}-x_{1}\right)^{2}+\left(y_{2}-y_{1}\right)^{2}}

    马氏距离

    马氏距离(Mahalanobis distance)数据的协方差距离。它是一种有效的计算两个未知样本集的相似度的方法。与欧氏距离不同,马氏距离考虑到各种特性之间的联系,并且与测量尺度无关。
    对于一个均值为:
    μ=(μ1,μ2,μ3,,μp)T \mu=\left(\mu_{1}, \mu_{2}, \mu_{3}, \dots, \mu_{p}\right)^{T}
    协方差矩阵为Σ的多变量矢量:
    x=(x1,x2,x3,,xp)T x=\left(x_{1}, x_{2}, x_{3}, \dots, x_{p}\right)^{T}
    其马氏距离为:
    DM(x)=(xμ)TΣ1(xμ) D_{M}(x)=\sqrt{(x-\mu)^{T} \Sigma^{-1}(x-\mu)}
    马氏距离也可以定义为两个服从同一分布并且其协方差矩阵为Σ的随机变量x与y的差异程度:
    d(x,y)=(xy)TΣ1(xy) d(\vec x, \vec y)=\sqrt{(\vec x-\vec y)^{T} \Sigma^{-1}(\vec x-\vec y)}
    如果协方差矩阵为单位矩阵(或者说去掉协方差矩阵),马氏距离就退化为欧氏距离。如果协方差矩阵为对角阵,其也可称为正规化的马氏距离:
    d(x,y)=i=1p(xiyi)2σi2 d(\vec x, \vec y)=\sqrt{\sum_{i=1}^{p} \frac{\left(x_{i}-y_{i}\right)^{2}}{\sigma_{i}^{2}}}
    其中σi是xi的标准差。
    协方差的物理意义:
    在概率论中,两个随机变量X与Y之间的相互关系有3种情况:正相关、负相关、不相关(这里的相关都是指线性相关)。
    我们可以定义一个表示X, Y 相互关系的数字特征,也就是协方差:
    cov(X,Y)=E(XEX)(YEY) cov(X, Y)=E(X-E X)(Y-E Y)
    当cov(X, Y)>0时,表明X与Y正相关;当cov(X, Y)<0时,表明X与Y负相关;当cov(X, Y)=0时,表明X与Y不相关。
    使用欧式距离衡量两个变量,距离近就一定相似吗?
    如果两个变量的度量尺度不同,如身高和体重,身高用毫米计算,而体重用千克计算。显然差10mm的身高与差10kg的体重是完全不同的。但在普通的欧氏距离中,这将会算作相同的差距。
    如果使用归一化后的欧氏距离,两个变量的欧氏距离近就一定相似吗?
    归一化可以消除不同变量间的度量尺度不同的问题,但是不同变量之间的方差还是不一样。第一个类别均值为0,方差为0.1,第二个类别均值为5,方差为5。那么一个值为2的点属于第一类的概率大还是第二类的概率大?距离上说应该是第一类,但是直觉上显然是第二类,因为第一类不太可能到达2这个位置。因此,在一个方差较小的维度下很小的差别就有可能成为离群点。
    如果维度间不独立同分布,样本点与欧氏距离近的样本点同类的概率一定会更大吗?
    如果维度间不是独立同分布的,那么两个点即使距离均值点距离相同,但显然更接近整体分布点集的点与该点集同类的概率更大。
    马氏距离的几何意义:
    马氏距离就是将变量按照主成分进行旋转,让维度间相互独立,然后进行标准化,使不同的维度独立同分布。由于主成分就是特征向量方向,每个方向的方差就是对应的特征值,所以只需要按照特征向量的方向旋转,然后缩放特征值的倍数。这样,离群点就被成功分离,这时候的新坐标系下的欧式距离就是马氏距离。

    余弦距离

    余弦距离,也称为余弦相似度,是用N维空间中两点与原点连接线段之间夹角的余弦值作为衡量两个个体间差异的大小的度量。余弦距离越大,表示夹角越小,那么两点越相似。如果余弦距离为1(最大值),那么表示两者非常相似。注意这里只能说明两点非常相似,并不一定相同。
    余弦距离公式:
    sim(X,Y)=cosθ=xyxy sim(X, Y)=\cos \theta=\frac{\vec x \cdot \vec y}{||x|| \cdot ||y||}
    向量a和向量b点乘的公式:
    ab=a1b1+a2b2++anbn a \cdot b=a_{1} b_{1}+a_{2} b_{2}+\ldots+a_{\mathrm{n}} b_{n}
    余弦距离的意义:
    当两点夹角为零时,表示两点的各个维度的值所占的比例相同。比如(2,2,2)和(6,6,6),(1,2,3)和(3,6,9)。

    SORT算法原理

    论文:Simple Online and Realtime Tracking
    论文地址:https://arxiv.org/pdf/1602.00763.pdf
    代码地址:https://github.com/abewley/sort

    在sort算法中,目标检测模型的性能是影响追踪效果的一个关键因素。SORT算法全称为Simple Online And Realtime Tracking, 对于tracking-by-detection的多目标跟踪方法,更多依赖的是其目标检测模型的性能的好坏。尽管只是简单的结合了Kalman滤波追踪和匈牙利指派算法,效果却可以匹配2016年的SOTA算法。另外,由于本文的算法复杂度低,追踪器可以实现260Hz的速度,比前者快了20倍。
    多目标追踪问题可以被看成是数据关联问题,目的是在视频帧序列中进行跨帧检测结果的关联。作者没有在目标追踪过程中使用任何的目标外观特征,而是仅使用检测框的位置和大小进行目标的运动估计和数据关联。另外,没有考虑遮挡问题,也没有通过目标的外观特征进行目标重识别,作者一切的核心就是围绕处理速度要快,要能够实时应用。
    作者使用CNN进行目标检测,使用kalman滤波进行目标运动状态估计,使用匈牙利匹配算法进行位置匹配。文章主要关注行人目标的追踪。

    SORT算法中的匈牙利匹配算法

    最大匹配的匈牙利算法

    该算法详细原理可以看我的另一篇专门介绍匈牙利算法最大匹配原理的博客。
    我们知道一个二分图可以用矩阵的形式来表示,如:

    graph_1 = [(0, 0, 0, 0, 1, 0, 1, 0),
               (0, 0, 0, 0, 1, 0, 0, 0),
               (0, 0, 0, 0, 1, 1, 0, 0),
               (0, 0, 0, 0, 0, 0, 1, 1),
               (1, 1, 1, 0, 0, 0, 0, 0),
               (0, 0, 1, 0, 0, 0, 0, 0),
               (1, 0, 0, 1, 0, 0, 0, 0),
               (0, 0, 0, 1, 0, 0, 0, 0)]
    

    上例表示这个二分图的U集合有8个点,V集合也有8个点,当两点之间有边时对应位置的值为1,否则为0。
    匈牙利算法的目标是从二分图中找出一个最大匹配,即匹配边最多的匹配方式。

    指派问题中的匈牙利算法

    进一步地,我们可以将二分图变成下面的形式:

         A    B    C    D
    甲   2    15   13   410   4    14   159    14   16   137    8    11   9
    

    此时U集合中任意点和V集合中任意点都有边,但边的代价不同。此时我们的目标是,在矩阵中选取n个元素(即n条不同代价的边),使得每行每列各有1个元素(U集合和V集合中的每一个点都有配对点),且边的代价和最小。
    我们将上述矩阵赋予一个实际问题的含义:有n项不同的任务,需要n个人分别完成其中的1项,每个人完成任务的时间不一样。于是就有一个问题,如何分配任务使得花费时间最少。
    最优解性质:
    若从矩阵的一行(列)各元素中分别减去该行(列)的最小元素,得到一个归约矩阵,这个规约矩阵的最优解与原矩阵的最优解相同。

    指派问题的匈牙利算法的基本思路:

    • 通过行/列变换让规约(费用)矩阵的每行和每列都出现0;
    • 找出不同行不同列的n个0;
    • 这些0对应的边就是最优指派(代价最少)。

    指派问题的匈牙利算法的主要步骤:

    • 先对规约(费用)矩阵先作行变换,再作列变换。行变换即费用矩阵的每一行的各个元素分别减去该行的最小元素。列变换即费用矩阵的每一列的各个元素分别减去该列的最小元素,有0的列则无需作列变换;
    • 在经过行变换和列变换的费用矩阵中寻找n个不同行不同列的0元素。如果找到,则这n个不同行不同列的0元素位置即对应最优指派。否则,进行下一步骤。一般使用标记法来寻找n个不同行不同列的0元素:
      依次检查新费用矩阵的各行,找出只有一个没有加标记的0元素的行,并将这个0元素加上标记,而与这个0元素在同一列的0元素全划去;
      依次检查新费用矩阵的各列,找出只有一个没有加标记的0元素的列,并将这个0元素加上标记,而与这个0元素在同一行的0元素全划去。
    • 对新费用矩阵进行调整:对每一个加了标记的0元素画一条横线或竖线,使得这些横线和竖线覆盖全部0元素;在这些横线和竖线没有经过的元素中找出最小的元素;未画横线的各行元素减去这个最小的数,画竖线的各列元素加上这个最小的数;重新在费用矩阵中找出n个不同行不同列的0元素,从而找出最优指派(代价最少)。

    指派问题的匈牙利算法求解举例:
    原始矩阵如下,该矩阵U集合的点为甲、乙、丙、丁,V集合的点为A、B、C、D。矩阵中每个值是每条边的代价。

        A    B    C    D
    甲  90   75   75   8035   85   55   65125  95   90   10545   110  95   115
    

    首先每行减去每行的最小值,矩阵变为:

        A    B    C    D
    甲  15   0    0    50    50   20   3035   5    0    150    65   50   70
    

    然后每列减去每列的最小值,矩阵变为:

        A    B    C    D
    甲  15   0    0    00    50   20   2535   5    0    100    65   50   65
    

    现在查找行或列中含有元素0的行或列,用最少数量的水平+垂直线覆盖所有的0。发现只要第一行、第一列、第三列就可以覆盖所有的0,线数量为3,小于U集合中点个数4。没有被覆盖的元素有50,5,65,25,10,65,其中最小元素为5。那么我们让没有被覆盖的每行减去最小值5,被覆盖的每列加上最小值5,然后继续寻找用最少数量的水平+垂直线覆盖所有的0。
    第二、三、四行没有被覆盖,每行减去最小值5。

        A    B    C    D
    甲  15   0    0    0-5   45   15   2030   0    -5   5-5   60   45   60
    

    第一、三列被覆盖,每行加上最小值5。

        A    B    C    D
    甲  20   0    5    00    45   20   2035   0    0    50    60   50   60
    

    现在我们可以用第一行、第三行、第一列覆盖所有的0,线数量为3,仍小于U集合中点个数4。没被覆盖的元素有45,20,20,60,50,60,最小值元素为20。那么我们让没有被覆盖的每行减去最小值20,被覆盖的每列加上最小值20,然后继续寻找用最少数量的水平+垂直线覆盖所有的0。
    第二、四行没有被覆盖,每行减去最小值20。

        A    B    C    D
    甲  20   0    5    0-20  25   0    035   0    0    5-20  40   30   40
    

    第一列被覆盖,每行加上最小值20。

        A    B    C    D
    甲  40   0    5    00    25   0    055   0    0    50    40   30   40
    

    现在我们用第一行、第二行、第三行、第四行可以覆盖所有的0。线数量为4等于U集合中点个数4。找到不同行不同列的n个0。即丁A、丙B、乙C、D甲位置的4个0。
    上面矩阵找到的最优解和原始矩阵的最优解等同。原始矩阵的最优指派(最小代价)就是这四个位置上的代价之和,即45,95,55,80。

    在SORT算法中,二分图的边权值即前一帧的M个目标与后一帧的N个目标中,两两目标之间的IOU。

    预测模型(卡尔曼滤波器)

    作者近似地认为目标的不同帧间地运动是和其他物体及相机运动无关的线性运动。每一个目标的状态可以表示为:
    x=[u,v,s,r,u˙,v˙,s˙]T x=[u, v, s, r, \dot{u}, \dot{v}, \dot{s}]^{T}
    其中u和v分别代表目标的中心坐标,而s和r分别代表目标边界框的比例(面积)和长宽比,长宽比被认为是常数,需要保持不变。
    当进行目标关联时,使用卡尔曼滤波器,用上一帧中目标的位置信息预测下一帧中这个目标的位置。若上一帧中没有检测到下一帧中的某个目标,则对于这个目标,重新初始化一个新的卡尔曼滤波器。关联完成后,使用新关联的下一帧中该目标的位置来更新卡尔曼滤波器。

    数据关联(匈牙利匹配)

    SORT中匈牙利匹配的原理见上面指派问题中的匈牙利匹配算法。SORT算法中的代价矩阵为上一帧的M个目标与下一帧的N个目标两两目标之间的IOU。当然,小于指定IOU阈值的指派结果是无效的(源码中阈值设置为0.3)。
    此外,作者发现使用IOU能够解决目标的短时被遮挡问题。这是因为目标被遮挡时,检测到了遮挡物,没有检测到原有目标,假设把遮挡物和原有目标进行了关联。那么在遮挡结束后,因为在相近大小的目标IOU往往较大,因此很快就可以恢复正确的关联。这是建立在遮挡物面积大于目标的基础上的。

    目标丢失问题的处理

    如果连续Tlost帧没有实现已追踪目标预测位置和检测框的IOU匹配,则认为目标消失。实验中设置 Tlost=1,文中指出是因为没有匹配的目标所使用的均速运动假设模型效果很差,并且帧数过多的re-id问题超出了本文讨论的范围(作者主要关注逐帧的目标追踪,而不关注长时间的目标丢失再重识别问题)。另外,尽早删除已丢失的目标有助于提升追踪效率。但是这样容易导致目标ID会频繁切换,造成跟踪计数的不准确。

    SORT算法过程

    对第一帧使用目标检测模型进行目标检测,得到第一帧中所有目标的分类和位置(假设有M个目标),并标注一个独有id。对每个目标初始化卡尔曼滤波跟踪器,预测每个目标在下一帧的位置;
    对第二帧使用目标检测模型进行目标检测,得到第二帧中所有目标的分类和位置(假设有N个目标),求第一帧M个目标和第二帧N个目标两两目标之间的IOU,建立代价矩阵,使用匈牙利匹配算法得到IOU最大的唯一匹配(数据关联部分),再去掉匹配值小于iou_threshold的匹配对;
    用第二帧中匹配到的目标的位置去更新卡尔曼跟踪器,计算第二帧时的卡尔曼增益Kk,状态估计值xk,估计误差协方差Pk。并输出状态估计值xk用来计算下一帧中的预测位置。对于本帧中没有匹配到的目标重新初始化卡尔曼滤波跟踪器;
    后面每一帧图像都按第一帧和第二帧的做法进行类似处理即可。

    deep SORT算法原理

    论文:Simple Online and Realtime Tracking With a Deep Association Metric
    论文地址:https://arxiv.org/pdf/1703.07402.pdf
    代码地址:https://github.com/nwojke/deep_sort

    状态估计

    使用一个8维空间去刻画轨迹在某时刻的状态:
    (u,v,γ,h,x˙,y˙,γ˙,h˙) (u, v, \gamma, h, \dot{x}, \dot{y}, \dot{\gamma}, \dot{h})
    使用一个kalman滤波器预测更新轨迹,该卡尔曼滤波器采用匀速模型和线性观测模型。通过卡尔曼估计对(u, v, r, h)进行估计,u,v是物体中心点的位置,r是长宽比,h是高。运动估计对于运动状态变化不是很剧烈和频繁的物体能取得比较好的效果。
    其观测变量为:
    (u,v,γ,h) (u, v, \gamma, h)

    轨迹处理

    对于每一个追踪目标,都有一个阈值ak用于记录轨迹从上一次成功匹配到当前时刻的时间(即连续没有匹配的帧数),我们称之为轨迹。当该值大于提前设定的阈值Amax则认为该轨迹终止,直观上说就是长时间匹配不上的轨迹则认为该轨迹已经结束。
    在匹配时,对于没有匹配成功的目标都认为可能产生新的轨迹。但由于这些检测结果可能是一些错误警告,所以对这种情形新生成的轨迹标注状态’tentative’,然后观查在接下来的连续若干帧(论文中是3帧)中是否连续匹配成功,是的话则认为是新轨迹产生,标注为’confirmed’,否则则认为是假性轨迹,状态标注为’deleted’。

    分配问题的评价指标

    在位置度量上,使用马氏距离(Mahalanobis distance)来评价卡尔曼滤波预测的状态和实际状态的匹配程度(运动匹配程度):
    d(1)(i,j)=(djyi)TSi1(djyi) d^{(1)}(i, j)=\left(d_{j}-y_{i}\right)^{T} S_{i}^{-1}\left(d_{j}-y_{i}\right)
    上式左边表示第j个检测到的目标和第i条轨迹之间的运动匹配度,其中Si是第i条轨迹由kalman滤波器预测得到的在当前时刻观测空间的协方差矩阵,yi是轨迹在当前时刻的预测观测量,dj是第j个目标的实际状态(u,v,r,h)。
    考虑到运动的连续性,可以通过该马氏距离对目标进行筛选,文中使用卡方分布的0.95分位点作为阈值 t^{(1)} =0.4877,我们可以定义一个门限函数:
    bij(1)=1[d(1)(i,j)t(1)] b_{i j}^{(1)}=\mathbf{1}\left[d^{(1)(i, j)} \leq t^{(1)}\right]
    当目标运动不确定性较低时,马氏距离是一个很好的关联度量。但在实际中,如相机运动时会造成马氏距离大量不能匹配,也就会使这个度量失效。因此,我们整合第二个度量标准,对每一个BBox检测框dj我们计算一个表面特征描述子:
    rj,rj=1 r_{j},\left|r_{j}\right|=1
    我们保存最新的Lk=100个轨迹的描述子,然后我们计算使用第i个轨迹和第j个轨迹的最小余弦距离(cosine distance)来衡量检测和轨迹之间的外观相似程度:
    d(2)(i,j)=min(1rjTrk(i)rk(i)Ri) d^{(2)}(i, j)=\min \left(1-r_{j}^{T} r_{k}^{(i)} | r_{k}^{(i)} \in R_{i}\right)
    同样的,该度量同样可以确定一个门限函数:
    bi,j(2)=1[d(2)(i,j)t(2)] b_{i, j}^{(2)}=\mathbb{1}\left[d^{(2)}(i, j) \leq t^{(2)}\right]
    最后的评价指标是上面两种距离的加权和:
    ci,j=λd(1)(i,j)+(1λ)d(2)(i,j) c_{i, j}=\lambda d^{(1)}(i, j)+(1-\lambda) d^{(2)}(i, j)
    bi,j=m=12bi,j(m) b_{i, j}=\prod_{m=1}^{2} b_{i, j}^{(m)}
    其中是λ是超参数,用于调整不同项的权重。
    总之,马氏距离对于短期的预测和匹配效果很好,而最小余弦距离对于长时间丢失的轨迹而言,匹配度度量的比较有效。超参数的选择要看具体的数据集,比如文中说对于相机运动幅度较大的数据集,直接不考虑运动匹配程度。

    级联匹配

    如果一条轨迹被遮挡了一段较长的时间,那么在kalman滤波器的不断预测中就会导致概率弥散。那么假设现在有两条轨迹竞争同一个目标,那么那条遮挡时间长的往往得到马氏距离更小,使目标倾向于匹配给丢失时间更长的轨迹,但是直观上,该目标应该匹配给时间上最近的轨迹。
    导致这种现象的原因正是由于kalman滤波器连续预测没法更新导致的概率弥散。假设本来协方差矩阵是一个正态分布,那么连续的预测不更新就会导致这个正态分布的方差越来越大,那么离均值欧氏距离远的点可能和之前分布中离得较近的点获得同样的马氏距离值。
    所以本文中才引入了级联匹配的策略将遮挡时间按等级分层,遮挡时间越小的匹配等级更高,即更容易被匹配。
    首先是得到追踪框集合T和检测框集合D,设置最大的Amax为轨迹最大允许丢失匹配的帧数。通过计算上面的评价指标(两种度量的加权和)得到成本矩阵,再通过级联条件,设定阈值分别对外观和位置因素进行计算,满足条件则返回1,否则返回0。然后初始化匹配矩阵为空,初始化未匹配矩阵等于D。通过匈牙利算法,对于每个属于追踪框集合的元素T,在检测框里面查找成本最低且满足阈值过滤条件的检测框作为匹配结果,同时更新匹配矩阵和非匹配矩阵。
    在匹配的最后阶段还对unconfirmed和age=1的未匹配轨迹进行基于IOU的匹配。这可以缓解因为表观突变或者部分遮挡导致的较大变化。当然有好处就有坏处,这样做也有可能导致一些新产生的轨迹被连接到了一些旧的轨迹上。但这种情况较少。

    深度表观描述子

    预训练的网络时一个在大规模ReID数据集上训练得到的,这个ReID数据集包含1261个人的1100000幅图像,使得学到的特征很适合行人跟踪。
    然后使用该预训练网络作为基础网络,构建wide ResNet,用来提取bounding box的表观特征。该网络在Nvidia GeForce GTX 1050 mobile GPU下提取出32个bounding boxes大约花费30ms,可以满足实时性要求。

    算法总结

    使用wide ResNet提取的特征进行匹配,大大减少了SORT中的ID switches, 经作者实验证明减少了大约45%, 在高速率视频流中也达到了很好的水准。该模型的速度主要取决于目标检测模型的速度,在匹配上面耗时很短。

    展开全文
  • 由于最近在做视频跟踪的小项目,这里对MeanShift的视频目标跟踪算法进行一下小结。 (一)MeanShift算法简介  MeanShift算法正是属于核密度估计法,它不需要任何先验知识而完全依靠特征空间中样本点的计算其密度...
  • DeepSORT多目标跟踪算法

    千次阅读 多人点赞 2020-03-26 12:06:13
    DeepSORT算法理解及实现。
  • 【技术向】DSST目标跟踪算法Matlab代码运行指南

    万次阅读 热门讨论 2016-11-24 18:24:42
    代码来源:http://www.cvl.isy.liu.se/en/research/objrec/visualtracking/scalvistrack/index.html 论文:Accurate Scale Estimation for Robust Visual Tracking(DSST)  1.运行环境 win8.1+matlabR2015a 2....
  • 视觉多目标跟踪算法综述

    千次阅读 2018-09-12 14:15:30
    1. 导言 目标跟踪是机器视觉中一类被广为研究的重要问题,分为单目标...例如,在自动驾驶系统中,目标跟踪算法要对运动的车、行人、其他动物的运动进行跟 踪,对它们在未来的位置、速度等信息作出预判。 目标跟...
  • 目标跟踪算法

    千次阅读 2018-12-04 19:33:52
    &nbsp; 第一部分:目标跟踪速览 先跟几个SOTA的tracker混个脸熟,大概了解一下目标...如果你问别人近几年有什么比较niubility的跟踪算法,大部分人都会扔给你吴毅老师的论文,OTB50和OTB100(OTB50这里指OTB-...
  • 17种视觉目标跟踪算法(含相关滤波和深度学习2大派系)的代码(含matlab、Python和c的实现)、论文以及自己学习心得和网上权威原理解读,自己上研究生区间实验室师兄弟专业整理,大部分代码均能调通。【BACF、C-COT...
  • 视觉多目标跟踪算法综述(上)

    万次阅读 多人点赞 2018-09-11 15:21:47
    目标跟踪是机器视觉中一类被广为研究的重要问题,分为单目标跟踪多目标跟踪。前者跟踪视频画面中的单个目标,后者则同时跟踪视频画面中的个目标,得到这些目标的运动轨迹。 基于视觉的目标自动跟踪在智能监控、...
  • 2. 在项目的源代码》右键新建项》项目名为:MainFun-BoostingApplication.cpp,这个就是项目的主函数,将BoostingApplication.cpp里的代码复制到MainFun-BoostingApplication.cpp来,在工程目录下删除...
  • MeanShift的目标跟踪算法  这几天在学meanshift跟踪的原理和实现,虽然还不是特别明白,但总想先找个程序跑一下试一下效果,网上的程序是基于c接口的老的opencv程序,在借鉴大神思路的基础上,对其代码进行了...
  • 目标跟踪算法研究综述

    万次阅读 多人点赞 2018-05-31 12:45:38
    入坑也算有些日子了,一直在看论文并没有对目标跟踪的研究进展和算法有一个系统性... 目标跟踪算法研究难点与挑战:实际复杂的应用环境 、背景相似干扰、光照条件的变化、遮挡等外界因素以及目标姿态变化,外观变形...
  • yolo结合多目标跟踪算法实现检测和跟踪效果

    千次阅读 热门讨论 2019-10-17 11:30:40
    代码如下: import os import yolov3 import cv2 import numpy as np import threading from timeit import default_timer as timer def detect_camera(videoPath=None, loop=None, output_path="", trackingAlt=...
  • 2017目标跟踪算法综述

    千次阅读 2017-07-31 16:46:35
    2017目标跟踪算法综述 本文所提的跟踪主要指的是单目标跟踪,目标跟踪暂时不作为考虑范围。 本文主要从常用的评价标准,如EAO,EFO,fps等,分析2016-2017年最新出的目标跟踪文章在应用层面的优缺点。 算法原理...
  • 经典目标跟踪算法

    千次阅读 2020-01-03 17:00:28
    相信很来这里的人和我第一次到这里一样,都是想找一种比较好的目标跟踪算法,或者想对目标跟踪这个领域有比较深入的了解,虽然这个问题是经典目标跟踪算法,但事实上,可能我们并不需要那些曾经辉煌但已被拍在沙滩...
  • kcf目标跟踪C代码

    2019-01-22 15:48:27
    经典的目标跟踪算法KCF的代码,标准C语言编写,方便移植
  • 最近项目有用到目标跟踪的算法,用的还是传统opencv,整理一下 1.基础框架 目标跟踪基础认识 视频图像跟踪算法综述 opencv实现目标跟踪的八种算法 2.CSRT追踪器 CSRT追踪器 官方描述 在具有通道和空间可靠性...
  • 该资源是包含代码和样例图像序列,代码附带详细的注释,深入浅出帮助你理解Meanshift算法目标跟踪的关系,巴适系数和meanshift向量的关系
  • 经典的目标跟踪算法

    2018-12-04 19:30:27
    &nbsp; 第一部分:目标跟踪速览 先跟几个SOTA的tracker混个脸熟,大概了解一下目标...如果你问别人近几年有什么比较niubility的跟踪算法,大部分人都会扔给你吴毅老师的论文,OTB50和OTB100(OTB50这里指OTB-...
  • deep sort多目标跟踪算法,算法原作者的实现是基于TensorFlow的(https://github.com/EYHN/deep_sort_yolov3/tree/eyhn) https://github.com/ZQPei/deep_sort_pytorch 用PyTorch实现了RE-ID(目标重识别)模块,并...
  • 2018目标跟踪算法综述

    万次阅读 多人点赞 2018-03-30 11:03:15
    相信很来这里的人和我第一次到这里一样,都是想找一种比较好的目标跟踪算法,或者想对目标跟踪这个领域有比较深入的了解,虽然这个问题是经典目标跟踪算法,但事实上,可能我们并不需要那些曾经辉煌但已被拍在沙滩...
  • 深度多目标跟踪算法综述

    千次阅读 2019-07-11 21:24:58
    基于深度学习的多目标跟踪算法分类 多目标跟踪算法按照轨迹生成的顺序可以分为离线的目标跟踪和在线的多目标跟踪算法。离线方式的多目标跟踪算法通常构造为目标检测关系的图模型,其中设计和计算检测之间的相似度...
  • 基于meanshift的单目标跟踪算法实现 说明: 1. RGB颜色空间刨分,采用16*16*16的直方图 2. 目标模型和候选模型的概率密度计算公式参照上文 3. opencv版本运行:按P停止,截取目标,再按P,进行单目标跟踪 4. Matlab...
  • ©PaperWeekly 原创 ·作者|黄飘学校|华中科技大学硕士研究方向|目标跟踪最近基于深度学习的多目标跟踪算法越来越,有用于特征提取的,有改进单目标跟踪器的,也有提升数据关联...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 50,212
精华内容 20,084
关键字:

多目标跟踪算法c代码