DeepSORT 多目标跟踪算法

整体思路

SORT 算法的思路是将目标检测算法得到的检测框与预测的跟踪框的 iou(交并比)输入到匈牙利算法中进行线性分配来关联帧间 Id。而 DeepSORT 算法则是将目标的外观信息加入到帧间匹配的计算中,这样在目标被遮挡但后续再次出现的情况下,还能正确匹配 Id,从而减少 Id Switch。

算法思路

状态估计(state estimation)和轨迹处理(track handing)

状态估计

用一个 8 维空间表示轨迹在某个时刻的状态即(u,v,γ,h,x˙,y˙,γ˙,h˙)(u, v, \gamma, h, \dot{x}, \dot{y}, \dot{\gamma}, \dot{h})(u,v,γ,h,x˙,y˙​,γ˙​,h˙),(u,v)(u,v)(u,v)表示 bbox 的中心坐标,γ\gammaγ表示宽高比,hhh表示高度,最后四个变量代表前四个变量的速度信息。使用一个基于匀速模型和线性观测模型的标准卡尔曼滤波器进行目标状态的预测,预测结果为(u,v,γ,h)(u,v,\gamma , h)(u,v,γ,h)。具体关于卡尔曼滤波的解释,可以见我单独说明的文章。

轨迹处理

针对跟踪器:
设置计数器,在使用卡尔曼滤波进行预测时递增,一旦预测的跟踪结果和目标检测算法检测的结果成功匹配,则将该跟踪器的计数器置零;如果一个跟踪器在一段时间内一直没能匹配上检测的结果,则认为跟踪的目标消失,从跟踪器列表中删除该跟踪器。

针对新检测结果:
当某一帧出现了新的检测结果时(即与当前跟踪结果无法匹配的检测结果),认为可能出现了新的目标,为其创建跟踪器。不过,仍需要观察,如果连续三帧中新跟踪器对目标的预测结果都能和检测结果匹配,那么确认出现了新的目标轨迹(代码实现中轨迹state置为confirmed),否则删除该跟踪器。

匹配问题

SORT 算法是将检测框和跟踪框的 IOU 情况作为输入,采用匈牙利算法(这是一种通过增广路径来求二分图最大匹配的一种方法),输出检测框和跟踪框的匹配结果。而 DeepSORT 为了避免大量的 Id Switch,同时考虑了运动信息的关联和目标外观信息的关联,使用融合度量的方式计算检测结果和跟踪结果的匹配程度。通过结合目标框的马氏距离和特征余弦距离两个度量来整合运动信息和外观信息,一方面,马氏距离基于运动信息,提供了有关目标的可能位置的信息,这对短期预测是有效的;另一方面,余弦距离考虑外观信息,这对长期遮挡的目标找回ID比较有效,因为此时运动不具有辨别力。

运动信息的关联

使用检测框和跟踪框之间的马氏距离来描述运动关联程度。
d(1)(i,j)=(dj−yi)TSi−1(dj−yi)d^{(1)}(i, j)=\left(\boldsymbol{d}_{j}-\boldsymbol{y}_{i}\right)^{\mathrm{T}} \boldsymbol{S}_{i}^{-1}\left(\boldsymbol{d}_{j}-\boldsymbol{y}_{i}\right)d(1)(i,j)=(dj​−yi​)TSi−1​(dj​−yi​)
其中,djd_jdj​表示第jjj个检测框的位置,yiy_iyi​表示第iii个跟踪器的预测框位置,SiS_iSi​则表示检测位置与平均跟踪位置之间的协方差矩阵。马氏距离通过计算检测位置与平均预测位置之间的标准差将状态测量的不确定性进行了考虑,并且通过逆χ2\chi^2χ2分布计算得来的95%95\%95%置信区间对马氏距离进行阈值化处理,若某一次关联的马氏距离小于指定的阈值t(1)t^{(1)}t(1),则设置运动状态关联成功,实验中设置阈值为9.4877。
bi,j(1)=1[d(1)(i,j)≤t(1)]b_{i, j}^{(1)}=\mathbb{1}\left[d^{(1)}(i, j) \leq t^{(1)}\right]bi,j(1)​=1[d(1)(i,j)≤t(1)]

目标外观信息的关联

