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),