supervisely 6.73.357__py3-none-any.whl → 6.73.358__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 (44) hide show
  1. supervisely/_utils.py +12 -0
  2. supervisely/api/annotation_api.py +3 -0
  3. supervisely/api/api.py +2 -2
  4. supervisely/api/app_api.py +27 -2
  5. supervisely/api/entity_annotation/tag_api.py +0 -1
  6. supervisely/api/nn/__init__.py +0 -0
  7. supervisely/api/nn/deploy_api.py +821 -0
  8. supervisely/api/nn/neural_network_api.py +248 -0
  9. supervisely/api/task_api.py +26 -467
  10. supervisely/app/fastapi/subapp.py +1 -0
  11. supervisely/nn/__init__.py +2 -1
  12. supervisely/nn/artifacts/artifacts.py +5 -5
  13. supervisely/nn/benchmark/object_detection/metric_provider.py +3 -0
  14. supervisely/nn/experiments.py +28 -5
  15. supervisely/nn/inference/cache.py +178 -114
  16. supervisely/nn/inference/gui/gui.py +18 -35
  17. supervisely/nn/inference/gui/serving_gui.py +3 -1
  18. supervisely/nn/inference/inference.py +1421 -1265
  19. supervisely/nn/inference/inference_request.py +412 -0
  20. supervisely/nn/inference/object_detection_3d/object_detection_3d.py +31 -24
  21. supervisely/nn/inference/session.py +2 -2
  22. supervisely/nn/inference/tracking/base_tracking.py +45 -79
  23. supervisely/nn/inference/tracking/bbox_tracking.py +220 -155
  24. supervisely/nn/inference/tracking/mask_tracking.py +274 -250
  25. supervisely/nn/inference/tracking/tracker_interface.py +23 -0
  26. supervisely/nn/inference/uploader.py +164 -0
  27. supervisely/nn/model/__init__.py +0 -0
  28. supervisely/nn/model/model_api.py +259 -0
  29. supervisely/nn/model/prediction.py +311 -0
  30. supervisely/nn/model/prediction_session.py +632 -0
  31. supervisely/nn/tracking/__init__.py +1 -0
  32. supervisely/nn/tracking/boxmot.py +114 -0
  33. supervisely/nn/tracking/tracking.py +24 -0
  34. supervisely/nn/training/train_app.py +61 -19
  35. supervisely/nn/utils.py +43 -3
  36. supervisely/task/progress.py +12 -2
  37. supervisely/video/video.py +107 -1
  38. {supervisely-6.73.357.dist-info → supervisely-6.73.358.dist-info}/METADATA +2 -1
  39. {supervisely-6.73.357.dist-info → supervisely-6.73.358.dist-info}/RECORD +43 -32
  40. supervisely/api/neural_network_api.py +0 -202
  41. {supervisely-6.73.357.dist-info → supervisely-6.73.358.dist-info}/LICENSE +0 -0
  42. {supervisely-6.73.357.dist-info → supervisely-6.73.358.dist-info}/WHEEL +0 -0
  43. {supervisely-6.73.357.dist-info → supervisely-6.73.358.dist-info}/entry_points.txt +0 -0
  44. {supervisely-6.73.357.dist-info → supervisely-6.73.358.dist-info}/top_level.txt +0 -0
@@ -637,6 +637,29 @@ class TrackerInterfaceV2:
637
637
  logger.info("Task stopped by user.", extra=self.log_extra)
638
638
  self.stop()
639
639
 
640
+ def notify_error(self, exception: Exception):
641
+ logger.debug(f"Notify error: {str(exception)}", extra=self.log_extra)
642
+ error = type(exception).__name__
643
+ message = str(exception)
644
+ if self.direct_progress:
645
+ self.api.vid_ann_tool.set_direct_tracking_error(
646
+ self.session_id,
647
+ self.video_id,
648
+ self.track_id,
649
+ f"{type(exception).__name__}: {str(exception)}",
650
+ )
651
+ else:
652
+ self.api.video.notify_tracking_error(self.track_id, error, message)
653
+
654
+ def notify_warning(self, message: str):
655
+ logger.debug(f"Notify warning: {message}", extra=self.log_extra)
656
+ if self.direct_progress:
657
+ self.api.vid_ann_tool.set_direct_tracking_warning(
658
+ self.session_id, self.video_id, self.track_id, message
659
+ )
660
+ else:
661
+ self.api.video.notify_tracking_warning(self.track_id, self.video_id, message)
662
+
640
663
  def _upload_exception_handler(self, exception: Exception):
