-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Expand file tree
/
Copy pathtest_polygonzone.py
More file actions
194 lines (177 loc) Β· 7.16 KB
/
Copy pathtest_polygonzone.py
File metadata and controls
194 lines (177 loc) Β· 7.16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
from contextlib import ExitStack as DoesNotRaise
import numpy as np
import pytest
import supervision as sv
from tests.helpers import _create_detections
DETECTION_BOXES = np.array(
[
[35.0, 35.0, 65.0, 65.0],
[60.0, 60.0, 90.0, 90.0],
[85.0, 85.0, 115.0, 115.0],
[110.0, 110.0, 140.0, 140.0],
[135.0, 135.0, 165.0, 165.0],
[160.0, 160.0, 190.0, 190.0],
[185.0, 185.0, 215.0, 215.0],
[210.0, 210.0, 240.0, 240.0],
[235.0, 235.0, 265.0, 265.0],
],
dtype=np.float32,
)
DETECTIONS = _create_detections(
xyxy=DETECTION_BOXES, class_id=np.array([0, 0, 0, 0, 0, 0, 0, 0, 0])
)
POLYGON = np.array([[100, 100], [200, 100], [200, 200], [100, 200]])
class TestPolygonZoneInit:
@pytest.mark.parametrize(
("polygon", "triggering_anchors", "exception"),
[
(POLYGON, [sv.Position.CENTER], DoesNotRaise()),
(
POLYGON,
[],
pytest.raises(ValueError, match="Triggering anchors cannot be empty"),
),
],
)
def test_empty_anchors_raises(self, polygon, triggering_anchors, exception):
with exception:
sv.PolygonZone(polygon, triggering_anchors=triggering_anchors)
def test_generator_triggering_anchors_is_materialized(self):
"""A generator passed for triggering_anchors must not be silently exhausted."""
zone = sv.PolygonZone(
POLYGON, triggering_anchors=(p for p in [sv.Position.CENTER])
)
detections = _create_detections(
xyxy=[[140.0, 140.0, 160.0, 160.0]], class_id=[0]
)
assert zone.trigger(detections)[0]
class TestPolygonZoneTrigger:
@pytest.mark.parametrize(
("detections", "polygon_zone", "expected_results", "exception"),
[
(
DETECTIONS,
sv.PolygonZone(
POLYGON,
triggering_anchors=(
sv.Position.TOP_LEFT,
sv.Position.TOP_RIGHT,
sv.Position.BOTTOM_LEFT,
sv.Position.BOTTOM_RIGHT,
),
),
np.array(
[False, False, False, True, True, True, False, False, False],
dtype=bool,
),
DoesNotRaise(),
), # all four corners must be inside
(
DETECTIONS,
sv.PolygonZone(POLYGON),
np.array(
[False, False, True, True, True, True, False, False, False],
dtype=bool,
),
DoesNotRaise(),
), # default anchor (BOTTOM_CENTER)
(
DETECTIONS,
sv.PolygonZone(
POLYGON,
triggering_anchors=[sv.Position.BOTTOM_CENTER],
),
np.array(
[False, False, True, True, True, True, False, False, False],
dtype=bool,
),
DoesNotRaise(),
), # explicit BOTTOM_CENTER matches default
(
sv.Detections.empty(),
sv.PolygonZone(POLYGON),
np.array([], dtype=bool),
DoesNotRaise(),
), # empty detections return empty array
],
)
def test_anchor_configurations(
self,
detections: sv.Detections,
polygon_zone: sv.PolygonZone,
expected_results: np.ndarray,
exception: Exception,
) -> None:
with exception:
in_zone = polygon_zone.trigger(detections)
assert np.all(in_zone == expected_results)
assert polygon_zone.current_count == int(np.sum(expected_results))
def test_straddling_detection_assigned_to_one_zone(self) -> None:
"""Detection straddling two adjacent zones is counted in exactly one zone.
The old implementation clipped each detection box to the zone's bounding box
before computing anchors, so a box straddling two adjacent zones got a
different anchor per zone and was double-counted. The fix computes anchors
from the original unclipped box, giving a single consistent position.
Setup: zone_left covers x 0-99, zone_right covers x 100-200.
Detection [60, 80, 140, 120] has BOTTOM_CENTER = ceil((60+140)/2), ceil(120)
= (100, 120), which falls in zone_right only.
"""
zone_left = sv.PolygonZone(
np.array([[0, 0], [99, 0], [99, 200], [0, 200]], dtype=np.int32)
)
zone_right = sv.PolygonZone(
np.array([[100, 0], [200, 0], [200, 200], [100, 200]], dtype=np.int32)
)
detections = _create_detections(
xyxy=[[60.0, 80.0, 140.0, 120.0]],
class_id=[0],
)
results = np.array(
[zone.trigger(detections)[0] for zone in [zone_left, zone_right]],
dtype=bool,
)
assert np.sum(results) == 1, (
"Detection should appear in exactly one zone, not zero or multiple"
)
assert results[1], "BOTTOM_CENTER (100, 120) should be in zone_right"
def test_out_of_bounds_anchor_excluded(self) -> None:
"""Anchors with negative coordinates are excluded, not wrapped or clamped."""
zone = sv.PolygonZone(
np.array([[0, 0], [100, 0], [100, 100], [0, 100]]),
triggering_anchors=[sv.Position.CENTER],
)
# CENTER = (ceil((-50+0)/2), ceil((25+75)/2)) = (-25, 50) β x < 0.
detections = _create_detections(
xyxy=[[-50.0, 25.0, 0.0, 75.0]],
class_id=[0],
)
result = zone.trigger(detections)
assert not result[0]
def test_anchor_on_polygon_boundary_included(self) -> None:
"""An anchor landing exactly on a polygon corner is counted as inside."""
polygon = np.array([[0, 0], [100, 0], [100, 100], [0, 100]])
zone = sv.PolygonZone(polygon, triggering_anchors=[sv.Position.BOTTOM_RIGHT])
# BOTTOM_RIGHT = (ceil(x2), ceil(y2)) = (100, 100) β the polygon corner.
detections = _create_detections(
xyxy=[[50.0, 50.0, 100.0, 100.0]],
class_id=[0],
)
result = zone.trigger(detections)
assert result[0]
def test_require_all_anchors_false_triggers_on_any_anchor(self) -> None:
"""With require_all_anchors=False, any anchor inside triggers."""
# Box [85, 85, 115, 115] has only BOTTOM_RIGHT (115, 115) inside POLYGON
# ([100, 100]..[200, 200]); the other three corners are outside.
detections = _create_detections(xyxy=[[85.0, 85.0, 115.0, 115.0]], class_id=[0])
anchors = (
sv.Position.TOP_LEFT,
sv.Position.TOP_RIGHT,
sv.Position.BOTTOM_LEFT,
sv.Position.BOTTOM_RIGHT,
)
all_required = sv.PolygonZone(POLYGON, triggering_anchors=anchors)
any_anchor = sv.PolygonZone(
POLYGON, triggering_anchors=anchors, require_all_anchors=False
)
assert not all_required.trigger(detections)[0]
assert any_anchor.trigger(detections)[0]