supervisely 6.73.324__py3-none-any.whl → 6.73.326__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/file_api.py +3 -1
- supervisely/convert/converter.py +44 -6
- supervisely/convert/volume/__init__.py +3 -0
- supervisely/convert/volume/nii/nii_planes_volume_converter.py +162 -0
- supervisely/convert/volume/nii/nii_volume_converter.py +108 -40
- supervisely/convert/volume/nii/nii_volume_helper.py +61 -0
- supervisely/volume/volume.py +19 -21
- {supervisely-6.73.324.dist-info → supervisely-6.73.326.dist-info}/METADATA +1 -1
- {supervisely-6.73.324.dist-info → supervisely-6.73.326.dist-info}/RECORD +13 -12
- {supervisely-6.73.324.dist-info → supervisely-6.73.326.dist-info}/LICENSE +0 -0
- {supervisely-6.73.324.dist-info → supervisely-6.73.326.dist-info}/WHEEL +0 -0
- {supervisely-6.73.324.dist-info → supervisely-6.73.326.dist-info}/entry_points.txt +0 -0
- {supervisely-6.73.324.dist-info → supervisely-6.73.326.dist-info}/top_level.txt +0 -0
supervisely/api/file_api.py
CHANGED
|
@@ -25,6 +25,7 @@ import supervisely.io.env as env
|
|
|
25
25
|
import supervisely.io.fs as sly_fs
|
|
26
26
|
from supervisely._utils import batched, rand_str, run_coroutine
|
|
27
27
|
from supervisely.api.module_api import ApiField, ModuleApiBase
|
|
28
|
+
from supervisely.api.remote_storage_api import RemoteStorageApi
|
|
28
29
|
from supervisely.io.fs import (
|
|
29
30
|
ensure_base_path,
|
|
30
31
|
get_file_ext,
|
|
@@ -1420,7 +1421,8 @@ class FileApi(ModuleApiBase):
|
|
|
1420
1421
|
api.file.upload_directory(9, local_path, path_to_dir)
|
|
1421
1422
|
"""
|
|
1422
1423
|
if not remote_dir.startswith("/"):
|
|
1423
|
-
|
|
1424
|
+
if not RemoteStorageApi.is_bucket_url(remote_dir):
|
|
1425
|
+
remote_dir = "/" + remote_dir
|
|
1424
1426
|
|
|
1425
1427
|
if self.dir_exists(team_id, remote_dir):
|
|
1426
1428
|
if change_name_if_conflict is True:
|
supervisely/convert/converter.py
CHANGED
|
@@ -57,12 +57,12 @@ class ImportManager:
|
|
|
57
57
|
self._labeling_interface = labeling_interface
|
|
58
58
|
self._upload_as_links = upload_as_links
|
|
59
59
|
self._remote_files_map = {}
|
|
60
|
+
self._modality = project_type
|
|
60
61
|
|
|
61
62
|
self._input_data = self._prepare_input_data(input_data)
|
|
62
63
|
self._unpack_archives(self._input_data)
|
|
63
64
|
remove_junk_from_dir(self._input_data)
|
|
64
65
|
|
|
65
|
-
self._modality = project_type
|
|
66
66
|
self._converter = self.get_converter()
|
|
67
67
|
if isinstance(self._converter, (HighColorDepthImageConverter, CSVConverter)):
|
|
68
68
|
self._converter.team_id = self._team_id
|
|
@@ -112,17 +112,27 @@ class ImportManager:
|
|
|
112
112
|
logger.info(f"Input data is a local file: {input_data}. Will use its directory")
|
|
113
113
|
return os.path.dirname(input_data)
|
|
114
114
|
elif self._api.storage.exists(self._team_id, input_data):
|
|
115
|
-
if self._upload_as_links
|
|
115
|
+
if self._upload_as_links and str(self._modality) in [
|
|
116
|
+
ProjectType.IMAGES.value,
|
|
117
|
+
ProjectType.VIDEOS.value,
|
|
118
|
+
]:
|
|
116
119
|
logger.info(f"Input data is a remote file: {input_data}. Scanning...")
|
|
117
|
-
return self.
|
|
120
|
+
return self._reproduce_remote_files(input_data)
|
|
118
121
|
else:
|
|
122
|
+
if self._upload_as_links and str(self._modality) == ProjectType.VOLUMES.value:
|
|
123
|
+
self._scan_remote_files(input_data)
|
|
119
124
|
logger.info(f"Input data is a remote file: {input_data}. Downloading...")
|
|
120
125
|
return self._download_input_data(input_data)
|
|
121
126
|
elif self._api.storage.dir_exists(self._team_id, input_data):
|
|
122
|
-
if self._upload_as_links
|
|
127
|
+
if self._upload_as_links and str(self._modality) in [
|
|
128
|
+
ProjectType.IMAGES.value,
|
|
129
|
+
ProjectType.VIDEOS.value,
|
|
130
|
+
]:
|
|
123
131
|
logger.info(f"Input data is a remote directory: {input_data}. Scanning...")
|
|
124
|
-
return self.
|
|
132
|
+
return self._reproduce_remote_files(input_data, is_dir=True)
|
|
125
133
|
else:
|
|
134
|
+
if self._upload_as_links and str(self._modality) == ProjectType.VOLUMES.value:
|
|
135
|
+
self._scan_remote_files(input_data, is_dir=True)
|
|
126
136
|
logger.info(f"Input data is a remote directory: {input_data}. Downloading...")
|
|
127
137
|
return self._download_input_data(input_data, is_dir=True)
|
|
128
138
|
else:
|
|
@@ -160,7 +170,35 @@ class ImportManager:
|
|
|
160
170
|
return local_path
|
|
161
171
|
|
|
162
172
|
def _scan_remote_files(self, remote_path, is_dir=False):
|
|
163
|
-
"""
|
|
173
|
+
"""
|
|
174
|
+
Scan remote directory. Collect local-remote paths mapping
|
|
175
|
+
Will be used to save relations between uploaded files and remote files (for volumes).
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
dir_path = remote_path.rstrip("/") if is_dir else os.path.dirname(remote_path)
|
|
179
|
+
dir_name = os.path.basename(dir_path)
|
|
180
|
+
|
|
181
|
+
local_path = os.path.join(get_data_dir(), dir_name)
|
|
182
|
+
|
|
183
|
+
if is_dir:
|
|
184
|
+
files = self._api.storage.list(self._team_id, remote_path, include_folders=False)
|
|
185
|
+
else:
|
|
186
|
+
files = [self._api.storage.get_info_by_path(self._team_id, remote_path)]
|
|
187
|
+
|
|
188
|
+
unique_directories = set()
|
|
189
|
+
for file in files:
|
|
190
|
+
new_path = file.path.replace(dir_path, local_path)
|
|
191
|
+
self._remote_files_map[new_path] = file.path
|
|
192
|
+
unique_directories.add(str(Path(file.path).parent))
|
|
193
|
+
|
|
194
|
+
logger.info(f"Scanned remote directories:\n - " + "\n - ".join(unique_directories))
|
|
195
|
+
return local_path
|
|
196
|
+
|
|
197
|
+
def _reproduce_remote_files(self, remote_path, is_dir=False):
|
|
198
|
+
"""
|
|
199
|
+
Scan remote directory and create dummy structure locally.
|
|
200
|
+
Will be used to detect annotation format (by dataset structure) remotely.
|
|
201
|
+
"""
|
|
164
202
|
|
|
165
203
|
dir_path = remote_path.rstrip("/") if is_dir else os.path.dirname(remote_path)
|
|
166
204
|
dir_name = os.path.basename(dir_path)
|
|
@@ -2,3 +2,6 @@
|
|
|
2
2
|
from supervisely.convert.volume.sly.sly_volume_converter import SLYVolumeConverter
|
|
3
3
|
from supervisely.convert.volume.dicom.dicom_converter import DICOMConverter
|
|
4
4
|
from supervisely.convert.volume.nii.nii_volume_converter import NiiConverter
|
|
5
|
+
from supervisely.convert.volume.nii.nii_planes_volume_converter import (
|
|
6
|
+
NiiPlaneStructuredConverter,
|
|
7
|
+
)
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from supervisely import ProjectMeta, logger
|
|
6
|
+
from supervisely.annotation.obj_class import ObjClass
|
|
7
|
+
from supervisely.convert.volume.nii import nii_volume_helper as helper
|
|
8
|
+
from supervisely.convert.volume.nii.nii_volume_converter import NiiConverter
|
|
9
|
+
from supervisely.convert.volume.volume_converter import VolumeConverter
|
|
10
|
+
from supervisely.geometry.mask_3d import Mask3D
|
|
11
|
+
from supervisely.io.fs import get_file_ext, get_file_name
|
|
12
|
+
from supervisely.volume.volume import is_nifti_file
|
|
13
|
+
from supervisely.volume_annotation.volume_annotation import VolumeAnnotation
|
|
14
|
+
from supervisely.volume_annotation.volume_object import VolumeObject
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class NiiPlaneStructuredConverter(NiiConverter, VolumeConverter):
|
|
18
|
+
"""Convert NIfTI 3D volume file to Supervisely format.
|
|
19
|
+
The NIfTI file should be structured as follows:
|
|
20
|
+
- <prefix>_anatomic_<idx>.nii (or .nii.gz)
|
|
21
|
+
- <prefix>_inference_<idx>.nii (or .nii.gz)
|
|
22
|
+
where <prefix> is one of the following: cor, sag, axl
|
|
23
|
+
<idx> is the index of the volume (to match volumes with annotations)
|
|
24
|
+
|
|
25
|
+
Supports .nii and .nii.gz files.
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
📂 .
|
|
29
|
+
├── 🩻 axl_anatomic_1.nii
|
|
30
|
+
├── 🩻 axl_inference_1.nii class 1 (may contain multiple instances of the same class)
|
|
31
|
+
├── 🩻 cor_anatomic_1.nii
|
|
32
|
+
├── 🩻 cor_inference_1.nii class 1
|
|
33
|
+
├── 🩻 sag_anatomic_1.nii
|
|
34
|
+
├── 🩻 sag_inference_1.nii class 1
|
|
35
|
+
├── 🩻 sag_inference_2.nii class 2
|
|
36
|
+
└── 🩻 sag_inference_3.nii class 3
|
|
37
|
+
|
|
38
|
+
Additionally, if a TXT file with class color map is present, it will be used to
|
|
39
|
+
create the classes with names and colors corresponding to the pixel values in the NIfTI files.
|
|
40
|
+
The TXT file should be structured as follows:
|
|
41
|
+
|
|
42
|
+
```txt
|
|
43
|
+
1 Femur 255 0 0
|
|
44
|
+
2 Femoral cartilage 0 255 0
|
|
45
|
+
3 Tibia 0 0 255
|
|
46
|
+
4 Tibia cartilage 255 255 0
|
|
47
|
+
5 Patella 0 255 255
|
|
48
|
+
6 Patellar cartilage 255 0 255
|
|
49
|
+
7 Miniscus 175 175 175
|
|
50
|
+
```
|
|
51
|
+
where 1, 2, ... are the pixel values in the NIfTI files
|
|
52
|
+
Femur, Femoral cartilage, ... are the names of the classes
|
|
53
|
+
255, 0, 0, ... are the RGB colors of the classes
|
|
54
|
+
The class name will be used to create the corresponding ObjClass in Supervisely.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
class Item(VolumeConverter.BaseItem):
|
|
58
|
+
def __init__(self, *args, **kwargs):
|
|
59
|
+
super().__init__(*args, **kwargs)
|
|
60
|
+
self._is_semantic = False
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def is_semantic(self) -> bool:
|
|
64
|
+
return self._is_semantic
|
|
65
|
+
|
|
66
|
+
@is_semantic.setter
|
|
67
|
+
def is_semantic(self, value: bool):
|
|
68
|
+
self._is_semantic = value
|
|
69
|
+
|
|
70
|
+
def validate_format(self) -> bool:
|
|
71
|
+
# create Items
|
|
72
|
+
converted_dir_name = "converted"
|
|
73
|
+
|
|
74
|
+
volumes_dict = defaultdict(list)
|
|
75
|
+
ann_dict = defaultdict(list)
|
|
76
|
+
cls_color_map = None
|
|
77
|
+
|
|
78
|
+
for root, _, files in os.walk(self._input_data):
|
|
79
|
+
if converted_dir_name in root:
|
|
80
|
+
continue
|
|
81
|
+
for file in files:
|
|
82
|
+
path = os.path.join(root, file)
|
|
83
|
+
if is_nifti_file(path):
|
|
84
|
+
full_name = get_file_name(path)
|
|
85
|
+
if full_name.endswith(".nii"):
|
|
86
|
+
full_name = get_file_name(full_name)
|
|
87
|
+
prefix = full_name.split("_")[0]
|
|
88
|
+
if prefix not in helper.PlanePrefix.values():
|
|
89
|
+
continue
|
|
90
|
+
name = full_name.split("_")[1]
|
|
91
|
+
if name in helper.LABEL_NAME or name[:-1] in helper.LABEL_NAME:
|
|
92
|
+
ann_dict[prefix].append(path)
|
|
93
|
+
else:
|
|
94
|
+
volumes_dict[prefix].append(path)
|
|
95
|
+
ext = get_file_ext(path)
|
|
96
|
+
if ext == ".txt":
|
|
97
|
+
cls_color_map = helper.read_cls_color_map(path)
|
|
98
|
+
if cls_color_map is None:
|
|
99
|
+
logger.warning(f"Failed to read class color map from {path}.")
|
|
100
|
+
|
|
101
|
+
self._items = []
|
|
102
|
+
for prefix, paths in volumes_dict.items():
|
|
103
|
+
if len(paths) == 1:
|
|
104
|
+
item = self.Item(item_path=paths[0])
|
|
105
|
+
item.ann_data = ann_dict.get(prefix, [])
|
|
106
|
+
item.is_semantic = len(item.ann_data) == 1
|
|
107
|
+
if cls_color_map is not None:
|
|
108
|
+
item.custom_data["cls_color_map"] = cls_color_map
|
|
109
|
+
self._items.append(item)
|
|
110
|
+
elif len(paths) > 1:
|
|
111
|
+
logger.info(
|
|
112
|
+
f"Found {len(paths)} volumes with prefix {prefix}. Will try to match them by directories."
|
|
113
|
+
)
|
|
114
|
+
for path in paths:
|
|
115
|
+
item = self.Item(item_path=path)
|
|
116
|
+
possible_ann_paths = []
|
|
117
|
+
for ann_path in ann_dict.get(prefix):
|
|
118
|
+
if Path(ann_path).parent == Path(path).parent:
|
|
119
|
+
possible_ann_paths.append(ann_path)
|
|
120
|
+
item.ann_data = possible_ann_paths
|
|
121
|
+
item.is_semantic = len(possible_ann_paths) == 1
|
|
122
|
+
if cls_color_map is not None:
|
|
123
|
+
item.custom_data["cls_color_map"] = cls_color_map
|
|
124
|
+
self._items.append(item)
|
|
125
|
+
self._meta = ProjectMeta()
|
|
126
|
+
return self.items_count > 0
|
|
127
|
+
|
|
128
|
+
def to_supervisely(
|
|
129
|
+
self,
|
|
130
|
+
item: VolumeConverter.Item,
|
|
131
|
+
meta: ProjectMeta = None,
|
|
132
|
+
renamed_classes: dict = None,
|
|
133
|
+
renamed_tags: dict = None,
|
|
134
|
+
) -> VolumeAnnotation:
|
|
135
|
+
"""Convert to Supervisely format."""
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
objs = []
|
|
139
|
+
spatial_figures = []
|
|
140
|
+
for idx, ann_path in enumerate(item.ann_data, start=1):
|
|
141
|
+
for mask, pixel_id in helper.get_annotation_from_nii(ann_path):
|
|
142
|
+
class_id = pixel_id if item.is_semantic else idx
|
|
143
|
+
class_name = f"Segment_{class_id}"
|
|
144
|
+
color = None
|
|
145
|
+
if item.custom_data.get("cls_color_map") is not None:
|
|
146
|
+
class_info = item.custom_data["cls_color_map"].get(class_id)
|
|
147
|
+
if class_info is not None:
|
|
148
|
+
class_name, color = class_info
|
|
149
|
+
class_name = renamed_classes.get(class_name, class_name)
|
|
150
|
+
obj_class = meta.get_obj_class(class_name)
|
|
151
|
+
if obj_class is None:
|
|
152
|
+
obj_class = ObjClass(class_name, Mask3D, color)
|
|
153
|
+
meta = meta.add_obj_class(obj_class)
|
|
154
|
+
self._meta_changed = True
|
|
155
|
+
self._meta = meta
|
|
156
|
+
obj = VolumeObject(obj_class, mask_3d=mask)
|
|
157
|
+
spatial_figures.append(obj.figure)
|
|
158
|
+
objs.append(obj)
|
|
159
|
+
return VolumeAnnotation(item.volume_meta, objects=objs, spatial_figures=spatial_figures)
|
|
160
|
+
except Exception as e:
|
|
161
|
+
logger.warning(f"Failed to convert {item.path} to Supervisely format: {e}")
|
|
162
|
+
return item.create_empty_annotation()
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
|
|
4
|
-
import magic
|
|
5
|
-
|
|
6
4
|
from supervisely import ProjectMeta, generate_free_name, logger
|
|
7
5
|
from supervisely._utils import batched, is_development
|
|
8
6
|
from supervisely.annotation.obj_class import ObjClass
|
|
9
|
-
from supervisely.annotation.obj_class_collection import ObjClassCollection
|
|
10
7
|
from supervisely.api.api import Api
|
|
11
8
|
from supervisely.convert.base_converter import AvailableVolumeConverters
|
|
12
9
|
from supervisely.convert.volume.nii import nii_volume_helper as helper
|
|
@@ -18,12 +15,47 @@ from supervisely.io.fs import (
|
|
|
18
15
|
get_file_name_with_ext,
|
|
19
16
|
list_files,
|
|
20
17
|
)
|
|
21
|
-
from supervisely.
|
|
18
|
+
from supervisely.task.progress import tqdm_sly
|
|
19
|
+
from supervisely.volume.volume import is_nifti_file, read_nrrd_serie_volume_np
|
|
22
20
|
from supervisely.volume_annotation.volume_annotation import VolumeAnnotation
|
|
23
21
|
from supervisely.volume_annotation.volume_object import VolumeObject
|
|
24
22
|
|
|
25
23
|
|
|
26
24
|
class NiiConverter(VolumeConverter):
|
|
25
|
+
"""
|
|
26
|
+
Convert NIfTI 3D volume file to Supervisely format.
|
|
27
|
+
Supports .nii and .nii.gz files.
|
|
28
|
+
|
|
29
|
+
The NIfTI file should be structured as follows:
|
|
30
|
+
- <volume_name>.nii
|
|
31
|
+
- <volume_name>/
|
|
32
|
+
- <cls_name_1>.nii
|
|
33
|
+
- <cls_name_2>.nii
|
|
34
|
+
- ...
|
|
35
|
+
- ...
|
|
36
|
+
|
|
37
|
+
where <volume_name> is the name of the volume
|
|
38
|
+
If the volume has annotations, they should be in the corresponding directory
|
|
39
|
+
with the same name as the volume (without extension)
|
|
40
|
+
<cls_name> is the name of the annotation class
|
|
41
|
+
<cls_name>.nii:
|
|
42
|
+
- represent objects of the single class
|
|
43
|
+
- should be unique for the current volume (e.g. tumor.nii.gz, lung.nii.gz)
|
|
44
|
+
- can contain multiple objects of the class (each object should be represented by a different value in the mask)
|
|
45
|
+
|
|
46
|
+
Example:
|
|
47
|
+
📂 .
|
|
48
|
+
├── 📂 CTChest
|
|
49
|
+
│ ├── 🩻 lung.nii.gz
|
|
50
|
+
│ └── 🩻 tumor.nii.gz
|
|
51
|
+
├── 🩻 CTChest.nii.gz
|
|
52
|
+
└── 🩻 Spine.nii.gz
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, *args, **kwargs):
|
|
56
|
+
super().__init__(*args, **kwargs)
|
|
57
|
+
self._supports_links = True
|
|
58
|
+
self._meta_changed = False
|
|
27
59
|
|
|
28
60
|
def __str__(self) -> str:
|
|
29
61
|
return AvailableVolumeConverters.NII
|
|
@@ -34,6 +66,9 @@ class NiiConverter(VolumeConverter):
|
|
|
34
66
|
# nrrds_dict = {}
|
|
35
67
|
nifti_dict = {}
|
|
36
68
|
nifti_dirs = {}
|
|
69
|
+
|
|
70
|
+
planes_detected = {p: False for p in helper.PlanePrefix.values()}
|
|
71
|
+
|
|
37
72
|
for root, _, files in os.walk(self._input_data):
|
|
38
73
|
dir_name = os.path.basename(root)
|
|
39
74
|
nifti_dirs[dir_name] = root
|
|
@@ -41,13 +76,17 @@ class NiiConverter(VolumeConverter):
|
|
|
41
76
|
continue
|
|
42
77
|
for file in files:
|
|
43
78
|
path = os.path.join(root, file)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if
|
|
47
|
-
name = get_file_name(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
79
|
+
if is_nifti_file(path): # is nifti
|
|
80
|
+
name = get_file_name(path)
|
|
81
|
+
if name.endswith(".nii"):
|
|
82
|
+
name = get_file_name(name)
|
|
83
|
+
nifti_dict[name] = path
|
|
84
|
+
for prefix in planes_detected.keys():
|
|
85
|
+
if name.startswith(prefix):
|
|
86
|
+
planes_detected[prefix] = True
|
|
87
|
+
|
|
88
|
+
if any(planes_detected.values()):
|
|
89
|
+
return False
|
|
51
90
|
|
|
52
91
|
self._items = []
|
|
53
92
|
skip_files = []
|
|
@@ -69,6 +108,39 @@ class NiiConverter(VolumeConverter):
|
|
|
69
108
|
self._meta = ProjectMeta()
|
|
70
109
|
return self.items_count > 0
|
|
71
110
|
|
|
111
|
+
def to_supervisely(
|
|
112
|
+
self,
|
|
113
|
+
item: VolumeConverter.Item,
|
|
114
|
+
meta: ProjectMeta = None,
|
|
115
|
+
renamed_classes: dict = None,
|
|
116
|
+
renamed_tags: dict = None,
|
|
117
|
+
) -> VolumeAnnotation:
|
|
118
|
+
"""Convert to Supervisely format."""
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
objs = []
|
|
122
|
+
spatial_figures = []
|
|
123
|
+
for ann_path in item.ann_data:
|
|
124
|
+
ann_name = get_file_name(ann_path)
|
|
125
|
+
if ann_name.endswith(".nii"):
|
|
126
|
+
ann_name = get_file_name(ann_name)
|
|
127
|
+
|
|
128
|
+
ann_name = renamed_classes.get(ann_name, ann_name)
|
|
129
|
+
for mask, _ in helper.get_annotation_from_nii(ann_path):
|
|
130
|
+
obj_class = meta.get_obj_class(ann_name)
|
|
131
|
+
if obj_class is None:
|
|
132
|
+
obj_class = ObjClass(ann_name, Mask3D)
|
|
133
|
+
meta = meta.add_obj_class(obj_class)
|
|
134
|
+
self._meta_changed = True
|
|
135
|
+
self._meta = meta
|
|
136
|
+
obj = VolumeObject(obj_class, mask_3d=mask)
|
|
137
|
+
spatial_figures.append(obj.figure)
|
|
138
|
+
objs.append(obj)
|
|
139
|
+
return VolumeAnnotation(item.volume_meta, objects=objs, spatial_figures=spatial_figures)
|
|
140
|
+
except Exception as e:
|
|
141
|
+
logger.warning(f"Failed to convert {item.path} to Supervisely format: {e}")
|
|
142
|
+
return item.create_empty_annotation()
|
|
143
|
+
|
|
72
144
|
def upload_dataset(
|
|
73
145
|
self,
|
|
74
146
|
api: Api,
|
|
@@ -78,7 +150,7 @@ class NiiConverter(VolumeConverter):
|
|
|
78
150
|
):
|
|
79
151
|
"""Upload converted data to Supervisely"""
|
|
80
152
|
|
|
81
|
-
meta, renamed_classes,
|
|
153
|
+
meta, renamed_classes, _ = self.merge_metas_with_conflicts(api, dataset_id)
|
|
82
154
|
|
|
83
155
|
existing_names = set([vol.name for vol in api.volume.get_list(dataset_id)])
|
|
84
156
|
|
|
@@ -91,14 +163,17 @@ class NiiConverter(VolumeConverter):
|
|
|
91
163
|
|
|
92
164
|
converted_dir_name = "converted"
|
|
93
165
|
converted_dir = os.path.join(self._input_data, converted_dir_name)
|
|
94
|
-
meta_changed = False
|
|
95
166
|
|
|
96
167
|
for batch in batched(self._items, batch_size=batch_size):
|
|
97
168
|
item_names = []
|
|
98
169
|
item_paths = []
|
|
99
170
|
|
|
100
171
|
for item in batch:
|
|
101
|
-
#
|
|
172
|
+
# if self._upload_as_links:
|
|
173
|
+
# remote_path = self.remote_files_map.get(item.path)
|
|
174
|
+
# if remote_path is not None:
|
|
175
|
+
# item.custom_data = {"remote_path": remote_path}
|
|
176
|
+
|
|
102
177
|
item.path = helper.nifti_to_nrrd(item.path, converted_dir)
|
|
103
178
|
ext = get_file_ext(item.path)
|
|
104
179
|
if ext.lower() != ext:
|
|
@@ -112,35 +187,28 @@ class NiiConverter(VolumeConverter):
|
|
|
112
187
|
item_names.append(item.name)
|
|
113
188
|
item_paths.append(item.path)
|
|
114
189
|
|
|
115
|
-
|
|
116
|
-
|
|
190
|
+
# upload volume
|
|
191
|
+
volume_np, volume_meta = read_nrrd_serie_volume_np(item.path)
|
|
192
|
+
progress_nrrd = tqdm_sly(
|
|
193
|
+
desc=f"Uploading volume '{item.name}'",
|
|
194
|
+
total=sum(volume_np.shape),
|
|
195
|
+
leave=True if progress_cb is None else False,
|
|
196
|
+
position=1,
|
|
117
197
|
)
|
|
198
|
+
# if item.custom_data is not None:
|
|
199
|
+
# volume_meta.update(item.custom_data)
|
|
200
|
+
api.volume.upload_np(dataset_id, item.name, volume_np, volume_meta, progress_nrrd)
|
|
201
|
+
info = api.volume.get_info_by_name(dataset_id, item.name)
|
|
202
|
+
item.volume_meta = info.meta
|
|
118
203
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
ann_name = get_file_name(ann_name)
|
|
126
|
-
for mask, _ in helper.get_annotation_from_nii(ann_path):
|
|
127
|
-
obj_class = meta.get_obj_class(ann_name)
|
|
128
|
-
if obj_class is None:
|
|
129
|
-
obj_class = ObjClass(ann_name, Mask3D)
|
|
130
|
-
meta = meta.add_obj_class(obj_class)
|
|
131
|
-
meta_changed = True
|
|
132
|
-
obj = VolumeObject(obj_class, mask_3d=mask)
|
|
133
|
-
spatial_figures.append(obj.figure)
|
|
134
|
-
objs.append(obj)
|
|
135
|
-
ann = VolumeAnnotation(
|
|
136
|
-
volume_info.meta, objects=objs, spatial_figures=spatial_figures
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
if meta_changed:
|
|
140
|
-
self._meta = meta
|
|
141
|
-
_, _, _ = self.merge_metas_with_conflicts(api, dataset_id)
|
|
204
|
+
# create and upload annotation
|
|
205
|
+
if item.ann_data is not None:
|
|
206
|
+
ann = self.to_supervisely(item, meta, renamed_classes, None)
|
|
207
|
+
|
|
208
|
+
if self._meta_changed:
|
|
209
|
+
meta, renamed_classes, _ = self.merge_metas_with_conflicts(api, dataset_id)
|
|
142
210
|
|
|
143
|
-
api.volume.annotation.append(
|
|
211
|
+
api.volume.annotation.append(info.id, ann)
|
|
144
212
|
|
|
145
213
|
if log_progress:
|
|
146
214
|
progress_cb(len(batch))
|
|
@@ -4,10 +4,71 @@ from typing import Generator
|
|
|
4
4
|
import nrrd
|
|
5
5
|
import numpy as np
|
|
6
6
|
|
|
7
|
+
from supervisely.collection.str_enum import StrEnum
|
|
7
8
|
from supervisely.geometry.mask_3d import Mask3D
|
|
8
9
|
from supervisely.io.fs import ensure_base_path, get_file_ext, get_file_name
|
|
10
|
+
from supervisely.sly_logger import logger
|
|
9
11
|
from supervisely.volume.volume import convert_3d_nifti_to_nrrd
|
|
10
12
|
|
|
13
|
+
VOLUME_NAME = "anatomic"
|
|
14
|
+
LABEL_NAME = ["inference", "label", "annotation", "mask", "segmentation"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PlanePrefix(str, StrEnum):
|
|
18
|
+
"""Prefix for plane names."""
|
|
19
|
+
|
|
20
|
+
CORONAL = "cor"
|
|
21
|
+
SAGITTAL = "sag"
|
|
22
|
+
AXIAL = "axl"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def read_cls_color_map(path: str) -> dict:
|
|
26
|
+
"""Read class color map from TXT file.
|
|
27
|
+
|
|
28
|
+
```txt
|
|
29
|
+
1 Femur 255 0 0
|
|
30
|
+
2 Femoral cartilage 0 255 0
|
|
31
|
+
3 Tibia 0 0 255
|
|
32
|
+
4 Tibia cartilage 255 255 0
|
|
33
|
+
5 Patella 0 255 255
|
|
34
|
+
6 Patellar cartilage 255 0 255
|
|
35
|
+
7 Miniscus 175 175 175
|
|
36
|
+
```
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
cls_color_map = {}
|
|
40
|
+
if not os.path.exists(path):
|
|
41
|
+
return None
|
|
42
|
+
try:
|
|
43
|
+
with open(path, "r") as file:
|
|
44
|
+
for line in file:
|
|
45
|
+
parts = line.strip().split()
|
|
46
|
+
color = None
|
|
47
|
+
try:
|
|
48
|
+
color = list(map(int, parts[-3:]))
|
|
49
|
+
except:
|
|
50
|
+
pass
|
|
51
|
+
cls_id = int(parts[0])
|
|
52
|
+
if not color:
|
|
53
|
+
cls_name = " ".join(parts[1:])
|
|
54
|
+
else:
|
|
55
|
+
cls_name = " ".join(parts[1:-3])
|
|
56
|
+
if cls_id in cls_color_map:
|
|
57
|
+
logger.warning(f"Duplicate class ID {cls_id} found in color map.")
|
|
58
|
+
if cls_name in cls_color_map:
|
|
59
|
+
logger.warning(f"Duplicate class name {cls_name} found in color map.")
|
|
60
|
+
if len(color) != 3:
|
|
61
|
+
logger.warning(f"Invalid color format for class {cls_name}. Expected 3 values.")
|
|
62
|
+
if any(c < 0 or c > 255 for c in color):
|
|
63
|
+
logger.warning(
|
|
64
|
+
f"Invalid color value for class {cls_name}. Expected values between 0 and 255."
|
|
65
|
+
)
|
|
66
|
+
cls_color_map[cls_id] = (cls_name, color)
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logger.warning(f"Failed to read class color map from {path}: {e}")
|
|
69
|
+
return None
|
|
70
|
+
return cls_color_map
|
|
71
|
+
|
|
11
72
|
|
|
12
73
|
def nifti_to_nrrd(nii_file_path: str, converted_dir: str) -> str:
|
|
13
74
|
"""Convert NIfTI 3D volume file to NRRD 3D volume file."""
|
supervisely/volume/volume.py
CHANGED
|
@@ -816,30 +816,28 @@ def convert_3d_nifti_to_nrrd(path: str) -> Tuple[np.ndarray, dict]:
|
|
|
816
816
|
path = "/home/admin/work/volumes/vol_01.nii"
|
|
817
817
|
data, header = sly.volume.convert_nifti_to_nrrd(path)
|
|
818
818
|
"""
|
|
819
|
+
import SimpleITK as sitk
|
|
819
820
|
|
|
820
|
-
|
|
821
|
+
nifti_image = sitk.ReadImage(path)
|
|
822
|
+
nifti_image = _sitk_image_orient_ras(nifti_image)
|
|
823
|
+
data = sitk.GetArrayFromImage(nifti_image)
|
|
824
|
+
data = np.transpose(data, (2, 1, 0))
|
|
825
|
+
|
|
826
|
+
direction = np.array(nifti_image.GetDirection()).reshape(3, 3)
|
|
827
|
+
spacing = np.array(nifti_image.GetSpacing())
|
|
828
|
+
origin = np.array(nifti_image.GetOrigin())
|
|
829
|
+
|
|
830
|
+
space_directions = (direction.T * spacing[:, None]).tolist()
|
|
821
831
|
|
|
822
|
-
orientation_map = {
|
|
823
|
-
('R', 'A', 'S'): "right-anterior-superior",
|
|
824
|
-
('L', 'P', 'S'): "left-posterior-superior",
|
|
825
|
-
('R', 'P', 'I'): "right-posterior-inferior",
|
|
826
|
-
('L', 'A', 'I'): "left-anterior-inferior"
|
|
827
|
-
}
|
|
828
|
-
nifti = nib.load(path)
|
|
829
|
-
reordered_to_ras_nifti = nib.as_closest_canonical(nifti)
|
|
830
|
-
data = reordered_to_ras_nifti.get_fdata()
|
|
831
|
-
affine = reordered_to_ras_nifti.affine
|
|
832
|
-
orientation = nib.aff2axcodes(affine)
|
|
833
|
-
space_directions = affine[:3, :3].tolist()
|
|
834
|
-
space_origin = affine[:3, 3].tolist()
|
|
835
832
|
header = {
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
833
|
+
"dimension": 3,
|
|
834
|
+
"space": "right-anterior-superior",
|
|
835
|
+
"sizes": list(data.shape),
|
|
836
|
+
"space directions": space_directions,
|
|
837
|
+
"endian": "little",
|
|
838
|
+
"encoding": "gzip",
|
|
839
|
+
"space origin": origin
|
|
840
|
+
}
|
|
843
841
|
return data, header
|
|
844
842
|
|
|
845
843
|
|
|
@@ -25,7 +25,7 @@ supervisely/api/annotation_api.py,sha256=kuk4qwojTJxYr2iqAKbW-QhWw_DFc4TsjA2Wc2M
|
|
|
25
25
|
supervisely/api/api.py,sha256=6TczKT1t0MWlbArSW31RmeyWP04pqngfUO_NrG5FETE,66287
|
|
26
26
|
supervisely/api/app_api.py,sha256=RsbVej8WxWVn9cNo5s3Fqd1symsCdsfOaKVBKEUapRY,71927
|
|
27
27
|
supervisely/api/dataset_api.py,sha256=GH7prDRJKyJlTv_7_Y-RkTwJN7ED4EkXNqqmi3iIdI4,41352
|
|
28
|
-
supervisely/api/file_api.py,sha256=
|
|
28
|
+
supervisely/api/file_api.py,sha256=bVWv6kf3B5n6qlB14HmUa6iUr8ara5cr-pPK8QC7XWg,92932
|
|
29
29
|
supervisely/api/github_api.py,sha256=NIexNjEer9H5rf5sw2LEZd7C1WR-tK4t6IZzsgeAAwQ,623
|
|
30
30
|
supervisely/api/image_annotation_tool_api.py,sha256=YcUo78jRDBJYvIjrd-Y6FJAasLta54nnxhyaGyanovA,5237
|
|
31
31
|
supervisely/api/image_api.py,sha256=WIML_6N1qgOWBm3acexmGSWz4hAaSxlYmUtbytROaP8,192375
|
|
@@ -566,7 +566,7 @@ supervisely/collection/key_indexed_collection.py,sha256=x2UVlkprspWhhae9oLUzjTWB
|
|
|
566
566
|
supervisely/collection/str_enum.py,sha256=Zp29yFGvnxC6oJRYNNlXhO2lTSdsriU1wiGHj6ahEJE,1250
|
|
567
567
|
supervisely/convert/__init__.py,sha256=ropgB1eebG2bfLoJyf2jp8Vv9UkFujaW3jVX-71ho1g,1353
|
|
568
568
|
supervisely/convert/base_converter.py,sha256=O2SP4I_Hd0aSn8kbOUocy8orkc_-iD-TQ-z4ieUqabA,18579
|
|
569
|
-
supervisely/convert/converter.py,sha256=
|
|
569
|
+
supervisely/convert/converter.py,sha256=ymhjzy75bhtpOTJSB7Xfq5tcfZjK_DMxJXIa_uuEitA,10668
|
|
570
570
|
supervisely/convert/image/__init__.py,sha256=JEuyaBiiyiYmEUYqdn8Mog5FVXpz0H1zFubKkOOm73I,1395
|
|
571
571
|
supervisely/convert/image/image_converter.py,sha256=8vak8ZoKTN1ye2ZmCTvCZ605-Rw1AFLIEo7bJMfnR68,10426
|
|
572
572
|
supervisely/convert/image/image_helper.py,sha256=fdV0edQD6hVGQ8TXn2JGDzsnrAXPDMacHBQsApzOME8,3677
|
|
@@ -658,14 +658,15 @@ supervisely/convert/video/mot/mot_converter.py,sha256=wXbv-9Psc2uVnhzHuOt5VnRIvS
|
|
|
658
658
|
supervisely/convert/video/sly/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
659
659
|
supervisely/convert/video/sly/sly_video_converter.py,sha256=S2qif7JFxqIi9VN_ez_iBtoJXpG9W6Ky2k5Er3-DtUo,4418
|
|
660
660
|
supervisely/convert/video/sly/sly_video_helper.py,sha256=D8PgoXpi0y3z-VEqvBLDf_gSUQ2hTL3irrfJyGhaV0Y,6758
|
|
661
|
-
supervisely/convert/volume/__init__.py,sha256=
|
|
661
|
+
supervisely/convert/volume/__init__.py,sha256=RpSYjufciJT6AdhI9Oqp70b3XoFTtSkxFNexoqeOPW4,353
|
|
662
662
|
supervisely/convert/volume/volume_converter.py,sha256=3jpt2Yn_G4FSP_vHFsJHQfYNQpT7q6ar_sRyr_xrPnA,5335
|
|
663
663
|
supervisely/convert/volume/dicom/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
664
664
|
supervisely/convert/volume/dicom/dicom_converter.py,sha256=__QP8fMAaq_BdWFYh1_nAYT2gpY1WwZzdlDj39YwHhw,3195
|
|
665
665
|
supervisely/convert/volume/dicom/dicom_helper.py,sha256=1EXmxl5Z8Xi3ZkZnfJ4EbiPCVyITSXUc0Cn_oo02pPE,1284
|
|
666
666
|
supervisely/convert/volume/nii/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
667
|
-
supervisely/convert/volume/nii/
|
|
668
|
-
supervisely/convert/volume/nii/
|
|
667
|
+
supervisely/convert/volume/nii/nii_planes_volume_converter.py,sha256=9TtN_AgCQgv16Olip6inFanCA5JlEEJ7JQf-0XjIw_Q,7091
|
|
668
|
+
supervisely/convert/volume/nii/nii_volume_converter.py,sha256=IZ6DJeLLbLAW-kifOJ_9ddV3h7gL3AswM2TTbXB9Os0,8476
|
|
669
|
+
supervisely/convert/volume/nii/nii_volume_helper.py,sha256=RvYab6Z530Qw-qTAsZ3WM8WZKqhijia9OC-g4_zOSEs,3142
|
|
669
670
|
supervisely/convert/volume/sly/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
670
671
|
supervisely/convert/volume/sly/sly_volume_converter.py,sha256=XmSuxnRqxchG87b244f3h0UHvOt6IkajMquL1drWlCM,5595
|
|
671
672
|
supervisely/convert/volume/sly/sly_volume_helper.py,sha256=gUY0GW3zDMlO2y-zQQG36uoXMrKkKz4-ErM1CDxFCxE,5620
|
|
@@ -1059,7 +1060,7 @@ supervisely/volume/__init__.py,sha256=EBZBY_5mzabXzMUQh5akusIGd16XnX9n8J0jIi_JmW
|
|
|
1059
1060
|
supervisely/volume/nrrd_encoder.py,sha256=1lqwwyqxEvctw1ysQ70x4xPSV1uy1g5YcH5CURwL7-c,4084
|
|
1060
1061
|
supervisely/volume/nrrd_loader.py,sha256=_yqahKcqSRxunHZ5LtnUWIRA7UvIhPKOhAUwYijSGY4,9065
|
|
1061
1062
|
supervisely/volume/stl_converter.py,sha256=WIMQgHO_u4JT58QdcMXcb_euF1BFhM7D52IVX_0QTxE,6285
|
|
1062
|
-
supervisely/volume/volume.py,sha256=
|
|
1063
|
+
supervisely/volume/volume.py,sha256=bUPrDQAr4ZIkSQMzpSWXjsHRqcXUq2Z2H6Fe1uLdYmw,25687
|
|
1063
1064
|
supervisely/volume_annotation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
1064
1065
|
supervisely/volume_annotation/constants.py,sha256=BdFIh56fy7vzLIjt0gH8xP01EIU-qgQIwbSHVUcABCU,569
|
|
1065
1066
|
supervisely/volume_annotation/plane.py,sha256=wyezAcc8tLp38O44CwWY0wjdQxf3VjRdFLWooCrk-Nw,16301
|
|
@@ -1081,9 +1082,9 @@ supervisely/worker_proto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
|
|
|
1081
1082
|
supervisely/worker_proto/worker_api_pb2.py,sha256=VQfi5JRBHs2pFCK1snec3JECgGnua3Xjqw_-b3aFxuM,59142
|
|
1082
1083
|
supervisely/worker_proto/worker_api_pb2_grpc.py,sha256=3BwQXOaP9qpdi0Dt9EKG--Lm8KGN0C5AgmUfRv77_Jk,28940
|
|
1083
1084
|
supervisely_lib/__init__.py,sha256=7-3QnN8Zf0wj8NCr2oJmqoQWMKKPKTECvjH9pd2S5vY,159
|
|
1084
|
-
supervisely-6.73.
|
|
1085
|
-
supervisely-6.73.
|
|
1086
|
-
supervisely-6.73.
|
|
1087
|
-
supervisely-6.73.
|
|
1088
|
-
supervisely-6.73.
|
|
1089
|
-
supervisely-6.73.
|
|
1085
|
+
supervisely-6.73.326.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
1086
|
+
supervisely-6.73.326.dist-info/METADATA,sha256=0hQAKuBU9KX8mYNuop6LSvLzz0RQmCGZSdFIqqgmJUE,33596
|
|
1087
|
+
supervisely-6.73.326.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
|
|
1088
|
+
supervisely-6.73.326.dist-info/entry_points.txt,sha256=U96-5Hxrp2ApRjnCoUiUhWMqijqh8zLR03sEhWtAcms,102
|
|
1089
|
+
supervisely-6.73.326.dist-info/top_level.txt,sha256=kcFVwb7SXtfqZifrZaSE3owHExX4gcNYe7Q2uoby084,28
|
|
1090
|
+
supervisely-6.73.326.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|