scale-nucleus 0.1.22__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 +453 -699
  14. nucleus/annotation.py +435 -80
  15. nucleus/autocurate.py +9 -0
  16. nucleus/connection.py +87 -0
  17. nucleus/constants.py +12 -2
  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 +1139 -215
  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 +21 -1
  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 +30 -18
  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 +139 -35
  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.22.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.22.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.22.dist-info/METADATA +0 -85
  73. scale_nucleus-0.1.22.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
@@ -8,122 +8,111 @@ from .constants import (
8
8
  ANNOTATION_ID_KEY,
9
9
  ANNOTATIONS_KEY,
10
10
  BOX_TYPE,
11
+ CATEGORY_TYPE,
11
12
  CUBOID_TYPE,
12
13
  DIMENSIONS_KEY,
13
14
  GEOMETRY_KEY,
14
15
  HEIGHT_KEY,
15
16
  INDEX_KEY,
16
17
  LABEL_KEY,
18
+ LABELS_KEY,
17
19
  MASK_TYPE,
18
20
  MASK_URL_KEY,
19
21
  METADATA_KEY,
22
+ MULTICATEGORY_TYPE,
20
23
  POLYGON_TYPE,
21
24
  POSITION_KEY,
22
25
  REFERENCE_ID_KEY,
26
+ TAXONOMY_NAME_KEY,
23
27
  TYPE_KEY,
24
28
  VERTICES_KEY,
25
29
  WIDTH_KEY,
26
30
  X_KEY,
27
- YAW_KEY,
28
31
  Y_KEY,
32
+ YAW_KEY,
29
33
  Z_KEY,
30
34
  )
31
35
 
32
36
 
33
37
  class Annotation:
38
+ """Internal base class, not to be used directly.
39
+
40
+ .. todo ::
41
+ Inherit common constructor parameters from here
42
+ """
43
+
34
44
  reference_id: str
35
45
 
36
46
  @classmethod
37
47
  def from_json(cls, payload: dict):
48
+ """Instantiates annotation object from schematized JSON dict payload."""
38
49
  if payload.get(TYPE_KEY, None) == BOX_TYPE:
39
50
  return BoxAnnotation.from_json(payload)
40
51
  elif payload.get(TYPE_KEY, None) == POLYGON_TYPE:
41
52
  return PolygonAnnotation.from_json(payload)
42
53
  elif payload.get(TYPE_KEY, None) == CUBOID_TYPE:
43
54
  return CuboidAnnotation.from_json(payload)
55
+ elif payload.get(TYPE_KEY, None) == CATEGORY_TYPE:
56
+ return CategoryAnnotation.from_json(payload)
57
+ elif payload.get(TYPE_KEY, None) == MULTICATEGORY_TYPE:
58
+ return MultiCategoryAnnotation.from_json(payload)
44
59
  else:
45
60
  return SegmentationAnnotation.from_json(payload)
46
61
 
47
- def to_payload(self):
62
+ def to_payload(self) -> dict:
63
+ """Serializes annotation object to schematized JSON dict."""
48
64
  raise NotImplementedError(
49
- "For serialization, use a specific subclass (i.e. SegmentationAnnotation), "
65
+ "For serialization, use a specific subclass (e.g. SegmentationAnnotation), "
50
66
  "not the base annotation class."
51
67
  )
52
68
 
53
69
  def to_json(self) -> str:
70
+ """Serializes annotation object to schematized JSON string."""
54
71
  return json.dumps(self.to_payload(), allow_nan=False)
55
72
 
56
73
 
57
- @dataclass
58
- class Segment:
59
- label: str
60
- index: int
61
- metadata: Optional[dict] = None
62
-
63
- @classmethod
64
- def from_json(cls, payload: dict):
65
- return cls(
66
- label=payload.get(LABEL_KEY, ""),
67
- index=payload.get(INDEX_KEY, None),
68
- metadata=payload.get(METADATA_KEY, None),
69
- )
74
+ @dataclass # pylint: disable=R0902
75
+ class BoxAnnotation(Annotation): # pylint: disable=R0902
76
+ """A bounding box annotation.
70
77
 
71
- def to_payload(self) -> dict:
72
- payload = {
73
- LABEL_KEY: self.label,
74
- INDEX_KEY: self.index,
75
- }
76
- if self.metadata is not None:
77
- payload[METADATA_KEY] = self.metadata
78
- return payload
78
+ ::
79
79
 
80
+ from nucleus import BoxAnnotation
80
81
 
81
- @dataclass
82
- class SegmentationAnnotation(Annotation):
83
- mask_url: str
84
- annotations: List[Segment]
85
- reference_id: str
86
- annotation_id: Optional[str] = None
87
-
88
- def __post_init__(self):
89
- if not self.mask_url:
90
- raise Exception("You must specify a mask_url.")
91
-
92
- @classmethod
93
- def from_json(cls, payload: dict):
94
- if MASK_URL_KEY not in payload:
95
- raise ValueError(f"Missing {MASK_URL_KEY} in json")
96
- return cls(
97
- mask_url=payload[MASK_URL_KEY],
98
- annotations=[
99
- Segment.from_json(ann)
100
- for ann in payload.get(ANNOTATIONS_KEY, [])
101
- ],
102
- reference_id=payload[REFERENCE_ID_KEY],
103
- 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"}
104
91
  )
105
92
 
106
- def to_payload(self) -> dict:
107
- payload = {
108
- TYPE_KEY: MASK_TYPE,
109
- MASK_URL_KEY: self.mask_url,
110
- ANNOTATIONS_KEY: [ann.to_payload() for ann in self.annotations],
111
- ANNOTATION_ID_KEY: self.annotation_id,
112
- }
113
-
114
- payload[REFERENCE_ID_KEY] = self.reference_id
115
-
116
- return payload
117
-
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
+ """
118
115
 
