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.
Files changed (42) hide show
  1. cli/slices.py +14 -28
  2. nucleus/__init__.py +211 -18
  3. nucleus/annotation.py +28 -5
  4. nucleus/connection.py +9 -1
  5. nucleus/constants.py +9 -3
  6. nucleus/dataset.py +197 -59
  7. nucleus/dataset_item.py +11 -1
  8. nucleus/job.py +1 -1
  9. nucleus/metrics/__init__.py +2 -1
  10. nucleus/metrics/base.py +34 -56
  11. nucleus/metrics/categorization_metrics.py +6 -2
  12. nucleus/metrics/cuboid_utils.py +4 -6
  13. nucleus/metrics/errors.py +4 -0
  14. nucleus/metrics/filtering.py +369 -19
  15. nucleus/metrics/polygon_utils.py +3 -3
  16. nucleus/metrics/segmentation_loader.py +30 -0
  17. nucleus/metrics/segmentation_metrics.py +256 -195
  18. nucleus/metrics/segmentation_to_poly_metrics.py +229 -105
  19. nucleus/metrics/segmentation_utils.py +239 -8
  20. nucleus/model.py +66 -10
  21. nucleus/model_run.py +1 -1
  22. nucleus/{shapely_not_installed.py → package_not_installed.py} +3 -3
  23. nucleus/payload_constructor.py +4 -0
  24. nucleus/prediction.py +6 -3
  25. nucleus/scene.py +7 -0
  26. nucleus/slice.py +160 -16
  27. nucleus/utils.py +51 -12
  28. nucleus/validate/__init__.py +1 -0
  29. nucleus/validate/client.py +57 -8
  30. nucleus/validate/constants.py +1 -0
  31. nucleus/validate/data_transfer_objects/eval_function.py +22 -0
  32. nucleus/validate/data_transfer_objects/scenario_test_evaluations.py +13 -5
  33. nucleus/validate/eval_functions/available_eval_functions.py +33 -20
  34. nucleus/validate/eval_functions/config_classes/segmentation.py +2 -46
  35. nucleus/validate/scenario_test.py +71 -13
  36. nucleus/validate/scenario_test_evaluation.py +21 -21
  37. nucleus/validate/utils.py +1 -1
  38. {scale_nucleus-0.12b1.dist-info → scale_nucleus-0.14.14b0.dist-info}/LICENSE +0 -0
  39. {scale_nucleus-0.12b1.dist-info → scale_nucleus-0.14.14b0.dist-info}/METADATA +13 -11
  40. {scale_nucleus-0.12b1.dist-info → scale_nucleus-0.14.14b0.dist-info}/RECORD +42 -41
  41. {scale_nucleus-0.12b1.dist-info → scale_nucleus-0.14.14b0.dist-info}/WHEEL +1 -1
  42. {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 .metric_utils import compute_average_precision
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 = SegmentationMaskLoader(S3FileSystem(anon=False))
75
- # NOTE: We store histogram for re-use in subsequently calculated metrics
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
- ) -> MetricResult:
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
- annotation_img = self.loader.fetch(annotation.mask_url)
98
- pred_img = self.loader.fetch(prediction.mask_url)
99
- return self._metric_impl(
100
- np.asarray(annotation_img, dtype=np.int32),
101
- np.asarray(pred_img, dtype=np.int32),
102
- annotation,
103
- prediction,
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: Optional[SegmentationAnnotation],
112
- prediction: Optional[SegmentationPrediction],
119
+ annotation: SegmentationAnnotation,
120
+ prediction: SegmentationPrediction,
113
121
  ):
114
122
  pass
115
123
 
116
124
  def _calculate_confusion_matrix(
117
- self, annotation, annotation_img, prediction, prediction_img
118
- ):
119
- # NOTE: This creates a max(class_index) * max(class_index) MAT. If we have np.int16 this could become
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
- _fast_hist(annotation_img, prediction_img, num_classes)
131
- if self.confusion is None
132
- else self.confusion
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: Optional[SegmentationAnnotation],
179
- prediction: Optional[SegmentationPrediction],
275
+ annotation: SegmentationAnnotation,
276
+ prediction: SegmentationPrediction,
180
277
  ) -> ScalarResult:
