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,815 @@
1
+ from __future__ import annotations
2
+
3
+ from copy import deepcopy
4
+ from dataclasses import dataclass
5
+ from typing import TYPE_CHECKING, Any, cast
6
+
7
+ import numpy as np
8
+ import numpy.typing as npt
9
+
10
+ from supervision.config import ORIENTED_BOX_COORDINATES
11
+ from supervision.detection.core import Detections
12
+ from supervision.detection.utils.iou_and_nms import (
13
+ box_iou_batch,
14
+ mask_iou_batch,
15
+ oriented_box_iou_batch,
16
+ )
17
+ from supervision.draw.color import LEGACY_COLOR_PALETTE
18
+ from supervision.metrics.core import AveragingMethod, Metric, MetricTarget
19
+ from supervision.metrics.utils.matching import (
20
+ _match_detection_batch_with_target_indices,
21
+ )
22
+ from supervision.metrics.utils.object_size import (
23
+ ObjectSizeCategory,
24
+ get_detection_size_category,
25
+ )
26
+ from supervision.metrics.utils.utils import ensure_pandas_installed
27
+
28
+ if TYPE_CHECKING:
29
+ import pandas as pd
30
+
31
+
32
+ class Precision(Metric["PrecisionResult"]):
33
+ """
34
+ Precision is a metric used to evaluate object detection models. It is the ratio of
35
+ true positive detections to the total number of predicted detections. We calculate
36
+ it at different IoU thresholds.
37
+
38
+ In simple terms, Precision is a measure of a model's accuracy, calculated as:
39
+
40
+ `Precision = TP / (TP + FP)`
41
+
42
+ Here, `TP` is the number of true positives (correct detections), and `FP` is the
43
+ number of false positive detections (detected, but incorrectly).
44
+
45
+ Examples:
46
+ ```pycon
47
+ >>> import numpy as np
48
+ >>> import supervision as sv
49
+ >>> from supervision.metrics import Precision
50
+ >>> predictions = sv.Detections(
51
+ ... xyxy=np.array([[0, 0, 10, 10]]),
52
+ ... class_id=np.array([0]),
53
+ ... confidence=np.array([0.9])
54
+ ... )
55
+ >>> targets = sv.Detections(
56
+ ... xyxy=np.array([[0, 0, 10, 10]]),
57
+ ... class_id=np.array([0])
58
+ ... )
59
+ >>> precision_metric = Precision()
60
+ >>> precision_result = precision_metric.update(predictions, targets).compute()
61
+ >>> round(float(precision_result.precision_at_50), 2)
62
+ 1.0
63
+
64
+ ```
65
+
66
+ ![example_plot](
67
+ https://media.roboflow.com/supervision-docs/metrics/precision_plot_example.png
68
+ ){ align=center width="800" }
69
+ """
70
+
71
+ def __init__(
72
+ self,
73
+ metric_target: MetricTarget = MetricTarget.BOXES,
74
+ averaging_method: AveragingMethod = AveragingMethod.WEIGHTED,
75
+ ):
76
+ """
77
+ Initialize the Precision metric.
78
+
79
+ Args:
80
+ metric_target: The type of detection data to use.
81
+ averaging_method: The averaging method used to compute the
82
+ precision. Determines how the precision is aggregated across classes.
83
+ """
84
+ self._metric_target = metric_target
85
+ self.averaging_method = averaging_method
86
+
87
+ self._predictions_list: list[Detections] = []
88
+ self._targets_list: list[Detections] = []
89
+
90
+ def reset(self) -> None:
91
+ """
92
+ Reset the metric to its initial state, clearing all stored data.
93
+ """
94
+ self._predictions_list = []
95
+ self._targets_list = []
96
+
97
+ def update(
98
+ self,
99
+ predictions: Detections | list[Detections],
100
+ targets: Detections | list[Detections],
101
+ ) -> Precision:
102
+ """
103
+ Add new predictions and targets to the metric, but do not compute the result.
104
+
105
+ Args:
106
+ predictions: The predicted detections.
107
+ targets: The target detections.
108
+
109
+ Returns:
110
+ The updated metric instance.
111
+ """
112
+ if not isinstance(predictions, list):
113
+ predictions = [predictions]
114
+ if not isinstance(targets, list):
115
+ targets = [targets]
116
+
117
+ if len(predictions) != len(targets):
118
+ raise ValueError(
119
+ f"The number of predictions ({len(predictions)}) and"
120
+ f" targets ({len(targets)}) during the update must be the same."
121
+ )
122
+
123
+ self._predictions_list.extend(predictions)
124
+ self._targets_list.extend(targets)
125
+
126
+ return self
127
+
128
+ def compute(self) -> PrecisionResult:
129
+ """
130
+ Calculate the precision metric based on the stored predictions and ground-truth
131
+ data, at different IoU thresholds.
132
+
133
+ Returns:
134
+ The precision metric result.
135
+ """
136
+ result = self._compute(self._predictions_list, self._targets_list)
137
+ result.small_objects = self._compute(
138
+ self._predictions_list, self._targets_list, ObjectSizeCategory.SMALL
139
+ )
140
+ result.medium_objects = self._compute(
141
+ self._predictions_list, self._targets_list, ObjectSizeCategory.MEDIUM
142
+ )
143
+ result.large_objects = self._compute(
144
+ self._predictions_list, self._targets_list, ObjectSizeCategory.LARGE
145
+ )
146
+
147
+ return result
148
+
149
+ def _compute(
150
+ self,
151
+ predictions_list: list[Detections],
152
+ targets_list: list[Detections],
153
+ size_category: ObjectSizeCategory = ObjectSizeCategory.ANY,
154
+ ) -> PrecisionResult:
155
+ """Build per-image stats tuples and delegate to class-level computation.
156
+
157
+ Each stats tuple is
158
+ ``(matches, ignored_matches, confidence, class_ids, true_class_ids)``:
159
+ - Both empty: skip (no information).
160
+ - Targets empty, predictions present: all predictions are FPs; true_class_ids
161
+ is ``zeros((0,))``.
162
+ - Targets present: IoU matching produces ``matches`` array.
163
+ """
164
+ if size_category != ObjectSizeCategory.ANY:
165
+ # Score the requested bucket on bucket-filtered targets so detections
166
+ # outside the bucket cannot consume the only available target.
167
+ targets_list = [
168
+ self._filter_detections_by_size(targets, size_category)
169
+ for targets in targets_list
170
+ ]
171
+ size_category = ObjectSizeCategory.ANY
172
+
173
+ iou_thresholds = np.linspace(0.5, 0.95, 10, dtype=np.float32)
174
+ stats: list[Any] = []
175
+
176
+ for predictions, targets in zip(predictions_list, targets_list):
177
+ prediction_contents = self._detections_content(predictions)
178
+ target_contents = self._detections_content(targets)
179
+ prediction_size_mask = np.ones(len(predictions), dtype=bool)
180
+ target_size_mask = np.ones(len(targets), dtype=bool)
181
+ if size_category != ObjectSizeCategory.ANY:
182
+ if len(predictions) > 0:
183
+ prediction_size_mask = (
184
+ get_detection_size_category(predictions, self._metric_target)
185
+ == size_category.value
186
+ )
187
+ if len(targets) > 0:
188
+ target_size_mask = (
189
+ get_detection_size_category(targets, self._metric_target)
190
+ == size_category.value
191
+ )
192
+
193
+ if len(targets) == 0 and len(predictions) > 0:
194
+ # Only predictions are present (e.g. a background image); every
195
+ # prediction is a false positive.
196
+ if predictions.class_id is None or predictions.confidence is None:
197
+ raise ValueError(
198
+ "Precision metric requires `class_id` and `confidence` "
199
+ "on predictions."
200
+ )
201
+ prediction_class_ids = np.asarray(predictions.class_id, dtype=np.int32)[
202
+ prediction_size_mask
203
+ ]
204
+ prediction_confidence = np.asarray(
205
+ predictions.confidence, dtype=np.float32
206
+ )[prediction_size_mask]
207
+ if len(prediction_class_ids) == 0:
208
+ continue
209
+ stats.append(
210
+ (
211
+ np.zeros(
212
+ (len(prediction_class_ids), iou_thresholds.size),
213
+ dtype=np.bool_,
214
+ ),
215
+ np.zeros(
216
+ (len(prediction_class_ids), iou_thresholds.size),
217
+ dtype=np.bool_,
218
+ ),
219
+ prediction_confidence,
220
+ prediction_class_ids,
221
+ np.zeros((0,), dtype=np.int32),
222
+ )
223
+ )
224
+ elif len(targets) > 0:
225
+ if predictions.class_id is None or targets.class_id is None:
226
+ raise ValueError(
227
+ "Precision metric requires `class_id` on both predictions "
228
+ "and targets."
229
+ )
230
+ if len(predictions) == 0:
231
+ target_class_ids = np.asarray(targets.class_id, dtype=np.int32)[
232
+ target_size_mask
233
+ ]
234
+ if len(target_class_ids) == 0:
235
+ continue
236
+ stats.append(
237
+ (
238
+ np.zeros((0, iou_thresholds.size), dtype=bool),
239
+ np.zeros((0, iou_thresholds.size), dtype=bool),
240
+ np.zeros((0,), dtype=np.float32),
241
+ np.zeros((0,), dtype=int),
242
+ target_class_ids,
243
+ )
244
+ )
245
+
246
+ else:
247
+ if predictions.confidence is None:
248
+ raise ValueError(
249
+ "Precision metric requires `confidence` on predictions."
250
+ )
251
+ prediction_class_ids = np.asarray(
252
+ predictions.class_id, dtype=np.int32
253
+ )
254
+ target_class_ids = np.asarray(targets.class_id, dtype=np.int32)
255
+ prediction_confidence = np.asarray(
256
+ predictions.confidence, dtype=np.float32
257
+ )
258
+ if self._metric_target == MetricTarget.BOXES:
259
+ iou = box_iou_batch(target_contents, prediction_contents)
260
+ elif self._metric_target == MetricTarget.MASKS:
261
+ iou = mask_iou_batch(target_contents, prediction_contents)
262
+ elif self._metric_target == MetricTarget.ORIENTED_BOUNDING_BOXES:
263
+ iou = oriented_box_iou_batch(
264
+ target_contents, prediction_contents
265
+ )
266
+ else:
267
+ raise ValueError(
268
+ "Unsupported metric target for IoU calculation"
269
+ )
270
+
271
+ matches, matched_target_indices = (
272
+ _match_detection_batch_with_target_indices(
273
+ prediction_class_ids,
274
+ target_class_ids,
275
+ iou,
276
+ iou_thresholds,
277
+ )
278
+ )
279
+ ignored_matches = np.zeros_like(matches, dtype=bool)
280
+ if size_category != ObjectSizeCategory.ANY:
281
+ valid_target_match = matched_target_indices >= 0
282
+ matched_scored_target = np.zeros_like(matches, dtype=bool)
283
+ if np.any(valid_target_match):
284
+ matched_scored_target[valid_target_match] = (
285
+ target_size_mask[
286
+ matched_target_indices[valid_target_match]
287
+ ]
288
+ )
289
+ prediction_scored = (
290
+ prediction_size_mask[:, None] | matched_scored_target
291
+ )
292
+ ignored_matches = ~prediction_scored | (
293
+ valid_target_match & ~matched_scored_target
294
+ )
295
+ prediction_keep = np.any(~ignored_matches, axis=1)
296
+ matches = (
297
+ matches[prediction_keep]
298
+ & matched_scored_target[prediction_keep]
299
+ )
300
+ ignored_matches = ignored_matches[prediction_keep]
301
+ prediction_confidence = prediction_confidence[prediction_keep]
302
+ prediction_class_ids = prediction_class_ids[prediction_keep]
303
+ target_class_ids = target_class_ids[target_size_mask]
304
+ if (
305
+ len(prediction_class_ids) == 0
306
+ and len(target_class_ids) == 0
307
+ ):
308
+ continue
309
+ stats.append(
310
+ (
311
+ matches,
312
+ ignored_matches,
313
+ prediction_confidence,
314
+ prediction_class_ids,
315
+ target_class_ids,
316
+ )
317
+ )
318
+
319
+ if not stats:
320
+ return PrecisionResult(
321
+ metric_target=self._metric_target,
322
+ averaging_method=self.averaging_method,
323
+ precision_scores=np.zeros(iou_thresholds.shape[0]),
324
+ precision_per_class=np.zeros((0, iou_thresholds.shape[0])),
325
+ iou_thresholds=iou_thresholds,
326
+ matched_classes=np.array([], dtype=int),
327
+ small_objects=None,
328
+ medium_objects=None,
329
+ large_objects=None,
330
+ )
331
+
332
+ concatenated_stats = [np.concatenate(items, 0) for items in zip(*stats)]
333
+ precision_scores, precision_per_class, unique_classes = (
334
+ self._compute_precision_for_classes(*concatenated_stats)
335
+ )
336
+
337
+ return PrecisionResult(
338
+ metric_target=self._metric_target,
339
+ averaging_method=self.averaging_method,
340
+ precision_scores=precision_scores,
341
+ precision_per_class=precision_per_class,
342
+ iou_thresholds=iou_thresholds,
343
+ matched_classes=unique_classes,
344
+ small_objects=None,
345
+ medium_objects=None,
346
+ large_objects=None,
347
+ )
348
+
349
+ def _compute_precision_for_classes(
350
+ self,
351
+ matches: npt.NDArray[np.bool_],
352
+ ignored_matches: npt.NDArray[np.bool_],
353
+ prediction_confidence: npt.NDArray[np.float32],
354
+ prediction_class_ids: npt.NDArray[np.int32],
355
+ true_class_ids: npt.NDArray[np.int32],
356
+ ) -> tuple[
357
+ npt.NDArray[np.float64],
358
+ npt.NDArray[np.float64],
359
+ npt.NDArray[np.int32],
360
+ ]:
361
+ """Compute precision scores from concatenated stats across all images.
362
+
363
+ ``unique_classes`` is the union of GT and predicted classes so that
364
+ predictions of classes absent from GT still count as false positives.
365
+ """
366
+ sorted_indices = np.argsort(-prediction_confidence)
367
+ matches = matches[sorted_indices]
368
+ ignored_matches = ignored_matches[sorted_indices]
369
+ prediction_class_ids = prediction_class_ids[sorted_indices]
370
+ # Predictions whose class never appears in the ground truth are still
371
+ # false positives, so include those classes in the confusion matrix
372
+ # (their true-instance count is zero).
373
+ unique_classes = np.unique(
374
+ np.concatenate((true_class_ids, prediction_class_ids))
375
+ )
376
+ true_classes, true_counts = np.unique(true_class_ids, return_counts=True)
377
+ class_counts = np.zeros(unique_classes.shape[0], dtype=int)
378
+ class_counts[np.searchsorted(unique_classes, true_classes)] = true_counts
379
+
380
+ # Shape: PxTh,P,C,C -> CxThx3
381
+ confusion_matrix = self._compute_confusion_matrix(
382
+ matches, ignored_matches, prediction_class_ids, unique_classes, class_counts
383
+ )
384
+
385
+ # Shape: CxThx3 -> CxTh
386
+ precision_per_class = self._compute_precision(confusion_matrix)
387
+
388
+ # Shape: CxTh -> Th
389
+ if self.averaging_method == AveragingMethod.MACRO:
390
+ precision_scores = np.mean(precision_per_class, axis=0)
391
+ elif self.averaging_method == AveragingMethod.MICRO:
392
+ confusion_matrix_merged = confusion_matrix.sum(0)
393
+ precision_scores = self._compute_precision(confusion_matrix_merged)
394
+ elif self.averaging_method == AveragingMethod.WEIGHTED:
395
+ class_counts = class_counts.astype(np.float32)
396
+ if class_counts.sum() == 0:
397
+ # No ground-truth support (e.g. only false-positive classes, or a
398
+ # size bucket with predictions but no targets): weighting is
399
+ # undefined, so report 0 as the empty case did before.
400
+ precision_scores = np.zeros(precision_per_class.shape[1])
401
+ else:
402
+ precision_scores = np.average(
403
+ precision_per_class, axis=0, weights=class_counts
404
+ )
405
+
406
+ return precision_scores, precision_per_class, unique_classes
407
+
408
+ @staticmethod
409
+ def _match_detection_batch(
410
+ predictions_classes: npt.NDArray[np.int32],
411
+ target_classes: npt.NDArray[np.int32],
412
+ iou: npt.NDArray[np.float32],
413
+ iou_thresholds: npt.NDArray[np.float32],
414
+ ) -> npt.NDArray[np.bool_]:
415
+ result_correct, _ = _match_detection_batch_with_target_indices(
416
+ predictions_classes, target_classes, iou, iou_thresholds
417
+ )
418
+ return result_correct
419
+
420
+ @staticmethod
421
+ def _compute_confusion_matrix(
422
+ sorted_matches: npt.NDArray[np.bool_],
423
+ sorted_ignored_matches: npt.NDArray[np.bool_],
424
+ sorted_prediction_class_ids: npt.NDArray[np.int32],
425
+ unique_classes: npt.NDArray[np.int32],
426
+ class_counts: npt.NDArray[np.int32],
427
+ ) -> npt.NDArray[np.float64]:
428
+ """
429
+ Compute the confusion matrix for each class and IoU threshold.
430
+
431
+ Assumes the matches and prediction_class_ids are sorted by confidence
432
+ in descending order.
433
+
434
+ Args:
435
+ sorted_matches: shape (P, Th), that is True
436
+ if the prediction is a true positive at the given IoU threshold.
437
+ sorted_ignored_matches: shape (P, Th), that is True
438
+ if the prediction should not affect the given IoU threshold.
439
+ sorted_prediction_class_ids: shape (P,), containing
440
+ the class id for each prediction.
441
+ unique_classes: shape (C,), containing the unique
442
+ class ids.
443
+ class_counts: shape (C,), containing the number
444
+ of true instances for each class.
445
+
446
+ Returns:
447
+ shape (C, Th, 3), containing the true positives, false
448
+ positives, and false negatives for each class and IoU threshold.
449
+ """
450
+
451
+ num_thresholds = sorted_matches.shape[1]
452
+ num_classes = unique_classes.shape[0]
453
+
454
+ confusion_matrix: npt.NDArray[np.float64] = np.zeros(
455
+ (num_classes, num_thresholds, 3), dtype=np.float64
456
+ )
457
+ for class_idx, class_id in enumerate(unique_classes):
458
+ is_class = sorted_prediction_class_ids == class_id
459
+ num_true = class_counts[class_idx]
460
+ num_predictions = is_class.sum()
461
+
462
+ if num_predictions == 0:
463
+ true_positives = np.zeros(num_thresholds)
464
+ false_positives = np.zeros(num_thresholds)
465
+ false_negatives = np.full(num_thresholds, num_true)
466
+ elif num_true == 0:
467
+ true_positives = np.zeros(num_thresholds)
468
+ false_positives = (~sorted_ignored_matches[is_class]).sum(0)
469
+ false_negatives = np.zeros(num_thresholds)
470
+ else:
471
+ true_positives = sorted_matches[is_class].sum(0)
472
+ false_positives = (
473
+ ~sorted_matches[is_class] & ~sorted_ignored_matches[is_class]
474
+ ).sum(0)
475
+ false_negatives = num_true - true_positives
476
+ confusion_matrix[class_idx] = np.stack(
477
+ [true_positives, false_positives, false_negatives], axis=1
478
+ )
479
+ result_matrix: npt.NDArray[np.float64] = confusion_matrix
480
+ return result_matrix
481
+
482
+ @staticmethod
483
+ def _compute_precision(
484
+ confusion_matrix: npt.NDArray[np.float64],
485
+ ) -> npt.NDArray[np.float64]:
486
+ """
487
+ Broadcastable function, computing the precision from the confusion matrix.
488
+
489
+ Args:
490
+ confusion_matrix: shape (N, ..., 3), where the last dimension
491
+ contains the true positives, false positives, and false negatives.
492
+
493
+ Returns:
494
+ shape (N, ...), containing the precision for each element.
495
+ """
496
+ if not confusion_matrix.shape[-1] == 3:
497
+ raise ValueError(
498
+ f"Confusion matrix must have shape (..., 3), got "
499
+ f"{confusion_matrix.shape}"
500
+ )
501
+ true_positives = confusion_matrix[..., 0]
502
+ false_positives = confusion_matrix[..., 1]
503
+
504
+ denominator = true_positives + false_positives
505
+ precision = np.divide(
506
+ true_positives,
507
+ denominator,
508
+ out=np.zeros_like(true_positives),
509
+ where=denominator != 0,
510
+ )
511
+
512
+ result_precision: npt.NDArray[np.float64] = precision
513
+ return result_precision
514
+
515
+ def _detections_content(self, detections: Detections) -> npt.NDArray[Any]:
516
+ """Return boxes, masks or oriented bounding boxes from detections."""
517
+ if self._metric_target == MetricTarget.BOXES:
518
+ return cast(npt.NDArray[Any], detections.xyxy)
519
+ if self._metric_target == MetricTarget.MASKS:
520
+ if detections.mask is not None:
521
+ return cast(npt.NDArray[Any], detections.mask)
522
+ if len(detections) > 0:
523
+ raise ValueError(
524
+ "Precision with `MetricTarget.MASKS` requires detections to "
525
+ "include masks."
526
+ )
527
+ return self._make_empty_content()
528
+ if self._metric_target == MetricTarget.ORIENTED_BOUNDING_BOXES:
529
+ obb = detections.data.get(ORIENTED_BOX_COORDINATES)
530
+ if obb is not None and len(obb) > 0:
531
+ result_obb: npt.NDArray[np.float32] = np.array(obb, dtype=np.float32)
532
+ return result_obb
533
+ return self._make_empty_content()
534
+ raise ValueError(f"Invalid metric target: {self._metric_target}")
535
+
536
+ def _make_empty_content(self) -> npt.NDArray[Any]:
537
+ if self._metric_target == MetricTarget.BOXES:
538
+ empty_boxes: npt.NDArray[np.float32] = np.empty((0, 4), dtype=np.float32)
539
+ return empty_boxes
540
+
541
+ if self._metric_target == MetricTarget.MASKS:
542
+ empty_masks: npt.NDArray[np.bool_] = np.empty((0, 0, 0), dtype=bool)
543
+ return empty_masks
544
+
545
+ if self._metric_target == MetricTarget.ORIENTED_BOUNDING_BOXES:
546
+ empty_obb: npt.NDArray[np.float32] = np.empty((0, 4, 2), dtype=np.float32)
547
+ return empty_obb
548
+
549
+ raise ValueError(f"Invalid metric target: {self._metric_target}")
550
+
551
+ def _filter_detections_by_size(
552
+ self, detections: Detections, size_category: ObjectSizeCategory
553
+ ) -> Detections:
554
+ """Return a copy of detections with contents filtered by object size."""
555
+ new_detections = deepcopy(detections)
556
+ if detections.is_empty() or size_category == ObjectSizeCategory.ANY:
557
+ return new_detections
558
+
559
+ sizes = get_detection_size_category(new_detections, self._metric_target)
560
+ size_mask = sizes == size_category.value
561
+
562
+ new_detections.xyxy = new_detections.xyxy[size_mask]
563
+ if new_detections.mask is not None:
564
+ new_detections.mask = new_detections.mask[size_mask]
565
+ if new_detections.class_id is not None:
566
+ new_detections.class_id = new_detections.class_id[size_mask]
567
+ if new_detections.confidence is not None:
568
+ new_detections.confidence = new_detections.confidence[size_mask]
569
+ if new_detections.tracker_id is not None:
570
+ new_detections.tracker_id = new_detections.tracker_id[size_mask]
571
+ if new_detections.data is not None:
572
+ for key, value in new_detections.data.items():
573
+ new_detections.data[key] = np.array(value)[size_mask]
574
+
575
+ return new_detections
576
+
577
+ def _filter_predictions_and_targets_by_size(
578
+ self,
579
+ predictions_list: list[Detections],
580
+ targets_list: list[Detections],
581
+ size_category: ObjectSizeCategory,
582
+ ) -> tuple[list[Detections], list[Detections]]:
583
+ """
584
+ Filter predictions and targets by object size category.
585
+ """
586
+ new_predictions_list = []
587
+ new_targets_list = []
588
+ for predictions, targets in zip(predictions_list, targets_list):
589
+ new_predictions_list.append(
590
+ self._filter_detections_by_size(predictions, size_category)
591
+ )
592
+ new_targets_list.append(
593
+ self._filter_detections_by_size(targets, size_category)
594
+ )
595
+ return new_predictions_list, new_targets_list
596
+
597
+
598
+ @dataclass
599
+ class PrecisionResult:
600
+ """
601
+ The results of the precision metric calculation.
602
+
603
+ Defaults to `0` if no detections or targets were provided.
604
+
605
+ Attributes:
606
+ metric_target: the type of data used for the metric -
607
+ boxes, masks or oriented bounding boxes.
608
+ averaging_method: the averaging method used to compute the
609
+ precision. Determines how the precision is aggregated across classes.
610
+ precision_at_50: the precision at IoU threshold of `0.5`.
611
+ precision_at_75: the precision at IoU threshold of `0.75`.
612
+ precision_scores: the precision scores at each IoU threshold.
613
+ Shape: `(num_iou_thresholds,)`
614
+ precision_per_class: the precision scores per class and
615
+ IoU threshold. Shape: `(num_classes, num_iou_thresholds)`
616
+ iou_thresholds: the IoU thresholds used in the calculations.
617
+ matched_classes: the class IDs present in either predictions or ground
618
+ truth. Corresponds to the rows of `precision_per_class`. Classes
619
+ that appear only in predictions (no ground-truth instances) are
620
+ included; their per-threshold precision values will be `0.0`.
621
+ small_objects: the Precision metric results
622
+ for small objects (area < 32²).
623
+ medium_objects: the Precision metric results
624
+ for medium objects (32² ≤ area < 96²).
625
+ large_objects: the Precision metric results
626
+ for large objects (area ≥ 96²).
627
+ """
628
+
629
+ metric_target: MetricTarget
630
+ averaging_method: AveragingMethod
631
+
632
+ @property
633
+ def precision_at_50(self) -> float:
634
+ return float(self.precision_scores[0])
635
+
636
+ @property
637
+ def precision_at_75(self) -> float:
638
+ return float(self.precision_scores[5])
639
+
640
+ precision_scores: npt.NDArray[np.float64]
641
+ precision_per_class: npt.NDArray[np.float64]
642
+ iou_thresholds: npt.NDArray[np.float32]
643
+ matched_classes: npt.NDArray[np.int32]
644
+
645
+ small_objects: PrecisionResult | None
646
+ medium_objects: PrecisionResult | None
647
+ large_objects: PrecisionResult | None
648
+
649
+ def __str__(self) -> str:
650
+ """
651
+ Format as a pretty string.
652
+
653
+ Example:
654
+ ```pycon
655
+ >>> import numpy as np
656
+ >>> import supervision as sv
657
+ >>> from supervision.metrics import Precision
658
+ >>> predictions = sv.Detections(
659
+ ... xyxy=np.array([[0, 0, 10, 10]]),
660
+ ... class_id=np.array([0]),
661
+ ... confidence=np.array([0.9])
662
+ ... )
663
+ >>> targets = sv.Detections(
664
+ ... xyxy=np.array([[0, 0, 10, 10]]),
665
+ ... class_id=np.array([0])
666
+ ... )
667
+ >>> precision_metric = Precision()
668
+ >>> precision_result = precision_metric.update(
669
+ ... predictions, targets
670
+ ... ).compute()
671
+ >>> print(precision_result) # doctest: +ELLIPSIS
672
+ PrecisionResult:
673
+ Metric target: MetricTarget.BOXES
674
+ Averaging method: AveragingMethod.WEIGHTED
675
+ P @ 50: 1.0000
676
+ P @ 75: 1.0000
677
+ P @ thresh: [1. ... 1.]
678
+ IoU thresh: [0.5 0.55 ... 0.95]
679
+ Precision per class:
680
+ 0: [1. ... 1.]
681
+ ...
682
+ Medium objects:
683
+ PrecisionResult:
684
+ Metric target: MetricTarget.BOXES
685
+ Averaging method: AveragingMethod.WEIGHTED
686
+ P @ 50: 0.0000
687
+ ...
688
+
689
+ ```
690
+ """
691
+ out_str = (
692
+ f"{self.__class__.__name__}:\n"
693
+ f"Metric target: {self.metric_target}\n"
694
+ f"Averaging method: {self.averaging_method}\n"
695
+ f"P @ 50: {self.precision_at_50:.4f}\n"
696
+ f"P @ 75: {self.precision_at_75:.4f}\n"
697
+ f"P @ thresh: {self.precision_scores}\n"
698
+ f"IoU thresh: {self.iou_thresholds}\n"
699
+ f"Precision per class:\n"
700
+ )
701
+ if self.precision_per_class.size == 0:
702
+ out_str += " No results\n"
703
+ for class_id, precision_of_class in zip(
704
+ self.matched_classes, self.precision_per_class
705
+ ):
706
+ out_str += f" {class_id}: {precision_of_class}\n"
707
+
708
+ indent = " "
709
+ if self.small_objects is not None:
710
+ indented = indent + str(self.small_objects).replace("\n", f"\n{indent}")
711
+ out_str += f"\nSmall objects:\n{indented}"
712
+ if self.medium_objects is not None:
713
+ indented = indent + str(self.medium_objects).replace("\n", f"\n{indent}")
714
+ out_str += f"\nMedium objects:\n{indented}"
715
+ if self.large_objects is not None:
716
+ indented = indent + str(self.large_objects).replace("\n", f"\n{indent}")
717
+ out_str += f"\nLarge objects:\n{indented}"
718
+
719
+ return out_str
720
+
721
+ def to_pandas(self) -> pd.DataFrame:
722
+ """
723
+ Convert the result to a pandas DataFrame.
724
+
725
+ Returns:
726
+ The result as a DataFrame.
727
+ """
728
+ ensure_pandas_installed()
729
+ import pandas as pd
730
+
731
+ pandas_data = {
732
+ "P@50": self.precision_at_50,
733
+ "P@75": self.precision_at_75,
734
+ }
735
+
736
+ if self.small_objects is not None:
737
+ small_objects_df = self.small_objects.to_pandas()
738
+ for key, value in small_objects_df.items():
739
+ pandas_data[f"small_objects_{key}"] = value
740
+ if self.medium_objects is not None:
741
+ medium_objects_df = self.medium_objects.to_pandas()
742
+ for key, value in medium_objects_df.items():
743
+ pandas_data[f"medium_objects_{key}"] = value
744
+ if self.large_objects is not None:
745
+ large_objects_df = self.large_objects.to_pandas()
746
+ for key, value in large_objects_df.items():
747
+ pandas_data[f"large_objects_{key}"] = value
748
+
749
+ return pd.DataFrame(pandas_data, index=[0])
750
+
751
+ def plot(self) -> None:
752
+ """
753
+ Plot the precision results.
754
+
755
+ ![example_plot](
756
+ https://media.roboflow.com/supervision-docs/metrics/precision_plot_example.png
757
+ ){ align=center width="800" }
758
+ """
759
+
760
+ from matplotlib import pyplot as plt
761
+
762
+ labels = ["Precision@50", "Precision@75"]
763
+ values = [self.precision_at_50, self.precision_at_75]
764
+ colors = [LEGACY_COLOR_PALETTE[0]] * 2
765
+
766
+ if self.small_objects is not None:
767
+ small_objects = self.small_objects
768
+ labels += ["Small: P@50", "Small: P@75"]
769
+ values += [small_objects.precision_at_50, small_objects.precision_at_75]
770
+ colors += [LEGACY_COLOR_PALETTE[3]] * 2
771
+
772
+ if self.medium_objects is not None:
773
+ medium_objects = self.medium_objects
774
+ labels += ["Medium: P@50", "Medium: P@75"]
775
+ values += [medium_objects.precision_at_50, medium_objects.precision_at_75]
776
+ colors += [LEGACY_COLOR_PALETTE[2]] * 2
777
+
778
+ if self.large_objects is not None:
779
+ large_objects = self.large_objects
780
+ labels += ["Large: P@50", "Large: P@75"]
781
+ values += [large_objects.precision_at_50, large_objects.precision_at_75]
782
+ colors += [LEGACY_COLOR_PALETTE[4]] * 2
783
+
784
+ plt.rcParams["font.family"] = "monospace"
785
+
786
+ _, ax = plt.subplots(figsize=(10, 6))
787
+ ax.set_ylim(0, 1)
788
+ ax.set_ylabel("Value", fontweight="bold")
789
+ title = (
790
+ f"Precision, by Object Size"
791
+ f"\n(target: {self.metric_target.value},"
792
+ f" averaging: {self.averaging_method.value})"
793
+ )
794
+ ax.set_title(title, fontweight="bold")
795
+
796
+ x_positions = range(len(labels))
797
+ bars = ax.bar(x_positions, values, color=colors, align="center")
798
+
799
+ ax.set_xticks(x_positions)
800
+ ax.set_xticklabels(labels, rotation=45, ha="right")
801
+
802
+ for bar in bars:
803
+ y_value = bar.get_height()
804
+ ax.text(
805
+ bar.get_x() + bar.get_width() / 2,
806
+ y_value + 0.02,
807
+ f"{y_value:.2f}",
808
+ ha="center",
809
+ va="bottom",
810
+ )
811
+
812
+ plt.rcParams["font.family"] = "sans-serif"
813
+
814
+ plt.tight_layout()
815
+ plt.show()