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

Draft
wants to merge 107 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
107 commits
Select commit Hold shift + click to select a range
d2e524e
ADD: Added KSPTracker for offline trackering
Ashp116 Jun 16, 2025
b83da07
ADD: Added IOU calcuations of bboxes
Ashp116 Jun 16, 2025
53b8c91
ADD: Added a function to build a directed graph from detections
Ashp116 Jun 16, 2025
cb557cb
ADD: Added K-Shortest Paths algorithm
Ashp116 Jun 16, 2025
b082cd0
ADD: Added a function to process tracks from source to sink
Ashp116 Jun 16, 2025
0c260e3
BUG: Fixed the reset function
Ashp116 Jun 16, 2025
1adc1cf
BUG: Fixed IOU calculation, can_connected_nodes(), and update_detecti…
Ashp116 Jun 16, 2025
f24c25a
FIX: Fixes for bugs found for the KSP tracker issues during testing
Ashp116 Jun 16, 2025
1aba6c2
MISC: Added comments for functions and fixed formatting
Ashp116 Jun 17, 2025
db52617
MISC: Removed unused imports
Ashp116 Jun 17, 2025
31b5a5b
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] Jun 17, 2025
206d532
MISC: Fixed formatting
Ashp116 Jun 17, 2025
0544904
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] Jun 17, 2025
f5dcaf3
MISC: Add type annotation for pre-commit
Ashp116 Jun 17, 2025
9d5d37d
MISC: Add networkx to dependency
Ashp116 Jun 19, 2025
0452a2f
FIX: Switch to sv.box_iou_batch
Ashp116 Jun 19, 2025
f3b849b
FIX: process_tracks returned None
Ashp116 Jun 19, 2025
614b2fb
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] Jun 19, 2025
0e6bc41
UPDATE: Updated the way the directed acyclic graph is built
Ashp116 Jun 19, 2025
f82a112
UPDATE: Updated process_tasks
Ashp116 Jun 19, 2025
0543291
FIX: Removed the width and height parameters
Ashp116 Jun 19, 2025
1235657
FIX: Updated xyxy array type
Ashp116 Jun 19, 2025
c85e0b8
FIX: Fixes after testing
Ashp116 Jun 19, 2025
49a6b2e
UPDATE: Update KSP
Ashp116 Jun 19, 2025
1f965e6
ADD: Added Docstrings
Ashp116 Jun 19, 2025
1c0626a
Merge branch 'feature/offline-tracker-ksp' into fix/ksp-tracker
Ashp116 Jun 19, 2025
3ab72d9
Merge pull request #1 from Ashp116/fix/ksp-tracker
Ashp116 Jun 19, 2025
8f9e00b
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] Jun 19, 2025
0e37b38
FIX: precommit fix
Ashp116 Jun 19, 2025
cc1f5d7
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] Jun 19, 2025
1f43fc5
UPDATE: Made process_tracks return list of detections
Ashp116 Jun 19, 2025
2cf26d6
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] Jun 19, 2025
11b68a4
UPDATE: removed all unused occurrences of 'max_gap'
Ashp116 Jun 19, 2025
6b8fbc3
UPDATE: Updated KSP solver
Ashp116 Jun 20, 2025
5134e5e
ADD: Docstrings
Ashp116 Jun 20, 2025
c0a0269
UPDATE: Updated graph to be a member variable
Ashp116 Jun 20, 2025
31715af
UPDATE: Docstrings
Ashp116 Jun 20, 2025
c8535ff
BUG: Diagnosing untracked detections
Ashp116 Jun 20, 2025
35404f2
BUG: Debugging KSP thru visuals
Ashp116 Jun 21, 2025
01c8cf1
BUG: Bellman Ford path visualization
Ashp116 Jun 22, 2025
e113c24
BUG: Same edge weights
Ashp116 Jun 22, 2025
0aaec74
BUG: Debugging KSP...
Ashp116 Jun 23, 2025
764c776
PROGRESS: Able to get less tracer switchs
Ashp116 Jun 23, 2025
ccd77af
CHECKPOINT: Changing the entirely
Ashp116 Jun 25, 2025
af750b6
UPDATE: Changed the entire logic for KSP
Ashp116 Jun 25, 2025
c764107
FIX: Testing and changing got something relative to expectation?
Ashp116 Jun 28, 2025
1113154
MISC: Clean up!
Ashp116 Jun 28, 2025
daa90dc
UPDATE: Tracker with the least change in detection position track a d…
Ashp116 Jun 28, 2025
4e79595
MISC: Docstrings
Ashp116 Jun 28, 2025
1a3a37a
Merge branch 'roboflow:main' into fix/ksp-disjoints
Ashp116 Jun 28, 2025
253a158
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] Jun 28, 2025
ab49445
Merge pull request #2 from Ashp116/fix/ksp-disjoints
Ashp116 Jun 28, 2025
23672e4
MISC: Precommit
Ashp116 Jun 28, 2025
13f2de2
MISC: Precommit
Ashp116 Jun 28, 2025
18e52c9
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] Jun 28, 2025
704f476
MISC: Precommit
Ashp116 Jun 28, 2025
077167b
Merge branch 'feature/offline-tracker-ksp' of https://github.com/Ashp…
Ashp116 Jun 28, 2025
789d8cf
MISC: Precommit
Ashp116 Jun 28, 2025
a23cf66
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] Jun 28, 2025
2aa2e0a
UPDATE: Added num_of_tracks param
Ashp116 Jun 28, 2025
dd2b4ff
Merge branch 'feature/offline-tracker-ksp' of https://github.com/Ashp…
Ashp116 Jun 28, 2025
9b70a31
UPDATE: Added tqdm and small changes
Ashp116 Jul 1, 2025
12b924a
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] Jul 1, 2025
b6ce7a2
UPDATE: Changes reflecting the comments from the code review
Ashp116 Jul 2, 2025
286f946
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] Jul 2, 2025
89e792f
Pre-commit
Ashp116 Jul 2, 2025
9aadfa8
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] Jul 2, 2025
2de57cc
Debug: Debugging the KSP solve
Ashp116 Jul 2, 2025
04dc790
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] Jul 2, 2025
23efbe9
Debug: Debugging the KSP ith itr
Ashp116 Jul 2, 2025
47066b7
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] Jul 2, 2025
aa5bae4
Debug: Debugging the KSP solve
Ashp116 Jul 2, 2025
589c1dc
Debug: Change base penalty
Ashp116 Jul 2, 2025
9c7b994
UPDATE: Changed base_penalty to path_overlap_penalty hyper params
Ashp116 Jul 2, 2025
69bc783
UPDATE: Docstrings
Ashp116 Jul 2, 2025
203d936
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] Jul 2, 2025
eb61e98
Pre-commit
Ashp116 Jul 2, 2025
e5397ad
FIX: process_tracks docstrings
Ashp116 Jul 2, 2025
048de8c
FIX: process_tracks docstrings
Ashp116 Jul 2, 2025
7dcc03c
Pre-commit check
Ashp116 Jul 2, 2025
97eda16
Pre-commit
Ashp116 Jul 2, 2025
4a14d05
UPDATE: Updates referring to the code review
Ashp116 Jul 3, 2025
44d78aa
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] Jul 3, 2025
875acd5
Pre-commit
Ashp116 Jul 3, 2025
19fad3b
ADD: Added documentation and changed folder name
Ashp116 Jul 4, 2025
e6020c4
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] Jul 4, 2025
b80eca7
OOPS: The ksp files were not added
Ashp116 Jul 4, 2025
d82f341
UPDATE: Updated mkdocs
Ashp116 Jul 4, 2025
6fe4324
UPDATE: Changes for code review
Ashp116 Jul 5, 2025
0a6869d
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] Jul 5, 2025
35c06e0
ADD: Added doors
Ashp116 Jul 9, 2025
ee98e51
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] Jul 9, 2025
ceb0218
ADD: Added customizable doors and frame doors
Ashp116 Jul 9, 2025
1bf7d8e
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] Jul 9, 2025
f521b47
UPDATE: Removed debug path lens
Ashp116 Jul 9, 2025
8f1cc52
UPDATE: Docstrings
Ashp116 Jul 9, 2025
ab51903
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] Jul 9, 2025
1ecd0f3
Precommit
Ashp116 Jul 9, 2025
297c919
Merge branch 'feature/offline-tracker-ksp' of https://github.com/Ashp…
Ashp116 Jul 9, 2025
55eed45
Precommit
Ashp116 Jul 9, 2025
5029b88
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] Jul 9, 2025
b62d140
Precommit
Ashp116 Jul 9, 2025
784fbb8
UPDATE: Updated docs
Ashp116 Jul 9, 2025
35cea66
UPDATE: Editable hyper parameters for entry and exit costs
Ashp116 Jul 13, 2025
7eeb8b8
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] Jul 13, 2025
b8d8289
BUG: Debugging the detections vs tracker Id
Ashp116 Jul 13, 2025
a9866bd
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] Jul 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
277 changes: 277 additions & 0 deletions trackers/core/ksptracker/tracker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
from dataclasses import dataclass
from typing import Any, Dict, List, Union

