DEV Community

Cover image for ffmpeg और फ्रेम डिफरेंस से वीडियो को अपने आप 9:16 में रीफ्रेम कैसे करें

ffmpeg और फ्रेम डिफरेंस से वीडियो को अपने आप 9:16 में रीफ्रेम कैसे करें

वीडियो एडिटरों में ऑटो रीफ्रेम को आमतौर पर AI सब्जेक्ट ट्रैकिंग बताकर बेचा जाता है। सबसे आम मामले के लिए, यानी जब कोई व्यक्ति स्थिर बैकग्राउंड के सामने चल रहा हो, एक बहुत सरल तरीका वही फ्रेमिंग निकाल देता है: हर फ्रेम की पिछले फ्रेम से तुलना करो और देखो कि तस्वीर कहाँ बदली। डेस्कटॉप ऐप osFoundry का reframe टूल इसी तरह काम करता है, Rust और ffmpeg में। नीचे वही तरीका करीब 60 लाइन के Python में है, जिसे तीन शॉट वाली 7.5 सेकंड की 1920×1080 क्लिप पर चलाया गया है। यह ऐप जैसे ही crop देता है, पिक्सल तक बराबर।

1. वीडियो को 64×36 के ग्रे फ्रेम में छोटा करें

यह पता लगाने के लिए कि कुछ कहाँ हिला, फुल साइज़ फ्रेम देखने की ज़रूरत नहीं है। ffmpeg वीडियो को raw bytes के रूप में दे सकता है, प्रति सेकंड चार फ्रेम, 64×36 में स्केल करके और हर पिक्सल के लिए एक ग्रे बाइट में बदलकर:

import math, re, subprocess, sys

FFMPEG, SRC = sys.argv[1], sys.argv[2]
W, H, FPS = 64, 36, 4

raw = subprocess.run(
    [FFMPEG, "-loglevel", "error", "-i", SRC,
     "-vf", f"fps={FPS},scale={W}:{H}", "-pix_fmt", "gray", "-f", "rawvideo", "-"],
    capture_output=True, check=True).stdout
frames = [raw[i:i + W * H] for i in range(0, len(raw) - W * H + 1, W * H)]
Enter fullscreen mode Exit fullscreen mode

हर फ्रेम 2,304 बाइट का है। 7.5 सेकंड की क्लिप से 30 फ्रेम मिलते हैं, यानी तुलना के लिए 29 जोड़े। छोटा करने से ज़्यादातर ग्रेन भी औसत होकर खत्म हो जाता है, जो अगले स्टेप में मायने रखता है।

2. बदली हुई जगह का केंद्र निकालें

फ्रेम के हर जोड़े के लिए हर सेल का अंतर निकालें, छोटे अंतर को नज़रअंदाज़ करें, और बाकी का centre of mass निकालें, जिसमें हर सेल का वज़न इस पर निर्भर है कि वह कितना बदला:

