supervisely 6.73.452__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 (189) hide show
  1. supervisely/__init__.py +25 -1
  2. supervisely/annotation/annotation.py +8 -2
  3. supervisely/annotation/json_geometries_map.py +13 -12
  4. supervisely/api/annotation_api.py +6 -3
  5. supervisely/api/api.py +2 -0
  6. supervisely/api/app_api.py +10 -1
  7. supervisely/api/dataset_api.py +74 -12
  8. supervisely/api/entities_collection_api.py +10 -0
  9. supervisely/api/entity_annotation/figure_api.py +28 -0
  10. supervisely/api/entity_annotation/object_api.py +3 -3
  11. supervisely/api/entity_annotation/tag_api.py +63 -12
  12. supervisely/api/guides_api.py +210 -0
  13. supervisely/api/image_api.py +4 -0
  14. supervisely/api/labeling_job_api.py +83 -1
  15. supervisely/api/labeling_queue_api.py +33 -7
  16. supervisely/api/module_api.py +5 -0
  17. supervisely/api/project_api.py +71 -26
  18. supervisely/api/storage_api.py +3 -1
  19. supervisely/api/task_api.py +13 -2
  20. supervisely/api/team_api.py +4 -3
  21. supervisely/api/video/video_annotation_api.py +119 -3
  22. supervisely/api/video/video_api.py +65 -14
  23. supervisely/app/__init__.py +1 -1
  24. supervisely/app/content.py +23 -7
  25. supervisely/app/development/development.py +18 -2
  26. supervisely/app/fastapi/__init__.py +1 -0
  27. supervisely/app/fastapi/custom_static_files.py +1 -1
  28. supervisely/app/fastapi/multi_user.py +105 -0
  29. supervisely/app/fastapi/subapp.py +88 -42
  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 +6 -0
  35. supervisely/app/widgets/activity_feed/__init__.py +0 -0
  36. supervisely/app/widgets/activity_feed/activity_feed.py +239 -0
  37. supervisely/app/widgets/activity_feed/style.css +78 -0
  38. supervisely/app/widgets/activity_feed/template.html +22 -0
  39. supervisely/app/widgets/card/card.py +20 -0
  40. supervisely/app/widgets/classes_list_selector/classes_list_selector.py +121 -9
  41. supervisely/app/widgets/classes_list_selector/template.html +60 -93
  42. supervisely/app/widgets/classes_mapping/classes_mapping.py +13 -12
  43. supervisely/app/widgets/classes_table/classes_table.py +1 -0
  44. supervisely/app/widgets/deploy_model/deploy_model.py +56 -35
  45. supervisely/app/widgets/ecosystem_model_selector/ecosystem_model_selector.py +1 -1
  46. supervisely/app/widgets/experiment_selector/experiment_selector.py +8 -0
  47. supervisely/app/widgets/fast_table/fast_table.py +184 -60
  48. supervisely/app/widgets/fast_table/template.html +1 -1
  49. supervisely/app/widgets/heatmap/__init__.py +0 -0
  50. supervisely/app/widgets/heatmap/heatmap.py +564 -0
  51. supervisely/app/widgets/heatmap/script.js +533 -0
  52. supervisely/app/widgets/heatmap/style.css +233 -0
  53. supervisely/app/widgets/heatmap/template.html +21 -0
  54. supervisely/app/widgets/modal/__init__.py +0 -0
  55. supervisely/app/widgets/modal/modal.py +198 -0
  56. supervisely/app/widgets/modal/template.html +10 -0
  57. supervisely/app/widgets/object_class_view/object_class_view.py +3 -0
  58. supervisely/app/widgets/radio_tabs/radio_tabs.py +18 -2
  59. supervisely/app/widgets/radio_tabs/template.html +1 -0
  60. supervisely/app/widgets/select/select.py +6 -3
  61. supervisely/app/widgets/select_class/__init__.py +0 -0
  62. supervisely/app/widgets/select_class/select_class.py +363 -0
  63. supervisely/app/widgets/select_class/template.html +50 -0
  64. supervisely/app/widgets/select_cuda/select_cuda.py +22 -0
  65. supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +65 -7
  66. supervisely/app/widgets/select_tag/__init__.py +0 -0
  67. supervisely/app/widgets/select_tag/select_tag.py +352 -0
  68. supervisely/app/widgets/select_tag/template.html +64 -0
  69. supervisely/app/widgets/select_team/select_team.py +37 -4
  70. supervisely/app/widgets/select_team/template.html +4 -5
  71. supervisely/app/widgets/select_user/__init__.py +0 -0
  72. supervisely/app/widgets/select_user/select_user.py +270 -0
  73. supervisely/app/widgets/select_user/template.html +13 -0
  74. supervisely/app/widgets/select_workspace/select_workspace.py +59 -10
  75. supervisely/app/widgets/select_workspace/template.html +9 -12
  76. supervisely/app/widgets/table/table.py +68 -13
  77. supervisely/app/widgets/tree_select/tree_select.py +2 -0
  78. supervisely/aug/aug.py +6 -2
  79. supervisely/convert/base_converter.py +1 -0
  80. supervisely/convert/converter.py +2 -2
  81. supervisely/convert/image/image_converter.py +3 -1
  82. supervisely/convert/image/image_helper.py +48 -4
  83. supervisely/convert/image/label_studio/label_studio_converter.py +2 -0
  84. supervisely/convert/image/medical2d/medical2d_helper.py +2 -24
  85. supervisely/convert/image/multispectral/multispectral_converter.py +6 -0
  86. supervisely/convert/image/pascal_voc/pascal_voc_converter.py +8 -5
  87. supervisely/convert/image/pascal_voc/pascal_voc_helper.py +7 -0
  88. supervisely/convert/pointcloud/kitti_3d/kitti_3d_converter.py +33 -3
  89. supervisely/convert/pointcloud/kitti_3d/kitti_3d_helper.py +12 -5
  90. supervisely/convert/pointcloud/las/las_converter.py +13 -1
  91. supervisely/convert/pointcloud/las/las_helper.py +110 -11
  92. supervisely/convert/pointcloud/nuscenes_conv/nuscenes_converter.py +27 -16
  93. supervisely/convert/pointcloud/pointcloud_converter.py +91 -3
  94. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_converter.py +58 -22
  95. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_helper.py +21 -47
  96. supervisely/convert/video/__init__.py +1 -0
  97. supervisely/convert/video/multi_view/__init__.py +0 -0
  98. supervisely/convert/video/multi_view/multi_view.py +543 -0
  99. supervisely/convert/video/sly/sly_video_converter.py +359 -3
  100. supervisely/convert/video/video_converter.py +22 -2
  101. supervisely/convert/volume/dicom/dicom_converter.py +13 -5
  102. supervisely/convert/volume/dicom/dicom_helper.py +30 -18
  103. supervisely/geometry/constants.py +1 -0
  104. supervisely/geometry/geometry.py +4 -0
  105. supervisely/geometry/helpers.py +5 -1
  106. supervisely/geometry/oriented_bbox.py +676 -0
  107. supervisely/geometry/rectangle.py +2 -1
  108. supervisely/io/env.py +76 -1
  109. supervisely/io/fs.py +21 -0
  110. supervisely/nn/benchmark/base_evaluator.py +104 -11
  111. supervisely/nn/benchmark/instance_segmentation/evaluator.py +1 -8
  112. supervisely/nn/benchmark/object_detection/evaluator.py +20 -4
  113. supervisely/nn/benchmark/object_detection/vis_metrics/pr_curve.py +10 -5
  114. supervisely/nn/benchmark/semantic_segmentation/evaluator.py +34 -16
  115. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/confusion_matrix.py +1 -1
  116. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/frequently_confused.py +1 -1
  117. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/overview.py +1 -1
  118. supervisely/nn/benchmark/visualization/evaluation_result.py +66 -4
  119. supervisely/nn/inference/cache.py +43 -18
  120. supervisely/nn/inference/gui/serving_gui_template.py +5 -2
  121. supervisely/nn/inference/inference.py +795 -199
  122. supervisely/nn/inference/inference_request.py +42 -9
  123. supervisely/nn/inference/predict_app/gui/classes_selector.py +83 -12
  124. supervisely/nn/inference/predict_app/gui/gui.py +676 -488
  125. supervisely/nn/inference/predict_app/gui/input_selector.py +205 -26
  126. supervisely/nn/inference/predict_app/gui/model_selector.py +2 -4
  127. supervisely/nn/inference/predict_app/gui/output_selector.py +46 -6
  128. supervisely/nn/inference/predict_app/gui/settings_selector.py +756 -59
  129. supervisely/nn/inference/predict_app/gui/tags_selector.py +1 -1
  130. supervisely/nn/inference/predict_app/gui/utils.py +236 -119
  131. supervisely/nn/inference/predict_app/predict_app.py +2 -2
  132. supervisely/nn/inference/session.py +43 -35
  133. supervisely/nn/inference/tracking/bbox_tracking.py +113 -34
  134. supervisely/nn/inference/tracking/tracker_interface.py +7 -2
  135. supervisely/nn/inference/uploader.py +139 -12
  136. supervisely/nn/live_training/__init__.py +7 -0
  137. supervisely/nn/live_training/api_server.py +111 -0
  138. supervisely/nn/live_training/artifacts_utils.py +243 -0
  139. supervisely/nn/live_training/checkpoint_utils.py +229 -0
  140. supervisely/nn/live_training/dynamic_sampler.py +44 -0
  141. supervisely/nn/live_training/helpers.py +14 -0
  142. supervisely/nn/live_training/incremental_dataset.py +146 -0
  143. supervisely/nn/live_training/live_training.py +497 -0
  144. supervisely/nn/live_training/loss_plateau_detector.py +111 -0
  145. supervisely/nn/live_training/request_queue.py +52 -0
  146. supervisely/nn/model/model_api.py +9 -0
  147. supervisely/nn/prediction_dto.py +12 -1
  148. supervisely/nn/tracker/base_tracker.py +11 -1
  149. supervisely/nn/tracker/botsort/botsort_config.yaml +0 -1
  150. supervisely/nn/tracker/botsort/tracker/mc_bot_sort.py +7 -4
  151. supervisely/nn/tracker/botsort_tracker.py +94 -65
  152. supervisely/nn/tracker/visualize.py +87 -90
  153. supervisely/nn/training/gui/classes_selector.py +16 -1
  154. supervisely/nn/training/train_app.py +28 -29
  155. supervisely/project/data_version.py +115 -51
  156. supervisely/project/download.py +1 -1
  157. supervisely/project/pointcloud_episode_project.py +37 -8
  158. supervisely/project/pointcloud_project.py +30 -2
  159. supervisely/project/project.py +14 -2
  160. supervisely/project/project_meta.py +27 -1
  161. supervisely/project/project_settings.py +32 -18
  162. supervisely/project/versioning/__init__.py +1 -0
  163. supervisely/project/versioning/common.py +20 -0
  164. supervisely/project/versioning/schema_fields.py +35 -0
  165. supervisely/project/versioning/video_schema.py +221 -0
  166. supervisely/project/versioning/volume_schema.py +87 -0
  167. supervisely/project/video_project.py +717 -15
  168. supervisely/project/volume_project.py +623 -5
  169. supervisely/template/experiment/experiment.html.jinja +4 -4
  170. supervisely/template/experiment/experiment_generator.py +14 -21
  171. supervisely/template/live_training/__init__.py +0 -0
  172. supervisely/template/live_training/header.html.jinja +96 -0
  173. supervisely/template/live_training/live_training.html.jinja +51 -0
  174. supervisely/template/live_training/live_training_generator.py +464 -0
  175. supervisely/template/live_training/sly-style.css +402 -0
  176. supervisely/template/live_training/template.html.jinja +18 -0
  177. supervisely/versions.json +28 -26
  178. supervisely/video/sampling.py +39 -20
  179. supervisely/video/video.py +40 -11
  180. supervisely/video_annotation/video_object.py +29 -4
  181. supervisely/volume/stl_converter.py +2 -0
  182. supervisely/worker_api/agent_rpc.py +24 -1
  183. supervisely/worker_api/rpc_servicer.py +31 -7
  184. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/METADATA +56 -39
  185. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/RECORD +189 -142
  186. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/WHEEL +1 -1
  187. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/entry_points.txt +0 -0
  188. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info/licenses}/LICENSE +0 -0
  189. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/top_level.txt +0 -0
@@ -13,6 +13,7 @@ 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
@@ -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
@@ -22,7 +24,6 @@ from supervisely.geometry.rectangle import Rectangle
22
24
  from supervisely.nn.inference.cache import InferenceImageCache
23
25
  from supervisely.sly_logger import logger
24
26
  from supervisely.video_annotation.key_id_map import KeyIdMap
25
- from supervisely.annotation.label import LabelingStatus
26
27
 
27
28
 
28
29
  class TrackerInterface:
@@ -83,7 +84,7 @@ class TrackerInterface:
83
84
  @property
84
85
  def video_info(self):
85
86
  if self._video_info is None:
86
- 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)
87
88
  return self._video_info
88
89
 
89
90
  def add_object_geometries(self, geometries: List[Geometry], object_id: int, start_fig: int):
@@ -132,6 +133,10 @@ class TrackerInterface:
132
133
  h = self.video_info.frame_height
133
134
  w = self.video_info.frame_width
134
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
135
140
  cropped = geometry.crop(rect)
136
141
  if len(cropped) == 0:
137
142
  return None
@@ -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}}}