Skip to content

Commit 16f9b09

Browse files
committed
Add New Tracker
1 parent e8cb7e4 commit 16f9b09

File tree

6 files changed

+220
-33
lines changed

6 files changed

+220
-33
lines changed

motrackers/__init__.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
"""
2-
Multi Object Trackers
2+
Multi-object Trackers in Python
33
44
Author: Aditya M. Deshpande
5-
6-
Website: http://adipandas.github.io/
5+
Blog: http://adipandas.github.io/
6+
Github: adipandas
77
"""
88

99
from motrackers.simple_tracker import SimpleTracker
1010
from motrackers.simple_tracker2 import SimpleTracker2
11+
from motrackers.iou_tracker import IOUTracker

motrackers/iou_tracker.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"""
2+
Implementation of this algorithm is heavily based on the following:
3+
https://github.com/bochinski/iou-tracker
4+
"""
5+
6+
import numpy as np
7+
from motrackers.utils import get_centroids, iou
8+
from motrackers import SimpleTracker2
9+
10+
11+
class IOUTracker(SimpleTracker2):
12+
def __init__(self, max_lost=2, iou_threshold=0.5, min_detection_confidence=0.4, max_detection_confidence=0.7):
13+
self.iou_threshold = iou_threshold
14+
self.max_detection_confidence = max_detection_confidence
15+
self.min_detection_confidence = min_detection_confidence
16+
super(IOUTracker, self).__init__(max_lost=max_lost)
17+
18+
def update(self, bboxes: list, class_ids: list, detection_scores: list):
19+
"""
20+
Update the tracker based on the new bboxes as input.
21+
22+
Parameters
23+
----------
24+
bboxes : list
25+
List of bounding boxes detected in the current frame/timestep. Each element of the list represent
26+
coordinates of bounding box as tuple (top-left-x, top-left-y, bottom-right-x, bottom-right-y).
27+
class_ids : list
28+
List of class_ids (int) corresponding to labels of the detected object. Default is `None`.
29+
detection_scores: list
30+
List of detection scores / probability of each detected object or objectness.
31+
32+
Returns
33+
-------
34+
outputs : list
35+
List of tracks being currently tracked by the tracker.
36+
Each element of this list contains the tuple in
37+
format (frame#, trackid, class_id, centroid, bbox, info_dict).
38+
class_id is the id for label of the detection.
39+
centroid represents the pixel coordinates of the centroid of bounding box, i.e., (x, y).
40+
bbox is the bounding box coordinates as (x_top_left, y_top_left, x_bottom_right, y_bottom_right).
41+
info_dict is the dictionary of information which may be useful from the tracker (example:
42+
number of times tracker was lost while tracking.).
43+
44+
"""
45+
46+
assert len(bboxes) == len(class_ids), "Containers must be of same length. len(bboxes)={}," \
47+
" len(class_ids)={}".format(len(bboxes), len(class_ids))
48+
49+
assert len(bboxes) == len(detection_scores), "Containers must be of same length. len(bboxes)={}," \
50+
" len(class_ids)={}".format(len(bboxes), len(detection_scores))
51+
52+
self.frame_count += 1
53+
54+
new_bboxes = np.array(bboxes, dtype='int')
55+
new_class_ids = np.array(class_ids, dtype='int')
56+
new_detection_scores = np.array(detection_scores)
57+
58+
new_centroids = get_centroids(new_bboxes)
59+
60+
new_detections = list(zip(
61+
range(len(bboxes)), new_bboxes, new_class_ids, new_centroids, new_detection_scores
62+
))
63+
64+
track_ids = list(self.tracks.keys())
65+
66+
updated_tracks = []
67+
for track_id in track_ids:
68+
if len(new_detections) > 0:
69+
idx, bb, cid, ctrd, scr = max(new_detections, key=lambda x: iou(self.tracks[track_id].bbox, x[1]))
70+
71+
if iou(self.tracks[track_id].bbox, bb) > self.iou_threshold and self.tracks[track_id].class_id == cid:
72+
max_score = max(self.tracks[track_id].info['max_score'], scr)
73+
self._update_track(track_id, ctrd, bb, score=scr, max_score=max_score)
74+
75+
updated_tracks.append(track_id)
76+
77+
del new_detections[idx]
78+
79+
if len(updated_tracks) == 0 or track_id is not updated_tracks[-1]:
80+
self.tracks[track_id].lost += 1
81+
82+
if self.tracks[track_id].lost > self.max_lost and \
83+
self.tracks[track_id].info['max_score'] >= self.max_detection_confidence:
84+
self._remove_track(track_id)
85+
86+
for idx, bb, cid, ctrd, scr in new_detections:
87+
self._add_track(ctrd, bb, cid, score=scr, max_score=scr)
88+
89+
outputs = self._get_tracks(self.tracks)
90+
return outputs

