Skip to content

fix: prevent single object from appearing in multiple polygon zones (#1987)#1991

Open
Adithi-Sreenath wants to merge 5 commits intoroboflow:developfrom
Adithi-Sreenath:fix/polygon-zone-multiple-zones
Open

fix: prevent single object from appearing in multiple polygon zones (#1987)#1991
Adithi-Sreenath wants to merge 5 commits intoroboflow:developfrom
Adithi-Sreenath:fix/polygon-zone-multiple-zones

Conversation

@Adithi-Sreenath
Copy link

Description

Fixes PolygonZone allowing objects to appear in multiple zones simultaneously.
Resolves #1987

When checking if a detection is inside a polygon zone, the previous implementation would clip the bounding box to fit within each ROI's dimensions before calculating anchor points. This caused the same detection to produce different anchor points for different ROIs, allowing it to be counted as present in multiple zones.

Example:

  • Detection box: [441, 258, 718, 713]
  • ROI1 bounds: [0, 167, 457, 427]
  • ROI2 bounds: [458, 67, 1011, 309]

Before fix:

  • Clipped to ROI1: [441, 258, 457, 427] → anchor at (449, 427) → detected in ROI1 ✗
  • Clipped to ROI2: [458, 258, 718, 309] → anchor at (588, 309) → detected in ROI2 ✗
  • Result: Object counted in BOTH zones

After fix:

  • Original box center: (580, 485)
  • Check against ROI1: center not in ROI1 → False ✓
  • Check against ROI2: center in ROI2 → True ✓
  • Result: Object counted in only ONE zone

Bug visualization

Before Fix

Before fix

After Fix

After fix

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • This change requires a documentation update

How has this change been tested?

  1. Automated tests: All 6 tests in test/detection/test_polygonzone.py pass:
    • test_polygon_zone_trigger
    • test_polygon_zone_initialization
  2. Manual verification: Ran bug_recreation.py and fix_test.py on a sample image to confirm:
    • Original detection spanning multiple ROIs → previously counted in multiple zones
    • After fix → correctly counted in only one zone

Any specific deployment considerations

  • Only the trigger() method of PolygonZone was modified.
  • No API or interface changes.
  • No secrets or additional dependencies.

Docs

  • Docs updated?
    • Updated docstring of trigger() explaining anchor calculation and bug fix.

when checking if a detection is inside a polygon zone, the previous implementation
would clip the bounding box to fit within each ROI's dimensions before calculating
anchor points. This caused the same detection to produce different anchor points
for different ROIs, allowing it to be counted as present in multiple zones.
@CLAassistant
Copy link

CLAassistant commented Oct 14, 2025

CLA assistant check
All committers have signed the CLA.

@Borda Borda added the bug Something isn't working label Feb 4, 2026
Copy link
Member

@Borda Borda left a comment

Choose a reason for hiding this comment

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

@Adithi-Sreenath could you pls add also tests for this chnage 🦩

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes the PolygonZone.trigger logic so that a single detection can no longer be reported as inside multiple non-overlapping polygon zones, aligning behavior with the expectations in issue #1987.

Changes:

  • Refactors PolygonZone.trigger to compute anchor points from the original (unclipped) detection boxes, perform explicit bounds checks against the zone mask, and remove the dependency on clip_boxes.
  • Adds explicit handling of empty Detections in trigger, returning an empty boolean array instead of relying on downstream numpy behavior.
  • Updates the trigger docstring to document the new anchor calculation behavior and its impact on multi-zone counting.

Comment on lines +82 to +83
Anchor points are calculated from original (unclipped) detection boxes.
to ensure a single object can only appear in one zone. This prevents
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

The second sentence of this new docstring paragraph starts with a lowercase "to" after a period, which reads like a broken sentence. Consider merging it into a single sentence ("...boxes to ensure a single object can only appear in one zone.") to keep the docstring grammatically correct and clearer.

Suggested change
Anchor points are calculated from original (unclipped) detection boxes.
to ensure a single object can only appear in one zone. This prevents
Anchor points are calculated from original (unclipped) detection boxes to
ensure a single object can only appear in one zone. This prevents

Copilot uses AI. Check for mistakes.
Comment on lines +98 to +105
all_anchors = np.array(
[
np.ceil(clipped_detections.get_anchors_coordinates(anchor)).astype(int)
for anchor in self.triggering_anchors
np.ceil(detections.get_anchors_coordinates(anchors)).astype(int)
for anchors in self.triggering_anchors
]
)

is_in_zone: npt.NDArray[np.bool_] = (
self.mask[all_clipped_anchors[:, :, 1], all_clipped_anchors[:, :, 0]]
.transpose()
.astype(bool)
)
is_in_zone: npt.NDArray[np.bool_] = np.zeros(len(detections), dtype=bool)
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

With the removal of clip_boxes in trigger, the frame_resolution_wh attribute initialized from the polygon bounds is now unused anywhere in the class. To avoid confusion and dead state, consider removing this attribute (and any related code) or reusing it if it is still intended to be part of the public API.

Copilot uses AI. Check for mistakes.
Comment on lines +98 to +122
all_anchors = np.array(
[
np.ceil(clipped_detections.get_anchors_coordinates(anchor)).astype(int)
for anchor in self.triggering_anchors
np.ceil(detections.get_anchors_coordinates(anchors)).astype(int)
for anchors in self.triggering_anchors
]
)

is_in_zone: npt.NDArray[np.bool_] = (
self.mask[all_clipped_anchors[:, :, 1], all_clipped_anchors[:, :, 0]]
.transpose()
.astype(bool)
)
is_in_zone: npt.NDArray[np.bool_] = np.zeros(len(detections), dtype=bool)

for detection_idx in range(len(detections)):
anchors = all_anchors[:, detection_idx, :]
all_in_zone = True

for anchor in anchors:
x, y = anchor

if x < 0 or y < 0 or x >= self.mask.shape[1] or y >= self.mask.shape[0]:
all_in_zone = False
break

if not self.mask[y, x]:
all_in_zone = False
break

is_in_zone[detection_idx] = all_in_zone
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

This new anchor-handling logic fixes the multi-zone superposition bug, but there is no automated test that specifically covers the scenario from issue #1987 (a single detection spanning multiple non-overlapping polygon zones). Adding a regression test that instantiates multiple PolygonZone instances with the reported ROIs and verifies the detection is counted in exactly one zone would help prevent future regressions for this case.

Copilot generated this review using guidance from repository custom instructions.
Copy link
Author

Choose a reason for hiding this comment

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

@Borda sure will look into it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working waiting for author

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Polygon Zone Superposition - Clipping bug

3 participants