Open
Conversation
added 2 commits
March 15, 2026 12:57
Test all-zero input, all-opaque input, and idempotency of the connected-components core (dilation=0, blur_size=0). Documents that the default dilation+blur post-processing is not idempotent — each call expands the feathered edge of surviving regions — with a note in the clean_matte docstring.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
clean_matte silently produces different output each time it is called on the same matte at default settings. The dilation and Gaussian blur post-processing expand the feathered edge of surviving regions on every call, so there is no fixed point. A caller running clean_matte in a refinement loop or applying it twice to the same matte will get different results each pass without any warning. The function's docstring does not mention this.
If this behavior is intentional (ie. dilation and blur are one-shot operations meant to be applied once per frame in a batch pipeline) this PR simply adds a note to the docstring so callers are not surprised. If it is not intentional, I'm happy to propose a fix.
Demonstration
import numpy as np
from CorridorKeyModule.core import color_utils as cu
matte = np.zeros((100, 100), dtype=np.float32)
matte[20:80, 20:80] = 1.0 # large blob
matte[5:8, 5:8] = 1.0 # small blob (removed on first pass)
first = cu.clean_matte(matte.copy())
second = cu.clean_matte(first.copy())
print(np.allclose(first, second)) # False
print(np.max(np.abs(first - second))) # ~0.045
The connected-components filter alone is stable — calling with dilation=0, blur_size=0 is idempotent.
Impact
For the primary use case, one call per frame in a batch pipeline, this is harmless.
The latent risk is any workflow where a caller passes the output of clean_matte back into clean_matte a second time. This is not hypothetical: a refinement loop, a retry on a bad frame, or simply calling the function twice on the same matte will silently produce a different alpha. Each pass expands the feathered edge of the surviving region outward by the dilation radius and re-blurs it. The matte grows slightly larger and softer with every call.
In a compositor, a matte that drifts between passes means the green edge you pulled on the first pass is no longer the same edge on the second pass. The subject gets slightly fatter each time, and there is no error. The function returns successfully and the output looks plausible.
The demonstration above shows the maximum per-pixel difference after two passes is ~0.045, roughly 4.5% opacity. That is visible in a composite over a contrasting background.
Change
Adds a note to the clean_matte docstring and three boundary tests:
No production behavior is changed. No GPU or model weights required.
Checklist
uv run pytestpassesuv run ruff checkpassesuv run ruff format --checkpasses