supervisely 6.73.368__py3-none-any.whl → 6.73.370__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.
@@ -2,7 +2,6 @@
2
2
  import os
3
3
  import re
4
4
  import tempfile
5
- from collections import OrderedDict
6
5
  from typing import Dict, List
7
6
  from uuid import UUID
8
7
 
@@ -470,7 +469,7 @@ class VolumeFigureApi(FigureApi):
470
469
  for figure in figures:
471
470
  if figure.key() == key:
472
471
  geometry_data = figure.geometry.data
473
- header = self._create_header_for_geometry(figure.geometry)
472
+ header = figure.geometry.create_header()
474
473
  geometry_bytes = encode(geometry_data.astype(uint8), header)
475
474
  self.upload_sf_geometries([key], {key: geometry_bytes}, key_id_map)
476
475
 
@@ -625,19 +624,6 @@ class VolumeFigureApi(FigureApi):
625
624
  geometry = Mask3D.create_from_file(figure_path)
626
625
  spatial_figure._set_3d_geometry(geometry)
627
626
 
628
- def _create_header_for_geometry(self, geometry: Mask3D) -> OrderedDict:
629
- """
630
- Create header for encoding Mask3D to NRRD bytes
631
- """
632
- header = OrderedDict()
633
- if geometry._space is not None:
634
- header["space"] = geometry._space
635
- if geometry._space_directions is not None:
636
- header["space directions"] = geometry._space_directions
637
- if geometry._space_origin is not None:
638
- header["space origin"] = geometry._space_origin.to_json()["space_origin"]
639
- return header
640
-
641
627
  def download(
642
628
  self, dataset_id: int, volume_ids: List[int] = None, skip_geometry: bool = False, **kwargs
643
629
  ) -> Dict[int, List[FigureInfo]]:
@@ -52,7 +52,7 @@ class ExperimentSelector(Widget):
52
52
  self._experiment_info = experiment_info
53
53
 
54
54
  task_id = experiment_info.task_id
55
- if task_id == "debug-session":
55
+ if task_id == "debug-session" or task_id == -1:
56
56
  pass
57
57
  elif type(task_id) is str:
58
58
  if task_id.isdigit():
@@ -392,7 +392,7 @@ class ExperimentSelector(Widget):
392
392
  if result:
393
393
  task_type, model_row = result
394
394
  if task_type is not None and model_row is not None:
395
- if model_row.task_id == "debug-session":
395
+ if model_row.task_id == "debug-session" or model_row.task_id == -1:
396
396
  self.__debug_row = (task_type, model_row)
397
397
  continue
398
398
  table_rows[task_type].append(model_row)
@@ -1,10 +1,10 @@
1
1
  import os
2
+ from collections import defaultdict, namedtuple
3
+ from pathlib import Path
2
4
  from typing import Generator
3
5
 
4
6
  import nrrd
5
7
  import numpy as np
6
- from pathlib import Path
7
- from collections import defaultdict, namedtuple
8
8
 
9
9
  from supervisely import Api
10
10
  from supervisely.collection.str_enum import StrEnum
@@ -107,13 +107,13 @@ def nifti_to_nrrd(nii_file_path: str, converted_dir: str) -> str:
107
107
  def get_annotation_from_nii(path: str) -> Generator[Mask3D, None, None]:
108
108
  """Get annotation from NIfTI 3D volume file."""
109
109
 
110
- data, _ = convert_3d_nifti_to_nrrd(path)
110
+ data, header = convert_3d_nifti_to_nrrd(path)
111
111
  unique_classes = np.unique(data)
112
112
 
113
113
  for class_id in unique_classes:
114
114
  if class_id == 0:
115
115
  continue
116
- mask = Mask3D(data == class_id)
116
+ mask = Mask3D(data == class_id, volume_header=header)
117
117
  yield mask, class_id
118
118
 
119
119
 
@@ -12,6 +12,8 @@ INTERIOR = "interior"
12
12
  MULTICHANNEL_BITMAP = "multichannelBitmap"
13
13
  ORIGIN = "origin"
14
14
  SPACE_ORIGIN = "space_origin"
15
+ SPACE = "space"
16
+ SPACE_DIRECTIONS = "space_directions"
15
17
  POINTS = "points"
16
18
  ROWS = "rows"
17
19
  TYPE = "type"
@@ -6,9 +6,9 @@ from __future__ import annotations
6
6
  import base64
7
7
  import gzip
8
8
  import tempfile
9
+ from collections import OrderedDict
9
10
  from typing import Dict, List, Literal, Optional, Tuple, Union
10
11
 
11
- import nrrd
12
12
  import numpy as np
13
13
 
14
14
  from supervisely import logger
@@ -22,6 +22,8 @@ from supervisely.geometry.constants import (
22
22
  ID,
23
23
  LABELER_LOGIN,
24
24
  MASK_3D,
25
+ SPACE,
26
+ SPACE_DIRECTIONS,
25
27
  SPACE_ORIGIN,
26
28
  UPDATED_AT,
27
29
  )
@@ -183,6 +185,10 @@ class Mask3D(Geometry):
183
185
  :type updated_at: str, optional
184
186
  :param created_at: Date and Time when Mask 3D was created. Date Format is the same as in "updated_at" parameter.
185
187
  :type created_at: str, optional
188
+ :param volume_header: NRRD header dictionary. Optional.
189
+ :type volume_header: dict, optional
190
+ :param convert_to_ras: If True, converts the mask to RAS orientation. Default is True.
191
+ :type convert_to_ras: bool, optional
186
192
  :raises: :class:`ValueError`, if data is not bool or no pixels set to True in data
187
193
  :Usage example:
188
194
 
@@ -219,6 +225,8 @@ class Mask3D(Geometry):
219
225
  labeler_login: Optional[str] = None,
220
226
  updated_at: Optional[str] = None,
221
227
  created_at: Optional[str] = None,
228
+ volume_header: Optional[Dict] = None,
229
+ convert_to_ras: bool = True,
222
230
  ):
