supervisely 6.73.452__py3-none-any.whl → 6.73.513__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. supervisely/__init__.py +25 -1
  2. supervisely/annotation/annotation.py +8 -2
  3. supervisely/annotation/json_geometries_map.py +13 -12
  4. supervisely/api/annotation_api.py +6 -3
  5. supervisely/api/api.py +2 -0
  6. supervisely/api/app_api.py +10 -1
  7. supervisely/api/dataset_api.py +74 -12
  8. supervisely/api/entities_collection_api.py +10 -0
  9. supervisely/api/entity_annotation/figure_api.py +28 -0
  10. supervisely/api/entity_annotation/object_api.py +3 -3
  11. supervisely/api/entity_annotation/tag_api.py +63 -12
  12. supervisely/api/guides_api.py +210 -0
  13. supervisely/api/image_api.py +4 -0
  14. supervisely/api/labeling_job_api.py +83 -1
  15. supervisely/api/labeling_queue_api.py +33 -7
  16. supervisely/api/module_api.py +5 -0
  17. supervisely/api/project_api.py +71 -26
  18. supervisely/api/storage_api.py +3 -1
  19. supervisely/api/task_api.py +13 -2
  20. supervisely/api/team_api.py +4 -3
  21. supervisely/api/video/video_annotation_api.py +119 -3
  22. supervisely/api/video/video_api.py +65 -14
  23. supervisely/app/__init__.py +1 -1
  24. supervisely/app/content.py +23 -7
  25. supervisely/app/development/development.py +18 -2
  26. supervisely/app/fastapi/__init__.py +1 -0
  27. supervisely/app/fastapi/custom_static_files.py +1 -1
  28. supervisely/app/fastapi/multi_user.py +105 -0
  29. supervisely/app/fastapi/subapp.py +88 -42
  30. supervisely/app/fastapi/websocket.py +77 -9
  31. supervisely/app/singleton.py +21 -0
  32. supervisely/app/v1/app_service.py +18 -2
  33. supervisely/app/v1/constants.py +7 -1
  34. supervisely/app/widgets/__init__.py +6 -0
  35. supervisely/app/widgets/activity_feed/__init__.py +0 -0
  36. supervisely/app/widgets/activity_feed/activity_feed.py +239 -0
  37. supervisely/app/widgets/activity_feed/style.css +78 -0
  38. supervisely/app/widgets/activity_feed/template.html +22 -0
  39. supervisely/app/widgets/card/card.py +20 -0
  40. supervisely/app/widgets/classes_list_selector/classes_list_selector.py +121 -9
  41. supervisely/app/widgets/classes_list_selector/template.html +60 -93
  42. supervisely/app/widgets/classes_mapping/classes_mapping.py +13 -12
  43. supervisely/app/widgets/classes_table/classes_table.py +1 -0
  44. supervisely/app/widgets/deploy_model/deploy_model.py +56 -35
  45. supervisely/app/widgets/ecosystem_model_selector/ecosystem_model_selector.py +1 -1
  46. supervisely/app/widgets/experiment_selector/experiment_selector.py +8 -0
  47. supervisely/app/widgets/fast_table/fast_table.py +184 -60
  48. supervisely/app/widgets/fast_table/template.html +1 -1
  49. supervisely/app/widgets/heatmap/__init__.py +0 -0
  50. supervisely/app/widgets/heatmap/heatmap.py +564 -0
  51. supervisely/app/widgets/heatmap/script.js +533 -0
  52. supervisely/app/widgets/heatmap/style.css +233 -0
  53. supervisely/app/widgets/heatmap/template.html +21 -0
  54. supervisely/app/widgets/modal/__init__.py +0 -0
  55. supervisely/app/widgets/modal/modal.py +198 -0
  56. supervisely/app/widgets/modal/template.html +10 -0
  57. supervisely/app/widgets/object_class_view/object_class_view.py +3 -0
  58. supervisely/app/widgets/radio_tabs/radio_tabs.py +18 -2
  59. supervisely/app/widgets/radio_tabs/template.html +1 -0
  60. supervisely/app/widgets/select/select.py +6 -3
  61. supervisely/app/widgets/select_class/__init__.py +0 -0
  62. supervisely/app/widgets/select_class/select_class.py +363 -0
  63. supervisely/app/widgets/select_class/template.html +50 -0
  64. supervisely/app/widgets/select_cuda/select_cuda.py +22 -0
  65. supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +65 -7
  66. supervisely/app/widgets/select_tag/__init__.py +0 -0
  67. supervisely/app/widgets/select_tag/select_tag.py +352 -0
  68. supervisely/app/widgets/select_tag/template.html +64 -0
  69. supervisely/app/widgets/select_team/select_team.py +37 -4
  70. supervisely/app/widgets/select_team/template.html +4 -5
  71. supervisely/app/widgets/select_user/__init__.py +0 -0
  72. supervisely/app/widgets/select_user/select_user.py +270 -0
  73. supervisely/app/widgets/select_user/template.html +13 -0
  74. supervisely/app/widgets/select_workspace/select_workspace.py +59 -10
  75. supervisely/app/widgets/select_workspace/template.html +9 -12
  76. supervisely/app/widgets/table/table.py +68 -13
  77. supervisely/app/widgets/tree_select/tree_select.py +2 -0
  78. supervisely/aug/aug.py +6 -2
  79. supervisely/convert/base_converter.py +1 -0
  80. supervisely/convert/converter.py +2 -2
  81. supervisely/convert/image/image_converter.py +3 -1
  82. supervisely/convert/image/image_helper.py +48 -4
  83. supervisely/convert/image/label_studio/label_studio_converter.py +2 -0
  84. supervisely/convert/image/medical2d/medical2d_helper.py +2 -24
  85. supervisely/convert/image/multispectral/multispectral_converter.py +6 -0
  86. supervisely/convert/image/pascal_voc/pascal_voc_converter.py +8 -5
  87. supervisely/convert/image/pascal_voc/pascal_voc_helper.py +7 -0
  88. supervisely/convert/pointcloud/kitti_3d/kitti_3d_converter.py +33 -3
  89. supervisely/convert/pointcloud/kitti_3d/kitti_3d_helper.py +12 -5
  90. supervisely/convert/pointcloud/las/las_converter.py +13 -1
  91. supervisely/convert/pointcloud/las/las_helper.py +110 -11
  92. supervisely/convert/pointcloud/nuscenes_conv/nuscenes_converter.py +27 -16
  93. supervisely/convert/pointcloud/pointcloud_converter.py +91 -3
  94. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_converter.py +58 -22
  95. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_helper.py +21 -47
  96. supervisely/convert/video/__init__.py +1 -0
  97. supervisely/convert/video/multi_view/__init__.py +0 -0
  98. supervisely/convert/video/multi_view/multi_view.py +543 -0
  99. supervisely/convert/video/sly/sly_video_converter.py +359 -3
  100. supervisely/convert/video/video_converter.py +22 -2
  101. supervisely/convert/volume/dicom/dicom_converter.py +13 -5
  102. supervisely/convert/volume/dicom/dicom_helper.py +30 -18
  103. supervisely/geometry/constants.py +1 -0
  104. supervisely/geometry/geometry.py +4 -0
  105. supervisely/geometry/helpers.py +5 -1
  106. supervisely/geometry/oriented_bbox.py +676 -0
  107. supervisely/geometry/rectangle.py +2 -1
  108. supervisely/io/env.py +76 -1
  109. supervisely/io/fs.py +21 -0
  110. supervisely/nn/benchmark/base_evaluator.py +104 -11
  111. supervisely/nn/benchmark/instance_segmentation/evaluator.py +1 -8
  112. supervisely/nn/benchmark/object_detection/evaluator.py +20 -4
  113. supervisely/nn/benchmark/object_detection/vis_metrics/pr_curve.py +10 -5
  114. supervisely/nn/benchmark/semantic_segmentation/evaluator.py +34 -16
  115. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/confusion_matrix.py +1 -1
  116. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/frequently_confused.py +1 -1
  117. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/overview.py +1 -1
  118. supervisely/nn/benchmark/visualization/evaluation_result.py +66 -4
  119. supervisely/nn/inference/cache.py +43 -18
  120. supervisely/nn/inference/gui/serving_gui_template.py +5 -2
  121. supervisely/nn/inference/inference.py +795 -199
  122. supervisely/nn/inference/inference_request.py +42 -9
  123. supervisely/nn/inference/predict_app/gui/classes_selector.py +83 -12
  124. supervisely/nn/inference/predict_app/gui/gui.py +676 -488
  125. supervisely/nn/inference/predict_app/gui/input_selector.py +205 -26
  126. supervisely/nn/inference/predict_app/gui/model_selector.py +2 -4
  127. supervisely/nn/inference/predict_app/gui/output_selector.py +46 -6
  128. supervisely/nn/inference/predict_app/gui/settings_selector.py +756 -59
  129. supervisely/nn/inference/predict_app/gui/tags_selector.py +1 -1
  130. supervisely/nn/inference/predict_app/gui/utils.py +236 -119
  131. supervisely/nn/inference/predict_app/predict_app.py +2 -2
  132. supervisely/nn/inference/session.py +43 -35
  133. supervisely/nn/inference/tracking/bbox_tracking.py +113 -34
  134. supervisely/nn/inference/tracking/tracker_interface.py +7 -2
  135. supervisely/nn/inference/uploader.py +139 -12
  136. supervisely/nn/live_training/__init__.py +7 -0
  137. supervisely/nn/live_training/api_server.py +111 -0
  138. supervisely/nn/live_training/artifacts_utils.py +243 -0
  139. supervisely/nn/live_training/checkpoint_utils.py +229 -0
  140. supervisely/nn/live_training/dynamic_sampler.py +44 -0
  141. supervisely/nn/live_training/helpers.py +14 -0
  142. supervisely/nn/live_training/incremental_dataset.py +146 -0
  143. supervisely/nn/live_training/live_training.py +497 -0
  144. supervisely/nn/live_training/loss_plateau_detector.py +111 -0
  145. supervisely/nn/live_training/request_queue.py +52 -0
  146. supervisely/nn/model/model_api.py +9 -0
  147. supervisely/nn/prediction_dto.py +12 -1
  148. supervisely/nn/tracker/base_tracker.py +11 -1
  149. supervisely/nn/tracker/botsort/botsort_config.yaml +0 -1
  150. supervisely/nn/tracker/botsort/tracker/mc_bot_sort.py +7 -4
  151. supervisely/nn/tracker/botsort_tracker.py +94 -65
  152. supervisely/nn/tracker/visualize.py +87 -90
  153. supervisely/nn/training/gui/classes_selector.py +16 -1
  154. supervisely/nn/training/train_app.py +28 -29
  155. supervisely/project/data_version.py +115 -51
  156. supervisely/project/download.py +1 -1
  157. supervisely/project/pointcloud_episode_project.py +37 -8
  158. supervisely/project/pointcloud_project.py +30 -2
  159. supervisely/project/project.py +14 -2
  160. supervisely/project/project_meta.py +27 -1
  161. supervisely/project/project_settings.py +32 -18
  162. supervisely/project/versioning/__init__.py +1 -0
  163. supervisely/project/versioning/common.py +20 -0
  164. supervisely/project/versioning/schema_fields.py +35 -0
  165. supervisely/project/versioning/video_schema.py +221 -0
  166. supervisely/project/versioning/volume_schema.py +87 -0
  167. supervisely/project/video_project.py +717 -15
  168. supervisely/project/volume_project.py +623 -5
  169. supervisely/template/experiment/experiment.html.jinja +4 -4
  170. supervisely/template/experiment/experiment_generator.py +14 -21
  171. supervisely/template/live_training/__init__.py +0 -0
  172. supervisely/template/live_training/header.html.jinja +96 -0
  173. supervisely/template/live_training/live_training.html.jinja +51 -0
  174. supervisely/template/live_training/live_training_generator.py +464 -0
  175. supervisely/template/live_training/sly-style.css +402 -0
  176. supervisely/template/live_training/template.html.jinja +18 -0
  177. supervisely/versions.json +28 -26
  178. supervisely/video/sampling.py +39 -20
  179. supervisely/video/video.py +40 -11
  180. supervisely/video_annotation/video_object.py +29 -4
  181. supervisely/volume/stl_converter.py +2 -0
  182. supervisely/worker_api/agent_rpc.py +24 -1
  183. supervisely/worker_api/rpc_servicer.py +31 -7
  184. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/METADATA +56 -39
  185. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/RECORD +189 -142
  186. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/WHEEL +1 -1
  187. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/entry_points.txt +0 -0
  188. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info/licenses}/LICENSE +0 -0
  189. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/top_level.txt +0 -0
@@ -1,21 +1,21 @@
1
- from typing import Union, Dict, List, Tuple, Iterator, Optional
2
- import numpy as np
1
+ import shutil
2
+ import tempfile
3
+ from collections import defaultdict
4
+ from pathlib import Path
5
+ from typing import Iterator, List, Optional, Tuple, Union
6
+
3
7
  import cv2
4
8
  import ffmpeg
5
- from pathlib import Path
6
- from collections import defaultdict
7
- from dataclasses import dataclass
8
- import tempfile
9
- import shutil
9
+ import numpy as np
10
10
 
11
- import supervisely as sly
12
- from supervisely import logger
11
+ from supervisely import VideoAnnotation, logger
12
+ from supervisely.geometry.geometry import Geometry
13
13
  from supervisely.nn.model.prediction import Prediction
14
- from supervisely import VideoAnnotation
15
14
  from supervisely.nn.tracker.utils import predictions_to_video_annotation
16
15
 
17
16
 
18
17
  class TrackingVisualizer:
18
+
19
19
  def __init__(
20
20
  self,
21
21
  show_labels: bool = True,
@@ -29,7 +29,7 @@ class TrackingVisualizer:
29
29
  codec: str = "mp4",
30
30
  output_fps: float = 30.0,
31
31
  colorize_tracks: bool = True,
32
-
32
+ trajectory_thickness: int = 2,
33
33
  ):
