From fa756fbec5c645ee7ab54459d96fbc7e0b8890d7 Mon Sep 17 00:00:00 2001 From: 0xDevNinja Date: Tue, 14 Jul 2026 14:27:40 +0530 Subject: [PATCH] fix(green_screen): make chromakey compositing portable across FFmpeg builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CI Linux FFmpeg build carried the keyed frame forward without an alpha plane, so overlay drew opaque green over the background (corner stayed green) instead of compositing — the E2E test failed there even though it passed on macOS/Windows. Force `format=yuva420p` immediately after chromakey so the keyed transparency always has an explicit alpha plane, and size the background to the frame up front (color=...:size=WxH, passing the probed width/height into _process_chromakey) instead of scaling a 1x1 source with scale2ref — dropping scale2ref also removes the format negotiation that discarded the alpha on some builds. Output is flattened to yuv420p after the overlay. --- tests/tools/test_green_screen_chromakey.py | 16 ++++++++---- tools/video/green_screen_processor.py | 29 +++++++++++++--------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/tests/tools/test_green_screen_chromakey.py b/tests/tools/test_green_screen_chromakey.py index 1a0cb05e..ab4bcf3a 100644 --- a/tests/tools/test_green_screen_chromakey.py +++ b/tests/tools/test_green_screen_chromakey.py @@ -50,16 +50,20 @@ def test_chromakey_filter_scales_background_to_frame(): fd.mkdir() pd.mkdir() (fd / "frame_0000.png").write_bytes(b"stub") - tool._process_chromakey(fd, pd, "#0E172A", 1) + tool._process_chromakey(fd, pd, "#0E172A", 1, 320, 240) finally: GreenScreenProcessor.run_command = orig ffmpeg_cmds = [c for c in captured if c and c[0] == "ffmpeg"] assert ffmpeg_cmds, "no ffmpeg command built" - fc_idx = ffmpeg_cmds[0].index("-filter_complex") - fc = ffmpeg_cmds[0][fc_idx + 1] - assert "scale2ref" in fc, f"background not scaled to frame: {fc}" + cmd = ffmpeg_cmds[0] + fc = cmd[cmd.index("-filter_complex") + 1] + # Background must be sized to the frame, not the old 1x1 no-op. + assert "size=320x240" in " ".join(cmd), "background not sized to frame" + assert "size=1x1" not in " ".join(cmd), "still using the 1x1 background" assert "[0:v]scale=iw:ih[bg]" not in fc, "still using the 1x1 no-op scale" + # Alpha must be forced so keyed transparency survives on every FFmpeg build. + assert "format=yuva420p" in fc, f"keyed alpha not forced: {fc}" @pytest.mark.skipif(shutil.which("ffmpeg") is None or shutil.which("ffprobe") is None, @@ -79,7 +83,9 @@ def test_chromakey_preserves_frame_size_and_keys(tmp_path): capture_output=True, check=True, timeout=60, ) - ok = GreenScreenProcessor()._process_chromakey(frames_dir, processed_dir, "#0E172A", 1) + ok = GreenScreenProcessor()._process_chromakey( + frames_dir, processed_dir, "#0E172A", 1, 320, 240 + ) assert ok out = processed_dir / "frame_0000.png" diff --git a/tools/video/green_screen_processor.py b/tools/video/green_screen_processor.py index ab5e3ce6..fdc9b7bf 100644 --- a/tools/video/green_screen_processor.py +++ b/tools/video/green_screen_processor.py @@ -161,7 +161,7 @@ class GreenScreenProcessor(BaseTool): if method == "chromakey": ok = self._process_chromakey( - frames_dir, processed_dir, bg_color, frame_count + frames_dir, processed_dir, bg_color, frame_count, width, height ) else: ok = self._process_rembg( @@ -445,6 +445,8 @@ class GreenScreenProcessor(BaseTool): processed_dir: Path, bg_color: str, frame_count: int, + width: int, + height: int, ) -> bool: """Process frames using FFmpeg chromakey filter. @@ -461,20 +463,23 @@ class GreenScreenProcessor(BaseTool): out_path = processed_dir / frame.name cmd = [ "ffmpeg", "-y", - "-f", "lavfi", "-i", f"color=c={ffmpeg_bg}:size=1x1", + # Background sized to the frame up front. The old code used a 1x1 + # color source and `[0:v]scale=iw:ih` — a no-op (iw/ih were the + # 1x1 source's own size) — and overlay takes the size of its + # FIRST input, so every frame was clipped to a single pixel and + # the whole video came out a solid color with the subject gone. + "-f", "lavfi", "-i", f"color=c={ffmpeg_bg}:size={width}x{height}", "-i", str(frame), "-filter_complex", ( - # The background is a 1x1 color source; it MUST be scaled up - # to the frame's dimensions. `scale=iw:ih` on [0:v] alone is a - # no-op (iw/ih are the 1x1 source's own size), and overlay - # takes the size of its FIRST input — so that leaves a 1x1 - # canvas and the keyed frame gets clipped to a single pixel, - # producing a solid-color video with the subject gone. - # scale2ref resizes [0:v] to match the frame [1:v] first. - f"[1:v]chromakey=color=0x00FF00:similarity=0.3:blend=0.08[fg];" - f"[0:v][fg]scale2ref[bg][fg2];" - f"[bg][fg2]overlay=0:0" + # Force an explicit alpha format after chromakey so the keyed + # transparency survives filter negotiation on every FFmpeg + # build — without it, some Linux builds carry the keyed frame + # forward without an alpha plane and overlay draws opaque + # green over the background instead of compositing. + f"[1:v]chromakey=color=0x00FF00:similarity=0.3:blend=0.08," + f"format=yuva420p[fg];" + f"[0:v][fg]overlay=0:0:format=auto,format=yuv420p" ), "-frames:v", "1", str(out_path),