import networkx as nx
import numpy as np
import supervision as sv

from trackers.core.base import BaseTracker


@dataclass(frozen=True)
class TrackNode:
"""Represents a detection node in the tracking graph.

Attributes:
frame_id (int): Frame index where detection occurred
detection_id (int): Detection index within the frame
bbox (tuple): Bounding box coordinates (x1, y1, x2, y2)
confidence (float): Detection confidence score
"""

frame_id: int
detection_id: int
bbox: tuple
confidence: float

def __hash__(self) -> int:
"""Generates hash based on frame_id and detection_id.

Returns:
int: Hash value of the node
"""
return hash((self.frame_id, self.detection_id))

def __eq__(self, other: Any) -> bool:
"""Compares equality based on frame_id and detection_id.

Args:
other (Any): Object to compare with

Returns:
bool: True if nodes are equal, False otherwise
"""
if not isinstance(other, TrackNode):
return False
return (self.frame_id, self.detection_id) == (
other.frame_id,
other.detection_id,
)


class KSPTracker(BaseTracker):
"""Offline tracker using K-Shortest Paths (KSP) algorithm.

Attributes:
max_gap (int): Maximum allowed frame gap between detections in a track
min_confidence (float): Minimum confidence threshold for detections
max_paths (int): Maximum number of paths to find in KSP algorithm
max_distance (float): Maximum allowed dissimilarity (1 - IoU) for edges
detection_buffer (List[sv.Detections]): Buffer storing all frame detections
"""

