论文题目:SIMPLE ONLINE AND REALTIME TRACKING WITH A DEEP ASSOCIATION METRIC
摘要
DeepSort是在Sort目标追踪基础上的改进。引入了在行人重识别数据集上离线训练的深度学习模型,在实时目标追踪过程中,提取目标的表观特征进行最近邻匹配,可以改善有遮挡情况下的目标追踪效果。同时,也减少了目标ID跳变的问题。
核心思想
算法的核心思想还是用一个传统的单假设追踪方法,方法使用了递归的卡尔曼滤波和逐帧的数据关联。
轨迹处理和状态估计
该部分的思路和sort很接近。
运动状态估计:
使用8个参数来进行运动状态的描述,其中(u,v)是bounding box的中心坐标,r是长宽比,h表示高度。其余四个变量表示对应的在图像坐标系中的速度信息。使用一个基于常量速度模型和线性观测模型的标准kalman滤波器进行目标运动状态的预测,预测的结果为(u,v,r,h)。目标的创建与移除
对每一个追踪目标,记录自其上一次检测结果与追踪结果匹配之后的帧数 ,一旦一个目标的检测结果与追踪结果正确关联之后,就将该参数设置为0。如果 超过了设置的最大阈值 ,则认为对该目标的追踪过程已结束。对新目标出现的判断则是,如果某次检测结果中的某个目标始终无法与已经存在的追踪器进行关联,那么则认为可能出现了新目标。如果连续的3帧中潜在的新的追踪器对目标位置的预测结果都能够与检测结果正确关联,那么则确认是出现了新的运动目标;如果不能达到该要求,则认为是出现了“虚警”,需要删除该运动目标。
指派问题
传统的解决检测结果与追踪预测结果的关联的方法是使用匈牙利方法。本文作者同时考虑了运动信息的关联和目标外观信息的关联。
- 运动信息的关联:使用了对已存在的运动目标的运动状态的kalman预测结果与检测结果之间的马氏距离进行运行信息的关联。
表示第j个检测框的位置, 表示第i个追踪器对目标的预测位置, 表示检测位置与平均追踪位置之间的协方差矩阵?。如果某次关联的马氏距离小于指定的阈值 ,则设置运动状态的关联成功。使用的函数为,作者设置 。 - 当运动的不确定性很低的时候,上述的马氏距离匹配是一个合适的关联度量方法,但是在图像空间中使用kalman滤波进行运动状态估计只是一个比较粗糙的预测。特别是相机存在运动时会使得马氏距离的关联方法失效,造成出现ID switch的现象。因此作者引入了第二种关联方法,对每一个的检测块
求一个特征向量
,限制条件是
。作者对每一个追踪目标构建一个gallary,存储每一个追踪目标成功关联的最近100帧的特征向量。那么第二种度量方式就是计算第i个追踪器的最近100个成功关联的特征集与当前帧第j个检测结果的特征向量间的最小余弦距离。计算公式为:
如果上面的距离小于指定的阈值,那么这个关联就是成功的。阈值是从单独的训练集里得到的。 - 使用两种度量方式的线性加权作为最终的度量,,只有
位于两种度量阈值的交集内时,才认为实现了正确的关联。
距离度量对短期的预测和匹配效果很好,但对于长时间的遮挡的情况,使用外观特征的度量比较有效。对于存在相机运动的情况,可以设置 .
级联匹配
当一个目标长时间被遮挡之后,kalman滤波预测的不确定性就会大大增加,状态空间内的可观察性就会大大降低。假如此时两个追踪器竞争同一个检测结果的匹配权,往往遮挡时间较长的那条轨迹的马氏距离更小,使得检测结果更可能和遮挡时间较长的那条轨迹相关联,这种不理想的效果往往会破坏追踪的持续性。这么理解吧,假设本来协方差矩阵是一个正态分布,那么连续的预测不更新就会导致这个正态分布的方差越来越大,那么离均值欧氏距离远的点可能和之前分布中离得较近的点获得同样的马氏距离值。
所以,作者使用了级联匹配来对更加频繁出现的目标赋予优先权,具体的算法如下图:
级联匹配的核心思想就是由小到大对消失时间相同的轨迹进行匹配,这样首先保证了对最近出现的目标赋予最大的优先权,也解决了上面所述的问题。
在匹配的最后阶段还对unconfirmed和age=1的未匹配轨迹进行基于IoU的匹配。这可以缓解因为表观突变或者部分遮挡导致的较大变化。当然有好处就有坏处,这样做也有可能导致一些新产生的轨迹被连接到了一些旧的轨迹上。但这种情况较少。
深度特征描述器
网络结构:
在行人重识别数据集上离线训练模型。输入128维的归一化的特征。在GTX1050m显卡上,输入30个bounding box提取特征的时间约为30ms。预训练的模型和代码位于https://github.com/nwojke/deep_sort
实验
作者使用《Poi:
Multiple object tracking with high performance detection and appearance feature》文章训练的高性能faster rcnn模型进行检测。检测的置信度阈值设置为0.3。
和sort对比,好处是:
- 减少了45%的ID switch;
- 结合了深度外观信息,对遮挡目标的追踪效果大大提升;
- FP的升高很多,文章中提出这主要是由于静态场景中detection的错误以及过长的允许丢失的track age所导致的(相对于SORT只用相邻帧进行匹配来说,Deep SORT允许高达30帧的丢失,而Kalman的等速运动模型没有改变,这主要造成了FP的升高)。
- 20Hz,依旧实用;
- 达到了state-of-art online tracking的效果。
参考:
https://www.cnblogs.com/YiXiaoZhou/p/7074037.html
http://www.cnblogs.com/yanwei-li/p/8643446.html
代码解析
deep_sort代码(此处)处理流程解析:
按视频帧顺序处理,每一帧的处理流程如下:
- 读取当前帧目标检测框的位置及各检测框图像块的深度特征(此处在处理实际使用时需要自己来提取);
- 根据置信度对检测框进行过滤,即对置信度不足够高的检测框及特征予以删除;
- 对检测框进行非最大值抑制,消除一个目标身上多个框的情况;
预测:使用kalman滤波预测目标在当前帧的位置
执行kalman滤波公式1和2: 和 ,其中, 为目标的状态信息(代码中的mean),为上一帧中目标的信息[center x,center y,aspect ration,height,0,0,0,0]; 为目标的估计误差(代码中的covariance);A为状态转移矩阵;Q为系统误差;
#mean mean_pos = measurement mean_vel = np.zeros_like(mean_pos) mean = np.r_[mean_pos, mean_vel] #covariance self._std_weight_position = 1. / 20 self._std_weight_velocity = 1. / 160 (可调参数) 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))
#矩阵A ndim, dt = 4, 1. self._motion_mat = np.eye(2 * ndim, 2 * ndim) for i in range(ndim): self._motion_mat[i, ndim + i] = dt
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]] #矩阵Q motion_cov = np.diag(np.square(np.r_[std_pos, std_vel])) #kalman滤波公式1 mean = np.dot(self._motion_mat, mean) #kalman滤波公式2 covariance = np.linalg.multi_dot(( self._motion_mat, covariance, self._motion_mat.T)) + motion_cov
预测完之后,对每一个追踪器设置
self.time_since_update += 1
更新:更新kalman追踪器参数及特征集,另外进行目标消失、新目标出现的判断
检测结果与追踪预测结果的匹配
- 区分已确认状态的追踪器和未确认状态的追踪器;
已确认状态的追踪器进行级联匹配
- 对同一消失时间的多个追踪器,计算当前帧新检测的每个目标的深度特征与各追踪器已保存的特征集之间的余弦距离矩阵。假如当前帧有11个检测目标,已有10个追踪器,每个追踪器已保留前面6帧目标的深度特征,则计算得到的cost_matrix大小为10*11,计算过程为首先对每一个追踪器的6个特征,计算与当前帧11个新检测目标特征之间的(1 - 余弦距离),得到6*11矩阵,对每个检测块求最小余弦距离,得到1*11矩阵,存入cost_matrix对应行,表示对当前追踪器而言与当前各检测块之间最小的余弦距离;
cost_matrix = self.metric.distance(features, targets) #distance函数 cost_matrix = np.zeros((len(targets), len(features))) for i, target in enumerate(targets): cost_matrix[i, :] = self._metric(self.samples[target], features) #_metric函数 distances = _cosine_distance(x, y) return distances.min(axis=0) return cost_matrix
- 在计算特征的cost_matrix的基础上,计算kalman滤波预测位置与检测框之间的马氏距离,具体过程为,先将各检测框由[x,y,w,h]转化为[center x,center y, aspect ration,height],对每一个追踪器,也就是cost_matrix中的每一行,计算预测结果与检测结果之间的马氏距离,假设该帧有11个检测结果,那么马氏距离为1*11的矩阵,对cost_matrix中当前行,将马氏距离大于指定阈值的位置处置为1e+5。这样做实现了作者论文中所说的两种度量方式的gating,但是没有体现 参数的作用,另外使用cholesky分解计算马氏距离部分不是很理解。
- 将cost_matrix中大于max_distance的元素置为cost_matrix > max_distance
- 使用匈牙利算法以cost_matrix为输入进行指派
- 指派完成后,分类未匹配的检测、未匹配的追踪、匹配对(cost_matrix中阈值大于max_distance的匹配对也送入未匹配检测和未匹配追踪中去)
未级联匹配上追踪器和未确认状态的追踪与未级联匹配上的检测之间基于IOU进行匹配,具体实现是计算cost_matrix矩阵,矩阵的每一行表示一个追踪器和每一个检测结果之间的(1 - IOU)。对cost_matrix中大于max_distance的元素置为max_distance,然后使用匈牙利算法以cost_matrix矩阵作为输入进行指派,指派完成后依然统计matchs,unmatched_detections,unmatched_tracks;
匹配上的,去做参数更新
- 参数更新的过程就是计算kalman滤波的公式3,4,5。其中公式3中的R矩阵为
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))
- 参数更新完成之后,特征插入追踪器特征集,对应参数进行重新初始化
self.features.append(detection.feature) self.hits += 1 self.time_since_update = 0 #重置为0 #满足条件时确认追踪器 if self.state == TrackState.Tentative and self.hits >= self._n_init: self.state = TrackState.Confirmed
未匹配的追踪器有可能要删除
未匹配的追踪器表示虽然预测到了新的位置,但是检测框匹配不上
#待定状态的追踪器直接删除 if self.state == TrackState.Tentative: self.state = TrackState.Deleted #已经时confirm状态的追踪器,虽然连续多帧对目标进行了预测, #但中间过程中没有任何一帧能够实现与检测结果的关联,说明目标 #可能已经移除了画面,此时直接设置追踪器为待删除状态 elif self.time_since_update > self._max_age: self.state = TrackState.Deleted
未匹配的检测,初始化为新的追踪器
没有匹配上的检测,说明是出现了新的待追踪目标,此时初始化一个新的kalman滤波器,再初始化一个新的追踪器#根据初始检测位置初始化新的kalman滤波器的mean和covariance mean, covariance = self.kf.initiate(detection.to_xyah()) #初始化一个新的tracker self.tracks.append(Track( mean, covariance, self._next_id, self.n_init, self.max_age, detection.feature)) #Tracker的构造函数 self.mean = mean #初始的mean self.covariance = covariance #初始的covariance self.track_id = track_id #id self.hits = 1 self.age = 1 self.time_since_update = 0 #初始值为0 self.state = TrackState.Tentative #初始为待定状态 self.features = [] if feature is not None: self.features.append(feature) #特征入库 self._n_init = n_init self._max_age = max_age #总的目标id++ self._next_id += 1
删除待删除状态的追踪器
self.tracks = [t for t in self.tracks if not t.is_deleted()]
- 更新留下来的追踪器的特征集
#每个activate的追踪器保留最近的self.budget条特征 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:] #以dict的形式插入总库 self.samples = {k: self.samples[k] for k in active_targets}