Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:06:10 +08:00

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 == []