当运动的不确定性很低的时候,上述的马氏距离匹配是一个合适的关联度量方法,但是在图像空间中使用卡尔曼滤波进行运动状态估计只是一个比较粗糙的预测。特别是相机存在运动时会在图像平面中引入快速位移,会使得遮挡情况下马氏距离度量非常不准确的使得关联方法失效,造成 ID switch 的现象。

因此作者引入了第二种关联方法,对每一个检测框djd_jdj​求一个特征向量rjr_jrj​ (通过 REID 的 CNN 网络计算得到的对应的 128 维特征向量),限制条件是∣∣rj∣∣=1||rj||=1∣∣rj∣∣=1。作者对每一个跟踪目标构建一个gallary,存储每一个跟踪目标成功关联的最近100帧的特征向量。那么第二种度量方式就是计算第iii个跟踪器的最近 100 个成功关联的特征集与当前帧第jjj个检测结果的特征向量间的最小余弦距离。计算公式如下:(注意:轨迹太长,导致外观发生变化,发生变化后,再使用最小余弦距离作为度量会出问题,所以在计算距离时,轨迹中的检测数量不能太多)
d(2)(i,j)=min⁡{1−rjTrk(i)∣rk(i)∈Ri}d^{(2)}(i, j)=\min \left\{1-r_{j}^{\mathrm{T}} \boldsymbol{r}_{k}^{(i)} | \boldsymbol{r}_{k}^{(i)} \in \mathcal{R}_{i}\right\}d(2)(i,j)=min{1−rjT​rk(i)​∣rk(i)​∈Ri​}
如果上面的距离小于指定的阈值,那么这个关联就是成功的。阈值是从单独的训练集里得到的,具体如下。
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]bi,j(2)​=1[d(2)(i,j)≤t(2)]

关联方式融合

使用两种度量方式的线性加权作为最终的度量。
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)ci,j​=λd(1)(i,j)+(1−λ)d(2)(i,j)
注意:只有当两个指标都满足各自阈值条件的时候才进行融合。距离度量对短期的预测和匹配效果很好,但对于长时间的遮挡的情况,使用外观特征的度量比较有效。作者指出,对于存在相机运动的情况,可以设置λ=0\lambda=0λ=0。但是,马氏距离的阈值仍然生效,如果不满足第一个度量的标准,就不能进入ci,jc_{i,j}ci,j​的融合阶段。

此时,需要考虑的阈值就变为下式。此时,仅当关联在两个度量的选通区域内,称其为可接受。实际上在代码实现的过程中,是以外观距离为主,运动距离只是作为门限矩阵进行进一步过滤代价矩阵。
bi,j=∏m=12bi,j(m)b_{i, j}=\prod_{m=1}^{2} b_{i, j}^{(m)}bi,j​=m=1∏2​bi,j(m)​

级联匹配

一个目标长时间被遮挡之后,卡尔曼滤波预测的不确定性就会大大增加,状态空间内的可观察性就会大大降低。假如此时两个跟踪器竞争同一个检测结果的匹配权,往往遮挡时间较长的那条轨迹因为长时间未更新位置信息,追踪预测位置的不确定性更大,即协方差会更大,马氏距离计算时使用了协方差的倒数,因此马氏距离会更小,因此使得检测结果更可能和遮挡时间较长的那条轨迹相关联,这种不理想的效果往往会破坏追踪的持续性。

简单理解,假设本来协方差矩阵是一个正态分布,那么连续的预测不更新就会导致这个正态分布的方差越来越大,那么离均值欧氏距离远的点可能和之前分布中离得较近的点获得同样的马氏距离值。

所以,作者使用了级联匹配来对更加频繁出现的目标赋予优先权,具体算法如下图(图源自论文)。

T表示当前的跟踪状态集合,D表示当前的检测状态集合。
第一行根据公式5计算融合度量的代价矩阵;
第二行计算融合的阈值;
第三行初始化已匹配集合为空集;
第四行初始化未匹配集合U为检测集合D;
第五行表示对跟踪状态集合从1到最大跟踪时间Amax,由近到远循环;
第六行表示根据时间选择跟踪的轨迹;
第七行表示计算最小匹配的轨迹的ID即xi,jx_{i,j}xi,j​;
第八行表示将第七步中匹配的ID加入到M中;
第九行表示将上述ID从U中删除;
第十行表示结束循环;
第十一行表示返回最终匹配集合M和未匹配集合U。

