supervisely 6.73.392__py3-none-any.whl → 6.73.394__py3-none-any.whl

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