mirror of
https://github.com/calesthio/OpenMontage.git
synced 2026-08-11 01:41:43 +08:00
Merge pull request #337 from 0xDevNinja/fix/green-screen-chromakey-1x1-collapse
fix(green_screen): scale chromakey background to frame size, not 1x1
This commit is contained in:
116
tests/tools/test_green_screen_chromakey.py
Normal file
116
tests/tools/test_green_screen_chromakey.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""Regression tests: chromakey compositing must not collapse frames to 1x1.
|
||||
|
||||
`_process_chromakey` built the background from a 1x1 lavfi color source and
|
||||
tried to size it with `[0:v]scale=iw:ih` — a no-op, since iw/ih are the 1x1
|
||||
source's own dimensions. FFmpeg's `overlay` takes the size of its first input
|
||||
(the 1x1 background), so every processed frame was clipped to a single pixel.
|
||||
`_reconstruct_video` then upscaled those 1x1 frames, yielding a solid-color
|
||||
video with the keyed subject entirely gone. The fix uses `scale2ref` to resize
|
||||
the background to the actual frame dimensions.
|
||||
"""
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from tools.video.green_screen_processor import GreenScreenProcessor # noqa: E402
|
||||
|
||||
|
||||
def test_chromakey_filter_scales_background_to_frame():
|
||||
"""Offline: the built filtergraph must not leave a 1x1 background."""
|
||||
captured = []
|
||||
|
||||
def fake_run(self, cmd, **kwargs):
|
||||
captured.append(list(cmd))
|
||||
|
||||
class _R:
|
||||
returncode = 0
|
||||
stdout = ""
|
||||
stderr = ""
|
||||
|
||||
return _R()
|
||||
|
||||
tool = GreenScreenProcessor()
|
||||
orig = GreenScreenProcessor.run_command
|
||||
GreenScreenProcessor.run_command = fake_run
|
||||
try:
|
||||
frames_dir = Path(PROJECT_ROOT) / "tests" / "tools" # any dir; glob may be empty
|
||||
# Drive the filter build directly with a temp frame present.
|
||||
import tempfile
|
||||
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
fd = Path(td) / "frames"
|
||||
pd = Path(td) / "processed"
|
||||
fd.mkdir()
|
||||
pd.mkdir()
|
||||
(fd / "frame_0000.png").write_bytes(b"stub")
|
||||
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"
|
||||
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,
|
||||
reason="ffmpeg/ffprobe required")
|
||||
def test_chromakey_preserves_frame_size_and_keys(tmp_path):
|
||||
"""End-to-end: output keeps the source size and keys green -> background."""
|
||||
frames_dir = tmp_path / "frames"
|
||||
processed_dir = tmp_path / "processed"
|
||||
frames_dir.mkdir()
|
||||
processed_dir.mkdir()
|
||||
|
||||
frame = frames_dir / "frame_0000.png"
|
||||
subprocess.run(
|
||||
["ffmpeg", "-y", "-f", "lavfi", "-i",
|
||||
"color=c=0x00FF00:size=320x240,drawbox=x=100:y=80:w=120:h=80:color=red:t=fill",
|
||||
"-frames:v", "1", str(frame)],
|
||||
capture_output=True, check=True, timeout=60,
|
||||
)
|
||||
|
||||
ok = GreenScreenProcessor()._process_chromakey(
|
||||
frames_dir, processed_dir, "#0E172A", 1, 320, 240
|
||||
)
|
||||
assert ok
|
||||
|
||||
out = processed_dir / "frame_0000.png"
|
||||
assert out.exists()
|
||||
|
||||
def _size(p):
|
||||
r = subprocess.run(
|
||||
["ffprobe", "-v", "quiet", "-show_entries", "stream=width,height",
|
||||
"-of", "csv=p=0", str(p)],
|
||||
capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
w, h = r.stdout.strip().split(",")
|
||||
return int(w), int(h)
|
||||
|
||||
assert _size(out) == (320, 240), "frame collapsed instead of keeping source size"
|
||||
|
||||
def _pixel(p, x, y):
|
||||
r = subprocess.run(
|
||||
["ffmpeg", "-v", "quiet", "-i", str(p), "-vf", f"crop=1:1:{x}:{y}",
|
||||
"-f", "rawvideo", "-pix_fmt", "rgb24", "-"],
|
||||
capture_output=True, timeout=30,
|
||||
)
|
||||
return r.stdout[:3]
|
||||
|
||||
center = _pixel(out, 160, 120) # red box -> stays red
|
||||
corner = _pixel(out, 10, 10) # was green -> keyed to dark background
|
||||
assert center[0] > 150 and center[1] < 80, f"subject lost, center={center!r}"
|
||||
assert corner[0] < 60 and corner[1] < 60, f"green not keyed, corner={corner!r}"
|
||||
@@ -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,13 +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",
|
||||
(
|
||||
f"[0:v]scale=iw:ih[bg];"
|
||||
f"[1:v]chromakey=color=0x00FF00:similarity=0.3:blend=0.08[fg];"
|
||||
f"[bg][fg]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),
|
||||
|
||||
Reference in New Issue
Block a user