superiorvision 0.30.0.dev0__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.
Files changed (96) hide show
  1. superiorvision/__init__.py +19 -0
  2. superiorvision-0.30.0.dev0.dist-info/METADATA +398 -0
  3. superiorvision-0.30.0.dev0.dist-info/RECORD +96 -0
  4. superiorvision-0.30.0.dev0.dist-info/WHEEL +5 -0
  5. superiorvision-0.30.0.dev0.dist-info/licenses/LICENSE.md +9 -0
  6. superiorvision-0.30.0.dev0.dist-info/top_level.txt +2 -0
  7. supervision/__init__.py +318 -0
  8. supervision/annotators/__init__.py +0 -0
  9. supervision/annotators/base.py +21 -0
  10. supervision/annotators/core.py +3551 -0
  11. supervision/annotators/utils.py +541 -0
  12. supervision/assets/__init__.py +4 -0
  13. supervision/assets/downloader.py +166 -0
  14. supervision/assets/list.py +84 -0
  15. supervision/classification/__init__.py +0 -0
  16. supervision/classification/core.py +216 -0
  17. supervision/config.py +13 -0
  18. supervision/dataset/__init__.py +0 -0
  19. supervision/dataset/core.py +1313 -0
  20. supervision/dataset/formats/__init__.py +0 -0
  21. supervision/dataset/formats/coco.py +708 -0
  22. supervision/dataset/formats/createml.py +331 -0
  23. supervision/dataset/formats/labelme.py +405 -0
  24. supervision/dataset/formats/pascal_voc.py +449 -0
  25. supervision/dataset/formats/yolo.py +502 -0
  26. supervision/dataset/utils.py +265 -0
  27. supervision/detection/__init__.py +0 -0
  28. supervision/detection/compact_mask.py +1927 -0
  29. supervision/detection/core.py +3864 -0
  30. supervision/detection/line_zone.py +924 -0
  31. supervision/detection/overlap_filter.py +426 -0
  32. supervision/detection/tensor_utils.py +1596 -0
  33. supervision/detection/tensor_views.py +48 -0
  34. supervision/detection/tools/__init__.py +0 -0
  35. supervision/detection/tools/csv_sink.py +242 -0
  36. supervision/detection/tools/inference_slicer.py +776 -0
  37. supervision/detection/tools/json_sink.py +215 -0
  38. supervision/detection/tools/polygon_zone.py +266 -0
  39. supervision/detection/tools/smoother.py +218 -0
  40. supervision/detection/tools/transformers.py +265 -0
  41. supervision/detection/utils/__init__.py +12 -0
  42. supervision/detection/utils/_typing.py +11 -0
  43. supervision/detection/utils/boxes.py +510 -0
  44. supervision/detection/utils/converters.py +830 -0
  45. supervision/detection/utils/internal.py +806 -0
  46. supervision/detection/utils/iou_and_nms.py +1606 -0
  47. supervision/detection/utils/masks.py +625 -0
  48. supervision/detection/utils/polygons.py +128 -0
  49. supervision/detection/utils/vlms.py +97 -0
  50. supervision/detection/vlm.py +945 -0
  51. supervision/draw/__init__.py +0 -0
  52. supervision/draw/base.py +14 -0
  53. supervision/draw/color.py +598 -0
  54. supervision/draw/utils.py +527 -0
  55. supervision/geometry/__init__.py +0 -0
  56. supervision/geometry/core.py +208 -0
  57. supervision/geometry/utils.py +54 -0
  58. supervision/key_points/__init__.py +0 -0
  59. supervision/key_points/annotators.py +997 -0
  60. supervision/key_points/core.py +1506 -0
  61. supervision/key_points/skeletons.py +2646 -0
  62. supervision/keypoint/__init__.py +13 -0
  63. supervision/keypoint/annotators.py +13 -0
  64. supervision/keypoint/core.py +8 -0
  65. supervision/metrics/__init__.py +40 -0
  66. supervision/metrics/core.py +74 -0
  67. supervision/metrics/detection.py +1632 -0
  68. supervision/metrics/f1_score.py +815 -0
  69. supervision/metrics/mean_average_precision.py +1723 -0
  70. supervision/metrics/mean_average_recall.py +734 -0
  71. supervision/metrics/precision.py +815 -0
  72. supervision/metrics/recall.py +774 -0
  73. supervision/metrics/utils/__init__.py +0 -0
  74. supervision/metrics/utils/matching.py +56 -0
  75. supervision/metrics/utils/object_size.py +256 -0
  76. supervision/metrics/utils/utils.py +9 -0
  77. supervision/py.typed +0 -0
  78. supervision/tracker/__init__.py +5 -0
  79. supervision/tracker/byte_tracker/__init__.py +0 -0
  80. supervision/tracker/byte_tracker/core.py +448 -0
  81. supervision/tracker/byte_tracker/kalman_filter.py +186 -0
  82. supervision/tracker/byte_tracker/matching.py +211 -0
  83. supervision/tracker/byte_tracker/single_object_track.py +194 -0
  84. supervision/tracker/byte_tracker/utils.py +34 -0
  85. supervision/utils/__init__.py +0 -0
  86. supervision/utils/conversion.py +223 -0
  87. supervision/utils/deprecate.py +54 -0
  88. supervision/utils/file.py +209 -0
  89. supervision/utils/image.py +988 -0
  90. supervision/utils/internal.py +209 -0
  91. supervision/utils/iterables.py +103 -0
  92. supervision/utils/logger.py +61 -0
  93. supervision/utils/notebook.py +124 -0
  94. supervision/utils/tensor.py +362 -0
  95. supervision/utils/video.py +533 -0
  96. supervision/validators/__init__.py +379 -0
