Skip to content

Commit ceb0218

Browse files
committed
ADD: Added customizable doors and frame doors
1 parent ee98e51 commit ceb0218

2 files changed

Lines changed: 137 additions & 112 deletions

File tree

trackers/core/ksp/solver.py

Lines changed: 61 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from collections import defaultdict
22
from dataclasses import dataclass
3-
from typing import Any, List, Optional, Tuple
3+
from typing import Any, List, Optional, Tuple, Set
44

55
import networkx as nx
66
import numpy as np
@@ -10,18 +10,6 @@
1010

1111
@dataclass(frozen=True)
1212
class TrackNode:
13-
"""
14-
Represents a detection node in the tracking graph.
15-
16-
Attributes:
17-
frame_id (int): Frame index where detection occurred.
18-
det_idx (int): Detection index in the frame.
19-
class_id (int): Class ID of the detection.
20-
position (tuple): Center position of the detection.
21-
bbox (np.ndarray): Bounding box coordinates.
22-
confidence (float): Detection confidence score.
23-
"""
24-
2513
frame_id: int
2614
det_idx: int
2715
class_id: int
@@ -43,11 +31,6 @@ def __str__(self):
4331

4432

4533
class KSPSolver:
46-
"""
47-
Solver for the K-Shortest Paths (KSP) tracking problem.
48-
Builds a graph from detections and extracts multiple disjoint paths.
49-
"""
50-
5134
def __init__(
5235
self,
5336
path_overlap_penalty: float = 40,
@@ -56,17 +39,7 @@ def __init__(
5639
size_weight: float = 0.1,
5740
conf_weight: float = 0.1,
5841
):
59-
"""
60-
Initialize the KSPSolver.
61-
62-
Args:
63-
path_overlap_penalty (float): Penalty for edge reuse in successive paths.
64-
iou_weight (float): Weight for IoU penalty.
65-
dist_weight (float): Weight for center distance.
66-
size_weight (float): Weight for size penalty.
67-
conf_weight (float): Weight for confidence penalty.
68-
"""
69-
self.path_overlap_penalty = 40
42+
self.path_overlap_penalty = path_overlap_penalty if path_overlap_penalty is not None else 40
7043
self.weight_key = "weight"
7144
self.source = "SOURCE"
7245
self.sink = "SINK"
@@ -84,65 +57,80 @@ def __init__(
8457
if conf_weight is not None:
8558
self.weights["conf"] = conf_weight
8659

60+
# Entry/exit region settings
61+
self.entry_exit_regions: List[Tuple[int, int, int, int]] = [] # (x1, y1, x2, y2)
62+
63+
# Border region settings
64+
self.use_border_regions = True
65+
self.active_borders: Set[str] = {"left", "right", "top", "bottom"}
66+
self.border_margin = 40
67+
self.frame_size = (1920, 1080)
68+
8769
self.reset()
8870

8971
def reset(self) -> None:
90-
"""
91-
Reset the solver state and clear all detections and graph.
92-
This clears the detection buffer and initializes a new empty graph.
93-
"""
9472
self.detection_per_frame = []
9573
self.graph = nx.DiGraph()
9674

9775
def append_frame(self, detections: sv.Detections) -> None:
98-
"""
99-
Add detections for a new frame to the buffer.
100-
101-
Args:
102-
detections (sv.Detections): Detections for the frame.
103-
"""
10476
self.detection_per_frame.append(detections)
10577

10678
def _get_center(self, bbox: np.ndarray) -> np.ndarray:
79+
x1, y1, x2, y2 = bbox
80+
return np.array([(x1 + x2) / 2, (y1 + y2) / 2])
81+
82+
def set_entry_exit_regions(self, regions: List[Tuple[int, int, int, int]]) -> None:
83+
"""
84+
Set rectangular entry/exit zones (x1, y1, x2, y2).
10785
"""
108-
Compute the center of a bounding box.
86+
self.entry_exit_regions = regions
10987

110-
Args:
111-
bbox (np.ndarray): Bounding box coordinates (x1, y1, x2, y2).
88+
def set_border_entry_exit(
89+
self,
90+
use_border: Optional[bool] = True,
91+
borders: Optional[Set[str]] = None,
92+
margin: Optional[int] = 40,
93+
frame_size: Optional[Tuple[int, int]] = (1920, 1080),
94+
) -> None:
95+
"""
96+
Configure border-based entry/exit zones.
11297
113-
Returns:
114-
np.ndarray: Center coordinates (x, y).
98+
Args:
99+
use_border (bool): Enable/disable border-based entry/exit.
100+
borders (set): Set of borders to use. {"left", "right", "top", "bottom"}
101+
margin (int): Border thickness in pixels.
102+
frame_size (Tuple[int, int]): Size of the image (width, height).
115103
"""
116-
x1, y1, x2, y2 = bbox
117-
return np.array([(x1 + x2) / 2, (y1 + y2) / 2])
104+
self.use_border_regions = use_border
105+
self.active_borders = borders if borders is not None else {"left", "right", "top", "bottom"}
106+
self.border_margin = margin
107+
self.frame_size = frame_size
118108

119-
def _in_door(self, node: TrackNode):
109+
def _in_door(self, node: TrackNode) -> bool:
120110
x, y = node.position
121-
width, height = (1920, 1080)
122-
123-
border_margin = 40
124-
in_border = (
125-
x <= border_margin
126-
or x >= width - border_margin
127-
or y <= border_margin
128-
or y >= height - border_margin
129-
)
130111

131-
return in_border
112+
# Check custom rectangular regions
113+
for x1, y1, x2, y2 in self.entry_exit_regions:
114+
if x1 <= x <= x2 and y1 <= y <= y2:
115+
return True
132116

133-
def _edge_cost(self, nodeU: TrackNode, nodeV: TrackNode) -> float:
134-
"""
135-
Compute the cost of connecting two detections (nodes) in the graph.
136-
The cost is a weighted sum of IoU penalty, center distance,
137-
size penalty, and confidence penalty.
117+
# Check image border zones
118+
if self.use_border_regions:
119+
width, height = self.frame_size
120+
m = self.border_margin
138121

139-
Args:
140-
nodeU (TrackNode): Source node.
141-
nodeV (TrackNode): Target node.
122+
if "left" in self.active_borders and x <= m:
123+
return True
124+
if "right" in self.active_borders and x >= width - m:
125+
return True
126+
if "top" in self.active_borders and y <= m:
127+
return True
128+
if "bottom" in self.active_borders and y >= height - m:
129+
return True
142130

143-
Returns:
144-
float: Edge cost.
145-
"""
131+
return False
132+
133+
def _edge_cost(self, nodeU: TrackNode, nodeV: TrackNode) -> float:
146134
bboxU, bboxV = nodeU.bbox, nodeV.bbox
147135
conf_u, conf_v = nodeU.confidence, nodeV.confidence
148136

@@ -151,9 +139,7 @@ def _edge_cost(self, nodeU: TrackNode, nodeV: TrackNode) -> float:
151139

152140
area_a = (bboxU[2] - bboxU[0]) * (bboxU[3] - bboxU[1])
153141
area_b = (bboxV[2] - bboxV[0]) * (bboxV[3] - bboxV[1])
154-
size_penalty = np.log(
155-
(max(area_a, area_b) / (min(area_a, area_b) + 1e-6)) + 1e-6
156-
)
142+
size_penalty = np.log((max(area_a, area_b) / (min(area_a, area_b) + 1e-6)) + 1e-6)
157143

158144
conf_penalty = 1 - min(conf_u, conf_v)
159145

@@ -165,10 +151,6 @@ def _edge_cost(self, nodeU: TrackNode, nodeV: TrackNode) -> float:
165151
)
166152

167153
def _build_graph(self):
168-
"""
169-
Build the tracking graph from all buffered detections.
170-
Each detection is a node, and edges connect detections in consecutive frames.
171-
"""
172154
G = nx.DiGraph()
173155
G.add_node(self.source)
174156
G.add_node(self.sink)
@@ -193,10 +175,8 @@ def _build_graph(self):
193175
for t in range(len(node_frames) - 1):
194176
for node_a in node_frames[t]:
195177
if self._in_door(node_a):
196-
G.add_edge(self.source, node_a, weight=(t) * 2)
197-
G.add_edge(
198-
node_a, self.sink, weight=((len(node_frames) - 1) - (t)) * 2
199-
)
178+
G.add_edge(self.source, node_a, weight=t * 2)
179+
G.add_edge(node_a, self.sink, weight=(len(node_frames) - 1 - t) * 2)
200180

201181
for node_b in node_frames[t + 1]:
202182
cost = self._edge_cost(node_a, node_b)
@@ -209,22 +189,7 @@ def _build_graph(self):
209189

210190
self.graph = G
211191

212-
def solve(
213-
self,
214-
k: Optional[int] = None,
215-
) -> List[List[TrackNode]]:
216-
"""
217-
Extract up to k node-disjoint shortest paths from the graph using a
218-
successive shortest path approach.
219-
220-
Args:
221-
k (Optional[int]): Maximum number of paths to extract. If None,
222-
uses the maximum number of detections in any frame.
223-
224-
Returns:
225-
List[List[TrackNode]]: List of node-disjoint paths (tracks),
226-
each path is a list of TrackNode objects.
227-
"""
192+
def solve(self, k: Optional[int] = None) -> List[List[TrackNode]]:
228193
self._build_graph()
229194

230195
G_base = self.graph.copy()
@@ -237,32 +202,23 @@ def solve(
237202
for _i in tqdm(range(k), desc="Extracting k-shortest paths", leave=True):
238203
G_mod = G_base.copy()
239204

240-
# Update edge weights to penalize reused edges
241205
for u, v, data in G_mod.edges(data=True):
242206
base = data[self.weight_key]
243207
penalty = self.path_overlap_penalty * 1000 * edge_reuse[(u, v)] * base
244208
data[self.weight_key] = base + penalty
245209

246210
try:
247-
# Find shortest path from source to sink
248-
_, path = nx.single_source_dijkstra(
249-
G_mod, self.source, self.sink, weight=self.weight_key
250-
)
211+
_, path = nx.single_source_dijkstra(G_mod, self.source, self.sink, weight=self.weight_key)
251212
except nx.NetworkXNoPath:
252213
print(f"No path found from source to sink at {_i}th iteration")
253214
break
254215

255-
# Check for duplicate paths
256216
if path[1:-1] in paths:
257217
print("Duplicate path found!")
258-
# NOTE: Changed to continue for debugging to extrapolate the
259-
# track detects to investigate the reason for fewer paths generated
260-
# Change this to break when done
261218
continue
262219

263220
paths.append(path[1:-1])
264221

265-
# Mark edges in this path as reused for future penalty
266222
for u, v in zip(path[:-1], path[1:]):
267223
edge_reuse[(u, v)] += 1
268224

0 commit comments

Comments
 (0)