Repository navigation
Edge treatment "Decontaminate colors" fails every preview and render: the segmentation result is read-only #40
Description
Context
Selecting Edge treatment → Decontaminate colors makes every preview and every render fail. The
default edge treatment is unaffected, which is presumably why this has gone unnoticed.
Found on main's behaviour during manual testing on Windows, 2026-09-04. Unrelated to the transform
stage in #25 / #27 — the failing code is untouched by that branch.
The error does reach the user, in the banner and in the log:
Preview failed: preview failed: assignment destination is read-only
Current State (verified 2026-09-04)
Two deliberate decisions contradict each other:
| Where | What it does |
|---|---|
src/matteloop/jobs/segmentation_host.py:505 |
output.setflags(write=False) — the segmentation result is frozen before it is handed back |
src/matteloop/jobs/render.py:1581 |
_decontaminate_edge_colors_in_place(segmented) — writes into that array for this edge mode only |
Traceback, abbreviated:
File "src/matteloop/jobs/render.py", line 1554, in _produce_cut_frame
_decontaminate_edge_colors_in_place(segmented)
File "src/matteloop/jobs/render.py", line 1574, in _decontaminate_edge_colors_in_place
rgba[alpha == 0, :3] = 0
ValueError: assignment destination is read-only
_produce_cut_frame is the single decode/crop/segment path, so this hits preview and render alike.
The freeze is not an accident: the array is a copy out of the shared-memory slot the child process
writes into, and making it read-only is a reasonable guard against later mutation aliasing that
buffer.
Why no test caught it
The fake segmentation port in tests/jobs/test_preview.py:237 returns a freshly allocated
np.zeros((128, 128, 4), dtype=np.uint8) — writable. The real host returns a frozen array. Every
test that exercises DECONTAMINATE_COLORS therefore passes on a value the production path never
produces, and the divergence sits precisely on the property the code breaks on.
Files referencing the mode (tests/core/test_fingerprints.py, tests/core/test_parameters.py,
tests/jobs/test_preview.py, tests/ui/test_parameter_persistence.py,
tests/ui/test_parameter_requests.py) all cover plumbing — that the mode is stored, fingerprinted
and requested — not that it can run.
Proposed Change
The fix is a decision, not a one-liner, because both sides are intentional:
- Copy before writing — have
_produce_cut_framework on a writable copy when the mode is
active. Keeps the host's guarantee intact; costs one full-frame copy per frame, only for this
mode. - Return the array writable — drop
setflags(write=False)and rely on the copy already made
atsegmentation_host.py:509. Cheaper, but gives up a guard that presumably exists for a reason;
whoever added it should say whether that reason still holds. - Make the postprocess pure — return a new array instead of mutating, and drop
_in_placefrom
the name. Clearest contract, touches the most call sites.
(1) looks like the smallest correct change, but the choice belongs to whoever wrote the freeze.
Whichever is chosen, the fake needs to stop being more permissive than the real port: a fake that
returns a read-only array would have failed on the first run. That is the change that keeps this
class of bug from recurring.
Acceptance Criteria
- A preview with
EdgeMode.DECONTAMINATE_COLORSon a real segmentation host produces a frame
instead of raising. - A render with that mode completes and its output differs from the default mode's output on a
frame with translucent edges (i.e. the postprocess actually ran). - The segmentation fake used in tests returns a read-only array, matching the host, and the
existing tests still pass against it. uv run ruff check . && uv run mypy src && QT_QPA_PLATFORM=offscreen uv run pytest -qgreen,
plusuv run python scripts/check_guardrails.py.
Notes
How the mode got selected in the first place is worth recording: the "Edge treatment" combo box sits
in the middle of the inspector, and until #27 the mouse wheel changed the value of any field the
pointer passed over while scrolling the panel. The setting was almost certainly never chosen
deliberately — the same session also ended up with 11 fps and a 0.4 % alpha threshold the same way.
Activity
fix: scroll the inspector instead of editing the field under the pointer
sensitron commentedon Sep 5, 2026
#54 fixes the crash. Leaving this open: two of the four acceptance criteria
could not be checked on the machine the fix was built on.
Covered by #54
_produce_cut_framedecontaminates a writable copy (variant 1). The host'ssetflags(write=False)stays — that array is a copy out of the shared-memoryslot the child writes into, so the postprocess is the side that yields.
frozen_segmentation_result, and the existing tests pass against it. This isthe part that keeps the class of bug from recurring — the fakes were more
permissive than the real port on exactly the property the code broke on.
scripts/check_guardrails.pyand pytest green(1783 passed, 4 skipped).
The trigger was
tests/jobs/test_preview.py::test_decontaminate_is_deterministic_local_rgba_postprocess,failing with
ValueError: assignment destination is read-onlyatrender.py:1603. Both it and the new render-level test were confirmed to failon the unfixed code and pass with the fix.
Still open — needs Windows, real weights, real footage
EdgeMode.DECONTAMINATE_COLORSon a real segmentationhost produces a frame instead of raising.
default mode's on a frame with translucent edges.
These were deliberately not attempted rather than approximated. Note that the
green
windowsCI job does not cover them: it runs the same suite againstthe same fakes, without model weights or video. AC 2 is covered in spirit by a
new test (two renders of one frozen frame under
STANDARDandDECONTAMINATE_COLORSmust differ, and the segmenter's array must come backunwritten and still read-only), but that too runs against a fake.
Whoever does the manual check: the mode is reachable via Edge treatment in the
inspector, and a frame with hair or motion blur against a contrasting background
is the one that shows the difference.
sensitron commentedon Sep 5, 2026
Manual verification on Windows — AC 1 and AC 2
AC 3 and AC 4 were already covered. AC 1 and AC 2 need real weights and real footage, so
they were run by hand on Windows against
mainat b9a2535.The green
windowsCI job does not cover AC 1 or AC 2. It runs the same suite against thesame fakes, with no weights and no video. The check mark proves nothing here — which is why
this was done by hand.
Environment
b9a2535(main),matteloop 0.2.1onnxruntime-directml1.24.4('DmlExecutionProvider', 'CPUExecutionProvider')CPUExecutionProviderbirefnet-portrait, cache namespace2.0.75, 972 666 916 B on disk (matches the manifest)assets/demo/golden-retriever.mp4, 752×416, 145 frames, 24 fpsDirectML was deliberately not used. On this 4 GB GPU, 1024×1024 BiRefNet models fail inside
session.run()withDXGI_ERROR_DEVICE_HUNG(measured 2026-09-03). That is an unrelateddefect and would only have obscured this check. The DirectML path for this mode therefore
remains unverified.
Precondition: the freeze is still in place
Before anything else, one frame was pushed through a real
SegmentationClientand thereturned array inspected directly:
This matters: if the host had started handing back writable arrays, both criteria below would
pass for the wrong reason and prove nothing.
segmentation_host.py'soutput.setflags(write=False)is intact, so the fix is genuinely being exercised against thecondition that broke it.
AC 1 — preview with
DECONTAMINATE_COLORSon a real host: PASSEDA real
PreviewService.preview()against a realSegmentationClientsubprocess with the realbirefnet-portraitweights produced a frame. No exception, no banner.Independently confirmed in the GUI: the maintainer loaded the demo clip, set Edge treatment to
"Decontaminate colors" and triggered Preview Frame — the frame rendered. (Noted separately
because it is an observation at the screen, not a measurement; the app writes nothing at INFO
level that would corroborate it after the fact.)
AC 2 — render differs from the default mode: PASSED
Two real
RenderService.render()calls over the same cut, identical in every field exceptedge_mode, withregenerate=Trueand separate output directories.c2938bb6e7210ea0…3a0d9baf47aa7429…rebuiltFalseFalsesegment()callsNot byte-identical, and segmentation demonstrably ran fresh in both renders rather than being
served from a cut cache.
The pixel evidence is more specific than "the files differ". Decoding both WebPs and splitting
the difference by alpha class over all 8 frames:
0 < a < 255)a == 0)a == 255)Three things follow:
so the RGB difference is attributable to the postprocess alone and not to segmentation
noise. That is the control that makes this comparison meaningful.
a == 0group is thergba[alpha == 0, :3] = 0zeroing, invisible on its own but thereason the black premultiplication stops darkening the edge.
Composited over a light background the difference is also visible by eye: the standard render
carries a grey fringe along the silhouette that the decontaminated one does not. Side-by-side
crops and an amplified difference map were produced but are not attached — the numbers above
are the evidence, the pictures only illustrate them. Available on request.
What this does not cover
PreviewService/RenderServicedirectly. The Qt UI path iscovered only by the single manual preview click above; no GUI render was done.
Two side findings, unrelated to the fix but worth recording
uv sync --reinstall-package matteloopcan silently disarm ONNX Runtime. A strayonnxruntime1.29.0 was installed next toonnxruntime-directml1.24.4; both own the sameonnxruntimeimport package. Removing the former took the shared files with it, after whichmatteloop --providersreported:uv sync --reinstall-package onnxruntime-directmlrepairs it. Note that--providersdegrades to a text line here rather than failing loudly — the same shape as Segmentation is unreachable on main: the rembg version gate rejects the installed runtime #22: a broken
runtime that still looks like a working install.
The wheel-scroll values from Edge treatment "Decontaminate colors" fails every preview and render: the segmentation result is read-only #40's Notes are still persisted in
HKCU\Software\MatteLoop\MatteLoop\parameterson this machine:fps = 11,alpha_threshold = 0.4,stretch_x = 1.2,padding = 1. They survive restarts and nothingresets them, so a user who hit the scroll bug once keeps those settings indefinitely.
Also: validating the harness pulled
u2netp(4.5 MB) into the model cache under2.0.75.Not closing this — whether it is done is the maintainer's call.
sensitron commentedon Sep 5, 2026
The two side findings from the verification above are now tracked separately: #55 (
--providersreports a dead ONNX Runtime as ordinary output and exits 0) and #56 (inspector parameters persist with no reset, so the pre-#27 wheel-scroll damage is permanent). Neither blocks this issue.