mirror of
https://github.com/vondas-network/videobeaux.git
synced 2025-12-11 18:35:01 +01:00
169 lines
7.4 KiB
Python
169 lines
7.4 KiB
Python
# videobeaux/programs/dynakey.py
|
||
# Keyer that respects Videobeaux GLOBALS (-i/-o/-F/-h come from the CLI).
|
||
# MP4-only output (libx264, yuv420p), optional background video or solid fill.
|
||
# Dynamic mode smoothly morphs similarity/blend over time.
|
||
|
||
from videobeaux.utils.ffmpeg_operations import run_ffmpeg_with_progress
|
||
|
||
def register_arguments(parser):
|
||
parser.description = (
|
||
"Dynamic Color/Chroma Keyer (FFmpeg) with despill, alpha-edge soften, solid/bg compositing, and art modes."
|
||
)
|
||
print("✅ dynakey ready. Use global -i/-o/-F. This mode adds only feature flags.")
|
||
|
||
# Key engine + params
|
||
parser.add_argument("--engine", choices=["colorkey", "chromakey"], default="colorkey",
|
||
help="colorkey=RGB distance; chromakey=YUV chroma distance (more broadcast-like).")
|
||
parser.add_argument("--key", default="0x00FF00",
|
||
help="Key color. colorkey: 0xRRGGBB (e.g., 0x00FF00). chromakey: g/b/r or hex.")
|
||
parser.add_argument("--similarity", type=float, default=0.12,
|
||
help="Color distance threshold to remove (~0.05–0.4 typical).")
|
||
parser.add_argument("--blend", type=float, default=0.02,
|
||
help="Edge feather (0–1).")
|
||
|
||
# Soften keyed edges by blurring alpha only
|
||
parser.add_argument("--soften", type=float, default=0.0,
|
||
help="Extra edge soften in pixels (alpha-only blur).")
|
||
|
||
# Spill + pre
|
||
parser.add_argument("--despill", action="store_true", help="Reduce green/blue spill halos on edges.")
|
||
parser.add_argument("--pre-denoise", action="store_true", help="Mild denoise before key (hqdn3d).")
|
||
parser.add_argument("--pre-smooth-chroma", action="store_true",
|
||
help="Light chroma smoothing before key (yuv444p + blur).")
|
||
|
||
# Background composition (video or solid color)
|
||
parser.add_argument("--bg", help="Optional background VIDEO path (composite keyed FG over it).")
|
||
parser.add_argument("--bg-solid", default="black",
|
||
help="Solid background if no --bg. Accepts names (black, white) or hex (#RRGGBB/0xRRGGBB).")
|
||
|
||
# Art modes (post-key stylization)
|
||
parser.add_argument("--art", choices=["none", "trails", "lighten-feedback"], default="none",
|
||
help="Stylize after key: 'trails' (lagfun) or 'lighten-feedback' (tblend).")
|
||
parser.add_argument("--art-intensity", type=float, default=0.96,
|
||
help="Trails decay (0.85–0.995).")
|
||
|
||
# Dynamic morphing (no-ZMQ): sweep similarity/blend over N segments
|
||
parser.add_argument("--dynamic", action="store_true", help="Morph key params over time, then xfade between variants.")
|
||
parser.add_argument("--segments", type=int, default=4, help="Number of morph segments (>=2).")
|
||
parser.add_argument("--cycle-sim-min", type=float, default=0.08, help="Min similarity in morph.")
|
||
parser.add_argument("--cycle-sim-max", type=float, default=0.25, help="Max similarity in morph.")
|
||
parser.add_argument("--cycle-blend-min", type=float, default=0.0, help="Min blend in morph.")
|
||
parser.add_argument("--cycle-blend-max", type=float, default=0.08, help="Max blend in morph.")
|
||
|
||
# Output cadence
|
||
parser.add_argument("--fps", type=float, default=30, help="Output FPS (default 30).")
|
||
|
||
|
||
def _norm_hex(s: str) -> str:
|
||
"""Normalize color strings to FFmpeg-friendly forms."""
|
||
s = s.strip()
|
||
if s.startswith("#"):
|
||
return "0x" + s[1:]
|
||
return s
|
||
|
||
|
||
def _key_filter(engine, key, similarity, blend):
|
||
"""Return filter that keys to RGBA (alpha created)."""
|
||
if engine == "chromakey":
|
||
return f"chromakey={key}:{similarity}:{blend},format=rgba"
|
||
return f"colorkey={key}:{similarity}:{blend},format=rgba"
|
||
|
||
|
||
def _post_art(mode, intensity):
|
||
if mode == "trails":
|
||
return f",lagfun=decay={float(intensity)}"
|
||
if mode == "lighten-feedback":
|
||
return ",tblend=all_mode=lighten"
|
||
return ""
|
||
|
||
|
||
def run(args):
|
||
# ---------- Build pre-chain ----------
|
||
pre = []
|
||
if args.pre_denoise:
|
||
pre.append("hqdn3d=l=2.0:c=1.5")
|
||
if args.pre_smooth_chroma:
|
||
pre.append("format=yuv444p,boxblur=0:1")
|
||
pre_chain = (",".join(pre) + ",") if pre else ""
|
||
|
||
# ---------- Foreground key(s) ----------
|
||
graph = []
|
||
key_color = _norm_hex(args.key)
|
||
|
||
def keyed_stream(src_label, sim, blnd, out_label):
|
||
chain = _key_filter(args.engine, key_color, sim, blnd)
|
||
# alpha-only soften using gblur planes=a (avoids label collisions)
|
||
if args.soften and args.soften > 0:
|
||
chain += f",gblur=sigma={float(args.soften)}:steps=1:planes=a"
|
||
if args.despill:
|
||
chain += ",despill"
|
||
chain += _post_art(args.art, args.art_intensity)
|
||
graph.append(f"[{src_label}]{chain}[{out_label}]")
|
||
|
||
if not args.dynamic:
|
||
graph.append(f"[0:v]{pre_chain}setpts=PTS-STARTPTS[pre]")
|
||
keyed_stream("pre", args.similarity, args.blend, "FG")
|
||
fg_out = "FG"
|
||
else:
|
||
N = max(2, int(args.segments))
|
||
graph.append(f"[0:v]{pre_chain}setpts=PTS-STARTPTS,format=yuv420p[pre]")
|
||
graph.append(f"[pre]split={N}" + "".join(f"[s{i}]" for i in range(N)))
|
||
|
||
def lerp(a, b, i, n): return a if n <= 1 else (a + (b - a) * (i / (n - 1)))
|
||
segs = []
|
||
for i in range(N):
|
||
sim_i = round(lerp(args.cycle_sim_min, args.cycle_sim_max, i, N), 4)
|
||
bln_i = round(lerp(args.cycle_blend_min, args.cycle_blend_max, i, N), 4)
|
||
outl = f"K{i}"
|
||
keyed_stream(f"s{i}", sim_i, bln_i, outl)
|
||
segs.append(outl)
|
||
|
||
# xfade through segments for a smooth morph (1s fades starting at 1s)
|
||
cur = segs[0]
|
||
for idx, nxt in enumerate(segs[1:], start=1):
|
||
outl = f"XF{idx}"
|
||
graph.append(f"[{cur}][{nxt}]xfade=transition=fade:duration=1.0:offset=1.0[{outl}]")
|
||
cur = outl
|
||
fg_out = cur
|
||
|
||
# ---------- Background (video or solid) & composite ----------
|
||
if args.bg:
|
||
# Use provided background video; scale to FG dimensions using scale2ref, then overlay FG (RGBA) over BG.
|
||
graph.append(f"[1:v]setpts=PTS-STARTPTS[BG0]")
|
||
graph.append(f"[BG0][{fg_out}]scale2ref[BG][FGref]") # BG resized to FG size
|
||
graph.append(f"[BG][{fg_out}]overlay=shortest=1:format=auto[out_v]")
|
||
need_bg_input = True
|
||
else:
|
||
# Create solid background sized to FG with scale2ref, then overlay.
|
||
solid = _norm_hex(args.bg_solid)
|
||
graph.append(f"color=c={solid}:s=16x16:d=1[BGsrc]")
|
||
graph.append(f"[BGsrc][{fg_out}]scale2ref[BG][FGref]")
|
||
graph.append(f"[BG][{fg_out}]overlay=shortest=1:format=auto[out_v]")
|
||
need_bg_input = False
|
||
|
||
filter_complex = ";".join(graph)
|
||
|
||
# ---------- FFmpeg command (MP4 only; globals provide -i/-o/-F) ----------
|
||
command = ["ffmpeg", "-i", args.input]
|
||
if need_bg_input:
|
||
command += ["-i", args.bg]
|
||
command += [
|
||
"-filter_complex", filter_complex,
|
||
"-map", "[out_v]",
|
||
"-map", "0:a",
|
||
"-c:v", "libx264",
|
||
"-profile:v", "high",
|
||
"-level:v", "4.2",
|
||
"-pix_fmt", "yuv420p",
|
||
"-r", str(args.fps),
|
||
"-movflags", "+faststart",
|
||
"-shortest",
|
||
args.output, # NOTE: your CLI states 'no extension'; we honor whatever it passes here.
|
||
]
|
||
|
||
run_ffmpeg_with_progress(
|
||
(command[:1] + ["-y"] + command[1:]) if getattr(args, "force", False) else command,
|
||
args.input,
|
||
args.output
|
||
)
|