supervisely 6.73.428__py3-none-any.whl → 6.73.430__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.
@@ -1,9 +1,8 @@
1
- from supervisely.sly_logger import logger
2
-
3
1
  try:
4
2
  from supervisely.nn.tracker.botsort_tracker import BotSortTracker
3
+ from supervisely.nn.tracker.calculate_metrics import TrackingEvaluator, evaluate
4
+ TRACKING_LIBS_INSTALLED = True
5
5
  except ImportError:
6
- logger.error(
7
- "Failed to import tracker modules. Please try install extras with 'pip install supervisely[tracking]'"
8
- )
9
- raise
6
+ TRACKING_LIBS_INSTALLED = False
7
+
8
+ from supervisely.nn.tracker.visualize import TrackingVisualizer, visualize
@@ -426,8 +426,8 @@ class BoTSORT(object):
426
426
  if track.score < self.new_track_thresh:
427
427
  continue
428
428
  # Fill track_id for new detection
429
- detection_track_map[orig_idx]["track_id"] = int(track.track_id)
430
429
  track.activate(self.kalman_filter, self.frame_id)
430
+ detection_track_map[orig_idx]["track_id"] = int(track.track_id)
431
431
  activated_starcks.append(track)
432
432
 
433
433
  """ Step 5: Update state"""
@@ -1,6 +1,5 @@
1
1
  import supervisely as sly
2
2
  from supervisely.nn.tracker.base_tracker import BaseTracker
3
- from supervisely.nn.tracker.botsort.tracker.mc_bot_sort import BoTSORT
4
3
  from supervisely import Annotation, VideoAnnotation
5
4
 
6
5
  from dataclasses import dataclass
@@ -11,6 +10,7 @@ import yaml
11
10
  import os
12
11
  from pathlib import Path
13
12
  from supervisely import logger
13
+ from supervisely.nn.tracker.botsort.tracker.mc_bot_sort import BoTSORT
14
14
 
15
15
 
16
16
  @dataclass
@@ -38,6 +38,13 @@ class BotSortTracker(BaseTracker):
38
38
 
39
39
  def __init__(self, settings: dict = None, device: str = None):
40
40
  super().__init__(settings=settings, device=device)
41
+
42
+ from supervisely.nn.tracker import TRACKING_LIBS_INSTALLED
43
+ if not TRACKING_LIBS_INSTALLED:
44
+ raise ImportError(
45
+ "Tracking dependencies are not installed. "
46
+ "Please install supervisely with `pip install supervisely[tracking]`."
47
+ )
41
48
 
42
49
  # Load default settings from YAML file
43
50
  self.settings = self._load_default_settings()
@@ -232,7 +239,7 @@ class BotSortTracker(BaseTracker):
232
239
 
233
240
  video_object = video_objects[track_id]
234
241
  rect = sly.Rectangle(top=y1, left=x1, bottom=y2, right=x2)
235
- frame_figures.append(sly.VideoFigure(video_object, rect, frame_idx))
242
+ frame_figures.append(sly.VideoFigure(video_object, rect, frame_idx, track_id=str(track_id)))
236
243
 
237
244
  frames.append(sly.Frame(frame_idx, frame_figures))
238
245
 
