scale-nucleus 0.1.24__py3-none-any.whl → 0.6.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. cli/client.py +14 -0
  2. cli/datasets.py +77 -0
  3. cli/helpers/__init__.py +0 -0
  4. cli/helpers/nucleus_url.py +10 -0
  5. cli/helpers/web_helper.py +40 -0
  6. cli/install_completion.py +33 -0
  7. cli/jobs.py +42 -0
  8. cli/models.py +35 -0
  9. cli/nu.py +42 -0
  10. cli/reference.py +8 -0
  11. cli/slices.py +62 -0
  12. cli/tests.py +121 -0
  13. nucleus/__init__.py +446 -710
  14. nucleus/annotation.py +405 -85
  15. nucleus/autocurate.py +9 -0
  16. nucleus/connection.py +87 -0
  17. nucleus/constants.py +5 -1
  18. nucleus/data_transfer_object/__init__.py +0 -0
  19. nucleus/data_transfer_object/dataset_details.py +9 -0
  20. nucleus/data_transfer_object/dataset_info.py +26 -0
  21. nucleus/data_transfer_object/dataset_size.py +5 -0
  22. nucleus/data_transfer_object/scenes_list.py +18 -0
  23. nucleus/dataset.py +1137 -212
  24. nucleus/dataset_item.py +130 -26
  25. nucleus/dataset_item_uploader.py +297 -0
  26. nucleus/deprecation_warning.py +32 -0
  27. nucleus/errors.py +9 -0
  28. nucleus/job.py +71 -3
  29. nucleus/logger.py +9 -0
  30. nucleus/metadata_manager.py +45 -0
  31. nucleus/metrics/__init__.py +10 -0
  32. nucleus/metrics/base.py +117 -0
  33. nucleus/metrics/categorization_metrics.py +197 -0
  34. nucleus/metrics/errors.py +7 -0
  35. nucleus/metrics/filters.py +40 -0
  36. nucleus/metrics/geometry.py +198 -0
  37. nucleus/metrics/metric_utils.py +28 -0
  38. nucleus/metrics/polygon_metrics.py +480 -0
  39. nucleus/metrics/polygon_utils.py +299 -0
  40. nucleus/model.py +121 -15
  41. nucleus/model_run.py +34 -57
  42. nucleus/payload_constructor.py +29 -19
  43. nucleus/prediction.py +259 -17
  44. nucleus/pydantic_base.py +26 -0
  45. nucleus/retry_strategy.py +4 -0
  46. nucleus/scene.py +204 -19
  47. nucleus/slice.py +230 -67
  48. nucleus/upload_response.py +20 -9
  49. nucleus/url_utils.py +4 -0
  50. nucleus/utils.py +134 -37
  51. nucleus/validate/__init__.py +24 -0
  52. nucleus/validate/client.py +168 -0
  53. nucleus/validate/constants.py +20 -0
  54. nucleus/validate/data_transfer_objects/__init__.py +0 -0
  55. nucleus/validate/data_transfer_objects/eval_function.py +81 -0
  56. nucleus/validate/data_transfer_objects/scenario_test.py +19 -0
  57. nucleus/validate/data_transfer_objects/scenario_test_evaluations.py +11 -0
  58. nucleus/validate/data_transfer_objects/scenario_test_metric.py +12 -0
  59. nucleus/validate/errors.py +6 -0
  60. nucleus/validate/eval_functions/__init__.py +0 -0
  61. nucleus/validate/eval_functions/available_eval_functions.py +212 -0
  62. nucleus/validate/eval_functions/base_eval_function.py +60 -0
  63. nucleus/validate/scenario_test.py +143 -0
  64. nucleus/validate/scenario_test_evaluation.py +114 -0
  65. nucleus/validate/scenario_test_metric.py +14 -0
  66. nucleus/validate/utils.py +8 -0
  67. {scale_nucleus-0.1.24.dist-info → scale_nucleus-0.6.4.dist-info}/LICENSE +0 -0
  68. scale_nucleus-0.6.4.dist-info/METADATA +213 -0
  69. scale_nucleus-0.6.4.dist-info/RECORD +71 -0
  70. {scale_nucleus-0.1.24.dist-info → scale_nucleus-0.6.4.dist-info}/WHEEL +1 -1
  71. scale_nucleus-0.6.4.dist-info/entry_points.txt +3 -0
  72. scale_nucleus-0.1.24.dist-info/METADATA +0 -85
  73. scale_nucleus-0.1.24.dist-info/RECORD +0 -21
