Skip to content

Test/clean matte boundaries#177

Open
rocketmark wants to merge 2 commits intonikopueringer:mainfrom
rocketmark:test/clean-matte-boundaries
Open

Test/clean matte boundaries#177
rocketmark wants to merge 2 commits intonikopueringer:mainfrom
rocketmark:test/clean-matte-boundaries

Conversation

@rocketmark
Copy link
Contributor

@rocketmark rocketmark commented Mar 15, 2026

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:

  • All-zero matte — connected-components pass on an empty matte produces no spurious output
  • All-opaque matte — single full-frame component is above any reasonable threshold and is preserved
  • Idempotency (connected-components core) — stable at dilation=0, blur_size=0; the test docstring records the non-idempotency finding explicitly

No production behavior is changed. No GPU or model weights required.

Checklist

  • uv run pytest passes
  • uv run ruff check passes
  • uv run ruff format --check passes

Mark Stalzer 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant