From 50a36a750af58fc02d3ad6de4126f525f427949b Mon Sep 17 00:00:00 2001 From: Nishant-ZFYII Date: Tue, 10 Feb 2026 17:15:07 -0500 Subject: [PATCH] Fix overlap filter to use segmentation masks when available (#1987) --- .../core_steps/analytics/overlap/v1.py | 75 +++++- .../analytics/test_coords_overlap_v1.py | 229 +++++++++++++++++- 2 files changed, 291 insertions(+), 13 deletions(-) diff --git a/inference/core/workflows/core_steps/analytics/overlap/v1.py b/inference/core/workflows/core_steps/analytics/overlap/v1.py index 8404e0681a..796aa7021d 100644 --- a/inference/core/workflows/core_steps/analytics/overlap/v1.py +++ b/inference/core/workflows/core_steps/analytics/overlap/v1.py @@ -33,8 +33,9 @@ - **Overlap class detections**: Objects matching the specified `overlap_class_name` (e.g., "bicycle", "pallet", "car") - **Other detections**: All remaining objects that may overlap with the overlap class 3. For each overlap class detection, identifies other detections that spatially overlap with it using one of two overlap modes: - - **Center Overlap**: Checks if the center point of other detections falls within the overlap class bounding box (more precise, requires the center to be inside) - - **Any Overlap**: Checks if there's any spatial intersection between bounding boxes (more lenient, any overlap counts) + - **Center Overlap**: Checks if the center point of other detections falls within the overlap class region (more precise, requires the center to be inside) + - **Any Overlap**: Checks if there's any spatial intersection between detections (more lenient, any overlap counts) + When instance segmentation masks are available, overlap is computed at the pixel level using the actual mask shapes. For plain object detection predictions (no masks), overlap is computed using bounding boxes. 4. Collects all detections that overlap with any overlap class instance 5. Filters out the overlap class detections themselves from the output 6. Returns only the overlapping detections (objects that are positioned relative to the overlap class) @@ -88,7 +89,7 @@ class OverlapManifest(WorkflowBlockManifest): ) overlap_type: Literal["Center Overlap", "Any Overlap"] = Field( default="Center Overlap", - description="Method for determining spatial overlap between detections. 'Center Overlap' checks if the center point of other detections falls within the overlap class bounding box (more precise, requires center to be inside). 'Any Overlap' checks if there's any spatial intersection between bounding boxes (more lenient, any overlap counts). Center Overlap is stricter and better for containment relationships, while Any Overlap is more inclusive and better for detecting any proximity or partial overlap.", + description="Method for determining spatial overlap between detections. 'Center Overlap' checks if the center point of other detections falls within the overlap class region (more precise, requires center to be inside). 'Any Overlap' checks if there's any spatial intersection between detections (more lenient, any overlap counts). Center Overlap is stricter and better for containment relationships, while Any Overlap is more inclusive and better for detecting any proximity or partial overlap. When segmentation masks are present, overlap is computed at the pixel level using the actual mask geometry; otherwise bounding boxes are used.", examples=["Center Overlap", "Any Overlap"], ) overlap_class_name: Union[str] = Field( @@ -144,6 +145,34 @@ def coords_overlap( or other[1] > overlap[3] ) + @classmethod + def masks_overlap( + cls, + overlap_mask: np.ndarray, + other_mask: np.ndarray, + other_bbox: list[int], + overlap_type: Literal["Center Overlap", "Any Overlap"], + ) -> bool: + """Check overlap using segmentation masks instead of bounding boxes. + + Args: + overlap_mask: Boolean mask of the overlap-class detection. + other_mask: Boolean mask of the other detection. + other_bbox: Bounding box [x1, y1, x2, y2] of the other detection. + overlap_type: "Center Overlap" checks whether the center of + *other_bbox* falls on a True pixel in *overlap_mask*. + "Any Overlap" checks whether the two masks share any True pixel. + """ + if overlap_type == "Center Overlap": + cx = int((other_bbox[0] + other_bbox[2]) / 2) + cy = int((other_bbox[1] + other_bbox[3]) / 2) + h, w = overlap_mask.shape + if 0 <= cy < h and 0 <= cx < w: + return bool(overlap_mask[cy, cx]) + return False + else: + return bool(np.logical_and(overlap_mask, other_mask).any()) + def run( self, predictions: sv.Detections, @@ -151,29 +180,51 @@ def run( overlap_class_name: str, ) -> BlockResult: - overlaps = [] + has_masks = ( + getattr(predictions, "mask", None) is not None + and len(predictions.mask) == len(predictions.xyxy) + ) + + overlap_indices = [] others = {} for i in range(len(predictions.xyxy)): data = get_data_item(predictions.data, i) if data["class_name"] == overlap_class_name: - overlaps.append(predictions.xyxy[i]) + overlap_indices.append(i) else: others[i] = predictions.xyxy[i] # set of indices representing the overlapped objects idx = set() - for overlap in overlaps: + for oi in overlap_indices: if not others: break - overlapped = { - k - for k in others - if OverlapBlockV1.coords_overlap(overlap, others[k], overlap_type) - } + + if has_masks: + overlapped = { + k + for k in others + if OverlapBlockV1.masks_overlap( + predictions.mask[oi], + predictions.mask[k], + predictions.xyxy[k], + overlap_type, + ) + } + else: + overlapped = { + k + for k in others + if OverlapBlockV1.coords_overlap( + predictions.xyxy[oi], others[k], overlap_type + ) + } + # once it's overlapped we don't need to check again for k in overlapped: del others[k] idx = idx.union(overlapped) - return {OUTPUT_KEY: predictions[list(idx)]} + selected = sorted(idx) + return {OUTPUT_KEY: predictions[selected]} diff --git a/tests/workflows/unit_tests/core_steps/analytics/test_coords_overlap_v1.py b/tests/workflows/unit_tests/core_steps/analytics/test_coords_overlap_v1.py index 30d5c5ba49..f894f21df8 100644 --- a/tests/workflows/unit_tests/core_steps/analytics/test_coords_overlap_v1.py +++ b/tests/workflows/unit_tests/core_steps/analytics/test_coords_overlap_v1.py @@ -1,6 +1,11 @@ +import numpy as np import pytest +import supervision as sv -from inference.core.workflows.core_steps.analytics.overlap.v1 import OverlapBlockV1 +from inference.core.workflows.core_steps.analytics.overlap.v1 import ( + OUTPUT_KEY, + OverlapBlockV1, +) def test_coords_overlap(): @@ -16,3 +21,225 @@ def test_coords_overlap(): assert OverlapBlockV1.coords_overlap( [0, 0, 20, 20], [15, 15, 35, 35], "Any Overlap" ) + + +def _make_segmentation_detections( + xyxy: np.ndarray, + masks: np.ndarray, + class_names: list, +) -> sv.Detections: + """Helper to build sv.Detections with segmentation masks.""" + n = len(xyxy) + return sv.Detections( + xyxy=xyxy, + mask=masks, + confidence=np.ones(n) * 0.9, + class_id=np.arange(n), + data={"class_name": np.array(class_names)}, + ) + + +def test_overlap_run_masks_no_false_positive(): + """ + Regression test for GitHub Issue #1987: + https://github.com/roboflow/inference/issues/1987 + + When instance segmentation masks are present, the overlap block must + use pixel-level mask overlap — not just bounding-box overlap. + + Scenario (100x100 image): + "container" — bbox [0,0,100,100], mask covers ONLY bottom-left + quadrant (rows 50-99, cols 0-49) + "item" — bbox [60,10,90,40], in the TOP-RIGHT area + + Bounding boxes overlap, but masks do NOT. + The block should report NO overlap. + """ + image_h, image_w = 100, 100 + + xyxy = np.array( + [ + [0, 0, 100, 100], # container — large bbox + [60, 10, 90, 40], # item — top-right bbox + ], + dtype=np.float32, + ) + + masks = np.zeros((2, image_h, image_w), dtype=bool) + masks[0, 50:100, 0:50] = True # container mask: bottom-left only + masks[1, 10:40, 60:90] = True # item mask: top-right + + detections = _make_segmentation_detections( + xyxy=xyxy, masks=masks, class_names=["container", "item"] + ) + + # Sanity check: masks do NOT overlap at the pixel level + assert not np.logical_and(masks[0], masks[1]).any(), ( + "Sanity check failed: masks should not overlap" + ) + + block = OverlapBlockV1() + + result_any = block.run( + predictions=detections, + overlap_type="Any Overlap", + overlap_class_name="container", + ) + result_center = block.run( + predictions=detections, + overlap_type="Center Overlap", + overlap_class_name="container", + ) + + # Masks don't overlap, so no detections should be returned + assert len(result_any[OUTPUT_KEY]) == 0, ( + "Expected no overlap — masks do not intersect" + ) + assert len(result_center[OUTPUT_KEY]) == 0, ( + "Expected no overlap — masks do not intersect" + ) + + +def test_masks_overlap(): + """Unit tests for the masks_overlap classmethod, mirroring test_coords_overlap.""" + image_h, image_w = 100, 100 + + # overlap mask: bottom-left quadrant + overlap_mask = np.zeros((image_h, image_w), dtype=bool) + overlap_mask[50:100, 0:50] = True + + # other mask in top-right — no pixel overlap + other_mask_far = np.zeros((image_h, image_w), dtype=bool) + other_mask_far[10:40, 60:90] = True + other_bbox_far = [60, 10, 90, 40] + + # other mask inside overlap — pixel overlap exists + other_mask_inside = np.zeros((image_h, image_w), dtype=bool) + other_mask_inside[60:80, 20:40] = True + other_bbox_inside = [20, 60, 40, 80] + + # No overlap cases + assert not OverlapBlockV1.masks_overlap( + overlap_mask, other_mask_far, other_bbox_far, "Center Overlap" + ) + assert not OverlapBlockV1.masks_overlap( + overlap_mask, other_mask_far, other_bbox_far, "Any Overlap" + ) + + # Overlap cases + assert OverlapBlockV1.masks_overlap( + overlap_mask, other_mask_inside, other_bbox_inside, "Center Overlap" + ) + assert OverlapBlockV1.masks_overlap( + overlap_mask, other_mask_inside, other_bbox_inside, "Any Overlap" + ) + + +def test_overlap_run_bbox_fallback_without_masks(): + """ + When predictions have no masks (plain object detection), the block + falls back to bounding-box overlap — same behavior as before the fix. + """ + xyxy = np.array( + [ + [0, 0, 100, 100], # container + [10, 10, 50, 50], # item — bbox inside container + ], + dtype=np.float32, + ) + + detections = sv.Detections( + xyxy=xyxy, + confidence=np.array([0.9, 0.9]), + class_id=np.array([0, 1]), + data={"class_name": np.array(["container", "item"])}, + ) + + block = OverlapBlockV1() + result = block.run( + predictions=detections, + overlap_type="Any Overlap", + overlap_class_name="container", + ) + + # No masks → bbox overlap is used; bboxes overlap → item is found + assert len(result[OUTPUT_KEY]) == 1 + assert result[OUTPUT_KEY].data["class_name"][0] == "item" + + +def test_overlap_run_with_masks_true_positive(): + """ + Control test: when masks DO overlap, the block should (and does) + report them as overlapping. This verifies the block works correctly + in the non-buggy case (where bbox overlap matches mask overlap). + + Scenario (100x100 image): + "container" — bbox [0,0,100,100], mask covers bottom-left quadrant + "item" — bbox [20,60,40,80], placed INSIDE the container mask + """ + image_h, image_w = 100, 100 + + xyxy = np.array( + [ + [0, 0, 100, 100], # container + [20, 60, 40, 80], # item — inside container's mask + ], + dtype=np.float32, + ) + + masks = np.zeros((2, image_h, image_w), dtype=bool) + masks[0, 50:100, 0:50] = True # container mask: bottom-left + masks[1, 60:80, 20:40] = True # item mask: inside container mask + + detections = _make_segmentation_detections( + xyxy=xyxy, masks=masks, class_names=["container", "item"] + ) + + # Sanity check: masks DO overlap + assert np.logical_and(masks[0], masks[1]).any(), ( + "Sanity check failed: masks should overlap" + ) + + block = OverlapBlockV1() + result = block.run( + predictions=detections, + overlap_type="Any Overlap", + overlap_class_name="container", + ) + + # This correctly reports overlap (both bbox and mask agree) + assert len(result[OUTPUT_KEY]) == 1 + assert result[OUTPUT_KEY].data["class_name"][0] == "item" + + +def test_overlap_run_no_overlap_at_all(): + """ + When neither bboxes nor masks overlap, the block correctly + reports no overlap. Baseline sanity test. + """ + image_h, image_w = 100, 100 + + xyxy = np.array( + [ + [0, 0, 30, 30], # container — top-left + [70, 70, 90, 90], # item — bottom-right, far away + ], + dtype=np.float32, + ) + + masks = np.zeros((2, image_h, image_w), dtype=bool) + masks[0, 0:30, 0:30] = True + masks[1, 70:90, 70:90] = True + + detections = _make_segmentation_detections( + xyxy=xyxy, masks=masks, class_names=["container", "item"] + ) + + block = OverlapBlockV1() + result = block.run( + predictions=detections, + overlap_type="Any Overlap", + overlap_class_name="container", + ) + + assert len(result[OUTPUT_KEY]) == 0