scale-nucleus 0.1.24__py3-none-any.whl → 0.6.4__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 (73) hide show
  1. cli/client.py +14 -0
  2. cli/datasets.py +77 -0
  3. cli/helpers/__init__.py +0 -0
  4. cli/helpers/nucleus_url.py +10 -0
  5. cli/helpers/web_helper.py +40 -0
  6. cli/install_completion.py +33 -0
  7. cli/jobs.py +42 -0
  8. cli/models.py +35 -0
  9. cli/nu.py +42 -0
  10. cli/reference.py +8 -0
  11. cli/slices.py +62 -0
  12. cli/tests.py +121 -0
  13. nucleus/__init__.py +446 -710
  14. nucleus/annotation.py +405 -85
  15. nucleus/autocurate.py +9 -0
  16. nucleus/connection.py +87 -0
  17. nucleus/constants.py +5 -1
  18. nucleus/data_transfer_object/__init__.py +0 -0
  19. nucleus/data_transfer_object/dataset_details.py +9 -0
  20. nucleus/data_transfer_object/dataset_info.py +26 -0
  21. nucleus/data_transfer_object/dataset_size.py +5 -0
  22. nucleus/data_transfer_object/scenes_list.py +18 -0
  23. nucleus/dataset.py +1137 -212
  24. nucleus/dataset_item.py +130 -26
  25. nucleus/dataset_item_uploader.py +297 -0
  26. nucleus/deprecation_warning.py +32 -0
  27. nucleus/errors.py +9 -0
  28. nucleus/job.py +71 -3
  29. nucleus/logger.py +9 -0
  30. nucleus/metadata_manager.py +45 -0
  31. nucleus/metrics/__init__.py +10 -0
  32. nucleus/metrics/base.py +117 -0
  33. nucleus/metrics/categorization_metrics.py +197 -0
  34. nucleus/metrics/errors.py +7 -0
  35. nucleus/metrics/filters.py +40 -0
  36. nucleus/metrics/geometry.py +198 -0
  37. nucleus/metrics/metric_utils.py +28 -0
  38. nucleus/metrics/polygon_metrics.py +480 -0
  39. nucleus/metrics/polygon_utils.py +299 -0
  40. nucleus/model.py +121 -15
  41. nucleus/model_run.py +34 -57
  42. nucleus/payload_constructor.py +29 -19
  43. nucleus/prediction.py +259 -17
  44. nucleus/pydantic_base.py +26 -0
  45. nucleus/retry_strategy.py +4 -0
  46. nucleus/scene.py +204 -19
  47. nucleus/slice.py +230 -67
  48. nucleus/upload_response.py +20 -9
  49. nucleus/url_utils.py +4 -0
  50. nucleus/utils.py +134 -37
  51. nucleus/validate/__init__.py +24 -0
  52. nucleus/validate/client.py +168 -0
  53. nucleus/validate/constants.py +20 -0
  54. nucleus/validate/data_transfer_objects/__init__.py +0 -0
  55. nucleus/validate/data_transfer_objects/eval_function.py +81 -0
  56. nucleus/validate/data_transfer_objects/scenario_test.py +19 -0
  57. nucleus/validate/data_transfer_objects/scenario_test_evaluations.py +11 -0
  58. nucleus/validate/data_transfer_objects/scenario_test_metric.py +12 -0
  59. nucleus/validate/errors.py +6 -0
  60. nucleus/validate/eval_functions/__init__.py +0 -0
  61. nucleus/validate/eval_functions/available_eval_functions.py +212 -0
  62. nucleus/validate/eval_functions/base_eval_function.py +60 -0
  63. nucleus/validate/scenario_test.py +143 -0
  64. nucleus/validate/scenario_test_evaluation.py +114 -0
  65. nucleus/validate/scenario_test_metric.py +14 -0
  66. nucleus/validate/utils.py +8 -0
  67. {scale_nucleus-0.1.24.dist-info → scale_nucleus-0.6.4.dist-info}/LICENSE +0 -0
  68. scale_nucleus-0.6.4.dist-info/METADATA +213 -0
  69. scale_nucleus-0.6.4.dist-info/RECORD +71 -0
  70. {scale_nucleus-0.1.24.dist-info → scale_nucleus-0.6.4.dist-info}/WHEEL +1 -1
  71. scale_nucleus-0.6.4.dist-info/entry_points.txt +3 -0
  72. scale_nucleus-0.1.24.dist-info/METADATA +0 -85
  73. scale_nucleus-0.1.24.dist-info/RECORD +0 -21
