visiongraph.tracker.FlateTracker
1from argparse import ArgumentParser 2from collections import defaultdict 3from dataclasses import dataclass 4from typing import List, Optional, Dict 5 6from visiongraph.result.ResultList import ResultList 7from visiongraph.result.spatial.ObjectDetectionResult import ObjectDetectionResult 8from visiongraph.tracker.BaseObjectDetectionTracker import BaseObjectDetectionTracker 9from visiongraph.tracker.ObjectAssignmentSolver import ObjectAssignmentSolver, CostFunctionType 10 11 12@dataclass 13class _FlateTrack: 14 id: int 15 reference: ObjectDetectionResult 16 age: int = 0 17 stale: int = 0 18 19 def update_reference(self): 20 self.reference.tracking_id = self.id 21 22 23class FlateTracker(BaseObjectDetectionTracker): 24 """ 25 Fast localization and tracking engine. 26 """ 27 28 def __init__( 29 self, 30 max_cost: float = 0.5, 31 min_alive: int = 0, 32 max_lost: int = 5, 33 class_aware: bool = False, 34 cost_function: Optional[CostFunctionType] = None, 35 ): 36 """ 37 Initializes the FlateTracker with specified parameters. 38 39 :param max_cost: Maximum cost for a trackable match. 40 :param min_alive: Minimum number of frames a track must be visible to be considered alive. 41 :param max_lost: Maximum number of frames a track can be lost before it is removed. 42 :param class_aware: If True, run class-aware matching. 43 :param cost_function: Cost function to use for matching. Defaults to L2 distance. 44 """ 45 self.max_cost: float = max_cost 46 47 self.min_alive: int = min_alive 48 self.max_lost: int = max_lost 49 50 self.include_stale: bool = False 51 52 self.class_aware: bool = class_aware 53 self.cost_function: CostFunctionType = ( 54 cost_function if cost_function is not None else ObjectAssignmentSolver.l2_cost_function 55 ) 56 57 self._tracks: List[_FlateTrack] = [] 58 self._unique_id: int = 0 59 60 def setup(self): 61 """ 62 Prepares the tracker for a new tracking session by clearing existing tracks and resetting the unique ID. 63 """ 64 self._tracks.clear() 65 self._unique_id = 0 66 67 def _new_id(self) -> int: 68 """ 69 Generates a new unique track ID. 70 71 :return: A new unique track ID. 72 """ 73 track_id = self._unique_id 74 self._unique_id += 1 75 return track_id 76 77 def process(self, detections: List[ObjectDetectionResult]) -> ResultList[ObjectDetectionResult]: 78 """ 79 Processes the given detections to update tracks and create new ones if necessary. 80 81 :param detections: A list of detected objects to process. 82 83 :return: A list of tracked objects. 84 """ 85 if not self._tracks and not detections: 86 return ResultList([]) 87 88 if not self._tracks: 89 for det in detections: 90 tr = _FlateTrack(self._new_id(), det) 91 tr.update_reference() 92 self._tracks.append(tr) 93 return ResultList([t.reference for t in self._tracks]) 94 95 if not detections: 96 for t in self._tracks: 97 t.stale += 1 98 t.reference.staleness = t.stale 99 self._tracks = [t for t in self._tracks if t.stale <= self.max_lost] 100 return ResultList( 101 [t.reference for t in self._tracks if t.age >= self.min_alive and (self.include_stale or t.stale == 0)] 102 ) 103 104 tracks_ref = [t.reference for t in self._tracks] 105 106 solver = ObjectAssignmentSolver(self.cost_function, self.max_cost) 107 108 assignments: Dict[ObjectDetectionResult, Optional[ObjectDetectionResult]] = {} 109 unassigned_destinations: List[ObjectDetectionResult] = [] 110 111 if not self.class_aware: 112 result = solver.solve(tracks_ref, detections) 113 assignments = result.assignments 114 unassigned_destinations = result.unassigned_destinations 115 else: 116 # Class-aware matching 117 track_classes = [r.class_id for r in tracks_ref] 118 det_classes = [d.class_id for d in detections] 119 120 # Group by class 121 tracks_by_class = defaultdict(list) 122 for i, c in enumerate(track_classes): 123 if c is not None: 124 tracks_by_class[c].append(tracks_ref[i]) 125 126 dets_by_class = defaultdict(list) 127 for i, c in enumerate(det_classes): 128 if c is not None: 129 dets_by_class[c].append(detections[i]) 130 131 known_classes = sorted(set(tracks_by_class.keys()) | set(dets_by_class.keys())) 132 133 matched_tracks = set() 134 matched_dets = set() 135 136 # Step 1: per-class matching for known classes 137 for cls_id in known_classes: 138 t_subset = tracks_by_class.get(cls_id, []) 139 d_subset = dets_by_class.get(cls_id, []) 140 141 if not t_subset or not d_subset: 142 continue 143 144 res = solver.solve(t_subset, d_subset) 145 146 for src, dst in res.assignments.items(): 147 assignments[src] = dst 148 matched_tracks.add(src) 149 if dst: 150 matched_dets.add(dst) 151 152 # Step 2: handle unknown-class tracks against any remaining detections 153 unknown_class_tracks = [ 154 t for i, t in enumerate(tracks_ref) if track_classes[i] is None and t not in matched_tracks 155 ] 156 rem_dets = [d for d in detections if d not in matched_dets] 157 158 if unknown_class_tracks and rem_dets: 159 res = solver.solve(unknown_class_tracks, rem_dets) 160 for src, dst in res.assignments.items(): 161 assignments[src] = dst 162 matched_tracks.add(src) 163 if dst: 164 matched_dets.add(dst) 165 166 # Fill unassigned destinations 167 unassigned_destinations = [d for d in detections if d not in matched_dets] 168 169 # Ensure all tracks are in assignments 170 for t in tracks_ref: 171 if t not in assignments: 172 assignments[t] = None 173 174 # Update tracks 175 for track in self._tracks: 176 dest = assignments.get(track.reference) 177 178 if dest is not None: 179 track.age += 1 180 track.stale = 0 181 track.reference = dest 182 track.update_reference() 183 else: 184 track.stale += 1 185 186 track.reference.staleness = track.stale 187 188 # Create new tracks 189 for det in unassigned_destinations: 190 tr = _FlateTrack(self._new_id(), det) 191 tr.update_reference() 192 tr.reference.staleness = 0 193 self._tracks.append(tr) 194 195 # Clean up stale tracks 196 self._tracks = [t for t in self._tracks if t.stale <= self.max_lost] 197 198 return ResultList( 199 [t.reference for t in self._tracks if t.age >= self.min_alive and (self.include_stale or t.stale == 0)] 200 ) 201 202 def release(self): 203 """ 204 Releases resources and clears all tracks. 205 """ 206 self._tracks.clear() 207 208 def configure(self, args): 209 """ 210 Configures the tracker with parameters from the provided argument parser. 211 """ 212 self.max_cost = self._get_param(args, "tracker_max_cost", self.max_cost) 213 self.min_alive = self._get_param(args, "tracker_min_alive", self.min_alive) 214 self.max_lost = self._get_param(args, "tracker_max_lost", self.max_lost) 215 216 @staticmethod 217 def add_params(parser: ArgumentParser): 218 """ 219 Adds command line parameters for configuring the tracker. 220 """ 221 parser.add_argument("--tracker-max-cost", type=float, default=0.5, help="Max cost for trackable match.") 222 parser.add_argument("--tracker-min-alive", type=int, default=0, help="Min frames trackable visible.") 223 parser.add_argument("--tracker-max-lost", type=int, default=5, help="Max frames trackable not visible.")
class
FlateTracker(visiongraph.GraphNode.GraphNode[visiongraph.result.ResultList.ResultList[visiongraph.result.spatial.ObjectDetectionResult.ObjectDetectionResult], visiongraph.result.ResultList.ResultList[visiongraph.result.spatial.ObjectDetectionResult.ObjectDetectionResult]], abc.ABC):
24class FlateTracker(BaseObjectDetectionTracker): 25 """ 26 Fast localization and tracking engine. 27 """ 28 29 def __init__( 30 self, 31 max_cost: float = 0.5, 32 min_alive: int = 0, 33 max_lost: int = 5, 34 class_aware: bool = False, 35 cost_function: Optional[CostFunctionType] = None, 36 ): 37 """ 38 Initializes the FlateTracker with specified parameters. 39 40 :param max_cost: Maximum cost for a trackable match. 41 :param min_alive: Minimum number of frames a track must be visible to be considered alive. 42 :param max_lost: Maximum number of frames a track can be lost before it is removed. 43 :param class_aware: If True, run class-aware matching. 44 :param cost_function: Cost function to use for matching. Defaults to L2 distance. 45 """ 46 self.max_cost: float = max_cost 47 48 self.min_alive: int = min_alive 49 self.max_lost: int = max_lost 50 51 self.include_stale: bool = False 52 53 self.class_aware: bool = class_aware 54 self.cost_function: CostFunctionType = ( 55 cost_function if cost_function is not None else ObjectAssignmentSolver.l2_cost_function 56 ) 57 58 self._tracks: List[_FlateTrack] = [] 59 self._unique_id: int = 0 60 61 def setup(self): 62 """ 63 Prepares the tracker for a new tracking session by clearing existing tracks and resetting the unique ID. 64 """ 65 self._tracks.clear() 66 self._unique_id = 0 67 68 def _new_id(self) -> int: 69 """ 70 Generates a new unique track ID. 71 72 :return: A new unique track ID. 73 """ 74 track_id = self._unique_id 75 self._unique_id += 1 76 return track_id 77 78 def process(self, detections: List[ObjectDetectionResult]) -> ResultList[ObjectDetectionResult]: 79 """ 80 Processes the given detections to update tracks and create new ones if necessary. 81 82 :param detections: A list of detected objects to process. 83 84 :return: A list of tracked objects. 85 """ 86 if not self._tracks and not detections: 87 return ResultList([]) 88 89 if not self._tracks: 90 for det in detections: 91 tr = _FlateTrack(self._new_id(), det) 92 tr.update_reference() 93 self._tracks.append(tr) 94 return ResultList([t.reference for t in self._tracks]) 95 96 if not detections: 97 for t in self._tracks: 98 t.stale += 1 99 t.reference.staleness = t.stale 100 self._tracks = [t for t in self._tracks if t.stale <= self.max_lost] 101 return ResultList( 102 [t.reference for t in self._tracks if t.age >= self.min_alive and (self.include_stale or t.stale == 0)] 103 ) 104 105 tracks_ref = [t.reference for t in self._tracks] 106 107 solver = ObjectAssignmentSolver(self.cost_function, self.max_cost) 108 109 assignments: Dict[ObjectDetectionResult, Optional[ObjectDetectionResult]] = {} 110 unassigned_destinations: List[ObjectDetectionResult] = [] 111 112 if not self.class_aware: 113 result = solver.solve(tracks_ref, detections) 114 assignments = result.assignments 115 unassigned_destinations = result.unassigned_destinations 116 else: 117 # Class-aware matching 118 track_classes = [r.class_id for r in tracks_ref] 119 det_classes = [d.class_id for d in detections] 120 121 # Group by class 122 tracks_by_class = defaultdict(list) 123 for i, c in enumerate(track_classes): 124 if c is not None: 125 tracks_by_class[c].append(tracks_ref[i]) 126 127 dets_by_class = defaultdict(list) 128 for i, c in enumerate(det_classes): 129 if c is not None: 130 dets_by_class[c].append(detections[i]) 131 132 known_classes = sorted(set(tracks_by_class.keys()) | set(dets_by_class.keys())) 133 134 matched_tracks = set() 135 matched_dets = set() 136 137 # Step 1: per-class matching for known classes 138 for cls_id in known_classes: 139 t_subset = tracks_by_class.get(cls_id, []) 140 d_subset = dets_by_class.get(cls_id, []) 141 142 if not t_subset or not d_subset: 143 continue 144 145 res = solver.solve(t_subset, d_subset) 146 147 for src, dst in res.assignments.items(): 148 assignments[src] = dst 149 matched_tracks.add(src) 150 if dst: 151 matched_dets.add(dst) 152 153 # Step 2: handle unknown-class tracks against any remaining detections 154 unknown_class_tracks = [ 155 t for i, t in enumerate(tracks_ref) if track_classes[i] is None and t not in matched_tracks 156 ] 157 rem_dets = [d for d in detections if d not in matched_dets] 158 159 if unknown_class_tracks and rem_dets: 160 res = solver.solve(unknown_class_tracks, rem_dets) 161 for src, dst in res.assignments.items(): 162 assignments[src] = dst 163 matched_tracks.add(src) 164 if dst: 165 matched_dets.add(dst) 166 167 # Fill unassigned destinations 168 unassigned_destinations = [d for d in detections if d not in matched_dets] 169 170 # Ensure all tracks are in assignments 171 for t in tracks_ref: 172 if t not in assignments: 173 assignments[t] = None 174 175 # Update tracks 176 for track in self._tracks: 177 dest = assignments.get(track.reference) 178 179 if dest is not None: 180 track.age += 1 181 track.stale = 0 182 track.reference = dest 183 track.update_reference() 184 else: 185 track.stale += 1 186 187 track.reference.staleness = track.stale 188 189 # Create new tracks 190 for det in unassigned_destinations: 191 tr = _FlateTrack(self._new_id(), det) 192 tr.update_reference() 193 tr.reference.staleness = 0 194 self._tracks.append(tr) 195 196 # Clean up stale tracks 197 self._tracks = [t for t in self._tracks if t.stale <= self.max_lost] 198 199 return ResultList( 200 [t.reference for t in self._tracks if t.age >= self.min_alive and (self.include_stale or t.stale == 0)] 201 ) 202 203 def release(self): 204 """ 205 Releases resources and clears all tracks. 206 """ 207 self._tracks.clear() 208 209 def configure(self, args): 210 """ 211 Configures the tracker with parameters from the provided argument parser. 212 """ 213 self.max_cost = self._get_param(args, "tracker_max_cost", self.max_cost) 214 self.min_alive = self._get_param(args, "tracker_min_alive", self.min_alive) 215 self.max_lost = self._get_param(args, "tracker_max_lost", self.max_lost) 216 217 @staticmethod 218 def add_params(parser: ArgumentParser): 219 """ 220 Adds command line parameters for configuring the tracker. 221 """ 222 parser.add_argument("--tracker-max-cost", type=float, default=0.5, help="Max cost for trackable match.") 223 parser.add_argument("--tracker-min-alive", type=int, default=0, help="Min frames trackable visible.") 224 parser.add_argument("--tracker-max-lost", type=int, default=5, help="Max frames trackable not visible.")
Fast localization and tracking engine.
FlateTracker( max_cost: float = 0.5, min_alive: int = 0, max_lost: int = 5, class_aware: bool = False, cost_function: Optional[Callable[[List[ObjectDetectionResult], List[ObjectDetectionResult]], numpy.ndarray]] = None)
29 def __init__( 30 self, 31 max_cost: float = 0.5, 32 min_alive: int = 0, 33 max_lost: int = 5, 34 class_aware: bool = False, 35 cost_function: Optional[CostFunctionType] = None, 36 ): 37 """ 38 Initializes the FlateTracker with specified parameters. 39 40 :param max_cost: Maximum cost for a trackable match. 41 :param min_alive: Minimum number of frames a track must be visible to be considered alive. 42 :param max_lost: Maximum number of frames a track can be lost before it is removed. 43 :param class_aware: If True, run class-aware matching. 44 :param cost_function: Cost function to use for matching. Defaults to L2 distance. 45 """ 46 self.max_cost: float = max_cost 47 48 self.min_alive: int = min_alive 49 self.max_lost: int = max_lost 50 51 self.include_stale: bool = False 52 53 self.class_aware: bool = class_aware 54 self.cost_function: CostFunctionType = ( 55 cost_function if cost_function is not None else ObjectAssignmentSolver.l2_cost_function 56 ) 57 58 self._tracks: List[_FlateTrack] = [] 59 self._unique_id: int = 0
Initializes the FlateTracker with specified parameters.
Parameters
- max_cost: Maximum cost for a trackable match.
- min_alive: Minimum number of frames a track must be visible to be considered alive.
- max_lost: Maximum number of frames a track can be lost before it is removed.
- class_aware: If True, run class-aware matching.
- cost_function: Cost function to use for matching. Defaults to L2 distance.
cost_function: Callable[[List[ObjectDetectionResult], List[ObjectDetectionResult]], numpy.ndarray]
def
setup(self):
61 def setup(self): 62 """ 63 Prepares the tracker for a new tracking session by clearing existing tracks and resetting the unique ID. 64 """ 65 self._tracks.clear() 66 self._unique_id = 0
Prepares the tracker for a new tracking session by clearing existing tracks and resetting the unique ID.
78 def process(self, detections: List[ObjectDetectionResult]) -> ResultList[ObjectDetectionResult]: 79 """ 80 Processes the given detections to update tracks and create new ones if necessary. 81 82 :param detections: A list of detected objects to process. 83 84 :return: A list of tracked objects. 85 """ 86 if not self._tracks and not detections: 87 return ResultList([]) 88 89 if not self._tracks: 90 for det in detections: 91 tr = _FlateTrack(self._new_id(), det) 92 tr.update_reference() 93 self._tracks.append(tr) 94 return ResultList([t.reference for t in self._tracks]) 95 96 if not detections: 97 for t in self._tracks: 98 t.stale += 1 99 t.reference.staleness = t.stale 100 self._tracks = [t for t in self._tracks if t.stale <= self.max_lost] 101 return ResultList( 102 [t.reference for t in self._tracks if t.age >= self.min_alive and (self.include_stale or t.stale == 0)] 103 ) 104 105 tracks_ref = [t.reference for t in self._tracks] 106 107 solver = ObjectAssignmentSolver(self.cost_function, self.max_cost) 108 109 assignments: Dict[ObjectDetectionResult, Optional[ObjectDetectionResult]] = {} 110 unassigned_destinations: List[ObjectDetectionResult] = [] 111 112 if not self.class_aware: 113 result = solver.solve(tracks_ref, detections) 114 assignments = result.assignments 115 unassigned_destinations = result.unassigned_destinations 116 else: 117 # Class-aware matching 118 track_classes = [r.class_id for r in tracks_ref] 119 det_classes = [d.class_id for d in detections] 120 121 # Group by class 122 tracks_by_class = defaultdict(list) 123 for i, c in enumerate(track_classes): 124 if c is not None: 125 tracks_by_class[c].append(tracks_ref[i]) 126 127 dets_by_class = defaultdict(list) 128 for i, c in enumerate(det_classes): 129 if c is not None: 130 dets_by_class[c].append(detections[i]) 131 132 known_classes = sorted(set(tracks_by_class.keys()) | set(dets_by_class.keys())) 133 134 matched_tracks = set() 135 matched_dets = set() 136 137 # Step 1: per-class matching for known classes 138 for cls_id in known_classes: 139 t_subset = tracks_by_class.get(cls_id, []) 140 d_subset = dets_by_class.get(cls_id, []) 141 142 if not t_subset or not d_subset: 143 continue 144 145 res = solver.solve(t_subset, d_subset) 146 147 for src, dst in res.assignments.items(): 148 assignments[src] = dst 149 matched_tracks.add(src) 150 if dst: 151 matched_dets.add(dst) 152 153 # Step 2: handle unknown-class tracks against any remaining detections 154 unknown_class_tracks = [ 155 t for i, t in enumerate(tracks_ref) if track_classes[i] is None and t not in matched_tracks 156 ] 157 rem_dets = [d for d in detections if d not in matched_dets] 158 159 if unknown_class_tracks and rem_dets: 160 res = solver.solve(unknown_class_tracks, rem_dets) 161 for src, dst in res.assignments.items(): 162 assignments[src] = dst 163 matched_tracks.add(src) 164 if dst: 165 matched_dets.add(dst) 166 167 # Fill unassigned destinations 168 unassigned_destinations = [d for d in detections if d not in matched_dets] 169 170 # Ensure all tracks are in assignments 171 for t in tracks_ref: 172 if t not in assignments: 173 assignments[t] = None 174 175 # Update tracks 176 for track in self._tracks: 177 dest = assignments.get(track.reference) 178 179 if dest is not None: 180 track.age += 1 181 track.stale = 0 182 track.reference = dest 183 track.update_reference() 184 else: 185 track.stale += 1 186 187 track.reference.staleness = track.stale 188 189 # Create new tracks 190 for det in unassigned_destinations: 191 tr = _FlateTrack(self._new_id(), det) 192 tr.update_reference() 193 tr.reference.staleness = 0 194 self._tracks.append(tr) 195 196 # Clean up stale tracks 197 self._tracks = [t for t in self._tracks if t.stale <= self.max_lost] 198 199 return ResultList( 200 [t.reference for t in self._tracks if t.age >= self.min_alive and (self.include_stale or t.stale == 0)] 201 )
Processes the given detections to update tracks and create new ones if necessary.
Parameters
- detections: A list of detected objects to process.
Returns
A list of tracked objects.
def
release(self):
203 def release(self): 204 """ 205 Releases resources and clears all tracks. 206 """ 207 self._tracks.clear()
Releases resources and clears all tracks.
def
configure(self, args):
209 def configure(self, args): 210 """ 211 Configures the tracker with parameters from the provided argument parser. 212 """ 213 self.max_cost = self._get_param(args, "tracker_max_cost", self.max_cost) 214 self.min_alive = self._get_param(args, "tracker_min_alive", self.min_alive) 215 self.max_lost = self._get_param(args, "tracker_max_lost", self.max_lost)
Configures the tracker with parameters from the provided argument parser.
@staticmethod
def
add_params(parser: argparse.ArgumentParser):
217 @staticmethod 218 def add_params(parser: ArgumentParser): 219 """ 220 Adds command line parameters for configuring the tracker. 221 """ 222 parser.add_argument("--tracker-max-cost", type=float, default=0.5, help="Max cost for trackable match.") 223 parser.add_argument("--tracker-min-alive", type=int, default=0, help="Min frames trackable visible.") 224 parser.add_argument("--tracker-max-lost", type=int, default=5, help="Max frames trackable not visible.")
Adds command line parameters for configuring the tracker.