级联匹配的核心思想就是由小到大对消失时间相同的轨迹进行匹配,这样首先保证了对最近出现的目标赋予最大的优先权,也解决了上面所述的问题。在匹配的最后阶段还对 unconfirmed和age=1的未匹配轨迹进行基于IoU的匹配。这可以缓解因为表观突变或者部分遮挡导致的较大变化。

深度特征提取

网络结果如下图。

要求输入的图像为128*64,输出128维的特征向量。我在使用Pytorch实现时将上述结构中的池化换成了stride为2的卷积,输出隐层换位256维特征向量,以增大一定参数量的代价试图获得更好的结果。

由于主要用于行人识别,所以在行人重识别数据集(MARS)上离线训练模型,学到的参数很适合提取行人特征,最后输出256维的归一化后的特征。

核心模型结构代码如下。

class BasicBlock(nn.Module):def __init__(self, c_in, c_out, is_downsample=False):super(BasicBlock, self).__init__()self.is_downsample = is_downsampleif is_downsample:self.conv1 = nn.Conv2d(c_in, c_out, 3, stride=2, padding=1, bias=False)else:self.conv1 = nn.Conv2d(c_in, c_out, 3, stride=1, padding=1, bias=False)self.bn1 = nn.BatchNorm2d(c_out)self.relu = nn.ReLU(True)self.conv2 = nn.Conv2d(c_out, c_out, 3, stride=1, padding=1, bias=False)self.bn2 = nn.BatchNorm2d(c_out)if is_downsample:self.downsample = nn.Sequential(nn.Conv2d(c_in, c_out, 1, stride=2, bias=False),nn.BatchNorm2d(c_out))elif c_in != c_out:self.downsample = nn.Sequential(nn.Conv2d(c_in, c_out, 1, stride=1, bias=False),nn.BatchNorm2d(c_out))self.is_downsample = Truedef forward(self, x):y = self.conv1(x)y = self.bn1(y)y = self.relu(y)y = self.conv2(y)y = self.bn2(y)if self.is_downsample:x = self.downsample(x)return F.relu(x.add(y), True)  # 残差连接def make_layers(c_in, c_out, repeat_times, is_downsample=False):blocks = []for i in range(repeat_times):if i == 0:blocks += [BasicBlock(c_in, c_out, is_downsample=is_downsample), ]else:blocks += [BasicBlock(c_out, c_out), ]return nn.Sequential(*blocks)class Net(nn.Module):def __init__(self, num_classes=1261, reid=False):""":param num_classes: 分类器层输出的类别数目:param reid: 是否为reid模式,若为True,直接返回特征向量而不做分类"""super(Net, self).__init__()# 3 128 64self.conv = nn.Sequential(nn.Conv2d(3, 64, 3, stride=1, padding=1),nn.BatchNorm2d(64),nn.ReLU(inplace=True),nn.MaxPool2d(3, 2, padding=1),)# 32 64 32self.layer1 = make_layers(64, 64, 2, False)# 32 64 32self.layer2 = make_layers(64, 128, 2, True)# 64 32 16self.layer3 = make_layers(128, 256, 2, True)# 128 16 8self.layer4 = make_layers(256, 512, 2, True)# 256 8 4self.avgpool = nn.AvgPool2d((8, 4), 1)# 256 1 1 self.reid = reidself.classifier = nn.Sequential(nn.Linear(512, 256),nn.BatchNorm1d(256),nn.ReLU(inplace=True),nn.Dropout(),nn.Linear(256, num_classes),)def forward(self, x):x = self.conv(x)x = self.layer1(x)x = self.layer2(x)x = self.layer3(x)x = self.layer4(x)x = self.avgpool(x)x = x.view(x.size(0), -1)# B x 256if self.reid:x = x / x.norm(p=2, dim=1, keepdim=True)  # 张量单位化return x# 分类器x = self.classifier(x)return x

使用GPU在MARS数据集训练50轮的结果如下。

流程描述

如下图。

相比于SORT,DeepSORT主要更新就是加入了深度特征提取器和级联匹配,其余并没有太大变化,还是按照SORT那一套进行。

