Skip to content

Feature: Offline Tracker (KSP Tracker) #89

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: main
Choose a base branch
from

Conversation

Ashp116
Copy link

@Ashp116 Ashp116 commented Jun 17, 2025

Description

This PR introduces a new offline Multiple Object Tracker (MOT) based on the K-Shortest Paths (KSP) optimization algorithm. This tracker is based on the method proposed in the paper: "Multiple Object Tracking using K-Shortest Paths Optimization" by Jérôme Berclaz et al. The tracker models detections across frames as a directed acyclic graph and solves for globally optimal object trajectories by computing disjoint shortest paths.

The inspiration for this PR: #59

List any dependencies that are required for this change.

Yes, this PR needs networkx to build a directed acyclic graph.

Type of change

  • New feature (non-breaking change which adds functionality)
  • [➖] This change requires a documentation update: maybe? I'm not sure

How has this change been tested? Please provide a testcase or example of how you tested the change?

This PR can be tested using the Colab notebook provided here. The notebook can be used to test KSP Tracker. Currently, the notebook is using one of the MOT17 Challenge Datasets, specifically MOT17-02-FRCNN.

Any specific deployment considerations

N/A

Docs

  • [❌] Docs updated? What were the changes? Not yet

@CLAassistant
Copy link

CLAassistant commented Jun 17, 2025

CLA assistant check
All committers have signed the CLA.

Copy link
Collaborator

@soumik12345 soumik12345 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Ashp116, thanks for the PR!

I have mentioned some feedback regarding the accuracy of the implementation as per the paper and also regarding how the API for using a offline tracker should be like.

from dataclasses import dataclass
from typing import Any, Dict, List, Union

import networkx as nx
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add networkx as a optional dependency under ksptracker. Simply run the following command and it should update pyproject.toml and uv.lock

uv add networkx --optional ksptracker

self.detection_buffer.append(detections)
return detections

def _calc_iou(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The box_iou_batch function from supervision seems like a drop-in replacement from this.

paths.append(path[1:-1])
return paths

def process_tracks(self) -> sv.Detections:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, process_tracks should return a list[sv.Detections] where a list of tracklets are mapped to the corresponding frame of the video.


# Assign track IDs
assignments = {}
for track_id, path in enumerate(paths, start=1):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this loop takes a significant amount of time to execute, please wrap the enumerate with a tqdm progress bar. For example, like the following:

from tqdm.auto import tqdm

for track_id, path in tqdm(enumerate(paths, start=1), total=len(paths), desc="Processing tracks"):

for node in path:
assignments[(node.frame_id, node.detection_id)] = track_id

return
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I mentioned in my previous comment, please make sure process_tracks returns a list[sv.Detections] where a list of tracklets are mapped to the corresponding frame of the video. I think it misses the _update_detections_with_tracks call to apply the track IDs to the detections and return the final result.



@dataclass(frozen=True)
class TrackNode:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like this implementation uses the detections themselves as graph nodes, while the paper proposes a graph where nodes represent a grid of discrete physical locations at every point in time which allows the tracker to interpolate trajectories through areas with missing detections.

List[List[TrackNode]]: List of paths, each path is list of TrackNodes
"""
paths: List[List[TrackNode]] = []
for path in nx.shortest_simple_paths(graph, "source", "sink", weight="weight"):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nx.shortest_simple_paths generates all paths without repeated nodes between a source and target in order of increasing length, but that does not necessarily guarantee node-disjoint paths. This means that a single detection could be assigned to multiple tracks.

assignments = {}
for track_id, path in enumerate(paths, start=1):
for node in path:
assignments[(node.frame_id, node.detection_id)] = track_id
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This simply overwrites the track_id, meaning only the last path found containing that detection gets the assignment. This doesn't seem like an accurate representation of the disjoint-path formulation described in the paper.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants