supervisely 6.73.438__py3-none-any.whl → 6.73.513__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 (203) hide show
  1. supervisely/__init__.py +137 -1
  2. supervisely/_utils.py +81 -0
  3. supervisely/annotation/annotation.py +8 -2
  4. supervisely/annotation/json_geometries_map.py +14 -11
  5. supervisely/annotation/label.py +80 -3
  6. supervisely/api/annotation_api.py +14 -11
  7. supervisely/api/api.py +59 -38
  8. supervisely/api/app_api.py +11 -2
  9. supervisely/api/dataset_api.py +74 -12
  10. supervisely/api/entities_collection_api.py +10 -0
  11. supervisely/api/entity_annotation/figure_api.py +52 -4
  12. supervisely/api/entity_annotation/object_api.py +3 -3
  13. supervisely/api/entity_annotation/tag_api.py +63 -12
  14. supervisely/api/guides_api.py +210 -0
  15. supervisely/api/image_api.py +72 -1
  16. supervisely/api/labeling_job_api.py +83 -1
  17. supervisely/api/labeling_queue_api.py +33 -7
  18. supervisely/api/module_api.py +9 -0
  19. supervisely/api/project_api.py +71 -26
  20. supervisely/api/storage_api.py +3 -1
  21. supervisely/api/task_api.py +13 -2
  22. supervisely/api/team_api.py +4 -3
  23. supervisely/api/video/video_annotation_api.py +119 -3
  24. supervisely/api/video/video_api.py +65 -14
  25. supervisely/api/video/video_figure_api.py +24 -11
  26. supervisely/app/__init__.py +1 -1
  27. supervisely/app/content.py +23 -7
  28. supervisely/app/development/development.py +18 -2
  29. supervisely/app/fastapi/__init__.py +1 -0
  30. supervisely/app/fastapi/custom_static_files.py +1 -1
  31. supervisely/app/fastapi/multi_user.py +105 -0
  32. supervisely/app/fastapi/subapp.py +88 -42
  33. supervisely/app/fastapi/websocket.py +77 -9
  34. supervisely/app/singleton.py +21 -0
  35. supervisely/app/v1/app_service.py +18 -2
  36. supervisely/app/v1/constants.py +7 -1
  37. supervisely/app/widgets/__init__.py +6 -0
  38. supervisely/app/widgets/activity_feed/__init__.py +0 -0
  39. supervisely/app/widgets/activity_feed/activity_feed.py +239 -0
  40. supervisely/app/widgets/activity_feed/style.css +78 -0
  41. supervisely/app/widgets/activity_feed/template.html +22 -0
  42. supervisely/app/widgets/card/card.py +20 -0
  43. supervisely/app/widgets/classes_list_selector/classes_list_selector.py +121 -9
  44. supervisely/app/widgets/classes_list_selector/template.html +60 -93
  45. supervisely/app/widgets/classes_mapping/classes_mapping.py +13 -12
  46. supervisely/app/widgets/classes_table/classes_table.py +1 -0
  47. supervisely/app/widgets/deploy_model/deploy_model.py +56 -35
  48. supervisely/app/widgets/dialog/dialog.py +12 -0
  49. supervisely/app/widgets/dialog/template.html +2 -1
  50. supervisely/app/widgets/ecosystem_model_selector/ecosystem_model_selector.py +1 -1
  51. supervisely/app/widgets/experiment_selector/experiment_selector.py +8 -0
  52. supervisely/app/widgets/fast_table/fast_table.py +184 -60
  53. supervisely/app/widgets/fast_table/template.html +1 -1
  54. supervisely/app/widgets/heatmap/__init__.py +0 -0
  55. supervisely/app/widgets/heatmap/heatmap.py +564 -0
  56. supervisely/app/widgets/heatmap/script.js +533 -0
  57. supervisely/app/widgets/heatmap/style.css +233 -0
  58. supervisely/app/widgets/heatmap/template.html +21 -0
  59. supervisely/app/widgets/modal/__init__.py +0 -0
  60. supervisely/app/widgets/modal/modal.py +198 -0
  61. supervisely/app/widgets/modal/template.html +10 -0
  62. supervisely/app/widgets/object_class_view/object_class_view.py +3 -0
  63. supervisely/app/widgets/radio_tabs/radio_tabs.py +18 -2
  64. supervisely/app/widgets/radio_tabs/template.html +1 -0
  65. supervisely/app/widgets/select/select.py +6 -3
  66. supervisely/app/widgets/select_class/__init__.py +0 -0
  67. supervisely/app/widgets/select_class/select_class.py +363 -0
  68. supervisely/app/widgets/select_class/template.html +50 -0
  69. supervisely/app/widgets/select_cuda/select_cuda.py +22 -0
  70. supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +65 -7
  71. supervisely/app/widgets/select_tag/__init__.py +0 -0
  72. supervisely/app/widgets/select_tag/select_tag.py +352 -0
  73. supervisely/app/widgets/select_tag/template.html +64 -0
  74. supervisely/app/widgets/select_team/select_team.py +37 -4
  75. supervisely/app/widgets/select_team/template.html +4 -5
  76. supervisely/app/widgets/select_user/__init__.py +0 -0
  77. supervisely/app/widgets/select_user/select_user.py +270 -0
  78. supervisely/app/widgets/select_user/template.html +13 -0
  79. supervisely/app/widgets/select_workspace/select_workspace.py +59 -10
  80. supervisely/app/widgets/select_workspace/template.html +9 -12
  81. supervisely/app/widgets/table/table.py +68 -13
  82. supervisely/app/widgets/tree_select/tree_select.py +2 -0
  83. supervisely/aug/aug.py +6 -2
  84. supervisely/convert/base_converter.py +1 -0
  85. supervisely/convert/converter.py +2 -2
  86. supervisely/convert/image/csv/csv_converter.py +24 -15
  87. supervisely/convert/image/image_converter.py +3 -1
  88. supervisely/convert/image/image_helper.py +48 -4
  89. supervisely/convert/image/label_studio/label_studio_converter.py +2 -0
  90. supervisely/convert/image/medical2d/medical2d_helper.py +2 -24
  91. supervisely/convert/image/multispectral/multispectral_converter.py +6 -0
  92. supervisely/convert/image/pascal_voc/pascal_voc_converter.py +8 -5
  93. supervisely/convert/image/pascal_voc/pascal_voc_helper.py +7 -0
  94. supervisely/convert/pointcloud/kitti_3d/kitti_3d_converter.py +33 -3
  95. supervisely/convert/pointcloud/kitti_3d/kitti_3d_helper.py +12 -5
  96. supervisely/convert/pointcloud/las/las_converter.py +13 -1
  97. supervisely/convert/pointcloud/las/las_helper.py +110 -11
  98. supervisely/convert/pointcloud/nuscenes_conv/nuscenes_converter.py +27 -16
  99. supervisely/convert/pointcloud/pointcloud_converter.py +91 -3
  100. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_converter.py +58 -22
  101. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_helper.py +21 -47
  102. supervisely/convert/video/__init__.py +1 -0
  103. supervisely/convert/video/multi_view/__init__.py +0 -0
  104. supervisely/convert/video/multi_view/multi_view.py +543 -0
  105. supervisely/convert/video/sly/sly_video_converter.py +359 -3
  106. supervisely/convert/video/video_converter.py +24 -4
  107. supervisely/convert/volume/dicom/dicom_converter.py +13 -5
  108. supervisely/convert/volume/dicom/dicom_helper.py +30 -18
  109. supervisely/geometry/constants.py +1 -0
  110. supervisely/geometry/geometry.py +4 -0
  111. supervisely/geometry/helpers.py +5 -1
  112. supervisely/geometry/oriented_bbox.py +676 -0
  113. supervisely/geometry/polyline_3d.py +110 -0
  114. supervisely/geometry/rectangle.py +2 -1
  115. supervisely/io/env.py +76 -1
  116. supervisely/io/fs.py +21 -0
  117. supervisely/nn/benchmark/base_evaluator.py +104 -11
  118. supervisely/nn/benchmark/instance_segmentation/evaluator.py +1 -8
  119. supervisely/nn/benchmark/object_detection/evaluator.py +20 -4
  120. supervisely/nn/benchmark/object_detection/vis_metrics/pr_curve.py +10 -5
  121. supervisely/nn/benchmark/semantic_segmentation/evaluator.py +34 -16
  122. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/confusion_matrix.py +1 -1
  123. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/frequently_confused.py +1 -1
  124. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/overview.py +1 -1
  125. supervisely/nn/benchmark/visualization/evaluation_result.py +66 -4
  126. supervisely/nn/inference/cache.py +43 -18
  127. supervisely/nn/inference/gui/serving_gui_template.py +5 -2
  128. supervisely/nn/inference/inference.py +916 -222
  129. supervisely/nn/inference/inference_request.py +55 -10
  130. supervisely/nn/inference/predict_app/gui/classes_selector.py +83 -12
  131. supervisely/nn/inference/predict_app/gui/gui.py +676 -488
  132. supervisely/nn/inference/predict_app/gui/input_selector.py +205 -26
  133. supervisely/nn/inference/predict_app/gui/model_selector.py +2 -4
  134. supervisely/nn/inference/predict_app/gui/output_selector.py +46 -6
  135. supervisely/nn/inference/predict_app/gui/settings_selector.py +756 -59
  136. supervisely/nn/inference/predict_app/gui/tags_selector.py +1 -1
  137. supervisely/nn/inference/predict_app/gui/utils.py +236 -119
  138. supervisely/nn/inference/predict_app/predict_app.py +2 -2
  139. supervisely/nn/inference/session.py +43 -35
  140. supervisely/nn/inference/tracking/bbox_tracking.py +118 -35
  141. supervisely/nn/inference/tracking/point_tracking.py +5 -1
  142. supervisely/nn/inference/tracking/tracker_interface.py +10 -1
  143. supervisely/nn/inference/uploader.py +139 -12
  144. supervisely/nn/live_training/__init__.py +7 -0
  145. supervisely/nn/live_training/api_server.py +111 -0
  146. supervisely/nn/live_training/artifacts_utils.py +243 -0
  147. supervisely/nn/live_training/checkpoint_utils.py +229 -0
  148. supervisely/nn/live_training/dynamic_sampler.py +44 -0
  149. supervisely/nn/live_training/helpers.py +14 -0
  150. supervisely/nn/live_training/incremental_dataset.py +146 -0
  151. supervisely/nn/live_training/live_training.py +497 -0
  152. supervisely/nn/live_training/loss_plateau_detector.py +111 -0
  153. supervisely/nn/live_training/request_queue.py +52 -0
  154. supervisely/nn/model/model_api.py +9 -0
  155. supervisely/nn/model/prediction.py +2 -1
  156. supervisely/nn/model/prediction_session.py +26 -14
  157. supervisely/nn/prediction_dto.py +19 -1
  158. supervisely/nn/tracker/base_tracker.py +11 -1
  159. supervisely/nn/tracker/botsort/botsort_config.yaml +0 -1
  160. supervisely/nn/tracker/botsort/tracker/mc_bot_sort.py +7 -4
  161. supervisely/nn/tracker/botsort_tracker.py +94 -65
  162. supervisely/nn/tracker/utils.py +4 -5
  163. supervisely/nn/tracker/visualize.py +93 -93
  164. supervisely/nn/training/gui/classes_selector.py +16 -1
  165. supervisely/nn/training/gui/train_val_splits_selector.py +52 -31
  166. supervisely/nn/training/train_app.py +46 -31
  167. supervisely/project/data_version.py +115 -51
  168. supervisely/project/download.py +1 -1
  169. supervisely/project/pointcloud_episode_project.py +37 -8
  170. supervisely/project/pointcloud_project.py +30 -2
  171. supervisely/project/project.py +14 -2
  172. supervisely/project/project_meta.py +27 -1
  173. supervisely/project/project_settings.py +32 -18
  174. supervisely/project/versioning/__init__.py +1 -0
  175. supervisely/project/versioning/common.py +20 -0
  176. supervisely/project/versioning/schema_fields.py +35 -0
  177. supervisely/project/versioning/video_schema.py +221 -0
  178. supervisely/project/versioning/volume_schema.py +87 -0
  179. supervisely/project/video_project.py +717 -15
  180. supervisely/project/volume_project.py +623 -5
  181. supervisely/template/experiment/experiment.html.jinja +4 -4
  182. supervisely/template/experiment/experiment_generator.py +14 -21
  183. supervisely/template/live_training/__init__.py +0 -0
  184. supervisely/template/live_training/header.html.jinja +96 -0
  185. supervisely/template/live_training/live_training.html.jinja +51 -0
  186. supervisely/template/live_training/live_training_generator.py +464 -0
  187. supervisely/template/live_training/sly-style.css +402 -0
  188. supervisely/template/live_training/template.html.jinja +18 -0
  189. supervisely/versions.json +28 -26
  190. supervisely/video/sampling.py +39 -20
  191. supervisely/video/video.py +41 -12
  192. supervisely/video_annotation/video_figure.py +38 -4
  193. supervisely/video_annotation/video_object.py +29 -4
  194. supervisely/volume/stl_converter.py +2 -0
  195. supervisely/worker_api/agent_rpc.py +24 -1
  196. supervisely/worker_api/rpc_servicer.py +31 -7
  197. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/METADATA +58 -40
  198. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/RECORD +203 -155
  199. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/WHEEL +1 -1
  200. supervisely_lib/__init__.py +6 -1
  201. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/entry_points.txt +0 -0
  202. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info/licenses}/LICENSE +0 -0
  203. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/top_level.txt +0 -0
