supervisely 6.73.410__py3-none-any.whl → 6.73.470__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (190) hide show
  1. supervisely/__init__.py +136 -1
  2. supervisely/_utils.py +81 -0
  3. supervisely/annotation/json_geometries_map.py +2 -0
  4. supervisely/annotation/label.py +80 -3
  5. supervisely/api/annotation_api.py +9 -9
  6. supervisely/api/api.py +67 -43
  7. supervisely/api/app_api.py +72 -5
  8. supervisely/api/dataset_api.py +108 -33
  9. supervisely/api/entity_annotation/figure_api.py +113 -49
  10. supervisely/api/image_api.py +82 -0
  11. supervisely/api/module_api.py +10 -0
  12. supervisely/api/nn/deploy_api.py +15 -9
  13. supervisely/api/nn/ecosystem_models_api.py +201 -0
  14. supervisely/api/nn/neural_network_api.py +12 -3
  15. supervisely/api/pointcloud/pointcloud_api.py +38 -0
  16. supervisely/api/pointcloud/pointcloud_episode_annotation_api.py +3 -0
  17. supervisely/api/project_api.py +213 -6
  18. supervisely/api/task_api.py +11 -1
  19. supervisely/api/video/video_annotation_api.py +4 -2
  20. supervisely/api/video/video_api.py +79 -1
  21. supervisely/api/video/video_figure_api.py +24 -11
  22. supervisely/api/volume/volume_api.py +38 -0
  23. supervisely/app/__init__.py +1 -1
  24. supervisely/app/content.py +14 -6
  25. supervisely/app/fastapi/__init__.py +1 -0
  26. supervisely/app/fastapi/custom_static_files.py +1 -1
  27. supervisely/app/fastapi/multi_user.py +88 -0
  28. supervisely/app/fastapi/subapp.py +175 -42
  29. supervisely/app/fastapi/templating.py +1 -1
  30. supervisely/app/fastapi/websocket.py +77 -9
  31. supervisely/app/singleton.py +21 -0
  32. supervisely/app/v1/app_service.py +18 -2
  33. supervisely/app/v1/constants.py +7 -1
  34. supervisely/app/widgets/__init__.py +11 -1
  35. supervisely/app/widgets/agent_selector/template.html +1 -0
  36. supervisely/app/widgets/card/card.py +20 -0
  37. supervisely/app/widgets/dataset_thumbnail/dataset_thumbnail.py +11 -2
  38. supervisely/app/widgets/dataset_thumbnail/template.html +3 -1
  39. supervisely/app/widgets/deploy_model/deploy_model.py +750 -0
  40. supervisely/app/widgets/dialog/dialog.py +12 -0
  41. supervisely/app/widgets/dialog/template.html +2 -1
  42. supervisely/app/widgets/dropdown_checkbox_selector/__init__.py +0 -0
  43. supervisely/app/widgets/dropdown_checkbox_selector/dropdown_checkbox_selector.py +87 -0
  44. supervisely/app/widgets/dropdown_checkbox_selector/template.html +12 -0
  45. supervisely/app/widgets/ecosystem_model_selector/__init__.py +0 -0
  46. supervisely/app/widgets/ecosystem_model_selector/ecosystem_model_selector.py +195 -0
  47. supervisely/app/widgets/experiment_selector/experiment_selector.py +454 -263
  48. supervisely/app/widgets/fast_table/fast_table.py +713 -126
  49. supervisely/app/widgets/fast_table/script.js +492 -95
  50. supervisely/app/widgets/fast_table/style.css +54 -0
  51. supervisely/app/widgets/fast_table/template.html +45 -5
  52. supervisely/app/widgets/heatmap/__init__.py +0 -0
  53. supervisely/app/widgets/heatmap/heatmap.py +523 -0
  54. supervisely/app/widgets/heatmap/script.js +378 -0
  55. supervisely/app/widgets/heatmap/style.css +227 -0
  56. supervisely/app/widgets/heatmap/template.html +21 -0
  57. supervisely/app/widgets/input_tag/input_tag.py +102 -15
  58. supervisely/app/widgets/input_tag_list/__init__.py +0 -0
  59. supervisely/app/widgets/input_tag_list/input_tag_list.py +274 -0
  60. supervisely/app/widgets/input_tag_list/template.html +70 -0
  61. supervisely/app/widgets/radio_table/radio_table.py +10 -2
  62. supervisely/app/widgets/radio_tabs/radio_tabs.py +18 -2
  63. supervisely/app/widgets/radio_tabs/template.html +1 -0
  64. supervisely/app/widgets/select/select.py +6 -4
  65. supervisely/app/widgets/select_dataset/select_dataset.py +6 -0
  66. supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +83 -7
  67. supervisely/app/widgets/table/table.py +68 -13
  68. supervisely/app/widgets/tabs/tabs.py +22 -6
  69. supervisely/app/widgets/tabs/template.html +5 -1
  70. supervisely/app/widgets/transfer/style.css +3 -0
  71. supervisely/app/widgets/transfer/template.html +3 -1
  72. supervisely/app/widgets/transfer/transfer.py +48 -45
  73. supervisely/app/widgets/tree_select/tree_select.py +2 -0
  74. supervisely/convert/image/csv/csv_converter.py +24 -15
  75. supervisely/convert/pointcloud/nuscenes_conv/nuscenes_converter.py +43 -41
  76. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_converter.py +75 -51
  77. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_helper.py +137 -124
  78. supervisely/convert/video/video_converter.py +2 -2
  79. supervisely/geometry/polyline_3d.py +110 -0
  80. supervisely/io/env.py +161 -1
  81. supervisely/nn/artifacts/__init__.py +1 -1
  82. supervisely/nn/artifacts/artifacts.py +10 -2
  83. supervisely/nn/artifacts/detectron2.py +1 -0
  84. supervisely/nn/artifacts/hrda.py +1 -0
  85. supervisely/nn/artifacts/mmclassification.py +20 -0
  86. supervisely/nn/artifacts/mmdetection.py +5 -3
  87. supervisely/nn/artifacts/mmsegmentation.py +1 -0
  88. supervisely/nn/artifacts/ritm.py +1 -0
  89. supervisely/nn/artifacts/rtdetr.py +1 -0
  90. supervisely/nn/artifacts/unet.py +1 -0
  91. supervisely/nn/artifacts/utils.py +3 -0
  92. supervisely/nn/artifacts/yolov5.py +2 -0
  93. supervisely/nn/artifacts/yolov8.py +1 -0
  94. supervisely/nn/benchmark/semantic_segmentation/metric_provider.py +18 -18
  95. supervisely/nn/experiments.py +9 -0
  96. supervisely/nn/inference/cache.py +37 -17
  97. supervisely/nn/inference/gui/serving_gui_template.py +39 -13
  98. supervisely/nn/inference/inference.py +953 -211
  99. supervisely/nn/inference/inference_request.py +15 -8
  100. supervisely/nn/inference/instance_segmentation/instance_segmentation.py +1 -0
  101. supervisely/nn/inference/object_detection/object_detection.py +1 -0
  102. supervisely/nn/inference/predict_app/__init__.py +0 -0
  103. supervisely/nn/inference/predict_app/gui/__init__.py +0 -0
  104. supervisely/nn/inference/predict_app/gui/classes_selector.py +160 -0
  105. supervisely/nn/inference/predict_app/gui/gui.py +915 -0
  106. supervisely/nn/inference/predict_app/gui/input_selector.py +344 -0
  107. supervisely/nn/inference/predict_app/gui/model_selector.py +77 -0
  108. supervisely/nn/inference/predict_app/gui/output_selector.py +179 -0
  109. supervisely/nn/inference/predict_app/gui/preview.py +93 -0
  110. supervisely/nn/inference/predict_app/gui/settings_selector.py +881 -0
  111. supervisely/nn/inference/predict_app/gui/tags_selector.py +110 -0
  112. supervisely/nn/inference/predict_app/gui/utils.py +399 -0
  113. supervisely/nn/inference/predict_app/predict_app.py +176 -0
  114. supervisely/nn/inference/session.py +47 -39
  115. supervisely/nn/inference/tracking/bbox_tracking.py +5 -1
  116. supervisely/nn/inference/tracking/point_tracking.py +5 -1
  117. supervisely/nn/inference/tracking/tracker_interface.py +4 -0
  118. supervisely/nn/inference/uploader.py +9 -5
  119. supervisely/nn/model/model_api.py +44 -22
  120. supervisely/nn/model/prediction.py +15 -1
  121. supervisely/nn/model/prediction_session.py +70 -14
  122. supervisely/nn/prediction_dto.py +7 -0
  123. supervisely/nn/tracker/__init__.py +6 -8
  124. supervisely/nn/tracker/base_tracker.py +54 -0
  125. supervisely/nn/tracker/botsort/__init__.py +1 -0
  126. supervisely/nn/tracker/botsort/botsort_config.yaml +30 -0
  127. supervisely/nn/tracker/botsort/osnet_reid/__init__.py +0 -0
  128. supervisely/nn/tracker/botsort/osnet_reid/osnet.py +566 -0
  129. supervisely/nn/tracker/botsort/osnet_reid/osnet_reid_interface.py +88 -0
  130. supervisely/nn/tracker/botsort/tracker/__init__.py +0 -0
  131. supervisely/nn/tracker/{bot_sort → botsort/tracker}/basetrack.py +1 -2
  132. supervisely/nn/tracker/{utils → botsort/tracker}/gmc.py +51 -59
  133. supervisely/nn/tracker/{deep_sort/deep_sort → botsort/tracker}/kalman_filter.py +71 -33
  134. supervisely/nn/tracker/botsort/tracker/matching.py +202 -0
  135. supervisely/nn/tracker/{bot_sort/bot_sort.py → botsort/tracker/mc_bot_sort.py} +68 -81
  136. supervisely/nn/tracker/botsort_tracker.py +273 -0
  137. supervisely/nn/tracker/calculate_metrics.py +264 -0
  138. supervisely/nn/tracker/utils.py +273 -0
  139. supervisely/nn/tracker/visualize.py +520 -0
  140. supervisely/nn/training/gui/gui.py +152 -49
  141. supervisely/nn/training/gui/hyperparameters_selector.py +1 -1
  142. supervisely/nn/training/gui/model_selector.py +8 -6
  143. supervisely/nn/training/gui/train_val_splits_selector.py +144 -71
  144. supervisely/nn/training/gui/training_artifacts.py +3 -1
  145. supervisely/nn/training/train_app.py +225 -46
  146. supervisely/project/pointcloud_episode_project.py +12 -8
  147. supervisely/project/pointcloud_project.py +12 -8
  148. supervisely/project/project.py +221 -75
  149. supervisely/template/experiment/experiment.html.jinja +105 -55
  150. supervisely/template/experiment/experiment_generator.py +258 -112
  151. supervisely/template/experiment/header.html.jinja +31 -13
  152. supervisely/template/experiment/sly-style.css +7 -2
  153. supervisely/versions.json +3 -1
  154. supervisely/video/sampling.py +42 -20
  155. supervisely/video/video.py +41 -12
  156. supervisely/video_annotation/video_figure.py +38 -4
  157. supervisely/volume/stl_converter.py +2 -0
  158. supervisely/worker_api/agent_rpc.py +24 -1
  159. supervisely/worker_api/rpc_servicer.py +31 -7
  160. {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/METADATA +22 -14
  161. {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/RECORD +167 -148
  162. supervisely_lib/__init__.py +6 -1
  163. supervisely/app/widgets/experiment_selector/style.css +0 -27
  164. supervisely/app/widgets/experiment_selector/template.html +0 -61
  165. supervisely/nn/tracker/bot_sort/__init__.py +0 -21
  166. supervisely/nn/tracker/bot_sort/fast_reid_interface.py +0 -152
  167. supervisely/nn/tracker/bot_sort/matching.py +0 -127
  168. supervisely/nn/tracker/bot_sort/sly_tracker.py +0 -401
  169. supervisely/nn/tracker/deep_sort/__init__.py +0 -6
  170. supervisely/nn/tracker/deep_sort/deep_sort/__init__.py +0 -1
  171. supervisely/nn/tracker/deep_sort/deep_sort/detection.py +0 -49
  172. supervisely/nn/tracker/deep_sort/deep_sort/iou_matching.py +0 -81
  173. supervisely/nn/tracker/deep_sort/deep_sort/linear_assignment.py +0 -202
  174. supervisely/nn/tracker/deep_sort/deep_sort/nn_matching.py +0 -176
  175. supervisely/nn/tracker/deep_sort/deep_sort/track.py +0 -166
  176. supervisely/nn/tracker/deep_sort/deep_sort/tracker.py +0 -145
  177. supervisely/nn/tracker/deep_sort/deep_sort.py +0 -301
  178. supervisely/nn/tracker/deep_sort/generate_clip_detections.py +0 -90
  179. supervisely/nn/tracker/deep_sort/preprocessing.py +0 -70
  180. supervisely/nn/tracker/deep_sort/sly_tracker.py +0 -273
  181. supervisely/nn/tracker/tracker.py +0 -285
  182. supervisely/nn/tracker/utils/kalman_filter.py +0 -492
  183. supervisely/nn/tracking/__init__.py +0 -1
  184. supervisely/nn/tracking/boxmot.py +0 -114
  185. supervisely/nn/tracking/tracking.py +0 -24
  186. /supervisely/{nn/tracker/utils → app/widgets/deploy_model}/__init__.py +0 -0
  187. {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/LICENSE +0 -0
  188. {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/WHEEL +0 -0
  189. {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/entry_points.txt +0 -0
  190. {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,881 @@
1
+ import os
2
+ import random
3
+ import shutil
4
+ import subprocess
5
+ import threading
6
+ from contextlib import contextmanager, nullcontext
7
+ from pathlib import Path
8
+ from typing import Any, Callable, Dict, List
9
+
10
+ import cv2
11
+ import yaml
12
+
13
+ from supervisely._utils import logger
14
+ from supervisely.annotation.annotation import Annotation
15
+ from supervisely.annotation.label import Label
16
+ from supervisely.api.api import Api
17
+ from supervisely.api.video.video_api import VideoInfo
18
+ from supervisely.app.widgets import (
19
+ Button,
20
+ Card,
21
+ Container,
22
+ Editor,
23
+ Field,
24
+ GridGallery,
25
+ Input,
26
+ InputNumber,
27
+ OneOf,
28
+ Progress,
29
+ Select,
30
+ Text,
31
+ VideoPlayer,
32
+ )
33
+ from supervisely.app.widgets.checkbox.checkbox import Checkbox
34
+ from supervisely.app.widgets.empty.empty import Empty
35
+ from supervisely.app.widgets.widget import Widget
36
+ from supervisely.nn.inference.inference import (
37
+ _filter_duplicated_predictions_from_ann,
38
+ update_meta_and_ann,
39
+ update_meta_and_ann_for_video_annotation,
40
+ )
41
+ from supervisely.nn.inference.predict_app.gui.input_selector import InputSelector
42
+ from supervisely.nn.inference.predict_app.gui.model_selector import ModelSelector
43
+ from supervisely.nn.inference.predict_app.gui.utils import (
44
+ video_annotation_from_predictions,
45
+ )
46
+ from supervisely.nn.model.model_api import ModelAPI, Prediction
47
+ from supervisely.nn.tracker import TrackingVisualizer
48
+ from supervisely.project import ProjectMeta
49
+ from supervisely.project.project_meta import ProjectType
50
+ from supervisely.video.video import VideoFrameReader
51
+ from supervisely.video_annotation.video_annotation import KeyIdMap, VideoAnnotation
52
+
53
+
54
+ class InferenceMode:
55
+ FULL_IMAGE = "Full Image"
56
+ SLIDING_WINDOW = "Sliding Window"
57
+
58
+
59
+ class AddPredictionsMode:
60
+ APPEND = "Merge with existing labels"
61
+ REPLACE = "Replace existing labels"
62
+ IOU_MERGE = "Merge by IoU threshold"
63
+ REPLACE_EXISTING_LABELS_AND_SAVE_IMAGE_TAGS = "Replace existing labels and save image tags"
64
+
65
+
66
+ class Preview:
67
+ lock_message = "Select previous step to unlock"
68
+
69
+ def __init__(
70
+ self,
71
+ api: Api,
72
+ preview_dir: str,
73
+ get_model_api_fn: Callable[[], ModelAPI],
74
+ get_input_settings_fn: Callable[[], Dict[str, Any]],
75
+ get_settings_fn: Callable[[], Dict[str, Any]],
76
+ ):
77
+ self.api = api
78
+ self.preview_dir = preview_dir
79
+ self.get_model_api_fn = get_model_api_fn
80
+ self.get_input_settings_fn = get_input_settings_fn
81
+ self.get_settings_fn = get_settings_fn
82
+ os.makedirs(self.preview_dir, exist_ok=True)
83
+ os.makedirs(Path(self.preview_dir, "annotated"), exist_ok=True)
84
+ self.image_preview_path = None
85
+ self.image_peview_url = None
86
+ self.video_preview_path = None
87
+ self.video_preview_annotated_path = None
88
+ self.video_peview_url = None
89
+
90
+ self.progress_widget = Progress(show_percents=True, hide_on_finish=True)
91
+ self.download_error = Text("", status="warning")
92
+ self.download_error.hide()
93
+ self.progress_container = Container(widgets=[self.download_error, self.progress_widget])
94
+ self.loading_container = Container(widgets=[self.download_error, Text("Loading...")])
95
+
96
+ self.image_gallery = GridGallery(
97
+ 2,
98
+ sync_views=True,
99
+ enable_zoom=True,
100
+ resize_on_zoom=True,
101
+ empty_message="",
102
+ )
103
+ self.image_preview_container = Container(widgets=[self.image_gallery])
104
+
105
+ self.video_player = VideoPlayer()
106
+ self.video_preview_container = Container(widgets=[self.video_player])
107
+
108
+ self.locked_text = Text("Select input and model to unlock", status="info")
109
+ self.empty_text = Text("Click preview to visualize predictions")
110
+ self.error_text = Text("Failed to generate preview", status="error")
111
+
112
+ self.select = Select(
113
+ items=[
114
+ Select.Item("locked", content=self.locked_text),
115
+ Select.Item("empty", content=self.empty_text),
116
+ Select.Item(ProjectType.IMAGES.value, content=self.image_preview_container),
117
+ Select.Item(ProjectType.VIDEOS.value, content=self.video_preview_container),
118
+ Select.Item("error", content=self.error_text),
119
+ Select.Item("loading", content=self.loading_container),
120
+ Select.Item("progress", content=self.progress_container),
121
+ ]
122
+ )
123
+ self.select.set_value("empty")
124
+ self.oneof = OneOf(self.select)
125
+
126
+ self.run_button = Button("Preview", icon="zmdi zmdi-slideshow")
127
+ self.run_button.disable()
128
+ self.card = Card(
129
+ title="Preview",
130
+ description="Preview model predictions on a random image or video from the selected input source.",
131
+ content=self.oneof,
132
+ content_top_right=self.run_button,
133
+ lock_message=self.lock_message,
134
+ )
135
+
136
+ @self.run_button.click
137
+ def _run_preview():
138
+ self.run_preview()
139
+
140
+ def lock(self):
141
+ self.run_button.disable()
142
+ self.card.lock(self.lock_message)
143
+
144
+ def unlock(self):
145
+ self.run_button.enable()
146
+ self.card.unlock()
147
+
148
+ @contextmanager
149
+ def progress(self, message: str, total: int, **kwargs):
150
+ current_item = self.select.get_value()
151
+ try:
152
+ with self.progress_widget(message=message, total=total, **kwargs) as pbar:
153
+ self.select_item("progress")
154
+ yield pbar
155
+ finally:
156
+ self.select_item(current_item)
157
+
158
+ def select_item(self, item: str):
159
+ self.select.set_value(item)
160
+
161
+ def _download_video_by_frames(
162
+ self, video_info: VideoInfo, save_path: str, frames_number=150, progress_cb=None
163
+ ):
164
+ if Path(save_path).exists():
165
+ Path(save_path).unlink()
166
+ tmp_dir = Path(self.preview_dir, "tmp_frames")
167
+ if tmp_dir.exists():
168
+ shutil.rmtree(tmp_dir)
169
+ os.makedirs(tmp_dir, exist_ok=True)
170
+ self.api.video.download_frames(
171
+ video_info.id,
172
+ frames=list(range(frames_number)),
173
+ paths=[str(tmp_dir / f"frame_{i}.jpg") for i in range(frames_number)],
174
+ progress_cb=progress_cb,
175
+ )
176
+ fps = int(video_info.frames_count / video_info.duration)
177
+ fourcc = cv2.VideoWriter.fourcc(*"mp4v") # or 'avc1', 'XVID', 'H264'
178
+ out = cv2.VideoWriter(
179
+ save_path, fourcc, fps, (video_info.frame_width, video_info.frame_height)
180
+ )
181
+ for i in range(frames_number):
182
+ frame_path = tmp_dir / f"frame_{i}.jpg"
183
+ if not frame_path.exists():
184
+ continue
185
+ img = cv2.imread(str(frame_path))
186
+ out.write(img)
187
+ out.release()
188
+ shutil.rmtree(tmp_dir)
189
+
190
+ def _download_full_video(
191
+ self, video_id: int, save_path: str, duration: int = 5, progress_cb=None
192
+ ):
193
+ if Path(save_path).exists():
194
+ Path(save_path).unlink()
195
+ temp = Path(self.preview_dir) / f"temp_{video_id}.mp4"
196
+ if temp.exists():
197
+ temp.unlink()
198
+ self.api.video.download_path(video_id, temp, progress_cb=progress_cb)
199
+ minutes = duration // 60
200
+ hours = minutes // 60
201
+ minutes = minutes % 60
202
+ seconds = duration % 60
203
+ duration_str = f"{hours:02}:{minutes:02}:{seconds:02}"
204
+ try:
205
+ process = subprocess.Popen(
206
+ [
207
+ "ffmpeg",
208
+ "-y",
209
+ "-i",
210
+ str(temp),
211
+ "-c",
212
+ "copy",
213
+ "-t",
214
+ duration_str,
215
+ save_path,
216
+ ],
217
+ stderr=subprocess.PIPE,
218
+ )
219
+ process.wait()
220
+ logger.debug("FFmpeg exited with code: " + str(process.returncode))
221
+ logger.debug(f"FFmpeg stderr: {process.stderr.read().decode()}")
222
+ if len(VideoFrameReader(save_path).read_frames()) == 0:
223
+ raise RuntimeError("No frames read from the video")
224
+ temp.unlink()
225
+ except Exception as e:
226
+ if Path(save_path).exists():
227
+ Path(save_path).unlink()
228
+ shutil.copy(temp, save_path)
229
+ temp.unlink()
230
+ logger.warning(f"FFmpeg trimming failed: {str(e)}", exc_info=True)
231
+
232
+ def _download_video_preview(self, video_info: VideoInfo, with_progress=True):
233
+ video_id = video_info.id
234
+ duration = 5
235
+ video_path = Path(self.preview_dir, video_info.name)
236
+ self.video_preview_path = video_path
237
+ self.video_preview_annotated_path = Path(self.preview_dir, "annotated") / Path(
238
+ self.video_preview_path
239
+ ).relative_to(self.preview_dir)
240
+ success = False
241
+ try:
242
+ try:
243
+ size = int(video_info.file_meta["size"])
244
+ size = int(size / video_info.duration * duration)
245
+ except:
246
+ size = None
247
+ with (
248
+ self.progress("Downloading video part:", total=size, unit="B", unit_scale=True)
249
+ if with_progress and size
250
+ else nullcontext()
251
+ ) as pbar:
252
+ success = self._partial_download(
253
+ video_id, duration, str(self.video_preview_path), progress_cb=pbar.update
254
+ )
255
+ except Exception as e:
256
+ logger.warning(f"Partial download failed: {str(e)}", exc_info=True)
257
+ success = False
258
+ if success:
259
+ return
260
+
261
+ video_length_threshold = 120 # seconds
262
+ if video_info.duration > video_length_threshold:
263
+ self.download_error.text = (
264
+ f"Partial download failed. Will Download separate video frames"
265
+ )
266
+ self.download_error.show()
267
+
268
+ fps = int(video_info.frames_count / video_info.duration)
269
+ frames_number = min(video_info.frames_count, int(fps * duration))
270
+ with (
271
+ self.progress(
272
+ "Downloading video frames:", total=frames_number, unit="it", unit_scale=False
273
+ )
274
+ if with_progress
275
+ else nullcontext()
276
+ ) as pbar:
277
+ self._download_video_by_frames(
278
+ video_info,
279
+ str(self.video_preview_path),
280
+ frames_number=frames_number,
281
+ progress_cb=pbar.update,
282
+ )
283
+ else:
284
+ self.download_error.text = f"Partial download failed. Will Download full video"
285
+ self.download_error.show()
286
+ size = int(video_info.file_meta["size"])
287
+ with (
288
+ self.progress("Downloading video:", total=size, unit="B", unit_scale=True)
289
+ if with_progress
290
+ else nullcontext()
291
+ ) as pbar:
292
+ self._download_full_video(
293
+ video_info.id,
294
+ str(self.video_preview_path),
295
+ duration=duration,
296
+ progress_cb=pbar.update,
297
+ )
298
+
299
+ def _partial_download(self, video_id: int, duration: int, save_path: str, progress_cb=None):
300
+ if Path(save_path).exists():
301
+ Path(save_path).unlink()
302
+ duration_minutes = duration // 60
303
+ duration_hours = duration_minutes // 60
304
+ duration_minutes = duration_minutes % 60
305
+ duration_seconds = duration % 60
306
+ duration_str = f"{duration_hours:02}:{duration_minutes:02}:{duration_seconds:02}"
307
+ response = self.api.video._download(video_id, is_stream=True)
308
+ process = subprocess.Popen(
309
+ [
310
+ "ffmpeg",
311
+ "-y",
312
+ "-t",
313
+ duration_str,
314
+ "-probesize",
315
+ "50M",
316
+ "-analyzeduration",
317
+ "50M",
318
+ "-i",
319
+ "pipe:0",
320
+ "-movflags",
321
+ "frag_keyframe+empty_moov+default_base_moof",
322
+ "-c",
323
+ "copy",
324
+ save_path,
325
+ ],
326
+ stdin=subprocess.PIPE,
327
+ stderr=subprocess.PIPE,
328
+ )
329
+
330
+ bytes_written = 0
331
+ try:
332
+ for chunk in response.iter_content(chunk_size=8192):
333
+ process.stdin.write(chunk)
334
+ bytes_written += len(chunk)
335
+ if progress_cb:
336
+ progress_cb(len(chunk))
337
+ except (BrokenPipeError, IOError):
338
+ logger.debug("FFmpeg process closed the pipe, stopping download.", exc_info=True)
339
+ pass
340
+ finally:
341
+ process.stdin.close()
342
+ process.wait()
343
+ response.close()
344
+ logger.debug("FFmpeg exited with code: " + str(process.returncode))
345
+ logger.debug(f"FFmpeg stderr: {process.stderr.read().decode()}")
346
+ logger.debug(f"Total bytes written: {bytes_written}")
347
+ try:
348
+ with VideoFrameReader(save_path) as reader:
349
+ if len(reader.read_frames()) == 0:
350
+ return False
351
+ return True
352
+ except Exception as e:
353
+ return False
354
+
355
+ def _download_preview_item(self, with_progress: bool = False):
356
+ input_settings = self.get_input_settings_fn()
357
+ video_ids = input_settings.get("video_ids", None)
358
+ if video_ids is None:
359
+ project_id = input_settings.get("project_id", None)
360
+ dataset_ids = input_settings.get("dataset_ids", None)
361
+ if dataset_ids:
362
+ images = []
363
+ candidate_ids = list(dataset_ids)
364
+ random.shuffle(candidate_ids)
365
+ dataset_id = None
366
+ for ds_id in candidate_ids:
367
+ images = self.api.image.get_list(ds_id)
368
+ if images:
369
+ dataset_id = ds_id
370
+ break
371
+ if not images:
372
+ raise RuntimeError("No images found in the selected datasets")
373
+ else:
374
+ datasets = self.api.dataset.get_list(project_id)
375
+ total_items = sum(ds.items_count for ds in datasets)
376
+ if total_items == 0:
377
+ raise RuntimeError("No images found in the selected datasets")
378
+ images = []
379
+ while not images:
380
+ dataset_id = random.choice(datasets).id
381
+ images = self.api.image.get_list(dataset_id)
382
+ image_id = random.choice(images).id
383
+ image_info = self.api.image.get_info_by_id(image_id)
384
+ self.image_preview_path = Path(self.preview_dir, image_info.name)
385
+ self.api.image.download_path(image_id, self.image_preview_path)
386
+ self._current_item_id = image_id
387
+ ann_info = self.api.annotation.download(image_id)
388
+ self._project_meta = ProjectMeta.from_json(
389
+ self.api.project.get_meta(image_info.project_id)
390
+ )
391
+ self._image_annotation = Annotation.from_json(ann_info.annotation, self._project_meta)
392
+ self.image_peview_url = f"./static/preview/{image_info.name}"
393
+ elif len(video_ids) == 0:
394
+ self._current_item_id = None
395
+ self.video_preview_path = None
396
+ self.video_peview_url = None
397
+ self.video_preview_annotated_path = None
398
+ else:
399
+ video_id = random.choice(video_ids)
400
+ video_id = video_ids[0]
401
+ video_info = self.api.video.get_info_by_id(video_id)
402
+ self._download_video_preview(video_info, with_progress)
403
+ self._current_item_id = video_id
404
+ self.video_peview_url = f"./static/preview/annotated/{video_info.name}"
405
+ self._project_meta = ProjectMeta.from_json(
406
+ self.api.project.get_meta(video_info.project_id)
407
+ )
408
+ self._video_annotation = VideoAnnotation.from_json(
409
+ self.api.video.annotation.download(video_id), self._project_meta, KeyIdMap()
410
+ )
411
+
412
+ def set_image_preview(self):
413
+
414
+ def _maybe_merge_annotations(
415
+ source: Annotation,
416
+ pred: Annotation,
417
+ predictions_mode: str,
418
+ model_prediction_suffix: str,
419
+ iou_threshold: float = None,
420
+ ):
421
+ project_meta, pred, _ = update_meta_and_ann(
422
+ self._project_meta, pred, model_prediction_suffix
423
+ )
424
+ if predictions_mode == AddPredictionsMode.REPLACE:
425
+ return pred
426
+ elif predictions_mode == AddPredictionsMode.IOU_MERGE:
427
+ iou_threshold = iou_threshold if iou_threshold is not None else 0.9
428
+ pred = _filter_duplicated_predictions_from_ann(source, pred, iou_threshold)
429
+ return source.merge(pred)
430
+ elif predictions_mode in [
431
+ AddPredictionsMode.APPEND,
432
+ AddPredictionsMode.REPLACE_EXISTING_LABELS_AND_SAVE_IMAGE_TAGS,
433
+ ]:
434
+ return source.merge(pred)
435
+ else:
436
+ raise RuntimeError(f"Unknown predictions mode: {predictions_mode}")
437
+
438
+ self.image_gallery.clean_up()
439
+ if not self._current_item_id:
440
+ self._download_preview_item(with_progress=True)
441
+ image_id = self._current_item_id
442
+ model_api = self.get_model_api_fn()
443
+ settings = self.get_settings_fn()
444
+ inference_settings = settings.get("inference_settings", {})
445
+ with self.progress("Running Model:", total=1) as pbar:
446
+ prediction = model_api.predict(
447
+ image_id=image_id, inference_settings=inference_settings, tqdm=pbar
448
+ )[0]
449
+ prediction_annotation = _maybe_merge_annotations(
450
+ source=self._image_annotation,
451
+ pred=prediction.annotation,
452
+ predictions_mode=settings.get("predictions_mode", AddPredictionsMode.APPEND),
453
+ model_prediction_suffix=settings.get("model_prediction_suffix", ""),
454
+ iou_threshold=inference_settings.get("existing_objects_iou_thresh"),
455
+ )
456
+ self.image_gallery.append(
457
+ self.image_peview_url, title="Source", annotation=self._image_annotation
458
+ )
459
+ self.image_gallery.append(
460
+ self.image_peview_url, title="Prediction", annotation=prediction_annotation
461
+ )
462
+ self.select_item(ProjectType.IMAGES.value)
463
+
464
+ def set_video_preview(
465
+ self,
466
+ ):
467
+ self.video_player.set_video(None)
468
+ input_settings = self.get_input_settings_fn()
469
+ video_ids = input_settings.get("video_ids", None)
470
+ if not video_ids:
471
+ raise RuntimeError("No videos selected")
472
+ if not self._current_item_id:
473
+ self._download_preview_item(with_progress=True)
474
+ video_id = self._current_item_id
475
+
476
+ frame_start = 0
477
+ seconds = 5
478
+ video_info = self.api.video.get_info_by_id(video_id)
479
+ fps = int(video_info.frames_count / video_info.duration)
480
+ frames_number = min(video_info.frames_count, int(fps * seconds))
481
+ model_api = self.get_model_api_fn()
482
+ project_meta = ProjectMeta.from_json(self.api.project.get_meta(video_info.project_id))
483
+
484
+ settings = self.get_settings_fn()
485
+ inference_settings = settings.get("inference_settings", {})
486
+ tracking = settings.get("tracking", False)
487
+ with self.progress("Running model:", total=frames_number) as pbar:
488
+ with model_api.predict_detached(
489
+ video_id=video_id,
490
+ inference_settings=inference_settings,
491
+ tracking=tracking,
492
+ start_frame=frame_start,
493
+ num_frames=frames_number,
494
+ tqdm=pbar,
495
+ ) as session:
496
+ predictions: List[Prediction] = list(session)
497
+
498
+ if os.path.exists(self.video_preview_annotated_path):
499
+ os.remove(self.video_preview_annotated_path)
500
+ if tracking:
501
+ pred_video_annotation = session.final_result.get("video_ann", {})
502
+ if pred_video_annotation is None:
503
+ raise RuntimeError("Model did not return video annotation")
504
+ pred_video_annotation = VideoAnnotation.from_json(
505
+ pred_video_annotation, project_meta=project_meta
506
+ )
507
+ _, pred_video_annotation, _ = update_meta_and_ann_for_video_annotation(
508
+ self._project_meta,
509
+ pred_video_annotation,
510
+ settings.get("model_prediction_suffix", ""),
511
+ )
512
+ visualizer = TrackingVisualizer(
513
+ output_fps=fps,
514
+ box_thickness=video_info.frame_height // 110,
515
+ text_scale=video_info.frame_height / 900,
516
+ trajectory_thickness=video_info.frame_height // 110,
517
+ )
518
+ else:
519
+ pred_video_annotation = video_annotation_from_predictions(
520
+ predictions,
521
+ model_api.get_model_meta(),
522
+ frame_size=(video_info.frame_height, video_info.frame_width),
523
+ )
524
+ visualizer = TrackingVisualizer(
525
+ output_fps=fps,
526
+ box_thickness=video_info.frame_height // 110,
527
+ text_scale=video_info.frame_height / 900,
528
+ show_trajectories=False,
529
+ )
530
+ _, pred_video_annotation, _ = update_meta_and_ann_for_video_annotation(
531
+ self._project_meta,
532
+ pred_video_annotation,
533
+ settings.get("model_prediction_suffix", ""),
534
+ )
535
+ visualizer.visualize_video_annotation(
536
+ pred_video_annotation,
537
+ source=self.video_preview_path,
538
+ output_path=self.video_preview_annotated_path,
539
+ )
540
+ self.video_player.set_video(self.video_peview_url)
541
+ self.select_item(ProjectType.VIDEOS.value)
542
+
543
+ def set_error(self, text: str):
544
+ self.error_text.text = text
545
+ self.select_item("error")
546
+
547
+ def run_preview(self):
548
+ self.download_error.hide()
549
+ self.select_item("loading")
550
+ try:
551
+ input_settings = self.get_input_settings_fn()
552
+ video_ids = input_settings.get("video_ids", None)
553
+ if video_ids is None:
554
+ self.set_image_preview()
555
+ elif len(video_ids) == 0:
556
+ self.set_error("No videos selected")
557
+ else:
558
+ self.set_video_preview()
559
+ except Exception as e:
560
+ logger.error(f"Failed to generate preview: {str(e)}", exc_info=True)
561
+ self.set_error("Failed to generate preview: " + str(e))
562
+
563
+ def _preload_item(self):
564
+ threading.Thread(
565
+ target=self._download_preview_item, kwargs={"with_progress": False}, daemon=True
566
+ ).start()
567
+
568
+ def update_item_type(self, item_type: str):
569
+ self.select_item("empty")
570
+ self._current_item_id = None
571
+ self.download_error.hide()
572
+ # self._preload_item() # need to handle race condition with run_preview and multiple clicks
573
+
574
+
575
+ class SettingsSelector:
576
+ title = "Inference (settings + preview)"
577
+ description = "Select additional settings for model inference"
578
+ lock_message = "Select previous step to unlock"
579
+
580
+ def __init__(
581
+ self,
582
+ api: Api,
583
+ static_dir: str,
584
+ input_selector: InputSelector,
585
+ model_selector: ModelSelector,
586
+ ):
587
+ # Init Step
588
+ self.api = api
589
+ self.static_dir = static_dir
590
+ self.input_selector = input_selector
591
+ self.model_selector = model_selector
592
+ self.display_widgets: List[Any] = []
593
+ # -------------------------------- #
594
+
595
+ # Init Base Widgets
596
+ self.validator_text = None
597
+ self.button = None
598
+ self.run_button = None
599
+ self.container = None
600
+ self.cards = None
601
+ # -------------------------------- #
602
+
603
+ # Init Step Widgets
604
+ self.inference_mode_selector = None
605
+ self.inference_mode_field = None
606
+ self.model_prediction_suffix_input = None
607
+ self.model_prediction_suffix_field = None
608
+ # self.model_prediction_suffix_checkbox = None
609
+ self.predictions_mode_selector = None
610
+ self.predictions_mode_field = None
611
+ self.inference_settings_editor = None
612
+ # -------------------------------- #
613
+
614
+ self.settings_widgets = []
615
+ self.image_settings_widgets = []
616
+ self.video_settings_widgets = []
617
+
618
+ # Prediction Mode
619
+ self.prediction_modes = [
620
+ AddPredictionsMode.APPEND,
621
+ AddPredictionsMode.REPLACE,
622
+ AddPredictionsMode.IOU_MERGE,
623
+ # AddPredictionsMode.REPLACE_EXISTING_LABELS_AND_SAVE_IMAGE_TAGS, # @TODO: Implement later
624
+ ]
625
+ self.iou_merge_input = InputNumber(value=0.9, min=0.0, max=1.0, step=0.05, controls=False)
626
+ self.iou_merge_input_field = Field(
627
+ content=self.iou_merge_input,
628
+ title="IoU Threshold",
629
+ description="IoU threshold for merging predictions with existing labels. Predictions with IoU above this threshold will be considered duplicates and removed.",
630
+ )
631
+ self.prediction_modes_contents = [Empty(), Empty(), self.iou_merge_input_field]
632
+ self.predictions_mode_selector = Select(
633
+ items=[
634
+ Select.Item(mode, content=content)
635
+ for mode, content in zip(self.prediction_modes, self.prediction_modes_contents)
636
+ ]
637
+ )
638
+ self.predictions_mode_selector.set_value(self.prediction_modes[0])
639
+ self.predicitons_mode_one_of = OneOf(self.predictions_mode_selector)
640
+ self.predictions_mode_field = Field(
641
+ content=Container(
642
+ widgets=[self.predictions_mode_selector, self.predicitons_mode_one_of]
643
+ ),
644
+ title="Add predictions mode",
645
+ description="Select how to add predictions to the project: by merging with existing labels or by replacing them.",
646
+ )
647
+ # Add widgets to display ------------ #
648
+ self.image_settings_widgets.extend([self.predictions_mode_field])
649
+ # ----------------------------------- #
650
+
651
+ # Tracking
652
+ self.tracking_checkbox = Checkbox(content="Enable tracking", checked=True)
653
+ self.tracking_checkbox_field = Field(
654
+ content=self.tracking_checkbox,
655
+ title="Tracking",
656
+ description="Enable tracking for video predictions. The tracking algorithm is BoT-SORT version improved by Supervisely team.",
657
+ )
658
+ # Add widgets to display ------------ #
659
+ self.video_settings_widgets.extend([self.tracking_checkbox_field])
660
+ self.image_settings_container = Container(widgets=self.image_settings_widgets, gap=15)
661
+ self.video_settings_container = Container(widgets=self.video_settings_widgets, gap=15)
662
+ self.image_or_video_container = Container(
663
+ widgets=[self.image_settings_container, self.video_settings_container], gap=0
664
+ )
665
+ self.video_settings_container.hide()
666
+ self.settings_widgets.extend([self.image_or_video_container])
667
+ # ----------------------------------- #
668
+
669
+ # Class / Tag Suffix
670
+ self.model_prediction_suffix_input = Input(
671
+ value="_model", minlength=1, placeholder="Enter suffix e.g: _model"
672
+ )
673
+ self.model_meta_has_conflicting_names_text = Text(
674
+ text="Project and Model metas have conflicting names. This suffix will be added to conflicting class and tag names of model predictions",
675
+ status="info",
676
+ )
677
+ self.model_prediction_suffix_field = Field(
678
+ content=self.model_prediction_suffix_input,
679
+ title="Class and tag suffix",
680
+ description=(
681
+ "Suffix that will be added to conflicting class and tag names. "
682
+ "E.g. your project has a class 'person' with shape 'bitmap' and model has class 'person' with shape 'rectangle', "
683
+ "then suffix will be added to the model predictions to avoid conflicts. E.g. 'person_model'."
684
+ ),
685
+ )
686
+ self.model_prediction_suffix_container = Container(
687
+ widgets=[
688
+ self.model_meta_has_conflicting_names_text,
689
+ self.model_prediction_suffix_field,
690
+ ],
691
+ gap=5,
692
+ )
693
+ self.model_prediction_suffix_container.hide()
694
+ # Add widgets to display ------------ #
695
+ self.settings_widgets.extend([self.model_prediction_suffix_container])
696
+ # ----------------------------------- #
697
+
698
+ # Inference Settings
699
+ self.inference_settings_editor = Editor("", language_mode="yaml", height_px=300)
700
+ self.inference_settings_field = Field(
701
+ content=self.inference_settings_editor,
702
+ title="Inference and Tracking Settings",
703
+ )
704
+ # Add widgets to display ------------ #
705
+ self.settings_widgets.extend([self.inference_settings_field])
706
+ # ----------------------------------- #
707
+
708
+ # Preview
709
+ self.preview_dir = os.path.join(self.static_dir, "preview")
710
+ self.preview = Preview(
711
+ api=self.api,
712
+ preview_dir=self.preview_dir,
713
+ get_model_api_fn=lambda: self.model_selector.model.model_api,
714
+ get_input_settings_fn=self.input_selector.get_settings,
715
+ get_settings_fn=self.get_settings,
716
+ )
717
+
718
+ self.settings_container = Container(widgets=self.settings_widgets, gap=15)
719
+ self.display_widgets.extend([self.settings_container])
720
+ # Base Widgets
721
+ self.validator_text = Text("")
722
+ self.validator_text.hide()
723
+ self.button = Button("Select")
724
+ # Add widgets to display ------------ #
725
+ self.display_widgets.extend([self.validator_text, self.button])
726
+ # ----------------------------------- #
727
+
728
+ # Card Layout
729
+ self.container = Container(self.display_widgets)
730
+ self.settings_card = Card(
731
+ title=self.title,
732
+ description=self.description,
733
+ content=self.container,
734
+ lock_message=self.lock_message,
735
+ )
736
+ self.cards = [self.settings_card, self.preview.card]
737
+ self.cards_container = Container(
738
+ widgets=self.cards,
739
+ gap=15,
740
+ direction="horizontal",
741
+ fractions=[3, 7],
742
+ )
743
+ # ----------------------------------- #
744
+
745
+ def lock(self):
746
+ self.settings_card.lock(self.lock_message)
747
+ self.preview.lock()
748
+
749
+ def unlock(self):
750
+ self.settings_card.unlock()
751
+ self.preview.unlock()
752
+
753
+ def disable(self):
754
+ for widget in self.widgets_to_disable:
755
+ if isinstance(widget, Editor):
756
+ widget.readonly = True
757
+ else:
758
+ widget.disable()
759
+
760
+ def enable(self):
761
+ for widget in self.widgets_to_disable:
762
+ if isinstance(widget, Editor):
763
+ widget.readonly = False
764
+ else:
765
+ widget.enable()
766
+
767
+ @property
768
+ def widgets_to_disable(self) -> List[Widget]:
769
+ return [
770
+ self.tracking_checkbox,
771
+ self.predictions_mode_selector,
772
+ self.model_prediction_suffix_input,
773
+ self.inference_settings_editor,
774
+ ]
775
+
776
+ def set_inference_settings(self, settings: Dict[str, Any]):
777
+ settings = "# Inference settings\n" + settings
778
+ if isinstance(settings, str):
779
+ self.inference_settings_editor.set_text(settings)
780
+ else:
781
+ self.inference_settings_editor.set_text(yaml.safe_dump(settings))
782
+
783
+ def set_tracking_settings(self, settings: Dict[str, Any]):
784
+ if self.input_selector.radio.get_value() != ProjectType.VIDEOS.value:
785
+ return
786
+
787
+ current_settings = self.inference_settings_editor.get_text()
788
+ if isinstance(settings, str):
789
+ all_settings = current_settings + "\n\n# Tracking settings\n" + settings
790
+ self.inference_settings_editor.set_text(all_settings)
791
+ else:
792
+ all_settings = current_settings + "\n\n# Tracking settings\n" + yaml.safe_dump(settings)
793
+ self.inference_settings_editor.set_text(all_settings)
794
+
795
+ def set_default_tracking_settings(self):
796
+ nn_dir = Path(__file__).parents[3]
797
+ config_path = nn_dir / "tracker" / "botsort" / "botsort_config.yaml"
798
+
799
+ with open(config_path, "r", encoding="utf-8") as file:
800
+ botsort_config = yaml.safe_load(file)
801
+ self.set_tracking_settings(botsort_config)
802
+
803
+ def get_inference_settings(self) -> Dict:
804
+ text = self.inference_settings_editor.get_text()
805
+ inference_settings_text = text.split("# Tracking settings")[0]
806
+ settings = yaml.safe_load(inference_settings_text)
807
+ settings = settings if settings is not None else {}
808
+ if (
809
+ self.input_selector.radio.get_value() == ProjectType.IMAGES.value
810
+ and self.predictions_mode_selector.get_value() == AddPredictionsMode.IOU_MERGE
811
+ ):
812
+ settings["existing_objects_iou_thresh"] = self.iou_merge_input.get_value()
813
+ return settings
814
+
815
+ def get_tracking_settings(self) -> Dict:
816
+ if self.input_selector.radio.get_value() != ProjectType.VIDEOS.value:
817
+ return {}
818
+
819
+ text = self.inference_settings_editor.get_text()
820
+ text_parts = text.split("# Tracking settings")
821
+ if len(text_parts) > 1:
822
+ tracking_settings_text = text_parts[1]
823
+ settings = yaml.safe_load(tracking_settings_text)
824
+ if settings:
825
+ return settings
826
+ return {}
827
+
828
+ def get_settings(self) -> Dict[str, Any]:
829
+ settings = {
830
+ # "inference_mode": self.inference_mode_selector.get_value(),
831
+ "inference_mode": InferenceMode.FULL_IMAGE,
832
+ "model_prediction_suffix": self.model_prediction_suffix_input.get_value(),
833
+ "predictions_mode": self.predictions_mode_selector.get_value(),
834
+ "inference_settings": self.get_inference_settings(),
835
+ }
836
+ if self.input_selector.radio.get_value() == ProjectType.VIDEOS.value:
837
+ settings["tracking_settings"] = self.get_tracking_settings()
838
+ if self.input_selector.get_settings().get("video_ids", None) is not None:
839
+ settings["tracking"] = self.tracking_checkbox.is_checked()
840
+ return settings
841
+
842
+ def load_from_json(self, data):
843
+ # inference_mode = data.get("inference_mode", None)
844
+ # if inference_mode:
845
+ # self.inference_mode_selector.set_value(inference_mode)
846
+
847
+ model_prediction_suffix = data.get("model_prediction_suffix", None)
848
+ if model_prediction_suffix is not None:
849
+ self.model_prediction_suffix_input.set_value(model_prediction_suffix)
850
+
851
+ predictions_mode = data.get("predictions_mode", None)
852
+ if predictions_mode:
853
+ self.predictions_mode_selector.set_value(predictions_mode)
854
+
855
+ inference_settings = data.get("inference_settings", None)
856
+ if inference_settings is not None:
857
+ self.set_inference_settings(inference_settings)
858
+
859
+ tracking_settings = data.get("tracking_settings", None)
860
+ if tracking_settings is not None:
861
+ self.set_tracking_settings(tracking_settings)
862
+
863
+ tracking = data.get("tracking", None)
864
+ if tracking == True:
865
+ self.tracking_checkbox.check()
866
+ elif tracking == False:
867
+ self.tracking_checkbox.uncheck()
868
+
869
+ def update_item_type(self, item_type: str):
870
+ if item_type == ProjectType.IMAGES.value:
871
+ self.video_settings_container.hide()
872
+ self.image_settings_container.show()
873
+ elif item_type == ProjectType.VIDEOS.value:
874
+ self.image_settings_container.hide()
875
+ self.video_settings_container.show()
876
+ else:
877
+ raise ValueError(f"Unsupported item type: {item_type}")
878
+ self.preview.update_item_type(item_type)
879
+
880
+ def validate_step(self) -> bool:
881
+ return True