eye-cv 1.0.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.
Files changed (94) hide show
  1. eye/__init__.py +115 -0
  2. eye/__init___supervision_original.py +120 -0
  3. eye/annotators/__init__.py +0 -0
  4. eye/annotators/base.py +22 -0
  5. eye/annotators/core.py +2699 -0
  6. eye/annotators/line.py +107 -0
  7. eye/annotators/modern.py +529 -0
  8. eye/annotators/trace.py +142 -0
  9. eye/annotators/utils.py +177 -0
  10. eye/assets/__init__.py +2 -0
  11. eye/assets/downloader.py +95 -0
  12. eye/assets/list.py +83 -0
  13. eye/classification/__init__.py +0 -0
  14. eye/classification/core.py +188 -0
  15. eye/config.py +2 -0
  16. eye/core/__init__.py +0 -0
  17. eye/core/trackers/__init__.py +1 -0
  18. eye/core/trackers/botsort_tracker.py +336 -0
  19. eye/core/trackers/bytetrack_tracker.py +284 -0
  20. eye/core/trackers/sort_tracker.py +200 -0
  21. eye/core/tracking.py +146 -0
  22. eye/dataset/__init__.py +0 -0
  23. eye/dataset/core.py +919 -0
  24. eye/dataset/formats/__init__.py +0 -0
  25. eye/dataset/formats/coco.py +258 -0
  26. eye/dataset/formats/pascal_voc.py +279 -0
  27. eye/dataset/formats/yolo.py +272 -0
  28. eye/dataset/utils.py +259 -0
  29. eye/detection/__init__.py +0 -0
  30. eye/detection/auto_convert.py +155 -0
  31. eye/detection/core.py +1529 -0
  32. eye/detection/detections_enhanced.py +392 -0
  33. eye/detection/line_zone.py +859 -0
  34. eye/detection/lmm.py +184 -0
  35. eye/detection/overlap_filter.py +270 -0
  36. eye/detection/tools/__init__.py +0 -0
  37. eye/detection/tools/csv_sink.py +181 -0
  38. eye/detection/tools/inference_slicer.py +288 -0
  39. eye/detection/tools/json_sink.py +142 -0
  40. eye/detection/tools/polygon_zone.py +202 -0
  41. eye/detection/tools/smoother.py +123 -0
  42. eye/detection/tools/smoothing.py +179 -0
  43. eye/detection/tools/smoothing_config.py +202 -0
  44. eye/detection/tools/transformers.py +247 -0
  45. eye/detection/utils.py +1175 -0
  46. eye/draw/__init__.py +0 -0
  47. eye/draw/color.py +154 -0
  48. eye/draw/utils.py +374 -0
  49. eye/filters.py +112 -0
  50. eye/geometry/__init__.py +0 -0
  51. eye/geometry/core.py +128 -0
  52. eye/geometry/utils.py +47 -0
  53. eye/keypoint/__init__.py +0 -0
  54. eye/keypoint/annotators.py +442 -0
  55. eye/keypoint/core.py +687 -0
  56. eye/keypoint/skeletons.py +2647 -0
  57. eye/metrics/__init__.py +21 -0
  58. eye/metrics/core.py +72 -0
  59. eye/metrics/detection.py +843 -0
  60. eye/metrics/f1_score.py +648 -0
  61. eye/metrics/mean_average_precision.py +628 -0
  62. eye/metrics/mean_average_recall.py +697 -0
  63. eye/metrics/precision.py +653 -0
  64. eye/metrics/recall.py +652 -0
  65. eye/metrics/utils/__init__.py +0 -0
  66. eye/metrics/utils/object_size.py +158 -0
  67. eye/metrics/utils/utils.py +9 -0
  68. eye/py.typed +0 -0
  69. eye/quick.py +104 -0
  70. eye/tracker/__init__.py +0 -0
  71. eye/tracker/byte_tracker/__init__.py +0 -0
  72. eye/tracker/byte_tracker/core.py +386 -0
  73. eye/tracker/byte_tracker/kalman_filter.py +205 -0
  74. eye/tracker/byte_tracker/matching.py +69 -0
  75. eye/tracker/byte_tracker/single_object_track.py +178 -0
  76. eye/tracker/byte_tracker/utils.py +18 -0
  77. eye/utils/__init__.py +0 -0
  78. eye/utils/conversion.py +132 -0
  79. eye/utils/file.py +159 -0
  80. eye/utils/image.py +794 -0
  81. eye/utils/internal.py +200 -0
  82. eye/utils/iterables.py +84 -0
  83. eye/utils/notebook.py +114 -0
  84. eye/utils/video.py +307 -0
  85. eye/utils_eye/__init__.py +1 -0
  86. eye/utils_eye/geometry.py +71 -0
  87. eye/utils_eye/nms.py +55 -0
  88. eye/validators/__init__.py +140 -0
  89. eye/web.py +271 -0
  90. eye_cv-1.0.0.dist-info/METADATA +319 -0
  91. eye_cv-1.0.0.dist-info/RECORD +94 -0
  92. eye_cv-1.0.0.dist-info/WHEEL +5 -0
  93. eye_cv-1.0.0.dist-info/licenses/LICENSE +21 -0
  94. eye_cv-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,843 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Callable, List, Optional, Tuple