34
34
  """
35
35
  Initialize the visualizer with configuration.
@@ -58,6 +58,7 @@ class TrackingVisualizer:
58
58
  self.text_scale = text_scale
59
59
  self.text_thickness = text_thickness
60
60
  self.trajectory_length = trajectory_length
61
+ self.trajectory_thickness = trajectory_thickness
61
62
  self.colorize_tracks = colorize_tracks
62
63
 
63
64
  # Output settings
@@ -71,7 +72,7 @@ class TrackingVisualizer:
71
72
  self.track_colors = {}
72
73
  self.color_palette = self._generate_color_palette()
73
74
  self._temp_dir = None
74
-
75
+
75
76
  def _generate_color_palette(self, num_colors: int = 100) -> List[Tuple[int, int, int]]:
76
77
  """
77
78
  Generate bright, distinct color palette for track visualization.
@@ -88,11 +89,11 @@ class TrackingVisualizer:
88
89
  bgr_color = cv2.cvtColor(hsv_color, cv2.COLOR_HSV2BGR)[0][0]
89
90
  colors.append(tuple(map(int, bgr_color)))
90
91
  return colors
91
-
92
+
92
93
  def _get_track_color(self, track_id: int) -> Tuple[int, int, int]:
93
94
  """Get consistent color for track ID from palette."""
