@@ -44,6 +44,23 @@ def test_empty_anchors_raises(self, polygon, triggering_anchors, exception) -> N
4444 with exception :
4545 sv .PolygonZone (polygon , triggering_anchors = triggering_anchors )
4646
47+ def test_generator_triggering_anchors_is_materialized (self ):
48+ """A generator passed for triggering_anchors must not be silently exhausted.
49+
50+ Calls trigger() twice on the same zone: a materialized list keeps returning
51+ results on repeated calls, whereas an un-materialized generator would be
52+ exhausted after the first trigger() and silently yield no anchors on the
53+ second.
54+ """
55+ zone = sv .PolygonZone (
56+ POLYGON , triggering_anchors = (p for p in [sv .Position .CENTER ])
57+ )
58+ detections = _create_detections (
59+ xyxy = [[140.0 , 140.0 , 160.0 , 160.0 ]], class_id = [0 ]
60+ )
61+ assert zone .trigger (detections )[0 ]
62+ assert zone .trigger (detections )[0 ]
63+
4764
4865class TestPolygonZoneTrigger :
4966 @pytest .mark .parametrize (
@@ -165,6 +182,106 @@ def test_anchor_on_polygon_boundary_included(self) -> None:
165182 result = zone .trigger (detections )
166183 assert result [0 ]
167184
185+ def test_anchor_on_polygon_boundary_included_any_mode (self ) -> None :
186+ """With require_all_anchors=False and multiple anchors, an anchor landing
187+ exactly on the polygon boundary is enough to trigger, even though the
188+ other anchors of the same detection fall outside the polygon."""
189+ polygon = np .array ([[0 , 0 ], [100 , 0 ], [100 , 100 ], [0 , 100 ]])
190+ anchors = [sv .Position .TOP_LEFT , sv .Position .BOTTOM_RIGHT ]
191+ # TOP_LEFT = (-50, -50) is outside; BOTTOM_RIGHT = (100, 100) is exactly
192+ # the polygon corner (boundary), which counts as inside.
193+ detections = _create_detections (
194+ xyxy = [[- 50.0 , - 50.0 , 100.0 , 100.0 ]],
195+ class_id = [0 ],
196+ )
197+ any_anchor_zone = sv .PolygonZone (
198+ polygon , triggering_anchors = anchors , require_all_anchors = False
199+ )
200+ all_anchors_zone = sv .PolygonZone (
201+ polygon , triggering_anchors = anchors , require_all_anchors = True
202+ )
203+ assert any_anchor_zone .trigger (detections )[0 ]
204+ assert not all_anchors_zone .trigger (detections )[0 ]
205+
206+ def test_require_all_anchors_false_triggers_on_any_anchor (self ) -> None :
207+ """With require_all_anchors=False, any anchor inside triggers."""
208+ # Box [85, 85, 115, 115] has only BOTTOM_RIGHT (115, 115) inside POLYGON
209+ # ([100, 100]..[200, 200]); the other three corners are outside.
210+ detections = _create_detections (xyxy = [[85.0 , 85.0 , 115.0 , 115.0 ]], class_id = [0 ])
211+ anchors = (
212+ sv .Position .TOP_LEFT ,
213+ sv .Position .TOP_RIGHT ,
214+ sv .Position .BOTTOM_LEFT ,
215+ sv .Position .BOTTOM_RIGHT ,
216+ )
217+ all_required = sv .PolygonZone (POLYGON , triggering_anchors = anchors )
218+ any_anchor = sv .PolygonZone (
219+ POLYGON , triggering_anchors = anchors , require_all_anchors = False
220+ )
221+ assert not all_required .trigger (detections )[0 ]
222+ result = any_anchor .trigger (detections )
223+ assert result [0 ]
224+ assert any_anchor .current_count == 1
225+
226+ def test_require_all_anchors_false_all_outside_does_not_trigger (self ) -> None :
227+ """With require_all_anchors=False, a detection with every anchor outside the
228+ zone still does not trigger (exercises the np.any all-False branch)."""
229+ # Box [0, 0, 20, 20] has all four corners well outside POLYGON
230+ # ([100, 100]..[200, 200]).
231+ detections = _create_detections (xyxy = [[0.0 , 0.0 , 20.0 , 20.0 ]], class_id = [0 ])
232+ anchors = (
233+ sv .Position .TOP_LEFT ,
234+ sv .Position .TOP_RIGHT ,
235+ sv .Position .BOTTOM_LEFT ,
236+ sv .Position .BOTTOM_RIGHT ,
237+ )
238+ any_anchor = sv .PolygonZone (
239+ POLYGON , triggering_anchors = anchors , require_all_anchors = False
240+ )
241+ result = any_anchor .trigger (detections )
242+ assert not result [0 ]
243+ assert any_anchor .current_count == 0
244+
245+ def test_require_all_anchors_default_matches_explicit_true (self ) -> None :
246+ """Omitting require_all_anchors and passing require_all_anchors=True
247+ explicitly must produce identical trigger() results, pinning the
248+ documented default (True)."""
249+ anchors = (
250+ sv .Position .TOP_LEFT ,
251+ sv .Position .TOP_RIGHT ,
252+ sv .Position .BOTTOM_LEFT ,
253+ sv .Position .BOTTOM_RIGHT ,
254+ )
255+ default_zone = sv .PolygonZone (POLYGON , triggering_anchors = anchors )
256+ explicit_true_zone = sv .PolygonZone (
257+ POLYGON , triggering_anchors = anchors , require_all_anchors = True
258+ )
259+ assert np .array_equal (
260+ default_zone .trigger (DETECTIONS ), explicit_true_zone .trigger (DETECTIONS )
261+ )
262+
263+ def test_require_all_anchors_has_no_effect_with_single_anchor (self ) -> None :
264+ """With a single triggering anchor, require_all_anchors is a no-op: both
265+ settings must produce identical trigger() results (per the docstring:
266+ "Has no effect when triggering_anchors has a single entry")."""
267+ # Box [140, 140, 160, 160] has its BOTTOM_CENTER inside POLYGON.
268+ detections = _create_detections (
269+ xyxy = [[140.0 , 140.0 , 160.0 , 160.0 ]], class_id = [0 ]
270+ )
271+ require_all = sv .PolygonZone (
272+ POLYGON ,
273+ triggering_anchors = [sv .Position .BOTTOM_CENTER ],
274+ require_all_anchors = True ,
275+ )
276+ require_any = sv .PolygonZone (
277+ POLYGON ,
278+ triggering_anchors = [sv .Position .BOTTOM_CENTER ],
279+ require_all_anchors = False ,
280+ )
281+ assert np .array_equal (
282+ require_all .trigger (detections ), require_any .trigger (detections )
283+ )
284+
168285 def test_half_pixel_anchor_uses_nearest_pixel (self ) -> None :
169286 """Half-pixel anchors should not be biased toward the larger x and y."""
170287 zone_left = sv .PolygonZone (
0 commit comments