9194ef5abd
Docs/Test Workflow / Test docs build (push) Failing after 0s
Check links & references / links-check (push) Failing after 1s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.10) (push) Failing after 0s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.11) (push) Failing after 0s
PR Conflict Labeler / main (push) Failing after 2s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.12) (push) Failing after 2s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.13) (push) Failing after 0s
Pytest/Test Workflow / Build this Package (push) Failing after 5s
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.10) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.11) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.12) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.13) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.10) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.11) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.12) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.13) (push) Has been cancelled
Pytest/Test Workflow / testing-guardian (push) Has been cancelled
191 lines
6.5 KiB
Python
191 lines
6.5 KiB
Python
import numpy as np
|
|
import pytest
|
|
|
|
import supervision as sv
|
|
from supervision.tracker.byte_tracker import matching
|
|
|
|
|
|
def _detections_from_boxes(
|
|
boxes: list[list[float]], confidence: list[float] | None = None
|
|
) -> sv.Detections:
|
|
"""Create detections with class ids and confidence for tracker regressions."""
|
|
if confidence is None:
|
|
confidence = [1.0] * len(boxes)
|
|
return sv.Detections(
|
|
xyxy=np.array(boxes, dtype=np.float32),
|
|
class_id=np.zeros(len(boxes), dtype=int),
|
|
confidence=np.array(confidence, dtype=np.float32),
|
|
)
|
|
|
|
|
|
def test_top_level_bytetrack_access_returns_class() -> None:
|
|
"""Top-level ByteTrack access should still resolve to the class object."""
|
|
sv.__dict__.pop("ByteTrack", None)
|
|
tracker_cls = sv.ByteTrack
|
|
assert tracker_cls is not None
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("detections", "expected_results"),
|
|
[
|
|
(
|
|
[
|
|
sv.Detections(
|
|
xyxy=np.array([[10, 10, 20, 20], [30, 30, 40, 40]]),
|
|
class_id=np.array([1, 1]),
|
|
confidence=np.array([1, 1]),
|
|
),
|
|
sv.Detections(
|
|
xyxy=np.array([[10, 10, 20, 20], [30, 30, 40, 40]]),
|
|
class_id=np.array([1, 1]),
|
|
confidence=np.array([1, 1]),
|
|
),
|
|
],
|
|
sv.Detections(
|
|
xyxy=np.array([[10, 10, 20, 20], [30, 30, 40, 40]]),
|
|
class_id=np.array([1, 1]),
|
|
confidence=np.array([1, 1]),
|
|
tracker_id=np.array([1, 2]),
|
|
),
|
|
),
|
|
],
|
|
)
|
|
def test_byte_tracker(
|
|
detections: list[sv.Detections],
|
|
expected_results: sv.Detections,
|
|
) -> None:
|
|
"""ByteTrack should preserve stable tracker ids for repeated detections."""
|
|
byte_tracker = sv.ByteTrack()
|
|
tracked_detections = [byte_tracker.update_with_detections(d) for d in detections]
|
|
assert tracked_detections[-1] == expected_results
|
|
|
|
|
|
def test_byte_tracker_does_not_skip_external_ids_for_short_lived_tracks() -> None:
|
|
"""Unconfirmed short-lived tracks should not consume external ids."""
|
|
# A transient false-positive appears and disappears before becoming confirmed.
|
|
# It should not consume an external tracker id.
|
|
frames = [
|
|
_detections_from_boxes([[0, 0, 10, 10]]),
|
|
_detections_from_boxes([[0, 0, 10, 10], [100, 100, 110, 110]]),
|
|
_detections_from_boxes([[0, 0, 10, 10]]),
|
|
_detections_from_boxes([[0, 0, 10, 10], [200, 200, 210, 210]]),
|
|
_detections_from_boxes([[0, 0, 10, 10], [200, 200, 210, 210]]),
|
|
]
|
|
|
|
byte_tracker = sv.ByteTrack(minimum_consecutive_frames=1)
|
|
|
|
tracked = [byte_tracker.update_with_detections(frame) for frame in frames]
|
|
assert tracked[-1].tracker_id is not None
|
|
assert np.array_equal(np.sort(tracked[-1].tracker_id), np.array([1, 2]))
|
|
|
|
|
|
def test_high_activation_threshold_can_start_track_at_score_one() -> None:
|
|
"""A high activation threshold must still allow maximum-confidence tracks."""
|
|
byte_tracker = sv.ByteTrack(track_activation_threshold=0.95)
|
|
detections = _detections_from_boxes([[0, 0, 10, 10]], confidence=[1.0])
|
|
|
|
tracked = byte_tracker.update_with_detections(detections)
|
|
|
|
assert tracked.tracker_id is not None
|
|
assert np.array_equal(tracked.tracker_id, np.array([1]))
|
|
|
|
|
|
def test_update_with_detections_does_not_mutate_input() -> None:
|
|
"""Tracking should return a copy instead of writing ids into the input."""
|
|
byte_tracker = sv.ByteTrack()
|
|
detections = _detections_from_boxes([[0, 0, 10, 10]])
|
|
|
|
_ = byte_tracker.update_with_detections(detections)
|
|
|
|
assert detections.tracker_id is None
|
|
|
|
|
|
def test_update_with_tensors_activates_on_second_consecutive_frame() -> None:
|
|
"""A second consecutive tensor frame should activate the delayed track."""
|
|
byte_tracker = sv.ByteTrack(minimum_consecutive_frames=2)
|
|
tensors = np.array([[0, 0, 10, 10, 0.9]], dtype=np.float32)
|
|
|
|
first_frame = byte_tracker.update_with_tensors(tensors)
|
|
second_frame = byte_tracker.update_with_tensors(tensors)
|
|
|
|
assert first_frame == []
|
|
assert len(second_frame) == 1
|
|
assert second_frame[0].is_activated
|
|
assert second_frame[0].external_track_id == 1
|
|
|
|
|
|
def test_score_equal_to_activation_threshold_keeps_existing_track() -> None:
|
|
"""A detection exactly at the threshold should remain eligible to match."""
|
|
byte_tracker = sv.ByteTrack(track_activation_threshold=0.5)
|
|
_ = byte_tracker.update_with_detections(
|
|
_detections_from_boxes([[0, 0, 10, 10]], confidence=[1.0])
|
|
)
|
|
|
|
tracked = byte_tracker.update_with_detections(
|
|
_detections_from_boxes([[0, 0, 10, 10]], confidence=[0.5])
|
|
)
|
|
|
|
assert tracked.tracker_id is not None
|
|
assert np.array_equal(tracked.tracker_id, np.array([1]))
|
|
|
|
|
|
def test_linear_assignment_does_not_mutate_cost_matrix() -> None:
|
|
"""Assignment should not rewrite the caller-owned cost matrix."""
|
|
cost_matrix = np.array([[0.1, 0.9], [0.8, 0.2]], dtype=np.float32)
|
|
original = cost_matrix.copy()
|
|
|
|
_ = matching.linear_assignment(cost_matrix, thresh=0.5)
|
|
|
|
assert np.array_equal(cost_matrix, original)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"tensors",
|
|
[
|
|
pytest.param(
|
|
np.array([[np.nan, 0, 10, 10, 0.9]], dtype=np.float32),
|
|
id="nan",
|
|
),
|
|
pytest.param(
|
|
np.array([[0, np.inf, 10, 10, 0.9]], dtype=np.float32),
|
|
id="inf",
|
|
),
|
|
pytest.param(
|
|
np.array([[0, 0, 0, 10, 0.9]], dtype=np.float32),
|
|
id="zero-width",
|
|
),
|
|
pytest.param(
|
|
np.array([[10, 0, 0, 10, 0.9]], dtype=np.float32),
|
|
id="negative-width",
|
|
),
|
|
pytest.param(
|
|
np.array([[0, 0, 10, 0, 0.9]], dtype=np.float32),
|
|
id="zero-height",
|
|
),
|
|
pytest.param(
|
|
np.array([[0, 10, 10, 0, 0.9]], dtype=np.float32),
|
|
id="negative-height",
|
|
),
|
|
pytest.param(np.empty((0, 5), dtype=np.float32), id="empty"),
|
|
],
|
|
)
|
|
def test_update_with_tensors_ignores_invalid_boxes(
|
|
tensors: np.ndarray,
|
|
) -> None:
|
|
"""Invalid tensors should be dropped before track creation."""
|
|
byte_tracker = sv.ByteTrack()
|
|
|
|
tracks = byte_tracker.update_with_tensors(tensors)
|
|
|
|
assert tracks == []
|
|
|
|
|
|
def test_update_with_tensors_respects_min_consecutive_frames_on_first_frame() -> None:
|
|
"""First-frame tensor updates should not emit unconfirmed track id -1."""
|
|
byte_tracker = sv.ByteTrack(minimum_consecutive_frames=2)
|
|
tensors = np.array([[0, 0, 10, 10, 0.9]], dtype=np.float32)
|
|
|
|
tracks = byte_tracker.update_with_tensors(tensors)
|
|
|
|
assert tracks == []
|