def centroid(prev, curr, floor=12):
    total = sx = sy = 0
    for i, (a, b) in enumerate(zip(prev, curr)):
        d = abs(b - a)
        if d < floor:
            continue
        total += d
        sx += d * (i % W)
        sy += d * (i // W)
    if total < W * H * 0.5:
        return None
    return sx / total / W, sy / total / H

samples = [centroid(a, b) for a, b in zip(frames, frames[1:])]
Enter fullscreen mode Exit fullscreen mode

तीसरे शॉट का 64×36 ग्रे फ्रेम (बाएँ) और पिछले फ्रेम से उसका अंतर (दाएँ): सिर्फ चलती हुई महिला चमकती है

ये दो constants ही इसे इस्तेमाल लायक बनाते हैं। 255 में से 12 का noise floor उन बदलावों को हटा देता है जो सेंसर और compression से आते हैं। इस क्लिप में floor 12 रखने पर औसतन 2,304 में से 381 सेल बदले हुए गिने जाते हैं। floor 1 रखने पर 1,855 गिने जाते हैं, यानी लगभग पूरा फ्रेम, और केंद्र तस्वीर के बीच की ओर खिंच जाता है: पहला सैंपल चौड़ाई के 0.40 की जगह 0.44 पर आता है।

दूसरी जाँच तब None लौटाती है जब कुल बदलाव बहुत कम हो। जब कुछ नहीं हिला, तो "बदलाव का केंद्र" असल में noise का केंद्र होता है, और उसके पीछे चलने से फ्रेम बिना वजह खिसकता है। None का मतलब है "पिछली पोज़िशन रखो"।

3. फ्रेम कितनी तेज़ी से हिल सकता है, इसकी सीमा रखें

जब भी शॉट में दूसरा व्यक्ति आता है, कच्चा centroid कूद जाता है। हर सैंपल पर कदम को फ्रेम के 5% तक सीमित कर देने से वह एक झटके में इधर-उधर नहीं भागता:

def smooth(samples, step=0.05):
    cur = next((s for s in samples if s), (0.5, 0.5))
    out = []
    for s in samples:
        if s:
            cur = tuple(min(1, max(0, c + max(-step, min(step, t - c))))
                        for c, t in zip(cur, s))
        out.append(cur)
    return out

track = smooth(samples)
Enter fullscreen mode Exit fullscreen mode

यह पहले असली observation से शुरू होता है, इसलिए जो क्लिप स्थिर फ्रेम से खुलती है वह बीच से pan करती हुई नहीं आती।

4. ffmpeg के scene score से कट खोजें

ffmpeg का select फ़िल्टर हर फ्रेम को 0 से 1 के बीच का scene score देता है, और showinfo उन फ्रेमों को प्रिंट करता है जो पास होते हैं। टाइमस्टैम्प stdout पर नहीं, stderr पर आते हैं:

log = subprocess.run(
    [FFMPEG, "-hide_banner", "-i", SRC,
     "-filter_complex", "select='gt(scene,0.35)',showinfo", "-f", "null", "-"],
    capture_output=True, text=True).stderr
cuts = [round(float(t) * 1000) for t in re.findall(r"pts_time:([\d.]+)", log)]
Enter fullscreen mode Exit fullscreen mode

इस क्लिप में यह 1,767 ms और 4,733 ms पर कट ढूँढता है। हर फ्रेम का score प्रिंट करने (select='gte(scene,0)',metadata=print) पर पता चलता है कि फ़ासला कितना बड़ा है: दोनों कटों का score 0.616 और 0.432 है, और 225 फ्रेमों में से कोई दूसरा 0.068 से ऊपर नहीं जाता।

5. हर शॉट के लिए एक crop

आखिर में, हर शॉट के अंदर track का औसत लें और उस बिंदु के चारों ओर 9:16 की विंडो रखें। 1080 × 9/16 यानी 607.5 होता है; चौड़ाई को घटाकर सम संख्या 606 कर दिया गया है क्योंकि H.264 को सम dimensions चाहिए।

duration = len(frames) * 1000 // FPS
per = duration / len(track)
bounds = [0] + [c for c in cuts if 0 < c < duration] + [duration]
cw = int(1080 * 9 / 16) // 2 * 2
for start, end in zip(bounds, bounds[1:]):
    a = int(start // per)
    part = track[a:max(a + 1, math.ceil(end / per))]
    bx = sum(p[0] for p in part) / len(part)
    x = min(max(round(1920 * bx - cw / 2), 0), 1920 - cw)
    print(f"{start / 1000:.2f}-{end / 1000:.2f}s  crop={cw}:1080:{x}:0")
Enter fullscreen mode Exit fullscreen mode
0.00-1.77s  crop=606:1080:369:0
1.77-4.73s  crop=606:1080:668:0
4.73-7.50s  crop=606:1080:932:0
Enter fullscreen mode Exit fullscreen mode

ये वही तीन crop हैं जो ऐप ने इस क्लिप के लिए निकाले थे। पूरी स्क्रिप्ट इस Mac पर 3.5 सेकंड में चली, ज़्यादातर समय pure-Python लूप में गया।

कट दिखने से ज़्यादा मायने रखते हैं। पूरी क्लिप पर औसत लें तो सब्जेक्ट x = 707 पर बैठता है, सीधे सेंटर crop (657) से सिर्फ 50 पिक्सल दूर, क्योंकि महिला एक शॉट में बाईं ओर और दूसरे में दाईं ओर चलती है। औसत का मतलब तभी बनता है जब वह हर शॉट के हिसाब से निकाला जाए।

शॉट को वापस जोड़ना

इन crop से एक filter_complex बनता है: हर शॉट को trim करें, crop करें और टुकड़ों को जोड़ दें। ऐप यही रूप बनाता है:

[0:v]trim=start=0.000:end=1.767,setpts=PTS-STARTPTS,crop=606:1080:369:0[v0];
[0:v]trim=start=1.767:end=4.733,setpts=PTS-STARTPTS,crop=606:1080:668:0[v1];
[0:v]trim=start=4.733:end=7.500,setpts=PTS-STARTPTS,crop=606:1080:932:0[v2];
[v0][v1][v2]concat=n=3:v=1:a=0[out]
Enter fullscreen mode Exit fullscreen mode

(इसे -filter_complex में देते समय लाइन ब्रेक हटा दें।) आउटपुट 606×1080 है और इनपुट के बराबर लंबा है।

ऊपर की पंक्ति: हर शॉट का सेंटर crop। नीचे की पंक्ति: ऊपर निकाले गए crop

ऐप का Rust बताता है कि वह शॉट के अंदर crop को animate क्यों नहीं करता, जबकि ffmpeg ऐसा कर सकता है:

/// 🔴 Per scene, not per frame. ffmpeg can animate a crop, but a crop that moves continuously is
/// a pan the editor never asked for; cutting between fixed framings is what an editor would
/// actually do, and it is also what stays honest when the tracking is wrong — a bad framing
/// lasts one scene rather than smearing across the whole clip.
pub fn reframe_segments(
    cuts: &[i64],
    duration_ms: i64,
    track: &[(f64, f64)],
    width: u32,
    height: u32,
    target: f64,
) -> Vec<ReframeSegment> {
Enter fullscreen mode Exit fullscreen mode

जहाँ फ्रेम डिफरेंस फेल हो जाता है

यह तरीका यह खोजता है कि क्या हिल रहा है, यह नहीं कि कौन ज़रूरी है। यह तब काम करता है जब सब्जेक्ट ही मुख्य रूप से हिलने वाली चीज़ हो और कैमरा स्थिर हो। यह तब गड़बड़ करता है जब:

  • कैमरा pan करे, तो हर सेल बदलती है और केंद्र बीच के पास आ जाता है;
  • बैकग्राउंड व्यस्त हो, जैसे ट्रैफिक या भीड़;
  • दो लोग विपरीत किनारों पर हिलें, और औसत उन दोनों के बीच आ जाए।

रिफ्लेक्शन एक हल्का मामला है। इस क्लिप के पहले शॉट में खिड़की में महिला का प्रतिबिंब भी हिलता है, लेकिन वह उसके पास ही रहता है, इसलिए फ्रेम फिर भी उसे ढूँढ लेता है। मुश्किल मामलों को चेहरे या व्यक्ति का detector बेहतर संभालता है, लेकिन उसकी कीमत एक model है। ऐसे मामलों के लिए ऐप का जवाब मैन्युअल override है: यूज़र सब्जेक्ट पर एक बिंदु खींचकर रख देता है, और analysis छोड़ दिया जाता है।

यहाँ इस्तेमाल हुआ डेस्कटॉप ऐप osFoundry के डाउनलोड पेज से मुफ़्त है।

Top comments (0)