supervisely 6.73.359__py3-none-any.whl → 6.73.361__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.
@@ -31,6 +31,8 @@ from supervisely import (
31
31
  Project,
32
32
  ProjectInfo,
33
33
  ProjectMeta,
34
+ ProjectType,
35
+ VideoProject,
34
36
  WorkflowMeta,
35
37
  WorkflowSettings,
36
38
  batched,
@@ -152,8 +154,8 @@ class TrainApp:
152
154
  self.sly_project = None
153
155
  # -------------------------- #
154
156
 
155
- # Train/Val splits
156
- self.train_split, self.val_split = None, None
157
+ self._train_split = None
158
+ self._val_split = None
157
159
  # -------------------------- #
158
160
 
159
161
  # Input
@@ -376,6 +378,8 @@ class TrainApp:
376
378
  :return: List of selected classes names.
377
379
  :rtype: List[str]
378
380
  """
381
+ if not self._has_classes_selector:
382
+ return []
379
383
  selected_classes = set(self.gui.classes_selector.get_selected_classes())
380
384
  # remap classes with project_meta order
381
385
  return [x for x in self.project_meta.obj_classes.keys() if x in selected_classes]
@@ -388,8 +392,29 @@ class TrainApp:
388
392
  :return: Number of selected classes.
389
393
  :rtype: int
390
394
  """
395
+ if not self._has_classes_selector:
396
+ return 0
391
397
  return len(self.gui.classes_selector.get_selected_classes())
392
398
 
399
+ @property
400
+ def tags(self) -> List[str]:
401
+ """
402
+ Returns the selected tags for training.
403
+ """
404
+ if not self._has_tags_selector:
405
+ return []
406
+ selected_tags = set(self.gui.tags_selector.get_selected_tags())
407
+ return [x for x in self.project_meta.tag_metas.keys() if x in selected_tags]
408
+
409
+ @property
410
+ def num_tags(self) -> int:
411
+ """
412
+ Returns the number of selected tags for training.
413
+ """
414
+ if not self._has_tags_selector:
415
+ return 0
416
+ return len(self.gui.tags_selector.get_selected_tags())
417
+
393
418
  # Hyperparameters
394
419
  @property
395
420
  def hyperparameters(self) -> Dict[str, Any]:
@@ -448,6 +473,24 @@ class TrainApp:
448
473
  # Output
449
474
  # ----------------------------------------- #
450
475
 
476
+ # Helper properties
477
+ @property
478
+ def _has_splits_selector(self) -> bool:
479
+ """Return True if Train/Val splits selector is enabled in GUI."""
480
+ return self.gui.train_val_splits_selector is not None
481
+
482
+ @property
483
+ def _has_classes_selector(self) -> bool:
484
+ """Return True if Classes selector is enabled in GUI."""
485
+ return self.gui.classes_selector is not None
486
+
487
+ @property
488
+ def _has_tags_selector(self) -> bool:
489
+ """Return True if Tags selector is enabled in GUI."""
490
+ return self.gui.tags_selector is not None
491
+
492
+ # ----------------------------------------- #
493
+
451
494
  # Wrappers
452
495
  @property
453
496
  def start(self):
@@ -546,6 +589,7 @@ class TrainApp:
546
589
  try:
547
590
  # Convert GT project
548
591
  gt_project_id, bm_splits_data = None, train_splits_data
592
+ # @TODO: check with anyshape classes
549
593
  if self._app_options.get("auto_convert_classes", True):
550
594
  if self.gui.need_convert_shapes_for_bm:
551
595
  self._set_text_status("convert_gt_project")
@@ -644,9 +688,12 @@ class TrainApp:
644
688
  :return: Application state.
645
689
  :rtype: dict
646
690
  """
691
+ # Prepare optional sections depending on what selectors are enabled in GUI
647
692
  train_val_splits = self._get_train_val_splits_for_app_state()
648
- model = self._get_model_config_for_app_state(experiment_info)
693
+ classes = self.classes
694
+ tags = self.tags
649
695
 
696
+ model = self._get_model_config_for_app_state(experiment_info)
650
697
  options = {
651
698
  "model_benchmark": {
652
699
  "enable": self.gui.hyperparameters_selector.get_model_benchmark_checkbox_value(),
@@ -656,12 +703,14 @@ class TrainApp:
656
703
  }
657
704
 
658
705
  app_state = {
659
- "train_val_split": train_val_splits,
660
- "classes": self.classes,
661
706
  "model": model,
662
707
  "hyperparameters": self.hyperparameters_yaml,
663
708
  "options": options,
664
709
  }
710
+
711
+ app_state["train_val_split"] = train_val_splits
712
+ app_state["classes"] = classes
713
+ app_state["tags"] = tags
665
714
  return app_state
666
715
 
667
716
  def load_app_state(self, app_state: dict) -> None:
@@ -675,12 +724,13 @@ class TrainApp:
675
724
 
676
725
  app_state = {
677
726
  "input": {"project_id": 55555},
678
- "train_val_splits": {
727
+ "train_val_split": {
679
728
  "method": "random",
680
729
  "split": "train",
681
730
  "percent": 90
682
731
  },
683
732
  "classes": ["apple"],
733
+ "tags": ["green", "red"],
684
734
  "model": {
685
735
  "source": "Pretrained models",
686
736
  "model_name": "rtdetr_r50vd_coco_objects365"
@@ -786,25 +836,44 @@ class TrainApp:
786
836
 
787
837
  # Preprocess
788
838
  # Download Project
839
+ def _read_project(self, remove_unselected_classes: bool = True) -> None:
840
+ """
841
+ Reads the project data from Supervisely.
842
+
843
+ :param remove_unselected_classes: Whether to remove unselected classes from the project.
844
+ :type remove_unselected_classes: bool
845
+ """
846
+ if self.project_info.type == ProjectType.IMAGES.value:
847
+ self.sly_project = Project(self.project_dir, OpenMode.READ)
848
+ if remove_unselected_classes:
849
+ self.sly_project.remove_classes_except(self.project_dir, self.classes, True)
850
+ elif self.project_info.type == ProjectType.VIDEOS.value:
851
+ self.sly_project = VideoProject(self.project_dir, OpenMode.READ)
852
+ else:
853
+ raise ValueError(
854
+ f"Unsupported project type: {self.project_info.type}. Only images and videos are supported."
855
+ )
856
+
789
857
  def _download_project(self) -> None:
790
858
  """
791
859
  Downloads the project data from Supervisely.
792
860
  If the cache is enabled, it will attempt to retrieve the project from the cache.
793
861
  """
794
862
  dataset_infos = [dataset for _, dataset in self._api.dataset.tree(self.project_id)]
795
-
796
- if self.gui.train_val_splits_selector.get_split_method() == "Based on datasets":
797
- selected_ds_ids = (
798
- self.gui.train_val_splits_selector.get_train_dataset_ids()
799
- + self.gui.train_val_splits_selector.get_val_dataset_ids()
800
- )
801
- dataset_infos = [ds_info for ds_info in dataset_infos if ds_info.id in selected_ds_ids]
863
+ if self.gui.train_val_splits_selector is not None:
864
+ if self.gui.train_val_splits_selector.get_split_method() == "Based on datasets":
865
+ selected_ds_ids = (
866
+ self.gui.train_val_splits_selector.get_train_dataset_ids()
867
+ + self.gui.train_val_splits_selector.get_val_dataset_ids()
868
+ )
869
+ dataset_infos = [
870
+ ds_info for ds_info in dataset_infos if ds_info.id in selected_ds_ids
871
+ ]
802
872
 
803
873
  total_images = sum(ds_info.images_count for ds_info in dataset_infos)
804
- if not self.gui.input_selector.get_cache_value() or is_development():
874
+ if not self.gui.input_selector.get_cache_value():
805
875
  self._download_no_cache(dataset_infos, total_images)
806
- self.sly_project = Project(self.project_dir, OpenMode.READ)
807
- self.sly_project.remove_classes_except(self.project_dir, self.classes, True)
876
+ self._read_project()
808
877
  return
809
878
 
810
879
  try:
@@ -818,8 +887,7 @@ class TrainApp:
818
887
  sly_fs.clean_dir(self.project_dir)
819
888
  self._download_no_cache(dataset_infos, total_images)
820
889
  finally:
821
- self.sly_project = Project(self.project_dir, OpenMode.READ)
822
- self.sly_project.remove_classes_except(self.project_dir, self.classes, True)
890
+ self._read_project()
823
891
  logger.info(f"Project downloaded successfully to: '{self.project_dir}'")
824
892
 
825
893
  def _download_no_cache(self, dataset_infos: List[DatasetInfo], total_images: int) -> None:
@@ -919,6 +987,15 @@ class TrainApp:
919
987
  All images and annotations will be renamed and moved to the appropriate directories.
920
988
  Assigns self.sly_project to the new project, which contains only 2 datasets: train and val.
921
989
  """
990
+ if not self._has_splits_selector:
991
+ # Splits disabled in options, init empty splits
992
+ self.train_dataset_dir = None
993
+ self.val_dataset_dir = None
994
+ self._train_val_split_file = None
995
+ self._train_split = []
996
+ self._val_split = []
997
+ return
998
+
922
999
  # Load splits
923
1000
  self.gui.train_val_splits_selector.set_sly_project(self.sly_project)
924
1001
  self._train_split, self._val_split = (
@@ -1005,7 +1082,7 @@ class TrainApp:
1005
1082
 
1006
1083
  # Clean up temporary directory
1007
1084
  sly_fs.remove_dir(project_split_path)
1008
- self.sly_project = Project(self.project_dir, OpenMode.READ)
1085
+ self._read_project(False)
1009
1086
 
1010
1087
  # ----------------------------------------- #
1011
1088
 
@@ -1272,6 +1349,9 @@ class TrainApp:
1272
1349
  train_dataset_ids = None
1273
1350
  train_images_ids = None
1274
1351
 
1352
+ if not self._has_splits_selector:
1353
+ return {} # splits disabled in options
1354
+
1275
1355
  split_method = self.gui.train_val_splits_selector.get_split_method()
1276
1356
  train_set, val_set = self._train_split, self._val_split
1277
1357
  if split_method == "Based on datasets":
@@ -1482,6 +1562,9 @@ class TrainApp:
1482
1562
  :param remote_dir: Remote directory path.
1483
1563
  :type remote_dir: str
1484
1564
  """
1565
+ if not self._has_splits_selector:
1566
+ return # splits disabled in options
1567
+
1485
1568
  local_train_val_split_path = join(self.output_dir, self._train_val_split_file)
1486
1569
  remote_train_val_split_path = join(remote_dir, self._train_val_split_file)
1487
1570
 
@@ -1575,9 +1658,6 @@ class TrainApp:
1575
1658
  "export": export_weights,
1576
1659
  "app_state": self._app_state_file,
1577
1660
  "model_meta": self._model_meta_file,
1578
- "train_val_split": self._train_val_split_file,
1579
- "train_size": len(self._train_split),
1580
- "val_size": len(self._val_split),
1581
1661
  "hyperparameters": self._hyperparameters_file,
1582
1662
  "artifacts_dir": remote_dir,
1583
1663
  "datetime": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
@@ -1587,6 +1667,11 @@ class TrainApp:
1587
1667
  "logs": {"type": "tensorboard", "link": f"{remote_dir}logs/"},
1588
1668
  }
1589
1669
 
1670
+ if self._has_splits_selector:
1671
+ experiment_info["train_val_split"] = self._train_val_split_file
1672
+ experiment_info["train_size"] = len(self._train_split)
1673
+ experiment_info["val_size"] = len(self._val_split)
1674
+
1590
1675
  remote_checkpoints_dir = join(remote_dir, self._remote_checkpoints_dir_name)
1591
1676
  checkpoint_files = self._api.file.list(
1592
1677
  self.team_id, remote_checkpoints_dir, return_type="fileinfo"
@@ -1703,6 +1788,9 @@ class TrainApp:
1703
1788
  :return: Train and val splits information based on selected split method.
1704
1789
  :rtype: dict
1705
1790
  """
1791
+ if not self._has_splits_selector:
1792
+ return {} # splits disabled in options
1793
+
1706
1794
  split_method = self.gui.train_val_splits_selector.get_split_method()
1707
1795
  train_val_splits = {"method": split_method.lower()}
1708
1796
  if split_method == "Random":
@@ -2067,20 +2155,25 @@ class TrainApp:
2067
2155
  else:
2068
2156
  raise ValueError(f"Task type: '{task_type}' is not supported for Model Benchmark")
2069
2157
 
2070
- if self.gui.train_val_splits_selector.get_split_method() == "Based on datasets":
2071
- train_info = {
2072
- "app_session_id": self.task_id,
2073
- "train_dataset_ids": train_dataset_ids,
2074
- "train_images_ids": None,
2075
- "images_count": len(self._train_split),
2076
- }
2158
+ if self._has_splits_selector:
2159
+ if self.gui.train_val_splits_selector.get_split_method() == "Based on datasets":
2160
+ train_info = {
2161
+ "app_session_id": self.task_id,
2162
+ "train_dataset_ids": train_dataset_ids,
2163
+ "train_images_ids": None,
2164
+ "images_count": len(self._train_split),
2165
+ }
2166
+ else:
2167
+ train_info = {
2168
+ "app_session_id": self.task_id,
2169
+ "train_dataset_ids": None,
2170
+ "train_images_ids": train_images_ids,
2171
+ "images_count": len(self._train_split),
2172
+ }
2077
2173
  else:
2078
- train_info = {
2079
- "app_session_id": self.task_id,
2080
- "train_dataset_ids": None,
2081
- "train_images_ids": train_images_ids,
2082
- "images_count": len(self._train_split),
2083
- }
2174
+ # @TODO: Add train info for apps without splits
2175
+ train_info = None
2176
+
2084
2177
  bm.train_info = train_info
2085
2178
 
2086
2179
  # 2. Run inference
@@ -2144,14 +2237,17 @@ class TrainApp:
2144
2237
  """
2145
2238
  Adds the input data to the workflow.
2146
2239
  """
2147
- try:
2148
- project_version_id = self._api.project.version.create(
2149
- self.project_info,
2150
- self._app_name,
2151
- f"This backup was created automatically by Supervisely before the {self._app_name} task with ID: {self._api.task_id}",
2152
- )
2153
- except Exception as e:
2154
- logger.warning(f"Failed to create a project version: {repr(e)}")
2240
+ if self.project_info.type == ProjectType.IMAGES.value:
2241
+ try:
2242
+ project_version_id = self._api.project.version.create(
2243
+ self.project_info,
2244
+ self._app_name,
2245
+ f"This backup was created automatically by Supervisely before the {self._app_name} task with ID: {self._api.task_id}",
2246
+ )
2247
+ except Exception as e:
2248
+ logger.warning(f"Failed to create a project version: {repr(e)}")
2249
+ project_version_id = None
2250
+ else:
2155
2251
  project_version_id = None
2156
2252
 
2157
2253
  try:
@@ -409,12 +409,22 @@ def _project_meta_changed(meta1: ProjectMeta, meta2: ProjectMeta) -> bool:
409
409
  return False
410
410
 
411
411
 
412
+ def _get_ds_full_name(
413
+ dataset_info: DatasetInfo, all_ds_infos: List[DatasetInfo], suffix: str = ""
414
+ ) -> str:
415
+ if dataset_info.parent_id is None:
416
+ return dataset_info.name + suffix
417
+ parent = next((ds_info for ds_info in all_ds_infos if ds_info.id == dataset_info.parent_id))
418
+ return _get_ds_full_name(parent, all_ds_infos, "/" + dataset_info.name)
419
+
420
+
412
421
  def _validate_dataset(
413
422
  api: Api,
414
423
  project_id: int,
415
424
  project_type: str,
416
425
  project_meta: ProjectMeta,
417
426
  dataset_info: DatasetInfo,
427
+ all_ds_infos: List[DatasetInfo] = None,
418
428
  ):
419
429
  try:
420
430
  project_class = get_project_class(project_type)
@@ -430,10 +440,12 @@ def _validate_dataset(
430
440
  except:
431
441
  logger.debug("Validating dataset failed. Unable to download items infos.", exc_info=True)
432
442
  return False
443
+ if all_ds_infos is None:
444
+ all_ds_infos = api.dataset.get_list(project_id, recursive=True)
433
445
  project_meta_changed = _project_meta_changed(project_meta, project.meta)
434
446
  for dataset in project.datasets:
435
447
  dataset: Dataset
436
- if dataset.name.endswith(dataset_info.name): # TODO: fix it later
448
+ if dataset.name == _get_ds_full_name(dataset_info, all_ds_infos):
437
449
  diff = set(items_infos_dict.keys()).difference(set(dataset.get_items_names()))
438
450
  if diff:
439
451
  logger.debug(
@@ -481,7 +493,11 @@ def _validate_dataset(
481
493
 
482
494
 
483
495
  def _validate(
484
- api: Api, project_info: ProjectInfo, project_meta: ProjectMeta, dataset_infos: List[DatasetInfo]
496
+ api: Api,
497
+ project_info: ProjectInfo,
498
+ project_meta: ProjectMeta,
499
+ dataset_infos: List[DatasetInfo],
500
+ all_ds_infos: List[DatasetInfo] = None,
485
501
  ):
486
502
  project_id = project_info.id
487
503
  to_download, cached = _split_by_cache(
@@ -498,6 +514,7 @@ def _validate(
498
514
  project_info.type,
499
515
  project_meta,
500
516
  dataset_info,
517
+ all_ds_infos,
501
518
  ):
502
519
  to_download.add(ds_path)
503
520
  cached.remove(ds_path)
@@ -520,7 +537,7 @@ def _add_save_items_infos_to_kwargs(kwargs: dict, project_type: str):
520
537
 
521
538
 
522
539
  def _add_resume_download_to_kwargs(kwargs: dict, project_type: str):
523
- supported_force_projects = (str(ProjectType.IMAGES),)
540
+ supported_force_projects = (str(ProjectType.IMAGES), (str(ProjectType.VIDEOS)))
524
541
  if project_type in supported_force_projects:
525
542
  kwargs["resume_download"] = True
526
543
  return kwargs
@@ -592,13 +609,14 @@ def download_to_cache(
592
609
  project_meta = ProjectMeta.from_json(api.project.get_meta(project_id))
593
610
  if dataset_infos is not None and dataset_ids is not None:
594
611
  raise ValueError("dataset_infos and dataset_ids cannot be specified at the same time")
612
+ all_ds_infos = api.dataset.get_list(project_id, recursive=True)
595
613
  if dataset_infos is None:
596
614
  if dataset_ids is None:
597
- dataset_infos = api.dataset.get_list(project_id, recursive=True)
615
+ dataset_infos = all_ds_infos
598
616
  else:
599
- dataset_infos = [api.dataset.get_info_by_id(dataset_id) for dataset_id in dataset_ids]
617
+ dataset_infos = [ds_info for ds_info in all_ds_infos if ds_info.id in dataset_ids]
600
618
  path_to_info = {_get_dataset_path(api, dataset_infos, info.id): info for info in dataset_infos}
601
- to_download, cached = _validate(api, project_info, project_meta, dataset_infos)
619
+ to_download, cached = _validate(api, project_info, project_meta, dataset_infos, all_ds_infos)
602
620
  if progress_cb is not None:
603
621
  cached_items_n = sum(path_to_info[ds_path].items_count for ds_path in cached)
604
622
  progress_cb(cached_items_n)
@@ -16,7 +16,7 @@ from supervisely.api.dataset_api import DatasetInfo
16
16
  from supervisely.api.module_api import ApiField
17
17
  from supervisely.api.video.video_api import VideoInfo
18
18
  from supervisely.collection.key_indexed_collection import KeyIndexedCollection
19
- from supervisely.io.fs import mkdir, touch, touch_async
19
+ from supervisely.io.fs import clean_dir, mkdir, touch, touch_async
20
20
  from supervisely.io.json import dump_json_file, dump_json_file_async, load_json_file
21
21
  from supervisely.project.project import Dataset, OpenMode, Project
22
22
  from supervisely.project.project import read_single_project as read_project_wrapper
@@ -1056,6 +1056,7 @@ class VideoProject(Project):
1056
1056
  save_video_info: bool = False,
1057
1057
  log_progress: bool = True,
1058
1058
  progress_cb: Optional[Union[tqdm, Callable]] = None,
1059
+ resume_download: Optional[bool] = False,
1059
1060
  ) -> None:
1060
1061
  """
1061
1062
  Download video project from Supervisely to the given directory.
@@ -1109,6 +1110,7 @@ class VideoProject(Project):
1109
1110
  save_video_info=save_video_info,
1110
1111
  log_progress=log_progress,
1111
1112
  progress_cb=progress_cb,
1113
+ resume_download=resume_download,
1112
1114
  )
1113
1115
 
1114
1116
  @staticmethod
@@ -1182,6 +1184,7 @@ class VideoProject(Project):
1182
1184
  log_progress: bool = True,
1183
1185
  progress_cb: Optional[Union[tqdm, Callable]] = None,
1184
1186
  include_custom_data: bool = False,
1187
+ resume_download: Optional[bool] = False,
1185
1188
  **kwargs,
1186
1189
  ) -> None:
1187
1190
  """
@@ -1238,6 +1241,7 @@ class VideoProject(Project):
1238
1241
  log_progress=log_progress,
1239
1242
  progress_cb=progress_cb,
1240
1243
  include_custom_data=include_custom_data,
1244
+ resume_download=resume_download,
1241
1245
  **kwargs,
1242
1246
  )
1243
1247
 
@@ -1252,6 +1256,7 @@ def download_video_project(
1252
1256
  log_progress: bool = True,
1253
1257
  progress_cb: Optional[Union[tqdm, Callable]] = None,
1254
1258
  include_custom_data: Optional[bool] = False,
1259
+ resume_download: Optional[bool] = False,
1255
1260
  ) -> None:
1256
1261
  """
1257
1262
  Download video project to the local directory.
@@ -1312,9 +1317,22 @@ def download_video_project(
1312
1317
  LOG_BATCH_SIZE = 1
1313
1318
 
1314
1319
  key_id_map = KeyIdMap()
1315
- project_fs = VideoProject(dest_dir, OpenMode.CREATE)
1316
- meta = ProjectMeta.from_json(api.project.get_meta(project_id))
1320
+
1321
+ meta = ProjectMeta.from_json(api.project.get_meta(project_id, with_settings=True))
1322
+ if os.path.exists(dest_dir) and resume_download:
1323
+ dump_json_file(meta.to_json(), os.path.join(dest_dir, "meta.json"))
1324
+ try:
1325
+ project_fs = VideoProject(dest_dir, OpenMode.READ)
1326
+ except RuntimeError as e:
1327
+ if "Project is empty" in str(e):
1328
+ clean_dir(dest_dir)
1329
+ project_fs = None
1330
+ else:
1331
+ raise
1332
+ if project_fs is None:
1333
+ project_fs = VideoProject(dest_dir, OpenMode.CREATE)
1317
1334
  project_fs.set_meta(meta)
1335
+
1318
1336
  if progress_cb is not None:
1319
1337
  log_progress = False
1320
1338
 
@@ -1549,6 +1567,7 @@ async def download_video_project_async(
1549
1567
  log_progress: bool = True,
1550
1568
  progress_cb: Optional[Union[tqdm, Callable]] = None,
1551
1569
  include_custom_data: Optional[bool] = False,
1570
+ resume_download: Optional[bool] = False,
1552
1571
  **kwargs,
1553
1572
  ) -> None:
1554
1573
  """
@@ -1603,9 +1622,19 @@ async def download_video_project_async(
1603
1622
 
1604
1623
  key_id_map = KeyIdMap()
1605
1624
 
1606
- project_fs = VideoProject(dest_dir, OpenMode.CREATE)
1607
-
1608
- meta = ProjectMeta.from_json(api.project.get_meta(project_id))
1625
+ meta = ProjectMeta.from_json(api.project.get_meta(project_id, with_settings=True))
1626
+ if os.path.exists(dest_dir) and resume_download:
1627
+ dump_json_file(meta.to_json(), os.path.join(dest_dir, "meta.json"))
1628
+ try:
1629
+ project_fs = VideoProject(dest_dir, OpenMode.READ)
1630
+ except RuntimeError as e:
1631
+ if "Project is empty" in str(e):
1632
+ clean_dir(dest_dir)
1633
+ project_fs = None
1634
+ else:
1635
+ raise
1636
+ if project_fs is None:
1637
+ project_fs = VideoProject(dest_dir, OpenMode.CREATE)
1609
1638
  project_fs.set_meta(meta)
1610
1639
 
1611
1640
  if progress_cb is not None: