supervisely 6.73.325__py3-none-any.whl → 6.73.327__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.
Files changed (28) hide show
  1. supervisely/annotation/annotation.py +1 -1
  2. supervisely/app/widgets/pretrained_models_selector/pretrained_models_selector.py +17 -14
  3. supervisely/app/widgets/pretrained_models_selector/template.html +2 -1
  4. supervisely/convert/image/yolo/yolo_helper.py +95 -25
  5. supervisely/convert/volume/nii/nii_planes_volume_converter.py +54 -6
  6. supervisely/convert/volume/nii/nii_volume_converter.py +7 -7
  7. supervisely/convert/volume/nii/nii_volume_helper.py +49 -0
  8. supervisely/nn/inference/gui/serving_gui_template.py +2 -3
  9. supervisely/nn/inference/inference.py +33 -25
  10. supervisely/nn/training/gui/classes_selector.py +24 -19
  11. supervisely/nn/training/gui/gui.py +90 -37
  12. supervisely/nn/training/gui/hyperparameters_selector.py +32 -15
  13. supervisely/nn/training/gui/input_selector.py +13 -2
  14. supervisely/nn/training/gui/model_selector.py +16 -6
  15. supervisely/nn/training/gui/train_val_splits_selector.py +10 -1
  16. supervisely/nn/training/gui/training_artifacts.py +23 -4
  17. supervisely/nn/training/gui/training_logs.py +15 -3
  18. supervisely/nn/training/gui/training_process.py +14 -13
  19. supervisely/nn/training/train_app.py +59 -24
  20. supervisely/nn/utils.py +9 -0
  21. supervisely/project/project.py +16 -3
  22. supervisely/volume/volume.py +19 -21
  23. {supervisely-6.73.325.dist-info → supervisely-6.73.327.dist-info}/METADATA +1 -1
  24. {supervisely-6.73.325.dist-info → supervisely-6.73.327.dist-info}/RECORD +28 -28
  25. {supervisely-6.73.325.dist-info → supervisely-6.73.327.dist-info}/LICENSE +0 -0
  26. {supervisely-6.73.325.dist-info → supervisely-6.73.327.dist-info}/WHEEL +0 -0
  27. {supervisely-6.73.325.dist-info → supervisely-6.73.327.dist-info}/entry_points.txt +0 -0
  28. {supervisely-6.73.325.dist-info → supervisely-6.73.327.dist-info}/top_level.txt +0 -0
@@ -3088,7 +3088,7 @@ class Annotation:
3088
3088
  def to_yolo(
3089
3089
  self,
3090
3090
  class_names: List[str],
3091
- task_type: Literal["detection", "segmentation", "pose"] = "detection",
3091
+ task_type: Literal["detect", "segment", "pose"] = "detect",
3092
3092
  ) -> List[str]:
