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
@@ -1,16 +1,21 @@
1
1
  import abc
2
+ import logging
3
+ from enum import Enum
2
4
  from typing import List, Optional, Union
3
5
 
4
- import fsspec
5
6
  import numpy as np
6
- from PIL import Image
7
- from s3fs import S3FileSystem
8
7
 
9
- from nucleus.annotation import AnnotationList
8
+ from nucleus.annotation import AnnotationList, SegmentationAnnotation
10
9
  from nucleus.metrics.base import MetricResult
11
- from nucleus.metrics.filtering import ListOfAndFilters, ListOfOrAndFilters
10
+ from nucleus.metrics.filtering import (
11
+ ListOfAndFilters,
12
+ ListOfOrAndFilters,
13
+ apply_filters,
14
+ )
12
15
  from nucleus.metrics.segmentation_utils import (
13
16
  instance_mask_to_polys,
17
+ rasterize_polygons_to_segmentation_mask,
18
+ setup_iou_thresholds,
14
19
  transform_poly_codes_to_poly_preds,
15
20
  )
16
21
  from nucleus.prediction import PredictionList
@@ -19,20 +24,25 @@ from .base import Metric, ScalarResult
19
24
  from .polygon_metrics import (
20
25
  PolygonAveragePrecision,
21
26
  PolygonIOU,
22
- PolygonMAP,
23
27
  PolygonPrecision,
24
28
  PolygonRecall,
25
29
  )
30
+ from .segmentation_loader import (
31
+ DummyLoader,
32
+ InMemoryLoader,
33
+ SegmentationMaskLoader,
34
+ )
35
+ from .segmentation_metrics import (
36
+ SegmentationIOU,
37
+ SegmentationMAP,
38
+ SegmentationPrecision,
39
+ SegmentationRecall,
40
+ )
26
41
 
27
42
 
28
- class SegmentationMaskLoader:
29
- def __init__(self, fs: fsspec):
30
- self.fs = fs
31
-
32
- def fetch(self, url: str):
33
- with self.fs.open(url) as fh:
34
- img = Image.open(fh)
35
- return img
43
+ class SegToPolyMode(str, Enum):
44
+ GENERATE_GT_FROM_POLY = "gt_from_poly"
45
+ GENERATE_PRED_POLYS_FROM_MASK = "gt_from_poly"
36
46
 
37
47
 
38
48
  class SegmentationMaskToPolyMetric(Metric):
@@ -46,6 +56,7 @@ class SegmentationMaskToPolyMetric(Metric):
46
56
  prediction_filters: Optional[
47
57
  Union[ListOfOrAndFilters, ListOfAndFilters]
48
58
  ] = None,
59
+ mode: SegToPolyMode = SegToPolyMode.GENERATE_GT_FROM_POLY,
49
60
  ):
50
61
  """Initializes PolygonMetric abstract object.
51
62
 
@@ -71,11 +82,15 @@ class SegmentationMaskToPolyMetric(Metric):
71
82
  (AND), forming a more selective `and` multiple field predicate.
72
83
  Finally, the most outer list combines these filters as a disjunction (OR).
73
84
  """
74
- super().__init__(annotation_filters, prediction_filters)
85
+ # Since segmentation annotations are very different from everything else we can't rely on the upper filtering
86
+ super().__init__(None, None)
87
+ self._annotation_filters = annotation_filters
88
+ self._prediction_filters = prediction_filters
75
89
  self.enforce_label_match = enforce_label_match
76
90
  assert 0 <= confidence_threshold <= 1
77
91
  self.confidence_threshold = confidence_threshold
78
- self.loader = SegmentationMaskLoader(S3FileSystem(anon=False))
92
+ self.loader: SegmentationMaskLoader = DummyLoader()
93
+ self.mode = mode
79
94
 
80
95
  def call_metric(
81
96
  self, annotations: AnnotationList, predictions: PredictionList
@@ -88,22 +103,92 @@ class SegmentationMaskToPolyMetric(Metric):
88
103
  if predictions.segmentation_predictions
89
104
  else None
90
105
  )
