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
75 changes: 63 additions & 12 deletions inference/core/workflows/core_steps/analytics/overlap/v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -144,36 +145,86 @@ 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())
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

(Claude)
Performance: full-image np.logical_and for "Any Overlap" (minor)

np.logical_and(overlap_mask, other_mask).any() allocates a full (H, W) intermediate boolean
array on every comparison. For large images (e.g., 4K = 3840x2160) with many detections, this can be
costly. A more efficient approach would be to restrict the comparison to the intersection of the two
bounding boxes:

Suggested change
return bool(np.logical_and(overlap_mask, other_mask).any())
x1 = max(overlap_bbox[0], other_bbox[0])
y1 = max(overlap_bbox[1], other_bbox[1])
x2 = min(overlap_bbox[2], other_bbox[2])
y2 = min(overlap_bbox[3], other_bbox[3])
if x1 >= x2 or y1 >= y2:
return False
return bool(np.logical_and(overlap_mask[y1:y2, x1:x2], other_mask[y1:y2, x1:x2]).any())

This will also require to pass extra param to this method, so signature will need to receive: overlap_bbox: list[int]
Then, it will also need predictions.xyxy[oi] passed when OverlapBlockV1.masks_overlap is called and some test assertions fixed
Would be nice to use named params when masks_overlap is called (though it's up to you)


def run(
self,
predictions: sv.Detections,
overlap_type: Literal["Center Overlap", "Any Overlap"],
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]}
Original file line number Diff line number Diff line change
@@ -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():
Expand All @@ -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