94
95
  return self.color_palette[track_id % len(self.color_palette)]
95
-
96
+
96
97
  def _get_video_info(self, video_path: Path) -> Tuple[int, int, float, int]:
97
98
  """
98
99
  Get video metadata using ffmpeg.
@@ -104,13 +105,13 @@ class TrackingVisualizer:
104
105
  probe = ffmpeg.probe(str(video_path))
105
106
  video_stream = next((stream for stream in probe['streams']
106
107
  if stream['codec_type'] == 'video'), None)
107
-
108
+
108
109
  if video_stream is None:
109
110
  raise ValueError(f"No video stream found in: {video_path}")
110
-
111
+
111
112
  width = int(video_stream['width'])
112
113
  height = int(video_stream['height'])
113
-
114
+
114
115
  # Extract FPS
115
116
  fps_str = video_stream.get('r_frame_rate', '30/1')
116
117
  if '/' in fps_str:
@@ -118,19 +119,19 @@ class TrackingVisualizer:
118
119
  fps = num / den if den != 0 else 30.0
119
120
  else:
120
121
  fps = float(fps_str)
121
-
122
+
122
123
  # Get total frames
123
124
  total_frames = int(video_stream.get('nb_frames', 0))
124
125
  if total_frames == 0:
125
126
  # Fallback: estimate from duration and fps
126
127
  duration = float(video_stream.get('duration', 0))
127
128
  total_frames = int(duration * fps) if duration > 0 else 0
128
-
129
+
129
130
  return width, height, fps, total_frames
130
-
131
+
131
132
  except Exception as e:
132
133
  raise ValueError(f"Could not read video metadata {video_path}: {str(e)}")
133
-
134
+
134
135
  def _create_frame_iterator(self, source: Union[str, Path]) -> Iterator[Tuple[int, np.ndarray]]:
135
136
  """
136
137
  Create iterator that yields (frame_index, frame) tuples.
@@ -142,38 +143,38 @@ class TrackingVisualizer:
142
143
  Tuple of (frame_index, frame_array)
143
144
  """
144
145
  source = Path(source)
145
-
146
+
146
147
  if source.is_file():
147
148
  yield from self._iterate_video_frames(source)
148
149
  elif source.is_dir():
149
150
  yield from self._iterate_directory_frames(source)
150
151
  else:
151
152
  raise ValueError(f"Source must be a video file or directory, got: {source}")
152
-
153
+
153
154
  def _iterate_video_frames(self, video_path: Path) -> Iterator[Tuple[int, np.ndarray]]:
154
155
  """Iterate through video frames using ffmpeg."""
155
156
  width, height, fps, total_frames = self._get_video_info(video_path)
156
-
157
+
157
158
  # Store video info for later use
158
159
  self.source_fps = fps
159
160
  self.frame_size = (width, height)
160
-
161
+
161
162
  process = (
162
163
  ffmpeg
163
164
  .input(str(video_path))
164
165
  .output('pipe:', format='rawvideo', pix_fmt='bgr24', loglevel='quiet')
165
166
  .run_async(pipe_stdout=True, pipe_stderr=False)
166
167
  )
167
-
168
+
168
169
  try:
169
170
  frame_size_bytes = width * height * 3
170
171
  frame_idx = 0
171
-
172
+
172
173
  while True:
173
174
  frame_data = process.stdout.read(frame_size_bytes)
174
175
  if len(frame_data) != frame_size_bytes:
175
176
  break
176
-
177
+
177
178
  frame = np.frombuffer(frame_data, np.uint8).reshape([height, width, 3])
178
179
  yield frame_idx, frame
179
180
  frame_idx += 1
@@ -186,26 +187,26 @@ class TrackingVisualizer:
186
187
  if process.stderr:
187
188
  process.stderr.close()
188
189
  process.wait()
189
-
190
+
190
191
  def _iterate_directory_frames(self, frames_dir: Path) -> Iterator[Tuple[int, np.ndarray]]:
191
192
  """Iterate through image frames in directory."""
192
193
  if not frames_dir.is_dir():
193
194
  raise ValueError(f"Directory does not exist: {frames_dir}")
194
-
195
+
195
196
  # Support common image extensions
196
197
  extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.tiff']
197
198
  image_files = []
198
199
  for ext in extensions:
199
200
  image_files.extend(frames_dir.glob(f'*{ext}'))
200
201
  image_files.extend(frames_dir.glob(f'*{ext.upper()}'))
201
-
202
+
202
203
  image_files = sorted(image_files)
203
204
  if not image_files:
204
205
  raise ValueError(f"No image files found in directory: {frames_dir}")
205
-
206
+
206
207
  # Set fps from config for image sequences
207
208
  self.source_fps = self.output_fps
208
-
209
+
209
210
  for frame_idx, img_path in enumerate(image_files):
210
211
  frame = cv2.imread(str(img_path))
211
212
  if frame is not None:
@@ -215,7 +216,7 @@ class TrackingVisualizer:
215
216
  yield frame_idx, frame
216
217
  else:
217
218
  logger.warning(f"Could not read image: {img_path}")
218
-
219
+
219
220
  def _extract_tracks_from_annotation(self) -> None:
220
221
  """