@@ -0,0 +1,448 @@
1
+ import numpy as np
2
+ import numpy.typing as npt
3
+ import torch
4
+
5
+ from supervision.detection.core import Detections
6
+ from supervision.detection.utils import box_iou_batch
7
+ from supervision.tracker.byte_tracker import matching
8
+ from supervision.tracker.byte_tracker.kalman_filter import KalmanFilter
9
+ from supervision.tracker.byte_tracker.single_object_track import STrack, TrackState
10
+ from supervision.tracker.byte_tracker.utils import IdCounter
11
+ from supervision.utils.deprecate import TargetMode, deprecated_class
12
+
13
+
14
+ def _valid_tracking_tensors(
15
+ tensors: torch.Tensor,
16
+ ) -> torch.Tensor:
17
+ """Identify finite tensors with positive-width and positive-height boxes."""
18
+ bboxes = tensors[:, :4]
19
+ widths = bboxes[:, 2] - bboxes[:, 0]
20
+ heights = bboxes[:, 3] - bboxes[:, 1]
21
+ return torch.isfinite(tensors).all(dim=1) & (widths > 0) & (heights > 0)
22
+
23
+
24
+ @deprecated_class(
25
+ target=TargetMode.NOTIFY,
26
+ deprecated_in="0.28.0",
27
+ remove_in="0.31.0",
28
+ )
29
+ class ByteTrack:
30
+ """
31
+ Initialize the ByteTrack object.
32
+
33
+ !!! deprecated "Deprecated"
34
+
35
+ `ByteTrack` is deprecated since `supervision-0.28.0` and will be removed in
36
+ `supervision-0.31.0`. Use `ByteTrackTracker` from the private `tracktors`
37
+ fork instead. Note: the update method is renamed from
38
+ `update_with_detections()` to `update()`.
39
+
40
+ <video controls>
41
+ <source src="https://media.roboflow.com/supervision/video-examples/how-to/track-objects/annotate-video-with-traces.mp4" type="video/mp4">
42
+ </video>
43
+
44
+ Args:
45
+ track_activation_threshold: Detection confidence threshold
46
+ for track activation. Increasing track_activation_threshold improves accuracy
47
+ and stability but might miss true detections. Decreasing it increases
48
+ completeness but risks introducing noise and instability.
49
+ lost_track_buffer: Number of frames to buffer when a track is lost.
50
+ Increasing lost_track_buffer enhances occlusion handling, significantly
51
+ reducing the likelihood of track fragmentation or disappearance caused
52
+ by brief detection gaps.
53
+ minimum_matching_threshold: Threshold for matching tracks with detections.
54
+ Decreasing minimum_matching_threshold improves accuracy but risks fragmentation.
55
+ Increasing it improves completeness but risks false positives and drift.
56
+ frame_rate: The frame rate of the video. Accepts float values (e.g. 23.976,
57
+ 29.97) for accurate lost-track-buffer calculation.
58
+ minimum_consecutive_frames: Number of consecutive frames that an object must
59
+ be tracked before it is considered a 'valid' track.
60
+ Increasing minimum_consecutive_frames prevents the creation of accidental tracks from
61
+ false detection or double detection, but risks missing shorter tracks.
62
+ """ # noqa: E501 // docs
63
+
64
+ def __init__(
65
+ self,
66
+ track_activation_threshold: float = 0.25,
67
+ lost_track_buffer: int = 30,
68
+ minimum_matching_threshold: float = 0.8,
69
+ frame_rate: float = 30,
70
+ minimum_consecutive_frames: int = 1,
71
+ ) -> None:
72
+ self.track_activation_threshold = track_activation_threshold
73
+ self.minimum_matching_threshold = minimum_matching_threshold
74
+
75
+ self.frame_id = 0
76
+ self.det_thresh = self.track_activation_threshold + 0.1
77
+ if self.det_thresh > 1.0:
78
+ self.det_thresh = self.track_activation_threshold
79
+ self.max_time_lost = int(frame_rate / 30.0 * lost_track_buffer)
80
+ self.minimum_consecutive_frames = minimum_consecutive_frames
81
+ self.kalman_filter = KalmanFilter()
82
+ self.shared_kalman = KalmanFilter()
83
+
84
+ self.tracked_tracks: list[STrack] = []
85
+ self.lost_tracks: list[STrack] = []
86
+ self.removed_tracks: list[STrack] = []
87
+
88
+ # Warning, possible bug: If you also set internal_id to start at 1,
89
+ # all traces will be connected across objects.
90
+ self.internal_id_counter = IdCounter()
91
+ self.external_id_counter = IdCounter(start_id=1)
92
+
93
+ def update_with_detections(self, detections: Detections) -> Detections:
94
+ """
95
+ Updates the tracker with the provided detections and returns the updated
96
+ detection results.
97
+
98
+ Args:
99
+ detections: The detections to pass through the tracker.
100
+
101
+ Example:
102
+ ```python
103
+ import supervision as sv
104
+ from ultralytics import YOLO
105
+
106
+ model = YOLO("<MODEL_PATH>")
107
+ tracker = sv.ByteTrack()
108
+
109
+ box_annotator = sv.BoxAnnotator()
110
+ label_annotator = sv.LabelAnnotator()
111
+
112
+ def callback(frame: np.ndarray, index: int) -> np.ndarray:
113
+ results = model(frame)[0]
114
+ detections = sv.Detections.from_ultralytics(results)
115
+ detections = tracker.update_with_detections(detections)
116
+
117
+ labels = [f"#{tracker_id}" for tracker_id in detections.tracker_id]
118
+
119
+ annotated_frame = box_annotator.annotate(
120
+ scene=frame.copy(), detections=detections)
121
+ annotated_frame = label_annotator.annotate(
122
+ scene=annotated_frame, detections=detections, labels=labels)
123
+ return annotated_frame
124
+
125
+ sv.process_video(
126
+ source_path="<SOURCE_VIDEO_PATH>",
127
+ target_path="<TARGET_VIDEO_PATH>",
128
+ callback=callback
129
+ )
130
+ ```
131
+ """
132
+ if detections.confidence is None:
133
+ raise ValueError("Detections confidence must be provided for tracking.")
134
+
135
+ tensors = torch.cat((detections.xyxy, detections.confidence[:, None]), dim=1)
136
+ tracks = self.update_with_tensors(tensors=tensors)
137
+
138
+ if len(tracks) > 0:
139
+ detection_bounding_boxes = tensors[:, :4]
140
+ track_bounding_boxes = torch.stack(
141
+ [
142
+ track.tlbr.to(
143
+ dtype=detection_bounding_boxes.dtype,
144
+ device=detection_bounding_boxes.device,
145
+ )
146
+ for track in tracks
147
+ ]
148
+ )
149
+
150
+ ious = box_iou_batch(detection_bounding_boxes, track_bounding_boxes)
151
+
152
+ iou_costs = 1 - ious
153
+
154
+ matches, _, _ = matching.linear_assignment(iou_costs, 0.5)
155
+ tracked_detections = detections.select(slice(None))
156
+ tracked_detections.tracker_id = torch.full(
157
+ (len(detections),),
158
+ -1,
159
+ dtype=torch.long,
160
+ device=detections.xyxy.device,
161
+ )
162
+ for i_detection, i_track in matches:
163
+ detection_index = int(i_detection.item())
164
+ track_index = int(i_track.item())
165
+ tracked_detections.tracker_id[detection_index] = tracks[
166
+ track_index
167
+ ].external_track_id
168
+
169
+ return tracked_detections.select(tracked_detections.tracker_id != -1)
170
+
171
+ else:
172
+ empty = detections.select(
173
+ torch.zeros(
174
+ len(detections), dtype=torch.bool, device=detections.xyxy.device
175
+ )
176
+ )
177
+ empty.tracker_id = torch.empty(
178
+ 0, dtype=torch.long, device=detections.xyxy.device
179
+ )
180
+ return empty
181
+
182
+ def reset(self) -> None:
183
+ """
184
+ Resets the internal state of the ByteTrack tracker.
185
+
186
+ This method clears the tracking data, including tracked, lost,
187
+ and removed tracks, as well as resetting the frame counter. It's
188
+ particularly useful when processing multiple videos sequentially,
189
+ ensuring the tracker starts with a clean state for each new video.
190
+ """
191
+ self.frame_id = 0
192
+ self.internal_id_counter.reset()
193
+ self.external_id_counter.reset()
194
+ self.tracked_tracks = []
195
+ self.lost_tracks = []
196
+ self.removed_tracks = []
197
+
198
+ def update_with_tensors(
199
+ self, tensors: npt.NDArray[np.float32] | torch.Tensor
200
+ ) -> list[STrack]:
201
+ """
202
+ Updates the tracker with the provided tensors and returns the updated tracks.
203
+
204
+ Args:
205
+ tensors: The new tensors to update with.
206
+
207
+ Returns:
208
+ Updated tracks.
209
+ """
210
+ tensors = torch.as_tensor(tensors)
211
+ if not tensors.is_floating_point():
212
+ tensors = tensors.to(dtype=torch.float32)
213
+ self.frame_id += 1
214
+ activated_starcks = []
215
+ refind_stracks = []
216
+ lost_stracks = []
217
+ removed_stracks = []
218
+
219
+ tensors = tensors[_valid_tracking_tensors(tensors)]
220
+ scores = tensors[:, 4]
221
+ bboxes = tensors[:, :4]
222
+
223
+ remain_inds = scores >= self.track_activation_threshold
224
+ inds_low = scores > 0.1
225
+ inds_high = scores < self.track_activation_threshold
226
+
227
+ inds_second = torch.logical_and(inds_low, inds_high)
228
+ dets_second = bboxes[inds_second]
229
+ dets = bboxes[remain_inds]
230
+ scores_keep = scores[remain_inds]
231
+ scores_second = scores[inds_second]
232
+
233
+ if len(dets) > 0:
234
+ """Detections"""
235
+ detections = [
236
+ STrack(
237
+ STrack.tlbr_to_tlwh(tlbr),
238
+ score_keep,
239
+ self.minimum_consecutive_frames,
240
+ self.shared_kalman,
241
+ self.internal_id_counter,
242
+ self.external_id_counter,
243
+ )
244
+ for (tlbr, score_keep) in zip(dets, scores_keep)
245
+ ]
246
+ else:
247
+ detections = []
248
+
249
+ """ Add newly detected tracklets to tracked_stracks"""
250
+ unconfirmed = []
251
+ tracked_stracks: list[STrack] = []
252
+
253
+ for track in self.tracked_tracks:
254
+ if not track.is_activated:
255
+ unconfirmed.append(track)
256
+ else:
257
+ tracked_stracks.append(track)
258
+
259
+ """ Step 2: First association, with high score detection boxes"""
260
+ strack_pool = joint_tracks(tracked_stracks, self.lost_tracks)
261
+ # Predict the current location with KF
262
+ STrack.multi_predict(strack_pool, self.shared_kalman)
263
+ dists = matching.iou_distance(strack_pool, detections)
264
+
265
+ dists = matching.fuse_score(dists, detections)
266
+ matches, u_track, u_detection = matching.linear_assignment(
267
+ dists, thresh=self.minimum_matching_threshold
268
+ )
269
+
270
+ for itracked, idet in matches:
271
+ itracked, idet = int(itracked.item()), int(idet.item())
272
+ track = strack_pool[itracked]
273
+ det = detections[idet]
274
+ if track.state == TrackState.Tracked:
275
+ track.update(detections[idet], self.frame_id)
276
+ activated_starcks.append(track)
277
+ else:
278
+ track.re_activate(det, self.frame_id)
279
+ refind_stracks.append(track)
280
+
281
+ """ Step 3: Second association, with low score detection boxes"""
282
+ # association the untrack to the low score detections
283
+ if len(dets_second) > 0:
284
+ """Detections"""
285
+ detections_second = [
286
+ STrack(
287
+ STrack.tlbr_to_tlwh(tlbr),
288
+ score_second,
289
+ self.minimum_consecutive_frames,
290
+ self.shared_kalman,
291
+ self.internal_id_counter,
292
+ self.external_id_counter,
293
+ )
294
+ for (tlbr, score_second) in zip(dets_second, scores_second)
295
+ ]
296
+ else:
297
+ detections_second = []
298
+ r_tracked_stracks = [
299
+ strack_pool[i]
300
+ for i in u_track
301
+ if strack_pool[i].state == TrackState.Tracked
302
+ ]
303
+ dists = matching.iou_distance(r_tracked_stracks, detections_second)
304
+ matches, u_track, _u_detection_second = matching.linear_assignment(
305
+ dists, thresh=0.5
306
+ )
307
+ for itracked, idet in matches:
308
+ itracked, idet = int(itracked.item()), int(idet.item())
309
+ track = r_tracked_stracks[itracked]
310
+ det = detections_second[idet]
311
+ if track.state == TrackState.Tracked:
312
+ track.update(det, self.frame_id)
313
+ activated_starcks.append(track)
314
+ else:
315
+ track.re_activate(det, self.frame_id)
316
+ refind_stracks.append(track)
317
+
318
+ for it in u_track:
319
+ track = r_tracked_stracks[it]
320
+ if not track.state == TrackState.Lost:
321
+ track.state = TrackState.Lost
322
+ lost_stracks.append(track)
323
+
324
+ """Deal with unconfirmed tracks, usually tracks with only one beginning frame"""
325
+ detections = [detections[i] for i in u_detection]
326
+ dists = matching.iou_distance(unconfirmed, detections)
327
+
328
+ dists = matching.fuse_score(dists, detections)
329
+ matches, u_unconfirmed, u_detection = matching.linear_assignment(
330
+ dists, thresh=0.7
331
+ )
332
+ for itracked, idet in matches:
333
+ itracked, idet = int(itracked.item()), int(idet.item())
334
+ unconfirmed[itracked].update(detections[idet], self.frame_id)
335
+ activated_starcks.append(unconfirmed[itracked])
336
+ for it in u_unconfirmed:
337
+ track = unconfirmed[it]
338
+ track.state = TrackState.Removed
339
+ removed_stracks.append(track)
340
+
341
+ """ Step 4: Init new stracks"""
342
+ for inew in u_detection:
343
+ track = detections[inew]
344
+ if track.score < self.det_thresh:
345
+ continue
346
+ track.activate(self.kalman_filter, self.frame_id)
347
+ activated_starcks.append(track)
348
+ """ Step 5: Update state"""
349
+ for track in self.lost_tracks:
350
+ if self.frame_id - track.frame_id > self.max_time_lost:
351
+ track.state = TrackState.Removed
352
+ removed_stracks.append(track)
353
+
354
+ self.tracked_tracks = [
355
+ t for t in self.tracked_tracks if t.state == TrackState.Tracked
356
+ ]
357
+ self.tracked_tracks = joint_tracks(self.tracked_tracks, activated_starcks)
358
+ self.tracked_tracks = joint_tracks(self.tracked_tracks, refind_stracks)
359
+ self.lost_tracks = sub_tracks(self.lost_tracks, self.tracked_tracks)
360
+ self.lost_tracks.extend(lost_stracks)
361
+ self.lost_tracks = sub_tracks(self.lost_tracks, self.removed_tracks)
362
+ self.removed_tracks = removed_stracks
363
+ self.tracked_tracks, self.lost_tracks = remove_duplicate_tracks(
364
+ self.tracked_tracks, self.lost_tracks
365
+ )
366
+ output_stracks = [track for track in self.tracked_tracks if track.is_activated]
367
+
368
+ return output_stracks
369
+
370
+
371
+ def joint_tracks(
372
+ track_list_a: list[STrack], track_list_b: list[STrack]
373
+ ) -> list[STrack]:
374
+ """
375
+ Joins two lists of tracks, ensuring that the resulting list does not
376
+ contain tracks with duplicate internal_track_id values.
377
+
378
+ Args:
379
+ track_list_a: First list of tracks.
380
+ track_list_b: Second list of tracks.
381
+
382
+ Returns:
383
+ Combined list of tracks from track_list_a and track_list_b
384
+ without duplicate internal_track_id values.
385
+ """
386
+ seen_track_ids = set()
387
+ result = []
388
+
389
+ for track in track_list_a + track_list_b:
390
+ if track.internal_track_id not in seen_track_ids:
391
+ seen_track_ids.add(track.internal_track_id)
392
+ result.append(track)
393
+
394
+ return result
395
+
396
+
397
+ def sub_tracks(track_list_a: list[STrack], track_list_b: list[STrack]) -> list[STrack]:
398
+ """
399
+ Returns a list of tracks from track_list_a after removing any tracks
400
+ that share the same internal_track_id with tracks in track_list_b.
401
+
402
+ Args:
403
+ track_list_a: List of tracks.
404
+ track_list_b: List of tracks to be subtracted from track_list_a.
405
+ Returns:
406
+ List of remaining tracks from track_list_a after subtraction.
407
+ """
408
+ tracks = {track.internal_track_id: track for track in track_list_a}
409
+ track_ids_b = {track.internal_track_id for track in track_list_b}
410
+
411
+ for track_id in track_ids_b:
412
+ tracks.pop(track_id, None)
413
+
414
+ return list(tracks.values())
415
+
416
+
417
+ def remove_duplicate_tracks(
418
+ tracks_a: list[STrack], tracks_b: list[STrack]
419
+ ) -> tuple[list[STrack], list[STrack]]:
420
+ pairwise_distance = matching.iou_distance(tracks_a, tracks_b)
421
+ matching_pairs = (
422
+ torch.where(pairwise_distance < 0.05)
423
+ if isinstance(pairwise_distance, torch.Tensor)
424
+ else np.where(pairwise_distance < 0.05)
425
+ )
426
+
427
+ duplicates_a, duplicates_b = set(), set()
428
+ for track_index_a, track_index_b in zip(*matching_pairs):
429
+ if isinstance(track_index_a, torch.Tensor):
430
+ track_index_a, track_index_b = (
431
+ int(track_index_a.item()),
432
+ int(track_index_b.item()),
433
+ )
434
+ time_a = tracks_a[track_index_a].frame_id - tracks_a[track_index_a].start_frame
435
+ time_b = tracks_b[track_index_b].frame_id - tracks_b[track_index_b].start_frame
436
+ if time_a > time_b:
437
+ duplicates_b.add(track_index_b)
438
+ else:
439
+ duplicates_a.add(track_index_a)
440
+
441
+ result_a = [
442
+ track for index, track in enumerate(tracks_a) if index not in duplicates_a
443
+ ]
444
+ result_b = [
445
+ track for index, track in enumerate(tracks_b) if index not in duplicates_b
446
+ ]
447
+
448
+ return result_a, result_b
@@ -0,0 +1,186 @@
1
+ import numpy as np
2
+ import numpy.typing as npt
3
+ import torch
4
+
5
+ TensorLike = npt.NDArray[np.floating] | torch.Tensor
6
+
7
+
8
+ def _as_float_tensor(
9
+ value: TensorLike,
10
+ *,
11
+ device: torch.device | None = None,
12
+ dtype: torch.dtype | None = None,
13
+ ) -> torch.Tensor:
14
+ tensor = torch.as_tensor(value, device=device)
15
+ if dtype is not None:
16
+ return tensor.to(dtype=dtype)
17
+ return tensor if tensor.is_floating_point() else tensor.to(dtype=torch.float32)
18
+
19
+
20
+ class KalmanFilter:
21
+ """
22
+ A simple Kalman filter for tracking bounding boxes in image space.
23
+
24
+ The 8-dimensional state space is (x, y, a, h, vx, vy, va, vh), where
25
+ (x, y) is the bounding box center, a is the aspect ratio (w/h), h is
26
+ the height, and their respective velocities.
27
+
28
+ Object motion follows a constant velocity model. The bounding box location
29
+ (x, y, a, h) is taken as direct observation of the state space (linear
30
+ observation model).
31
+ """
32
+
33
+ def __init__(self) -> None:
34
+ self._std_weight_position: float = 1.0 / 20
35
+ self._std_weight_velocity: float = 1.0 / 160
36
+
37
+ def initiate(self, measurement: TensorLike) -> tuple[torch.Tensor, torch.Tensor]:
38
+ """
39
+ Create track from unassociated measurement.
40
+
41
+ Args:
42
+ measurement: The initial measurement vector.
43
+
44
+ Returns:
45
+ The mean vector and covariance matrix of the new track.
46
+ """
47
+ measurement = _as_float_tensor(measurement)
48
+ mean = torch.cat((measurement, torch.zeros_like(measurement)))
49
+ std = torch.stack(
50
+ (
51
+ 2 * self._std_weight_position * measurement[3],
52
+ 2 * self._std_weight_position * measurement[3],
53
+ measurement.new_tensor(1e-2),
54
+ 2 * self._std_weight_position * measurement[3],
55
+ 10 * self._std_weight_velocity * measurement[3],
56
+ 10 * self._std_weight_velocity * measurement[3],
57
+ measurement.new_tensor(1e-5),
58
+ 10 * self._std_weight_velocity * measurement[3],
59
+ )
60
+ )
61
+ return mean, torch.diag(std.square())
62
+
63
+ def predict(
64
+ self, mean: TensorLike, covariance: TensorLike
65
+ ) -> tuple[torch.Tensor, torch.Tensor]:
66
+ """
67
+ Run Kalman filter prediction step.
68
+
69
+ Args:
70
+ mean: The object state mean at the previous time step.
71
+ covariance: The object state covariance at the previous time step.
72
+
73
+ Returns:
74
+ The mean vector and covariance matrix of the predicted state.
75
+ """
76
+ mean = _as_float_tensor(mean)
77
+ covariance = _as_float_tensor(covariance, device=mean.device, dtype=mean.dtype)
78
+ motion = torch.eye(8, dtype=mean.dtype, device=mean.device)
79
+ motion[range(4), range(4, 8)] = 1
80
+ std = torch.stack(
81
+ (
82
+ self._std_weight_position * mean[3],
83
+ self._std_weight_position * mean[3],
84
+ mean.new_tensor(1e-2),
85
+ self._std_weight_position * mean[3],
86
+ self._std_weight_velocity * mean[3],
87
+ self._std_weight_velocity * mean[3],
88
+ mean.new_tensor(1e-5),
89
+ self._std_weight_velocity * mean[3],
90
+ )
91
+ )
92
+ return mean @ motion.T, motion @ covariance @ motion.T + torch.diag(
93
+ std.square()
94
+ )
95
+
96
+ def project(
97
+ self, mean: TensorLike, covariance: TensorLike
98
+ ) -> tuple[torch.Tensor, torch.Tensor]:
99
+ """
100
+ Project state distribution to measurement space.
101
+
102
+ Args:
103
+ mean: The state's mean vector.
104
+ covariance: The state's covariance matrix.
105
+
106
+ Returns:
107
+ The projected mean and covariance matrix of the given state estimate.
108
+ """
109
+ mean = _as_float_tensor(mean)
110
+ covariance = _as_float_tensor(covariance, device=mean.device, dtype=mean.dtype)
111
+ update = torch.eye(4, 8, dtype=mean.dtype, device=mean.device)
112
+ std = torch.stack(
113
+ (
114
+ self._std_weight_position * mean[3],
115
+ self._std_weight_position * mean[3],
116
+ mean.new_tensor(1e-1),
117
+ self._std_weight_position * mean[3],
118
+ )
119
+ )
120
+ return update @ mean, update @ covariance @ update.T + torch.diag(std.square())
121
+
122
+ def multi_predict(
123
+ self, mean: TensorLike, covariance: TensorLike
124
+ ) -> tuple[torch.Tensor, torch.Tensor]:
125
+ """
126
+ Run Kalman filter prediction step (Vectorized version).
127
+ Args:
128
+ mean: The object state means at the previous time step.
129
+ covariance: The object state covariances at the previous time step.
130
+
131
+ Returns:
132
+ The mean vector and covariance matrix of the predicted state.
133
+ """
134
+ mean = _as_float_tensor(mean)
135
+ covariance = _as_float_tensor(covariance, device=mean.device, dtype=mean.dtype)
136
+ motion = torch.eye(8, dtype=mean.dtype, device=mean.device)
137
+ motion[range(4), range(4, 8)] = 1
138
+ height = mean[:, 3]
139
+ std = torch.stack(
140
+ (
141
+ self._std_weight_position * height,
142
+ self._std_weight_position * height,
143
+ torch.full_like(height, 1e-2),
144
+ self._std_weight_position * height,
145
+ self._std_weight_velocity * height,
146
+ self._std_weight_velocity * height,
147
+ torch.full_like(height, 1e-5),
148
+ self._std_weight_velocity * height,
149
+ ),
150
+ dim=1,
151
+ )
152
+ return (
153
+ mean @ motion.T,
154
+ motion @ covariance @ motion.T + torch.diag_embed(std.square()),
155
+ )
156
+
157
+ def update(
158
+ self,
159
+ mean: TensorLike,
160
+ covariance: TensorLike,
161
+ measurement: TensorLike,
162
+ ) -> tuple[torch.Tensor, torch.Tensor]:
163
+ """
164
+ Run Kalman filter correction step.
165
+
166
+ Args:
167
+ mean: The predicted state's mean vector.
168
+ covariance: The state's covariance matrix.
169
+ measurement: The measurement vector.
170
+
171
+ Returns:
172
+ The measurement-corrected state distribution.
173
+ """
174
+ mean = _as_float_tensor(mean)
175
+ covariance = _as_float_tensor(covariance, device=mean.device, dtype=mean.dtype)
176
+ measurement = _as_float_tensor(
177
+ measurement, device=mean.device, dtype=mean.dtype
178
+ )
179
+ projected_mean, projected_cov = self.project(mean, covariance)
180
+ update = torch.eye(4, 8, dtype=mean.dtype, device=mean.device)
181
+ kalman_gain = torch.linalg.solve(projected_cov, (covariance @ update.T).T).T
182
+ innovation = measurement - projected_mean
183
+ return (
184
+ mean + innovation @ kalman_gain.T,
185
+ covariance - kalman_gain @ projected_cov @ kalman_gain.T,
186
+ )