nucleus/annotation.py CHANGED
@@ -1,5 +1,5 @@
1
1
  import json
2
- from dataclasses import dataclass
2
+ from dataclasses import dataclass, field
3
3
  from enum import Enum
4
4
  from typing import Dict, List, Optional, Sequence, Union
5
5
  from urllib.parse import urlparse
@@ -15,9 +15,11 @@ from .constants import (
15
15
  HEIGHT_KEY,
16
16
  INDEX_KEY,
17
17
  LABEL_KEY,
18
+ LABELS_KEY,
18
19
  MASK_TYPE,
19
20
  MASK_URL_KEY,
20
21
  METADATA_KEY,
22
+ MULTICATEGORY_TYPE,
21
23
  POLYGON_TYPE,
22
24
  POSITION_KEY,
23
25
  REFERENCE_ID_KEY,
@@ -26,17 +28,24 @@ from .constants import (
26
28
  VERTICES_KEY,
27
29
  WIDTH_KEY,
28
30
  X_KEY,
29
- YAW_KEY,
30
31
  Y_KEY,
32
+ YAW_KEY,
31
33
  Z_KEY,
32
34
  )
33
35
 
34
36
 
35
37
  class Annotation:
38
+ """Internal base class, not to be used directly.
39
+
40
+ .. todo ::
41
+ Inherit common constructor parameters from here
42
+ """
43
+
36
44
  reference_id: str
37
45
 
38
46
  @classmethod
39
47
  def from_json(cls, payload: dict):
48
+ """Instantiates annotation object from schematized JSON dict payload."""
40
49
  if payload.get(TYPE_KEY, None) == BOX_TYPE:
41
50
  return BoxAnnotation.from_json(payload)
42
51
  elif payload.get(TYPE_KEY, None) == POLYGON_TYPE:
@@ -45,90 +54,65 @@ class Annotation:
45
54
  return CuboidAnnotation.from_json(payload)
46
55
  elif payload.get(TYPE_KEY, None) == CATEGORY_TYPE:
47
56
  return CategoryAnnotation.from_json(payload)
57
+ elif payload.get(TYPE_KEY, None) == MULTICATEGORY_TYPE:
58
+ return MultiCategoryAnnotation.from_json(payload)
48
59
  else:
49
60
  return SegmentationAnnotation.from_json(payload)
50
61
 
51
- def to_payload(self):
62
+ def to_payload(self) -> dict:
63
+ """Serializes annotation object to schematized JSON dict."""
52
64
  raise NotImplementedError(
53
- "For serialization, use a specific subclass (i.e. SegmentationAnnotation), "
65
+ "For serialization, use a specific subclass (e.g. SegmentationAnnotation), "
54
66
  "not the base annotation class."
55
67
  )
56
68
 
57
69
  def to_json(self) -> str:
70
+ """Serializes annotation object to schematized JSON string."""
58
71
  return json.dumps(self.to_payload(), allow_nan=False)
59
72
 
60
73
 
61
- @dataclass
62
- class Segment:
63
- label: str
64
- index: int
65
- metadata: Optional[dict] = None
66
-
67
- @classmethod
68
- def from_json(cls, payload: dict):
69
- return cls(
70
- label=payload.get(LABEL_KEY, ""),
71
- index=payload.get(INDEX_KEY, None),
72
- metadata=payload.get(METADATA_KEY, None),
73
- )
74
-
75
- def to_payload(self) -> dict:
76
- payload = {
77
- LABEL_KEY: self.label,
78
- INDEX_KEY: self.index,
79
- }
80
- if self.metadata is not None:
81
- payload[METADATA_KEY] = self.metadata
82
- return payload
83
-
74
+ @dataclass # pylint: disable=R0902
75
+ class BoxAnnotation(Annotation): # pylint: disable=R0902
76
+ """A bounding box annotation.
84
77
 
85
- @dataclass
86
- class SegmentationAnnotation(Annotation):
87
- mask_url: str
88
- annotations: List[Segment]
89
- reference_id: str
90
- annotation_id: Optional[str] = None
78
+ ::
91
79
 
92
- def __post_init__(self):
93
- if not self.mask_url:
94
- raise Exception("You must specify a mask_url.")
80
+ from nucleus import BoxAnnotation
95
81
 
96
- @classmethod
97
- def from_json(cls, payload: dict):
98
- if MASK_URL_KEY not in payload:
99
- raise ValueError(f"Missing {MASK_URL_KEY} in json")
100
- return cls(
101
- mask_url=payload[MASK_URL_KEY],
102
- annotations=[
103
- Segment.from_json(ann)
104
- for ann in payload.get(ANNOTATIONS_KEY, [])
105
- ],
106
- reference_id=payload[REFERENCE_ID_KEY],
107
- annotation_id=payload.get(ANNOTATION_ID_KEY, None),
82
+ box = BoxAnnotation(
83
+ label="car",
84
+ x=0,
85
+ y=0,
86
+ width=10,
87
+ height=10,
88
+ reference_id="image_1",
89
+ annotation_id="image_1_car_box_1",
90
+ metadata={"vehicle_color": "red"}
108
91
  )
109
92
 
110
- def to_payload(self) -> dict:
111
- payload = {
112
- TYPE_KEY: MASK_TYPE,
113
- MASK_URL_KEY: self.mask_url,
114
- ANNOTATIONS_KEY: [ann.to_payload() for ann in self.annotations],
115
- ANNOTATION_ID_KEY: self.annotation_id,
116
- }
117
-
118
- payload[REFERENCE_ID_KEY] = self.reference_id
119
-
120
- return payload
121
-
93
+ Parameters:
94
+ label (str): The label for this annotation.
95
+ x (Union[float, int]): The distance, in pixels, between the left border
96
+ of the bounding box and the left border of the image.
97
+ y (Union[float, int]): The distance, in pixels, between the top border
98
+ of the bounding box and the top border of the image.
99
+ width (Union[float, int]): The width in pixels of the annotation.
100
+ height (Union[float, int]): The height in pixels of the annotation.
101
+ reference_id (str): User-defined ID of the image to which to apply this
102
+ annotation.
103
+ annotation_id (Optional[str]): The annotation ID that uniquely
104
+ identifies this annotation within its target dataset item. Upon
105
+ ingest, a matching annotation id will be ignored by default, and
106
+ overwritten if update=True for dataset.annotate. If no annotation
107
+ ID is passed, one will be automatically generated using the label,
108
+ x, y, width, and height, so that you can make inserts idempotently
109
+ as identical boxes will be ignored.
110
+ metadata (Optional[Dict]): Arbitrary key/value dictionary of info to
111
+ attach to this annotation. Strings, floats and ints are supported best
112
+ by querying and insights features within Nucleus. For more details see
113
+ our `metadata guide <https://nucleus.scale.com/docs/upload-metadata>`_.
114
+ """
122
115
 
123
- class AnnotationTypes(Enum):
124
- BOX = BOX_TYPE
125
- POLYGON = POLYGON_TYPE
126
- CUBOID = CUBOID_TYPE
127
- CATEGORY = CATEGORY_TYPE
128
-
129
-
130
- @dataclass # pylint: disable=R0902
131
- class BoxAnnotation(Annotation): # pylint: disable=R0902
132
116
  label: str
133
117
  x: Union[float, int]
134
118
  y: Union[float, int]
@@ -175,6 +159,13 @@ class BoxAnnotation(Annotation): # pylint: disable=R0902
175
159
 
176
160
  @dataclass
177
161
  class Point:
162
+ """A point in 2D space.
163
+
164
+ Parameters:
165
+ x (float): The x coordinate of the point.
166
+ y (float): The y coordinate of the point.
167
+ """
168
+
178
169
  x: float
179
170
  y: float
180
171
 
@@ -187,21 +178,36 @@ class Point:
187
178
 
188
179
 
189
180
  @dataclass
190
- class Point3D:
191
- x: float
192
- y: float
193
- z: float
181
+ class PolygonAnnotation(Annotation):
182
+ """A polygon annotation consisting of an ordered list of 2D points.
194
183
 
195
- @classmethod
196
- def from_json(cls, payload: Dict[str, float]):
197
- return cls(payload[X_KEY], payload[Y_KEY], payload[Z_KEY])
184
+ ::
198
185
 
199
- def to_payload(self) -> dict:
200
- return {X_KEY: self.x, Y_KEY: self.y, Z_KEY: self.z}
186
+ from nucleus import PolygonAnnotation
201
187
 
188
+ polygon = PolygonAnnotation(
189
+ label="bus",
190
+ vertices=[Point(100, 100), Point(150, 200), Point(200, 100)],
191
+ reference_id="image_2",
192
+ annotation_id="image_2_bus_polygon_1",
193
+ metadata={"vehicle_color": "yellow"}
194
+ )
195
+
196
+ Parameters:
197
+ label (str): The label for this annotation.
198
+ vertices (List[:class:`Point`]): The list of points making up the polygon.
199
+ reference_id (str): User-defined ID of the image to which to apply this
200
+ annotation.
201
+ annotation_id (Optional[str]): The annotation ID that uniquely identifies
202
+ this annotation within its target dataset item. Upon ingest, a matching
203
+ annotation id will be ignored by default, and updated if update=True
204
+ for dataset.annotate.
205
+ metadata (Optional[Dict]): Arbitrary key/value dictionary of info to
206
+ attach to this annotation. Strings, floats and ints are supported best
207
+ by querying and insights features within Nucleus. For more details see
208
+ our `metadata guide <https://nucleus.scale.com/docs/upload-metadata>`_.
209
+ """
202
210
 
203
- @dataclass
204
- class PolygonAnnotation(Annotation):
205
211
  label: str
206
212
  vertices: List[Point]
207
213
  reference_id: str
@@ -251,8 +257,62 @@ class PolygonAnnotation(Annotation):
251
257
  return payload
252
258
 
253
259
 
260
+ @dataclass
261
+ class Point3D:
262
+ """A point in 3D space.
263
+
264
+ Parameters:
265
+ x (float): The x coordinate of the point.
266
+ y (float): The y coordinate of the point.
267
+ z (float): The z coordinate of the point.
268
+ """
269
+
270
+ x: float
271
+ y: float
272
+ z: float
273
+
274
+ @classmethod
275
+ def from_json(cls, payload: Dict[str, float]):
276
+ return cls(payload[X_KEY], payload[Y_KEY], payload[Z_KEY])
277
+
278
+ def to_payload(self) -> dict:
279
+ return {X_KEY: self.x, Y_KEY: self.y, Z_KEY: self.z}
280
+
281
+
254
282
  @dataclass # pylint: disable=R0902
255
283
  class CuboidAnnotation(Annotation): # pylint: disable=R0902
284
+ """A 3D Cuboid annotation.
285
+
286
+ ::
287
+
288
+ from nucleus import CuboidAnnotation
289
+
290
+ cuboid = CuboidAnnotation(
291
+ label="car",
292
+ position=Point3D(100, 100, 10),
293
+ dimensions=Point3D(5, 10, 5),
294
+ yaw=0,
295
+ reference_id="pointcloud_1",
296
+ annotation_id="pointcloud_1_car_cuboid_1",
297
+ metadata={"vehicle_color": "green"}
298
+ )
299
+
300
+ Parameters:
301
+ label (str): The label for this annotation.
302
+ position (:class:`Point3D`): The point at the center of the cuboid
303
+ dimensions (:class:`Point3D`): The length (x), width (y), and height (z) of the cuboid
304
+ yaw (float): The rotation, in radians, about the Z axis of the cuboid
305
+ reference_id (str): User-defined ID of the image to which to apply this annotation.
306
+ annotation_id (Optional[str]): The annotation ID that uniquely identifies this
307
+ annotation within its target dataset item. Upon ingest, a matching
308
+ annotation id will be ignored by default, and updated if update=True
309
+ for dataset.annotate.
310
+ metadata (Optional[str]): Arbitrary key/value dictionary of info to attach to this
311
+ annotation. Strings, floats and ints are supported best by querying
312
+ and insights features within Nucleus. For more details see our `metadata
313
+ guide <https://nucleus.scale.com/docs/upload-metadata>`_.
314
+ """
315
+
256
316
  label: str
257
317
  position: Point3D
258
318
  dimensions: Point3D
@@ -296,11 +356,184 @@ class CuboidAnnotation(Annotation): # pylint: disable=R0902
296
356
  return payload
297
357
 
298
358
 
359
+ @dataclass
360
+ class Segment:
361
+ """Segment represents either a class or an instance depending on the task type.
362
+
363
+ For semantic segmentation, this object should store the mapping between a single
364
+ class index and the string label.
365
+
366
+ For instance segmentation, you can use this class to store the label of a single
367
+ instance, whose extent in the image is represented by the value of ``index``.
368
+
369
+ In both cases, additional metadata can be attached to the segment.
370
+
371
+ Parameters:
372
+ label (str): The label name of the class for the class or instance
373
+ represented by index in the associated mask.
374
+ index (int): The integer pixel value in the mask this mapping refers to.
375
+ metadata (Optional[Dict]): Arbitrary key/value dictionary of info to attach to this segment.
376
+ Strings, floats and ints are supported best by querying and insights
377
+ features within Nucleus. For more details see our `metadata guide
378
+ <https://nucleus.scale.com/docs/upload-metadata>`_.
379
+ """
380
+
381
+ label: str
382
+ index: int
383
+ metadata: Optional[dict] = None
384
+
385
+ @classmethod
386
+ def from_json(cls, payload: dict):
387
+ return cls(
388
+ label=payload.get(LABEL_KEY, ""),
389
+ index=payload.get(INDEX_KEY, None),
390
+ metadata=payload.get(METADATA_KEY, None),
391
+ )
392
+
393
+ def to_payload(self) -> dict:
394
+ payload = {
395
+ LABEL_KEY: self.label,
396
+ INDEX_KEY: self.index,
397
+ }
398
+ if self.metadata is not None:
399
+ payload[METADATA_KEY] = self.metadata
400
+ return payload
401
+
402
+
403
+ @dataclass
404
+ class SegmentationAnnotation(Annotation):
405
+ """A segmentation mask on a 2D image.
406
+
407
+ When uploading a mask annotation, Nucleus expects the mask file to be in
408
+ PNG format with each pixel being a 0-255 uint8. Currently, Nucleus only
409
+ supports uploading masks from URL.
410
+
411
+ Nucleus automatically enforces the constraint that each DatasetItem can
412
+ have at most one ground truth segmentation mask. As a consequence, if
413
+ during upload a duplicate mask is detected for a given image, by default it
414
+ will be ignored. You can change this behavior by setting ``update = True``,
415
+ which will replace the existing segmentation mask with the new mask.
416
+
417
+ ::
418
+
419
+ from nucleus import SegmentationAnnotation
420
+
421
+ segmentation = SegmentationAnnotation(
422
+ mask_url="s3://your-bucket-name/segmentation-masks/image_2_mask_id1.png",
423
+ annotations=[
424
+ Segment(label="grass", index="1"),
425
+ Segment(label="road", index="2"),
426
+ Segment(label="bus", index="3", metadata={"vehicle_color": "yellow"}),
427
+ Segment(label="tree", index="4")
428
+ ],
429
+ reference_id="image_2",
430
+ annotation_id="image_2_mask_1",
431
+ )
432
+
433
+ Parameters:
434
+ mask_url (str): A URL pointing to the segmentation prediction mask which is
435
+ accessible to Scale. The mask is an HxW int8 array saved in PNG format,
436
+ with each pixel value ranging from [0, N), where N is the number of
437
+ possible classes (for semantic segmentation) or instances (for instance
438
+ segmentation).
439
+
440
+ The height and width of the mask must be the same as the
441
+ original image. One example for semantic segmentation: the mask is 0
442
+ for pixels where there is background, 1 where there is a car, and 2
443
+ where there is a pedestrian.
444
+
445
+ Another example for instance segmentation: the mask is 0 for one car,
446
+ 1 for another car, 2 for a motorcycle and 3 for another motorcycle.
447
+ The class name for each value in the mask is stored in the list of
448
+ Segment objects passed for "annotations"
449
+ annotations (List[:class:`Segment`]): The list of mappings between the integer values contained
450
+ in mask_url and string class labels. In the semantic segmentation
451
+ example above these would map that 0 to background, 1 to car and 2 to
452
+ pedestrian. In the instance segmentation example above, 0 and 1 would
453
+ both be mapped to car, 2 and 3 would both be mapped to motorcycle
454
+ reference_id (str): User-defined ID of the image to which to apply this annotation.
455
+ annotation_id (Optional[str]): For segmentation annotations, this value is ignored
456
+ because there can only be one segmentation annotation per dataset item.
457
+ Therefore regardless of annotation ID, if there is an existing
458
+ segmentation on a dataset item, it will be ignored unless update=True
459
+ is passed to :meth:`Dataset.annotate`, in which case it will be overwritten.
460
+ Storing a custom ID here may be useful in order to tie this annotation
461
+ to an external database, and its value will be returned for any export.
462
+ """
463
+
464
+ mask_url: str
465
+ annotations: List[Segment]
466
+ reference_id: str
467
+ annotation_id: Optional[str] = None
468
+
469
+ def __post_init__(self):
470
+ if not self.mask_url:
471
+ raise Exception("You must specify a mask_url.")
472
+
473
+ @classmethod
474
+ def from_json(cls, payload: dict):
475
+ if MASK_URL_KEY not in payload:
476
+ raise ValueError(f"Missing {MASK_URL_KEY} in json")
477
+ return cls(
478
+ mask_url=payload[MASK_URL_KEY],
479
+ annotations=[
480
+ Segment.from_json(ann)
481
+ for ann in payload.get(ANNOTATIONS_KEY, [])
482
+ ],
483
+ reference_id=payload[REFERENCE_ID_KEY],
484
+ annotation_id=payload.get(ANNOTATION_ID_KEY, None),
485
+ )
486
+
487
+ def to_payload(self) -> dict:
488
+ payload = {
489
+ TYPE_KEY: MASK_TYPE,
490
+ MASK_URL_KEY: self.mask_url,
491
+ ANNOTATIONS_KEY: [ann.to_payload() for ann in self.annotations],
492
+ ANNOTATION_ID_KEY: self.annotation_id,
493
+ }
494
+
495
+ payload[REFERENCE_ID_KEY] = self.reference_id
496
+
497
+ return payload
498
+
499
+
500
+ class AnnotationTypes(Enum):
501
+ BOX = BOX_TYPE
502
+ POLYGON = POLYGON_TYPE
503
+ CUBOID = CUBOID_TYPE
504
+ CATEGORY = CATEGORY_TYPE
505
+ MULTICATEGORY = MULTICATEGORY_TYPE
506
+
507
+
299
508
  @dataclass
300
509
  class CategoryAnnotation(Annotation):
510
+ """A category annotation.
511
+
512
+ ::
513
+
514
+ from nucleus import CategoryAnnotation
515
+
516
+ category = CategoryAnnotation(
517
+ label="dress",
518
+ reference_id="image_1",
519
+ taxonomy_name="clothing_type",
520
+ metadata={"dress_color": "navy"}
521
+ )
522
+
523
+ Parameters:
524
+ label (str): The label for this annotation.
525
+ reference_id (str): User-defined ID of the image to which to apply this annotation.
526
+ taxonomy_name (Optional[str]): The name of the taxonomy this annotation conforms to.
527
+ See :meth:`Dataset.add_taxonomy`.
528
+ metadata (Optional[Dict]): Arbitrary key/value dictionary of info to attach to this annotation.
529
+ Strings, floats and ints are supported best by querying and insights
530
+ features within Nucleus. For more details see our `metadata guide
531
+ <https://nucleus.scale.com/docs/upload-metadata>`_.
532
+ """
533
+
301
534
  label: str
302
- taxonomy_name: str
303
535
  reference_id: str
536
+ taxonomy_name: Optional[str] = None
304
537
  metadata: Optional[Dict] = None
305
538
 
306
539
  def __post_init__(self):
@@ -310,20 +543,106 @@ class CategoryAnnotation(Annotation):
310
543
  def from_json(cls, payload: dict):
311
544
  return cls(
312
545
  label=payload[LABEL_KEY],
313
- taxonomy_name=payload[TAXONOMY_NAME_KEY],
314
546
  reference_id=payload[REFERENCE_ID_KEY],
547
+ taxonomy_name=payload.get(TAXONOMY_NAME_KEY, None),
315
548
  metadata=payload.get(METADATA_KEY, {}),
316
549
  )
317
550
 
318
551
  def to_payload(self) -> dict:
319
- return {
552
+ payload = {
320
553
  LABEL_KEY: self.label,
321
- TAXONOMY_NAME_KEY: self.taxonomy_name,
322
554
  TYPE_KEY: CATEGORY_TYPE,
323
555
  GEOMETRY_KEY: {},
324
556
  REFERENCE_ID_KEY: self.reference_id,
325
557
  METADATA_KEY: self.metadata,
326
558
  }
559
+ if self.taxonomy_name is not None:
560
+ payload[TAXONOMY_NAME_KEY] = self.taxonomy_name
561
+ return payload
562
+
563
+
564
+ @dataclass
565
+ class MultiCategoryAnnotation(Annotation):
566
+ """This class is not yet supported: MultiCategory annotation support coming soon!"""
567
+
568
+ labels: List[str]
569
+ reference_id: str
570
+ taxonomy_name: Optional[str] = None
571
+ metadata: Optional[Dict] = None
572
+
573
+ def __post_init__(self):
574
+ self.metadata = self.metadata if self.metadata else {}
575
+
576
+ @classmethod
577
+ def from_json(cls, payload: dict):
578
+ return cls(
579
+ labels=payload[LABELS_KEY],
580
+ reference_id=payload[REFERENCE_ID_KEY],
581
+ taxonomy_name=payload.get(TAXONOMY_NAME_KEY, None),
582
+ metadata=payload.get(METADATA_KEY, {}),
583
+ )
584
+
585
+ def to_payload(self) -> dict:
586
+ payload = {
587
+ LABELS_KEY: self.labels,
588
+ TYPE_KEY: MULTICATEGORY_TYPE,
589
+ GEOMETRY_KEY: {},
590
+ REFERENCE_ID_KEY: self.reference_id,
591
+ METADATA_KEY: self.metadata,
592
+ }
593
+ if self.taxonomy_name is not None:
594
+ payload[TAXONOMY_NAME_KEY] = self.taxonomy_name
595
+ return payload
596
+
597
+
598
+ @dataclass
599
+ class AnnotationList:
600
+ """Wrapper class separating a list of annotations by type."""
601
+
602
+ box_annotations: List[BoxAnnotation] = field(default_factory=list)
603
+ polygon_annotations: List[PolygonAnnotation] = field(default_factory=list)
604
+ cuboid_annotations: List[CuboidAnnotation] = field(default_factory=list)
605
+ category_annotations: List[CategoryAnnotation] = field(
606
+ default_factory=list
607
+ )
608
+ multi_category_annotations: List[MultiCategoryAnnotation] = field(
609
+ default_factory=list
610
+ )
611
+ segmentation_annotations: List[SegmentationAnnotation] = field(
612
+ default_factory=list
613
+ )
614
+
615
+ def add_annotations(self, annotations: List[Annotation]):
616
+ for annotation in annotations:
617
+ assert isinstance(
618
+ annotation, Annotation
619
+ ), "Expected annotation to be of type 'Annotation"
620
+
621
+ if isinstance(annotation, BoxAnnotation):
622
+ self.box_annotations.append(annotation)
623
+ elif isinstance(annotation, PolygonAnnotation):
624
+ self.polygon_annotations.append(annotation)
625
+ elif isinstance(annotation, CuboidAnnotation):
626
+ self.cuboid_annotations.append(annotation)
627
+ elif isinstance(annotation, CategoryAnnotation):
628
+ self.category_annotations.append(annotation)
629
+ elif isinstance(annotation, MultiCategoryAnnotation):
630
+ self.multi_category_annotations.append(annotation)
631
+ else:
632
+ assert isinstance(
633
+ annotation, SegmentationAnnotation
634
+ ), f"Unexpected annotation type: {type(annotation)}"
635
+ self.segmentation_annotations.append(annotation)
636
+
637
+ def __len__(self):
638
+ return (
639
+ len(self.box_annotations)
640
+ + len(self.polygon_annotations)
641
+ + len(self.cuboid_annotations)
642
+ + len(self.category_annotations)
643
+ + len(self.multi_category_annotations)
644
+ + len(self.segmentation_annotations)
645
+ )
327
646
 
328
647
 
329
648
  def is_local_path(path: str) -> bool:
@@ -337,5 +656,6 @@ def check_all_mask_paths_remote(
337
656
  if hasattr(annotation, MASK_URL_KEY):
338
657
  if is_local_path(getattr(annotation, MASK_URL_KEY)):
339
658
  raise ValueError(
340
- f"Found an annotation with a local path, which is not currently supported. Use a remote path instead. {annotation}"
659
+ "Found an annotation with a local path, which is not currently"
660
+ f"supported. Use a remote path instead. {annotation}"
341
661
  )
nucleus/autocurate.py CHANGED
@@ -1,5 +1,13 @@
1
+ """Compute active learning metrics on your predictions.
2
+
3
+ For more details on usage see the example colab in scripts/autocurate_bdd.ipynb
4
+ """
5
+
6
+
1
7
  import datetime
8
+
2
9
  import requests
10
+
3
11
  from nucleus.constants import (
4
12
  JOB_CREATION_TIME_KEY,
5
13
  JOB_LAST_KNOWN_STATUS_KEY,
@@ -9,6 +17,7 @@ from nucleus.job import AsyncJob
9
17
 
10
18
 
11
19
  def entropy(name, model_run, client):
20
+ """Computes the mean entropy across all predictions for each image."""
12
21
  model_run_ids = [model_run.model_run_id]
13
22
  dataset_id = model_run.dataset_id
14
23
  response = client.make_request(