@@ -7,12 +7,13 @@ import numpy as np
7
7
  from pydantic import ValidationError
8
8
 
9
9
  from supervisely.annotation.annotation import Annotation
10
- from supervisely.annotation.label import Geometry, Label
10
+ from supervisely.annotation.label import Geometry, Label, LabelingStatus
11
11
  from supervisely.annotation.obj_class import ObjClass
12
12
  from supervisely.api.api import Api
13
13
  from supervisely.api.module_api import ApiField
14
14
  from supervisely.api.video.video_figure_api import FigureInfo
15
15
  from supervisely.geometry.helpers import deserialize_geometry
16
+ from supervisely.geometry.oriented_bbox import OrientedBBox
16
17
  from supervisely.geometry.rectangle import Rectangle
17
18
  from supervisely.imaging import image as sly_image
18
19
  from supervisely.nn.inference.inference import Uploader
@@ -76,7 +77,7 @@ class BBoxTracking(BaseTracking):
76
77
  min(frame_index for (_, _, frame_index) in items),
77
78
  max(frame_index for (_, _, frame_index) in items),
78
79
  ]
79
- pos_inc = inference_request.progress.current - video_interface.global_pos
80
+ pos_inc = len(items)
80
81
 
81
82
  video_interface._notify(
82
83
  pos_increment=pos_inc,
@@ -109,7 +110,7 @@ class BBoxTracking(BaseTracking):
109
110
  init = False
110
111
  for _ in video_interface.frames_loader_generator():
111
112
  geom = video_interface.geometries[fig_id]
112
- if not isinstance(geom, Rectangle):
113
+ if not isinstance(geom, (Rectangle, OrientedBBox)):
113
114
  raise TypeError(f"Tracking does not work with {geom.geometry_name()}.")
114
115
 
115
116
  imgs = video_interface.frames
@@ -117,18 +118,27 @@ class BBoxTracking(BaseTracking):
117
118
  "", # TODO: can this be useful?
118
119
  [geom.top, geom.left, geom.bottom, geom.right],
119
120
  None,
121
+ geom.angle if isinstance(geom, OrientedBBox) else None,
120
122
  )
