Ultralytics YOLO27: Join the waitlist.
Get Started

Multi-Object Tracking with Ultralytics YOLO

YOLO multi-object tracking with trajectory paths

Multi-object tracking in video analytics identifies the location and class of objects within each frame and also maintains a unique ID for each detected object as the video progresses. Applications range from surveillance and security to real-time sports analytics.

See the unreleased YOLO27 preview for planned tracking support.

Trackers: OC-SORT, Deep OC-SORT, FastTracker, TrackTrack

Starting with ultralytics 8.4.63, OC-SORT, Deep OC-SORT, FastTracker, and TrackTrack are available alongside BoT-SORT and ByteTrack.

Why Choose Ultralytics YOLO for Object Tracking?#

The output from Ultralytics trackers is consistent with standard object detection but has the added value of object IDs. This makes it easy to track objects in video streams and perform subsequent analytics. Key reasons to use Ultralytics YOLO for object tracking:

  • Efficiency: Process video streams in real-time without compromising accuracy.
  • Flexibility: Supports multiple tracking algorithms and configurations.
  • Ease of Use: Simple Python API and CLI options for quick integration and deployment.
  • Customizability: Easy to use with custom-trained YOLO models, allowing integration into domain-specific applications.


Watch: How to Run Multi-Object Tracking with Ultralytics YOLO26 | BoT-SORT & ByteTrack | VisionAI 🚀

Real-world Applications#

TransportationRetailAquaculture
Vehicle TrackingPeople TrackingFish Tracking

Quick Start#

Run tracking on a video with the default TrackTrack tracker. Swap to another tracker by changing the tracker argument.

Example
from ultralytics import YOLO

model = YOLO("yolo26n.pt")

# Default tracker (TrackTrack)
results = model.track(source="https://youtu.be/LNwODJXcvt4", show=True)

# Switch to ByteTrack
results = model.track(source="https://youtu.be/LNwODJXcvt4", show=True, tracker="bytetrack.yaml")

To run the tracker on video streams, use a trained Detect, Segment, Pose, or OBB model such as YOLO26n, YOLO26n-seg, YOLO26n-pose, or YOLO26n-obb, all available in the YOLO26 collection on Ultralytics Platform. You can train custom models locally or with Ultralytics Platform cloud training.

Example
from ultralytics import YOLO

# Load an official or custom model
model = YOLO("yolo26n.pt")  # Load an official Detect model
model = YOLO("yolo26n-seg.pt")  # Load an official Segment model
model = YOLO("yolo26n-pose.pt")  # Load an official Pose model
model = YOLO("path/to/best.pt")  # Load a custom-trained model

# Perform tracking with the model
results = model.track("https://youtu.be/LNwODJXcvt4", show=True)  # Tracking with default tracker
results = model.track("https://youtu.be/LNwODJXcvt4", show=True, tracker="bytetrack.yaml")  # with ByteTrack

Supported Trackers#

Ultralytics YOLO ships with six built-in trackers. Enable one by passing its YAML config file to the tracker argument.

TrackerConfig fileMotion modelAppearance / ReIDCamera motion compensationOcclusion handling
BoT-SORTbotsort.yamlLinear KalmanOptional (with_reid)Configurable (gmc_method)Track buffer + ReID rebinding
ByteTrackbytetrack.yamlLinear KalmanNoneNoTwo-stage low-conf rescue
OC-SORTocsort.yamlObservation-centric KalmanNoneNoORU, OCM, OCR re-update from last observation
Deep OC-SORTdeepocsort.yamlObservation-centric KalmanOptional (with_reid)Configurable (gmc_method)OC-SORT + optional adaptive appearance EMA
FastTrackerfasttrack.yamlLinear Kalman + rollbackNoneNoKalman rollback + bbox enlargement on occlusion
TrackTracktracktrack.yamlLinear Kalman (NSA)Optional (with_reid)Configurable (gmc_method)Iterative multi-cue association + TAI

Which Tracker Should I Use?#

Use this flow to pick a starting point; tracktrack.yaml is used when you pass no tracker:

  1. Need the fastest, simplest baseline? → ByteTrack (no ReID, no camera-motion compensation, minimum overhead).
  2. Handheld, drone, or moving-camera footage? → BoT-SORT (adds camera-motion compensation and optional ReID).
  3. Non-linear motion (sports, dancing, abrupt turns) and no ReID? → OC-SORT (observation-centric corrections without appearance cost).
  4. Crowded moving-camera scenes where ID swaps are the main problem? → Deep OC-SORT or TrackTrack (both support optional appearance matching; TrackTrack also adds multi-cue association and duplicate-ID suppression).
  5. Frequent partial overlap in real-time, no ReID budget? → FastTracker (occlusion-aware ByteTrack variant with Kalman rollback).

Switching Trackers#

Pass the tracker config filename to tracker=. All other code stays the same.

Example
from ultralytics import YOLO

model = YOLO("yolo26n.pt")

results = model.track(source="path/to/video.mp4", tracker="bytetrack.yaml")
results = model.track(source="path/to/video.mp4", tracker="ocsort.yaml")
results = model.track(source="path/to/video.mp4", tracker="tracktrack.yaml")

Configuration#

Tracking Arguments#

Tracking configuration shares inference arguments with Predict mode, such as conf, iou, and show.

Example
from ultralytics import YOLO

# Configure the tracking parameters and run the tracker
model = YOLO("yolo26n.pt")
results = model.track(source="https://youtu.be/LNwODJXcvt4", conf=0.1, iou=0.5, show=True)

Custom Tracker Configuration#

To use a modified tracker configuration, copy a tracker config file (for example, as custom_tracker.yaml) from ultralytics/cfg/trackers and modify any settings except tracker_type.

Example
from ultralytics import YOLO

# Load the model and run the tracker with a custom configuration file
model = YOLO("yolo26n.pt")
results = model.track(source="https://youtu.be/LNwODJXcvt4", tracker="custom_tracker.yaml")

Shared Tracker Arguments#

The following parameters are common to most tracker YAML files; not every parameter appears in every config:

Tracker Threshold Information

Detections at or above track_high_thresh enter the first association stage. Detections between track_low_thresh and track_high_thresh can recover existing tracks when the selected tracker enables low-confidence association, but they do not start new tracks. Detections at or below track_low_thresh are ignored.

ParameterValid Values or RangesDescription
tracker_typebotsort, bytetrack, ocsort, deepocsort, fasttrack, tracktrackSpecifies the tracker type.
track_high_thresh0.0-1.0Threshold for the first association. Affects how confidently a detection is matched to an existing track.
track_low_thresh0.0-1.0Lower bound for low-confidence recovery detections. OC-SORT and Deep OC-SORT use these only when use_byte: True; TrackTrack includes them in its penalized association pool.
new_track_thresh0.0-1.0Threshold to initialize a new track if the detection does not match any existing tracks.
track_buffer>=0Frames lost tracks are kept alive before removal. Higher value means more tolerance for occlusion.
match_thresh0.0-1.0Threshold for matching tracks. Higher values make matching more lenient.
fuse_scoreTrue, FalseWhether to fuse confidence scores with IoU distances before matching.
gmc_methodsparseOptFlow, orb, sift, ecc, noneGlobal motion compensation method. Helps account for camera movement.
proximity_thresh0.0-1.0Minimum IoU required for a valid ReID match. Ensures spatial closeness before using appearance cues.
appearance_thresh0.0-1.0Minimum normalized appearance similarity required for ReID.
with_reidTrue, FalseEnable appearance-based matching for better tracking across occlusions. Supported by BoT-SORT, Deep OC-SORT, and TrackTrack.
modelauto or compatible ReID model pathReID model. auto uses native YOLO backbone features when available; otherwise falls back to yolo26n-cls.pt. A custom encoder can be a .pt checkpoint or an exported model such as .torchscript, .onnx, .engine, or an OpenVINO model directory.