91
- pred_img = self.loader.fetch(prediction.mask_url)
92
- pred_value, pred_polys = instance_mask_to_polys(
93
- np.asarray(pred_img)
94
- ) # typing: ignore
95
- code_to_label = {s.index: s.label for s in prediction.annotations}
96
- poly_predictions = transform_poly_codes_to_poly_preds(
97
- prediction.reference_id, pred_value, pred_polys, code_to_label
106
+ annotations.polygon_annotations = apply_filters(
107
+ annotations.polygon_annotations, self._annotation_filters # type: ignore
98
108
  )
99
- return self.call_poly_metric(
100
- annotations, PredictionList(polygon_predictions=poly_predictions)
109
+ annotations.box_annotations = apply_filters(
110
+ annotations.box_annotations, self._annotation_filters # type: ignore
111
+ )
112
+ predictions.segmentation_predictions = apply_filters(
113
+ predictions.segmentation_predictions, self._prediction_filters # type: ignore
114
+ )
115
+ if prediction:
116
+ if self.mode == SegToPolyMode.GENERATE_GT_FROM_POLY:
117
+ pred_img = self.loader.fetch(prediction.mask_url)
118
+ ann_img, segments = rasterize_polygons_to_segmentation_mask(
119
+ annotations.polygon_annotations
120
+ + annotations.box_annotations, # type:ignore
121
+ pred_img.shape,
122
+ )
123
+ # TODO: apply Segmentation filters after?
124
+ annotations.segmentation_annotations = [
125
+ SegmentationAnnotation(
126
+ "__no_url",
127
+ annotations=segments,
128
+ reference_id=annotations.polygon_annotations[
129
+ 0
130
+ ].reference_id,
131
+ )
132
+ ]
133
+ return self.call_segmentation_metric(
134
+ annotations,
135
+ np.asarray(ann_img),
136
+ predictions,
137
+ np.asarray(pred_img),
138
+ )
139
+ elif self.mode == SegToPolyMode.GENERATE_PRED_POLYS_FROM_MASK:
140
+ pred_img = self.loader.fetch(prediction.mask_url)
141
+ pred_value, pred_polys = instance_mask_to_polys(
142
+ np.asarray(pred_img)
143
+ ) # typing: ignore
144
+ code_to_label = {
145
+ s.index: s.label for s in prediction.annotations
146
+ }
147
+ poly_predictions = transform_poly_codes_to_poly_preds(
148
+ prediction.reference_id,
149
+ pred_value,
150
+ pred_polys,
151
+ code_to_label,
152
+ )
153
+ return self.call_poly_metric(
154
+ annotations,
155
+ PredictionList(polygon_predictions=poly_predictions),
156
+ )
157
+ else:
158
+ raise RuntimeError(
159
+ f"Misonconfigured class. Got mode '{self.mode}', expected one of {list(SegToPolyMode)}"
160
+ )
161
+ else:
162
+ return ScalarResult(0, weight=0)
163
+
164
+ def call_segmentation_metric(
165
+ self,
166
+ annotations: AnnotationList,
167
+ ann_img: np.ndarray,
168
+ predictions: PredictionList,
169
+ pred_img: np.ndarray,
170
+ ):
171
+ metric = self.configure_metric()
172
+ metric.loader = InMemoryLoader(
173
+ {
174
+ annotations.segmentation_annotations[0].mask_url: ann_img,
175
+ predictions.segmentation_predictions[0].mask_url: pred_img,
176
+ }
101
177
  )
178
+ return metric(annotations, predictions)
102
179
 
103
- @abc.abstractmethod
104
180
  def call_poly_metric(
105
181
  self, annotations: AnnotationList, predictions: PredictionList
106
182
  ):
183
+ metric = self.configure_metric()
184
+ return metric(annotations, predictions)
185
+
186
+ def aggregate_score(self, results: List[MetricResult]) -> ScalarResult:
187
+ metric = self.configure_metric()
188
+ return metric.aggregate_score(results) # type: ignore
189
+
190
+ @abc.abstractmethod
191
+ def configure_metric(self):
107
192
  pass
108
193
 
109
194
 
@@ -119,6 +204,7 @@ class SegmentationToPolyIOU(SegmentationMaskToPolyMetric):
119
204
  prediction_filters: Optional[
120
205
  Union[ListOfOrAndFilters, ListOfAndFilters]
121
206
  ] = None,
207
+ mode: SegToPolyMode = SegToPolyMode.GENERATE_GT_FROM_POLY,
122
208
  ):
123
209
  """Initializes PolygonIOU object.
124
210
 
@@ -154,22 +240,25 @@ class SegmentationToPolyIOU(SegmentationMaskToPolyMetric):
154
240
  confidence_threshold,
155
241
  annotation_filters,
156
242
  prediction_filters,
157
- )
158
- self.metric = PolygonIOU(
159
- self.enforce_label_match,
160
- self.iou_threshold,
161
- self.confidence_threshold,
162
- self.annotation_filters,
163
- self.prediction_filters,
243
+ mode,
164
244
  )
165
245
 
166
- def call_poly_metric(
167
- self, annotations: AnnotationList, predictions: PredictionList
168
- ):
169
- return self.metric(annotations, predictions)
170
-
171
- def aggregate_score(self, results: List[MetricResult]) -> ScalarResult:
172
- return self.metric.aggregate_score(results) # type: ignore
246
+ def configure_metric(self):
247
+ if self.mode == SegToPolyMode.GENERATE_GT_FROM_POLY:
248
+ metric = SegmentationIOU(
249
+ self.annotation_filters,
250
+ self.prediction_filters,
251
+ self.iou_threshold,
252
+ )
253
+ else:
254
+ metric = PolygonIOU(
255
+ self.enforce_label_match,
256
+ self.iou_threshold,
257
+ self.confidence_threshold,
258
+ self.annotation_filters,
259
+ self.prediction_filters,
260
+ )
261
+ return metric
173
262
 
174
263
 
175
264
  class SegmentationToPolyPrecision(SegmentationMaskToPolyMetric):
@@ -184,6 +273,7 @@ class SegmentationToPolyPrecision(SegmentationMaskToPolyMetric):
184
273
  prediction_filters: Optional[
185
274
  Union[ListOfOrAndFilters, ListOfAndFilters]
186
275
  ] = None,
276
+ mode: SegToPolyMode = SegToPolyMode.GENERATE_GT_FROM_POLY,
187
277
  ):
188
278
  """Initializes SegmentationToPolyPrecision object.
189
279
 
@@ -219,22 +309,25 @@ class SegmentationToPolyPrecision(SegmentationMaskToPolyMetric):
219
309
  confidence_threshold,
220
310
  annotation_filters,
221
311
  prediction_filters,
312
+ mode,
222
313
  )
