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.
@@ -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
- remote_dir = "/" + remote_dir
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:
@@ -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._scan_remote_files(input_data)
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._scan_remote_files(input_data, is_dir=True)
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
- """Scan remote directory and create dummy structure locally"""
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.volume.volume import is_nifti_file
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
- mime = magic.from_file(path, mime=True)
45
- if mime == "application/gzip" or mime == "application/octet-stream":
46
- if is_nifti_file(path): # is nifti
47
- name = get_file_name(path)
48
- if name.endswith(".nii"):
49
- name = get_file_name(name)
50
- nifti_dict[name] = path
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, renamed_tags = self.merge_metas_with_conflicts(api, dataset_id)
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
- # nii_path = item.path
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
- volume_info = api.volume.upload_nrrd_serie_path(
116
- dataset_id, name=item.name, path=item.path
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
- if isinstance(item.ann_data, list) and len(item.ann_data) > 0:
120
- objs = []
121
- spatial_figures = []
122
- for ann_path in item.ann_data:
123
- ann_name = get_file_name(ann_path)
124
- if ann_name.endswith(".nii"):
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(volume_info.id, ann)
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."""
@@ -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
- import nibabel as nib # pylint: disable=import-error
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
- "space": orientation_map.get(orientation, "unknown"),
837
- "space directions": space_directions,
838
- "space origin": space_origin,
839
- "sizes": data.shape,
840
- "type": str(data.dtype),
841
- "dimension": len(data.shape),
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: supervisely
3
- Version: 6.73.324
3
+ Version: 6.73.326
4
4
  Summary: Supervisely Python SDK.
5
5
  Home-page: https://github.com/supervisely/supervisely
6
6
  Author: Supervisely
@@ -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=S2xZAy36YzWA1R41SIlLXX9JwSf9ax18AIAIQGgHGlA,92801
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=tWxTDfFv7hwzQhUQrBxzfr6WP8FUGFX_ewg5T2HbUYo,8959
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=NjVfOa9uH1BdYvB-RynW6L28x0f_tqL9p7tHSIQ6Sso,245
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/nii_volume_converter.py,sha256=kI2JmeFuLfLWgYGCEozoaka1QH4TocnfgyN0em6maa0,5946
668
- supervisely/convert/volume/nii/nii_volume_helper.py,sha256=kzh20fsdeI8efA0vawW0M6Wh48nMlCLzHBQFuSNVFmc,1136
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=7ebCIICqZMwRP3ruRy3PtSeiUpBpSlyRH20VJybBQbI,25828
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.324.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
1085
- supervisely-6.73.324.dist-info/METADATA,sha256=mUZAJc6JxtQ7H7I31gf_QWoBAjl4qXsS-CilUCvf8f8,33596
1086
- supervisely-6.73.324.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
1087
- supervisely-6.73.324.dist-info/entry_points.txt,sha256=U96-5Hxrp2ApRjnCoUiUhWMqijqh8zLR03sEhWtAcms,102
1088
- supervisely-6.73.324.dist-info/top_level.txt,sha256=kcFVwb7SXtfqZifrZaSE3owHExX4gcNYe7Q2uoby084,28
1089
- supervisely-6.73.324.dist-info/RECORD,,
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,,