fix: prevent single object from appearing in multiple polygon zones (#1987)#1991
fix: prevent single object from appearing in multiple polygon zones (#1987)#1991Adithi-Sreenath wants to merge 5 commits intoroboflow:developfrom
Conversation
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.
Borda
left a comment
There was a problem hiding this comment.
@Adithi-Sreenath could you pls add also tests for this chnage 🦩
There was a problem hiding this comment.
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.triggerto compute anchor points from the original (unclipped) detection boxes, perform explicit bounds checks against the zone mask, and remove the dependency onclip_boxes. - Adds explicit handling of empty
Detectionsintrigger, returning an empty boolean array instead of relying on downstream numpy behavior. - Updates the
triggerdocstring to document the new anchor calculation behavior and its impact on multi-zone counting.
| Anchor points are calculated from original (unclipped) detection boxes. | ||
| to ensure a single object can only appear in one zone. This prevents |
There was a problem hiding this comment.
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.
| 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 |
| 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) |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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.
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:
Before fix:
After fix:
Bug visualization
Before Fix
After Fix
Type of change
How has this change been tested?
test/detection/test_polygonzone.pypass:test_polygon_zone_triggertest_polygon_zone_initializationbug_recreation.pyandfix_test.pyon a sample image to confirm:Any specific deployment considerations
trigger()method ofPolygonZonewas modified.Docs
trigger()explaining anchor calculation and bug fix.