223
- self.metric = PolygonPrecision(
224
- self.enforce_label_match,
225
- self.iou_threshold,
226
- self.confidence_threshold,
227
- self.annotation_filters,
228
- self.prediction_filters,
229
- )
230
-
231
- def call_poly_metric(
232
- self, annotations: AnnotationList, predictions: PredictionList
233
- ):
234
- return self.metric(annotations, predictions)
235
314
 
236
- def aggregate_score(self, results: List[MetricResult]) -> ScalarResult:
237
- return self.metric.aggregate_score(results) # type: ignore
315
+ def configure_metric(self):
316
+ if self.mode == SegToPolyMode.GENERATE_GT_FROM_POLY:
317
+ metric = SegmentationPrecision(
318
+ self.annotation_filters,
319
+ self.prediction_filters,
320
+ self.iou_threshold,
321
+ )
322
+ else:
323
+ metric = PolygonPrecision(
324
+ self.enforce_label_match,
325
+ self.iou_threshold,
326
+ self.confidence_threshold,
327
+ self.annotation_filters,
328
+ self.prediction_filters,
329
+ )
330
+ return metric
238
331
 
239
332
 
240
333
  class SegmentationToPolyRecall(SegmentationMaskToPolyMetric):
@@ -284,6 +377,7 @@ class SegmentationToPolyRecall(SegmentationMaskToPolyMetric):
284
377
  prediction_filters: Optional[
285
378
  Union[ListOfOrAndFilters, ListOfAndFilters]
286
379
  ] = None,
