Skip to content
Merged
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
2 changes: 1 addition & 1 deletion scripts/guardrails-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"long_functions": 1
},
"src/matteloop/jobs/render.py": {
"lines": 2871,
"lines": 2872,
"long_functions": 11
},
"src/matteloop/jobs/segmentation_host.py": {
Expand Down
1 change: 1 addition & 0 deletions src/matteloop/jobs/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -1580,6 +1580,7 @@ def _produce_cut_frame(
)
tracker.register(segmented)
if request.segmentation.edge_mode is EdgeMode.DECONTAMINATE_COLORS:
segmented = tracker.register(segmented.copy())
_decontaminate_edge_colors_in_place(segmented)
result = Image.frombytes(
"RGBA",
Expand Down
8 changes: 7 additions & 1 deletion tests/jobs/render_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ def decode(
self._explode()


def frozen_segmentation_result(result: np.ndarray) -> np.ndarray:
"""Match SegmentationHost: hand segmentation output back read-only."""
result.setflags(write=False)
return result


class FakeSegmenter:
def __init__(self) -> None:
self.calls: list[SegmentRequest] = []
Expand All @@ -119,7 +125,7 @@ def segment(self, frame: np.ndarray, request: SegmentRequest) -> np.ndarray:
result = np.zeros(frame.shape[:2] + (4,), dtype=np.uint8)
result[24:104, 32:96, :3] = frame[24:104, 32:96, :3]
result[24:104, 32:96, 3] = 255
return result
return frozen_segmentation_result(result)


class FakeEncoder:
Expand Down
3 changes: 2 additions & 1 deletion tests/jobs/test_preview.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
FakeSegmenter,
FakeSource,
binding,
frozen_segmentation_result,
job,
render_service,
request,
Expand Down Expand Up @@ -238,7 +239,7 @@ def segment(self, frame, request):
result[0, 0] = (10, 20, 30, 0)
result[0, 1] = (25, 50, 100, 128)
result[0, 2] = (7, 8, 9, 255)
return result
return frozen_segmentation_result(result)

segmenter = EdgeSegmenter()
service = PreviewService(
Expand Down
47 changes: 45 additions & 2 deletions tests/jobs/test_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@
from matteloop.core.specs import (
CollisionPolicy,
CropSpec,
EdgeMode,
FramingSpec,
OutputSpec,
SamplingSpec,
SegmentationSpec,
TransformSpec,
)
from matteloop.core.state import JobKind
Expand Down Expand Up @@ -51,6 +53,7 @@
FakeEncoder,
FakeSegmenter,
FakeSource,
frozen_segmentation_result,
job,
render_service,
request,
Expand Down Expand Up @@ -1204,6 +1207,44 @@ def test_render_encodes_and_validates_a_real_lossless_webp(tmp_path) -> None:
assert artifact.ownership_current == 0


def test_decontaminate_edge_mode_renders_without_mutating_frozen_segmentation(
tmp_path,
) -> None:
import numpy as np

frame = np.zeros((128, 128, 4), dtype=np.uint8)
frame[0, 0] = (200, 150, 50, 255) # opaque
frame[1, 1] = (100, 75, 30, 128) # translucent, black-premultiplied
frame[2, 2] = (10, 20, 30, 0) # fully transparent, non-zero RGB
frozen = frozen_segmentation_result(frame)
original = frozen.copy()

class FixedSegmenter(FakeSegmenter):
def segment(self, frame, request):
self.calls.append(request)
return frozen

standard_artifact = render_service(segmenter=FixedSegmenter()).render(
request(tmp_path), job(tmp_path, "standard-run", JobKind.RENDER)
)
standard_bytes = standard_artifact.output_path.read_bytes()

decontaminate_request = request(
tmp_path,
segmentation=SegmentationSpec(
"birefnet-portrait", EdgeMode.DECONTAMINATE_COLORS
),
)
decontaminate_artifact = render_service(segmenter=FixedSegmenter()).render(
decontaminate_request, job(tmp_path, "decontaminate-run", JobKind.RENDER)
)
decontaminate_bytes = decontaminate_artifact.output_path.read_bytes()

assert standard_bytes != decontaminate_bytes
assert frozen.flags.writeable is False
assert np.array_equal(frozen, original)


def test_render_keeps_decode_working_set_bounded_across_60_frames(tmp_path) -> None:
"""No decoded frame may outlive the frame that produced it.

Expand Down Expand Up @@ -1436,7 +1477,9 @@ class FirstEmptySegmenter(FakeSegmenter):
def segment(self, frame, request):
if not self.calls:
self.calls.append(request)
return __import__("numpy").zeros(frame.shape[:2] + (4,), dtype="uint8")
return frozen_segmentation_result(
__import__("numpy").zeros(frame.shape[:2] + (4,), dtype="uint8")
)
return super().segment(frame, request)

render_request = replace(
Expand Down Expand Up @@ -1464,7 +1507,7 @@ def segment(self, frame, request):
result = np.zeros(frame.shape[:2] + (4,), dtype=np.uint8)
if visible_size is not None:
result[:visible_size, :visible_size, 3] = 255
return result
return frozen_segmentation_result(result)

render_request = replace(
request(tmp_path),
Expand Down
3 changes: 2 additions & 1 deletion tests/ui/test_preview_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
PreviewRuntime,
)
from matteloop.ui.store import ReducerStore
from tests.jobs.render_support import frozen_segmentation_result


@dataclass(frozen=True)
Expand Down Expand Up @@ -80,7 +81,7 @@ def prepare(

def segment(self, frame, request):
del request
return frame
return frozen_segmentation_result(frame.copy())

def preview(
self, request, playhead: Fraction, context: JobContext
Expand Down
3 changes: 2 additions & 1 deletion tests/ui/test_render_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
from matteloop.ui.render_pipeline import _StageReporter, render_prepared
from matteloop.ui.store import ReducerStore
from tests.fixtures.media_factory import make_video
from tests.jobs.render_support import frozen_segmentation_result


@dataclass(frozen=True)
Expand All @@ -56,7 +57,7 @@ class Metadata:
class FakeSegmenter:
def segment(self, frame, request):
del request
return frame
return frozen_segmentation_result(frame.copy())


def test_stage_change_preserves_current_frame_and_overall_counts(tmp_path) -> None:
Expand Down
Loading