221
222
  Extract tracking data from Supervisely VideoAnnotation.
@@ -224,29 +225,22 @@ class TrackingVisualizer:
224
225
  """
225
226
  self.tracks_by_frame = defaultdict(list)
226
227
  self.track_colors = {}
227
-
228
+
228
229
  # Map object keys to track info
229
230
  objects = {}
230
231
  for i, obj in enumerate(self.annotation.objects):
231
232
  objects[obj.key] = (i, obj.obj_class.name)
232
-
233
+
233
234
  # Extract tracks from frames
234
235
  for frame in self.annotation.frames:
235
236
  frame_idx = frame.index
236
237
  for figure in frame.figures:
237
- if figure.geometry.geometry_name() != 'rectangle':
238
- continue
239
-
240
238
  object_key = figure.parent_object.key
241
239
  if object_key not in objects:
242
240
  continue
243
-
241
+
244
242
  track_id, class_name = objects[object_key]
245
-
246
- # Extract bbox coordinates
247
- rect = figure.geometry
248
- bbox = (rect.left, rect.top, rect.right, rect.bottom)
249
-
243
+
250
244
  if track_id not in self.track_colors:
251
245
  if self.colorize_tracks:
252
246
  # auto-color override everything
@@ -263,26 +257,30 @@ class TrackingVisualizer:
263
257
 
264
258
  self.track_colors[track_id] = color
265
259
 
266
-
267
- self.tracks_by_frame[frame_idx].append((track_id, bbox, class_name))
268
-
260
+ self.tracks_by_frame[frame_idx].append((track_id, figure.geometry, class_name))
261
+
269
262
  logger.info(f"Extracted tracks from {len(self.tracks_by_frame)} frames")
270
-
271
- def _draw_detection(self, img: np.ndarray, track_id: int, bbox: Tuple[int, int, int, int],
272
- class_name: str) -> Optional[Tuple[int, int]]:
263
+
264
+ def _draw_detection(
265
+ self, img: np.ndarray, track_id: int, geometry: Geometry, class_name: str
266
+ ) -> Optional[Tuple[int, int]]:
273
267
  """
274
268
  Draw single detection with track ID and class label.
275
269
  Returns the center point of the bbox for trajectory drawing.
276
270
  """
271
+ rect = geometry.to_bbox()
272
+ bbox = (rect.left, rect.top, rect.right, rect.bottom)
273
+
277
274
  x1, y1, x2, y2 = map(int, bbox)
278
275
 
279
276
  if x2 <= x1 or y2 <= y1:
280
277
  return None
281
-
278
+
282
279
  color = self.track_colors[track_id]
283
280
 
284
281
  # Draw bounding box
285
- cv2.rectangle(img, (x1, y1), (x2, y2), color, self.box_thickness)
282
+ geometry.draw_contour(img, color=color, thickness=self.box_thickness)
283
+ # cv2.rectangle(img, (x1, y1), (x2, y2), color, self.box_thickness)
286
284
 
287
285
  # Draw label if enabled
288
286
  if self.show_labels:
@@ -304,7 +302,6 @@ class TrackingVisualizer:
304
302
  # Return center point for trajectory
305
303
  return (x1 + x2) // 2, (y1 + y2) // 2
306
304
 
307
-
308
305
  def _draw_trajectories(self, img: np.ndarray) -> None:
309
306
  """Draw trajectory lines for all tracks, filtering out big jumps."""
310
307
  if not self.show_trajectories:
@@ -312,24 +309,24 @@ class TrackingVisualizer:
312
309
 
313
310
  max_jump = 200
314
311
 
315
- for track_id, centers in self.track_centers.items():
316
- if len(centers) < 2:
312
+ for centers_with_colors in self.track_centers.values():
313
+
314
+ if len(centers_with_colors) < 2:
317
315
  continue
318
316
 
319
- color = self.track_colors[track_id]
320
- points = centers[-self.trajectory_length:]
317
+ points, colors = zip(*centers_with_colors[-self.trajectory_length :])
321
318
 
322
319
  for i in range(1, len(points)):
320
+ color = colors[i]
323
321
  p1, p2 = points[i - 1], points[i]
324
322
  if p1 is None or p2 is None:
325
323
  continue
326
-
324
+
327
325
  if np.hypot(p2[0] - p1[0], p2[1] - p1[1]) > max_jump:
328
326
  continue
329
- cv2.line(img, p1, p2, color, 2)
327
+ cv2.line(img, p1, p2, color, self.trajectory_thickness)
330
328
  cv2.circle(img, p1, 3, color, -1)
331
329
 
332
-
333
330
  def _process_single_frame(self, frame: np.ndarray, frame_idx: int) -> np.ndarray:
334
331
  """