@@ -0,0 +1,198 @@
1
+ from typing import List, Tuple, Union
2
+
3
+ import numpy as np
4
+
5
+ TOLERANCE = 1e-8
6
+
7
+
8
+ class GeometryPolygon:
9
+ def __init__(
10
+ self,
11
+ points: Union[np.ndarray, List[Tuple[float, float]]],
12
+ is_rectangle: bool = False,
13
+ ):
14
+ self.points = (
15
+ points if isinstance(points, np.ndarray) else np.array(points)
16
+ )
17
+ self.is_rectangle = is_rectangle
18
+ points_x = self.points[:, 0]
19
+ points_y = self.points[:, 1]
20
+ if is_rectangle:
21
+ self.signed_area = np.abs(self.points[2] - self.points[0]).prod()
22
+ self.area = self.signed_area
23
+ else:
24
+ self.signed_area = (
25
+ points_x @ np.roll(points_y, -1)
26
+ - points_x @ np.roll(points_y, 1)
27
+ ) / 2
28
+ self.area = np.abs(self.signed_area)
29
+
30
+ def __len__(self):
31
+ return len(self.points)
32
+
33
+ def __getitem__(self, idx):
34
+ return self.points[idx]
35
+
36
+ def __repr__(self) -> str:
37
+ return f"GeometryPolygon({self.points})"
38
+
39
+
40
+ # alpha * a1 + (1 - alpha) * a2 = beta * b1 + (1 - beta) * b2
41
+ def segment_intersection(
42
+ segment1: Tuple[np.ndarray, np.ndarray],
43
+ segment2: Tuple[np.ndarray, np.ndarray],
44
+ ) -> Tuple[float, float, np.ndarray]:
45
+ a1, a2 = segment1
46
+ b1, b2 = segment2
47
+ x2_x2 = b2[0] - a2[0]
48
+ y2_y2 = b2[1] - a2[1]
49
+ x1x2 = a1[0] - a2[0]
50
+ y1y2 = a1[1] - a2[1]
51
+ y1_y2_ = b1[1] - b2[1]
52
+ x1_x2_ = b1[0] - b2[0]
53
+
54
+ if np.abs(y1_y2_ * x1x2 - x1_x2_ * y1y2) < TOLERANCE:
55
+ beta = 1.0
56
+ else:
57
+ beta = (x2_x2 * y1y2 - y2_y2 * x1x2) / (y1_y2_ * x1x2 - x1_x2_ * y1y2)
58
+
59
+ if x1x2 == 0:
60
+ alpha = (y2_y2 + y1_y2_ * beta) / (y1y2 + TOLERANCE)
61
+ else:
62
+ alpha = (x2_x2 + x1_x2_ * beta) / (x1x2 + TOLERANCE)
63
+
64
+ return alpha, beta, alpha * a1 + (1 - alpha) * a2
65
+
66
+
67
+ def convex_polygon_intersection_area(
68
+ polygon_a: GeometryPolygon, polygon_b: GeometryPolygon
69
+ ) -> float:
70
+ # pylint: disable=R0912
71
+ sa = polygon_a.signed_area
72
+ sb = polygon_b.signed_area
73
+ if sa * sb < 0:
74
+ sign = -1
75
+ else:
76
+ sign = 1
77
+ na = len(polygon_a)
78
+ nb = len(polygon_b)
79
+ ps = [] # point set
80
+ for i in range(na):
81
+ a1 = polygon_a[i - 1]
82
+ a2 = polygon_a[i]
83
+ flag = False
84
+ sum_s = 0
85
+ for j in range(nb):
86
+ b1 = polygon_b[j - 1]
87
+ b2 = polygon_b[j]
88
+ sum_s += np.abs(GeometryPolygon([a1, b1, b2]).signed_area)
89
+
90
+ if np.abs(np.abs(sum_s) - np.abs(sb)) < TOLERANCE:
91
+ flag = True
92
+
93
+ if flag:
94
+ ps.append(a1)
95
+ for j in range(nb):
96
+ b1 = polygon_b[j - 1]
97
+ b2 = polygon_b[j]
98
+ a, b, p = segment_intersection((a1, a2), (b1, b2))
99
+ if 0 < a < 1 and 0 < b < 1:
100
+ ps.append(p)
101
+
102
+ for i in range(nb):
103
+ a1 = polygon_b[i - 1]
104
+ a2 = polygon_b[i]
105
+ flag = False
106
+ sum_s = 0
107
+ for j in range(na):
108
+ b1 = polygon_a[j - 1]
109
+ b2 = polygon_a[j]
110
+ sum_s += np.abs(GeometryPolygon([a1, b1, b2]).signed_area)
111
+ if np.abs(np.abs(sum_s) - np.abs(sa)) < TOLERANCE:
112
+ flag = True
113
+ if flag:
114
+ ps.append(a1)
115
+
116
+ def unique(ar):
117
+ res = []
118
+ for i, a in enumerate(ar):
119
+ if np.abs(a - ar[i - 1]).sum() > TOLERANCE:
120
+ res.append(a)
121
+
122
+ return res
123
+
124
+ ps = sorted(ps, key=lambda x: (x[0] + TOLERANCE * x[1]))
125
+ ps = unique(ps)
126
+
127
+ if len(ps) == 0:
128
+ return 0
129
+
130
+ tmp = ps[0]
131
+
132
+ res = []
133
+ res.append(tmp)
134
+ ps = sorted(
135
+ ps[1:],
136
+ key=lambda x: -((x - tmp) @ np.array((0, 1)) / len(x - tmp)),
137
+ )
138
+ res.extend(ps)
139
+
140
+ return GeometryPolygon(res).signed_area * sign
141
+
142
+
143
+ def area(box):
144
+ if box[2] <= box[0] or box[3] <= box[1]:
145
+ return 0
146
+ return (box[2] - box[0]) * (box[3] - box[1])
147
+
148
+
149
+ def iou(box_a, box_b):
150
+ box_c = intersection(box_a, box_b)
151
+ return area(box_c) / (area(box_a) + area(box_b) - area(box_c))
152
+
153
+
154
+ def intersection(box_a, box_b):
155
+ """boxes are left, top, right, bottom where left < right and top < bottom"""
156
+ box_c = [
157
+ max(box_a[0], box_b[0]),
158
+ max(box_a[1], box_b[1]),
159
+ min(box_a[2], box_b[2]),
160
+ min(box_a[3], box_b[3]),
161
+ ]
162
+ return box_c
163
+
164
+
165
+ def rectangle_intersection_area(
166
+ polygon_a: GeometryPolygon, polygon_b: GeometryPolygon
167
+ ) -> float:
168
+ minx_a, miny_a = np.min(polygon_a.points, axis=0)
169
+ maxx_a, maxy_a = np.max(polygon_a.points, axis=0)
170
+ minx_b, miny_b = np.min(polygon_b.points, axis=0)
171
+ maxx_b, maxy_b = np.max(polygon_b.points, axis=0)
172
+
173
+ minx_c = max(minx_a, minx_b)
174
+ miny_c = max(miny_a, miny_b)
175
+ maxx_c = min(maxx_a, maxx_b)
176
+ maxy_c = min(maxy_a, maxy_b)
177
+ return max(maxx_c - minx_c, 0) * max(maxy_c - miny_c, 0)
178
+
179
+
180
+ def polygon_intersection_area(
181
+ polygon_a: GeometryPolygon, polygon_b: GeometryPolygon
182
+ ) -> float:
183
+ if polygon_a.is_rectangle and polygon_b.is_rectangle:
184
+ return rectangle_intersection_area(polygon_a, polygon_b)
185
+
186
+ na = len(polygon_a)
187
+ nb = len(polygon_b)
188
+ res = 0.0
189
+ for i in range(1, na - 1):
190
+ sa = polygon_a[[0, i, i + 1]]
191
+ for j in range(1, nb - 1):
192
+ sb = polygon_b[[0, j, j + 1]]
193
+ tmp = convex_polygon_intersection_area(
194
+ GeometryPolygon(sa), GeometryPolygon(sb)
195
+ )
196
+ res += tmp
197
+
198
+ return np.abs(res)
@@ -0,0 +1,28 @@
1
+ import numpy as np
2
+
3
+
4
+ def compute_average_precision(recall, precision):
5
+ """Compute the average precision, given the recall and precision curves.
6
+ Code originally from https://github.com/rbgirshick/py-faster-rcnn.
7
+ # Arguments
8
+ recall: The recall curve (list).
9
+ precision: The precision curve (list).
10
+ # Returns
11
+ The average precision as computed in py-faster-rcnn.
12
+ """
13
+ # correct AP calculation
14
+ # first append sentinel values at the end
15
+ mrec = np.concatenate(([0.0], recall, [1.0]))
16
+ mpre = np.concatenate(([0.0], precision, [0.0]))
17
+
18
+ # compute the precision envelope
19
+ for i in range(mpre.size - 1, 0, -1):
20
+ mpre[i - 1] = np.maximum(mpre[i - 1], mpre[i])
21
+
22
+ # to calculate area under PR curve, look for points
23
+ # where X axis (recall) changes value
24
+ i = np.where(mrec[1:] != mrec[:-1])[0]
25
+
26
+ # and sum (\Delta recall) * prec
27
+ ap = np.sum((mrec[i + 1] - mrec[i]) * mpre[i + 1])
28
+ return ap
@@ -0,0 +1,480 @@
1
+ import sys
2
+ from abc import abstractmethod
3
+ from typing import List, Union
4
+
5
+ import numpy as np
6
+
7
+ from nucleus.annotation import AnnotationList, BoxAnnotation, PolygonAnnotation
8
+ from nucleus.prediction import BoxPrediction, PolygonPrediction, PredictionList
9
+
10
+ from .base import Metric, ScalarResult
11
+ from .filters import confidence_filter, polygon_label_filter
12
+ from .metric_utils import compute_average_precision
13
+ from .polygon_utils import (
14
+ BoxOrPolygonAnnotation,
15
+ BoxOrPolygonPrediction,
16
+ get_true_false_positives_confidences,
17
+ group_boxes_or_polygons_by_label,
18
+ iou_assignments,
19
+ label_match_wrapper,
20
+ num_true_positives,
21
+ )
22
+
23
+
24
+ class PolygonMetric(Metric):
25
+ """Abstract class for metrics of box and polygons.
26
+
27
+ The PolygonMetric class automatically filters incoming annotations and
28
+ predictions for only box and polygon annotations. It also filters
29
+ predictions whose confidence is less than the provided confidence_threshold.
30
+ Finally, it provides support for enforcing matching labels. If
31
+ `enforce_label_match` is set to True, then annotations and predictions will
32
+ only be matched if they have the same label.
33
+
34
+ To create a new concrete PolygonMetric, override the `eval` function
35
+ with logic to define a metric between box/polygon annotations and predictions.
36
+ ::
37
+
38
+ from typing import List
39
+ from nucleus import BoxAnnotation, Point, PolygonPrediction
40
+ from nucleus.annotation import AnnotationList
41
+ from nucleus.prediction import PredictionList
42
+ from nucleus.metrics import ScalarResult, PolygonMetric
43
+ from nucleus.metrics.polygon_utils import BoxOrPolygonAnnotation, BoxOrPolygonPrediction
44
+
45
+ class MyPolygonMetric(PolygonMetric):
46
+ def eval(
47
+ self,
48
+ annotations: List[BoxOrPolygonAnnotation],
49
+ predictions: List[BoxOrPolygonPrediction],
50
+ ) -> ScalarResult:
51
+ value = (len(annotations) - len(predictions)) ** 2
52
+ weight = len(annotations)
53
+ return ScalarResult(value, weight)
54
+
55
+ box_anno = BoxAnnotation(
56
+ label="car",
57
+ x=0,
58
+ y=0,
59
+ width=10,
60
+ height=10,
61
+ reference_id="image_1",
62
+ annotation_id="image_1_car_box_1",
63
+ metadata={"vehicle_color": "red"}
64
+ )
65
+
66
+ polygon_pred = PolygonPrediction(
67
+ label="bus",
68
+ vertices=[Point(100, 100), Point(150, 200), Point(200, 100)],
69
+ reference_id="image_2",
70
+ annotation_id="image_2_bus_polygon_1",
71
+ confidence=0.8,
72
+ metadata={"vehicle_color": "yellow"}
73
+ )
74
+
75
+ annotations = AnnotationList(box_annotations=[box_anno])
76
+ predictions = PredictionList(polygon_predictions=[polygon_pred])
77
+ metric = MyPolygonMetric()
78
+ metric(annotations, predictions)
79
+ """
80
+
81
+ def __init__(
82
+ self,
83
+ enforce_label_match: bool = False,
84
+ confidence_threshold: float = 0.0,
85
+ ):
86
+ """Initializes PolygonMetric abstract object.
87
+
88
+ Args:
89
+ enforce_label_match: whether to enforce that annotation and prediction labels must match. Default False
90
+ confidence_threshold: minimum confidence threshold for predictions. Must be in [0, 1]. Default 0.0
91
+ """
92
+ self.enforce_label_match = enforce_label_match
93
+ assert 0 <= confidence_threshold <= 1
94
+ self.confidence_threshold = confidence_threshold
95
+
96
+ @abstractmethod
97
+ def eval(
98
+ self,
99
+ annotations: List[BoxOrPolygonAnnotation],
100
+ predictions: List[BoxOrPolygonPrediction],
101
+ ) -> ScalarResult:
102
+ # Main evaluation function that subclasses must override.
103
+ pass
104
+
105
+ def aggregate_score(self, results: List[ScalarResult]) -> ScalarResult: # type: ignore[override]
106
+ return ScalarResult.aggregate(results)
107
+
108
+ def __call__(
109
+ self, annotations: AnnotationList, predictions: PredictionList
110
+ ) -> ScalarResult:
111
+ if self.confidence_threshold > 0:
112
+ predictions = confidence_filter(
113
+ predictions, self.confidence_threshold
114
+ )
115
+ polygon_annotations: List[Union[BoxAnnotation, PolygonAnnotation]] = []
116
+ polygon_annotations.extend(annotations.box_annotations)
117
+ polygon_annotations.extend(annotations.polygon_annotations)
118
+ polygon_predictions: List[Union[BoxPrediction, PolygonPrediction]] = []
119
+ polygon_predictions.extend(predictions.box_predictions)
120
+ polygon_predictions.extend(predictions.polygon_predictions)
121
+
122
+ eval_fn = label_match_wrapper(self.eval)
123
+ result = eval_fn(
124
+ polygon_annotations,
125
+ polygon_predictions,
126
+ enforce_label_match=self.enforce_label_match,
127
+ )
128
+ return result
129
+
130
+
131
+ class PolygonIOU(PolygonMetric):
132
+ """Calculates the average IOU between box or polygon annotations and predictions.
133
+ ::
134
+
135
+ from nucleus import BoxAnnotation, Point, PolygonPrediction
136
+ from nucleus.annotation import AnnotationList
137
+ from nucleus.prediction import PredictionList
138
+ from nucleus.metrics import PolygonIOU
139
+
140
+ box_anno = BoxAnnotation(
141
+ label="car",
142
+ x=0,
143
+ y=0,
144
+ width=10,
145
+ height=10,
146
+ reference_id="image_1",
147
+ annotation_id="image_1_car_box_1",
148
+ metadata={"vehicle_color": "red"}
149
+ )
150
+
151
+ polygon_pred = PolygonPrediction(
152
+ label="bus",
153
+ vertices=[Point(100, 100), Point(150, 200), Point(200, 100)],
154
+ reference_id="image_2",
155
+ annotation_id="image_2_bus_polygon_1",
156
+ confidence=0.8,
157
+ metadata={"vehicle_color": "yellow"}
158
+ )
159
+
160
+ annotations = AnnotationList(box_annotations=[box_anno])
161
+ predictions = PredictionList(polygon_predictions=[polygon_pred])
162
+ metric = PolygonIOU()
163
+ metric(annotations, predictions)
164
+ """
165
+
166
+ # TODO: Remove defaults once these are surfaced more cleanly to users.
167
+ def __init__(
168
+ self,
169
+ enforce_label_match: bool = False,
170
+ iou_threshold: float = 0.0,
171
+ confidence_threshold: float = 0.0,
172
+ ):
173
+ """Initializes PolygonIOU object.
174
+
175
+ Args:
176
+ enforce_label_match: whether to enforce that annotation and prediction labels must match. Defaults to False
177
+ iou_threshold: IOU threshold to consider detection as valid. Must be in [0, 1]. Default 0.0
178
+ confidence_threshold: minimum confidence threshold for predictions. Must be in [0, 1]. Default 0.0
179
+ """
180
+ assert (
181
+ 0 <= iou_threshold <= 1
182
+ ), "IoU threshold must be between 0 and 1."
183
+ self.iou_threshold = iou_threshold
184
+ super().__init__(enforce_label_match, confidence_threshold)
185
+
186
+ def eval(
187
+ self,
188
+ annotations: List[BoxOrPolygonAnnotation],
189
+ predictions: List[BoxOrPolygonPrediction],
190
+ ) -> ScalarResult:
191
+ iou_assigns = iou_assignments(
192
+ annotations, predictions, self.iou_threshold
193
+ )
194
+ weight = max(len(annotations), len(predictions))
195
+ avg_iou = iou_assigns.sum() / max(weight, sys.float_info.epsilon)
196
+ return ScalarResult(avg_iou, weight)
197
+
198
+
199
+ class PolygonPrecision(PolygonMetric):
200
+ """Calculates the precision between box or polygon annotations and predictions.
201
+ ::
202
+
203
+ from nucleus import BoxAnnotation, Point, PolygonPrediction
204
+ from nucleus.annotation import AnnotationList
205
+ from nucleus.prediction import PredictionList
206
+ from nucleus.metrics import PolygonPrecision
207
+
208
+ box_anno = BoxAnnotation(
209
+ label="car",
210
+ x=0,
211
+ y=0,
212
+ width=10,
213
+ height=10,
214
+ reference_id="image_1",
215
+ annotation_id="image_1_car_box_1",
216
+ metadata={"vehicle_color": "red"}
217
+ )
218
+
219
+ polygon_pred = PolygonPrediction(
220
+ label="bus",
221
+ vertices=[Point(100, 100), Point(150, 200), Point(200, 100)],
222
+ reference_id="image_2",
223
+ annotation_id="image_2_bus_polygon_1",
224
+ confidence=0.8,
225
+ metadata={"vehicle_color": "yellow"}
226
+ )
227
+
228
+ annotations = AnnotationList(box_annotations=[box_anno])
229
+ predictions = PredictionList(polygon_predictions=[polygon_pred])
230
+ metric = PolygonPrecision()
231
+ metric(annotations, predictions)
232
+ """
233
+
234
+ # TODO: Remove defaults once these are surfaced more cleanly to users.
235
+ def __init__(
236
+ self,
237
+ enforce_label_match: bool = False,
238
+ iou_threshold: float = 0.5,
239
+ confidence_threshold: float = 0.0,
240
+ ):
241
+ """Initializes PolygonPrecision object.
242
+
243
+ Args:
244
+ enforce_label_match: whether to enforce that annotation and prediction labels must match. Defaults to False
245
+ iou_threshold: IOU threshold to consider detection as valid. Must be in [0, 1]. Default 0.5
246
+ confidence_threshold: minimum confidence threshold for predictions. Must be in [0, 1]. Default 0.0
247
+ """
248
+ assert (
249
+ 0 <= iou_threshold <= 1
250
+ ), "IoU threshold must be between 0 and 1."
251
+ self.iou_threshold = iou_threshold
252
+ super().__init__(enforce_label_match, confidence_threshold)
253
+
254
+ def eval(
255
+ self,
256
+ annotations: List[BoxOrPolygonAnnotation],
257
+ predictions: List[BoxOrPolygonPrediction],
258
+ ) -> ScalarResult:
259
+ true_positives = num_true_positives(
260
+ annotations, predictions, self.iou_threshold
261
+ )
262
+ weight = len(predictions)
263
+ return ScalarResult(
264
+ true_positives / max(weight, sys.float_info.epsilon), weight
265
+ )
266
+
267
+
268
+ class PolygonRecall(PolygonMetric):
269
+ """Calculates the recall between box or polygon annotations and predictions.
270
+ ::
271
+
272
+ from nucleus import BoxAnnotation, Point, PolygonPrediction
273
+ from nucleus.annotation import AnnotationList
274
+ from nucleus.prediction import PredictionList
275
+ from nucleus.metrics import PolygonRecall
276
+
277
+ box_anno = BoxAnnotation(
278
+ label="car",
279
+ x=0,
280
+ y=0,
281
+ width=10,
282
+ height=10,
283
+ reference_id="image_1",
284
+ annotation_id="image_1_car_box_1",
285
+ metadata={"vehicle_color": "red"}
286
+ )
287
+
288
+ polygon_pred = PolygonPrediction(
289
+ label="bus",
290
+ vertices=[Point(100, 100), Point(150, 200), Point(200, 100)],
291
+ reference_id="image_2",
292
+ annotation_id="image_2_bus_polygon_1",
293
+ confidence=0.8,
294
+ metadata={"vehicle_color": "yellow"}
295
+ )
296
+
297
+ annotations = AnnotationList(box_annotations=[box_anno])
298
+ predictions = PredictionList(polygon_predictions=[polygon_pred])
299
+ metric = PolygonRecall()
300
+ metric(annotations, predictions)
301
+ """
302
+
303
+ # TODO: Remove defaults once these are surfaced more cleanly to users.
304
+ def __init__(
305
+ self,
306
+ enforce_label_match: bool = False,
307
+ iou_threshold: float = 0.5,
308
+ confidence_threshold: float = 0.0,
309
+ ):
310
+ """Initializes PolygonRecall object.
311
+
312
+ Args:
313
+ enforce_label_match: whether to enforce that annotation and prediction labels must match. Defaults to False
314
+ iou_threshold: IOU threshold to consider detection as valid. Must be in [0, 1]. Default 0.5
315
+ confidence_threshold: minimum confidence threshold for predictions. Must be in [0, 1]. Default 0.0
316
+ """
317
+ assert (
318
+ 0 <= iou_threshold <= 1
319
+ ), "IoU threshold must be between 0 and 1."
320
+ self.iou_threshold = iou_threshold
321
+ super().__init__(enforce_label_match, confidence_threshold)
322
+
323
+ def eval(
324
+ self,
325
+ annotations: List[BoxOrPolygonAnnotation],
326
+ predictions: List[BoxOrPolygonPrediction],
327
+ ) -> ScalarResult:
328
+ true_positives = num_true_positives(
329
+ annotations, predictions, self.iou_threshold
330
+ )
331
+ weight = len(annotations) + sys.float_info.epsilon
332
+ return ScalarResult(
333
+ true_positives / max(weight, sys.float_info.epsilon), weight
334
+ )
335
+
336
+
337
+ class PolygonAveragePrecision(PolygonMetric):
338
+ """Calculates the average precision between box or polygon annotations and predictions.
339
+ ::
340
+
341
+ from nucleus import BoxAnnotation, Point, PolygonPrediction
342
+ from nucleus.annotation import AnnotationList
343
+ from nucleus.prediction import PredictionList
344
+ from nucleus.metrics import PolygonAveragePrecision
345
+
346
+ box_anno = BoxAnnotation(
347
+ label="car",
348
+ x=0,
349
+ y=0,
350
+ width=10,
351
+ height=10,
352
+ reference_id="image_1",
353
+ annotation_id="image_1_car_box_1",
354
+ metadata={"vehicle_color": "red"}
355
+ )
356
+
357
+ polygon_pred = PolygonPrediction(
358
+ label="bus",
359
+ vertices=[Point(100, 100), Point(150, 200), Point(200, 100)],
360
+ reference_id="image_2",
361
+ annotation_id="image_2_bus_polygon_1",
362
+ confidence=0.8,
363
+ metadata={"vehicle_color": "yellow"}
364
+ )
365
+
366
+ annotations = AnnotationList(box_annotations=[box_anno])
367
+ predictions = PredictionList(polygon_predictions=[polygon_pred])
368
+ metric = PolygonAveragePrecision(label="car")
369
+ metric(annotations, predictions)
370
+ """
371
+
372
+ # TODO: Remove defaults once these are surfaced more cleanly to users.
373
+ def __init__(
374
+ self,
375
+ label,
376
+ iou_threshold: float = 0.5,
377
+ ):
378
+ """Initializes PolygonRecall object.
379
+
380
+ Args:
381
+ iou_threshold: IOU threshold to consider detection as valid. Must be in [0, 1]. Default 0.5
382
+ """
383
+ assert (
384
+ 0 <= iou_threshold <= 1
385
+ ), "IoU threshold must be between 0 and 1."
386
+ self.iou_threshold = iou_threshold
387
+ self.label = label
388
+ super().__init__(enforce_label_match=False, confidence_threshold=0)
389
+
390
+ def eval(
391
+ self,
392
+ annotations: List[BoxOrPolygonAnnotation],
393
+ predictions: List[BoxOrPolygonPrediction],
394
+ ) -> ScalarResult:
395
+ annotations_filtered = polygon_label_filter(annotations, self.label)
396
+ predictions_filtered = polygon_label_filter(predictions, self.label)
397
+ (
398
+ true_false_positives,
399
+ confidences,
400
+ ) = get_true_false_positives_confidences(
401
+ annotations_filtered, predictions_filtered, self.iou_threshold
402
+ )
403
+ idxes = np.argsort(-confidences)
404
+ true_false_positives_sorted = true_false_positives[idxes]
405
+ cumulative_true_positives = np.cumsum(true_false_positives_sorted)
406
+ total_predictions = np.arange(1, len(true_false_positives) + 1)
407
+ precisions = cumulative_true_positives / total_predictions
408
+ recalls = cumulative_true_positives / len(annotations)
409
+ average_precision = compute_average_precision(precisions, recalls)
410
+ weight = 1
411
+ return ScalarResult(average_precision, weight)
412
+
413
+
414
+ class PolygonMAP(PolygonMetric):
415
+ """Calculates the mean average precision between box or polygon annotations and predictions.
416
+ ::
417
+
418
+ from nucleus import BoxAnnotation, Point, PolygonPrediction
419
+ from nucleus.annotation import AnnotationList
420
+ from nucleus.prediction import PredictionList
421
+ from nucleus.metrics import PolygonMAP
422
+
423
+ box_anno = BoxAnnotation(
424
+ label="car",
425
+ x=0,
426
+ y=0,
427
+ width=10,
428
+ height=10,
429
+ reference_id="image_1",
430
+ annotation_id="image_1_car_box_1",
431
+ metadata={"vehicle_color": "red"}
432
+ )
433
+
434
+ polygon_pred = PolygonPrediction(
435
+ label="bus",
436
+ vertices=[Point(100, 100), Point(150, 200), Point(200, 100)],
437
+ reference_id="image_2",
438
+ annotation_id="image_2_bus_polygon_1",
439
+ confidence=0.8,
440
+ metadata={"vehicle_color": "yellow"}
441
+ )
442
+
443
+ annotations = AnnotationList(box_annotations=[box_anno])
444
+ predictions = PredictionList(polygon_predictions=[polygon_pred])
445
+ metric = PolygonMAP()
446
+ metric(annotations, predictions)
447
+ """
448
+
449
+ # TODO: Remove defaults once these are surfaced more cleanly to users.
450
+ def __init__(
451
+ self,
452
+ iou_threshold: float = 0.5,
453
+ ):
454
+ """Initializes PolygonRecall object.
455
+
456
+ Args:
457
+ iou_threshold: IOU threshold to consider detection as valid. Must be in [0, 1]. Default 0.5
458
+ """
459
+ assert (
460
+ 0 <= iou_threshold <= 1
461
+ ), "IoU threshold must be between 0 and 1."
462
+ self.iou_threshold = iou_threshold
463
+ super().__init__(enforce_label_match=False, confidence_threshold=0)
464
+
465
+ def eval(
466
+ self,
467
+ annotations: List[BoxOrPolygonAnnotation],
468
+ predictions: List[BoxOrPolygonPrediction],
469
+ ) -> ScalarResult:
470
+ grouped_inputs = group_boxes_or_polygons_by_label(
471
+ annotations, predictions
472
+ )
473
+ results: List[ScalarResult] = []
474
+ for label, group in grouped_inputs.items():
475
+ annotations_group, predictions_group = group
476
+ metric = PolygonAveragePrecision(label)
477
+ result = metric.eval(annotations_group, predictions_group)
478
+ results.append(result)
479
+ average_result = ScalarResult.aggregate(results)
480
+ return average_result