9194ef5abd
Docs/Test Workflow / Test docs build (push) Failing after 0s
Check links & references / links-check (push) Failing after 1s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.10) (push) Failing after 0s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.11) (push) Failing after 0s
PR Conflict Labeler / main (push) Failing after 2s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.12) (push) Failing after 2s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.13) (push) Failing after 0s
Pytest/Test Workflow / Build this Package (push) Failing after 5s
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.10) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.11) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.12) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.13) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.10) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.11) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.12) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.13) (push) Has been cancelled
Pytest/Test Workflow / testing-guardian (push) Has been cancelled
887 lines
27 KiB
Python
887 lines
27 KiB
Python
from contextlib import ExitStack as DoesNotRaise
|
|
|
|
import numpy as np
|
|
import numpy.typing as npt
|
|
import pytest
|
|
|
|
from supervision.detection.compact_mask import CompactMask
|
|
from supervision.detection.utils.masks import (
|
|
_compact_masks_to_roi,
|
|
_mask_to_roi,
|
|
_masks_to_roi,
|
|
calculate_masks_centroids,
|
|
contains_holes,
|
|
contains_multiple_segments,
|
|
filter_segments_by_distance,
|
|
mask_to_roi,
|
|
move_masks,
|
|
resize_masks,
|
|
)
|
|
|
|
|
|
class TestMaskROIHelpers:
|
|
"""Tests for _mask_to_roi, _compact_masks_to_roi, _masks_to_roi helpers."""
|
|
|
|
def test_mask_to_roi_public_helper_exposes_exclusive_bounds(self) -> None:
|
|
"""Public mask_to_roi should return slice-friendly exclusive bounds."""
|
|
mask = np.zeros((10, 15), dtype=bool)
|
|
mask[3, 5] = True
|
|
assert mask_to_roi(mask) == (5, 3, 6, 4)
|
|
|
|
def test_mask_to_roi_all_false_returns_none(self):
|
|
"""All-false mask should return None."""
|
|
mask = np.zeros((10, 15), dtype=bool)
|
|
assert _mask_to_roi(mask) is None
|
|
|
|
def test_mask_to_roi_single_pixel_exclusive_bounds(self):
|
|
"""Single true pixel at (row=3, col=5) gives bounds (5, 3, 6, 4)."""
|
|
mask = np.zeros((10, 15), dtype=bool)
|
|
mask[3, 5] = True
|
|
assert _mask_to_roi(mask) == (5, 3, 6, 4)
|
|
|
|
def test_mask_to_roi_boundary_row_col_zero(self):
|
|
"""True pixel at top-left boundary should give (0, 0, 1, 1)."""
|
|
mask = np.zeros((10, 15), dtype=bool)
|
|
mask[0, 0] = True
|
|
assert _mask_to_roi(mask) == (0, 0, 1, 1)
|
|
|
|
def test_mask_to_roi_full_image(self):
|
|
"""Full-image true mask should span the entire array."""
|
|
h, w = 8, 12
|
|
mask = np.ones((h, w), dtype=bool)
|
|
assert _mask_to_roi(mask) == (0, 0, w, h)
|
|
|
|
def test_mask_to_roi_region(self):
|
|
"""Region [10:20, 15:25] in 80x90 mask gives exclusive bounds (15,10,25,20)."""
|
|
mask = np.zeros((80, 90), dtype=bool)
|
|
mask[10:20, 15:25] = True
|
|
assert _mask_to_roi(mask) == (15, 10, 25, 20)
|
|
|
|
def test_compact_masks_to_roi_empty_returns_none(self):
|
|
"""Zero-length CompactMask should return None."""
|
|
masks = np.zeros((0, 10, 10), dtype=bool)
|
|
xyxy = np.empty((0, 4), dtype=np.float32)
|
|
cm = CompactMask.from_dense(masks, xyxy, image_shape=(10, 10))
|
|
assert _compact_masks_to_roi(cm, (10, 10)) is None
|
|
|
|
def test_compact_masks_to_roi_single_detection_coordinates(self):
|
|
"""Single compact mask with known crop should give correct exclusive bounds."""
|
|
masks = np.zeros((1, 20, 20), dtype=bool)
|
|
masks[0, 5:10, 3:8] = True
|
|
xyxy = np.array([[3.0, 5.0, 7.0, 9.0]], dtype=np.float32)
|
|
cm = CompactMask.from_dense(masks, xyxy, image_shape=(20, 20))
|
|
result = _compact_masks_to_roi(cm, (20, 20))
|
|
assert result is not None
|
|
x1, y1, x2, y2 = result
|
|
assert x1 <= 3
|
|
assert y1 <= 5
|
|
assert x2 >= 8
|
|
assert y2 >= 10
|
|
|
|
def test_compact_masks_to_roi_two_detections_union(self):
|
|
"""Two disjoint compact masks union should span both regions."""
|
|
masks = np.zeros((2, 30, 40), dtype=bool)
|
|
masks[0, 2:8, 1:6] = True
|
|
masks[1, 15:25, 20:35] = True
|
|
xyxy = np.array(
|
|
[[1.0, 2.0, 5.0, 7.0], [20.0, 15.0, 34.0, 24.0]], dtype=np.float32
|
|
)
|
|
cm = CompactMask.from_dense(masks, xyxy, image_shape=(30, 40))
|
|
result = _compact_masks_to_roi(cm, (30, 40))
|
|
assert result is not None
|
|
x1, y1, x2, y2 = result
|
|
assert x1 <= 1
|
|
assert y1 <= 2
|
|
assert x2 >= 35
|
|
assert y2 >= 25
|
|
|
|
def test_masks_to_roi_empty_array_returns_none(self):
|
|
"""Zero-element dense mask array (N=0) should return None."""
|
|
masks = np.zeros((0, 10, 10), dtype=bool)
|
|
assert _masks_to_roi(masks, (10, 10)) is None
|
|
|
|
def test_masks_to_roi_2d_array(self):
|
|
"""2D boolean array (single mask) should work as union mask."""
|
|
mask = np.zeros((10, 15), dtype=bool)
|
|
mask[3:6, 7:11] = True
|
|
assert _masks_to_roi(mask, (10, 15)) == (7, 3, 11, 6)
|
|
|
|
def test_masks_to_roi_dense_with_xyxy_uses_box_union(self):
|
|
"""Dense path with xyxy should return union of boxes (O(N) path)."""
|
|
masks = np.zeros((2, 30, 40), dtype=bool)
|
|
masks[0, 5:10, 5:10] = True
|
|
masks[1, 20:25, 25:30] = True
|
|
xyxy = np.array([[5.0, 5.0, 9.0, 9.0], [25.0, 20.0, 29.0, 24.0]])
|
|
result = _masks_to_roi(masks, (30, 40), xyxy)
|
|
assert result is not None
|
|
x1, y1, x2, y2 = result
|
|
assert x1 <= 5
|
|
assert y1 <= 5
|
|
assert x2 >= 30
|
|
assert y2 >= 25
|
|
|
|
def test_masks_to_roi_dense_with_xyxy_loose_box_returns_box_union(self):
|
|
"""Loose xyxy (larger than pixel region) returns box union, not tight bounds."""
|
|
masks = np.zeros((1, 30, 40), dtype=bool)
|
|
masks[0, 10:12, 10:12] = True # tiny 2x2 pixel region
|
|
# box is much larger than the pixel region
|
|
xyxy = np.array([[2.0, 2.0, 20.0, 20.0]])
|
|
result = _masks_to_roi(masks, (30, 40), xyxy)
|
|
assert result is not None
|
|
x1, y1, x2, y2 = result
|
|
# fast-path returns box union (conservative bound), not tight pixel bound
|
|
assert x1 <= 2
|
|
assert y1 <= 2
|
|
assert x2 >= 21 # floor(20.0) + 1
|
|
assert y2 >= 21
|
|
|
|
def test_masks_to_roi_dense_with_xyxy_falls_back_for_pixels_outside_box(self):
|
|
"""Dense xyxy path scans pixels when true pixels fall outside the box union."""
|
|
masks = np.zeros((1, 30, 40), dtype=bool)
|
|
masks[0, 10:12, 10:12] = True
|
|
masks[0, 25:27, 30:32] = True
|
|
xyxy = np.array([[10.0, 10.0, 11.0, 11.0]])
|
|
|
|
result = _masks_to_roi(masks, (30, 40), xyxy)
|
|
|
|
assert result == (10, 10, 32, 27)
|
|
|
|
def test_masks_to_roi_dense_with_xyxy_all_false_returns_none(self):
|
|
"""All-false masks with xyxy provided should still return None."""
|
|
masks = np.zeros((2, 30, 40), dtype=bool)
|
|
xyxy = np.array([[5.0, 5.0, 20.0, 20.0], [10.0, 10.0, 25.0, 25.0]])
|
|
result = _masks_to_roi(masks, (30, 40), xyxy)
|
|
assert result is None
|
|
|
|
|
|
def test_resize_masks_does_not_upscale_small_masks() -> None:
|
|
"""resize_masks returns small masks unchanged instead of upscaling."""
|
|
masks = np.zeros((2, 4, 5), dtype=bool)
|
|
masks[:, 1:3, 2:4] = True
|
|
|
|
result = resize_masks(masks, max_dimension=640)
|
|
|
|
assert result is masks
|
|
assert result.shape == (2, 4, 5)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("masks", "offset", "resolution_wh", "expected_result", "exception"),
|
|
[
|
|
(
|
|
np.array(
|
|
[
|
|
[
|
|
[False, False, False, False],
|
|
[False, True, True, False],
|
|
[False, True, True, False],
|
|
[False, False, False, False],
|
|
]
|
|
],
|
|
dtype=bool,
|
|
),
|
|
np.array([0, 0]),
|
|
(4, 4),
|
|
np.array(
|
|
[
|
|
[
|
|
[False, False, False, False],
|
|
[False, True, True, False],
|
|
[False, True, True, False],
|
|
[False, False, False, False],
|
|
]
|
|
],
|
|
dtype=bool,
|
|
),
|
|
DoesNotRaise(),
|
|
),
|
|
(
|
|
np.array(
|
|
[
|
|
[
|
|
[False, False, False, False],
|
|
[False, True, True, False],
|
|
[False, True, True, False],
|
|
[False, False, False, False],
|
|
]
|
|
],
|
|
dtype=bool,
|
|
),
|
|
np.array([-1, -1]),
|
|
(4, 4),
|
|
np.array(
|
|
[
|
|
[
|
|
[True, True, False, False],
|
|
[True, True, False, False],
|
|
[False, False, False, False],
|
|
[False, False, False, False],
|
|
]
|
|
],
|
|
dtype=bool,
|
|
),
|
|
DoesNotRaise(),
|
|
),
|
|
(
|
|
np.array(
|
|
[
|
|
[
|
|
[False, False, False, False],
|
|
[False, True, True, False],
|
|
[False, True, True, False],
|
|
[False, False, False, False],
|
|
]
|
|
],
|
|
dtype=bool,
|
|
),
|
|
np.array([-2, -2]),
|
|
(4, 4),
|
|
np.array(
|
|
[
|
|
[
|
|
[True, False, False, False],
|
|
[False, False, False, False],
|
|
[False, False, False, False],
|
|
[False, False, False, False],
|
|
]
|
|
],
|
|
dtype=bool,
|
|
),
|
|
DoesNotRaise(),
|
|
),
|
|
(
|
|
np.array(
|
|
[
|
|
[
|
|
[False, False, False, False],
|
|
[False, True, True, False],
|
|
[False, True, True, False],
|
|
[False, False, False, False],
|
|
]
|
|
],
|
|
dtype=bool,
|
|
),
|
|
np.array([-3, -3]),
|
|
(4, 4),
|
|
np.array(
|
|
[
|
|
[
|
|
[False, False, False, False],
|
|
[False, False, False, False],
|
|
[False, False, False, False],
|
|
[False, False, False, False],
|
|
]
|
|
],
|
|
dtype=bool,
|
|
),
|
|
DoesNotRaise(),
|
|
),
|
|
(
|
|
np.array(
|
|
[
|
|
[
|
|
[False, False, False, False],
|
|
[False, True, True, False],
|
|
[False, True, True, False],
|
|
[False, False, False, False],
|
|
]
|
|
],
|
|
dtype=bool,
|
|
),
|
|
np.array([-2, -1]),
|
|
(4, 4),
|
|
np.array(
|
|
[
|
|
[
|
|
[True, False, False, False],
|
|
[True, False, False, False],
|
|
[False, False, False, False],
|
|
[False, False, False, False],
|
|
]
|
|
],
|
|
dtype=bool,
|
|
),
|
|
DoesNotRaise(),
|
|
),
|
|
(
|
|
np.array(
|
|
[
|
|
[
|
|
[False, False, False, False],
|
|
[False, True, True, False],
|
|
[False, True, True, False],
|
|
[False, False, False, False],
|
|
]
|
|
],
|
|
dtype=bool,
|
|
),
|
|
np.array([-1, -2]),
|
|
(4, 4),
|
|
np.array(
|
|
[
|
|
[
|
|
[True, True, False, False],
|
|
[False, False, False, False],
|
|
[False, False, False, False],
|
|
[False, False, False, False],
|
|
]
|
|
],
|
|
dtype=bool,
|
|
),
|
|
DoesNotRaise(),
|
|
),
|
|
(
|
|
np.array(
|
|
[
|
|
[
|
|
[False, False, False, False],
|
|
[False, True, True, False],
|
|
[False, True, True, False],
|
|
[False, False, False, False],
|
|
]
|
|
],
|
|
dtype=bool,
|
|
),
|
|
np.array([-2, 2]),
|
|
(4, 4),
|
|
np.array(
|
|
[
|
|
[
|
|
[False, False, False, False],
|
|
[False, False, False, False],
|
|
[False, False, False, False],
|
|
[True, False, False, False],
|
|
]
|
|
],
|
|
dtype=bool,
|
|
),
|
|
DoesNotRaise(),
|
|
),
|
|
(
|
|
np.array(
|
|
[
|
|
[
|
|
[False, False, False, False],
|
|
[False, True, True, False],
|
|
[False, True, True, False],
|
|
[False, False, False, False],
|
|
]
|
|
],
|
|
dtype=bool,
|
|
),
|
|
np.array([3, 3]),
|
|
(4, 4),
|
|
np.array(
|
|
[
|
|
[
|
|
[False, False, False, False],
|
|
[False, False, False, False],
|
|
[False, False, False, False],
|
|
[False, False, False, False],
|
|
]
|
|
],
|
|
dtype=bool,
|
|
),
|
|
DoesNotRaise(),
|
|
),
|
|
(
|
|
np.array(
|
|
[
|
|
[
|
|
[False, False, False, False],
|
|
[False, True, True, False],
|
|
[False, True, True, False],
|
|
[False, False, False, False],
|
|
]
|
|
],
|
|
dtype=bool,
|
|
),
|
|
np.array([3, 3]),
|
|
(6, 6),
|
|
np.array(
|
|
[
|
|
[
|
|
[False, False, False, False, False, False],
|
|
[False, False, False, False, False, False],
|
|
[False, False, False, False, False, False],
|
|
[False, False, False, False, False, False],
|
|
[False, False, False, False, True, True],
|
|
[False, False, False, False, True, True],
|
|
]
|
|
],
|
|
dtype=bool,
|
|
),
|
|
DoesNotRaise(),
|
|
),
|
|
],
|
|
)
|
|
def test_move_masks(
|
|
masks: np.ndarray,
|
|
offset: np.ndarray,
|
|
resolution_wh: tuple[int, int],
|
|
expected_result: np.ndarray,
|
|
exception: Exception,
|
|
) -> None:
|
|
with exception:
|
|
result = move_masks(masks=masks, offset=offset, resolution_wh=resolution_wh)
|
|
np.testing.assert_array_equal(result, expected_result)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("masks", "expected_result", "exception"),
|
|
[
|
|
(
|
|
np.array(
|
|
[
|
|
[
|
|
[0, 0, 0, 0],
|
|
[0, 0, 0, 0],
|
|
[0, 0, 0, 0],
|
|
[0, 0, 0, 0],
|
|
]
|
|
]
|
|
),
|
|
np.array([[0, 0]]),
|
|
DoesNotRaise(),
|
|
), # single mask with all zeros
|
|
(
|
|
np.array(
|
|
[
|
|
[
|
|
[1, 1, 1, 1],
|
|
[1, 1, 1, 1],
|
|
[1, 1, 1, 1],
|
|
[1, 1, 1, 1],
|
|
]
|
|
]
|
|
),
|
|
np.array([[2, 2]]),
|
|
DoesNotRaise(),
|
|
), # single mask with all ones
|
|
(
|
|
np.array(
|
|
[
|
|
[
|
|
[0, 1, 1, 0],
|
|
[1, 1, 1, 1],
|
|
[1, 1, 1, 1],
|
|
[0, 1, 1, 0],
|
|
]
|
|
]
|
|
),
|
|
np.array([[2, 2]]),
|
|
DoesNotRaise(),
|
|
), # single mask with symmetric ones
|
|
(
|
|
np.array(
|
|
[
|
|
[
|
|
[0, 0, 0, 0],
|
|
[0, 0, 1, 1],
|
|
[0, 0, 1, 1],
|
|
[0, 0, 0, 0],
|
|
]
|
|
]
|
|
),
|
|
np.array([[3, 2]]),
|
|
DoesNotRaise(),
|
|
), # single mask with asymmetric ones
|
|
(
|
|
np.array(
|
|
[
|
|
[
|
|
[0, 1, 1, 0],
|
|
[1, 1, 1, 1],
|
|
[1, 1, 1, 1],
|
|
[0, 1, 1, 0],
|
|
],
|
|
[
|
|
[0, 0, 0, 0],
|
|
[0, 0, 1, 1],
|
|
[0, 0, 1, 1],
|
|
[0, 0, 0, 0],
|
|
],
|
|
]
|
|
),
|
|
np.array([[2, 2], [3, 2]]),
|
|
DoesNotRaise(),
|
|
), # two masks
|
|
],
|
|
)
|
|
def test_calculate_masks_centroids(
|
|
masks: np.ndarray,
|
|
expected_result: np.ndarray,
|
|
exception: Exception,
|
|
) -> None:
|
|
with exception:
|
|
result = calculate_masks_centroids(masks=masks)
|
|
assert np.array_equal(result, expected_result)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("mask", "expected_result", "exception"),
|
|
[
|
|
(
|
|
np.array([[0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 0, 0], [0, 1, 1, 0]]).astype(
|
|
bool
|
|
),
|
|
False,
|
|
DoesNotRaise(),
|
|
), # foreground object in one continuous piece
|
|
(
|
|
np.array([[1, 0, 0, 0], [1, 0, 0, 0], [0, 0, 0, 0], [0, 1, 1, 0]]).astype(
|
|
bool
|
|
),
|
|
False,
|
|
DoesNotRaise(),
|
|
), # foreground object in 2 separate elements
|
|
(
|
|
np.array([[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]).astype(
|
|
bool
|
|
),
|
|
False,
|
|
DoesNotRaise(),
|
|
), # no foreground pixels in mask
|
|
(
|
|
np.array([[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]]).astype(
|
|
bool
|
|
),
|
|
False,
|
|
DoesNotRaise(),
|
|
), # only foreground pixels in mask
|
|
(
|
|
np.array([[1, 1, 1, 0], [1, 0, 1, 0], [1, 1, 1, 0], [0, 0, 0, 0]]).astype(
|
|
bool
|
|
),
|
|
True,
|
|
DoesNotRaise(),
|
|
), # foreground object has 1 hole
|
|
(
|
|
np.array([[1, 1, 1, 0], [1, 0, 1, 1], [1, 1, 0, 1], [0, 1, 1, 1]]).astype(
|
|
bool
|
|
),
|
|
True,
|
|
DoesNotRaise(),
|
|
), # foreground object has 2 holes
|
|
],
|
|
)
|
|
def test_contains_holes(
|
|
mask: npt.NDArray[np.bool_], expected_result: bool, exception: Exception
|
|
) -> None:
|
|
with exception:
|
|
result = contains_holes(mask)
|
|
assert result == expected_result
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("mask", "connectivity", "expected_result", "exception"),
|
|
[
|
|
(
|
|
np.array([[0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 0, 0], [0, 1, 1, 0]]).astype(
|
|
bool
|
|
),
|
|
4,
|
|
False,
|
|
DoesNotRaise(),
|
|
), # foreground object in one continuous piece
|
|
(
|
|
np.array([[1, 0, 0, 0], [1, 0, 0, 0], [0, 0, 0, 0], [0, 1, 1, 0]]).astype(
|
|
bool
|
|
),
|
|
4,
|
|
True,
|
|
DoesNotRaise(),
|
|
), # foreground object in 2 separate elements
|
|
(
|
|
np.array([[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]).astype(
|
|
bool
|
|
),
|
|
4,
|
|
False,
|
|
DoesNotRaise(),
|
|
), # no foreground pixels in mask
|
|
(
|
|
np.array([[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]]).astype(
|
|
bool
|
|
),
|
|
4,
|
|
False,
|
|
DoesNotRaise(),
|
|
), # only foreground pixels in mask
|
|
(
|
|
np.array([[1, 1, 1, 0], [1, 0, 1, 1], [1, 1, 0, 1], [0, 1, 1, 1]]).astype(
|
|
bool
|
|
),
|
|
4,
|
|
False,
|
|
DoesNotRaise(),
|
|
), # foreground object has 2 holes, but is in single piece
|
|
(
|
|
np.array([[1, 1, 0, 0], [1, 1, 0, 1], [1, 0, 1, 1], [0, 0, 1, 1]]).astype(
|
|
bool
|
|
),
|
|
4,
|
|
True,
|
|
DoesNotRaise(),
|
|
), # foreground object in 2 elements with respect to 4-way connectivity
|
|
(
|
|
np.array([[1, 1, 0, 0], [1, 1, 0, 1], [1, 0, 1, 1], [0, 0, 1, 1]]).astype(
|
|
bool
|
|
),
|
|
8,
|
|
False,
|
|
DoesNotRaise(),
|
|
), # foreground object in single piece with respect to 8-way connectivity
|
|
(
|
|
np.array([[1, 1, 0, 0], [1, 1, 0, 1], [1, 0, 1, 1], [0, 0, 1, 1]]).astype(
|
|
bool
|
|
),
|
|
5,
|
|
None,
|
|
pytest.raises(ValueError, match="Incorrect connectivity value"),
|
|
), # Incorrect connectivity parameter value, raises ValueError
|
|
],
|
|
)
|
|
def test_contains_multiple_segments(
|
|
mask: npt.NDArray[np.bool_],
|
|
connectivity: int,
|
|
expected_result: bool,
|
|
exception: Exception,
|
|
) -> None:
|
|
with exception:
|
|
result = contains_multiple_segments(mask=mask, connectivity=connectivity)
|
|
assert result == expected_result
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
(
|
|
"mask",
|
|
"connectivity",
|
|
"mode",
|
|
"absolute_distance",
|
|
"relative_distance",
|
|
"expected_result",
|
|
"exception",
|
|
),
|
|
[
|
|
# single component, unchanged
|
|
(
|
|
np.array(
|
|
[
|
|
[0, 0, 0, 0, 0, 0],
|
|
[0, 1, 1, 1, 0, 0],
|
|
[0, 1, 1, 1, 0, 0],
|
|
[0, 1, 1, 1, 0, 0],
|
|
[0, 0, 0, 0, 0, 0],
|
|
[0, 0, 0, 0, 0, 0],
|
|
],
|
|
dtype=bool,
|
|
),
|
|
8,
|
|
"edge",
|
|
2.0,
|
|
None,
|
|
np.array(
|
|
[
|
|
[0, 0, 0, 0, 0, 0],
|
|
[0, 1, 1, 1, 0, 0],
|
|
[0, 1, 1, 1, 0, 0],
|
|
[0, 1, 1, 1, 0, 0],
|
|
[0, 0, 0, 0, 0, 0],
|
|
[0, 0, 0, 0, 0, 0],
|
|
],
|
|
dtype=bool,
|
|
),
|
|
DoesNotRaise(),
|
|
),
|
|
# two components, edge distance 2, kept with abs=1
|
|
(
|
|
np.array(
|
|
[
|
|
[0, 0, 0, 0, 0, 0],
|
|
[0, 1, 1, 1, 0, 1],
|
|
[0, 1, 1, 1, 0, 1],
|
|
[0, 1, 1, 1, 0, 1],
|
|
[0, 0, 0, 0, 0, 0],
|
|
[0, 0, 0, 0, 0, 0],
|
|
],
|
|
dtype=bool,
|
|
),
|
|
8,
|
|
"edge",
|
|
2.0,
|
|
None,
|
|
np.array(
|
|
[
|
|
[0, 0, 0, 0, 0, 0],
|
|
[0, 1, 1, 1, 0, 1],
|
|
[0, 1, 1, 1, 0, 1],
|
|
[0, 1, 1, 1, 0, 1],
|
|
[0, 0, 0, 0, 0, 0],
|
|
[0, 0, 0, 0, 0, 0],
|
|
],
|
|
dtype=bool,
|
|
),
|
|
DoesNotRaise(),
|
|
),
|
|
# centroid mode, far centroids, dropped with small relative threshold
|
|
(
|
|
np.array(
|
|
[
|
|
[1, 1, 1, 0, 0, 0],
|
|
[1, 1, 1, 0, 0, 0],
|
|
[1, 1, 1, 0, 0, 0],
|
|
[0, 0, 0, 0, 0, 0],
|
|
[0, 0, 0, 1, 1, 1],
|
|
[0, 0, 0, 1, 1, 1],
|
|
],
|
|
dtype=bool,
|
|
),
|
|
8,
|
|
"centroid",
|
|
None,
|
|
0.3, # diagonal ~8.49, threshold ~2.55, centroid gap ~4.24
|
|
np.array(
|
|
[
|
|
[1, 1, 1, 0, 0, 0],
|
|
[1, 1, 1, 0, 0, 0],
|
|
[1, 1, 1, 0, 0, 0],
|
|
[0, 0, 0, 0, 0, 0],
|
|
[0, 0, 0, 0, 0, 0],
|
|
[0, 0, 0, 0, 0, 0],
|
|
],
|
|
dtype=bool,
|
|
),
|
|
DoesNotRaise(),
|
|
),
|
|
# centroid mode, larger relative threshold, kept
|
|
(
|
|
np.array(
|
|
[
|
|
[1, 1, 1, 0, 0, 0],
|
|
[1, 1, 1, 0, 0, 0],
|
|
[1, 1, 1, 0, 0, 0],
|
|
[0, 0, 0, 0, 0, 0],
|
|
[0, 0, 0, 1, 1, 1],
|
|
[0, 0, 0, 1, 1, 1],
|
|
],
|
|
dtype=bool,
|
|
),
|
|
8,
|
|
"centroid",
|
|
None,
|
|
0.6, # diagonal ~8.49, threshold ~5.09, centroid gap ~4.24
|
|
np.array(
|
|
[
|
|
[1, 1, 1, 0, 0, 0],
|
|
[1, 1, 1, 0, 0, 0],
|
|
[1, 1, 1, 0, 0, 0],
|
|
[0, 0, 0, 0, 0, 0],
|
|
[0, 0, 0, 1, 1, 1],
|
|
[0, 0, 0, 1, 1, 1],
|
|
],
|
|
dtype=bool,
|
|
),
|
|
DoesNotRaise(),
|
|
),
|
|
# empty mask
|
|
(
|
|
np.zeros((4, 4), dtype=bool),
|
|
4,
|
|
"edge",
|
|
2.0,
|
|
None,
|
|
np.zeros((4, 4), dtype=bool),
|
|
DoesNotRaise(),
|
|
),
|
|
# full mask
|
|
(
|
|
np.ones((4, 4), dtype=bool),
|
|
8,
|
|
"centroid",
|
|
None,
|
|
0.2,
|
|
np.ones((4, 4), dtype=bool),
|
|
DoesNotRaise(),
|
|
),
|
|
# two components, pixel distance = 2, kept with abs=2
|
|
(
|
|
np.array(
|
|
[
|
|
[0, 0, 0, 0, 0, 0, 0, 0],
|
|
[0, 1, 1, 1, 0, 1, 1, 1],
|
|
[0, 1, 1, 1, 0, 1, 1, 1],
|
|
[0, 1, 1, 1, 0, 1, 1, 1],
|
|
[0, 0, 0, 0, 0, 0, 0, 0],
|
|
[0, 0, 0, 0, 0, 0, 0, 0],
|
|
],
|
|
dtype=bool,
|
|
),
|
|
8,
|
|
"edge",
|
|
2.0, # was 1.0
|
|
None,
|
|
np.array(
|
|
[
|
|
[0, 0, 0, 0, 0, 0, 0, 0],
|
|
[0, 1, 1, 1, 0, 1, 1, 1],
|
|
[0, 1, 1, 1, 0, 1, 1, 1],
|
|
[0, 1, 1, 1, 0, 1, 1, 1],
|
|
[0, 0, 0, 0, 0, 0, 0, 0],
|
|
[0, 0, 0, 0, 0, 0, 0, 0],
|
|
],
|
|
dtype=bool,
|
|
),
|
|
DoesNotRaise(),
|
|
),
|
|
# two components, pixel distance = 3, dropped with abs=2
|
|
(
|
|
np.array(
|
|
[
|
|
[0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
[0, 1, 1, 1, 0, 0, 0, 1, 1],
|
|
[0, 1, 1, 1, 0, 0, 0, 1, 1],
|
|
[0, 1, 1, 1, 0, 0, 0, 1, 1],
|
|
[0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
[0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
],
|
|
dtype=bool,
|
|
),
|
|
8,
|
|
"edge",
|
|
2.0, # keep threshold below 3 so the right blob is removed
|
|
None,
|
|
np.array(
|
|
[
|
|
[0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
[0, 1, 1, 1, 0, 0, 0, 0, 0],
|
|
[0, 1, 1, 1, 0, 0, 0, 0, 0],
|
|
[0, 1, 1, 1, 0, 0, 0, 0, 0],
|
|
[0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
[0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
],
|
|
dtype=bool,
|
|
),
|
|
DoesNotRaise(),
|
|
),
|
|
],
|
|
)
|
|
def test_filter_segments_by_distance_sweep(
|
|
mask: npt.NDArray,
|
|
connectivity: int,
|
|
mode: str,
|
|
absolute_distance: float | None,
|
|
relative_distance: float | None,
|
|
expected_result: npt.NDArray | None,
|
|
exception: Exception,
|
|
) -> None:
|
|
with exception:
|
|
result = filter_segments_by_distance(
|
|
mask=mask,
|
|
connectivity=connectivity,
|
|
mode=mode, # type: ignore[arg-type]
|
|
absolute_distance=absolute_distance,
|
|
relative_distance=relative_distance,
|
|
)
|
|
assert np.array_equal(result, expected_result)
|