supervisely 6.73.393__py3-none-any.whl → 6.73.394__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/api/entity_annotation/entity_annotation_api.py +3 -1
- supervisely/api/entity_annotation/figure_api.py +25 -16
- supervisely/api/module_api.py +2 -0
- supervisely/api/volume/volume_annotation_api.py +4 -2
- supervisely/api/volume/volume_figure_api.py +36 -7
- supervisely/convert/base_converter.py +2 -2
- supervisely/convert/volume/nii/nii_planes_volume_converter.py +51 -13
- supervisely/convert/volume/nii/nii_volume_converter.py +1 -1
- supervisely/convert/volume/nii/nii_volume_helper.py +96 -36
- supervisely/convert/volume/sly/sly_volume_converter.py +32 -3
- supervisely/project/volume_project.py +6 -0
- supervisely/volume_annotation/volume_figure.py +45 -1
- supervisely/volume_annotation/volume_object.py +23 -6
- {supervisely-6.73.393.dist-info → supervisely-6.73.394.dist-info}/METADATA +1 -1
- {supervisely-6.73.393.dist-info → supervisely-6.73.394.dist-info}/RECORD +19 -19
- {supervisely-6.73.393.dist-info → supervisely-6.73.394.dist-info}/LICENSE +0 -0
- {supervisely-6.73.393.dist-info → supervisely-6.73.394.dist-info}/WHEEL +0 -0
- {supervisely-6.73.393.dist-info → supervisely-6.73.394.dist-info}/entry_points.txt +0 -0
- {supervisely-6.73.393.dist-info → supervisely-6.73.394.dist-info}/top_level.txt +0 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# coding: utf-8
|
|
2
2
|
|
|
3
3
|
# docs
|
|
4
|
+
from typing import Callable, Dict, List, Optional, Union
|
|
5
|
+
|
|
4
6
|
from tqdm import tqdm
|
|
5
|
-
|
|
7
|
+
|
|
6
8
|
from supervisely._utils import batched
|
|
7
9
|
from supervisely.api.module_api import ApiField, ModuleApi
|
|
8
10
|
from supervisely.video_annotation.key_id_map import KeyIdMap
|
|
@@ -69,6 +69,7 @@ class FigureInfo(NamedTuple):
|
|
|
69
69
|
meta: dict
|
|
70
70
|
area: str
|
|
71
71
|
priority: Optional[int] = None
|
|
72
|
+
custom_data: Optional[dict] = None
|
|
72
73
|
|
|
73
74
|
@property
|
|
74
75
|
def bbox(self) -> Optional[Rectangle]:
|
|
@@ -135,6 +136,7 @@ class FigureApi(RemoveableBulkModuleApi):
|
|
|
135
136
|
ApiField.META,
|
|
136
137
|
ApiField.AREA,
|
|
137
138
|
ApiField.PRIORITY,
|
|
139
|
+
ApiField.CUSTOM_DATA,
|
|
138
140
|
]
|
|
139
141
|
|
|
140
142
|
@staticmethod
|
|
@@ -229,7 +231,8 @@ class FigureApi(RemoveableBulkModuleApi):
|
|
|
229
231
|
meta: Dict,
|
|
230
232
|
geometry_json: Dict,
|
|
231
233
|
geometry_type: str,
|
|
232
|
-
track_id: int = None,
|
|
234
|
+
track_id: Optional[int] = None,
|
|
235
|
+
custom_data: Optional[dict] = None,
|
|
233
236
|
) -> int:
|
|
234
237
|
""""""
|
|
235
238
|
input_figure = {
|
|
@@ -242,6 +245,9 @@ class FigureApi(RemoveableBulkModuleApi):
|
|
|
242
245
|
if track_id is not None:
|
|
243
246
|
input_figure[ApiField.TRACK_ID] = track_id
|
|
244
247
|
|
|
248
|
+
if custom_data is not None:
|
|
249
|
+
input_figure[ApiField.CUSTOM_DATA] = custom_data
|
|
250
|
+
|
|
245
251
|
body = {ApiField.ENTITY_ID: entity_id, ApiField.FIGURES: [input_figure]}
|
|
246
252
|
|
|
247
253
|
response = self._api.post("figures.bulk.add", body)
|
|
@@ -354,21 +360,22 @@ class FigureApi(RemoveableBulkModuleApi):
|
|
|
354
360
|
"""
|
|
355
361
|
filters = [{"field": "id", "operator": "in", "value": ids}]
|
|
356
362
|
fields = [
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
363
|
+
ApiField.ID,
|
|
364
|
+
ApiField.CREATED_AT,
|
|
365
|
+
ApiField.UPDATED_AT,
|
|
366
|
+
ApiField.IMAGE_ID,
|
|
367
|
+
ApiField.OBJECT_ID,
|
|
368
|
+
ApiField.CLASS_ID,
|
|
369
|
+
ApiField.PROJECT_ID,
|
|
370
|
+
ApiField.DATASET_ID,
|
|
371
|
+
ApiField.GEOMETRY,
|
|
372
|
+
ApiField.GEOMETRY_TYPE,
|
|
373
|
+
ApiField.GEOMETRY_META,
|
|
374
|
+
ApiField.TAGS,
|
|
375
|
+
ApiField.META,
|
|
376
|
+
ApiField.AREA,
|
|
377
|
+
ApiField.PRIORITY,
|
|
378
|
+
ApiField.CUSTOM_DATA,
|
|
372
379
|
]
|
|
373
380
|
figures_infos = self.get_list_all_pages(
|
|
374
381
|
"figures.list",
|
|
@@ -488,6 +495,7 @@ class FigureApi(RemoveableBulkModuleApi):
|
|
|
488
495
|
ApiField.META,
|
|
489
496
|
ApiField.AREA,
|
|
490
497
|
ApiField.PRIORITY,
|
|
498
|
+
ApiField.CUSTOM_DATA,
|
|
491
499
|
]
|
|
492
500
|
if skip_geometry is True:
|
|
493
501
|
fields = [x for x in fields if x != ApiField.GEOMETRY]
|
|
@@ -840,6 +848,7 @@ class FigureApi(RemoveableBulkModuleApi):
|
|
|
840
848
|
ApiField.META,
|
|
841
849
|
ApiField.AREA,
|
|
842
850
|
ApiField.PRIORITY,
|
|
851
|
+
ApiField.CUSTOM_DATA,
|
|
843
852
|
]
|
|
844
853
|
if skip_geometry is True:
|
|
845
854
|
fields = [x for x in fields if x != ApiField.GEOMETRY]
|
supervisely/api/module_api.py
CHANGED
|
@@ -312,11 +312,13 @@ class VolumeAnnotationAPI(EntityAnnotationAPI):
|
|
|
312
312
|
|
|
313
313
|
for nrrd_path in nrrd_paths:
|
|
314
314
|
object_key = None
|
|
315
|
+
custom_data = None
|
|
315
316
|
|
|
316
317
|
# searching connection between interpolation and spatial figure in annotations and set its object_key
|
|
317
318
|
for sf in ann.spatial_figures:
|
|
318
319
|
if sf.key().hex == get_file_name(nrrd_path):
|
|
319
320
|
object_key = sf.parent_object.key()
|
|
321
|
+
custom_data = sf.custom_data
|
|
320
322
|
break
|
|
321
323
|
|
|
322
324
|
if object_key:
|
|
@@ -341,12 +343,12 @@ class VolumeAnnotationAPI(EntityAnnotationAPI):
|
|
|
341
343
|
geometry = Mask3D(np.zeros((3, 3, 3), dtype=np.bool_))
|
|
342
344
|
|
|
343
345
|
if transfer_type == "download":
|
|
344
|
-
new_object = VolumeObject(new_obj_class, mask_3d=geometry)
|
|
346
|
+
new_object = VolumeObject(new_obj_class, mask_3d=geometry, custom_data=custom_data)
|
|
345
347
|
elif transfer_type == "upload":
|
|
346
348
|
if class_created:
|
|
347
349
|
self._api.project.update_meta(project_id, project_meta)
|
|
348
350
|
new_object = VolumeObject(new_obj_class)
|
|
349
|
-
new_object.figure = VolumeFigure(new_object, geometry, key=sf.key())
|
|
351
|
+
new_object.figure = VolumeFigure(new_object, geometry, key=sf.key(), custom_data=custom_data)
|
|
350
352
|
|
|
351
353
|
# add new Volume object to VolumeAnnotation with spatial figure
|
|
352
354
|
ann = ann.add_objects([new_object])
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import os
|
|
3
3
|
import re
|
|
4
4
|
import tempfile
|
|
5
|
-
from typing import Dict, List
|
|
5
|
+
from typing import Dict, List, Literal, Optional
|
|
6
6
|
from uuid import UUID
|
|
7
7
|
|
|
8
8
|
from numpy import uint8
|
|
@@ -33,7 +33,8 @@ class VolumeFigureApi(FigureApi):
|
|
|
33
33
|
plane_name: str,
|
|
34
34
|
slice_index: int,
|
|
35
35
|
geometry_json: dict,
|
|
36
|
-
geometry_type,
|
|
36
|
+
geometry_type: str,
|
|
37
|
+
custom_data: Optional[dict] = None,
|
|
37
38
|
# track_id=None,
|
|
38
39
|
):
|
|
39
40
|
"""
|
|
@@ -98,6 +99,7 @@ class VolumeFigureApi(FigureApi):
|
|
|
98
99
|
},
|
|
99
100
|
geometry_json,
|
|
100
101
|
geometry_type,
|
|
102
|
+
custom_data=custom_data,
|
|
101
103
|
# track_id,
|
|
102
104
|
)
|
|
103
105
|
|
|
@@ -420,7 +422,7 @@ class VolumeFigureApi(FigureApi):
|
|
|
420
422
|
def _append_bulk_mask3d(
|
|
421
423
|
self,
|
|
422
424
|
entity_id: int,
|
|
423
|
-
figures: List,
|
|
425
|
+
figures: List[VolumeFigure],
|
|
424
426
|
figures_keys: List,
|
|
425
427
|
key_id_map: KeyIdMap,
|
|
426
428
|
field_name=ApiField.ENTITY_ID,
|
|
@@ -449,10 +451,11 @@ class VolumeFigureApi(FigureApi):
|
|
|
449
451
|
for figure in figures:
|
|
450
452
|
empty_figures.append(
|
|
451
453
|
{
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
454
|
+
ApiField.OBJECT_ID: key_id_map.get_object_id(figure.volume_object.key()),
|
|
455
|
+
ApiField.GEOMETRY_TYPE: Mask3D.name(),
|
|
456
|
+
ApiField.LABELING_TOOL: Mask3D.name(),
|
|
457
|
+
ApiField.ENTITY_ID: entity_id,
|
|
458
|
+
ApiField.CUSTOM_DATA: figure.custom_data
|
|
456
459
|
}
|
|
457
460
|
)
|
|
458
461
|
for batch_keys, batch_jsons in zip(
|
|
@@ -643,3 +646,29 @@ class VolumeFigureApi(FigureApi):
|
|
|
643
646
|
if kwargs.get("image_ids", False) is not False:
|
|
644
647
|
volume_ids = kwargs["image_ids"] # backward compatibility
|
|
645
648
|
return super().download(dataset_id, volume_ids, skip_geometry)
|
|
649
|
+
|
|
650
|
+
def update_custom_data(
|
|
651
|
+
self,
|
|
652
|
+
figure_id: int,
|
|
653
|
+
custom_data: Dict[str, str],
|
|
654
|
+
update_strategy: Literal["replace", "merge"] = "merge",
|
|
655
|
+
) -> None:
|
|
656
|
+
"""
|
|
657
|
+
Update custom data for a specific figure in a volume.
|
|
658
|
+
|
|
659
|
+
:param figure_id: ID of the figure to update.
|
|
660
|
+
:type figure_id: int
|
|
661
|
+
:param custom_data: Custom data to update.
|
|
662
|
+
:type custom_data: Dict[str, str]
|
|
663
|
+
:param update_strategy: Strategy to apply, either "replace" or "merge".
|
|
664
|
+
:type update_strategy: Literal["replace", "merge"]
|
|
665
|
+
:return: None
|
|
666
|
+
:rtype: :class:`NoneType`
|
|
667
|
+
|
|
668
|
+
"""
|
|
669
|
+
data = {
|
|
670
|
+
ApiField.ID: figure_id,
|
|
671
|
+
ApiField.CUSTOM_DATA: custom_data,
|
|
672
|
+
ApiField.UPDATE_STRATEGY: update_strategy,
|
|
673
|
+
}
|
|
674
|
+
self._api.post("figures.custom-data.update", data)
|
|
@@ -83,13 +83,13 @@ class BaseConverter:
|
|
|
83
83
|
item_path: str,
|
|
84
84
|
ann_data: Union[str, dict] = None,
|
|
85
85
|
shape: Union[Tuple, List] = None,
|
|
86
|
-
custom_data: dict =
|
|
86
|
+
custom_data: Optional[dict] = None,
|
|
87
87
|
):
|
|
88
88
|
self._path: str = item_path
|
|
89
89
|
self._name: str = None
|
|
90
90
|
self._ann_data: Union[str, dict, list] = ann_data
|
|
91
91
|
self._shape: Union[Tuple, List] = shape
|
|
92
|
-
self._custom_data: dict = custom_data
|
|
92
|
+
self._custom_data: dict = custom_data or {}
|
|
93
93
|
|
|
94
94
|
@property
|
|
95
95
|
def name(self) -> str:
|
|
@@ -2,7 +2,8 @@ import os
|
|
|
2
2
|
from collections import defaultdict
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
|
-
from supervisely import ProjectMeta, logger
|
|
5
|
+
from supervisely import Api, ProjectMeta, logger
|
|
6
|
+
from supervisely._utils import batched, is_development
|
|
6
7
|
from supervisely.annotation.obj_class import ObjClass
|
|
7
8
|
from supervisely.convert.volume.nii import nii_volume_helper as helper
|
|
8
9
|
from supervisely.convert.volume.nii.nii_volume_converter import NiiConverter
|
|
@@ -12,7 +13,6 @@ from supervisely.io.fs import get_file_ext, get_file_name, list_files_recursivel
|
|
|
12
13
|
from supervisely.volume.volume import is_nifti_file
|
|
13
14
|
from supervisely.volume_annotation.volume_annotation import VolumeAnnotation
|
|
14
15
|
from supervisely.volume_annotation.volume_object import VolumeObject
|
|
15
|
-
from supervisely._utils import batched, is_development
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
class NiiPlaneStructuredConverter(NiiConverter, VolumeConverter):
|
|
@@ -83,6 +83,12 @@ class NiiPlaneStructuredConverter(NiiConverter, VolumeConverter):
|
|
|
83
83
|
ann_dict = defaultdict(list)
|
|
84
84
|
cls_color_map = None
|
|
85
85
|
|
|
86
|
+
ann_to_score_path = {}
|
|
87
|
+
csv_files = list_files_recursively(self._input_data, [".csv"], None, True)
|
|
88
|
+
csv_nameparts = {
|
|
89
|
+
helper.parse_name_parts(os.path.basename(file)): file for file in csv_files
|
|
90
|
+
}
|
|
91
|
+
|
|
86
92
|
for root, _, files in os.walk(self._input_data):
|
|
87
93
|
if converted_dir_name in root:
|
|
88
94
|
continue
|
|
@@ -91,17 +97,25 @@ class NiiPlaneStructuredConverter(NiiConverter, VolumeConverter):
|
|
|
91
97
|
if is_nifti_file(path):
|
|
92
98
|
name_parts = helper.parse_name_parts(file)
|
|
93
99
|
if name_parts is None:
|
|
94
|
-
logger.
|
|
100
|
+
logger.debug(
|
|
95
101
|
f"File recognized as NIfTI, but failed to parse plane identifier from name. Path: {path}",
|
|
96
102
|
)
|
|
97
103
|
continue
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
104
|
+
if name_parts.is_ann:
|
|
105
|
+
dict_to_use = ann_dict
|
|
106
|
+
score_path = helper.find_best_name_match(
|
|
107
|
+
name_parts, list(csv_nameparts.keys())
|
|
108
|
+
)
|
|
109
|
+
if score_path is not None:
|
|
110
|
+
full_score_path = csv_nameparts[score_path]
|
|
111
|
+
ann_to_score_path[name_parts.full_name] = full_score_path
|
|
112
|
+
else:
|
|
113
|
+
dict_to_use = volumes_dict
|
|
114
|
+
|
|
115
|
+
if name_parts.patient_uuid is None and name_parts.case_uuid is None:
|
|
116
|
+
key = name_parts.plane
|
|
117
|
+
else:
|
|
118
|
+
key = f"{name_parts.plane}_{name_parts.patient_uuid}_{name_parts.case_uuid}"
|
|
105
119
|
dict_to_use[key].append(path)
|
|
106
120
|
ext = get_file_ext(path)
|
|
107
121
|
if ext == ".txt":
|
|
@@ -113,7 +127,17 @@ class NiiPlaneStructuredConverter(NiiConverter, VolumeConverter):
|
|
|
113
127
|
for key, paths in volumes_dict.items():
|
|
114
128
|
if len(paths) == 1:
|
|
115
129
|
item = self.Item(item_path=paths[0])
|
|
130
|
+
name_parts = helper.parse_name_parts(os.path.basename(item.path))
|
|
116
131
|
item.ann_data = ann_dict.get(key, [])
|
|
132
|
+
|
|
133
|
+
ann_path = os.path.basename(item.ann_data[0]) if item.ann_data else None
|
|
134
|
+
if ann_path in ann_to_score_path:
|
|
135
|
+
score_path = ann_to_score_path[ann_path]
|
|
136
|
+
try:
|
|
137
|
+
scores = helper.get_scores_from_table(score_path, name_parts.plane)
|
|
138
|
+
item.custom_data["scores"] = scores
|
|
139
|
+
except Exception as e:
|
|
140
|
+
logger.warning(f"Failed to read scores from {score_path}: {e}")
|
|
117
141
|
item.is_semantic = len(item.ann_data) == 1
|
|
118
142
|
if cls_color_map is not None:
|
|
119
143
|
item.custom_data["cls_color_map"] = cls_color_map
|
|
@@ -123,12 +147,23 @@ class NiiPlaneStructuredConverter(NiiConverter, VolumeConverter):
|
|
|
123
147
|
f"Found {len(paths)} volumes with key {key}. Will try to match them by directories."
|
|
124
148
|
)
|
|
125
149
|
for path in paths:
|
|
150
|
+
name_parts = helper.parse_name_parts(os.path.basename(path))
|
|
126
151
|
item = self.Item(item_path=path)
|
|
127
152
|
possible_ann_paths = []
|
|
128
153
|
for ann_path in ann_dict.get(key, []):
|
|
129
154
|
if Path(ann_path).parent == Path(path).parent:
|
|
130
155
|
possible_ann_paths.append(ann_path)
|
|
131
156
|
item.ann_data = possible_ann_paths
|
|
157
|
+
scores_paths = [
|
|
158
|
+
ann_to_score_path.get(ann_name, None) for ann_name in possible_ann_paths
|
|
159
|
+
]
|
|
160
|
+
scores_paths = [path for path in scores_paths if path is not None]
|
|
161
|
+
if scores_paths:
|
|
162
|
+
try:
|
|
163
|
+
scores = helper.get_scores_from_table(scores_paths[0], name_parts.plane)
|
|
164
|
+
item.custom_data["scores"] = scores
|
|
165
|
+
except Exception as e:
|
|
166
|
+
logger.warning(f"Failed to read scores from {scores_paths[0]}: {e}")
|
|
132
167
|
item.is_semantic = len(possible_ann_paths) == 1
|
|
133
168
|
if cls_color_map is not None:
|
|
134
169
|
item.custom_data["cls_color_map"] = cls_color_map
|
|
@@ -149,6 +184,7 @@ class NiiPlaneStructuredConverter(NiiConverter, VolumeConverter):
|
|
|
149
184
|
try:
|
|
150
185
|
objs = []
|
|
151
186
|
spatial_figures = []
|
|
187
|
+
scores = item.custom_data.get("scores", {})
|
|
152
188
|
for idx, ann_path in enumerate(item.ann_data, start=1):
|
|
153
189
|
for mask, pixel_value in helper.get_annotation_from_nii(ann_path):
|
|
154
190
|
class_id = pixel_value if item.is_semantic else idx
|
|
@@ -170,7 +206,9 @@ class NiiPlaneStructuredConverter(NiiConverter, VolumeConverter):
|
|
|
170
206
|
meta = meta.add_obj_class(obj_class)
|
|
171
207
|
self._meta_changed = True
|
|
172
208
|
self._meta = meta
|
|
173
|
-
|
|
209
|
+
obj_scores = scores.get(class_id, {})
|
|
210
|
+
obj_scores = {k: v for k, v in obj_scores.items()}
|
|
211
|
+
obj = VolumeObject(obj_class, mask_3d=mask, custom_data=obj_scores)
|
|
174
212
|
spatial_figures.append(obj.figure)
|
|
175
213
|
objs.append(obj)
|
|
176
214
|
return VolumeAnnotation(item.volume_meta, objects=objs, spatial_figures=spatial_figures)
|
|
@@ -210,7 +248,7 @@ class NiiPlaneStructuredAnnotationConverter(NiiConverter, VolumeConverter):
|
|
|
210
248
|
|
|
211
249
|
def validate_format(self) -> bool:
|
|
212
250
|
try:
|
|
213
|
-
from nibabel import
|
|
251
|
+
from nibabel import filebasedimages, load
|
|
214
252
|
except ImportError:
|
|
215
253
|
raise ImportError(
|
|
216
254
|
"No module named nibabel. Please make sure that module is installed from pip and try again."
|
|
@@ -235,8 +273,8 @@ class NiiPlaneStructuredAnnotationConverter(NiiConverter, VolumeConverter):
|
|
|
235
273
|
for root, _, files in os.walk(self._input_data):
|
|
236
274
|
for file in files:
|
|
237
275
|
path = os.path.join(root, file)
|
|
276
|
+
name_parts = helper.parse_name_parts(file)
|
|
238
277
|
if is_nii(file):
|
|
239
|
-
name_parts = helper.parse_name_parts(file)
|
|
240
278
|
if name_parts is None or not name_parts.is_ann:
|
|
241
279
|
continue
|
|
242
280
|
try:
|
|
@@ -193,7 +193,7 @@ class NiiConverter(VolumeConverter):
|
|
|
193
193
|
volume_np, volume_meta = read_nrrd_serie_volume_np(item.path)
|
|
194
194
|
progress_nrrd = tqdm_sly(
|
|
195
195
|
desc=f"Uploading volume '{item.name}'",
|
|
196
|
-
total=sum(volume_np.shape),
|
|
196
|
+
total=sum(volume_np.shape) + 1,
|
|
197
197
|
leave=True if progress_cb is None else False,
|
|
198
198
|
position=1,
|
|
199
199
|
)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from collections import defaultdict, namedtuple
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import Generator
|
|
4
|
+
from typing import Generator, List, Union
|
|
5
5
|
|
|
6
6
|
import nrrd
|
|
7
7
|
import numpy as np
|
|
@@ -17,6 +17,7 @@ VOLUME_NAME = "anatomic"
|
|
|
17
17
|
LABEL_NAME = ["inference", "label", "annotation", "mask", "segmentation"]
|
|
18
18
|
MASK_PIXEL_VALUE = "Mask pixel value: "
|
|
19
19
|
|
|
20
|
+
|
|
20
21
|
class PlanePrefix(str, StrEnum):
|
|
21
22
|
"""Prefix for plane names."""
|
|
22
23
|
|
|
@@ -117,15 +118,79 @@ def get_annotation_from_nii(path: str) -> Generator[Mask3D, None, None]:
|
|
|
117
118
|
yield mask, class_id
|
|
118
119
|
|
|
119
120
|
|
|
121
|
+
def get_scores_from_table(csv_file_path: str, plane: str) -> dict:
|
|
122
|
+
"""Get scores from CSV table and return nested dictionary structure.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
csv_file_path: Path to the CSV file containing layer scores
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Nested dictionary with structure:
|
|
129
|
+
{
|
|
130
|
+
"label_index": {
|
|
131
|
+
"slice_index": {
|
|
132
|
+
"127": {
|
|
133
|
+
"score": float_value,
|
|
134
|
+
"comment": ""
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
"""
|
|
140
|
+
import csv
|
|
141
|
+
|
|
142
|
+
if plane == PlanePrefix.CORONAL:
|
|
143
|
+
plane = "0-1-0"
|
|
144
|
+
elif plane == PlanePrefix.SAGITTAL:
|
|
145
|
+
plane = "1-0-0"
|
|
146
|
+
elif plane == PlanePrefix.AXIAL:
|
|
147
|
+
plane = "0-0-1"
|
|
148
|
+
|
|
149
|
+
result = defaultdict(lambda: defaultdict(dict))
|
|
150
|
+
|
|
151
|
+
if not os.path.exists(csv_file_path):
|
|
152
|
+
logger.warning(f"CSV file not found: {csv_file_path}")
|
|
153
|
+
return result
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
with open(csv_file_path, "r") as file:
|
|
157
|
+
reader = csv.DictReader(file)
|
|
158
|
+
label_columns = [col for col in reader.fieldnames if col.startswith("Label-")]
|
|
159
|
+
|
|
160
|
+
for row in reader:
|
|
161
|
+
frame_idx = int(row["Layer"]) - 1 # Assuming Layer is 1-indexed in CSV
|
|
162
|
+
|
|
163
|
+
for label_col in label_columns:
|
|
164
|
+
label_index = int(label_col.split("-")[1])
|
|
165
|
+
score = f"{float(row[label_col]):.2f}"
|
|
166
|
+
result[label_index][plane][frame_idx] = {"score": score, "comment": None}
|
|
167
|
+
|
|
168
|
+
except Exception as e:
|
|
169
|
+
logger.warning(f"Failed to read CSV file {csv_file_path}: {e}")
|
|
170
|
+
return {}
|
|
171
|
+
|
|
172
|
+
return result
|
|
173
|
+
|
|
174
|
+
|
|
120
175
|
class AnnotationMatcher:
|
|
121
176
|
def __init__(self, items, dataset_id):
|
|
122
177
|
self._items = items
|
|
123
178
|
self._ds_id = dataset_id
|
|
124
179
|
self._ann_paths = defaultdict(list)
|
|
125
|
-
|
|
126
180
|
self._item_by_filename = {}
|
|
127
181
|
self._item_by_path = {}
|
|
128
182
|
|
|
183
|
+
self.set_items(items)
|
|
184
|
+
|
|
185
|
+
self._project_wide = False
|
|
186
|
+
self._volumes = None
|
|
187
|
+
|
|
188
|
+
def set_items(self, items):
|
|
189
|
+
self._items = items
|
|
190
|
+
self._ann_paths.clear()
|
|
191
|
+
self._item_by_filename.clear()
|
|
192
|
+
self._item_by_path.clear()
|
|
193
|
+
|
|
129
194
|
for item in items:
|
|
130
195
|
path = Path(item.ann_data)
|
|
131
196
|
dataset_name = path.parts[-2]
|
|
@@ -135,9 +200,6 @@ class AnnotationMatcher:
|
|
|
135
200
|
self._item_by_filename[filename] = item
|
|
136
201
|
self._item_by_path[(dataset_name, filename)] = item
|
|
137
202
|
|
|
138
|
-
self._project_wide = False
|
|
139
|
-
self._volumes = None
|
|
140
|
-
|
|
141
203
|
def get_volumes(self, api: Api):
|
|
142
204
|
dataset_info = api.dataset.get_info_by_id(self._ds_id)
|
|
143
205
|
datasets = {dataset_info.name: dataset_info}
|
|
@@ -166,11 +228,8 @@ class AnnotationMatcher:
|
|
|
166
228
|
|
|
167
229
|
def match_items(self):
|
|
168
230
|
"""Match annotation files with corresponding volumes using regex-based matching."""
|
|
169
|
-
import re
|
|
170
|
-
|
|
171
231
|
item_to_volume = {}
|
|
172
232
|
|
|
173
|
-
# Perform matching
|
|
174
233
|
for dataset_name, volumes in self._volumes.items():
|
|
175
234
|
volume_names = [parse_name_parts(name) for name in list(volumes.keys())]
|
|
176
235
|
_volume_names = [vol for vol in volume_names if vol is not None]
|
|
@@ -191,7 +250,7 @@ class AnnotationMatcher:
|
|
|
191
250
|
if ann_name is None:
|
|
192
251
|
logger.warning(f"Failed to parse annotation name: {ann_file}")
|
|
193
252
|
continue
|
|
194
|
-
match =
|
|
253
|
+
match = find_best_name_match(ann_name, volume_names)
|
|
195
254
|
if match is not None:
|
|
196
255
|
if match.plane != ann_name.plane:
|
|
197
256
|
logger.warning(
|
|
@@ -200,7 +259,6 @@ class AnnotationMatcher:
|
|
|
200
259
|
continue
|
|
201
260
|
item_to_volume[self._item_by_filename[ann_file]] = volumes[match.full_name]
|
|
202
261
|
|
|
203
|
-
# Mark volumes having only one matching item as semantic and validate shape.
|
|
204
262
|
volume_to_items = defaultdict(list)
|
|
205
263
|
for item, volume in item_to_volume.items():
|
|
206
264
|
volume_to_items[volume.id].append(item)
|
|
@@ -306,9 +364,11 @@ def parse_name_parts(full_name: str) -> NameParts:
|
|
|
306
364
|
is_ann = False
|
|
307
365
|
if VOLUME_NAME in full_name:
|
|
308
366
|
type = "anatomic"
|
|
309
|
-
|
|
367
|
+
elif any(part in full_name for part in LABEL_NAME):
|
|
310
368
|
type = next((part for part in LABEL_NAME if part in full_name), None)
|
|
311
369
|
is_ann = type is not None
|
|
370
|
+
elif "score" in name_no_ext or get_file_ext(full_name) == ".csv":
|
|
371
|
+
type = "score"
|
|
312
372
|
|
|
313
373
|
if type is None:
|
|
314
374
|
return
|
|
@@ -367,58 +427,58 @@ def parse_name_parts(full_name: str) -> NameParts:
|
|
|
367
427
|
)
|
|
368
428
|
|
|
369
429
|
|
|
370
|
-
def
|
|
430
|
+
def find_best_name_match(item: NameParts, pool: List[NameParts]) -> Union[NameParts, None]:
|
|
371
431
|
"""
|
|
372
|
-
Finds the best matching NameParts object from `
|
|
432
|
+
Finds the best matching NameParts object from `pool` for the given annotation NameParts `item`.
|
|
373
433
|
Prefers an exact match where all fields except `type` are the same, and `type` is 'anatomic'.
|
|
374
434
|
Returns the matched NameParts object or None if not found.
|
|
375
435
|
"""
|
|
376
|
-
|
|
377
|
-
|
|
436
|
+
pool_item_names = [i.full_name for i in pool]
|
|
437
|
+
item_name = item.full_name
|
|
378
438
|
# Prefer exact match except for type
|
|
379
|
-
for
|
|
380
|
-
if
|
|
439
|
+
for i in pool:
|
|
440
|
+
if i.name_no_ext == item.name_no_ext.replace(item.type, i.type):
|
|
381
441
|
logger.debug(
|
|
382
|
-
"Found exact match
|
|
383
|
-
extra={"
|
|
442
|
+
"Found exact match.",
|
|
443
|
+
extra={"item": item_name, "pool_item": i.full_name},
|
|
384
444
|
)
|
|
385
|
-
return
|
|
445
|
+
return i
|
|
386
446
|
|
|
387
447
|
logger.debug(
|
|
388
448
|
"Failed to find exact match, trying to find a fallback match UUIDs.",
|
|
389
|
-
extra={"
|
|
449
|
+
extra={"item": item_name, "pool_items": pool_item_names},
|
|
390
450
|
)
|
|
391
451
|
|
|
392
452
|
# Fallback: match by plane and patient_uuid, type='anatomic'
|
|
393
|
-
for
|
|
453
|
+
for i in pool:
|
|
394
454
|
if (
|
|
395
|
-
|
|
396
|
-
and
|
|
397
|
-
and
|
|
455
|
+
i.plane == item.plane
|
|
456
|
+
and i.patient_uuid == item.patient_uuid
|
|
457
|
+
and i.case_uuid == item.case_uuid
|
|
398
458
|
):
|
|
399
459
|
logger.debug(
|
|
400
|
-
"Found fallback match for
|
|
401
|
-
extra={"
|
|
460
|
+
"Found fallback match for item by UUIDs.",
|
|
461
|
+
extra={"item": item_name, "i": i.full_name},
|
|
402
462
|
)
|
|
403
|
-
return
|
|
463
|
+
return i
|
|
404
464
|
|
|
405
465
|
logger.debug(
|
|
406
466
|
"Failed to find fallback match, trying to find a fallback match by plane.",
|
|
407
|
-
extra={"
|
|
467
|
+
extra={"item": item_name, "pool_items": pool_item_names},
|
|
408
468
|
)
|
|
409
469
|
|
|
410
470
|
# Fallback: match by plane and type='anatomic'
|
|
411
|
-
for
|
|
412
|
-
if
|
|
471
|
+
for i in pool:
|
|
472
|
+
if i.plane == item.plane:
|
|
413
473
|
logger.debug(
|
|
414
|
-
"Found fallback match for
|
|
415
|
-
extra={"
|
|
474
|
+
"Found fallback match for item by plane.",
|
|
475
|
+
extra={"item": item_name, "i": i.full_name},
|
|
416
476
|
)
|
|
417
|
-
return
|
|
477
|
+
return i
|
|
418
478
|
|
|
419
479
|
logger.debug(
|
|
420
|
-
"Failed to find any match for
|
|
421
|
-
extra={"
|
|
480
|
+
"Failed to find any match for item.",
|
|
481
|
+
extra={"item": item_name, "pool_items": pool_item_names},
|
|
422
482
|
)
|
|
423
483
|
|
|
424
484
|
return None
|
|
@@ -4,6 +4,7 @@ from typing import List
|
|
|
4
4
|
import supervisely.convert.volume.sly.sly_volume_helper as sly_volume_helper
|
|
5
5
|
from supervisely.volume_annotation.volume_annotation import VolumeAnnotation
|
|
6
6
|
from supervisely import ProjectMeta, logger
|
|
7
|
+
from supervisely.project.volume_project import VolumeProject, VolumeDataset
|
|
7
8
|
from supervisely.convert.base_converter import AvailableVolumeConverters
|
|
8
9
|
from supervisely.convert.volume.volume_converter import VolumeConverter
|
|
9
10
|
from supervisely.io.fs import JUNK_FILES, get_file_ext, get_file_name
|
|
@@ -40,7 +41,7 @@ class SLYVolumeConverter(VolumeConverter):
|
|
|
40
41
|
ann = VolumeAnnotation.from_json(ann_json, meta) # , KeyIdMap())
|
|
41
42
|
return True
|
|
42
43
|
except Exception as e:
|
|
43
|
-
logger.
|
|
44
|
+
logger.warning(f"Failed to validate annotation: {repr(e)}")
|
|
44
45
|
return False
|
|
45
46
|
|
|
46
47
|
def validate_key_file(self, key_file_path: str) -> bool:
|
|
@@ -51,6 +52,9 @@ class SLYVolumeConverter(VolumeConverter):
|
|
|
51
52
|
return False
|
|
52
53
|
|
|
53
54
|
def validate_format(self) -> bool:
|
|
55
|
+
if self.read_sly_project(self._input_data):
|
|
56
|
+
return True
|
|
57
|
+
|
|
54
58
|
detected_ann_cnt = 0
|
|
55
59
|
vol_list, stl_dict, ann_dict, mask_dict = [], {}, {}, {}
|
|
56
60
|
for root, _, files in os.walk(self._input_data):
|
|
@@ -70,7 +74,7 @@ class SLYVolumeConverter(VolumeConverter):
|
|
|
70
74
|
ProjectMeta.from_json(meta_json)
|
|
71
75
|
)
|
|
72
76
|
except Exception as e:
|
|
73
|
-
logger.
|
|
77
|
+
logger.warning(f"Failed to merge meta: {repr(e)}")
|
|
74
78
|
continue
|
|
75
79
|
|
|
76
80
|
elif file in JUNK_FILES: # add better check
|
|
@@ -139,5 +143,30 @@ class SLYVolumeConverter(VolumeConverter):
|
|
|
139
143
|
ann_json = sly_volume_helper.rename_in_json(ann_json, renamed_classes, renamed_tags)
|
|
140
144
|
return VolumeAnnotation.from_json(ann_json, meta) # , KeyIdMap())
|
|
141
145
|
except Exception as e:
|
|
142
|
-
logger.
|
|
146
|
+
logger.warning(f"Failed to read annotation: {repr(e)}")
|
|
143
147
|
return item.create_empty_annotation()
|
|
148
|
+
|
|
149
|
+
def read_sly_project(self, input_data: str) -> bool:
|
|
150
|
+
try:
|
|
151
|
+
project_fs = VolumeProject.read_single(input_data)
|
|
152
|
+
self._meta = project_fs.meta
|
|
153
|
+
self._items = []
|
|
154
|
+
|
|
155
|
+
for dataset_fs in project_fs.datasets:
|
|
156
|
+
dataset_fs: VolumeDataset
|
|
157
|
+
|
|
158
|
+
for item_name in dataset_fs:
|
|
159
|
+
img_path, ann_path = dataset_fs.get_item_paths(item_name)
|
|
160
|
+
item = self.Item(
|
|
161
|
+
item_path=img_path,
|
|
162
|
+
ann_data=ann_path,
|
|
163
|
+
shape=None,
|
|
164
|
+
interpolation_dir=dataset_fs.get_interpolation_dir(item_name),
|
|
165
|
+
mask_dir=dataset_fs.get_mask_dir(item_name),
|
|
166
|
+
)
|
|
167
|
+
self._items.append(item)
|
|
168
|
+
return True
|
|
169
|
+
|
|
170
|
+
except Exception as e:
|
|
171
|
+
logger.info(f"Failed to read Supervisely project: {repr(e)}")
|
|
172
|
+
return False
|
|
@@ -478,6 +478,12 @@ def download_volume_project(
|
|
|
478
478
|
figure_path = dataset_fs.get_interpolation_path(volume_name, sf)
|
|
479
479
|
mesh_paths.append(figure_path)
|
|
480
480
|
|
|
481
|
+
figs = api.volume.figure.download(dataset.id, [volume_id], skip_geometry=True)[volume_id]
|
|
482
|
+
figs_ids_map = {fig.id: fig for fig in figs}
|
|
483
|
+
for ann_fig in ann.figures + ann.spatial_figures:
|
|
484
|
+
fig = figs_ids_map.get(ann_fig.geometry.sly_id)
|
|
485
|
+
ann_fig.custom_data.update(fig.custom_data)
|
|
486
|
+
|
|
481
487
|
api.volume.figure.download_stl_meshes(mesh_ids, mesh_paths)
|
|
482
488
|
api.volume.figure.download_sf_geometries(mask_ids, mask_paths)
|
|
483
489
|
|
|
@@ -52,6 +52,8 @@ class VolumeFigure(VideoFigure):
|
|
|
52
52
|
:type updated_at: str, optional
|
|
53
53
|
:param created_at: Date and Time when VolumeFigure was created. Date Format is the same as in "updated_at" parameter.
|
|
54
54
|
:type created_at: str, optional
|
|
55
|
+
:param custom_data: Custom data associated with the VolumeFigure.
|
|
56
|
+
:type custom_data: dict, optional
|
|
55
57
|
:Usage example:
|
|
56
58
|
|
|
57
59
|
.. code-block:: python
|
|
@@ -98,6 +100,7 @@ class VolumeFigure(VideoFigure):
|
|
|
98
100
|
labeler_login: Optional[str] = None,
|
|
99
101
|
updated_at: Optional[str] = None,
|
|
100
102
|
created_at: Optional[str] = None,
|
|
103
|
+
custom_data: Optional[dict] = None,
|
|
101
104
|
**kwargs,
|
|
102
105
|
):
|
|
103
106
|
# only Mask3D can be created without 'plane_name' and 'slice_index'
|
|
@@ -128,6 +131,7 @@ class VolumeFigure(VideoFigure):
|
|
|
128
131
|
Plane.validate_name(plane_name)
|
|
129
132
|
self._plane_name = plane_name
|
|
130
133
|
self._slice_index = slice_index
|
|
134
|
+
self._custom_data = custom_data or {}
|
|
131
135
|
|
|
132
136
|
@property
|
|
133
137
|
def volume_object(self) -> VolumeObject:
|
|
@@ -282,6 +286,34 @@ class VolumeFigure(VideoFigure):
|
|
|
282
286
|
|
|
283
287
|
return Plane.get_normal(self.plane_name)
|
|
284
288
|
|
|
289
|
+
@property
|
|
290
|
+
def custom_data(self) -> Optional[dict]:
|
|
291
|
+
"""
|
|
292
|
+
Get custom data associated with the VolumeFigure.
|
|
293
|
+
|
|
294
|
+
:return: Custom data associated with the VolumeFigure.
|
|
295
|
+
:rtype: dict
|
|
296
|
+
:Usage example:
|
|
297
|
+
|
|
298
|
+
.. code-block:: python
|
|
299
|
+
|
|
300
|
+
import supervisely as sly
|
|
301
|
+
|
|
302
|
+
obj_class_heart = sly.ObjClass('heart', sly.Rectangle)
|
|
303
|
+
volume_obj_heart = sly.VolumeObject(obj_class_heart)
|
|
304
|
+
volume_figure_heart = sly.VolumeFigure(
|
|
305
|
+
volume_obj_heart,
|
|
306
|
+
geometry=sly.Rectangle(0, 0, 100, 100),
|
|
307
|
+
plane_name="axial",
|
|
308
|
+
slice_index=7,
|
|
309
|
+
custom_data={"key": "value"}
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
print(volume_figure_heart.custom_data)
|
|
313
|
+
# Output: {'key': 'value'}
|
|
314
|
+
"""
|
|
315
|
+
return self._custom_data
|
|
316
|
+
|
|
285
317
|
def _validate_geometry_type(self):
|
|
286
318
|
if (
|
|
287
319
|
self.parent_object.obj_class.geometry_type != AnyGeometry
|
|
@@ -344,6 +376,7 @@ class VolumeFigure(VideoFigure):
|
|
|
344
376
|
labeler_login=None,
|
|
345
377
|
updated_at=None,
|
|
346
378
|
created_at=None,
|
|
379
|
+
custom_data=None,
|
|
347
380
|
):
|
|
348
381
|
"""
|
|
349
382
|
Makes a copy of VolumeFigure with new fields, if fields are given, otherwise it will use fields of the original VolumeFigure.
|
|
@@ -366,6 +399,8 @@ class VolumeFigure(VideoFigure):
|
|
|
366
399
|
:type updated_at: str, optional
|
|
367
400
|
:param created_at: Date and Time when VolumeFigure was created. Date Format is the same as in "updated_at" parameter.
|
|
368
401
|
:type created_at: str, optional
|
|
402
|
+
:param custom_data: Custom data associated with the VolumeFigure.
|
|
403
|
+
:type custom_data: dict, optional
|
|
369
404
|
:return: VolumeFigure object
|
|
370
405
|
:rtype: :class:`VolumeFigure`
|
|
371
406
|
|
|
@@ -422,6 +457,7 @@ class VolumeFigure(VideoFigure):
|
|
|
422
457
|
labeler_login=take_with_default(labeler_login, self.labeler_login),
|
|
423
458
|
updated_at=take_with_default(updated_at, self.updated_at),
|
|
424
459
|
created_at=take_with_default(created_at, self.created_at),
|
|
460
|
+
custom_data=take_with_default(custom_data, self.custom_data),
|
|
425
461
|
)
|
|
426
462
|
|
|
427
463
|
def get_meta(self):
|
|
@@ -542,6 +578,7 @@ class VolumeFigure(VideoFigure):
|
|
|
542
578
|
labeler_login = data.get(LABELER_LOGIN, None)
|
|
543
579
|
updated_at = data.get(UPDATED_AT, None)
|
|
544
580
|
created_at = data.get(CREATED_AT, None)
|
|
581
|
+
custom_data = data.get(ApiField.CUSTOM_DATA, None)
|
|
545
582
|
|
|
546
583
|
return cls(
|
|
547
584
|
volume_object=volume_object,
|
|
@@ -553,6 +590,7 @@ class VolumeFigure(VideoFigure):
|
|
|
553
590
|
labeler_login=labeler_login,
|
|
554
591
|
updated_at=updated_at,
|
|
555
592
|
created_at=created_at,
|
|
593
|
+
custom_data=custom_data,
|
|
556
594
|
)
|
|
557
595
|
|
|
558
596
|
def to_json(self, key_id_map=None, save_meta=True):
|
|
@@ -596,11 +634,13 @@ class VolumeFigure(VideoFigure):
|
|
|
596
634
|
# "planeName": "axial",
|
|
597
635
|
# "sliceIndex": 7
|
|
598
636
|
# },
|
|
599
|
-
# "objectKey": "bf63ffe342e949899d3ddcb6b0f73f54"
|
|
637
|
+
# "objectKey": "bf63ffe342e949899d3ddcb6b0f73f54",
|
|
638
|
+
# "custom_data": {}
|
|
600
639
|
# }
|
|
601
640
|
"""
|
|
602
641
|
|
|
603
642
|
json_data = super().to_json(key_id_map, save_meta)
|
|
643
|
+
json_data[ApiField.CUSTOM_DATA] = self.custom_data
|
|
604
644
|
if type(self._geometry) == ClosedSurfaceMesh:
|
|
605
645
|
json_data.pop(ApiField.GEOMETRY)
|
|
606
646
|
json_data.pop(ApiField.META)
|
|
@@ -616,6 +656,7 @@ class VolumeFigure(VideoFigure):
|
|
|
616
656
|
labeler_login: Optional[str] = None,
|
|
617
657
|
updated_at: Optional[str] = None,
|
|
618
658
|
created_at: Optional[str] = None,
|
|
659
|
+
custom_data: Optional[dict] = None,
|
|
619
660
|
) -> VolumeFigure:
|
|
620
661
|
"""
|
|
621
662
|
Create a VolumeFigure from Mask 3D geometry.
|
|
@@ -634,6 +675,8 @@ class VolumeFigure(VideoFigure):
|
|
|
634
675
|
:type updated_at: str, optional
|
|
635
676
|
:param created_at: The date and time when the VolumeFigure was created (ISO 8601 format, e.g., '2021-01-22T19:37:50.158Z').
|
|
636
677
|
:type created_at: str, optional
|
|
678
|
+
:param custom_data: Custom data associated with the VolumeFigure.
|
|
679
|
+
:type custom_data: dict, optional
|
|
637
680
|
:return: A VolumeFigure object created from Mask3D geometry.
|
|
638
681
|
:rtype: VolumeFigure
|
|
639
682
|
"""
|
|
@@ -656,6 +699,7 @@ class VolumeFigure(VideoFigure):
|
|
|
656
699
|
labeler_login=labeler_login,
|
|
657
700
|
updated_at=updated_at,
|
|
658
701
|
created_at=created_at,
|
|
702
|
+
custom_data=custom_data,
|
|
659
703
|
)
|
|
660
704
|
|
|
661
705
|
def _set_3d_geometry(self, new_geometry: Mask3D) -> None:
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# coding: utf-8
|
|
2
2
|
|
|
3
3
|
import uuid
|
|
4
|
-
|
|
5
4
|
from typing import Optional, Union
|
|
5
|
+
|
|
6
6
|
from numpy import ndarray
|
|
7
7
|
|
|
8
|
+
from supervisely.geometry.mask_3d import Mask3D
|
|
8
9
|
from supervisely.video_annotation.video_object import VideoObject
|
|
9
10
|
from supervisely.volume_annotation import volume_figure
|
|
10
11
|
from supervisely.volume_annotation.volume_tag_collection import VolumeTagCollection
|
|
11
|
-
from supervisely.geometry.mask_3d import Mask3D
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class VolumeObject(VideoObject):
|
|
@@ -31,6 +31,8 @@ class VolumeObject(VideoObject):
|
|
|
31
31
|
:type created_at: str, optional
|
|
32
32
|
:param mask_3d: Path for local geometry file, array with geometry data or Mask3D geometry object
|
|
33
33
|
:type mask_3d: Union[str, ndarray, Mask3D], optional
|
|
34
|
+
:param custom_data: Custom data associated with the VolumeObject.
|
|
35
|
+
:type custom_data: dict, optional
|
|
34
36
|
:Usage example:
|
|
35
37
|
|
|
36
38
|
.. code-block:: python
|
|
@@ -58,6 +60,7 @@ class VolumeObject(VideoObject):
|
|
|
58
60
|
updated_at: Optional[str] = None,
|
|
59
61
|
created_at: Optional[str] = None,
|
|
60
62
|
mask_3d: Optional[Union[Mask3D, ndarray, str]] = None,
|
|
63
|
+
custom_data: Optional[dict] = None,
|
|
61
64
|
):
|
|
62
65
|
super().__init__(
|
|
63
66
|
obj_class=obj_class,
|
|
@@ -68,19 +71,33 @@ class VolumeObject(VideoObject):
|
|
|
68
71
|
updated_at=updated_at,
|
|
69
72
|
created_at=created_at,
|
|
70
73
|
)
|
|
71
|
-
|
|
72
74
|
if mask_3d is not None:
|
|
73
75
|
if isinstance(mask_3d, str):
|
|
74
76
|
self.figure = volume_figure.VolumeFigure(
|
|
75
|
-
self,
|
|
77
|
+
self,
|
|
78
|
+
geometry=Mask3D.create_from_file(mask_3d),
|
|
79
|
+
labeler_login=labeler_login,
|
|
80
|
+
updated_at=updated_at,
|
|
81
|
+
created_at=created_at,
|
|
82
|
+
custom_data=custom_data,
|
|
76
83
|
)
|
|
77
84
|
elif isinstance(mask_3d, ndarray):
|
|
78
85
|
self.figure = volume_figure.VolumeFigure(
|
|
79
|
-
self,
|
|
86
|
+
self,
|
|
87
|
+
geometry=Mask3D(mask_3d),
|
|
88
|
+
labeler_login=labeler_login,
|
|
89
|
+
updated_at=updated_at,
|
|
90
|
+
created_at=created_at,
|
|
91
|
+
custom_data=custom_data,
|
|
80
92
|
)
|
|
81
93
|
elif isinstance(mask_3d, Mask3D):
|
|
82
94
|
self.figure = volume_figure.VolumeFigure(
|
|
83
|
-
self,
|
|
95
|
+
self,
|
|
96
|
+
geometry=mask_3d,
|
|
97
|
+
labeler_login=labeler_login,
|
|
98
|
+
updated_at=updated_at,
|
|
99
|
+
created_at=created_at,
|
|
100
|
+
custom_data=custom_data,
|
|
84
101
|
)
|
|
85
102
|
else:
|
|
86
103
|
raise TypeError("mask_3d type must be one of [Mask3D, ndarray, str]")
|
|
@@ -35,7 +35,7 @@ supervisely/api/import_storage_api.py,sha256=BDCgmR0Hv6OoiRHLCVPKt3iDxSVlQp1WrnK
|
|
|
35
35
|
supervisely/api/issues_api.py,sha256=BqDJXmNoTzwc3xe6_-mA7FDFC5QQ-ahGbXk_HmpkSeQ,17925
|
|
36
36
|
supervisely/api/labeling_job_api.py,sha256=G2_BV_WtA2lAhfw_nAQmWmv1P-pwimD0ba9GVKoGjiA,55537
|
|
37
37
|
supervisely/api/labeling_queue_api.py,sha256=ilNjAL1d9NSa9yabQn6E-W26YdtooT3ZGXIFZtGnAvY,30158
|
|
38
|
-
supervisely/api/module_api.py,sha256=
|
|
38
|
+
supervisely/api/module_api.py,sha256=LKRciU6kKiBTUxb_3iYd5yfUBrhm9Sl0epDd8YBTnPc,45413
|
|
39
39
|
supervisely/api/object_class_api.py,sha256=7-npNFMYjWNtSXYZg6syc6bX56_oCzDU2kFRPGQWCwA,10399
|
|
40
40
|
supervisely/api/plugin_api.py,sha256=SFm0IlTTOjuHBLUMgG4d4k6U3cWJocE-SVb-f08fwMQ,5286
|
|
41
41
|
supervisely/api/project_api.py,sha256=WNTMqAa0ZedYesfiZEkZtaFr5huxIpJ8TFYygTnlAWQ,80309
|
|
@@ -50,8 +50,8 @@ supervisely/api/user_api.py,sha256=m29GP9tvem8P2fJZgg7DAZ9yhFdBX26ZBcWxCKdnhn4,2
|
|
|
50
50
|
supervisely/api/video_annotation_tool_api.py,sha256=3A9-U8WJzrTShP_n9T8U01M9FzGYdeS51CCBTzUnooo,6686
|
|
51
51
|
supervisely/api/workspace_api.py,sha256=24O9uR5eIA2JdD0eQLi9LGaaHISdb2gUqnxJtx7bTew,9222
|
|
52
52
|
supervisely/api/entity_annotation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
53
|
-
supervisely/api/entity_annotation/entity_annotation_api.py,sha256=
|
|
54
|
-
supervisely/api/entity_annotation/figure_api.py,sha256=
|
|
53
|
+
supervisely/api/entity_annotation/entity_annotation_api.py,sha256=R7irdsYmUecsibuUFbcPRiS6tV3GnCHi9NfWeuoN7_0,3085
|
|
54
|
+
supervisely/api/entity_annotation/figure_api.py,sha256=KjTpHd7Tl--sG56bC16Ih0cZ7h94lAYpyviOmwOKdCU,35759
|
|
55
55
|
supervisely/api/entity_annotation/object_api.py,sha256=gbcNvN_KY6G80Me8fHKQgryc2Co7VU_kfFd1GYILZ4E,8875
|
|
56
56
|
supervisely/api/entity_annotation/tag_api.py,sha256=IapvSZmakjdOn0yvqP2tQRY8gkZg0bcvIZBwWRcafrg,18996
|
|
57
57
|
supervisely/api/nn/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -75,9 +75,9 @@ supervisely/api/video/video_frame_api.py,sha256=4GwSI4xdCNYEUvTqzKc-Ewd44fw5zqkF
|
|
|
75
75
|
supervisely/api/video/video_object_api.py,sha256=IC0NP8EoIT_d3xxDRgz2cA3ixSiuJ5ymy64eS-RfmDM,2227
|
|
76
76
|
supervisely/api/video/video_tag_api.py,sha256=wPe1HeJyg9kV1z2UJq6BEte5sKBoPJ2UGAHpGivis9c,14911
|
|
77
77
|
supervisely/api/volume/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
78
|
-
supervisely/api/volume/volume_annotation_api.py,sha256=
|
|
78
|
+
supervisely/api/volume/volume_annotation_api.py,sha256=_v9IcWYYIajlCIUjVXNc30iWqgfh8i5RRL1kL1hliVE,22376
|
|
79
79
|
supervisely/api/volume/volume_api.py,sha256=rz_yaBbbTkVeAHmF449zPI8Va_YpDHfHYjXgjGAjMJg,55390
|
|
80
|
-
supervisely/api/volume/volume_figure_api.py,sha256=
|
|
80
|
+
supervisely/api/volume/volume_figure_api.py,sha256=Fs7j3h76kw7EI-o3vJHjpvL4Vxn3Fu-DzhArgK_qrPk,26523
|
|
81
81
|
supervisely/api/volume/volume_object_api.py,sha256=F7pLV2MTlBlyN6fEKdxBSUatIMGWSuu8bWj3Hvcageo,2139
|
|
82
82
|
supervisely/api/volume/volume_tag_api.py,sha256=yNGgXz44QBSW2VGlNDOVLqLXnH8Q2fFrxDFb_girYXA,3639
|
|
83
83
|
supervisely/app/__init__.py,sha256=4yW79U_xvo7vjg6-vRhjtt0bO8MxMSx2PD8dMamS9Q8,633
|
|
@@ -575,7 +575,7 @@ supervisely/collection/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
|
|
|
575
575
|
supervisely/collection/key_indexed_collection.py,sha256=x2UVlkprspWhhae9oLUzjTWBoIouiWY9UQSS_MozfH0,37643
|
|
576
576
|
supervisely/collection/str_enum.py,sha256=Zp29yFGvnxC6oJRYNNlXhO2lTSdsriU1wiGHj6ahEJE,1250
|
|
577
577
|
supervisely/convert/__init__.py,sha256=ropgB1eebG2bfLoJyf2jp8Vv9UkFujaW3jVX-71ho1g,1353
|
|
578
|
-
supervisely/convert/base_converter.py,sha256=
|
|
578
|
+
supervisely/convert/base_converter.py,sha256=bc-QlT7kliHxrhM0bdHzgNVSfzDGgecrmaZH_nFZsL0,18597
|
|
579
579
|
supervisely/convert/converter.py,sha256=022I1UieyaPDVb8lOcKW20jSt1_1TcbIWhghSmieHAE,10885
|
|
580
580
|
supervisely/convert/image/__init__.py,sha256=JEuyaBiiyiYmEUYqdn8Mog5FVXpz0H1zFubKkOOm73I,1395
|
|
581
581
|
supervisely/convert/image/image_converter.py,sha256=8vak8ZoKTN1ye2ZmCTvCZ605-Rw1AFLIEo7bJMfnR68,10426
|
|
@@ -674,11 +674,11 @@ supervisely/convert/volume/dicom/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRk
|
|
|
674
674
|
supervisely/convert/volume/dicom/dicom_converter.py,sha256=Hw4RxU_qvllk6M26udZE6G-m1RWR8-VVPcEPwFlqrVg,3354
|
|
675
675
|
supervisely/convert/volume/dicom/dicom_helper.py,sha256=OrKlyt1hA5BOXKhE1LF1WxBIv3b6t96xRras4OSAuNM,2891
|
|
676
676
|
supervisely/convert/volume/nii/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
677
|
-
supervisely/convert/volume/nii/nii_planes_volume_converter.py,sha256
|
|
678
|
-
supervisely/convert/volume/nii/nii_volume_converter.py,sha256=
|
|
679
|
-
supervisely/convert/volume/nii/nii_volume_helper.py,sha256=
|
|
677
|
+
supervisely/convert/volume/nii/nii_planes_volume_converter.py,sha256=QTdmtqLrRBFSa0IZKhAnFkLl1J3nayzQQDwpglvEN64,16915
|
|
678
|
+
supervisely/convert/volume/nii/nii_volume_converter.py,sha256=QTFg0FW0raSVqgAfY56S7r6tHUYyNOnd4Y9Bdw-e6bc,8623
|
|
679
|
+
supervisely/convert/volume/nii/nii_volume_helper.py,sha256=nkfTG2NGmnf4AfrZ0lULSHaUBx1G24NJUO_5FNejolE,16032
|
|
680
680
|
supervisely/convert/volume/sly/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
681
|
-
supervisely/convert/volume/sly/sly_volume_converter.py,sha256=
|
|
681
|
+
supervisely/convert/volume/sly/sly_volume_converter.py,sha256=TI1i_aVYFFoqLHqVzCXnFeR6xobhGcgN_xWFZcpRqbE,6730
|
|
682
682
|
supervisely/convert/volume/sly/sly_volume_helper.py,sha256=gUY0GW3zDMlO2y-zQQG36uoXMrKkKz4-ErM1CDxFCxE,5620
|
|
683
683
|
supervisely/decorators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
684
684
|
supervisely/decorators/inference.py,sha256=p0fBSg3ek2tt29h7OxQwhtvLcBhKe9kSgA8G5zZHXjE,13777
|
|
@@ -1047,7 +1047,7 @@ supervisely/project/project_type.py,sha256=7mQ7zg6r7Bm2oFn5aR8n_PeLqMmOaPZd6ph7Z
|
|
|
1047
1047
|
supervisely/project/readme_template.md,sha256=SFAfNF_uxSBJJ45A8qZ0MRuHnwSE4Gu_Z7UJqPMgRzg,9254
|
|
1048
1048
|
supervisely/project/upload.py,sha256=ys95MXFh-rtq-EAsNsiRi3wgbFUCEsY2un3_bd5hJkE,3753
|
|
1049
1049
|
supervisely/project/video_project.py,sha256=7i8__1zoU2Uryicjfa2_7p3JLnSPTv14ctLJPQGgnPY,66315
|
|
1050
|
-
supervisely/project/volume_project.py,sha256=
|
|
1050
|
+
supervisely/project/volume_project.py,sha256=BhFDE6GTxbhuJ-y4Bum-70bjRJ0FiIowkMru7PZ-0mk,23548
|
|
1051
1051
|
supervisely/pyscripts_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
1052
1052
|
supervisely/pyscripts_utils/utils.py,sha256=scEwHJvHRQa8NHIOn2eTwH6-Zc8CGdLoxM-WzH9jcRo,314
|
|
1053
1053
|
supervisely/report/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -1098,8 +1098,8 @@ supervisely/volume_annotation/constants.py,sha256=BdFIh56fy7vzLIjt0gH8xP01EIU-qg
|
|
|
1098
1098
|
supervisely/volume_annotation/plane.py,sha256=wyezAcc8tLp38O44CwWY0wjdQxf3VjRdFLWooCrk-Nw,16301
|
|
1099
1099
|
supervisely/volume_annotation/slice.py,sha256=9m3jtUYz4PYKV3rgbeh2ofDebkyg4TomNbkC6BwZ0lA,4635
|
|
1100
1100
|
supervisely/volume_annotation/volume_annotation.py,sha256=pGu6n8_5JkFpir4HTVRf302gGD2EqJ96Gh4M0_236Qg,32047
|
|
1101
|
-
supervisely/volume_annotation/volume_figure.py,sha256=
|
|
1102
|
-
supervisely/volume_annotation/volume_object.py,sha256=
|
|
1101
|
+
supervisely/volume_annotation/volume_figure.py,sha256=xA5AFLDHjW8ZVul9FYk6J0NnCHzLqkZhKeasCRyiPDU,26982
|
|
1102
|
+
supervisely/volume_annotation/volume_object.py,sha256=m2nHZVt_6sWRs62y5x01V_FcCVnmfPeGCyCr8lXqahE,4241
|
|
1103
1103
|
supervisely/volume_annotation/volume_object_collection.py,sha256=Tc4AovntgoFj5hpTLBv7pCQ3eL0BjorOVpOh2nAE_tA,5706
|
|
1104
1104
|
supervisely/volume_annotation/volume_tag.py,sha256=MEk1ky7X8zWe2JgV-j8jXt14e8yu2g1kScU26n9lOMk,9494
|
|
1105
1105
|
supervisely/volume_annotation/volume_tag_collection.py,sha256=b19ALxQc6qNRwlkbGijQIAL0q79ulh7IPZDsOivvO78,5827
|
|
@@ -1114,9 +1114,9 @@ supervisely/worker_proto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
|
|
|
1114
1114
|
supervisely/worker_proto/worker_api_pb2.py,sha256=VQfi5JRBHs2pFCK1snec3JECgGnua3Xjqw_-b3aFxuM,59142
|
|
1115
1115
|
supervisely/worker_proto/worker_api_pb2_grpc.py,sha256=3BwQXOaP9qpdi0Dt9EKG--Lm8KGN0C5AgmUfRv77_Jk,28940
|
|
1116
1116
|
supervisely_lib/__init__.py,sha256=7-3QnN8Zf0wj8NCr2oJmqoQWMKKPKTECvjH9pd2S5vY,159
|
|
1117
|
-
supervisely-6.73.
|
|
1118
|
-
supervisely-6.73.
|
|
1119
|
-
supervisely-6.73.
|
|
1120
|
-
supervisely-6.73.
|
|
1121
|
-
supervisely-6.73.
|
|
1122
|
-
supervisely-6.73.
|
|
1117
|
+
supervisely-6.73.394.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
1118
|
+
supervisely-6.73.394.dist-info/METADATA,sha256=O20x_UllEknnW4feMLTsIIfJC7a2pAgFVusQhNkYV_w,35254
|
|
1119
|
+
supervisely-6.73.394.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
|
|
1120
|
+
supervisely-6.73.394.dist-info/entry_points.txt,sha256=U96-5Hxrp2ApRjnCoUiUhWMqijqh8zLR03sEhWtAcms,102
|
|
1121
|
+
supervisely-6.73.394.dist-info/top_level.txt,sha256=kcFVwb7SXtfqZifrZaSE3owHExX4gcNYe7Q2uoby084,28
|
|
1122
|
+
supervisely-6.73.394.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|