Tracker-specific Arguments#

Each algorithm exposes additional knobs on top of the shared parameters. See the per-tracker sections below for descriptions and tuning advice, or refer directly to the config files:

Enabling Re-Identification (ReID)#

ReID is disabled by default to minimize overhead. Enable it by setting with_reid: True in a tracker config file.

ReID model options:

  • model: auto — Uses native YOLO detector features, adding minimal overhead. Ideal when you need some ReID without a large performance hit. Falls back to yolo26n-cls.pt if the detector does not expose compatible features.
  • Custom ReID model — Point model: at a .pt checkpoint or a compatible exported embedding model, such as .torchscript, .onnx, .engine, or an OpenVINO model directory. Exported models are loaded through AutoBackend and must output an embedding tensor directly.

Ready-to-use ONNX encoders are published for every model size. Set model: to one of these names and the file is downloaded automatically the first time the tracker runs (the same way YOLO weights are fetched) — no manual export or download step required:

# In your tracker config (e.g. tracktrack.yaml)
with_reid: True
model: yolo26n-reid.onnx # downloaded on first use; swap n→s/m/l/x for a larger encoder
Modelsize
(pixels)
params
(M)
FLOPs
(B)
YOLO26n-reid.onnx4482.82.0
YOLO26s-reid.onnx4487.56.6
YOLO26m-reid.onnx44812.420.1
YOLO26l-reid.onnx44815.325.2
YOLO26x-reid.onnx44832.755.9
ReID is tracking-only

Only ONNX ReID encoders for the tracker appearance branch are currently available. ReID train, val, and predict modes, as well as dedicated ReID export recipes, are still under development.

For better performance with a separate classification model, export it to a faster backend like TensorRT:

Exporting a ReID model to TensorRT
from torch import nn

from ultralytics import YOLO

# Load the classification model
model = YOLO("yolo26n-cls.pt")

# Add average pooling layer
head = model.model.model[-1]
pool = nn.Sequential(nn.AdaptiveAvgPool2d((1, 1)), nn.Flatten(start_dim=1))
pool.f, pool.i = head.f, head.i
model.model.model[-1] = pool

# Export to TensorRT
model.export(format="engine", quantize=16, dynamic=True, batch=32)

Once exported, point to the TensorRT model path in your tracker config.

Tracker Details#

The sections below describe each tracker's design, specific parameters, and tuning tips.



Watch: Which Object Tracker Performs Better? | Speed, FPS & ID Stability | Ultralytics YOLO26 📊

BoT-SORT#

BoT-SORT (Aharon et al., 2022) extends ByteTrack with camera-motion compensation and optional ReID:

  • Camera Motion Compensation (CMC): an affine warp estimated each frame (sparse optical flow by default; ORB / ECC also available) is applied to Kalman states before IoU matching.
  • Optional ReID: appearance embeddings can be fused into the cost matrix. Disabled by default; enable with with_reid: True.

Best for: general-purpose tracking, especially moving cameras. Add ReID only when look-alike crowds cause ID swaps.

BoT-SORT-specific arguments:

ParameterValid Values or RangesDescription
gmc_methodsparseOptFlow, orb, sift, ecc, noneCamera-motion-compensation backend. sparseOptFlow is the default. none disables CMC.
with_reidTrue, FalseEnable appearance-based matching. Off by default.
modelauto or compatible ReID model pathauto uses native YOLO features when available; custom models can be .pt checkpoints or compatible exports.
proximity_thresh0.0-1.0Minimum IoU before appearance features are considered.
appearance_thresh0.0-1.0Minimum normalized appearance similarity required for a ReID match. Raise to make matching stricter.

Tuning tips:

  • Static camera: set gmc_method: none to save a few ms/frame.
  • Heavy camera motion: keep sparseOptFlow; ecc is more accurate but slower.
  • Look-alike crowds: turn on with_reid: True and raise appearance_thresh (e.g. 0.85+).

ByteTrack#

ByteTrack (Zhang et al., ECCV 2022) is the lightweight baseline. It uses linear Kalman + IoU with a two-stage association:

  • Stage 1: match high-score detections against active tracks.
  • Stage 2: retry unmatched tracks against low-score detections to recover through brief partial occlusion.

There is no appearance model and no camera-motion compensation.

Best for: static or near-static cameras where detector cost dominates and you want minimum tracker overhead.

ByteTrack-specific arguments: None beyond the shared tracker arguments.

Tuning tips:

  • Noisy detector: lower track_low_thresh so the second stage has more candidates.
  • High-recall detector: raise track_high_thresh to reduce fragmented IDs.
  • Frequent ID flicker: raise track_buffer so briefly-missed tracks survive.

OC-SORT#

OC-SORT (Cao et al., CVPR 2023) is an observation-centric extension of SORT. It keeps SORT's lightweight design (no appearance features) and adds three corrections:

  • Observation-Centric Re-update (ORU): replays a virtual trajectory between the last observation and the current detection, re-running the Kalman update to repair drifted velocity.
  • Observation-Centric Momentum (OCM): penalizes detections moving in the wrong direction via a velocity-consistency term.
  • Observation-Centric Recovery (OCR): re-checks unmatched detections against recently lost tracks using their last observation rather than the predicted state.

Best for: non-linear motion without the cost of a ReID model.

OC-SORT-specific arguments:

ParameterValid Values or RangesDescription
delta_t>=1Temporal window (frames) for velocity-direction computation in OCM. Larger values smooth more.
inertia0.0-1.0Weight of the velocity-consistency cost. Higher values penalize sudden direction changes.
use_byteTrue, FalseEnable a ByteTrack-style second association pass over low-confidence detections.

Tuning tips:

  • Non-linear motion: raise inertia (e.g. 0.3-0.4).
  • Sparse detections: enable use_byte: True.
  • Long occlusions: raise track_buffer so OCR has more lost tracks to rebind.

With OBB models, OCR uses the Kalman-predicted oriented box because an oriented last-observation history is not retained.

Deep OC-SORT#

Deep OC-SORT augments OC-SORT with appearance information and camera-motion compensation:

  • Appearance matching: cosine embedding distance is gated by overlap and fused with motion cost.
  • Dynamic appearance EMA: track embeddings update with an EMA whose smoothing factor adapts to detection confidence.
  • Camera Motion Compensation: Kalman states are warped frame-to-frame via sparse optical flow, ORB, or ECC.

Best for: crowded or moving-camera scenes where ID swaps between visually different but spatially close objects are common.

Deep OC-SORT-specific arguments:

