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
988 lines
32 KiB
Python
988 lines
32 KiB
Python
from contextlib import ExitStack as DoesNotRaise
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
from supervision import Detections, LineZone, LineZoneAnnotatorMulticlass
|
|
from supervision.geometry.core import Point, Position, Vector
|
|
from tests.helpers import _create_detections
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("vector", "expected_result", "exception"),
|
|
[
|
|
(
|
|
Vector(start=Point(x=0.0, y=0.0), end=Point(x=0.0, y=0.0)),
|
|
None,
|
|
pytest.raises(ValueError, match="magnitude of the vector"),
|
|
),
|
|
(
|
|
Vector(start=Point(x=1.0, y=1.0), end=Point(x=1.0, y=1.0)),
|
|
None,
|
|
pytest.raises(ValueError, match="cannot be zero"),
|
|
),
|
|
(
|
|
Vector(start=Point(x=0.0, y=0.0), end=Point(x=0.0, y=4.0)),
|
|
(
|
|
Vector(start=Point(x=0.0, y=0.0), end=Point(x=-1.0, y=0.0)),
|
|
Vector(start=Point(x=0.0, y=4.0), end=Point(x=1.0, y=4.0)),
|
|
),
|
|
DoesNotRaise(),
|
|
),
|
|
(
|
|
Vector(Point(0.0, 0.0), Point(4.0, 0.0)),
|
|
(
|
|
Vector(start=Point(x=0.0, y=0.0), end=Point(x=0.0, y=1.0)),
|
|
Vector(start=Point(x=4.0, y=0.0), end=Point(x=4.0, y=-1.0)),
|
|
),
|
|
DoesNotRaise(),
|
|
),
|
|
(
|
|
Vector(Point(0.0, 0.0), Point(3.0, 4.0)),
|
|
(
|
|
Vector(start=Point(x=0, y=0), end=Point(x=-0.8, y=0.6)),
|
|
Vector(start=Point(x=3, y=4), end=Point(x=3.8, y=3.4)),
|
|
),
|
|
DoesNotRaise(),
|
|
),
|
|
(
|
|
Vector(Point(0.0, 0.0), Point(4.0, 3.0)),
|
|
(
|
|
Vector(start=Point(x=0, y=0), end=Point(x=-0.6, y=0.8)),
|
|
Vector(start=Point(x=4, y=3), end=Point(x=4.6, y=2.2)),
|
|
),
|
|
DoesNotRaise(),
|
|
),
|
|
(
|
|
Vector(Point(0.0, 0.0), Point(3.0, -4.0)),
|
|
(
|
|
Vector(start=Point(x=0, y=0), end=Point(x=0.8, y=0.6)),
|
|
Vector(start=Point(x=3, y=-4), end=Point(x=2.2, y=-4.6)),
|
|
),
|
|
DoesNotRaise(),
|
|
),
|
|
],
|
|
)
|
|
def test_calculate_region_of_interest_limits(
|
|
vector: Vector,
|
|
expected_result: tuple[Vector, Vector] | None,
|
|
exception: Exception,
|
|
) -> None:
|
|
with exception:
|
|
result = LineZone._calculate_region_of_interest_limits(vector=vector)
|
|
assert result == expected_result
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("vector", "xyxy_sequence", "expected_crossed_in", "expected_crossed_out"),
|
|
[
|
|
( # Vertical line, simple crossing
|
|
Vector(Point(0, 0), Point(0, 10)),
|
|
[
|
|
[4, 4, 6, 6],
|
|
[4 - 10, 4, 6 - 10, 6],
|
|
[4, 4, 6, 6],
|
|
[4 - 10, 4, 6 - 10, 6],
|
|
],
|
|
[False, False, True, False],
|
|
[False, True, False, True],
|
|
),
|
|
( # Vertical line reversed, simple crossing
|
|
Vector(Point(0, 10), Point(0, 0)),
|
|
[
|
|
[4, 4, 6, 6],
|
|
[4 - 10, 4, 6 - 10, 6],
|
|
[4, 4, 6, 6],
|
|
[4 - 10, 4, 6 - 10, 6],
|
|
],
|
|
[False, True, False, True],
|
|
[False, False, True, False],
|
|
),
|
|
( # Horizontal line, simple crossing
|
|
Vector(Point(0, 0), Point(10, 0)),
|
|
[
|
|
[4, 4, 6, 6],
|
|
[4, 4 - 10, 6, 6 - 10],
|
|
[4, 4, 6, 6],
|
|
[4, 4 - 10, 6, 6 - 10],
|
|
],
|
|
[False, True, False, True],
|
|
[False, False, True, False],
|
|
),
|
|
( # Horizontal line reversed, simple crossing
|
|
Vector(Point(10, 0), Point(0, 0)),
|
|
[
|
|
[4, 4, 6, 6],
|
|
[4, 4 - 10, 6, 6 - 10],
|
|
[4, 4, 6, 6],
|
|
[4, 4 - 10, 6, 6 - 10],
|
|
],
|
|
[False, False, True, False],
|
|
[False, True, False, True],
|
|
),
|
|
( # Diagonal line, simple crossing
|
|
Vector(Point(5, 0), Point(0, 5)),
|
|
[
|
|
[0, 0, 2, 2],
|
|
[0 + 10, 0 + 10, 2 + 10, 2 + 10],
|
|
[0, 0, 2, 2],
|
|
[0 + 10, 0 + 10, 2 + 10, 2 + 10],
|
|
],
|
|
[False, True, False, True],
|
|
[False, False, True, False],
|
|
),
|
|
( # Crossing beside - right side
|
|
Vector(Point(0, 0), Point(10, 0)),
|
|
[
|
|
[20, 4, 24, 6],
|
|
[20, 4 - 10, 24, 6 - 10],
|
|
[20, 4, 24, 6],
|
|
[20, 4 - 10, 24, 6 - 10],
|
|
],
|
|
[False, False, False, False],
|
|
[False, False, False, False],
|
|
),
|
|
( # Horizontal line, simple crossing, far away
|
|
Vector(Point(0, 0), Point(10, 0)),
|
|
[
|
|
[4, 1e32, 6, 1e32 + 2],
|
|
[4, -1e32, 6, -1e32 + 2],
|
|
[4, 1e32, 6, 1e32 + 2],
|
|
[4, -1e32, 6, -1e32 + 2],
|
|
],
|
|
[False, True, False, True],
|
|
[False, False, True, False],
|
|
),
|
|
( # Crossing beside - left side
|
|
Vector(Point(0, 0), Point(10, 0)),
|
|
[
|
|
[-20, 4, -24, 6],
|
|
[-20, 4 - 10, -24, 6 - 10],
|
|
[-20, 4, -24, 6],
|
|
[-20, 4 - 10, -24, 6 - 10],
|
|
],
|
|
[False, False, False, False],
|
|
[False, False, False, False],
|
|
),
|
|
( # Move above
|
|
Vector(Point(0, 0), Point(10, 0)),
|
|
[
|
|
[-4, 4, -2, 6],
|
|
[-4 + 20, 4, -2 + 20, 6],
|
|
[-4, 4, -2, 6],
|
|
[-4 + 20, 4, -2 + 20, 6],
|
|
],
|
|
[False, False, False, False],
|
|
[False, False, False, False],
|
|
),
|
|
( # Move below
|
|
Vector(Point(0, 0), Point(10, 0)),
|
|
[
|
|
[-4, -6, -2, -4],
|
|
[-4 + 20, -6, -2 + 20, -4],
|
|
[-4, -6, -2, -4],
|
|
[-4 + 20, -6, -2 + 20, -4],
|
|
],
|
|
[False, False, False, False],
|
|
[False, False, False, False],
|
|
),
|
|
( # Move into line partway
|
|
Vector(Point(0, 0), Point(10, 0)),
|
|
[
|
|
[4, 4, 6, 6],
|
|
[4 + 5, 4, 6 + 5, 6],
|
|
[4, 4, 6, 6],
|
|
[4 + 5, 4, 6 + 5, 6],
|
|
],
|
|
[False, False, False, False],
|
|
[False, False, False, False],
|
|
),
|
|
( # V-shaped crossing from outside limits - not supported.
|
|
Vector(Point(0, 0), Point(10, 0)),
|
|
[[-3, 6, -1, 8], [4, -6, 6, -4], [11, 6, 13, 8]],
|
|
[False, False, False],
|
|
[False, False, False],
|
|
),
|
|
( # Diagonal movement, from within limits to outside - not supported
|
|
Vector(Point(0, 0), Point(10, 0)),
|
|
[[4, 1, 6, 3], [11, 1 - 20, 13, 3 - 20]],
|
|
[False, False],
|
|
[False, False],
|
|
),
|
|
( # Diagonal movement, from outside limits to within - not supported
|
|
Vector(Point(0, 0), Point(10, 0)),
|
|
[
|
|
[11, 21, 13, 23],
|
|
[4, -3, 6, -1],
|
|
],
|
|
[False, False],
|
|
[False, False],
|
|
),
|
|
( # Diagonal crossing, from outside to outside limits - not supported.
|
|
Vector(Point(0, 0), Point(10, 0)),
|
|
[
|
|
[-4, 4, -2, 8],
|
|
[-4 + 16, -4, -2 + 16, -6],
|
|
[-4, 4, -2, 8],
|
|
[-4 + 16, -4, -2 + 16, -6],
|
|
],
|
|
[False, False, False, False],
|
|
[False, False, False, False],
|
|
),
|
|
],
|
|
)
|
|
def test_line_zone_one_detection_default_anchors(
|
|
vector: Vector,
|
|
xyxy_sequence: list[list[float]],
|
|
expected_crossed_in: list[bool],
|
|
expected_crossed_out: list[bool],
|
|
) -> None:
|
|
line_zone = LineZone(start=vector.start, end=vector.end)
|
|
|
|
crossed_in_list = []
|
|
crossed_out_list = []
|
|
for i, bbox in enumerate(xyxy_sequence):
|
|
detections = _create_detections(
|
|
xyxy=[bbox],
|
|
tracker_id=[0],
|
|
)
|
|
crossed_in, crossed_out = line_zone.trigger(detections)
|
|
crossed_in_list.append(crossed_in[0])
|
|
crossed_out_list.append(crossed_out[0])
|
|
|
|
assert crossed_in_list == expected_crossed_in, (
|
|
f"expected {expected_crossed_in}, got {crossed_in_list}"
|
|
)
|
|
assert crossed_out_list == expected_crossed_out, (
|
|
f"expected {expected_crossed_out}, got {crossed_out_list}"
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
(
|
|
"vector",
|
|
"xyxy_sequence",
|
|
"triggering_anchors",
|
|
"expected_crossed_in",
|
|
"expected_crossed_out",
|
|
),
|
|
[
|
|
( # Scrape line, left side, corner anchors
|
|
Vector(Point(0, 0), Point(10, 0)),
|
|
[
|
|
[-2, 4, 2, 6],
|
|
[-2, 4 - 10, 2, 6 - 10],
|
|
[-2, 4, 2, 6],
|
|
[-2, 4 - 10, 2, 6 - 10],
|
|
],
|
|
[
|
|
Position.TOP_LEFT,
|
|
Position.BOTTOM_LEFT,
|
|
Position.TOP_RIGHT,
|
|
Position.BOTTOM_RIGHT,
|
|
],
|
|
[False, False, False, False],
|
|
[False, False, False, False],
|
|
),
|
|
( # Scrape line, left side, right anchors
|
|
Vector(Point(0, 0), Point(10, 0)),
|
|
[
|
|
[-2, 4, 2, 6],
|
|
[-2, 4 - 10, 2, 6 - 10],
|
|
[-2, 4, 2, 6],
|
|
[-2, 4 - 10, 2, 6 - 10],
|
|
],
|
|
[Position.TOP_RIGHT, Position.BOTTOM_RIGHT],
|
|
[False, True, False, True],
|
|
[False, False, True, False],
|
|
),
|
|
( # Scrape line, left side, center anchor (along line point)
|
|
Vector(Point(0, 0), Point(10, 0)),
|
|
[
|
|
[-2, 4, 2, 6],
|
|
[-2, 4 - 10, 2, 6 - 10],
|
|
[-2, 4, 2, 6],
|
|
[-2, 4 - 10, 2, 6 - 10],
|
|
],
|
|
[Position.CENTER],
|
|
[False, True, False, True],
|
|
[False, False, True, False],
|
|
),
|
|
( # Scrape line, left side, center anchor (along line point)
|
|
Vector(Point(0, 0), Point(10, 0)),
|
|
[
|
|
[-2, 4, 2, 6],
|
|
[-2, 4 - 10, 2, 6 - 10],
|
|
[-2, 4, 2, 6],
|
|
[-2, 4 - 10, 2, 6 - 10],
|
|
[-2, 4 - 10, 2, 6 - 10],
|
|
],
|
|
[Position.CENTER],
|
|
[False, True, False, True, False],
|
|
[False, False, True, False, False],
|
|
),
|
|
( # Scrape line, right side, corner anchors
|
|
Vector(Point(0, 0), Point(10, 0)),
|
|
[
|
|
[8, 4, 12, 6],
|
|
[8, 4 - 10, 12, 6 - 10],
|
|
[8, 4, 12, 6],
|
|
[8, 4 - 10, 12, 6 - 10],
|
|
],
|
|
[
|
|
Position.TOP_LEFT,
|
|
Position.BOTTOM_LEFT,
|
|
Position.TOP_RIGHT,
|
|
Position.BOTTOM_RIGHT,
|
|
],
|
|
[False, False, False, False],
|
|
[False, False, False, False],
|
|
),
|
|
( # Scrape line, right side, left anchors
|
|
Vector(Point(0, 0), Point(10, 0)),
|
|
[
|
|
[8, 4, 12, 6],
|
|
[8, 4 - 10, 12, 6 - 10],
|
|
[8, 4, 12, 6],
|
|
[8, 4 - 10, 12, 6 - 10],
|
|
],
|
|
[Position.TOP_LEFT, Position.BOTTOM_LEFT],
|
|
[False, True, False, True],
|
|
[False, False, True, False],
|
|
),
|
|
( # Scrape line, right side, center anchor (along line point)
|
|
Vector(Point(0, 0), Point(10, 0)),
|
|
[
|
|
[8, 4, 12, 6],
|
|
[8, 4 - 10, 12, 6 - 10],
|
|
[8, 4, 12, 6],
|
|
[8, 4 - 10, 12, 6 - 10],
|
|
],
|
|
[Position.CENTER],
|
|
[False, True, False, True],
|
|
[False, False, True, False],
|
|
),
|
|
( # Simple crossing, one anchor
|
|
Vector(Point(0, 0), Point(10, 0)),
|
|
[
|
|
[4, 4, 6, 6],
|
|
[4, 4 - 10, 6, 6 - 10],
|
|
[4, 4, 6, 6],
|
|
[4, 4 - 10, 6, 6 - 10],
|
|
],
|
|
[Position.CENTER],
|
|
[False, True, False, True],
|
|
[False, False, True, False],
|
|
),
|
|
( # Simple crossing, all box anchors
|
|
Vector(Point(0, 0), Point(10, 0)),
|
|
[
|
|
[4, 4, 6, 6],
|
|
[4, 4 - 10, 6, 6 - 10],
|
|
[4, 4, 6, 6],
|
|
[4, 4 - 10, 6, 6 - 10],
|
|
],
|
|
[
|
|
Position.CENTER,
|
|
Position.CENTER_LEFT,
|
|
Position.CENTER_RIGHT,
|
|
Position.TOP_CENTER,
|
|
Position.TOP_LEFT,
|
|
Position.TOP_RIGHT,
|
|
Position.BOTTOM_LEFT,
|
|
Position.BOTTOM_CENTER,
|
|
Position.BOTTOM_RIGHT,
|
|
],
|
|
[False, True, False, True],
|
|
[False, False, True, False],
|
|
),
|
|
],
|
|
)
|
|
def test_line_zone_one_detection(
|
|
vector: Vector,
|
|
xyxy_sequence: list[list[float]],
|
|
triggering_anchors: list[Position],
|
|
expected_crossed_in: list[bool],
|
|
expected_crossed_out: list[bool],
|
|
) -> None:
|
|
line_zone = LineZone(
|
|
start=vector.start, end=vector.end, triggering_anchors=triggering_anchors
|
|
)
|
|
|
|
crossed_in_list = []
|
|
crossed_out_list = []
|
|
for i, bbox in enumerate(xyxy_sequence):
|
|
detections = _create_detections(
|
|
xyxy=[bbox],
|
|
tracker_id=[0],
|
|
)
|
|
crossed_in, crossed_out = line_zone.trigger(detections)
|
|
crossed_in_list.append(crossed_in[0])
|
|
crossed_out_list.append(crossed_out[0])
|
|
|
|
assert crossed_in_list == expected_crossed_in, (
|
|
f"expected {expected_crossed_in}, got {crossed_in_list}"
|
|
)
|
|
assert crossed_out_list == expected_crossed_out, (
|
|
f"expected {expected_crossed_out}, got {crossed_out_list}"
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
(
|
|
"vector",
|
|
"xyxy_sequence",
|
|
"anchors",
|
|
"expected_crossed_in",
|
|
"expected_crossed_out",
|
|
"exception",
|
|
),
|
|
[
|
|
( # One stays, one crosses
|
|
Vector(Point(0, 0), Point(10, 0)),
|
|
[
|
|
[[4, 4, 6, 6], [4, 4, 6, 6]],
|
|
[[4, 4, 6, 6], [4, 4 - 10, 6, 6 - 10]],
|
|
[[4, 4, 6, 6], [4, 4, 6, 6]],
|
|
[[4, 4, 6, 6], [4, 4 - 10, 6, 6 - 10]],
|
|
],
|
|
[
|
|
Position.TOP_LEFT,
|
|
Position.TOP_RIGHT,
|
|
Position.BOTTOM_LEFT,
|
|
Position.BOTTOM_RIGHT,
|
|
],
|
|
[[False, False], [False, True], [False, False], [False, True]],
|
|
[[False, False], [False, False], [False, True], [False, False]],
|
|
DoesNotRaise(),
|
|
),
|
|
( # Both cross at the same time
|
|
Vector(Point(0, 0), Point(10, 0)),
|
|
[
|
|
[[4, 4, 6, 6], [4, 4, 6, 6]],
|
|
[[4, 4 - 10, 6, 6 - 10], [4, 4 - 10, 6, 6 - 10]],
|
|
[[4, 4, 6, 6], [4, 4, 6, 6]],
|
|
[[4, 4 - 10, 6, 6 - 10], [4, 4 - 10, 6, 6 - 10]],
|
|
],
|
|
[
|
|
Position.TOP_LEFT,
|
|
Position.TOP_RIGHT,
|
|
Position.BOTTOM_LEFT,
|
|
Position.BOTTOM_RIGHT,
|
|
],
|
|
[[False, False], [True, True], [False, False], [True, True]],
|
|
[[False, False], [False, False], [True, True], [False, False]],
|
|
DoesNotRaise(),
|
|
),
|
|
],
|
|
)
|
|
def test_line_zone_multiple_detections(
|
|
vector: Vector,
|
|
xyxy_sequence: list[list[list[float]]],
|
|
anchors: list[Position],
|
|
expected_crossed_in: list[list[bool]],
|
|
expected_crossed_out: list[list[bool]],
|
|
exception: Exception,
|
|
) -> None:
|
|
with exception:
|
|
line_zone = LineZone(
|
|
start=vector.start, end=vector.end, triggering_anchors=anchors
|
|
)
|
|
crossed_in_list = []
|
|
crossed_out_list = []
|
|
for bboxes in xyxy_sequence:
|
|
detections = _create_detections(
|
|
xyxy=bboxes,
|
|
tracker_id=[i for i in range(0, len(bboxes))],
|
|
)
|
|
crossed_in, crossed_out = line_zone.trigger(detections)
|
|
crossed_in_list.append(list(crossed_in))
|
|
crossed_out_list.append(list(crossed_out))
|
|
|
|
assert crossed_in_list == expected_crossed_in
|
|
assert crossed_out_list == expected_crossed_out
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
(
|
|
"vector",
|
|
"xyxy_sequence",
|
|
"triggering_anchors",
|
|
"minimum_crossing_threshold",
|
|
"expected_crossed_in",
|
|
"expected_crossed_out",
|
|
),
|
|
[
|
|
( # Detection lingers around line, all crosses counted
|
|
Vector(Point(0, 0), Point(10, 0)),
|
|
[
|
|
[2, 4, 3, 6],
|
|
[2, 4 - 10, 3, 6],
|
|
[2, 4, 3, 6],
|
|
[2, 4 - 10, 3, 6],
|
|
[2, 4 - 10, 3, 6],
|
|
],
|
|
[
|
|
Position.TOP_LEFT,
|
|
],
|
|
1,
|
|
[False, True, False, True, False],
|
|
[False, False, True, False, False],
|
|
),
|
|
( # Detection lingers around line, only final cross counted
|
|
Vector(Point(0, 0), Point(10, 0)),
|
|
[
|
|
[2, 4, 3, 6],
|
|
[2, 4 - 10, 3, 6],
|
|
[2, 4, 3, 6],
|
|
[2, 4 - 10, 3, 6],
|
|
[2, 4 - 10, 3, 6],
|
|
],
|
|
[
|
|
Position.TOP_LEFT,
|
|
],
|
|
2,
|
|
[False, False, False, False, True],
|
|
[False, False, False, False, False],
|
|
),
|
|
( # Detection lingers around line for a long time
|
|
Vector(Point(0, 0), Point(10, 0)),
|
|
[
|
|
[2, 4, 3, 6],
|
|
[2, 4 - 10, 3, 6],
|
|
[2, 4, 3, 6],
|
|
[2, 4 - 10, 3, 6],
|
|
[2, 4, 3, 6],
|
|
[2, 4 - 10, 3, 6],
|
|
[2, 4, 3, 6],
|
|
[2, 4 - 10, 3, 6],
|
|
[2, 4, 3, 6],
|
|
[2, 4 - 10, 3, 6],
|
|
[2, 4, 3, 6],
|
|
[2, 4 - 10, 3, 6],
|
|
[2, 4 - 10, 3, 6],
|
|
],
|
|
[
|
|
Position.TOP_LEFT,
|
|
],
|
|
2,
|
|
[False] * 12 + [True],
|
|
[False] * 13,
|
|
),
|
|
( # Detection lingers around line, longer cycle
|
|
Vector(Point(0, 0), Point(10, 0)),
|
|
[
|
|
[2, 4, 3, 6],
|
|
[2, 4 - 10, 3, 6],
|
|
[2, 4, 3, 6],
|
|
[2, 4, 3, 6],
|
|
[2, 4, 3, 6],
|
|
[2, 4 - 10, 3, 6],
|
|
[2, 4 - 10, 3, 6],
|
|
[2, 4 - 10, 3, 6],
|
|
[2, 4 - 10, 3, 6],
|
|
],
|
|
[
|
|
Position.TOP_LEFT,
|
|
],
|
|
4,
|
|
[False] * 8 + [True],
|
|
[False] * 9,
|
|
),
|
|
],
|
|
)
|
|
def test_line_zone_one_detection_long_horizon(
|
|
vector: Vector,
|
|
xyxy_sequence: list[list[float]],
|
|
triggering_anchors: list[Position],
|
|
minimum_crossing_threshold: int,
|
|
expected_crossed_in: list[bool],
|
|
expected_crossed_out: list[bool],
|
|
) -> None:
|
|
line_zone = LineZone(
|
|
start=vector.start,
|
|
end=vector.end,
|
|
triggering_anchors=triggering_anchors,
|
|
minimum_crossing_threshold=minimum_crossing_threshold,
|
|
)
|
|
|
|
crossed_in_list = []
|
|
crossed_out_list = []
|
|
for i, bbox in enumerate(xyxy_sequence):
|
|
detections = _create_detections(
|
|
xyxy=[bbox],
|
|
tracker_id=[0],
|
|
)
|
|
crossed_in, crossed_out = line_zone.trigger(detections)
|
|
crossed_in_list.append(crossed_in[0])
|
|
crossed_out_list.append(crossed_out[0])
|
|
|
|
assert crossed_in_list == expected_crossed_in, (
|
|
f"expected {expected_crossed_in}, got {crossed_in_list}"
|
|
)
|
|
assert crossed_out_list == expected_crossed_out, (
|
|
f"expected {expected_crossed_out}, got {crossed_out_list}"
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
(
|
|
"vector",
|
|
"xyxy_sequence",
|
|
"anchors",
|
|
"minimum_crossing_threshold",
|
|
"expected_crossed_in",
|
|
"expected_crossed_out",
|
|
"expected_count_in",
|
|
"expected_count_out",
|
|
"exception",
|
|
),
|
|
[
|
|
( # One stays, one crosses, one disappears before crossing
|
|
Vector(Point(0, 0), Point(10, 0)),
|
|
[
|
|
[[4, 4, 6, 6], [4, 4, 6, 6], [4, 4, 6, 6]],
|
|
[[4, 4, 6, 6], [4, 4 - 10, 6, 6 - 10], [4, 4, 6, 6]],
|
|
[[4, 4, 6, 6], [4, 4, 6, 6]],
|
|
[[4, 4, 6, 6], [4, 4 - 10, 6, 6 - 10]],
|
|
[[4, 4, 6, 6], [4, 4 - 10, 6, 6 - 10]],
|
|
],
|
|
[
|
|
Position.TOP_LEFT,
|
|
],
|
|
1,
|
|
[
|
|
[False, False, False],
|
|
[False, True, False],
|
|
[False, False],
|
|
[False, True],
|
|
[False, False],
|
|
],
|
|
[
|
|
[False, False, False],
|
|
[False, False, False],
|
|
[False, True],
|
|
[False, False],
|
|
[False, False],
|
|
],
|
|
[0, 1, 1, 2, 2],
|
|
[0, 0, 1, 1, 1],
|
|
DoesNotRaise(),
|
|
),
|
|
( # One stays, one crosses, one disappears immediately after crossing
|
|
Vector(Point(0, 0), Point(10, 0)),
|
|
[
|
|
[[4, 4, 6, 6], [4, 4, 6, 6], [4, 4, 6, 6]],
|
|
[[4, 4, 6, 6], [4, 4 - 10, 6, 6 - 10], [4, 4, 6, 6]],
|
|
[[4, 4, 6, 6], [4, 4, 6, 6], [4, 4 - 10, 6, 6 - 10]],
|
|
[[4, 4, 6, 6], [4, 4 - 10, 6, 6 - 10]],
|
|
[[4, 4, 6, 6], [4, 4 - 10, 6, 6 - 10]],
|
|
],
|
|
[
|
|
Position.TOP_LEFT,
|
|
],
|
|
1,
|
|
[
|
|
[False, False, False],
|
|
[False, True, False],
|
|
[False, False, True],
|
|
[False, True],
|
|
[False, False],
|
|
],
|
|
[
|
|
[False, False, False],
|
|
[False, False, False],
|
|
[False, True, False],
|
|
[False, False],
|
|
[False, False],
|
|
],
|
|
[0, 1, 2, 3, 3],
|
|
[0, 0, 1, 1, 1],
|
|
DoesNotRaise(),
|
|
),
|
|
( # One stays, one crosses, one disappears before crossing
|
|
Vector(Point(0, 0), Point(10, 0)),
|
|
[
|
|
[[4, 4, 6, 6], [4, 4, 6, 6], [4, 4, 6, 6]],
|
|
[[4, 4, 6, 6], [4, 4 - 10, 6, 6 - 10], [4, 4, 6, 6]],
|
|
[[4, 4, 6, 6], [4, 4, 6, 6]],
|
|
[[4, 4, 6, 6], [4, 4 - 10, 6, 6 - 10]],
|
|
[[4, 4, 6, 6], [4, 4 - 10, 6, 6 - 10]],
|
|
],
|
|
[
|
|
Position.TOP_LEFT,
|
|
],
|
|
2,
|
|
[
|
|
[False, False, False],
|
|
[False, False, False],
|
|
[False, False],
|
|
[False, False],
|
|
[False, True],
|
|
],
|
|
[
|
|
[False, False, False],
|
|
[False, False, False],
|
|
[False, False],
|
|
[False, False],
|
|
[False, False],
|
|
],
|
|
[0, 0, 0, 0, 1],
|
|
[0, 0, 0, 0, 0],
|
|
DoesNotRaise(),
|
|
),
|
|
( # One stays, one crosses, one disappears immediately after crossing
|
|
Vector(Point(0, 0), Point(10, 0)),
|
|
[
|
|
[[4, 4, 6, 6], [4, 4, 6, 6], [4, 4, 6, 6]],
|
|
[[4, 4, 6, 6], [4, 4 - 10, 6, 6 - 10], [4, 4, 6, 6]],
|
|
[[4, 4, 6, 6], [4, 4, 6, 6], [4, 4 - 10, 6, 6 - 10]],
|
|
[[4, 4, 6, 6], [4, 4 - 10, 6, 6 - 10]],
|
|
[[4, 4, 6, 6], [4, 4 - 10, 6, 6 - 10]],
|
|
],
|
|
[
|
|
Position.TOP_LEFT,
|
|
],
|
|
2,
|
|
[
|
|
[False, False, False],
|
|
[False, False, False],
|
|
[False, False, False],
|
|
[False, False],
|
|
[False, True],
|
|
],
|
|
[
|
|
[False, False, False],
|
|
[False, False, False],
|
|
[False, False, False],
|
|
[False, False],
|
|
[False, False],
|
|
],
|
|
[0, 0, 0, 0, 1],
|
|
[0, 0, 0, 0, 0],
|
|
DoesNotRaise(),
|
|
),
|
|
],
|
|
)
|
|
def test_line_zone_long_horizon_disappearing_detections(
|
|
vector: Vector,
|
|
xyxy_sequence: list[list[list[float] | None]],
|
|
anchors: list[Position],
|
|
minimum_crossing_threshold: int,
|
|
expected_crossed_in: list[list[bool]],
|
|
expected_crossed_out: list[list[bool]],
|
|
expected_count_in: list[int],
|
|
expected_count_out: list[int],
|
|
exception: Exception,
|
|
) -> None:
|
|
with exception:
|
|
line_zone = LineZone(
|
|
start=vector.start,
|
|
end=vector.end,
|
|
triggering_anchors=anchors,
|
|
minimum_crossing_threshold=minimum_crossing_threshold,
|
|
)
|
|
crossed_in_list = []
|
|
crossed_out_list = []
|
|
count_in_list = []
|
|
count_out_list = []
|
|
for bboxes in xyxy_sequence:
|
|
detections = _create_detections(
|
|
xyxy=bboxes,
|
|
tracker_id=[i for i in range(0, len(bboxes))],
|
|
)
|
|
crossed_in, crossed_out = line_zone.trigger(detections)
|
|
crossed_in_list.append(list(crossed_in))
|
|
crossed_out_list.append(list(crossed_out))
|
|
count_in_list.append(line_zone.in_count)
|
|
count_out_list.append(line_zone.out_count)
|
|
|
|
assert crossed_in_list == expected_crossed_in
|
|
assert crossed_out_list == expected_crossed_out
|
|
assert count_in_list == expected_count_in
|
|
assert count_out_list == expected_count_out
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
(
|
|
"xyxy_sequence",
|
|
"tracker_id_sequence",
|
|
"class_id_sequence",
|
|
"expected_in_count_per_class",
|
|
"expected_out_count_per_class",
|
|
),
|
|
[
|
|
pytest.param(
|
|
[
|
|
[[4, 4, 6, 6]], # frame 0: object 0, class 0, position 4,4,6,6
|
|
[[4, -6, 6, -4]], # frame 1: object 0, class 0, position 4,-6,6,-4
|
|
[[4, 4, 6, 6]], # frame 2: object 0, class 0, position 4,4,6,6
|
|
[[4, 4, 6, 6]], # frame 3: object 0, class 1, position 4,4,6,6
|
|
[[4, -6, 6, -4]], # frame 4: object 0, class 1, position 4,-6,6,-4
|
|
[[4, 4, 6, 6]], # frame 5: object 0, class 1, position 4,4,6,6
|
|
],
|
|
# tracker_id_sequence
|
|
[[0], [0], [0], [0], [0], [0]],
|
|
# class_id_sequence
|
|
[[0], [0], [0], [1], [1], [1]],
|
|
# expected_in_count_per_class
|
|
{0: 1, 1: 1},
|
|
# expected_out_count_per_class
|
|
{0: 1, 1: 1},
|
|
id="single_object_tracker_id_reuse_with_different_classes",
|
|
),
|
|
pytest.param(
|
|
[
|
|
# frame 0: objects 0&1 cross IN
|
|
[[4, 4, 6, 6], [4, 4, 6, 6]],
|
|
# frame 1
|
|
[[4, -6, 6, -4], [4, -6, 6, -4]],
|
|
# frame 2: objects 0&1 cross OUT
|
|
[[4, 4, 6, 6], [4, 4, 6, 6]],
|
|
# frame 3: objects 2&3 cross IN
|
|
[[4, 4, 6, 6], [4, 4, 6, 6]],
|
|
# frame 4
|
|
[[4, -6, 6, -4], [4, -6, 6, -4]],
|
|
# frame 5: objects 2&3 cross OUT
|
|
[[4, 4, 6, 6], [4, 4, 6, 6]],
|
|
],
|
|
# tracker_id_sequence
|
|
[[0, 1], [0, 1], [0, 1], [2, 3], [2, 3], [2, 3]],
|
|
# class_id_sequence
|
|
[[0, 1], [0, 1], [0, 1], [4, 5], [4, 5], [4, 5]],
|
|
# expected_in_count_per_class
|
|
{0: 1, 1: 1, 4: 1, 5: 1},
|
|
# expected_out_count_per_class
|
|
{0: 1, 1: 1, 4: 1, 5: 1},
|
|
id="multiple_objects_tracker_id_reuse_with_different_classes",
|
|
),
|
|
],
|
|
)
|
|
def test_line_zone_tracker_id_reuse_with_different_classes(
|
|
xyxy_sequence: list[list[list[float]]],
|
|
tracker_id_sequence: list[list[int]],
|
|
class_id_sequence: list[list[int]],
|
|
expected_in_count_per_class: dict[int, int],
|
|
expected_out_count_per_class: dict[int, int],
|
|
) -> None:
|
|
line_zone = LineZone(start=Point(0, 0), end=Point(10, 0))
|
|
|
|
for xyxy, tracker_id, class_id in zip(
|
|
xyxy_sequence, tracker_id_sequence, class_id_sequence
|
|
):
|
|
detections = _create_detections(
|
|
xyxy=xyxy, tracker_id=tracker_id, class_id=class_id
|
|
)
|
|
line_zone.trigger(detections)
|
|
|
|
assert line_zone.in_count_per_class == expected_in_count_per_class
|
|
assert line_zone.out_count_per_class == expected_out_count_per_class
|
|
|
|
|
|
def test_line_zone_trigger_does_not_call_np_cross(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""Guard against reintroducing np.cross, deprecated for 2-D input in NumPy 2.0."""
|
|
|
|
def _raise(*args, **kwargs):
|
|
raise AssertionError("np.cross must not be called on 2-D vectors")
|
|
|
|
monkeypatch.setattr(np, "cross", _raise)
|
|
|
|
line_zone = LineZone(start=Point(0, 0), end=Point(0, 10))
|
|
for xyxy in [[4, 4, 6, 6], [-6, 4, -4, 6]]:
|
|
detections = _create_detections(xyxy=[xyxy], tracker_id=[0])
|
|
crossed_in, crossed_out = line_zone.trigger(detections)
|
|
|
|
assert not crossed_in[0]
|
|
assert crossed_out[0]
|
|
assert line_zone.out_count == 1
|
|
|
|
|
|
def test_line_zone_trigger_evicts_stale_crossing_history() -> None:
|
|
"""History for tracker IDs absent from the current frame is evicted."""
|
|
line_zone = LineZone(start=Point(0, 0), end=Point(10, 0))
|
|
first_detections = _create_detections(
|
|
xyxy=[[4, 4, 6, 6]], tracker_id=[0], class_id=[1]
|
|
)
|
|
second_detections = _create_detections(
|
|
xyxy=[[4, 4, 6, 6]], tracker_id=[1], class_id=[2]
|
|
)
|
|
|
|
line_zone.trigger(first_detections)
|
|
# Trigger twice with second_detections so tracker_id=0 accumulates
|
|
# crossing_history_length absent frames (default=2) and is evicted.
|
|
line_zone.trigger(second_detections)
|
|
line_zone.trigger(second_detections)
|
|
|
|
assert set(line_zone.crossing_state_history) == {1}
|
|
|
|
|
|
def test_line_zone_trigger_evicts_stale_crossing_history_on_empty_frames() -> None:
|
|
"""Empty frames age out tracker crossing history."""
|
|
line_zone = LineZone(start=Point(0, 0), end=Point(10, 0))
|
|
detections = _create_detections(xyxy=[[4, 4, 6, 6]], tracker_id=[0], class_id=[1])
|
|
|
|
line_zone.trigger(detections)
|
|
for _ in range(line_zone.crossing_history_length):
|
|
line_zone.trigger(Detections.empty())
|
|
|
|
assert not line_zone.crossing_state_history
|
|
|
|
|
|
def test_line_zone_trigger_evicts_stale_crossing_history_on_class_change() -> None:
|
|
"""Class changes must not split a tracker crossing history."""
|
|
line_zone = LineZone(start=Point(0, 0), end=Point(10, 0))
|
|
first_detections = _create_detections(
|
|
xyxy=[[4, 4, 6, 6]], tracker_id=[0], class_id=[1]
|
|
)
|
|
second_detections = _create_detections(
|
|
xyxy=[[4, 4, 6, 6]], tracker_id=[0], class_id=[2]
|
|
)
|
|
|
|
line_zone.trigger(first_detections)
|
|
for _ in range(line_zone.crossing_history_length):
|
|
line_zone.trigger(second_detections)
|
|
|
|
assert set(line_zone.crossing_state_history) == {0}
|
|
|
|
|
|
def test_line_zone_class_flicker_keeps_crossing_counts_continuous() -> None:
|
|
"""A tracker's class change must not suppress a real crossing."""
|
|
line_zone = LineZone(start=Point(0, 0), end=Point(10, 0))
|
|
detections_sequence = [
|
|
_create_detections(xyxy=[[4, 4, 6, 6]], tracker_id=[0], class_id=[0]),
|
|
_create_detections(xyxy=[[4, -6, 6, -4]], tracker_id=[0], class_id=[1]),
|
|
_create_detections(xyxy=[[4, -6, 6, -4]], tracker_id=[0], class_id=[1]),
|
|
_create_detections(xyxy=[[4, 4, 6, 6]], tracker_id=[0], class_id=[1]),
|
|
]
|
|
|
|
crossed_in = []
|
|
crossed_out = []
|
|
for detections in detections_sequence:
|
|
crossed_in_frame, crossed_out_frame = line_zone.trigger(detections)
|
|
crossed_in.append(bool(crossed_in_frame[0]))
|
|
crossed_out.append(bool(crossed_out_frame[0]))
|
|
|
|
assert crossed_in == [False, True, False, False]
|
|
assert crossed_out == [False, False, False, True]
|
|
assert line_zone.in_count_per_class == {1: 1}
|
|
assert line_zone.out_count_per_class == {1: 1}
|
|
assert set(line_zone.crossing_state_history) == {0}
|
|
|
|
|
|
def test_line_zone_annotator_multiclass_supports_none_class_id() -> None:
|
|
line_zone = LineZone(start=Point(0, 0), end=Point(0, 10))
|
|
for xyxy in [[4, 4, 6, 6], [-6, 4, -4, 6]]:
|
|
detections = _create_detections(xyxy=[xyxy], tracker_id=[0])
|
|
line_zone.trigger(detections)
|
|
|
|
assert line_zone.out_count_per_class == {None: 1}
|
|
|
|
frame = np.zeros((100, 100, 3), dtype=np.uint8)
|
|
annotator = LineZoneAnnotatorMulticlass(force_draw_class_ids=False)
|
|
annotated_frame = annotator.annotate(frame=frame.copy(), line_zones=[line_zone])
|
|
|
|
assert annotated_frame.shape == frame.shape
|
|
assert not np.array_equal(annotated_frame, frame)
|