supervisely 6.73.438__py3-none-any.whl → 6.73.513__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.
- supervisely/__init__.py +137 -1
- supervisely/_utils.py +81 -0
- supervisely/annotation/annotation.py +8 -2
- supervisely/annotation/json_geometries_map.py +14 -11
- supervisely/annotation/label.py +80 -3
- supervisely/api/annotation_api.py +14 -11
- supervisely/api/api.py +59 -38
- supervisely/api/app_api.py +11 -2
- supervisely/api/dataset_api.py +74 -12
- supervisely/api/entities_collection_api.py +10 -0
- supervisely/api/entity_annotation/figure_api.py +52 -4
- supervisely/api/entity_annotation/object_api.py +3 -3
- supervisely/api/entity_annotation/tag_api.py +63 -12
- supervisely/api/guides_api.py +210 -0
- supervisely/api/image_api.py +72 -1
- supervisely/api/labeling_job_api.py +83 -1
- supervisely/api/labeling_queue_api.py +33 -7
- supervisely/api/module_api.py +9 -0
- supervisely/api/project_api.py +71 -26
- supervisely/api/storage_api.py +3 -1
- supervisely/api/task_api.py +13 -2
- supervisely/api/team_api.py +4 -3
- supervisely/api/video/video_annotation_api.py +119 -3
- supervisely/api/video/video_api.py +65 -14
- supervisely/api/video/video_figure_api.py +24 -11
- supervisely/app/__init__.py +1 -1
- supervisely/app/content.py +23 -7
- supervisely/app/development/development.py +18 -2
- supervisely/app/fastapi/__init__.py +1 -0
- supervisely/app/fastapi/custom_static_files.py +1 -1
- supervisely/app/fastapi/multi_user.py +105 -0
- supervisely/app/fastapi/subapp.py +88 -42
- supervisely/app/fastapi/websocket.py +77 -9
- supervisely/app/singleton.py +21 -0
- supervisely/app/v1/app_service.py +18 -2
- supervisely/app/v1/constants.py +7 -1
- supervisely/app/widgets/__init__.py +6 -0
- supervisely/app/widgets/activity_feed/__init__.py +0 -0
- supervisely/app/widgets/activity_feed/activity_feed.py +239 -0
- supervisely/app/widgets/activity_feed/style.css +78 -0
- supervisely/app/widgets/activity_feed/template.html +22 -0
- supervisely/app/widgets/card/card.py +20 -0
- supervisely/app/widgets/classes_list_selector/classes_list_selector.py +121 -9
- supervisely/app/widgets/classes_list_selector/template.html +60 -93
- supervisely/app/widgets/classes_mapping/classes_mapping.py +13 -12
- supervisely/app/widgets/classes_table/classes_table.py +1 -0
- supervisely/app/widgets/deploy_model/deploy_model.py +56 -35
- supervisely/app/widgets/dialog/dialog.py +12 -0
- supervisely/app/widgets/dialog/template.html +2 -1
- supervisely/app/widgets/ecosystem_model_selector/ecosystem_model_selector.py +1 -1
- supervisely/app/widgets/experiment_selector/experiment_selector.py +8 -0
- supervisely/app/widgets/fast_table/fast_table.py +184 -60
- supervisely/app/widgets/fast_table/template.html +1 -1
- supervisely/app/widgets/heatmap/__init__.py +0 -0
- supervisely/app/widgets/heatmap/heatmap.py +564 -0
- supervisely/app/widgets/heatmap/script.js +533 -0
- supervisely/app/widgets/heatmap/style.css +233 -0
- supervisely/app/widgets/heatmap/template.html +21 -0
- supervisely/app/widgets/modal/__init__.py +0 -0
- supervisely/app/widgets/modal/modal.py +198 -0
- supervisely/app/widgets/modal/template.html +10 -0
- supervisely/app/widgets/object_class_view/object_class_view.py +3 -0
- supervisely/app/widgets/radio_tabs/radio_tabs.py +18 -2
- supervisely/app/widgets/radio_tabs/template.html +1 -0
- supervisely/app/widgets/select/select.py +6 -3
- supervisely/app/widgets/select_class/__init__.py +0 -0
- supervisely/app/widgets/select_class/select_class.py +363 -0
- supervisely/app/widgets/select_class/template.html +50 -0
- supervisely/app/widgets/select_cuda/select_cuda.py +22 -0
- supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +65 -7
- supervisely/app/widgets/select_tag/__init__.py +0 -0
- supervisely/app/widgets/select_tag/select_tag.py +352 -0
- supervisely/app/widgets/select_tag/template.html +64 -0
- supervisely/app/widgets/select_team/select_team.py +37 -4
- supervisely/app/widgets/select_team/template.html +4 -5
- supervisely/app/widgets/select_user/__init__.py +0 -0
- supervisely/app/widgets/select_user/select_user.py +270 -0
- supervisely/app/widgets/select_user/template.html +13 -0
- supervisely/app/widgets/select_workspace/select_workspace.py +59 -10
- supervisely/app/widgets/select_workspace/template.html +9 -12
- supervisely/app/widgets/table/table.py +68 -13
- supervisely/app/widgets/tree_select/tree_select.py +2 -0
- supervisely/aug/aug.py +6 -2
- supervisely/convert/base_converter.py +1 -0
- supervisely/convert/converter.py +2 -2
- supervisely/convert/image/csv/csv_converter.py +24 -15
- supervisely/convert/image/image_converter.py +3 -1
- supervisely/convert/image/image_helper.py +48 -4
- supervisely/convert/image/label_studio/label_studio_converter.py +2 -0
- supervisely/convert/image/medical2d/medical2d_helper.py +2 -24
- supervisely/convert/image/multispectral/multispectral_converter.py +6 -0
- supervisely/convert/image/pascal_voc/pascal_voc_converter.py +8 -5
- supervisely/convert/image/pascal_voc/pascal_voc_helper.py +7 -0
- supervisely/convert/pointcloud/kitti_3d/kitti_3d_converter.py +33 -3
- supervisely/convert/pointcloud/kitti_3d/kitti_3d_helper.py +12 -5
- supervisely/convert/pointcloud/las/las_converter.py +13 -1
- supervisely/convert/pointcloud/las/las_helper.py +110 -11
- supervisely/convert/pointcloud/nuscenes_conv/nuscenes_converter.py +27 -16
- supervisely/convert/pointcloud/pointcloud_converter.py +91 -3
- supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_converter.py +58 -22
- supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_helper.py +21 -47
- supervisely/convert/video/__init__.py +1 -0
- supervisely/convert/video/multi_view/__init__.py +0 -0
- supervisely/convert/video/multi_view/multi_view.py +543 -0
- supervisely/convert/video/sly/sly_video_converter.py +359 -3
- supervisely/convert/video/video_converter.py +24 -4
- supervisely/convert/volume/dicom/dicom_converter.py +13 -5
- supervisely/convert/volume/dicom/dicom_helper.py +30 -18
- supervisely/geometry/constants.py +1 -0
- supervisely/geometry/geometry.py +4 -0
- supervisely/geometry/helpers.py +5 -1
- supervisely/geometry/oriented_bbox.py +676 -0
- supervisely/geometry/polyline_3d.py +110 -0
- supervisely/geometry/rectangle.py +2 -1
- supervisely/io/env.py +76 -1
- supervisely/io/fs.py +21 -0
- supervisely/nn/benchmark/base_evaluator.py +104 -11
- supervisely/nn/benchmark/instance_segmentation/evaluator.py +1 -8
- supervisely/nn/benchmark/object_detection/evaluator.py +20 -4
- supervisely/nn/benchmark/object_detection/vis_metrics/pr_curve.py +10 -5
- supervisely/nn/benchmark/semantic_segmentation/evaluator.py +34 -16
- supervisely/nn/benchmark/semantic_segmentation/vis_metrics/confusion_matrix.py +1 -1
- supervisely/nn/benchmark/semantic_segmentation/vis_metrics/frequently_confused.py +1 -1
- supervisely/nn/benchmark/semantic_segmentation/vis_metrics/overview.py +1 -1
- supervisely/nn/benchmark/visualization/evaluation_result.py +66 -4
- supervisely/nn/inference/cache.py +43 -18
- supervisely/nn/inference/gui/serving_gui_template.py +5 -2
- supervisely/nn/inference/inference.py +916 -222
- supervisely/nn/inference/inference_request.py +55 -10
- supervisely/nn/inference/predict_app/gui/classes_selector.py +83 -12
- supervisely/nn/inference/predict_app/gui/gui.py +676 -488
- supervisely/nn/inference/predict_app/gui/input_selector.py +205 -26
- supervisely/nn/inference/predict_app/gui/model_selector.py +2 -4
- supervisely/nn/inference/predict_app/gui/output_selector.py +46 -6
- supervisely/nn/inference/predict_app/gui/settings_selector.py +756 -59
- supervisely/nn/inference/predict_app/gui/tags_selector.py +1 -1
- supervisely/nn/inference/predict_app/gui/utils.py +236 -119
- supervisely/nn/inference/predict_app/predict_app.py +2 -2
- supervisely/nn/inference/session.py +43 -35
- supervisely/nn/inference/tracking/bbox_tracking.py +118 -35
- supervisely/nn/inference/tracking/point_tracking.py +5 -1
- supervisely/nn/inference/tracking/tracker_interface.py +10 -1
- supervisely/nn/inference/uploader.py +139 -12
- supervisely/nn/live_training/__init__.py +7 -0
- supervisely/nn/live_training/api_server.py +111 -0
- supervisely/nn/live_training/artifacts_utils.py +243 -0
- supervisely/nn/live_training/checkpoint_utils.py +229 -0
- supervisely/nn/live_training/dynamic_sampler.py +44 -0
- supervisely/nn/live_training/helpers.py +14 -0
- supervisely/nn/live_training/incremental_dataset.py +146 -0
- supervisely/nn/live_training/live_training.py +497 -0
- supervisely/nn/live_training/loss_plateau_detector.py +111 -0
- supervisely/nn/live_training/request_queue.py +52 -0
- supervisely/nn/model/model_api.py +9 -0
- supervisely/nn/model/prediction.py +2 -1
- supervisely/nn/model/prediction_session.py +26 -14
- supervisely/nn/prediction_dto.py +19 -1
- supervisely/nn/tracker/base_tracker.py +11 -1
- supervisely/nn/tracker/botsort/botsort_config.yaml +0 -1
- supervisely/nn/tracker/botsort/tracker/mc_bot_sort.py +7 -4
- supervisely/nn/tracker/botsort_tracker.py +94 -65
- supervisely/nn/tracker/utils.py +4 -5
- supervisely/nn/tracker/visualize.py +93 -93
- supervisely/nn/training/gui/classes_selector.py +16 -1
- supervisely/nn/training/gui/train_val_splits_selector.py +52 -31
- supervisely/nn/training/train_app.py +46 -31
- supervisely/project/data_version.py +115 -51
- supervisely/project/download.py +1 -1
- supervisely/project/pointcloud_episode_project.py +37 -8
- supervisely/project/pointcloud_project.py +30 -2
- supervisely/project/project.py +14 -2
- supervisely/project/project_meta.py +27 -1
- supervisely/project/project_settings.py +32 -18
- supervisely/project/versioning/__init__.py +1 -0
- supervisely/project/versioning/common.py +20 -0
- supervisely/project/versioning/schema_fields.py +35 -0
- supervisely/project/versioning/video_schema.py +221 -0
- supervisely/project/versioning/volume_schema.py +87 -0
- supervisely/project/video_project.py +717 -15
- supervisely/project/volume_project.py +623 -5
- supervisely/template/experiment/experiment.html.jinja +4 -4
- supervisely/template/experiment/experiment_generator.py +14 -21
- supervisely/template/live_training/__init__.py +0 -0
- supervisely/template/live_training/header.html.jinja +96 -0
- supervisely/template/live_training/live_training.html.jinja +51 -0
- supervisely/template/live_training/live_training_generator.py +464 -0
- supervisely/template/live_training/sly-style.css +402 -0
- supervisely/template/live_training/template.html.jinja +18 -0
- supervisely/versions.json +28 -26
- supervisely/video/sampling.py +39 -20
- supervisely/video/video.py +41 -12
- supervisely/video_annotation/video_figure.py +38 -4
- supervisely/video_annotation/video_object.py +29 -4
- supervisely/volume/stl_converter.py +2 -0
- supervisely/worker_api/agent_rpc.py +24 -1
- supervisely/worker_api/rpc_servicer.py +31 -7
- {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/METADATA +58 -40
- {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/RECORD +203 -155
- {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/WHEEL +1 -1
- supervisely_lib/__init__.py +6 -1
- {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/entry_points.txt +0 -0
- {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info/licenses}/LICENSE +0 -0
- {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/top_level.txt +0 -0
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
# coding: utf-8
|
|
2
|
+
import io
|
|
3
|
+
import json
|
|
2
4
|
import os
|
|
3
5
|
import re
|
|
4
|
-
import
|
|
5
|
-
from collections import namedtuple
|
|
6
|
-
from typing import Callable, Dict, List, Optional, Tuple, Union
|
|
6
|
+
import struct
|
|
7
|
+
from collections import defaultdict, namedtuple
|
|
8
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
|
7
9
|
|
|
8
10
|
import numpy
|
|
9
11
|
from tqdm import tqdm
|
|
10
12
|
|
|
13
|
+
import supervisely as sly
|
|
14
|
+
import supervisely.volume_annotation.constants as volume_constants
|
|
11
15
|
from supervisely._utils import batched
|
|
12
16
|
from supervisely.api.api import Api
|
|
13
17
|
from supervisely.api.module_api import ApiField
|
|
18
|
+
from supervisely.api.project_api import ProjectInfo
|
|
19
|
+
from supervisely.api.volume.volume_api import VolumeInfo
|
|
14
20
|
from supervisely.collection.key_indexed_collection import KeyIndexedCollection
|
|
15
21
|
from supervisely.geometry.closed_surface_mesh import ClosedSurfaceMesh
|
|
16
22
|
from supervisely.geometry.mask_3d import Mask3D
|
|
@@ -19,6 +25,11 @@ from supervisely.project.project import OpenMode
|
|
|
19
25
|
from supervisely.project.project_meta import ProjectMeta
|
|
20
26
|
from supervisely.project.project_type import ProjectType
|
|
21
27
|
from supervisely.project.video_project import VideoDataset, VideoProject
|
|
28
|
+
from supervisely.project.versioning.common import (
|
|
29
|
+
DEFAULT_VOLUME_SCHEMA_VERSION,
|
|
30
|
+
get_volume_snapshot_schema,
|
|
31
|
+
)
|
|
32
|
+
from supervisely.project.versioning.schema_fields import VersionSchemaField
|
|
22
33
|
from supervisely.sly_logger import logger
|
|
23
34
|
from supervisely.task.progress import Progress, tqdm_sly
|
|
24
35
|
from supervisely.video_annotation.key_id_map import KeyIdMap
|
|
@@ -111,6 +122,14 @@ class VolumeProject(VideoProject):
|
|
|
111
122
|
class DatasetDict(KeyIndexedCollection):
|
|
112
123
|
item_type = VolumeDataset
|
|
113
124
|
|
|
125
|
+
_SERIALIZATION_MAGIC = b"SLYVOLPAR"
|
|
126
|
+
_SERIALIZATION_VERSION = 1
|
|
127
|
+
_SECTION_PROJECT_INFO = 1
|
|
128
|
+
_SECTION_PROJECT_META = 2
|
|
129
|
+
_SECTION_DATASETS = 3
|
|
130
|
+
_SECTION_VOLUMES = 4
|
|
131
|
+
_SECTION_ANNOTATIONS = 5
|
|
132
|
+
|
|
114
133
|
def get_classes_stats(
|
|
115
134
|
self,
|
|
116
135
|
dataset_names: Optional[List[str]] = None,
|
|
@@ -202,6 +221,194 @@ class VolumeProject(VideoProject):
|
|
|
202
221
|
progress_cb=progress_cb,
|
|
203
222
|
)
|
|
204
223
|
|
|
224
|
+
@staticmethod
|
|
225
|
+
def download_bin(
|
|
226
|
+
api: Api,
|
|
227
|
+
project_id: int,
|
|
228
|
+
dest_dir: Optional[str] = None,
|
|
229
|
+
dataset_ids: Optional[List[int]] = None,
|
|
230
|
+
download_volumes: bool = True,
|
|
231
|
+
log_progress: bool = False,
|
|
232
|
+
progress_cb: Optional[Union[tqdm, Callable]] = None,
|
|
233
|
+
return_bytesio: bool = False,
|
|
234
|
+
schema_version: str = DEFAULT_VOLUME_SCHEMA_VERSION,
|
|
235
|
+
*args,
|
|
236
|
+
**kwargs,
|
|
237
|
+
) -> Union[str, io.BytesIO]:
|
|
238
|
+
"""
|
|
239
|
+
Download a Volume Project snapshot into a Parquet-backed binary blob (`.tar.zst` file or in-memory BytesIO).
|
|
240
|
+
|
|
241
|
+
The snapshot stores:
|
|
242
|
+
|
|
243
|
+
- Project info and meta
|
|
244
|
+
- Dataset tree (dataset infos)
|
|
245
|
+
- Volume infos (optionally)
|
|
246
|
+
- Volume annotations (for the included volumes)
|
|
247
|
+
|
|
248
|
+
The resulting binary snapshot can be restored later with :func:`upload_bin`.
|
|
249
|
+
|
|
250
|
+
:param api: Supervisely API client.
|
|
251
|
+
:type api: :class:`~supervisely.api.api.Api`
|
|
252
|
+
:param project_id: Source Volume Project ID on the server.
|
|
253
|
+
:type project_id: int
|
|
254
|
+
:param dest_dir: Local folder where the snapshot file will be written. Required when `return_bytesio=False`.
|
|
255
|
+
:type dest_dir: str, optional
|
|
256
|
+
:param dataset_ids: Optional list of dataset IDs to include. If provided, only these datasets will be included (recursively, preserving tree structure where applicable).
|
|
257
|
+
:type dataset_ids: List[int], optional
|
|
258
|
+
:param download_volumes: If False, only project/meta/dataset tree is stored (volume infos and annotations are skipped). This is useful for “structure-only” snapshots.
|
|
259
|
+
:type download_volumes: bool, optional
|
|
260
|
+
:param log_progress: If True, show a progress bar (unless a custom ``progress_cb`` is provided).
|
|
261
|
+
:type log_progress: bool
|
|
262
|
+
:param progress_cb: Optional callback (or tqdm-like object) called with incremental progress.
|
|
263
|
+
:type progress_cb: tqdm or callable, optional
|
|
264
|
+
:param return_bytesio: If True, return an in-memory :class:`io.BytesIO` with snapshot bytes. If False, write snapshot to ``dest_dir`` and return the file path.
|
|
265
|
+
:type return_bytesio: bool, optional
|
|
266
|
+
:param schema_version: Snapshot schema version. Controls the internal Parquet layout/fields. Supported values are the keys from :func:`~supervisely.project.volume_schema.get_volume_snapshot_schema` (currently: ``"v2.0.0"``).
|
|
267
|
+
:type schema_version: str, optional
|
|
268
|
+
:return: Snapshot file path (when ``return_bytesio=False``) or a BytesIO (when ``return_bytesio=True``).
|
|
269
|
+
:rtype: str or io.BytesIO
|
|
270
|
+
:raises ValueError: If ``dest_dir`` is not provided and ``return_bytesio`` is False.
|
|
271
|
+
:raises RuntimeError: If required optional dependencies (e.g. pyarrow) are missing.
|
|
272
|
+
|
|
273
|
+
:Usage example:
|
|
274
|
+
|
|
275
|
+
.. code-block:: python
|
|
276
|
+
|
|
277
|
+
import supervisely as sly
|
|
278
|
+
import os
|
|
279
|
+
|
|
280
|
+
api = sly.Api(os.environ["SERVER_ADDRESS"], os.environ["API_TOKEN"])
|
|
281
|
+
|
|
282
|
+
# 1) Save snapshot to disk
|
|
283
|
+
out_path = sly.VolumeProject.download_bin(
|
|
284
|
+
api,
|
|
285
|
+
project_id=123,
|
|
286
|
+
dest_dir="/tmp/vol_project_snapshot",
|
|
287
|
+
download_volumes=True,
|
|
288
|
+
log_progress=True,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# 2) Create an in-memory snapshot (BytesIO) and restore it
|
|
292
|
+
blob = sly.VolumeProject.download_bin(
|
|
293
|
+
api,
|
|
294
|
+
project_id=123,
|
|
295
|
+
return_bytesio=True,
|
|
296
|
+
download_volumes=False, # structure-only
|
|
297
|
+
)
|
|
298
|
+
restored = sly.VolumeProject.upload_bin(api, blob, workspace_id=45, project_name="Restored")
|
|
299
|
+
"""
|
|
300
|
+
|
|
301
|
+
pa = VolumeProject._require_pyarrow()
|
|
302
|
+
snapshot_schema = get_volume_snapshot_schema(schema_version)
|
|
303
|
+
|
|
304
|
+
if dest_dir is None and not return_bytesio:
|
|
305
|
+
raise ValueError(
|
|
306
|
+
"Local save directory dest_dir must be specified if return_bytesio is False"
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
ds_filters = (
|
|
310
|
+
[{"field": "id", "operator": "in", "value": dataset_ids}]
|
|
311
|
+
if dataset_ids is not None
|
|
312
|
+
else None
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
project_info = api.project.get_info_by_id(project_id)
|
|
316
|
+
project_meta = api.project.get_meta(project_id, with_settings=True)
|
|
317
|
+
project_meta_obj = ProjectMeta.from_json(project_meta)
|
|
318
|
+
dataset_infos = api.dataset.get_list(project_id, filters=ds_filters, recursive=True, include_custom_data=True)
|
|
319
|
+
|
|
320
|
+
dataset_records = [dataset_info._asdict() for dataset_info in dataset_infos]
|
|
321
|
+
volume_records: List[Dict] = []
|
|
322
|
+
annotations: Dict[str, Dict] = {}
|
|
323
|
+
key_id_map = KeyIdMap()
|
|
324
|
+
|
|
325
|
+
for dataset_info in dataset_infos:
|
|
326
|
+
if dataset_ids is not None and dataset_info.id not in dataset_ids:
|
|
327
|
+
continue
|
|
328
|
+
|
|
329
|
+
volumes = api.volume.get_list(dataset_info.id)
|
|
330
|
+
if len(volumes) == 0:
|
|
331
|
+
continue
|
|
332
|
+
|
|
333
|
+
if not download_volumes:
|
|
334
|
+
continue
|
|
335
|
+
|
|
336
|
+
ds_progress = progress_cb
|
|
337
|
+
if log_progress and progress_cb is None:
|
|
338
|
+
ds_progress = tqdm_sly(
|
|
339
|
+
desc="Collecting volumes from: {!r}".format(dataset_info.name),
|
|
340
|
+
total=len(volumes),
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
volume_ids = [volume_info.id for volume_info in volumes]
|
|
344
|
+
ann_jsons = api.volume.annotation.download_bulk(dataset_info.id, volume_ids)
|
|
345
|
+
|
|
346
|
+
# insert custom_data into ann_jsons (api does not return it in download_bulk atm)
|
|
347
|
+
# Build mappings:
|
|
348
|
+
# - volume_id -> ann_json
|
|
349
|
+
# - volume_id -> {figure_id -> spatial_figure_dict}
|
|
350
|
+
ann_by_volume_id: Dict[int, Dict[str, Any]] = {}
|
|
351
|
+
spatial_figures_by_volume: Dict[int, Dict[int, Dict[str, Any]]] = {}
|
|
352
|
+
for ann_json in ann_jsons:
|
|
353
|
+
volume_id = ann_json.get(ApiField.VOLUME_ID)
|
|
354
|
+
if volume_id is None:
|
|
355
|
+
continue
|
|
356
|
+
ann_by_volume_id[volume_id] = ann_json
|
|
357
|
+
figures_list = ann_json.get(volume_constants.SPATIAL_FIGURES, []) or []
|
|
358
|
+
fig_id_to_spatial_figure: Dict[int, Dict[str, Any]] = {}
|
|
359
|
+
for spatial_figure in figures_list:
|
|
360
|
+
fig_id = spatial_figure.get("id")
|
|
361
|
+
if fig_id is not None:
|
|
362
|
+
fig_id_to_spatial_figure[fig_id] = spatial_figure
|
|
363
|
+
spatial_figures_by_volume[volume_id] = fig_id_to_spatial_figure
|
|
364
|
+
|
|
365
|
+
figures_dict = api.volume.figure.download(dataset_info.id, volume_ids)
|
|
366
|
+
for volume_id, figure_infos in figures_dict.items():
|
|
367
|
+
ann_json = ann_by_volume_id.get(volume_id)
|
|
368
|
+
if ann_json is None:
|
|
369
|
+
continue
|
|
370
|
+
fig_id_to_spatial_figure = spatial_figures_by_volume.get(volume_id, {})
|
|
371
|
+
for figure_info in figure_infos:
|
|
372
|
+
spatial_figure = fig_id_to_spatial_figure.get(figure_info.id)
|
|
373
|
+
if spatial_figure is not None:
|
|
374
|
+
spatial_figure[ApiField.CUSTOM_DATA] = figure_info.custom_data
|
|
375
|
+
|
|
376
|
+
for volume_info, ann_json in zip(volumes, ann_jsons):
|
|
377
|
+
ann_dict = snapshot_schema.annotation_dict_from_raw(
|
|
378
|
+
api=api,
|
|
379
|
+
raw_ann_json=ann_json,
|
|
380
|
+
project_meta_obj=project_meta_obj,
|
|
381
|
+
key_id_map=key_id_map,
|
|
382
|
+
)
|
|
383
|
+
volume_records.append(volume_info._asdict())
|
|
384
|
+
annotations[str(volume_info.id)] = ann_dict
|
|
385
|
+
if progress_cb is not None:
|
|
386
|
+
progress_cb(1)
|
|
387
|
+
if ds_progress is not None:
|
|
388
|
+
ds_progress(1)
|
|
389
|
+
|
|
390
|
+
project_info_dict = project_info._asdict()
|
|
391
|
+
project_info_dict[VersionSchemaField.SCHEMA_VERSION] = schema_version
|
|
392
|
+
payload = {
|
|
393
|
+
"project_info": project_info_dict,
|
|
394
|
+
"project_meta": project_meta,
|
|
395
|
+
"dataset_infos": dataset_records,
|
|
396
|
+
"volume_infos": volume_records,
|
|
397
|
+
"annotations": annotations,
|
|
398
|
+
}
|
|
399
|
+
blob = VolumeProject._serialize_payload_to_parquet_blob(pa, payload, snapshot_schema)
|
|
400
|
+
|
|
401
|
+
if return_bytesio:
|
|
402
|
+
stream = io.BytesIO(blob)
|
|
403
|
+
stream.seek(0)
|
|
404
|
+
return stream
|
|
405
|
+
|
|
406
|
+
os.makedirs(dest_dir, exist_ok=True)
|
|
407
|
+
file_path = os.path.join(dest_dir, f"{project_info.id}_{project_info.name}.arrow")
|
|
408
|
+
with open(file_path, "wb") as out:
|
|
409
|
+
out.write(blob)
|
|
410
|
+
return file_path
|
|
411
|
+
|
|
205
412
|
@staticmethod
|
|
206
413
|
def upload(
|
|
207
414
|
directory: str,
|
|
@@ -263,6 +470,417 @@ class VolumeProject(VideoProject):
|
|
|
263
470
|
progress_cb=progress_cb,
|
|
264
471
|
)
|
|
265
472
|
|
|
473
|
+
@staticmethod
|
|
474
|
+
def upload_bin(
|
|
475
|
+
api: Api,
|
|
476
|
+
file: Union[str, io.BytesIO],
|
|
477
|
+
workspace_id: int,
|
|
478
|
+
project_name: Optional[str] = None,
|
|
479
|
+
log_progress: bool = True,
|
|
480
|
+
progress_cb: Optional[Union[tqdm, Callable]] = None,
|
|
481
|
+
skip_missed_entities: bool = False,
|
|
482
|
+
*args,
|
|
483
|
+
**kwargs,
|
|
484
|
+
) -> ProjectInfo:
|
|
485
|
+
"""
|
|
486
|
+
Restore a volume project from a Parquet blob produced by :func:`download_bin`.
|
|
487
|
+
|
|
488
|
+
:param api: Supervisely API client.
|
|
489
|
+
:type api: :class:`~supervisely.api.api.Api`
|
|
490
|
+
:param file: Snapshot file path (``.tar.zst``) or an in-memory :class:`io.BytesIO` stream.
|
|
491
|
+
:type file: Union[str, io.BytesIO]
|
|
492
|
+
:param workspace_id: Target workspace ID where the project will be created.
|
|
493
|
+
:type workspace_id: int
|
|
494
|
+
:param project_name: Optional new project name. If not provided, the name from the snapshot will be used. If the name already exists in the workspace, a free name will be chosen.
|
|
495
|
+
:type project_name: str, optional
|
|
496
|
+
:param log_progress: If True, show a progress bar (unless a custom ``progress_cb`` is provided).
|
|
497
|
+
:type log_progress: bool
|
|
498
|
+
:param progress_cb: Optional callback (or tqdm-like object) called with incremental progress.
|
|
499
|
+
:type progress_cb: tqdm or callable, optional
|
|
500
|
+
:param skip_missed_entities: If True, skip volumes that cannot be restored because their source hash is missing in the snapshot payload. If False, such cases raise an error.
|
|
501
|
+
:type skip_missed_entities: bool
|
|
502
|
+
:return: Info of the newly created project.
|
|
503
|
+
:rtype: :class:`~supervisely.api.project_api.ProjectInfo`
|
|
504
|
+
:raises RuntimeError: If the snapshot contains volumes without hashes and ``skip_missed_entities`` is False.
|
|
505
|
+
"""
|
|
506
|
+
|
|
507
|
+
pa = VolumeProject._require_pyarrow()
|
|
508
|
+
|
|
509
|
+
if isinstance(file, io.BytesIO):
|
|
510
|
+
raw_data = file.getbuffer()
|
|
511
|
+
else:
|
|
512
|
+
with open(file, "rb") as src:
|
|
513
|
+
raw_data = src.read()
|
|
514
|
+
|
|
515
|
+
payload = VolumeProject._deserialize_payload_from_parquet(pa, raw_data)
|
|
516
|
+
|
|
517
|
+
project_meta = ProjectMeta.from_json(payload["project_meta"])
|
|
518
|
+
project_info: Dict = payload.get("project_info", {})
|
|
519
|
+
dataset_records: List[Dict] = payload.get("dataset_infos", [])
|
|
520
|
+
volume_records: List[Dict] = payload.get("volume_infos", [])
|
|
521
|
+
annotations: Dict[str, Dict] = payload.get("annotations", {})
|
|
522
|
+
|
|
523
|
+
project_title = project_name or project_info.get("name")
|
|
524
|
+
if api.project.exists(workspace_id, project_title):
|
|
525
|
+
project_title = api.project.get_free_name(workspace_id, project_title)
|
|
526
|
+
src_project_desc = project_info.get("description")
|
|
527
|
+
new_project_info = api.project.create(
|
|
528
|
+
workspace_id,
|
|
529
|
+
project_title,
|
|
530
|
+
ProjectType.VOLUMES,
|
|
531
|
+
description=src_project_desc,
|
|
532
|
+
readme=project_info.get("readme"),
|
|
533
|
+
)
|
|
534
|
+
api.project.update_meta(new_project_info.id, project_meta)
|
|
535
|
+
|
|
536
|
+
custom_data = new_project_info.custom_data
|
|
537
|
+
source_project_id = payload["project_info"].get("id")
|
|
538
|
+
version_info = payload["project_info"].get("version") or {}
|
|
539
|
+
custom_data["restored_from"] = {
|
|
540
|
+
"project_id": source_project_id,
|
|
541
|
+
"version_num": version_info.get("version"),
|
|
542
|
+
}
|
|
543
|
+
original_custom_data = payload["project_info"].get("custom_data") or {}
|
|
544
|
+
custom_data.update(original_custom_data)
|
|
545
|
+
api.project.update_custom_data(new_project_info.id, custom_data, silent=True)
|
|
546
|
+
|
|
547
|
+
dataset_mapping: Dict[int, sly.DatasetInfo] = {}
|
|
548
|
+
sorted_datasets = sorted(
|
|
549
|
+
dataset_records,
|
|
550
|
+
key=lambda data: (data.get("parent_id") is not None, data.get("parent_id") or 0),
|
|
551
|
+
)
|
|
552
|
+
for dataset_data in sorted_datasets:
|
|
553
|
+
parent_ds_info = dataset_mapping.get(dataset_data.get("parent_id"))
|
|
554
|
+
new_parent_id = parent_ds_info.id if parent_ds_info else None
|
|
555
|
+
new_dataset_info = api.dataset.create(
|
|
556
|
+
project_id=new_project_info.id,
|
|
557
|
+
name=dataset_data.get("name"),
|
|
558
|
+
description=dataset_data.get("description"),
|
|
559
|
+
parent_id=new_parent_id,
|
|
560
|
+
custom_data=dataset_data.get("custom_data"),
|
|
561
|
+
)
|
|
562
|
+
dataset_mapping[dataset_data.get("id")] = new_dataset_info
|
|
563
|
+
|
|
564
|
+
volume_mapping: Dict[int, VolumeInfo] = {}
|
|
565
|
+
volumes_by_dataset: Dict[int, List[Dict]] = defaultdict(list)
|
|
566
|
+
for volume_data in volume_records:
|
|
567
|
+
volumes_by_dataset[volume_data.get("dataset_id")].append(volume_data)
|
|
568
|
+
|
|
569
|
+
for old_dataset_id, dataset_volumes in volumes_by_dataset.items():
|
|
570
|
+
new_dataset_info = dataset_mapping.get(old_dataset_id)
|
|
571
|
+
if new_dataset_info is None:
|
|
572
|
+
continue
|
|
573
|
+
|
|
574
|
+
dataset_volumes_to_upload: List[Dict] = []
|
|
575
|
+
missing_names: List[str] = []
|
|
576
|
+
for vol in dataset_volumes:
|
|
577
|
+
if vol.get("hash"):
|
|
578
|
+
dataset_volumes_to_upload.append(vol)
|
|
579
|
+
else:
|
|
580
|
+
missing_names.append(vol.get("name") or str(vol.get("id")))
|
|
581
|
+
|
|
582
|
+
if missing_names:
|
|
583
|
+
if skip_missed_entities:
|
|
584
|
+
for vol_name in missing_names:
|
|
585
|
+
logger.warning(
|
|
586
|
+
"Volume %r skipped during restoration because its source hash is unavailable.",
|
|
587
|
+
vol_name,
|
|
588
|
+
)
|
|
589
|
+
if len(dataset_volumes_to_upload) == 0:
|
|
590
|
+
continue
|
|
591
|
+
else:
|
|
592
|
+
raise RuntimeError(
|
|
593
|
+
"Cannot restore volumes without available hash. Missing volume names: {}".format(
|
|
594
|
+
", ".join(missing_names)
|
|
595
|
+
)
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
hashes = [volume.get("hash") for volume in dataset_volumes_to_upload]
|
|
599
|
+
names = [volume.get("name") for volume in dataset_volumes_to_upload]
|
|
600
|
+
metas = [volume.get("meta") for volume in dataset_volumes_to_upload]
|
|
601
|
+
|
|
602
|
+
ds_progress = progress_cb
|
|
603
|
+
if log_progress and progress_cb is None:
|
|
604
|
+
ds_progress = tqdm_sly(
|
|
605
|
+
desc="Uploading volumes to {!r}".format(new_dataset_info.name),
|
|
606
|
+
total=len(dataset_volumes_to_upload),
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
new_volume_infos = api.volume.upload_hashes(
|
|
610
|
+
new_dataset_info.id,
|
|
611
|
+
names=names,
|
|
612
|
+
hashes=hashes,
|
|
613
|
+
metas=metas,
|
|
614
|
+
progress_cb=ds_progress,
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
for old_volume, new_volume in zip(dataset_volumes_to_upload, new_volume_infos):
|
|
618
|
+
volume_mapping[old_volume.get("id")] = new_volume
|
|
619
|
+
|
|
620
|
+
for volume_id_str, ann_json in annotations.items():
|
|
621
|
+
new_volume_info = volume_mapping.get(int(volume_id_str))
|
|
622
|
+
if new_volume_info is None:
|
|
623
|
+
if skip_missed_entities:
|
|
624
|
+
logger.warning(
|
|
625
|
+
"Annotation for volume %s skipped because the source volume was not restored.",
|
|
626
|
+
volume_id_str,
|
|
627
|
+
)
|
|
628
|
+
continue
|
|
629
|
+
ann_json["volumeId"] = new_volume_info.id
|
|
630
|
+
ann = VolumeAnnotation.from_json(ann_json, project_meta, None)
|
|
631
|
+
api.volume.annotation.append(new_volume_info.id, ann, None)
|
|
632
|
+
|
|
633
|
+
return api.project.get_info_by_id(new_project_info.id)
|
|
634
|
+
|
|
635
|
+
@staticmethod
|
|
636
|
+
def _require_pyarrow():
|
|
637
|
+
try:
|
|
638
|
+
import pyarrow as pa # pylint: disable=import-error
|
|
639
|
+
except ModuleNotFoundError as exc:
|
|
640
|
+
raise ModuleNotFoundError(
|
|
641
|
+
"VolumeProject binary versioning requires the optional dependency 'pyarrow'. "
|
|
642
|
+
"Install it with `pip install pyarrow` to use download_bin/upload_bin."
|
|
643
|
+
) from exc
|
|
644
|
+
return pa
|
|
645
|
+
|
|
646
|
+
@staticmethod
|
|
647
|
+
def _serialize_payload_to_parquet_blob(pa_module, payload: Dict[str, Dict], snapshot_schema) -> bytes:
|
|
648
|
+
dataset_records: List[Dict] = payload.get("dataset_infos", []) or []
|
|
649
|
+
volume_records: List[Dict] = payload.get("volume_infos", []) or []
|
|
650
|
+
annotations_dict: Dict[str, Dict] = payload.get("annotations", {}) or {}
|
|
651
|
+
|
|
652
|
+
dataset_rows = [snapshot_schema.dataset_row_from_record(r) for r in dataset_records]
|
|
653
|
+
dataset_table = pa_module.Table.from_pylist(
|
|
654
|
+
dataset_rows, schema=snapshot_schema.datasets_table_schema(pa_module)
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
volume_rows = [snapshot_schema.volume_row_from_record(r) for r in volume_records]
|
|
658
|
+
volume_table = pa_module.Table.from_pylist(
|
|
659
|
+
volume_rows, schema=snapshot_schema.volumes_table_schema(pa_module)
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
ann_rows = []
|
|
663
|
+
for volume_id_str, ann in annotations_dict.items():
|
|
664
|
+
try:
|
|
665
|
+
src_volume_id = int(volume_id_str)
|
|
666
|
+
except (TypeError, ValueError):
|
|
667
|
+
continue
|
|
668
|
+
ann_rows.append(
|
|
669
|
+
snapshot_schema.annotation_row_from_dict(src_volume_id=src_volume_id, annotation=ann)
|
|
670
|
+
)
|
|
671
|
+
annotations_table = pa_module.Table.from_pylist(
|
|
672
|
+
ann_rows, schema=snapshot_schema.annotations_table_schema(pa_module)
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
sections = [
|
|
676
|
+
(
|
|
677
|
+
VolumeProject._SECTION_PROJECT_INFO,
|
|
678
|
+
VolumeProject._json_bytes(payload.get("project_info", {})),
|
|
679
|
+
),
|
|
680
|
+
(
|
|
681
|
+
VolumeProject._SECTION_PROJECT_META,
|
|
682
|
+
VolumeProject._json_bytes(payload.get("project_meta", {})),
|
|
683
|
+
),
|
|
684
|
+
(
|
|
685
|
+
VolumeProject._SECTION_DATASETS,
|
|
686
|
+
VolumeProject._table_to_parquet_bytes(pa_module, dataset_table),
|
|
687
|
+
),
|
|
688
|
+
(
|
|
689
|
+
VolumeProject._SECTION_VOLUMES,
|
|
690
|
+
VolumeProject._table_to_parquet_bytes(pa_module, volume_table),
|
|
691
|
+
),
|
|
692
|
+
(
|
|
693
|
+
VolumeProject._SECTION_ANNOTATIONS,
|
|
694
|
+
VolumeProject._table_to_parquet_bytes(pa_module, annotations_table),
|
|
695
|
+
),
|
|
696
|
+
]
|
|
697
|
+
|
|
698
|
+
return VolumeProject._assemble_sections(sections)
|
|
699
|
+
|
|
700
|
+
@staticmethod
|
|
701
|
+
def _build_table(pa_module, columns: Dict[str, Tuple[List, Any]]):
|
|
702
|
+
arrays = {}
|
|
703
|
+
for name, (values, dtype) in columns.items():
|
|
704
|
+
arrays[name] = pa_module.array(values, type=dtype)
|
|
705
|
+
return pa_module.table(arrays)
|
|
706
|
+
|
|
707
|
+
@staticmethod
|
|
708
|
+
def _table_to_parquet_bytes(pa_module, table) -> bytes:
|
|
709
|
+
from pyarrow import parquet as pq # pylint: disable=import-error
|
|
710
|
+
|
|
711
|
+
sink = pa_module.BufferOutputStream()
|
|
712
|
+
pq.write_table(table, sink)
|
|
713
|
+
return sink.getvalue().to_pybytes()
|
|
714
|
+
|
|
715
|
+
@staticmethod
|
|
716
|
+
def _parquet_bytes_to_table(pa_module, data: bytes):
|
|
717
|
+
if not data:
|
|
718
|
+
return pa_module.table({})
|
|
719
|
+
from pyarrow import parquet as pq # pylint: disable=import-error
|
|
720
|
+
|
|
721
|
+
buffer = pa_module.BufferReader(data)
|
|
722
|
+
return pq.read_table(buffer)
|
|
723
|
+
|
|
724
|
+
@staticmethod
|
|
725
|
+
def _json_dumps(data) -> str:
|
|
726
|
+
if isinstance(data, str):
|
|
727
|
+
return data
|
|
728
|
+
return json.dumps(data, ensure_ascii=False)
|
|
729
|
+
|
|
730
|
+
@staticmethod
|
|
731
|
+
def _json_bytes(data) -> bytes:
|
|
732
|
+
return VolumeProject._json_dumps(data).encode("utf-8")
|
|
733
|
+
|
|
734
|
+
@staticmethod
|
|
735
|
+
def _assemble_sections(sections: List[Tuple[int, bytes]]) -> bytes:
|
|
736
|
+
if len(sections) > 255:
|
|
737
|
+
raise RuntimeError("Too many sections for VolumeProject binary payload")
|
|
738
|
+
buffer = io.BytesIO()
|
|
739
|
+
buffer.write(VolumeProject._SERIALIZATION_MAGIC)
|
|
740
|
+
buffer.write(struct.pack(">B", VolumeProject._SERIALIZATION_VERSION))
|
|
741
|
+
buffer.write(struct.pack(">B", len(sections)))
|
|
742
|
+
for section_type, payload in sections:
|
|
743
|
+
if payload is None:
|
|
744
|
+
payload = b""
|
|
745
|
+
buffer.write(struct.pack(">B", section_type))
|
|
746
|
+
buffer.write(struct.pack(">Q", len(payload)))
|
|
747
|
+
buffer.write(payload)
|
|
748
|
+
return buffer.getvalue()
|
|
749
|
+
|
|
750
|
+
@staticmethod
|
|
751
|
+
def _parse_parquet_sections(raw_data) -> Dict[int, bytes]:
|
|
752
|
+
magic = VolumeProject._SERIALIZATION_MAGIC
|
|
753
|
+
view = raw_data if isinstance(raw_data, memoryview) else memoryview(raw_data)
|
|
754
|
+
header_len = len(magic) + 2
|
|
755
|
+
if len(view) < header_len:
|
|
756
|
+
logger.warning(
|
|
757
|
+
f"VolumeProject binary payload too small: {len(view)} bytes (need >= {header_len}). First bytes(hex)={view[: min(len(view), 16)].tobytes().hex()}",
|
|
758
|
+
)
|
|
759
|
+
raise RuntimeError("Corrupted VolumeProject binary payload")
|
|
760
|
+
if view[: len(magic)].tobytes() != magic:
|
|
761
|
+
found = view[: len(magic)].tobytes()
|
|
762
|
+
logger.warning(
|
|
763
|
+
f"VolumeProject binary payload magic mismatch. expected={magic.hex()} found={found.hex()} total_bytes={len(view)} prefix16(hex)={view[:16].tobytes().hex()}",
|
|
764
|
+
)
|
|
765
|
+
raise RuntimeError(
|
|
766
|
+
"Unsupported VolumeProject binary payload format (magic mismatch). "
|
|
767
|
+
"Expected magic={!r}, found={!r}".format(magic, found)
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
offset = len(magic)
|
|
771
|
+
version = view[offset]
|
|
772
|
+
offset += 1
|
|
773
|
+
if version != VolumeProject._SERIALIZATION_VERSION:
|
|
774
|
+
logger.warning(
|
|
775
|
+
"VolumeProject binary payload version mismatch. expected=%d found=%d total_bytes=%d",
|
|
776
|
+
VolumeProject._SERIALIZATION_VERSION,
|
|
777
|
+
version,
|
|
778
|
+
len(view),
|
|
779
|
+
)
|
|
780
|
+
raise RuntimeError(
|
|
781
|
+
"Unsupported VolumeProject binary payload version: {}".format(version)
|
|
782
|
+
)
|
|
783
|
+
|
|
784
|
+
section_count = view[offset]
|
|
785
|
+
offset += 1
|
|
786
|
+
sections: Dict[int, bytes] = {}
|
|
787
|
+
for _ in range(section_count):
|
|
788
|
+
if offset + 9 > len(view):
|
|
789
|
+
raise RuntimeError("Corrupted VolumeProject binary payload")
|
|
790
|
+
section_type = view[offset]
|
|
791
|
+
offset += 1
|
|
792
|
+
length = int.from_bytes(view[offset : offset + 8], "big")
|
|
793
|
+
offset += 8
|
|
794
|
+
if offset + length > len(view):
|
|
795
|
+
raise RuntimeError("Corrupted VolumeProject binary payload")
|
|
796
|
+
sections[section_type] = view[offset : offset + length].tobytes()
|
|
797
|
+
offset += length
|
|
798
|
+
return sections
|
|
799
|
+
|
|
800
|
+
@staticmethod
|
|
801
|
+
def _deserialize_payload_from_parquet(pa_module, raw_data) -> Dict:
|
|
802
|
+
sections = VolumeProject._parse_parquet_sections(raw_data)
|
|
803
|
+
|
|
804
|
+
try:
|
|
805
|
+
project_info = json.loads(sections[VolumeProject._SECTION_PROJECT_INFO].decode("utf-8"))
|
|
806
|
+
project_meta = json.loads(sections[VolumeProject._SECTION_PROJECT_META].decode("utf-8"))
|
|
807
|
+
except KeyError as exc:
|
|
808
|
+
raise RuntimeError("VolumeProject payload missing metadata section") from exc
|
|
809
|
+
|
|
810
|
+
if VolumeProject._SECTION_DATASETS not in sections:
|
|
811
|
+
logger.warning("VolumeProject blob has no datasets section; treating as empty.")
|
|
812
|
+
if VolumeProject._SECTION_VOLUMES not in sections:
|
|
813
|
+
logger.warning("VolumeProject blob has no volumes section; treating as empty.")
|
|
814
|
+
if VolumeProject._SECTION_ANNOTATIONS not in sections:
|
|
815
|
+
logger.warning("VolumeProject blob has no annotations section; treating as empty.")
|
|
816
|
+
|
|
817
|
+
dataset_table = VolumeProject._parquet_bytes_to_table(
|
|
818
|
+
pa_module, sections.get(VolumeProject._SECTION_DATASETS, b"")
|
|
819
|
+
)
|
|
820
|
+
volume_table = VolumeProject._parquet_bytes_to_table(
|
|
821
|
+
pa_module, sections.get(VolumeProject._SECTION_VOLUMES, b"")
|
|
822
|
+
)
|
|
823
|
+
annotations_table = VolumeProject._parquet_bytes_to_table(
|
|
824
|
+
pa_module, sections.get(VolumeProject._SECTION_ANNOTATIONS, b"")
|
|
825
|
+
)
|
|
826
|
+
|
|
827
|
+
dataset_records: List[Dict] = []
|
|
828
|
+
if dataset_table is not None and dataset_table.num_rows:
|
|
829
|
+
col_json = (
|
|
830
|
+
VersionSchemaField.JSON
|
|
831
|
+
if VersionSchemaField.JSON in dataset_table.column_names
|
|
832
|
+
else "json"
|
|
833
|
+
)
|
|
834
|
+
if col_json in dataset_table.column_names:
|
|
835
|
+
dataset_jsons = dataset_table.column(col_json).to_pylist()
|
|
836
|
+
dataset_records = [json.loads(item) for item in dataset_jsons]
|
|
837
|
+
|
|
838
|
+
volume_records: List[Dict] = []
|
|
839
|
+
if volume_table is not None and volume_table.num_rows:
|
|
840
|
+
col_json = (
|
|
841
|
+
VersionSchemaField.JSON
|
|
842
|
+
if VersionSchemaField.JSON in volume_table.column_names
|
|
843
|
+
else "json"
|
|
844
|
+
)
|
|
845
|
+
if col_json in volume_table.column_names:
|
|
846
|
+
volume_jsons = volume_table.column(col_json).to_pylist()
|
|
847
|
+
volume_records = [json.loads(item) for item in volume_jsons]
|
|
848
|
+
|
|
849
|
+
annotations: Dict[str, Dict] = {}
|
|
850
|
+
if annotations_table is not None and annotations_table.num_rows:
|
|
851
|
+
col_vol_id = (
|
|
852
|
+
VersionSchemaField.SRC_VOLUME_ID
|
|
853
|
+
if VersionSchemaField.SRC_VOLUME_ID in annotations_table.column_names
|
|
854
|
+
else "volume_id"
|
|
855
|
+
)
|
|
856
|
+
col_ann = (
|
|
857
|
+
VersionSchemaField.ANNOTATION
|
|
858
|
+
if VersionSchemaField.ANNOTATION in annotations_table.column_names
|
|
859
|
+
else "annotation"
|
|
860
|
+
)
|
|
861
|
+
if col_vol_id in annotations_table.column_names and col_ann in annotations_table.column_names:
|
|
862
|
+
annotation_ids = annotations_table.column(col_vol_id).to_pylist()
|
|
863
|
+
annotation_payloads = annotations_table.column(col_ann).to_pylist()
|
|
864
|
+
for volume_id, annotation_json in zip(annotation_ids, annotation_payloads):
|
|
865
|
+
if volume_id is None:
|
|
866
|
+
continue
|
|
867
|
+
annotations[str(volume_id)] = json.loads(annotation_json)
|
|
868
|
+
|
|
869
|
+
return {
|
|
870
|
+
"project_info": project_info,
|
|
871
|
+
"project_meta": project_meta,
|
|
872
|
+
"dataset_infos": dataset_records,
|
|
873
|
+
"volume_infos": volume_records,
|
|
874
|
+
"annotations": annotations,
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
@staticmethod
|
|
878
|
+
def _load_mask_geometries(api: Api, ann: VolumeAnnotation, key_id_map: KeyIdMap) -> None:
|
|
879
|
+
for sf in ann.spatial_figures:
|
|
880
|
+
if sf.geometry.name() != Mask3D.name():
|
|
881
|
+
continue
|
|
882
|
+
api.volume.figure.load_sf_geometry(sf, key_id_map)
|
|
883
|
+
|
|
266
884
|
@staticmethod
|
|
267
885
|
def get_train_val_splits_by_count(project_dir: str, train_count: int, val_count: int) -> None:
|
|
268
886
|
"""
|
|
@@ -270,7 +888,7 @@ class VolumeProject(VideoProject):
|
|
|
270
888
|
:raises: :class:`NotImplementedError` in all cases.
|
|
271
889
|
"""
|
|
272
890
|
raise NotImplementedError(
|
|
273
|
-
|
|
891
|
+
"Static method 'get_train_val_splits_by_count()' is not supported for VolumeProject class now."
|
|
274
892
|
)
|
|
275
893
|
|
|
276
894
|
@staticmethod
|
|
@@ -285,7 +903,7 @@ class VolumeProject(VideoProject):
|
|
|
285
903
|
:raises: :class:`NotImplementedError` in all cases.
|
|
286
904
|
"""
|
|
287
905
|
raise NotImplementedError(
|
|
288
|
-
|
|
906
|
+
"Static method 'get_train_val_splits_by_tag()' is not supported for VolumeProject class now."
|
|
289
907
|
)
|
|
290
908
|
|
|
291
909
|
@staticmethod
|