ParameterValid Values or RangesDescription
with_reidTrue, FalseEnable appearance-based matching. Off by default.
modelauto or compatible ReID model pathauto uses native YOLO features when available; custom models can be .pt checkpoints or compatible exports.
proximity_thresh0.0-1.0Minimum IoU before appearance features are considered.
appearance_thresh0.0-1.0Minimum normalized appearance similarity required for a ReID match.
alpha_fixed_emb0.0-1.0Base EMA factor for track-embedding updates. Higher values preserve the older embedding longer.
gmc_methodsparseOptFlow, orb, sift, ecc, noneGlobal motion compensation method.
delta_t>=1Temporal window (frames) for velocity-direction computation in OCM (inherited from OC-SORT).
inertia0.0-1.0Weight of the velocity-consistency cost (inherited from OC-SORT).
use_byteTrue, FalseEnable a ByteTrack-style second association over low-confidence detections (inherited from OC-SORT).

Tuning tips:

  • Identity swaps in crowds: raise appearance_thresh (e.g. 0.92-0.95) and raise alpha_fixed_emb so embeddings adapt more slowly.
  • Moving camera: set gmc_method: sparseOptFlow (Deep OC-SORT defaults to none).
  • Lower latency: keep with_reid: False (default) for motion + CMC only; enable ReID only when ID swaps dominate errors.

FastTracker#

FastTracker is an occlusion-aware ByteTrack variant with no appearance model:

  • Occlusion detection: flags tracks occluded when coverage by other active tracks exceeds occ_cover_thresh.
  • Kalman rollback on occlusion: rolls the Kalman state back to a pre-occlusion frame using ring-buffered history.
  • Motion dampening and search expansion: velocity is dampened and the predicted bbox is enlarged while occluded.
  • Init-IoU suppression: prevents new tracks from spawning on top of active tracks.

Best for: real-time detection-only pipelines with frequent target-on-target overlap (crowds, queues, sports).

FastTracker-specific arguments:

ParameterValid Values or RangesDescription
reset_velocity_offset_occ>=0History frames back to restore Kalman velocity on occlusion onset.
reset_pos_offset_occ>=0History frames back to restore Kalman position on occlusion onset.
enlarge_bbox_occ>=1.0Height scaling applied to the predicted bbox while occluded (width scales via XYAH aspect ratio).
dampen_motion_occ0.0-1.0Velocity multiplier while occluded. Lower values make the track "slow down" through occlusion.
active_occ_to_lost_thresh>=1Max consecutive occluded frames before an active track is moved to lost.
occ_cover_thresh0.0-1.0Fraction of a track's area covered by another active track to declare occlusion.
occ_reappear_window>=0Frames a recently-occluded lost track stays preferentially re-findable.
init_iou_suppress0.0-1.0Suppress new-track initialization if its IoU with any active track exceeds this. Set to 1.0 to disable.

Tuning tips:

  • Frequent partial occlusions: lower occ_cover_thresh (e.g. 0.5-0.6).
  • Duplicate IDs around overlap: lower init_iou_suppress (e.g. 0.5).
  • Long occlusions: raise occ_reappear_window and track_buffer together.
  • Fast-moving targets: raise dampen_motion_occ (closer to 1.0); raise enlarge_bbox_occ only when a wider search region improves recovery.

TrackTrack#

TrackTrack (Shim et al., CVPR 2025) is the default tracker. It reasons from each track's perspective with multi-cue iterative association:

  • Track-Perspective-Based Association (TPA): combines HMIoU, optional cosine ReID distance, confidence-projection distance, and corner-angle distance. Assignment is solved iteratively with a relaxing threshold.
  • Track-Aware Initialization (TAI): suppresses duplicate spawns before a new ID is created.

Best for: crowded scenes with frequent occlusion where duplicate IDs are a problem.

TrackTrack-specific arguments:

ParameterValid Values or RangesDescription
iou_weight0.0-1.0Weight of HMIoU distance when ReID is enabled; otherwise HMIoU is used directly.
reid_weight0.0-1.0Weight of cosine ReID distance when ReID is enabled; otherwise it is ignored.
conf_weight0.0-1.0Weight of confidence-projection distance.
angle_weight0.0-1.0Weight of corner-angle distance.
penalty_p0.0-1.0Cost penalty for low-confidence detections.
penalty_q0.0-1.0Cost penalty for detections recovered by secondary NMS.
reduce_step0.0-1.0Match-threshold relaxation per iteration.
tai_thr0.0-1.0IoU threshold for Track-Aware Initialization NMS.
min_track_len>=0Minimum history length before a new track is confirmed.
lost_match_thr0.0-1.0Looser cost gate for relaxed lost-rebind pass; 0 disables it.
with_reidTrue, FalseEnable cosine-ReID appearance matching (uses native YOLO features). Off by default.
modelauto or compatible ReID model pathauto uses native YOLO features when available; custom models can be .pt checkpoints or compatible exports.
gmc_methodsparseOptFlow, orb, sift, ecc, noneGlobal motion compensation method.

Tuning tips:

  • Crowded pedestrians: lower tai_thr (e.g. 0.45) to suppress more duplicate spawns; raise track_buffer for longer occlusions.
  • Fast camera motion: keep gmc_method: sparseOptFlow enabled.
  • Small/fast objects: raise angle_weight slightly and lower min_track_len.
  • Enable ReID only when needed: it adds inference cost; for short occlusions, the default multi-cue cost is usually sufficient.

With Segment and Pose models, TrackTrack skips loose-NMS detection recovery so masks and keypoints remain aligned with their detections.

Python Examples#



Watch: How to Build Interactive Object Tracking with Ultralytics YOLO26: Build it in Minutes | Python!

Persisting Tracks Loop#

Here is a Python script using OpenCV (cv2) and YOLO26 to run object tracking on video frames. This script assumes the necessary packages (opencv-python and ultralytics) are already installed. The persist=True argument tells the tracker that the current image or frame is the next in a sequence and to expect tracks from the previous image in the current image.

Persisting tracks and selecting a tracker

Use persist=True only when passing consecutive frames from the same video stream to model.track(). This lets the tracker reuse state from earlier frames and maintain consistent track IDs over time. Do not use persist=True across unrelated images or a different stream, since previous track state can carry over.

You can also choose a tracker backend by passing a tracker configuration file, such as tracker="botsort.yaml", tracker="bytetrack.yaml", or tracker="tracktrack.yaml".

Streaming for-loop with tracking
import cv2

from ultralytics import YOLO

# Load the YOLO26 model
model = YOLO("yolo26n.pt")

# Open the video file
video_path = "path/to/video.mp4"
cap = cv2.VideoCapture(video_path)

# Loop through the video frames
while cap.isOpened():
    # Read a frame from the video
    success, frame = cap.read()

    if success:
        # Run YOLO26 tracking on the frame, persisting tracks between frames
        # and using the BoT-SORT tracker backend
        results = model.track(frame, persist=True, tracker="botsort.yaml")

        # Visualize the results on the frame
        annotated_frame = results[0].plot()

        # Display the annotated frame
        cv2.imshow("YOLO26 Tracking", annotated_frame)

        # Break the loop if 'q' is pressed
        if cv2.waitKey(1) & 0xFF == ord("q"):
            break
    else:
        # Break the loop if the end of the video is reached
        break

