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.
- cli/client.py +14 -0
- cli/datasets.py +77 -0
- cli/helpers/__init__.py +0 -0
- cli/helpers/nucleus_url.py +10 -0
- cli/helpers/web_helper.py +40 -0
- cli/install_completion.py +33 -0
- cli/jobs.py +42 -0
- cli/models.py +35 -0
- cli/nu.py +42 -0
- cli/reference.py +8 -0
- cli/slices.py +62 -0
- cli/tests.py +121 -0
- nucleus/__init__.py +446 -710
- nucleus/annotation.py +405 -85
- nucleus/autocurate.py +9 -0
- nucleus/connection.py +87 -0
- nucleus/constants.py +5 -1
- nucleus/data_transfer_object/__init__.py +0 -0
- nucleus/data_transfer_object/dataset_details.py +9 -0
- nucleus/data_transfer_object/dataset_info.py +26 -0
- nucleus/data_transfer_object/dataset_size.py +5 -0
- nucleus/data_transfer_object/scenes_list.py +18 -0
- nucleus/dataset.py +1137 -212
- nucleus/dataset_item.py +130 -26
- nucleus/dataset_item_uploader.py +297 -0
- nucleus/deprecation_warning.py +32 -0
- nucleus/errors.py +9 -0
- nucleus/job.py +71 -3
- nucleus/logger.py +9 -0
- nucleus/metadata_manager.py +45 -0
- nucleus/metrics/__init__.py +10 -0
- nucleus/metrics/base.py +117 -0
- nucleus/metrics/categorization_metrics.py +197 -0
- nucleus/metrics/errors.py +7 -0
- nucleus/metrics/filters.py +40 -0
- nucleus/metrics/geometry.py +198 -0
- nucleus/metrics/metric_utils.py +28 -0
- nucleus/metrics/polygon_metrics.py +480 -0
- nucleus/metrics/polygon_utils.py +299 -0
- nucleus/model.py +121 -15
- nucleus/model_run.py +34 -57
- nucleus/payload_constructor.py +29 -19
- nucleus/prediction.py +259 -17
- nucleus/pydantic_base.py +26 -0
- nucleus/retry_strategy.py +4 -0
- nucleus/scene.py +204 -19
- nucleus/slice.py +230 -67
- nucleus/upload_response.py +20 -9
- nucleus/url_utils.py +4 -0
- nucleus/utils.py +134 -37
- nucleus/validate/__init__.py +24 -0
- nucleus/validate/client.py +168 -0
- nucleus/validate/constants.py +20 -0
- nucleus/validate/data_transfer_objects/__init__.py +0 -0
- nucleus/validate/data_transfer_objects/eval_function.py +81 -0
- nucleus/validate/data_transfer_objects/scenario_test.py +19 -0
- nucleus/validate/data_transfer_objects/scenario_test_evaluations.py +11 -0
- nucleus/validate/data_transfer_objects/scenario_test_metric.py +12 -0
- nucleus/validate/errors.py +6 -0
- nucleus/validate/eval_functions/__init__.py +0 -0
- nucleus/validate/eval_functions/available_eval_functions.py +212 -0
- nucleus/validate/eval_functions/base_eval_function.py +60 -0
- nucleus/validate/scenario_test.py +143 -0
- nucleus/validate/scenario_test_evaluation.py +114 -0
- nucleus/validate/scenario_test_metric.py +14 -0
- nucleus/validate/utils.py +8 -0
- {scale_nucleus-0.1.24.dist-info → scale_nucleus-0.6.4.dist-info}/LICENSE +0 -0
- scale_nucleus-0.6.4.dist-info/METADATA +213 -0
- scale_nucleus-0.6.4.dist-info/RECORD +71 -0
- {scale_nucleus-0.1.24.dist-info → scale_nucleus-0.6.4.dist-info}/WHEEL +1 -1
- scale_nucleus-0.6.4.dist-info/entry_points.txt +3 -0
- scale_nucleus-0.1.24.dist-info/METADATA +0 -85
- 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
|