def __init__(
self,
max_gap: int = 30,
min_confidence: float = 0.3,
max_paths: int = 1000,
max_distance: float = 0.3,
) -> None:
"""Initialize KSP tracker with configuration parameters.

Args:
max_gap (int): Max frame gap between connected detections
min_confidence (float): Minimum detection confidence
max_paths (int): Maximum number of paths to find
max_distance (float): Max dissimilarity (1-IoU) for connections
"""
self.max_gap = max_gap
self.min_confidence = min_confidence
self.max_paths = max_paths
self.max_distance = max_distance
self.reset()

def reset(self) -> None:
"""Reset the tracker's internal state."""
self.detection_buffer: List[sv.Detections] = []

def update(self, detections: sv.Detections) -> sv.Detections:
"""Update tracker with new detections (stores without processing).

Args:
detections (sv.Detections): New detections for current frame

Returns:
sv.Detections: Input detections (unmodified)
"""
self.detection_buffer.append(detections)
return detections

def _calc_iou(
self, bbox1: Union[np.ndarray, tuple], bbox2: Union[np.ndarray, tuple]
) -> float:
"""Calculate Intersection over Union (IoU) between two bounding boxes.

Args:
bbox1 (Union[np.ndarray, tuple]): First bounding box (x1, y1, x2, y2)
bbox2 (Union[np.ndarray, tuple]): Second bounding box (x1, y1, x2, y2)

Returns:
float: IoU value between 0.0 and 1.0
"""
bbox1 = np.array(bbox1)
bbox2 = np.array(bbox2)

x1 = max(bbox1[0], bbox2[0])
y1 = max(bbox1[1], bbox2[1])
x2 = min(bbox1[2], bbox2[2])
y2 = min(bbox1[3], bbox2[3])

inter_area = max(0, x2 - x1) * max(0, y2 - y1)
area1 = (bbox1[2] - bbox1[0]) * (bbox1[3] - bbox1[1])
area2 = (bbox2[2] - bbox2[0]) * (bbox2[3] - bbox2[1])
union = area1 + area2 - inter_area + 1e-5 # epsilon to avoid div by 0