# Release the video capture object and close the display window
cap.release()
cv2.destroyAllWindows()

Note the change from model(frame) to model.track(frame), which enables object tracking instead of simple detection. This modified script will run the tracker on each frame of the video, visualize the results, and display them in a window. The loop can be exited by pressing 'q'.

Plotting Tracks Over Time#

Visualizing object tracks over consecutive frames can provide valuable insights into the movement patterns and behavior of detected objects within a video. Ultralytics YOLO26 makes plotting these tracks straightforward.

The following example uses YOLO26 tracking to plot the movement of detected objects across multiple video frames. The script opens a video file, reads it frame by frame, and uses the YOLO model to identify and track objects. By retaining the center points of the detected bounding boxes and connecting them, it draws lines that represent the paths followed by the tracked objects.

Plotting tracks over multiple video frames
from collections import defaultdict

import cv2
import numpy as np

from ultralytics import YOLO

# Load the YOLO26 model
model = YOLO("yolo26n.pt")

# Open the video file
video_path = "path/to/video.mp4"
cap = cv2.VideoCapture(video_path)

# Store the track history
track_history = defaultdict(list)

# Loop through the video frames
while cap.isOpened():
    # Read a frame from the video
    success, frame = cap.read()

    if success:
        # Run YOLO26 tracking on the frame, persisting tracks between frames
        result = model.track(frame, persist=True)[0]

        # Get the boxes and track IDs
        if result.boxes and result.boxes.is_track:
            boxes = result.boxes.xywh.cpu()
            track_ids = result.boxes.id.int().cpu().tolist()

            # Visualize the result on the frame
            frame = result.plot()

            # Plot the tracks
            for box, track_id in zip(boxes, track_ids):
                x, y, w, h = box
                track = track_history[track_id]
                track.append((float(x), float(y)))  # x, y center point
                if len(track) > 30:  # retain 30 tracks for 30 frames
                    track.pop(0)

                # Draw the tracking lines
                points = np.hstack(track).astype(np.int32).reshape((-1, 1, 2))
                cv2.polylines(frame, [points], isClosed=False, color=(230, 230, 230), thickness=10)

        # Display the annotated frame
        cv2.imshow("YOLO26 Tracking", frame)

        # Break the loop if 'q' is pressed
        if cv2.waitKey(1) & 0xFF == ord("q"):
            break
    else:
        # Break the loop if the end of the video is reached
        break

# Release the video capture object and close the display window
cap.release()
cv2.destroyAllWindows()

Multithreaded Tracking#

Multithreaded tracking provides the capability to run object tracking on multiple video streams simultaneously. This is particularly useful when handling multiple video inputs, such as from multiple surveillance cameras, where concurrent processing improves throughput.

The Python script below uses Python's threading module to run multiple instances of the tracker concurrently. Each thread runs the tracker on one video source, and all the threads run simultaneously in the background.

To ensure that each thread receives the correct parameters (the model to use and the video source), the script defines a function run_tracker_in_thread that accepts these parameters and contains the main tracking loop. This function reads the video frame by frame, runs the tracker, and saves the annotated results.

Two different models are used in this example, yolo26n.pt and yolo26n-seg.pt, each tracking objects in a different source: a local video file and a webcam. The sources are specified in SOURCES.

The daemon=True parameter in threading.Thread means that these threads will be closed as soon as the main program finishes. We then start the threads with start() and use join() to make the main thread wait until both tracker threads have finished.

Finally, after all threads have completed their task, the windows displaying the results are closed using cv2.destroyAllWindows().

Multithreaded tracking implementation
import threading

import cv2

from ultralytics import YOLO

# Define model names and video sources
MODEL_NAMES = ["yolo26n.pt", "yolo26n-seg.pt"]
SOURCES = ["path/to/video.mp4", "0"]  # local video, 0 for webcam

def run_tracker_in_thread(model_name, filename):
    """Run YOLO tracker in its own thread for concurrent processing.

    Args:
        model_name (str): Model checkpoint name or path loaded inside the thread.
        filename (str): The path to the video file or the identifier for the webcam/external camera source.
    """
    model = YOLO(model_name)
    results = model.track(filename, save=True, stream=True)
    for r in results:
        pass

# Create and start tracker threads using a for loop
tracker_threads = []
for video_file, model_name in zip(SOURCES, MODEL_NAMES):
    thread = threading.Thread(target=run_tracker_in_thread, args=(model_name, video_file), daemon=True)
    tracker_threads.append(thread)
    thread.start()

# Wait for all tracker threads to finish
for thread in tracker_threads:
    thread.join()

# Clean up and close windows
cv2.destroyAllWindows()

This example can easily be extended to handle more video files and models by creating more threads and applying the same methodology.

Contribute New Trackers#

If you have implemented or adapted a multi-object tracking algorithm for Ultralytics YOLO, consider contributing it to ultralytics/trackers with a matching config in ultralytics/cfg/trackers. Contributions expand the tracking options available to the whole community.

