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
supervisely/api/api.py
CHANGED
|
@@ -10,6 +10,7 @@ import glob
|
|
|
10
10
|
import json
|
|
11
11
|
import os
|
|
12
12
|
import shutil
|
|
13
|
+
import threading
|
|
13
14
|
from logging import Logger
|
|
14
15
|
from pathlib import Path
|
|
15
16
|
from typing import (
|
|
@@ -41,6 +42,7 @@ import supervisely.api.dataset_api as dataset_api
|
|
|
41
42
|
import supervisely.api.entities_collection_api as entities_collection_api
|
|
42
43
|
import supervisely.api.file_api as file_api
|
|
43
44
|
import supervisely.api.github_api as github_api
|
|
45
|
+
import supervisely.api.guides_api as guides_api
|
|
44
46
|
import supervisely.api.image_annotation_tool_api as image_annotation_tool_api
|
|
45
47
|
import supervisely.api.image_api as image_api
|
|
46
48
|
import supervisely.api.import_storage_api as import_stoarge_api
|
|
@@ -357,6 +359,7 @@ class Api:
|
|
|
357
359
|
self.user = user_api.UserApi(self)
|
|
358
360
|
self.labeling_job = labeling_job_api.LabelingJobApi(self)
|
|
359
361
|
self.labeling_queue = labeling_queue_api.LabelingQueueApi(self)
|
|
362
|
+
self.guides = guides_api.GuidesApi(self)
|
|
360
363
|
self.video = video_api.VideoApi(self)
|
|
361
364
|
# self.project_class = project_class_api.ProjectClassApi(self)
|
|
362
365
|
self.object_class = object_class_api.ObjectClassApi(self)
|
|
@@ -392,13 +395,15 @@ class Api:
|
|
|
392
395
|
else not self.server_address.startswith("https://")
|
|
393
396
|
)
|
|
394
397
|
|
|
395
|
-
if check_instance_version:
|
|
396
|
-
self._check_version(None if check_instance_version is True else check_instance_version)
|
|
397
|
-
|
|
398
398
|
self.async_httpx_client: httpx.AsyncClient = None
|
|
399
399
|
self.httpx_client: httpx.Client = None
|
|
400
400
|
self._semaphore = None
|
|
401
401
|
self._instance_version = None
|
|
402
|
+
self._version_check_completed = False
|
|
403
|
+
self._version_check_lock = threading.Lock()
|
|
404
|
+
|
|
405
|
+
if check_instance_version:
|
|
406
|
+
self._check_version(None if check_instance_version is True else check_instance_version)
|
|
402
407
|
|
|
403
408
|
@classmethod
|
|
404
409
|
def normalize_server_address(cls, server_address: str) -> str:
|
|
@@ -600,38 +605,49 @@ class Api:
|
|
|
600
605
|
:type version: Optional[str], e.g. "6.9.13"
|
|
601
606
|
"""
|
|
602
607
|
|
|
603
|
-
#
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
+
# Thread-safe one-time check with double-checked locking pattern
|
|
609
|
+
if self._version_check_completed:
|
|
610
|
+
return
|
|
611
|
+
|
|
612
|
+
with self._version_check_lock:
|
|
613
|
+
# Double-check inside the lock
|
|
614
|
+
if self._version_check_completed:
|
|
615
|
+
return
|
|
616
|
+
|
|
617
|
+
self._version_check_completed = True
|
|
618
|
+
|
|
619
|
+
# Since it's a informational message, we don't raise an exception if the check fails
|
|
620
|
+
# in any case, we don't want to interrupt the user's workflow.
|
|
621
|
+
try:
|
|
622
|
+
check_result = self.is_version_supported(version)
|
|
623
|
+
if check_result is None:
|
|
624
|
+
logger.debug(
|
|
625
|
+
"Failed to check if the instance version meets the minimum requirements "
|
|
626
|
+
"of current SDK version. "
|
|
627
|
+
"Ensure that the MINIMUM_INSTANCE_VERSION_FOR_SDK environment variable is set. "
|
|
628
|
+
"Usually you can ignore this message, but if you're adding new features, "
|
|
629
|
+
"which will require upgrade of the Supervisely instance, you should update "
|
|
630
|
+
"it supervisely.__init__.py file."
|
|
631
|
+
)
|
|
632
|
+
if check_result is False:
|
|
633
|
+
message = (
|
|
634
|
+
"The current version of the Supervisely instance is not supported by the SDK. "
|
|
635
|
+
"Some features may not work correctly."
|
|
636
|
+
)
|
|
637
|
+
if not is_community():
|
|
638
|
+
message += (
|
|
639
|
+
" Please upgrade the Supervisely instance to the latest version (recommended) "
|
|
640
|
+
"or downgrade the SDK to the version that supports the current instance (not recommended). "
|
|
641
|
+
"Refer to this docs for more information: "
|
|
642
|
+
"https://docs.supervisely.com/enterprise-edition/get-supervisely/upgrade "
|
|
643
|
+
"Check out changelog for the latest version of Supervisely: "
|
|
644
|
+
"https://app.supervisely.com/changelog"
|
|
645
|
+
)
|
|
646
|
+
logger.warning(message)
|
|
647
|
+
except Exception as e:
|
|
608
648
|
logger.debug(
|
|
609
|
-
"
|
|
610
|
-
"of current SDK version. "
|
|
611
|
-
"Ensure that the MINIMUM_INSTANCE_VERSION_FOR_SDK environment variable is set. "
|
|
612
|
-
"Usually you can ignore this message, but if you're adding new features, "
|
|
613
|
-
"which will require upgrade of the Supervisely instance, you should update "
|
|
614
|
-
"it supervisely.__init__.py file."
|
|
615
|
-
)
|
|
616
|
-
if check_result is False:
|
|
617
|
-
message = (
|
|
618
|
-
"The current version of the Supervisely instance is not supported by the SDK. "
|
|
619
|
-
"Some features may not work correctly."
|
|
649
|
+
f"Tried to check version compatibility between SDK and instance, but failed: {e}"
|
|
620
650
|
)
|
|
621
|
-
if not is_community():
|
|
622
|
-
message += (
|
|
623
|
-
" Please upgrade the Supervisely instance to the latest version (recommended) "
|
|
624
|
-
"or downgrade the SDK to the version that supports the current instance (not recommended). "
|
|
625
|
-
"Refer to this docs for more information: "
|
|
626
|
-
"https://docs.supervisely.com/enterprise-edition/get-supervisely/upgrade "
|
|
627
|
-
"Check out changelog for the latest version of Supervisely: "
|
|
628
|
-
"https://app.supervisely.com/changelog"
|
|
629
|
-
)
|
|
630
|
-
logger.warning(message)
|
|
631
|
-
except Exception as e:
|
|
632
|
-
logger.debug(
|
|
633
|
-
f"Tried to check version compatibility between SDK and instance, but failed: {e}"
|
|
634
|
-
)
|
|
635
651
|
|
|
636
652
|
def post(
|
|
637
653
|
self,
|
|
@@ -686,7 +702,8 @@ class Api:
|
|
|
686
702
|
)
|
|
687
703
|
|
|
688
704
|
if response.status_code != requests.codes.ok: # pylint: disable=no-member
|
|
689
|
-
self.
|
|
705
|
+
if not self._version_check_completed:
|
|
706
|
+
self._check_version()
|
|
690
707
|
Api._raise_for_status(response)
|
|
691
708
|
return response
|
|
692
709
|
except requests.RequestException as exc:
|
|
@@ -1103,7 +1120,8 @@ class Api:
|
|
|
1103
1120
|
timeout=timeout,
|
|
1104
1121
|
)
|
|
1105
1122
|
if response.status_code != httpx.codes.OK:
|
|
1106
|
-
self.
|
|
1123
|
+
if not self._version_check_completed:
|
|
1124
|
+
self._check_version()
|
|
1107
1125
|
Api._raise_for_status_httpx(response)
|
|
1108
1126
|
return response
|
|
1109
1127
|
except (httpx.RequestError, httpx.HTTPStatusError) as exc:
|
|
@@ -1319,7 +1337,8 @@ class Api:
|
|
|
1319
1337
|
httpx.codes.OK,
|
|
1320
1338
|
httpx.codes.PARTIAL_CONTENT,
|
|
1321
1339
|
]:
|
|
1322
|
-
self.
|
|
1340
|
+
if not self._version_check_completed:
|
|
1341
|
+
self._check_version()
|
|
1323
1342
|
Api._raise_for_status_httpx(resp)
|
|
1324
1343
|
|
|
1325
1344
|
hhash = resp.headers.get("x-content-checksum-sha256", None)
|
|
@@ -1433,7 +1452,8 @@ class Api:
|
|
|
1433
1452
|
timeout=timeout,
|
|
1434
1453
|
)
|
|
1435
1454
|
if response.status_code != httpx.codes.OK:
|
|
1436
|
-
self.
|
|
1455
|
+
if not self._version_check_completed:
|
|
1456
|
+
self._check_version()
|
|
1437
1457
|
Api._raise_for_status_httpx(response)
|
|
1438
1458
|
return response
|
|
1439
1459
|
except (httpx.RequestError, httpx.HTTPStatusError) as exc:
|
|
@@ -1574,7 +1594,8 @@ class Api:
|
|
|
1574
1594
|
httpx.codes.OK,
|
|
1575
1595
|
httpx.codes.PARTIAL_CONTENT,
|
|
1576
1596
|
]:
|
|
1577
|
-
self.
|
|
1597
|
+
if not self._version_check_completed:
|
|
1598
|
+
self._check_version()
|
|
1578
1599
|
Api._raise_for_status_httpx(resp)
|
|
1579
1600
|
|
|
1580
1601
|
# received hash of the content to check integrity of the data stream
|
supervisely/api/app_api.py
CHANGED
|
@@ -140,7 +140,7 @@ def check_workflow_compatibility(api, min_instance_version: str) -> bool:
|
|
|
140
140
|
"instance_version", api.instance_version
|
|
141
141
|
)
|
|
142
142
|
|
|
143
|
-
if instance_version == "unknown":
|
|
143
|
+
if instance_version is None or instance_version == "unknown":
|
|
144
144
|
# to check again on the next call
|
|
145
145
|
del _workflow_compatibility_version_cache["instance_version"]
|
|
146
146
|
logger.info(
|
|
@@ -1750,6 +1750,7 @@ class AppApi(TaskApi):
|
|
|
1750
1750
|
module_id: Optional[int] = None,
|
|
1751
1751
|
redirect_requests: Dict[str, int] = {},
|
|
1752
1752
|
kubernetes_settings: Optional[Union[KubernetesSettings, Dict[str, Any]]] = None,
|
|
1753
|
+
multi_user_session: bool = False,
|
|
1753
1754
|
) -> SessionInfo:
|
|
1754
1755
|
"""Start a new application session (task).
|
|
1755
1756
|
|
|
@@ -1783,13 +1784,20 @@ class AppApi(TaskApi):
|
|
|
1783
1784
|
:type redirect_requests: dict
|
|
1784
1785
|
:param kubernetes_settings: Kubernetes settings for the task. If not specified, default settings will be used.
|
|
1785
1786
|
:type kubernetes_settings: Optional[Union[KubernetesSettings, Dict[str, Any]]]
|
|
1787
|
+
:param multi_user_session: If True, the application session will be created as multi-user.
|
|
1788
|
+
In this case, multiple users will be able to connect to the same application session.
|
|
1789
|
+
All users will have separate application states.
|
|
1790
|
+
Available only for applications that support multi-user sessions.
|
|
1791
|
+
:type multi_user_session: bool, default is False
|
|
1786
1792
|
:return: SessionInfo object with information about the started task.
|
|
1787
1793
|
:rtype: SessionInfo
|
|
1788
1794
|
:raises ValueError: If both app_id and module_id are not provided.
|
|
1789
1795
|
:raises ValueError: If both app_id and module_id are provided.
|
|
1790
1796
|
"""
|
|
1791
1797
|
users_ids = None
|
|
1792
|
-
if users_id
|
|
1798
|
+
if isinstance(users_id, list) and all(isinstance(u, int) for u in users_id):
|
|
1799
|
+
users_ids = users_id
|
|
1800
|
+
elif isinstance(users_id, int):
|
|
1793
1801
|
users_ids = [users_id]
|
|
1794
1802
|
|
|
1795
1803
|
new_params = {}
|
|
@@ -1818,6 +1826,7 @@ class AppApi(TaskApi):
|
|
|
1818
1826
|
module_id=module_id,
|
|
1819
1827
|
redirect_requests=redirect_requests,
|
|
1820
1828
|
kubernetes_settings=kubernetes_settings,
|
|
1829
|
+
multi_user_session=multi_user_session,
|
|
1821
1830
|
)
|
|
1822
1831
|
if type(result) is not list:
|
|
1823
1832
|
result = [result]
|
supervisely/api/dataset_api.py
CHANGED
|
@@ -1021,13 +1021,66 @@ class DatasetApi(UpdateableModule, RemoveableModuleApi):
|
|
|
1021
1021
|
|
|
1022
1022
|
return dataset_tree
|
|
1023
1023
|
|
|
1024
|
-
def
|
|
1024
|
+
def _yield_tree(
|
|
1025
|
+
self, tree: Dict[DatasetInfo, Dict], path: List[str]
|
|
1026
|
+
) -> Generator[Tuple[List[str], DatasetInfo], None, None]:
|
|
1027
|
+
"""
|
|
1028
|
+
Helper method for recursive tree traversal.
|
|
1029
|
+
Yields tuples of (path, dataset) for all datasets in the tree. For each node (dataset) at the current level,
|
|
1030
|
+
yields its (path, dataset) before recursively traversing and yielding from its children.
|
|
1031
|
+
|
|
1032
|
+
:param tree: Tree structure to yield from.
|
|
1033
|
+
:type tree: Dict[DatasetInfo, Dict]
|
|
1034
|
+
:param path: Current path (used for recursion).
|
|
1035
|
+
:type path: List[str]
|
|
1036
|
+
:return: Generator of tuples of (path, dataset).
|
|
1037
|
+
:rtype: Generator[Tuple[List[str], DatasetInfo], None, None]
|
|
1038
|
+
"""
|
|
1039
|
+
for dataset, children in tree.items():
|
|
1040
|
+
yield path, dataset
|
|
1041
|
+
new_path = path + [dataset.name]
|
|
1042
|
+
if children:
|
|
1043
|
+
yield from self._yield_tree(children, new_path)
|
|
1044
|
+
|
|
1045
|
+
def _find_dataset_in_tree(
|
|
1046
|
+
self, tree: Dict[DatasetInfo, Dict], target_id: int, path: List[str] = None
|
|
1047
|
+
) -> Tuple[Optional[DatasetInfo], Optional[Dict], List[str]]:
|
|
1048
|
+
"""Find a specific dataset in the tree and return its subtree and path.
|
|
1049
|
+
|
|
1050
|
+
:param tree: Tree structure to search in.
|
|
1051
|
+
:type tree: Dict[DatasetInfo, Dict]
|
|
1052
|
+
:param target_id: ID of the dataset to find.
|
|
1053
|
+
:type target_id: int
|
|
1054
|
+
:param path: Current path (used for recursion).
|
|
1055
|
+
:type path: List[str], optional
|
|
1056
|
+
:return: Tuple of (found_dataset, its_subtree, path_to_dataset).
|
|
1057
|
+
:rtype: Tuple[Optional[DatasetInfo], Optional[Dict], List[str]]
|
|
1058
|
+
"""
|
|
1059
|
+
if path is None:
|
|
1060
|
+
path = []
|
|
1061
|
+
|
|
1062
|
+
for dataset, children in tree.items():
|
|
1063
|
+
if dataset.id == target_id:
|
|
1064
|
+
return dataset, children, path
|
|
1065
|
+
# Search in children
|
|
1066
|
+
if children:
|
|
1067
|
+
found_dataset, found_children, found_path = self._find_dataset_in_tree(
|
|
1068
|
+
children, target_id, path + [dataset.name]
|
|
1069
|
+
)
|
|
1070
|
+
if found_dataset is not None:
|
|
1071
|
+
return found_dataset, found_children, found_path
|
|
1072
|
+
return None, None, []
|
|
1073
|
+
|
|
1074
|
+
def tree(self, project_id: int, dataset_id: Optional[int] = None) -> Generator[Tuple[List[str], DatasetInfo], None, None]:
|
|
1025
1075
|
"""Yields tuples of (path, dataset) for all datasets in the project.
|
|
1026
1076
|
Path of the dataset is a list of parents, e.g. ["ds1", "ds2", "ds3"].
|
|
1027
1077
|
For root datasets, the path is an empty list.
|
|
1028
1078
|
|
|
1029
1079
|
:param project_id: Project ID in which the Dataset is located.
|
|
1030
1080
|
:type project_id: int
|
|
1081
|
+
:param dataset_id: Optional Dataset ID to start the tree from. If provided, only yields
|
|
1082
|
+
the subtree starting from this dataset (including the dataset itself and all its children).
|
|
1083
|
+
:type dataset_id: Optional[int]
|
|
1031
1084
|
:return: Generator of tuples of (path, dataset).
|
|
1032
1085
|
:rtype: Generator[Tuple[List[str], DatasetInfo], None, None]
|
|
1033
1086
|
:Usage example:
|
|
@@ -1040,11 +1093,17 @@ class DatasetApi(UpdateableModule, RemoveableModuleApi):
|
|
|
1040
1093
|
|
|
1041
1094
|
project_id = 123
|
|
1042
1095
|
|
|
1096
|
+
# Get all datasets in the project
|
|
1043
1097
|
for parents, dataset in api.dataset.tree(project_id):
|
|
1044
1098
|
parents: List[str]
|
|
1045
1099
|
dataset: sly.DatasetInfo
|
|
1046
1100
|
print(parents, dataset.name)
|
|
1047
1101
|
|
|
1102
|
+
# Get only a specific branch starting from dataset_id = 456
|
|
1103
|
+
for parents, dataset in api.dataset.tree(project_id, dataset_id=456):
|
|
1104
|
+
parents: List[str]
|
|
1105
|
+
dataset: sly.DatasetInfo
|
|
1106
|
+
print(parents, dataset.name)
|
|
1048
1107
|
|
|
1049
1108
|
# Output:
|
|
1050
1109
|
# [] ds1
|
|
@@ -1052,17 +1111,20 @@ class DatasetApi(UpdateableModule, RemoveableModuleApi):
|
|
|
1052
1111
|
# ["ds1", "ds2"] ds3
|
|
1053
1112
|
"""
|
|
1054
1113
|
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1114
|
+
full_tree = self.get_tree(project_id)
|
|
1115
|
+
|
|
1116
|
+
if dataset_id is None:
|
|
1117
|
+
# Return the full tree
|
|
1118
|
+
yield from self._yield_tree(full_tree, [])
|
|
1119
|
+
else:
|
|
1120
|
+
# Find the specific dataset and return only its subtree
|
|
1121
|
+
target_dataset, subtree, dataset_path = self._find_dataset_in_tree(full_tree, dataset_id)
|
|
1122
|
+
if target_dataset is not None:
|
|
1123
|
+
# Yield the target dataset first, then its children
|
|
1124
|
+
yield dataset_path, target_dataset
|
|
1125
|
+
if subtree:
|
|
1126
|
+
new_path = dataset_path + [target_dataset.name]
|
|
1127
|
+
yield from self._yield_tree(subtree, new_path)
|
|
1066
1128
|
|
|
1067
1129
|
def get_nested(self, project_id: int, dataset_id: int) -> List[DatasetInfo]:
|
|
1068
1130
|
"""Returns a list of all nested datasets in the specified dataset.
|
|
@@ -281,6 +281,7 @@ class EntitiesCollectionApi(UpdateableModule, RemoveableModuleApi):
|
|
|
281
281
|
description: Optional[str] = None,
|
|
282
282
|
type: str = CollectionType.DEFAULT,
|
|
283
283
|
ai_search_key: Optional[str] = None,
|
|
284
|
+
change_name_if_conflict=False,
|
|
284
285
|
) -> EntitiesCollectionInfo:
|
|
285
286
|
"""
|
|
286
287
|
Creates Entities Collections.
|
|
@@ -295,6 +296,8 @@ class EntitiesCollectionApi(UpdateableModule, RemoveableModuleApi):
|
|
|
295
296
|
:type type: str
|
|
296
297
|
:param ai_search_key: AI search key for the collection. Defaults to None.
|
|
297
298
|
:type ai_search_key: Optional[str]
|
|
299
|
+
:param change_name_if_conflict: Checks if given name already exists and adds suffix to the end of the name. Defaults to False.
|
|
300
|
+
:type change_name_if_conflict: bool
|
|
298
301
|
:return: Information about new Entities Collection
|
|
299
302
|
:rtype: :class:`EntitiesCollectionInfo`
|
|
300
303
|
:Usage example:
|
|
@@ -316,6 +319,13 @@ class EntitiesCollectionApi(UpdateableModule, RemoveableModuleApi):
|
|
|
316
319
|
new_collection = api.entities_collection.create(project_id, name, description, type, ai_search_key)
|
|
317
320
|
print(new_collection)
|
|
318
321
|
"""
|
|
322
|
+
|
|
323
|
+
name = self._get_effective_new_name(
|
|
324
|
+
parent_id=project_id,
|
|
325
|
+
name=name,
|
|
326
|
+
change_name_if_conflict=change_name_if_conflict,
|
|
327
|
+
)
|
|
328
|
+
|
|
319
329
|
method = "entities-collections.add"
|
|
320
330
|
data = {
|
|
321
331
|
ApiField.PROJECT_ID: project_id,
|
|
@@ -24,6 +24,7 @@ from requests_toolbelt import MultipartDecoder, MultipartEncoder
|
|
|
24
24
|
from tqdm import tqdm
|
|
25
25
|
|
|
26
26
|
from supervisely._utils import batched, logger, run_coroutine
|
|
27
|
+
from supervisely.annotation.label import LabelingStatus
|
|
27
28
|
from supervisely.api.module_api import ApiField, ModuleApi, RemoveableBulkModuleApi
|
|
28
29
|
from supervisely.geometry.rectangle import Rectangle
|
|
29
30
|
from supervisely.video_annotation.key_id_map import KeyIdMap
|
|
@@ -221,6 +222,8 @@ class FigureApi(RemoveableBulkModuleApi):
|
|
|
221
222
|
"meta",
|
|
222
223
|
"area",
|
|
223
224
|
"priority",
|
|
225
|
+
"nnCreated",
|
|
226
|
+
"nnUpdated",
|
|
224
227
|
]
|
|
225
228
|
return self._get_info_by_id(id, "figures.info", {ApiField.FIELDS: fields})
|
|
226
229
|
|
|
@@ -233,6 +236,7 @@ class FigureApi(RemoveableBulkModuleApi):
|
|
|
233
236
|
geometry_type: str,
|
|
234
237
|
track_id: Optional[int] = None,
|
|
235
238
|
custom_data: Optional[dict] = None,
|
|
239
|
+
status: Optional[LabelingStatus] = None,
|
|
236
240
|
) -> int:
|
|
237
241
|
""""""
|
|
238
242
|
input_figure = {
|
|
@@ -242,6 +246,13 @@ class FigureApi(RemoveableBulkModuleApi):
|
|
|
242
246
|
ApiField.GEOMETRY: geometry_json,
|
|
243
247
|
}
|
|
244
248
|
|
|
249
|
+
if status is None:
|
|
250
|
+
status = LabelingStatus.MANUAL
|
|
251
|
+
|
|
252
|
+
nn_created, nn_updated = LabelingStatus.to_flags(status)
|
|
253
|
+
input_figure[ApiField.NN_CREATED] = nn_created
|
|
254
|
+
input_figure[ApiField.NN_UPDATED] = nn_updated
|
|
255
|
+
|
|
245
256
|
if track_id is not None:
|
|
246
257
|
input_figure[ApiField.TRACK_ID] = track_id
|
|
247
258
|
|
|
@@ -376,6 +387,8 @@ class FigureApi(RemoveableBulkModuleApi):
|
|
|
376
387
|
ApiField.AREA,
|
|
377
388
|
ApiField.PRIORITY,
|
|
378
389
|
ApiField.CUSTOM_DATA,
|
|
390
|
+
ApiField.NN_CREATED,
|
|
391
|
+
ApiField.NN_UPDATED,
|
|
379
392
|
]
|
|
380
393
|
figures_infos = self.get_list_all_pages(
|
|
381
394
|
"figures.list",
|
|
@@ -496,6 +509,8 @@ class FigureApi(RemoveableBulkModuleApi):
|
|
|
496
509
|
ApiField.AREA,
|
|
497
510
|
ApiField.PRIORITY,
|
|
498
511
|
ApiField.CUSTOM_DATA,
|
|
512
|
+
ApiField.NN_CREATED,
|
|
513
|
+
ApiField.NN_UPDATED,
|
|
499
514
|
]
|
|
500
515
|
if skip_geometry is True:
|
|
501
516
|
fields = [x for x in fields if x != ApiField.GEOMETRY]
|
|
@@ -580,10 +595,13 @@ class FigureApi(RemoveableBulkModuleApi):
|
|
|
580
595
|
"""
|
|
581
596
|
geometries = {}
|
|
582
597
|
for idx, part in self._download_geometries_generator(ids):
|
|
583
|
-
|
|
584
|
-
progress_cb
|
|
585
|
-
|
|
586
|
-
|
|
598
|
+
try:
|
|
599
|
+
if progress_cb is not None:
|
|
600
|
+
progress_cb(len(part.content))
|
|
601
|
+
geometry_json = json.loads(part.content)
|
|
602
|
+
geometries[idx] = geometry_json
|
|
603
|
+
except Exception as e:
|
|
604
|
+
raise RuntimeError(f"Failed to decode geometry for figure ID {idx}") from e
|
|
587
605
|
|
|
588
606
|
if len(geometries) != len(ids):
|
|
589
607
|
raise RuntimeError("Not all geometries were downloaded")
|
|
@@ -854,6 +872,8 @@ class FigureApi(RemoveableBulkModuleApi):
|
|
|
854
872
|
ApiField.AREA,
|
|
855
873
|
ApiField.PRIORITY,
|
|
856
874
|
ApiField.CUSTOM_DATA,
|
|
875
|
+
ApiField.NN_CREATED,
|
|
876
|
+
ApiField.NN_UPDATED,
|
|
857
877
|
]
|
|
858
878
|
if skip_geometry is True:
|
|
859
879
|
fields = [x for x in fields if x != ApiField.GEOMETRY]
|
|
@@ -1026,3 +1046,31 @@ class FigureApi(RemoveableBulkModuleApi):
|
|
|
1026
1046
|
image_ids=image_ids,
|
|
1027
1047
|
skip_geometry=skip_geometry,
|
|
1028
1048
|
)
|
|
1049
|
+
|
|
1050
|
+
def restore_batch(self, ids: List[int], progress_cb: Optional[Callable] = None, batch_size: int = 50):
|
|
1051
|
+
"""
|
|
1052
|
+
Restore archived figures in batches from the Supervisely server.
|
|
1053
|
+
|
|
1054
|
+
:param ids: IDs of figures in Supervisely.
|
|
1055
|
+
:type ids: List[int]
|
|
1056
|
+
:param progress_cb: Optional callback to track restore progress. Receives number of restored figures in the current batch.
|
|
1057
|
+
:type progress_cb: Optional[Callable]
|
|
1058
|
+
:param batch_size: Number of figure IDs to send in a single request.
|
|
1059
|
+
:type batch_size: int
|
|
1060
|
+
"""
|
|
1061
|
+
for ids_batch in batched(ids, batch_size=batch_size):
|
|
1062
|
+
self._api.post(
|
|
1063
|
+
"figures.bulk.restore",
|
|
1064
|
+
{ApiField.FIGURE_IDS: ids_batch},
|
|
1065
|
+
)
|
|
1066
|
+
if progress_cb is not None:
|
|
1067
|
+
progress_cb(len(ids_batch))
|
|
1068
|
+
|
|
1069
|
+
def restore(self, id: int):
|
|
1070
|
+
"""
|
|
1071
|
+
Restore a single archived figure with the specified ID from the Supervisely server.
|
|
1072
|
+
|
|
1073
|
+
:param id: Figure ID in Supervisely.
|
|
1074
|
+
:type id: int
|
|
1075
|
+
"""
|
|
1076
|
+
self.restore_batch([id])
|
|
@@ -214,6 +214,7 @@ class ObjectApi(RemoveableBulkModuleApi):
|
|
|
214
214
|
objects,
|
|
215
215
|
key_id_map: KeyIdMap = None,
|
|
216
216
|
is_pointcloud=False,
|
|
217
|
+
is_video_multi_view: bool = False,
|
|
217
218
|
):
|
|
218
219
|
""""""
|
|
219
220
|
if len(objects) == 0:
|
|
@@ -225,8 +226,7 @@ class ObjectApi(RemoveableBulkModuleApi):
|
|
|
225
226
|
for obj in objects:
|
|
226
227
|
new_obj = {ApiField.CLASS_ID: objcls_name_id_map[obj.obj_class.name]}
|
|
227
228
|
|
|
228
|
-
if not is_pointcloud:
|
|
229
|
-
# if entity_id is not None:
|
|
229
|
+
if not is_video_multi_view and not is_pointcloud:
|
|
230
230
|
new_obj[ApiField.ENTITY_ID] = entity_id
|
|
231
231
|
items.append(new_obj)
|
|
232
232
|
|
|
@@ -238,7 +238,7 @@ class ObjectApi(RemoveableBulkModuleApi):
|
|
|
238
238
|
KeyIdMap.add_objects_to(key_id_map, [obj.key() for obj in objects], ids)
|
|
239
239
|
|
|
240
240
|
# add tags to objects
|
|
241
|
-
tag_api.append_to_objects(entity_id, project_id, objects, key_id_map)
|
|
241
|
+
tag_api.append_to_objects(entity_id, project_id, objects, key_id_map, is_video_multi_view)
|
|
242
242
|
|
|
243
243
|
return ids
|
|
244
244
|
|
|
@@ -5,6 +5,8 @@ from typing import Any, Dict, List, Optional, Union
|
|
|
5
5
|
from supervisely._utils import batched
|
|
6
6
|
from supervisely.api.module_api import ApiField, ModuleApi
|
|
7
7
|
from supervisely.collection.key_indexed_collection import KeyIndexedCollection
|
|
8
|
+
from supervisely.project.project_meta import ProjectMeta
|
|
9
|
+
from supervisely.project.project_settings import LabelingInterface
|
|
8
10
|
from supervisely.task.progress import tqdm_sly
|
|
9
11
|
from supervisely.video_annotation.key_id_map import KeyIdMap
|
|
10
12
|
|
|
@@ -157,7 +159,12 @@ class TagApi(ModuleApi):
|
|
|
157
159
|
return ids
|
|
158
160
|
|
|
159
161
|
def append_to_objects(
|
|
160
|
-
self,
|
|
162
|
+
self,
|
|
163
|
+
entity_id: int,
|
|
164
|
+
project_id: int,
|
|
165
|
+
objects: KeyIndexedCollection,
|
|
166
|
+
key_id_map: KeyIdMap,
|
|
167
|
+
is_video_multi_view: bool = False,
|
|
161
168
|
):
|
|
162
169
|
"""
|
|
163
170
|
Add Tags to Annotation Objects for a specific entity (image etc.).
|
|
@@ -170,6 +177,8 @@ class TagApi(ModuleApi):
|
|
|
170
177
|
:type objects: KeyIndexedCollection
|
|
171
178
|
:param key_id_map: KeyIdMap object.
|
|
172
179
|
:type key_id_map: KeyIdMap
|
|
180
|
+
:param is_video_multi_view: If True, indicates that the entity is a multi-view video.
|
|
181
|
+
:type is_video_multi_view: bool
|
|
173
182
|
:return: List of tags IDs
|
|
174
183
|
:rtype: list
|
|
175
184
|
:Usage example:
|
|
@@ -210,12 +219,16 @@ class TagApi(ModuleApi):
|
|
|
210
219
|
raise RuntimeError("SDK error: len(tags_keys) != len(tags_to_add)")
|
|
211
220
|
if len(tags_keys) == 0:
|
|
212
221
|
return
|
|
213
|
-
ids = self.append_to_objects_json(entity_id, tags_to_add, project_id)
|
|
222
|
+
ids = self.append_to_objects_json(entity_id, tags_to_add, project_id, is_video_multi_view)
|
|
214
223
|
KeyIdMap.add_tags_to(key_id_map, tags_keys, ids)
|
|
215
224
|
return ids
|
|
216
225
|
|
|
217
226
|
def append_to_objects_json(
|
|
218
|
-
self,
|
|
227
|
+
self,
|
|
228
|
+
entity_id: int,
|
|
229
|
+
tags_json: List[Dict],
|
|
230
|
+
project_id: Optional[int] = None,
|
|
231
|
+
is_video_multi_view: bool = False,
|
|
219
232
|
) -> List[int]:
|
|
220
233
|
"""
|
|
221
234
|
Add Tags to Annotation Objects for specific entity (image etc.).
|
|
@@ -224,6 +237,11 @@ class TagApi(ModuleApi):
|
|
|
224
237
|
:type entity_id: int
|
|
225
238
|
:param tags_json: Collection of tags in JSON format
|
|
226
239
|
:type tags_json: dict
|
|
240
|
+
:param project_id: Project ID in Supervisely. Uses to get tag name to tag ID mapping.
|
|
241
|
+
Not required if `multi_view` is True.
|
|
242
|
+
:type project_id: int, optional
|
|
243
|
+
:param is_video_multi_view: If True, indicates that the entity is a multi-view video.
|
|
244
|
+
:type is_video_multi_view: bool
|
|
227
245
|
:return: List of tags IDs
|
|
228
246
|
:rtype: list
|
|
229
247
|
|
|
@@ -262,10 +280,15 @@ class TagApi(ModuleApi):
|
|
|
262
280
|
# 80421103
|
|
263
281
|
# ]
|
|
264
282
|
"""
|
|
283
|
+
project_meta = self._api.optimization_context.get("project_meta")
|
|
284
|
+
|
|
285
|
+
if isinstance(project_meta, ProjectMeta):
|
|
286
|
+
if project_meta.labeling_interface == LabelingInterface.MULTIVIEW:
|
|
287
|
+
is_video_multi_view = True
|
|
265
288
|
|
|
266
289
|
if len(tags_json) == 0:
|
|
267
290
|
return []
|
|
268
|
-
if project_id is not None:
|
|
291
|
+
if project_id is not None and not is_video_multi_view:
|
|
269
292
|
json_data = {ApiField.PROJECT_ID: project_id, ApiField.TAGS: tags_json}
|
|
270
293
|
else:
|
|
271
294
|
json_data = {ApiField.ENTITY_ID: entity_id, ApiField.TAGS: tags_json}
|
|
@@ -280,6 +303,8 @@ class TagApi(ModuleApi):
|
|
|
280
303
|
batch_size: int = 100,
|
|
281
304
|
log_progress: bool = False,
|
|
282
305
|
progress: Optional[tqdm_sly] = None,
|
|
306
|
+
is_video_multi_view: bool = False,
|
|
307
|
+
entity_id: Optional[int] = None,
|
|
283
308
|
) -> List[Dict[str, Union[str, int, None]]]:
|
|
284
309
|
"""
|
|
285
310
|
For images project:
|
|
@@ -306,6 +331,11 @@ class TagApi(ModuleApi):
|
|
|
306
331
|
:type log_progress: bool
|
|
307
332
|
:param progress: Progress bar object to display progress.
|
|
308
333
|
:type progress: Optional[tqdm_sly]
|
|
334
|
+
:param is_video_multi_view: If True, indicates that the entity is a multi-view video.
|
|
335
|
+
:type is_video_multi_view: bool
|
|
336
|
+
:param entity_id: ID of the entity in Supervisely to add a tag to its objects.
|
|
337
|
+
Required if `is_video_multi_view` is True.
|
|
338
|
+
:type entity_id: Optional[int]
|
|
309
339
|
:return: List of tags infos as dictionaries.
|
|
310
340
|
:rtype: List[Dict[str, Union[str, int, None]]]
|
|
311
341
|
|
|
@@ -363,6 +393,12 @@ class TagApi(ModuleApi):
|
|
|
363
393
|
if progress is not None:
|
|
364
394
|
log_progress = False
|
|
365
395
|
|
|
396
|
+
project_meta = self._api.optimization_context.get("project_meta")
|
|
397
|
+
|
|
398
|
+
if isinstance(project_meta, ProjectMeta):
|
|
399
|
+
if project_meta.labeling_interface == LabelingInterface.MULTIVIEW:
|
|
400
|
+
is_video_multi_view = True
|
|
401
|
+
|
|
366
402
|
result = []
|
|
367
403
|
|
|
368
404
|
if len(tags_list) == 0:
|
|
@@ -373,7 +409,12 @@ class TagApi(ModuleApi):
|
|
|
373
409
|
total=len(tags_list),
|
|
374
410
|
)
|
|
375
411
|
for batch in batched(tags_list, batch_size):
|
|
376
|
-
|
|
412
|
+
if is_video_multi_view:
|
|
413
|
+
if entity_id is None:
|
|
414
|
+
raise ValueError("entity_id must be provided when is_video_multi_view is True")
|
|
415
|
+
data = {ApiField.ENTITY_ID: entity_id, ApiField.TAGS: batch}
|
|
416
|
+
else:
|
|
417
|
+
data = {ApiField.PROJECT_ID: project_id, ApiField.TAGS: batch}
|
|
377
418
|
if type(self) is TagApi:
|
|
378
419
|
response = self._api.post("figures.tags.bulk.add", data)
|
|
379
420
|
else:
|
|
@@ -463,6 +504,8 @@ class TagApi(ModuleApi):
|
|
|
463
504
|
tags_map: Dict[int, Any],
|
|
464
505
|
batch_size: int = 100,
|
|
465
506
|
log_progress: bool = False,
|
|
507
|
+
is_video_multi_view: bool = False,
|
|
508
|
+
entity_id: Optional[int] = None,
|
|
466
509
|
) -> List[Dict[str, Union[str, int, None]]]:
|
|
467
510
|
"""
|
|
468
511
|
For images project:
|
|
@@ -483,8 +526,13 @@ class TagApi(ModuleApi):
|
|
|
483
526
|
:type batch_size: int
|
|
484
527
|
:param log_progress: If True, will display a progress bar.
|
|
485
528
|
:type log_progress: bool
|
|
529
|
+
:param is_video_multi_view: If True, indicates that the entity is a multi-view video.
|
|
530
|
+
:type is_video_multi_view: bool
|
|
531
|
+
:param entity_id: ID of the entity in Supervisely to add a tag to its objects.
|
|
532
|
+
Required if `is_video_multi_view` is True.
|
|
533
|
+
:type entity_id: Optional[int]
|
|
486
534
|
:return: List of tags infos as dictionaries.
|
|
487
|
-
:rtype: List[
|
|
535
|
+
:rtype: List[Dict[str, Union[str, int, None]]]
|
|
488
536
|
|
|
489
537
|
Usage example:
|
|
490
538
|
.. code-block:: python
|
|
@@ -527,11 +575,14 @@ class TagApi(ModuleApi):
|
|
|
527
575
|
raise ValueError(f"Tag {tag.name} meta has no sly_id")
|
|
528
576
|
|
|
529
577
|
data.append(
|
|
530
|
-
{
|
|
531
|
-
ApiField.TAG_ID: tag.meta.sly_id,
|
|
532
|
-
OBJ_ID_FIELD: obj_id,
|
|
533
|
-
**tag.to_json()
|
|
534
|
-
}
|
|
578
|
+
{ApiField.TAG_ID: tag.meta.sly_id, OBJ_ID_FIELD: obj_id, **tag.to_json()}
|
|
535
579
|
)
|
|
536
580
|
|
|
537
|
-
return self.add_to_objects(
|
|
581
|
+
return self.add_to_objects(
|
|
582
|
+
project_id,
|
|
583
|
+
data,
|
|
584
|
+
batch_size,
|
|
585
|
+
log_progress,
|
|
586
|
+
is_video_multi_view=is_video_multi_view,
|
|
587
|
+
entity_id=entity_id,
|
|
588
|
+
)
|