Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions src/supervision/detection/tools/polygon_zone.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ class PolygonZone:
which anchors of the detections bounding box to consider when deciding on
whether the detection fits within the PolygonZone
(default: (sv.Position.BOTTOM_CENTER,)).
require_all_anchors: If `True` (default), a detection is considered inside
the zone only when *every* anchor in `triggering_anchors` is inside.
If `False`, the detection triggers as soon as *any* anchor is inside.
Has no effect when `triggering_anchors` has a single entry.
current_count: The current count of detected objects within the zone
mask: The 2D bool mask for the polygon zone

Expand Down Expand Up @@ -62,11 +66,14 @@ def __init__(
self,
polygon: npt.NDArray[np.int64],
triggering_anchors: Iterable[Position] = (Position.BOTTOM_CENTER,),
require_all_anchors: bool = True,
):
self.polygon = polygon.astype(int)
self.triggering_anchors = triggering_anchors
if not list(self.triggering_anchors):
# Materialize once so we can safely accept generators without exhausting them.
self.triggering_anchors = list(triggering_anchors)
if not self.triggering_anchors:
raise ValueError("Triggering anchors cannot be empty.")
self.require_all_anchors = require_all_anchors
Comment on lines 71 to +76
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Fixed by materializing the iterable once with list(triggering_anchors) at the top of init, and added a regression test that passes a generator and asserts trigger() still works. Pushed in the latest commit.


self.current_count = 0

Expand Down Expand Up @@ -108,7 +115,9 @@ def trigger(self, detections: Detections) -> npt.NDArray[np.bool_]:
in_bounds = (x >= 0) & (y >= 0) & (x < mask_w) & (y < mask_h)
x_safe = np.clip(x, 0, mask_w - 1)
y_safe = np.clip(y, 0, mask_h - 1)
is_in_zone = np.all(in_bounds & self.mask[y_safe, x_safe], axis=0)
anchor_hits = in_bounds & self.mask[y_safe, x_safe]
reduce = np.all if self.require_all_anchors else np.any
is_in_zone = reduce(anchor_hits, axis=0)
self.current_count = int(np.sum(is_in_zone))
return is_in_zone.astype(bool)

Expand Down
28 changes: 28 additions & 0 deletions tests/detection/test_polygonzone.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ 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(
Expand Down Expand Up @@ -164,3 +174,21 @@ def test_anchor_on_polygon_boundary_included(self) -> None:
)
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])
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That line measures 86 characters including leading indent, which is under the configured 88-character limit. ruff format also leaves it alone in pre-commit, both locally and in pre-commit.ci on this PR. Looks like a Copilot false positive.

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]
Loading