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,520 @@
1
+ import shutil
2
+ import tempfile
3
+ from collections import defaultdict
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import Dict, Iterator, List, Optional, Tuple, Union
7
+
8
+ import cv2
9
+ import ffmpeg
10
+ import numpy as np
11
+
12
+ import supervisely as sly
13
+ from supervisely import VideoAnnotation, logger
14
+ from supervisely.nn.model.prediction import Prediction
15
+ from supervisely.nn.tracker.utils import predictions_to_video_annotation
16
+
17
+
18
+ class TrackingVisualizer:
19
+
20
+ def __init__(
21
+ self,
22
+ show_labels: bool = True,
23
+ show_classes: bool = True,
24
+ show_trajectories: bool = True,
25
+ show_frame_number: bool = False,
26
+ box_thickness: int = 2,
27
+ text_scale: float = 0.6,
28
+ text_thickness: int = 2,
29
+ trajectory_length: int = 30,
30
+ codec: str = "mp4",
31
+ output_fps: float = 30.0,
32
+ colorize_tracks: bool = True,
33
+ trajectory_thickness: int = 2,
34
+ ):
35
+ """
36
+ Initialize the visualizer with configuration.
37
+
38
+ Args:
39
+ show_labels: Whether to show track IDs.
40
+ show_classes: Whether to show class names.
41
+ show_trajectories: Whether to draw trajectories.
42
+ show_frame_number: Whether to overlay frame number.
43
+ box_thickness: Thickness of bounding boxes.
44
+ text_scale: Scale of label text.
45
+ text_thickness: Thickness of label text.
46
+ trajectory_length: How many points to keep in trajectory.
47
+ codec: Output video codec.
48
+ output_fps: Output video framerate.
49
+ colorize_tracks (bool, default=True): if True, ignore colors from project meta and generate new colors for each tracked object; if False, try to use colors from project meta when possible.
50
+ """
51
+ # Visualization settings
52
+ self.show_labels = show_labels
53
+ self.show_classes = show_classes
54
+ self.show_trajectories = show_trajectories
55
+ self.show_frame_number = show_frame_number
56
+
57
+ # Style settings
58
+ self.box_thickness = box_thickness
59
+ self.text_scale = text_scale
60
+ self.text_thickness = text_thickness
61
+ self.trajectory_length = trajectory_length
62
+ self.trajectory_thickness = trajectory_thickness
63
+ self.colorize_tracks = colorize_tracks
64
+
65
+ # Output settings
66
+ self.codec = codec
67
+ self.output_fps = output_fps
68
+
69
+ # Internal state
70
+ self.annotation = None
71
+ self.tracks_by_frame = {}
72
+ self.track_centers = defaultdict(list)
73
+ self.track_colors = {}
74
+ self.color_palette = self._generate_color_palette()
75
+ self._temp_dir = None
76
+
77
+ def _generate_color_palette(self, num_colors: int = 100) -> List[Tuple[int, int, int]]:
78
+ """
79
+ Generate bright, distinct color palette for track visualization.
80
+ Uses HSV space with random hue and fixed high saturation/value.
81
+ """
82
+ np.random.seed(42)
83
+ colors = []
84
+ for i in range(num_colors):
85
+ hue = np.random.randint(0, 180)
86
+ saturation = 200 + np.random.randint(55)
87
+ value = 200 + np.random.randint(55)
88
+
89
+ hsv_color = np.uint8([[[hue, saturation, value]]])
90
+ bgr_color = cv2.cvtColor(hsv_color, cv2.COLOR_HSV2BGR)[0][0]
91
+ colors.append(tuple(map(int, bgr_color)))
92
+ return colors
93
+
94
+ def _get_track_color(self, track_id: int) -> Tuple[int, int, int]:
95
+ """Get consistent color for track ID from palette."""
96
+ return self.color_palette[track_id % len(self.color_palette)]
97
+
98
+ def _get_video_info(self, video_path: Path) -> Tuple[int, int, float, int]:
99
+ """
100
+ Get video metadata using ffmpeg.
101
+
102
+ Returns:
103
+ Tuple of (width, height, fps, total_frames)
104
+ """
105
+ try:
106
+ probe = ffmpeg.probe(str(video_path))
107
+ video_stream = next((stream for stream in probe['streams']
108
+ if stream['codec_type'] == 'video'), None)
109
+
110
+ if video_stream is None:
111
+ raise ValueError(f"No video stream found in: {video_path}")
112
+
113
+ width = int(video_stream['width'])
114
+ height = int(video_stream['height'])
115
+
116
+ # Extract FPS
117
+ fps_str = video_stream.get('r_frame_rate', '30/1')
118
+ if '/' in fps_str:
119
+ num, den = map(int, fps_str.split('/'))
120
+ fps = num / den if den != 0 else 30.0
121
+ else:
122
+ fps = float(fps_str)
123
+
124
+ # Get total frames
125
+ total_frames = int(video_stream.get('nb_frames', 0))
126
+ if total_frames == 0:
127
+ # Fallback: estimate from duration and fps
128
+ duration = float(video_stream.get('duration', 0))
129
+ total_frames = int(duration * fps) if duration > 0 else 0
130
+
131
+ return width, height, fps, total_frames
132
+
133
+ except Exception as e:
134
+ raise ValueError(f"Could not read video metadata {video_path}: {str(e)}")
135
+
136
+ def _create_frame_iterator(self, source: Union[str, Path]) -> Iterator[Tuple[int, np.ndarray]]:
137
+ """
138
+ Create iterator that yields (frame_index, frame) tuples.
139
+
140
+ Args:
141
+ source: Path to video file or directory with frame images
142
+
143
+ Yields:
144
+ Tuple of (frame_index, frame_array)
145
+ """
146
+ source = Path(source)
147
+
148
+ if source.is_file():
149
+ yield from self._iterate_video_frames(source)
150
+ elif source.is_dir():
151
+ yield from self._iterate_directory_frames(source)
152
+ else:
153
+ raise ValueError(f"Source must be a video file or directory, got: {source}")
154
+
155
+ def _iterate_video_frames(self, video_path: Path) -> Iterator[Tuple[int, np.ndarray]]:
156
+ """Iterate through video frames using ffmpeg."""
157
+ width, height, fps, total_frames = self._get_video_info(video_path)
158
+
159
+ # Store video info for later use
160
+ self.source_fps = fps
161
+ self.frame_size = (width, height)
162
+
163
+ process = (
164
+ ffmpeg
165
+ .input(str(video_path))
166
+ .output('pipe:', format='rawvideo', pix_fmt='bgr24', loglevel='quiet')
167
+ .run_async(pipe_stdout=True, pipe_stderr=False)
168
+ )
169
+
170
+ try:
171
+ frame_size_bytes = width * height * 3
172
+ frame_idx = 0
173
+
174
+ while True:
175
+ frame_data = process.stdout.read(frame_size_bytes)
176
+ if len(frame_data) != frame_size_bytes:
177
+ break
178
+
179
+ frame = np.frombuffer(frame_data, np.uint8).reshape([height, width, 3])
180
+ yield frame_idx, frame
181
+ frame_idx += 1
182
+
183
+ except ffmpeg.Error as e:
184
+ logger.error(f"ffmpeg error: {e.stderr.decode() if e.stderr else str(e)}", exc_info=True)
185
+
186
+ finally:
187
+ process.stdout.close()
188
+ if process.stderr:
189
+ process.stderr.close()
190
+ process.wait()
191
+
192
+ def _iterate_directory_frames(self, frames_dir: Path) -> Iterator[Tuple[int, np.ndarray]]:
193
+ """Iterate through image frames in directory."""
194
+ if not frames_dir.is_dir():
195
+ raise ValueError(f"Directory does not exist: {frames_dir}")
196
+
197
+ # Support common image extensions
198
+ extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.tiff']
199
+ image_files = []
200
+ for ext in extensions:
201
+ image_files.extend(frames_dir.glob(f'*{ext}'))
202
+ image_files.extend(frames_dir.glob(f'*{ext.upper()}'))
203
+
204
+ image_files = sorted(image_files)
205
+ if not image_files:
206
+ raise ValueError(f"No image files found in directory: {frames_dir}")
207
+
208
+ # Set fps from config for image sequences
209
+ self.source_fps = self.output_fps
210
+
211
+ for frame_idx, img_path in enumerate(image_files):
212
+ frame = cv2.imread(str(img_path))
213
+ if frame is not None:
214
+ if frame_idx == 0:
215
+ h, w = frame.shape[:2]
216
+ self.frame_size = (w, h)
217
+ yield frame_idx, frame
218
+ else:
219
+ logger.warning(f"Could not read image: {img_path}")
220
+
221
+ def _extract_tracks_from_annotation(self) -> None:
222
+ """
223
+ Extract tracking data from Supervisely VideoAnnotation.
224
+
225
+ Populates self.tracks_by_frame with frame-indexed tracking data.
226
+ """
227
+ self.tracks_by_frame = defaultdict(list)
228
+ self.track_colors = {}
229
+
230
+ # Map object keys to track info
231
+ objects = {}
232
+ for i, obj in enumerate(self.annotation.objects):
233
+ objects[obj.key] = (i, obj.obj_class.name)
234
+
235
+ # Extract tracks from frames
236
+ for frame in self.annotation.frames:
237
+ frame_idx = frame.index
238
+ for figure in frame.figures:
239
+ if figure.geometry.geometry_name() != 'rectangle':
240
+ continue
241
+
242
+ object_key = figure.parent_object.key
243
+ if object_key not in objects:
244
+ continue
245
+
246
+ track_id, class_name = objects[object_key]
247
+
248
+ # Extract bbox coordinates
249
+ rect = figure.geometry
250
+ bbox = (rect.left, rect.top, rect.right, rect.bottom)
251
+
252
+ if track_id not in self.track_colors:
253
+ if self.colorize_tracks:
254
+ # auto-color override everything
255
+ color = self._get_track_color(track_id)
256
+ else:
257
+ # try to use annotation color
258
+ color = figure.video_object.obj_class.color
259
+ if color:
260
+ # convert rgb → bgr
261
+ color = color[::-1]
262
+ else:
263
+ # fallback to auto-color if annotation missing
264
+ color = self._get_track_color(track_id)
265
+
266
+ self.track_colors[track_id] = color
267
+
268
+ self.tracks_by_frame[frame_idx].append((track_id, bbox, class_name))
269
+
270
+ logger.info(f"Extracted tracks from {len(self.tracks_by_frame)} frames")
271
+
272
+ def _draw_detection(self, img: np.ndarray, track_id: int, bbox: Tuple[int, int, int, int],
273
+ class_name: str) -> Optional[Tuple[int, int]]:
274
+ """
275
+ Draw single detection with track ID and class label.
276
+ Returns the center point of the bbox for trajectory drawing.
277
+ """
278
+ x1, y1, x2, y2 = map(int, bbox)
279
+
280
+ if x2 <= x1 or y2 <= y1:
281
+ return None
282
+
283
+ color = self.track_colors[track_id]
284
+
285
+ # Draw bounding box
286
+ cv2.rectangle(img, (x1, y1), (x2, y2), color, self.box_thickness)
287
+
288
+ # Draw label if enabled
289
+ if self.show_labels:
290
+ label = f"ID:{track_id}"
291
+ if self.show_classes:
292
+ label += f" ({class_name})"
293
+
294
+ label_y = y1 - 10 if y1 > 30 else y2 + 25
295
+ (text_w, text_h), _ = cv2.getTextSize(
296
+ label, cv2.FONT_HERSHEY_SIMPLEX, self.text_scale, self.text_thickness
297
+ )
298
+
299
+ cv2.rectangle(img, (x1, label_y - text_h - 5),
300
+ (x1 + text_w, label_y + 5), color, -1)
301
+ cv2.putText(img, label, (x1, label_y),
302
+ cv2.FONT_HERSHEY_SIMPLEX, self.text_scale,
303
+ (255, 255, 255), self.text_thickness, cv2.LINE_AA)
304
+
305
+ # Return center point for trajectory
306
+ return (x1 + x2) // 2, (y1 + y2) // 2
307
+
308
+ def _draw_trajectories(self, img: np.ndarray) -> None:
309
+ """Draw trajectory lines for all tracks, filtering out big jumps."""
310
+ if not self.show_trajectories:
311
+ return
312
+
313
+ max_jump = 200
314
+
315
+ for track_id, centers in self.track_centers.items():
316
+ if len(centers) < 2:
317
+ continue
318
+
319
+ color = self.track_colors[track_id]
320
+ points = centers[-self.trajectory_length:]
321
+
322
+ for i in range(1, len(points)):
323
+ p1, p2 = points[i - 1], points[i]
324
+ if p1 is None or p2 is None:
325
+ continue
326
+
327
+ if np.hypot(p2[0] - p1[0], p2[1] - p1[1]) > max_jump:
328
+ continue
329
+ cv2.line(img, p1, p2, color, self.trajectory_thickness)
330
+ cv2.circle(img, p1, 3, color, -1)
331
+
332
+ def _process_single_frame(self, frame: np.ndarray, frame_idx: int) -> np.ndarray:
333
+ """
334
+ Process single frame: add annotations and return processed frame.
335
+
336
+ Args:
337
+ frame: Input frame
338
+ frame_idx: Frame index
339
+
340
+ Returns:
341
+ Annotated frame
342
+ """
343
+ img = frame.copy()
344
+ active_ids = set()
345
+ # Draw detections for current frame
346
+ if frame_idx in self.tracks_by_frame:
347
+ for track_id, bbox, class_name in self.tracks_by_frame[frame_idx]:
348
+ center = self._draw_detection(img, track_id, bbox, class_name)
349
+ self.track_centers[track_id].append(center)
350
+ if len(self.track_centers[track_id]) > self.trajectory_length:
351
+ self.track_centers[track_id].pop(0)
352
+ active_ids.add(track_id)
353
+
354
+ for tid in self.track_centers.keys():
355
+ if tid not in active_ids:
356
+ self.track_centers[tid].append(None)
357
+ if len(self.track_centers[tid]) > self.trajectory_length:
358
+ self.track_centers[tid].pop(0)
359
+
360
+ # Draw trajectories
361
+ self._draw_trajectories(img)
362
+
363
+ # Add frame number if requested
364
+ if self.show_frame_number:
365
+ cv2.putText(img, f"Frame: {frame_idx + 1}", (10, 30),
366
+ cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2, cv2.LINE_AA)
367
+
368
+ return img
369
+
370
+ def _save_processed_frame(self, frame: np.ndarray, frame_idx: int) -> str:
371
+ """
372
+ Save processed frame to temporary directory.
373
+
374
+ Args:
375
+ frame: Processed frame
376
+ frame_idx: Frame index
377
+
378
+ Returns:
379
+ Path to saved frame
380
+ """
381
+ frame_path = self._temp_dir / f"frame_{frame_idx:08d}.jpg"
382
+ cv2.imwrite(str(frame_path), frame, [cv2.IMWRITE_JPEG_QUALITY, 95])
383
+ return str(frame_path)
384
+
385
+ def _create_video_from_frames(self, output_path: Union[str, Path]) -> None:
386
+ """
387
+ Create final video from processed frames using ffmpeg.
388
+
389
+ Args:
390
+ output_path: Path for output video
391
+ """
392
+ output_path = Path(output_path)
393
+ output_path.parent.mkdir(parents=True, exist_ok=True)
394
+
395
+ # Create video from frame sequence
396
+ input_pattern = str(self._temp_dir / "frame_%08d.jpg")
397
+
398
+ try:
399
+ (
400
+ ffmpeg
401
+ .input(input_pattern, pattern_type='sequence', framerate=self.source_fps)
402
+ .output(str(output_path), vcodec='libx264', pix_fmt='yuv420p', crf=18)
403
+ .overwrite_output()
404
+ .run(capture_stdout=True, capture_stderr=True)
405
+ )
406
+ logger.info(f"Video saved to {output_path}")
407
+
408
+ except ffmpeg.Error as e:
409
+ error_msg = e.stderr.decode() if e.stderr else "Unknown ffmpeg error"
410
+ raise ValueError(f"Failed to create video: {error_msg}")
411
+
412
+ def _cleanup_temp_directory(self) -> None:
413
+ """Clean up temporary directory and all its contents."""
414
+ if self._temp_dir and self._temp_dir.exists():
415
+ shutil.rmtree(self._temp_dir)
416
+ self._temp_dir = None
417
+
418
+ def visualize_video_annotation(self, annotation: VideoAnnotation,
419
+ source: Union[str, Path],
420
+ output_path: Union[str, Path]) -> None:
421
+ """
422
+ Visualize tracking annotations on video using streaming approach.
423
+
424
+ Args:
425
+ annotation: Supervisely VideoAnnotation object with tracking data
426
+ source: Path to video file or directory containing frame images
427
+ output_path: Path for output video file
428
+
429
+ Raises:
430
+ TypeError: If annotation is not VideoAnnotation
431
+ ValueError: If source is invalid or annotation is empty
432
+ """
433
+ if not isinstance(annotation, VideoAnnotation):
434
+ raise TypeError(f"Annotation must be VideoAnnotation, got {type(annotation)}")
435
+
436
+ # Store annotation
437
+ self.annotation = annotation
438
+
439
+ # Create temporary directory for processed frames
440
+ self._temp_dir = Path(tempfile.mkdtemp(prefix="video_viz_"))
441
+
442
+ try:
443
+ # Extract tracking data
444
+ self._extract_tracks_from_annotation()
445
+
446
+ if not self.tracks_by_frame:
447
+ logger.warning("No tracking data found in annotation")
448
+
449
+ # Reset trajectory tracking
450
+ self.track_centers = defaultdict(list)
451
+
452
+ # Process frames one by one
453
+ frame_count = 0
454
+ for frame_idx, frame in self._create_frame_iterator(source):
455
+ # Process frame
456
+ processed_frame = self._process_single_frame(frame, frame_idx)
457
+
458
+ # Save processed frame
459
+ self._save_processed_frame(processed_frame, frame_idx)
460
+
461
+ frame_count += 1
462
+
463
+ # Progress logging
464
+ if frame_count % 100 == 0:
465
+ logger.info(f"Processed {frame_count} frames")
466
+
467
+ logger.info(f"Finished processing {frame_count} frames")
468
+
469
+ # Create final video from saved frames
470
+ self._create_video_from_frames(output_path)
471
+
472
+ finally:
473
+ # Always cleanup temporary files
474
+ self._cleanup_temp_directory()
475
+
476
+ def __del__(self):
477
+ """Cleanup temporary directory on object destruction."""
478
+ self._cleanup_temp_directory()
479
+
480
+
481
+ def visualize(
482
+ predictions: Union[VideoAnnotation, List[Prediction]],
483
+ source: Union[str, Path],
484
+ output_path: Union[str, Path],
485
+ show_labels: bool = True,
486
+ show_classes: bool = True,
487
+ show_trajectories: bool = True,
488
+ box_thickness: int = 2,
489
+ colorize_tracks: bool = True,
490
+ **kwargs
491
+ ) -> None:
492
+ """
493
+ Visualize tracking results from either VideoAnnotation or list of Prediction.
494
+
495
+ Args:
496
+ predictions (supervisely.VideoAnnotation | List[Prediction]): Tracking data to render; either a Supervisely VideoAnnotation or a list of Prediction objects.
497
+ source (str | Path): Path to an input video file or a directory of sequential frames (e.g., frame_000001.jpg).
498
+ output_path (str | Path): Path to the output video file to be created.
499
+ show_labels (bool, default=True): Draw per-object labels (track IDs).
500
+ show_classes (bool, default=True): Draw class names for each object.
501
+ show_trajectories (bool, default=True): Render object trajectories across frames.
502
+ box_thickness (int, default=2): Bounding-box line thickness in pixels.
503
+ colorize_tracks (bool, default=True): if True, ignore colors from project meta and generate new colors for each tracked object; if False, try to use colors from project meta when possible.
504
+ """
505
+ visualizer = TrackingVisualizer(
506
+ show_labels=show_labels,
507
+ show_classes=show_classes,
508
+ show_trajectories=show_trajectories,
509
+ box_thickness=box_thickness,
510
+ colorize_tracks=colorize_tracks,
511
+ **kwargs
512
+ )
513
+
514
+ if isinstance(predictions, VideoAnnotation):
515
+ visualizer.visualize_video_annotation(predictions, source, output_path)
516
+ elif isinstance(predictions, list):
517
+ predictions = predictions_to_video_annotation(predictions)
518
+ visualizer.visualize_video_annotation(predictions, source, output_path)
519
+ else:
520
+ raise TypeError(f"Predictions must be VideoAnnotation or list of Prediction, got {type(predictions)}")