@@ -0,0 +1,264 @@
1
+ import numpy as np
2
+ from collections import defaultdict
3
+ from typing import Dict, List, Union
4
+
5
+ from scipy.optimize import linear_sum_assignment # pylint: disable=import-error
6
+
7
+ import supervisely as sly
8
+ from supervisely.video_annotation.video_annotation import VideoAnnotation
9
+
10
+ import motmetrics as mm # pylint: disable=import-error
11
+
12
+ class TrackingEvaluator:
13
+ """
14
+ Evaluator for video tracking metrics including MOTA, MOTP, IDF1.
15
+ """
16
+
17
+ def __init__(self, iou_threshold: float = 0.5):
18
+ """Initialize evaluator with IoU threshold for matching."""
19
+ from supervisely.nn.tracker import TRACKING_LIBS_INSTALLED
20
+ if not TRACKING_LIBS_INSTALLED:
21
+ raise ImportError(
22
+ "Tracking dependencies are not installed. "
23
+ "Please install supervisely with `pip install supervisely[tracking]`."
24
+ )
25
+
26
+ if not 0.0 <= iou_threshold <= 1.0:
27
+ raise ValueError("iou_threshold must be in [0.0, 1.0]")
28
+ self.iou_threshold = iou_threshold
29
+
30
+ def evaluate(
31
+ self,
32
+ gt_annotation: VideoAnnotation,
33
+ pred_annotation: VideoAnnotation,
34
+ ) -> Dict[str, Union[float, int]]:
35
+ """Main entry: extract tracks from annotations, compute basic and MOT metrics, return results."""
36
+ self._validate_annotations(gt_annotation, pred_annotation)
37
+ self.img_height, self.img_width = gt_annotation.img_size
38
+
39
+ gt_tracks = self._extract_tracks(gt_annotation)
40
+ pred_tracks = self._extract_tracks(pred_annotation)
41
+
42
+ basic = self._compute_basic_metrics(gt_tracks, pred_tracks)
43
+ mot = self._compute_mot_metrics(gt_tracks, pred_tracks)
44
+
45
+ results = {
46
+ # basic detection
47
+ "precision": basic["precision"],
48
+ "recall": basic["recall"],
49
+ "f1": basic["f1"],
50
+ "avg_iou": basic["avg_iou"],
51
+ "true_positives": basic["tp"],
52
+ "false_positives": basic["fp"],
53
+ "false_negatives": basic["fn"],
54
+ "total_gt_objects": basic["total_gt"],
55
+ "total_pred_objects": basic["total_pred"],
56
+
57
+ # motmetrics
58
+ "mota": mot["mota"],
59
+ "motp": mot["motp"],
60
+ "idf1": mot["idf1"],
61
+ "id_switches": mot["id_switches"],
62
+ "fragmentations": mot["fragmentations"],
63
+ "num_misses": mot["num_misses"],
64
+ "num_false_positives": mot["num_false_positives"],
65
+
66
+ # config
67
+ "iou_threshold": self.iou_threshold,
68
+ }
69
+ return results
70
+
71
+ def _validate_annotations(self, gt: VideoAnnotation, pred: VideoAnnotation):
72
+ """Minimal type validation for annotations."""
73
+ if not isinstance(gt, VideoAnnotation) or not isinstance(pred, VideoAnnotation):
74
+ raise TypeError("gt_annotation and pred_annotation must be VideoAnnotation instances")
75
+
76
+ def _extract_tracks(self, annotation: VideoAnnotation) -> Dict[int, List[Dict]]:
77
+ """
78
+ Extract tracks from a VideoAnnotation into a dict keyed by frame index.
79
+ Each element is a dict: {'track_id': int, 'bbox': [x1,y1,x2,y2], 'confidence': float, 'class_name': str}
80
+ """
81
+ frames_to_tracks = defaultdict(list)
82
+
83
+ for frame in annotation.frames:
84
+ frame_idx = frame.index
85
+ for figure in frame.figures:
86
+ # use track_id if present, otherwise fallback to object's key int
87
+ track_id = int(figure.track_id) if figure.track_id is not None else figure.video_object.key().int
88
+
89
+ bbox = figure.geometry
90
+ if not isinstance(bbox, sly.Rectangle):
91
+ bbox = bbox.to_bbox()
92
+
93
+ x1 = float(bbox.left)
94
+ y1 = float(bbox.top)
95
+ x2 = float(bbox.right)
96
+ y2 = float(bbox.bottom)
97
+
98
+ frames_to_tracks[frame_idx].append({
99
+ "track_id": track_id,
100
+ "bbox": [x1, y1, x2, y2],
101
+ "confidence": float(getattr(figure, "confidence", 1.0)),
102
+ "class_name": figure.video_object.obj_class.name
103
+ })
104
+
105
+ return dict(frames_to_tracks)
106
+
107
+ def _compute_basic_metrics(self, gt_tracks: Dict[int, List[Dict]], pred_tracks: Dict[int, List[Dict]]):
108
+ """
109
+ Compute per-frame true positives / false positives / false negatives and average IoU.
110
+ Matching is performed with Hungarian algorithm (scipy). Matches with IoU < threshold are discarded.
111
+ """
112
+ tp = fp = fn = 0
113
+ total_iou = 0.0
114
+ iou_count = 0
115
+
116
+ frames = sorted(set(list(gt_tracks.keys()) + list(pred_tracks.keys())))
117
+ for f in frames:
118
+ gts = gt_tracks.get(f, [])
119
+ preds = pred_tracks.get(f, [])
120
+
121
+ if not gts and not preds:
122
+ continue
123
+ if not gts:
124
+ fp += len(preds)
125
+ continue
126
+ if not preds:
127
+ fn += len(gts)
128
+ continue
129
+
130
+ gt_boxes = np.array([g["bbox"] for g in gts])
131
+ pred_boxes = np.array([p["bbox"] for p in preds])
132
+
133
+ # get cost matrix from motmetrics (cost = 1 - IoU)
134
+ cost_mat = mm.distances.iou_matrix(gt_boxes, pred_boxes, max_iou=1.0)
135
+ # replace NaNs (if any) with a large cost so Hungarian will avoid them
136
+ cost_for_assignment = np.where(np.isnan(cost_mat), 1e6, cost_mat)
137
+
138
+ # Hungarian assignment (minimize cost -> maximize IoU)
139
+ row_idx, col_idx = linear_sum_assignment(cost_for_assignment)
140
+
141
+ matched_gt = set()
142
+ matched_pred = set()
143
+ for r, c in zip(row_idx, col_idx):
144
+ if r < cost_mat.shape[0] and c < cost_mat.shape[1]:
145
+ # IoU = 1 - cost
146
+ cost_val = cost_mat[r, c]
147
+ if np.isnan(cost_val):
148
+ continue
149
+ iou_val = 1.0 - float(cost_val)
150
+ if iou_val >= self.iou_threshold:
151
+ matched_gt.add(r)
152
+ matched_pred.add(c)
153
+ total_iou += iou_val
154
+ iou_count += 1
155
+
156
+ frame_tp = len(matched_gt)
157
+ frame_fp = len(preds) - len(matched_pred)
158
+ frame_fn = len(gts) - len(matched_gt)
159
+
160
+ tp += frame_tp
161
+ fp += frame_fp
162
+ fn += frame_fn
163
+
164
+ precision = tp / (tp + fp) if (tp + fp) > 0 else 0.0
165
+ recall = tp / (tp + fn) if (tp + fn) > 0 else 0.0
166
+ f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0.0
167
+ avg_iou = total_iou / iou_count if iou_count > 0 else 0.0
168
+
169
+ total_gt = sum(len(v) for v in gt_tracks.values())
170
+ total_pred = sum(len(v) for v in pred_tracks.values())
171
+
172
+ return {
173
+ "precision": precision,
174
+ "recall": recall,
175
+ "f1": f1,
176
+ "avg_iou": avg_iou,
177
+ "tp": tp,
178
+ "fp": fp,
179
+ "fn": fn,
180
+ "total_gt": total_gt,
181
+ "total_pred": total_pred,
182
+ }
183
+
184
+ def _compute_mot_metrics(self, gt_tracks: Dict[int, List[Dict]], pred_tracks: Dict[int, List[Dict]]):
185
+ """
186
+ Use motmetrics.MOTAccumulator to collect associations per frame and compute common MOT metrics.
187
+ Distance matrix is taken directly from motmetrics.distances.iou_matrix (which returns 1 - IoU).
188
+ Pairs with distance > (1 - iou_threshold) are set to infinity to exclude them from matching.
189
+ """
190
+ acc = mm.MOTAccumulator(auto_id=True)
191
+
192
+ frames = sorted(set(list(gt_tracks.keys()) + list(pred_tracks.keys())))
193
+ for f in frames:
194
+ gts = gt_tracks.get(f, [])
195
+ preds = pred_tracks.get(f, [])
196
+
197
+ gt_ids = [g["track_id"] for g in gts]
198
+ pred_ids = [p["track_id"] for p in preds]
199
+
200
+ if gts and preds:
201
+ gt_boxes = np.array([g["bbox"] for g in gts])
202
+ pred_boxes = np.array([p["bbox"] for p in preds])
203
+
204
+ # motmetrics provides a distance matrix (1 - IoU)
205
+ dist_mat = mm.distances.iou_matrix(gt_boxes, pred_boxes, max_iou=1.0)
206
+ # exclude pairs with IoU < threshold => distance > 1 - threshold
207
+ dist_mat = np.array(dist_mat, dtype=float)
208
+ dist_mat[np.isnan(dist_mat)] = np.inf
209
+ dist_mat[dist_mat > (1.0 - self.iou_threshold)] = np.inf
210
+ else:
211
+ dist_mat = np.full((len(gts), len(preds)), np.inf)
212
+
213
+ acc.update(gt_ids, pred_ids, dist_mat)
214
+
215
+ mh = mm.metrics.create()
216
+ summary = mh.compute(
217
+ acc,
218
+ metrics=[
219
+ "mota",
220
+ "motp",
221
+ "idf1",
222
+ "num_switches",
223
+ "num_fragmentations",
224
+ "num_misses",
225
+ "num_false_positives",
226
+ ],
227
+ name="eval",
228
+ )
229
+
230
+ def get_val(col: str, default=0.0):
231
+ if summary.empty or col not in summary.columns:
232
+ return float(default)
233
+ v = summary.iloc[0][col]
234
+ return float(v) if not np.isnan(v) else float(default)
235
+
236
+ return {
237
+ "mota": get_val("mota", 0.0),
238
+ "motp": get_val("motp", 0.0),
239
+ "idf1": get_val("idf1", 0.0),
240
+ "id_switches": int(get_val("num_switches", 0.0)),
241
+ "fragmentations": int(get_val("num_fragmentations", 0.0)),
242
+ "num_misses": int(get_val("num_misses", 0.0)),
243
+ "num_false_positives": int(get_val("num_false_positives", 0.0)),
244
+ }
245
+
246
+
247
+ def evaluate(
248
+ gt_annotation: VideoAnnotation,
249
+ pred_annotation: VideoAnnotation,
250
+ iou_threshold: float = 0.5,
251
+ ) -> Dict[str, Union[float, int]]:
252
+ """
253
+ Evaluate tracking predictions against ground truth.
254
+
255
+ Args:
256
+ gt_annotation: Ground-truth annotation, an object of class supervisely VideoAnnotation containing reference object tracks.
257
+ pred_annotation: Predicted annotation, an object of class supervisely VideoAnnotation to be compared against the ground truth.
258
+ iou_threshold: Minimum Intersection-over-Union required for a detection to be considered a valid match.
259
+
260
+ Returns:
261
+ dict: json with evaluation metrics.
262
+ """
263
+ evaluator = TrackingEvaluator(iou_threshold=iou_threshold)
264
+ return evaluator.evaluate(gt_annotation, pred_annotation)
@@ -0,0 +1,274 @@
1
+
2
+ from typing import List, Union, Dict, Tuple
3
+ from pathlib import Path
4
+ from collections import defaultdict
5
+ import numpy as np
6
+
7
+ import supervisely as sly
8
+ from supervisely.nn.model.prediction import Prediction
9
+ from supervisely import VideoAnnotation
10
+ from supervisely import logger
11
+
12
+
13
+ def predictions_to_video_annotation(
14
+ predictions: List[Prediction],
15
+ ) -> VideoAnnotation:
16
+ """
17
+ Convert list of Prediction objects to VideoAnnotation.
18
+
19
+ Args:
20
+ predictions: List of Prediction objects, one per frame
21
+
22
+ Returns:
23
+ VideoAnnotation object with tracked objects
24
+
25
+ """
26
+
27
+ if not predictions:
28
+ raise ValueError("Empty predictions list provided")
29
+
30
+ frame_shape = predictions[0].annotation.img_size
31
+ img_h, img_w = frame_shape
32
+ video_objects = {}
33
+ frames = []
34
+
35
+ for pred in predictions:
36
+ frame_figures = []
37
+ frame_idx = pred.frame_index
38
+
39
+ # Get data using public properties
40
+ boxes = pred.boxes # Public property - np.array (N, 4) in tlbr format
41
+ classes = pred.classes # Public property - list of class names
42
+ track_ids = pred.track_ids # Public property - can be None
43
+
44
+ # Skip frame if no detections
45
+ if len(boxes) == 0:
46
+ frames.append(sly.Frame(frame_idx, []))
47
+ continue
48
+
49
+ for bbox, class_name, track_id in zip(boxes, classes, track_ids):
50
+ # Clip bbox to image boundaries
51
+ # Note: pred.boxes returns tlbr format (top, left, bottom, right)
52
+ top, left, bottom, right = bbox
53
+ dims = np.array([img_h, img_w, img_h, img_w]) - 1
54
+ top, left, bottom, right = np.clip([top, left, bottom, right], 0, dims)
55
+
56
+ # Convert to integer coordinates
57
+ top, left, bottom, right = int(top), int(left), int(bottom), int(right)
58
+
59
+ # Get or create VideoObject
60
+ if track_id not in video_objects:
61
+ # Find obj_class from prediction annotation
62
+ obj_class = None
63
+ for label in pred.annotation.labels:
64
+ if label.obj_class.name == class_name:
65
+ obj_class = label.obj_class
66
+ break
67
+
68
+ if obj_class is None:
69
+ # Create obj_class if not found (fallback)
70
+ obj_class = sly.ObjClass(class_name, sly.Rectangle)
71
+
72
+ video_objects[track_id] = sly.VideoObject(obj_class)
73
+
74
+ video_object = video_objects[track_id]
75
+ rect = sly.Rectangle(top=top, left=left, bottom=bottom, right=right)
76
+ frame_figures.append(sly.VideoFigure(video_object, rect, frame_idx, track_id=str(track_id)))
77
+
78
+ frames.append(sly.Frame(frame_idx, frame_figures))
79
+
80
+ objects = list(video_objects.values())
81
+
82
+ return VideoAnnotation(
83
+ img_size=frame_shape,
84
+ frames_count=len(predictions),
85
+ objects=sly.VideoObjectCollection(objects),
86
+ frames=sly.FrameCollection(frames)
87
+ )
88
+
89
+ def video_annotation_to_mot(
90
+ annotation: VideoAnnotation,
91
+ output_path: Union[str, Path] = None,
92
+ class_to_id_mapping: Dict[str, int] = None
93
+ ) -> Union[str, List[str]]:
94
+ """
95
+ Convert Supervisely VideoAnnotation to MOT format.
96
+ MOT format: frame_id,track_id,left,top,width,height,confidence,class_id,visibility
97
+ """
98
+ mot_lines = []
99
+
100
+ # Create default class mapping if not provided
101
+ if class_to_id_mapping is None:
102
+ unique_classes = set()
103
+ for frame in annotation.frames:
104
+ for figure in frame.figures:
105
+ unique_classes.add(figure.video_object.obj_class.name)
106
+ class_to_id_mapping = {cls_name: idx + 1 for idx, cls_name in enumerate(sorted(unique_classes))}
107
+
108
+ # Extract tracks
109
+ for frame in annotation.frames:
110
+ frame_id = frame.index + 1 # MOT uses 1-based frame indexing
111
+
112
+ for figure in frame.figures:
113
+ # Get track ID from VideoFigure.track_id (official API)
114
+ if figure.track_id is not None:
115
+ track_id = int(figure.track_id)
116
+ else:
117
+ track_id = figure.video_object.key().int
118
+
119
+ # Get bounding box
120
+ if isinstance(figure.geometry, sly.Rectangle):
121
+ bbox = figure.geometry
122
+ else:
123
+ bbox = figure.geometry.to_bbox()
124
+
125
+ left = bbox.left
126
+ top = bbox.top
127
+ width = bbox.width
128
+ height = bbox.height
129
+
130
+ # Get class ID
131
+ class_name = figure.video_object.obj_class.name
132
+ class_id = class_to_id_mapping.get(class_name, 1)
133
+
134
+ # Get confidence (default)
135
+ confidence = 1.0
136
+
137
+ # Visibility (assume visible)
138
+ visibility = 1
139
+
140
+ # Create MOT line
141
+ mot_line = f"{frame_id},{track_id},{left:.2f},{top:.2f},{width:.2f},{height:.2f},{confidence:.3f},{class_id},{visibility}"
142
+ mot_lines.append(mot_line)
143
+
144
+ # Save to file if path provided
145
+ if output_path:
146
+ output_path = Path(output_path)
147
+ output_path.parent.mkdir(parents=True, exist_ok=True)
148
+
149
+ with open(output_path, 'w') as f:
150
+ for line in mot_lines:
151
+ f.write(line + '\n')
152
+
153
+ logger.info(f"Saved MOT format to: {output_path} ({len(mot_lines)} detections)")
154
+ return str(output_path)
155
+
156
+ return mot_lines
157
+
158
+ def mot_to_video_annotation(
159
+ mot_file_path: Union[str, Path],
160
+ img_size: Tuple[int, int] = (1080, 1920),
161
+ class_mapping: Dict[int, str] = None,
162
+ default_class_name: str = "person"
163
+ ) -> VideoAnnotation:
164
+ """
165
+ Convert MOT format tracking data to Supervisely VideoAnnotation.
166
+ MOT format: frame_id,track_id,left,top,width,height,confidence,class_id,visibility
167
+ """
168
+ mot_file_path = Path(mot_file_path)
169
+
170
+ if not mot_file_path.exists():
171
+ raise FileNotFoundError(f"MOT file not found: {mot_file_path}")
172
+
173
+ logger.info(f"Loading MOT data from: {mot_file_path}")
174
+ logger.info(f"Image size: {img_size} (height, width)")
175
+
176
+ # Default class mapping
177
+ if class_mapping is None:
178
+ class_mapping = {1: default_class_name}
179
+
180
+ # Parse MOT file
181
+ video_objects = {} # track_id -> VideoObject
182
+ frames_data = defaultdict(list) # frame_idx -> list of figures
183
+ max_frame_idx = 0
184
+ img_h, img_w = img_size
185
+
186
+ with open(mot_file_path, 'r') as f:
187
+ for line_num, line in enumerate(f, 1):
188
+ line = line.strip()
189
+ if not line or line.startswith('#'):
190
+ continue
191
+
192
+ try:
193
+ parts = line.split(',')
194
+ if len(parts) < 6: # Minimum required fields
195
+ continue
196
+
197
+ frame_id = int(parts[0])
198
+ track_id = int(parts[1])
199
+ left = float(parts[2])
200
+ top = float(parts[3])
201
+ width = float(parts[4])
202
+ height = float(parts[5])
203
+
204
+ # Optional fields
205
+ confidence = float(parts[6]) if len(parts) > 6 and parts[6] != '-1' else 1.0
206
+ class_id = int(parts[7]) if len(parts) > 7 and parts[7] != '-1' else 1
207
+ visibility = float(parts[8]) if len(parts) > 8 and parts[8] != '-1' else 1.0
208
+
209
+ frame_idx = frame_id - 1 # Convert to 0-based indexing
210
+ max_frame_idx = max(max_frame_idx, frame_idx)
211
+
212
+ # Skip low confidence detections
213
+ if confidence < 0.1:
214
+ continue
215
+
216
+ # Calculate coordinates with safer clipping
217
+ right = left + width
218
+ bottom = top + height
219
+
220
+ # Clip to image boundaries
221
+ left = max(0, int(left))
222
+ top = max(0, int(top))
223
+ right = min(int(right), img_w - 1)
224
+ bottom = min(int(bottom), img_h - 1)
225
+
226
+ # Skip invalid boxes
227
+ if right <= left or bottom <= top:
228
+ continue
229
+
230
+ # Get class name
231
+ class_name = class_mapping.get(class_id, default_class_name)
232
+
233
+ # Create VideoObject if not exists
234
+ if track_id not in video_objects:
235
+ obj_class = sly.ObjClass(class_name, sly.Rectangle)
236
+ video_objects[track_id] = sly.VideoObject(obj_class)
237
+
238
+ video_object = video_objects[track_id]
239
+
240
+ # Create rectangle and figure with track_id
241
+ rect = sly.Rectangle(top=top, left=left, bottom=bottom, right=right)
242
+ figure = sly.VideoFigure(video_object, rect, frame_idx, track_id=str(track_id))
243
+
244
+ frames_data[frame_idx].append(figure)
245
+
246
+ except (ValueError, IndexError) as e:
247
+ logger.warning(f"Skipped invalid MOT line {line_num}: {line} - {e}")
248
+ continue
249
+
250
+ # Create frames
251
+ frames = []
252
+ if frames_data:
253
+ frames_count = max(frames_data.keys()) + 1
254
+
255
+ for frame_idx in range(frames_count):
256
+ figures = frames_data.get(frame_idx, [])
257
+ frames.append(sly.Frame(frame_idx, figures))
258
+ else:
259
+ frames_count = 1
260
+ frames = [sly.Frame(0, [])]
261
+
262
+ # Create VideoAnnotation
263
+ objects = list(video_objects.values())
264
+
265
+ annotation = VideoAnnotation(
266
+ img_size=img_size,
267
+ frames_count=frames_count,
268
+ objects=sly.VideoObjectCollection(objects),
269
+ frames=sly.FrameCollection(frames)
270
+ )
271
+
272
+ logger.info(f"Created VideoAnnotation with {len(objects)} tracks and {frames_count} frames")
273
+
274
+ return annotation
@@ -0,0 +1,519 @@
1
+ from typing import Union, Dict, List, Tuple, Iterator, Optional
2
+ import numpy as np
3
+ import cv2
4
+ import ffmpeg
5
+ from pathlib import Path
6
+ from collections import defaultdict
7
+ from dataclasses import dataclass
8
+ import tempfile
9
+ import shutil
10
+
11
+ import supervisely as sly
12
+ from supervisely import logger
13
+ from supervisely.nn.model.prediction import Prediction
14
+ from supervisely import VideoAnnotation
15
+ from supervisely.nn.tracker.utils import predictions_to_video_annotation
16
+
17
+
18
+ class TrackingVisualizer:
19
+ def __init__(
20
+ self,
21
+ show_labels: bool = True,
22
+ show_classes: bool = True,
23
+ show_trajectories: bool = True,
24
+ show_frame_number: bool = False,
25
+ box_thickness: int = 2,
26
+ text_scale: float = 0.6,
27
+ text_thickness: int = 2,
28
+ trajectory_length: int = 30,
29
+ codec: str = "mp4",
30
+ output_fps: float = 30.0,
31
+ colorize_tracks: bool = True,
32
+
33
+ ):
34
+ """
35
+ Initialize the visualizer with configuration.
36
+
37
+ Args:
38
+ show_labels: Whether to show track IDs.
39
+ show_classes: Whether to show class names.
40
+ show_trajectories: Whether to draw trajectories.
41
+ show_frame_number: Whether to overlay frame number.
42
+ box_thickness: Thickness of bounding boxes.
43
+ text_scale: Scale of label text.
44
+ text_thickness: Thickness of label text.
45
+ trajectory_length: How many points to keep in trajectory.
46
+ codec: Output video codec.
47
+ output_fps: Output video framerate.
48
+ 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.
49
+ """
50
+ # Visualization settings
51
+ self.show_labels = show_labels
52
+ self.show_classes = show_classes
53
+ self.show_trajectories = show_trajectories
54
+ self.show_frame_number = show_frame_number
55
+
56
+ # Style settings
57
+ self.box_thickness = box_thickness
58
+ self.text_scale = text_scale
59
+ self.text_thickness = text_thickness
60
+ self.trajectory_length = trajectory_length
61
+ self.colorize_tracks = colorize_tracks
62
+
63
+ # Output settings
64
+ self.codec = codec
65
+ self.output_fps = output_fps
66
+
67
+ # Internal state
68
+ self.annotation = None
69
+ self.tracks_by_frame = {}
70
+ self.track_centers = defaultdict(list)
71
+ self.track_colors = {}
72
+ self.color_palette = self._generate_color_palette()
73
+ self._temp_dir = None
74
+
75
+ def _generate_color_palette(self, num_colors: int = 100) -> List[Tuple[int, int, int]]:
76
+ """
77
+ Generate bright, distinct color palette for track visualization.
78
+ Uses HSV space with random hue and fixed high saturation/value.
79
+ """
80
+ np.random.seed(42)
81
+ colors = []
82
+ for i in range(num_colors):
83
+ hue = np.random.randint(0, 180)
84
+ saturation = 200 + np.random.randint(55)
85
+ value = 200 + np.random.randint(55)
86
+
87
+ hsv_color = np.uint8([[[hue, saturation, value]]])
88
+ bgr_color = cv2.cvtColor(hsv_color, cv2.COLOR_HSV2BGR)[0][0]
89
+ colors.append(tuple(map(int, bgr_color)))
90
+ return colors
91
+
92
+ def _get_track_color(self, track_id: int) -> Tuple[int, int, int]:
93
+ """Get consistent color for track ID from palette."""
94
+ return self.color_palette[track_id % len(self.color_palette)]
95
+
96
+ def _get_video_info(self, video_path: Path) -> Tuple[int, int, float, int]:
97
+ """
98
+ Get video metadata using ffmpeg.
99
+
100
+ Returns:
101
+ Tuple of (width, height, fps, total_frames)
102
+ """
103
+ try:
104
+ probe = ffmpeg.probe(str(video_path))
105
+ video_stream = next((stream for stream in probe['streams']
106
+ if stream['codec_type'] == 'video'), None)
107
+
108
+ if video_stream is None:
109
+ raise ValueError(f"No video stream found in: {video_path}")
110
+
111
+ width = int(video_stream['width'])
112
+ height = int(video_stream['height'])
113
+
114
+ # Extract FPS
115
+ fps_str = video_stream.get('r_frame_rate', '30/1')
116
+ if '/' in fps_str:
117
+ num, den = map(int, fps_str.split('/'))
118
+ fps = num / den if den != 0 else 30.0
119
+ else:
120
+ fps = float(fps_str)
121
+
122
+ # Get total frames
123
+ total_frames = int(video_stream.get('nb_frames', 0))
124
+ if total_frames == 0:
125
+ # Fallback: estimate from duration and fps
126
+ duration = float(video_stream.get('duration', 0))
127
+ total_frames = int(duration * fps) if duration > 0 else 0
128
+
129
+ return width, height, fps, total_frames
130
+
131
+ except Exception as e:
132
+ raise ValueError(f"Could not read video metadata {video_path}: {str(e)}")
133
+
134
+ def _create_frame_iterator(self, source: Union[str, Path]) -> Iterator[Tuple[int, np.ndarray]]:
135
+ """
136
+ Create iterator that yields (frame_index, frame) tuples.
137
+
138
+ Args:
139
+ source: Path to video file or directory with frame images
140
+
141
+ Yields:
142
+ Tuple of (frame_index, frame_array)
143
+ """
144
+ source = Path(source)
145
+
146
+ if source.is_file():
147
+ yield from self._iterate_video_frames(source)
148
+ elif source.is_dir():
149
+ yield from self._iterate_directory_frames(source)
150
+ else:
151
+ raise ValueError(f"Source must be a video file or directory, got: {source}")
152
+
153
+ def _iterate_video_frames(self, video_path: Path) -> Iterator[Tuple[int, np.ndarray]]:
154
+ """Iterate through video frames using ffmpeg."""
155
+ width, height, fps, total_frames = self._get_video_info(video_path)
156
+
157
+ # Store video info for later use
158
+ self.source_fps = fps
159
+ self.frame_size = (width, height)
160
+
161
+ process = (
162
+ ffmpeg
163
+ .input(str(video_path))
164
+ .output('pipe:', format='rawvideo', pix_fmt='bgr24')
165
+ .run_async(pipe_stdout=True, pipe_stderr=True)
166
+ )
167
+
168
+ try:
169
+ frame_size_bytes = width * height * 3
170
+ frame_idx = 0
171
+
172
+ while True:
173
+ frame_data = process.stdout.read(frame_size_bytes)
174
+ if len(frame_data) != frame_size_bytes:
175
+ break
176
+
177
+ frame = np.frombuffer(frame_data, np.uint8).reshape([height, width, 3])
178
+ yield frame_idx, frame
179
+ frame_idx += 1
180
+
181
+ finally:
182
+ process.stdout.close()
183
+ if process.stderr:
184
+ process.stderr.close()
185
+ process.wait()
186
+
187
+ def _iterate_directory_frames(self, frames_dir: Path) -> Iterator[Tuple[int, np.ndarray]]:
188
+ """Iterate through image frames in directory."""
189
+ if not frames_dir.is_dir():
190
+ raise ValueError(f"Directory does not exist: {frames_dir}")
191
+
192
+ # Support common image extensions
193
+ extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.tiff']
194
+ image_files = []
195
+ for ext in extensions:
196
+ image_files.extend(frames_dir.glob(f'*{ext}'))
197
+ image_files.extend(frames_dir.glob(f'*{ext.upper()}'))
198
+
199
+ image_files = sorted(image_files)
200
+ if not image_files:
201
+ raise ValueError(f"No image files found in directory: {frames_dir}")
202
+
203
+ # Set fps from config for image sequences
204
+ self.source_fps = self.output_fps
205
+
206
+ for frame_idx, img_path in enumerate(image_files):
207
+ frame = cv2.imread(str(img_path))
208
+ if frame is not None:
209
+ if frame_idx == 0:
210
+ h, w = frame.shape[:2]
211
+ self.frame_size = (w, h)
212
+ yield frame_idx, frame
213
+ else:
214
+ logger.warning(f"Could not read image: {img_path}")
215
+
216
+ def _extract_tracks_from_annotation(self) -> None:
217
+ """
218
+ Extract tracking data from Supervisely VideoAnnotation.
219
+
220
+ Populates self.tracks_by_frame with frame-indexed tracking data.
221
+ """
222
+ self.tracks_by_frame = defaultdict(list)
223
+ self.track_colors = {}
224
+
225
+ # Map object keys to track info
226
+ objects = {}
227
+ for i, obj in enumerate(self.annotation.objects):
228
+ objects[obj.key] = (i, obj.obj_class.name)
229
+
230
+ # Extract tracks from frames
231
+ for frame in self.annotation.frames:
232
+ frame_idx = frame.index
233
+ for figure in frame.figures:
234
+ if figure.geometry.geometry_name() != 'rectangle':
235
+ continue
236
+
237
+ object_key = figure.parent_object.key
238
+ if object_key not in objects:
239
+ continue
240
+
241
+ track_id, class_name = objects[object_key]
242
+
243
+ # Extract bbox coordinates
244
+ rect = figure.geometry
245
+ bbox = (rect.left, rect.top, rect.right, rect.bottom)
246
+
247
+ if track_id not in self.track_colors:
248
+ if self.colorize_tracks:
249
+ # auto-color override everything
250
+ color = self._get_track_color(track_id)
251
+ else:
252
+ # try to use annotation color
253
+ color = figure.video_object.obj_class.color
254
+ if color:
255
+ # convert rgb → bgr
256
+ color = color[::-1]
257
+ else:
258
+ # fallback to auto-color if annotation missing
259
+ color = self._get_track_color(track_id)
260
+
261
+ self.track_colors[track_id] = color
262
+
263
+
264
+ self.tracks_by_frame[frame_idx].append((track_id, bbox, class_name))
265
+
266
+ logger.info(f"Extracted tracks from {len(self.tracks_by_frame)} frames")
267
+
268
+ def _draw_detection(self, img: np.ndarray, track_id: int, bbox: Tuple[int, int, int, int],
269
+ class_name: str) -> Optional[Tuple[int, int]]:
270
+ """
271
+ Draw single detection with track ID and class label.
272
+ Returns the center point of the bbox for trajectory drawing.
273
+ """
274
+ x1, y1, x2, y2 = map(int, bbox)
275
+
276
+ if x2 <= x1 or y2 <= y1:
277
+ return None
278
+
279
+ color = self.track_colors[track_id]
280
+
281
+ # Draw bounding box
282
+ cv2.rectangle(img, (x1, y1), (x2, y2), color, self.box_thickness)
283
+
284
+ # Draw label if enabled
285
+ if self.show_labels:
286
+ label = f"ID:{track_id}"
287
+ if self.show_classes:
288
+ label += f" ({class_name})"
289
+
290
+ label_y = y1 - 10 if y1 > 30 else y2 + 25
291
+ (text_w, text_h), _ = cv2.getTextSize(
292
+ label, cv2.FONT_HERSHEY_SIMPLEX, self.text_scale, self.text_thickness
293
+ )
294
+
295
+ cv2.rectangle(img, (x1, label_y - text_h - 5),
296
+ (x1 + text_w, label_y + 5), color, -1)
297
+ cv2.putText(img, label, (x1, label_y),
298
+ cv2.FONT_HERSHEY_SIMPLEX, self.text_scale,
299
+ (255, 255, 255), self.text_thickness, cv2.LINE_AA)
300
+
301
+ # Return center point for trajectory
302
+ return (x1 + x2) // 2, (y1 + y2) // 2
303
+
304
+
305
+ def _draw_trajectories(self, img: np.ndarray) -> None:
306
+ """Draw trajectory lines for all tracks, filtering out big jumps."""
307
+ if not self.show_trajectories:
308
+ return
309
+
310
+ max_jump = 200
311
+
312
+ for track_id, centers in self.track_centers.items():
313
+ if len(centers) < 2:
314
+ continue
315
+
316
+ color = self.track_colors[track_id]
317
+ points = centers[-self.trajectory_length:]
318
+
319
+ for i in range(1, len(points)):
320
+ p1, p2 = points[i - 1], points[i]
321
+ if p1 is None or p2 is None:
322
+ continue
323
+
324
+ if np.hypot(p2[0] - p1[0], p2[1] - p1[1]) > max_jump:
325
+ continue
326
+ cv2.line(img, p1, p2, color, 2)
327
+ cv2.circle(img, p1, 3, color, -1)
328
+
329
+
330
+ def _process_single_frame(self, frame: np.ndarray, frame_idx: int) -> np.ndarray:
331
+ """
332
+ Process single frame: add annotations and return processed frame.
333
+
334
+ Args:
335
+ frame: Input frame
336
+ frame_idx: Frame index
337
+
338
+ Returns:
339
+ Annotated frame
340
+ """
341
+ img = frame.copy()
342
+ active_ids = set()
343
+ # Draw detections for current frame
344
+ if frame_idx in self.tracks_by_frame:
345
+ for track_id, bbox, class_name in self.tracks_by_frame[frame_idx]:
346
+ center = self._draw_detection(img, track_id, bbox, class_name)
347
+ self.track_centers[track_id].append(center)
348
+ if len(self.track_centers[track_id]) > self.trajectory_length:
349
+ self.track_centers[track_id].pop(0)
350
+ active_ids.add(track_id)
351
+
352
+ for tid in self.track_centers.keys():
353
+ if tid not in active_ids:
354
+ self.track_centers[tid].append(None)
355
+ if len(self.track_centers[tid]) > self.trajectory_length:
356
+ self.track_centers[tid].pop(0)
357
+
358
+ # Draw trajectories
359
+ self._draw_trajectories(img)
360
+
361
+ # Add frame number if requested
362
+ if self.show_frame_number:
363
+ cv2.putText(img, f"Frame: {frame_idx + 1}", (10, 30),
364
+ cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2, cv2.LINE_AA)
365
+
366
+ return img
367
+
368
+ def _save_processed_frame(self, frame: np.ndarray, frame_idx: int) -> str:
369
+ """
370
+ Save processed frame to temporary directory.
371
+
372
+ Args:
373
+ frame: Processed frame
374
+ frame_idx: Frame index
375
+
376
+ Returns:
377
+ Path to saved frame
378
+ """
379
+ frame_path = self._temp_dir / f"frame_{frame_idx:08d}.jpg"
380
+ cv2.imwrite(str(frame_path), frame, [cv2.IMWRITE_JPEG_QUALITY, 95])
381
+ return str(frame_path)
382
+
383
+ def _create_video_from_frames(self, output_path: Union[str, Path]) -> None:
384
+ """
385
+ Create final video from processed frames using ffmpeg.
386
+
387
+ Args:
388
+ output_path: Path for output video
389
+ """
390
+ output_path = Path(output_path)
391
+ output_path.parent.mkdir(parents=True, exist_ok=True)
392
+
393
+ # Create video from frame sequence
394
+ input_pattern = str(self._temp_dir / "frame_%08d.jpg")
395
+
396
+ try:
397
+ (
398
+ ffmpeg
399
+ .input(input_pattern, pattern_type='sequence', framerate=self.source_fps)
400
+ .output(str(output_path), vcodec='libx264', pix_fmt='yuv420p', crf=18)
401
+ .overwrite_output()
402
+ .run(capture_stdout=True, capture_stderr=True)
403
+ )
404
+ logger.info(f"Video saved to {output_path}")
405
+
406
+ except ffmpeg.Error as e:
407
+ error_msg = e.stderr.decode() if e.stderr else "Unknown ffmpeg error"
408
+ raise ValueError(f"Failed to create video: {error_msg}")
409
+
410
+ def _cleanup_temp_directory(self) -> None:
411
+ """Clean up temporary directory and all its contents."""
412
+ if self._temp_dir and self._temp_dir.exists():
413
+ shutil.rmtree(self._temp_dir)
414
+ self._temp_dir = None
415
+
416
+ def visualize_video_annotation(self, annotation: VideoAnnotation,
417
+ source: Union[str, Path],
418
+ output_path: Union[str, Path]) -> None:
419
+ """
420
+ Visualize tracking annotations on video using streaming approach.
421
+
422
+ Args:
423
+ annotation: Supervisely VideoAnnotation object with tracking data
424
+ source: Path to video file or directory containing frame images
425
+ output_path: Path for output video file
426
+
427
+ Raises:
428
+ TypeError: If annotation is not VideoAnnotation
429
+ ValueError: If source is invalid or annotation is empty
430
+ """
431
+ if not isinstance(annotation, VideoAnnotation):
432
+ raise TypeError(f"Annotation must be VideoAnnotation, got {type(annotation)}")
433
+
434
+ # Store annotation
435
+ self.annotation = annotation
436
+
437
+ # Create temporary directory for processed frames
438
+ self._temp_dir = Path(tempfile.mkdtemp(prefix="video_viz_"))
439
+
440
+ try:
441
+ # Extract tracking data
442
+ self._extract_tracks_from_annotation()
443
+
444
+ if not self.tracks_by_frame:
445
+ logger.warning("No tracking data found in annotation")
446
+
447
+ # Reset trajectory tracking
448
+ self.track_centers = defaultdict(list)
449
+
450
+ # Process frames one by one
451
+ frame_count = 0
452
+ for frame_idx, frame in self._create_frame_iterator(source):
453
+ # Process frame
454
+ processed_frame = self._process_single_frame(frame, frame_idx)
455
+
456
+ # Save processed frame
457
+ self._save_processed_frame(processed_frame, frame_idx)
458
+
459
+ frame_count += 1
460
+
461
+ # Progress logging
462
+ if frame_count % 100 == 0:
463
+ logger.info(f"Processed {frame_count} frames")
464
+
465
+ logger.info(f"Finished processing {frame_count} frames")
466
+
467
+ # Create final video from saved frames
468
+ self._create_video_from_frames(output_path)
469
+
470
+ finally:
471
+ # Always cleanup temporary files
472
+ self._cleanup_temp_directory()
473
+
474
+ def __del__(self):
475
+ """Cleanup temporary directory on object destruction."""
476
+ self._cleanup_temp_directory()
477
+
478
+
479
+ def visualize(
480
+ predictions: Union[VideoAnnotation, List[Prediction]],
481
+ source: Union[str, Path],
482
+ output_path: Union[str, Path],
483
+ show_labels: bool = True,
484
+ show_classes: bool = True,
485
+ show_trajectories: bool = True,
486
+ box_thickness: int = 2,
487
+ colorize_tracks: bool = True,
488
+ **kwargs
489
+ ) -> None:
490
+ """
491
+ Visualize tracking results from either VideoAnnotation or list of Prediction.
492
+
493
+ Args:
494
+ predictions (supervisely.VideoAnnotation | List[Prediction]): Tracking data to render; either a Supervisely VideoAnnotation or a list of Prediction objects.
495
+ source (str | Path): Path to an input video file or a directory of sequential frames (e.g., frame_000001.jpg).
496
+ output_path (str | Path): Path to the output video file to be created.
497
+ show_labels (bool, default=True): Draw per-object labels (track IDs).
498
+ show_classes (bool, default=True): Draw class names for each object.
499
+ show_trajectories (bool, default=True): Render object trajectories across frames.
500
+ box_thickness (int, default=2): Bounding-box line thickness in pixels.
501
+ 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.
502
+ """
503
+ visualizer = TrackingVisualizer(
504
+ show_labels=show_labels,
505
+ show_classes=show_classes,
506
+ show_trajectories=show_trajectories,
507
+ box_thickness=box_thickness,
508
+ colorize_tracks=colorize_tracks,
509
+ **kwargs
510
+ )
511
+
512
+ if isinstance(predictions, VideoAnnotation):
513
+ visualizer.visualize_video_annotation(predictions, source, output_path)
514
+ elif isinstance(predictions, list):
515
+ predictions = predictions_to_video_annotation(predictions)
516
+ visualizer.visualize_video_annotation(predictions, source, output_path)
517
+ else:
518
+ raise TypeError(f"Predictions must be VideoAnnotation or list of Prediction, got {type(predictions)}")
519
+
@@ -174,6 +174,7 @@ can pass them in the terminal before running the script:
174
174
  model = api.nn.deploy(
175
175
  model="{{ experiment.paths.artifacts_dir.path }}/checkpoints/{{ experiment.training.checkpoints.pytorch.name }}",
176
176
  device="cuda:0", # or "cpu"
177
+ workspace_id={{ experiment.project.workspace_id }}
177
178
  )
