scale-nucleus 0.12b1__py3-none-any.whl → 0.14.14b0__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/slices.py +14 -28
- nucleus/__init__.py +211 -18
- nucleus/annotation.py +28 -5
- nucleus/connection.py +9 -1
- nucleus/constants.py +9 -3
- nucleus/dataset.py +197 -59
- nucleus/dataset_item.py +11 -1
- nucleus/job.py +1 -1
- nucleus/metrics/__init__.py +2 -1
- nucleus/metrics/base.py +34 -56
- nucleus/metrics/categorization_metrics.py +6 -2
- nucleus/metrics/cuboid_utils.py +4 -6
- nucleus/metrics/errors.py +4 -0
- nucleus/metrics/filtering.py +369 -19
- nucleus/metrics/polygon_utils.py +3 -3
- nucleus/metrics/segmentation_loader.py +30 -0
- nucleus/metrics/segmentation_metrics.py +256 -195
- nucleus/metrics/segmentation_to_poly_metrics.py +229 -105
- nucleus/metrics/segmentation_utils.py +239 -8
- nucleus/model.py +66 -10
- nucleus/model_run.py +1 -1
- nucleus/{shapely_not_installed.py → package_not_installed.py} +3 -3
- nucleus/payload_constructor.py +4 -0
- nucleus/prediction.py +6 -3
- nucleus/scene.py +7 -0
- nucleus/slice.py +160 -16
- nucleus/utils.py +51 -12
- nucleus/validate/__init__.py +1 -0
- nucleus/validate/client.py +57 -8
- nucleus/validate/constants.py +1 -0
- nucleus/validate/data_transfer_objects/eval_function.py +22 -0
- nucleus/validate/data_transfer_objects/scenario_test_evaluations.py +13 -5
- nucleus/validate/eval_functions/available_eval_functions.py +33 -20
- nucleus/validate/eval_functions/config_classes/segmentation.py +2 -46
- nucleus/validate/scenario_test.py +71 -13
- nucleus/validate/scenario_test_evaluation.py +21 -21
- nucleus/validate/utils.py +1 -1
- {scale_nucleus-0.12b1.dist-info → scale_nucleus-0.14.14b0.dist-info}/LICENSE +0 -0
- {scale_nucleus-0.12b1.dist-info → scale_nucleus-0.14.14b0.dist-info}/METADATA +13 -11
- {scale_nucleus-0.12b1.dist-info → scale_nucleus-0.14.14b0.dist-info}/RECORD +42 -41
- {scale_nucleus-0.12b1.dist-info → scale_nucleus-0.14.14b0.dist-info}/WHEEL +1 -1
- {scale_nucleus-0.12b1.dist-info → scale_nucleus-0.14.14b0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,30 @@
|
|
1
|
+
import abc
|
2
|
+
from typing import Dict
|
3
|
+
|
4
|
+
import numpy as np
|
5
|
+
|
6
|
+
|
7
|
+
class SegmentationMaskLoader(abc.ABC):
|
8
|
+
@abc.abstractmethod
|
9
|
+
def fetch(self, url: str) -> np.ndarray:
|
10
|
+
pass
|
11
|
+
|
12
|
+
|
13
|
+
class DummyLoader(SegmentationMaskLoader):
|
14
|
+
def fetch(self, url: str) -> np.ndarray:
|
15
|
+
raise NotImplementedError(
|
16
|
+
"This dummy loader has to be replaced with an actual implementation of an image loader"
|
17
|
+
)
|
18
|
+
|
19
|
+
|
20
|
+
class InMemoryLoader(SegmentationMaskLoader):
|
21
|
+
"""We use this loader in the tests, this allows us to serve images from memory instead of fetching
|
22
|
+
from a filesystem.
|
23
|
+
"""
|
24
|
+
|
25
|
+
def __init__(self, url_to_array: Dict[str, np.ndarray]):
|
26
|
+
self.url_to_array = url_to_array
|
27
|
+
|
28
|
+
def fetch(self, url: str):
|
29
|
+
array = self.url_to_array[url]
|
30
|
+
return array
|
@@ -1,42 +1,26 @@
|
|
1
1
|
import abc
|
2
|
-
from typing import List, Optional, Union
|
2
|
+
from typing import List, Optional, Set, Tuple, Union
|
3
3
|
|
4
|
-
import fsspec
|
5
4
|
import numpy as np
|
6
|
-
from PIL import Image
|
7
|
-
from s3fs import S3FileSystem
|
8
5
|
|
9
|
-
from nucleus.annotation import AnnotationList, SegmentationAnnotation
|
6
|
+
from nucleus.annotation import AnnotationList, Segment, SegmentationAnnotation
|
10
7
|
from nucleus.metrics.base import MetricResult
|
11
8
|
from nucleus.metrics.filtering import ListOfAndFilters, ListOfOrAndFilters
|
12
9
|
from nucleus.prediction import PredictionList, SegmentationPrediction
|
13
10
|
|
14
11
|
from .base import Metric, ScalarResult
|
15
|
-
from .
|
12
|
+
from .segmentation_loader import DummyLoader, SegmentationMaskLoader
|
13
|
+
from .segmentation_utils import (
|
14
|
+
FALSE_POSITIVES,
|
15
|
+
convert_to_instance_seg_confusion,
|
16
|
+
fast_confusion_matrix,
|
17
|
+
non_max_suppress_confusion,
|
18
|
+
setup_iou_thresholds,
|
19
|
+
)
|
16
20
|
|
17
21
|
# pylint: disable=useless-super-delegation
|
18
22
|
|
19
23
|
|
20
|
-
def _fast_hist(label_true, label_pred, n_class):
|
21
|
-
"""Calculates confusion matrix - fast!"""
|
22
|
-
mask = (label_true >= 0) & (label_true < n_class)
|
23
|
-
hist = np.bincount(
|
24
|
-
n_class * label_true[mask].astype(int) + label_pred[mask],
|
25
|
-
minlength=n_class ** 2,
|
26
|
-
).reshape(n_class, n_class)
|
27
|
-
return hist
|
28
|
-
|
29
|
-
|
30
|
-
class SegmentationMaskLoader:
|
31
|
-
def __init__(self, fs: fsspec):
|
32
|
-
self.fs = fs
|
33
|
-
|
34
|
-
def fetch(self, url: str):
|
35
|
-
with self.fs.open(url) as fh:
|
36
|
-
img = Image.open(fh)
|
37
|
-
return img
|
38
|
-
|
39
|
-
|
40
24
|
class SegmentationMaskMetric(Metric):
|
41
25
|
def __init__(
|
42
26
|
self,
|
@@ -46,6 +30,7 @@ class SegmentationMaskMetric(Metric):
|
|
46
30
|
prediction_filters: Optional[
|
47
31
|
Union[ListOfOrAndFilters, ListOfAndFilters]
|
48
32
|
] = None,
|
33
|
+
iou_threshold: float = 0.5,
|
49
34
|
):
|
50
35
|
"""Initializes PolygonMetric abstract object.
|
51
36
|
|
@@ -71,13 +56,12 @@ class SegmentationMaskMetric(Metric):
|
|
71
56
|
"""
|
72
57
|
# TODO -> add custom filtering to Segmentation(Annotation|Prediction).annotations.(metadata|label)
|
73
58
|
super().__init__(annotation_filters, prediction_filters)
|
74
|
-
self.loader =
|
75
|
-
|
76
|
-
self.confusion: Optional[np.ndarray] = None
|
59
|
+
self.loader: SegmentationMaskLoader = DummyLoader()
|
60
|
+
self.iou_threshold = iou_threshold
|
77
61
|
|
78
62
|
def call_metric(
|
79
63
|
self, annotations: AnnotationList, predictions: PredictionList
|
80
|
-
) ->
|
64
|
+
) -> ScalarResult:
|
81
65
|
assert (
|
82
66
|
len(annotations.segmentation_annotations) <= 1
|
83
67
|
), f"Expected only one segmentation mask, got {annotations.segmentation_annotations}"
|
@@ -94,29 +78,71 @@ class SegmentationMaskMetric(Metric):
|
|
94
78
|
if predictions.segmentation_predictions
|
95
79
|
else None
|
96
80
|
)
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
81
|
+
if (
|
82
|
+
annotation
|
83
|
+
and prediction
|
84
|
+
and annotation.annotations
|
85
|
+
and prediction.annotations
|
86
|
+
):
|
87
|
+
annotation_img = self.get_mask_channel(annotation)
|
88
|
+
pred_img = self.get_mask_channel(prediction)
|
89
|
+
return self._metric_impl(
|
90
|
+
np.asarray(annotation_img, dtype=np.int32),
|
91
|
+
np.asarray(pred_img, dtype=np.int32),
|
92
|
+
annotation,
|
93
|
+
prediction,
|
94
|
+
)
|
95
|
+
else:
|
96
|
+
return ScalarResult(0, weight=0)
|
97
|
+
|
98
|
+
def get_mask_channel(self, ann_or_pred):
|
99
|
+
"""Some annotations are stored as RGB instead of L (single-channel).
|
100
|
+
We expect the image to be faux-single-channel with all the channels repeating so we choose the first one.
|
101
|
+
"""
|
102
|
+
img = self.loader.fetch(ann_or_pred.mask_url)
|
103
|
+
if len(img.shape) > 2:
|
104
|
+
# TODO: Do we have to do anything more advanced? Currently expect all channels to have same data
|
105
|
+
min_dim = np.argmin(img.shape)
|
106
|
+
if min_dim == 0:
|
107
|
+
img = img[0, :, :]
|
108
|
+
elif min_dim == 1:
|
109
|
+
img = img[:, 0, :]
|
110
|
+
else:
|
111
|
+
img = img[:, :, 0]
|
112
|
+
return img
|
105
113
|
|
106
114
|
@abc.abstractmethod
|
107
115
|
def _metric_impl(
|
108
116
|
self,
|
109
117
|
annotation_img: np.ndarray,
|
110
118
|
prediction_img: np.ndarray,
|
111
|
-
annotation:
|
112
|
-
prediction:
|
119
|
+
annotation: SegmentationAnnotation,
|
120
|
+
prediction: SegmentationPrediction,
|
113
121
|
):
|
114
122
|
pass
|
115
123
|
|
116
124
|
def _calculate_confusion_matrix(
|
117
|
-
self,
|
118
|
-
|
119
|
-
|
125
|
+
self,
|
126
|
+
annotation,
|
127
|
+
annotation_img,
|
128
|
+
prediction,
|
129
|
+
prediction_img,
|
130
|
+
iou_threshold,
|
131
|
+
) -> Tuple[np.ndarray, Set[int]]:
|
132
|
+
"""This calculates a confusion matrix with ground_truth_index X predicted_index summary
|
133
|
+
|
134
|
+
Notes:
|
135
|
+
If filtering has been applied we filter out missing segments from the confusion matrix.
|
136
|
+
|
137
|
+
Returns:
|
138
|
+
Class-based confusion matrix and a set of indexes that are not considered a part of the taxonomy (and are
|
139
|
+
only considered for FPs not as a part of mean calculations)
|
140
|
+
|
141
|
+
|
142
|
+
TODO(gunnar): Allow pre-seeding confusion matrix (all of the metrics calculate the same confusion matrix ->
|
143
|
+
we can calculate it once and then use it for all other metrics in the chain)
|
144
|
+
"""
|
145
|
+
# NOTE: This creates a max(class_index) * max(class_index) MAT. If we have np.int32 this could become
|
120
146
|
# huge. We could probably use a sparse matrix instead or change the logic to only create count(index) ** 2
|
121
147
|
# matrix (we only need to keep track of available indexes)
|
122
148
|
num_classes = (
|
@@ -126,11 +152,80 @@ class SegmentationMaskMetric(Metric):
|
|
126
152
|
)
|
127
153
|
+ 1 # to include 0
|
128
154
|
)
|
129
|
-
confusion = (
|
130
|
-
|
131
|
-
|
132
|
-
|
155
|
+
confusion = fast_confusion_matrix(
|
156
|
+
annotation_img, prediction_img, num_classes
|
157
|
+
)
|
158
|
+
confusion = self._filter_confusion_matrix(
|
159
|
+
confusion, annotation, prediction
|
160
|
+
)
|
161
|
+
confusion = non_max_suppress_confusion(confusion, iou_threshold)
|
162
|
+
false_positive = Segment(FALSE_POSITIVES, index=confusion.shape[0] - 1)
|
163
|
+
if annotation.annotations[-1].label != FALSE_POSITIVES:
|
164
|
+
annotation.annotations.append(false_positive)
|
165
|
+
if annotation.annotations is not prediction.annotations:
|
166
|
+
# Probably likely that this structure is re-used -> check if same list instance and only append once
|
167
|
+
# TODO(gunnar): Should this uniqueness be handled by the base class?
|
168
|
+
prediction.annotations.append(false_positive)
|
169
|
+
|
170
|
+
# TODO(gunnar): Detect non_taxonomy classes for segmentation as well as instance segmentation
|
171
|
+
non_taxonomy_classes = set()
|
172
|
+
if self._is_instance_segmentation(annotation, prediction):
|
173
|
+
(
|
174
|
+
confusion,
|
175
|
+
_,
|
176
|
+
non_taxonomy_classes,
|
177
|
+
) = convert_to_instance_seg_confusion(
|
178
|
+
confusion, annotation, prediction
|
179
|
+
)
|
180
|
+
else:
|
181
|
+
ann_labels = list(
|
182
|
+
dict.fromkeys(s.label for s in annotation.annotations)
|
183
|
+
)
|
184
|
+
pred_labels = list(
|
185
|
+
dict.fromkeys(s.label for s in prediction.annotations)
|
186
|
+
)
|
187
|
+
missing_or_filtered_labels = set(ann_labels) - set(pred_labels)
|
188
|
+
non_taxonomy_classes = {
|
189
|
+
segment.index
|
190
|
+
for segment in annotation.annotations
|
191
|
+
if segment.label in missing_or_filtered_labels
|
192
|
+
}
|
193
|
+
|
194
|
+
return confusion, non_taxonomy_classes
|
195
|
+
|
196
|
+
def _is_instance_segmentation(self, annotation, prediction):
|
197
|
+
"""Guesses that we're dealing with instance segmentation if we have multiple segments with same label.
|
198
|
+
Degenerate case is same as semseg so fine to misclassify in that case."""
|
199
|
+
# This is a trick to get ordered sets
|
200
|
+
ann_labels = list(
|
201
|
+
dict.fromkeys(s.label for s in annotation.annotations)
|
202
|
+
)
|
203
|
+
pred_labels = list(
|
204
|
+
dict.fromkeys(s.label for s in prediction.annotations)
|
133
205
|
)
|
206
|
+
# NOTE: We assume instance segmentation if labels are duplicated in annotations or predictions
|
207
|
+
is_instance_segmentation = len(ann_labels) != len(
|
208
|
+
annotation.annotations
|
209
|
+
) or len(pred_labels) != len(prediction.annotations)
|
210
|
+
return is_instance_segmentation
|
211
|
+
|
212
|
+
def _filter_confusion_matrix(self, confusion, annotation, prediction):
|
213
|
+
if self.annotation_filters or self.prediction_filters:
|
214
|
+
new_confusion = np.zeros_like(confusion)
|
215
|
+
# we mask the confusion matrix instead of the images
|
216
|
+
if self.annotation_filters:
|
217
|
+
annotation_indexes = {
|
218
|
+
segment.index for segment in annotation.annotations
|
219
|
+
}
|
220
|
+
for row in annotation_indexes:
|
221
|
+
new_confusion[row, :] = confusion[row, :]
|
222
|
+
if self.prediction_filters:
|
223
|
+
prediction_indexes = {
|
224
|
+
segment.index for segment in prediction.annotations
|
225
|
+
}
|
226
|
+
for col in prediction_indexes:
|
227
|
+
new_confusion[:, col] = confusion[:, col]
|
228
|
+
confusion = new_confusion
|
134
229
|
return confusion
|
135
230
|
|
136
231
|
|
@@ -143,6 +238,7 @@ class SegmentationIOU(SegmentationMaskMetric):
|
|
143
238
|
prediction_filters: Optional[
|
144
239
|
Union[ListOfOrAndFilters, ListOfAndFilters]
|
145
240
|
] = None,
|
241
|
+
iou_threshold: float = 0.5,
|
146
242
|
):
|
147
243
|
"""Initializes PolygonIOU object.
|
148
244
|
|
@@ -169,30 +265,36 @@ class SegmentationIOU(SegmentationMaskMetric):
|
|
169
265
|
super().__init__(
|
170
266
|
annotation_filters,
|
171
267
|
prediction_filters,
|
268
|
+
iou_threshold,
|
172
269
|
)
|
173
270
|
|
174
271
|
def _metric_impl(
|
175
272
|
self,
|
176
273
|
annotation_img: np.ndarray,
|
177
274
|
prediction_img: np.ndarray,
|
178
|
-
annotation:
|
179
|
-
prediction:
|
275
|
+
annotation: SegmentationAnnotation,
|
276
|
+
prediction: SegmentationPrediction,
|
180
277
|
) -> ScalarResult:
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
278
|
+
confusion, non_taxonomy_classes = self._calculate_confusion_matrix(
|
279
|
+
annotation,
|
280
|
+
annotation_img,
|
281
|
+
prediction,
|
282
|
+
prediction_img,
|
283
|
+
self.iou_threshold,
|
187
284
|
)
|
188
285
|
|
189
286
|
with np.errstate(divide="ignore", invalid="ignore"):
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
- np.diag(
|
287
|
+
tp = confusion[:-1, :-1]
|
288
|
+
fp = confusion[:, -1]
|
289
|
+
iou = np.diag(tp) / (
|
290
|
+
tp.sum(axis=1) + tp.sum(axis=0) + fp.sum() - np.diag(tp)
|
194
291
|
)
|
195
|
-
|
292
|
+
non_taxonomy_classes = non_taxonomy_classes - {
|
293
|
+
confusion.shape[1] - 1
|
294
|
+
}
|
295
|
+
iou.put(list(non_taxonomy_classes), np.nan)
|
296
|
+
mean_iou = np.nanmean(iou)
|
297
|
+
return ScalarResult(value=mean_iou, weight=annotation_img.size) # type: ignore
|
196
298
|
|
197
299
|
def aggregate_score(self, results: List[MetricResult]) -> ScalarResult:
|
198
300
|
return ScalarResult.aggregate(results) # type: ignore
|
@@ -207,6 +309,7 @@ class SegmentationPrecision(SegmentationMaskMetric):
|
|
207
309
|
prediction_filters: Optional[
|
208
310
|
Union[ListOfOrAndFilters, ListOfAndFilters]
|
209
311
|
] = None,
|
312
|
+
iou_threshold: float = 0.5,
|
210
313
|
):
|
211
314
|
"""Calculates mean per-class precision
|
212
315
|
|
@@ -233,93 +336,37 @@ class SegmentationPrecision(SegmentationMaskMetric):
|
|
233
336
|
super().__init__(
|
234
337
|
annotation_filters,
|
235
338
|
prediction_filters,
|
339
|
+
iou_threshold,
|
236
340
|
)
|
237
341
|
|
238
342
|
def _metric_impl(
|
239
343
|
self,
|
240
344
|
annotation_img: np.ndarray,
|
241
345
|
prediction_img: np.ndarray,
|
242
|
-
annotation:
|
243
|
-
prediction:
|
244
|
-
) -> ScalarResult:
|
245
|
-
if annotation is None or prediction is None:
|
246
|
-
# TODO: Throw error when we wrap each item in try catch
|
247
|
-
return ScalarResult(0, weight=0)
|
248
|
-
|
249
|
-
self.confusion = self._calculate_confusion_matrix(
|
250
|
-
annotation, annotation_img, prediction, prediction_img
|
251
|
-
)
|
252
|
-
|
253
|
-
with np.errstate(divide="ignore", invalid="ignore"):
|
254
|
-
true_pos = np.diag(self.confusion)
|
255
|
-
precision = true_pos / np.sum(self.confusion, axis=0)
|
256
|
-
mean_precision = np.nanmean(precision)
|
257
|
-
return ScalarResult(value=mean_precision, weight=1) # type: ignore
|
258
|
-
|
259
|
-
def aggregate_score(self, results: List[MetricResult]) -> ScalarResult:
|
260
|
-
return ScalarResult.aggregate(results) # type: ignore
|
261
|
-
|
262
|
-
|
263
|
-
class SegmentationAveragePrecision(SegmentationMaskMetric):
|
264
|
-
def __init__(
|
265
|
-
self,
|
266
|
-
annotation_filters: Optional[
|
267
|
-
Union[ListOfOrAndFilters, ListOfAndFilters]
|
268
|
-
] = None,
|
269
|
-
prediction_filters: Optional[
|
270
|
-
Union[ListOfOrAndFilters, ListOfAndFilters]
|
271
|
-
] = None,
|
272
|
-
):
|
273
|
-
"""Initializes SegmentationAveragePrecision object.
|
274
|
-
|
275
|
-
Args:
|
276
|
-
annotation_filters: Filter predicates. Allowed formats are:
|
277
|
-
ListOfAndFilters where each Filter forms a chain of AND predicates.
|
278
|
-
or
|
279
|
-
ListOfOrAndFilters where Filters are expressed in disjunctive normal form (DNF), like
|
280
|
-
[[MetadataFilter("short_haired", "==", True), FieldFilter("label", "in", ["cat", "dog"]), ...].
|
281
|
-
DNF allows arbitrary boolean logical combinations of single field predicates. The innermost structures
|
282
|
-
each describe a single column predicate. The list of inner predicates is interpreted as a conjunction
|
283
|
-
(AND), forming a more selective `and` multiple field predicate.
|
284
|
-
Finally, the most outer list combines these filters as a disjunction (OR).
|
285
|
-
prediction_filters: Filter predicates. Allowed formats are:
|
286
|
-
ListOfAndFilters where each Filter forms a chain of AND predicates.
|
287
|
-
or
|
288
|
-
ListOfOrAndFilters where Filters are expressed in disjunctive normal form (DNF), like
|
289
|
-
[[MetadataFilter("short_haired", "==", True), FieldFilter("label", "in", ["cat", "dog"]), ...].
|
290
|
-
DNF allows arbitrary boolean logical combinations of single field predicates. The innermost structures
|
291
|
-
each describe a single column predicate. The list of inner predicates is interpreted as a conjunction
|
292
|
-
(AND), forming a more selective `and` multiple field predicate.
|
293
|
-
Finally, the most outer list combines these filters as a disjunction (OR).
|
294
|
-
"""
|
295
|
-
super().__init__(
|
296
|
-
annotation_filters,
|
297
|
-
prediction_filters,
|
298
|
-
)
|
299
|
-
|
300
|
-
def _metric_impl(
|
301
|
-
self,
|
302
|
-
annotation_img: np.ndarray,
|
303
|
-
prediction_img: np.ndarray,
|
304
|
-
annotation: Optional[SegmentationAnnotation],
|
305
|
-
prediction: Optional[SegmentationPrediction],
|
346
|
+
annotation: SegmentationAnnotation,
|
347
|
+
prediction: SegmentationPrediction,
|
306
348
|
) -> ScalarResult:
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
349
|
+
confusion, non_taxonomy_classes = self._calculate_confusion_matrix(
|
350
|
+
annotation,
|
351
|
+
annotation_img,
|
352
|
+
prediction,
|
353
|
+
prediction_img,
|
354
|
+
self.iou_threshold,
|
313
355
|
)
|
314
356
|
|
315
357
|
with np.errstate(divide="ignore", invalid="ignore"):
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
358
|
+
# TODO(gunnar): Logic can be simplified
|
359
|
+
confused = confusion[:-1, :-1]
|
360
|
+
tp = confused.diagonal()
|
361
|
+
fp = confusion[:, -1][:-1] + confused.sum(axis=0) - tp
|
362
|
+
tp_and_fp = tp + fp
|
363
|
+
precision = tp / tp_and_fp
|
364
|
+
non_taxonomy_classes = non_taxonomy_classes - {
|
365
|
+
confusion.shape[1] - 1
|
366
|
+
}
|
367
|
+
precision.put(list(non_taxonomy_classes), np.nan)
|
368
|
+
avg_precision = np.nanmean(precision)
|
369
|
+
return ScalarResult(value=np.nan_to_num(avg_precision), weight=confusion.sum()) # type: ignore
|
323
370
|
|
324
371
|
def aggregate_score(self, results: List[MetricResult]) -> ScalarResult:
|
325
372
|
return ScalarResult.aggregate(results) # type: ignore
|
@@ -337,6 +384,7 @@ class SegmentationRecall(SegmentationMaskMetric):
|
|
337
384
|
prediction_filters: Optional[
|
338
385
|
Union[ListOfOrAndFilters, ListOfAndFilters]
|
339
386
|
] = None,
|
387
|
+
iou_threshold: float = 0.5,
|
340
388
|
):
|
341
389
|
"""Initializes PolygonRecall object.
|
342
390
|
|
@@ -361,29 +409,33 @@ class SegmentationRecall(SegmentationMaskMetric):
|
|
361
409
|
Finally, the most outer list combines these filters as a disjunction (OR).
|
362
410
|
"""
|
363
411
|
super().__init__(
|
364
|
-
annotation_filters
|
365
|
-
prediction_filters
|
412
|
+
annotation_filters,
|
413
|
+
prediction_filters,
|
414
|
+
iou_threshold,
|
366
415
|
)
|
367
416
|
|
368
417
|
def _metric_impl(
|
369
418
|
self,
|
370
419
|
annotation_img: np.ndarray,
|
371
420
|
prediction_img: np.ndarray,
|
372
|
-
annotation:
|
373
|
-
prediction:
|
421
|
+
annotation: SegmentationAnnotation,
|
422
|
+
prediction: SegmentationPrediction,
|
374
423
|
) -> ScalarResult:
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
424
|
+
confusion, non_taxonomy_classes = self._calculate_confusion_matrix(
|
425
|
+
annotation,
|
426
|
+
annotation_img,
|
427
|
+
prediction,
|
428
|
+
prediction_img,
|
429
|
+
self.iou_threshold,
|
381
430
|
)
|
382
431
|
|
383
432
|
with np.errstate(divide="ignore", invalid="ignore"):
|
384
|
-
|
385
|
-
recall
|
386
|
-
|
433
|
+
recall = confusion.diagonal() / confusion.sum(axis=1)
|
434
|
+
recall.put(
|
435
|
+
list(non_taxonomy_classes), np.nan
|
436
|
+
) # We don't consider non taxonomy classes, i.e. FPs and background
|
437
|
+
mean_recall = np.nanmean(recall)
|
438
|
+
return ScalarResult(value=np.nan_to_num(mean_recall), weight=annotation_img.size) # type: ignore
|
387
439
|
|
388
440
|
def aggregate_score(self, results: List[MetricResult]) -> ScalarResult:
|
389
441
|
return ScalarResult.aggregate(results) # type: ignore
|
@@ -424,6 +476,8 @@ class SegmentationMAP(SegmentationMaskMetric):
|
|
424
476
|
metric(annotations, predictions)
|
425
477
|
"""
|
426
478
|
|
479
|
+
iou_setups = {"coco"}
|
480
|
+
|
427
481
|
# TODO: Remove defaults once these are surfaced more cleanly to users.
|
428
482
|
def __init__(
|
429
483
|
self,
|
@@ -433,6 +487,7 @@ class SegmentationMAP(SegmentationMaskMetric):
|
|
433
487
|
prediction_filters: Optional[
|
434
488
|
Union[ListOfOrAndFilters, ListOfAndFilters]
|
435
489
|
] = None,
|
490
|
+
iou_thresholds: Union[List[float], str] = "coco",
|
436
491
|
):
|
437
492
|
"""Initializes PolygonRecall object.
|
438
493
|
|
@@ -455,44 +510,42 @@ class SegmentationMAP(SegmentationMaskMetric):
|
|
455
510
|
each describe a single column predicate. The list of inner predicates is interpreted as a conjunction
|
456
511
|
(AND), forming a more selective `and` multiple field predicate.
|
457
512
|
Finally, the most outer list combines these filters as a disjunction (OR).
|
513
|
+
map_thresholds: Provide a list of threshold to compute over or literal "coco"
|
458
514
|
"""
|
459
515
|
super().__init__(
|
460
|
-
annotation_filters
|
461
|
-
prediction_filters
|
516
|
+
annotation_filters,
|
517
|
+
prediction_filters,
|
462
518
|
)
|
519
|
+
self.iou_thresholds = setup_iou_thresholds(iou_thresholds)
|
463
520
|
|
464
521
|
def _metric_impl(
|
465
522
|
self,
|
466
523
|
annotation_img: np.ndarray,
|
467
524
|
prediction_img: np.ndarray,
|
468
|
-
annotation:
|
469
|
-
prediction:
|
525
|
+
annotation: SegmentationAnnotation,
|
526
|
+
prediction: SegmentationPrediction,
|
470
527
|
) -> ScalarResult:
|
471
|
-
if annotation is None or prediction is None:
|
472
|
-
# TODO: Throw error when we wrap each item in try catch
|
473
|
-
return ScalarResult(0, weight=0)
|
474
528
|
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
return ScalarResult(0, weight=0)
|
529
|
+
ap_per_threshold = []
|
530
|
+
weight = 0
|
531
|
+
for iou_threshold in self.iou_thresholds:
|
532
|
+
ap = SegmentationPrecision(
|
533
|
+
self.annotation_filters, self.prediction_filters, iou_threshold
|
534
|
+
)
|
535
|
+
ap.loader = self.loader
|
536
|
+
ap_result = ap(
|
537
|
+
AnnotationList(segmentation_annotations=[annotation]),
|
538
|
+
PredictionList(segmentation_predictions=[prediction]),
|
539
|
+
)
|
540
|
+
ap_per_threshold.append(ap_result.value) # type: ignore
|
541
|
+
weight += ap_result.weight # type: ignore
|
542
|
+
|
543
|
+
thresholds = np.concatenate([[0], self.iou_thresholds, [1]])
|
544
|
+
steps = np.diff(thresholds)
|
545
|
+
mean_ap = (
|
546
|
+
np.array(ap_per_threshold + [ap_per_threshold[-1]]) * steps
|
547
|
+
).sum()
|
548
|
+
return ScalarResult(mean_ap, weight=weight)
|
496
549
|
|
497
550
|
def aggregate_score(self, results: List[MetricResult]) -> ScalarResult:
|
498
551
|
return ScalarResult.aggregate(results) # type: ignore
|
@@ -542,6 +595,7 @@ class SegmentationFWAVACC(SegmentationMaskMetric):
|
|
542
595
|
prediction_filters: Optional[
|
543
596
|
Union[ListOfOrAndFilters, ListOfAndFilters]
|
544
597
|
] = None,
|
598
|
+
iou_threshold: float = 0.5,
|
545
599
|
):
|
546
600
|
"""Initializes SegmentationFWAVACC object.
|
547
601
|
|
@@ -566,33 +620,40 @@ class SegmentationFWAVACC(SegmentationMaskMetric):
|
|
566
620
|
Finally, the most outer list combines these filters as a disjunction (OR).
|
567
621
|
"""
|
568
622
|
super().__init__(
|
569
|
-
annotation_filters
|
570
|
-
prediction_filters
|
623
|
+
annotation_filters,
|
624
|
+
prediction_filters,
|
625
|
+
iou_threshold,
|
571
626
|
)
|
572
627
|
|
573
628
|
def _metric_impl(
|
574
629
|
self,
|
575
630
|
annotation_img: np.ndarray,
|
576
631
|
prediction_img: np.ndarray,
|
577
|
-
annotation:
|
578
|
-
prediction:
|
632
|
+
annotation: SegmentationAnnotation,
|
633
|
+
prediction: SegmentationPrediction,
|
579
634
|
) -> ScalarResult:
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
635
|
+
confusion, non_taxonomy_classes = self._calculate_confusion_matrix(
|
636
|
+
annotation,
|
637
|
+
annotation_img,
|
638
|
+
prediction,
|
639
|
+
prediction_img,
|
640
|
+
self.iou_threshold,
|
586
641
|
)
|
587
642
|
with np.errstate(divide="ignore", invalid="ignore"):
|
588
|
-
iu = np.diag(
|
589
|
-
|
590
|
-
+
|
591
|
-
- np.diag(
|
643
|
+
iu = np.diag(confusion) / (
|
644
|
+
confusion.sum(axis=1)
|
645
|
+
+ confusion.sum(axis=0)
|
646
|
+
- np.diag(confusion)
|
592
647
|
)
|
593
|
-
|
594
|
-
|
595
|
-
|
648
|
+
predicted_counts = confusion.sum(axis=0).astype(np.float_)
|
649
|
+
predicted_counts.put(list(non_taxonomy_classes), np.nan)
|
650
|
+
freq = predicted_counts / np.nansum(predicted_counts)
|
651
|
+
iu.put(list(non_taxonomy_classes), np.nan)
|
652
|
+
fwavacc = (
|
653
|
+
np.nan_to_num(freq[freq > 0]) * np.nan_to_num(iu[freq > 0])
|
654
|
+
).sum()
|
655
|
+
mean_fwavacc = np.nanmean(fwavacc)
|
656
|
+
return ScalarResult(value=np.nan_to_num(mean_fwavacc), weight=confusion.sum()) # type: ignore
|
596
657
|
|
597
658
|
def aggregate_score(self, results: List[MetricResult]) -> ScalarResult:
|
598
659
|
return ScalarResult.aggregate(results) # type: ignore
|