335
332
  Process single frame: add annotations and return processed frame.
@@ -345,29 +342,30 @@ class TrackingVisualizer:
345
342
  active_ids = set()
346
343
  # Draw detections for current frame
347
344
  if frame_idx in self.tracks_by_frame:
348
- for track_id, bbox, class_name in self.tracks_by_frame[frame_idx]:
349
- center = self._draw_detection(img, track_id, bbox, class_name)
350
- self.track_centers[track_id].append(center)
345
+ for track_id, geometry, class_name in self.tracks_by_frame[frame_idx]:
346
+ center = self._draw_detection(img, track_id, geometry, class_name)
347
+ color = self.track_colors[track_id]
348
+ self.track_centers[track_id].append((center, color))
351
349
  if len(self.track_centers[track_id]) > self.trajectory_length:
352
350
  self.track_centers[track_id].pop(0)
353
351
  active_ids.add(track_id)
354
-
352
+
355
353
  for tid in self.track_centers.keys():
356
354
  if tid not in active_ids:
357
- self.track_centers[tid].append(None)
355
+ self.track_centers[tid].append((None, None))
358
356
  if len(self.track_centers[tid]) > self.trajectory_length:
359
357
  self.track_centers[tid].pop(0)
360
-
358
+
361
359
  # Draw trajectories
362
360
  self._draw_trajectories(img)
363
-
361
+
364
362
  # Add frame number if requested
365
363
  if self.show_frame_number:
366
364
  cv2.putText(img, f"Frame: {frame_idx + 1}", (10, 30),
367
365
  cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2, cv2.LINE_AA)
368
-
366
+
369
367
  return img
370
-
368
+
371
369
  def _save_processed_frame(self, frame: np.ndarray, frame_idx: int) -> str:
372
370
  """
373
371
  Save processed frame to temporary directory.
@@ -382,7 +380,7 @@ class TrackingVisualizer:
382
380
  frame_path = self._temp_dir / f"frame_{frame_idx:08d}.jpg"
383
381
  cv2.imwrite(str(frame_path), frame, [cv2.IMWRITE_JPEG_QUALITY, 95])
384
382
  return str(frame_path)
385
-
383
+
386
384
  def _create_video_from_frames(self, output_path: Union[str, Path]) -> None:
387
385
  """
388
386
  Create final video from processed frames using ffmpeg.
@@ -392,10 +390,10 @@ class TrackingVisualizer:
392
390
  """
393
391
  output_path = Path(output_path)
394
392
  output_path.parent.mkdir(parents=True, exist_ok=True)
395
-
393
+
396
394
  # Create video from frame sequence
397
395
  input_pattern = str(self._temp_dir / "frame_%08d.jpg")
398
-
396
+
399
397
  try:
400
398
  (
401
399
  ffmpeg
@@ -405,17 +403,17 @@ class TrackingVisualizer:
405
403
  .run(capture_stdout=True, capture_stderr=True)
406
404
  )
407
405
  logger.info(f"Video saved to {output_path}")
408
-
406
+
409
407
  except ffmpeg.Error as e:
410
408
  error_msg = e.stderr.decode() if e.stderr else "Unknown ffmpeg error"
411
409
  raise ValueError(f"Failed to create video: {error_msg}")
412
-
410
+
413
411
  def _cleanup_temp_directory(self) -> None:
414
412
  """Clean up temporary directory and all its contents."""
415
413
  if self._temp_dir and self._temp_dir.exists():
416
414
  shutil.rmtree(self._temp_dir)
417
415
  self._temp_dir = None
418
-
416
+
419
417
  def visualize_video_annotation(self, annotation: VideoAnnotation,
420
418
  source: Union[str, Path],
421
419
  output_path: Union[str, Path]) -> None:
@@ -433,43 +431,43 @@ class TrackingVisualizer:
433
431
  """
434
432
  if not isinstance(annotation, VideoAnnotation):
435
433
  raise TypeError(f"Annotation must be VideoAnnotation, got {type(annotation)}")
436
-
434
+
437
435
  # Store annotation
438
436
  self.annotation = annotation
439
-
437
+
440
438
  # Create temporary directory for processed frames
441
439
  self._temp_dir = Path(tempfile.mkdtemp(prefix="video_viz_"))
442
-
440
+
443
441
  try:
