supervisely 6.73.345__py3-none-any.whl → 6.73.347__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/convert/volume/__init__.py +1 -1
- supervisely/convert/volume/nii/nii_planes_volume_converter.py +187 -5
- supervisely/convert/volume/nii/nii_volume_converter.py +1 -1
- supervisely/convert/volume/nii/nii_volume_helper.py +207 -0
- supervisely/nn/benchmark/semantic_segmentation/metric_provider.py +1 -1
- supervisely/nn/benchmark/semantic_segmentation/vis_metrics/confusion_matrix.py +1 -1
- supervisely/nn/benchmark/semantic_segmentation/visualizer.py +4 -2
- supervisely/nn/inference/inference.py +31 -1
- {supervisely-6.73.345.dist-info → supervisely-6.73.347.dist-info}/METADATA +1 -1
- {supervisely-6.73.345.dist-info → supervisely-6.73.347.dist-info}/RECORD +14 -14
- {supervisely-6.73.345.dist-info → supervisely-6.73.347.dist-info}/LICENSE +0 -0
- {supervisely-6.73.345.dist-info → supervisely-6.73.347.dist-info}/WHEEL +0 -0
- {supervisely-6.73.345.dist-info → supervisely-6.73.347.dist-info}/entry_points.txt +0 -0
- {supervisely-6.73.345.dist-info → supervisely-6.73.347.dist-info}/top_level.txt +0 -0
|
@@ -3,5 +3,5 @@ from supervisely.convert.volume.sly.sly_volume_converter import SLYVolumeConvert
|
|
|
3
3
|
from supervisely.convert.volume.dicom.dicom_converter import DICOMConverter
|
|
4
4
|
from supervisely.convert.volume.nii.nii_volume_converter import NiiConverter
|
|
5
5
|
from supervisely.convert.volume.nii.nii_planes_volume_converter import (
|
|
6
|
-
NiiPlaneStructuredConverter,
|
|
6
|
+
NiiPlaneStructuredConverter, NiiPlaneStructuredAnnotationConverter
|
|
7
7
|
)
|
|
@@ -2,16 +2,17 @@ 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 ProjectMeta, logger, Api
|
|
6
6
|
from supervisely.annotation.obj_class import ObjClass
|
|
7
7
|
from supervisely.convert.volume.nii import nii_volume_helper as helper
|
|
8
8
|
from supervisely.convert.volume.nii.nii_volume_converter import NiiConverter
|
|
9
9
|
from supervisely.convert.volume.volume_converter import VolumeConverter
|
|
10
10
|
from supervisely.geometry.mask_3d import Mask3D
|
|
11
|
-
from supervisely.io.fs import get_file_ext, get_file_name
|
|
11
|
+
from supervisely.io.fs import get_file_ext, get_file_name, list_files_recursively
|
|
12
12
|
from supervisely.volume.volume import is_nifti_file
|
|
13
13
|
from supervisely.volume_annotation.volume_annotation import VolumeAnnotation
|
|
14
14
|
from supervisely.volume_annotation.volume_object import VolumeObject
|
|
15
|
+
from supervisely._utils import batched, is_development
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
class NiiPlaneStructuredConverter(NiiConverter, VolumeConverter):
|
|
@@ -58,6 +59,7 @@ class NiiPlaneStructuredConverter(NiiConverter, VolumeConverter):
|
|
|
58
59
|
def __init__(self, *args, **kwargs):
|
|
59
60
|
super().__init__(*args, **kwargs)
|
|
60
61
|
self._is_semantic = False
|
|
62
|
+
self.volume_meta = None
|
|
61
63
|
|
|
62
64
|
@property
|
|
63
65
|
def is_semantic(self) -> bool:
|
|
@@ -67,6 +69,12 @@ class NiiPlaneStructuredConverter(NiiConverter, VolumeConverter):
|
|
|
67
69
|
def is_semantic(self, value: bool):
|
|
68
70
|
self._is_semantic = value
|
|
69
71
|
|
|
72
|
+
def create_empty_annotation(self):
|
|
73
|
+
return VolumeAnnotation(self.volume_meta)
|
|
74
|
+
|
|
75
|
+
def __str__(self):
|
|
76
|
+
return "nii_custom"
|
|
77
|
+
|
|
70
78
|
def validate_format(self) -> bool:
|
|
71
79
|
# create Items
|
|
72
80
|
converted_dir_name = "converted"
|
|
@@ -87,8 +95,7 @@ class NiiPlaneStructuredConverter(NiiConverter, VolumeConverter):
|
|
|
87
95
|
prefix = full_name.split("_")[0]
|
|
88
96
|
if prefix not in helper.PlanePrefix.values():
|
|
89
97
|
continue
|
|
90
|
-
|
|
91
|
-
if name in helper.LABEL_NAME or name[:-1] in helper.LABEL_NAME:
|
|
98
|
+
if any(label_name in full_name for label_name in helper.LABEL_NAME):
|
|
92
99
|
ann_dict[prefix].append(path)
|
|
93
100
|
else:
|
|
94
101
|
volumes_dict[prefix].append(path)
|
|
@@ -114,7 +121,7 @@ class NiiPlaneStructuredConverter(NiiConverter, VolumeConverter):
|
|
|
114
121
|
for path in paths:
|
|
115
122
|
item = self.Item(item_path=path)
|
|
116
123
|
possible_ann_paths = []
|
|
117
|
-
for ann_path in ann_dict.get(prefix):
|
|
124
|
+
for ann_path in ann_dict.get(prefix, []):
|
|
118
125
|
if Path(ann_path).parent == Path(path).parent:
|
|
119
126
|
possible_ann_paths.append(ann_path)
|
|
120
127
|
item.ann_data = possible_ann_paths
|
|
@@ -160,3 +167,178 @@ class NiiPlaneStructuredConverter(NiiConverter, VolumeConverter):
|
|
|
160
167
|
except Exception as e:
|
|
161
168
|
logger.warning(f"Failed to convert {item.path} to Supervisely format: {e}")
|
|
162
169
|
return item.create_empty_annotation()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class NiiPlaneStructuredAnnotationConverter(NiiConverter, VolumeConverter):
|
|
173
|
+
"""
|
|
174
|
+
Upload NIfTI Annotations
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
class Item(VolumeConverter.BaseItem):
|
|
178
|
+
def __init__(self, *args, **kwargs):
|
|
179
|
+
super().__init__(*args, **kwargs)
|
|
180
|
+
self._is_semantic = False
|
|
181
|
+
self.volume_meta = None
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def is_semantic(self) -> bool:
|
|
185
|
+
return self._is_semantic
|
|
186
|
+
|
|
187
|
+
@is_semantic.setter
|
|
188
|
+
def is_semantic(self, value: bool):
|
|
189
|
+
self._is_semantic = value
|
|
190
|
+
|
|
191
|
+
def create_empty_annotation(self):
|
|
192
|
+
return VolumeAnnotation(self.volume_meta)
|
|
193
|
+
|
|
194
|
+
def __init__(self, *args, **kwargs):
|
|
195
|
+
super().__init__(*args, **kwargs)
|
|
196
|
+
self._json_map = None
|
|
197
|
+
|
|
198
|
+
def __str__(self):
|
|
199
|
+
return "nii_custom_ann"
|
|
200
|
+
|
|
201
|
+
def validate_format(self) -> bool:
|
|
202
|
+
try:
|
|
203
|
+
from nibabel import load, filebasedimages
|
|
204
|
+
except ImportError:
|
|
205
|
+
raise ImportError(
|
|
206
|
+
"No module named nibabel. Please make sure that module is installed from pip and try again."
|
|
207
|
+
)
|
|
208
|
+
cls_color_map = None
|
|
209
|
+
|
|
210
|
+
has_volumes = lambda x: helper.VOLUME_NAME in x
|
|
211
|
+
if list_files_recursively(self._input_data, filter_fn=has_volumes):
|
|
212
|
+
return False
|
|
213
|
+
|
|
214
|
+
txts = list_files_recursively(self._input_data, [".txt"], None, True)
|
|
215
|
+
cls_color_map = next(iter(txts), None)
|
|
216
|
+
if cls_color_map is not None:
|
|
217
|
+
cls_color_map = helper.read_cls_color_map(cls_color_map)
|
|
218
|
+
|
|
219
|
+
jsons = list_files_recursively(self._input_data, [".json"], None, True)
|
|
220
|
+
json_map = next(iter(jsons), None)
|
|
221
|
+
if json_map is not None:
|
|
222
|
+
self._json_map = helper.read_json_map(json_map)
|
|
223
|
+
|
|
224
|
+
is_ann = lambda x: any(label_name in x for label_name in helper.LABEL_NAME)
|
|
225
|
+
for root, _, files in os.walk(self._input_data):
|
|
226
|
+
for file in files:
|
|
227
|
+
path = os.path.join(root, file)
|
|
228
|
+
if is_ann(file):
|
|
229
|
+
prefix = get_file_name(path).split("_")[0]
|
|
230
|
+
if prefix not in helper.PlanePrefix.values():
|
|
231
|
+
continue
|
|
232
|
+
try:
|
|
233
|
+
nii = load(path)
|
|
234
|
+
except filebasedimages.ImageFileError:
|
|
235
|
+
continue
|
|
236
|
+
item = self.Item(item_path=None, ann_data=path)
|
|
237
|
+
item.set_shape(nii.shape)
|
|
238
|
+
if cls_color_map is not None:
|
|
239
|
+
item.custom_data["cls_color_map"] = cls_color_map
|
|
240
|
+
self._items.append(item)
|
|
241
|
+
|
|
242
|
+
obj_classes = None
|
|
243
|
+
if cls_color_map is not None:
|
|
244
|
+
obj_classes = [ObjClass(name, Mask3D, color) for name, color in cls_color_map.values()]
|
|
245
|
+
|
|
246
|
+
self._meta = ProjectMeta(obj_classes=obj_classes)
|
|
247
|
+
return len(self._items) > 0
|
|
248
|
+
|
|
249
|
+
def to_supervisely(
|
|
250
|
+
self,
|
|
251
|
+
item: VolumeConverter.Item,
|
|
252
|
+
meta: ProjectMeta = None,
|
|
253
|
+
renamed_classes: dict = None,
|
|
254
|
+
renamed_tags: dict = None,
|
|
255
|
+
) -> VolumeAnnotation:
|
|
256
|
+
"""Convert to Supervisely format."""
|
|
257
|
+
import re
|
|
258
|
+
try:
|
|
259
|
+
objs = []
|
|
260
|
+
spatial_figures = []
|
|
261
|
+
ann_path = item.ann_data
|
|
262
|
+
ann_idx = 0
|
|
263
|
+
match = re.search(r"_(\d+)(?:\.[^.]+)+$", ann_path)
|
|
264
|
+
if match:
|
|
265
|
+
ann_idx = int(match.group(1))
|
|
266
|
+
for mask, pixel_id in helper.get_annotation_from_nii(ann_path):
|
|
267
|
+
class_id = pixel_id if item.is_semantic else ann_idx
|
|
268
|
+
class_name = f"Segment_{class_id}"
|
|
269
|
+
color = None
|
|
270
|
+
if item.custom_data.get("cls_color_map") is not None:
|
|
271
|
+
class_info = item.custom_data["cls_color_map"].get(class_id)
|
|
272
|
+
if class_info is not None:
|
|
273
|
+
class_name, color = class_info
|
|
274
|
+
class_name = renamed_classes.get(class_name, class_name)
|
|
275
|
+
obj_class = meta.get_obj_class(class_name)
|
|
276
|
+
if obj_class is None:
|
|
277
|
+
obj_class = ObjClass(class_name, Mask3D, color)
|
|
278
|
+
meta = meta.add_obj_class(obj_class)
|
|
279
|
+
self._meta_changed = True
|
|
280
|
+
self._meta = meta
|
|
281
|
+
obj = VolumeObject(obj_class, mask_3d=mask)
|
|
282
|
+
spatial_figures.append(obj.figure)
|
|
283
|
+
objs.append(obj)
|
|
284
|
+
return VolumeAnnotation(item.volume_meta, objects=objs, spatial_figures=spatial_figures)
|
|
285
|
+
except Exception as e:
|
|
286
|
+
logger.warning(f"Failed to convert {item.ann_data} to Supervisely format: {e}")
|
|
287
|
+
return item.create_empty_annotation()
|
|
288
|
+
|
|
289
|
+
def upload_dataset(
|
|
290
|
+
self, api: Api, dataset_id: int, batch_size: int = 50, log_progress=True
|
|
291
|
+
) -> None:
|
|
292
|
+
meta, renamed_classes, _ = self.merge_metas_with_conflicts(api, dataset_id)
|
|
293
|
+
|
|
294
|
+
matcher = helper.AnnotationMatcher(self._items, dataset_id)
|
|
295
|
+
if self._json_map is not None:
|
|
296
|
+
try:
|
|
297
|
+
matched_dict = matcher.match_from_json(api, self._json_map)
|
|
298
|
+
except Exception as e:
|
|
299
|
+
logger.error(f"Failed to match annotations from a json map: {e}")
|
|
300
|
+
matched_dict = {}
|
|
301
|
+
else:
|
|
302
|
+
matcher.get_volumes(api)
|
|
303
|
+
matched_dict = matcher.match_items()
|
|
304
|
+
if len(matched_dict) != len(self._items):
|
|
305
|
+
extra = {
|
|
306
|
+
"items count": len(self._items),
|
|
307
|
+
"matched count": len(matched_dict),
|
|
308
|
+
"unmatched count": len(self._items) - len(matched_dict),
|
|
309
|
+
}
|
|
310
|
+
logger.warning(
|
|
311
|
+
"Not all items were matched with volumes. Some items may be skipped.",
|
|
312
|
+
extra=extra,
|
|
313
|
+
)
|
|
314
|
+
if len(matched_dict) == 0:
|
|
315
|
+
raise RuntimeError(
|
|
316
|
+
"No items were matched with volumes. Please check the input data and try again."
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
if log_progress:
|
|
320
|
+
progress, progress_cb = self.get_progress(
|
|
321
|
+
len(matched_dict), "Uploading volumes annotations..."
|
|
322
|
+
)
|
|
323
|
+
else:
|
|
324
|
+
progress_cb = None
|
|
325
|
+
|
|
326
|
+
for item, volume in matched_dict.items():
|
|
327
|
+
item.volume_meta = volume.meta
|
|
328
|
+
ann = self.to_supervisely(item, meta, renamed_classes, None)
|
|
329
|
+
if self._meta_changed:
|
|
330
|
+
meta, renamed_classes, _ = self.merge_metas_with_conflicts(api, dataset_id)
|
|
331
|
+
self._meta_changed = False
|
|
332
|
+
api.volume.annotation.append(volume.id, ann, volume_info=volume)
|
|
333
|
+
progress_cb(1) if log_progress else None
|
|
334
|
+
|
|
335
|
+
res_ds_info = api.dataset.get_info_by_id(dataset_id)
|
|
336
|
+
if res_ds_info.items_count == 0:
|
|
337
|
+
logger.info("Resulting dataset is empty. Removing it.")
|
|
338
|
+
api.dataset.remove(dataset_id)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
if log_progress:
|
|
342
|
+
if is_development():
|
|
343
|
+
progress.close()
|
|
344
|
+
logger.info(f"Successfully uploaded {len(matched_dict)} annotations.")
|
|
@@ -207,7 +207,7 @@ class NiiConverter(VolumeConverter):
|
|
|
207
207
|
|
|
208
208
|
if self._meta_changed:
|
|
209
209
|
meta, renamed_classes, _ = self.merge_metas_with_conflicts(api, dataset_id)
|
|
210
|
-
|
|
210
|
+
self._meta_changed = False
|
|
211
211
|
api.volume.annotation.append(info.id, ann)
|
|
212
212
|
|
|
213
213
|
if log_progress:
|
|
@@ -3,7 +3,10 @@ from typing import Generator
|
|
|
3
3
|
|
|
4
4
|
import nrrd
|
|
5
5
|
import numpy as np
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from collections import defaultdict, namedtuple
|
|
6
8
|
|
|
9
|
+
from supervisely import Api
|
|
7
10
|
from supervisely.collection.str_enum import StrEnum
|
|
8
11
|
from supervisely.geometry.mask_3d import Mask3D
|
|
9
12
|
from supervisely.io.fs import ensure_base_path, get_file_ext, get_file_name
|
|
@@ -69,6 +72,20 @@ def read_cls_color_map(path: str) -> dict:
|
|
|
69
72
|
return None
|
|
70
73
|
return cls_color_map
|
|
71
74
|
|
|
75
|
+
def read_json_map(path: str) -> dict:
|
|
76
|
+
import json
|
|
77
|
+
|
|
78
|
+
"""Read JSON map from file."""
|
|
79
|
+
if not os.path.exists(path):
|
|
80
|
+
return None
|
|
81
|
+
try:
|
|
82
|
+
with open(path, "r") as file:
|
|
83
|
+
json_map = json.load(file)
|
|
84
|
+
except Exception as e:
|
|
85
|
+
logger.warning(f"Failed to read JSON map from {path}: {e}")
|
|
86
|
+
return None
|
|
87
|
+
return json_map
|
|
88
|
+
|
|
72
89
|
|
|
73
90
|
def nifti_to_nrrd(nii_file_path: str, converted_dir: str) -> str:
|
|
74
91
|
"""Convert NIfTI 3D volume file to NRRD 3D volume file."""
|
|
@@ -97,3 +114,193 @@ def get_annotation_from_nii(path: str) -> Generator[Mask3D, None, None]:
|
|
|
97
114
|
continue
|
|
98
115
|
mask = Mask3D(data == class_id)
|
|
99
116
|
yield mask, class_id
|
|
117
|
+
|
|
118
|
+
class AnnotationMatcher:
|
|
119
|
+
def __init__(self, items, dataset_id):
|
|
120
|
+
self._items = items
|
|
121
|
+
self._ds_id = dataset_id
|
|
122
|
+
self._ann_paths = defaultdict(list)
|
|
123
|
+
|
|
124
|
+
self._item_by_filename = {}
|
|
125
|
+
self._item_by_path = {}
|
|
126
|
+
|
|
127
|
+
for item in items:
|
|
128
|
+
path = Path(item.ann_data)
|
|
129
|
+
dataset_name = path.parts[-2]
|
|
130
|
+
filename = path.name
|
|
131
|
+
|
|
132
|
+
self._ann_paths[dataset_name].append(filename)
|
|
133
|
+
self._item_by_filename[filename] = item
|
|
134
|
+
self._item_by_path[(dataset_name, filename)] = item
|
|
135
|
+
|
|
136
|
+
self._project_wide = False
|
|
137
|
+
self._volumes = None
|
|
138
|
+
|
|
139
|
+
def get_volumes(self, api: Api):
|
|
140
|
+
dataset_info = api.dataset.get_info_by_id(self._ds_id)
|
|
141
|
+
datasets = {dataset_info.name: dataset_info}
|
|
142
|
+
project_id = dataset_info.project_id
|
|
143
|
+
if dataset_info.items_count > 0 and len(self._ann_paths.keys()) == 1:
|
|
144
|
+
self._project_wide = False
|
|
145
|
+
else:
|
|
146
|
+
datasets = {dsinfo.name: dsinfo for dsinfo in api.dataset.get_list(project_id, recursive=True)}
|
|
147
|
+
self._project_wide = True
|
|
148
|
+
|
|
149
|
+
volumes = defaultdict(lambda: {})
|
|
150
|
+
ds_filter = lambda ds_name: ds_name in self._ann_paths if self._project_wide else True
|
|
151
|
+
for ds_name, ds_info in datasets.items():
|
|
152
|
+
if ds_filter(ds_name):
|
|
153
|
+
volumes[ds_name].update(
|
|
154
|
+
{info.name: info for info in api.volume.get_list(ds_info.id)}
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
if len(volumes) == 0:
|
|
158
|
+
err_msg = "Failed to retrieve volumes from the project. Perhaps the input data structure is incorrect."
|
|
159
|
+
raise RuntimeError(err_msg)
|
|
160
|
+
|
|
161
|
+
self._volumes = volumes
|
|
162
|
+
|
|
163
|
+
def match_items(self):
|
|
164
|
+
"""Match annotation files with corresponding volumes using regex-based matching."""
|
|
165
|
+
import re
|
|
166
|
+
|
|
167
|
+
def extract_prefix(ann_file):
|
|
168
|
+
import re
|
|
169
|
+
pattern = r'^(?P<prefix>cor|sag|axl).*?(?:' + "|".join(LABEL_NAME) + r')'
|
|
170
|
+
m = re.match(pattern, ann_file, re.IGNORECASE)
|
|
171
|
+
if m:
|
|
172
|
+
return m.group("prefix").lower()
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
def is_volume_match(volume_name, prefix):
|
|
176
|
+
pattern = r'^' + re.escape(prefix) + r'.*?anatomic'
|
|
177
|
+
return re.match(pattern, volume_name, re.IGNORECASE) is not None
|
|
178
|
+
|
|
179
|
+
def find_best_volume_match(prefix, available_volumes):
|
|
180
|
+
candidates = {name: volume for name, volume in available_volumes.items() if is_volume_match(name, prefix)}
|
|
181
|
+
if not candidates:
|
|
182
|
+
return None, None
|
|
183
|
+
|
|
184
|
+
# Prefer an exact candidate
|
|
185
|
+
ann_name_no_ext = ann_file.split(".")[0]
|
|
186
|
+
exact_candidate = re.sub(r'(' + '|'.join(LABEL_NAME) + r')', 'anatomic', ann_name_no_ext, flags=re.IGNORECASE)
|
|
187
|
+
for name in candidates:
|
|
188
|
+
if re.fullmatch(re.escape(exact_candidate), name, re.IGNORECASE):
|
|
189
|
+
return name, candidates[name]
|
|
190
|
+
|
|
191
|
+
# Otherwise, choose the candidate with the shortest name
|
|
192
|
+
best_match = sorted(candidates.keys(), key=len)[0]
|
|
193
|
+
return best_match, candidates[best_match]
|
|
194
|
+
|
|
195
|
+
item_to_volume = {}
|
|
196
|
+
|
|
197
|
+
def process_annotation_file(ann_file, dataset_name, volumes):
|
|
198
|
+
prefix = extract_prefix(ann_file)
|
|
199
|
+
if prefix is None:
|
|
200
|
+
logger.warning(f"Failed to extract prefix from annotation file {ann_file}. Skipping.")
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
matched_name, matched_volume = find_best_volume_match(prefix, volumes)
|
|
204
|
+
if not matched_volume:
|
|
205
|
+
logger.warning(f"No matching volume found for annotation with prefix '{prefix}' in dataset {dataset_name}.")
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
# Retrieve the correct item based on matching mode.
|
|
209
|
+
item = (
|
|
210
|
+
self._item_by_path.get((dataset_name, ann_file))
|
|
211
|
+
if self._project_wide
|
|
212
|
+
else self._item_by_filename.get(ann_file)
|
|
213
|
+
)
|
|
214
|
+
if not item:
|
|
215
|
+
logger.warning(f"Item not found for annotation file {ann_file} in {'dataset ' + dataset_name if self._project_wide else 'single dataset mode'}.")
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
item_to_volume[item] = matched_volume
|
|
219
|
+
ann_file = ann_file.split(".")[0]
|
|
220
|
+
ann_supposed_match = re.sub(r'(' + '|'.join(LABEL_NAME) + r')', 'anatomic', ann_file, flags=re.IGNORECASE)
|
|
221
|
+
if matched_name.lower() != ann_supposed_match:
|
|
222
|
+
logger.debug(f"Fuzzy matched {ann_file} to volume {matched_name} using prefix '{prefix}'.")
|
|
223
|
+
|
|
224
|
+
# Perform matching
|
|
225
|
+
for dataset_name, volumes in self._volumes.items():
|
|
226
|
+
ann_files = self._ann_paths.get(dataset_name, []) if self._project_wide else list(self._ann_paths.values())[0]
|
|
227
|
+
for ann_file in ann_files:
|
|
228
|
+
process_annotation_file(ann_file, dataset_name, volumes)
|
|
229
|
+
|
|
230
|
+
# Mark volumes having only one matching item as semantic and validate shape.
|
|
231
|
+
volume_to_items = defaultdict(list)
|
|
232
|
+
for item, volume in item_to_volume.items():
|
|
233
|
+
volume_to_items[volume.id].append(item)
|
|
234
|
+
|
|
235
|
+
for volume_id, items in volume_to_items.items():
|
|
236
|
+
if len(items) == 1:
|
|
237
|
+
items[0].is_semantic = True
|
|
238
|
+
|
|
239
|
+
items_to_remove = []
|
|
240
|
+
for item, volume in item_to_volume.items():
|
|
241
|
+
volume_shape = tuple(volume.file_meta["sizes"])
|
|
242
|
+
if item.shape != volume_shape:
|
|
243
|
+
logger.warning(f"Volume shape mismatch: {item.shape} != {volume_shape}")
|
|
244
|
+
# items_to_remove.append(item)
|
|
245
|
+
for item in items_to_remove:
|
|
246
|
+
del item_to_volume[item]
|
|
247
|
+
|
|
248
|
+
return item_to_volume
|
|
249
|
+
|
|
250
|
+
def match_from_json(self, api: Api, json_map: dict):
|
|
251
|
+
"""
|
|
252
|
+
Match annotation files with corresponding volumes based on a JSON map.
|
|
253
|
+
|
|
254
|
+
Example json structure:
|
|
255
|
+
{
|
|
256
|
+
"cor_inference_1.nii": 123,
|
|
257
|
+
"sag_mask_2.nii": 456
|
|
258
|
+
}
|
|
259
|
+
Where key is the annotation file name and value is the volume ID.
|
|
260
|
+
|
|
261
|
+
For project-wide matching, the key should include dataset name:
|
|
262
|
+
{
|
|
263
|
+
"dataset1/cor_inference_1.nii": 123,
|
|
264
|
+
"dataset2/sag_mask_2.nii": 456
|
|
265
|
+
}
|
|
266
|
+
"""
|
|
267
|
+
item_to_volume = {}
|
|
268
|
+
|
|
269
|
+
for ann_path, volume_id in json_map.items():
|
|
270
|
+
# Check if it's a project-wide path (contains dataset name)
|
|
271
|
+
path_parts = Path(ann_path)
|
|
272
|
+
if len(path_parts.parts) > 1:
|
|
273
|
+
# Project-wide format: "dataset_name/filename.nii"
|
|
274
|
+
dataset_name = path_parts.parts[-2]
|
|
275
|
+
ann_name = path_parts.name
|
|
276
|
+
item = self._item_by_path.get((dataset_name, ann_name))
|
|
277
|
+
else:
|
|
278
|
+
# Single dataset format: "filename.nii"
|
|
279
|
+
ann_name = path_parts.name
|
|
280
|
+
item = self._item_by_filename.get(ann_name)
|
|
281
|
+
|
|
282
|
+
if item:
|
|
283
|
+
volume = api.volume.get_info_by_id(volume_id)
|
|
284
|
+
if volume:
|
|
285
|
+
item_to_volume[item] = volume
|
|
286
|
+
|
|
287
|
+
# Validate shape
|
|
288
|
+
volume_shape = tuple(volume.file_meta["sizes"])
|
|
289
|
+
if item.shape != volume_shape:
|
|
290
|
+
logger.warning(
|
|
291
|
+
f"Volume shape mismatch: {item.shape} != {volume_shape} for {ann_path}. Using anyway."
|
|
292
|
+
)
|
|
293
|
+
else:
|
|
294
|
+
logger.warning(f"Volume {volume_id} not found for {ann_path}.")
|
|
295
|
+
else:
|
|
296
|
+
logger.warning(f"Item not found for annotation file {ann_path}.")
|
|
297
|
+
|
|
298
|
+
# Set semantic flag for volumes with only one associated item
|
|
299
|
+
volume_to_items = defaultdict(list)
|
|
300
|
+
for item, volume in item_to_volume.items():
|
|
301
|
+
volume_to_items[volume.id].append(item)
|
|
302
|
+
for volume_id, items in volume_to_items.items():
|
|
303
|
+
if len(items) == 1:
|
|
304
|
+
items[0].is_semantic = True
|
|
305
|
+
|
|
306
|
+
return item_to_volume
|
|
@@ -144,8 +144,8 @@ class MetricProvider:
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
def get_classwise_error_data(self):
|
|
147
|
-
self.eval_data.drop(["mean"], inplace=True)
|
|
148
147
|
bar_data = self.eval_data.copy()
|
|
148
|
+
bar_data.drop(["mean"], inplace=True)
|
|
149
149
|
bar_data = bar_data[["IoU", "E_extent_oU", "E_boundary_oU", "E_segment_oU"]]
|
|
150
150
|
bar_data.sort_values(by="IoU", ascending=False, inplace=True)
|
|
151
151
|
labels = list(bar_data.index)
|
|
@@ -39,7 +39,7 @@ class ConfusionMatrix(SemanticSegmVisMetric):
|
|
|
39
39
|
# # Confusion Matrix figure
|
|
40
40
|
confusion_matrix, class_names = self.eval_result.mp.confusion_matrix
|
|
41
41
|
|
|
42
|
-
x = class_names
|
|
42
|
+
x = [el for el in class_names if el != "mean"]
|
|
43
43
|
y = x[::-1].copy()
|
|
44
44
|
if len(x) >= 20:
|
|
45
45
|
text_anns = [[str(el) for el in row] for row in confusion_matrix]
|
|
@@ -240,6 +240,8 @@ class SemanticSegmentationVisualizer(BaseVisualizer):
|
|
|
240
240
|
for src_images in self.api.image.get_list_generator(
|
|
241
241
|
pred_dataset_info.id, force_metadata_for_links=False, batch_size=100
|
|
242
242
|
):
|
|
243
|
+
if len(src_images) == 0:
|
|
244
|
+
continue
|
|
243
245
|
dst_images = self.api.image.copy_batch_optimized(
|
|
244
246
|
pred_dataset_info.id,
|
|
245
247
|
src_images,
|
|
@@ -274,8 +276,8 @@ class SemanticSegmentationVisualizer(BaseVisualizer):
|
|
|
274
276
|
)
|
|
275
277
|
|
|
276
278
|
p.update(len(src_images))
|
|
277
|
-
except Exception:
|
|
278
|
-
raise RuntimeError("Match data was not created properly")
|
|
279
|
+
except Exception as e:
|
|
280
|
+
raise RuntimeError(f"Match data was not created properly. {e}")
|
|
279
281
|
|
|
280
282
|
def _get_sample_data_for_gallery(self):
|
|
281
283
|
# get sample images with annotations for visualization
|
|
@@ -1705,8 +1705,38 @@ class Inference:
|
|
|
1705
1705
|
if src_dataset_id in new_dataset_id:
|
|
1706
1706
|
return new_dataset_id[src_dataset_id]
|
|
1707
1707
|
dataset_info = api.dataset.get_info_by_id(src_dataset_id)
|
|
1708
|
+
|
|
1709
|
+
def _create_parent_recursively(output_project_id, src_parent_id):
|
|
1710
|
+
"""Create parent datasets recursively and return the ID of the top-level parent"""
|
|
1711
|
+
if src_parent_id in new_dataset_id:
|
|
1712
|
+
return new_dataset_id[src_parent_id]
|
|
1713
|
+
src_parent_info = dataset_infos_dict.get(src_parent_id)
|
|
1714
|
+
if src_parent_info is None:
|
|
1715
|
+
src_parent_info = api.dataset.get_info_by_id(src_parent_id)
|
|
1716
|
+
if src_parent_info.parent_id is not None:
|
|
1717
|
+
parent_id = _create_parent_recursively(
|
|
1718
|
+
output_project_id, src_parent_info.parent_id
|
|
1719
|
+
)
|
|
1720
|
+
else:
|
|
1721
|
+
parent_id = None
|
|
1722
|
+
dst_parent = api.dataset.create(
|
|
1723
|
+
output_project_id,
|
|
1724
|
+
src_parent_info.name,
|
|
1725
|
+
change_name_if_conflict=True,
|
|
1726
|
+
parent_id=parent_id,
|
|
1727
|
+
)
|
|
1728
|
+
new_dataset_id[src_parent_info.id] = dst_parent.id
|
|
1729
|
+
return dst_parent.id
|
|
1730
|
+
|
|
1731
|
+
parent_id = None
|
|
1732
|
+
if dataset_info.parent_id is not None:
|
|
1733
|
+
parent_id = _create_parent_recursively(output_project_id, dataset_info.parent_id)
|
|
1734
|
+
|
|
1708
1735
|
output_dataset_id = api.dataset.create(
|
|
1709
|
-
output_project_id,
|
|
1736
|
+
output_project_id,
|
|
1737
|
+
dataset_info.name,
|
|
1738
|
+
change_name_if_conflict=True,
|
|
1739
|
+
parent_id=parent_id,
|
|
1710
1740
|
).id
|
|
1711
1741
|
new_dataset_id[src_dataset_id] = output_dataset_id
|
|
1712
1742
|
return output_dataset_id
|
|
@@ -658,15 +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=NaACs000WT2iy_g63TiZZ6IlgCjyDXx6i2OHsGpCYOs,391
|
|
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=Hw4RxU_qvllk6M26udZE6G-m1RWR8-VVPcEPwFlqrVg,3354
|
|
665
665
|
supervisely/convert/volume/dicom/dicom_helper.py,sha256=OrKlyt1hA5BOXKhE1LF1WxBIv3b6t96xRras4OSAuNM,2891
|
|
666
666
|
supervisely/convert/volume/nii/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
667
|
-
supervisely/convert/volume/nii/nii_planes_volume_converter.py,sha256=
|
|
668
|
-
supervisely/convert/volume/nii/nii_volume_converter.py,sha256=
|
|
669
|
-
supervisely/convert/volume/nii/nii_volume_helper.py,sha256=
|
|
667
|
+
supervisely/convert/volume/nii/nii_planes_volume_converter.py,sha256=TrV7Mkczt8w2WpJizmOZwqeG9zlcLy-8p4D22B9nYyo,14344
|
|
668
|
+
supervisely/convert/volume/nii/nii_volume_converter.py,sha256=n8HWRvwXUzugTQt4PKpbSacsuC4EQxoYHAWXcXC5KE8,8526
|
|
669
|
+
supervisely/convert/volume/nii/nii_volume_helper.py,sha256=8cS1LCvDcgGuinBARTmbOm-lLQmJ___3gyemt26W_-Y,11572
|
|
670
670
|
supervisely/convert/volume/sly/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
671
671
|
supervisely/convert/volume/sly/sly_volume_converter.py,sha256=XmSuxnRqxchG87b244f3h0UHvOt6IkajMquL1drWlCM,5595
|
|
672
672
|
supervisely/convert/volume/sly/sly_volume_helper.py,sha256=gUY0GW3zDMlO2y-zQQG36uoXMrKkKz4-ErM1CDxFCxE,5620
|
|
@@ -826,13 +826,13 @@ supervisely/nn/benchmark/semantic_segmentation/base_vis_metric.py,sha256=mwGjRUT
|
|
|
826
826
|
supervisely/nn/benchmark/semantic_segmentation/benchmark.py,sha256=8rnU6I94q0GUdXWwluZu0_Sac_eU2-Az133tHF1dA3U,1202
|
|
827
827
|
supervisely/nn/benchmark/semantic_segmentation/evaluation_params.yaml,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
828
828
|
supervisely/nn/benchmark/semantic_segmentation/evaluator.py,sha256=XafPMpGL6v0ZQ-m7DkEjoY7W6fGCJNKolql5BA3M8V0,7261
|
|
829
|
-
supervisely/nn/benchmark/semantic_segmentation/metric_provider.py,sha256=
|
|
829
|
+
supervisely/nn/benchmark/semantic_segmentation/metric_provider.py,sha256=478l7w2n-yueBMVABsakfIQEo3MksbCYmKNrwMFOl6w,6546
|
|
830
830
|
supervisely/nn/benchmark/semantic_segmentation/text_templates.py,sha256=7yRRD2FAdJHGSRqBVIjNjzCduKzaepA1OWtggi7B0Dg,8580
|
|
831
|
-
supervisely/nn/benchmark/semantic_segmentation/visualizer.py,sha256=
|
|
831
|
+
supervisely/nn/benchmark/semantic_segmentation/visualizer.py,sha256=T7WTjnyFLL3BAyH7yQIqI6R5VNj1M3guamwhDglOegE,13293
|
|
832
832
|
supervisely/nn/benchmark/semantic_segmentation/vis_metrics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
833
833
|
supervisely/nn/benchmark/semantic_segmentation/vis_metrics/acknowledgement.py,sha256=Lm82x8AIMKv1WqmqA0W9fugSlQ_JrP9dwYYYReZmhvI,440
|
|
834
834
|
supervisely/nn/benchmark/semantic_segmentation/vis_metrics/classwise_error_analysis.py,sha256=0bmL43a4cqw3grFoG68NN8Y1fkRpHBIRJptcxMor-78,1884
|
|
835
|
-
supervisely/nn/benchmark/semantic_segmentation/vis_metrics/confusion_matrix.py,sha256=
|
|
835
|
+
supervisely/nn/benchmark/semantic_segmentation/vis_metrics/confusion_matrix.py,sha256=nhfUPQr1dmZpYSluVX2XmMLk8AxzahKl2hcvGEXx9oQ,3264
|
|
836
836
|
supervisely/nn/benchmark/semantic_segmentation/vis_metrics/explore_predictions.py,sha256=QVtcGQv4S8W7jLANUsvuJaPP-OrUQ_LB2oEpjpLBecw,2936
|
|
837
837
|
supervisely/nn/benchmark/semantic_segmentation/vis_metrics/frequently_confused.py,sha256=SyVgMD66EFLfgrClb5RCJjLhgRfTYqGsUORPYIuSe58,3697
|
|
838
838
|
supervisely/nn/benchmark/semantic_segmentation/vis_metrics/iou_eou.py,sha256=IdUho3712dDLyVsgR01aNSQBcraPzYwpJmTc9AB0Txw,1401
|
|
@@ -883,7 +883,7 @@ supervisely/nn/benchmark/visualization/widgets/table/__init__.py,sha256=47DEQpj8
|
|
|
883
883
|
supervisely/nn/benchmark/visualization/widgets/table/table.py,sha256=atmDnF1Af6qLQBUjLhK18RMDKAYlxnsuVHMSEa5a-e8,4319
|
|
884
884
|
supervisely/nn/inference/__init__.py,sha256=QFukX2ip-U7263aEPCF_UCFwj6EujbMnsgrXp5Bbt8I,1623
|
|
885
885
|
supervisely/nn/inference/cache.py,sha256=LAirR5mFHCtK59EO1lefQ2qhpp0vBvRTH26EVrs13Y0,32073
|
|
886
|
-
supervisely/nn/inference/inference.py,sha256=
|
|
886
|
+
supervisely/nn/inference/inference.py,sha256=6rwm4Xbar-acekOV26k0JRsKR7u9zexayOEWg6IM-yY,171299
|
|
887
887
|
supervisely/nn/inference/session.py,sha256=jmkkxbe2kH-lEgUU6Afh62jP68dxfhF5v6OGDfLU62E,35757
|
|
888
888
|
supervisely/nn/inference/video_inference.py,sha256=8Bshjr6rDyLay5Za8IB8Dr6FURMO2R_v7aELasO8pR4,5746
|
|
889
889
|
supervisely/nn/inference/gui/__init__.py,sha256=wCxd-lF5Zhcwsis-wScDA8n1Gk_1O00PKgDviUZ3F1U,221
|
|
@@ -1082,9 +1082,9 @@ supervisely/worker_proto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
|
|
|
1082
1082
|
supervisely/worker_proto/worker_api_pb2.py,sha256=VQfi5JRBHs2pFCK1snec3JECgGnua3Xjqw_-b3aFxuM,59142
|
|
1083
1083
|
supervisely/worker_proto/worker_api_pb2_grpc.py,sha256=3BwQXOaP9qpdi0Dt9EKG--Lm8KGN0C5AgmUfRv77_Jk,28940
|
|
1084
1084
|
supervisely_lib/__init__.py,sha256=7-3QnN8Zf0wj8NCr2oJmqoQWMKKPKTECvjH9pd2S5vY,159
|
|
1085
|
-
supervisely-6.73.
|
|
1086
|
-
supervisely-6.73.
|
|
1087
|
-
supervisely-6.73.
|
|
1088
|
-
supervisely-6.73.
|
|
1089
|
-
supervisely-6.73.
|
|
1090
|
-
supervisely-6.73.
|
|
1085
|
+
supervisely-6.73.347.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
1086
|
+
supervisely-6.73.347.dist-info/METADATA,sha256=7GkOnoQKswP3aWdQvd8kRV27PVdnozTXeuKEqlGv734,33596
|
|
1087
|
+
supervisely-6.73.347.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
|
|
1088
|
+
supervisely-6.73.347.dist-info/entry_points.txt,sha256=U96-5Hxrp2ApRjnCoUiUhWMqijqh8zLR03sEhWtAcms,102
|
|
1089
|
+
supervisely-6.73.347.dist-info/top_level.txt,sha256=kcFVwb7SXtfqZifrZaSE3owHExX4gcNYe7Q2uoby084,28
|
|
1090
|
+
supervisely-6.73.347.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|