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/scene.py CHANGED
@@ -1,21 +1,37 @@
1
1
  import json
2
2
  from abc import ABC
3
3
  from dataclasses import dataclass, field
4
- from typing import Optional, Any, Dict, List
4
+ from typing import Any, Dict, List, Optional
5
+
5
6
  from nucleus.constants import (
6
7
  FRAMES_KEY,
8
+ IMAGE_LOCATION_KEY,
7
9
  LENGTH_KEY,
8
10
  METADATA_KEY,
9
11
  NUM_SENSORS_KEY,
10
- REFERENCE_ID_KEY,
11
12
  POINTCLOUD_LOCATION_KEY,
12
- IMAGE_LOCATION_KEY,
13
+ REFERENCE_ID_KEY,
13
14
  )
15
+
14
16
  from .annotation import is_local_path
15
- from .dataset_item import DatasetItemType, DatasetItem
17
+ from .dataset_item import DatasetItem, DatasetItemType
16
18
 
17
19
 
18
20
  class Frame:
21
+ """Collection of sensor data pertaining to a single timestep.
22
+
23
+ For 3D data, each Frame hosues a sensor-to-data mapping and must have exactly
24
+ one pointcloud with any number of camera images.
25
+
26
+ Parameters:
27
+ **kwargs (Dict[str, :class:`DatasetItem`]): Mappings from sensor name
28
+ to dataset item. Each frame of a lidar scene must contain exactly one
29
+ pointcloud and any number of images (e.g. from different angles).
30
+
31
+ Refer to our `guide to uploading 3D data
32
+ <https://docs.nucleus.scale.com/docs/uploading-3d-data>`_ for more info!
33
+ """
34
+
19
35
  def __init__(self, **kwargs):
20
36
  self.items = {}
21
37
  for key, value in kwargs.items():
@@ -31,31 +47,64 @@ class Frame:
31
47
  def __repr__(self) -> str:
32
48
  return f"Frame(items={self.items})"
33
49
 
34
- def add_item(self, item: DatasetItem, sensor_name: str):
50
+ def __eq__(self, other):
51
+ for key, value in self.items.items():
52
+ if key not in other.items:
53
+ return False
54
+ if value != other.items[key]:
55
+ return False
56
+ return True
57
+
58
+ def add_item(self, item: DatasetItem, sensor_name: str) -> None:
59
+ """Adds DatasetItem object to frame as sensor data.
60
+
61
+ Parameters:
62
+ item (:class:`DatasetItem`): Pointcloud or camera image item to add.
63
+ sensor_name: Name of the sensor, e.g. "lidar" or "front_cam."
64
+ """
35
65
  self.items[sensor_name] = item
36
66
 
37
- def get_item(self, sensor_name: str):
67
+ def get_item(self, sensor_name: str) -> DatasetItem:
68
+ """Fetches the DatasetItem object associated with the given sensor.
69
+
70
+ Parameters:
71
+ sensor_name: Name of the sensor, e.g. "lidar" or "front_cam."
72
+
73
+ Returns:
74
+ :class:`DatasetItem`: DatasetItem object pertaining to the sensor.
75
+ """
38
76
  if sensor_name not in self.items:
39
77
  raise ValueError(
40
78
  f"This frame does not have a {sensor_name} sensor"
41
79
  )
42
80
  return self.items[sensor_name]
43
81
 
44
- def get_items(self):
82
+ def get_items(self) -> List[DatasetItem]:
83
+ """Fetches all items in the frame.
84
+
85
+ Returns:
86
+ List[:class:`DatasetItem`]: List of all DatasetItem objects in the frame.
87
+ """
45
88
  return list(self.items.values())
46
89
 
47
- def get_sensors(self):
90
+ def get_sensors(self) -> List[str]:
91
+ """Fetches all sensor names of the frame.
92
+
93
+ Returns:
94
+ List of all sensor names of the frame."""
48
95
  return list(self.items.keys())
49
96
 
50
97
  @classmethod
51
98
  def from_json(cls, payload: dict):
99
+ """Instantiates frame object from schematized JSON dict payload."""
52
100
  items = {
53
- sensor: DatasetItem.from_json(item, is_scene=True)
101
+ sensor: DatasetItem.from_json(item)
54
102
  for sensor, item in payload.items()
55
103
  }
56
104
  return cls(**items)
57
105
 
58
106
  def to_payload(self) -> dict:
107
+ """Serializes frame object to schematized JSON dict."""
59
108
  return {
60
109
  sensor: dataset_item.to_payload(is_scene=True)
61
110
  for sensor, dataset_item in self.items.items()
@@ -66,30 +115,53 @@ class Frame:
66
115
  class Scene(ABC):
67
116
  reference_id: str
68
117
  frames: List[Frame] = field(default_factory=list)
69
- metadata: Optional[dict] = None
118
+ metadata: Optional[dict] = field(default_factory=dict)
70
119
 
71
120
  def __post_init__(self):
72
121
  self.sensors = set(
73
122
  flatten([frame.get_sensors() for frame in self.frames])
74
123
  )
75
124
  self.frames_dict = dict(enumerate(self.frames))
125
+ if self.metadata is None:
126
+ self.metadata = {}
127
+
128
+ def __eq__(self, other):
129
+ return all(
130
+ [
131
+ self.reference_id == other.reference_id,
132
+ self.frames == other.frames,
133
+ self.metadata == other.metadata,
134
+ ]
135
+ )
76
136
 
77
137
  @property
78
138
  def length(self) -> int:
139
+ """Number of frames in the scene."""
79
140
  return len(self.frames_dict)
80
141
 
81
142
  @property
82
143
  def num_sensors(self) -> int:
144
+ """Number of sensors in the scene."""
83
145
  return len(self.get_sensors())
84
146
 
85
147
  def validate(self):
148
+ # TODO: make private
86
149
  assert self.length > 0, "Must have at least 1 frame in a scene"
87
150
  for frame in self.frames_dict.values():
88
151
  assert isinstance(
89
152
  frame, Frame
90
153
  ), "Each frame in a scene must be a Frame object"
91
154
 
92
- def add_item(self, index: int, sensor_name: str, item: DatasetItem):
155
+ def add_item(
156
+ self, index: int, sensor_name: str, item: DatasetItem
157
+ ) -> None:
158
+ """Adds DatasetItem to the specified frame as sensor data.
159
+
160
+ Parameters:
161
+ index: Serial index of the frame to which to add the item.
162
+ item (:class:`DatasetItem`): Pointcloud or camera image item to add.
163
+ sensor_name: Name of the sensor, e.g. "lidar" or "front_cam."
164
+ """
93
165
  self.sensors.add(sensor_name)
94
166
  if index not in self.frames_dict:
95
167
  new_frame = Frame(**{sensor_name: item})
@@ -97,7 +169,17 @@ class Scene(ABC):
97
169
  else:
98
170
  self.frames_dict[index].items[sensor_name] = item
99
171
 
100
- def add_frame(self, frame: Frame, index: int, update: bool = False):
172
+ def add_frame(
173
+ self, frame: Frame, index: int, update: bool = False
174
+ ) -> None:
175
+ """Adds frame to scene at the specified index.
176
+
177
+ Parameters:
178
+ frame (:class:`Frame`): Frame object to add.
179
+ index: Serial index at which to add the frame.
180
+ update: Whether to overwrite the frame at the specified index, if it
181
+ exists. Default is False.
182
+ """
101
183
  if (
102
184
  index not in self.frames_dict
103
185
  or index in self.frames_dict
@@ -106,14 +188,26 @@ class Scene(ABC):
106
188
  self.frames_dict[index] = frame
107
189
  self.sensors.update(frame.get_sensors())
108
190
 
109
- def get_frame(self, index: int):
191
+ def get_frame(self, index: int) -> Frame:
192
+ """Fetches the Frame object at the specified index.
193
+
194
+ Parameters:
195
+ index: Serial index for which to retrieve the Frame.
196
+
197
+ Return:
198
+ :class:`Frame`: Frame object at the specified index."""
110
199
  if index not in self.frames_dict:
111
200
  raise ValueError(
112
201
  f"This scene does not have a frame at index {index}"
113
202
  )
114
203
  return self.frames_dict[index]
115
204
 
116
- def get_frames(self):
205
+ def get_frames(self) -> List[Frame]:
206
+ """Fetches a sorted list of Frames of the scene.
207
+
208
+ Returns:
209
+ List[:class:`Frame`]: List of Frames, sorted by index ascending.
210
+ """
117
211
  return [
118
212
  frame
119
213
  for _, frame in sorted(
@@ -121,14 +215,36 @@ class Scene(ABC):
121
215
  )
122
216
  ]
123
217
 
124
- def get_sensors(self):
218
+ def get_sensors(self) -> List[str]:
219
+ """Fetches all sensor names of the scene.
220
+
221
+ Returns:
222
+ List of all sensor names associated with frames in the scene."""
125
223
  return list(self.sensors)
126
224
 
127
- def get_item(self, index: int, sensor_name: str):
225
+ def get_item(self, index: int, sensor_name: str) -> DatasetItem:
226
+ """Fetches the DatasetItem object of the given frame and sensor.
227
+
228
+ Parameters:
229
+ index: Serial index of the frame from which to fetch the item.
230
+ sensor_name: Name of the sensor, e.g. "lidar" or "front_cam."
231
+
232
+ Returns:
233
+ :class:`DatasetItem`: DatasetItem object of the frame and sensor.
234
+ """
128
235
  frame = self.get_frame(index)
129
236
  return frame.get_item(sensor_name)
130
237
 
131
- def get_items_from_sensor(self, sensor_name: str):
238
+ def get_items_from_sensor(self, sensor_name: str) -> List[DatasetItem]:
239
+ """Fetches all DatasetItem objects of the given sensor.
240
+
241
+ Parameters:
242
+ sensor_name: Name of the sensor, e.g. "lidar" or "front_cam."
243
+
244
+ Returns:
245
+ List[:class:`DatasetItem`]: List of DatasetItem objects associated
246
+ with the specified sensor.
247
+ """
132
248
  if sensor_name not in self.sensors:
133
249
  raise ValueError(
134
250
  f"This scene does not have a {sensor_name} sensor"
@@ -143,10 +259,27 @@ class Scene(ABC):
143
259
  items_from_sensor.append(None)
144
260
  return items_from_sensor
145
261
 
146
- def get_items(self):
262
+ def get_items(self) -> List[DatasetItem]:
263
+ """Fetches all items in the scene.
264
+
265
+ Returns:
266
+ List[:class:`DatasetItem`]: Unordered list of all DatasetItem
267
+ objects in the scene.
268
+ """
147
269
  return flatten([frame.get_items() for frame in self.get_frames()])
148
270
 
149
271
  def info(self):
272
+ """Fetches information about the scene.
273
+
274
+ Returns:
275
+ Payload containing::
276
+
277
+ {
278
+ "reference_id": str,
279
+ "length": int,
280
+ "num_sensors": int
281
+ }
282
+ """
150
283
  return {
151
284
  REFERENCE_ID_KEY: self.reference_id,
152
285
  LENGTH_KEY: self.length,
@@ -154,6 +287,7 @@ class Scene(ABC):
154
287
  }
155
288
 
156
289
  def validate_frames_dict(self):
290
+ # TODO: make private
157
291
  is_continuous = set(list(range(len(self.frames_dict)))) == set(
158
292
  self.frames_dict.keys()
159
293
  )
@@ -163,15 +297,17 @@ class Scene(ABC):
163
297
 
164
298
  @classmethod
165
299
  def from_json(cls, payload: dict):
300
+ """Instantiates scene object from schematized JSON dict payload."""
166
301
  frames_payload = payload.get(FRAMES_KEY, [])
167
302
  frames = [Frame.from_json(frame) for frame in frames_payload]
168
303
  return cls(
169
304
  reference_id=payload[REFERENCE_ID_KEY],
170
305
  frames=frames,
171
- metadata=payload.get(METADATA_KEY, None),
306
+ metadata=payload.get(METADATA_KEY, {}),
172
307
  )
173
308
 
174
309
  def to_payload(self) -> dict:
310
+ """Serializes scene object to schematized JSON dict."""
175
311
  self.validate_frames_dict()
176
312
  ordered_frames = self.get_frames()
177
313
  frames_payload = [frame.to_payload() for frame in ordered_frames]
@@ -184,15 +320,64 @@ class Scene(ABC):
184
320
  return payload
185
321
 
186
322
  def to_json(self) -> str:
323
+ """Serializes scene object to schematized JSON string."""
187
324
  return json.dumps(self.to_payload(), allow_nan=False)
188
325
 
189
326
 
190
327
  @dataclass
191
328
  class LidarScene(Scene):
329
+ """Sequence of lidar pointcloud and camera images over time.
330
+
331
+ Nucleus 3D datasets are comprised of LidarScenes, which are sequences of
332
+ lidar pointclouds and camera images over time. These sequences are in turn
333
+ comprised of :class:`Frames <Frame>`.
334
+
335
+ By organizing data across multiple sensors over time, LidarScenes make it
336
+ easier to interpret pointclouds, allowing you to see objects move over time
337
+ by clicking through frames and providing context in the form of corresponding
338
+ images.
339
+
340
+ You can think of scenes and frames as nested groupings of sensor data across
341
+ time:
342
+
343
+ * LidarScene for a given location
344
+ * Frame at timestep 0
345
+ * DatasetItem of pointcloud
346
+ * DatasetItem of front camera image
347
+ * DatasetItem of rear camera image
348
+ * Frame at timestep 1
349
+ * ...
350
+ * ...
351
+ * LidarScene for another location
352
+ * ...
353
+
354
+ LidarScenes are uploaded to a :class:`Dataset` with any accompanying
355
+ metadata. Frames do not accept metadata, but each of its constituent
356
+ :class:`DatasetItems <DatasetItem>` does.
357
+
358
+ Note: Uploads with a different number of frames/items will error out (only
359
+ on scenes that now differ). Existing scenes are expected to retain the
360
+ same structure, i.e. the same number of frames, and same items per frame.
361
+ If a scene definition is changed (for example, additional frames added) the
362
+ update operation will be ignored. If you would like to alter the structure
363
+ of a scene, please delete the scene and re-upload.
364
+
365
+ Parameters:
366
+ reference_id (str): User-specified identifier to reference the scene.
367
+ frames (Optional[List[:class:`Frame`]]): List of frames to be a part of
368
+ the scene. A scene can be created before frames or items have been
369
+ added to it, but must be non-empty when uploading to a :class:`Dataset`.
370
+ metadata (Optional[Dict]): Optional metadata to include with the scene.
371
+
372
+ Refer to our `guide to uploading 3D data
373
+ <https://docs.nucleus.scale.com/docs/uploading-3d-data>`_ for more info!
374
+ """
375
+
192
376
  def __repr__(self) -> str:
193
377
  return f"LidarScene(reference_id='{self.reference_id}', frames={self.get_frames()}, metadata={self.metadata})"
194
378
 
195
379
  def validate(self):
380
+ # TODO: make private
196
381
  super().validate()
197
382
  lidar_sensors = flatten(
198
383
  [