121
123
 
122
124
  if not init:
123
125
  self.initialize(imgs[0], target)
124
126
  init = True
125
127
 
126
- geometry = self.predict(
127
- rgb_image=imgs[-1],
128
- prev_rgb_image=imgs[0],
129
- target_bbox=target,
130
- settings=self.custom_inference_settings_dict,
131
- )
128
+ if isinstance(geom, OrientedBBox):
129
+ geometry = self.predict_oriented(
130
+ rgb_image=imgs[-1],
131
+ prev_rgb_image=imgs[0],
132
+ target_bbox=target,
133
+ settings=self.custom_inference_settings_dict,
134
+ )
135
+ else:
136
+ geometry = self.predict(
137
+ rgb_image=imgs[-1],
138
+ prev_rgb_image=imgs[0],
139
+ target_bbox=target,
140
+ settings=self.custom_inference_settings_dict,
141
+ )
132
142
  sly_geometry = self._to_sly_geometry(geometry)
133
143
 
134
144
  uploader.put([(sly_geometry, obj_id, video_interface._cur_frames_indexes[-1])])
@@ -159,7 +169,7 @@ class BBoxTracking(BaseTracking):
159
169
  api.logger.info(
160
170
  "Finished tracking.", extra={"inference_request_uuid": inference_request.uuid}
161
171
  )
