Add unit tests covering most single-detection cases

* Add Colab for visuals: https://colab.research.google.com/drive/179sq8joJ-7JSYMqYNBQlMIPYEClq1PDi?usp=sharing
* Need to visualise other cases - not sure if all are necessary
This commit is contained in:
Linas Kondrackis 2024-05-31 20:13:13 +03:00
parent d59467b4f4
commit 33e10ff722
1 changed files with 279 additions and 222 deletions

View File

@ -1,5 +1,4 @@
from contextlib import ExitStack as DoesNotRaise
from itertools import chain, combinations
from test.test_utils import mock_detections
from typing import List, Optional, Tuple
@ -78,279 +77,331 @@ def test_calculate_region_of_interest_limits(
@pytest.mark.parametrize(
"vector, xyxy_sequence, expected_crossed_in, expected_crossed_out",
[
(
Vector(
Point(0, 0),
Point(0, 100),
),
( # Vertical line, simple crossing
Vector(Point(0, 0), Point(0, 10)),
[
[100, 50, 120, 70],
[-100, 50, -80, 70],
[4, 4, 6, 6],
[4 - 10, 4, 6 - 10, 6],
[4, 4, 6, 6],
[4 - 10, 4, 6 - 10, 6],
],
[False, False],
[False, True],
[False, False, True, False],
[False, True, False, True],
),
(
Vector(
Point(0, 0),
Point(0, 100),
),
( # Vertical line reversed, simple crossing
Vector(Point(0, 10), Point(0, 0)),
[
[-100, 50, -80, 70],
[100, 50, 120, 70],
[4, 4, 6, 6],
[4 - 10, 4, 6 - 10, 6],
[4, 4, 6, 6],
[4 - 10, 4, 6 - 10, 6],
],
[False, True],
[False, False],
[False, True, False, True],
[False, False, True, False],
),
(
Vector(
Point(0, 0),
Point(0, 100),
),
( # Horizontal line, simple crossing
Vector(Point(0, 0), Point(10, 0)),
[
[-100, 50, -80, 70],
[-10, 50, 20, 70],
[100, 50, 120, 70],
[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, False],
[False, True, False, True],
[False, False, True, False],
),
(
Vector(
Point(0, 0),
Point(100, 100),
),
( # Horizontal line reversed, simple crossing
Vector(Point(10, 0), Point(0, 0)),
[
[50, 45, 70, 30],
[40, 50, 50, 40],
[0, 50, 10, 40],
[4, 4, 6, 6],
[4, 4 - 10, 6, 6 - 10],
[4, 4, 6, 6],
[4, 4 - 10, 6, 6 - 10],
],
[False, False, False],
[False, False, True],
[False, False, True, False],
[False, True, False, True],
),
(
Vector(
Point(0, 0),
Point(100, 0),
),
( # Diagonal line, simple crossing
Vector(Point(5, 0), Point(0, 5)),
[
[50, -45, 70, -30],
[40, 50, 50, 40],
[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, False],
[False, True],
[False, True, False, True],
[False, False, True, False],
),
(
Vector(
Point(0, 0),
Point(0, -100),
),
( # Crossing beside - right side
Vector(Point(0, 0), Point(10, 0)),
[
[100, -50, 120, -70],
[-100, -50, -80, -70],
[20, 4, 24, 6],
[20, 4 - 10, 24, 6 - 10],
[20, 4, 24, 6],
[20, 4 - 10, 24, 6 - 10],
],
[False, True],
[False, False],
),
(
Vector(
Point(0, 0),
Point(50, 100),
),
[
[50, 50, 70, 30],
[40, 50, 50, 40],
[0, 50, 10, 40],
],
[False, False, False],
[False, False, True],
),
(
Vector(
Point(0, 0),
Point(0, 100),
),
[
[100, 50, 120, 70],
[-100, 50, -80, 70],
[100, 50, 120, 70],
[-100, 50, -80, 70],
[100, 50, 120, 70],
[-100, 50, -80, 70],
[100, 50, 120, 70],
[-100, 50, -80, 70],
],
[False, False, True, False, True, False, True, False],
[False, True, False, True, False, True, False, True],
),
(
Vector(
Point(0, 0),
Point(-100, 0),
),
[
[-50, 70, -40, 50],
[-50, -70, -40, -50],
[-50, 70, -40, 50],
[-50, -70, -40, -50],
[-50, 70, -40, 50],
[-50, -70, -40, -50],
[-50, 70, -40, 50],
[-50, -70, -40, -50],
],
[False, False, True, False, True, False, True, False],
[False, True, False, True, False, True, False, True],
),
(
Vector(
Point(0, 100),
Point(0, 200),
),
[
[-100, 150, -80, 170],
[-100, 50, -80, 70],
[-10, 50, 20, 70],
[100, 50, 120, 70],
], # detection goes "around" line start and hence never crosses it
[False, False, False, False],
[False, False, False, False],
),
(
Vector(
Point(0, 100),
Point(0, 200),
),
( # Crossing beside - left side
Vector(Point(0, 0), Point(10, 0)),
[
[-100, 150, -80, 170],
[-100, 250, -80, 270],
[-10, 250, 20, 270],
[100, 250, 120, 270],
], # detection goes "around" line end and hence never crosses it
[False, False, False, False],
[False, False, False, False],
),
(
Vector(
Point(-50, -50),
Point(-100, -150),
),
[
[-30, -80, -20, -100],
[-150, -60, -110, -70],
[-10, -100, 20, -130],
[-20, 4, -24, 6],
[-20, 4 - 10, -24, 6 - 10],
[-20, 4, -24, 6],
[-20, 4 - 10, -24, 6 - 10],
],
[False, True, False],
[False, False, True],
[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],
),
# ( # Diagonal crossing of a straight line - does not work.
# 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, True, False],
# [False, True, False, True],
# ),
# ( # V-shaped crossing - does not work.
# Vector(Point(0, 0), Point(10, 0)),
# [
# [-3, 6, -1, 8],
# [4, -6, 6, -4],
# [11, 6, 13, 8]
# ],
# [False, False, True],
# [False, True, False]
# ),
],
)
def test_line_zone_single_detection(
def test_line_zone_one_detection_default_anchors(
vector: Vector,
xyxy_sequence: List[List[int]],
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 = mock_detections(
xyxy=[bbox],
tracker_id=[0],
)
crossed_in, crossed_out = line_zone.trigger(detections)
assert crossed_in[0] == expected_crossed_in[i]
assert crossed_out[0] == expected_crossed_out[i]
assert line_zone.in_count == sum(expected_crossed_in[: (i + 1)])
assert line_zone.out_count == sum(expected_crossed_out[: (i + 1)])
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,"
"expected_crossed_in,"
"expected_crossed_out,"
"crossing_anchors",
"vector, xyxy_sequence, triggering_anchors, expected_crossed_in, "
"expected_crossed_out",
[
(
Vector(
Point(0, 0),
Point(100, 100),
),
( # Scrape line, left side, corner anchors
Vector(Point(0, 0), Point(10, 0)),
[
[50, 30, 60, 20],
[20, 50, 40, 30],
[-2, 4, 2, 6],
[-2, 4 - 10, 2, 6 - 10],
[-2, 4, 2, 6],
[-2, 4 - 10, 2, 6 - 10],
],
[False, False],
[False, True],
[Position.TOP_LEFT, Position.TOP_RIGHT, Position.BOTTOM_LEFT],
[
Position.TOP_LEFT,
Position.BOTTOM_LEFT,
Position.TOP_RIGHT,
Position.BOTTOM_RIGHT,
],
[False, False, False, False],
[False, False, False, False],
),
(
Vector(
Point(0, 0),
Point(0, 100),
),
( # Scrape line, left side, right anchors
Vector(Point(0, 0), Point(10, 0)),
[
[-100, 50, -80, 70],
[-100, 50, 120, 70],
[-2, 4, 2, 6],
[-2, 4 - 10, 2, 6 - 10],
[-2, 4, 2, 6],
[-2, 4 - 10, 2, 6 - 10],
],
[False, True],
[False, False],
[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, 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_single_detection_on_subset_of_anchors(
def test_line_zone_one_detection(
vector: Vector,
xyxy_sequence: List[List[int]],
triggering_anchors: List[Position],
expected_crossed_in: List[bool],
expected_crossed_out: List[bool],
crossing_anchors: List[Position],
) -> None:
def powerset(s):
return chain.from_iterable(combinations(s, r) for r in range(len(s) + 1))
line_zone = LineZone(
start=vector.start, end=vector.end, triggering_anchors=triggering_anchors
)
for anchors in powerset(
[
Position.TOP_LEFT,
Position.TOP_RIGHT,
Position.BOTTOM_LEFT,
Position.BOTTOM_RIGHT,
]
):
if not anchors:
continue
line_zone = LineZone(
start=vector.start, end=vector.end, triggering_anchors=anchors
crossed_in_list = []
crossed_out_list = []
for i, bbox in enumerate(xyxy_sequence):
detections = mock_detections(
xyxy=[bbox],
tracker_id=[0],
)
for i, bbox in enumerate(xyxy_sequence):
detections = mock_detections(
xyxy=[bbox],
tracker_id=[0],
)
crossed_in, crossed_out = line_zone.trigger(detections)
if all(anchor in crossing_anchors for anchor in anchors):
assert crossed_in == expected_crossed_in[i]
assert crossed_out == expected_crossed_out[i]
else:
assert np.all(not crossed_in)
assert np.all(not crossed_out)
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,"
"expected_crossed_in,"
"expected_crossed_out,"
"vector, xyxy_sequence, expected_crossed_in, expected_crossed_out, "
"anchors, exception",
[
(
Vector(
Point(0, 0),
Point(0, 100),
Point(0, 10),
),
[
[[100, 50, 120, 70], [100, 50, 120, 70]],
[[-100, 50, -80, 70], [100, 50, 120, 70]],
[[100, 50, 120, 70], [100, 50, 120, 70]],
[[10, 5, 12, 7], [10, 5, 12, 7]],
[[-10, 5, -8, 7], [10, 5, 12, 7]],
[[10, 5, 12, 7], [10, 5, 12, 7]],
],
[[False, False], [False, False], [True, False]],
[[False, False], [True, False], [False, False]],
@ -365,17 +416,17 @@ def test_line_zone_single_detection_on_subset_of_anchors(
(
Vector(
Point(0, 0),
Point(-100, 0),
Point(-10, 0),
),
[
[[-50, 70, -40, 50], [-80, -50, -70, -40]],
[[-50, -70, -40, -50], [-80, 50, -70, 40]],
[[-50, 70, -40, 50], [-80, 50, -70, 40]],
[[-50, -70, -40, -50], [-80, 50, -70, 40]],
[[-50, 70, -40, 50], [-80, 50, -70, 40]],
[[-50, -70, -40, -50], [-80, 50, -70, 40]],
[[-50, 70, -40, 50], [-80, 50, -70, 40]],
[[-50, -70, -40, -50], [-80, -50, -70, -40]],
[[-5, 7, -4, 5], [-8, -5, -7, -4]],
[[-5, -7, -4, -5], [-8, 5, -7, 4]],
[[-5, 7, -4, 5], [-8, 5, -7, 4]],
[[-5, -7, -4, -5], [-8, 5, -7, 4]],
[[-5, 7, -4, 5], [-8, 5, -7, 4]],
[[-5, -7, -4, -5], [-8, 5, -7, 4]],
[[-5, 7, -4, 5], [-8, 5, -7, 4]],
[[-5, -7, -4, -5], [-8, -5, -7, -4]],
],
[
(False, False),
@ -407,12 +458,12 @@ def test_line_zone_single_detection_on_subset_of_anchors(
),
(
Vector(
Point(-50, -50),
Point(-100, -150),
Point(-5, -5),
Point(-10, -15),
),
[
[[-30, -80, -20, -100], [100, 50, 120, 70]],
[[-100, -80, -20, -100], [100, 50, 120, 70]],
[[-3, -8, -2, -10], [10, 5, 12, 7]],
[[-10, -8, -2, -10], [10, 5, 12, 7]],
],
[[False, False], [True, False]],
[[False, False], [False, False]],
@ -422,9 +473,9 @@ def test_line_zone_single_detection_on_subset_of_anchors(
(
Vector(
Point(0, 0),
Point(-100, 0),
Point(-10, 0),
),
[[[-50, 70, -40, 50], [-80, -50, -70, -40]]],
[[[-5, 7, -4, 5], [-8, -5, -7, -4]]],
[(False, False)],
[(False, False)],
[], # raise because of empty anchors
@ -440,6 +491,12 @@ def test_line_zone_multiple_detections(
anchors: List[Position],
exception: Exception,
) -> None:
"""
Test LineZone with multiple detections.
A detection is represented by a sequence of xyxy bboxes which represent
subsequent positions of the detected object. If a line is crossed (in either
direction) by a detection it is crossed by exactly all anchors from @anchors.
"""
with exception:
line_zone = LineZone(
start=vector.start, end=vector.end, triggering_anchors=anchors