FAQ#

  • Multi-object tracking in video analytics involves both identifying objects and maintaining a unique ID for each detected object across video frames. Ultralytics YOLO supports this by providing real-time tracking along with object IDs, facilitating tasks such as security surveillance and sports analytics. The system uses trackers such as BoT-SORT, ByteTrack, OC-SORT, Deep OC-SORT, FastTracker, and TrackTrack, which can be configured via YAML files.

  • No. Standard Ultralytics .pt files store the YOLO model weights, while the tracker is created at inference time by model.track(). Track IDs depend on tracker state across consecutive frames, so a single standalone image can return detections such as boxes, classes, and confidences, but it cannot produce meaningful persistent tracking IDs by itself.

    For deployment, package the detector and tracker together in your application and call model.track() frame by frame with persist=True when frames come from the same video stream. Use separate model or tracker instances for unrelated streams so state does not carry over between videos.

  • You can configure a custom tracker by copying an existing tracker configuration file (e.g., custom_tracker.yaml) from the Ultralytics tracker configuration directory and modifying parameters as needed, except for the tracker_type. Use this file in your tracking model like so:

    Example
    from ultralytics import YOLO
    
    model = YOLO("yolo26n.pt")
    results = model.track(source="https://youtu.be/LNwODJXcvt4", tracker="custom_tracker.yaml")
  • To run object tracking on multiple video streams simultaneously, you can use Python's threading module. Each thread will handle a separate video stream. Here's an example of how you can set this up:

    Multithreaded Tracking
    import threading
    
    import cv2
    
    from ultralytics import YOLO
    
    # Define model names and video sources
    MODEL_NAMES = ["yolo26n.pt", "yolo26n-seg.pt"]
    SOURCES = ["path/to/video.mp4", "0"]  # local video, 0 for webcam
    
    def run_tracker_in_thread(model_name, filename):
        """Run YOLO tracker in its own thread for concurrent processing.
    
        Args:
            model_name (str): Model checkpoint name or path loaded inside the thread.
            filename (str): The path to the video file or the identifier for the webcam/external camera source.
        """
        model = YOLO(model_name)
        results = model.track(filename, save=True, stream=True)
        for r in results:
            pass
    
    # Create and start tracker threads using a for loop
    tracker_threads = []
    for video_file, model_name in zip(SOURCES, MODEL_NAMES):
        thread = threading.Thread(target=run_tracker_in_thread, args=(model_name, video_file), daemon=True)
        tracker_threads.append(thread)
        thread.start()
    
    # Wait for all tracker threads to finish
    for thread in tracker_threads:
        thread.join()
    
    # Clean up and close windows
    cv2.destroyAllWindows()
  • Multi-object tracking with Ultralytics YOLO has numerous applications, including:

    • Transportation: Vehicle tracking for traffic management and autonomous driving.
    • Retail: People tracking for in-store analytics and security.
    • Aquaculture: Fish tracking for monitoring aquatic environments.
    • Sports Analytics: Tracking players and equipment for performance analysis.
    • Security Systems: Monitoring suspicious activities and creating security alarms.

    These applications benefit from Ultralytics YOLO's ability to process high-frame-rate videos in real time with high accuracy.

  • To visualize object tracks over multiple video frames, you can use the YOLO model's tracking features along with OpenCV to draw the paths of detected objects. Here's an example script that demonstrates this:

    Plotting tracks over multiple video frames
    from collections import defaultdict
    
    import cv2
    import numpy as np
    
    from ultralytics import YOLO
    
    model = YOLO("yolo26n.pt")
    video_path = "path/to/video.mp4"
    cap = cv2.VideoCapture(video_path)
    track_history = defaultdict(list)
    
    while cap.isOpened():
        success, frame = cap.read()
        if success:
            result = model.track(frame, persist=True)[0]
            annotated_frame = result.plot()
            if result.boxes and result.boxes.is_track:
                boxes = result.boxes.xywh.cpu()
                track_ids = result.boxes.id.int().cpu().tolist()
                for box, track_id in zip(boxes, track_ids):
                    x, y, w, h = box
                    track = track_history[track_id]
                    track.append((float(x), float(y)))
                    if len(track) > 30:
                        track.pop(0)
                    points = np.hstack(track).astype(np.int32).reshape((-1, 1, 2))
                    cv2.polylines(annotated_frame, [points], isClosed=False, color=(230, 230, 230), thickness=10)
            cv2.imshow("YOLO26 Tracking", annotated_frame)
            if cv2.waitKey(1) & 0xFF == ord("q"):
                break
        else:
            break
    cap.release()
    cv2.destroyAllWindows()

    This script will plot the tracking lines showing the movement paths of the tracked objects over time, providing valuable insights into object behavior and patterns.

Comments679