motrackers/simple_tracker2.py

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from collections import OrderedDict
22
import numpy as np
33
from scipy.spatial import distance
4-
from motrackers.utils.misc import get_centroid
4+
from motrackers.utils.misc import get_centroids
55
from motrackers.track import Track
66

77

@@ -24,7 +24,7 @@ def __init__(self, max_lost=5):
2424
self.max_lost = max_lost
2525
self.frame_count = 0
2626

27-
def _add_object(self, centroid, bbox, class_id):
27+
def _add_track(self, centroid, bbox, class_id, **kwargs):
2828
"""
2929
Add a newly detected object to the queue
3030
@@ -46,6 +46,9 @@ class label
4646
centroid=centroid,
4747
bbox=bbox,
4848
class_id=class_id)
49+
for key, value in kwargs.items():
50+
self.tracks[self.next_track_id].info[key] = value
51+
4952
self.next_track_id += 1
5053

5154
def _remove_track(self, track_id):
@@ -63,7 +66,14 @@ def _remove_track(self, track_id):
6366
"""
6467
del self.tracks[track_id]
6568

66-
def get_tracks(self, tracks):
69+
def _update_track(self, track_id, centroid, bbox, **kwargs):
70+
self.tracks[track_id].centroid = centroid
71+
self.tracks[track_id].bbox = bbox
72+
self.tracks[track_id].lost = 0
73+
for key, value in kwargs.items():
74+
self.tracks[track_id].info[key] = value
75+
76+
def _get_tracks(self, tracks):
6777
"""
6878
Output the information of tracks
6979
@@ -95,7 +105,7 @@ def get_tracks(self, tracks):
95105

96106
return outputs
97107

98-
def update(self, bboxes: list, class_ids: list):
108+
def update(self, bboxes: list, class_ids: list, detection_scores: list):
99109
"""
100110
Update the tracker based on the new bboxes as input.
101111
@@ -106,6 +116,8 @@ def update(self, bboxes: list, class_ids: list):
106116
coordinates of bounding box as tuple (top-left-x, top-left-y, bottom-right-x, bottom-right-y).
107117
class_ids : list
108118
List of class_ids (int) corresponding to labels of the detected object. Default is `None`.
119+
detection_scores: list
120+
List of detection scores / probability of each detected object or objectness.
109121
110122
Returns
111123
-------
@@ -122,39 +134,45 @@ def update(self, bboxes: list, class_ids: list):
122134
"""
123135
self.frame_count += 1
124136

137+
new_bboxes = np.array(bboxes, dtype='int')
138+
new_class_ids = np.array(class_ids, dtype='int')
139+
new_detection_scores = np.array(detection_scores)
140+
141+
new_centroids = get_centroids(new_bboxes)
142+
143+
new_detections = list(zip(
144+
range(len(bboxes)), new_bboxes, new_class_ids, new_centroids, new_detection_scores
145+
))
146+
125147
if len(bboxes) == 0: # if no object detected
126148
lost_ids = list(self.tracks.keys())
127149
for track_id in lost_ids:
128150
self.tracks[track_id].lost += 1
129151
if self.tracks[track_id].lost > self.max_lost:
130152
self._remove_track(track_id)
131153

132-
outputs = self.get_tracks(self.tracks)
154+
outputs = self._get_tracks(self.tracks)
133155
return outputs
134156

135-
new_class_ids = np.array(class_ids, dtype='int')
136-
new_centroids = np.zeros((len(bboxes), 2), dtype="int")
137-
for (i, bbox) in enumerate(bboxes):
138-
new_centroids[i] = get_centroid(bbox)
139-
140-
if len(self.tracks):
141-
track_ids = list(self.tracks.keys())
157+
track_ids = list(self.tracks.keys())
158+
if len(track_ids):
142159
old_centroids = np.array([self.tracks[tid].centroid for tid in track_ids])
143-
D = distance.cdist(old_centroids, new_centroids) # (row, col) = distance between old (row) and new (col)
144-
row_idxs = D.min(axis=1).argsort() # old tracks sorted as per min distance from new
145-
col_idxs = D.argmin(axis=1)[row_idxs] # new tracks sorted as per min distance from old
160+
D = distance.cdist(old_centroids, new_centroids) # (row, col) = distance between old (row) and new (col)
161+
162+
row_idxs = D.min(axis=1).argsort() # old tracks sorted as per min distance from new
163+
col_idxs = D.argmin(axis=1)[row_idxs] # new tracks sorted as per min distance from old
146164

147165
assigned_rows, assigned_cols = set(), set()
148166
for (row_idx, col_idx) in zip(row_idxs, col_idxs):
149167
if row_idx in assigned_rows or col_idx in assigned_cols:
150168
continue
151169

152170
track_id = track_ids[row_idx]
171+
172+
col_idx, bbox, class_id, centroid, detection_score = new_detections[col_idx]
153173

154-
if self.tracks[track_id].class_id == new_class_ids[col_idx]:
155-
self.tracks[track_id].centroid = new_centroids[col_idx]
156-
self.tracks[track_id].bbox = bboxes[col_idx]
157-
self.tracks[track_id].lost = 0
174+
if self.tracks[track_id].class_id == class_id:
175+
self._update_track(track_id, centroid, bbox, score=detection_score)
158176
assigned_rows.add(row_idx)
159177
assigned_cols.add(col_idx)
160178

@@ -170,10 +188,10 @@ def update(self, bboxes: list, class_ids: list):
170188
self._remove_track(track_id)
171189
else:
172190
for col_idx in unassigned_cols:
173-
self._add_object(new_centroids[col_idx], bboxes[col_idx], class_ids[col_idx])
191+
self._add_track(new_centroids[col_idx], bboxes[col_idx], class_ids[col_idx])
174192
else:
175193
for i in range(0, len(bboxes)):
176-
self._add_object(new_centroids[i], bboxes[i], class_ids[i])
194+
self._add_track(new_centroids[i], bboxes[i], class_ids[i])
177195

178-
outputs = self.get_tracks(self.tracks)
196+
outputs = self._get_tracks(self.tracks)
179197
return outputs

motrackers/track.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@ def __init__(self, track_id, centroid, bbox=None, class_id=None):
88
99
Parameters
1010
----------
11-
track_id :
12-
centroid :
13-
bbox :
14-
class_id :
11+
track_id : int
12+
Track id.
13+
centroid : tuple
14+
Centroid of the track pixel coordinate (x, y).
15+
bbox : tuple, list, numpy.ndarray
16+
Bounding box of the track.
17+
class_id : int
18+
Class label id.
1519
"""
1620
self.id = track_id
1721
self.class_id = class_id
@@ -22,4 +26,8 @@ def __init__(self, track_id, centroid, bbox=None, class_id=None):
2226
self.bbox = bbox
2327
self.lost = 0
2428

