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
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Dict, List, Union
|
|
5
|
+
|
|
6
|
+
from supervisely.api.api import Api
|
|
7
|
+
import supervisely.convert.video.sly.sly_video_helper as sly_video_helper
|
|
8
|
+
from supervisely import OpenMode, ProjectMeta, VideoAnnotation, VideoProject, logger
|
|
9
|
+
from supervisely.convert.base_converter import AvailableVideoConverters
|
|
10
|
+
from supervisely.convert.video.video_converter import VideoConverter
|
|
11
|
+
from supervisely.io.fs import JUNK_FILES, file_exists, get_file_ext
|
|
12
|
+
from supervisely.io.json import load_json_file
|
|
13
|
+
from supervisely.project.project import find_project_dirs
|
|
14
|
+
from supervisely.project.project_settings import LabelingInterface
|
|
15
|
+
from supervisely.video.video import has_valid_ext, validate_ext
|
|
16
|
+
|
|
17
|
+
DATASET_ITEMS = "items"
|
|
18
|
+
NESTED_DATASETS = "datasets"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MultiViewVideoConverter(VideoConverter):
|
|
22
|
+
def __init__(self, *args, **kwargs):
|
|
23
|
+
super().__init__(*args, **kwargs)
|
|
24
|
+
self._supports_links = True
|
|
25
|
+
self._project_structure = None
|
|
26
|
+
|
|
27
|
+
def __str__(self) -> str:
|
|
28
|
+
return AvailableVideoConverters.MULTI_VIEW
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def ann_ext(self) -> str:
|
|
32
|
+
return ".json"
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def key_file_ext(self) -> str:
|
|
36
|
+
return ".json"
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def _create_project_node() -> Dict[str, dict]:
|
|
40
|
+
return {DATASET_ITEMS: [], NESTED_DATASETS: {}}
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def _append_to_project_structure(
|
|
44
|
+
cls, project_structure: Dict[str, dict], dataset_name: str, items: list
|
|
45
|
+
):
|
|
46
|
+
normalized_name = (dataset_name or "").replace("\\", "/").strip("/")
|
|
47
|
+
if not normalized_name:
|
|
48
|
+
normalized_name = dataset_name or "dataset"
|
|
49
|
+
parts = [part for part in normalized_name.split("/") if part]
|
|
50
|
+
if not parts:
|
|
51
|
+
parts = ["dataset"]
|
|
52
|
+
|
|
53
|
+
curr_ds = project_structure.setdefault(parts[0], cls._create_project_node())
|
|
54
|
+
for part in parts[1:]:
|
|
55
|
+
curr_ds = curr_ds[NESTED_DATASETS].setdefault(part, cls._create_project_node())
|
|
56
|
+
curr_ds[DATASET_ITEMS].extend(items)
|
|
57
|
+
|
|
58
|
+
def validate_labeling_interface(self) -> bool:
|
|
59
|
+
return self._labeling_interface == LabelingInterface.MULTIVIEW
|
|
60
|
+
|
|
61
|
+
def generate_meta_from_annotation(self, ann_path: str, meta: ProjectMeta) -> ProjectMeta:
|
|
62
|
+
meta = sly_video_helper.get_meta_from_annotation(ann_path, meta)
|
|
63
|
+
return meta
|
|
64
|
+
|
|
65
|
+
def validate_ann_file(self, ann_path: str, meta: ProjectMeta) -> bool:
|
|
66
|
+
try:
|
|
67
|
+
ann_json = load_json_file(ann_path)
|
|
68
|
+
if "annotation" in ann_json:
|
|
69
|
+
ann_json = ann_json["annotation"]
|
|
70
|
+
VideoAnnotation.from_json(ann_json, meta)
|
|
71
|
+
return True
|
|
72
|
+
except Exception:
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
def validate_key_file(self, key_file_path: str) -> bool:
|
|
76
|
+
try:
|
|
77
|
+
self._meta = ProjectMeta.from_json(load_json_file(key_file_path))
|
|
78
|
+
return True
|
|
79
|
+
except Exception:
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
def read_multiview_project(self, input_data: str) -> bool:
|
|
83
|
+
"""Read multi-view video project with multiple datasets."""
|
|
84
|
+
try:
|
|
85
|
+
self._items = []
|
|
86
|
+
project = {}
|
|
87
|
+
ds_cnt = 0
|
|
88
|
+
self._meta = None
|
|
89
|
+
|
|
90
|
+
logger.debug("Trying to find Supervisely video project format in the input data")
|
|
91
|
+
project_dirs = [d for d in find_project_dirs(input_data, project_class=VideoProject)]
|
|
92
|
+
if len(project_dirs) > 1:
|
|
93
|
+
logger.info("Found multiple possible Supervisely video projects in the input data")
|
|
94
|
+
elif len(project_dirs) == 1:
|
|
95
|
+
logger.info("Possible Supervisely video project found in the input data")
|
|
96
|
+
else:
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
meta = None
|
|
100
|
+
for project_dir in project_dirs:
|
|
101
|
+
project_fs = VideoProject(project_dir, mode=OpenMode.READ)
|
|
102
|
+
if meta is None:
|
|
103
|
+
meta = project_fs.meta
|
|
104
|
+
else:
|
|
105
|
+
meta = meta.merge(project_fs.meta)
|
|
106
|
+
|
|
107
|
+
for dataset in project_fs.datasets:
|
|
108
|
+
ds_items = []
|
|
109
|
+
for name in dataset.get_items_names():
|
|
110
|
+
video_path, ann_path = dataset.get_item_paths(name)
|
|
111
|
+
metadata_path = os.path.join(
|
|
112
|
+
dataset.directory, "metadata", f"{name}.meta.json"
|
|
113
|
+
)
|
|
114
|
+
item = self.Item(video_path)
|
|
115
|
+
if file_exists(ann_path):
|
|
116
|
+
if self.validate_ann_file(ann_path, meta):
|
|
117
|
+
item.ann_data = ann_path
|
|
118
|
+
if file_exists(metadata_path):
|
|
119
|
+
item.metadata = metadata_path
|
|
120
|
+
ds_items.append(item)
|
|
121
|
+
|
|
122
|
+
if len(ds_items) > 0:
|
|
123
|
+
self._append_to_project_structure(project, dataset.name, ds_items)
|
|
124
|
+
ds_cnt += 1
|
|
125
|
+
self._items.extend(ds_items)
|
|
126
|
+
|
|
127
|
+
if self.items_count > 0:
|
|
128
|
+
self._meta = meta
|
|
129
|
+
if ds_cnt > 1:
|
|
130
|
+
self._project_structure = project
|
|
131
|
+
return True
|
|
132
|
+
else:
|
|
133
|
+
return False
|
|
134
|
+
except Exception as e:
|
|
135
|
+
logger.debug(f"Not a multi-view video project: {repr(e)}")
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
def read_multiview_dataset(self, input_data: str) -> bool:
|
|
139
|
+
"""Read multi-view video datasets without project meta.json."""
|
|
140
|
+
try:
|
|
141
|
+
from supervisely import VideoDataset
|
|
142
|
+
from supervisely.io.fs import dirs_filter
|
|
143
|
+
|
|
144
|
+
self._items = []
|
|
145
|
+
project = {}
|
|
146
|
+
ds_cnt = 0
|
|
147
|
+
self._meta = None
|
|
148
|
+
logger.debug("Trying to read Supervisely video datasets")
|
|
149
|
+
|
|
150
|
+
def _check_function(path):
|
|
151
|
+
try:
|
|
152
|
+
dataset_ds = VideoDataset(path, OpenMode.READ)
|
|
153
|
+
return len(dataset_ds.get_items_names()) > 0
|
|
154
|
+
except:
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
meta = ProjectMeta()
|
|
158
|
+
dataset_dirs = [d for d in dirs_filter(input_data, _check_function)]
|
|
159
|
+
for dataset_dir in dataset_dirs:
|
|
160
|
+
dataset_fs = VideoDataset(dataset_dir, OpenMode.READ)
|
|
161
|
+
ds_items = []
|
|
162
|
+
for name in dataset_fs.get_items_names():
|
|
163
|
+
video_path, ann_path = dataset_fs.get_item_paths(name)
|
|
164
|
+
metadata_path = os.path.join(
|
|
165
|
+
dataset_fs.directory, "metadata", f"{name}.meta.json"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
item = self.Item(video_path)
|
|
169
|
+
if file_exists(ann_path):
|
|
170
|
+
meta = self.generate_meta_from_annotation(ann_path, meta)
|
|
171
|
+
if self.validate_ann_file(ann_path, meta):
|
|
172
|
+
item.ann_data = ann_path
|
|
173
|
+
if file_exists(metadata_path):
|
|
174
|
+
item.metadata = metadata_path
|
|
175
|
+
ds_items.append(item)
|
|
176
|
+
|
|
177
|
+
if len(ds_items) > 0:
|
|
178
|
+
self._append_to_project_structure(project, dataset_fs.name, ds_items)
|
|
179
|
+
ds_cnt += 1
|
|
180
|
+
self._items.extend(ds_items)
|
|
181
|
+
|
|
182
|
+
if self.items_count > 0:
|
|
183
|
+
self._meta = meta
|
|
184
|
+
if ds_cnt > 1: # multiple datasets
|
|
185
|
+
self._project_structure = project
|
|
186
|
+
return True
|
|
187
|
+
else:
|
|
188
|
+
return False
|
|
189
|
+
except Exception as e:
|
|
190
|
+
logger.debug(f"Failed to read Supervisely video datasets: {repr(e)}")
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
def read_multiview_folder_structure(self, input_data: str) -> bool:
|
|
194
|
+
"""Read multi-view folder layout: <dataset>/video (+optional ann, metadata)."""
|
|
195
|
+
try:
|
|
196
|
+
logger.debug("Trying to read folder-based multi-view structure")
|
|
197
|
+
self._items = []
|
|
198
|
+
project = {}
|
|
199
|
+
ds_cnt = 0
|
|
200
|
+
self._meta = None
|
|
201
|
+
self._project_structure = None
|
|
202
|
+
|
|
203
|
+
has_meta_file = False
|
|
204
|
+
for file in Path(input_data).rglob("meta.json"):
|
|
205
|
+
if file.is_file() and self.validate_key_file(str(file)):
|
|
206
|
+
has_meta_file = True
|
|
207
|
+
break
|
|
208
|
+
meta = self._meta if has_meta_file else ProjectMeta()
|
|
209
|
+
|
|
210
|
+
video_groups = self._find_video_groups(input_data)
|
|
211
|
+
|
|
212
|
+
for dataset_name, video_paths in video_groups.items():
|
|
213
|
+
ds_items = []
|
|
214
|
+
for path in video_paths:
|
|
215
|
+
item = self.Item(path)
|
|
216
|
+
|
|
217
|
+
# check both levels
|
|
218
|
+
possible_ann_dirs = [Path(path).parent.parent, Path(path).parent]
|
|
219
|
+
|
|
220
|
+
for possible_dir in possible_ann_dirs:
|
|
221
|
+
ann_path = possible_dir / "ann" / f"{item.name}.json"
|
|
222
|
+
if not ann_path.exists():
|
|
223
|
+
ann_path = possible_dir / f"{item.name}.json"
|
|
224
|
+
if ann_path.exists():
|
|
225
|
+
if not has_meta_file:
|
|
226
|
+
meta = self.generate_meta_from_annotation(str(ann_path), meta)
|
|
227
|
+
if self.validate_ann_file(str(ann_path), meta):
|
|
228
|
+
item.ann_data = str(ann_path)
|
|
229
|
+
|
|
230
|
+
item_meta = possible_dir / "metadata" / f"{item.name}.meta.json"
|
|
231
|
+
if not item_meta.exists():
|
|
232
|
+
item_meta = possible_dir / f"{item.name}.meta.json"
|
|
233
|
+
if item_meta.exists():
|
|
234
|
+
item.metadata = str(item_meta)
|
|
235
|
+
ds_items.append(item)
|
|
236
|
+
|
|
237
|
+
if len(ds_items) == 0:
|
|
238
|
+
continue
|
|
239
|
+
|
|
240
|
+
self._items.extend(ds_items)
|
|
241
|
+
ds_cnt += 1
|
|
242
|
+
|
|
243
|
+
self._append_to_project_structure(project, dataset_name, ds_items)
|
|
244
|
+
|
|
245
|
+
if self.items_count > 0:
|
|
246
|
+
self._meta = meta
|
|
247
|
+
if ds_cnt > 1:
|
|
248
|
+
self._project_structure = project
|
|
249
|
+
return True
|
|
250
|
+
else:
|
|
251
|
+
return False
|
|
252
|
+
except Exception as e:
|
|
253
|
+
logger.debug(f"Failed to read folder-based multi-view structure: {repr(e)}")
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
def _find_video_groups(self, path) -> Dict[str, List[str]]:
|
|
257
|
+
video_groups = defaultdict(list)
|
|
258
|
+
for file_path in Path(path).rglob("*"):
|
|
259
|
+
if file_path.is_file() and has_valid_ext(str(file_path)):
|
|
260
|
+
video_groups[file_path.parent].append(str(file_path))
|
|
261
|
+
|
|
262
|
+
sanitized = self._sanitize_dataset_names(video_groups)
|
|
263
|
+
return {sanitized[parent]: files for parent, files in video_groups.items()}
|
|
264
|
+
|
|
265
|
+
def _sanitize_dataset_names(self, video_groups: Dict[Path, list]) -> Dict[Path, str]:
|
|
266
|
+
name_counts = defaultdict(int)
|
|
267
|
+
sanitized = {}
|
|
268
|
+
for parent in video_groups:
|
|
269
|
+
base_name = parent.name or "root"
|
|
270
|
+
name_counts[base_name] += 1
|
|
271
|
+
count = name_counts[base_name]
|
|
272
|
+
sanitized[parent] = base_name if count == 1 else f"{base_name}_{count}"
|
|
273
|
+
return sanitized
|
|
274
|
+
|
|
275
|
+
def validate_format(self) -> bool:
|
|
276
|
+
if self.upload_as_links and self._supports_links:
|
|
277
|
+
self._download_remote_ann_files()
|
|
278
|
+
|
|
279
|
+
if self.read_multiview_project(self._input_data):
|
|
280
|
+
return True
|
|
281
|
+
|
|
282
|
+
if self.read_multiview_dataset(self._input_data):
|
|
283
|
+
return True
|
|
284
|
+
|
|
285
|
+
if self.read_multiview_folder_structure(self._input_data):
|
|
286
|
+
return True
|
|
287
|
+
|
|
288
|
+
detected_ann_cnt = 0
|
|
289
|
+
videos_list, ann_dict, meta_dict = [], {}, {}
|
|
290
|
+
for root, _, files in os.walk(self._input_data):
|
|
291
|
+
for file in files:
|
|
292
|
+
full_path = os.path.join(root, file)
|
|
293
|
+
if file == "key_id_map.json":
|
|
294
|
+
continue
|
|
295
|
+
if file == "meta.json":
|
|
296
|
+
is_valid = self.validate_key_file(full_path)
|
|
297
|
+
if is_valid:
|
|
298
|
+
continue
|
|
299
|
+
|
|
300
|
+
ext = get_file_ext(full_path)
|
|
301
|
+
if file in JUNK_FILES:
|
|
302
|
+
continue
|
|
303
|
+
|
|
304
|
+
elif file.endswith(".meta.json"):
|
|
305
|
+
meta_dict[file] = full_path
|
|
306
|
+
elif ext in self.ann_ext:
|
|
307
|
+
ann_dict[file] = full_path
|
|
308
|
+
else:
|
|
309
|
+
try:
|
|
310
|
+
validate_ext(ext) # validate video extension
|
|
311
|
+
videos_list.append(full_path)
|
|
312
|
+
except Exception:
|
|
313
|
+
continue
|
|
314
|
+
|
|
315
|
+
if self._meta is not None:
|
|
316
|
+
meta = self._meta
|
|
317
|
+
else:
|
|
318
|
+
meta = ProjectMeta()
|
|
319
|
+
|
|
320
|
+
self._items = []
|
|
321
|
+
for video_path in videos_list:
|
|
322
|
+
item = self.Item(video_path)
|
|
323
|
+
ann_name = f"{item.name}.json"
|
|
324
|
+
if ann_name in ann_dict:
|
|
325
|
+
ann_path = ann_dict[ann_name]
|
|
326
|
+
if self._meta is None:
|
|
327
|
+
meta = self.generate_meta_from_annotation(ann_path, meta)
|
|
328
|
+
is_valid = self.validate_ann_file(ann_path, meta)
|
|
329
|
+
if is_valid:
|
|
330
|
+
item.ann_data = ann_path
|
|
331
|
+
detected_ann_cnt += 1
|
|
332
|
+
|
|
333
|
+
meta_name = f"{item.name}.meta.json"
|
|
334
|
+
if meta_name in meta_dict:
|
|
335
|
+
meta_path = meta_dict[meta_name]
|
|
336
|
+
item.metadata = meta_path
|
|
337
|
+
|
|
338
|
+
self._items.append(item)
|
|
339
|
+
self._meta = meta
|
|
340
|
+
return len(self._items) > 0
|
|
341
|
+
|
|
342
|
+
def upload_dataset(self, api, dataset_id: int, batch_size: int = 10, log_progress=True):
|
|
343
|
+
"""Upload converted data to Supervisely."""
|
|
344
|
+
if self._project_structure:
|
|
345
|
+
self._upload_project(api, dataset_id, batch_size, log_progress)
|
|
346
|
+
else:
|
|
347
|
+
self._upload_single_dataset(api, dataset_id, self._items, batch_size, log_progress)
|
|
348
|
+
|
|
349
|
+
def _upload_project(self, api, dataset_id: int, batch_size: int = 10, log_progress=True):
|
|
350
|
+
"""Upload multi-view video project with multiple datasets."""
|
|
351
|
+
from supervisely import generate_free_name, is_development
|
|
352
|
+
|
|
353
|
+
dataset_info = api.dataset.get_info_by_id(dataset_id, raise_error=True)
|
|
354
|
+
project_id = dataset_info.project_id
|
|
355
|
+
existing_datasets = api.dataset.get_list(project_id, recursive=True)
|
|
356
|
+
existing_datasets = {ds.name for ds in existing_datasets}
|
|
357
|
+
|
|
358
|
+
if log_progress:
|
|
359
|
+
progress, progress_cb = self.get_progress(self.items_count, "Uploading project")
|
|
360
|
+
else:
|
|
361
|
+
progress, progress_cb = None, None
|
|
362
|
+
|
|
363
|
+
logger.info("Uploading multi-view video project structure")
|
|
364
|
+
|
|
365
|
+
def _upload_datasets_recursive(
|
|
366
|
+
project_structure: dict,
|
|
367
|
+
project_id: int,
|
|
368
|
+
dataset_id: int,
|
|
369
|
+
parent_id=None,
|
|
370
|
+
first_dataset=False,
|
|
371
|
+
):
|
|
372
|
+
for ds_name, value in project_structure.items():
|
|
373
|
+
ds_name = generate_free_name(existing_datasets, ds_name, extend_used_names=True)
|
|
374
|
+
if first_dataset:
|
|
375
|
+
first_dataset = False
|
|
376
|
+
api.dataset.update(dataset_id, ds_name) # rename first dataset
|
|
377
|
+
else:
|
|
378
|
+
dataset_id = api.dataset.create(project_id, ds_name, parent_id=parent_id).id
|
|
379
|
+
|
|
380
|
+
items = value.get(DATASET_ITEMS, [])
|
|
381
|
+
nested_datasets = value.get(NESTED_DATASETS, {})
|
|
382
|
+
logger.info(
|
|
383
|
+
f"Dataset: {ds_name}, items: {len(items)}, nested datasets: {len(nested_datasets)}"
|
|
384
|
+
)
|
|
385
|
+
if items:
|
|
386
|
+
self._upload_single_dataset(
|
|
387
|
+
api,
|
|
388
|
+
dataset_id,
|
|
389
|
+
items,
|
|
390
|
+
batch_size,
|
|
391
|
+
log_progress=False,
|
|
392
|
+
progress_cb=progress_cb,
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
if nested_datasets:
|
|
396
|
+
_upload_datasets_recursive(nested_datasets, project_id, dataset_id, dataset_id)
|
|
397
|
+
|
|
398
|
+
_upload_datasets_recursive(
|
|
399
|
+
self._project_structure, project_id, dataset_id, first_dataset=True
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
if is_development() and progress is not None:
|
|
403
|
+
progress.close()
|
|
404
|
+
|
|
405
|
+
def _upload_single_dataset(
|
|
406
|
+
self,
|
|
407
|
+
api: Api,
|
|
408
|
+
dataset_id: int,
|
|
409
|
+
items: list,
|
|
410
|
+
batch_size: int = 10,
|
|
411
|
+
log_progress=True,
|
|
412
|
+
progress_cb=None,
|
|
413
|
+
):
|
|
414
|
+
"""Upload videos from a single dataset."""
|
|
415
|
+
from supervisely import batched, generate_free_name, is_development
|
|
416
|
+
from supervisely.io.fs import get_file_size
|
|
417
|
+
from supervisely.io.json import load_json_file
|
|
418
|
+
|
|
419
|
+
meta, renamed_classes, renamed_tags = self.merge_metas_with_conflicts(api, dataset_id)
|
|
420
|
+
videos_in_dataset = api.video.get_list(dataset_id, force_metadata_for_links=False)
|
|
421
|
+
existing_names = {video_info.name for video_info in videos_in_dataset}
|
|
422
|
+
items_count = len(items)
|
|
423
|
+
convert_progress, convert_progress_cb = self.get_progress(
|
|
424
|
+
items_count, "Preparing videos..."
|
|
425
|
+
)
|
|
426
|
+
for item in items:
|
|
427
|
+
item_name, item_path = self.convert_to_mp4_if_needed(item.path)
|
|
428
|
+
item.name = item_name
|
|
429
|
+
item.path = item_path
|
|
430
|
+
convert_progress_cb(1)
|
|
431
|
+
if is_development():
|
|
432
|
+
convert_progress.close()
|
|
433
|
+
|
|
434
|
+
has_large_files = False
|
|
435
|
+
size_progress_cb = None
|
|
436
|
+
_progress_cb, progress, ann_progress, ann_progress_cb = None, None, None, None
|
|
437
|
+
if log_progress:
|
|
438
|
+
if progress_cb is None:
|
|
439
|
+
progress, _progress_cb = self.get_progress(items_count, "Uploading videos...")
|
|
440
|
+
else:
|
|
441
|
+
_progress_cb = progress_cb
|
|
442
|
+
if not self.upload_as_links:
|
|
443
|
+
file_sizes = [get_file_size(item.path) for item in items]
|
|
444
|
+
has_large_files = any(
|
|
445
|
+
[self._check_video_file_size(file_size) for file_size in file_sizes]
|
|
446
|
+
)
|
|
447
|
+
if has_large_files:
|
|
448
|
+
upload_progress = []
|
|
449
|
+
size_progress_cb = self._get_video_upload_progress(upload_progress)
|
|
450
|
+
|
|
451
|
+
batch_size = 1 if has_large_files and not self.upload_as_links else batch_size
|
|
452
|
+
for batch in batched(items, batch_size=batch_size):
|
|
453
|
+
item_names = []
|
|
454
|
+
item_paths = []
|
|
455
|
+
item_metas = []
|
|
456
|
+
anns = []
|
|
457
|
+
figures_cnt = 0
|
|
458
|
+
for item in batch:
|
|
459
|
+
item.name = generate_free_name(
|
|
460
|
+
existing_names, item.name, with_ext=True, extend_used_names=True
|
|
461
|
+
)
|
|
462
|
+
item_paths.append(item.path)
|
|
463
|
+
item_names.append(item.name)
|
|
464
|
+
|
|
465
|
+
if isinstance(item.metadata, str): # path to file
|
|
466
|
+
item_metas.append(load_json_file(item.metadata))
|
|
467
|
+
elif isinstance(item.metadata, dict):
|
|
468
|
+
item_metas.append(item.metadata)
|
|
469
|
+
else:
|
|
470
|
+
item_metas.append({})
|
|
471
|
+
|
|
472
|
+
ann = None
|
|
473
|
+
if not self.upload_as_links or self.supports_links:
|
|
474
|
+
ann = self.to_supervisely(item, meta, renamed_classes, renamed_tags)
|
|
475
|
+
if ann is not None:
|
|
476
|
+
figures_cnt += len(ann.figures)
|
|
477
|
+
anns.append(ann)
|
|
478
|
+
|
|
479
|
+
if self.upload_as_links:
|
|
480
|
+
vid_infos = api.video.upload_links(
|
|
481
|
+
dataset_id,
|
|
482
|
+
item_paths,
|
|
483
|
+
item_names,
|
|
484
|
+
metas=item_metas,
|
|
485
|
+
skip_download=True,
|
|
486
|
+
progress_cb=_progress_cb if log_progress else None,
|
|
487
|
+
force_metadata_for_links=False,
|
|
488
|
+
)
|
|
489
|
+
else:
|
|
490
|
+
vid_infos = api.video.upload_paths(
|
|
491
|
+
dataset_id,
|
|
492
|
+
item_names,
|
|
493
|
+
item_paths,
|
|
494
|
+
metas=item_metas,
|
|
495
|
+
progress_cb=_progress_cb if log_progress else None,
|
|
496
|
+
item_progress=(size_progress_cb if log_progress and has_large_files else None),
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
vid_ids = [vid_info.id for vid_info in vid_infos]
|
|
500
|
+
if log_progress and has_large_files and figures_cnt > 0:
|
|
501
|
+
ann_progress, ann_progress_cb = self.get_progress(
|
|
502
|
+
figures_cnt, "Uploading annotations..."
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
for idx, (ann, info) in enumerate(zip(anns, vid_infos)):
|
|
506
|
+
if ann is None:
|
|
507
|
+
anns[idx] = VideoAnnotation(
|
|
508
|
+
(info.frame_height, info.frame_width), info.frames_count
|
|
509
|
+
)
|
|
510
|
+
api.video.annotation.upload_anns_multiview(vid_ids, anns, ann_progress_cb)
|
|
511
|
+
|
|
512
|
+
if log_progress and is_development():
|
|
513
|
+
if progress is not None:
|
|
514
|
+
progress.close()
|
|
515
|
+
if ann_progress is not None:
|
|
516
|
+
ann_progress.close()
|
|
517
|
+
logger.info(f"Dataset ID:{dataset_id} has been successfully uploaded.")
|
|
518
|
+
|
|
519
|
+
def to_supervisely(
|
|
520
|
+
self,
|
|
521
|
+
item: VideoConverter.Item,
|
|
522
|
+
meta: ProjectMeta = None,
|
|
523
|
+
renamed_classes: dict = None,
|
|
524
|
+
renamed_tags: dict = None,
|
|
525
|
+
) -> VideoAnnotation:
|
|
526
|
+
if meta is None:
|
|
527
|
+
meta = self._meta
|
|
528
|
+
|
|
529
|
+
if item.ann_data is None:
|
|
530
|
+
if self._upload_as_links:
|
|
531
|
+
return None
|
|
532
|
+
return item.create_empty_annotation()
|
|
533
|
+
|
|
534
|
+
try:
|
|
535
|
+
ann_json = load_json_file(item.ann_data)
|
|
536
|
+
if "annotation" in ann_json:
|
|
537
|
+
ann_json = ann_json["annotation"]
|
|
538
|
+
if renamed_classes or renamed_tags:
|
|
539
|
+
ann_json = sly_video_helper.rename_in_json(ann_json, renamed_classes, renamed_tags)
|
|
540
|
+
return VideoAnnotation.from_json(ann_json, meta)
|
|
541
|
+
except Exception as e:
|
|
542
|
+
logger.warning(f"Failed to convert annotation: {repr(e)}")
|
|
543
|
+
return item.create_empty_annotation()
|