444
442
  # Extract tracking data
445
443
  self._extract_tracks_from_annotation()
446
-
444
+
447
445
  if not self.tracks_by_frame:
448
446
  logger.warning("No tracking data found in annotation")
449
-
447
+
450
448
  # Reset trajectory tracking
451
449
  self.track_centers = defaultdict(list)
452
-
450
+
453
451
  # Process frames one by one
454
452
  frame_count = 0
455
453
  for frame_idx, frame in self._create_frame_iterator(source):
456
454
  # Process frame
457
455
  processed_frame = self._process_single_frame(frame, frame_idx)
458
-
456
+
459
457
  # Save processed frame
460
458
  self._save_processed_frame(processed_frame, frame_idx)
461
-
459
+
462
460
  frame_count += 1
463
-
461
+
464
462
  # Progress logging
465
463
  if frame_count % 100 == 0:
466
464
  logger.info(f"Processed {frame_count} frames")
467
-
465
+
468
466
  logger.info(f"Finished processing {frame_count} frames")
469
-
467
+
470
468
  # Create final video from saved frames
471
469
  self._create_video_from_frames(output_path)
472
-
470
+
473
471
  finally:
474
472
  # Always cleanup temporary files
475
473
  self._cleanup_temp_directory()
@@ -477,7 +475,7 @@ class TrackingVisualizer:
477
475
  def __del__(self):
478
476
  """Cleanup temporary directory on object destruction."""
479
477
  self._cleanup_temp_directory()
480
-
478
+
481
479
 
482
480
  def visualize(
483
481
  predictions: Union[VideoAnnotation, List[Prediction]],
@@ -519,4 +517,3 @@ def visualize(
519
517
  visualizer.visualize_video_annotation(predictions, source, output_path)
520
518
  else:
521
519
  raise TypeError(f"Predictions must be VideoAnnotation or list of Prediction, got {type(predictions)}")
522
-
@@ -51,7 +51,19 @@ class ClassesSelector:
51
51
  text=f"<i class='zmdi zmdi-chart-donut' style='color: #7f858e'></i> <a href='{qa_stats_link}' target='_blank'> <b> QA & Stats </b></a>"
52
52
  )
53
53
 
54
- self.classes_table = ClassesTable(project_id=project_id)
54
+ models = model_selector.models
55
+ task_types = [model["meta"]["task_type"] for model in models]
56
+ task_types = list(set(task_types))
57
+ allowed_types = []
58
+ for task_type in task_types:
59
+ if task_type.endswith("detection"):
60
+ allowed_types.append(Rectangle)
61
+ elif task_type.endswith("segmentation"):
62
+ allowed_types.extend([Bitmap, Polygon])
63
+ elif task_type == TaskType.POSE_ESTIMATION:
64
+ allowed_types.append(GraphNodes)
65
+
66
+ self.classes_table = ClassesTable(project_id=project_id, allowed_types=allowed_types)
55
67
  if len(classes) > 0:
56
68
  self.classes_table.select_classes(classes)
57
69
  else:
@@ -107,6 +119,9 @@ class ClassesSelector:
107
119
  TaskType.INSTANCE_SEGMENTATION: {Bitmap},
108
120
  TaskType.SEMANTIC_SEGMENTATION: {Bitmap},
109
121
  TaskType.POSE_ESTIMATION: {GraphNodes},
122
+ TaskType.PROMPTABLE_SEGMENTATION: {Bitmap},
123
+ TaskType.INTERACTIVE_SEGMENTATION: {Bitmap},
124
+ TaskType.PROMPT_BASED_OBJECT_DETECTION: {Rectangle},
110
125
  }
111
126
 
112
127
  if task_type not in allowed_shapes:
@@ -43,6 +43,7 @@ from supervisely import (
43
43
  logger,
44
44
  )
45
45
  from supervisely._utils import abs_url, get_filename_from_headers
46
+ from supervisely.api.entities_collection_api import EntitiesCollectionInfo
46
47
  from supervisely.api.file_api import FileInfo
47
48
  from supervisely.app import get_synced_data_dir, show_dialog
48
49
  from supervisely.app.widgets import Progress
@@ -72,7 +73,6 @@ from supervisely.project.download import (
72
73
  is_cached,
73
74
  )
74
75
  from supervisely.template.experiment.experiment_generator import ExperimentGenerator
75
- from supervisely.api.entities_collection_api import EntitiesCollectionInfo
76
76
 
77
77
 
78
78
  class TrainApp:
@@ -3162,8 +3162,11 @@ class TrainApp:
3162
3162
 
3163
3163
  # Case 1: Use existing collections for training. No need to create new collections
3164
3164
  split_method = self.gui.train_val_splits_selector.get_split_method()
3165
+ self.gui.train_val_splits_selector._parse_collections()
3165
3166
  all_train_collections = self.gui.train_val_splits_selector.all_train_collections
3166
3167
  all_val_collections = self.gui.train_val_splits_selector.all_val_collections