最后,DeepSORT算法封装的跟踪器类源码如下,其中各功能已经详细备注。

import numpy as np
from . import kalman_filter
from . import linear_assignment
from . import iou_matching
from .track import Trackclass Tracker:"""多目标跟踪器实现"""def __init__(self, metric, max_iou_distance=0.7, max_age=70, n_init=3):self.metric = metricself.max_iou_distance = max_iou_distanceself.max_age = max_ageself.n_init = n_initself.kf = kalman_filter.KalmanFilter()self.tracks = []self._next_id = 1def predict(self):"""状态预测"""for track in self.tracks:track.predict(self.kf)def update(self, detections):"""状态更新"""# 级联匹配matches, unmatched_tracks, unmatched_detections = self._match(detections)# Update track set.for track_idx, detection_idx in matches:# 成功匹配的要用检测结果更新对于track的参数# 包括#   更新卡尔曼滤波一系列运动变量、命中次数以及重置time_since_update#   检测的深度特征保存到track的特征集中#   连续命中三帧,将track状态由tentative改为confirmedself.tracks[track_idx].update(self.kf, detections[detection_idx])for track_idx in unmatched_tracks:# 未成功匹配的track#   若未经过confirm则删除#   若已经confirm但连续max_age帧未匹配到检测结果也删除self.tracks[track_idx].mark_missed()for detection_idx in unmatched_detections:# 未匹配的检测,为其创建新的trackself._initiate_track(detections[detection_idx])self.tracks = [t for t in self.tracks if not t.is_deleted()]# Update distance metric.# 更新已经确认的track的特征集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():continuefeatures += track.featurestargets += [track.track_id for _ in track.features]track.features = []self.metric.partial_fit(np.asarray(features), np.asarray(targets), active_targets)def _match(self, detections):"""跟踪结果和检测结果的匹配:param detections::return:"""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分为确认track和未确认trackconfirmed_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()]# 将确认的track和检测结果进行级联匹配(使用外观特征)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)# 将上一步未成功匹配的track和未确认的track组合到一起形成iou_track_candidates于还没有匹配结果的检测结果进行IOU匹配iou_track_candidates = unconfirmed_tracks + [k for k in unmatched_tracks_a ifself.tracks[k].time_since_update == 1]unmatched_tracks_a = [k for k in unmatched_tracks_a ifself.tracks[k].time_since_update != 1]# 计算两两之间的iou,再通过1-iou得到cost matrixmatches_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_detectionsdef _initiate_track(self, detection):"""初始化新的跟踪器,对应新的检测结果:param detection::return:"""# 初始化卡尔曼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))# id自增self._next_id += 1

项目说明

本项目主要分为三大模块,deepsort算法模块(其中又分deep模块和sort模块),yolo3检测模块,以及web模块。最终封装为一个跟踪器模块,用于外部接口调用,该模块接受一个视频或者图片序列。

其中,deepsort算法模块包含深度外观特征提取器的deep模块(使用Pytorch实现及训练)以及原始sort跟踪算法模块(该模块部分内容参考SORT论文源码);yolo3检测模块调用封装好的Pytorch实现的YOLO3算法,做了本部分API的兼容;web模块则以Django为框架实现了模型的后端部署,用户通过网页提交视频,后端解析生成跟踪结果(由于机器限制,目前只返回部分帧的检测结果,实时生成依赖GPU服务器,个人电脑FPS较低。)

具体演示如下(浏览器访问)。



直接执行脚本也可以生成跟踪后的视频文件,如下。

补充说明

具体代码开源于我的Github,欢迎star或者fork。

