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,20 +1,29 @@
|
|
|
1
1
|
import os
|
|
2
|
-
from typing import List
|
|
2
|
+
from typing import Dict, List, Optional
|
|
3
3
|
|
|
4
4
|
import supervisely.convert.video.sly.sly_video_helper as sly_video_helper
|
|
5
|
-
from supervisely import
|
|
5
|
+
from supervisely import ProjectMeta, VideoAnnotation, logger
|
|
6
|
+
from supervisely.api.api import Api, ApiContext
|
|
6
7
|
from supervisely.convert.base_converter import AvailableVideoConverters
|
|
7
8
|
from supervisely.convert.video.video_converter import VideoConverter
|
|
8
|
-
from supervisely.io.fs import JUNK_FILES, get_file_ext
|
|
9
|
+
from supervisely.io.fs import JUNK_FILES, file_exists, get_file_ext
|
|
9
10
|
from supervisely.io.json import load_json_file
|
|
11
|
+
from supervisely.project.project import OpenMode, find_project_dirs
|
|
12
|
+
from supervisely.project.project_settings import LabelingInterface
|
|
13
|
+
from supervisely.project.video_project import VideoProject
|
|
10
14
|
from supervisely.video.video import validate_ext as validate_video_ext
|
|
11
15
|
|
|
16
|
+
DATASET_ITEMS = "items"
|
|
17
|
+
NESTED_DATASETS = "datasets"
|
|
18
|
+
|
|
12
19
|
|
|
13
20
|
class SLYVideoConverter(VideoConverter):
|
|
14
21
|
|
|
15
22
|
def __init__(self, *args, **kwargs):
|
|
16
23
|
super().__init__(*args, **kwargs)
|
|
17
24
|
self._supports_links = True
|
|
25
|
+
self._project_structure = None
|
|
26
|
+
self._multi_view_setting_enabled = False
|
|
18
27
|
|
|
19
28
|
def __str__(self) -> str:
|
|
20
29
|
return AvailableVideoConverters.SLY
|
|
@@ -48,9 +57,34 @@ class SLYVideoConverter(VideoConverter):
|
|
|
48
57
|
except Exception:
|
|
49
58
|
return False
|
|
50
59
|
|
|
60
|
+
@staticmethod
|
|
61
|
+
def _create_project_node() -> Dict[str, dict]:
|
|
62
|
+
return {DATASET_ITEMS: [], NESTED_DATASETS: {}}
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def _append_to_project_structure(
|
|
66
|
+
cls, project_structure: Dict[str, dict], dataset_name: str, items: List
|
|
67
|
+
):
|
|
68
|
+
normalized_name = (dataset_name or "").replace("\\", "/").strip("/")
|
|
69
|
+
if not normalized_name:
|
|
70
|
+
normalized_name = dataset_name or "dataset"
|
|
71
|
+
parts = [part for part in normalized_name.split("/") if part]
|
|
72
|
+
if not parts:
|
|
73
|
+
parts = ["dataset"]
|
|
74
|
+
|
|
75
|
+
curr_ds = project_structure.setdefault(parts[0], cls._create_project_node())
|
|
76
|
+
for part in parts[1:]:
|
|
77
|
+
curr_ds = curr_ds[NESTED_DATASETS].setdefault(part, cls._create_project_node())
|
|
78
|
+
curr_ds[DATASET_ITEMS].extend(items)
|
|
79
|
+
|
|
51
80
|
def validate_format(self) -> bool:
|
|
52
81
|
if self.upload_as_links and self._supports_links:
|
|
53
82
|
self._download_remote_ann_files()
|
|
83
|
+
if self.read_project_structure(self._input_data):
|
|
84
|
+
return True
|
|
85
|
+
|
|
86
|
+
if self.read_dataset_structure(self._input_data):
|
|
87
|
+
return True
|
|
54
88
|
detected_ann_cnt = 0
|
|
55
89
|
videos_list, ann_dict = [], {}
|
|
56
90
|
for root, _, files in os.walk(self._input_data):
|
|
@@ -123,3 +157,325 @@ class SLYVideoConverter(VideoConverter):
|
|
|
123
157
|
except Exception as e:
|
|
124
158
|
logger.warning(f"Failed to convert annotation: {repr(e)}")
|
|
125
159
|
return item.create_empty_annotation()
|
|
160
|
+
|
|
161
|
+
def read_project_structure(self, input_data: str) -> bool:
|
|
162
|
+
"""Read video project with multiple datasets."""
|
|
163
|
+
try:
|
|
164
|
+
self._items = []
|
|
165
|
+
project = {}
|
|
166
|
+
ds_cnt = 0
|
|
167
|
+
self._meta = None
|
|
168
|
+
|
|
169
|
+
logger.debug("Trying to find Supervisely video project format in the input data")
|
|
170
|
+
project_dirs = [d for d in find_project_dirs(input_data, project_class=VideoProject)]
|
|
171
|
+
if len(project_dirs) > 1:
|
|
172
|
+
logger.info("Found multiple possible Supervisely video projects in the input data")
|
|
173
|
+
elif len(project_dirs) == 1:
|
|
174
|
+
logger.info("Possible Supervisely video project found in the input data")
|
|
175
|
+
else:
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
meta = None
|
|
179
|
+
for project_dir in project_dirs:
|
|
180
|
+
project_fs = VideoProject(project_dir, mode=OpenMode.READ)
|
|
181
|
+
if meta is None:
|
|
182
|
+
meta = project_fs.meta
|
|
183
|
+
else:
|
|
184
|
+
meta = meta.merge(project_fs.meta)
|
|
185
|
+
|
|
186
|
+
for dataset in project_fs.datasets:
|
|
187
|
+
ds_items = []
|
|
188
|
+
for name in dataset.get_items_names():
|
|
189
|
+
video_path, ann_path = dataset.get_item_paths(name)
|
|
190
|
+
item = self.Item(video_path)
|
|
191
|
+
if file_exists(ann_path):
|
|
192
|
+
if self.validate_ann_file(ann_path, meta):
|
|
193
|
+
item.ann_data = ann_path
|
|
194
|
+
ds_items.append(item)
|
|
195
|
+
|
|
196
|
+
if len(ds_items) > 0:
|
|
197
|
+
self._append_to_project_structure(project, dataset.name, ds_items)
|
|
198
|
+
ds_cnt += 1
|
|
199
|
+
self._items.extend(ds_items)
|
|
200
|
+
|
|
201
|
+
if self.items_count > 0:
|
|
202
|
+
self._meta = meta
|
|
203
|
+
meta: ProjectMeta
|
|
204
|
+
if meta.labeling_interface == LabelingInterface.MULTIVIEW:
|
|
205
|
+
self._multi_view_setting_enabled = True
|
|
206
|
+
if ds_cnt > 1:
|
|
207
|
+
self._project_structure = project
|
|
208
|
+
return True
|
|
209
|
+
else:
|
|
210
|
+
return False
|
|
211
|
+
except Exception as e:
|
|
212
|
+
logger.debug(f"Not a video project: {repr(e)}")
|
|
213
|
+
return False
|
|
214
|
+
|
|
215
|
+
def read_dataset_structure(self, input_data: str) -> bool:
|
|
216
|
+
"""Read video datasets without project meta.json."""
|
|
217
|
+
try:
|
|
218
|
+
from supervisely import VideoDataset
|
|
219
|
+
from supervisely.io.fs import dirs_filter
|
|
220
|
+
|
|
221
|
+
self._items = []
|
|
222
|
+
project = {}
|
|
223
|
+
ds_cnt = 0
|
|
224
|
+
self._meta = None
|
|
225
|
+
logger.debug("Trying to read Supervisely video datasets")
|
|
226
|
+
|
|
227
|
+
def _check_function(path):
|
|
228
|
+
try:
|
|
229
|
+
dataset_ds = VideoDataset(path, OpenMode.READ)
|
|
230
|
+
return len(dataset_ds.get_items_names()) > 0
|
|
231
|
+
except Exception:
|
|
232
|
+
return False
|
|
233
|
+
|
|
234
|
+
meta = ProjectMeta()
|
|
235
|
+
dataset_dirs = [d for d in dirs_filter(input_data, _check_function)]
|
|
236
|
+
for dataset_dir in dataset_dirs:
|
|
237
|
+
dataset_fs = VideoDataset(dataset_dir, OpenMode.READ)
|
|
238
|
+
ds_items = []
|
|
239
|
+
for name in dataset_fs.get_items_names():
|
|
240
|
+
video_path, ann_path = dataset_fs.get_item_paths(name)
|
|
241
|
+
item = self.Item(video_path)
|
|
242
|
+
if file_exists(ann_path):
|
|
243
|
+
meta = self.generate_meta_from_annotation(ann_path, meta)
|
|
244
|
+
if self.validate_ann_file(ann_path, meta):
|
|
245
|
+
item.ann_data = ann_path
|
|
246
|
+
ds_items.append(item)
|
|
247
|
+
|
|
248
|
+
if len(ds_items) > 0:
|
|
249
|
+
self._append_to_project_structure(project, dataset_fs.name, ds_items)
|
|
250
|
+
ds_cnt += 1
|
|
251
|
+
self._items.extend(ds_items)
|
|
252
|
+
|
|
253
|
+
if self.items_count > 0:
|
|
254
|
+
self._meta = meta
|
|
255
|
+
if ds_cnt > 1: # multiple datasets
|
|
256
|
+
self._project_structure = project
|
|
257
|
+
return True
|
|
258
|
+
else:
|
|
259
|
+
return False
|
|
260
|
+
except Exception as e:
|
|
261
|
+
logger.debug(f"Failed to read Supervisely video datasets: {repr(e)}")
|
|
262
|
+
return False
|
|
263
|
+
|
|
264
|
+
def upload_dataset(
|
|
265
|
+
self, api: Api, dataset_id: int, batch_size: int = 10, log_progress=True
|
|
266
|
+
) -> Optional[int]:
|
|
267
|
+
"""Upload converted data to Supervisely."""
|
|
268
|
+
if self._project_structure:
|
|
269
|
+
return self._upload_project(api, dataset_id, batch_size, log_progress)
|
|
270
|
+
else:
|
|
271
|
+
self._upload_single_dataset(api, dataset_id, self._items, batch_size, log_progress)
|
|
272
|
+
|
|
273
|
+
def _upload_project(
|
|
274
|
+
self, api: Api, dataset_id: int, batch_size: int = 10, log_progress=True
|
|
275
|
+
) -> Optional[int]:
|
|
276
|
+
"""Upload video project with multiple datasets."""
|
|
277
|
+
from supervisely import generate_free_name, is_development
|
|
278
|
+
|
|
279
|
+
dataset_info = api.dataset.get_info_by_id(dataset_id, raise_error=True)
|
|
280
|
+
project_id = dataset_info.project_id
|
|
281
|
+
new_project_created = False
|
|
282
|
+
|
|
283
|
+
if self._multi_view_setting_enabled:
|
|
284
|
+
src_meta_json = api.project.get_meta(project_id, with_settings=True)
|
|
285
|
+
src_meta = ProjectMeta.from_json(src_meta_json)
|
|
286
|
+
|
|
287
|
+
if src_meta.labeling_interface == LabelingInterface.DEFAULT:
|
|
288
|
+
project_id, dataset_id = self._handle_multi_view_labeling_interface(
|
|
289
|
+
api, project_id, dataset_info
|
|
290
|
+
)
|
|
291
|
+
new_project_created = True
|
|
292
|
+
|
|
293
|
+
existing_datasets = api.dataset.get_list(project_id, recursive=True)
|
|
294
|
+
existing_datasets = {ds.name for ds in existing_datasets}
|
|
295
|
+
|
|
296
|
+
if log_progress:
|
|
297
|
+
progress, progress_cb = self.get_progress(self.items_count, "Uploading project")
|
|
298
|
+
else:
|
|
299
|
+
progress, progress_cb = None, None
|
|
300
|
+
|
|
301
|
+
logger.info("Uploading video project structure")
|
|
302
|
+
|
|
303
|
+
def _upload_datasets_recursive(
|
|
304
|
+
project_structure: dict,
|
|
305
|
+
project_id: int,
|
|
306
|
+
dataset_id: int,
|
|
307
|
+
parent_id=None,
|
|
308
|
+
first_dataset=False,
|
|
309
|
+
):
|
|
310
|
+
for ds_name, value in project_structure.items():
|
|
311
|
+
ds_name = generate_free_name(existing_datasets, ds_name, extend_used_names=True)
|
|
312
|
+
if first_dataset:
|
|
313
|
+
first_dataset = False
|
|
314
|
+
api.dataset.update(dataset_id, ds_name) # rename first dataset
|
|
315
|
+
else:
|
|
316
|
+
dataset_id = api.dataset.create(project_id, ds_name, parent_id=parent_id).id
|
|
317
|
+
|
|
318
|
+
items = value.get(DATASET_ITEMS, [])
|
|
319
|
+
nested_datasets = value.get(NESTED_DATASETS, {})
|
|
320
|
+
logger.info(
|
|
321
|
+
f"Dataset: {ds_name}, items: {len(items)}, nested datasets: {len(nested_datasets)}"
|
|
322
|
+
)
|
|
323
|
+
if items:
|
|
324
|
+
self._upload_single_dataset(
|
|
325
|
+
api,
|
|
326
|
+
dataset_id,
|
|
327
|
+
items,
|
|
328
|
+
batch_size,
|
|
329
|
+
log_progress=False,
|
|
330
|
+
progress_cb=progress_cb,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
if nested_datasets:
|
|
334
|
+
_upload_datasets_recursive(nested_datasets, project_id, dataset_id, dataset_id)
|
|
335
|
+
|
|
336
|
+
_upload_datasets_recursive(
|
|
337
|
+
self._project_structure, project_id, dataset_id, first_dataset=True
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
if is_development() and progress is not None:
|
|
341
|
+
progress.close()
|
|
342
|
+
|
|
343
|
+
if new_project_created:
|
|
344
|
+
logger.info(
|
|
345
|
+
"Data was uploaded to a new project with 'Multi-View' labeling interface setting."
|
|
346
|
+
)
|
|
347
|
+
return dataset_id
|
|
348
|
+
|
|
349
|
+
def _upload_single_dataset(
|
|
350
|
+
self,
|
|
351
|
+
api: Api,
|
|
352
|
+
dataset_id: int,
|
|
353
|
+
items: List,
|
|
354
|
+
batch_size: int = 10,
|
|
355
|
+
log_progress=True,
|
|
356
|
+
progress_cb=None,
|
|
357
|
+
):
|
|
358
|
+
"""Upload videos from a single dataset."""
|
|
359
|
+
from supervisely import batched, generate_free_name, is_development
|
|
360
|
+
from supervisely.io.fs import get_file_size
|
|
361
|
+
|
|
362
|
+
meta, renamed_classes, renamed_tags = self.merge_metas_with_conflicts(api, dataset_id)
|
|
363
|
+
videos_in_dataset = api.video.get_list(dataset_id, force_metadata_for_links=False)
|
|
364
|
+
existing_names = {video_info.name for video_info in videos_in_dataset}
|
|
365
|
+
items_count = len(items)
|
|
366
|
+
convert_progress, convert_progress_cb = self.get_progress(
|
|
367
|
+
items_count, "Preparing videos..."
|
|
368
|
+
)
|
|
369
|
+
for item in items:
|
|
370
|
+
item_name, item_path = self.convert_to_mp4_if_needed(item.path)
|
|
371
|
+
item.name = item_name
|
|
372
|
+
item.path = item_path
|
|
373
|
+
convert_progress_cb(1)
|
|
374
|
+
if is_development():
|
|
375
|
+
convert_progress.close()
|
|
376
|
+
|
|
377
|
+
has_large_files = False
|
|
378
|
+
size_progress_cb = None
|
|
379
|
+
_progress_cb, progress, ann_progress, ann_progress_cb = None, None, None, None
|
|
380
|
+
if log_progress:
|
|
381
|
+
if progress_cb is None:
|
|
382
|
+
progress, _progress_cb = self.get_progress(items_count, "Uploading videos...")
|
|
383
|
+
else:
|
|
384
|
+
_progress_cb = progress_cb
|
|
385
|
+
if not self.upload_as_links:
|
|
386
|
+
file_sizes = [get_file_size(item.path) for item in items]
|
|
387
|
+
has_large_files = any(
|
|
388
|
+
[self._check_video_file_size(file_size) for file_size in file_sizes]
|
|
389
|
+
)
|
|
390
|
+
if has_large_files:
|
|
391
|
+
upload_progress = []
|
|
392
|
+
size_progress_cb = self._get_video_upload_progress(upload_progress)
|
|
393
|
+
|
|
394
|
+
with ApiContext(api=api, project_meta=meta):
|
|
395
|
+
batch_size = 1 if has_large_files and not self.upload_as_links else batch_size
|
|
396
|
+
for batch in batched(items, batch_size=batch_size):
|
|
397
|
+
item_names = []
|
|
398
|
+
item_paths = []
|
|
399
|
+
anns = []
|
|
400
|
+
figures_cnt = 0
|
|
401
|
+
for item in batch:
|
|
402
|
+
item.name = generate_free_name(
|
|
403
|
+
existing_names, item.name, with_ext=True, extend_used_names=True
|
|
404
|
+
)
|
|
405
|
+
item_paths.append(item.path)
|
|
406
|
+
item_names.append(item.name)
|
|
407
|
+
|
|
408
|
+
ann = None
|
|
409
|
+
if not self.upload_as_links or self.supports_links:
|
|
410
|
+
ann = self.to_supervisely(item, meta, renamed_classes, renamed_tags)
|
|
411
|
+
if ann is not None:
|
|
412
|
+
figures_cnt += len(ann.figures)
|
|
413
|
+
anns.append(ann)
|
|
414
|
+
|
|
415
|
+
if self.upload_as_links:
|
|
416
|
+
vid_infos = api.video.upload_links(
|
|
417
|
+
dataset_id,
|
|
418
|
+
item_paths,
|
|
419
|
+
item_names,
|
|
420
|
+
skip_download=True,
|
|
421
|
+
progress_cb=_progress_cb if log_progress else None,
|
|
422
|
+
force_metadata_for_links=False,
|
|
423
|
+
)
|
|
424
|
+
else:
|
|
425
|
+
vid_infos = api.video.upload_paths(
|
|
426
|
+
dataset_id,
|
|
427
|
+
item_names,
|
|
428
|
+
item_paths,
|
|
429
|
+
progress_cb=_progress_cb if log_progress else None,
|
|
430
|
+
item_progress=(
|
|
431
|
+
size_progress_cb if log_progress and has_large_files else None
|
|
432
|
+
),
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
vid_ids = [vid_info.id for vid_info in vid_infos]
|
|
436
|
+
if log_progress and has_large_files and figures_cnt > 0:
|
|
437
|
+
ann_progress, ann_progress_cb = self.get_progress(
|
|
438
|
+
figures_cnt, "Uploading annotations..."
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
if meta.labeling_interface == LabelingInterface.MULTIVIEW:
|
|
442
|
+
for idx, (ann, info) in enumerate(zip(anns, vid_infos)):
|
|
443
|
+
if ann is None:
|
|
444
|
+
anns[idx] = VideoAnnotation(
|
|
445
|
+
(info.frame_height, info.frame_width), info.frames_count
|
|
446
|
+
)
|
|
447
|
+
api.video.annotation.upload_anns_multiview(vid_ids, anns, ann_progress_cb)
|
|
448
|
+
else:
|
|
449
|
+
for vid, ann, info in zip(vid_ids, anns, vid_infos):
|
|
450
|
+
if ann is None:
|
|
451
|
+
ann = VideoAnnotation(
|
|
452
|
+
(info.frame_height, info.frame_width), info.frames_count
|
|
453
|
+
)
|
|
454
|
+
api.video.annotation.append(vid, ann, progress_cb=ann_progress_cb)
|
|
455
|
+
|
|
456
|
+
if log_progress and is_development():
|
|
457
|
+
if progress is not None:
|
|
458
|
+
progress.close()
|
|
459
|
+
if ann_progress is not None:
|
|
460
|
+
ann_progress.close()
|
|
461
|
+
logger.info(f"Dataset ID:{dataset_id} has been successfully uploaded.")
|
|
462
|
+
|
|
463
|
+
def _handle_multi_view_labeling_interface(self, api: Api, project_id: int, dataset_info):
|
|
464
|
+
project_info = api.project.get_info_by_id(project_id)
|
|
465
|
+
if project_info.items_count == 0:
|
|
466
|
+
return project_id, dataset_info.id
|
|
467
|
+
logger.warning(
|
|
468
|
+
"The uploaded project has 'Multi-View' labeling interface setting enabled, "
|
|
469
|
+
"but the target project has 'Default' labeling interface. "
|
|
470
|
+
)
|
|
471
|
+
logger.warning("New project with 'Multi-View' labeling interface will be created.")
|
|
472
|
+
new_project = api.project.create(
|
|
473
|
+
workspace_id=project_info.workspace_id,
|
|
474
|
+
name=f"{project_info.name}_multi_view",
|
|
475
|
+
type=project_info.type,
|
|
476
|
+
change_name_if_conflict=True,
|
|
477
|
+
)
|
|
478
|
+
new_dataset = api.dataset.create(
|
|
479
|
+
new_project.id, dataset_info.name, change_name_if_conflict=True
|
|
480
|
+
)
|
|
481
|
+
return new_project.id, new_dataset.id
|
|
@@ -40,10 +40,12 @@ class VideoConverter(BaseConverter):
|
|
|
40
40
|
shape=None,
|
|
41
41
|
custom_data=None,
|
|
42
42
|
frame_count=None,
|
|
43
|
+
metadata=None,
|
|
43
44
|
):
|
|
44
45
|
self._path = item_path
|
|
45
46
|
self._name: str = None
|
|
46
47
|
self._ann_data = ann_data
|
|
48
|
+
self._metadata = metadata
|
|
47
49
|
self._type = "video"
|
|
48
50
|
if shape is None:
|
|
49
51
|
vcap = cv2.VideoCapture(item_path)
|
|
@@ -83,6 +85,14 @@ class VideoConverter(BaseConverter):
|
|
|
83
85
|
def name(self, name: str):
|
|
84
86
|
self._name = name
|
|
85
87
|
|
|
88
|
+
@property
|
|
89
|
+
def metadata(self) -> Optional[str]:
|
|
90
|
+
return self._metadata
|
|
91
|
+
|
|
92
|
+
@metadata.setter
|
|
93
|
+
def metadata(self, metadata: Optional[str]):
|
|
94
|
+
self._metadata = metadata
|
|
95
|
+
|
|
86
96
|
def create_empty_annotation(self) -> VideoAnnotation:
|
|
87
97
|
return VideoAnnotation(self._shape, self._frame_count)
|
|
88
98
|
|
|
@@ -120,9 +130,7 @@ class VideoConverter(BaseConverter):
|
|
|
120
130
|
log_progress=True,
|
|
121
131
|
):
|
|
122
132
|
"""Upload converted data to Supervisely"""
|
|
123
|
-
|
|
124
133
|
meta, renamed_classes, renamed_tags = self.merge_metas_with_conflicts(api, dataset_id)
|
|
125
|
-
|
|
126
134
|
videos_in_dataset = api.video.get_list(dataset_id, force_metadata_for_links=False)
|
|
127
135
|
existing_names = {video_info.name for video_info in videos_in_dataset}
|
|
128
136
|
|
|
@@ -156,6 +164,7 @@ class VideoConverter(BaseConverter):
|
|
|
156
164
|
for batch in batched(self._items, batch_size=batch_size):
|
|
157
165
|
item_names = []
|
|
158
166
|
item_paths = []
|
|
167
|
+
item_metas = []
|
|
159
168
|
anns = []
|
|
160
169
|
figures_cnt = 0
|
|
161
170
|
for item in batch:
|
|
@@ -165,6 +174,15 @@ class VideoConverter(BaseConverter):
|
|
|
165
174
|
item_paths.append(item.path)
|
|
166
175
|
item_names.append(item.name)
|
|
167
176
|
|
|
177
|
+
if isinstance(item.metadata, str): # path to file
|
|
178
|
+
from supervisely.io.json import load_json_file
|
|
179
|
+
|
|
180
|
+
item_metas.append(load_json_file(item.metadata))
|
|
181
|
+
elif isinstance(item.metadata, dict):
|
|
182
|
+
item_metas.append(item.metadata)
|
|
183
|
+
else:
|
|
184
|
+
item_metas.append({})
|
|
185
|
+
|
|
168
186
|
ann = None
|
|
169
187
|
if not self.upload_as_links or self.supports_links:
|
|
170
188
|
ann = self.to_supervisely(item, meta, renamed_classes, renamed_tags)
|
|
@@ -177,6 +195,7 @@ class VideoConverter(BaseConverter):
|
|
|
177
195
|
dataset_id,
|
|
178
196
|
item_paths,
|
|
179
197
|
item_names,
|
|
198
|
+
metas=item_metas,
|
|
180
199
|
skip_download=True,
|
|
181
200
|
progress_cb=progress_cb if log_progress else None,
|
|
182
201
|
force_metadata_for_links=False,
|
|
@@ -188,6 +207,7 @@ class VideoConverter(BaseConverter):
|
|
|
188
207
|
item_paths,
|
|
189
208
|
progress_cb=progress_cb if log_progress else None,
|
|
190
209
|
item_progress=(size_progress_cb if log_progress and has_large_files else None),
|
|
210
|
+
metas=item_metas,
|
|
191
211
|
)
|
|
192
212
|
vid_ids = [vid_info.id for vid_info in vid_infos]
|
|
193
213
|
|
|
@@ -266,8 +286,8 @@ class VideoConverter(BaseConverter):
|
|
|
266
286
|
if codec_type not in ["video", "audio"]:
|
|
267
287
|
continue
|
|
268
288
|
codec_name = stream["codecName"]
|
|
269
|
-
if codec_type == "video" and codec_name
|
|
270
|
-
logger.info(f"Video codec is not h264, transcoding is required: {codec_name}")
|
|
289
|
+
if codec_type == "video" and codec_name not in ["h264", "h265", "hevc", "av1"]:
|
|
290
|
+
logger.info(f"Video codec is not h264/h265/hevc/av1, transcoding is required: {codec_name}")
|
|
271
291
|
need_video_transc = True
|
|
272
292
|
elif codec_type == "audio" and codec_name != "aac":
|
|
273
293
|
logger.info(f"Audio codec is not aac, transcoding is required: {codec_name}")
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
from typing import List
|
|
2
2
|
|
|
3
|
+
from supervisely import ProjectMeta, generate_free_name, logger
|
|
3
4
|
from supervisely.api.api import Api
|
|
4
|
-
from supervisely import generate_free_name, logger, ProjectMeta
|
|
5
5
|
from supervisely.convert.base_converter import AvailableVolumeConverters
|
|
6
|
-
from supervisely.convert.volume.volume_converter import VolumeConverter
|
|
7
6
|
from supervisely.convert.volume.dicom import dicom_helper as h
|
|
8
|
-
from supervisely.volume.
|
|
7
|
+
from supervisely.convert.volume.volume_converter import VolumeConverter
|
|
8
|
+
from supervisely.volume.volume import (
|
|
9
|
+
get_extension,
|
|
10
|
+
inspect_dicom_series,
|
|
11
|
+
read_dicom_serie_volume,
|
|
12
|
+
)
|
|
13
|
+
|
|
9
14
|
|
|
10
15
|
class DICOMConverter(VolumeConverter):
|
|
11
16
|
class Item(VolumeConverter.Item):
|
|
12
17
|
"""Item class for DICOM series."""
|
|
18
|
+
|
|
13
19
|
def __init__(self, serie_id: str, item_paths: List[str], volume_meta: dict):
|
|
14
20
|
item_path = item_paths[0] if len(item_paths) > 0 else None
|
|
15
21
|
super().__init__(item_path, volume_meta=volume_meta)
|
|
@@ -62,9 +68,11 @@ class DICOMConverter(VolumeConverter):
|
|
|
62
68
|
continue
|
|
63
69
|
|
|
64
70
|
for dicom_path in dicom_paths:
|
|
65
|
-
h.
|
|
71
|
+
h.read_and_convert_to_monochrome2(dicom_path)
|
|
66
72
|
_, meta = read_dicom_serie_volume(dicom_paths, anonymize=True)
|
|
67
|
-
item = self.Item(
|
|
73
|
+
item = self.Item(
|
|
74
|
+
serie_id=dicom_id, item_paths=dicom_paths, volume_meta=meta
|
|
75
|
+
)
|
|
68
76
|
self._items.append(item)
|
|
69
77
|
self._meta = ProjectMeta()
|
|
70
78
|
|
|
@@ -3,7 +3,7 @@ from typing import List
|
|
|
3
3
|
|
|
4
4
|
import nrrd
|
|
5
5
|
import numpy as np
|
|
6
|
-
|
|
6
|
+
from pydicom import FileDataset
|
|
7
7
|
from supervisely import logger
|
|
8
8
|
from supervisely.io.fs import file_exists
|
|
9
9
|
from supervisely.volume.volume import read_dicom_serie_volume_np
|
|
@@ -38,31 +38,43 @@ def dcm_to_nrrd(id: str, paths: List[str]) -> str:
|
|
|
38
38
|
|
|
39
39
|
return nrrd_path, volume_meta
|
|
40
40
|
|
|
41
|
-
def convert_to_monochrome2(dcm_path: str):
|
|
42
|
-
import pydicom
|
|
43
41
|
|
|
44
|
-
|
|
42
|
+
def read_and_convert_to_monochrome2(dcm_path: str):
|
|
43
|
+
import pydicom
|
|
45
44
|
|
|
46
45
|
try:
|
|
47
46
|
dcm = pydicom.dcmread(dcm_path)
|
|
48
47
|
except Exception as e:
|
|
49
|
-
logger.
|
|
48
|
+
logger.warning("Failed to read DICOM file: " + str(e))
|
|
50
49
|
return
|
|
51
50
|
|
|
52
51
|
try:
|
|
53
52
|
if dcm.file_meta.TransferSyntaxUID.is_compressed:
|
|
54
53
|
dcm.decompress()
|
|
55
|
-
is_modified = True
|
|
56
54
|
except Exception as e:
|
|
57
|
-
logger.
|
|
55
|
+
logger.warning("Failed to decompress DICOM file: " + str(e))
|
|
58
56
|
return
|
|
59
57
|
|
|
58
|
+
convert_to_monochrome2(dcm_path, dcm)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def convert_to_monochrome2(dcm_path: str, dcm: FileDataset) -> FileDataset:
|
|
60
62
|
if getattr(dcm, "PhotometricInterpretation", None) == "YBR_FULL_422":
|
|
61
63
|
# * Convert dicom to monochrome
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
monochrome = None
|
|
65
|
+
pixel_array = dcm.pixel_array
|
|
66
|
+
|
|
67
|
+
if len(pixel_array.shape) == 4 and pixel_array.shape[-1] == 3:
|
|
68
|
+
monochrome = pixel_array[..., 0].astype(np.uint8)
|
|
69
|
+
elif len(pixel_array.shape) == 3 and pixel_array.shape[-1] == 3:
|
|
70
|
+
monochrome = pixel_array[..., 0].astype(np.uint8)
|
|
64
71
|
else:
|
|
65
|
-
logger.
|
|
72
|
+
logger.warning(
|
|
73
|
+
"Unexpected shape for YBR_FULL_422 data: " + str(pixel_array.shape)
|
|
74
|
+
)
|
|
75
|
+
return dcm
|
|
76
|
+
|
|
77
|
+
logger.debug("Monochrome shape: " + str(monochrome.shape))
|
|
66
78
|
|
|
67
79
|
try:
|
|
68
80
|
dcm.SamplesPerPixel = 1
|
|
@@ -71,15 +83,15 @@ def convert_to_monochrome2(dcm_path: str):
|
|
|
71
83
|
if len(monochrome.shape) == 3:
|
|
72
84
|
dcm.NumberOfFrames = str(monochrome.shape[0])
|
|
73
85
|
dcm.Rows, dcm.Columns = monochrome.shape[1:3]
|
|
86
|
+
elif len(monochrome.shape) == 2:
|
|
87
|
+
dcm.Rows, dcm.Columns = monochrome.shape[0:2]
|
|
88
|
+
if hasattr(dcm, "NumberOfFrames"):
|
|
89
|
+
delattr(dcm, "NumberOfFrames")
|
|
74
90
|
dcm.PixelData = monochrome.tobytes()
|
|
75
91
|
except AttributeError as ae:
|
|
76
92
|
logger.error(f"Error occurred while converting dicom to monochrome: {ae}")
|
|
93
|
+
return dcm
|
|
77
94
|
|
|
78
|
-
logger.info("Rewriting DICOM file with
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
try:
|
|
82
|
-
if is_modified:
|
|
83
|
-
dcm.save_as(dcm_path)
|
|
84
|
-
except Exception as e:
|
|
85
|
-
logger.warn("Failed to save DICOM file: " + str(e))
|
|
95
|
+
logger.info("Rewriting DICOM file with monochrome2 format")
|
|
96
|
+
dcm.save_as(dcm_path)
|
|
97
|
+
return dcm
|
supervisely/geometry/geometry.py
CHANGED
|
@@ -272,6 +272,7 @@ class Geometry(JsonSerializable):
|
|
|
272
272
|
)
|
|
273
273
|
from supervisely.geometry.polygon import Polygon
|
|
274
274
|
from supervisely.geometry.rectangle import Rectangle
|
|
275
|
+
from supervisely.geometry.oriented_bbox import OrientedBBox
|
|
275
276
|
|
|
276
277
|
res = []
|
|
277
278
|
if new_geometry == Bitmap:
|
|
@@ -282,6 +283,9 @@ class Geometry(JsonSerializable):
|
|
|
282
283
|
res = [self.to_bbox()]
|
|
283
284
|
elif new_geometry == Polygon:
|
|
284
285
|
res = geometry_to_polygon(self, approx_epsilon=approx_epsilon)
|
|
286
|
+
elif new_geometry == OrientedBBox:
|
|
287
|
+
bbox = self.to_bbox()
|
|
288
|
+
res = [OrientedBBox.from_bbox(bbox)]
|
|
285
289
|
|
|
286
290
|
if len(res) == 0:
|
|
287
291
|
logger.warn(
|
supervisely/geometry/helpers.py
CHANGED
|
@@ -13,6 +13,7 @@ from supervisely.geometry.point_location import PointLocation
|
|
|
13
13
|
from supervisely.geometry.polygon import Polygon
|
|
14
14
|
from supervisely.geometry.polyline import Polyline
|
|
15
15
|
from supervisely.geometry.rectangle import Rectangle
|
|
16
|
+
from supervisely.geometry.oriented_bbox import OrientedBBox
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
def _geometry_to_mask_base(
|
|
@@ -122,7 +123,7 @@ def deserialize_geometry(geometry_type_str: str, geometry_json: Dict) -> Geometr
|
|
|
122
123
|
|
|
123
124
|
|
|
124
125
|
def geometry_to_polygon(geometry: Geometry, approx_epsilon: Optional[int] = None) -> List[Geometry]:
|
|
125
|
-
if type(geometry) not in (Rectangle, Polyline, Polygon, Bitmap, AlphaMask):
|
|
126
|
+
if type(geometry) not in (Rectangle, Polyline, Polygon, Bitmap, AlphaMask, OrientedBBox):
|
|
126
127
|
raise KeyError(
|
|
127
128
|
"Can not convert {} to {}".format(geometry.geometry_name(), Polygon.__name__)
|
|
128
129
|
)
|
|
@@ -135,6 +136,9 @@ def geometry_to_polygon(geometry: Geometry, approx_epsilon: Optional[int] = None
|
|
|
135
136
|
|
|
136
137
|
if type(geometry) == Polygon:
|
|
137
138
|
return [geometry]
|
|
139
|
+
|
|
140
|
+
if type(geometry) == OrientedBBox:
|
|
141
|
+
return [Polygon(geometry.calculate_rotated_corners(), [])]
|
|
138
142
|
|
|
139
143
|
if type(geometry) in [AlphaMask, Bitmap]:
|
|
140
144
|
new_geometries = geometry.to_contours()
|