25-
self.info = {}
29+
self.info = dict(
30+
max_score=0.0,
31+
lost=0,
32+
score=0.0,
33+
)

motrackers/utils/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@
22
from .filechooser_utils import select_videofile
33
from .filechooser_utils import select_yolo_model
44
from .filechooser_utils import select_tfmobilenet
5+
from .misc import iou
6+
from .misc import get_centroid, get_centroids

motrackers/utils/misc.py

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,83 @@
1+
import numpy as np
12

23

34
def get_centroid(bounding_box):
45
"""
56
Calculate the centroid of bounding box.
67
7-
:param bounding_box: list of bounding box coordinates of top-left and bottom-right (xlt, ylt, xrb, yrb)
8-
:return: bounding box centroid coordinates (x, y)
9-
"""
8+
Parameters
9+
----------
10+
bounding_box : list
11+
list of bounding box coordinates of top-left and bottom-right (xlt, ylt, xrb, yrb)
12+
13+
Returns
14+
-------
15+
centroid: tuple
16+
Bounding box centroid pixel coordinates (x, y).
1017
18+
"""
1119
xlt, ylt, xrb, yrb = bounding_box
1220
centroid_x = int((xlt + xrb) / 2.0)
1321
centroid_y = int((ylt + yrb) / 2.0)
1422

1523
return centroid_x, centroid_y
24+
25+
26+
def get_centroids(bboxes):
27+
assert bboxes.shape[1] == 4
28+
29+
x = np.mean(bb[:, [0, 2]], axis=1, keepdims=True, dtype='int')
30+
y = np.mean(bb[:, [1, 3]], axis=1, keepdims=True, dtype='int')
31+
centroids = np.concatenate([x, y], axis=1)
32+
return centroids
33+
34+
35+
def iou(bbox1, bbox2):
36+
"""
37+
Calculates the intersection-over-union of two bounding boxes.
38+
Source: https://github.com/bochinski/iou-tracker/blob/master/util.py
39+
40+
Parameters
41+
----------
42+
bbox1 : numpy.array, list of floats
43+
bounding box in format (x-top-left, y-top-left, x-bottom-right, y-bottom-right) of length 4.
44+
bbox2 : numpy.array, list of floats
45+
bounding box in format (x-top-left, y-top-left, x-bottom-right, y-bottom-right) of length 4.
46+
47+
Returns
48+
-------
49+
iou: float
50+
intersection-over-onion of bbox1, bbox2.
51+
"""
52+
53+
bbox1 = [float(x) for x in bbox1]
54+
bbox2 = [float(x) for x in bbox2]
55+
56+
(x0_1, y0_1, x1_1, y1_1), (x0_2, y0_2, x1_2, y1_2) = bbox1, bbox2
57+
58+
# get the overlap rectangle
59+
overlap_x0 = max(x0_1, x0_2)
60+
overlap_y0 = max(y0_1, y0_2)
61+
overlap_x1 = min(x1_1, x1_2)
62+
overlap_y1 = min(y1_1, y1_2)
63+
64+
# check if there is an overlap
65+
if overlap_x1 - overlap_x0 <= 0 or overlap_y1 - overlap_y0 <= 0:
66+
return 0.0
67+
68+
# if yes, calculate the ratio of the overlap to each ROI size and the unified size
69+
size_1 = (x1_1 - x0_1) * (y1_1 - y0_1)
70+
size_2 = (x1_2 - x0_2) * (y1_2 - y0_2)
71+
size_intersection = (overlap_x1 - overlap_x0) * (overlap_y1 - overlap_y0)
72+
size_union = size_1 + size_2 - size_intersection
73+
74+
iou_ = size_intersection / size_union
75+
76+
return iou_
77+
78+
79+
if __name__ == '__main__':
80+
bb = np.random.random_integers(0, 100, size=(20,)).reshape((5, 4))
81+
c = get_centroids(bb)
82+
print(bb)
83+
print(c)

0 commit comments

Comments
 (0)