3168
+ latest_train_collection = self.gui.train_val_splits_selector.latest_train_collection
3169
+ latest_val_collection = self.gui.train_val_splits_selector.latest_val_collection
3167
3170
  if split_method == "Based on collections":
3168
3171
  current_selected_train_collection_ids = self.gui.train_val_splits_selector.train_val_splits.get_train_collections_ids()
3169
3172
  train_match = _check_match(current_selected_train_collection_ids, all_train_collections)
@@ -3178,9 +3181,6 @@ class TrainApp:
3178
3181
  # ------------------------------------------------------------ #
3179
3182
 
3180
3183
  # Case 2: Create new collections for selected train val splits. Need to create new collections
3181
- item_type = self.project_info.type
3182
- experiment_name = self.gui.training_process.get_experiment_name()
3183
-
3184
3184
  train_collection_idx = 1
3185
3185
  val_collection_idx = 1
3186
3186
 
@@ -3191,42 +3191,41 @@ class TrainApp:
3191
3191
  return None
3192
3192
 
3193
3193
  # Get train collection with max idx
3194
- if len(all_train_collections) > 0:
3195
- train_indices = [_extract_index_from_col_name(collection.name, "train") for collection in all_train_collections]
3196
- train_indices = [idx for idx in train_indices if idx is not None]
3197
- if len(train_indices) > 0:
3198
- train_collection_idx = max(train_indices) + 1
3194
+ if latest_train_collection:
3195
+ train_collection_idx = (
3196
+ _extract_index_from_col_name(latest_train_collection.name, "train") + 1
3197
+ )
3199
3198
 
3200
3199
  # Get val collection with max idx
3201
- if len(all_val_collections) > 0:
3202
- val_indices = [_extract_index_from_col_name(collection.name, "val") for collection in all_val_collections]
3203
- val_indices = [idx for idx in val_indices if idx is not None]
3204
- if len(val_indices) > 0:
3205
- val_collection_idx = max(val_indices) + 1
3200
+ if latest_val_collection:
3201
+ val_collection_idx = _extract_index_from_col_name(latest_val_collection.name, "val") + 1
3206
3202
  # -------------------------------- #
3207
3203
 
3208
3204
  # Create Train Collection
3209
3205
  train_img_ids = list(self._train_split_item_ids)
3210
- train_collection_description = f"Collection with train {item_type} for experiment: {experiment_name}"
3211
- train_collection = self._api.entities_collection.create(self.project_id, f"train_{train_collection_idx:03d}", train_collection_description)
3212
- train_collection_id = getattr(train_collection, "id", None)
3213
- if train_collection_id is None:
3214
- raise AttributeError("Train EntitiesCollectionInfo object does not have 'id' attribute")
3215
- self._api.entities_collection.add_items(train_collection_id, train_img_ids)
3216
- self._train_collection_id = train_collection_id
3206
+ self._train_collection_id = self._create_collection("train", train_collection_idx)
3207
+ self._api.entities_collection.add_items(self._train_collection_id, train_img_ids)
3217
3208
 
3218
3209
  # Create Val Collection
3219
3210
  val_img_ids = list(self._val_split_item_ids)
3220
- val_collection_description = f"Collection with val {item_type} for experiment: {experiment_name}"
3221
- val_collection = self._api.entities_collection.create(self.project_id, f"val_{val_collection_idx:03d}", val_collection_description)
3222
- val_collection_id = getattr(val_collection, "id", None)
3223
- if val_collection_id is None:
3224
- raise AttributeError("Val EntitiesCollectionInfo object does not have 'id' attribute")
3225
- self._api.entities_collection.add_items(val_collection_id, val_img_ids)
3226
- self._val_collection_id = val_collection_id
3211
+ self._val_collection_id = self._create_collection("val", val_collection_idx)
3212
+ self._api.entities_collection.add_items(self._val_collection_id, val_img_ids)
3227
3213
 
3228
3214
  # Update Project Custom Data
3229
- self._update_project_custom_data(train_collection_id, val_collection_id)
3215
+ self._update_project_custom_data(self._train_collection_id, self._val_collection_id)
3216
+
3217
+ def _create_collection(self, split_type: str, suffix: int) -> int:
3218
+ experiment_name = self.gui.training_process.get_experiment_name()
3219
+ description = f"Collection with {split_type} {self.project_info.type} for experiment: {experiment_name}"
3220
+ collection = self._api.entities_collection.create(
3221
+ project_id=self.project_id,
3222
+ name=f"{split_type}_{suffix:03d}",
3223
+ description=description,
3224
+ change_name_if_conflict=True,
3225
+ )
3226
+ if collection is None or collection.id is None: # pylint: disable=no-member
3227
+ raise RuntimeError(f"Failed to create {split_type} collection")
3228
+ return collection.id # pylint: disable=no-member
3230
3229
 
3231
3230
  def _update_project_custom_data(self, train_collection_id: int, val_collection_id: int):
3232
3231
  train_info = {