641
664
  logger.error(
642
665
  "Error in upload loop: %s", str(exception), exc_info=True, extra=self.log_extra
@@ -0,0 +1,164 @@
1
+ from __future__ import annotations
2
+
3
+ import threading
4
+ from logging import Logger
5
+ from queue import Empty, Queue
6
+ from types import TracebackType
7
+ from typing import Callable, Optional, Type
8
+
9
+ from supervisely.sly_logger import logger
10
+
11
+
12
+ class Uploader:
13
+ def __init__(
14
+ self,
15
+ upload_f: Callable,
16
+ notify_f: Callable = None,
17
+ exception_handler: Callable = None,
18
+ logger: Logger = None,
19
+ ):
20
+ self._upload_f = upload_f
21
+ self._notify_f = notify_f
22
+ self._exception_handler = exception_handler
23
+ if self._exception_handler is None:
24
+ self._exception_handler = self._default_exception_handler
25
+ self._logger = logger
26
+ self.exception = None
27
+ self._lock = threading.Lock()
28
+ self._q = Queue()
29
+ self._stop_event = threading.Event()
30
+ self._exception_event = threading.Event()
31
+ self._upload_thread = threading.Thread(
32
+ target=self._upload_loop,
33
+ daemon=True,
34
+ )
35
+ self.raise_from_notify = False
36
+ self._notify_thread = None
37
+ self._notify_q = Queue()
38
+ if self._notify_f is not None:
39
+ self._notify_thread = threading.Thread(target=self._notify_loop, daemon=True)
40
+ self._notify_thread.start()
41
+ self._upload_thread.start()
42
+
43
+ def _notify_loop(self):
44
+ try:
45
+ while (
46
+ not self._stop_event.is_set() or not self._notify_q.empty() or not self._q.empty()
47
+ ):
48
+ if self._exception_event.is_set():
49
+ return
50
+ items = []
51
+ try:
52
+ timeout = 0.1 if self._stop_event.is_set() else 1.0
53
+ item = self._notify_q.get(timeout=timeout)
54
+ items.append(item)
55
+ while True:
56
+ try:
57
+ items.append(self._notify_q.get_nowait())
58
+ except Empty:
59
+ break
60
+ if items:
61
+ self._notify_f(items)
62
+
63
+ for _ in range(len(items)):
64
+ self._notify_q.task_done()
65
+ except Empty:
66
+ continue
67
+ except StopIteration:
68
+ self.stop()
69
+ return
70
+ except Exception as e:
71
+ try:
72
+ raise RuntimeError("Error in notify loop") from e
73
+ except RuntimeError as e_:
74
+ e = e_
75
+ if self._logger is not None:
76
+ self._logger.error("Error in notify loop: %s", str(e), exc_info=True)
77
+ if self.raise_from_notify and not self._exception_event.is_set():
78
+ self.set_exception(e)
79
+ return
80
+
81
+ def _upload_loop(self):
82
+ try:
83
+ while not self._stop_event.is_set() or not self._q.empty():
84
+ if self._exception_event.is_set():
85
+ return
86
+ items = []
87
+ try:
88
+ timeout = 0.1 if self._stop_event.is_set() else 1.0
89
+ item = self._q.get(timeout=timeout)
90
+ items.append(item)
91
+ while True:
92
+ try:
93
+ items.append(self._q.get_nowait())
94
+ except Empty:
95
+ break
96
+ if items:
97
+ self._upload_f(items)
98
+ self.notify(items)
99
+
100
+ for _ in range(len(items)):
101
+ self._q.task_done()
102
+ except Empty:
103
+ continue
104
+ except StopIteration:
105
+ self.stop()
106
+ return
107
+ except Exception as e:
108
+ try:
109
+ raise RuntimeError("Error in upload loop") from e
110
+ except RuntimeError as e_:
111
+ e = e_
112
+ if self._logger is not None:
113
+ self._logger.error("Error in upload loop: %s", str(e), exc_info=True)
114
+ if not self._exception_event.is_set():
115
+ self.set_exception(e)
116
+ return
117
+
118
+ def put(self, items):
119
+ for item in items:
120
+ self._q.put(item)
121
+
122
+ def notify(self, items):
123
+ if self._notify_thread is not None:
124
+ for item in items:
125
+ self._notify_q.put(item)
126
+
127
+ def stop(self):
128
+ self._stop_event.set()
129
+
130
+ def join(self, timeout=None):
131
+ self._upload_thread.join(timeout=timeout)
132
+ if self._notify_thread is not None:
133
+ self._notify_thread.join(timeout=timeout)
134
+
135
+ def has_exception(self):
136
+ return self._exception_event.is_set()
137
+
138
+ def set_exception(self, exception: Exception):
139
+ with self._lock:
140
+ self._exception_event.set()
141
+ self.exception = exception
142
+
143
+ def __enter__(self):
144
+ return self
145
+
146
+ def _default_exception_handler(
147
+ self,
148
+ exception: Exception,
149
+ ):
150
+ raise exception
151
+
152
+ def __exit__(self, exc_type, exc_val, exc_tb):
153
+ self.stop()
154
+ try:
155
+ self.join(timeout=5)
156
+ except TimeoutError:
157
+ _logger = logger
158
+ if self._logger is not None:
159
+ _logger = self._logger
160
+ _logger.warning("Uploader thread didn't finish in time")
161
+ if exc_type is not None:
162
+ exc = exc_val.with_traceback(exc_tb)
163
+ return self._exception_handler(exc)
164
+ return False
File without changes
@@ -0,0 +1,259 @@
1
+ from __future__ import annotations
2
+ import os
3
+ from os import PathLike
4
+ from typing import List, Union
5
+
6
+ import numpy as np
7
+ import requests
8
+
9
+ import supervisely.io.env as sly_env
10
+ import supervisely.io.json as sly_json
11
+ from supervisely.api.module_api import ApiField
12
+ from supervisely.api.task_api import TaskApi
13
+ from supervisely.nn.experiments import ExperimentInfo
14
+ from supervisely.nn.model.prediction import Prediction
15
+ from supervisely.nn.model.prediction_session import PredictionSession
16
+ from supervisely.nn.utils import ModelSource
17
+ from supervisely.project.project_meta import ProjectMeta
18
+ from supervisely.api.api import Api
19
+
20
+
21
+ class ModelAPI:
22
+ def __init__(self, api: "Api" = None, task_id: int = None, url: str = None):
23
+ assert not (task_id is None and url is None), "Either `task_id` or `url` must be passed."
24
+ assert (
25
+ task_id is None or url is None
26
+ ), "Either `task_id` or `url` must be passed (not both)."
27
+ if task_id is not None:
28
+ assert api is not None, "API must be provided if `task_id` is passed."
29
+
30
+ self.api = api
31
+ self.task_id = task_id
32
+ self.url = url
33
+
34
+ if self.task_id is not None:
35
+ task_info = self.api.task.get_info_by_id(self.task_id)
36
+ if task_info is None:
37
+ raise ValueError(f"Task with id {self.task_id} not found.")
38
+ self.url = f'{self.api.server_address}/net/{task_info["meta"]["sessionToken"]}'
39
+
40
+ # region Requests
41
+ def _post(self, method: str, data: dict, raise_for_status: bool = True):
42
+ url = f"{self.url.rstrip('/')}/{method.lstrip('/')}"
43
+ response = requests.post(url, json=data)
44
+ if raise_for_status:
45
+ response.raise_for_status()
46
+ return response.json()
47
+
48
+ def _get(self, method: str, params: dict = None, raise_for_status: bool = True):
49
+ url = f"{self.url.rstrip('/')}/{method.lstrip('/')}"
50
+ response = requests.get(url, params=params)
51
+ if raise_for_status:
52
+ response.raise_for_status()
53
+ return response.json()
54
+
55
+ # ------------------------------------ #
56
+
57
+ # region Info
58
+ def get_info(self):
59
+ if self.task_id is not None:
60
+ return self.api.nn._deploy_api.get_deploy_info(self.task_id)
61
+ return self._post("get_deploy_info", {})
62
+
63
+ def get_settings(self):
64
+ if self.task_id is not None:
65
+ return self.api.task.send_request(self.task_id, "get_custom_inference_settings", {})[
66
+ "settings"
67
+ ]
68
+ else:
69
+ return self._post("get_custom_inference_settings", {})["settings"]
70
+
71
+ def get_model_meta(self):
72
+ if self.task_id is not None:
73
+ return ProjectMeta.from_json(
74
+ self.api.task.send_request(self.task_id, "get_output_classes_and_tags", {})
75
+ )
76
+ else:
77
+ return ProjectMeta.from_json(self._post("get_output_classes_and_tags", {}))
78
+
79
+ def get_classes(self):
80
+ model_meta = self.get_model_meta()
81
+ return [obj_class.name for obj_class in model_meta.obj_classes]
82
+
83
+ def list_pretrained_models(self) -> List[str]:
84
+ """Return a list of pretrained model names available for deployment"""
85
+ return self._post("list_pretrained_models", {})
86
+
87
+ def list_pretrained_model_infos(self) -> List[dict]:
88
+ """Return a list of pretrained model infos with full information about each model"""
89
+ return self._post("list_pretrained_model_infos", {})
90
+
91
+ def list_experiments(self) -> List[ExperimentInfo]:
92
+ """Return a list of training experiments in Supervisely"""
93
+ raise NotImplementedError
94
+
95
+ def is_deployed(self) -> bool:
96
+ if self.task_id is not None:
97
+ return self.api.task.is_ready(self.task_id)
98
+ return self._post("is_ready", {})["status"] == "ready"
99
+
100
+ def status(self):
101
+ if self.task_id is not None:
102
+ return self.api.task.send_request(self.task_id, "get_status", {})
103
+ return self._post("get_status", {})
104
+
105
+ def shutdown(self):
106
+ if self.task_id is not None:
107
+ return self.api.task.stop(self.task_id)
108
+ response = self._post("tasks.stop", {ApiField.ID: id})
109
+ return TaskApi.Status(response[ApiField.STATUS])
110
+
111
+ # --------------------- #
112
+
113
+ # region Load
114
+ def load(
115
+ self,
116
+ model: str,
117
+ device: str = None,
118
+ runtime: str = None,
119
+ ):
120
+ if self.url is not None:
121
+ if os.path.exists(model):
122
+ self._load_local_custom_model(model, device, runtime)
123
+ else:
124
+ self._load_local_pretrained_model(model, device, runtime)
125
+
126
+ elif model.startswith("/"):
127
+ team_id = sly_env.team_id()
128
+ artifacts_dir, checkpoint_name = (
129
+ self.api.nn._deploy_api._get_artifacts_dir_and_checkpoint_name(model)
130
+ )
131
+ self.api.nn._deploy_api.load_custom_model(
132
+ self.task_id,
133
+ team_id,
134
+ artifacts_dir,
135
+ checkpoint_name,
136
+ device=device,
137
+ runtime=runtime,
138
+ )
139
+ else:
140
+ self.api.nn._deploy_api.load_pretrained_model(
141
+ self.task_id, model, device=device, runtime=runtime
142
+ )
143
+
144
+ def _load_local_pretrained_model(self, model: str, device: str = None, runtime: str = None):
145
+ available_models = self.list_pretrained_models()
146
+ if model not in available_models:
147
+ raise ValueError(f"Model {model} not found in available models: {available_models}")
148
+
149
+ deploy_params = {
150
+ "model_files": {},
151
+ "model_source": ModelSource.PRETRAINED,
152
+ "model_info": {},
153
+ "device": device,
154
+ "runtime": runtime,
155
+ }
156
+ state = {"deploy_params": deploy_params, "model_name": model}
157
+ return self._post("deploy_from_api", {"state": state})
158
+
159
+ def _load_local_custom_model(self, model: str, device: str = None, runtime: str = None):
160
+ deploy_params = self._get_custom_model_params(model, device, runtime)
161
+ state = {"deploy_params": deploy_params, "model_name": model}
162
+ return self._post("deploy_from_api", {"state": state})
163
+
164
+ def _get_custom_model_params(self, model_name: str, device: str = None, runtime: str = None):
165
+ def _load_experiment_info(artifacts_dir):
166
+ experiment_path = os.path.join(artifacts_dir, "experiment_info.json")
167
+ model_info = sly_json.load_json_file(experiment_path)
168
+ model_meta_path = os.path.join(artifacts_dir, "model_meta.json")
169
+ model_info["model_meta"] = sly_json.load_json_file(model_meta_path)
170
+ original_model_files = model_info.get("model_files")
171
+ return model_info, original_model_files
172
+
173
+ def _prepare_local_model_files(artifacts_dir, checkpoint_path, original_model_files):
174
+ return {k: os.path.join(artifacts_dir, v) for k, v in original_model_files.items()} | {
175
+ "checkpoint": checkpoint_path
176
+ }
177
+
178
+ model_source = ModelSource.CUSTOM
179
+ artifacts_dir = os.path.dirname(os.path.dirname(model_name))
180
+ model_info, original_model_files = _load_experiment_info(artifacts_dir)
181
+ model_files = _prepare_local_model_files(artifacts_dir, model_name, original_model_files)
182
+ deploy_params = {
183
+ "model_files": model_files,
184
+ "model_source": model_source,
185
+ "model_info": model_info,
186
+ "device": device,
187
+ "runtime": runtime,
188
+ }
189
+ return deploy_params
190
+
191
+ # --------------------------------- #
192
+
193
+ # region Predict
194
+ def predict_detached(
195
+ self,
196
+ input: Union[np.ndarray, str, PathLike, list] = None,
197
+ image_id: int = None,
198
+ video_id: int = None,
199
+ dataset_id: int = None,
200
+ project_id: int = None,
201
+ batch_size: int = None,
202
+ conf: float = None,
203
+ classes: List[str] = None,
204
+ upload_mode: str = None,
205
+ **kwargs,
206
+ ) -> PredictionSession:
207
+ if upload_mode is not None:
208
+ kwargs["upload_mode"] = upload_mode
209
+ return PredictionSession(
210
+ self.url,
211
+ input=input,
212
+ image_id=image_id,
213
+ video_id=video_id,
214
+ dataset_id=dataset_id,
215
+ project_id=project_id,
216
+ api=self.api,
217
+ batch_size=batch_size,
218
+ conf=conf,
219
+ classes=classes,
220
+ **kwargs,
221
+ )
222
+
223
+ def predict(
224
+ self,
225
+ input: Union[np.ndarray, str, PathLike, list] = None,
226
+ image_id: Union[List[int], int] = None,
227
+ video_id: Union[List[int], int] = None,
228
+ dataset_id: Union[List[int], int] = None,
229
+ project_id: Union[List[int], int] = None,
230
+ batch_size: int = None,
231
+ conf: float = None,
232
+ img_size: int = None,
233
+ classes: List[str] = None,
234
+ upload_mode: str = None,
235
+ recursive: bool = None,
236
+ **kwargs,
237
+ ) -> List[Prediction]:
238
+ if "show_progress" not in kwargs:
239
+ kwargs["show_progress"] = True
240
+ if recursive is not None:
241
+ kwargs["recursive"] = recursive
242
+ if img_size is not None:
243
+ kwargs["img_size"] = img_size
244
+ return list(
245
+ self.predict_detached(
246
+ input,
247
+ image_id,
248
+ video_id,
249
+ dataset_id,
250
+ project_id,
251
+ batch_size,
252
+ conf,
253
+ classes,
254
+ upload_mode,
255
+ **kwargs,
256
+ )
257
+ )
258
+
259
+ # ------------------------------------ #