return inter_area / union

def _can_connect_nodes(self, node1: TrackNode, node2: TrackNode) -> bool:
"""Determine if two nodes can be connected based on IoU threshold.

Args:
node1 (TrackNode): First track node
node2 (TrackNode): Second track node

Returns:
bool: True if nodes can be connected, False otherwise
"""
if node2.frame_id <= node1.frame_id:
return False
if node2.frame_id - node1.frame_id > self.max_gap:
return False
iou = self._calc_iou(node1.bbox, node2.bbox)
return iou >= (1 - self.max_distance)

def _edge_cost(self, node1: TrackNode, node2: TrackNode) -> float:
"""Calculate edge cost between two nodes.

Args:
node1 (TrackNode): First track node
node2 (TrackNode): Second track node

Returns:
float: Edge cost based on IoU and frame gap
"""
iou = self._calc_iou(node1.bbox, node2.bbox)
frame_gap = node2.frame_id - node1.frame_id
return -iou * (1.0 / frame_gap)

def _build_graph(self, all_detections: List[sv.Detections]) -> nx.DiGraph:
"""Build directed graph from all detections.

Args:
all_detections (List[sv.Detections]): List of detections from frames

Returns:
nx.DiGraph: Directed graph with detection nodes and edges
"""
G = nx.DiGraph()
G.add_node("source")
G.add_node("sink")

# Add detection nodes and edges
for frame_idx, dets in enumerate(all_detections):
for det_idx in range(len(dets)):
if dets.confidence[det_idx] < self.min_confidence:
continue

node = TrackNode(
frame_id=frame_idx,
detection_id=det_idx,
bbox=tuple(dets.xyxy[det_idx]),
confidence=dets.confidence[det_idx],
)
G.add_node(node)

# Connect to source if first frame
if frame_idx == 0:
G.add_edge("source", node, weight=-node.confidence)

# Connect to sink if last frame
if frame_idx == len(all_detections) - 1:
G.add_edge(node, "sink", weight=0)

# Connect to future frames within max_gap
future_range = range(
frame_idx + 1,
min(frame_idx + self.max_gap + 1, len(all_detections)),
)
for future_idx in future_range:
future_dets = all_detections[future_idx]
for future_det_idx in range(len(future_dets)):
if future_dets.confidence[future_det_idx] < self.min_confidence:
continue

future_node = TrackNode(
frame_id=future_idx,
detection_id=future_det_idx,
bbox=tuple(future_dets.xyxy[future_det_idx]),
confidence=future_dets.confidence[future_det_idx],
)

if self._can_connect_nodes(node, future_node):
weight = self._edge_cost(node, future_node)
G.add_edge(node, future_node, weight=weight)

return G

def _update_detections_with_tracks(self, assignments: Dict) -> sv.Detections:
"""Update detections with track IDs based on assignments.

Args:
assignments (Dict): Maps (frame_id, det_id) to track_id

Returns:
sv.Detections: Updated detections with tracker_ids assigned
"""
all_detections = []
all_tracker_ids = []

for frame_idx, dets in enumerate(self.detection_buffer):
frame_tracker_ids = [-1] * len(dets)

for det_idx in range(len(dets)):
key = (frame_idx, det_idx)
if key in assignments:
frame_tracker_ids[det_idx] = assignments[key]

all_detections.append(dets)
all_tracker_ids.extend(frame_tracker_ids)

final_detections = sv.Detections.merge(all_detections)
final_detections.tracker_id = np.array(all_tracker_ids)

return final_detections

def ksp(self, graph: nx.DiGraph) -> List[List[TrackNode]]:
"""Find K-shortest paths in the graph.

Args:
graph (nx.DiGraph): Directed graph of detection nodes

Returns:
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"):
if len(paths) >= self.max_paths:
break
# Remove source and sink nodes from path
paths.append(path[1:-1])
return paths

def process_tracks(self) -> sv.Detections:
"""Process all buffered detections to create final tracks.

Returns:
sv.Detections: Detections with assigned track IDs
"""
graph = self._build_graph(self.detection_buffer)
paths = self.ksp(graph)

# Assign track IDs
assignments = {}
for track_id, path in enumerate(paths, start=1):
for node in path:
assignments[(node.frame_id, node.detection_id)] = track_id

return