只使用 FRCNN 视频序列
图像信息: {'file_name': 'MOT17-02-FRCNN/img1/000001.jpg', 'id': 1, 'frame_id': 1, 'prev_image_id': -1, 'next_image_id': 2, 'video_id': 1, 'height': 1080, 'width': 1920}
image_cnt + i + 1
,其中 image_cnt
在本序列中始终保持一个值,就是一个视频序列的帧数annotations: 只保留 人 的信息,其它物体的去掉。 {'id': 3851, 'category_id': 1, 'image_id': 159, 'track_id': 7, 'bbox': [1000.0, 445.0, 36.0, 99.0], 'conf': 1.0, 'iscrowd': 0, 'area': 3564.0}
object
在整个数据集中的序号gt.txt
文件中,原本类别是非静态的行人为1,其余人是-1,其他事物过滤image_cnt + frame_id
,其中 image_cnt
在本序列中始终保持一个值,就是一个视频序列的帧数 category_id = 1 # pedestrian(non-static)
if not track_id == tid_last:
tid_curr += 1
tid_last = track_id
[x, y, w, h]
进一步理解
273271,c9db000d5146c15.jpg
逗号前的是 id
{'file_name': '273271,c9db000d5146c15.jpg', 'id': 1, 'height': 1080, 'width': 1920}
1080
1920
{'id': 1, 'category_id': 1, 'image_id': 1, 'track_id': -1, 'bbox_vis': [72, 202, 163, 398], 'bbox': [72, 202, 163, 503], 'area': 81989, 'iscrowd': 0}
-1
,恒为 -1 。因为这个数据是用来做检测人的,没有轨迹信息get_loader
python3 tools/mix_data_ablation.py
,合成在一个 json
文件中
CrowdHuman
和 MOT17 half train
训练,在 MOT17 half val
上测试json
包括 images
、annotations
、videos
、categories
四个部分
{'file_name': 'MOT17-02-FRCNN/img1/000001.jpg', 'id': 1, 'frame_id': 1, 'prev_image_id': -1, 'next_image_id': 2, 'video_id': 1, 'height': 1080, 'width': 1920}
{'id': 302, 'category_id': 1, 'image_id': 1, 'track_id': 1, 'bbox': [1338.0, 418.0, 167.0, 379.0], 'conf': 1.0, 'iscrowd': 0, 'area': 63293.0}
[{'id': 1, 'file_name': 'MOT17-02-FRCNN'}, {'id': 2, 'file_name': 'MOT17-04-FRCNN'}, {'id': 3, 'file_name': 'MOT17-05-FRCNN'}, {'id': 4, 'file_name': 'MOT17-09-FRCNN'}, {'id': 5, 'file_name': 'MOT17-10-FRCNN'}, {'id': 6, 'file_name': 'MOT17-11-FRCNN'}, {'id': 7, 'file_name': 'MOT17-13-FRCNN'}]
[{'id': 1, 'name': 'pedestrian'}]
frame_id
:就是这个数据集中的图像从1开始、prev_image_id
:原先的 id+10000、next_image_id
:原先的 id+10000、video_id
:10 ;修改 id
:原先的 id+10000。prev_image_id
、next_image_id
和 id
是一样的 (MOT17 是不同的,例如 id: 201, frame_id: 201, prev_image_id: 200, next_image_id: 202)ann['id'] = ann['id'] + max_ann
(对象 ID 变换),ann['image_id'] = ann['image_id'] + max_img
(图像ID 变换)800*1440(h*w)
cv2.imread()
读取图像padded_img
r = min(input_size[0] / img.shape[0], input_size[1] / img.shape[1])
,取最小缩放边缩放图像padded_img
,brg->rgb
255.0
,减去 mean=[0.485, 0.456, 0.406]
,除以方差 std=[0.229, 0.224, 0.225]
(PS: 在最新的yolox中已经没有均值和方差了)hwc->chw
[1, 23625, 6]
,经过后处理,输出是[N, 7]
更新 tracker,获得当前帧活跃的 tracker (包括新的 tracker)使用
tracker/byte_tracker.py
BYTETracker 类中 update 函数
BYTETracker 类时用来更新每一帧的 tracker,STrack 是初始化每个 tracker 的类
注意区分 tracker 的种类和状态两个概念
先列举下所有种类和状态的 tracker,5 种
tracker 的种类:0-新的、1-正常的、2-丢失的、3-删除的, 由 state
属性标记
class TrackState(object):
New = 0
Tracked = 1
Lost = 2
Removed = 3
tracker 的状态:激活、休眠(未激活),由 is_activated
属性标记
新的检测目标 初始化为 tracker,state=0,is_activated =False
# 一个 tracker实例的基本类(包含 tracker 的基本属性)
class BaseTrack(object):
_count = 0
track_id = 0
is_activated = False
state = TrackState.New
history = OrderedDict()
features = []
curr_feature = None
score = 0
start_frame = 0
frame_id = 0
time_since_update = 0
# STrack 类,用来初始化每个 tracker 实例
class STrack(BaseTrack):
shared_kalman = KalmanFilter()
def __init__(self, tlwh, score):
# wait activate
self._tlwh = np.asarray(tlwh, dtype=np.float)
self.kalman_filter = None
self.mean, self.covariance = None, None
self.is_activated = False
self.score = score
self.tracklet_len = 0
activated_starcks = [] # 激活列表
refind_stracks = [] # 重新列表
lost_stracks = [] # 丢失列表
removed_stracks = [] # 删除列表
筛选当前帧的检测结果
# 0.5 高分检测框,score 是 cls_conf*obj_conf
remain_inds = scores > self.args.track_thresh
inds_low = scores > 0.1 # 低分检测框最小值
inds_high = scores < self.args.track_thresh # 低分检测框最大值
inds_second = np.logical_and(inds_low, inds_high) # 0.1
# 高分检测框以及置信度(cls_conf*obj_conf)
dets = bboxes[remain_inds]
scores_keep = scores[remain_inds]
# 低分检测框以及置信度(cls_conf*obj_conf)
dets_second = bboxes[inds_second]
scores_second = scores[inds_second]
区分上一帧中休眠和激活两种状态的 tracker
self.tracked_stracks
保存的是(上一帧)正常的(激活) tracker以及前一帧新的tracker(休眠)unconfirmed = []
tracked_stracks = [] # type: list[STrack]
for track in self.tracked_stracks:
if not track.is_activated:
unconfirmed.append(track) # 休眠状态的trackers / 新的
else:
tracked_stracks.append(track) # 优先关联激活状态的(正常的)trackers
第一次关联,使用高分检测框
这里关联的(上一帧) trackers(状态是激活的),
新的、正常的、丢失的删除的都可以和高分检测框 匹配(没有新的、删除的,注意这里丢失的trackers也会关联)
若匹配到正常的tracker,则更新位置,放入 activated_starcks 列表
若匹配到丢失的tracker,则将种类从丢失改为正常,状态改为激活,放入 refind_stracks 列表
与第二次关联的区别
if len(dets) > 0:
'''Detections'''
# 将检测框初始化为 tracker
# bbox和score;均值、方差为None、is_activated=False、tracklet_len=0、state=0
detections = [STrack(STrack.tlbr_to_tlwh(tlbr), s) for (tlbr, s) in zip(dets, scores_keep)]
else:
detections = []
''' Step 2: First association, with high score detection boxes'''
# 将正常的 tracker 和 已丢失的 tracker,根据 tracker_id 放在一起
strack_pool = joint_stracks(tracked_stracks, self.lost_stracks)
# Predict the current location with KF,更新均值和方差
# 卡尔曼预测,例 len(strack_pool)=17
STrack.multi_predict(strack_pool)
# 计算trackers和当前(高分)检测框的iou,然后 dists=1-iou,即iou越大dsits中的值越小,代价矩阵。例len(detections)=20,dists.shape=[17, 20]
dists = matching.iou_distance(strack_pool, detections)
if not self.args.mot20:
# dists=1-(iou 和 当前检测框的置信度相乘),先把score(20,)repeat成(17, 20)
# 确保代价函数更可信?
dists = matching.fuse_score(dists, detections)
# 仅根据代价函数进行数据关联,match_thresh=0.8,匈牙利&KM。
# matches.shape=[17,2], u_track:[], u_detection:[16,18,19]
matches, u_track, u_detection = matching.linear_assignment(dists, thresh=self.args.match_thresh)
# 根据匹配到的检测框,更新参数
for itracked, idet in matches:
track = strack_pool[itracked]
det = detections[idet]
if track.state == TrackState.Tracked:
# 正常的(state=1) trackers 直接使用 det 更新
# 更新 tracklet_len,frame_id,坐标,置信度,卡尔曼的均值和方差,state=1,is_activated=True,track_id 不变
track.update(detections[idet], self.frame_id)
activated_starcks.append(track) # 放入激活列表
else:
# 若不是正常tracker(这里只有丢失的),丢失的tracker根据det更新参数
# tracklet_len=0,frame_id,坐标,置信度,卡尔曼的均值和方差,state=1,is_activated=True,track_id 不变
track.re_activate(det, self.frame_id, new_id=False)
refind_stracks.append(track) # 放入重新列表
第二次关联,与低分数检测框
将上一轮 strack_pool 中没有匹配到的trackers:
u_track
,与低分检测框(detections_second)匹。strack_pool 中是上一帧的正常的,激活状态的 tracker,以及 丢失的,激活的tracker
注意: u_track 会有一个种类过滤,state=1(正常的tracker) 的才会在第二次关联中匹配。丢失的(不会在第二轮中匹配,与第一次关联的区别
注意:新的、删除的,本来就不在 strack_pool,从而不会在u_track
中
''' Step 3: Second association, with low score detection boxes'''
if len(dets_second) > 0:
'''Detections'''
detections_second = [STrack(STrack.tlbr_to_tlwh(tlbr), s) for (tlbr, s) in zip(dets_second, scores_second)]
else:
detections_second = []
# u_track 高分检测框没有匹配到的 trackers,这里有一个tracker的种类过滤
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_second)
matches, u_track, u_detection_second = matching.linear_assignment(dists, thresh=0.5)
for itracked, idet in matches:
track = r_tracked_stracks[itracked]
det = detections_second[idet]
if track.state == TrackState.Tracked:
track.update(det, self.frame_id)
activated_starcks.append(track)
else: # 这个 else 分支应该多余了
track.re_activate(det, self.frame_id, new_id=False)
refind_stracks.append(track)
高低分检测结果关联都没有匹配到的(上一帧的)tracker(正常、丢失两种),其种类设为 丢失
丢失的tracker,种类 设为丢失种类(状态没有变,还是激活状态),添加到丢失列表 lost_stracks
后续若有一帧匹配到了,就重新激活,若 30帧内,没有匹配到则设为删除种类
for it in u_track:
track = r_tracked_stracks[it]
if not track.state == TrackState.Lost:
# 若不是丢失种类的tracker,改变种类,放入 lost_stracks
track.mark_lost()
lost_stracks.append(track)
更新(上一帧的)新的,休眠状态的 tracker,与 没有匹配到 tracker 的 高分检测框关联
休眠状态的 tracker,是上一帧没有匹配到任何 tracker 的高分检测框(一个全新的tracker,tracker 初始化
is_activate=False
),因为只有连续两帧匹配到,才会设置is_activate=True
,即状态为激活
若匹配到就更新,加入到 activated_starcks 列表
没有匹配到直接设为删除种类,加入删除列表。注意此种 tracker 种类为删除,状态是未激活。还有连续30帧没有匹配到,种类为删除,状态是激活。
# 第一次关联没有匹配到的高分检测框
detections = [detections[i] for i in u_detection]
# 与休眠状态的tracker匹配
dists = matching.iou_distance(unconfirmed, detections)
if not self.args.mot20:
dists = matching.fuse_score(dists, detections)
matches, u_unconfirmed, u_detection = matching.linear_assignment(dists, thresh=0.7)
for itracked, idet in matches:
# 匹配到休眠的 tracker,将其参数和状态更新,放入到激活列表中
unconfirmed[itracked].update(detections[idet], self.frame_id)
activated_starcks.append(unconfirmed[itracked])
for it in u_unconfirmed:
# 没有关联的tracker,种类设为3(删除种类),放入删除列表
track = unconfirmed[it]
track.mark_removed()
removed_stracks.append(track)
初始化新的tracker,处理没有匹配到 任何 tracker的 高分检测框,作为新的tracker。
注意
state
是 1(正常种类),is_activated
设为 False,即状态是未激活,放入到激活列表 activated_starcks
只有处理全视频第一帧图像时,新的 tracker状态才被设为激活
等到下一帧匹配到检测框才被设为激活状态,即连续两帧都检测并且匹配到
# 对没有匹配到的检测框判断,<0.6 的过滤掉,>0.6 更新 tracker_id,均值、方差,状态,is_activate=False
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)
更新上一帧丢失种类的tracker,当前帧与此tracker最近帧超过 30帧,设为删除种类,加入删除列表
# lost_stracks 中记载的是丢失的traker,如果大于 30 帧,则设为删除种类
for track in self.lost_stracks:
# 有的丢失 tracker 在第一次关联中已经匹配到了,因此 end_frame 已经更新
if self.frame_id - track.end_frame > self.max_time_lost:
track.mark_removed()
removed_stracks.append(track) # 放入删除列表
整合与更新
activated_starcks[]
:包含正常种类,激活状态的tracker;包含正常的,未激活状态的(新的)tracker
refind_stracks[]
:包含正常的,激活状态的tracker(丢失又捡回来的tracker)
lost_stracks[]
:包含丢失的,激活状态的 tracker
removed_stracks[]
:包含删除的,激活状态的tracker;包含删除的;未激活状态的(新的)tracker
self.tracked_stracks
:包含正常的,激活状态的tracker、包含正常的;未激活状态的(新的)trcker;
self.lost_stracks
:包含丢失的,激活状态的tracker
self.removed_stracks
:包含删除的,激活状态的tracker
# 根据当前帧的匹配结果,过滤前一帧 (正常种类)tracker,包括(上一帧)新的、正常的,tracked_stracks 中若在当前帧匹配到了,state 必会设为1(正常种类)
self.tracked_stracks = [t for t in self.tracked_stracks if t.state == TrackState.Tracked]
# 上一步(过滤的)tracked_stracks 和 当前的激活列表(主要是当前帧产生的新的 tracker)
self.tracked_stracks = joint_stracks(self.tracked_stracks, activated_starcks)
# 上一步 tracked_stracks 和 当前的重新列表(主要是丢失的又重新匹配的)
self.tracked_stracks = joint_stracks(self.tracked_stracks, refind_stracks)
# self.lost_stracks 里是上一帧判定为丢失的 trackers,在经过前面的处理,有可能放入了重新列表和删除列表
# 返回 self.lost_stracks -(self.lost_stracks and self.tracked_stracks)的 tracker,这一步就是将重新列表里的tracker从 self.lost_stracks 删除
self.lost_stracks = sub_stracks(self.lost_stracks, self.tracked_stracks)
# 合并self.lost_stracks 和 当前帧的新的丢失的 tracker
self.lost_stracks.extend(lost_stracks)
# 返回 self.lost_stracks -(self.lost_stracks and removed_stracks)的 tracker
self.lost_stracks = sub_stracks(self.lost_stracks, self.removed_stracks)
# 合并以前帧和当前帧删除类型的 traker
self.removed_stracks.extend(removed_stracks)
# 筛选出 tracked_stracks 和 lost_stracks iou>0.85 的,然后比较时间跨度,时间跨度小的 tracker 去掉
# 就是去重,在 tracked_stracks 和 lost_stracks 中,将iou大的选择一个时间久的tracker保留
self.tracked_stracks, self.lost_stracks = remove_duplicate_stracks(self.tracked_stracks, self.lost_stracks)
# 输出,当前帧激活状态的tracker,新的、丢失的、删除的都不会输出
output_stracks = [track for track in self.tracked_stracks if track.is_activated]
总结
removed_stracks[]
activated_starcks[]
refind_stracks[]
lost_stracks[]
会有一个 垂直判断
vertical = tlwh[2] / tlwh[3] > 1.6
,为 False 时才会算作最后目标
这个是行人的标准,若训练自己的数据,需要根据实际修改,否则容易出现漏跟踪!
online_targets = tracker.update(outputs[0], [img_info['height'], img_info['width']], exp.test_size)
online_tlwhs = []
online_ids = []
online_scores = []
for t in online_targets:
tlwh = t.tlwh
tid = t.track_id
vertical = tlwh[2] / tlwh[3] > 1.6 # False 才会最终入选
if tlwh[2] * tlwh[3] > args.min_box_area and not vertical:
online_tlwhs.append(tlwh)
online_ids.append(tid)
online_scores.append(t.score)
results.append((frame_id + 1, online_tlwhs, online_ids, online_scores))
代码中评估使用的是 MOT17 中的 train
,data loader 使用的和训练检测模型一样,评估使用的是 yolox/evaluators/mot_evaluator.py
中的 MOTEvaluator 类,调用其 evaluate() 函数
步骤
results.append((frame_id, online_tlwhs, online_ids, online_scores))
save_format = '{frame},{id},{x1},{y1},{w},{h},{s},-1,-1,-1\n'
val_half.hson
的 cocoGtpy-motmetrics库
进行计算,指标和代码,指标都是根据标准库来计算的,就是一个视频序列对应一个
gt.txt
和p.txt
gt
的标注的标准格式如上所示,参考 mot17
,读入 pandas 是[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VZ7bpboY-1638276570201)(./1638274177722.png)]
COCO JSON 的 annotations 格式
for ind in range(bboxes.shape[0]):
label = self.dataloader.dataset.class_ids[int(cls[ind])]
pred_data = {
"image_id": int(img_id),
"category_id": label,
"bbox": bboxes[ind].numpy().tolist(), # xywh
"score": scores[ind].numpy().item(),
"segmentation": [],
} # COCO json format
data_list.append(pred_data)
coco.loadRes
函数,最终搞成 标准的 COCO JSNO 格式
val_half.json
的图像信息)(这个图像的 image id 不是从 1 开始的)
{'file_name': 'MOT17-02-FRCNN/img1/000302.jpg', 'id': 302, 'frame_id': 1, 'prev_image_id': 301, 'next_image_id': 303, 'video_id': 1, 'height': 1080, 'width': 1920}
xywh2xyxy
、增加 area
、id
、iscrowd=0
属性