DeepSORT多目标跟踪算法相关推荐

  1. DeepSORT 多目标跟踪算法笔记

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

  2. 基于深度学习的多目标跟踪算法——ReID与MOT的联系

    ©PaperWeekly 原创 · 作者|黄飘 学校|华中科技大学硕士 研究方向|多目标跟踪 最近基于深度学习的多目标跟踪算法越来越多,有用于特征提取的,有改进单目标跟踪器的,也有提升数据关联的.如果 ...

  3. SORT 多目标跟踪算法笔记

    SORT 是一种简单的在线实时多目标跟踪算法.文章要点为: 以 IoU 作为前后帧间目标关系度量指标: 利用卡尔曼滤波器预测当前位置: 通过匈牙利算法关联检测框到目标: 应用试探期甄别虚检: 使用 F ...

  4. YOLOv5+DeepSORT多目标跟踪与计数精讲(含行人计数和车辆计数)

    使用YOLOv5和DeepSORT对视频中的行人.车辆做多目标跟踪,并进行行人计数和车辆计数 课程链接:https://edu.csdn.net/course/detail/32669 采用先进的YO ...

  5. 转载:一线算法工程师整理!超实用的3大多目标跟踪算法

    转载文章,主要自己做技术储备收藏 本文已获公众号滴普科技2048实验室授权发布,如需转载请与原作者联系. 继上一篇对多目标跟踪的核心步骤.评价指标.数据集.核心算法.改进策略.未来的方向等展开详细介绍 ...

  6. 基于深度学习的多目标跟踪算法(上):端到端的数据关联

    ©PaperWeekly 原创 · 作者|黄飘 学校|华中科技大学硕士生 研究方向|多目标跟踪 最近基于深度学习的多目标跟踪算法越来越多,有用于特征提取的,有改进单目标跟踪器的,也有提升数据关联的.如 ...

  7. 多目标跟踪算法简述——量测-航机关联

    多目标跟踪算法简述--量测-航机关联 原创不易,路过的各位大佬请点个赞 针对机动目标跟踪的探讨.技术支持欢迎联系,也可以站内私信 WX: ZB823618313 多目标跟踪算法简述--量测-航机关联 ...

  8. 深度多目标跟踪算法综述

    其它机器学习.深度学习算法的全面系统讲解可以阅读<机器学习-原理.算法与应用>,清华大学出版社,雷明著,由SIGAI公众号作者倾力打造. 书的购买链接 书的勘误,优化,源代码资源 导言 基 ...

  9. 模板匹配算法_计算机视觉方向简介 | 多目标跟踪算法(附源码)

    点击上方"计算机视觉life",选择"星标" 快速获得最新干货 || 前言 目标跟踪是计算机视觉领域中研究的热点之一,分为单目标跟踪与多目标跟踪.前者跟踪视频画 ...

最新文章

  1. 使用Python,OpenCV进行图像哈希
  2. 一文读懂神经网络初始化!吴恩达Deeplearning.ai最新干货
  3. NCBI dbGap数据下载记录
  4. Python基础教程:type()函数-动态创建类
  5. php 大数运算类,PHP采用超长(超大)数字运算防止数字以科学计数法显示的方法
  6. oracle 日累计月,Oracle按月份累计求和
  7. 【鉴权/授权】一步一步实现一个简易JWT鉴权
  8. redis 查询缓存_Redis缓存总结:淘汰机制、缓存雪崩、数据不一致....
  9. WSARecv() 函数使用解析
  10. android主流技术框架,android开发现在流行什么IDE和开发框架?
  11. 百练 求排列的逆序数
  12. python svm 实战_opencv-python 入门实战:传统方法Hog+svm实现目标检测
  13. Java日志组件间关系
  14. 在线CSV转YAML工具
  15. 全新UI聚合支付系统四方系统源码+升级修复漏洞完美版
  16. mov和mp4格式哪个好_公文需带附件时,标准的格式排布
  17. 中国云计算市场“三足鼎立”
  18. 广告的术语和简称大全
  19. html svg 线条动画,线条之美,玩转 SVG 线条动画
  20. 英文名称:DSPE-PEG10-Mal的试剂分子式是C68H127N2O21P

热门文章

  1. 单例模式的5种实现方法及优缺点
  2. 创建订单 - 创建订单后前端的业务处理讲解
  3. JDK API实践:Spring怎样取舍Java I-O、集合、反射、动态代理等API的使用
  4. 区域数据导入功能(OCUpload插件使用)
  5. 文件上传案例的服务器端
  6. Filter_细节_过滤器拦截路径配置
  7. Session的底层实现原理
  8. JVM--类加载机制
  9. android和flask交互,java - 当我从Android向Flask Web服务发送参数时,如何解决“ SSL库故障”? - 堆栈内存溢出...
  10. Python中操作mysql知识(一)