223
231
  super().__init__(
224
232
  sly_id=sly_id,
@@ -253,6 +261,98 @@ class Mask3D(Geometry):
253
261
  self._space = None
254
262
  self._space_directions = None
255
263
 
264
+ if volume_header is not None:
265
+ self.set_volume_space_meta(volume_header)
266
+ if self.space is not None and self.space != "right-anterior-superior":
267
+ if convert_to_ras:
268
+ self.orient_ras()
269
+ else:
270
+ logger.debug(
271
+ "Mask3D is not in RAS orientation. It is recommended to use RAS orientation for 3D masks."
272
+ )
273
+
274
+ @property
275
+ def space_origin(self) -> Optional[List[float]]:
276
+ """
277
+ Get the space origin of the Mask3D as a list of floats.
278
+
279
+ :return: Space origin of the Mask3D.
280
+ :rtype: List[float] or None
281
+ """
282
+ if self._space_origin is not None:
283
+ return [self._space_origin.x, self._space_origin.y, self._space_origin.z]
284
+ return None
285
+
286
+ @space_origin.setter
287
+ def space_origin(self, value: Union[PointVolume, List[float], np.array]):
288
+ """
289
+ Set the space origin of the Mask3D.
290
+
291
+ :param value: Space origin of the Mask3D. If provided as a list or array, it should contain 3 floats in the order [x, y, z].
292
+ :type value: :class:`PointVolume<PointVolume>` or List[float]
293
+ """
294
+ if isinstance(value, PointVolume):
295
+ self._space_origin = value
296
+ elif isinstance(value, list) and len(value) == 3:
297
+ self._space_origin = PointVolume(x=value[0], y=value[1], z=value[2])
298
+ elif isinstance(value, np.ndarray) and value.shape == (3,):
299
+ self._space_origin = PointVolume(x=value[0], y=value[1], z=value[2])
300
+ else:
301
+ raise ValueError("Space origin must be a PointVolume or a list of 3 floats.")
302
+
303
+ @property
304
+ def space(self) -> Optional[str]:
305
+ """
306
+ Get the space of the Mask3D.
307
+
308
+ :return: Space of the Mask3D.
309
+ :rtype: :class:`str`
310
+ """
311
+ return self._space
312
+
313
+ @space.setter
314
+ def space(self, value: str):
315
+ """
316
+ Set the space of the Mask3D.
317
+
318
+ :param value: Space of the Mask3D.
319
+ :type value: str
320
+ """
321
+ if not isinstance(value, str):
322
+ raise ValueError("Space must be a string.")
323
+ self._space = value
324
+
325
+ @property
326
+ def space_directions(self) -> Optional[List[List[float]]]:
327
+ """
328
+ Get the space directions of the Mask3D.
329
+
330
+ :return: Space directions of the Mask3D.
331
+ :rtype: :class:`List[List[float]]`
332
+ """
333
+ return self._space_directions
334
+
335
+ @space_directions.setter
336
+ def space_directions(self, value: Union[List[List[float]], np.ndarray]):
337
+ """
338
+ Set the space directions of the Mask3D.
339
+
340
+ :param value: Space directions of the Mask3D. Should be a 3x3 array-like structure.
341
+ :type value: List[List[float]] or np.ndarray
342
+ """
343
+ if isinstance(value, np.ndarray):
344
+ if value.shape != (3, 3):
345
+ raise ValueError("Space directions must be a 3x3 array.")
346
+ self._space_directions = value.tolist()
347
+ elif (
348
+ isinstance(value, list)
349
+ and len(value) == 3
350
+ and all(isinstance(row, (list, np.ndarray)) and len(row) == 3 for row in value)
351
+ ):
352
+ self._space_directions = [list(row) for row in value]
353
+ else:
354
+ raise ValueError("Space directions must be a 3x3 array or list of lists.")
355
+
256
356
  @staticmethod
257
357
  def geometry_name():
258
358
  """Return geometry name"""
@@ -268,22 +368,8 @@ class Mask3D(Geometry):
268
368
  :param file_path: Path to nrrd file with data
269
369
  :type file_path: str
270
370
  """
271
- mask3d_data, mask3d_header = nrrd.read(file_path)
272
- figure.geometry.data = mask3d_data
273
- try:
274
- figure.geometry._space_origin = PointVolume(
275
- x=mask3d_header["space origin"][0],
276
- y=mask3d_header["space origin"][1],
277
- z=mask3d_header["space origin"][2],
278
- )
279
- figure.geometry._space = mask3d_header["space"]
280
- figure.geometry._space_directions = mask3d_header["space directions"]
281
- except KeyError as e:
282
- header_keys = ["'space'", "'space directions'", "'space origin'"]
283
- if str(e) in header_keys:
284
- logger.warning(
285
- f"The Mask3D geometry for figure ID '{get_file_name(file_path)}' doesn't contain optional space attributes that have similar names to {', '.join(header_keys)}. To set the values for these attributes, you can use information from the Volume associated with this figure object."
286
- )
371
+ mask3d = Mask3D.create_from_file(file_path)
372
+ figure._set_3d_geometry(mask3d)
287
373
  path_without_filename = "/".join(file_path.split("/")[:-1])
288
374
  remove_dir(path_without_filename)
289
375
 
@@ -295,22 +381,26 @@ class Mask3D(Geometry):
295
381
  :param file_path: Path to nrrd file with data
296
382
  :type file_path: str
297
383
  """
298
- mask3d_data, mask3d_header = nrrd.read(file_path)
299
- geometry = cls(data=mask3d_data)
300
- try:
301
- geometry._space_origin = PointVolume(
302
- x=mask3d_header["space origin"][0],
303
- y=mask3d_header["space origin"][1],
304
- z=mask3d_header["space origin"][2],
305
- )
306
- geometry._space = mask3d_header["space"]
307
- geometry._space_directions = mask3d_header["space directions"]
308
- except KeyError as e:
384
+ from supervisely.volume.volume import read_nrrd_serie_volume_np
385
+
386
+ mask3d_data, meta = read_nrrd_serie_volume_np(file_path)
387
+ direction = np.array(meta["directions"]).reshape(3, 3)
388
+ spacing = np.array(meta["spacing"])
389
+ space_directions = (direction.T * spacing[:, None]).tolist()
390
+ mask3d_header = {
391
+ "space": "right-anterior-superior",
392
+ "space directions": space_directions,
393
+ "space origin": meta.get("origin", None),
394
+ }
395
+
396
+ geometry = cls(data=mask3d_data, volume_header=mask3d_header)
397
+
398
+ fields_to_check = ["space", "space_directions", "space_origin"]
399
+ if any([getattr(geometry, value) is None for value in fields_to_check]):
309
400
  header_keys = ["'space'", "'space directions'", "'space origin'"]
310
- if str(e) in header_keys:
311
- logger.debug(
312
- f"The Mask3D geometry created from the file '{file_path}' doesn't contain optional space attributes that have similar names to {', '.join(header_keys)}. To set the values for these attributes, you can use information from the Volume associated with this figure object."
313
- )
401
+ logger.debug(
402
+ f"The Mask3D geometry created from the file '{file_path}' doesn't contain optional space attributes that have similar names to {', '.join(header_keys)}. To set the values for these attributes, you can use information from the Volume associated with this figure object."
403
+ )
314
404
  return geometry
315
405
 
316
406
  @classmethod
@@ -323,7 +413,7 @@ class Mask3D(Geometry):
323
413
  :return: A Mask3D geometry object.
324
414
  :rtype: Mask3D
325
415
  """
326
- with tempfile.NamedTemporaryFile(delete=True) as temp_file:
416
+ with tempfile.NamedTemporaryFile(delete=True, suffix=".nrrd") as temp_file:
327
417
  temp_file.write(geometry_bytes)
328
418
  return cls.create_from_file(temp_file.name)
329
419
 
@@ -368,12 +458,14 @@ class Mask3D(Geometry):
368
458
  GEOMETRY_TYPE: self.name(),
369
459
  }
370
460
 
371
- if self._space_origin:
372
- res[f"{self._impl_json_class_name()}"][f"{SPACE_ORIGIN}"] = [
373
- self._space_origin.x,
374
- self._space_origin.y,
375
- self._space_origin.z,
376
- ]
461
+ if self.space_origin:
462
+ res[f"{self._impl_json_class_name()}"][f"{SPACE_ORIGIN}"] = self.space_origin
463
+
464
+ if self.space:
465
+ res[f"{self._impl_json_class_name()}"][f"{SPACE}"] = self.space
466
+
467
+ if self.space_directions:
468
+ res[f"{self._impl_json_class_name()}"][f"{SPACE_DIRECTIONS}"] = self.space_directions
377
469
 
378
470
  self._add_creation_info(res)
379
471
  return res
@@ -426,18 +518,30 @@ class Mask3D(Geometry):
426
518
  created_at = json_data.get(CREATED_AT, None)
427
519
  sly_id = json_data.get(ID, None)
428
520
  class_id = json_data.get(CLASS_ID, None)
429
- instance = cls(
521
+
522
+ header = {}
523
+
524
+ space_origin = json_data[json_root_key].get(SPACE_ORIGIN, None)
525
+ if space_origin is not None:
526
+ header["space origin"] = space_origin
527
+
528
+ space = json_data[json_root_key].get(SPACE, None)
529
+ if space is not None:
530
+ header["space"] = space
531
+
532
+ space_directions = json_data[json_root_key].get(SPACE_DIRECTIONS, None)
533
+ if space_directions is not None:
534
+ header["space directions"] = space_directions
535
+
536
+ return cls(
430
537
  data=data.astype(np.bool_),
431
538
  sly_id=sly_id,
432
539
  class_id=class_id,
433
540
  labeler_login=labeler_login,
434
541
  updated_at=updated_at,
435
542
  created_at=created_at,
543
+ volume_header=header,
436
544
  )
437
- if SPACE_ORIGIN in json_data[json_root_key]:
438
- x, y, z = json_data[json_root_key][SPACE_ORIGIN]
439
- instance._space_origin = PointVolume(x=x, y=y, z=z)
440
- return instance
441
545
 
442
546
  @classmethod
443
547
  def _impl_json_class_name(cls):
@@ -474,7 +578,7 @@ class Mask3D(Geometry):
474
578
  path_for_mesh = f"meshes/{figure_id}.nrrd"
475
579
  api.volume.figure.download_stl_meshes([figure_id], [path_for_mesh])
476
580
 
477
- mask3d_data, _ = nrrd.read(path_for_mesh)
581
+ mask3d_data, _ = sly.volume.volume.read_nrrd_serie_volume_np(path_for_mesh)
478
582
  encoded_string = sly.Mask3D.data_2_base64(mask3d_data)
479
583
 
480
584
  print(encoded_string)
@@ -619,3 +723,73 @@ class Mask3D(Geometry):
619
723
  continue
620
724
  geometries_dict[key] = geometry_bytes
621
725
  return geometries_dict
726
+
727
+ def set_volume_space_meta(self, header: Dict):
728
+ """
729
+ Set space, space directions, and space origin attributes from a NRRD header dictionary.
730
+
731
+ :param header: NRRD header dictionary.
732
+ :type header: dict
733
+ """
734
+ if "space" in header:
735
+ self.space = header["space"]
736
+ if "space directions" in header:
737
+ self.space_directions = header["space directions"]
738
+ if "space origin" in header:
739
+ self.space_origin = PointVolume(
740
+ x=header["space origin"][0],
741
+ y=header["space origin"][1],
742
+ z=header["space origin"][2],
743
+ )
744
+
745
+ def create_header(self) -> OrderedDict:
746
+ """
747
+ Create header for encoding Mask3D to NRRD bytes
748
+
749
+ :return: Header for NRRD file
750
+ :rtype: OrderedDict
751
+ """
752
+ header = OrderedDict()
753
+ if self.space is not None:
754
+ header["space"] = self.space
755
+ if self.space_directions is not None:
756
+ header["space directions"] = self.space_directions
757
+ if self.space_origin is not None:
758
+ header["space origin"] = self.space_origin
759
+ return header
760
+
761
+ def orient_ras(self) -> None:
762
+ """
763
+ Transforms the mask data and updates spatial metadata (origin, directions, spacing)
764
+ to align with the RAS coordinate system using SimpleITK.
765
+
766
+ :rtype: None
767
+ """
768
+ import SimpleITK as sitk
769
+
770
+ from supervisely.volume.volume import _sitk_image_orient_ras
771
+
772
+ sitk_volume = sitk.GetImageFromArray(self.data)
773
+ if self.space_origin is not None:
774
+ sitk_volume.SetOrigin(self.space_origin)
775
+ if self.space_directions is not None:
776
+ # Convert space directions to spacing and direction
777
+ space_directions = np.array(self.space_directions)
778
+ spacing = np.linalg.norm(space_directions, axis=1)
779
+ direction = space_directions / spacing[:, np.newaxis]
780
+ sitk_volume.SetSpacing(spacing)
781
+ sitk_volume.SetDirection(direction.flatten())
782
+
783
+ sitk_volume = _sitk_image_orient_ras(sitk_volume)
784
+
785
+ # Extract transformed data and update object
786
+ self.data = sitk.GetArrayFromImage(sitk_volume)
787
+ new_direction = np.array(sitk_volume.GetDirection()).reshape(3, 3)
788
+ new_spacing = np.array(sitk_volume.GetSpacing())
789
+ new_space_directions = (new_direction.T * new_spacing[:, None]).tolist()
790
+ new_header = {
791
+ "space": "right-anterior-superior",
792
+ "space directions": new_space_directions,
793
+ "space origin": sitk_volume.GetOrigin(),
794
+ }
795
+ self.set_volume_space_meta(new_header)
@@ -878,10 +878,16 @@ class Inference:
878
878
 
879
879
  try:
880
880
  if is_production():
881
- self._add_workflow_input(model_source, model_files, model_info)
881
+ without_workflow = deploy_params.get("without_workflow", False)
882
+ if without_workflow is False:
883
+ self._add_workflow_input(model_source, model_files, model_info)
882
884
  except Exception as e:
883
885
  logger.warning(f"Failed to add input to the workflow: {repr(e)}")
884
886
 
887
+ # remove is_benchmark from deploy_params
888
+ if "without_workflow" in deploy_params:
889
+ deploy_params.pop("without_workflow")
890
+
885
891
  self._load_model(deploy_params)
886
892
  if self._model_meta is None:
887
893
  self._set_model_meta_from_classes()
@@ -3709,17 +3715,20 @@ class Inference:
3709
3715
  )
3710
3716
 
3711
3717
  app_name = sly_env.app_name()
3712
- meta = WorkflowMeta(node_settings=WorkflowSettings(title=f"Serve {app_name}"))
3718
+ meta = WorkflowMeta(node_settings=WorkflowSettings(title=app_name))
3713
3719
 
3714
3720
  logger.debug(
3715
3721
  f"Workflow Input: Checkpoint URL - {checkpoint_url}, Checkpoint Name - {checkpoint_name}"
3716
3722
  )
3717
- if checkpoint_url and self.api.file.exists(sly_env.team_id(), checkpoint_url):
3718
- self.api.app.workflow.add_input_file(checkpoint_url, model_weight=True, meta=meta)
3719
- else:
3720
- logger.debug(
3721
- f"Checkpoint {checkpoint_url} not found in Team Files. Cannot set workflow input"
3722
- )
3723
+ if model_source == ModelSource.CUSTOM:
3724
+ if checkpoint_url and self.api.file.exists(sly_env.team_id(), checkpoint_url):
3725
+ # self.api.app.workflow.add_input_file(checkpoint_url, model_weight=True, meta=meta)
3726
+ remote_checkpoint_dir = os.path.dirname(checkpoint_url)
3727
+ self.api.app.workflow.add_input_folder(remote_checkpoint_dir, meta=meta)
3728
+ else:
3729
+ logger.debug(
3730
+ f"Checkpoint {checkpoint_url} not found in Team Files. Cannot set workflow input"
3731
+ )
3723
3732
 
3724
3733
 
3725
3734
  def _exclude_duplicated_predictions(
@@ -242,7 +242,7 @@ class TrainGUI:
242
242
  else:
243
243
  self.task_id = sly_env.task_id(raise_not_found=False)
244
244
  if self.task_id is None:
245
- self.task_id = "debug-session"
245
+ self.task_id = -1
246
246
 
247
247
  self.framework_name = framework_name
248
248
  self.models = models
@@ -257,7 +257,7 @@ class TrainGUI:
257
257
  self.project_info = self._api.project.get_info_by_id(self.project_id)
258
258
  if self.project_info.type is None:
259
259
  raise ValueError(f"Project with ID: '{self.project_id}' does not exist or was archived")
260
-
260
+
261
261
  self.project_meta = ProjectMeta.from_json(self._api.project.get_meta(self.project_id))
262
262
 
263
263
  if self.workspace_id is None:
@@ -350,7 +350,10 @@ class TrainGUI:
350
350
  if model_name is None:
351
351
  experiment_name = "Enter experiment name"
352
352
  else:
353
- experiment_name = f"{self.task_id}_{self.project_info.name}_{model_name}"
353
+ if self.task_id == -1:
354
+ experiment_name = f"debug_{self.project_info.name}_{model_name}"
355
+ else:
356
+ experiment_name = f"{self.task_id}_{self.project_info.name}_{model_name}"
354
357
 
355
358
  if experiment_name == self.training_process.get_experiment_name():
356
359
  return
@@ -9,7 +9,7 @@ import shutil
9
9
  import subprocess
10
10
  from datetime import datetime
11
11
  from os import getcwd, listdir, walk
12
- from os.path import basename, exists, expanduser, isdir, isfile, join
12
+ from os.path import basename, dirname, exists, expanduser, isdir, isfile, join
13
13
  from typing import Any, Dict, List, Literal, Optional, Union
14
14
  from urllib.request import urlopen
15
15
 
@@ -128,7 +128,7 @@ class TrainApp:
128
128
  self._app_name = "custom-app"
129
129
  self.task_id = sly_env.task_id(raise_not_found=False)
130
130
  if self.task_id is None:
131
- self.task_id = "debug-session"
131
+ self.task_id = -1
132
132
  logger.info("TrainApp is running in debug mode")
133
133
 
134
134
  self.framework_name = framework_name
@@ -580,7 +580,7 @@ class TrainApp:
580
580
 
581
581
  # Step 6. Upload artifacts
582
582
  self._set_text_status("uploading")
583
- remote_dir, file_info = self._upload_artifacts()
583
+ remote_dir, session_link_file_info = self._upload_artifacts()
584
584
 
585
585
  # Step 7. [Optional] Run Model Benchmark
586
586
  mb_eval_lnk_file_info, mb_eval_report = None, None
@@ -650,12 +650,35 @@ class TrainApp:
650
650
 
651
651
  # Step 10. Set output widgets
652
652
  self._set_text_status("reset")
653
- self._set_training_output(experiment_info, remote_dir, file_info, mb_eval_report)
653
+ self._set_training_output(
654
+ experiment_info, remote_dir, session_link_file_info, mb_eval_report
655
+ )
654
656
  self._set_ws_progress_status("completed")
655
657
 
656
658
  # Step 11. Workflow output
657
659
  if is_production():
658
- self._workflow_output(remote_dir, file_info, mb_eval_lnk_file_info, mb_eval_report_id)
660
+ best_checkpoint_file_info = self._get_best_checkpoint_info(experiment_info, remote_dir)
661
+ self._workflow_output(
662
+ remote_dir, best_checkpoint_file_info, mb_eval_lnk_file_info, mb_eval_report_id
663
+ )
664
+
665
+ def _get_best_checkpoint_info(self, experiment_info: dict, remote_dir: str) -> FileInfo:
666
+ """
667
+ Returns the best checkpoint info.
668
+
669
+ :param experiment_info: Experiment info.
670
+ :type experiment_info: dict
671
+ :param remote_dir: Remote directory.
672
+ :type remote_dir: str
673
+ :return: Best checkpoint info.
674
+ :rtype: FileInfo
675
+ """
676
+ best_checkpoint_name = experiment_info.get("best_checkpoint")
677
+ remote_best_checkpoint_path = join(remote_dir, "checkpoints", best_checkpoint_name)
678
+ best_checkpoint_file_info = self._api.file.get_info_by_path(
679
+ self.team_id, remote_best_checkpoint_path
680
+ )
681
+ return best_checkpoint_file_info
659
682
 
660
683
  def register_inference_class(
661
684
  self, inference_class: Inference, inference_settings: Union[str, dict] = None
@@ -686,6 +709,8 @@ class TrainApp:
686
709
  """
687
710
  Returns the current state of the application.
688
711
 
712
+ :param experiment_info: Experiment info.
713
+ :type experiment_info: dict
689
714
  :return: Application state.
690
715
  :rtype: dict
691
716
  """
@@ -748,6 +773,27 @@ class TrainApp:
748
773
  """
749
774
  self.gui.load_from_app_state(app_state)
750
775
 
776
+ def add_output_files(self, paths: List[str]) -> None:
777
+ """
778
+ Copies files or directories to the output directory, which will be uploaded to the team files upon training completion.
779
+ If path is a file, it will be uploaded to the root artifacts directory.
780
+ If path is a directory, it will be uploded to the root artifacts directory with the same directory name and structure.
781
+
782
+ :param paths: List of paths to files or directories to be copied to the output directory.
783
+ :type paths: List[str]
784
+ :return: None
785
+ :rtype: None
786
+ """
787
+
788
+ for path in paths:
789
+ if sly_fs.file_exists(path):
790
+ shutil.copyfile(path, join(self.output_dir, sly_fs.get_file_name_with_ext(path)))
791
+ elif sly_fs.dir_exists(path):
792
+ shutil.copytree(path, join(self.output_dir, basename(path)))
793
+ else:
794
+ logger.warning(f"Provided path: '{path}' does not exist. Skipping...")
795
+ continue
796
+
751
797
  # Loaders
752
798
  def _load_models(self, models: Union[str, List[Dict[str, Any]]]) -> List[Dict[str, Any]]:
753
799
  """
@@ -1549,7 +1595,7 @@ class TrainApp:
1549
1595
  logger.debug(f"Uploading '{local_path}' to Supervisely")
1550
1596
  total_size = sly_fs.get_file_size(local_path)
1551
1597
  with self.progress_bar_main(
1552
- message=message, total=total_size, unit="bytes", unit_scale=True
1598
+ message=message, total=total_size, unit="B", unit_scale=True, unit_divisor=1024
1553
1599
  ) as upload_artifacts_pbar:
1554
1600
  self.progress_bar_main.show()
1555
1601
  file_info = self._api.file.upload(
@@ -1774,8 +1820,9 @@ class TrainApp:
1774
1820
  with self.progress_bar_main(
1775
1821
  message="Uploading demo files to Team Files",
1776
1822
  total=total_size,
1777
- unit="bytes",
1823
+ unit="B",
1778
1824
  unit_scale=True,
1825
+ unit_divisor=1024,
1779
1826
  ) as upload_artifacts_pbar:
1780
1827
  self.progress_bar_main.show()
1781
1828
  remote_dir = self._api.file.upload_directory_fast(
@@ -1863,7 +1910,7 @@ class TrainApp:
1863
1910
  f"Uploading artifacts directory: '{self.output_dir}' to Supervisely Team Files directory '{remote_artifacts_dir}'"
1864
1911
  )
1865
1912
  # Clean debug directory if exists
1866
- if task_id == "debug-session":
1913
+ if task_id == -1:
1867
1914
  if self._api.file.dir_exists(self.team_id, f"{remote_artifacts_dir}/", True):
1868
1915
  with self.progress_bar_main(
1869
1916
  message=f"[Debug] Cleaning train artifacts: '{remote_artifacts_dir}/'",
@@ -1888,8 +1935,9 @@ class TrainApp:
1888
1935
  with self.progress_bar_main(
1889
1936
  message="Uploading train artifacts to Team Files",
1890
1937
  total=total_size,
1891
- unit="bytes",
1938
+ unit="B",
1892
1939
  unit_scale=True,
1940
+ unit_divisor=1024,
1893
1941
  ) as upload_artifacts_pbar:
1894
1942
  self.progress_bar_main.show()
1895
1943
  remote_dir = self._api.file.upload_directory_fast(
@@ -1907,6 +1955,7 @@ class TrainApp:
1907
1955
  "state": {"slyFolder": f"{join(remote_dir, 'logs')}"}
1908
1956
  }
1909
1957
  self.gui.training_logs.tensorboard_offline_button.enable()
1958
+
1910
1959
  return remote_dir, file_info
1911
1960
 
1912
1961
  def _set_training_output(
@@ -2095,6 +2144,7 @@ class TrainApp:
2095
2144
  "model_meta": model_meta.to_json(),
2096
2145
  "task_type": task_type,
2097
2146
  }
2147
+ self._benchmark_params["without_workflow"] = True
2098
2148
 
2099
2149
  logger.info(f"Deploy parameters: {self._benchmark_params}")
2100
2150
 
@@ -2162,16 +2212,19 @@ class TrainApp:
2162
2212
  raise ValueError(f"Task type: '{task_type}' is not supported for Model Benchmark")
2163
2213
 
2164
2214
  if self._has_splits_selector:
2215
+ app_session_id = self.task_id
2216
+ if app_session_id == -1:
2217
+ app_session_id = None
2165
2218
  if self.gui.train_val_splits_selector.get_split_method() == "Based on datasets":
2166
2219
  train_info = {
2167
- "app_session_id": self.task_id,
2220
+ "app_session_id": app_session_id,
2168
2221
  "train_dataset_ids": train_dataset_ids,
2169
2222
  "train_images_ids": None,
2170
2223
  "images_count": len(self._train_split),
2171
2224
  }
2172
2225
  else:
2173
2226
  train_info = {
2174
- "app_session_id": self.task_id,
2227
+ "app_session_id": app_session_id,
2175
2228
  "train_dataset_ids": None,
2176
2229
  "train_images_ids": train_images_ids,
2177
2230
  "images_count": len(self._train_split),
@@ -2223,6 +2276,25 @@ class TrainApp:
2223
2276
 
2224
2277
  except Exception as e:
2225
2278
  logger.error(f"Model benchmark failed. {repr(e)}", exc_info=True)
2279
+ pred_error_message = (
2280
+ "Not found any predictions. Please make sure that your model produces predictions."
2281
+ )
2282
+ if isinstance(e, ValueError) and str(e) == pred_error_message:
2283
+ self.gui.training_artifacts.model_benchmark_fail_text.set(
2284
+ "The Model Evaluation report cannot be generated: The model is not making predictions. "
2285
+ "This indicates that your model may not have trained successfully or is underfitted. "
2286
+ "You can try increasing the number of epochs or adjusting the hyperparameters more carefully.",
2287
+ "warning",
2288
+ )
2289
+
2290
+ lnk_file_info, report, report_id, eval_metrics, primary_metric_name = (
2291
+ None,
2292
+ None,
2293
+ None,
2294
+ {},
2295
+ None,
2296
+ )
2297
+
2226
2298
  self._set_text_status("finalizing")
2227
2299
  self.progress_bar_main.hide()
2228
2300
  self.progress_bar_secondary.hide()
@@ -2288,6 +2360,14 @@ class TrainApp:
2288
2360
  ):
2289
2361
  """
2290
2362
  Adds the output data to the workflow.
2363
+
2364
+ :param team_files_dir: Team files directory.
2365
+ :type team_files_dir: str
2366
+ :param file_info: FileInfo of the best checkpoint.
2367
+ :type file_info: FileInfo
2368
+ :param model_benchmark_report: FileInfo of the model benchmark report link (.lnk).
2369
+ :type model_benchmark_report: Optional[FileInfo]
2370
+ :param model_benchmark_report_id: Model benchmark report ID.
2291
2371
  """
2292
2372
  try:
2293
2373
  module_id = (
@@ -2310,7 +2390,7 @@ class TrainApp:
2310
2390
 
2311
2391
  if file_info:
2312
2392
  relation_settings = WorkflowSettings(
2313
- title="Train Artifacts",
2393
+ title="Checkpoints",
2314
2394
  icon="folder",
2315
2395
  icon_color="#FFA500",
2316
2396
  icon_bg_color="#FFE8BE",
@@ -2321,7 +2401,10 @@ class TrainApp:
2321
2401
  relation_settings=relation_settings, node_settings=node_settings
2322
2402
  )
2323
2403
  logger.debug(f"Workflow Output: meta \n {meta}")
2324
- self._api.app.workflow.add_output_file(file_info, model_weight=True, meta=meta)
2404
+ # self._api.app.workflow.add_output_file(file_info, model_weight=True, meta=meta)
2405
+
2406
+ remote_checkpoint_dir = dirname(file_info.path)
2407
+ self._api.app.workflow.add_output_folder(remote_checkpoint_dir, meta=meta)
2325
2408
  else:
2326
2409
  logger.debug(
2327
2410
  f"File with checkpoints not found in Team Files. Cannot set workflow output."
@@ -2370,13 +2453,14 @@ class TrainApp:
2370
2453
  logger.debug("Tensorboard server is already running")
2371
2454
  return
2372
2455
  self._register_routes()
2456
+
2373
2457
  args = [
2374
2458
  "tensorboard",
2375
2459
  "--logdir",
2376
2460
  self.log_dir,
2377
2461
  "--host=localhost",
2378
2462
  f"--port={self._tensorboard_port}",
2379
- "--load_fast=true",
2463
+ "--load_fast=auto",
2380
2464
  "--reload_multifile=true",
2381
2465
  ]
2382
2466
  self._tensorboard_process = subprocess.Popen(args)
@@ -2522,6 +2606,9 @@ class TrainApp:
2522
2606
  self._set_ws_progress_status("finalizing")
2523
2607
  self._finalize(experiment_info)
2524
2608
  self.gui.training_process.start_button.loading = False
2609
+
2610
+ # Shutdown the app after training is finished
2611
+ self.app.shutdown()
2525
2612
  except Exception as e:
2526
2613
  message = f"Error occurred during finalizing and uploading training artifacts. {check_logs_text}"
2527
2614
  self._show_error(message, e)
@@ -2706,6 +2793,7 @@ class TrainApp:
2706
2793
  total=size,
2707
2794
  unit="B",
2708
2795
  unit_scale=True,
2796
+ unit_divisor=1024,
2709
2797
  ) as export_upload_main_pbar:
2710
2798
  logger.debug(f"Uploading {len(export_weights)} export weights of size {size} bytes")
2711
2799
  logger.debug(f"Destination paths: {file_dest_paths}")
@@ -3,7 +3,7 @@
3
3
 
4
4
 
5
5
  import os
6
- from typing import List, Tuple, Union
6
+ from typing import List, Optional, Tuple, Union
7
7
 
8
8
  import numpy as np
9
9
  import pydicom
@@ -15,6 +15,7 @@ import supervisely.volume.nrrd_encoder as nrrd_encoder
15
15
  from supervisely import logger
16
16
  from supervisely.geometry.mask_3d import Mask3D
17
17
  from supervisely.io.fs import get_file_ext, get_file_name, list_files_recursively
18
+ from supervisely.volume.stl_converter import matrix_from_nrrd_header
18
19
 
19
20
  # Do NOT use directly for extension validation. Use is_valid_ext() / has_valid_ext() below instead.
20
21
  ALLOWED_VOLUME_EXTENSIONS = [".nrrd", ".dcm"]
@@ -455,7 +456,7 @@ def inspect_dicom_series(root_dir: str, logging: bool = True) -> dict:
455
456
  return found_series
456
457
 
457
458
 
458
- def _sitk_image_orient_ras(sitk_volume):
459
+ def _sitk_image_orient_ras(sitk_volume: sitk.Image) -> sitk.Image:
459
460
  import SimpleITK as sitk
460
461
 
461
462
  if sitk_volume.GetDimension() == 4 and sitk_volume.GetSize()[3] == 1:
@@ -704,7 +705,7 @@ def read_nrrd_serie_volume(path: str) -> Tuple[sitk.Image, dict]:
704
705
  """
705
706
  Read NRRD volume with given path.
706
707
 
707
- :param path: Paths to DICOM volume files.
708
+ :param path: Path to NRRD volume files.
708
709
  :type path: List[str]
709
710
  :return: Volume data in SimpleITK.Image format and dictionary with metadata.
710
711
  :rtype: Tuple[SimpleITK.Image, dict]
@@ -741,12 +742,12 @@ def read_nrrd_serie_volume(path: str) -> Tuple[sitk.Image, dict]:
741
742
  return sitk_volume, meta
742
743
 
743
744
 
744
- def read_nrrd_serie_volume_np(paths: List[str]) -> Tuple[np.ndarray, dict]:
745
+ def read_nrrd_serie_volume_np(paths: str) -> Tuple[np.ndarray, dict]:
745
746
  """
746
747
  Read NRRD volume with given path.
747
748
 
748
- :param path: Paths to NRRD volume file.
749
- :type path: List[str]
749
+ :param paths: Path to NRRD volume file.
750
+ :type paths: str
750
751
  :return: Volume data in NumPy array format and dictionary with metadata.
751
752
  :rtype: Tuple[np.ndarray, dict]
752
753
  :Usage example:
@@ -868,17 +869,18 @@ def is_nifti_file(path: str) -> bool:
868
869
 
869
870
  def convert_3d_geometry_to_mesh(
870
871
  geometry: Mask3D,
871
- spacing: tuple = None,
872
+ spacing: tuple = (1.0, 1.0, 1.0),
872
873
  level: float = 0.5,
873
874
  apply_decimation: bool = False,
874
875
  decimation_fraction: float = 0.5,
876
+ volume_meta: Optional[dict] = None,
875
877
  ) -> Trimesh:
876
878
  """
877
879
  Converts a 3D geometry (Mask3D) to a Trimesh mesh.
878
880
 
879
881
  :param geometry: The 3D geometry to convert.
880
882
  :type geometry: supervisely.geometry.mask_3d.Mask3D
881
- :param spacing: Voxel spacing in (x, y, z). Default is taken from geometry meta.
883
+ :param spacing: Voxel spacing in (x, y, z).
882
884
  :type spacing: tuple
883
885
  :param level: Isosurface value for marching cubes. Default is 0.5.
884
886
  :type level: float
@@ -886,6 +888,8 @@ def convert_3d_geometry_to_mesh(
886
888
  :type apply_decimation: bool
887
889
  :param decimation_fraction: Fraction of faces to keep if decimation is applied. Default is 0.5.
888
890
  :type decimation_fraction: float
891
+ :param volume_meta: Metadata of the volume. Used for mesh alignment if geometry lacks specific fields. Default is None.
892
+ :type volume_meta: dict, optional
889
893
  :return: The resulting Trimesh mesh.
890
894
  :rtype: trimesh.Trimesh
891
895
 
@@ -893,37 +897,40 @@ def convert_3d_geometry_to_mesh(
893
897
 
894
898
  .. code-block:: python
895
899
 
900
+ volume_header = nrrd.read_header("path/to/volume.nrrd")
896
901
  mask3d = Mask3D.create_from_file("path/to/mask3d")
897
- mesh = convert_3d_geometry_to_mesh(mask3d, spacing=(1.0, 1.0, 1.0), level=0.7, apply_decimation=True)
902
+ mesh = convert_3d_geometry_to_mesh(mask3d, spacing=(1.0, 1.0, 1.0), level=0.7, apply_decimation=True, volume_meta=volume_header)
898
903
  """
899
904
  from skimage import measure
900
905
 
901
- # Flip the mask along the x-axis to correct mirroring
902
- mask = np.flip(geometry.data, axis=0)
903
- if spacing is None:
904
- try:
905
- spacing = tuple(
906
- float(abs(direction[i])) for i, direction in enumerate(geometry._space_directions)
907
- )
908
- except Exception as e:
909
- logger.warning(
910
- "Failed to get spacing from geometry meta. Using (1.0, 1.0, 1.0).", exc_info=1
911
- )
912
- spacing = (1.0, 1.0, 1.0)
913
-
914
- # marching_cubes expects (z, y, x) order
915
- verts, faces, normals, _ = measure.marching_cubes(
916
- mask.astype(np.float32), level=level, spacing=spacing
917
- )
906
+ if volume_meta is None:
907
+ volume_meta = {}
908
+
909
+ space_directions = geometry.space_directions or volume_meta.get("space directions")
910
+ space_origin = geometry.space_origin or volume_meta.get("space origin")
911
+
912
+ verts, faces, normals, _ = measure.marching_cubes(geometry.data, level=level, spacing=spacing)
918
913
  mesh = Trimesh(vertices=verts, faces=faces, vertex_normals=normals, process=False)
919
914
 
920
915
  if apply_decimation and 0 < decimation_fraction < 1:
921
916
  mesh = mesh.simplify_quadric_decimation(int(len(mesh.faces) * decimation_fraction))
922
917
 
918
+ if space_directions is not None and space_origin is not None:
919
+ header = {
920
+ "space directions": space_directions,
921
+ "space origin": space_origin,
922
+ }
923
+ align_mesh_to_volume(mesh, header)
924
+
925
+ # flip x and y axes to match initial mask orientation
926
+ mesh.apply_transform(np.diag([-1, -1, 1, 1]))
927
+
928
+ mesh.fix_normals()
929
+
923
930
  return mesh
924
931
 
925
932
 
926
- def export_3d_as_mesh(geometry: Mask3D, output_path: str, kwargs=None):
933
+ def export_3d_as_mesh(geometry: Mask3D, output_path: str, **kwargs):
927
934
  """
928
935
  Exports the 3D mesh representation of the object to a file in either STL or OBJ format.
929
936
 
@@ -936,7 +943,7 @@ def export_3d_as_mesh(geometry: Mask3D, output_path: str, kwargs=None):
936
943
  - level (float): Isosurface value for marching cubes. Default is 0.5.
937
944
  - apply_decimation (bool): Whether to simplify the mesh. Default is False.
938
945
  - decimation_fraction (float): Fraction of faces to keep if decimation is applied. Default is 0.5.
939
- :type kwargs: dict, optional
946
+ - volume_meta (dict): Metadata of the volume. Used for mesh alignment if geometry lacks specific fields. Default is None.
940
947
  :return: None
941
948
 
942
949
  :Usage example:
@@ -946,14 +953,34 @@ def export_3d_as_mesh(geometry: Mask3D, output_path: str, kwargs=None):
946
953
  mask3d_path = "path/to/mask3d"
947
954
  mask3d = Mask3D.create_from_file(mask3d_path)
948
955
 
949
- mask3d.export_3d_as_mesh(mask3d, "output.stl", {"spacing": (1.0, 1.0, 1.0), "level": 0.7, "apply_decimation": True})
956
+ mask3d.export_3d_as_mesh(mask3d, "output.stl", spacing=(1.0, 1.0, 1.0), level=0.7, apply_decimation=True)
950
957
  """
951
958
 
952
- if kwargs is None:
953
- kwargs = {}
954
-
955
959
  if get_file_ext(output_path).lower() not in [".stl", ".obj"]:
956
960
  raise ValueError('File extension must be either ".stl" or ".obj"')
957
961
 
958
962
  mesh = convert_3d_geometry_to_mesh(geometry, **kwargs)
959
963
  mesh.export(output_path)
964
+
965
+
966
+ def align_mesh_to_volume(mesh: Trimesh, volume_header: dict) -> None:
967
+ """
968
+ Transforms the given mesh in-place using spatial information from an NRRD header.
969
+ The mesh will be tranformed to match the coordinate system defined in the header.
970
+
971
+ :param mesh: The mesh object to be transformed. The transformation is applied in-place.
972
+ :type mesh: Trimesh
973
+ :param volume_header: The NRRD header containing spatial metadata, including "space directions",
974
+ "space origin", and "space". Field "space" should be in the format of
975
+ "right-anterior-superior", "left-anterior-superior", etc.
976
+ :type volume_header: dict
977
+ :returns: None
978
+ :rtype: None
979
+ """
980
+ from supervisely.geometry.constants import SPACE_ORIGIN
981
+ from supervisely.geometry.mask_3d import PointVolume
982
+
983
+ if isinstance(volume_header["space origin"], PointVolume):
984
+ volume_header["space origin"] = volume_header["space origin"].to_json()[SPACE_ORIGIN]
985
+ transform_mat = matrix_from_nrrd_header(volume_header)
986
+ mesh.apply_transform(transform_mat)
@@ -675,6 +675,6 @@ class VolumeFigure(VideoFigure):
675
675
  )
676
676
 
677
677
  self.geometry.data = new_geometry.data
678
- self.geometry._space = new_geometry._space
679
- self.geometry._space_origin = new_geometry._space_origin
680
- self.geometry._space_directions = new_geometry._space_directions
678
+ self.geometry.space = new_geometry.space
679
+ self.geometry.space_origin = new_geometry.space_origin
680
+ self.geometry.space_directions = new_geometry.space_directions
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: supervisely
3
- Version: 6.73.368
3
+ Version: 6.73.370
4
4
  Summary: Supervisely Python SDK.
5
5
  Home-page: https://github.com/supervisely/supervisely
6
6
  Author: Supervisely
@@ -77,7 +77,7 @@ supervisely/api/video/video_tag_api.py,sha256=wPe1HeJyg9kV1z2UJq6BEte5sKBoPJ2UGA
77
77
  supervisely/api/volume/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
78
78
  supervisely/api/volume/volume_annotation_api.py,sha256=NOHpLeqHLCeRs1KlXWoG91vtIXdUVTO69wh1ws0VmOQ,22246
79
79
  supervisely/api/volume/volume_api.py,sha256=rz_yaBbbTkVeAHmF449zPI8Va_YpDHfHYjXgjGAjMJg,55390
80
- supervisely/api/volume/volume_figure_api.py,sha256=WwmcMw7o3Nvyv52tzmz64yF-WJI0qzAU-zL2JlD7_w0,26039
80
+ supervisely/api/volume/volume_figure_api.py,sha256=upjIdiiQgOJ6och0KUg0rQo-q-PIipL5RX2V3fOBPvI,25437
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
@@ -255,7 +255,7 @@ supervisely/app/widgets/empty/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5N
255
255
  supervisely/app/widgets/empty/empty.py,sha256=fCr8I7CQ2XLo59bl2txjDrblOGiu0TzUcM-Pq6s7gKY,1285
256
256
  supervisely/app/widgets/empty/template.html,sha256=aDBKkin5aLuqByzNN517-rTYCGIg5SPKgnysYMPYjv8,40
257
257
  supervisely/app/widgets/experiment_selector/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
258
- supervisely/app/widgets/experiment_selector/experiment_selector.py,sha256=M6IHpTnUAyyz3nkDLtf_Wu5B2-YeNxGFWDV0o_4_-u0,19889
258
+ supervisely/app/widgets/experiment_selector/experiment_selector.py,sha256=MButdiR6j6bpvJRI9iYtO5UjQu_Dc4ABzcrPsy2YcRg,19933
259
259
  supervisely/app/widgets/experiment_selector/style.css,sha256=-zPPXHnJvatYj_xVVAb7T8uoSsUTyhm5xCKWkkFQ78E,548
260
260
  supervisely/app/widgets/experiment_selector/template.html,sha256=k7f_Xl6nDUXXwu6IY_RblYni5TbZRRxCBduY5O_SyFs,2908
261
261
  supervisely/app/widgets/fast_table/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -671,7 +671,7 @@ supervisely/convert/volume/dicom/dicom_helper.py,sha256=OrKlyt1hA5BOXKhE1LF1WxBI
671
671
  supervisely/convert/volume/nii/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
672
672
  supervisely/convert/volume/nii/nii_planes_volume_converter.py,sha256=dXoBA8AYUOEjLpV2cZJ5n1HDq4_gNhnD__NVsgfc_Qc,14551
673
673
  supervisely/convert/volume/nii/nii_volume_converter.py,sha256=BAOKX96-bp6WfTFLrCQNrXk2YhKqIFSU5LJ-auKiAfc,8514
674
- supervisely/convert/volume/nii/nii_volume_helper.py,sha256=ME_2bgbKZg4IYDFOYqhGRdt7LbwigdF2p6oSgPgPWpw,14132
674
+ supervisely/convert/volume/nii/nii_volume_helper.py,sha256=_FepXNu1RIFuzBOv0aAtrG-J2xN3uyyEMcq6pUD9fsk,14159
675
675
  supervisely/convert/volume/sly/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
676
676
  supervisely/convert/volume/sly/sly_volume_converter.py,sha256=XmSuxnRqxchG87b244f3h0UHvOt6IkajMquL1drWlCM,5595
677
677
  supervisely/convert/volume/sly/sly_volume_helper.py,sha256=gUY0GW3zDMlO2y-zQQG36uoXMrKkKz4-ErM1CDxFCxE,5620
@@ -687,7 +687,7 @@ supervisely/geometry/any_geometry.py,sha256=BOZBsuMYgtkA7cOKp-URjzV9zQkpHuvfp2QP
687
687
  supervisely/geometry/bitmap.py,sha256=-tyIXCfgvNn3c7jHs18aq693JR5xdvhfNf6Vmf4952g,21869
688
688
  supervisely/geometry/bitmap_base.py,sha256=lNamVL3gZ355oYlIEPc0yC84k1bxuCbUVI0ouaZ_Q4k,13814
689
689
  supervisely/geometry/closed_surface_mesh.py,sha256=3ZplCm3Q2bhPcxNmtv2U1UfdezRkC3_BxjwH4yl7wrs,1558
690
- supervisely/geometry/constants.py,sha256=TPYWGcr2GsbgEtKiZj1L_6wpmbaWU9Qjtlwjg136iVg,788
690
+ supervisely/geometry/constants.py,sha256=6lXpwTTFuswuH9WXMy4akHKshQ5C6fgQhdY-XCdVIMA,842
691
691
  supervisely/geometry/conversions.py,sha256=ZY6xWYFWaDA5KDJkcIBBP8LAmMfZwxMeVFfYUYEM6fw,1170
692
692
  supervisely/geometry/cuboid.py,sha256=GVHeUrVgfjUjE3PorV_vtge6_thDvvUYI5-9_HZjfWs,21077
693
693
  supervisely/geometry/cuboid_2d.py,sha256=enQ-7ZVix5SqC7ZEwxgC0Kvmz9J_wXL7NH3m02snNvc,13444
@@ -697,7 +697,7 @@ supervisely/geometry/graph.py,sha256=kSShcGU4kZgwAbvTrqGzC55qha0nI7M5luiMZSbNx_4
697
697
  supervisely/geometry/helpers.py,sha256=2gdYMFWTAr836gVXcp-lkDQs9tdaV0ou33kj3mzJBQA,5132
698
698
  supervisely/geometry/image_rotator.py,sha256=wrU8cXEUfuNcmPms2myUV4BpZqz_2oDArsEUFeiTpxs,6888
699
699
  supervisely/geometry/main_tests.py,sha256=K3Olsz9igHDW2IfIA5JOpjoE8bZ3ex2PXvVR2ZCDrHU,27199
700
- supervisely/geometry/mask_3d.py,sha256=MNvAIALV4vmM3VT4oOJR39mhO0rjBd7QHy1nZK8PbiE,20508
700
+ supervisely/geometry/mask_3d.py,sha256=gaac4wUoG-qmpVcttgfAh2WhS3VUWjcdNqw5V-Aa5GA,26720
701
701
  supervisely/geometry/multichannel_bitmap.py,sha256=dL0igkOCVZiIZ9LDU7srFLA50XGo4doE-B5_E1uboXM,4968
702
702
  supervisely/geometry/point.py,sha256=7ed_Ipd-Ab8ZeqJF5ft0kP9pKVb2iWXCxPuRhMuweMc,13228
703
703
  supervisely/geometry/point_3d.py,sha256=0ico0aV4fuKNBVrysDjUy1Cx1S9CEzBlEVE3AsbVd0E,1669
@@ -888,7 +888,7 @@ supervisely/nn/benchmark/visualization/widgets/table/__init__.py,sha256=47DEQpj8
888
888
  supervisely/nn/benchmark/visualization/widgets/table/table.py,sha256=atmDnF1Af6qLQBUjLhK18RMDKAYlxnsuVHMSEa5a-e8,4319
889
889
  supervisely/nn/inference/__init__.py,sha256=QFukX2ip-U7263aEPCF_UCFwj6EujbMnsgrXp5Bbt8I,1623
890
890
  supervisely/nn/inference/cache.py,sha256=rc_CRlCuTCzLDtcl1paTJib7ALTer0ge9o32WtoUMkY,34795
891
- supervisely/nn/inference/inference.py,sha256=OoslbS1Rog8PgvxBCrdkyxwMQw8ITlot1mQSxxSCd0c,177297
891
+ supervisely/nn/inference/inference.py,sha256=gq0yvMiFZ2FfJvvEmw7PRHa0GikCwRX9S2CE_0fuGX4,177798
892
892
  supervisely/nn/inference/inference_request.py,sha256=y6yw0vbaRRcEBS27nq3y0sL6Gmq2qLA_Bm0GrnJGegE,14267
893
893
  supervisely/nn/inference/session.py,sha256=dIg2F-OBl68pUzcmtmcI0YQIp1WWNnrJTVMjwFN91Q4,35824
894
894
  supervisely/nn/inference/uploader.py,sha256=21a9coOimCHhEqAbV-llZWcp12847DEMoQp3N16bpK0,5425
@@ -994,10 +994,10 @@ supervisely/nn/tracking/__init__.py,sha256=Ld1ed7ZZQZPkhX-5Xr-UbHZx5zLCm2-tInHnP
994
994
  supervisely/nn/tracking/boxmot.py,sha256=H9cQjYGL9nX_TLrfKDChhljTIiE9lffcgbwWCf_4PJU,4277
995
995
  supervisely/nn/tracking/tracking.py,sha256=WNrNm02B1pspA3d_AmzSJ-54RZTqWV2NZiC7FHe88bo,857
996
996
  supervisely/nn/training/__init__.py,sha256=gY4PCykJ-42MWKsqb9kl-skemKa8yB6t_fb5kzqR66U,111
997
- supervisely/nn/training/train_app.py,sha256=23nBt9fxa8LBIzFT_8cKwXoxRu2kkCmAC1-9dnUFgPg,112259
997
+ supervisely/nn/training/train_app.py,sha256=D2Fuy1SzoHTqeMWrdLLVLqeZN5Eu6M_CzU85y78na6I,116077
998
998
  supervisely/nn/training/gui/__init__.py,sha256=Nqnn8clbgv-5l0PgxcTOldg8mkMKrFn4TvPL-rYUUGg,1
999
999
  supervisely/nn/training/gui/classes_selector.py,sha256=Bpp-RFDQqcZ0kLJmS6ZnExkdscWwRusvF4vbWjEsKlQ,3926
1000
- supervisely/nn/training/gui/gui.py,sha256=_8l7dXoQGs3c5iIAUzWPGC6AQbN920iLW1riBDDV3hY,43183
1000
+ supervisely/nn/training/gui/gui.py,sha256=Z68uMPNkOyb70rpxfVDfJuGSzcoOhrqqDog8PABF2JQ,43312
1001
1001
  supervisely/nn/training/gui/hyperparameters_selector.py,sha256=5dUCYAx4E0HBLguj2B_s2nWeGGCWzv6vJeT0XvDJO3M,7746
1002
1002
  supervisely/nn/training/gui/input_selector.py,sha256=rmirJzpdxuYONI6y5_cvMdGWBJ--T20YTsISghATHu4,2510
1003
1003
  supervisely/nn/training/gui/model_selector.py,sha256=I6KRKyylpwUEC3CApEnzDKkWe5xqju0Az3D0Eg32Jdc,5352
@@ -1075,13 +1075,13 @@ supervisely/volume/__init__.py,sha256=EBZBY_5mzabXzMUQh5akusIGd16XnX9n8J0jIi_JmW
1075
1075
  supervisely/volume/nrrd_encoder.py,sha256=1lqwwyqxEvctw1ysQ70x4xPSV1uy1g5YcH5CURwL7-c,4084
1076
1076
  supervisely/volume/nrrd_loader.py,sha256=_yqahKcqSRxunHZ5LtnUWIRA7UvIhPKOhAUwYijSGY4,9065
1077
1077
  supervisely/volume/stl_converter.py,sha256=WIMQgHO_u4JT58QdcMXcb_euF1BFhM7D52IVX_0QTxE,6285
1078
- supervisely/volume/volume.py,sha256=ekU8gYhSXrTvWISd_HJT7lwtQ9Uh5t7qgVcFwzJ2NOc,29273
1078
+ supervisely/volume/volume.py,sha256=jDu_p1zPQxCojjtdJlVVTxfuKgVCYmMSY13Xz99k7pA,30765
1079
1079
  supervisely/volume_annotation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1080
1080
  supervisely/volume_annotation/constants.py,sha256=BdFIh56fy7vzLIjt0gH8xP01EIU-qgQIwbSHVUcABCU,569
1081
1081
  supervisely/volume_annotation/plane.py,sha256=wyezAcc8tLp38O44CwWY0wjdQxf3VjRdFLWooCrk-Nw,16301
1082
1082
  supervisely/volume_annotation/slice.py,sha256=9m3jtUYz4PYKV3rgbeh2ofDebkyg4TomNbkC6BwZ0lA,4635
1083
1083
  supervisely/volume_annotation/volume_annotation.py,sha256=pGu6n8_5JkFpir4HTVRf302gGD2EqJ96Gh4M0_236Qg,32047
1084
- supervisely/volume_annotation/volume_figure.py,sha256=TbwqWml7zELQJkrYxTrlblr8SYsmTjYVz-E-3Zd4oxo,25337
1084
+ supervisely/volume_annotation/volume_figure.py,sha256=3iFyknF8TlLCMBwgg8gJ26wnNTDTRaw8uEJFVkJGh78,25331
1085
1085
  supervisely/volume_annotation/volume_object.py,sha256=rWzOnycoSJ4-CvFgDOP_rPortU4CdcYR26txe5wJHNo,3577
1086
1086
  supervisely/volume_annotation/volume_object_collection.py,sha256=Tc4AovntgoFj5hpTLBv7pCQ3eL0BjorOVpOh2nAE_tA,5706
1087
1087
  supervisely/volume_annotation/volume_tag.py,sha256=MEk1ky7X8zWe2JgV-j8jXt14e8yu2g1kScU26n9lOMk,9494
@@ -1097,9 +1097,9 @@ supervisely/worker_proto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
1097
1097
  supervisely/worker_proto/worker_api_pb2.py,sha256=VQfi5JRBHs2pFCK1snec3JECgGnua3Xjqw_-b3aFxuM,59142
1098
1098
  supervisely/worker_proto/worker_api_pb2_grpc.py,sha256=3BwQXOaP9qpdi0Dt9EKG--Lm8KGN0C5AgmUfRv77_Jk,28940
1099
1099
  supervisely_lib/__init__.py,sha256=7-3QnN8Zf0wj8NCr2oJmqoQWMKKPKTECvjH9pd2S5vY,159
1100
- supervisely-6.73.368.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
1101
- supervisely-6.73.368.dist-info/METADATA,sha256=xfFo1v5HarzQuLuIy2_abk6oYG3SHJcRjiw9wC9qEB4,35154
1102
- supervisely-6.73.368.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
1103
- supervisely-6.73.368.dist-info/entry_points.txt,sha256=U96-5Hxrp2ApRjnCoUiUhWMqijqh8zLR03sEhWtAcms,102
1104
- supervisely-6.73.368.dist-info/top_level.txt,sha256=kcFVwb7SXtfqZifrZaSE3owHExX4gcNYe7Q2uoby084,28
1105
- supervisely-6.73.368.dist-info/RECORD,,
1100
+ supervisely-6.73.370.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
1101
+ supervisely-6.73.370.dist-info/METADATA,sha256=fcZ2RfmecBVHdsVjrjwfDZ4Uv2z21M0Y-jJYbnaiFRE,35154
1102
+ supervisely-6.73.370.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
1103
+ supervisely-6.73.370.dist-info/entry_points.txt,sha256=U96-5Hxrp2ApRjnCoUiUhWMqijqh8zLR03sEhWtAcms,102
1104
+ supervisely-6.73.370.dist-info/top_level.txt,sha256=kcFVwb7SXtfqZifrZaSE3owHExX4gcNYe7Q2uoby084,28
1105
+ supervisely-6.73.370.dist-info/RECORD,,