msight-vision 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cli/__init__.py +0 -0
- cli/launch_2d_viewer.py +10 -0
- cli/launch_custom_fuser.py +23 -0
- cli/launch_finite_difference_state_estimator.py +22 -0
- cli/launch_road_user_list_viewer.py +23 -0
- cli/launch_sort_tracker.py +24 -0
- cli/launch_yolo_onestage_detection.py +22 -0
- msight_vision/__init__.py +8 -0
- msight_vision/base.py +99 -0
- msight_vision/detector_yolo.py +87 -0
- msight_vision/fuser.py +325 -0
- msight_vision/localizer.py +32 -0
- msight_vision/msight_core/__init__.py +6 -0
- msight_vision/msight_core/detection.py +103 -0
- msight_vision/msight_core/fusion.py +64 -0
- msight_vision/msight_core/state_estimation.py +38 -0
- msight_vision/msight_core/tracking.py +31 -0
- msight_vision/msight_core/viewer.py +55 -0
- msight_vision/msight_core/warper.py +98 -0
- msight_vision/state_estimator.py +121 -0
- msight_vision/tracker.py +525 -0
- msight_vision/utils/__init__.py +3 -0
- msight_vision/utils/data.py +80 -0
- msight_vision/utils/typing.py +18 -0
- msight_vision/utils/vis.py +17 -0
- msight_vision/warper.py +89 -0
- msight_vision-0.1.0.dist-info/METADATA +28 -0
- msight_vision-0.1.0.dist-info/RECORD +31 -0
- msight_vision-0.1.0.dist-info/WHEEL +5 -0
- msight_vision-0.1.0.dist-info/entry_points.txt +7 -0
- msight_vision-0.1.0.dist-info/top_level.txt +2 -0
msight_vision/tracker.py
ADDED
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
from __future__ import print_function
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from filterpy.kalman import KalmanFilter
|
|
5
|
+
import numpy as np
|
|
6
|
+
import scipy
|
|
7
|
+
from .base import TrackerBase
|
|
8
|
+
import uuid
|
|
9
|
+
|
|
10
|
+
LAT0, LON0 = 42.229392, -83.739012
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def coord_normalization(lat, lon, center_lat=LAT0, center_lon=LON0):
|
|
14
|
+
# print("coord_normalization", lat, lon)
|
|
15
|
+
"from lat/lon to local coordinate with unit meter"
|
|
16
|
+
lat_norm = (lat - center_lat) * 111000.
|
|
17
|
+
lon_norm = (lon - center_lon) * 111000. * np.cos(center_lat/180.*np.pi)
|
|
18
|
+
return lat_norm, lon_norm
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def coord_unnormalization(lat_norm, lon_norm, center_lat=LAT0, center_lon=LON0):
|
|
22
|
+
"from local coordinate with unit meter to lat/lon"
|
|
23
|
+
lat = lat_norm / 111000. + center_lat
|
|
24
|
+
lon = lon_norm / 111000. / np.cos(center_lat/180.*np.pi) + center_lon
|
|
25
|
+
return lat, lon
|
|
26
|
+
|
|
27
|
+
def vpred2bbox(v, r=4):
|
|
28
|
+
|
|
29
|
+
if v == None or v.predicted_future == None:
|
|
30
|
+
return np.empty([0, 5])
|
|
31
|
+
|
|
32
|
+
pred_x = v.predicted_future['mean'][:, 0]
|
|
33
|
+
pred_y = v.predicted_future['mean'][:, 1]
|
|
34
|
+
realworld_x_norm, realworld_y_norm = coord_normalization(pred_x, pred_y)
|
|
35
|
+
bbox = [realworld_x_norm-r, realworld_y_norm-r,
|
|
36
|
+
realworld_x_norm+r, realworld_y_norm+r, np.float64(v.confidence).repeat(pred_x.shape[0])]
|
|
37
|
+
|
|
38
|
+
return np.array(bbox).transpose()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def linear_assignment(cost_matrix):
|
|
42
|
+
try:
|
|
43
|
+
import lap
|
|
44
|
+
_, x, y = lap.lapjv(cost_matrix, extend_cost=True)
|
|
45
|
+
return np.array([[y[i], i] for i in x if i >= 0])
|
|
46
|
+
except ImportError:
|
|
47
|
+
from scipy.optimize import linear_sum_assignment
|
|
48
|
+
# print(cost_matrix)
|
|
49
|
+
x, y = linear_sum_assignment(cost_matrix)
|
|
50
|
+
return np.array(list(zip(x, y)))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def iou_batch(bb_test, bb_gt):
|
|
54
|
+
"""
|
|
55
|
+
From SORT: Computes IOU between two bboxes in the form [x1,y1,x2,y2]
|
|
56
|
+
"""
|
|
57
|
+
bb_gt = np.expand_dims(bb_gt, 0)
|
|
58
|
+
bb_test = np.expand_dims(bb_test, 1)
|
|
59
|
+
|
|
60
|
+
xx1 = np.maximum(bb_test[..., 0], bb_gt[..., 0])
|
|
61
|
+
yy1 = np.maximum(bb_test[..., 1], bb_gt[..., 1])
|
|
62
|
+
xx2 = np.minimum(bb_test[..., 2], bb_gt[..., 2])
|
|
63
|
+
yy2 = np.minimum(bb_test[..., 3], bb_gt[..., 3])
|
|
64
|
+
w = np.maximum(0., xx2 - xx1)
|
|
65
|
+
h = np.maximum(0., yy2 - yy1)
|
|
66
|
+
wh = w * h
|
|
67
|
+
o = wh / ((bb_test[..., 2] - bb_test[..., 0]) * (bb_test[..., 3] - bb_test[..., 1])
|
|
68
|
+
+ (bb_gt[..., 2] - bb_gt[..., 0]) * (bb_gt[..., 3] - bb_gt[..., 1]) - wh)
|
|
69
|
+
return(o)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def dis_batch(bb_test, bb_gt):
|
|
73
|
+
bb_test_cx = (bb_test[:, 0] + bb_test[:, 2]) / 2
|
|
74
|
+
bb_test_cy = (bb_test[:, 1] + bb_test[:, 3]) / 2
|
|
75
|
+
bb_test_c = np.stack([bb_test_cx, bb_test_cy], axis=1)
|
|
76
|
+
bb_gt_cx = (bb_gt[:, 0] + bb_gt[:, 2]) / 2
|
|
77
|
+
bb_gt_cy = (bb_gt[:, 1] + bb_gt[:, 3]) / 2
|
|
78
|
+
bb_gt_c = np.stack([bb_gt_cx, bb_gt_cy], axis=1)
|
|
79
|
+
neg_dis = -scipy.spatial.distance.cdist(bb_test_c, bb_gt_c)
|
|
80
|
+
return neg_dis
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def convert_bbox_to_z(bbox):
|
|
84
|
+
"""
|
|
85
|
+
Takes a bounding box in the form [x1,y1,x2,y2] and returns z in the form
|
|
86
|
+
[x,y,s,r] where x,y is the centre of the box and s is the scale/area and r is
|
|
87
|
+
the aspect ratio
|
|
88
|
+
"""
|
|
89
|
+
w = bbox[2] - bbox[0]
|
|
90
|
+
h = bbox[3] - bbox[1]
|
|
91
|
+
x = bbox[0] + w/2.
|
|
92
|
+
y = bbox[1] + h/2.
|
|
93
|
+
s = w * h # scale is just area
|
|
94
|
+
r = w / float(h)
|
|
95
|
+
return np.array([x, y]).reshape((2, 1))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def convert_x_to_bbox(x, score=None):
|
|
99
|
+
"""
|
|
100
|
+
Takes a bounding box in the centre form [x,y,s,r] and returns it in the form
|
|
101
|
+
[x1,y1,x2,y2] where x1,y1 is the top left and x2,y2 is the bottom right
|
|
102
|
+
"""
|
|
103
|
+
w = 4
|
|
104
|
+
h = 4
|
|
105
|
+
if(score == None):
|
|
106
|
+
return np.array([x[0]-w/2., x[1]-h/2., x[0]+w/2., x[1]+h/2.]).reshape((1, 4))
|
|
107
|
+
else:
|
|
108
|
+
return np.array([x[0]-w/2., x[1]-h/2., x[0]+w/2., x[1]+h/2., score]).reshape((1, 5))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class KalmanBoxTracker(object):
|
|
112
|
+
"""
|
|
113
|
+
This class represents the internal state of individual tracked objects observed as bbox.
|
|
114
|
+
"""
|
|
115
|
+
count = 0
|
|
116
|
+
|
|
117
|
+
def __init__(self, bbox, category=0):
|
|
118
|
+
"""
|
|
119
|
+
Initialises a tracker using initial bounding box.
|
|
120
|
+
:param bbox: initial bounding box [x1, y1, x2, y2, score]
|
|
121
|
+
:param category: object category/class id
|
|
122
|
+
"""
|
|
123
|
+
# define constant velocity model
|
|
124
|
+
self.kf = KalmanFilter(dim_x=4, dim_z=2)
|
|
125
|
+
# self.kf = kinematic_kf(dim=2, order=1, dt=1)
|
|
126
|
+
self.kf.F = np.array(
|
|
127
|
+
[[1, 0, 1, 0], [0, 1, 0, 1], [0, 0, 1, 0], [0, 0, 0, 1]])
|
|
128
|
+
self.kf.H = np.array([[1, 0, 0, 0], [0, 1, 0, 0]])
|
|
129
|
+
|
|
130
|
+
# self.kf.R[0, 0] = 1.0
|
|
131
|
+
# self.kf.R[1, 1] = 10
|
|
132
|
+
# self.kf.P[2:, 2:] *= 10. # give high uncertainty to the unobservable initial velocities
|
|
133
|
+
# self.kf.P *= 10.
|
|
134
|
+
# self.kf.Q[-1,-1] *= 0.01
|
|
135
|
+
# self.kf.Q[:2,:2]*= 0.01
|
|
136
|
+
# self.kf.Q[2:,2:] *= 10
|
|
137
|
+
|
|
138
|
+
# empirical values from GPT-4
|
|
139
|
+
# self.kf.Q = np.array(
|
|
140
|
+
# [[0.000025, 0, 0.0005, 0],
|
|
141
|
+
# [0, 0.000025, 0, 0.0005],
|
|
142
|
+
# [0.0005, 0, 0.01, 0],
|
|
143
|
+
# [0, 0.0005, 0, 0.01]]
|
|
144
|
+
# )
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# self.kf.R[2:, 2:] *= 10.
|
|
148
|
+
# self.kf.P[4:, 4:] *= 1000. # give high uncertainty to the unobservable initial velocities
|
|
149
|
+
self.kf.P *= 10.
|
|
150
|
+
self.kf.P[2:, 2:] *= 1000. # give high uncertainty to the unobservable initial velocities
|
|
151
|
+
self.kf.Q = np.array(
|
|
152
|
+
[[1, 0, 0, 0],
|
|
153
|
+
[0, 1, 0, 0],
|
|
154
|
+
[0, 0, 1, 0],
|
|
155
|
+
[0, 0, 0, 1]]
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
self.kf.x[:2] = convert_bbox_to_z(bbox)
|
|
159
|
+
|
|
160
|
+
self.time_since_update = 0
|
|
161
|
+
self.id = KalmanBoxTracker.count
|
|
162
|
+
self.uuid = str(uuid.uuid4())
|
|
163
|
+
KalmanBoxTracker.count += 1
|
|
164
|
+
self.history = []
|
|
165
|
+
self.hits = 0
|
|
166
|
+
self.hit_streak = 0
|
|
167
|
+
self.age = 0
|
|
168
|
+
self.dlpred_box = np.zeros(4)
|
|
169
|
+
self.dlpred_boxes = None
|
|
170
|
+
self.dlpred_age = 0
|
|
171
|
+
self.category = category # Store object category
|
|
172
|
+
self.last_confidence = bbox[4] if len(bbox) > 4 else 1.0 # Store last confidence
|
|
173
|
+
|
|
174
|
+
def update(self, bbox, category=None):
|
|
175
|
+
"""
|
|
176
|
+
Updates the state vector with observed bbox.
|
|
177
|
+
:param bbox: observed bounding box
|
|
178
|
+
:param category: object category (optional, updates stored category if provided)
|
|
179
|
+
"""
|
|
180
|
+
self.time_since_update = 0
|
|
181
|
+
self.history = []
|
|
182
|
+
self.hits += 1
|
|
183
|
+
self.hit_streak += 1
|
|
184
|
+
self.kf.update(convert_bbox_to_z(bbox))
|
|
185
|
+
if category is not None:
|
|
186
|
+
self.category = category
|
|
187
|
+
if len(bbox) > 4:
|
|
188
|
+
self.last_confidence = bbox[4]
|
|
189
|
+
|
|
190
|
+
def update_pred(self, vehicle):
|
|
191
|
+
self.dlpred_boxes = vpred2bbox(vehicle)
|
|
192
|
+
try:
|
|
193
|
+
self.dlpred_box = self.dlpred_boxes[1]
|
|
194
|
+
except:
|
|
195
|
+
self.dlpred_box = np.zeros((4))
|
|
196
|
+
self.dlpred_age = 0
|
|
197
|
+
|
|
198
|
+
def update_pred_backup(self):
|
|
199
|
+
if not self.dlpred_boxes is None:
|
|
200
|
+
self.dlpred_age += 1
|
|
201
|
+
if self.dlpred_age < self.dlpred_boxes.shape[0] - 1:
|
|
202
|
+
self.dlpred_box = self.dlpred_boxes[self.dlpred_age+1]
|
|
203
|
+
|
|
204
|
+
def predict(self):
|
|
205
|
+
"""
|
|
206
|
+
Advances the state vector and returns the predicted bounding box estimate.
|
|
207
|
+
"""
|
|
208
|
+
# if((self.kf.x[6]+self.kf.x[2]) <= 0):
|
|
209
|
+
# self.kf.x[6] *= 0.0
|
|
210
|
+
self.kf.predict()
|
|
211
|
+
self.age += 1
|
|
212
|
+
if(self.time_since_update > 0):
|
|
213
|
+
self.hit_streak = 0
|
|
214
|
+
self.time_since_update += 1
|
|
215
|
+
self.history.append(convert_x_to_bbox(self.kf.x))
|
|
216
|
+
if sum(self.dlpred_box) == 0:
|
|
217
|
+
return self.history[-1]
|
|
218
|
+
else:
|
|
219
|
+
return [self.dlpred_box]
|
|
220
|
+
|
|
221
|
+
def get_state(self):
|
|
222
|
+
"""
|
|
223
|
+
Returns the current bounding box estimate.
|
|
224
|
+
"""
|
|
225
|
+
return convert_x_to_bbox(self.kf.x)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def associate_detections_to_trackers(detections, trackers, iou_threshold=0.3, iou_type='iou', vehicle_list = None):
|
|
229
|
+
"""
|
|
230
|
+
Assigns detections to tracked object (both represented as bounding boxes)
|
|
231
|
+
|
|
232
|
+
Returns 3 lists of matches, unmatched_detections and unmatched_trackers
|
|
233
|
+
"""
|
|
234
|
+
if(len(trackers) == 0):
|
|
235
|
+
return np.empty((0, 2), dtype=int), np.arange(len(detections)), np.empty((0, 5), dtype=int)
|
|
236
|
+
|
|
237
|
+
ori_iou_matrix = iou_batch(detections, trackers)
|
|
238
|
+
# neg_dis_matrix = dis_batch(detections, trackers)
|
|
239
|
+
# feat_dis_matrix = feat_dis_batch(detections, trackers)
|
|
240
|
+
# if iou_type == 'l2distance':
|
|
241
|
+
# iou_matrix = neg_dis_matrix
|
|
242
|
+
if iou_type == 'iou':
|
|
243
|
+
iou_matrix = ori_iou_matrix
|
|
244
|
+
# elif iou_type == 'iou+feat':
|
|
245
|
+
# iou_matrix = ori_iou_matrix
|
|
246
|
+
else:
|
|
247
|
+
raise NotImplementedError
|
|
248
|
+
|
|
249
|
+
if min(iou_matrix.shape) > 0:
|
|
250
|
+
a = (iou_matrix > iou_threshold).astype(np.int32)
|
|
251
|
+
if a.sum(1).max() == 1 and a.sum(0).max() == 1:
|
|
252
|
+
matched_indices = np.stack(np.where(a), axis=1)
|
|
253
|
+
else:
|
|
254
|
+
matched_indices = linear_assignment(-iou_matrix)
|
|
255
|
+
else:
|
|
256
|
+
matched_indices = np.empty(shape=(0, 2))
|
|
257
|
+
|
|
258
|
+
unmatched_detections = []
|
|
259
|
+
for d, det in enumerate(detections):
|
|
260
|
+
if(d not in matched_indices[:, 0]):
|
|
261
|
+
unmatched_detections.append(d)
|
|
262
|
+
unmatched_trackers = []
|
|
263
|
+
for t, trk in enumerate(trackers):
|
|
264
|
+
if(t not in matched_indices[:, 1]):
|
|
265
|
+
unmatched_trackers.append(t)
|
|
266
|
+
|
|
267
|
+
# filter out matched with low IOU
|
|
268
|
+
matches = []
|
|
269
|
+
for m in matched_indices:
|
|
270
|
+
if(iou_matrix[m[0], m[1]] < iou_threshold):
|
|
271
|
+
unmatched_detections.append(m[0])
|
|
272
|
+
unmatched_trackers.append(m[1])
|
|
273
|
+
else:
|
|
274
|
+
matches.append(m.reshape(1, 2))
|
|
275
|
+
if(len(matches) == 0):
|
|
276
|
+
matches = np.empty((0, 2), dtype=int)
|
|
277
|
+
else:
|
|
278
|
+
matches = np.concatenate(matches, axis=0)
|
|
279
|
+
|
|
280
|
+
return matches, np.array(unmatched_detections), np.array(unmatched_trackers)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class Sort(object):
|
|
284
|
+
def __init__(self, max_age=3, min_hits=1, iou_threshold=0.01, iou_type='iou'):
|
|
285
|
+
"""
|
|
286
|
+
Sets key parameters for SORT
|
|
287
|
+
"""
|
|
288
|
+
self.max_age = max_age
|
|
289
|
+
self.min_hits = min_hits
|
|
290
|
+
self.iou_threshold = iou_threshold
|
|
291
|
+
self.trackers = []
|
|
292
|
+
self.frame_count = 0
|
|
293
|
+
self.curr_matched = []
|
|
294
|
+
self.iou_type = iou_type
|
|
295
|
+
|
|
296
|
+
def update(self, dets=np.empty((0, 5)), categories=None, vehicle_list=None):
|
|
297
|
+
"""
|
|
298
|
+
Params:
|
|
299
|
+
dets - a numpy array of detections in the format [[x1,y1,x2,y2,score],[x1,y1,x2,y2,score],...]
|
|
300
|
+
categories - list of category ids corresponding to each detection
|
|
301
|
+
Requires: this method must be called once for each frame even with empty detections (use np.empty((0, 5)) for frames without detections).
|
|
302
|
+
Returns the a similar array, where the last column is the object ID.
|
|
303
|
+
|
|
304
|
+
NOTE: The number of objects returned may differ from the number of detections provided.
|
|
305
|
+
"""
|
|
306
|
+
if categories is None:
|
|
307
|
+
categories = [0] * len(dets)
|
|
308
|
+
|
|
309
|
+
self.frame_count += 1
|
|
310
|
+
# get predicted locations from existing trackers.
|
|
311
|
+
trks = np.zeros((len(self.trackers), 5))
|
|
312
|
+
to_del = []
|
|
313
|
+
ret = []
|
|
314
|
+
id = []
|
|
315
|
+
uuid = []
|
|
316
|
+
for t, trk in enumerate(trks):
|
|
317
|
+
pos = self.trackers[t].predict()[0]
|
|
318
|
+
trk[:] = [pos[0], pos[1], pos[2], pos[3], 0]
|
|
319
|
+
if np.any(np.isnan(pos)):
|
|
320
|
+
to_del.append(t)
|
|
321
|
+
trks = np.ma.compress_rows(np.ma.masked_invalid(trks))
|
|
322
|
+
for t in reversed(to_del):
|
|
323
|
+
self.trackers.pop(t)
|
|
324
|
+
matched, unmatched_dets, unmatched_trks = associate_detections_to_trackers(
|
|
325
|
+
dets, trks, self.iou_threshold, self.iou_type, vehicle_list)
|
|
326
|
+
self.curr_matched = matched
|
|
327
|
+
|
|
328
|
+
# update matched trackers with assigned detections
|
|
329
|
+
for m in matched:
|
|
330
|
+
self.trackers[m[1]].update(dets[m[0], :], categories[m[0]])
|
|
331
|
+
|
|
332
|
+
# create and initialise new trackers for unmatched detections
|
|
333
|
+
for i in unmatched_dets:
|
|
334
|
+
trk = KalmanBoxTracker(dets[i, :], category=categories[i])
|
|
335
|
+
self.trackers.append(trk)
|
|
336
|
+
i = len(self.trackers)
|
|
337
|
+
for trk in reversed(self.trackers):
|
|
338
|
+
d = trk.get_state()[0]
|
|
339
|
+
if (trk.time_since_update < 1) and (trk.hit_streak >= self.min_hits or self.frame_count <= self.min_hits):
|
|
340
|
+
# +1 as MOT benchmark requires positive
|
|
341
|
+
ret.append(np.concatenate((d, [trk.id+1])).reshape(1, -1))
|
|
342
|
+
id.append(trk.id+1)
|
|
343
|
+
uuid.append(trk.uuid)
|
|
344
|
+
i -= 1
|
|
345
|
+
# remove dead tracklet
|
|
346
|
+
if(trk.time_since_update > self.max_age):
|
|
347
|
+
self.trackers.pop(i)
|
|
348
|
+
if(len(ret) > 0):
|
|
349
|
+
return np.concatenate(ret), id, uuid
|
|
350
|
+
return np.empty((0, 5)), id, uuid
|
|
351
|
+
|
|
352
|
+
def update_pred(self, vehicle_list):
|
|
353
|
+
for tracker in self.trackers:
|
|
354
|
+
updated = False
|
|
355
|
+
for veh in vehicle_list:
|
|
356
|
+
# if we find the matched vehicle, then update the prediction
|
|
357
|
+
if veh.uuid == tracker.uuid:
|
|
358
|
+
tracker.update_pred(veh)
|
|
359
|
+
updated = True
|
|
360
|
+
# if we do not find the matched vehicle, then use the previous prediction, update it to the next frame
|
|
361
|
+
if not updated:
|
|
362
|
+
tracker.update_pred_backup()
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def vlist2bbox(vehicle_list, r=4):
|
|
367
|
+
|
|
368
|
+
if len(vehicle_list) == 0:
|
|
369
|
+
return np.empty([0, 5]), []
|
|
370
|
+
|
|
371
|
+
bboxes = []
|
|
372
|
+
categories = []
|
|
373
|
+
for i in range(len(vehicle_list)):
|
|
374
|
+
v = vehicle_list[i]
|
|
375
|
+
v.traj_id = "-1"
|
|
376
|
+
realworld_x_norm, realworld_y_norm = coord_normalization(v.x, v.y)
|
|
377
|
+
bbox = [realworld_x_norm-r, realworld_y_norm-r,
|
|
378
|
+
realworld_x_norm+r, realworld_y_norm+r, v.confidence]
|
|
379
|
+
bboxes.append(bbox)
|
|
380
|
+
categories.append(v.category if hasattr(v, 'category') else 0)
|
|
381
|
+
|
|
382
|
+
return np.array(bboxes), categories
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def update_vlist(bbs, updated_bbs, id, uuid, vehicle_list):
|
|
386
|
+
|
|
387
|
+
if len(vehicle_list) == 0:
|
|
388
|
+
return []
|
|
389
|
+
|
|
390
|
+
xcs = (bbs[:, 0] + bbs[:, 2]) / 2.0
|
|
391
|
+
ycs = (bbs[:, 1] + bbs[:, 3]) / 2.0
|
|
392
|
+
|
|
393
|
+
for i in range(len(updated_bbs)):
|
|
394
|
+
xc_ = (updated_bbs[i, 0] + updated_bbs[i, 2]) / 2.0
|
|
395
|
+
yc_ = (updated_bbs[i, 1] + updated_bbs[i, 3]) / 2.0
|
|
396
|
+
ds = ((xcs - xc_)**2 + (ycs - yc_)**2)**0.5
|
|
397
|
+
idx_min = np.argmin(ds)
|
|
398
|
+
vehicle_list[idx_min].traj_id = str(int(id[i]))
|
|
399
|
+
vehicle_list[idx_min]._uuid = uuid[i]
|
|
400
|
+
lat, lon = coord_unnormalization(xc_, yc_)
|
|
401
|
+
vehicle_list[idx_min].x = lat
|
|
402
|
+
vehicle_list[idx_min].y = lon
|
|
403
|
+
|
|
404
|
+
return vehicle_list
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def remove_untracked_vehicles(vehicle_list):
|
|
408
|
+
|
|
409
|
+
vehicle_list_new = []
|
|
410
|
+
for i in range(len(vehicle_list)):
|
|
411
|
+
v = vehicle_list[i]
|
|
412
|
+
if v.traj_id != '-1' and v.traj_id is not None:
|
|
413
|
+
vehicle_list_new.append(v)
|
|
414
|
+
|
|
415
|
+
return vehicle_list_new
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
# class Tracker(object):
|
|
419
|
+
|
|
420
|
+
# def __init__(self, max_age=3, min_hits=1, iou_threshold=0.01, iou_type='iou'):
|
|
421
|
+
# # create instance of SORT
|
|
422
|
+
# self.tracker = Sort(max_age=max_age, min_hits=min_hits, iou_threshold=iou_threshold, iou_type=iou_type)
|
|
423
|
+
|
|
424
|
+
# def track(self, vehicle_list):
|
|
425
|
+
# bbs = vlist2bbox(vehicle_list)
|
|
426
|
+
# updated_bbs, id, uuid = self.tracker.update(bbs)
|
|
427
|
+
# vehicle_list = update_vlist(bbs, updated_bbs, id, uuid, vehicle_list)
|
|
428
|
+
# vehicle_list = remove_untracked_vehicles(vehicle_list)
|
|
429
|
+
# return vehicle_list
|
|
430
|
+
|
|
431
|
+
# def update_pred(self, vehicle_list):
|
|
432
|
+
# self.tracker.update_pred(vehicle_list)
|
|
433
|
+
|
|
434
|
+
class SortTracker(TrackerBase):
|
|
435
|
+
def __init__(self, max_age=3, min_hits=1, iou_threshold=0.01, iou_type='iou', use_filtered_position=False, output_predicted=False):
|
|
436
|
+
"""
|
|
437
|
+
Sets key parameters for SORT
|
|
438
|
+
:param max_age: Maximum number of frames to keep a track without detection
|
|
439
|
+
:param min_hits: Minimum number of hits before a track is confirmed
|
|
440
|
+
:param iou_threshold: IOU threshold for matching
|
|
441
|
+
:param iou_type: Type of IOU to use ('iou' or 'l2distance')
|
|
442
|
+
:param use_filtered_position: If True, use Kalman filter's refined position instead of raw detection
|
|
443
|
+
:param output_predicted: If True, output predicted positions for temporarily missing objects
|
|
444
|
+
"""
|
|
445
|
+
self.tracker = Sort(max_age=max_age, min_hits=min_hits, iou_threshold=iou_threshold, iou_type=iou_type)
|
|
446
|
+
self.use_filtered_position = use_filtered_position
|
|
447
|
+
self.output_predicted = output_predicted
|
|
448
|
+
|
|
449
|
+
def track(self, object_list):
|
|
450
|
+
bbs, categories = vlist2bbox(object_list)
|
|
451
|
+
# print(bbs)
|
|
452
|
+
updated_bbs, id, uuid = self.tracker.update(bbs, categories)
|
|
453
|
+
object_list = update_vlist(bbs, updated_bbs, id, uuid, object_list)
|
|
454
|
+
object_list = remove_untracked_vehicles(object_list)
|
|
455
|
+
|
|
456
|
+
if self.use_filtered_position:
|
|
457
|
+
# Update object positions using Kalman filter's refined estimate
|
|
458
|
+
object_list = self._apply_filtered_positions(object_list)
|
|
459
|
+
|
|
460
|
+
if self.output_predicted:
|
|
461
|
+
# Add predicted objects for temporarily missing tracks
|
|
462
|
+
predicted_objects = self._get_predicted_objects(object_list)
|
|
463
|
+
object_list.extend(predicted_objects)
|
|
464
|
+
|
|
465
|
+
return object_list
|
|
466
|
+
|
|
467
|
+
def _apply_filtered_positions(self, object_list):
|
|
468
|
+
"""
|
|
469
|
+
Update object positions using Kalman filter's refined estimate.
|
|
470
|
+
:param object_list: list of tracked objects
|
|
471
|
+
:return: object list with refined positions
|
|
472
|
+
"""
|
|
473
|
+
for obj in object_list:
|
|
474
|
+
# Find the corresponding tracker by uuid
|
|
475
|
+
for tracker in self.tracker.trackers:
|
|
476
|
+
if tracker.uuid == obj._uuid:
|
|
477
|
+
# Get the filtered state from Kalman filter
|
|
478
|
+
state = tracker.kf.x
|
|
479
|
+
filtered_x = state[0, 0]
|
|
480
|
+
filtered_y = state[1, 0]
|
|
481
|
+
# Convert back to lat/lon
|
|
482
|
+
lat, lon = coord_unnormalization(filtered_x, filtered_y)
|
|
483
|
+
obj.x = lat
|
|
484
|
+
obj.y = lon
|
|
485
|
+
break
|
|
486
|
+
return object_list
|
|
487
|
+
|
|
488
|
+
def _get_predicted_objects(self, object_list):
|
|
489
|
+
"""
|
|
490
|
+
Create objects for temporarily missing tracks using predicted positions.
|
|
491
|
+
:param object_list: list of currently tracked objects
|
|
492
|
+
:return: list of predicted objects for missing tracks
|
|
493
|
+
"""
|
|
494
|
+
from msight_base import RoadUserPoint # Import here to avoid circular imports
|
|
495
|
+
|
|
496
|
+
# Get UUIDs of currently matched objects
|
|
497
|
+
matched_uuids = {obj._uuid for obj in object_list if hasattr(obj, '_uuid') and obj._uuid}
|
|
498
|
+
|
|
499
|
+
predicted_objects = []
|
|
500
|
+
for tracker in self.tracker.trackers:
|
|
501
|
+
# Check if this tracker is not in the current output but still alive
|
|
502
|
+
if tracker.uuid not in matched_uuids:
|
|
503
|
+
# Only output if track was previously confirmed (has enough hits)
|
|
504
|
+
if tracker.hits >= self.tracker.min_hits:
|
|
505
|
+
# Get predicted state from Kalman filter
|
|
506
|
+
state = tracker.kf.x
|
|
507
|
+
predicted_x = state[0, 0]
|
|
508
|
+
predicted_y = state[1, 0]
|
|
509
|
+
# Convert back to lat/lon
|
|
510
|
+
lat, lon = coord_unnormalization(predicted_x, predicted_y)
|
|
511
|
+
|
|
512
|
+
# Create a predicted object with stored category and reduced confidence
|
|
513
|
+
predicted_obj = RoadUserPoint(
|
|
514
|
+
x=lat,
|
|
515
|
+
y=lon,
|
|
516
|
+
category=tracker.category,
|
|
517
|
+
confidence=tracker.last_confidence * 0.5, # Reduce confidence for predicted
|
|
518
|
+
)
|
|
519
|
+
predicted_obj.traj_id = str(tracker.id + 1)
|
|
520
|
+
predicted_obj._uuid = tracker.uuid
|
|
521
|
+
predicted_obj.is_predicted = True # Mark as predicted
|
|
522
|
+
predicted_objects.append(predicted_obj)
|
|
523
|
+
|
|
524
|
+
return predicted_objects
|
|
525
|
+
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
import cv2
|
|
4
|
+
|
|
5
|
+
def get_time_from_name(name: str) -> datetime:
|
|
6
|
+
"""
|
|
7
|
+
Get time from name.
|
|
8
|
+
:param name: name of the file
|
|
9
|
+
:return: time in datetime format
|
|
10
|
+
"""
|
|
11
|
+
# name format is 2023-12-19 11-00-00-013294.jpg
|
|
12
|
+
time_str = name.split(".")[0].split("#")[0]
|
|
13
|
+
time_str = time_str.replace("-", " ").replace(" ", "-")
|
|
14
|
+
return datetime.strptime(time_str, "%Y-%m-%d-%H-%M-%S-%f")
|
|
15
|
+
|
|
16
|
+
class ImageRetriever:
|
|
17
|
+
def __init__(self, img_dir: Path, sensor_list:list = None, time_tolerance: float = 0.2):
|
|
18
|
+
self.img_dir = img_dir
|
|
19
|
+
self.img_buff = {}
|
|
20
|
+
self.timestamps = {}
|
|
21
|
+
self.time_tolerance = time_tolerance
|
|
22
|
+
if sensor_list is None:
|
|
23
|
+
# find all folders in the directory
|
|
24
|
+
self.sensor_list = [f.name for f in img_dir.iterdir() if f.is_dir()]
|
|
25
|
+
else:
|
|
26
|
+
self.sensor_list = sensor_list
|
|
27
|
+
for sensor_name in self.sensor_list:
|
|
28
|
+
# find all folders in the directory
|
|
29
|
+
self.img_buff[sensor_name] = [f for f in (img_dir / sensor_name).iterdir() if f.is_file()]
|
|
30
|
+
# sort the image files by name
|
|
31
|
+
self.img_buff[sensor_name].sort(key=lambda x: x.name.split("#")[0])
|
|
32
|
+
self.timestamps[sensor_name] = [get_time_from_name(f.name).timestamp() for f in self.img_buff[sensor_name]]
|
|
33
|
+
|
|
34
|
+
self.length = min([len(self.img_buff[sensor_name]) for sensor_name in self.sensor_list])
|
|
35
|
+
self.step = 0
|
|
36
|
+
self.main_sensor = self.sensor_list[0]
|
|
37
|
+
|
|
38
|
+
def _find_closest_timestamp(self, timestamps, target):
|
|
39
|
+
"""Binary search to find the index of the closest timestamp to the target."""
|
|
40
|
+
low, high = 0, len(timestamps) - 1
|
|
41
|
+
best_idx = -1
|
|
42
|
+
best_diff = float('inf')
|
|
43
|
+
while low <= high:
|
|
44
|
+
mid = (low + high) // 2
|
|
45
|
+
diff = abs(timestamps[mid] - target)
|
|
46
|
+
if diff < best_diff:
|
|
47
|
+
best_diff = diff
|
|
48
|
+
best_idx = mid
|
|
49
|
+
if timestamps[mid] < target:
|
|
50
|
+
low = mid + 1
|
|
51
|
+
else:
|
|
52
|
+
high = mid - 1
|
|
53
|
+
return best_idx, timestamps[best_idx] if best_idx != -1 else None, best_diff
|
|
54
|
+
|
|
55
|
+
def get_image(self):
|
|
56
|
+
print(f"Retrieving images at step {self.step}/{self.length}")
|
|
57
|
+
if self.step >= self.length:
|
|
58
|
+
return None
|
|
59
|
+
result = {}
|
|
60
|
+
current_time = self.timestamps[self.main_sensor][self.step]
|
|
61
|
+
for sensor_name in self.sensor_list:
|
|
62
|
+
# find the index of the closest timestamp in the sensor's timestamp list, use binary search
|
|
63
|
+
idx, closest_timestamp, best_diff = self._find_closest_timestamp(self.timestamps[sensor_name], current_time)
|
|
64
|
+
if best_diff > self.time_tolerance:
|
|
65
|
+
print(f"Warning: No close timestamp found for sensor {sensor_name} at time {current_time}. Closest time is {closest_timestamp} with difference {best_diff}.")
|
|
66
|
+
img_path = self.img_buff[sensor_name][idx]
|
|
67
|
+
# read image as numpy array
|
|
68
|
+
img = cv2.imread(str(img_path))
|
|
69
|
+
if img is None:
|
|
70
|
+
print(f"Error: {img_path} is not a valid image file.")
|
|
71
|
+
return None
|
|
72
|
+
result[sensor_name] = {}
|
|
73
|
+
result[sensor_name]["image"] = img
|
|
74
|
+
## get time from name
|
|
75
|
+
## datetime needs to be converted to timestamp
|
|
76
|
+
result[sensor_name]["timestamp"] = get_time_from_name(img_path.name).timestamp()
|
|
77
|
+
result[sensor_name]["path"] = img_path
|
|
78
|
+
result[sensor_name]["frame_id"] = img_path.stem.split("#")[-1] if "#" in img_path.stem else None
|
|
79
|
+
self.step += 1
|
|
80
|
+
return result
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from msight_base import RoadUserPoint
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def detection_to_roaduser_point(detected_object, sensor_id):
|
|
5
|
+
"""
|
|
6
|
+
Convert detection result to RoadUserPoint.
|
|
7
|
+
:param detection_result: DetectionResult2D instance
|
|
8
|
+
:return: list of RoadUserPoint instances
|
|
9
|
+
"""
|
|
10
|
+
# print(detected_object.lat, detected_object.lon, detected_object.class_id, detected_object.score)
|
|
11
|
+
road_user_point = RoadUserPoint(
|
|
12
|
+
x = detected_object.lat,
|
|
13
|
+
y = detected_object.lon,
|
|
14
|
+
category=detected_object.class_id,
|
|
15
|
+
confidence=detected_object.score,
|
|
16
|
+
)
|
|
17
|
+
road_user_point.sensor_data[sensor_id] = detected_object.to_dict()
|
|
18
|
+
return road_user_point
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import cv2
|
|
2
|
+
def visualize_detection_result(image, detection_result, box_color=(0, 255, 0), text_color=(0, 255, 0)):
|
|
3
|
+
"""
|
|
4
|
+
Visualize the detection result on the image.
|
|
5
|
+
:param image: input image as a numpy array
|
|
6
|
+
:param detection_result: DetectionResult2D instance
|
|
7
|
+
:return: image with bounding boxes drawn
|
|
8
|
+
"""
|
|
9
|
+
for obj in detection_result.object_list:
|
|
10
|
+
box = obj.box
|
|
11
|
+
class_id = obj.class_id
|
|
12
|
+
score = obj.score
|
|
13
|
+
# draw rectangle
|
|
14
|
+
cv2.rectangle(image, (int(box[0]), int(box[1])), (int(box[2]), int(box[3])), box_color, 2)
|
|
15
|
+
# put class_id and score
|
|
16
|
+
cv2.putText(image, f"{class_id}:{score:.2f}", (int(box[0]), int(box[1]) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, text_color, 2)
|
|
17
|
+
return image
|