119
- class AnnotationTypes(Enum):
120
- BOX = BOX_TYPE
121
- POLYGON = POLYGON_TYPE
122
- CUBOID = CUBOID_TYPE
123
-
124
-
125
- @dataclass # pylint: disable=R0902
126
- class BoxAnnotation(Annotation): # pylint: disable=R0902
127
116
  label: str
128
117
  x: Union[float, int]
129
118
  y: Union[float, int]
@@ -170,6 +159,13 @@ class BoxAnnotation(Annotation): # pylint: disable=R0902
170
159
 
171
160
  @dataclass
172
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
+
173
169
  x: float
174
170
  y: float
175
171
 
@@ -182,21 +178,36 @@ class Point:
182
178
 
183
179
 
184
180
  @dataclass
185
- class Point3D:
186
- x: float
187
- y: float
188
- z: float
181
+ class PolygonAnnotation(Annotation):
182
+ """A polygon annotation consisting of an ordered list of 2D points.
189
183
 
190
- @classmethod
191
- def from_json(cls, payload: Dict[str, float]):
192
- return cls(payload[X_KEY], payload[Y_KEY], payload[Z_KEY])
184
+ ::
193
185
 
194
- def to_payload(self) -> dict:
195
- return {X_KEY: self.x, Y_KEY: self.y, Z_KEY: self.z}
186
+ from nucleus import PolygonAnnotation
196
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
+ """
197
210
 
198
- @dataclass
199
- class PolygonAnnotation(Annotation):
200
211
  label: str
201
212
  vertices: List[Point]
202
213
  reference_id: str
@@ -246,8 +257,62 @@ class PolygonAnnotation(Annotation):
246
257
  return payload
247
258
 
248
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
+
249
282
  @dataclass # pylint: disable=R0902
250
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
+
251
316
  label: str
252
317
  position: Point3D
253
318
  dimensions: Point3D
@@ -291,6 +356,295 @@ class CuboidAnnotation(Annotation): # pylint: disable=R0902
291
356
  return payload
292
357
 
293
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
+
508
+ @dataclass
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
+
534
+ label: str
535
+ reference_id: str
536
+ taxonomy_name: Optional[str] = None
537
+ metadata: Optional[Dict] = None
538
+
539
+ def __post_init__(self):
540
+ self.metadata = self.metadata if self.metadata else {}
541
+
542
+ @classmethod
543
+ def from_json(cls, payload: dict):
544
+ return cls(
545
+ label=payload[LABEL_KEY],
546
+ reference_id=payload[REFERENCE_ID_KEY],
547
+ taxonomy_name=payload.get(TAXONOMY_NAME_KEY, None),
548
+ metadata=payload.get(METADATA_KEY, {}),
549
+ )
550
+
551
+ def to_payload(self) -> dict:
552
+ payload = {
553
+ LABEL_KEY: self.label,
554
+ TYPE_KEY: CATEGORY_TYPE,
555
+ GEOMETRY_KEY: {},
556
+ REFERENCE_ID_KEY: self.reference_id,
557
+ METADATA_KEY: self.metadata,
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
+ )
646
+
647
+
294
648
  def is_local_path(path: str) -> bool:
295
649
  return urlparse(path).scheme not in {"https", "http", "s3", "gs"}
296
650
 
@@ -302,5 +656,6 @@ def check_all_mask_paths_remote(
302
656
  if hasattr(annotation, MASK_URL_KEY):
303
657
  if is_local_path(getattr(annotation, MASK_URL_KEY)):
304
658
  raise ValueError(
305
- 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}"
306
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(