5
+
6
+ import matplotlib
7
+ import matplotlib.pyplot as plt
8
+ import numpy as np
9
+
10
+ from eye.dataset.core import DetectionDataset
11
+ from eye.detection.core import Detections
12
+ from eye.detection.utils import box_iou_batch
13
+
14
+
15
+ def detections_to_tensor(
16
+ detections: Detections, with_confidence: bool = False
17
+ ) -> np.ndarray:
18
+ """
19
+ Convert eye Detections to numpy tensors for further computation
20
+ Args:
21
+ detections (sv.Detections): Detections/Targets in the format of sv.Detections
22
+ with_confidence (bool): Whether to include confidence in the tensor
23
+ Returns:
24
+ (np.ndarray): Detections as numpy tensors as in (xyxy, class_id,
25
+ confidence) order
26
+ """
27
+ if detections.class_id is None:
28
+ raise ValueError(
29
+ "ConfusionMatrix can only be calculated for Detections with class_id"
30
+ )
31
+
32
+ arrays_to_concat = [detections.xyxy, np.expand_dims(detections.class_id, 1)]
33
+
34
+ if with_confidence:
35
+ if detections.confidence is None:
36
+ raise ValueError(
37
+ "ConfusionMatrix can only be calculated for Detections with confidence"
38
+ )
39
+ arrays_to_concat.append(np.expand_dims(detections.confidence, 1))
40
+
41
+ return np.concatenate(arrays_to_concat, axis=1)
42
+
43
+
44
+ def validate_input_tensors(predictions: List[np.ndarray], targets: List[np.ndarray]):
45
+ """
46
+ Checks for shape consistency of input tensors.
47
+ """
48
+ if len(predictions) != len(targets):
49
+ raise ValueError(
50
+ f"Number of predictions ({len(predictions)}) and"
51
+ f"targets ({len(targets)}) must be equal."
52
+ )
53
+ if len(predictions) > 0:
54
+ if not isinstance(predictions[0], np.ndarray) or not isinstance(
55
+ targets[0], np.ndarray
56
+ ):
57
+ raise ValueError(
58
+ f"Predictions and targets must be lists of numpy arrays."
59
+ f"Got {type(predictions[0])} and {type(targets[0])} instead."
60
+ )
61
+ if predictions[0].shape[1] != 6:
62
+ raise ValueError(
63
+ f"Predictions must have shape (N, 6)."
64
+ f"Got {predictions[0].shape} instead."
65
+ )
66
+ if targets[0].shape[1] != 5:
67
+ raise ValueError(
68
+ f"Targets must have shape (N, 5). Got {targets[0].shape} instead."
69
+ )
70
+
71
+
72
+ @dataclass
73
+ class ConfusionMatrix:
74
+ """
75
+ Confusion matrix for object detection tasks.
76
+
77
+ Attributes:
78
+ matrix (np.ndarray): An 2D `np.ndarray` of shape
79
+ `(len(classes) + 1, len(classes) + 1)`
80
+ containing the number of `TP`, `FP`, `FN` and `TN` for each class.
81
+ classes (List[str]): Model class names.
82
+ conf_threshold (float): Detection confidence threshold between `0` and `1`.
83
+ Detections with lower confidence will be excluded from the matrix.
84
+ iou_threshold (float): Detection IoU threshold between `0` and `1`.
85
+ Detections with lower IoU will be classified as `FP`.
86
+ """
87
+
88
+ matrix: np.ndarray
89
+ classes: List[str]
90
+ conf_threshold: float
91
+ iou_threshold: float
92
+
93
+ @classmethod
94
+ def from_detections(
95
+ cls,
96
+ predictions: List[Detections],
97
+ targets: List[Detections],
98
+ classes: List[str],
99
+ conf_threshold: float = 0.3,
100
+ iou_threshold: float = 0.5,
101
+ ) -> ConfusionMatrix:
102
+ """
103
+ Calculate confusion matrix based on predicted and ground-truth detections.
104
+
105
+ Args:
106
+ targets (List[Detections]): Detections objects from ground-truth.
107
+ predictions (List[Detections]): Detections objects predicted by the model.
108
+ classes (List[str]): Model class names.
109
+ conf_threshold (float): Detection confidence threshold between `0` and `1`.
110
+ Detections with lower confidence will be excluded.
111
+ iou_threshold (float): Detection IoU threshold between `0` and `1`.
112
+ Detections with lower IoU will be classified as `FP`.
113
+
114
+ Returns:
115
+ ConfusionMatrix: New instance of ConfusionMatrix.
116
+
117
+ Example:
118
+ ```python
119
+ import eye as sv
120
+
121
+ targets = [
122
+ sv.Detections(...),
123
+ sv.Detections(...)
124
+ ]
125
+
126
+ predictions = [
127
+ sv.Detections(...),
128
+ sv.Detections(...)
129
+ ]
130
+
131
+ confusion_matrix = sv.ConfusionMatrix.from_detections(
132
+ predictions=predictions,
133
+ targets=target,
134
+ classes=['person', ...]
135
+ )
136
+
137
+ print(confusion_matrix.matrix)
138
+ # np.array([
139
+ # [0., 0., 0., 0.],
140
+ # [0., 1., 0., 1.],
141
+ # [0., 1., 1., 0.],
142
+ # [1., 1., 0., 0.]
143
+ # ])
144
+ ```
145
+ """
146
+
147
+ prediction_tensors = []
148
+ target_tensors = []
149
+ for prediction, target in zip(predictions, targets):
150
+ prediction_tensors.append(
151
+ detections_to_tensor(prediction, with_confidence=True)
152
+ )
153
+ target_tensors.append(detections_to_tensor(target, with_confidence=False))
154
+ return cls.from_tensors(
155
+ predictions=prediction_tensors,
156
+ targets=target_tensors,
157
+ classes=classes,
158
+ conf_threshold=conf_threshold,
159
+ iou_threshold=iou_threshold,
160
+ )
161
+
162
+ @classmethod
163
+ def from_tensors(
164
+ cls,
165
+ predictions: List[np.ndarray],
166
+ targets: List[np.ndarray],
167
+ classes: List[str],
168
+ conf_threshold: float = 0.3,
169
+ iou_threshold: float = 0.5,
170
+ ) -> ConfusionMatrix:
171
+ """
172
+ Calculate confusion matrix based on predicted and ground-truth detections.
173
+
174
+ Args:
175
+ predictions (List[np.ndarray]): Each element of the list describes a single
176
+ image and has `shape = (M, 6)` where `M` is the number of detected
177
+ objects. Each row is expected to be in
178
+ `(x_min, y_min, x_max, y_max, class, conf)` format.
179
+ targets (List[np.ndarray]): Each element of the list describes a single
180
+ image and has `shape = (N, 5)` where `N` is the number of
181
+ ground-truth objects. Each row is expected to be in
182
+ `(x_min, y_min, x_max, y_max, class)` format.
183
+ classes (List[str]): Model class names.
184
+ conf_threshold (float): Detection confidence threshold between `0` and `1`.
185
+ Detections with lower confidence will be excluded.
186
+ iou_threshold (float): Detection iou threshold between `0` and `1`.
187
+ Detections with lower iou will be classified as `FP`.
188
+
189
+ Returns:
190
+ ConfusionMatrix: New instance of ConfusionMatrix.
191
+
192
+ Example:
193
+ ```python
194
+ import eye as sv
195
+ import numpy as np
196
+
197
+ targets = (
198
+ [
199
+ np.array(
200
+ [
201
+ [0.0, 0.0, 3.0, 3.0, 1],
202
+ [2.0, 2.0, 5.0, 5.0, 1],
203
+ [6.0, 1.0, 8.0, 3.0, 2],
204
+ ]
205
+ ),
206
+ np.array([1.0, 1.0, 2.0, 2.0, 2]),
207
+ ]
208
+ )
209
+
210
+ predictions = [
211
+ np.array(
212
+ [
213
+ [0.0, 0.0, 3.0, 3.0, 1, 0.9],
214
+ [0.1, 0.1, 3.0, 3.0, 0, 0.9],
215
+ [6.0, 1.0, 8.0, 3.0, 1, 0.8],
216
+ [1.0, 6.0, 2.0, 7.0, 1, 0.8],
217
+ ]
218
+ ),
219
+ np.array([[1.0, 1.0, 2.0, 2.0, 2, 0.8]])
220
+ ]
221
+
222
+ confusion_matrix = sv.ConfusionMatrix.from_tensors(
223
+ predictions=predictions,
224
+ targets=targets,
225
+ classes=['person', ...]
226
+ )
227
+
228
+ print(confusion_matrix.matrix)
229
+ # np.array([
230
+ # [0., 0., 0., 0.],
231
+ # [0., 1., 0., 1.],
232
+ # [0., 1., 1., 0.],
233
+ # [1., 1., 0., 0.]
234
+ # ])
235
+ ```
236
+ """
237
+ validate_input_tensors(predictions, targets)
238
+
239
+ num_classes = len(classes)
240
+ matrix = np.zeros((num_classes + 1, num_classes + 1))
241
+ for true_batch, detection_batch in zip(targets, predictions):
242
+ matrix += cls.evaluate_detection_batch(
243
+ predictions=detection_batch,
244
+ targets=true_batch,
245
+ num_classes=num_classes,
246
+ conf_threshold=conf_threshold,
247
+ iou_threshold=iou_threshold,
248
+ )
249
+ return cls(
250
+ matrix=matrix,
251
+ classes=classes,
252
+ conf_threshold=conf_threshold,
253
+ iou_threshold=iou_threshold,
254
+ )
255
+
256
+ @staticmethod
257
+ def evaluate_detection_batch(
258
+ predictions: np.ndarray,
259
+ targets: np.ndarray,
260
+ num_classes: int,
261
+ conf_threshold: float,
262
+ iou_threshold: float,
263
+ ) -> np.ndarray:
264
+ """
265
+ Calculate confusion matrix for a batch of detections for a single image.
266
+
267
+ Args:
268
+ predictions (np.ndarray): Batch prediction. Describes a single image and
269
+ has `shape = (M, 6)` where `M` is the number of detected objects.
270
+ Each row is expected to be in
271
+ `(x_min, y_min, x_max, y_max, class, conf)` format.
272
+ targets (np.ndarray): Batch target labels. Describes a single image and
273
+ has `shape = (N, 5)` where `N` is the number of ground-truth objects.
274
+ Each row is expected to be in
275
+ `(x_min, y_min, x_max, y_max, class)` format.
276
+ num_classes (int): Number of classes.
277
+ conf_threshold (float): Detection confidence threshold between `0` and `1`.
278
+ Detections with lower confidence will be excluded.
279
+ iou_threshold (float): Detection iou threshold between `0` and `1`.
280
+ Detections with lower iou will be classified as `FP`.
281
+
282
+ Returns:
283
+ np.ndarray: Confusion matrix based on a single image.
284
+ """
285
+ result_matrix = np.zeros((num_classes + 1, num_classes + 1))
286
+
287
+ conf_idx = 5
288
+ confidence = predictions[:, conf_idx]
289
+ detection_batch_filtered = predictions[confidence > conf_threshold]
290
+
291
+ class_id_idx = 4
292
+ true_classes = np.array(targets[:, class_id_idx], dtype=np.int16)
293
+ detection_classes = np.array(
294
+ detection_batch_filtered[:, class_id_idx], dtype=np.int16
295
+ )
296
+ true_boxes = targets[:, :class_id_idx]
297
+ detection_boxes = detection_batch_filtered[:, :class_id_idx]
298
+
299
+ iou_batch = box_iou_batch(
300
+ boxes_true=true_boxes, boxes_detection=detection_boxes
301
+ )
302
+ matched_idx = np.asarray(iou_batch > iou_threshold).nonzero()
303
+
304
+ if matched_idx[0].shape[0]:
305
+ matches = np.stack(
306
+ (matched_idx[0], matched_idx[1], iou_batch[matched_idx]), axis=1
307
+ )
308
+ matches = ConfusionMatrix._drop_extra_matches(matches=matches)
309
+ else:
310
+ matches = np.zeros((0, 3))
311
+
312
+ matched_true_idx, matched_detection_idx, _ = matches.transpose().astype(
313
+ np.int16
314
+ )
315
+
316
+ for i, true_class_value in enumerate(true_classes):
317
+ j = matched_true_idx == i
318
+ if matches.shape[0] > 0 and sum(j) == 1:
319
+ result_matrix[
320
+ true_class_value, detection_classes[matched_detection_idx[j]]
321
+ ] += 1 # TP
322
+ else:
323
+ result_matrix[true_class_value, num_classes] += 1 # FN
324
+
325
+ for i, detection_class_value in enumerate(detection_classes):
326
+ if not any(matched_detection_idx == i):
327
+ result_matrix[num_classes, detection_class_value] += 1 # FP
328
+
329
+ return result_matrix
330
+
331
+ @staticmethod
332
+ def _drop_extra_matches(matches: np.ndarray) -> np.ndarray:
333
+ """
334
+ Deduplicate matches. If there are multiple matches for the same true or
335
+ predicted box, only the one with the highest IoU is kept.
336
+ """
337
+ if matches.shape[0] > 0:
338
+ matches = matches[matches[:, 2].argsort()[::-1]]
339
+ matches = matches[np.unique(matches[:, 1], return_index=True)[1]]
340
+ matches = matches[matches[:, 2].argsort()[::-1]]
341
+ matches = matches[np.unique(matches[:, 0], return_index=True)[1]]
342
+ return matches
343
+
344
+ @classmethod
345
+ def benchmark(
346
+ cls,
347
+ dataset: DetectionDataset,
348
+ callback: Callable[[np.ndarray], Detections],
349
+ conf_threshold: float = 0.3,
350
+ iou_threshold: float = 0.5,
351
+ ) -> ConfusionMatrix:
352
+ """
353
+ Calculate confusion matrix from dataset and callback function.
354
+
355
+ Args:
356
+ dataset (DetectionDataset): Object detection dataset used for evaluation.
357
+ callback (Callable[[np.ndarray], Detections]): Function that takes an image
358
+ as input and returns Detections object.
359
+ conf_threshold (float): Detection confidence threshold between `0` and `1`.
360
+ Detections with lower confidence will be excluded.
361
+ iou_threshold (float): Detection IoU threshold between `0` and `1`.
362
+ Detections with lower IoU will be classified as `FP`.
363
+
364
+ Returns:
365
+ ConfusionMatrix: New instance of ConfusionMatrix.
366
+
367
+ Example:
368
+ ```python
369
+ import eye as sv
370
+ from ultralytics import YOLO
371
+
372
+ dataset = sv.DetectionDataset.from_yolo(...)
373
+
374
+ model = YOLO(...)
375
+ def callback(image: np.ndarray) -> sv.Detections:
376
+ result = model(image)[0]
377
+ return sv.Detections.from_ultralytics(result)
378
+
379
+ confusion_matrix = sv.ConfusionMatrix.benchmark(
380
+ dataset = dataset,
381
+ callback = callback
382
+ )
383
+
384
+ print(confusion_matrix.matrix)
385
+ # np.array([
386
+ # [0., 0., 0., 0.],
387
+ # [0., 1., 0., 1.],
388
+ # [0., 1., 1., 0.],
389
+ # [1., 1., 0., 0.]
390
+ # ])
391
+ ```
392
+ """
393
+ predictions, targets = [], []
394
+ for _, image, annotation in dataset:
395
+ predictions_batch = callback(image)
396
+ predictions.append(predictions_batch)
397
+ targets.append(annotation)
398
+ return cls.from_detections(
399
+ predictions=predictions,
400
+ targets=targets,
401
+ classes=dataset.classes,
402
+ conf_threshold=conf_threshold,
403
+ iou_threshold=iou_threshold,
404
+ )
405
+
406
+ def plot(
407
+ self,
408
+ save_path: Optional[str] = None,
409
+ title: Optional[str] = None,
410
+ classes: Optional[List[str]] = None,
411
+ normalize: bool = False,
412
+ fig_size: Tuple[int, int] = (12, 10),
413
+ ) -> matplotlib.figure.Figure:
414
+ """
415
+ Create confusion matrix plot and save it at selected location.
416
+
417
+ Args:
418
+ save_path (Optional[str]): Path to save the plot. If not provided,
419
+ plot will be displayed.
420
+ title (Optional[str]): Title of the plot.
421
+ classes (Optional[List[str]]): List of classes to be displayed on the plot.
422
+ If not provided, all classes will be displayed.
423
+ normalize (bool): If True, normalize the confusion matrix.
424
+ fig_size (Tuple[int, int]): Size of the plot.
425
+
426
+ Returns:
427
+ matplotlib.figure.Figure: Confusion matrix plot.
428
+ """
429
+
430
+ array = self.matrix.copy()
431
+
432
+ if normalize:
433
+ eps = 1e-8
434
+ array = array / (array.sum(0).reshape(1, -1) + eps)
435
+
436
+ array[array < 0.005] = np.nan
437
+
438
+ fig, ax = plt.subplots(figsize=fig_size, tight_layout=True, facecolor="white")
439
+
440
+ class_names = classes if classes is not None else self.classes
441
+ use_labels_for_ticks = class_names is not None and (0 < len(class_names) < 99)
442
+ if use_labels_for_ticks:
443
+ x_tick_labels = [*class_names, "FN"]
444
+ y_tick_labels = [*class_names, "FP"]
445
+ num_ticks = len(x_tick_labels)
446
+ else:
447
+ x_tick_labels = None
448
+ y_tick_labels = None
449
+ num_ticks = len(array)
450
+ im = ax.imshow(array, cmap="Blues")
451
+
452
+ cbar = ax.figure.colorbar(im, ax=ax)
453
+ cbar.mappable.set_clim(vmin=0, vmax=np.nanmax(array))
454
+
455
+ if x_tick_labels is None:
456
+ tick_interval = 2
457
+ else:
458
+ tick_interval = 1
459
+ ax.set_xticks(np.arange(0, num_ticks, tick_interval), labels=x_tick_labels)
460
+ ax.set_yticks(np.arange(0, num_ticks, tick_interval), labels=y_tick_labels)
461
+
462
+ plt.setp(ax.get_xticklabels(), rotation=90, ha="right", rotation_mode="default")
463
+
464
+ labelsize = 10 if num_ticks < 50 else 8
465
+ ax.tick_params(axis="both", which="both", labelsize=labelsize)
466
+
467
+ if num_ticks < 30:
468
+ for i in range(array.shape[0]):
469
+ for j in range(array.shape[1]):
470
+ n_preds = array[i, j]
471
+ if not np.isnan(n_preds):
472
+ ax.text(
473
+ j,
474
+ i,
475
+ f"{n_preds:.2f}" if normalize else f"{n_preds:.0f}",
476
+ ha="center",
477
+ va="center",
478
+ color="black"
479
+ if n_preds < 0.5 * np.nanmax(array)
480
+ else "white",
481
+ )
482
+
483
+ if title:
484
+ ax.set_title(title, fontsize=20)
485
+
486
+ ax.set_xlabel("Predicted")
487
+ ax.set_ylabel("True")
488
+ ax.set_facecolor("white")
489
+ if save_path:
490
+ fig.savefig(
491
+ save_path, dpi=250, facecolor=fig.get_facecolor(), transparent=True
492
+ )
493
+ return fig
494
+
495
+
496
+ @dataclass(frozen=True)
497
+ class MeanAveragePrecision:
498
+ """
499
+ Mean Average Precision for object detection tasks.
500
+
501
+ Attributes:
502
+ map50_95 (float): Mean Average Precision (mAP) calculated over IoU thresholds
503
+ ranging from `0.50` to `0.95` with a step size of `0.05`.
504
+ map50 (float): Mean Average Precision (mAP) calculated specifically at
505
+ an IoU threshold of `0.50`.
506
+ map75 (float): Mean Average Precision (mAP) calculated specifically at
507
+ an IoU threshold of `0.75`.
508
+ per_class_ap50_95 (np.ndarray): Average Precision (AP) values calculated over
509
+ IoU thresholds ranging from `0.50` to `0.95` with a step size of `0.05`,
510
+ provided for each individual class.
511
+ """
512
+
513
+ map50_95: float
514
+ map50: float
515
+ map75: float
516
+ per_class_ap50_95: np.ndarray
517
+
518
+ @classmethod
519
+ def from_detections(
520
+ cls,
521
+ predictions: List[Detections],
522
+ targets: List[Detections],
523
+ ) -> MeanAveragePrecision:
524
+ """
525
+ Calculate mean average precision based on predicted and ground-truth detections.
526
+
527
+ Args:
528
+ targets (List[Detections]): Detections objects from ground-truth.
529
+ predictions (List[Detections]): Detections objects predicted by the model.
530
+ Returns:
531
+ MeanAveragePrecision: New instance of ConfusionMatrix.
532
+
533
+ Example:
534
+ ```python
535
+ import eye as sv
536
+
537
+ targets = [
538
+ sv.Detections(...),
539
+ sv.Detections(...)
540
+ ]
541
+
542
+ predictions = [
543
+ sv.Detections(...),
544
+ sv.Detections(...)
545
+ ]
546
+
547
+ mean_average_precision = sv.MeanAveragePrecision.from_detections(
548
+ predictions=predictions,
549
+ targets=target,
550
+ )
551
+
552
+ print(mean_average_precison.map50_95)
553
+ # 0.2899
554
+ ```
555
+ """
556
+ prediction_tensors = []
557
+ target_tensors = []
558
+ for prediction, target in zip(predictions, targets):
559
+ prediction_tensors.append(
560
+ detections_to_tensor(prediction, with_confidence=True)
561
+ )
562
+ target_tensors.append(detections_to_tensor(target, with_confidence=False))
563
+ return cls.from_tensors(
564
+ predictions=prediction_tensors,
565
+ targets=target_tensors,
566
+ )
567
+
568
+ @classmethod
569
+ def benchmark(
570
+ cls,
571
+ dataset: DetectionDataset,
572
+ callback: Callable[[np.ndarray], Detections],
573
+ ) -> MeanAveragePrecision:
574
+ """
575
+ Calculate mean average precision from dataset and callback function.
576
+
577
+ Args:
578
+ dataset (DetectionDataset): Object detection dataset used for evaluation.
579
+ callback (Callable[[np.ndarray], Detections]): Function that takes
580
+ an image as input and returns Detections object.
581
+ Returns:
582
+ MeanAveragePrecision: New instance of MeanAveragePrecision.
583
+
584
+ Example:
585
+ ```python
586
+ import eye as sv
587
+ from ultralytics import YOLO
588
+
589
+ dataset = sv.DetectionDataset.from_yolo(...)
590
+
591
+ model = YOLO(...)
592
+ def callback(image: np.ndarray) -> sv.Detections:
593
+ result = model(image)[0]
594
+ return sv.Detections.from_ultralytics(result)
595
+
596
+ mean_average_precision = sv.MeanAveragePrecision.benchmark(
597
+ dataset = dataset,
598
+ callback = callback
599
+ )
600
+
601
+ print(mean_average_precision.map50_95)
602
+ # 0.433
603
+ ```
604
+ """
605
+ predictions, targets = [], []
606
+ for _, image, annotation in dataset:
607
+ predictions_batch = callback(image)
608
+ predictions.append(predictions_batch)
609
+ targets.append(annotation)
610
+ return cls.from_detections(
611
+ predictions=predictions,
612
+ targets=targets,
613
+ )
614
+
615
+ @classmethod
616
+ def from_tensors(
617
+ cls,
618
+ predictions: List[np.ndarray],
619
+ targets: List[np.ndarray],
620
+ ) -> MeanAveragePrecision:
621
+ """
622
+ Calculate Mean Average Precision based on predicted and ground-truth
623
+ detections at different threshold.
624
+
625
+ Args:
626
+ predictions (List[np.ndarray]): Each element of the list describes
627
+ a single image and has `shape = (M, 6)` where `M` is
628
+ the number of detected objects. Each row is expected to be
629
+ in `(x_min, y_min, x_max, y_max, class, conf)` format.
630
+ targets (List[np.ndarray]): Each element of the list describes a single
631
+ image and has `shape = (N, 5)` where `N` is the
632
+ number of ground-truth objects. Each row is expected to be in
633
+ `(x_min, y_min, x_max, y_max, class)` format.
634
+ Returns:
635
+ MeanAveragePrecision: New instance of MeanAveragePrecision.
636
+
637
+ Example:
638
+ ```python
639
+ import eye as sv
640
+ import numpy as np
641
+
642
+ targets = (
643
+ [
644
+ np.array(
645
+ [
646
+ [0.0, 0.0, 3.0, 3.0, 1],
647
+ [2.0, 2.0, 5.0, 5.0, 1],
648
+ [6.0, 1.0, 8.0, 3.0, 2],
649
+ ]
650
+ ),
651
+ np.array([[1.0, 1.0, 2.0, 2.0, 2]]),
652
+ ]
653
+ )
654
+
655
+ predictions = [
656
+ np.array(
657
+ [
658
+ [0.0, 0.0, 3.0, 3.0, 1, 0.9],
659
+ [0.1, 0.1, 3.0, 3.0, 0, 0.9],
660
+ [6.0, 1.0, 8.0, 3.0, 1, 0.8],
661
+ [1.0, 6.0, 2.0, 7.0, 1, 0.8],
662
+ ]
663
+ ),
664
+ np.array([[1.0, 1.0, 2.0, 2.0, 2, 0.8]])
665
+ ]
666
+
667
+ mean_average_precision = sv.MeanAveragePrecision.from_tensors(
668
+ predictions=predictions,
669
+ targets=targets,
670
+ )
671
+
672
+ print(mean_average_precision.map50_95)
673
+ # 0.6649
674
+ ```
675
+ """
676
+ validate_input_tensors(predictions, targets)
677
+ iou_thresholds = np.linspace(0.5, 0.95, 10)
678
+ stats = []
679
+
680
+ # Gather matching stats for predictions and targets
681
+ for true_objs, predicted_objs in zip(targets, predictions):
682
+ if predicted_objs.shape[0] == 0:
683
+ if true_objs.shape[0]:
684
+ stats.append(
685
+ (
686
+ np.zeros((0, iou_thresholds.size), dtype=bool),
687
+ *np.zeros((2, 0)),
688
+ true_objs[:, 4],
689
+ )
690
+ )
691
+ continue
692
+
693
+ if true_objs.shape[0]:
694
+ matches = cls._match_detection_batch(
695
+ predicted_objs, true_objs, iou_thresholds
696
+ )
697
+ stats.append(
698
+ (
699
+ matches,
700
+ predicted_objs[:, 5],
701
+ predicted_objs[:, 4],
702
+ true_objs[:, 4],
703
+ )
704
+ )
705
+
706
+ # Compute average precisions if any matches exist
707
+ if stats:
708
+ concatenated_stats = [np.concatenate(items, 0) for items in zip(*stats)]
709
+ average_precisions = cls._average_precisions_per_class(*concatenated_stats)
710
+ map50 = average_precisions[:, 0].mean()
711
+ map75 = average_precisions[:, 5].mean()
712
+ map50_95 = average_precisions.mean()
713
+ else:
714
+ map50, map75, map50_95 = 0, 0, 0
715
+ average_precisions = []
716
+
717
+ return cls(
718
+ map50_95=map50_95,
719
+ map50=map50,
720
+ map75=map75,
721
+ per_class_ap50_95=average_precisions,
722
+ )
723
+
724
+ @staticmethod
725
+ def compute_average_precision(recall: np.ndarray, precision: np.ndarray) -> float:
726
+ """
727
+ Compute the average precision using 101-point interpolation (COCO), given
728
+ the recall and precision curves.
729
+
730
+ Args:
731
+ recall (np.ndarray): The recall curve.
732
+ precision (np.ndarray): The precision curve.
733
+
734
+ Returns:
735
+ float: Average precision.
736
+ """
737
+ extended_recall = np.concatenate(([0.0], recall, [1.0]))
738
+ extended_precision = np.concatenate(([1.0], precision, [0.0]))
739
+ max_accumulated_precision = np.flip(
740
+ np.maximum.accumulate(np.flip(extended_precision))
741
+ )
742
+ interpolated_recall_levels = np.linspace(0, 1, 101)
743
+ interpolated_precision = np.interp(
744
+ interpolated_recall_levels, extended_recall, max_accumulated_precision
745
+ )
746
+ average_precision = np.trapz(interpolated_precision, interpolated_recall_levels)
747
+ return average_precision
748
+
749
+ @staticmethod
750
+ def _match_detection_batch(
751
+ predictions: np.ndarray, targets: np.ndarray, iou_thresholds: np.ndarray
752
+ ) -> np.ndarray:
753
+ """
754
+ Match predictions with target labels based on IoU levels.
755
+
756
+ Args:
757
+ predictions (np.ndarray): Batch prediction. Describes a single image and
758
+ has `shape = (M, 6)` where `M` is the number of detected objects.
759
+ Each row is expected to be in
760
+ `(x_min, y_min, x_max, y_max, class, conf)` format.
761
+ targets (np.ndarray): Batch target labels. Describes a single image and
762
+ has `shape = (N, 5)` where `N` is the number of ground-truth objects.
763
+ Each row is expected to be in
764
+ `(x_min, y_min, x_max, y_max, class)` format.
765
+ iou_thresholds (np.ndarray): Array contains different IoU thresholds.
766
+
767
+ Returns:
768
+ np.ndarray: Matched prediction with target labels result.
769
+ """
770
+ num_predictions, num_iou_levels = predictions.shape[0], iou_thresholds.shape[0]
771
+ correct = np.zeros((num_predictions, num_iou_levels), dtype=bool)
772
+ iou = box_iou_batch(targets[:, :4], predictions[:, :4])
773
+ correct_class = targets[:, 4:5] == predictions[:, 4]
774
+
775
+ for i, iou_level in enumerate(iou_thresholds):
776
+ matched_indices = np.where((iou >= iou_level) & correct_class)
777
+
778
+ if matched_indices[0].shape[0]:
779
+ combined_indices = np.stack(matched_indices, axis=1)
780
+ iou_values = iou[matched_indices][:, None]
781
+ matches = np.hstack([combined_indices, iou_values])
782
+
783
+ if matched_indices[0].shape[0] > 1:
784
+ matches = matches[matches[:, 2].argsort()[::-1]]
785
+ matches = matches[np.unique(matches[:, 1], return_index=True)[1]]
786
+ matches = matches[np.unique(matches[:, 0], return_index=True)[1]]
787
+
788
+ correct[matches[:, 1].astype(int), i] = True
789
+
790
+ return correct
791
+
792
+ @staticmethod
793
+ def _average_precisions_per_class(
794
+ matches: np.ndarray,
795
+ prediction_confidence: np.ndarray,
796
+ prediction_class_ids: np.ndarray,
797
+ true_class_ids: np.ndarray,
798
+ eps: float = 1e-16,
799
+ ) -> np.ndarray:
800
+ """
801
+ Compute the average precision, given the recall and precision curves.
802
+ Source: https://github.com/rafaelpadilla/Object-Detection-Metrics.
803
+
804
+ Args:
805
+ matches (np.ndarray): True positives.
806
+ prediction_confidence (np.ndarray): Objectness value from 0-1.
807
+ prediction_class_ids (np.ndarray): Predicted object classes.
808
+ true_class_ids (np.ndarray): True object classes.
809
+ eps (float): Small value to prevent division by zero.
810
+
811
+ Returns:
812
+ np.ndarray: Average precision for different IoU levels.
813
+ """
814
+ sorted_indices = np.argsort(-prediction_confidence)
815
+ matches = matches[sorted_indices]
816
+ prediction_class_ids = prediction_class_ids[sorted_indices]
817
+
818
+ unique_classes, class_counts = np.unique(true_class_ids, return_counts=True)
819
+ num_classes = unique_classes.shape[0]
820
+
821
+ average_precisions = np.zeros((num_classes, matches.shape[1]))
822
+
823
+ for class_idx, class_id in enumerate(unique_classes):
824
+ is_class = prediction_class_ids == class_id
825
+ total_true = class_counts[class_idx]
826
+ total_prediction = is_class.sum()
827
+
828
+ if total_prediction == 0 or total_true == 0:
829
+ continue
830
+
831
+ false_positives = (1 - matches[is_class]).cumsum(0)
832
+ true_positives = matches[is_class].cumsum(0)
833
+ recall = true_positives / (total_true + eps)
834
+ precision = true_positives / (true_positives + false_positives)
835
+
836
+ for iou_level_idx in range(matches.shape[1]):
837
+ average_precisions[class_idx, iou_level_idx] = (
838
+ MeanAveragePrecision.compute_average_precision(
839
+ recall[:, iou_level_idx], precision[:, iou_level_idx]
840
+ )
841
+ )
842
+
843
+ return average_precisions