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
5 changes: 5 additions & 0 deletions CorridorKeyModule/core/color_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,11 @@ def clean_matte(alpha_np: np.ndarray, area_threshold: int = 300, dilation: int =
"""
Cleans up small disconnected components (like tracking markers) from a predicted alpha matte.
alpha_np: Numpy array [H, W] or [H, W, 1] float (0.0 - 1.0)

Note: this function is not idempotent at default settings. The dilation and Gaussian blur
post-processing expand the feathered edge of surviving regions on each call, so running
clean_matte twice on the same matte produces slightly different output. The connected-
components filter alone (dilation=0, blur_size=0) is idempotent.
"""
# Needs to be 2D
is_3d = False
Expand Down
36 changes: 36 additions & 0 deletions tests/test_color_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,42 @@ def test_3d_input_preserved(self):
assert result.ndim == 3
assert result.shape[2] == 1

def test_all_zero_matte_returns_zeros(self):
"""An all-zero matte must return all zeros without crashing.

The connected-components pass on an empty matte must not produce
spurious output. Source: README 'Auto-Cleanup' boundary condition.
"""
matte = np.zeros((100, 100), dtype=np.float32)
result = cu.clean_matte(matte, area_threshold=300)
np.testing.assert_array_equal(result, np.zeros_like(matte))

def test_all_opaque_matte_preserved(self):
"""An all-opaque matte must be returned intact.

The single large component exceeds any reasonable threshold — cleanup
must not zero it out. Source: README 'Auto-Cleanup' boundary condition.
"""
matte = np.ones((100, 100), dtype=np.float32)
result = cu.clean_matte(matte, area_threshold=300)
assert result.min() > 0.9

def test_idempotent(self):
"""Running clean_matte twice produces the same result as running it once.

Tested with dilation=0 and blur_size=0 to isolate the connected-
components logic — the pure topological filter is the stable core.
Dilation + blur are not idempotent by design (they expand surviving
blobs, so a second pass would further expand feathered edges).
Source: README 'Auto-Cleanup' boundary condition.
"""
matte = np.zeros((100, 100), dtype=np.float32)
matte[20:80, 20:80] = 1.0 # large blob kept
matte[5:8, 5:8] = 1.0 # small blob removed on first pass
first = cu.clean_matte(matte.copy(), area_threshold=300, dilation=0, blur_size=0)
second = cu.clean_matte(first.copy(), area_threshold=300, dilation=0, blur_size=0)
np.testing.assert_allclose(first, second, atol=1e-6)


# ---------------------------------------------------------------------------
# create_checkerboard
Expand Down