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.
- 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 +453 -699
- nucleus/annotation.py +435 -80
- nucleus/autocurate.py +9 -0
- nucleus/connection.py +87 -0
- nucleus/constants.py +12 -2
- 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 +1139 -215
- nucleus/dataset_item.py +130 -26
- nucleus/dataset_item_uploader.py +297 -0
- nucleus/deprecation_warning.py +32 -0
- nucleus/errors.py +21 -1
- 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 +30 -18
- 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 +139 -35
- 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.22.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.22.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.22.dist-info/METADATA +0 -85
- 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 (
|
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
|
59
|
-
|
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
|
-
|
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
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
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
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
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
|
186
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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(
|