From 46ec0cec9c511a8e4ca794f64865c7b7cbb7f862 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Sat, 2 May 2026 11:57:09 +0000 Subject: [PATCH] Anamorphic source fix: pred-scale na square pixel (PAL DV 720x576 SAR 64:45 \u2192 1024x576) preden track/center/blur filter dela. Brez tega so se reels iz starih MPG-2 (npr. Global Kryner Like a Virgin) razteg-nili horizontalno. --- scripts/reframe.py | 69 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 56 insertions(+), 13 deletions(-) diff --git a/scripts/reframe.py b/scripts/reframe.py index 3ea707f..bb7b694 100644 --- a/scripts/reframe.py +++ b/scripts/reframe.py @@ -25,7 +25,12 @@ import numpy as np def get_video_info(path): - """Vrni dict z width, height, fps, duration.""" + """Vrni dict z width, height, fps, duration. + + width/height so SQUARE-PIXEL dimenzije (popravljeno za anamorphic SAR). + Pri broadcast 720x576 SAR 64:45 (PAL DV) bo vrnil 1024x576 (square pixel). + Pri standardnem source z SAR 1:1 ostane nespremenjeno. + """ cmd = [ "ffprobe", "-v", "quiet", "-print_format", "json", "-show_streams", "-show_format", str(path) @@ -35,9 +40,38 @@ def get_video_info(path): fps_str = vstream["r_frame_rate"] num, den = fps_str.split("/") fps = float(num) / float(den) + + raw_w = int(vstream["width"]) + raw_h = int(vstream["height"]) + + # SAR (sample aspect ratio) — razmerje stranic pikslja + sar = vstream.get("sample_aspect_ratio", "1:1") + try: + sar_n, sar_d = sar.split(":") + sar_n, sar_d = int(sar_n), int(sar_d) + if sar_n == 0 or sar_d == 0: + sar_n, sar_d = 1, 1 + except (ValueError, AttributeError): + sar_n, sar_d = 1, 1 + + # Square-pixel dimenzije: če SAR != 1:1, popravi širino + if sar_n != sar_d: + sq_w = int(round(raw_w * sar_n / sar_d)) + # Zaokroži na sodo (libx264 ima rad sode dimenzije) + sq_w = sq_w + (sq_w % 2) + sq_h = raw_h + print(f"📐 Anamorphic source: {raw_w}x{raw_h} SAR {sar_n}:{sar_d} → {sq_w}x{sq_h} (square pixel)", file=sys.stderr) + else: + sq_w = raw_w + sq_h = raw_h + return { - "width": int(vstream["width"]), - "height": int(vstream["height"]), + "width": sq_w, + "height": sq_h, + "raw_width": raw_w, + "raw_height": raw_h, + "sar_n": sar_n, + "sar_d": sar_d, "fps": fps, "duration": float(data["format"]["duration"]), } @@ -250,7 +284,7 @@ def build_center_filter(info, target_w, target_h): return f"scale={scale_w}:{scale_h},crop={target_w}:{target_h}:(in_w-{target_w})/2:0" -def build_blur_filter(info, target_w, target_h): +def build_blur_filter(info, target_w, target_h, anamorphic_prefix=""): """ 9:16 platno: spodaj/zgoraj blur kopija, v sredini originalni 16:9. """ @@ -258,10 +292,11 @@ def build_blur_filter(info, target_w, target_h): src_w = info["width"] src_h = info["height"] fg_h = int(target_w * src_h / src_w) + pre = (anamorphic_prefix + ",") if anamorphic_prefix else "" return ( - f"[0:v]scale={target_w}:{target_h}:force_original_aspect_ratio=increase," + f"[0:v]{pre}scale={target_w}:{target_h}:force_original_aspect_ratio=increase," f"crop={target_w}:{target_h},gblur=sigma=30[bg];" - f"[0:v]scale={target_w}:{fg_h}[fg];" + f"[0:v]{pre}scale={target_w}:{fg_h}[fg];" f"[bg][fg]overlay=0:(H-h)/2" ) @@ -364,17 +399,29 @@ def main(): info = get_video_info(work_input) print(f"📹 Vhod: {info['width']}x{info['height']} @ {info['fps']:.2f}fps, {info['duration']:.1f}s") + # Anamorphic correction prefix: če je SAR != 1:1, najprej scale-amo na square-pixel + # in resetiramo SAR=1, šele potem track/crop filter dela na pravilnih dimenzijah. + # Brez tega: 720x576 SAR 64:45 (DAR 16:9) PAL DV se pri scale na 1920 razteguje + # nepravilno (ker filter misli da je 720 širina, dejansko prikazana širina je 1024). + if info.get("sar_n", 1) != info.get("sar_d", 1): + anamorphic_prefix = f"scale={info['width']}:{info['height']}:flags=lanczos,setsar=1," + print(f"🔧 Anamorphic prefix: {anamorphic_prefix.rstrip(',')}", file=sys.stderr) + else: + anamorphic_prefix = "" + if args.mode == "track": print("🔍 Detektiram obraze (OpenCV)...") samples, _, _, _, _ = detect_face_centers(work_input, sample_fps=5) n_with_face = sum(1 for _, x in samples if x is not None) print(f" {n_with_face}/{len(samples)} vzorcev z obrazom") x_at = smooth_track(samples, info["duration"], smoothing_window=4.0) - vfilter = build_track_filter(info, x_at, args.target_width, args.target_height, info["fps"]) + vfilter = anamorphic_prefix + build_track_filter(info, x_at, args.target_width, args.target_height, info["fps"]) elif args.mode == "center": - vfilter = build_center_filter(info, args.target_width, args.target_height) + vfilter = anamorphic_prefix + build_center_filter(info, args.target_width, args.target_height) elif args.mode == "blur": - vfilter = build_blur_filter(info, args.target_width, args.target_height) + # blur uporablja filter_complex z [0:v] referenco — anamorphic prefix gre v posebni veji + vfilter = build_blur_filter(info, args.target_width, args.target_height, + anamorphic_prefix=anamorphic_prefix.rstrip(",")) preset = {"fast": "veryfast", "medium": "medium", "high": "slow"}[args.quality] crf = {"fast": "26", "medium": "21", "high": "18"}[args.quality] @@ -389,10 +436,6 @@ def main(): audio_filter.append(f"afade=t=out:st={fade_start}:d={args.fade_out}") audio_filter_str = ",".join(audio_filter) if audio_filter else None - # Force kvadraten pixel (SAR=1:1) — fix za broadcast .MPG ki ima 64:45 anamorphic - if not vfilter.endswith(",setsar=1"): - vfilter = vfilter + ",setsar=1" - if args.mode == "blur": # blur uporablja filter_complex cmd = [