181
- if annotation is None or prediction is None:
182
- # TODO: Throw error when we wrap each item in try catch
183
- return ScalarResult(0, weight=0)
184
-
185
- self.confusion = self._calculate_confusion_matrix(
186
- annotation, annotation_img, prediction, prediction_img
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
- iou = np.diag(self.confusion) / (
191
- self.confusion.sum(axis=1)
192
- + self.confusion.sum(axis=0)
193
- - np.diag(self.confusion)
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
- return ScalarResult(value=np.nanmean(iou), weight=annotation_img.size) # type: ignore
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: Optional[SegmentationAnnotation],
243
- prediction: Optional[SegmentationPrediction],
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
- if annotation is None or prediction is None:
308
- # TODO: Throw error when we wrap each item in try catch
309
- return ScalarResult(0, weight=0)
310
-
311
- self.confusion = self._calculate_confusion_matrix(
312
- annotation, annotation_img, prediction, prediction_img
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
- true_pos = np.diag(self.confusion)
317
- precision = true_pos / np.sum(self.confusion, axis=0)
318
- recall = true_pos / np.sum(self.confusion, axis=1)
319
- average_precision = compute_average_precision(
320
- np.nan_to_num(recall), np.nan_to_num(precision)
321
- )
322
- return ScalarResult(value=average_precision, weight=1)
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=annotation_filters,
365
- prediction_filters=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: Optional[SegmentationAnnotation],
373
- prediction: Optional[SegmentationPrediction],
421
+ annotation: SegmentationAnnotation,
422
+ prediction: SegmentationPrediction,
374
423
  ) -> ScalarResult:
375
- if annotation is None or prediction is None:
376
- # TODO: Throw error when we wrap each item in try catch
377
- return ScalarResult(0, weight=0)
378
-
379
- self.confusion = self._calculate_confusion_matrix(
380
- annotation, annotation_img, prediction, prediction_img
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
- true_pos = np.diag(self.confusion)
385
- recall = np.nanmean(true_pos / np.sum(self.confusion, axis=1))
386
- return ScalarResult(value=recall, weight=annotation_img.size) # type: ignore
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=annotation_filters,
461
- prediction_filters=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: Optional[SegmentationAnnotation],
469
- prediction: Optional[SegmentationPrediction],
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
- self.confusion = self._calculate_confusion_matrix(
476
- annotation, annotation_img, prediction, prediction_img
477
- )
478
- label_to_index = {a.label: a.index for a in annotation.annotations}
479
- num_classes = len(label_to_index.keys())
480
- ap_per_class = np.ndarray(num_classes) # type: ignore
481
- with np.errstate(divide="ignore", invalid="ignore"):
482
- for class_idx, (_, index) in enumerate(label_to_index.items()):
483
- true_pos = self.confusion[index, index]
484
- false_pos = self.confusion[:, index].sum()
485
- samples = true_pos + false_pos
486
- if samples:
487
- ap_per_class[class_idx] = true_pos / samples
488
- else:
489
- ap_per_class[class_idx] = np.nan
490
-
491
- if num_classes > 0:
492
- m_ap = np.nanmean(ap_per_class)
493
- return ScalarResult(m_ap, weight=1) # type: ignore
494
- else:
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=annotation_filters,
570
- prediction_filters=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: Optional[SegmentationAnnotation],
578
- prediction: Optional[SegmentationPrediction],
632
+ annotation: SegmentationAnnotation,
633
+ prediction: SegmentationPrediction,
579
634
  ) -> ScalarResult:
580
- if annotation is None or prediction is None:
581
- # TODO: Throw error when we wrap each item in try catch
582
- return ScalarResult(0, weight=0)
583
-
584
- self.confusion = self._calculate_confusion_matrix(
585
- annotation, annotation_img, prediction, prediction_img
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(self.confusion) / (
589
- self.confusion.sum(axis=1)
590
- + self.confusion.sum(axis=0)
591
- - np.diag(self.confusion)
643
+ iu = np.diag(confusion) / (
644
+ confusion.sum(axis=1)
645
+ + confusion.sum(axis=0)
646
+ - np.diag(confusion)
592
647
  )
593
- freq = self.confusion.sum(axis=1) / self.confusion.sum()
594
- fwavacc = (freq[freq > 0] * iu[freq > 0]).sum()
595
- return ScalarResult(value=np.nanmean(fwavacc), weight=1) # type: ignore
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