162
- video_interface._notify(True, task="Finished tracking")
172
+ video_interface._notify(True, task="Finished tracking")
163
173
 
164
174
  def _track_api(self, api: Api, context: dict, inference_request: InferenceRequest):
165
175
  track_t = time.monotonic()
@@ -218,8 +228,7 @@ class BBoxTracking(BaseTracking):
218
228
  },
219
229
  )
220
230
  for box_i, input_geom in enumerate(input_bboxes, 1):
221
- input_bbox = input_geom["data"]
222
- bbox = Rectangle.from_json(input_bbox)
231
+ bbox = self._deserialize_geometry(input_geom)
223
232
  predictions_for_object = []
224
233
  init = False
225
234
  frame_t = time.monotonic()
@@ -229,18 +238,27 @@ class BBoxTracking(BaseTracking):
229
238
  "", # TODO: can this be useful?
230
239
  [bbox.top, bbox.left, bbox.bottom, bbox.right],
231
240
  None,
241
+ bbox.angle if isinstance(bbox, OrientedBBox) else None,
232
242
  )
233
243
 
234
244
  if not init:
235
245
  self.initialize(imgs[0], target)
236
246
  init = True
237
247
 
238
- geometry = self.predict(
239
- rgb_image=imgs[-1],
240
- prev_rgb_image=imgs[0],
241
- target_bbox=target,
242
- settings=self.custom_inference_settings_dict,
243
- )
248
+ if isinstance(bbox, OrientedBBox):
249
+ geometry = self.predict_oriented(
250
+ rgb_image=imgs[-1],
251
+ prev_rgb_image=imgs[0],
252
+ target_bbox=target,
253
+ settings=self.custom_inference_settings_dict,
254
+ )
255
+ else:
256
+ geometry = self.predict(
257
+ rgb_image=imgs[-1],
258
+ prev_rgb_image=imgs[0],
259
+ target_bbox=target,
260
+ settings=self.custom_inference_settings_dict,
261
+ )
244
262
  sly_geometry = self._to_sly_geometry(geometry)
245
263
 
246
264
  predictions_for_object.append(
@@ -293,24 +311,33 @@ class BBoxTracking(BaseTracking):
293
311
  }
294
312
  results = [[] for _ in range(len(frames) - 1)]
295
313
  for geometry in geometries:
296
- if not isinstance(geometry, Rectangle):
314
+ if not isinstance(geometry, (Rectangle, OrientedBBox)):
297
315
  raise TypeError(f"Tracking does not work with {geometry.geometry_name()}.")
298
316
  target = PredictionBBox(
299
317
  "",
300
318
  [geometry.top, geometry.left, geometry.bottom, geometry.right],
301
319
  None,
320
+ angle=geometry.angle if isinstance(geometry, OrientedBBox) else None,
302
321
  )
303
322
  self.initialize(frames[0], target)
304
323
  for i in range(len(frames) - 1):
305
- pred_geometry = self.predict(
306
- rgb_image=frames[i + 1],
307
- prev_rgb_image=frames[i],
308
- target_bbox=target,
309
- settings=updated_settings,
310
- )
324
+ if isinstance(geometry, OrientedBBox):
325
+ pred_geometry = self.predict_oriented(
326
+ rgb_image=frames[i + 1],
327
+ prev_rgb_image=frames[i],
328
+ target_bbox=target,
329
+ settings=updated_settings,
330
+ )
331
+ else:
332
+ pred_geometry = self.predict(
333
+ rgb_image=frames[i + 1],
334
+ prev_rgb_image=frames[i],
335
+ target_bbox=target,
336
+ settings=updated_settings,
337
+ )
311
338
  sly_pred_geometry = self._to_sly_geometry(pred_geometry)
312
339
  results[i].append(
313
- {"type": Rectangle.geometry_name(), "data": sly_pred_geometry.to_json()}
340
+ {"type": sly_pred_geometry.geometry_name(), "data": sly_pred_geometry.to_json()}
314
341
  )
315
342
  return results
316
343
 
@@ -359,7 +386,7 @@ class BBoxTracking(BaseTracking):
359
386
  uploader.raise_from_notify
360
387
  for fig_i, figure in enumerate(figures, 1):
361
388
  figure = api.video.figure._convert_json_info(figure)
362
- if not figure.geometry_type == Rectangle.geometry_name():
389
+ if not figure.geometry_type in (Rectangle.geometry_name(), OrientedBBox.geometry_name()):
363
390
  raise TypeError(f"Tracking does not work with {figure.geometry_type}.")