3093
3093
  """
3094
3094
  Convert Supervisely annotation to YOLO annotation format.
@@ -4,7 +4,7 @@ from supervisely.api.api import Api
4
4
  from supervisely.app.content import DataJson, StateJson
5
5
  from supervisely.app.widgets import Widget
6
6
  from supervisely.io.fs import get_file_ext
7
- from supervisely.nn.utils import ModelSource
7
+ from supervisely.nn.utils import ModelSource, _get_model_name
8
8
 
9
9
 
10
10
  class PretrainedModelsSelector(Widget):
@@ -147,9 +147,11 @@ class PretrainedModelsSelector(Widget):
147
147
  if train_version == "v1":
148
148
  model_name = selected_model.get(model_name_column)
149
149
  if model_name is None:
150
- raise ValueError(
151
- "Could not find model name. Make sure you have column 'Model' in your models list."
152
- )
150
+ model_name = _get_model_name(selected_model)
151
+ if model_name is None:
152
+ raise ValueError(
153
+ "Could not find model name. Make sure you have column 'Model' in your models list."
154
+ )
153
155
 
154
156
  model_meta = selected_model.get("meta")
155
157
  if model_meta is None:
@@ -230,24 +232,25 @@ class PretrainedModelsSelector(Widget):
230
232
  for task_type in self._table_data:
231
233
  for arch_type in self._table_data[task_type]:
232
234
  for idx, model in enumerate(self._table_data[task_type][arch_type]):
233
- model_meta = model.get("meta", {})
234
- if model_meta.get("model_name") == model_name:
235
- self.set_active_task_type(task_type)
236
- self.set_active_arch_type(arch_type)
237
- self.set_active_row(idx)
238
- return
235
+ name_from_info = _get_model_name(model)
236
+ if name_from_info is not None:
237
+ if name_from_info.lower() == model_name.lower():
238
+ self.set_active_task_type(task_type)
239
+ self.set_active_arch_type(arch_type)
240
+ self.set_active_row(idx)
241
+ return
239
242
 
240
243
  def get_by_model_name(self, model_name: str) -> Union[Dict, None]:
241
244
  for task_type in self._table_data:
242
245
  for arch_type in self._table_data[task_type]:
243
246
  for idx, model in enumerate(self._table_data[task_type][arch_type]):
244
- model_meta = model.get("meta", {})
245
- if model_meta.get("model_name") == model_name:
246
- return model
247
+ name_from_info = _get_model_name(model)
248
+ if name_from_info is not None:
249
+ if name_from_info.lower() == model_name.lower():
250
+ return model
247
251
 
248
252
  def _filter_and_sort_models(self, models: List[Dict], sort_models: bool = True) -> Dict:
249
253
  filtered_models = {}
250
-
251
254
  for model in models:
252
255
  for key in model:
253
256
  if isinstance(model[key], (int, float)):
@@ -62,7 +62,8 @@
62
62
  <tr>
63
63
  <template v-for="(col, colKey) in data.{{{widget.widget_id}}}.tableData[state.{{{widget.widget_id}}}.selectedTaskType][state.{{{widget.widget_id}}}.selectedArchType][0]">
64
64
  <th v-if="colKey != 'meta'" :key="colKey">
65
- <div>{{ colKey }}</div>
65
+ <div v-if="colKey==='model_name'">Model</div>
66
+ <div v-else>{{ colKey }}</div>
66
67
  </th>
67
68
  </template>
68
69
  </tr>
@@ -17,7 +17,8 @@ from supervisely.geometry.polygon import Polygon
17
17
  from supervisely.geometry.polyline import Polyline
18
18
  from supervisely.geometry.rectangle import Rectangle
19
19
  from supervisely.imaging.color import generate_rgb
20
- from supervisely.io.fs import get_file_name_with_ext, touch
20
+ from supervisely.io.fs import get_file_name, get_file_name_with_ext, touch
21
+ from supervisely.nn.task_type import TaskType
21
22
  from supervisely.project.project import Dataset, OpenMode, Project
22
23
  from supervisely.project.project_meta import ProjectMeta
23
24
  from supervisely.sly_logger import logger
@@ -27,6 +28,20 @@ YOLO_DETECTION_COORDS_NUM = 4
27
28
  YOLO_SEGM_MIN_COORDS_NUM = 6
28
29
  YOLO_KEYPOINTS_MIN_COORDS_NUM = 6
29
30
 
31
+
32
+ class YOLOTaskType:
33
+ DETECT = "detect"
34
+ SEGMENT = "segment"
35
+ POSE = "pose"
36
+
37
+
38
+ SLY_YOLO_TASK_TYPE_MAP = {
39
+ TaskType.OBJECT_DETECTION: YOLOTaskType.DETECT,
40
+ TaskType.INSTANCE_SEGMENTATION: YOLOTaskType.SEGMENT,
41
+ TaskType.POSE_ESTIMATION: YOLOTaskType.POSE,
42
+ }
43
+
44
+
30
45
  coco_classes = [
31
46
  "person",
32
47
  "bicycle",
@@ -398,22 +413,22 @@ def keypoints_to_yolo_line(
398
413
 
399
414
  def convert_label_geometry_if_needed(
400
415
  label: Label,
401
- task_type: Literal["detection", "segmentation", "pose"],
416
+ task_type: Literal["detect", "segment", "pose"],
402
417
  verbose: bool = False,
403
418
  ) -> List[Label]:
404
- if task_type == "detection":
419
+ if task_type == YOLOTaskType.DETECT:
405
420
  available_geometry_type = Rectangle
406
421
  convertable_geometry_types = [Polygon, GraphNodes, Bitmap, Polyline, AlphaMask, AnyGeometry]
407
- elif task_type == "segmentation":
422
+ elif task_type == YOLOTaskType.SEGMENT:
408
423
  available_geometry_type = Polygon
409
424
  convertable_geometry_types = [Bitmap, AlphaMask, AnyGeometry]
410
- elif task_type == "pose":
425
+ elif task_type == YOLOTaskType.POSE:
411
426
  available_geometry_type = GraphNodes
412
427
  convertable_geometry_types = []
413
428
  else:
414
429
  raise ValueError(
415
430
  f"Unsupported task type: {task_type}. "
416
- "Supported types: 'detection', 'segmentation', 'pose'"
431
+ f"Supported types: '{YOLOTaskType.DETECT}', '{YOLOTaskType.SEGMENT}', '{YOLOTaskType.POSE}'"
417
432
  )
418
433
 
419
434
  if label.obj_class.geometry_type == available_geometry_type:
@@ -438,7 +453,7 @@ def label_to_yolo_lines(
438
453
  img_height: int,
439
454
  img_width: int,
440
455
  class_names: List[str],
441
- task_type: Literal["detection", "segmentation", "pose"],
456
+ task_type: Literal["detect", "segment", "pose"],
442
457
  ) -> List[str]:
443
458
  """
