-
Notifications
You must be signed in to change notification settings - Fork 169
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
Ashp116
wants to merge
107
commits into
roboflow:main
Choose a base branch
from
Ashp116:feature/offline-tracker-ksp
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
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 b83da07
ADD: Added IOU calcuations of bboxes
Ashp116 53b8c91
ADD: Added a function to build a directed graph from detections
Ashp116 cb557cb
ADD: Added K-Shortest Paths algorithm
Ashp116 b082cd0
ADD: Added a function to process tracks from source to sink
Ashp116 0c260e3
BUG: Fixed the reset function
Ashp116 1adc1cf
BUG: Fixed IOU calculation, can_connected_nodes(), and update_detecti…
Ashp116 f24c25a
FIX: Fixes for bugs found for the KSP tracker issues during testing
Ashp116 1aba6c2
MISC: Added comments for functions and fixed formatting
Ashp116 db52617
MISC: Removed unused imports
Ashp116 31b5a5b
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] 206d532
MISC: Fixed formatting
Ashp116 0544904
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] f5dcaf3
MISC: Add type annotation for pre-commit
Ashp116 9d5d37d
MISC: Add networkx to dependency
Ashp116 0452a2f
FIX: Switch to sv.box_iou_batch
Ashp116 f3b849b
FIX: process_tracks returned None
Ashp116 614b2fb
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] 0e6bc41
UPDATE: Updated the way the directed acyclic graph is built
Ashp116 f82a112
UPDATE: Updated process_tasks
Ashp116 0543291
FIX: Removed the width and height parameters
Ashp116 1235657
FIX: Updated xyxy array type
Ashp116 c85e0b8
FIX: Fixes after testing
Ashp116 49a6b2e
UPDATE: Update KSP
Ashp116 1f965e6
ADD: Added Docstrings
Ashp116 1c0626a
Merge branch 'feature/offline-tracker-ksp' into fix/ksp-tracker
Ashp116 3ab72d9
Merge pull request #1 from Ashp116/fix/ksp-tracker
Ashp116 8f9e00b
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] 0e37b38
FIX: precommit fix
Ashp116 cc1f5d7
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] 1f43fc5
UPDATE: Made process_tracks return list of detections
Ashp116 2cf26d6
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] 11b68a4
UPDATE: removed all unused occurrences of 'max_gap'
Ashp116 6b8fbc3
UPDATE: Updated KSP solver
Ashp116 5134e5e
ADD: Docstrings
Ashp116 c0a0269
UPDATE: Updated graph to be a member variable
Ashp116 31715af
UPDATE: Docstrings
Ashp116 c8535ff
BUG: Diagnosing untracked detections
Ashp116 35404f2
BUG: Debugging KSP thru visuals
Ashp116 01c8cf1
BUG: Bellman Ford path visualization
Ashp116 e113c24
BUG: Same edge weights
Ashp116 0aaec74
BUG: Debugging KSP...
Ashp116 764c776
PROGRESS: Able to get less tracer switchs
Ashp116 ccd77af
CHECKPOINT: Changing the entirely
Ashp116 af750b6
UPDATE: Changed the entire logic for KSP
Ashp116 c764107
FIX: Testing and changing got something relative to expectation?
Ashp116 1113154
MISC: Clean up!
Ashp116 daa90dc
UPDATE: Tracker with the least change in detection position track a d…
Ashp116 4e79595
MISC: Docstrings
Ashp116 1a3a37a
Merge branch 'roboflow:main' into fix/ksp-disjoints
Ashp116 253a158
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] ab49445
Merge pull request #2 from Ashp116/fix/ksp-disjoints
Ashp116 23672e4
MISC: Precommit
Ashp116 13f2de2
MISC: Precommit
Ashp116 18e52c9
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] 704f476
MISC: Precommit
Ashp116 077167b
Merge branch 'feature/offline-tracker-ksp' of https://github.com/Ashp…
Ashp116 789d8cf
MISC: Precommit
Ashp116 a23cf66
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] 2aa2e0a
UPDATE: Added num_of_tracks param
Ashp116 dd2b4ff
Merge branch 'feature/offline-tracker-ksp' of https://github.com/Ashp…
Ashp116 9b70a31
UPDATE: Added tqdm and small changes
Ashp116 12b924a
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] b6ce7a2
UPDATE: Changes reflecting the comments from the code review
Ashp116 286f946
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] 89e792f
Pre-commit
Ashp116 9aadfa8
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] 2de57cc
Debug: Debugging the KSP solve
Ashp116 04dc790
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] 23efbe9
Debug: Debugging the KSP ith itr
Ashp116 47066b7
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] aa5bae4
Debug: Debugging the KSP solve
Ashp116 589c1dc
Debug: Change base penalty
Ashp116 9c7b994
UPDATE: Changed base_penalty to path_overlap_penalty hyper params
Ashp116 69bc783
UPDATE: Docstrings
Ashp116 203d936
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] eb61e98
Pre-commit
Ashp116 e5397ad
FIX: process_tracks docstrings
Ashp116 048de8c
FIX: process_tracks docstrings
Ashp116 7dcc03c
Pre-commit check
Ashp116 97eda16
Pre-commit
Ashp116 4a14d05
UPDATE: Updates referring to the code review
Ashp116 44d78aa
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] 875acd5
Pre-commit
Ashp116 19fad3b
ADD: Added documentation and changed folder name
Ashp116 e6020c4
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] b80eca7
OOPS: The ksp files were not added
Ashp116 d82f341
UPDATE: Updated mkdocs
Ashp116 6fe4324
UPDATE: Changes for code review
Ashp116 0a6869d
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] 35c06e0
ADD: Added doors
Ashp116 ee98e51
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] ceb0218
ADD: Added customizable doors and frame doors
Ashp116 1bf7d8e
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] f521b47
UPDATE: Removed debug path lens
Ashp116 8f1cc52
UPDATE: Docstrings
Ashp116 ab51903
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] 1ecd0f3
Precommit
Ashp116 297c919
Merge branch 'feature/offline-tracker-ksp' of https://github.com/Ashp…
Ashp116 55eed45
Precommit
Ashp116 5029b88
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] b62d140
Precommit
Ashp116 784fbb8
UPDATE: Updated docs
Ashp116 35cea66
UPDATE: Editable hyper parameters for entry and exit costs
Ashp116 7eeb8b8
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] b8d8289
BUG: Debugging the detections vs tracker Id
Ashp116 a9866bd
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: | ||
Ashp116 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"""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 | ||
Ashp116 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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( | ||
Ashp116 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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"): | ||
Ashp116 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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: | ||
Ashp116 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"""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): | ||
Ashp116 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
for node in path: | ||
assignments[(node.frame_id, node.detection_id)] = track_id | ||
Ashp116 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
return | ||
Ashp116 marked this conversation as resolved.
Show resolved
Hide resolved
|
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.