Ask a question, share your results, or help someone else.

  1. bhaskar2443053bhaskar2443053GitHub

    I’m using YOLO-Pose to detect and track people throughout ~1-hour videos and need each person to keep the same ID. I already have high-quality ground-truth tracks from a SAM-based tracker.

    Do you have a script that can fine-tune YOLO-Pose (and its tracker/Re-ID parameters) against this ground truth to squeeze out the best IDF1 or HOTA scores ?

    1
    1. maxexplorermaxexplorerGitHub

      Hi!

      Thank you for your great work on YOLO and tracking integration. We are using YOLO with ByteTrack/BoT-SORT for object counting and tracking and want to improve ID stability.

      1. We notice that sometimes, even when the object visually looks the same and the scene and motion are stable, the track_id changes. What parameters or reasons should we check to understand why the ID changes unexpectedly?

      2. Is the match_thresh parameter based on IoU, and is it possible to print or visualize the IoU used during matching for debugging?

      3. Is there a way to output Kalman-predicted boxes of lost tracks so we can analyze them?

      4. Could increasing the video FPS improve tracking and IoU consistency, or is this mainly related to detection stability and parameter tuning?

      5. Does ByteTrack or BoT-SORT use a min_hits parameter before confirming tracks, or are tracks confirmed immediately after creation?

      6. If a track is lost, can a new ID only be assigned on the second frame after reappearance if conf > new_track_thresh and then conf > track_high_thresh, or can it happen immediately on the first detection?

      7. Is the track_buffer parameter the number of frames a track is kept using Kalman prediction after it is lost before being deleted?

      8. Do you have any tips to improve tracking stability and avoid unnecessary ID switches?

      Thank you for your support!

    2. Glenn JocherGlenn JocherUltralytics

      @bhaskar2443053 currently there isn't a built-in script for fine-tuning YOLO-Pose tracking parameters against ground truth. You would need to implement custom evaluation using libraries like TrackEval or MOTChallenge to compute IDF1/HOTA scores and optimize tracker parameters iteratively.

      @maxexplorer great questions about tracking stability! Here are the key points:

      1. ID changes typically occur due to low IoU between predicted and detected boxes, occlusions, or threshold mismatches. Check track_high_thresh and match_thresh in your tracker config.

      2. match_thresh is indeed IoU-based. For debugging, you can add logging to the tracker code, but there's no built-in visualization.

      3. Kalman predictions aren't directly exposed, but you can access them by modifying the tracker implementation.

      4. Higher FPS generally improves tracking by reducing inter-frame motion, making associations more reliable.

      5. Both trackers use confirmation logic - tracks need to meet certain criteria before being confirmed as valid tracks.

      6. New tracks can be assigned immediately if detection confidence exceeds new_track_thresh.

      7. Correct - track_buffer maintains lost tracks for that many frames using prediction.

      8. For stability: increase track_buffer, adjust match_thresh based on your scenario, and ensure consistent detection quality with appropriate conf thresholds.

      For detailed parameter tuning, refer to the tracker configuration files and our tracking documentation.

  2. rohith-aiunikarohith-aiunikaGitHub(edited)

    Hi YOLO Team, I've been using the following Python script to track objects with my YOLOv8 model: from ultralytics import YOLO

    model = YOLO(r"/home/best_pt_files/v1.0.2.pt") results = model.track(r"/home/video.MP4", save=False, stream=False)

    However, I noticed that my GPU memory usage gradually increases, leading to an "out of memory" error before the video finishes processing. Interestingly, when I use the following command from the terminal, the entire video tracks successfully without excessive GPU memory consumption: yolo track model=/home/best_pt_files/1.0.2.pt source=/home/video.MP4

    Previously, the Python script worked without any issues. The memory issue has only recently started occurring. Could you please help me understand the possible reasons for this memory increase and how to address it?

    i have experimented with persist= True and False,stream = true and false, show = true and false also but the issue is still there

    1
    1. Glenn JocherGlenn JocherUltralytics

      @rohith-aiunika thanks for reporting this. The memory buildup in Python scripts (but not CLI) typically occurs when not using streaming mode. To match the CLI's memory behavior, modify your Python script to process frames iteratively using stream=True:

      from ultralytics import YOLO
      
      model = YOLO("/home/best_pt_files/v1.0.2.pt")
      for result in model.track("/home/video.MP4", stream=True):  # Process frames iteratively
          pass  # Add your frame processing logic here

      This streams frames sequentially like the CLI, preventing memory accumulation. For more details, see the streaming implementation in our Tracking documentation. Let us know if this resolves the issue!

    2. rohith-aiunikarohith-aiunikaGitHub

      i have used stream = True, then the code is just executing without showing error and without tracking also

    3. Glenn JocherGlenn JocherUltralytics

      @rohith-aiunika thanks for the update. Let's ensure you're properly iterating through streaming results. Try adding a processing loop like this:

      for result in model.track(source=video_path, stream=True):
          annotated_frame = result[0].plot()
          cv2.imshow('Tracking', annotated_frame)
          if cv2.waitKey(1) == ord('q'):
              break

      This actively processes each frame and displays tracking annotations. If issues persist, you might need to verify your video source format using our supported inference sources documentation.

  3. mwaselamwaselaGitHub

    Hello, Made a custom model that i can successfully run and with a well tweaked confidence filter, am getting good results.

    Now Ive wrapped the classification and tracking functionality into a flask app, that returns the count ID's when accessed by a REST call like get, meaning it can be accessed by a separate app for processing and stuff. I want to be able to reset the counting through some other request. My camera feed is always live, i need a way to reset the track id's to zero when neccessary, they just keep counting until i exit the program.

    1
    1. Glenn JocherGlenn JocherUltralytics

      @mwasela hello,

      It's great to hear that your custom model is performing well. To reset the tracking IDs in your Flask app, you can implement a mechanism to reinitialize the tracker state upon receiving a specific request. This can be done by resetting the tracker object or reloading the model within your tracking function.

      For more detailed guidance on configuring and using trackers, please refer to our documentation: Multi-Object Tracking with Ultralytics YOLO.

  4. Orziqulov-JavohirOrziqulov-JavohirGitHub

    I really need help to do this programm: I wanna select region via mouse "dangerous zone" Track detected person, If person enters this zone crop and save person's photo Can you show me pls how to do it for 10 cameras, my gpu is RTX 3080TI

    1
    1. Orziqulov-JavohirOrziqulov-JavohirGitHub

      import cv2 from ultralytics import YOLO import time

      Load the YOLOv8 model

      model = YOLO("yolov8n.pt") # pretrained YOLOv8n model

      Initialize video capture for multiple cameras

      camera_ids = [

      'rtsp://admin:admin12345@172.17.67.20:554',

      'rtsp://admin:admin12345@172.17.67.16:554', 'rtsp://admin:admin12345@172.17.67.18:554', 'rtsp://admin:admin12345@172.17.67.19:554', 'rtsp://admin:admin12345@172.17.67.17:554', 'rtsp://admin:admin12345@172.17.67.21:554', 'rtsp://admin:admin12345@172.17.67.17:554', 'rtsp://admin:admin12345@172.17.67.21:554', 'rtsp://admin:admin12345@172.17.67.17:554',
      ]

      caps = [cv2.VideoCapture(camera_id) for camera_id in camera_ids]

      Open the video file

      roi_start = [(0, 0)] * len(caps) roi_end = [(0, 0)] * len(caps) drawing = [False] * len(caps)

      tracked_persons = [[] for _ in range(len(caps))] # List to track persons in dangerous zones

      def draw_rectangle(event, x, y, flags, param): global roi_start, roi_end, drawing idx = param # Use param to identify the camera index if event == cv2.EVENT_LBUTTONDOWN: drawing[idx] = True roi_start[idx] = (x, y) roi_end[idx] = (x, y) print(f"Camera {idx}: Started drawing rectangle at: {roi_start[idx]}") elif event == cv2.EVENT_MOUSEMOVE: if drawing[idx]: roi_end[idx] = (x, y) print(f"Camera {idx}: Updated rectangle end to: {roi_end[idx]}") elif event == cv2.EVENT_LBUTTONUP: drawing[idx] = False roi_end[idx] = (x, y) print(f"Camera {idx}: Finished drawing rectangle at: {roi_end[idx]}")

      Create a window and set mouse callback for each camera

      for idx in range(len(caps)): window_name = f'Video {idx}' cv2.namedWindow(window_name) cv2.setMouseCallback(window_name, draw_rectangle, idx)

      def is_inside_roi(x, y, roi_start, roi_end): x1, y1 = roi_start x2, y2 = roi_end return min(x1, x2) <= x <= max(x1, x2) and min(y1, y2) <= y <= max(y1, y2)

      while True: frames = [] for cap in caps: ret, frame = cap.read() if not ret: continue # frame = cv2.resize(frame, (640, 480)) frames.append(frame)

      for idx, frame in enumerate(frames):
          window_name = f'Video {idx}'
          
          # Draw the ROI rectangle on the frame
          if drawing[idx] or roi_start[idx] != roi_end[idx]:
              cv2.rectangle(frame, roi_start[idx], roi_end[idx], (0, 255, 0), 2)
              cv2.putText(frame, "Dangerous Zone", (roi_start[idx][0], roi_start[idx][1] - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 255, 0), 2)
      
          # Run YOLOv8 tracking on the frame, persisting tracks between frames
          results = model.track(frame, tracker="bytetrack.yaml", persist=True)
      
          for result in results:
              boxes = result.boxes
      
              for box in boxes:
                  x1, y1, x2, y2 = box.xyxy.tolist()[0][:4]
                  cls = box.cls.tolist()[0]
                  if cls == 0:  # Person class in COCO dataset
                      # Check if the center of the person is inside the ROI
                      cx, cy = int((x1 + x2) / 2), int((y1 + y2) / 2)
                      if is_inside_roi(cx, cy, roi_start[idx], roi_end[idx]):
                          # Check if this person is already tracked
                          person_id = box.id.tolist()[0] if box.id is not None else None
                          
                          if person_id not in tracked_persons[idx]:
                              tracked_persons[idx].append(person_id)
                              # Crop and save the frame
                              crop = frame[int(y1):int(y2), int(x1):int(x2)]
                              cv2.imwrite(f"cropped_camera{idx}_person{person_id}_{int(time.time())}.jpg", crop)
                              # Draw a label on the person
                              cv2.putText(frame, "Person in Dangerous Zone", (int(x1), int(y1) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 255), 2)
                              cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 0, 255), 2)
      
          cv2.imshow(window_name, frame)
      
      if cv2.waitKey(1) & 0xFF == ord('q'):
          break

      Release all video captures

      for cap in caps: cap.release() cv2.destroyAllWindows()

    2. Glenn JocherGlenn JocherUltralytics

      Your approach looks solid for setting up a security program to track and save images of individuals entering a designated "dangerous zone" across multiple cameras. To enhance your implementation, ensure you're using the latest versions of the Ultralytics package for optimal performance and bug fixes. For more detailed guidance on tracking configurations and additional features, you can refer to the Ultralytics tracking documentation. If you encounter any specific issues or need further assistance, feel free to ask!

    3. mariem158mariem158GitHub

      File "", line 91 results = model.track(frame, persist=True, tracker="botsort.yaml") ^ IndentationError: unexpected indent COMMENT CORRIGER CETTE ERREUR

    4. Glenn JocherGlenn JocherUltralytics

      The error you're encountering is due to an unexpected indent in your code. Ensure that the indentation is consistent throughout your script. If the issue persists, please verify it with the latest version of the Ultralytics package. For more details on tracking configurations, you can refer to the Ultralytics tracking documentation.

    1. GKYofficeGKYofficeGitHub

      Can I set the parameter ''save_txt'' to True in the track function, and then write code to evaluate the tracking data?

    2. Paula DerrengerPaula DerrengerUltralytics

      Currently, the Ultralytics YOLO framework doesn't provide built-in functionality for computing tracking metrics like MOTA or IDF1 directly through the API or command line. However, you can utilize external libraries such as MOTChallenge eval for calculating these metrics.

      After running your tracking with YOLOv8, you'd typically export your tracking results in a format compatible with these libraries (usually a .txt format with frame ID, object ID, bounding boxes, etc.). Then, use the external library to compute the desired metrics by providing it with both the ground truth and your results file.

      Here's a generic example on how you might save tracking results (modify based on your actual use case):

      from ultralytics import YOLO
      
      # Initialize and load your model
      model = YOLO("path/to/your/model.pt")
      
      # Tracking
      results = model.track(source='path/to/video.mp4', save_txt=True, project='runs/track', name='exp')
      
      # results are saved in 'runs/track/exp' in a format that can be used with MOTChallenge eval tools

      Afterwards, follow the documentation of the MOTChallenge eval toolkit to compute the metrics using the outputs stored in 'runs/track/exp'.

      If you encounter any specific issues while implementing this, feel free to ask!🛠️

  5. AdadAlShababAdadAlShababGitHub

    Can I run two models simultaneously in one video? Ex: model = YOLO('E:/Ultralytics/best.pt') model = YOLO('yolov8m-pose.pt') results = model.track(source="file.mp4", show=True, conf=0.3, save=False) results = model.track(source="file.mp4", show=True, conf=0.3, save=False)

    I want that two models will works simultaneously with cumulative results? Is it possible? Please let me know. Thanks in advance!!

    1
    1. Paula DerrengerPaula DerrengerUltralytics

      @AdadAlShabab yes, you can run two models simultaneously on one video, but you'll need to manage the inference in a way that allows both models to process the video frames concurrently. Here's a simplified example using threading to achieve this:

      import threading
      from ultralytics import YOLO
      
      # Load the models
      model1 = YOLO('E:/Ultralytics/best.pt')
      model2 = YOLO('yolov8m-pose.pt')
      
      # Function to run tracking
      def track_video(model, source, conf, show, save):
          results = model.track(source=source, show=show, conf=conf, save=save)
          # Process results here or return them
      
      # Create threads for each model
      thread1 = threading.Thread(target=track_video, args=(model1, "file.mp4", 0.3, True, False))
      thread2 = threading.Thread(target=track_video, args=(model2, "file.mp4", 0.3, True, False))
      
      # Start the threads
      thread1.start()
      thread2.start()
      
      # Wait for both threads to finish
      thread1.join()
      thread2.join()

      This script will run both models in separate threads. If you want to combine the results from both models, you'll need to implement a mechanism to synchronize and merge the results from both threads. Keep in mind that displaying the results (show=True) from two threads might not work as expected, and you might need to handle the visualization separately.

      Remember to handle the GPU memory usage carefully, as running two models simultaneously will require more memory. If you encounter any issues, consider running the models sequentially or on separate GPUs if available.

      4
  6. Journey2-20Journey2-20GitHub

    Hello I'm newer to Opencv and I'm trying to run a tracking camera for airsoft. How would i select specific bounding boxes?

    1. Paula DerrengerPaula DerrengerUltralytics

      Use model.track(frame, persist=True) and inspect results[0].boxes.xyxy with results[0].boxes.id; select the box containing your OpenCV mouse-click coordinates, store its track ID, then process or draw only boxes whose boxes.id matches that ID. Feel free to continue posting questions here so the Ultralytics team can help.

  7. yashpatel2010yashpatel2010GitHub

    In my jetson orin nano super developer kit with jetpack6.1 and deepstream7.1 i want to do cross camera person tracking. Is there any default way in deepstream configs to do it? Or any other lightweight thing that can be easily doable in jetson orin nano 8 gb super dev kit.

    1. Glenn JocherGlenn JocherUltralytics

      Ultralytics track is single-camera MOT, so cross-camera person IDs are not built in; on DeepStream 7.1 the practical Orin Nano path is NvDeepSORT/ReID per stream plus downstream association from the exported embeddings described in the DeepStream gst-nvtracker docs, or NVIDIA’s separate Multi-Camera Tracking workflow for full MTMC. Please keep the details here in Discussions and share your current tracker config if you want a minimal setup suggestion. (docs.nvidia.com)

  8. anvi97anvi97GitHub(edited)

    Hi @glenn-jocher,

    I am building a road surface detection system using YOLO segmentation.

    My pipeline:

    Apply bird eye view using cv2.getPerspectiveTransform()

    Mask only road area

    Detect potholes, zebra crossings, and speed bumps

    I am facing the following issues:

    1️⃣ Overlapping Potholes Not Merging Properly

    I want overlapping potholes to merge into one bounding box.

    In many cases it works, but sometimes:

    Two boxes are still shown for the same pothole

    Even when they overlap visually

    I am using YOLO with:

    results = model(frame, conf=0.45, iou=0.4)

    Is YOLO’s internal NMS preventing full merging? Should I apply custom merging after inference? Is segmentation-based merging better than bounding box merging for potholes?

    2️⃣ Object Size Changing After Bird Eye View

    I calculate pothole size using:

    pixels_per_foot = 220

    But I selected this value randomly (no real-world measurement).

    Because of this:

    Object size changes depending on frame position

    Same pothole appears different size

    Is it correct that without real-world calibration, size cannot be accurate? What is the correct method to calculate real-world size in a monocular dashcam setup?

    3️⃣ Zebra Crossing vs Black–White Speed Bump Confusion

    Black–white speed bumps are getting confused with zebra crossings because both have parallel white-black stripes.

    Currently I am detecting only yellow speed bumps to avoid confusion.

    What is the better approach to separate them?

    Use segmentation instead of boxes?

    Add geometry-based filtering?

    Train with more hard-negative examples?

    1. anvi97anvi97GitHub

      this is my code ..... from ultralytics import YOLO import cv2 import numpy as np

      model = YOLO("runs/segment/pothole_seg_model3/weights/best.pt") pixels_per_foot = 220

      video_path = "sample_video.mp4" cap = cv2.VideoCapture(video_path)

      if not cap.isOpened(): print(" Cannot open video") exit()

      width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) fps = cap.get(cv2.CAP_PROP_FPS) or 25.0

      fourcc = cv2.VideoWriter_fourcc(*'avc1') out = cv2.VideoWriter("pothole5.mp4", fourcc, fps, (width, height))

      STRETCH_AMOUNT = 50

      src = np.float32([ [0, height],
      [width, height],
      [922, 183],
      [265, 183]
      ])

      dst = np.float32([ [0, height], [width, height], [width + STRETCH_AMOUNT, 0], [0 - STRETCH_AMOUNT, 0] ])

      matrix = cv2.getPerspectiveTransform(src, dst) def compute_iou(box1, box2): x1 = max(box1[0], box2[0]) y1 = max(box1[1], box2[1]) x2 = min(box1[2], box2[2]) y2 = min(box1[3], box2[3])

      inter_area = max(0, x2 - x1) * max(0, y2 - y1)
      
      box1_area = (box1[2] - box1[0]) * (box1[3] - box1[1])
      box2_area = (box2[2] - box2[0]) * (box2[3] - box2[1])
      
      union = box1_area + box2_area - inter_area
      
      if union == 0:
          return 0
      
      return inter_area / union

      def should_merge(box1, box2, iou_thresh=0.1, dist_thresh=150): # Containment check - if one box is inside the other if (box1[0] >= box2[0] and box1[1] >= box2[1] and box1[2] <= box2[2] and box1[3] <= box2[3]): return True if (box2[0] >= box1[0] and box2[1] >= box1[1] and box2[2] <= box1[2] and box2[3] <= box1[3]): return True

      iou = compute_iou(box1, box2)
      if iou > iou_thresh:
          return True
      
      cx1, cy1 = (box1[0]+box1[2])/2, (box1[1]+box1[3])/2
      cx2, cy2 = (box2[0]+box2[2])/2, (box2[1]+box2[3])/2
      
      if np.sqrt((cx1-cx2)**2 + (cy1-cy2)**2) < dist_thresh:
          return True
      
      return False

      while True: ret, frame = cap.read() if not ret: break

      bird_eye = cv2.warpPerspective(frame, matrix, (width, height))
      
      results = model(bird_eye, conf=0.45, iou=0.4)[0]
      annotated = results.plot()
      
      if results.boxes is not None:
          for box in results.boxes:
              cls_id = int(box.cls.item())
      
              if cls_id == 0:  # pothole only
                  x1, y1, x2, y2 = map(int, box.xyxy.cpu().numpy()[0])
      
                  w_px = x2 - x1
                  h_px = y2 - y1
      
                  w_ft = w_px / pixels_per_foot
                  h_ft = h_px / pixels_per_foot
      
                  label = f"{w_ft:.2f}ft x {h_ft:.2f}ft"
      
                  cv2.putText(
                      annotated,
                      label,
                      (x1, y1 - 15),
                      cv2.FONT_HERSHEY_SIMPLEX,
                      0.6,
                      (0, 255, 255),
                      2
                  )
      
      cv2.imshow("Bird Eye + Pothole Detection", annotated)
      out.write(annotated)
      
      if cv2.waitKey(1) & 0xFF == 27:
          break

      cap.release() out.release() cv2.destroyAllWindows()

    2. Glenn JocherGlenn JocherUltralytics

      For (1), YOLO’s NMS will only suppress duplicate detections based on iou=...—it won’t “merge” two overlapping detections into a larger union box, and in your snippet compute_iou() / should_merge() aren’t actually applied to results.boxes, so you’re still visualizing the raw post-NMS output; since you’re using a Segment model, the most robust way to merge “split” potholes is to union the pothole masks and re-extract connected components (then box/measure those) rather than trying to union boxes.

      import cv2
      import numpy as np
      
      r = model(bird_eye, conf=0.45, iou=0.4)[0]
      
      if r.masks is not None and r.boxes is not None:
          cls = r.boxes.cls.int().cpu().numpy()
          pothole_idx = np.where(cls == 0)[0]
          if pothole_idx.size:
              m = r.masks.data[pothole_idx].cpu().numpy()  # (N,H,W), 0/1
              union = (m.sum(0) > 0).astype(np.uint8) * 255
              n, labels = cv2.connectedComponents(union)
              for k in range(1, n):
                  ys, xs = np.where(labels == k)
                  x1, y1, x2, y2 = xs.min(), ys.min(), xs.max(), ys.max()
                  cv2.rectangle(bird_eye, (x1, y1), (x2, y2), (0, 255, 255), 2)

      For (2), yes—without real-world calibration (and a valid “road plane” assumption), pixels_per_foot=220 can’t be physically meaningful, and even with a BEV warp you only get consistent scale if your homography is tied to metric coordinates; the usual monocular approach is: calibrate camera intrinsics, estimate the road plane, build a homography to a top-down plane in real units (or derive scale from at least one known road-plane measurement like lane width/stripe spacing), then measure in the BEV plane.

      For (3), the best fix is usually data: train both classes explicitly with lots of “hard negatives” (zebra crossings that look like bumps and vice versa) and keep labels consistent; segmentation often helps because you can then add simple shape/location constraints on the mask (zebra = large repeated pattern region, bump = single narrow band across lane) and optionally smooth decisions over time (if you’re doing video, using model.track(..., persist=True) from the Track mode docs can help stabilize per-frame flicker, even though it won’t merge instances within a frame).

      If you can paste !yolo checks output and one representative frame (plus what you consider the “correct” merged result), I can suggest concrete thresholds/post-processing for your exact outputs.

    3. anvi97anvi97GitHub

      Representative frame In the attached frame:

      The large water-filled pothole on the left should be treated as a single instance.

      The small region in the top-left corner (currently detected as separate) should be merged with the large region.

      Essentially, all visually continuous damaged area should result in one bounding box / one connected component.

      Currently:

      Mask union + connected components splits them due to small segmentation gaps

      Goal:

      Treat visually continuous pothole surface as one instance for measurement stability.

    4. Glenn JocherGlenn JocherUltralytics

      That split is expected because connectedComponents() treats any 1–2 px “mask hole” as a hard break, so add a small gap-closing step on the union mask (morphological close / dilate→erode) before components, with the kernel sized to the largest gap you still consider “visually continuous” (see Ultralytics YOLO segmentation docs for more mask/seg usage details):

      # union: uint8 {0,255}
      union = (m.sum(0) > 0).astype(np.uint8) * 255
      
      # bridge small gaps (tune kernel size to your expected mask discontinuities)
      k_close = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (25, 25))
      union_closed = cv2.morphologyEx(union, cv2.MORPH_CLOSE, k_close, iterations=1)
      
      # optional: remove tiny speckles after closing
      k_open = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7))
      union_closed = cv2.morphologyEx(union_closed, cv2.MORPH_OPEN, k_open, iterations=1)
      
      n, labels = cv2.connectedComponents(union_closed)
  9. mslabdelhak29mslabdelhak29GitHub

    Reid doesn't work how can i fix it i tried to change it True in botsort.yaml but it doesn't work

    1. Glenn JocherGlenn JocherUltralytics(edited)

      @mslabdelhak29 ReID is only supported by some trackers (for example BoT-SORT), not ByteTrack, and it’s enabled via the tracker YAML you pass at runtime. Copy the default config, enable ReID explicitly, and point track() to your copy:

      # custom_botsort.yaml
      tracker_type: botsort
      with_reid: True
      model: yolo26n-cls.pt  # or another YOLO classification weight you have locally
      from ultralytics import YOLO
      model = YOLO("yolo26n.pt")  # any YOLO detector
      results = model.track(source="path/to/video.mp4", tracker="custom_botsort.yaml", persist=True, show=False)

      If it still doesn’t work, you’re likely editing a different YAML than the one being loaded; always pass the path as above. Also share the output of yolo checks and the exact custom_botsort.yaml you used so we can verify your environment and keys. See the ReID section in the tracking guide and the BoT-SORT config for details: Multi‑Object Tracking docs and botsort.yaml config.