supervisely 6.73.410__py3-none-any.whl → 6.73.470__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.

Potentially problematic release.


This version of supervisely might be problematic. Click here for more details.

Files changed (190) hide show
  1. supervisely/__init__.py +136 -1
  2. supervisely/_utils.py +81 -0
  3. supervisely/annotation/json_geometries_map.py +2 -0
  4. supervisely/annotation/label.py +80 -3
  5. supervisely/api/annotation_api.py +9 -9
  6. supervisely/api/api.py +67 -43
  7. supervisely/api/app_api.py +72 -5
  8. supervisely/api/dataset_api.py +108 -33
  9. supervisely/api/entity_annotation/figure_api.py +113 -49
  10. supervisely/api/image_api.py +82 -0
  11. supervisely/api/module_api.py +10 -0
  12. supervisely/api/nn/deploy_api.py +15 -9
  13. supervisely/api/nn/ecosystem_models_api.py +201 -0
  14. supervisely/api/nn/neural_network_api.py +12 -3
  15. supervisely/api/pointcloud/pointcloud_api.py +38 -0
  16. supervisely/api/pointcloud/pointcloud_episode_annotation_api.py +3 -0
  17. supervisely/api/project_api.py +213 -6
  18. supervisely/api/task_api.py +11 -1
  19. supervisely/api/video/video_annotation_api.py +4 -2
  20. supervisely/api/video/video_api.py +79 -1
  21. supervisely/api/video/video_figure_api.py +24 -11
  22. supervisely/api/volume/volume_api.py +38 -0
  23. supervisely/app/__init__.py +1 -1
  24. supervisely/app/content.py +14 -6
  25. supervisely/app/fastapi/__init__.py +1 -0
  26. supervisely/app/fastapi/custom_static_files.py +1 -1
  27. supervisely/app/fastapi/multi_user.py +88 -0
  28. supervisely/app/fastapi/subapp.py +175 -42
  29. supervisely/app/fastapi/templating.py +1 -1
  30. supervisely/app/fastapi/websocket.py +77 -9
  31. supervisely/app/singleton.py +21 -0
  32. supervisely/app/v1/app_service.py +18 -2
  33. supervisely/app/v1/constants.py +7 -1
  34. supervisely/app/widgets/__init__.py +11 -1
  35. supervisely/app/widgets/agent_selector/template.html +1 -0
  36. supervisely/app/widgets/card/card.py +20 -0
  37. supervisely/app/widgets/dataset_thumbnail/dataset_thumbnail.py +11 -2
  38. supervisely/app/widgets/dataset_thumbnail/template.html +3 -1
  39. supervisely/app/widgets/deploy_model/deploy_model.py +750 -0
  40. supervisely/app/widgets/dialog/dialog.py +12 -0
  41. supervisely/app/widgets/dialog/template.html +2 -1
  42. supervisely/app/widgets/dropdown_checkbox_selector/__init__.py +0 -0
  43. supervisely/app/widgets/dropdown_checkbox_selector/dropdown_checkbox_selector.py +87 -0
  44. supervisely/app/widgets/dropdown_checkbox_selector/template.html +12 -0
  45. supervisely/app/widgets/ecosystem_model_selector/__init__.py +0 -0
  46. supervisely/app/widgets/ecosystem_model_selector/ecosystem_model_selector.py +195 -0
  47. supervisely/app/widgets/experiment_selector/experiment_selector.py +454 -263
  48. supervisely/app/widgets/fast_table/fast_table.py +713 -126
  49. supervisely/app/widgets/fast_table/script.js +492 -95
  50. supervisely/app/widgets/fast_table/style.css +54 -0
  51. supervisely/app/widgets/fast_table/template.html +45 -5
  52. supervisely/app/widgets/heatmap/__init__.py +0 -0
  53. supervisely/app/widgets/heatmap/heatmap.py +523 -0
  54. supervisely/app/widgets/heatmap/script.js +378 -0
  55. supervisely/app/widgets/heatmap/style.css +227 -0
  56. supervisely/app/widgets/heatmap/template.html +21 -0
  57. supervisely/app/widgets/input_tag/input_tag.py +102 -15
  58. supervisely/app/widgets/input_tag_list/__init__.py +0 -0
  59. supervisely/app/widgets/input_tag_list/input_tag_list.py +274 -0
  60. supervisely/app/widgets/input_tag_list/template.html +70 -0
  61. supervisely/app/widgets/radio_table/radio_table.py +10 -2
  62. supervisely/app/widgets/radio_tabs/radio_tabs.py +18 -2
  63. supervisely/app/widgets/radio_tabs/template.html +1 -0
  64. supervisely/app/widgets/select/select.py +6 -4
  65. supervisely/app/widgets/select_dataset/select_dataset.py +6 -0
  66. supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +83 -7
  67. supervisely/app/widgets/table/table.py +68 -13
  68. supervisely/app/widgets/tabs/tabs.py +22 -6
  69. supervisely/app/widgets/tabs/template.html +5 -1
  70. supervisely/app/widgets/transfer/style.css +3 -0
  71. supervisely/app/widgets/transfer/template.html +3 -1
  72. supervisely/app/widgets/transfer/transfer.py +48 -45
  73. supervisely/app/widgets/tree_select/tree_select.py +2 -0
  74. supervisely/convert/image/csv/csv_converter.py +24 -15
  75. supervisely/convert/pointcloud/nuscenes_conv/nuscenes_converter.py +43 -41
  76. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_converter.py +75 -51
  77. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_helper.py +137 -124
  78. supervisely/convert/video/video_converter.py +2 -2
  79. supervisely/geometry/polyline_3d.py +110 -0
  80. supervisely/io/env.py +161 -1
  81. supervisely/nn/artifacts/__init__.py +1 -1
  82. supervisely/nn/artifacts/artifacts.py +10 -2
  83. supervisely/nn/artifacts/detectron2.py +1 -0
  84. supervisely/nn/artifacts/hrda.py +1 -0
  85. supervisely/nn/artifacts/mmclassification.py +20 -0
  86. supervisely/nn/artifacts/mmdetection.py +5 -3
  87. supervisely/nn/artifacts/mmsegmentation.py +1 -0
  88. supervisely/nn/artifacts/ritm.py +1 -0
  89. supervisely/nn/artifacts/rtdetr.py +1 -0
  90. supervisely/nn/artifacts/unet.py +1 -0
  91. supervisely/nn/artifacts/utils.py +3 -0
  92. supervisely/nn/artifacts/yolov5.py +2 -0
  93. supervisely/nn/artifacts/yolov8.py +1 -0
  94. supervisely/nn/benchmark/semantic_segmentation/metric_provider.py +18 -18
  95. supervisely/nn/experiments.py +9 -0
  96. supervisely/nn/inference/cache.py +37 -17
  97. supervisely/nn/inference/gui/serving_gui_template.py +39 -13
  98. supervisely/nn/inference/inference.py +953 -211
  99. supervisely/nn/inference/inference_request.py +15 -8
  100. supervisely/nn/inference/instance_segmentation/instance_segmentation.py +1 -0
  101. supervisely/nn/inference/object_detection/object_detection.py +1 -0
  102. supervisely/nn/inference/predict_app/__init__.py +0 -0
  103. supervisely/nn/inference/predict_app/gui/__init__.py +0 -0
  104. supervisely/nn/inference/predict_app/gui/classes_selector.py +160 -0
  105. supervisely/nn/inference/predict_app/gui/gui.py +915 -0
  106. supervisely/nn/inference/predict_app/gui/input_selector.py +344 -0
  107. supervisely/nn/inference/predict_app/gui/model_selector.py +77 -0
  108. supervisely/nn/inference/predict_app/gui/output_selector.py +179 -0
  109. supervisely/nn/inference/predict_app/gui/preview.py +93 -0
  110. supervisely/nn/inference/predict_app/gui/settings_selector.py +881 -0
  111. supervisely/nn/inference/predict_app/gui/tags_selector.py +110 -0
  112. supervisely/nn/inference/predict_app/gui/utils.py +399 -0
  113. supervisely/nn/inference/predict_app/predict_app.py +176 -0
  114. supervisely/nn/inference/session.py +47 -39
  115. supervisely/nn/inference/tracking/bbox_tracking.py +5 -1
  116. supervisely/nn/inference/tracking/point_tracking.py +5 -1
  117. supervisely/nn/inference/tracking/tracker_interface.py +4 -0
  118. supervisely/nn/inference/uploader.py +9 -5
  119. supervisely/nn/model/model_api.py +44 -22
  120. supervisely/nn/model/prediction.py +15 -1
  121. supervisely/nn/model/prediction_session.py +70 -14
  122. supervisely/nn/prediction_dto.py +7 -0
  123. supervisely/nn/tracker/__init__.py +6 -8
  124. supervisely/nn/tracker/base_tracker.py +54 -0
  125. supervisely/nn/tracker/botsort/__init__.py +1 -0
  126. supervisely/nn/tracker/botsort/botsort_config.yaml +30 -0
  127. supervisely/nn/tracker/botsort/osnet_reid/__init__.py +0 -0
  128. supervisely/nn/tracker/botsort/osnet_reid/osnet.py +566 -0
  129. supervisely/nn/tracker/botsort/osnet_reid/osnet_reid_interface.py +88 -0
  130. supervisely/nn/tracker/botsort/tracker/__init__.py +0 -0
  131. supervisely/nn/tracker/{bot_sort → botsort/tracker}/basetrack.py +1 -2
  132. supervisely/nn/tracker/{utils → botsort/tracker}/gmc.py +51 -59
  133. supervisely/nn/tracker/{deep_sort/deep_sort → botsort/tracker}/kalman_filter.py +71 -33
  134. supervisely/nn/tracker/botsort/tracker/matching.py +202 -0
  135. supervisely/nn/tracker/{bot_sort/bot_sort.py → botsort/tracker/mc_bot_sort.py} +68 -81
  136. supervisely/nn/tracker/botsort_tracker.py +273 -0
  137. supervisely/nn/tracker/calculate_metrics.py +264 -0
  138. supervisely/nn/tracker/utils.py +273 -0
  139. supervisely/nn/tracker/visualize.py +520 -0
  140. supervisely/nn/training/gui/gui.py +152 -49
  141. supervisely/nn/training/gui/hyperparameters_selector.py +1 -1
  142. supervisely/nn/training/gui/model_selector.py +8 -6
  143. supervisely/nn/training/gui/train_val_splits_selector.py +144 -71
  144. supervisely/nn/training/gui/training_artifacts.py +3 -1
  145. supervisely/nn/training/train_app.py +225 -46
  146. supervisely/project/pointcloud_episode_project.py +12 -8
  147. supervisely/project/pointcloud_project.py +12 -8
  148. supervisely/project/project.py +221 -75
  149. supervisely/template/experiment/experiment.html.jinja +105 -55
  150. supervisely/template/experiment/experiment_generator.py +258 -112
  151. supervisely/template/experiment/header.html.jinja +31 -13
  152. supervisely/template/experiment/sly-style.css +7 -2
  153. supervisely/versions.json +3 -1
  154. supervisely/video/sampling.py +42 -20
  155. supervisely/video/video.py +41 -12
  156. supervisely/video_annotation/video_figure.py +38 -4
  157. supervisely/volume/stl_converter.py +2 -0
  158. supervisely/worker_api/agent_rpc.py +24 -1
  159. supervisely/worker_api/rpc_servicer.py +31 -7
  160. {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/METADATA +22 -14
  161. {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/RECORD +167 -148
  162. supervisely_lib/__init__.py +6 -1
  163. supervisely/app/widgets/experiment_selector/style.css +0 -27
  164. supervisely/app/widgets/experiment_selector/template.html +0 -61
  165. supervisely/nn/tracker/bot_sort/__init__.py +0 -21
  166. supervisely/nn/tracker/bot_sort/fast_reid_interface.py +0 -152
  167. supervisely/nn/tracker/bot_sort/matching.py +0 -127
  168. supervisely/nn/tracker/bot_sort/sly_tracker.py +0 -401
  169. supervisely/nn/tracker/deep_sort/__init__.py +0 -6
  170. supervisely/nn/tracker/deep_sort/deep_sort/__init__.py +0 -1
  171. supervisely/nn/tracker/deep_sort/deep_sort/detection.py +0 -49
  172. supervisely/nn/tracker/deep_sort/deep_sort/iou_matching.py +0 -81
  173. supervisely/nn/tracker/deep_sort/deep_sort/linear_assignment.py +0 -202
  174. supervisely/nn/tracker/deep_sort/deep_sort/nn_matching.py +0 -176
  175. supervisely/nn/tracker/deep_sort/deep_sort/track.py +0 -166
  176. supervisely/nn/tracker/deep_sort/deep_sort/tracker.py +0 -145
  177. supervisely/nn/tracker/deep_sort/deep_sort.py +0 -301
  178. supervisely/nn/tracker/deep_sort/generate_clip_detections.py +0 -90
  179. supervisely/nn/tracker/deep_sort/preprocessing.py +0 -70
  180. supervisely/nn/tracker/deep_sort/sly_tracker.py +0 -273
  181. supervisely/nn/tracker/tracker.py +0 -285
  182. supervisely/nn/tracker/utils/kalman_filter.py +0 -492
  183. supervisely/nn/tracking/__init__.py +0 -1
  184. supervisely/nn/tracking/boxmot.py +0 -114
  185. supervisely/nn/tracking/tracking.py +0 -24
  186. /supervisely/{nn/tracker/utils → app/widgets/deploy_model}/__init__.py +0 -0
  187. {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/LICENSE +0 -0
  188. {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/WHEEL +0 -0
  189. {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/entry_points.txt +0 -0
  190. {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/top_level.txt +0 -0
supervisely/__init__.py CHANGED
@@ -8,6 +8,22 @@ try:
8
8
  except TypeError as e:
9
9
  __version__ = "development"
10
10
 
11
+
12
+ class _ApiProtoNotAvailable:
13
+ """Placeholder class that raises an error when accessing any attribute"""
14
+
15
+ def __getattr__(self, name):
16
+ from supervisely.app.v1.constants import PROTOBUF_REQUIRED_ERROR
17
+
18
+ raise ImportError(f"Cannot access `api_proto.{name}` : " + PROTOBUF_REQUIRED_ERROR)
19
+
20
+ def __bool__(self):
21
+ return False
22
+
23
+ def __repr__(self):
24
+ return "<api_proto: not available - install supervisely[agent] to enable>"
25
+
26
+
11
27
  from supervisely.sly_logger import (
12
28
  logger,
13
29
  ServiceType,
@@ -112,7 +128,14 @@ from supervisely.worker_api.chunking import (
112
128
  ChunkedFileWriter,
113
129
  ChunkedFileReader,
114
130
  )
115
- import supervisely.worker_proto.worker_api_pb2 as api_proto
131
+
132
+ # Global import of api_proto works only if protobuf is installed and compatible
133
+ # Otherwise, we use a placeholder that raises an error when accessed
134
+ try:
135
+ import supervisely.worker_proto.worker_api_pb2 as api_proto
136
+ except Exception:
137
+ api_proto = _ApiProtoNotAvailable()
138
+
116
139
 
117
140
  from supervisely.api.api import Api, UserSession, ApiContext
118
141
  from supervisely.api import api
@@ -318,3 +341,115 @@ except Exception as e:
318
341
  from supervisely.io.env import configure_minimum_instance_version
319
342
 
320
343
  configure_minimum_instance_version()
344
+
345
+ LARGE_ENV_PLACEHOLDER = "@.@SLY_LARGE_ENV@.@"
346
+
347
+
348
+ def restore_env_vars():
349
+ try:
350
+ large_env_keys = []
351
+ for key, value in os.environ.items():
352
+ if value == LARGE_ENV_PLACEHOLDER:
353
+ large_env_keys.append(key)
354
+ if len(large_env_keys) == 0:
355
+ return
356
+
357
+ if utils.is_development():
358
+ logger.info(
359
+ "Large environment variables detected. Skipping restoration in development mode.",
360
+ extra={"keys": large_env_keys},
361
+ )
362
+ return
363
+
364
+ unknown_keys = []
365
+ state_keys = []
366
+ context_keys = []
367
+ for key in large_env_keys:
368
+ if key == "CONTEXT" or key.startswith("context."):
369
+ context_keys.append(key)
370
+ elif key.startswith("MODAL_STATE") or key.startswith("modal.state."):
371
+ state_keys.append(key)
372
+ else:
373
+ unknown_keys.append(key)
374
+
375
+ if state_keys or context_keys:
376
+ api = Api()
377
+ if state_keys:
378
+ task_info = api.task.get_info_by_id(env.task_id())
379
+ state = task_info.get("meta", {}).get("params", {}).get("state", {})
380
+ modal_state_envs = json.flatten_json(state)
381
+ modal_state_envs = json.modify_keys(modal_state_envs, prefix="modal.state.")
382
+
383
+ restored_keys = []
384
+ not_found_keys = []
385
+ for key in state_keys:
386
+ if key == "MODAL_STATE":
387
+ os.environ[key] = json.json.dumps(state)
388
+ elif key in modal_state_envs:
389
+ os.environ[key] = str(modal_state_envs[key])
390
+ elif key.replace("_", ".") in [k.upper() for k in modal_state_envs]:
391
+ # some env vars do not support dots in their names
392
+ k = next(k for k in modal_state_envs if k.upper() == key.replace("_", "."))
393
+ os.environ[key] = str(modal_state_envs[k])
394
+ else:
395
+ not_found_keys.append(key)
396
+ continue
397
+ restored_keys.append(key)
398
+
399
+ if restored_keys:
400
+ logger.info(
401
+ "Restored large environment variables from task state",
402
+ extra={"keys": restored_keys},
403
+ )
404
+
405
+ if not_found_keys:
406
+ logger.warning(
407
+ "Failed to restore some large environment variables from task state. "
408
+ "No such keys in the state.",
409
+ extra={"keys": not_found_keys},
410
+ )
411
+
412
+ if context_keys:
413
+ context = api.task.get_context(env.task_id())
414
+ context_envs = json.flatten_json(context)
415
+ context_envs = json.modify_keys(context_envs, prefix="context.")
416
+
417
+ restored_keys = []
418
+ not_found_keys = []
419
+ for key in context_keys:
420
+ if key == "CONTEXT":
421
+ os.environ[key] = json.json.dumps(context)
422
+ elif key in context_envs:
423
+ os.environ[key] = context_envs[key]
424
+ else:
425
+ not_found_keys.append(key)
426
+ continue
427
+ restored_keys.append(key)
428
+
429
+ if restored_keys:
430
+ logger.info(
431
+ "Restored large environment variables from task context",
432
+ extra={"keys": restored_keys},
433
+ )
434
+
435
+ if not_found_keys:
436
+ logger.warning(
437
+ "Failed to restore some large environment variables from task context. "
438
+ "No such keys in the context.",
439
+ extra={"keys": not_found_keys},
440
+ )
441
+
442
+ if unknown_keys:
443
+ logger.warning(
444
+ "Found unknown large environment variables. Can't restore them.",
445
+ extra={"keys": unknown_keys},
446
+ )
447
+
448
+ except Exception as e:
449
+ logger.warning(
450
+ "Failed to restore large environment variables.",
451
+ exc_info=True,
452
+ )
453
+
454
+
455
+ restore_env_vars()
supervisely/_utils.py CHANGED
@@ -319,6 +319,87 @@ def resize_image_url(
319
319
  return full_storage_url
320
320
 
321
321
 
322
+ def get_storage_url(
323
+ entity_type: Literal["dataset-entities", "dataset", "project", "file-storage"],
324
+ entity_id: int,
325
+ source_type: Literal["original", "preview"],
326
+ ) -> str:
327
+ """
328
+ Generate URL for storage resources endpoints.
329
+
330
+ :param entity_type: Type of entity ("dataset-entities", "dataset", "project", "file-storage")
331
+ :type entity_type: str
332
+ :param entity_id: ID of the entity
333
+ :type entity_id: int
334
+ :param source_type: Type of source ("original" or "preview")
335
+ :type source_type: Literal["original", "preview"]
336
+ :return: Storage URL
337
+ :rtype: str
338
+ """
339
+ relative_url = f"/storage-resources/{entity_type}/{source_type}/{entity_id}"
340
+ if is_development():
341
+ return abs_url(relative_url)
342
+ return relative_url
343
+
344
+
345
+ def get_image_storage_url(image_id: int, source_type: Literal["original", "preview"]) -> str:
346
+ """
347
+ Generate URL for image storage resources.
348
+
349
+ :param image_id: ID of the image
350
+ :type image_id: int
351
+ :param source_type: Type of source ("original" or "preview")
352
+ :type source_type: Literal["original", "preview"]
353
+ :return: Storage URL for image
354
+ :rtype: str
355
+ """
356
+ return get_storage_url("dataset-entities", image_id, source_type)
357
+
358
+
359
+ def get_dataset_storage_url(
360
+ dataset_id: int, source_type: Literal["original", "preview", "raw"]
361
+ ) -> str:
362
+ """
363
+ Generate URL for dataset storage resources.
364
+
365
+ :param dataset_id: ID of the dataset
366
+ :type dataset_id: int
367
+ :param source_type: Type of source ("original", "preview", or "raw")
368
+ :type source_type: Literal["original", "preview", "raw"]
369
+ :return: Storage URL for dataset
370
+ :rtype: str
371
+ """
372
+ return get_storage_url("dataset", dataset_id, source_type)
373
+
374
+
375
+ def get_project_storage_url(
376
+ project_id: int, source_type: Literal["original", "preview", "raw"]
377
+ ) -> str:
378
+ """
379
+ Generate URL for project storage resources.
380
+
381
+ :param project_id: ID of the project
382
+ :type project_id: int
383
+ :param source_type: Type of source ("original", "preview", or "raw")
384
+ :type source_type: Literal["original", "preview", "raw"]
385
+ :return: Storage URL for project
386
+ :rtype: str
387
+ """
388
+ return get_storage_url("project", project_id, source_type)
389
+
390
+
391
+ def get_file_storage_url(file_id: int) -> str:
392
+ """
393
+ Generate URL for file storage resources (raw files).
394
+
395
+ :param file_id: ID of the file
396
+ :type file_id: int
397
+ :return: Storage URL for file
398
+ :rtype: str
399
+ """
400
+ return get_storage_url("file-storage", file_id, "raw")
401
+
402
+
322
403
  def get_preview_link(title="preview"):
323
404
  return (
324
405
  f'<a href="javascript:;">{title}<i class="zmdi zmdi-cast" style="margin-left: 5px"></i></a>'
@@ -15,6 +15,7 @@ from supervisely.geometry.multichannel_bitmap import MultichannelBitmap
15
15
  from supervisely.geometry.closed_surface_mesh import ClosedSurfaceMesh
16
16
  from supervisely.geometry.alpha_mask import AlphaMask
17
17
  from supervisely.geometry.cuboid_2d import Cuboid2d
18
+ from supervisely.geometry.polyline_3d import Polyline3D
18
19
 
19
20
 
20
21
  _INPUT_GEOMETRIES = [
@@ -34,6 +35,7 @@ _INPUT_GEOMETRIES = [
34
35
  ClosedSurfaceMesh,
35
36
  AlphaMask,
36
37
  Cuboid2d,
38
+ Polyline3D,
37
39
  ]
38
40
  _JSON_SHAPE_TO_GEOMETRY_TYPE = {
39
41
  geometry.geometry_name(): geometry for geometry in _INPUT_GEOMETRIES
@@ -4,6 +4,8 @@
4
4
  # docs
5
5
  from __future__ import annotations
6
6
 
7
+
8
+ from supervisely.collection.str_enum import StrEnum
7
9
  from copy import deepcopy
8
10
  from typing import Dict, List, Optional, Tuple, Union
9
11
 
@@ -45,6 +47,47 @@ class LabelJsonFields:
45
47
  """"""
46
48
  SMART_TOOL_INPUT = "smartToolInput"
47
49
  """"""
50
+ NN_CREATED = "nnCreated"
51
+ """Flag indicating if the label was created by NN model or manually."""
52
+ NN_UPDATED = "nnUpdated"
53
+ """Flag indicating if the label was corrected by NN model or manually."""
54
+
55
+ class LabelingStatus(StrEnum):
56
+ """
57
+ Shows status of the label. Can be one of the following:
58
+
59
+ - AUTO: Specifies if the label was created by NN model.
60
+ - nn_created: True | Created by NN model
61
+ - nn_updated: True | Corrected by NN model
62
+ - MANUAL: Specifies if the label was created manually.
63
+ - nn_created: False | Manually created
64
+ - nn_updated: False | Not corrected by NN model
65
+ - CORRECTED: Specifies if the label was initially created by NN model and then manually corrected.
66
+ - nn_created: True | Created by NN model
67
+ - nn_updated: False | Manually corrected
68
+ """
69
+
70
+ AUTO = "auto"
71
+ MANUAL = "manual"
72
+ CORRECTED = "corrected"
73
+
74
+ @classmethod
75
+ def to_flags(cls, status: LabelingStatus) -> Tuple[bool, bool]:
76
+ if status == cls.AUTO:
77
+ return True, True
78
+ elif status == cls.CORRECTED:
79
+ return True, False
80
+ else:
81
+ return False, False
82
+
83
+ @classmethod
84
+ def from_flags(cls, nn_created: bool, nn_updated: bool) -> LabelingStatus:
85
+ if nn_created and nn_updated:
86
+ return cls.AUTO
87
+ elif nn_created and not nn_updated:
88
+ return cls.CORRECTED
89
+ else:
90
+ return cls.MANUAL
48
91
 
49
92
 
50
93
  class LabelBase:
@@ -65,6 +108,8 @@ class LabelBase:
65
108
  :type smart_tool_input: dict, optional
66
109
  :param sly_id: Label unique identifier.
67
110
  :type sly_id: int, optional
111
+ :param status: Sets labeling status. Shows how label was created and corrected.
112
+ :type status: LabelingStatus, optional
68
113
 
69
114
  :Usage example:
70
115
 
@@ -99,6 +144,7 @@ class LabelBase:
99
144
  binding_key: Optional[str] = None,
100
145
  smart_tool_input: Optional[Dict] = None,
101
146
  sly_id: Optional[int] = None,
147
+ status: Optional[LabelingStatus] = None,
102
148
  ):
103
149
  self._geometry = geometry
104
150
  self._obj_class = obj_class
@@ -112,9 +158,14 @@ class LabelBase:
112
158
 
113
159
  self._binding_key = binding_key
114
160
  self._smart_tool_input = smart_tool_input
115
-
116
161
  self._sly_id = sly_id
117
162
 
163
+ if status is None:
164
+ status = LabelingStatus.MANUAL
165
+ self._status = status
166
+ self._nn_created, self._nn_updated = LabelingStatus.to_flags(self.status)
167
+
168
+
118
169
  def _validate_geometry(self):
119
170
  """
120
171
  The function checks the name of the Object for compliance.
@@ -268,7 +319,9 @@ class LabelBase:
268
319
  # "interior": []
269
320
  # },
270
321
  # "geometryType": "rectangle",
271
- # "shape": "rectangle"
322
+ # "shape": "rectangle",
323
+ # "nnCreated": false,
324
+ # "nnUpdated": false
272
325
  # }
273
326
  """
274
327
  res = {
@@ -278,6 +331,8 @@ class LabelBase:
278
331
  **self.geometry.to_json(),
279
332
  GEOMETRY_TYPE: self.geometry.geometry_name(),
280
333
  GEOMETRY_SHAPE: self.geometry.geometry_name(),
334
+ LabelJsonFields.NN_CREATED: self._nn_created,
335
+ LabelJsonFields.NN_UPDATED: self._nn_updated,
281
336
  }
282
337
 
283
338
  if self.obj_class.sly_id is not None:
@@ -328,7 +383,9 @@ class LabelBase:
328
383
  "points": {
329
384
  "exterior": [[100, 100], [900, 700]],
330
385
  "interior": []
331
- }
386
+ },
387
+ "nnCreated": false,
388
+ "nnUpdated": false
332
389
  }
333
390
 
334
391
  label_dog = sly.Label.from_json(data, meta)
@@ -352,6 +409,10 @@ class LabelBase:
352
409
  binding_key = data.get(LabelJsonFields.INSTANCE_KEY)
353
410
  smart_tool_input = data.get(LabelJsonFields.SMART_TOOL_INPUT)
354
411
 
412
+ nn_created = data.get(LabelJsonFields.NN_CREATED, False)
413
+ nn_updated = data.get(LabelJsonFields.NN_UPDATED, False)
414
+ status = LabelingStatus.from_flags(nn_created, nn_updated)
415
+
355
416
  return cls(
356
417
  geometry=geometry,
357
418
  obj_class=obj_class,
@@ -360,6 +421,7 @@ class LabelBase:
360
421
  binding_key=binding_key,
361
422
  smart_tool_input=smart_tool_input,
362
423
  sly_id=data.get(LabelJsonFields.ID),
424
+ status=status,
363
425
  )
364
426
 
365
427
  @property
@@ -441,6 +503,7 @@ class LabelBase:
441
503
  description: Optional[str] = None,
442
504
  binding_key: Optional[str] = None,
443
505
  smart_tool_input: Optional[Dict] = None,
506
+ status: Optional[LabelingStatus] = None,
444
507
  ) -> LabelBase:
445
508
  """
446
509
  Makes a copy of Label with new fields, if fields are given, otherwise it will use fields of the original Label.
@@ -457,6 +520,8 @@ class LabelBase:
457
520
  :type binding_key: str, optional
458
521
  :param smart_tool_input: Smart Tool parameters that were used for labeling.
459
522
  :type smart_tool_input: dict, optional
523
+ :param status: Sets labeling status. Specifies if the label was created by NN model, manually or created by NN and then manually corrected.
524
+ :type status: LabelingStatus, optional
460
525
  :return: New instance of Label
461
526
  :rtype: :class:`Label<LabelBase>`
462
527
  :Usage example:
@@ -501,6 +566,7 @@ class LabelBase:
501
566
  description=take_with_default(description, self.description),
502
567
  binding_key=take_with_default(binding_key, self.binding_key),
503
568
  smart_tool_input=take_with_default(smart_tool_input, self._smart_tool_input),
569
+ status=take_with_default(status, self.status),
504
570
  )
505
571
 
506
572
  def crop(self, rect: Rectangle) -> List[LabelBase]:
@@ -864,6 +930,17 @@ class LabelBase:
864
930
  def labeler_login(self):
865
931
  return self.geometry.labeler_login
866
932
 
933
+ @property
934
+ def status(self) -> LabelingStatus:
935
+ """Labeling status. Specifies if the Label was created by NN model, manually or created by NN and then manually corrected."""
936
+ return self._status
937
+
938
+ @status.setter
939
+ def status(self, status: LabelingStatus):
940
+ """Set labeling status."""
941
+ self._status = status
942
+ self._nn_created, self._nn_updated = LabelingStatus.to_flags(self.status)
943
+
867
944
  @classmethod
868
945
  def _to_pixel_coordinate_system_json(cls, data: Dict, image_size: List[int]) -> Dict:
869
946
  """
@@ -865,7 +865,7 @@ class AnnotationApi(ModuleApi):
865
865
  return
866
866
  if len(img_ids) != len(anns):
867
867
  raise RuntimeError(
868
- 'Can not match "img_ids" and "anns" lists, len(img_ids) != len(anns)'
868
+ f'Lists "img_ids" and "anns" have different lengths: {len(img_ids)} != {len(anns)}.'
869
869
  )
870
870
 
871
871
  # use context to avoid redundant API calls
@@ -1312,14 +1312,14 @@ class AnnotationApi(ModuleApi):
1312
1312
 
1313
1313
  api.annotation.update_label(label_id, new_label)
1314
1314
  """
1315
- self._api.post(
1316
- "figures.editInfo",
1317
- {
1318
- ApiField.ID: label_id,
1319
- ApiField.TAGS: [tag.to_json() for tag in label.tags],
1320
- ApiField.GEOMETRY: label.geometry.to_json(),
1321
- },
1322
- )
1315
+ payload = {
1316
+ ApiField.ID: label_id,
1317
+ ApiField.TAGS: [tag.to_json() for tag in label.tags],
1318
+ ApiField.GEOMETRY: label.geometry.to_json(),
1319
+ ApiField.NN_CREATED: label._nn_created,
1320
+ ApiField.NN_UPDATED: label._nn_updated,
1321
+ }
1322
+ self._api.post("figures.editInfo", payload)
1323
1323
 
1324
1324
  def update_label_priority(self, label_id: int, priority: int) -> None:
1325
1325
  """Updates label's priority with given ID in Supervisely.
supervisely/api/api.py CHANGED
@@ -10,6 +10,7 @@ import glob
10
10
  import json
11
11
  import os
12
12
  import shutil
13
+ import threading
13
14
  from logging import Logger
14
15
  from pathlib import Path
15
16
  from typing import (
@@ -392,13 +393,15 @@ class Api:
392
393
  else not self.server_address.startswith("https://")
393
394
  )
394
395
 
395
- if check_instance_version:
396
- self._check_version(None if check_instance_version is True else check_instance_version)
397
-
398
396
  self.async_httpx_client: httpx.AsyncClient = None
399
397
  self.httpx_client: httpx.Client = None
400
398
  self._semaphore = None
401
399
  self._instance_version = None
400
+ self._version_check_completed = False
401
+ self._version_check_lock = threading.Lock()
402
+
403
+ if check_instance_version:
404
+ self._check_version(None if check_instance_version is True else check_instance_version)
402
405
 
403
406
  @classmethod
404
407
  def normalize_server_address(cls, server_address: str) -> str:
@@ -600,38 +603,49 @@ class Api:
600
603
  :type version: Optional[str], e.g. "6.9.13"
601
604
  """
602
605
 
603
- # Since it's a informational message, we don't raise an exception if the check fails
604
- # in any case, we don't want to interrupt the user's workflow.
605
- try:
606
- check_result = self.is_version_supported(version)
607
- if check_result is None:
606
+ # Thread-safe one-time check with double-checked locking pattern
607
+ if self._version_check_completed:
608
+ return
609
+
610
+ with self._version_check_lock:
611
+ # Double-check inside the lock
612
+ if self._version_check_completed:
613
+ return
614
+
615
+ self._version_check_completed = True
616
+
617
+ # Since it's a informational message, we don't raise an exception if the check fails
618
+ # in any case, we don't want to interrupt the user's workflow.
619
+ try:
620
+ check_result = self.is_version_supported(version)
621
+ if check_result is None:
622
+ logger.debug(
623
+ "Failed to check if the instance version meets the minimum requirements "
624
+ "of current SDK version. "
625
+ "Ensure that the MINIMUM_INSTANCE_VERSION_FOR_SDK environment variable is set. "
626
+ "Usually you can ignore this message, but if you're adding new features, "
627
+ "which will require upgrade of the Supervisely instance, you should update "
628
+ "it supervisely.__init__.py file."
629
+ )
630
+ if check_result is False:
631
+ message = (
632
+ "The current version of the Supervisely instance is not supported by the SDK. "
633
+ "Some features may not work correctly."
634
+ )
635
+ if not is_community():
636
+ message += (
637
+ " Please upgrade the Supervisely instance to the latest version (recommended) "
638
+ "or downgrade the SDK to the version that supports the current instance (not recommended). "
639
+ "Refer to this docs for more information: "
640
+ "https://docs.supervisely.com/enterprise-edition/get-supervisely/upgrade "
641
+ "Check out changelog for the latest version of Supervisely: "
642
+ "https://app.supervisely.com/changelog"
643
+ )
644
+ logger.warning(message)
645
+ except Exception as e:
608
646
  logger.debug(
609
- "Failed to check if the instance version meets the minimum requirements "
610
- "of current SDK version. "
611
- "Ensure that the MINIMUM_INSTANCE_VERSION_FOR_SDK environment variable is set. "
612
- "Usually you can ignore this message, but if you're adding new features, "
613
- "which will require upgrade of the Supervisely instance, you should update "
614
- "it supervisely.__init__.py file."
647
+ f"Tried to check version compatibility between SDK and instance, but failed: {e}"
615
648
  )
616
- if check_result is False:
617
- message = (
618
- "The current version of the Supervisely instance is not supported by the SDK. "
619
- "Some features may not work correctly."
620
- )
621
- if not is_community():
622
- message += (
623
- " Please upgrade the Supervisely instance to the latest version (recommended) "
624
- "or downgrade the SDK to the version that supports the current instance (not recommended). "
625
- "Refer to this docs for more information: "
626
- "https://docs.supervisely.com/enterprise-edition/get-supervisely/upgrade "
627
- "Check out changelog for the latest version of Supervisely: "
628
- "https://app.supervisely.com/changelog"
629
- )
630
- logger.warning(message)
631
- except Exception as e:
632
- logger.debug(
633
- f"Tried to check version compatibility between SDK and instance, but failed: {e}"
634
- )
635
649
 
636
650
  def post(
637
651
  self,
@@ -686,7 +700,8 @@ class Api:
686
700
  )
687
701
 
688
702
  if response.status_code != requests.codes.ok: # pylint: disable=no-member
689
- self._check_version()
703
+ if not self._version_check_completed:
704
+ self._check_version()
690
705
  Api._raise_for_status(response)
691
706
  return response
692
707
  except requests.RequestException as exc:
@@ -723,6 +738,7 @@ class Api:
723
738
  retries: Optional[int] = None,
724
739
  stream: Optional[bool] = False,
725
740
  use_public_api: Optional[bool] = True,
741
+ data: Optional[Dict] = None,
726
742
  ) -> requests.Response:
727
743
  """
728
744
  Performs GET request to server with given parameters.
@@ -730,13 +746,15 @@ class Api:
730
746
  :param method:
731
747
  :type method: str
732
748
  :param params: Dictionary to send in the body of the :class:`Request`.
733
- :type method: dict
749
+ :type params: dict
734
750
  :param retries: The number of attempts to connect to the server.
735
- :type method: int, optional
751
+ :type retries: int, optional
736
752
  :param stream: Define, if you'd like to get the raw socket response from the server.
737
- :type method: bool, optional
753
+ :type stream: bool, optional
738
754
  :param use_public_api:
739
- :type method: bool, optional
755
+ :type use_public_api: bool, optional
756
+ :param data: Dictionary to send in the body of the :class:`Request`.
757
+ :type data: dict, optional
740
758
  :return: Response object
741
759
  :rtype: :class:`Response<Response>`
742
760
  """
@@ -756,7 +774,9 @@ class Api:
756
774
  json_body = params
757
775
  if type(params) is dict:
758
776
  json_body = {**params, **self.additional_fields}
759
- response = requests.get(url, params=json_body, headers=self.headers, stream=stream)
777
+ response = requests.get(
778
+ url, params=json_body, data=data, headers=self.headers, stream=stream
779
+ )
760
780
 
761
781
  if response.status_code != requests.codes.ok: # pylint: disable=no-member
762
782
  Api._raise_for_status(response)
@@ -1098,7 +1118,8 @@ class Api:
1098
1118
  timeout=timeout,
1099
1119
  )
1100
1120
  if response.status_code != httpx.codes.OK:
1101
- self._check_version()
1121
+ if not self._version_check_completed:
1122
+ self._check_version()
1102
1123
  Api._raise_for_status_httpx(response)
1103
1124
  return response
1104
1125
  except (httpx.RequestError, httpx.HTTPStatusError) as exc:
@@ -1314,7 +1335,8 @@ class Api:
1314
1335
  httpx.codes.OK,
1315
1336
  httpx.codes.PARTIAL_CONTENT,
1316
1337
  ]:
1317
- self._check_version()
1338
+ if not self._version_check_completed:
1339
+ self._check_version()
1318
1340
  Api._raise_for_status_httpx(resp)
1319
1341
 
1320
1342
  hhash = resp.headers.get("x-content-checksum-sha256", None)
@@ -1428,7 +1450,8 @@ class Api:
1428
1450
  timeout=timeout,
1429
1451
  )
1430
1452
  if response.status_code != httpx.codes.OK:
1431
- self._check_version()
1453
+ if not self._version_check_completed:
1454
+ self._check_version()
1432
1455
  Api._raise_for_status_httpx(response)
1433
1456
  return response
1434
1457
  except (httpx.RequestError, httpx.HTTPStatusError) as exc:
@@ -1569,7 +1592,8 @@ class Api:
1569
1592
  httpx.codes.OK,
1570
1593
  httpx.codes.PARTIAL_CONTENT,
1571
1594
  ]:
1572
- self._check_version()
1595
+ if not self._version_check_completed:
1596
+ self._check_version()
1573
1597
  Api._raise_for_status_httpx(resp)
1574
1598
 
1575
1599
  # received hash of the content to check integrity of the data stream