380
+ mode: SegToPolyMode = SegToPolyMode.GENERATE_GT_FROM_POLY,
287
381
  ):
288
382
  """Initializes PolygonRecall object.
289
383
 
@@ -317,24 +411,27 @@ class SegmentationToPolyRecall(SegmentationMaskToPolyMetric):
317
411
  super().__init__(
318
412
  enforce_label_match,
319
413
  confidence_threshold,
320
- annotation_filters=annotation_filters,
321
- prediction_filters=prediction_filters,
322
- )
323
- self.metric = PolygonRecall(
324
- self.enforce_label_match,
325
- self.iou_threshold,
326
- self.confidence_threshold,
327
- self.annotation_filters,
328
- self.prediction_filters,
414
+ annotation_filters,
415
+ prediction_filters,
416
+ mode,
329
417
  )
330
418
 
331
- def call_poly_metric(
332
- self, annotations: AnnotationList, predictions: PredictionList
333
- ):
334
- return self.metric(annotations, predictions)
335
-
336
- def aggregate_score(self, results: List[MetricResult]) -> ScalarResult:
337
- return self.metric.aggregate_score(results) # type: ignore
419
+ def configure_metric(self):
420
+ if self.mode == SegToPolyMode.GENERATE_GT_FROM_POLY:
421
+ metric = SegmentationRecall(
422
+ self.annotation_filters,
423
+ self.prediction_filters,
424
+ self.iou_threshold,
425
+ )
426
+ else:
427
+ metric = PolygonRecall(
428
+ self.enforce_label_match,
429
+ self.iou_threshold,
430
+ self.confidence_threshold,
431
+ self.annotation_filters,
432
+ self.prediction_filters,
433
+ )
434
+ return metric
338
435
 
339
436
 
340
437
  class SegmentationToPolyAveragePrecision(SegmentationMaskToPolyMetric):
@@ -383,6 +480,7 @@ class SegmentationToPolyAveragePrecision(SegmentationMaskToPolyMetric):
383
480
  prediction_filters: Optional[
384
481
  Union[ListOfOrAndFilters, ListOfAndFilters]
385
482
  ] = None,
483
+ mode: SegToPolyMode = SegToPolyMode.GENERATE_GT_FROM_POLY,
386
484
  ):
387
485
  """Initializes PolygonRecall object.
388
486
 
@@ -418,20 +516,23 @@ class SegmentationToPolyAveragePrecision(SegmentationMaskToPolyMetric):
418
516
  annotation_filters=annotation_filters,
419
517
  prediction_filters=prediction_filters,
420
518
  )
421
- self.metric = PolygonAveragePrecision(
422
- self.label,
423
- self.iou_threshold,
424
- self.annotation_filters,
425
- self.prediction_filters,
426
- )
427
519
 
428
- def call_poly_metric(
429
- self, annotations: AnnotationList, predictions: PredictionList
430
- ):
431
- return self.metric(annotations, predictions)
432
-
433
- def aggregate_score(self, results: List[MetricResult]) -> ScalarResult:
434
- return self.metric.aggregate_score(results) # type: ignore
520
+ def configure_metric(self):
521
+ if self.mode == SegToPolyMode.GENERATE_GT_FROM_POLY:
522
+ # TODO(gunnar): Add a label filter
523
+ metric = SegmentationPrecision(
524
+ self.annotation_filters,
525
+ self.prediction_filters,
526
+ self.iou_threshold,
527
+ )
528
+ else:
529
+ metric = PolygonAveragePrecision(
530
+ self.label,
531
+ self.iou_threshold,
532
+ self.annotation_filters,
533
+ self.prediction_filters,
534
+ )
535
+ return metric
435
536
 
436
537
 
437
538
  class SegmentationToPolyMAP(SegmentationMaskToPolyMetric):
@@ -472,18 +573,20 @@ class SegmentationToPolyMAP(SegmentationMaskToPolyMetric):
472
573
  # TODO: Remove defaults once these are surfaced more cleanly to users.
473
574
  def __init__(
474
575
  self,
475
- iou_threshold: float = 0.5,
576
+ iou_threshold: float = -1,
577
+ iou_thresholds: Union[List[float], str] = "coco",
476
578
  annotation_filters: Optional[
477
579
  Union[ListOfOrAndFilters, ListOfAndFilters]
478
580
  ] = None,
479
581
  prediction_filters: Optional[
480
582
  Union[ListOfOrAndFilters, ListOfAndFilters]
481
583
  ] = None,
584
+ mode: SegToPolyMode = SegToPolyMode.GENERATE_GT_FROM_POLY,
482
585
  ):
483
586
  """Initializes PolygonRecall object.
484
587
 
485
588
  Args:
486
- iou_threshold: IOU threshold to consider detection as valid. Must be in [0, 1]. Default 0.5
589
+ iou_thresholds: IOU thresholds to check AP at
487
590
  annotation_filters: Filter predicates. Allowed formats are:
488
591
  ListOfAndFilters where each Filter forms a chain of AND predicates.
489
592
  or
@@ -503,26 +606,47 @@ class SegmentationToPolyMAP(SegmentationMaskToPolyMetric):
503
606
  (AND), forming a more selective `and` multiple field predicate.
504
607
  Finally, the most outer list combines these filters as a disjunction (OR).
505
608
  """
506
- assert (
507
- 0 <= iou_threshold <= 1
508
- ), "IoU threshold must be between 0 and 1."
509
- self.iou_threshold = iou_threshold
609
+ if iou_threshold:
610
+ logging.warning(
611
+ "Got deprecated parameter 'iou_threshold'. Ignoring it."
612
+ )
613
+ self.iou_thresholds = setup_iou_thresholds(iou_thresholds)
510
614
  super().__init__(
511
- enforce_label_match=False,
512
- confidence_threshold=0,
513
- annotation_filters=annotation_filters,
514
- prediction_filters=prediction_filters,
515
- )
516
- self.metric = PolygonMAP(
517
- self.iou_threshold,
518
- self.annotation_filters,
519
- self.prediction_filters,
615
+ False, 0, annotation_filters, prediction_filters, mode
520
616
  )
521
617
 
522
- def call_poly_metric(
523
- self, annotations: AnnotationList, predictions: PredictionList
524
- ):
525
- return self.metric(annotations, predictions)
526
-
527
- def aggregate_score(self, results: List[MetricResult]) -> ScalarResult:
528
- return self.metric.aggregate_score(results) # type: ignore
618
+ def configure_metric(self):
619
+ if self.mode == SegToPolyMode.GENERATE_GT_FROM_POLY:
620
+ # TODO(gunnar): Add a label filter
621
+ metric = SegmentationMAP(
622
+ self.annotation_filters,
623
+ self.prediction_filters,
624
+ self.iou_thresholds,
625
+ )
626
+ else:
627
+
628
+ def patched_average_precision(annotations, predictions):
629
+ ap_per_threshold = []
630
+ labels = [p.label for p in predictions.polygon_predictions]
631
+ for threshold in self.iou_thresholds:
632
+ ap_per_label = []
633
+ for label in labels:
634
+ call_metric = PolygonAveragePrecision(
635
+ label,
636
+ iou_threshold=threshold,
637
+ annotation_filters=self.annotation_filters,
638
+ prediction_filters=self.prediction_filters,
639
+ )
640
+ result = call_metric(annotations, predictions)
641
+ ap_per_label.append(result.value) # type: ignore
642
+ ap_per_threshold = np.mean(ap_per_label)
643
+
644
+ thresholds = np.concatenate([[0], self.iou_thresholds, [1]])
645
+ steps = np.diff(thresholds)
646
+ mean_ap = (
647
+ np.array(ap_per_threshold + [ap_per_threshold[-1]]) * steps
648
+ ).sum()
649
+ return ScalarResult(mean_ap)
650
+
651
+ metric = patched_average_precision
652
+ return metric