advcloud commited on
Commit
efe15e5
·
1 Parent(s): 0da82a3

first commit

Browse files
byte_track/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ import sys
2
+ import os
3
+ sys.path.append(os.path.dirname(__file__))
byte_track/bytetracker.py ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .tracker.byte_tracker import BYTETracker
2
+ import cv2
3
+ import numpy as np
4
+
5
+ class ByteTrack(object):
6
+ def __init__(self, detector, min_box_area=10):
7
+ self.min_box_area = min_box_area
8
+
9
+ self.rgb_means = (0.485, 0.456, 0.406)
10
+ self.std = (0.229, 0.224, 0.225)
11
+
12
+ self.detector = detector
13
+ self.input_shape = tuple(detector.model.get_inputs()[0].shape[2:])
14
+ self.tracker = BYTETracker(frame_rate=30)
15
+
16
+ def inference(self, image, conf_thresh=0.25, classes=None):
17
+
18
+ dets, image_info = self.detector.detect(image, conf_thres=conf_thresh, input_shape=self.input_shape, classes=classes)
19
+
20
+ class_ids=[]
21
+ ids=[]
22
+ bboxes=[]
23
+ scores=[]
24
+
25
+ if isinstance(dets, np.ndarray) and len(dets) > 0:
26
+ class_ids = dets[:, -1].tolist()
27
+ bboxes, ids, scores = self._tracker_update(
28
+ dets,
29
+ image_info,
30
+ )
31
+ # image = self.draw_tracking_info(
32
+ # image,
33
+ # bboxes,
34
+ # ids,
35
+ # scores,
36
+ # )
37
+
38
+ # return image, len(bboxes), class_ids
39
+ return bboxes, ids, scores, class_ids
40
+
41
+ def get_id_color(self, index):
42
+ temp_index = abs(int(index)) * 3
43
+ color = ((37 * temp_index) % 255, (17 * temp_index) % 255,
44
+ (29 * temp_index) % 255)
45
+ return color
46
+
47
+ def draw_tracking_info(
48
+ self,
49
+ image,
50
+ tlwhs,
51
+ ids,
52
+ scores,
53
+ frame_id=0,
54
+ elapsed_time=0.,
55
+ ):
56
+ text_scale = 1.5
57
+ text_thickness = 2
58
+ line_thickness = 2
59
+
60
+ # text = 'frame: %d ' % (frame_id)
61
+ # text += 'elapsed time: %.0fms ' % (elapsed_time * 1000)
62
+ # text += 'num: %d' % (len(tlwhs))
63
+ # cv2.putText(
64
+ # image,
65
+ # text,
66
+ # (0, int(15 * text_scale)),
67
+ # cv2.FONT_HERSHEY_PLAIN,
68
+ # 2,
69
+ # (0, 255, 0),
70
+ # thickness=text_thickness,
71
+ # )
72
+
73
+ for index, tlwh in enumerate(tlwhs):
74
+ x1, y1 = int(tlwh[0]), int(tlwh[1])
75
+ x2, y2 = x1 + int(tlwh[2]), y1 + int(tlwh[3])
76
+ color = self.get_id_color(ids[index])
77
+ cv2.rectangle(image, (x1, y1), (x2, y2), color, line_thickness)
78
+
79
+ text = str(ids[index])
80
+ cv2.putText(image, text, (x1, y1 - 5), cv2.FONT_HERSHEY_PLAIN,
81
+ text_scale, (0, 0, 0), text_thickness + 3)
82
+ cv2.putText(image, text, (x1, y1 - 5), cv2.FONT_HERSHEY_PLAIN,
83
+ text_scale, (255, 255, 255), text_thickness)
84
+ return image
85
+
86
+ def _tracker_update(self, dets, image_info):
87
+ online_targets = []
88
+ if dets is not None:
89
+ online_targets = self.tracker.update(
90
+ dets[:, :-1],
91
+ [image_info['height'], image_info['width']],
92
+ [image_info['height'], image_info['width']],
93
+ )
94
+ online_tlwhs = []
95
+ online_ids = []
96
+ online_scores = []
97
+ for online_target in online_targets:
98
+ tlwh = online_target.tlwh
99
+ track_id = online_target.track_id
100
+ vertical = tlwh[2] / tlwh[3] > 1.6
101
+ if tlwh[2] * tlwh[3] > self.min_box_area and not vertical:
102
+ online_tlwhs.append(tlwh)
103
+ online_ids.append(track_id)
104
+ online_scores.append(online_target.score)
105
+
106
+ return online_tlwhs, online_ids, online_scores
byte_track/tracker/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ import sys
2
+ import os
3
+ sys.path.append(os.path.dirname(__file__))
4
+
byte_track/tracker/basetrack.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from collections import OrderedDict
3
+
4
+
5
+ class TrackState(object):
6
+ New = 0
7
+ Tracked = 1
8
+ Lost = 2
9
+ Removed = 3
10
+
11
+
12
+ class BaseTrack(object):
13
+ _count = 0
14
+
15
+ track_id = 0
16
+ is_activated = False
17
+ state = TrackState.New
18
+
19
+ history = OrderedDict()
20
+ features = []
21
+ curr_feature = None
22
+ score = 0
23
+ start_frame = 0
24
+ frame_id = 0
25
+ time_since_update = 0
26
+
27
+ # multi-camera
28
+ location = (np.inf, np.inf)
29
+
30
+ @property
31
+ def end_frame(self):
32
+ return self.frame_id
33
+
34
+ @staticmethod
35
+ def next_id():
36
+ BaseTrack._count += 1
37
+ return BaseTrack._count
38
+
39
+ def activate(self, *args):
40
+ raise NotImplementedError
41
+
42
+ def predict(self):
43
+ raise NotImplementedError
44
+
45
+ def update(self, *args, **kwargs):
46
+ raise NotImplementedError
47
+
48
+ def mark_lost(self):
49
+ self.state = TrackState.Lost
50
+
51
+ def mark_removed(self):
52
+ self.state = TrackState.Removed
byte_track/tracker/byte_tracker.py ADDED
@@ -0,0 +1,326 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from .kalman_filter import KalmanFilter
3
+ import matching
4
+ from .basetrack import BaseTrack, TrackState
5
+
6
+ class STrack(BaseTrack):
7
+ shared_kalman = KalmanFilter()
8
+ def __init__(self, tlwh, score):
9
+
10
+ # wait activate
11
+ self._tlwh = np.asarray(tlwh, dtype=np.float)
12
+ self.kalman_filter = None
13
+ self.mean, self.covariance = None, None
14
+ self.is_activated = False
15
+
16
+ self.score = score
17
+ self.tracklet_len = 0
18
+
19
+ def predict(self):
20
+ mean_state = self.mean.copy()
21
+ if self.state != TrackState.Tracked:
22
+ mean_state[7] = 0
23
+ self.mean, self.covariance = self.kalman_filter.predict(mean_state, self.covariance)
24
+
25
+ @staticmethod
26
+ def multi_predict(stracks):
27
+ if len(stracks) > 0:
28
+ multi_mean = np.asarray([st.mean.copy() for st in stracks])
29
+ multi_covariance = np.asarray([st.covariance for st in stracks])
30
+ for i, st in enumerate(stracks):
31
+ if st.state != TrackState.Tracked:
32
+ multi_mean[i][7] = 0
33
+ multi_mean, multi_covariance = STrack.shared_kalman.multi_predict(multi_mean, multi_covariance)
34
+ for i, (mean, cov) in enumerate(zip(multi_mean, multi_covariance)):
35
+ stracks[i].mean = mean
36
+ stracks[i].covariance = cov
37
+
38
+ def activate(self, kalman_filter, frame_id):
39
+ """Start a new tracklet"""
40
+ self.kalman_filter = kalman_filter
41
+ self.track_id = self.next_id()
42
+ self.mean, self.covariance = self.kalman_filter.initiate(self.tlwh_to_xyah(self._tlwh))
43
+
44
+ self.tracklet_len = 0
45
+ self.state = TrackState.Tracked
46
+ if frame_id == 1:
47
+ self.is_activated = True
48
+ # self.is_activated = True
49
+ self.frame_id = frame_id
50
+ self.start_frame = frame_id
51
+
52
+ def re_activate(self, new_track, frame_id, new_id=False):
53
+ self.mean, self.covariance = self.kalman_filter.update(
54
+ self.mean, self.covariance, self.tlwh_to_xyah(new_track.tlwh)
55
+ )
56
+ self.tracklet_len = 0
57
+ self.state = TrackState.Tracked
58
+ self.is_activated = True
59
+ self.frame_id = frame_id
60
+ if new_id:
61
+ self.track_id = self.next_id()
62
+ self.score = new_track.score
63
+
64
+ def update(self, new_track, frame_id):
65
+ """
66
+ Update a matched track
67
+ :type new_track: STrack
68
+ :type frame_id: int
69
+ :type update_feature: bool
70
+ :return:
71
+ """
72
+ self.frame_id = frame_id
73
+ self.tracklet_len += 1
74
+
75
+ new_tlwh = new_track.tlwh
76
+ self.mean, self.covariance = self.kalman_filter.update(
77
+ self.mean, self.covariance, self.tlwh_to_xyah(new_tlwh))
78
+ self.state = TrackState.Tracked
79
+ self.is_activated = True
80
+
81
+ self.score = new_track.score
82
+
83
+ @property
84
+ # @jit(nopython=True)
85
+ def tlwh(self):
86
+ """Get current position in bounding box format `(top left x, top left y,
87
+ width, height)`.
88
+ """
89
+ if self.mean is None:
90
+ return self._tlwh.copy()
91
+ ret = self.mean[:4].copy()
92
+ ret[2] *= ret[3]
93
+ ret[:2] -= ret[2:] / 2
94
+ return ret
95
+
96
+ @property
97
+ # @jit(nopython=True)
98
+ def tlbr(self):
99
+ """Convert bounding box to format `(min x, min y, max x, max y)`, i.e.,
100
+ `(top left, bottom right)`.
101
+ """
102
+ ret = self.tlwh.copy()
103
+ ret[2:] += ret[:2]
104
+ return ret
105
+
106
+ @staticmethod
107
+ # @jit(nopython=True)
108
+ def tlwh_to_xyah(tlwh):
109
+ """Convert bounding box to format `(center x, center y, aspect ratio,
110
+ height)`, where the aspect ratio is `width / height`.
111
+ """
112
+ ret = np.asarray(tlwh).copy()
113
+ ret[:2] += ret[2:] / 2
114
+ ret[2] /= ret[3]
115
+ return ret
116
+
117
+ def to_xyah(self):
118
+ return self.tlwh_to_xyah(self.tlwh)
119
+
120
+ @staticmethod
121
+ # @jit(nopython=True)
122
+ def tlbr_to_tlwh(tlbr):
123
+ ret = np.asarray(tlbr).copy()
124
+ ret[2:] -= ret[:2]
125
+ return ret
126
+
127
+ @staticmethod
128
+ # @jit(nopython=True)
129
+ def tlwh_to_tlbr(tlwh):
130
+ ret = np.asarray(tlwh).copy()
131
+ ret[2:] += ret[:2]
132
+ return ret
133
+
134
+ def __repr__(self):
135
+ return 'OT_{}_({}-{})'.format(self.track_id, self.start_frame, self.end_frame)
136
+
137
+
138
+ class BYTETracker(object):
139
+ def __init__(self, track_thresh=0.5,match_thresh=0.8, track_buffer=30, mot20=False, frame_rate=30):
140
+ self.tracked_stracks = [] # type: list[STrack]
141
+ self.lost_stracks = [] # type: list[STrack]
142
+ self.removed_stracks = [] # type: list[STrack]
143
+
144
+ self.track_thresh = track_thresh
145
+ self.track_buffer = track_buffer
146
+ self.mot20 = mot20
147
+ self.match_thresh = match_thresh
148
+
149
+ self.frame_id = 0
150
+ self.det_thresh = track_thresh + 0.1
151
+ self.buffer_size = int(frame_rate / 30.0 * self.track_buffer)
152
+ self.max_time_lost = self.buffer_size
153
+ self.kalman_filter = KalmanFilter()
154
+
155
+ def update(self, output_results, img_info, img_size):
156
+ self.frame_id += 1
157
+ activated_starcks = []
158
+ refind_stracks = []
159
+ lost_stracks = []
160
+ removed_stracks = []
161
+
162
+ if output_results.shape[1] == 5:
163
+ scores = output_results[:, 4]
164
+ bboxes = output_results[:, :4]
165
+ else:
166
+ output_results = output_results.cpu().numpy()
167
+ scores = output_results[:, 4] * output_results[:, 5]
168
+ bboxes = output_results[:, :4] # x1y1x2y2
169
+ img_h, img_w = img_info[0], img_info[1]
170
+ scale = min(img_size[0] / float(img_h), img_size[1] / float(img_w))
171
+ bboxes /= scale
172
+
173
+ remain_inds = scores > self.track_thresh
174
+ inds_low = scores > 0.1
175
+ inds_high = scores < self.track_thresh
176
+
177
+ inds_second = np.logical_and(inds_low, inds_high)
178
+ dets_second = bboxes[inds_second]
179
+ dets = bboxes[remain_inds]
180
+ scores_keep = scores[remain_inds]
181
+ scores_second = scores[inds_second]
182
+
183
+ if len(dets) > 0:
184
+ '''Detections'''
185
+ detections = [STrack(STrack.tlbr_to_tlwh(tlbr), s) for
186
+ (tlbr, s) in zip(dets, scores_keep)]
187
+ else:
188
+ detections = []
189
+
190
+ ''' Add newly detected tracklets to tracked_stracks'''
191
+ unconfirmed = []
192
+ tracked_stracks = [] # type: list[STrack]
193
+ for track in self.tracked_stracks:
194
+ if not track.is_activated:
195
+ unconfirmed.append(track)
196
+ else:
197
+ tracked_stracks.append(track)
198
+
199
+ ''' Step 2: First association, with high score detection boxes'''
200
+ strack_pool = joint_stracks(tracked_stracks, self.lost_stracks)
201
+ # Predict the current location with KF
202
+ STrack.multi_predict(strack_pool)
203
+ dists = matching.iou_distance(strack_pool, detections)
204
+ if not self.mot20:
205
+ dists = matching.fuse_score(dists, detections)
206
+ matches, u_track, u_detection = matching.linear_assignment(dists, thresh=self.match_thresh)
207
+
208
+ for itracked, idet in matches:
209
+ track = strack_pool[itracked]
210
+ det = detections[idet]
211
+ if track.state == TrackState.Tracked:
212
+ track.update(detections[idet], self.frame_id)
213
+ activated_starcks.append(track)
214
+ else:
215
+ track.re_activate(det, self.frame_id, new_id=False)
216
+ refind_stracks.append(track)
217
+
218
+ ''' Step 3: Second association, with low score detection boxes'''
219
+ # association the untrack to the low score detections
220
+ if len(dets_second) > 0:
221
+ '''Detections'''
222
+ detections_second = [STrack(STrack.tlbr_to_tlwh(tlbr), s) for
223
+ (tlbr, s) in zip(dets_second, scores_second)]
224
+ else:
225
+ detections_second = []
226
+ r_tracked_stracks = [strack_pool[i] for i in u_track if strack_pool[i].state == TrackState.Tracked]
227
+ dists = matching.iou_distance(r_tracked_stracks, detections_second)
228
+ matches, u_track, u_detection_second = matching.linear_assignment(dists, thresh=0.5)
229
+ for itracked, idet in matches:
230
+ track = r_tracked_stracks[itracked]
231
+ det = detections_second[idet]
232
+ if track.state == TrackState.Tracked:
233
+ track.update(det, self.frame_id)
234
+ activated_starcks.append(track)
235
+ else:
236
+ track.re_activate(det, self.frame_id, new_id=False)
237
+ refind_stracks.append(track)
238
+
239
+ for it in u_track:
240
+ track = r_tracked_stracks[it]
241
+ if not track.state == TrackState.Lost:
242
+ track.mark_lost()
243
+ lost_stracks.append(track)
244
+
245
+ '''Deal with unconfirmed tracks, usually tracks with only one beginning frame'''
246
+ detections = [detections[i] for i in u_detection]
247
+ dists = matching.iou_distance(unconfirmed, detections)
248
+ if not self.mot20:
249
+ dists = matching.fuse_score(dists, detections)
250
+ matches, u_unconfirmed, u_detection = matching.linear_assignment(dists, thresh=0.7)
251
+ for itracked, idet in matches:
252
+ unconfirmed[itracked].update(detections[idet], self.frame_id)
253
+ activated_starcks.append(unconfirmed[itracked])
254
+ for it in u_unconfirmed:
255
+ track = unconfirmed[it]
256
+ track.mark_removed()
257
+ removed_stracks.append(track)
258
+
259
+ """ Step 4: Init new stracks"""
260
+ for inew in u_detection:
261
+ track = detections[inew]
262
+ if track.score < self.det_thresh:
263
+ continue
264
+ track.activate(self.kalman_filter, self.frame_id)
265
+ activated_starcks.append(track)
266
+ """ Step 5: Update state"""
267
+ for track in self.lost_stracks:
268
+ if self.frame_id - track.end_frame > self.max_time_lost:
269
+ track.mark_removed()
270
+ removed_stracks.append(track)
271
+
272
+ # print('Ramained match {} s'.format(t4-t3))
273
+
274
+ self.tracked_stracks = [t for t in self.tracked_stracks if t.state == TrackState.Tracked]
275
+ self.tracked_stracks = joint_stracks(self.tracked_stracks, activated_starcks)
276
+ self.tracked_stracks = joint_stracks(self.tracked_stracks, refind_stracks)
277
+ self.lost_stracks = sub_stracks(self.lost_stracks, self.tracked_stracks)
278
+ self.lost_stracks.extend(lost_stracks)
279
+ self.lost_stracks = sub_stracks(self.lost_stracks, self.removed_stracks)
280
+ self.removed_stracks.extend(removed_stracks)
281
+ self.tracked_stracks, self.lost_stracks = remove_duplicate_stracks(self.tracked_stracks, self.lost_stracks)
282
+ # get scores of lost tracks
283
+ output_stracks = [track for track in self.tracked_stracks if track.is_activated]
284
+
285
+ return output_stracks
286
+
287
+
288
+ def joint_stracks(tlista, tlistb):
289
+ exists = {}
290
+ res = []
291
+ for t in tlista:
292
+ exists[t.track_id] = 1
293
+ res.append(t)
294
+ for t in tlistb:
295
+ tid = t.track_id
296
+ if not exists.get(tid, 0):
297
+ exists[tid] = 1
298
+ res.append(t)
299
+ return res
300
+
301
+
302
+ def sub_stracks(tlista, tlistb):
303
+ stracks = {}
304
+ for t in tlista:
305
+ stracks[t.track_id] = t
306
+ for t in tlistb:
307
+ tid = t.track_id
308
+ if stracks.get(tid, 0):
309
+ del stracks[tid]
310
+ return list(stracks.values())
311
+
312
+
313
+ def remove_duplicate_stracks(stracksa, stracksb):
314
+ pdist = matching.iou_distance(stracksa, stracksb)
315
+ pairs = np.where(pdist < 0.15)
316
+ dupa, dupb = list(), list()
317
+ for p, q in zip(*pairs):
318
+ timep = stracksa[p].frame_id - stracksa[p].start_frame
319
+ timeq = stracksb[q].frame_id - stracksb[q].start_frame
320
+ if timep > timeq:
321
+ dupb.append(q)
322
+ else:
323
+ dupa.append(p)
324
+ resa = [t for i, t in enumerate(stracksa) if not i in dupa]
325
+ resb = [t for i, t in enumerate(stracksb) if not i in dupb]
326
+ return resa, resb
byte_track/tracker/kalman_filter.py ADDED
@@ -0,0 +1,270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # vim: expandtab:ts=4:sw=4
2
+ import numpy as np
3
+ import scipy.linalg
4
+
5
+
6
+ """
7
+ Table for the 0.95 quantile of the chi-square distribution with N degrees of
8
+ freedom (contains values for N=1, ..., 9). Taken from MATLAB/Octave's chi2inv
9
+ function and used as Mahalanobis gating threshold.
10
+ """
11
+ chi2inv95 = {
12
+ 1: 3.8415,
13
+ 2: 5.9915,
14
+ 3: 7.8147,
15
+ 4: 9.4877,
16
+ 5: 11.070,
17
+ 6: 12.592,
18
+ 7: 14.067,
19
+ 8: 15.507,
20
+ 9: 16.919}
21
+
22
+
23
+ class KalmanFilter(object):
24
+ """
25
+ A simple Kalman filter for tracking bounding boxes in image space.
26
+
27
+ The 8-dimensional state space
28
+
29
+ x, y, a, h, vx, vy, va, vh
30
+
31
+ contains the bounding box center position (x, y), aspect ratio a, height h,
32
+ and their respective velocities.
33
+
34
+ Object motion follows a constant velocity model. The bounding box location
35
+ (x, y, a, h) is taken as direct observation of the state space (linear
36
+ observation model).
37
+
38
+ """
39
+
40
+ def __init__(self):
41
+ ndim, dt = 4, 1.
42
+
43
+ # Create Kalman filter model matrices.
44
+ self._motion_mat = np.eye(2 * ndim, 2 * ndim)
45
+ for i in range(ndim):
46
+ self._motion_mat[i, ndim + i] = dt
47
+ self._update_mat = np.eye(ndim, 2 * ndim)
48
+
49
+ # Motion and observation uncertainty are chosen relative to the current
50
+ # state estimate. These weights control the amount of uncertainty in
51
+ # the model. This is a bit hacky.
52
+ self._std_weight_position = 1. / 20
53
+ self._std_weight_velocity = 1. / 160
54
+
55
+ def initiate(self, measurement):
56
+ """Create track from unassociated measurement.
57
+
58
+ Parameters
59
+ ----------
60
+ measurement : ndarray
61
+ Bounding box coordinates (x, y, a, h) with center position (x, y),
62
+ aspect ratio a, and height h.
63
+
64
+ Returns
65
+ -------
66
+ (ndarray, ndarray)
67
+ Returns the mean vector (8 dimensional) and covariance matrix (8x8
68
+ dimensional) of the new track. Unobserved velocities are initialized
69
+ to 0 mean.
70
+
71
+ """
72
+ mean_pos = measurement
73
+ mean_vel = np.zeros_like(mean_pos)
74
+ mean = np.r_[mean_pos, mean_vel]
75
+
76
+ std = [
77
+ 2 * self._std_weight_position * measurement[3],
78
+ 2 * self._std_weight_position * measurement[3],
79
+ 1e-2,
80
+ 2 * self._std_weight_position * measurement[3],
81
+ 10 * self._std_weight_velocity * measurement[3],
82
+ 10 * self._std_weight_velocity * measurement[3],
83
+ 1e-5,
84
+ 10 * self._std_weight_velocity * measurement[3]]
85
+ covariance = np.diag(np.square(std))
86
+ return mean, covariance
87
+
88
+ def predict(self, mean, covariance):
89
+ """Run Kalman filter prediction step.
90
+
91
+ Parameters
92
+ ----------
93
+ mean : ndarray
94
+ The 8 dimensional mean vector of the object state at the previous
95
+ time step.
96
+ covariance : ndarray
97
+ The 8x8 dimensional covariance matrix of the object state at the
98
+ previous time step.
99
+
100
+ Returns
101
+ -------
102
+ (ndarray, ndarray)
103
+ Returns the mean vector and covariance matrix of the predicted
104
+ state. Unobserved velocities are initialized to 0 mean.
105
+
106
+ """
107
+ std_pos = [
108
+ self._std_weight_position * mean[3],
109
+ self._std_weight_position * mean[3],
110
+ 1e-2,
111
+ self._std_weight_position * mean[3]]
112
+ std_vel = [
113
+ self._std_weight_velocity * mean[3],
114
+ self._std_weight_velocity * mean[3],
115
+ 1e-5,
116
+ self._std_weight_velocity * mean[3]]
117
+ motion_cov = np.diag(np.square(np.r_[std_pos, std_vel]))
118
+
119
+ #mean = np.dot(self._motion_mat, mean)
120
+ mean = np.dot(mean, self._motion_mat.T)
121
+ covariance = np.linalg.multi_dot((
122
+ self._motion_mat, covariance, self._motion_mat.T)) + motion_cov
123
+
124
+ return mean, covariance
125
+
126
+ def project(self, mean, covariance):
127
+ """Project state distribution to measurement space.
128
+
129
+ Parameters
130
+ ----------
131
+ mean : ndarray
132
+ The state's mean vector (8 dimensional array).
133
+ covariance : ndarray
134
+ The state's covariance matrix (8x8 dimensional).
135
+
136
+ Returns
137
+ -------
138
+ (ndarray, ndarray)
139
+ Returns the projected mean and covariance matrix of the given state
140
+ estimate.
141
+
142
+ """
143
+ std = [
144
+ self._std_weight_position * mean[3],
145
+ self._std_weight_position * mean[3],
146
+ 1e-1,
147
+ self._std_weight_position * mean[3]]
148
+ innovation_cov = np.diag(np.square(std))
149
+
150
+ mean = np.dot(self._update_mat, mean)
151
+ covariance = np.linalg.multi_dot((
152
+ self._update_mat, covariance, self._update_mat.T))
153
+ return mean, covariance + innovation_cov
154
+
155
+ def multi_predict(self, mean, covariance):
156
+ """Run Kalman filter prediction step (Vectorized version).
157
+ Parameters
158
+ ----------
159
+ mean : ndarray
160
+ The Nx8 dimensional mean matrix of the object states at the previous
161
+ time step.
162
+ covariance : ndarray
163
+ The Nx8x8 dimensional covariance matrics of the object states at the
164
+ previous time step.
165
+ Returns
166
+ -------
167
+ (ndarray, ndarray)
168
+ Returns the mean vector and covariance matrix of the predicted
169
+ state. Unobserved velocities are initialized to 0 mean.
170
+ """
171
+ std_pos = [
172
+ self._std_weight_position * mean[:, 3],
173
+ self._std_weight_position * mean[:, 3],
174
+ 1e-2 * np.ones_like(mean[:, 3]),
175
+ self._std_weight_position * mean[:, 3]]
176
+ std_vel = [
177
+ self._std_weight_velocity * mean[:, 3],
178
+ self._std_weight_velocity * mean[:, 3],
179
+ 1e-5 * np.ones_like(mean[:, 3]),
180
+ self._std_weight_velocity * mean[:, 3]]
181
+ sqr = np.square(np.r_[std_pos, std_vel]).T
182
+
183
+ motion_cov = []
184
+ for i in range(len(mean)):
185
+ motion_cov.append(np.diag(sqr[i]))
186
+ motion_cov = np.asarray(motion_cov)
187
+
188
+ mean = np.dot(mean, self._motion_mat.T)
189
+ left = np.dot(self._motion_mat, covariance).transpose((1, 0, 2))
190
+ covariance = np.dot(left, self._motion_mat.T) + motion_cov
191
+
192
+ return mean, covariance
193
+
194
+ def update(self, mean, covariance, measurement):
195
+ """Run Kalman filter correction step.
196
+
197
+ Parameters
198
+ ----------
199
+ mean : ndarray
200
+ The predicted state's mean vector (8 dimensional).
201
+ covariance : ndarray
202
+ The state's covariance matrix (8x8 dimensional).
203
+ measurement : ndarray
204
+ The 4 dimensional measurement vector (x, y, a, h), where (x, y)
205
+ is the center position, a the aspect ratio, and h the height of the
206
+ bounding box.
207
+
208
+ Returns
209
+ -------
210
+ (ndarray, ndarray)
211
+ Returns the measurement-corrected state distribution.
212
+
213
+ """
214
+ projected_mean, projected_cov = self.project(mean, covariance)
215
+
216
+ chol_factor, lower = scipy.linalg.cho_factor(
217
+ projected_cov, lower=True, check_finite=False)
218
+ kalman_gain = scipy.linalg.cho_solve(
219
+ (chol_factor, lower), np.dot(covariance, self._update_mat.T).T,
220
+ check_finite=False).T
221
+ innovation = measurement - projected_mean
222
+
223
+ new_mean = mean + np.dot(innovation, kalman_gain.T)
224
+ new_covariance = covariance - np.linalg.multi_dot((
225
+ kalman_gain, projected_cov, kalman_gain.T))
226
+ return new_mean, new_covariance
227
+
228
+ def gating_distance(self, mean, covariance, measurements,
229
+ only_position=False, metric='maha'):
230
+ """Compute gating distance between state distribution and measurements.
231
+ A suitable distance threshold can be obtained from `chi2inv95`. If
232
+ `only_position` is False, the chi-square distribution has 4 degrees of
233
+ freedom, otherwise 2.
234
+ Parameters
235
+ ----------
236
+ mean : ndarray
237
+ Mean vector over the state distribution (8 dimensional).
238
+ covariance : ndarray
239
+ Covariance of the state distribution (8x8 dimensional).
240
+ measurements : ndarray
241
+ An Nx4 dimensional matrix of N measurements, each in
242
+ format (x, y, a, h) where (x, y) is the bounding box center
243
+ position, a the aspect ratio, and h the height.
244
+ only_position : Optional[bool]
245
+ If True, distance computation is done with respect to the bounding
246
+ box center position only.
247
+ Returns
248
+ -------
249
+ ndarray
250
+ Returns an array of length N, where the i-th element contains the
251
+ squared Mahalanobis distance between (mean, covariance) and
252
+ `measurements[i]`.
253
+ """
254
+ mean, covariance = self.project(mean, covariance)
255
+ if only_position:
256
+ mean, covariance = mean[:2], covariance[:2, :2]
257
+ measurements = measurements[:, :2]
258
+
259
+ d = measurements - mean
260
+ if metric == 'gaussian':
261
+ return np.sum(d * d, axis=1)
262
+ elif metric == 'maha':
263
+ cholesky_factor = np.linalg.cholesky(covariance)
264
+ z = scipy.linalg.solve_triangular(
265
+ cholesky_factor, d.T, lower=True, check_finite=False,
266
+ overwrite_b=True)
267
+ squared_maha = np.sum(z * z, axis=0)
268
+ return squared_maha
269
+ else:
270
+ raise ValueError('invalid distance metric')
byte_track/tracker/matching.py ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import scipy
3
+ import lap
4
+ from scipy.spatial.distance import cdist
5
+ from cython_bbox import bbox_overlaps as bbox_ious
6
+ import kalman_filter
7
+
8
+ def merge_matches(m1, m2, shape):
9
+ O,P,Q = shape
10
+ m1 = np.asarray(m1)
11
+ m2 = np.asarray(m2)
12
+
13
+ M1 = scipy.sparse.coo_matrix((np.ones(len(m1)), (m1[:, 0], m1[:, 1])), shape=(O, P))
14
+ M2 = scipy.sparse.coo_matrix((np.ones(len(m2)), (m2[:, 0], m2[:, 1])), shape=(P, Q))
15
+
16
+ mask = M1*M2
17
+ match = mask.nonzero()
18
+ match = list(zip(match[0], match[1]))
19
+ unmatched_O = tuple(set(range(O)) - set([i for i, j in match]))
20
+ unmatched_Q = tuple(set(range(Q)) - set([j for i, j in match]))
21
+
22
+ return match, unmatched_O, unmatched_Q
23
+
24
+
25
+ def _indices_to_matches(cost_matrix, indices, thresh):
26
+ matched_cost = cost_matrix[tuple(zip(*indices))]
27
+ matched_mask = (matched_cost <= thresh)
28
+
29
+ matches = indices[matched_mask]
30
+ unmatched_a = tuple(set(range(cost_matrix.shape[0])) - set(matches[:, 0]))
31
+ unmatched_b = tuple(set(range(cost_matrix.shape[1])) - set(matches[:, 1]))
32
+
33
+ return matches, unmatched_a, unmatched_b
34
+
35
+
36
+ def linear_assignment(cost_matrix, thresh):
37
+ if cost_matrix.size == 0:
38
+ return np.empty((0, 2), dtype=int), tuple(range(cost_matrix.shape[0])), tuple(range(cost_matrix.shape[1]))
39
+ matches, unmatched_a, unmatched_b = [], [], []
40
+ cost, x, y = lap.lapjv(cost_matrix, extend_cost=True, cost_limit=thresh)
41
+ for ix, mx in enumerate(x):
42
+ if mx >= 0:
43
+ matches.append([ix, mx])
44
+ unmatched_a = np.where(x < 0)[0]
45
+ unmatched_b = np.where(y < 0)[0]
46
+ matches = np.asarray(matches)
47
+ return matches, unmatched_a, unmatched_b
48
+
49
+
50
+ def ious(atlbrs, btlbrs):
51
+ """
52
+ Compute cost based on IoU
53
+ :type atlbrs: list[tlbr] | np.ndarray
54
+ :type atlbrs: list[tlbr] | np.ndarray
55
+
56
+ :rtype ious np.ndarray
57
+ """
58
+ ious = np.zeros((len(atlbrs), len(btlbrs)), dtype=np.float)
59
+ if ious.size == 0:
60
+ return ious
61
+
62
+ ious = bbox_ious(
63
+ np.ascontiguousarray(atlbrs, dtype=np.float),
64
+ np.ascontiguousarray(btlbrs, dtype=np.float)
65
+ )
66
+
67
+ return ious
68
+
69
+
70
+ def iou_distance(atracks, btracks):
71
+ """
72
+ Compute cost based on IoU
73
+ :type atracks: list[STrack]
74
+ :type btracks: list[STrack]
75
+
76
+ :rtype cost_matrix np.ndarray
77
+ """
78
+
79
+ if (len(atracks)>0 and isinstance(atracks[0], np.ndarray)) or (len(btracks) > 0 and isinstance(btracks[0], np.ndarray)):
80
+ atlbrs = atracks
81
+ btlbrs = btracks
82
+ else:
83
+ atlbrs = [track.tlbr for track in atracks]
84
+ btlbrs = [track.tlbr for track in btracks]
85
+ _ious = ious(atlbrs, btlbrs)
86
+ cost_matrix = 1 - _ious
87
+
88
+ return cost_matrix
89
+
90
+ def v_iou_distance(atracks, btracks):
91
+ """
92
+ Compute cost based on IoU
93
+ :type atracks: list[STrack]
94
+ :type btracks: list[STrack]
95
+
96
+ :rtype cost_matrix np.ndarray
97
+ """
98
+
99
+ if (len(atracks)>0 and isinstance(atracks[0], np.ndarray)) or (len(btracks) > 0 and isinstance(btracks[0], np.ndarray)):
100
+ atlbrs = atracks
101
+ btlbrs = btracks
102
+ else:
103
+ atlbrs = [track.tlwh_to_tlbr(track.pred_bbox) for track in atracks]
104
+ btlbrs = [track.tlwh_to_tlbr(track.pred_bbox) for track in btracks]
105
+ _ious = ious(atlbrs, btlbrs)
106
+ cost_matrix = 1 - _ious
107
+
108
+ return cost_matrix
109
+
110
+ def embedding_distance(tracks, detections, metric='cosine'):
111
+ """
112
+ :param tracks: list[STrack]
113
+ :param detections: list[BaseTrack]
114
+ :param metric:
115
+ :return: cost_matrix np.ndarray
116
+ """
117
+
118
+ cost_matrix = np.zeros((len(tracks), len(detections)), dtype=np.float)
119
+ if cost_matrix.size == 0:
120
+ return cost_matrix
121
+ det_features = np.asarray([track.curr_feat for track in detections], dtype=np.float)
122
+ #for i, track in enumerate(tracks):
123
+ #cost_matrix[i, :] = np.maximum(0.0, cdist(track.smooth_feat.reshape(1,-1), det_features, metric))
124
+ track_features = np.asarray([track.smooth_feat for track in tracks], dtype=np.float)
125
+ cost_matrix = np.maximum(0.0, cdist(track_features, det_features, metric)) # Nomalized features
126
+ return cost_matrix
127
+
128
+
129
+ def gate_cost_matrix(kf, cost_matrix, tracks, detections, only_position=False):
130
+ if cost_matrix.size == 0:
131
+ return cost_matrix
132
+ gating_dim = 2 if only_position else 4
133
+ gating_threshold = kalman_filter.chi2inv95[gating_dim]
134
+ measurements = np.asarray([det.to_xyah() for det in detections])
135
+ for row, track in enumerate(tracks):
136
+ gating_distance = kf.gating_distance(
137
+ track.mean, track.covariance, measurements, only_position)
138
+ cost_matrix[row, gating_distance > gating_threshold] = np.inf
139
+ return cost_matrix
140
+
141
+
142
+ def fuse_motion(kf, cost_matrix, tracks, detections, only_position=False, lambda_=0.98):
143
+ if cost_matrix.size == 0:
144
+ return cost_matrix
145
+ gating_dim = 2 if only_position else 4
146
+ gating_threshold = kalman_filter.chi2inv95[gating_dim]
147
+ measurements = np.asarray([det.to_xyah() for det in detections])
148
+ for row, track in enumerate(tracks):
149
+ gating_distance = kf.gating_distance(
150
+ track.mean, track.covariance, measurements, only_position, metric='maha')
151
+ cost_matrix[row, gating_distance > gating_threshold] = np.inf
152
+ cost_matrix[row] = lambda_ * cost_matrix[row] + (1 - lambda_) * gating_distance
153
+ return cost_matrix
154
+
155
+
156
+ def fuse_iou(cost_matrix, tracks, detections):
157
+ if cost_matrix.size == 0:
158
+ return cost_matrix
159
+ reid_sim = 1 - cost_matrix
160
+ iou_dist = iou_distance(tracks, detections)
161
+ iou_sim = 1 - iou_dist
162
+ fuse_sim = reid_sim * (1 + iou_sim) / 2
163
+ det_scores = np.array([det.score for det in detections])
164
+ det_scores = np.expand_dims(det_scores, axis=0).repeat(cost_matrix.shape[0], axis=0)
165
+ #fuse_sim = fuse_sim * (1 + det_scores) / 2
166
+ fuse_cost = 1 - fuse_sim
167
+ return fuse_cost
168
+
169
+
170
+ def fuse_score(cost_matrix, detections):
171
+ if cost_matrix.size == 0:
172
+ return cost_matrix
173
+ iou_sim = 1 - cost_matrix
174
+ det_scores = np.array([det.score for det in detections])
175
+ det_scores = np.expand_dims(det_scores, axis=0).repeat(cost_matrix.shape[0], axis=0)
176
+ fuse_sim = iou_sim * det_scores
177
+ fuse_cost = 1 - fuse_sim
178
+ return fuse_cost