178
179
 
179
180
  # 3. Predict
@@ -247,7 +248,7 @@ model = api.nn.deploy(
247
248
  {% endtabs %}
248
249
 
249
250
  > For more information, see [Model
250
- API](https://docs.supervisely.com/neural-networks/overview-1/model-api){:target="_blank"}
251
+ API](https://docs.supervisely.com/neural-networks/inference-and-deployment/model-api){:target="_blank"}
251
252
  documentation.
252
253
 
253
254
  ### Predict via API
@@ -315,55 +316,47 @@ predictions = model.predict(
315
316
  {% endtabs %}
316
317
 
317
318
  > For more information, see [Prediction
318
- API](https://docs.supervisely.com/neural-networks/overview-1/prediction-api){:target="_blank"}.
319
+ API](https://docs.supervisely.com/neural-networks/inference-and-deployment/prediction-api){:target="_blank"}.
319
320
 
320
321
  {% if experiment.model.task_type == "object detection" %}
321
322
 
322
323
  ## Tracking Objects in Video
323
324
 
324
- You can track objects in video using `boxmot` library.
325
- [BoxMot](https://github.com/mikel-brostrom/boxmot){:target="_blank"} is a
326
- third-party library that implements lightweight neural networks for tracking-by-detection task (when the tracking is
327
- performed on the objects predicted by a separate detector). For `boxmot` models you can use even CPU device.
325
+ Supervisely now supports **tracking-by-detection** out of the box. We leverage a lightweight tracking algorithm (such as [BoT-SORT](https://github.com/NirAharon/BoT-SORT){:target="_blank"}) which identifies the unique objects across video frames and assigns IDs to them. This allows us to connect separate detections from different frames into a single track for each object.
328
326
 
329
- First, install [BoxMot](https://github.com/mikel-brostrom/boxmot){:target="_blank"}:
330
-
331
- ```bash
332
- pip install boxmot
333
- ```
334
-
335
- Supervisely SDK has the `track()` method from `supervisely.nn.tracking` which allows you to apply `boxmot` models
336
- together with a detector in a single line of code. This method takes two arguments: a `boxmot` tracker, and a
337
- `PredictionSession` of a detector. It returns a `sly.VideoAnnotation` with the tracked objects.
327
+ To apply tracking via API, first, deploy your detection model or connect to it, and then use the `predict()` method with `tracking=True` parameter. You can also specify tracking configuration parameters by passing `tracking_config={...}` with your custom settings.
338
328
 
339
329
  ```python
340
330
  import supervisely as sly
341
- from supervisely.nn.tracking import track
342
- import boxmot
343
- from pathlib import Path
344
-
345
- # Deploy a detector
346
- detector = api.nn.deploy(
347
- model="{{ experiment.model.framework }}/{{ experiment.model.name }}",
348
- device="cuda:0", # Use GPU for detection
349
- )
350
331
 
351
- # Load BoxMot tracker
352
- tracker = boxmot.BotSort(
353
- reid_weights=Path('osnet_x0_25_msmt17.pt'),
354
- device="cpu", # Use CPU for tracking
332
+ api = sly.Api()
333
+
334
+ # Deploy your model
335
+ model = api.nn.deploy(
336
+ model="{{ experiment.paths.artifacts_dir.path }}/checkpoints/{{ experiment.training.checkpoints.pytorch.name }}",
337
+ device="cuda",
338
+ workspace_id={{ experiment.project.workspace_id }},
355
339
  )
356
340
 
357
- # Track objects in a single line
358
- video_ann: sly.VideoAnnotation = track(
359
- video_id=42,
360
- detector=detector,
361
- tracker=tracker,
341
+ # Apply tracking
342
+ predictions = model.predict(
343
+ video_id=YOUR_VIDEO_ID, # Video ID in Supervisely
344
+ tracking=True,
345
+ tracking_config={
346
+ "tracker": "botsort", # botsort is a powerful tracking algorithm used by default
347
+ # You can pass other tracking parameters here, see the docs for details
348
+ }
362
349
  )
350
+
351
+ # Processing results
352
+ for pred in predictions:
353
+ frame_index = pred.frame_index
354
+ annotation = pred.annotation
355
+ track_ids = pred.track_ids
356
+ print(f"Frame {frame_index}: {len(track_ids)} tracks")
363
357
  ```
364
358
 
365
- > For more information, see the section [Tracking Objects in
366
- Video](https://docs.supervisely.com/neural-networks/overview-1/prediction-api#tracking-objects-in-video){:target="_blank"}.
359
+ > You can also apply trackers in your own code or applications. Read more about this in the docs [Video Object Tracking](https://docs.supervisely.com/neural-networks/inference-and-deployment/video-object-tracking){:target="_blank"}.
367
360
 
368
361
  {% endif %}
369
362
 
@@ -409,7 +402,7 @@ docker image for the corresponding model's framework, and run the `docker run` c
409
402
  )
410
403
  ```
411
404
 
412
- See the [Model API](https://docs.supervisely.com/neural-networks/overview-1/model-api){:target="_blank"} documentation for more details on how to use the `ModelAPI` class.
405
+ See the [Model API](https://docs.supervisely.com/neural-networks/inference-and-deployment/model-api){:target="_blank"} documentation for more details on how to use the `ModelAPI` class.
413
406
 
414
407
  Alternatively, you can use `docker run` with the `predict` action to make predictions in a single command. This is a
415
408
  quick way to start inference on your local images, videos, or directories without deploying the model. The container will be automatically stopped after the predictions are made.
@@ -427,7 +420,7 @@ docker run \
427
420
  ```
428
421
 
429
422
  > For more information, see [Deploy in Docker
430
- Container](https://docs.supervisely.com/neural-networks/overview-1/deploy_and_predict_with_supervisely_sdk#deploy-in-docker-container){:target="_blank"}
423
+ Container](https://docs.supervisely.com/neural-networks/inference-and-deployment/deploy_and_predict_with_supervisely_sdk#deploy-in-docker-container){:target="_blank"}
431
424
  documentation.
432
425
 
433
426
  ## Deploy locally with Supervisely SDK
@@ -472,7 +465,7 @@ This approach helps you to quickly set up the environment and run inference with
472
465
  )
473
466
  ```
474
467
 
475
- > For more information, see [Local Deployment](https://docs.supervisely.com/neural-networks/overview-1/deploy_and_predict_with_supervisely_sdk){:target="_blank"} and [Prediction API](https://docs.supervisely.com/neural-networks/overview-1/prediction-api){:target="_blank"} documentations.
468
+ > For more information, see [Local Deployment](https://docs.supervisely.com/neural-networks/inference-and-deployment/deploy_and_predict_with_supervisely_sdk){:target="_blank"} and [Prediction API](https://docs.supervisely.com/neural-networks/inference-and-deployment/prediction-api){:target="_blank"} documentations.
476
469
 
477
470
  {% if experiment.training.checkpoints.onnx.name or experiment.training.checkpoints.tensorrt.name %}
478
471
  ### Deploy ONNX/TensorRT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: supervisely
3
- Version: 6.73.428
3
+ Version: 6.73.430
4
4
  Summary: Supervisely Python SDK.
5
5
  Home-page: https://github.com/supervisely/supervisely
6
6
  Author: Supervisely
@@ -994,9 +994,12 @@ supervisely/nn/model/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hS
994
994
  supervisely/nn/model/model_api.py,sha256=tSstGGdzm3cHc81P7IoM447sMVesUYJpspzMqUY70pg,10322
995
995
  supervisely/nn/model/prediction.py,sha256=5a-SLvxV3h6dp5KUs1ENZBY-rkWpddShjI8DBWQHhtU,11945
996
996
  supervisely/nn/model/prediction_session.py,sha256=W7DKIWvufCUQhIdk8q_fTsIOxW_DuZbkr1j_NVcT5gk,27386
997
- supervisely/nn/tracker/__init__.py,sha256=fTyXg-q_LuYx9sv6TueDDhxCfwQmDxUkIK4iLZgfBOk,283
997
+ supervisely/nn/tracker/__init__.py,sha256=1Pv1zLedcZaTk1BS3ezscQbVizq7vQABlNbLhEhHkOI,326
998
998
  supervisely/nn/tracker/base_tracker.py,sha256=2d23JlHizOqVye324YT20EE8RP52uwoQUkPYvPXJTdw,1668
999
- supervisely/nn/tracker/botsort_tracker.py,sha256=jNtzhdUMoBYMvvreEV4bFQbYav97koiD4gLU5LvICr8,9817
999
+ supervisely/nn/tracker/botsort_tracker.py,sha256=scQIessMnF9xaBFkAkSDnUfY0jYEftpod1nUxMisZXU,10141
1000
+ supervisely/nn/tracker/calculate_metrics.py,sha256=JjXI4VYWYSZ5j2Ed81FNYozkS3v2UAM73ztjLrHGg58,10434
1001
+ supervisely/nn/tracker/utils.py,sha256=UxGXcMfFgqh7y26oSk9MIeG_lOECn9kbytQWkiGVL2A,10046
1002
+ supervisely/nn/tracker/visualize.py,sha256=DY2cnRm4w6_e47xVuF9fwSnOP27ZWTRfv4MFPCyxsD4,20146
1000
1003
  supervisely/nn/tracker/botsort/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
1001
1004
  supervisely/nn/tracker/botsort/botsort_config.yaml,sha256=q_7Gp1-15lGYOLv7JvxVJ69mm6hbCLbUAl_ZBOYNGpw,535
1002
1005
  supervisely/nn/tracker/botsort/osnet_reid/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -1007,7 +1010,7 @@ supervisely/nn/tracker/botsort/tracker/basetrack.py,sha256=oPcXpQWev6YmpjzqWwQKZ
1007
1010
  supervisely/nn/tracker/botsort/tracker/gmc.py,sha256=E6LkkVEYeBUjKaQ7VnYxMAj0fdDDWH8Es_tC1a0Z0rs,11754
1008
1011
  supervisely/nn/tracker/botsort/tracker/kalman_filter.py,sha256=waTArMcbmpHAzb57aJ-IcS1o5-i-TX5U-0N_M_28nlo,9671
1009
1012
  supervisely/nn/tracker/botsort/tracker/matching.py,sha256=bgnheHwWD3XZSI3OJVfdrU5bYJ44rxPHzzSElfg6LZM,6600
1010
- supervisely/nn/tracker/botsort/tracker/mc_bot_sort.py,sha256=AE_yi4pVg_bIsK-6-ZlPziqb98tA-9iWgjT8aRE-8sA,17775
1013
+ supervisely/nn/tracker/botsort/tracker/mc_bot_sort.py,sha256=dFjWmubyJLrUP4i-CJaOhPEkQD-WD144deW7Ua5a7Rc,17775
1011
1014
  supervisely/nn/training/__init__.py,sha256=gY4PCykJ-42MWKsqb9kl-skemKa8yB6t_fb5kzqR66U,111
1012
1015
  supervisely/nn/training/train_app.py,sha256=DW9J6qZEebRFdq59sexvEZTWyExisbz7Z2lHXwEkPjY,131937
1013
1016
  supervisely/nn/training/gui/__init__.py,sha256=Nqnn8clbgv-5l0PgxcTOldg8mkMKrFn4TvPL-rYUUGg,1
@@ -1075,7 +1078,7 @@ supervisely/template/base_generator.py,sha256=3nesbfRpueyRYljQSTnkMjeC8ERTOfjI88
1075
1078
  supervisely/template/extensions.py,sha256=kTYxu_LrvFyUN3HByCebGq8ra7zUygcEyw4qTUHq3M4,5255
1076
1079
  supervisely/template/template_renderer.py,sha256=SzGxRdbP59uxqcZT8kZbaHN2epK8Vjfh-0jKBpkdCBY,9709
1077
1080
  supervisely/template/experiment/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1078
- supervisely/template/experiment/experiment.html.jinja,sha256=ZTdlsPrSBdsOl7yfokJq0n1B2eBnFYUNZ85QvY5L-uM,18750
1081
+ supervisely/template/experiment/experiment.html.jinja,sha256=DiprWy6YAE1Gg7fBinZMxEpXSHCbJLbkTZLNKYTUhdw,19045
1079
1082
  supervisely/template/experiment/experiment_generator.py,sha256=b0zqP_dt3bvXxjZx6DxTog2-ih-Q7pj4w_5c70WEfjU,44924
1080
1083
  supervisely/template/experiment/header.html.jinja,sha256=sWGWeoYeixdLUOnQDjzrfGI8K6vL93dOt7fAgNWqkvY,13751
1081
1084
  supervisely/template/experiment/sidebar.html.jinja,sha256=4IxuJzcU1OT93mXMixE7EAMYfcn_lOVfCjS3VkEieSk,9323
@@ -1124,9 +1127,9 @@ supervisely/worker_proto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
1124
1127
  supervisely/worker_proto/worker_api_pb2.py,sha256=VQfi5JRBHs2pFCK1snec3JECgGnua3Xjqw_-b3aFxuM,59142
1125
1128
  supervisely/worker_proto/worker_api_pb2_grpc.py,sha256=3BwQXOaP9qpdi0Dt9EKG--Lm8KGN0C5AgmUfRv77_Jk,28940
1126
1129
  supervisely_lib/__init__.py,sha256=7-3QnN8Zf0wj8NCr2oJmqoQWMKKPKTECvjH9pd2S5vY,159
1127
- supervisely-6.73.428.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
1128
- supervisely-6.73.428.dist-info/METADATA,sha256=GzysKJg4nHPCZiWzNZM2cXwAjNfMrlh2HmakZioubeg,35433
1129
- supervisely-6.73.428.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
1130
- supervisely-6.73.428.dist-info/entry_points.txt,sha256=U96-5Hxrp2ApRjnCoUiUhWMqijqh8zLR03sEhWtAcms,102
1131
- supervisely-6.73.428.dist-info/top_level.txt,sha256=kcFVwb7SXtfqZifrZaSE3owHExX4gcNYe7Q2uoby084,28
1132
- supervisely-6.73.428.dist-info/RECORD,,
1130
+ supervisely-6.73.430.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
1131
+ supervisely-6.73.430.dist-info/METADATA,sha256=WakARGRoin99pVVtAhIE5zrXuM6VygJ1MDezLxlD-hk,35433
1132
+ supervisely-6.73.430.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
1133
+ supervisely-6.73.430.dist-info/entry_points.txt,sha256=U96-5Hxrp2ApRjnCoUiUhWMqijqh8zLR03sEhWtAcms,102
1134
+ supervisely-6.73.430.dist-info/top_level.txt,sha256=kcFVwb7SXtfqZifrZaSE3owHExX4gcNYe7Q2uoby084,28
1135
+ supervisely-6.73.430.dist-info/RECORD,,