364
391
  api.logger.info("figure:", extra={"figure": figure._asdict()})
365
392
  sly_geometry: Rectangle = deserialize_geometry(
@@ -378,6 +405,7 @@ class BBoxTracking(BaseTracking):
378
405
  sly_geometry.right,
379
406
  ],
380
407
  None,
408
+ sly_geometry.angle if isinstance(sly_geometry, OrientedBBox) else None,
381
409
  )
382
410
 
383
411
  if not init:
@@ -386,12 +414,20 @@ class BBoxTracking(BaseTracking):
386
414
 
387
415
  logger.debug("Start prediction")
388
416
  t = time.time()
389
- geometry = self.predict(
390
- rgb_image=next_frame.image,
391
- prev_rgb_image=frame.image,
392
- target_bbox=target,
393
- settings=self.custom_inference_settings_dict,
394
- )
417
+ if target.angle is not None:
418
+ geometry = self.predict_oriented(
419
+ rgb_image=next_frame.image,
420
+ prev_rgb_image=frame.image,
421
+ target_bbox=target,
422
+ settings=self.custom_inference_settings_dict,
423
+ )
424
+ else:
425
+ geometry = self.predict(
426
+ rgb_image=next_frame.image,
427
+ prev_rgb_image=frame.image,
428
+ target_bbox=target,
429
+ settings=self.custom_inference_settings_dict,
430
+ )
395
431
  logger.debug("Prediction done. Time: %f sec", time.time() - t)
396
432
  sly_geometry = self._to_sly_geometry(geometry)
397
433
 
@@ -536,6 +572,46 @@ class BBoxTracking(BaseTracking):
536
572
  :rtype: PredictionBBox
