Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
63 changes: 10 additions & 53 deletions docs/modules/tracking.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,28 +42,12 @@ while cap.isOpened():

# 1. Detect faces
faces = detector.detect(frame)

# 2. Track the faces
tracked_faces = tracker.update_with_faces(faces)

# 2. Build detections array: [x1, y1, x2, y2, score]
dets = np.array([[*f.bbox, f.confidence] for f in faces])
dets = dets if len(dets) > 0 else np.empty((0, 5))

# 3. Update tracker
tracks = tracker.update(dets)

# 4. Map track IDs back to face objects
if len(tracks) > 0 and len(faces) > 0:
face_bboxes = np.array([f.bbox for f in faces], dtype=np.float32)
track_ids = tracks[:, 4].astype(int)

face_centers = xyxy_to_cxcywh(face_bboxes)[:, :2]
track_centers = xyxy_to_cxcywh(tracks[:, :4])[:, :2]

for ti in range(len(tracks)):
dists = (track_centers[ti, 0] - face_centers[:, 0]) ** 2 + (track_centers[ti, 1] - face_centers[:, 1]) ** 2
faces[int(np.argmin(dists))].track_id = track_ids[ti]

# 5. Draw
tracked_faces = [f for f in faces if f.track_id is not None]
# 3. Draw
tracked_faces = [f for f in tracked_faces if f.track_id is not None]
draw_tracks(image=frame, faces=tracked_faces)
cv2.imshow("Tracking", frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
Expand Down Expand Up @@ -97,23 +81,9 @@ while True:
break

faces = detector.detect(frame)
dets = np.array([[*f.bbox, f.confidence] for f in faces])
dets = dets if len(dets) > 0 else np.empty((0, 5))

tracks = tracker.update(dets)

if len(tracks) > 0 and len(faces) > 0:
face_bboxes = np.array([f.bbox for f in faces], dtype=np.float32)
track_ids = tracks[:, 4].astype(int)

face_centers = xyxy_to_cxcywh(face_bboxes)[:, :2]
track_centers = xyxy_to_cxcywh(tracks[:, :4])[:, :2]
tracked_faces = tracker.update_with_faces(faces)

for ti in range(len(tracks)):
dists = (track_centers[ti, 0] - face_centers[:, 0]) ** 2 + (track_centers[ti, 1] - face_centers[:, 1]) ** 2
faces[int(np.argmin(dists))].track_id = track_ids[ti]

draw_tracks(image=frame, faces=[f for f in faces if f.track_id is not None])
tracked_faces = [f for f in tracked_faces if f.track_id is not None]
cv2.imshow("Face Tracking - Press 'q' to quit", frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
Expand Down Expand Up @@ -148,24 +118,11 @@ tracker = BYTETracker(

## Input / Output

**Input** — `(N, 5)` numpy array with `[x1, y1, x2, y2, confidence]` per detection:

```python
detections = np.array([
[100, 50, 200, 160, 0.95],
[300, 80, 380, 200, 0.87],
])
```
**Input** — list of detected faces.

**Output** — `(M, 5)` numpy array with `[x1, y1, x2, y2, track_id]` per active track:

```python
tracks = tracker.update(detections)
# array([[101.2, 51.3, 199.8, 159.8, 1.],
# [300.5, 80.2, 379.7, 200.1, 2.]])
```
**Output** — list of tracked faces.

The output bounding boxes come from the Kalman filter prediction, so they may differ slightly from the input. Track IDs are integers that persist across frames for the same object.
The output bounding box for each face come from the Kalman filter prediction, so they may differ slightly from the input. Track IDs are integers that persist across frames for the same object.

---

Expand Down
31 changes: 31 additions & 0 deletions uniface/tracking/bytetrack/tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from . import matching
from .basetrack import BaseTrack, TrackState
from .kalman import KalmanFilter
from uniface.types import Face
from uniface.common import xyxy_to_cxcywh


class STrack(BaseTrack):
Expand Down Expand Up @@ -190,6 +192,35 @@ def reset(self) -> None:
self.removed_stracks = []
BaseTrack.reset_id()

def update_with_faces(self, faces: list[Face]) -> list[Face]:
"""
Update tracker with new faces
Args:
faces: Faces to update.
Returns:
List of tracked faces
"""
dets = np.array([[*f.bbox, f.confidence] for f in faces])
dets = dets if len(dets) > 0 else np.empty((0, 5))

# 3. Update tracker
tracks = self.update(dets)

# 4. Map track IDs back to face objects
if len(tracks) > 0 and len(faces) > 0:
face_bboxes = np.array([f.bbox for f in faces], dtype=np.float32)
track_ids = tracks[:, 4].astype(int)

face_centers = xyxy_to_cxcywh(face_bboxes)[:, :2]
track_centers = xyxy_to_cxcywh(tracks[:, :4])[:, :2]

for ti in range(len(tracks)):
dists = (track_centers[ti, 0] - face_centers[:, 0]) ** 2 + (
track_centers[ti, 1] - face_centers[:, 1]) ** 2
faces[int(np.argmin(dists))].track_id = track_ids[ti]

return faces

def update(self, detections: np.ndarray) -> np.ndarray:
"""Update tracker with new detections.

Expand Down
Loading