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.
@@ -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,3 @@
1
+ from .data import ImageRetriever
2
+ from .typing import detection_to_roaduser_point
3
+ from .vis import visualize_detection_result
@@ -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