论文地址:https://arxiv.org/pdf/1703.07402.pdf
论文官方源码地址:https://github.com/nwojke/deep_sort
MOT16 benchmark地址:https://motchallenge.net/data/MOT16/
上述最后两个文件的内容解压缩后:
- MOT16 benchmark 解压为MOT16文件夹
- npy文件解压后将其中的detections文件夹剪切,并放置在resources文件夹中。(新建resources)
源码解读:
源码的入口是deep_sort_app.py 程序,在该程序的入口包含args = parse_args()函数,说明需要在命令行运行时需要输入命令行参数。如下图所示:
也正如作者github中关于运行的描述,在命令行或终端运行时需要输入如下指令。
1 python deep_sort_app.py \ 2 --sequence_dir=./MOT16/test/MOT16-06 \ 3 --detection_file=./resources/detections/MOT16_POI_test/MOT16-06.npy \ 4 --min_confidence=0.3 \ 5 --nn_budget=100 \ 6 --display=True
如果想要在pycharm中运行,需要需要在pycharm中设置命令行参数。步骤如下:
- 选择Run - Edit Configurations...
2. 在界面中的Parameters:中填写命令行参数。 如果不懂argparse命令行相关设置也不要紧,可以直接将github中给出的命令行参数去掉 "python deep_sort_app.py \" 即可。
以MOT16-14的数据集为切入点进行解读,之后不再赘述。
deep_sort_app.py主要由以下几个函数组成,其中核心程序在于def run()程序中的frame_callba ck()函数中,用于实现帧与帧之间跟踪器的处理。
*************************************Processing frame 00001******************************************
1. 首先,获取当前帧数frame_idx的检测信息,并过滤掉置信度小于阈值的bbox。
这里会将检测信息中包含的bbox坐标的信息、置信度、features存到一个检测类的对象中。并将多个bbox的对象存入列表中。[坐标info(array) 置信度(float) features(array) ]
检测的坐标info中存储的是bbox[左下坐标的x,y,宽,高]。
在第一帧中,一共有18个b-box。
2. 对这些b-box进行NMS操作
为了便于操作,将各对象中包含的坐标信息和置信度读取成列表的形式。NMS的操作与yolo中有所不同。NMS操作如下:
- 将box info转换为[x1, y1, x2, y2]的形式。 【x,y,w,h】 2 【x1, y1, x2, y2】
- 计算bbox的面积。 需要注意的是,使用像素坐标点计算面积需要+1噢 (y2-y1+1)*(x2-x1+1)
- 对置信度进行排序,返回索引的排序结果。
- 进行NMS,每次取置信度最大的bbox与其余的bbox进行iou处理,通过相交面积 / 做iou的bbox的面积产生 overlap。
overlap = (w * h) / area[idxs[:last]] # 相交的面积/比较的框的面积
- 去掉当前置信度最大的和overlap超过1的索引。【个人感觉此处的NMS并不会有什么作用,面积比超过或等于1的检测框一般在行人检测不存在】
3. 进行kalman的相关运算:
主要包含两个部分,分别代表了Kalman滤波中的时间更新和状态更新。
3.1 时间更新
由于此时并没有tracker,不需要对状态进行估计。下述循环也就不存在,该函数直接跳出。
3.2 状态更新
首先,会进行匹配级联,返回匹配成功的matches,未匹配的跟踪器unmatched_tracks,未匹配的检测器unmatched_detections。
由于第一帧还没有tracker,在匹配的过程中就不存在已确认和未确认的tracker 。(论文中描述,新生成的跟踪器前三帧是试探期,只有在三帧内都可以匹配成功才会确认,否则将会被删除。)
自然,也就不会存在匹配成功的和未匹配的tracker, 只会有未匹配到的检测器18个(全都是未匹配的检测框)。
利用检测框创建新的tracker:
# Update track set.
由于此时匹配的跟踪器和未匹配的跟踪器均为[],并不会对匹配的跟踪器进行更新。只是对未匹配的检测框创建其对应的新tracker。
detection.to_xyah 是将检测框的(左下坐标x1, y1, w, h)转化为(中心点坐标 x, y, 长宽比a, h)
初始化kalman相关的变量。mean是一组8维的kalman滤波状态变量x。
然后,为当前检测框 生成新的Track对象,并存储在tracks列表中:
Tracker类的初始化如下图所示,可以看出,刚生成的Tracker对象是Tentative(试探性的,同论文中所述一致,刚建立的tracker处于试探期,需要在后续三帧中都可以成功关联,该tracker才会确认;否则将会被删除。)另外,每一帧的特征feature也会进行存储,用于后续余弦距离的计算。
第一帧根据18个detections生成了18个处于试探期的trackers。
由于此时的trackers均处于试探期,没有确认,所以不会更新其马氏距离。
在可视化时,会绘制检测框和跟踪框两种。
其中,检测框绘制颜色为0,0,255。由于此时是BGR的色域,所以检测框为红色框。 跟踪框会为每一个id设置不同的颜色。但需要当前的tracker处于确认的状态。 所以在前三帧中,只会绘制检测框,即仅有红色框,如下图所示。
第一帧除了创建了Tracker,没有什么有含金量的内容。步入第二帧。
*************************************Processing frame 00002******************************************
第二帧中,关于检测结果的提取和NMS操作与第一帧相同,不再赘述。经过NMS后剩余的检测框detections有12个:
此时,由于第一帧构建了18个trackers,会利用kalman滤波进行估计,得到18个跟踪预测结果。【即时间更新,tracker.predict()】。而且预测完成后会对每一个tracker的time_since_update+=1。 该操作与SORT中相同,用于衡量其每一次是否关联成功,后续在匹配中会用到。
其中,kalman滤波的时间更新,self._std_weight_position为初始化的1/20(0.05),self. _std_weight_velocity为初始化的1/160(0.00625),mean[3]是上一状态估计值的长宽比。其中可以对应到kalman fitter的时间更新第一步和时间更新第二步的操作。【mean 和 covariance的计算】
目前,有18个trackers, 12个detections,将会涉及到对tracker进行update的相关操作。【tracker.update(detections)】
- 首先要进行检测结果和跟踪预测结果的匹配级联(Matching Cascade)
步骤如下:
part 1 : 将现有的trackers划分为confirmed_tracks和unconfirmed_tracks两种:
此时18个trackers均是unconfirmed_tracks。confirmed_trackers为空列表。
part 2:针对confirmed_trakers使用外观特征进行级联匹配。
但由于在第二帧,还没有confirmed_trackers,所以该步操作并不会进行。返回的匹配结果均为未匹配的检测框。
part 3:unconfirmed tracks与还没有匹配上的检测结果通过IOU计算的方式进行关联
unmatched_tracks_a并不参与IOU关联的运算,仅用于后续unmatched_tracks的构成。
iou_track_candidates共有18个(源自第一帧建立的track);
unmatched_detections共有12个 (源自第二帧的检测结果)。
首先,计算这些框两两之间的iou值,并通过1-iou得到matrix_cost。
然后,再将cost_matrix中的值大于0.7的,全部置为0.70001。而这些值表示检测框与trackers的IOU较小。
P.S. 该矩阵为(18,12)的矩阵
将cost_matrix矩阵作为匈牙利算法的输入,得到匹配的结果:
从匹配结果上可以看出,第0列是iou_track_candidates的索引(18个), 第1列是unmatched_ detections(12个)的索引。
再对匹配的结果进行进一步的处理: (进一步的关联处理)
Ⅰ. 将仍然没有匹配的检测结果和trackers进行统计。
Ⅱ. 将cost_matrix>0.7的过滤器与追踪器之间的关联去掉。【cost_matrix>0.7,说明二者的IOU小于0.3,即二者之间的关联不大】
Ⅲ. 记录匹配成功的track id 和 detection id
Ⅳ. 最终返回处理后的结果
part 4 将part 2中处理的结果与part 3 中处理的结果进行整合,得到最终的匹配结果和未匹配结果。
- 匹配级联完成之后,更新多目标跟踪器trackers集合
part 1: 针对match的,根据检测的结果更新track的参数。 【Kalman 状态更新过程】
主要包含以下步骤:
Ⅰ. 与SORT一样,更新Kalman滤波中的一系列运动变量、关联次数以及重置time_since_update.
Ⅱ. 将当前帧的features保存到该跟踪器中。
Ⅲ.如果已经连续关联3帧,将该track的状态由tentative改为confirmed。由于此时还没有成功关联3帧,各tracker还是1 tentative的状态。
此时,每个track中将包含2帧中目标的features
part 2: 针对unmatched tracks:
对于未匹配的跟踪器,处理手段主要包含以下两种情况:
Ⅰ. 当tracker处于tentative(试探期)时, 直接将该tracker和其id删除;
Ⅱ. 当tracker处于确认状态,并且self.time_since_update大于max_age(源码中设置的是30),也就是说,如果已确认的tracker 30帧以上未关联成功,就将该tracker即其id删除。
此时,[9, 10, 11, 15, 16, 17]这些id的tracker将会被删除。
part 3: 针对unmatched_detections:
对于未匹配的检测框,要为其创建新的tracker【处于试探期的】。
因为当前帧没有unmatched_detection,所以该操作没有进行。 【该操作与第一帧根据detections 生成tracker的操作相同】
- 更新已确认的距离度量矩阵
由于此时的trackers仍然都处于试探期,所以并不会进行更新。
同样,该帧的trackers都未得到确认,在显示时仅会显示检测框的红色,并且在结果保存时,并不会写到txt中。
*************************************Processing frame 00003******************************************
第三帧中,有14个检测框,12个未确认的tracker(处于tentative状态)。
首先,要对12个未确认的tracker进行Kalman状态估计。然后对每一个tracker的time_since_ update += 1。【更新完成后匹配成功的会置0,不成功的将会进行删除,或者死缓的操作。】
【tracker.predict()】
重点还是放在12个未确认的tracker和14个detections如何匹配,以及tracker如何更新上!【tracker.update()】
一、 第三帧的检测结果与跟踪的预测结果相匹配
step 1: 将现有的trackers划分为已确认和未确认两组。
step 2:首先对已经确认的trackers进行匹配级联。
由于此时没有已经确认的trackers,所以该步依然不会有实质性的操作。返回结果中,匹配的tracker,未匹配的tracker均为空列表[],未匹配的检测框detections为当前的检测结果。
step 3: unconfirmed tracks与还没有匹配上的检测结果通过IOU计算的方式进行关联
unmatched_tracks_a并不参与IOU关联的运算,仅用于后续unmatched_tracks的构成。
此时,unconfirmed_tracks包含12个,未匹配的检测器包含14个。对它们之间进行IOU关联运算。
IOU关联运算,首先计算iou_track_candidates与unmatched_detections两两之间的cost_matrix矩阵。cost_matrix矩阵的计算通过iou计算,再通过1-iou。
然后,将cost_matrix矩阵中>0.7的值全部赋值为0.70001。
再将处理后的cost_matrix输入到匈牙利算法中进行匹配,得到线性的匹配结果indices。左侧为跟踪器的id,右侧为检测器的id。
step 4:对匹配结果进一步处理,获取最终的匹配,未匹配【检测、跟踪】的结果。
首先,对所有的检测结果id 在线性匹配结果indices中遍历,将没有出现的检测结果记录到未匹配检测unmatched_detections中。
其次,对所有跟踪结果id 在线性匹配结果indices中遍历,将没有出现的检测结果记录到未匹配跟踪器unmatched_tracker中。
最后,对cost_matrix中值大于0.7 对应的跟踪id和检测id加入到未匹配的行列中。【因为它们之间的iou太小】 将cost_matrix中值小于0.7对应的跟踪id和检测id结果对加入到匹配的结果中。并返回结果。
这些计算都是基于cost_matrix和匈牙利算法计算的结果indices。
step 5: 对step2 和step4的结果进行整合
第三帧级联匹配的结果:
成功匹配的结果有11个,未匹配的检测结果有3个,未匹配的跟踪器有1个。
二、根据级联匹配的结果更新跟踪器的集合【frame 3】:
1. 状态的变更(试探 确认 删除 1 2 3 )
2. 新跟踪器的创建
step 1:针对matches,根据检测结果更新track参数。(11个)
A. 根据检测结果,更新Kalman参数
主要包括以下步骤:
a. 根据检测的结果更新Kalman状态参数。 主要是计算状态变量x(mean)和Pk(covariance);
b. 将当前帧标定的特征保存到当前的跟踪器中;
c. 更新当前track的状态是否更新。 是否从试探期转化到确认期。(3帧 存在)
此时,有11个track已经经历了3帧,状态将会被修改为确认状态Confirmed,在代码中以2进行保存。
step 2:针对unmatched_tracks (删1个)
对于没有成功匹配的跟踪器,主要由两种情况:
A. 没有匹配的跟踪器是确认状态Confirmed: 30帧的死缓,如果30帧都没有成功关联,则修改其状态为Delete,在代码中以3进行保存。
B. 没有匹配的跟踪器是试探状态Tentative: 修改其状态为Delete,在代码中以3进行保存。
状态表:
在第三帧中,一中最后的结果,将会有1个跟踪器被删除。
step 3: 针对unmatched_detections: (建3个)
需要创建3个新的跟踪器(试探期)。
三、 更新Cosine矩阵【需要保存的特征】
此时,已经包含状态为Confirmed的tracks,便需要对distance metric进行更新。tracks即active_targets中:
A. 对于已经确认的跟踪器,提取器特征集
B. 计算Cosine metric
最多会保存100条特征集合。
四、 可视化及结果的保存
由于此时已经包含了状态为Confirmed的跟踪器,所以在显示的过程中不会仅显示红色的检测框。并且这些已经确认的跟踪器也会保存在txt中。
可以看出,第3帧与前两帧不同的是在更新时,产生了Confirmed状态的跟踪器。增添了Cosine矩阵的计算。同时,在显示时,也增添了 追踪的结果。
*************************************Processing frame 00004******************************************
由于第3帧中,产生了状态为Confirmed trackers,所以在第4帧中,将会增添已确认trackers的级联匹配操作。
关于前3帧相同的操作,这里将会进行简述,只对新增的对Confirmed状态的trackers的级联匹配进行详述。
第4帧中,一共有14个检测框。
根据现有的trackers,对每一个trackers进行kalman跟踪结果预测,也就是时间更新的状态估计。预测完之后,对每一个trackers的self.time_since_update += 1。 其中,有11个是Confirmed 的状态,有3个是tentative状态。
进入14的detections与14个trk的匹配过程。【update】
1. 将现有的跟踪器划分为已确认和未确认两组;
此时,已经存在confirmed_tracks。
2. 对confirmed_tracks与detections进行级联匹配。
前3帧中,由于此时没有confirmed_tracks,所以不进行该过程的实际操作。第4帧中,存在confirmed_tracks,需要对其进行级联匹配,这也是第四帧中新增的一部分。
A. 首先获取处于确认状态的trackers的indices和 检测框的indices,将检测框的indices命名为unmatched_detections:
B. 对已经确认的tracker进行级联匹配
级联匹配是什么呢?为什么会有级联匹配呢?
由于已经确认的tracker存在可能没有匹配到的状态,即处于死缓的tracker。所以需要对那些死缓期间的trackers,需要对几帧没有匹配到的tracker依次进行级联。
所以cascade_depth为30,与死缓的帧数一致。当cascade_depth为0时,即为非死缓的tracker。
track_indices_1列表的作用就是统计不同level的tracker。【正常状态的和不同死缓的】,对他们进行关联操作,可以看出,正常状态-1帧未关联-2帧未关联- .... - 29帧未关联,优先级递减。
关联操作与检测框与未确认的跟踪器的关联操作是一致的。
结果如下:
可以看出,在有已确认的trackers时,基本上已经把detections的结果大多数都关联了。留给没有确认的trackers的detections不多了。这种优胜劣汰的机制也与人的感知十分的神似~
其中,min_cost_matching函数中,包含distance_metric的传入,其调用函数为gated_metric():
其中,包含余弦距离(外观特征)和马氏距离(运动信息)的计算。
余弦距离:
self._metric()函数中调用下述函数实现余弦距离的计算:
3. 对未确认的trackers进行IOU匹配。
匹配结果如下:
4. 获得最终的匹配情况
5. 进行跟踪器集合的更新
A. 状态的转换
B. 新跟踪器的生成
6. 更新现有已确认的跟踪器的x和Pk
7.进行可视化和保存