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.
- cli/client.py +14 -0
- cli/datasets.py +77 -0
- cli/helpers/__init__.py +0 -0
- cli/helpers/nucleus_url.py +10 -0
- cli/helpers/web_helper.py +40 -0
- cli/install_completion.py +33 -0
- cli/jobs.py +42 -0
- cli/models.py +35 -0
- cli/nu.py +42 -0
- cli/reference.py +8 -0
- cli/slices.py +62 -0
- cli/tests.py +121 -0
- nucleus/__init__.py +446 -710
- nucleus/annotation.py +405 -85
- nucleus/autocurate.py +9 -0
- nucleus/connection.py +87 -0
- nucleus/constants.py +5 -1
- nucleus/data_transfer_object/__init__.py +0 -0
- nucleus/data_transfer_object/dataset_details.py +9 -0
- nucleus/data_transfer_object/dataset_info.py +26 -0
- nucleus/data_transfer_object/dataset_size.py +5 -0
- nucleus/data_transfer_object/scenes_list.py +18 -0
- nucleus/dataset.py +1137 -212
- nucleus/dataset_item.py +130 -26
- nucleus/dataset_item_uploader.py +297 -0
- nucleus/deprecation_warning.py +32 -0
- nucleus/errors.py +9 -0
- nucleus/job.py +71 -3
- nucleus/logger.py +9 -0
- nucleus/metadata_manager.py +45 -0
- nucleus/metrics/__init__.py +10 -0
- nucleus/metrics/base.py +117 -0
- nucleus/metrics/categorization_metrics.py +197 -0
- nucleus/metrics/errors.py +7 -0
- nucleus/metrics/filters.py +40 -0
- nucleus/metrics/geometry.py +198 -0
- nucleus/metrics/metric_utils.py +28 -0
- nucleus/metrics/polygon_metrics.py +480 -0
- nucleus/metrics/polygon_utils.py +299 -0
- nucleus/model.py +121 -15
- nucleus/model_run.py +34 -57
- nucleus/payload_constructor.py +29 -19
- nucleus/prediction.py +259 -17
- nucleus/pydantic_base.py +26 -0
- nucleus/retry_strategy.py +4 -0
- nucleus/scene.py +204 -19
- nucleus/slice.py +230 -67
- nucleus/upload_response.py +20 -9
- nucleus/url_utils.py +4 -0
- nucleus/utils.py +134 -37
- nucleus/validate/__init__.py +24 -0
- nucleus/validate/client.py +168 -0
- nucleus/validate/constants.py +20 -0
- nucleus/validate/data_transfer_objects/__init__.py +0 -0
- nucleus/validate/data_transfer_objects/eval_function.py +81 -0
- nucleus/validate/data_transfer_objects/scenario_test.py +19 -0
- nucleus/validate/data_transfer_objects/scenario_test_evaluations.py +11 -0
- nucleus/validate/data_transfer_objects/scenario_test_metric.py +12 -0
- nucleus/validate/errors.py +6 -0
- nucleus/validate/eval_functions/__init__.py +0 -0
- nucleus/validate/eval_functions/available_eval_functions.py +212 -0
- nucleus/validate/eval_functions/base_eval_function.py +60 -0
- nucleus/validate/scenario_test.py +143 -0
- nucleus/validate/scenario_test_evaluation.py +114 -0
- nucleus/validate/scenario_test_metric.py +14 -0
- nucleus/validate/utils.py +8 -0
- {scale_nucleus-0.1.24.dist-info → scale_nucleus-0.6.4.dist-info}/LICENSE +0 -0
- scale_nucleus-0.6.4.dist-info/METADATA +213 -0
- scale_nucleus-0.6.4.dist-info/RECORD +71 -0
- {scale_nucleus-0.1.24.dist-info → scale_nucleus-0.6.4.dist-info}/WHEEL +1 -1
- scale_nucleus-0.6.4.dist-info/entry_points.txt +3 -0
- scale_nucleus-0.1.24.dist-info/METADATA +0 -85
- 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 (
|
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
|
63
|
-
|
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
|
-
|
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
|
-
|
93
|
-
if not self.mask_url:
|
94
|
-
raise Exception("You must specify a mask_url.")
|
80
|
+
from nucleus import BoxAnnotation
|
95
81
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
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
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
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
|
191
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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(
|