Skip to content

Commit 80b0181

Browse files
committed
feat(detection): add require_all_anchors to PolygonZone
Currently a detection counts as 'in the zone' only when every anchor in triggering_anchors is inside. For boxes that straddle the zone boundary this means a detection with many anchors (e.g. the four corners) is often under-counted unless the user shrinks triggering_anchors to a single point. Add require_all_anchors: bool = True so callers can opt into 'any anchor inside is enough'. Default preserves current behaviour. Closes #1022.
1 parent fb2dec9 commit 80b0181

2 files changed

Lines changed: 27 additions & 1 deletion

File tree

src/supervision/detection/tools/polygon_zone.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ class PolygonZone:
3232
which anchors of the detections bounding box to consider when deciding on
3333
whether the detection fits within the PolygonZone
3434
(default: (sv.Position.BOTTOM_CENTER,)).
35+
require_all_anchors: If `True` (default), a detection is considered inside
36+
the zone only when *every* anchor in `triggering_anchors` is inside.
37+
If `False`, the detection triggers as soon as *any* anchor is inside.
38+
Has no effect when `triggering_anchors` has a single entry.
3539
current_count: The current count of detected objects within the zone
3640
mask: The 2D bool mask for the polygon zone
3741
@@ -62,11 +66,13 @@ def __init__(
6266
self,
6367
polygon: npt.NDArray[np.int64],
6468
triggering_anchors: Iterable[Position] = (Position.BOTTOM_CENTER,),
69+
require_all_anchors: bool = True,
6570
):
6671
self.polygon = polygon.astype(int)
6772
self.triggering_anchors = triggering_anchors
6873
if not list(self.triggering_anchors):
6974
raise ValueError("Triggering anchors cannot be empty.")
75+
self.require_all_anchors = require_all_anchors
7076

7177
self.current_count = 0
7278

@@ -108,7 +114,9 @@ def trigger(self, detections: Detections) -> npt.NDArray[np.bool_]:
108114
in_bounds = (x >= 0) & (y >= 0) & (x < mask_w) & (y < mask_h)
109115
x_safe = np.clip(x, 0, mask_w - 1)
110116
y_safe = np.clip(y, 0, mask_h - 1)
111-
is_in_zone = np.all(in_bounds & self.mask[y_safe, x_safe], axis=0)
117+
anchor_hits = in_bounds & self.mask[y_safe, x_safe]
118+
reduce = np.all if self.require_all_anchors else np.any
119+
is_in_zone = reduce(anchor_hits, axis=0)
112120
self.current_count = int(np.sum(is_in_zone))
113121
return is_in_zone.astype(bool)
114122

tests/detection/test_polygonzone.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,3 +164,21 @@ def test_anchor_on_polygon_boundary_included(self) -> None:
164164
)
165165
result = zone.trigger(detections)
166166
assert result[0]
167+
168+
def test_require_all_anchors_false_triggers_on_any_anchor(self) -> None:
169+
"""With require_all_anchors=False, any anchor inside triggers."""
170+
# Box [85, 85, 115, 115] has only BOTTOM_RIGHT (115, 115) inside POLYGON
171+
# ([100, 100]..[200, 200]); the other three corners are outside.
172+
detections = _create_detections(xyxy=[[85.0, 85.0, 115.0, 115.0]], class_id=[0])
173+
anchors = (
174+
sv.Position.TOP_LEFT,
175+
sv.Position.TOP_RIGHT,
176+
sv.Position.BOTTOM_LEFT,
177+
sv.Position.BOTTOM_RIGHT,
178+
)
179+
all_required = sv.PolygonZone(POLYGON, triggering_anchors=anchors)
180+
any_anchor = sv.PolygonZone(
181+
POLYGON, triggering_anchors=anchors, require_all_anchors=False
182+
)
183+
assert not all_required.trigger(detections)[0]
184+
assert any_anchor.trigger(detections)[0]

0 commit comments

Comments
 (0)