444
459
  Convert the Supervisely Label to a line in the YOLO format.
@@ -449,21 +464,21 @@ def label_to_yolo_lines(
449
464
 
450
465
  lines = []
451
466
  for label in labels:
452
- if task_type == "detection":
467
+ if task_type == YOLOTaskType.DETECT:
453
468
  yolo_line = rectangle_to_yolo_line(
454
469
  class_idx=class_idx,
455
470
  geometry=label.geometry,
456
471
  img_height=img_height,
457
472
  img_width=img_width,
458
473
  )
459
- elif task_type == "segmentation":
474
+ elif task_type == YOLOTaskType.SEGMENT:
460
475
  yolo_line = polygon_to_yolo_line(
461
476
  class_idx=class_idx,
462
477
  geometry=label.geometry,
463
478
  img_height=img_height,
464
479
  img_width=img_width,
465
480
  )
466
- elif task_type == "pose":
481
+ elif task_type == YOLOTaskType.POSE:
467
482
  nodes_field = label.obj_class.geometry_type.items_json_field
468
483
  max_kpts_count = len(label.obj_class.geometry_config[nodes_field])
469
484
  yolo_line = keypoints_to_yolo_line(
@@ -474,7 +489,10 @@ def label_to_yolo_lines(
474
489
  max_kpts_count=max_kpts_count,
475
490
  )
476
491
  else:
477
- raise ValueError(f"Unsupported task type: {task_type}")
492
+ raise ValueError(
493
+ f"Unsupported task type: {task_type}. "
494
+ f"Supported types: '{YOLOTaskType.DETECT}', '{YOLOTaskType.SEGMENT}', '{YOLOTaskType.POSE}'"
495
+ )
478
496
 
479
497
  if yolo_line is not None:
480
498
  lines.append(yolo_line)
@@ -485,12 +503,11 @@ def label_to_yolo_lines(
485
503
  def sly_ann_to_yolo(
486
504
  ann: Annotation,
487
505
  class_names: List[str],
488
- task_type: Literal["detection", "segmentation", "pose"] = "detection",
506
+ task_type: Literal["detect", "segment", "pose"] = "detect",
489
507
  ) -> List[str]:
490
508
  """
491
509
  Convert the Supervisely annotation to the YOLO format.
492
510
  """
493
-
494
511
  h, w = ann.img_size
495
512
  yolo_lines = []
496
513
  for label in ann.labels:
@@ -509,11 +526,12 @@ def sly_ds_to_yolo(
509
526
  dataset: Dataset,
510
527
  meta: ProjectMeta,
511
528
  dest_dir: Optional[str] = None,
512
- task_type: Literal["detection", "segmentation", "pose"] = "detection",
529
+ task_type: Literal["detect", "segment", "pose"] = "detect",
513
530
  log_progress: bool = False,
514
531
  progress_cb: Optional[Union[tqdm, Callable]] = None,
532
+ is_val: Optional[bool] = None,
515
533
  ) -> str:
516
-
534
+ task_type = validate_task_type(task_type)
517
535
  if progress_cb is not None:
518
536
  log_progress = False
519
537
 
@@ -543,15 +561,19 @@ def sly_ds_to_yolo(
543
561
  ann_path = dataset.get_ann_path(name)
544
562
  ann = Annotation.load_json_file(ann_path, meta)
545
563
 
546
- images_dir = val_images_dir if ann.img_tags.get("val") else train_images_dir
547
- labels_dir = val_labels_dir if ann.img_tags.get("val") else train_labels_dir
564
+ if is_val is not None:
565
+ images_dir = val_images_dir if is_val else train_images_dir
566
+ labels_dir = val_labels_dir if is_val else train_labels_dir
567
+ else:
568
+ images_dir = val_images_dir if ann.img_tags.get("val") else train_images_dir
569
+ labels_dir = val_labels_dir if ann.img_tags.get("val") else train_labels_dir
548
570
 
549
571
  img_path = Path(dataset.get_img_path(name))
550
572
  img_name = f"{dataset.short_name}_{get_file_name_with_ext(img_path)}"
551
573
  img_name = generate_free_name(used_names, img_name, with_ext=True, extend_used_names=True)
552
574
  shutil.copy2(img_path, images_dir / img_name)
553
575
 
554
- label_path = str(labels_dir / f"{img_name}.txt")
576
+ label_path = str(labels_dir / f"{get_file_name(img_name)}.txt")
555
577
  yolo_lines = ann.to_yolo(class_names, task_type)
556
578
  if len(yolo_lines) > 0:
557
579
  with open(label_path, "w") as f:
@@ -565,7 +587,8 @@ def sly_ds_to_yolo(
565
587
  # * save data config file if it does not exist
566
588
  config_path = dest_dir / "data_config.yaml"
567
589
  if not config_path.exists():
568
- save_yolo_config(meta, dest_dir, with_keypoint=task_type == "pose")
590
+ with_keypoint = task_type is YOLOTaskType.POSE
591
+ save_yolo_config(meta, dest_dir, with_keypoint=with_keypoint)
569
592
 
570
593
  return str(dest_dir)
571
594
 
@@ -578,6 +601,8 @@ def save_yolo_config(meta: ProjectMeta, dest_dir: str, with_keypoint: bool = Fal
578
601
  data_yaml = {
579
602
  "train": f"../{str(dest_dir.name)}/images/train",
580
603
  "val": f"../{str(dest_dir.name)}/images/val",
604
+ "train_labels": f"../{str(dest_dir.name)}/labels/train",
605
+ "val_labels": f"../{str(dest_dir.name)}/labels/val",
581
606
  "nc": len(class_names),
582
607
  "names": class_names,
583
608
  "colors": class_colors,
@@ -590,6 +615,7 @@ def save_yolo_config(meta: ProjectMeta, dest_dir: str, with_keypoint: bool = Fal
590
615
  field_name = obj_class.geometry_type.items_json_field
591
616
  max_kpts_count = max(max_kpts_count, len(obj_class.geometry_config[field_name]))
592
617
  data_yaml["kpt_shape"] = [max_kpts_count, 3]
618
+ data_yaml["flip_idx"] = [i for i in range(max_kpts_count)]
593
619
  with open(save_path, "w") as f:
594
620
  yaml.dump(data_yaml, f, default_flow_style=None)
595
621
 
@@ -599,21 +625,31 @@ def save_yolo_config(meta: ProjectMeta, dest_dir: str, with_keypoint: bool = Fal
599
625
  def sly_project_to_yolo(
600
626
  project: Union[Project, str],
601
627
  dest_dir: Optional[str] = None,
602
- task_type: Literal["detection", "segmentation", "pose"] = "detection",
628
+ task_type: Literal["detect", "segment", "pose"] = "detect",
603
629
  log_progress: bool = False,
604
630
  progress_cb: Optional[Callable] = None,
631
+ val_datasets: Optional[List[str]] = None,
605
632
  ):
606
633
  """
607
634
  Convert Supervisely project to YOLO format.
608
635
 
636
+ :param project: Supervisely project or path to the directory with the project.
637
+ :type project: :class:`supervisely.project.project.Project` or :class:`str`
609
638
  :param dest_dir: Destination directory.
610
639
  :type dest_dir: :class:`str`, optional
640
+ :param task_type: Task type.
641
+ :type task_type: :class:`str`, optional
611
642
  :param log_progress: Show uploading progress bar.
612
643
  :type log_progress: :class:`bool`
613
644
  :param progress_cb: Function for tracking conversion progress (for all items in the project).
614
645
  :type progress_cb: callable, optional
615
- :return: None
616
- :rtype: NoneType
646
+ :param val_datasets: List of dataset names for validation.
647
+ Full dataset names are required (e.g., 'ds0/nested_ds1/ds3').
648
+ If specified, datasets from the list will be marked as val, others as train.
649
+ If not specified, the function will determine the validation datasets automatically.
650
+ :type val_datasets: :class:`list`, optional
651
+ :return: Path to the destination directory.
652
+ :rtype: :class:`str`
617
653
 
618
654
  :Usage example:
619
655
 
@@ -627,6 +663,7 @@ def sly_project_to_yolo(
627
663
  # Convert Project to YOLO format
628
664
  sly.Project(project_directory).to_yolo(log_progress=True)
629
665
  """
666
+ task_type = validate_task_type(task_type)
630
667
  if isinstance(project, str):
631
668
  project = Project(project, mode=OpenMode.READ)
632
669
 
@@ -644,9 +681,15 @@ def sly_project_to_yolo(
644
681
  desc="Converting Supervisely project to YOLO format", total=project.total_items
645
682
  ).update
646
683
 
647
- save_yolo_config(project.meta, dest_dir, with_keypoint=task_type == "pose")
684
+ with_keypoint = task_type is YOLOTaskType.POSE
685
+ save_yolo_config(project.meta, dest_dir, with_keypoint=with_keypoint)
648
686
 
649
687
  for dataset in project.datasets:
688
+ if val_datasets is not None:
689
+ is_val = dataset.name in val_datasets
690
+ else:
691
+ is_val = None
692
+
650
693
  dataset: Dataset
651
694
  dataset.to_yolo(
652
695
  meta=project.meta,
@@ -654,18 +697,23 @@ def sly_project_to_yolo(
654
697
  task_type=task_type,
655
698
  log_progress=log_progress,
656
699
  progress_cb=progress_cb,
700
+ is_val=is_val,
657
701
  )
658
702
  logger.info(f"Dataset '{dataset.short_name}' has been converted to YOLO format.")
659
703
  logger.info(f"Project '{project.name}' has been converted to YOLO format.")
660
704
 
705
+ return str(dest_dir)
706
+
661
707
 
662
708
  def to_yolo(
663
709
  input_data: Union[Project, Dataset, str],
664
710
  dest_dir: Optional[str] = None,
665
- task_type: Literal["detection", "segmentation", "pose"] = "detection",
711
+ task_type: Literal["detect", "segment", "pose"] = "detect",
666
712
  meta: Optional[ProjectMeta] = None,
667
713
  log_progress: bool = True,
668
714
  progress_cb: Optional[Callable] = None,
715
+ val_datasets: Optional[List[str]] = None,
716
+ is_val: Optional[bool] = None,
669
717
  ) -> Union[None, str]:
670
718
  """
671
719
  Universal function to convert Supervisely project or dataset to YOLO format.
@@ -691,6 +739,13 @@ def to_yolo(
691
739
  :type log_progress: :class:`bool`
692
740
  :param progress_cb: Function for tracking conversion progress (for all items in the project).
693
741
  :type progress_cb: callable, optional
742
+ :param val_datasets: List of dataset names for validation.
743
+ Full dataset names are required (e.g., 'ds0/nested_ds1/ds3').
744
+ If specified, datasets from the list will be marked as val, others as train.
745
+ If not specified, the function will determine the validation datasets automatically.
746
+ :type val_datasets: :class:`list`, optional
747
+ :param is_val: Whether the dataset is for validation.
748
+ :type is_val: :class:`bool`, optional
694
749
  :return: None, list of YOLO lines, or path to the destination directory.
695
750
  :rtype: NoneType, list, str
696
751
 
@@ -711,7 +766,7 @@ def to_yolo(
711
766
 
712
767
  # Convert Dataset to YOLO format
713
768
  dataset: sly.Dataset = project_fs.datasets.get("dataset_name")
714
- sly.convert.to_yolo(dataset, dest_dir="./yolo", meta=project_fs.meta)
769
+ sly.convert.to_yolo(dataset, dest_dir="./yolo", meta=project_fs.meta, is_val=True)
715
770
  """
716
771
  if isinstance(input_data, str):
717
772
  try:
@@ -728,6 +783,7 @@ def to_yolo(
728
783
  task_type=task_type,
729
784
  log_progress=log_progress,
730
785
  progress_cb=progress_cb,
786
+ val_datasets=val_datasets,
731
787
  )
732
788
  elif isinstance(input_data, Dataset):
733
789
  return sly_ds_to_yolo(
@@ -737,6 +793,20 @@ def to_yolo(
737
793
  task_type=task_type,
738
794
  log_progress=log_progress,
739
795
  progress_cb=progress_cb,
796
+ is_val=is_val,
740
797
  )
741
798
  else:
742
799
  raise ValueError("Unsupported input type. Only Project or Dataset are supported.")
800
+
801
+
802
+ def validate_task_type(task_type: Literal["detect", "segment", "pose"]) -> str:
803
+ if task_type not in [YOLOTaskType.DETECT, YOLOTaskType.SEGMENT, YOLOTaskType.POSE]:
804
+ task_type = SLY_YOLO_TASK_TYPE_MAP.get(task_type)
805
+ if task_type is None:
806
+ raise ValueError(
807
+ f"Unsupported task type: {task_type}. "
808
+ f"Supported types: '{YOLOTaskType.DETECT}', '{SLY_YOLO_TASK_TYPE_MAP[TaskType.OBJECT_DETECTION]}', "
809
+ f"'{YOLOTaskType.SEGMENT}', '{SLY_YOLO_TASK_TYPE_MAP[TaskType.INSTANCE_SEGMENTATION]}', "
810
+ f"'{YOLOTaskType.POSE}', '{SLY_YOLO_TASK_TYPE_MAP[TaskType.POSE_ESTIMATION]}'"
811
+ )
812
+ return task_type
@@ -8,7 +8,7 @@ from supervisely.convert.volume.nii import nii_volume_helper as helper
8
8
  from supervisely.convert.volume.nii.nii_volume_converter import NiiConverter
9
9
  from supervisely.convert.volume.volume_converter import VolumeConverter
10
10
  from supervisely.geometry.mask_3d import Mask3D
11
- from supervisely.io.fs import get_file_name
11
+ from supervisely.io.fs import get_file_ext, get_file_name
12
12
  from supervisely.volume.volume import is_nifti_file
13
13
  from supervisely.volume_annotation.volume_annotation import VolumeAnnotation
14
14
  from supervisely.volume_annotation.volume_object import VolumeObject
@@ -34,14 +34,46 @@ class NiiPlaneStructuredConverter(NiiConverter, VolumeConverter):
34
34
  ├── 🩻 sag_inference_1.nii class 1
35
35
  ├── 🩻 sag_inference_2.nii class 2
36
36
  └── 🩻 sag_inference_3.nii class 3
37
+
38
+ Additionally, if a TXT file with class color map is present, it will be used to
39
+ create the classes with names and colors corresponding to the pixel values in the NIfTI files.
40
+ The TXT file should be structured as follows:
41
+
42
+ ```txt
43
+ 1 Femur 255 0 0
44
+ 2 Femoral cartilage 0 255 0
45
+ 3 Tibia 0 0 255
46
+ 4 Tibia cartilage 255 255 0
47
+ 5 Patella 0 255 255
48
+ 6 Patellar cartilage 255 0 255
49
+ 7 Miniscus 175 175 175
50
+ ```
51
+ where 1, 2, ... are the pixel values in the NIfTI files
52
+ Femur, Femoral cartilage, ... are the names of the classes
53
+ 255, 0, 0, ... are the RGB colors of the classes
54
+ The class name will be used to create the corresponding ObjClass in Supervisely.
37
55
  """
38
56
 
57
+ class Item(VolumeConverter.BaseItem):
58
+ def __init__(self, *args, **kwargs):
59
+ super().__init__(*args, **kwargs)
60
+ self._is_semantic = False
61
+
62
+ @property
63
+ def is_semantic(self) -> bool:
64
+ return self._is_semantic
65
+
66
+ @is_semantic.setter
67
+ def is_semantic(self, value: bool):
68
+ self._is_semantic = value
69
+
39
70
  def validate_format(self) -> bool:
40
71
  # create Items
41
72
  converted_dir_name = "converted"
42
73
 
43
74
  volumes_dict = defaultdict(list)
44
75
  ann_dict = defaultdict(list)
76
+ cls_color_map = None
45
77
 
46
78
  for root, _, files in os.walk(self._input_data):
47
79
  if converted_dir_name in root:
@@ -56,17 +88,24 @@ class NiiPlaneStructuredConverter(NiiConverter, VolumeConverter):
56
88
  if prefix not in helper.PlanePrefix.values():
57
89
  continue
58
90
  name = full_name.split("_")[1]
59
- idx = 1 if len(name.split("_")) < 3 else int(name.split("_")[2])
60
91
  if name in helper.LABEL_NAME or name[:-1] in helper.LABEL_NAME:
61
92
  ann_dict[prefix].append(path)
62
93
  else:
63
94
  volumes_dict[prefix].append(path)
95
+ ext = get_file_ext(path)
96
+ if ext == ".txt":
97
+ cls_color_map = helper.read_cls_color_map(path)
98
+ if cls_color_map is None:
99
+ logger.warning(f"Failed to read class color map from {path}.")
64
100
 
65
101
  self._items = []
66
102
  for prefix, paths in volumes_dict.items():
67
103
  if len(paths) == 1:
68
104
  item = self.Item(item_path=paths[0])
69
- item.ann_data = ann_dict.get(prefix)
105
+ item.ann_data = ann_dict.get(prefix, [])
106
+ item.is_semantic = len(item.ann_data) == 1
107
+ if cls_color_map is not None:
108
+ item.custom_data["cls_color_map"] = cls_color_map
70
109
  self._items.append(item)
71
110
  elif len(paths) > 1:
72
111
  logger.info(
@@ -79,6 +118,9 @@ class NiiPlaneStructuredConverter(NiiConverter, VolumeConverter):
79
118
  if Path(ann_path).parent == Path(path).parent:
80
119
  possible_ann_paths.append(ann_path)
81
120
  item.ann_data = possible_ann_paths
121
+ item.is_semantic = len(possible_ann_paths) == 1
122
+ if cls_color_map is not None:
123
+ item.custom_data["cls_color_map"] = cls_color_map
82
124
  self._items.append(item)
83
125
  self._meta = ProjectMeta()
84
126
  return self.items_count > 0
@@ -96,12 +138,18 @@ class NiiPlaneStructuredConverter(NiiConverter, VolumeConverter):
96
138
  objs = []
97
139
  spatial_figures = []
98
140
  for idx, ann_path in enumerate(item.ann_data, start=1):
99
- for mask, _ in helper.get_annotation_from_nii(ann_path):
100
- class_name = f"Segment_{idx}"
141
+ for mask, pixel_id in helper.get_annotation_from_nii(ann_path):
142
+ class_id = pixel_id if item.is_semantic else idx
143
+ class_name = f"Segment_{class_id}"
144
+ color = None
145
+ if item.custom_data.get("cls_color_map") is not None:
146
+ class_info = item.custom_data["cls_color_map"].get(class_id)
147
+ if class_info is not None:
148
+ class_name, color = class_info
101
149
  class_name = renamed_classes.get(class_name, class_name)
102
150
  obj_class = meta.get_obj_class(class_name)
103
151
  if obj_class is None:
104
- obj_class = ObjClass(class_name, Mask3D)
152
+ obj_class = ObjClass(class_name, Mask3D, color)
105
153
  meta = meta.add_obj_class(obj_class)
106
154
  self._meta_changed = True
107
155
  self._meta = meta
@@ -169,11 +169,11 @@ class NiiConverter(VolumeConverter):
169
169
  item_paths = []
170
170
 
171
171
  for item in batch:
172
- if self._upload_as_links:
173
- remote_path = self.remote_files_map.get(item.path)
174
- if remote_path is not None:
175
- item.custom_data = {"remote_path": remote_path}
176
- # nii_path = item.path
172
+ # if self._upload_as_links:
173
+ # remote_path = self.remote_files_map.get(item.path)
174
+ # if remote_path is not None:
175
+ # item.custom_data = {"remote_path": remote_path}
176
+
177
177
  item.path = helper.nifti_to_nrrd(item.path, converted_dir)
178
178
  ext = get_file_ext(item.path)
179
179
  if ext.lower() != ext:
@@ -195,8 +195,8 @@ class NiiConverter(VolumeConverter):
195
195
  leave=True if progress_cb is None else False,
196
196
  position=1,
197
197
  )
198
- if item.custom_data is not None:
199
- volume_meta.update(item.custom_data)
198
+ # if item.custom_data is not None:
199
+ # volume_meta.update(item.custom_data)
200
200
  api.volume.upload_np(dataset_id, item.name, volume_np, volume_meta, progress_nrrd)
201
201
  info = api.volume.get_info_by_name(dataset_id, item.name)
202
202
  item.volume_meta = info.meta
@@ -7,6 +7,7 @@ import numpy as np
7
7
  from supervisely.collection.str_enum import StrEnum
8
8
  from supervisely.geometry.mask_3d import Mask3D
9
9
  from supervisely.io.fs import ensure_base_path, get_file_ext, get_file_name
10
+ from supervisely.sly_logger import logger
10
11
  from supervisely.volume.volume import convert_3d_nifti_to_nrrd
11
12
 
12
13
  VOLUME_NAME = "anatomic"
@@ -21,6 +22,54 @@ class PlanePrefix(str, StrEnum):
21
22
  AXIAL = "axl"
22
23
 
23
24
 
25
+ def read_cls_color_map(path: str) -> dict:
26
+ """Read class color map from TXT file.
27
+
28
+ ```txt
29
+ 1 Femur 255 0 0
30
+ 2 Femoral cartilage 0 255 0
31
+ 3 Tibia 0 0 255
32
+ 4 Tibia cartilage 255 255 0
33
+ 5 Patella 0 255 255
34
+ 6 Patellar cartilage 255 0 255
35
+ 7 Miniscus 175 175 175
36
+ ```
37
+ """
38
+
39
+ cls_color_map = {}
40
+ if not os.path.exists(path):
41
+ return None
42
+ try:
43
+ with open(path, "r") as file:
44
+ for line in file:
45
+ parts = line.strip().split()
46
+ color = None
47
+ try:
48
+ color = list(map(int, parts[-3:]))
49
+ except:
50
+ pass
51
+ cls_id = int(parts[0])
52
+ if not color:
53
+ cls_name = " ".join(parts[1:])
54
+ else:
55
+ cls_name = " ".join(parts[1:-3])
56
+ if cls_id in cls_color_map:
57
+ logger.warning(f"Duplicate class ID {cls_id} found in color map.")
58
+ if cls_name in cls_color_map:
59
+ logger.warning(f"Duplicate class name {cls_name} found in color map.")
60
+ if len(color) != 3:
61
+ logger.warning(f"Invalid color format for class {cls_name}. Expected 3 values.")
62
+ if any(c < 0 or c > 255 for c in color):
63
+ logger.warning(
64
+ f"Invalid color value for class {cls_name}. Expected values between 0 and 255."
65
+ )
66
+ cls_color_map[cls_id] = (cls_name, color)
67
+ except Exception as e:
68
+ logger.warning(f"Failed to read class color map from {path}: {e}")
69
+ return None
70
+ return cls_color_map
71
+
72
+
24
73
  def nifti_to_nrrd(nii_file_path: str, converted_dir: str) -> str:
25
74
  """Convert NIfTI 3D volume file to NRRD 3D volume file."""
26
75
 
@@ -23,7 +23,7 @@ from supervisely.app.widgets.pretrained_models_selector.pretrained_models_select
23
23
  )
24
24
  from supervisely.nn.experiments import get_experiment_infos
25
25
  from supervisely.nn.inference.gui.serving_gui import ServingGUI
26
- from supervisely.nn.utils import ModelSource, RuntimeType
26
+ from supervisely.nn.utils import ModelSource, RuntimeType, _get_model_name
27
27
 
28
28
 
29
29
  class ServingGUITemplate(ServingGUI):
@@ -149,8 +149,7 @@ class ServingGUITemplate(ServingGUI):
149
149
  @property
150
150
  def model_name(self) -> Optional[str]:
151
151
  if self.model_source == ModelSource.PRETRAINED:
152
- model_meta = self.model_info.get("meta", {})
153
- return model_meta.get("model_name")
152
+ return _get_model_name(self.model_info)
154
153
  else:
155
154
  return self.model_info.get("model_name")
156
155