537
573
  """
538
574
  raise NotImplementedError
575
+
576
+
577
+ def _get_circumscribed_box(self, tlbr, angle):
578
+ top, left, bottom, right = tlbr
579
+ cx = (left + right) / 2
580
+ cy = (top + bottom) / 2
581
+ half_w = (right - left) / 2
582
+ half_h = (bottom - top) / 2
583
+ cos_a = np.cos(angle)
584
+ sin_a = np.sin(angle)
585
+ dx = abs(half_w * cos_a) + abs(half_h * sin_a)
586
+ dy = abs(half_w * sin_a) + abs(half_h * cos_a)
587
+
588
+ return [cy - dy, cx - dx, cy + dy, cx + dx]
589
+
590
+ def _inscribe_oriented_box(self, tracked_box, angle):
591
+ top, left, bottom, right = tracked_box
592
+ cx = (left + right) / 2
593
+ cy = (top + bottom) / 2
594
+ dx = (right - left) / 2
595
+ dy = (bottom - top) / 2
596
+ cos_a = abs(np.cos(angle))
597
+ sin_a = abs(np.sin(angle))
598
+ det = cos_a * cos_a - sin_a * sin_a # = cos(2*angle)
599
+ if abs(det) < 1e-10: # angle ≈ 45° or 135°
600
+ half_w = half_h = min(dx, dy) / (cos_a + sin_a)
601
+ else:
602
+ half_w = abs(dx * cos_a - dy * sin_a) / abs(det)
603
+ half_h = abs(dy * cos_a - dx * sin_a) / abs(det)
604
+
605
+ return [cy - half_h, cx - half_w, cy + half_h, cx + half_w]
606
+
607
+ def predict_oriented(self, rgb_image: np.ndarray, settings: Dict[str, Any], prev_rgb_image: np.ndarray, target_bbox: PredictionBBox) -> PredictionBBox:
608
+ if not target_bbox.angle:
609
+ predicted = self.predict(rgb_image, settings, prev_rgb_image, target_bbox)
610
+ return PredictionBBox(predicted.class_name, predicted.bbox_tlbr, predicted.score, 0)
611
+ circumscribed_bbox = self._get_circumscribed_box(target_bbox.bbox_tlbr, target_bbox.angle)
612
+ circumscribed_target = self.predict(rgb_image, settings, prev_rgb_image, PredictionBBox(target_bbox.class_name, circumscribed_bbox, target_bbox.score))
613
+ inscribed_bbox = self._inscribe_oriented_box(circumscribed_target.bbox_tlbr, target_bbox.angle)
614
+ return PredictionBBox(target_bbox.class_name, inscribed_bbox, circumscribed_target.score, target_bbox.angle)
539
615
 
540
616
  def visualize(
541
617
  self,
@@ -560,12 +636,15 @@ class BBoxTracking(BaseTracking):
560
636
 
561
637
  def _to_sly_geometry(self, dto: PredictionBBox) -> Rectangle:
562
638
  top, left, bottom, right = dto.bbox_tlbr
563
- geometry = Rectangle(top=top, left=left, bottom=bottom, right=right)
639
+ if dto.angle is not None:
640
+ geometry = OrientedBBox(top=top, left=left, bottom=bottom, right=right, angle=dto.angle)
641
+ else:
642
+ geometry = Rectangle(top=top, left=left, bottom=bottom, right=right)
564
643
  return geometry
565
644
 
566
645
  def _create_label(self, dto: PredictionBBox) -> Rectangle:
567
646
  geometry = self._to_sly_geometry(dto)
568
- return Label(geometry, ObjClass("", Rectangle))
647
+ return Label(geometry, ObjClass("", type(geometry)))
569
648
 
570
649
  def _get_obj_class_shape(self):
571
650
  return Rectangle
@@ -588,8 +667,12 @@ class BBoxTracking(BaseTracking):
588
667
  # for example empty mask
589
668
  continue
590
669
  if isinstance(label, list):
670
+ for lb in label:
671
+ lb.status = LabelingStatus.AUTO
591
672
  labels.extend(label)
592
673
  continue
674
+
675
+ label.status = LabelingStatus.AUTO
593
676
  labels.append(label)
594
677
 
595
678
  # create annotation with correct image resolution
@@ -10,7 +10,7 @@ from pydantic import ValidationError
10
10
 
11
11
  import supervisely.nn.inference.tracking.functional as F
12
12
  from supervisely.annotation.annotation import Annotation
13
- from supervisely.annotation.label import Geometry, Label
13
+ from supervisely.annotation.label import Geometry, Label, LabelingStatus
14
14
  from supervisely.annotation.obj_class import ObjClass
15
15
  from supervisely.api.api import Api
16
16
  from supervisely.api.module_api import ApiField
@@ -610,8 +610,12 @@ class PointTracking(BaseTracking):
610
610
  # for example empty mask
611
611
  continue
612
612
  if isinstance(label, list):
613
+ for lb in label:
614
+ lb.status = LabelingStatus.AUTO
613
615
  labels.extend(label)
614
616
  continue
617
+
618
+ label.status = LabelingStatus.AUTO
615
619
  labels.append(label)
616
620
 
617
621
  # create annotation with correct image resolution
@@ -10,11 +10,13 @@ from typing import OrderedDict as OrderedDictType
10
10
  import numpy as np
11
11
 
12
12
  from supervisely._utils import find_value_by_keys
13
+ from supervisely.annotation.label import LabelingStatus
13
14
  from supervisely.api.api import Api
14
15
  from supervisely.api.module_api import ApiField
15
16
  from supervisely.geometry.geometry import Geometry
16
17
  from supervisely.geometry.graph import GraphNodes
17
18
  from supervisely.geometry.helpers import deserialize_geometry
19
+ from supervisely.geometry.oriented_bbox import OrientedBBox
18
20
  from supervisely.geometry.point import Point
19
21
  from supervisely.geometry.polygon import Polygon
20
22
  from supervisely.geometry.polyline import Polyline
@@ -82,7 +84,7 @@ class TrackerInterface:
82
84
  @property
83
85
  def video_info(self):
84
86
  if self._video_info is None:
85
- self._video_info = self.api.video.get_info_by_id(self.video_id)
87
+ self._video_info = self.api.video.get_info_by_id(self.video_id, raise_error=True)
86
88
  return self._video_info
87
89
 
88
90
  def add_object_geometries(self, geometries: List[Geometry], object_id: int, start_fig: int):
@@ -131,6 +133,10 @@ class TrackerInterface:
131
133
  h = self.video_info.frame_height
132
134
  w = self.video_info.frame_width
133
135
  rect = Rectangle.from_size((h, w))
136
+ if isinstance(geometry, OrientedBBox):
137
+ if rect.contains_point_location(geometry.center):
138
+ return geometry
139
+ return None
134
140
  cropped = geometry.crop(rect)
135
141
  if len(cropped) == 0:
136
142
  return None
@@ -166,6 +172,8 @@ class TrackerInterface:
166
172
  ApiField.GEOMETRY: geometry.to_json(),
167
173
  ApiField.META: {ApiField.FRAME: frame_index},
168
174
  ApiField.TRACK_ID: self.track_id,
175
+ ApiField.NN_CREATED: True,
176
+ ApiField.NN_UPDATED: True,
169
177
  }
170
178
  for geometry, frame_index in geometries_frame_indexes
171
179
  ]
@@ -195,6 +203,7 @@ class TrackerInterface:
195
203
  geometry.to_json(),
196
204
  geometry.geometry_name(),
197
205
  self.track_id,
206
+ status=LabelingStatus.AUTO,
198
207
  )
199
208
  self.logger.debug(f"Added {geometry.geometry_name()} to frame #{frame_ind}")
200
209
  if notify:
@@ -1,12 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import queue
3
4
  import threading
5
+ from concurrent.futures import Future, ThreadPoolExecutor, wait
4
6
  from logging import Logger
5
- from queue import Empty, Queue
6
- from types import TracebackType
7
- from typing import Callable, Optional, Type
7
+ from typing import Callable, List
8
8
 
9
- from supervisely.sly_logger import logger
9
+ from supervisely.sly_logger import logger as sly_logger
10
10
 
11
11
 
12
12
  class Uploader:
@@ -25,7 +25,7 @@ class Uploader:
25
25
  self._logger = logger
26
26
  self.exception = None
27
27
  self._lock = threading.Lock()
28
- self._q = Queue()
28
+ self._q = queue.Queue()
29
29
  self._stop_event = threading.Event()
30
30
  self._exception_event = threading.Event()
31
31
  self._upload_thread = threading.Thread(
@@ -34,7 +34,7 @@ class Uploader:
34
34
  )
35
35
  self.raise_from_notify = False
36
36
  self._notify_thread = None
37
- self._notify_q = Queue()
37
+ self._notify_q = queue.Queue()
38
38
  if self._notify_f is not None:
39
39
  self._notify_thread = threading.Thread(target=self._notify_loop, daemon=True)
40
40
  self._notify_thread.start()
@@ -55,14 +55,14 @@ class Uploader:
55
55
  while True:
56
56
  try:
57
57
  items.append(self._notify_q.get_nowait())
58
- except Empty:
58
+ except queue.Empty:
59
59
  break
60
60
  if items:
61
61
  self._notify_f(items)
62
62
 
63
63
  for _ in range(len(items)):
64
64
  self._notify_q.task_done()
65
- except Empty:
65
+ except queue.Empty:
66
66
  continue
67
67
  except StopIteration:
68
68
  self.stop()
@@ -91,7 +91,7 @@ class Uploader:
91
91
  while True:
92
92
  try:
93
93
  items.append(self._q.get_nowait())
94
- except Empty:
94
+ except queue.Empty:
95
95
  break
96
96
  if items:
97
97
  self._upload_f(items)
@@ -99,7 +99,7 @@ class Uploader:
99
99
 
100
100
  for _ in range(len(items)):
101
101
  self._q.task_done()
102
- except Empty:
102
+ except queue.Empty:
103
103
  continue
104
104
  except StopIteration:
105
105
  self.stop()
@@ -143,7 +143,7 @@ class Uploader:
143
143
  self,
144
144
  exception: Exception,
145
145
  ):
146
- raise exception
146
+ return False # propagate
147
147
 
148
148
  def __exit__(self, exc_type, exc_val, exc_tb):
149
149
  self.stop()
@@ -152,7 +152,7 @@ class Uploader:
152
152
  if self._upload_thread.is_alive():
153
153
  raise TimeoutError("Uploader thread didn't finish in time")
154
154
  except TimeoutError:
155
- _logger = logger
155
+ _logger = sly_logger
156
156
  if self._logger is not None:
157
157
  _logger = self._logger
158
158
  _logger.warning("Uploader thread didn't finish in time")
@@ -166,3 +166,130 @@ class Uploader:
166
166
  except Exception as exc:
167
167
  return self._exception_handler(exc)
168
168
  return False
169
+
170
+
171
+ class Downloader:
172
+
173
+ def __init__(
174
+ self,
175
+ download_f: Callable,
176
+ max_workers: int = 8,
177
+ buffer_size: int = 100,
178
+ exception_handler: Callable = None,
179
+ logger: Logger = None,
180
+ ):
181
+ self._download_f = download_f
182
+ self._max_workers = max_workers
183
+ self._logger = logger
184
+ self._exception_handler = exception_handler
185
+ if self._exception_handler is None:
186
+ self._exception_handler = self._default_exception_handler
187
+ self._input_q = queue.Queue()
188
+ self._buffer_q = queue.Queue(buffer_size)
189
+ self._output_q = queue.Queue()
190
+ self._executor: ThreadPoolExecutor = None
191
+ self._download_futures: List[Future] = None
192
+ self._stop_event = threading.Event()
193
+
194
+ def _download_loop(self):
195
+ while not self._stop_event.is_set():
196
+ try:
197
+ item = self._buffer_q.get(timeout=0.2)
198
+ try:
199
+ output = self._download_f(item)
200
+ self._output_q.put(output)
201
+ finally:
202
+ self._buffer_q.task_done()
203
+ except queue.Empty:
204
+ continue
205
+ except Exception as e:
206
+ logger = self._logger or sly_logger
207
+ logger.debug("Error in downloader thread", exc_info=True)
208
+
209
+ def start(self):
210
+ if self.is_alive():
211
+ raise RuntimeError("Downloader already started")
212
+ self._executor = ThreadPoolExecutor(max_workers=self._max_workers)
213
+ self._download_futures = []
214
+ for _ in range(self._max_workers):
215
+ self._download_futures.append(self._executor.submit(self._download_loop))
216
+
217
+ def put(self, item):
218
+ self._input_q.put(item)
219
+
220
+ def get(self, wait=True, timeout: float = None):
221
+ return self._output_q.get(block=wait, timeout=timeout)
222
+
223
+ def _move_input_to_buffer(self):
224
+ try:
225
+ item = self._input_q.get_nowait()
226
+ except queue.Empty:
227
+ return
228
+ for _ in range(10):
229
+ try:
230
+ self._buffer_q.put_nowait(item)
231
+ return
232
+ except queue.Full:
233
+ pass
234
+ try:
235
+ self._buffer_q.get_nowait()
236
+ except queue.Empty:
237
+ pass
238
+ try:
239
+ self._buffer_q.put_nowait(item)
240
+ return
241
+ except:
242
+ raise RuntimeError("Unable to move item from input to buffer")
243
+
244
+ def next(self, n: int = 1, raise_on_error=False):
245
+ for _ in range(n):
246
+ try:
247
+ self._move_input_to_buffer()
248
+ except Exception:
249
+ if raise_on_error:
250
+ raise
251
+ logger = sly_logger
252
+ if self._logger is not None:
253
+ logger = self._logger
254
+ logger.debug("Error moving buffer", exc_info=True)
255
+ continue
256
+
257
+ def is_alive(self):
258
+ return self._executor is not None and any(not f.done() for f in self._download_futures)
259
+
260
+ def stop(self):
261
+ self._stop_event.set()
262
+ for future in self._download_futures:
263
+ future.cancel()
264
+ self._executor.shutdown(wait=False)
265
+
266
+ def join(self, timeout=None):
267
+ _, not_done = wait(self._download_futures, timeout=timeout)
268
+ if not_done:
269
+ raise TimeoutError("Timeout waiting for downloads to complete")
270
+
271
+ def __enter__(self):
272
+ self.start()
273
+ return self
274
+
275
+ def _default_exception_handler(
276
+ self,
277
+ exception: Exception,
278
+ ):
279
+ return False # propagate
280
+
281
+ def __exit__(self, exc_type, exc_val, exc_tb):
282
+ self.stop()
283
+ try:
284
+ self.join(timeout=30)
285
+ if self.is_alive():
286
+ raise TimeoutError("Downloader threads didn't finish in time")
287
+ except TimeoutError:
288
+ _logger = sly_logger
289
+ if self._logger is not None:
290
+ _logger = self._logger
291
+ _logger.warning("Downloader threads didn't finish in time")
292
+ if exc_type is not None:
293
+ exc = exc_val.with_traceback(exc_tb)
294
+ return self._exception_handler(exc)
295
+ return False
@@ -0,0 +1,7 @@
1
+ from .loss_plateau_detector import LossPlateauDetector
2
+ from .request_queue import RequestQueue, RequestType
3
+ from .artifacts_utils import upload_artifacts
4
+ from .live_training import LiveTraining
5
+ from .incremental_dataset import IncrementalDataset
6
+ from .dynamic_sampler import DynamicSampler
7
+ from .checkpoint_utils import resolve_checkpoint
@@ -0,0 +1,111 @@
1
+ from fastapi import FastAPI, HTTPException, Request, Response
2
+ import uvicorn
3
+ import threading
4
+ import asyncio
5
+
6
+ from .request_queue import RequestQueue, RequestType
7
+ import supervisely as sly
8
+ from supervisely import logger
9
+
10
+
11
+ def start_api_server(
12
+ app: sly.Application,
13
+ request_queue: RequestQueue,
14
+ host: str = "0.0.0.0",
15
+ port: int = 8000
16
+ ) -> threading.Thread:
17
+ """Start FastAPI server in a daemon thread."""
18
+ server = app.get_server()
19
+ create_api(server, request_queue)
20
+
21
+ config = uvicorn.Config(app, host=host, port=port, log_level="info")
22
+ server = uvicorn.Server(config)
23
+
24
+ thread = threading.Thread(target=server.run, daemon=True, name="APIServer")
25
+ thread.start()
26
+
27
+ logger.debug(f"Live Training API server started: http://{host}:{port}")
28
+
29
+ return thread
30
+
31
+
32
+ def create_api(app: FastAPI, request_queue: RequestQueue) -> FastAPI:
33
+
34
+ @app.post("/start")
35
+ async def start(response: Response):
36
+ """Start the live training process."""
37
+ future = request_queue.put(RequestType.START)
38
+ result = await _wait_for_result(future, response, timeout=None)
39
+ return result
40
+
41
+ @app.post("/predict")
42
+ async def predict(request: Request, response: Response):
43
+ """Run inference on an image."""
44
+ sly_api = _api_from_request(request)
45
+ state = request.state.state
46
+ img_np = sly_api.image.download_np(state['image_id'])
47
+ future = request_queue.put(
48
+ RequestType.PREDICT,
49
+ {'image': img_np, 'image_id': state['image_id']}
50
+ )
51
+ result = await _wait_for_result(future, response)
52
+ return result
53
+
54
+ @app.post("/add-sample")
55
+ async def add_sample(request: Request, response: Response):
56
+ """Add a new training sample."""
57
+ sly_api = _api_from_request(request)
58
+ state = request.state.state
59
+ img_np = sly_api.image.download_np(state['image_id'])
60
+ ann_json = sly_api.annotation.download_json(state['image_id'])
61
+ img_info = sly_api.image.get_info_by_id(state['image_id'])
62
+ future = request_queue.put(
63
+ RequestType.ADD_SAMPLE,
64
+ {
65
+ 'image': img_np,
66
+ 'annotation': ann_json,
67
+ 'image_id': state['image_id'],
68
+ 'image_name': img_info.name
69
+ }
70
+ )
71
+ result = await _wait_for_result(future, response)
72
+ return result
73
+
74
+ @app.post("/status")
75
+ async def status(response: Response):
76
+ """Check the status of the training process."""
77
+ future = request_queue.put(RequestType.STATUS)
78
+ result = await _wait_for_result(future, response)
79
+ return result
80
+
81
+ return app
82
+
83
+
84
+ async def _wait_for_result(future: asyncio.Future, response: Response, timeout: float = 600.0):
85
+ """Wait for the future to complete with a timeout."""
86
+ try:
87
+ result = await asyncio.wait_for(future, timeout=timeout)
88
+ except asyncio.TimeoutError:
89
+ # raise HTTPException(503, detail={"error": "Request timeout - training may be busy"})
90
+ response.status_code = 503
91
+ result = _error_response_message("Request timeout - training may be busy")
92
+ except Exception as e:
93
+ # raise HTTPException(500, detail={"error": str(e)})
94
+ response.status_code = 500
95
+ result = _error_response_message(str(e))
96
+ return result
97
+
98
+
99
+ def _api_from_request(request: Request) -> sly.Api:
100
+ api = None
101
+ try:
102
+ api = request.state.api
103
+ finally:
104
+ if not isinstance(api, sly.Api):
105
+ logger.warning("sly.Api instance not found in request.state.api. Creating API from app's credentials.")
106
+ api = sly.Api()
107
+ return api
108
+
109
+
110
+ def _error_response_message(message: str):
111
+ return {"error": {"details": {"message": message}}}