A utility class for smoothing detections over multiple frames in video tracking.
It maintains a history of detections for each track and provides smoothed
predictions based on these histories.
Warning
DetectionsSmoother requires the tracker_id for each detection. Refer to
Roboflow Trackers for
information on integrating tracking into your inference pipeline.
This class is not compatible with segmentation models.
When detections in a frame disagree on confidence presence — some tracks
carry confidence scores and others do not — confidence is set to None
for all smoothed detections in that frame.
classDetectionsSmoother:""" A utility class for smoothing detections over multiple frames in video tracking. It maintains a history of detections for each track and provides smoothed predictions based on these histories. <video controls> <source src="https://media.roboflow.com/supervision-detection-smoothing.mp4" type="video/mp4"> </video> !!! warning - `DetectionsSmoother` requires the `tracker_id` for each detection. Refer to [Roboflow Trackers](/latest/trackers/) for information on integrating tracking into your inference pipeline. - This class is not compatible with segmentation models. - When detections in a frame disagree on confidence presence — some tracks carry confidence scores and others do not — `confidence` is set to `None` for all smoothed detections in that frame. Example: ```pycon >>> import numpy as np >>> import supervision as sv >>> smoother = sv.DetectionsSmoother(length=3) >>> detections_1 = sv.Detections( ... xyxy=np.array([[0, 0, 10, 10]]), ... confidence=np.array([0.5]), ... tracker_id=np.array([1]) ... ) >>> detections_2 = sv.Detections( ... xyxy=np.array([[2, 2, 12, 12]]), ... confidence=np.array([0.7]), ... tracker_id=np.array([1]) ... ) >>> smoothed = smoother.update_with_detections(detections_1) >>> smoothed.xyxy array([[ 0., 0., 10., 10.]]) >>> smoothed = smoother.update_with_detections(detections_2) >>> smoothed.xyxy array([[ 1., 1., 11., 11.]]) >>> smoothed.confidence array([0.6]) ``` ```python import supervision as sv from ultralytics import YOLO video_info = sv.VideoInfo.from_video_path(video_path="<SOURCE_FILE_PATH>") frame_generator = sv.get_video_frames_generator( source_path="<SOURCE_FILE_PATH>") model = YOLO("<MODEL_PATH>") tracker = sv.ByteTrack(frame_rate=video_info.fps) smoother = sv.DetectionsSmoother() box_annotator = sv.BoxAnnotator() with sv.VideoSink("<TARGET_FILE_PATH>", video_info=video_info) as sink: for frame in frame_generator: result = model(frame)[0] detections = sv.Detections.from_ultralytics(result) detections = tracker.update_with_detections(detections) detections = smoother.update_with_detections(detections) annotated_frame = box_annotator.annotate(frame.copy(), detections) sink.write_frame(annotated_frame) ``` """def__init__(self,length:int=5)->None:""" Args: length: The maximum number of frames to consider for smoothing detections. Defaults to 5. """self.tracks:defaultdict[int,deque[Detections|None]]=defaultdict(lambda:deque(maxlen=length))defupdate_with_detections(self,detections:Detections)->Detections:""" Updates the smoother with a new set of detections from a frame. Args: detections: The detections to add to the smoother. """ifdetections.tracker_idisNone:warnings.warn("Smoothing skipped. DetectionsSmoother requires tracker_id. Refer to ""https://supervision.roboflow.com/latest/trackers for more ""information.",category=SupervisionWarnings,)returndetectionsfordetection_idxinrange(len(detections)):tracker_id_value=detections.tracker_id[detection_idx]tracker_id=int(tracker_id_value)self.tracks[tracker_id].append(cast(Detections,detections[detection_idx]))fortrack_idinself.tracks.keys():iftrack_idnotindetections.tracker_id:self.tracks[track_id].append(None)fortrack_idinlist(self.tracks.keys()):ifall([disNonefordinself.tracks[track_id]]):delself.tracks[track_id]returnself.get_smoothed_detections()defget_track(self,track_id:int)->Detections|None:"""Return the smoothed `Detections` for a single track. Averages `xyxy` over all valid (non-`None`) frames in the track window. `confidence` is averaged only over frames that carry it; frames with `confidence=None` are excluded. Returns `None` when the track is unknown or its entire window is empty. Args: track_id: The tracker ID whose smoothed detection to retrieve. Returns: Smoothed `Detections` for the track, or `None` if the track is unknown or all frames in its window are empty. """track=self.tracks.get(track_id,None)iftrackisNone:returnNonevalid:list[Detections]=[dfordintrackifdisnotNone]iflen(valid)==0:returnNoneret=deepcopy(valid[0])ret.xyxy=np.mean([d.xyxyfordinvalid],axis=0)# Average confidence only over frames that carry it; frames with# confidence=None contribute nothing to the mean. Retain None when# no frame in the window carries confidence.confidences=[d.confidencefordinvalidifd.confidenceisnotNone]ret.confidence=np.mean(np.array(confidences),axis=0)ifconfidenceselseNonereturnretdefget_smoothed_detections(self)->Detections:tracked_detections=[]fortrack_idinself.tracks:track=self.get_track(track_id)iftrackisnotNone:tracked_detections.append(track)# Detections.merge requires all-or-none for optional fields.# When tracks disagree on confidence presence, drop it from all to# prevent ValueError inside Detections.merge (stack_or_none invariant).iftracked_detectionsandany(d.confidenceisNonefordintracked_detections):fordintracked_detections:d.confidence=Nonedetections=Detections.merge(tracked_detections)iflen(detections)==0:detections.tracker_id=np.array([],dtype=int)returndetections
def__init__(self,length:int=5)->None:""" Args: length: The maximum number of frames to consider for smoothing detections. Defaults to 5. """self.tracks:defaultdict[int,deque[Detections|None]]=defaultdict(lambda:deque(maxlen=length))
Return the smoothed Detections for a single track.
Averages xyxy over all valid (non-None) frames in the track window.
confidence is averaged only over frames that carry it; frames with
confidence=None are excluded. Returns None when the track is unknown
or its entire window is empty.
defget_track(self,track_id:int)->Detections|None:"""Return the smoothed `Detections` for a single track. Averages `xyxy` over all valid (non-`None`) frames in the track window. `confidence` is averaged only over frames that carry it; frames with `confidence=None` are excluded. Returns `None` when the track is unknown or its entire window is empty. Args: track_id: The tracker ID whose smoothed detection to retrieve. Returns: Smoothed `Detections` for the track, or `None` if the track is unknown or all frames in its window are empty. """track=self.tracks.get(track_id,None)iftrackisNone:returnNonevalid:list[Detections]=[dfordintrackifdisnotNone]iflen(valid)==0:returnNoneret=deepcopy(valid[0])ret.xyxy=np.mean([d.xyxyfordinvalid],axis=0)# Average confidence only over frames that carry it; frames with# confidence=None contribute nothing to the mean. Retain None when# no frame in the window carries confidence.confidences=[d.confidencefordinvalidifd.confidenceisnotNone]ret.confidence=np.mean(np.array(confidences),axis=0)ifconfidenceselseNonereturnret
defupdate_with_detections(self,detections:Detections)->Detections:""" Updates the smoother with a new set of detections from a frame. Args: detections: The detections to add to the smoother. """ifdetections.tracker_idisNone:warnings.warn("Smoothing skipped. DetectionsSmoother requires tracker_id. Refer to ""https://supervision.roboflow.com/latest/trackers for more ""information.",category=SupervisionWarnings,)returndetectionsfordetection_idxinrange(len(detections)):tracker_id_value=detections.tracker_id[detection_idx]tracker_id=int(tracker_id_value)self.tracks[tracker_id].append(cast(Detections,detections[detection_idx]))fortrack_idinself.tracks.keys():iftrack_idnotindetections.tracker_id:self.tracks[track_id].append(None)fortrack_idinlist(self.tracks.keys()):ifall([disNonefordinself.tracks[track_id]]):delself.tracks[track_id]returnself.get_smoothed_detections()