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.
- supervisely/__init__.py +25 -1
- supervisely/annotation/annotation.py +8 -2
- supervisely/annotation/json_geometries_map.py +13 -12
- supervisely/api/annotation_api.py +6 -3
- supervisely/api/api.py +2 -0
- supervisely/api/app_api.py +10 -1
- supervisely/api/dataset_api.py +74 -12
- supervisely/api/entities_collection_api.py +10 -0
- supervisely/api/entity_annotation/figure_api.py +28 -0
- supervisely/api/entity_annotation/object_api.py +3 -3
- supervisely/api/entity_annotation/tag_api.py +63 -12
- supervisely/api/guides_api.py +210 -0
- supervisely/api/image_api.py +4 -0
- supervisely/api/labeling_job_api.py +83 -1
- supervisely/api/labeling_queue_api.py +33 -7
- supervisely/api/module_api.py +5 -0
- supervisely/api/project_api.py +71 -26
- supervisely/api/storage_api.py +3 -1
- supervisely/api/task_api.py +13 -2
- supervisely/api/team_api.py +4 -3
- supervisely/api/video/video_annotation_api.py +119 -3
- supervisely/api/video/video_api.py +65 -14
- supervisely/app/__init__.py +1 -1
- supervisely/app/content.py +23 -7
- supervisely/app/development/development.py +18 -2
- supervisely/app/fastapi/__init__.py +1 -0
- supervisely/app/fastapi/custom_static_files.py +1 -1
- supervisely/app/fastapi/multi_user.py +105 -0
- supervisely/app/fastapi/subapp.py +88 -42
- supervisely/app/fastapi/websocket.py +77 -9
- supervisely/app/singleton.py +21 -0
- supervisely/app/v1/app_service.py +18 -2
- supervisely/app/v1/constants.py +7 -1
- supervisely/app/widgets/__init__.py +6 -0
- supervisely/app/widgets/activity_feed/__init__.py +0 -0
- supervisely/app/widgets/activity_feed/activity_feed.py +239 -0
- supervisely/app/widgets/activity_feed/style.css +78 -0
- supervisely/app/widgets/activity_feed/template.html +22 -0
- supervisely/app/widgets/card/card.py +20 -0
- supervisely/app/widgets/classes_list_selector/classes_list_selector.py +121 -9
- supervisely/app/widgets/classes_list_selector/template.html +60 -93
- supervisely/app/widgets/classes_mapping/classes_mapping.py +13 -12
- supervisely/app/widgets/classes_table/classes_table.py +1 -0
- supervisely/app/widgets/deploy_model/deploy_model.py +56 -35
- supervisely/app/widgets/ecosystem_model_selector/ecosystem_model_selector.py +1 -1
- supervisely/app/widgets/experiment_selector/experiment_selector.py +8 -0
- supervisely/app/widgets/fast_table/fast_table.py +184 -60
- supervisely/app/widgets/fast_table/template.html +1 -1
- supervisely/app/widgets/heatmap/__init__.py +0 -0
- supervisely/app/widgets/heatmap/heatmap.py +564 -0
- supervisely/app/widgets/heatmap/script.js +533 -0
- supervisely/app/widgets/heatmap/style.css +233 -0
- supervisely/app/widgets/heatmap/template.html +21 -0
- supervisely/app/widgets/modal/__init__.py +0 -0
- supervisely/app/widgets/modal/modal.py +198 -0
- supervisely/app/widgets/modal/template.html +10 -0
- supervisely/app/widgets/object_class_view/object_class_view.py +3 -0
- supervisely/app/widgets/radio_tabs/radio_tabs.py +18 -2
- supervisely/app/widgets/radio_tabs/template.html +1 -0
- supervisely/app/widgets/select/select.py +6 -3
- supervisely/app/widgets/select_class/__init__.py +0 -0
- supervisely/app/widgets/select_class/select_class.py +363 -0
- supervisely/app/widgets/select_class/template.html +50 -0
- supervisely/app/widgets/select_cuda/select_cuda.py +22 -0
- supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +65 -7
- supervisely/app/widgets/select_tag/__init__.py +0 -0
- supervisely/app/widgets/select_tag/select_tag.py +352 -0
- supervisely/app/widgets/select_tag/template.html +64 -0
- supervisely/app/widgets/select_team/select_team.py +37 -4
- supervisely/app/widgets/select_team/template.html +4 -5
- supervisely/app/widgets/select_user/__init__.py +0 -0
- supervisely/app/widgets/select_user/select_user.py +270 -0
- supervisely/app/widgets/select_user/template.html +13 -0
- supervisely/app/widgets/select_workspace/select_workspace.py +59 -10
- supervisely/app/widgets/select_workspace/template.html +9 -12
- supervisely/app/widgets/table/table.py +68 -13
- supervisely/app/widgets/tree_select/tree_select.py +2 -0
- supervisely/aug/aug.py +6 -2
- supervisely/convert/base_converter.py +1 -0
- supervisely/convert/converter.py +2 -2
- supervisely/convert/image/image_converter.py +3 -1
- supervisely/convert/image/image_helper.py +48 -4
- supervisely/convert/image/label_studio/label_studio_converter.py +2 -0
- supervisely/convert/image/medical2d/medical2d_helper.py +2 -24
- supervisely/convert/image/multispectral/multispectral_converter.py +6 -0
- supervisely/convert/image/pascal_voc/pascal_voc_converter.py +8 -5
- supervisely/convert/image/pascal_voc/pascal_voc_helper.py +7 -0
- supervisely/convert/pointcloud/kitti_3d/kitti_3d_converter.py +33 -3
- supervisely/convert/pointcloud/kitti_3d/kitti_3d_helper.py +12 -5
- supervisely/convert/pointcloud/las/las_converter.py +13 -1
- supervisely/convert/pointcloud/las/las_helper.py +110 -11
- supervisely/convert/pointcloud/nuscenes_conv/nuscenes_converter.py +27 -16
- supervisely/convert/pointcloud/pointcloud_converter.py +91 -3
- supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_converter.py +58 -22
- supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_helper.py +21 -47
- supervisely/convert/video/__init__.py +1 -0
- supervisely/convert/video/multi_view/__init__.py +0 -0
- supervisely/convert/video/multi_view/multi_view.py +543 -0
- supervisely/convert/video/sly/sly_video_converter.py +359 -3
- supervisely/convert/video/video_converter.py +22 -2
- supervisely/convert/volume/dicom/dicom_converter.py +13 -5
- supervisely/convert/volume/dicom/dicom_helper.py +30 -18
- supervisely/geometry/constants.py +1 -0
- supervisely/geometry/geometry.py +4 -0
- supervisely/geometry/helpers.py +5 -1
- supervisely/geometry/oriented_bbox.py +676 -0
- supervisely/geometry/rectangle.py +2 -1
- supervisely/io/env.py +76 -1
- supervisely/io/fs.py +21 -0
- supervisely/nn/benchmark/base_evaluator.py +104 -11
- supervisely/nn/benchmark/instance_segmentation/evaluator.py +1 -8
- supervisely/nn/benchmark/object_detection/evaluator.py +20 -4
- supervisely/nn/benchmark/object_detection/vis_metrics/pr_curve.py +10 -5
- supervisely/nn/benchmark/semantic_segmentation/evaluator.py +34 -16
- supervisely/nn/benchmark/semantic_segmentation/vis_metrics/confusion_matrix.py +1 -1
- supervisely/nn/benchmark/semantic_segmentation/vis_metrics/frequently_confused.py +1 -1
- supervisely/nn/benchmark/semantic_segmentation/vis_metrics/overview.py +1 -1
- supervisely/nn/benchmark/visualization/evaluation_result.py +66 -4
- supervisely/nn/inference/cache.py +43 -18
- supervisely/nn/inference/gui/serving_gui_template.py +5 -2
- supervisely/nn/inference/inference.py +795 -199
- supervisely/nn/inference/inference_request.py +42 -9
- supervisely/nn/inference/predict_app/gui/classes_selector.py +83 -12
- supervisely/nn/inference/predict_app/gui/gui.py +676 -488
- supervisely/nn/inference/predict_app/gui/input_selector.py +205 -26
- supervisely/nn/inference/predict_app/gui/model_selector.py +2 -4
- supervisely/nn/inference/predict_app/gui/output_selector.py +46 -6
- supervisely/nn/inference/predict_app/gui/settings_selector.py +756 -59
- supervisely/nn/inference/predict_app/gui/tags_selector.py +1 -1
- supervisely/nn/inference/predict_app/gui/utils.py +236 -119
- supervisely/nn/inference/predict_app/predict_app.py +2 -2
- supervisely/nn/inference/session.py +43 -35
- supervisely/nn/inference/tracking/bbox_tracking.py +113 -34
- supervisely/nn/inference/tracking/tracker_interface.py +7 -2
- supervisely/nn/inference/uploader.py +139 -12
- supervisely/nn/live_training/__init__.py +7 -0
- supervisely/nn/live_training/api_server.py +111 -0
- supervisely/nn/live_training/artifacts_utils.py +243 -0
- supervisely/nn/live_training/checkpoint_utils.py +229 -0
- supervisely/nn/live_training/dynamic_sampler.py +44 -0
- supervisely/nn/live_training/helpers.py +14 -0
- supervisely/nn/live_training/incremental_dataset.py +146 -0
- supervisely/nn/live_training/live_training.py +497 -0
- supervisely/nn/live_training/loss_plateau_detector.py +111 -0
- supervisely/nn/live_training/request_queue.py +52 -0
- supervisely/nn/model/model_api.py +9 -0
- supervisely/nn/prediction_dto.py +12 -1
- supervisely/nn/tracker/base_tracker.py +11 -1
- supervisely/nn/tracker/botsort/botsort_config.yaml +0 -1
- supervisely/nn/tracker/botsort/tracker/mc_bot_sort.py +7 -4
- supervisely/nn/tracker/botsort_tracker.py +94 -65
- supervisely/nn/tracker/visualize.py +87 -90
- supervisely/nn/training/gui/classes_selector.py +16 -1
- supervisely/nn/training/train_app.py +28 -29
- supervisely/project/data_version.py +115 -51
- supervisely/project/download.py +1 -1
- supervisely/project/pointcloud_episode_project.py +37 -8
- supervisely/project/pointcloud_project.py +30 -2
- supervisely/project/project.py +14 -2
- supervisely/project/project_meta.py +27 -1
- supervisely/project/project_settings.py +32 -18
- supervisely/project/versioning/__init__.py +1 -0
- supervisely/project/versioning/common.py +20 -0
- supervisely/project/versioning/schema_fields.py +35 -0
- supervisely/project/versioning/video_schema.py +221 -0
- supervisely/project/versioning/volume_schema.py +87 -0
- supervisely/project/video_project.py +717 -15
- supervisely/project/volume_project.py +623 -5
- supervisely/template/experiment/experiment.html.jinja +4 -4
- supervisely/template/experiment/experiment_generator.py +14 -21
- supervisely/template/live_training/__init__.py +0 -0
- supervisely/template/live_training/header.html.jinja +96 -0
- supervisely/template/live_training/live_training.html.jinja +51 -0
- supervisely/template/live_training/live_training_generator.py +464 -0
- supervisely/template/live_training/sly-style.css +402 -0
- supervisely/template/live_training/template.html.jinja +18 -0
- supervisely/versions.json +28 -26
- supervisely/video/sampling.py +39 -20
- supervisely/video/video.py +40 -11
- supervisely/video_annotation/video_object.py +29 -4
- supervisely/volume/stl_converter.py +2 -0
- supervisely/worker_api/agent_rpc.py +24 -1
- supervisely/worker_api/rpc_servicer.py +31 -7
- {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/METADATA +56 -39
- {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/RECORD +189 -142
- {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/WHEEL +1 -1
- {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/entry_points.txt +0 -0
- {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info/licenses}/LICENSE +0 -0
- {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 =
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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":
|
|
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
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
|
|
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("",
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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}}}
|