From 33e10ff722c4708b0ed39f35a841a2e5ffc6729d Mon Sep 17 00:00:00 2001 From: Linas Kondrackis Date: Fri, 31 May 2024 20:13:13 +0300 Subject: [PATCH] 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 --- test/detection/test_line_counter.py | 501 ++++++++++++++++------------ 1 file changed, 279 insertions(+), 222 deletions(-) diff --git a/test/detection/test_line_counter.py b/test/detection/test_line_counter.py index d9c21f15..7642d965 100644 --- a/test/detection/test_line_counter.py +++ b/test/detection/test_line_counter.py @@ -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