Auto reframe di aplikasi edit video biasanya dijual sebagai pelacakan subjek berbasis AI. Untuk kasus yang paling umum, yaitu orang yang bergerak di depan latar yang diam, ada cara yang jauh lebih sederhana untuk mendapatkan framing yang sama: bandingkan tiap frame dengan frame sebelumnya dan lihat di mana gambarnya berubah. Fitur reframe di aplikasi desktop osFoundry bekerja seperti ini, dengan Rust dan ffmpeg. Di bawah ini metode yang sama dalam sekitar 60 baris Python, dijalankan pada klip 1920×1080 berdurasi 7.5 detik yang terdiri dari tiga shot. Hasil crop-nya sama dengan aplikasi, sampai ke piksel.
1. Perkecil video menjadi frame abu-abu 64×36
Untuk mencari bagian yang bergerak, frame tidak perlu dilihat dalam ukuran penuh. ffmpeg bisa memberikan video sebagai byte mentah, empat frame per detik, diperkecil ke 64×36 dan diubah menjadi satu byte abu-abu per piksel:
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)] Setiap frame berukuran 2304 byte. Klip 7.5 detik menghasilkan 30 frame, jadi ada 29 pasangan untuk dibandingkan. Memperkecil ukuran juga merata-ratakan sebagian besar noise, dan itu penting untuk langkah berikutnya.
2. Cari pusat bagian yang berubah
Untuk setiap pasang frame, hitung selisih tiap sel, abaikan selisih kecil, lalu hitung pusat massa dari sisanya, dibobot menurut seberapa besar tiap sel berubah:
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:])] Dua konstanta inilah yang membuatnya bisa dipakai. Batas noise 12 (dari 255) membuang perubahan yang berasal dari sensor dan kompresi. Pada klip ini, dengan batas 12, rata-rata 381 dari 2304 sel dihitung berubah. Dengan batas 1, jumlahnya 1855, hampir seluruh frame, dan pusatnya tertarik ke tengah gambar: sampel pertama jatuh di 0.44 dari lebar, bukan 0.40.
Pemeriksaan kedua mengembalikan None bila total perubahannya sangat kecil. Kalau tidak ada yang bergerak, "pusat perubahan" hanyalah pusat noise, dan mengikutinya membuat frame bergeser tanpa alasan. None berarti "pertahankan posisi sebelumnya".
3. Batasi seberapa cepat frame boleh bergerak
Centroid mentah melompat setiap kali orang kedua masuk ke shot. Membatasi langkah sebesar 5% dari frame per sampel mencegahnya menyambar ke sisi lain:
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) Fungsi ini mulai dari pengamatan nyata yang pertama, sehingga klip yang dibuka dengan gambar diam tidak bergeser masuk dari tengah.
4. Cari cut dengan scene score ffmpeg
Filter select di ffmpeg memberi setiap frame skor scene antara 0 dan 1, dan showinfo mencetak frame yang lolos. Timestamp-nya keluar di stderr, bukan stdout:
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)] Pada klip ini, hasilnya cut di 1767 ms dan 4733 ms. Mencetak skor setiap frame (select='gte(scene,0)',metadata=print) memperlihatkan seberapa lebar selisihnya: kedua cut mendapat skor 0.616 dan 0.432, dan tidak ada frame lain dari 225 frame yang skornya lebih dari 0.068.
5. Satu crop per shot
Terakhir, rata-ratakan jalur pelacakan di dalam tiap shot, lalu letakkan jendela 9:16 di sekitar titik itu. 1080 × 9/16 adalah 607.5; lebarnya dibulatkan ke bawah menjadi 606 yang genap, karena H.264 membutuhkan dimensi genap.
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") 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 Ini sama dengan tiga crop yang dihasilkan aplikasi untuk klip ini. Seluruh skrip memakan waktu 3.5 detik di Mac ini, sebagian besar di loop Python murni.
Cut lebih penting daripada kelihatannya. Jika dirata-rata untuk seluruh klip, subjek berada di x = 707, hanya 50 piksel dari crop tengah biasa (657), karena perempuan itu berjalan di kiri pada satu shot dan di kanan pada shot lain. Rata-rata baru bermakna kalau dihitung per shot.
Menyambung kembali shot-nya
Crop-crop itu menjadi satu filter_complex: potong tiap shot, crop, lalu gabungkan. Bentuk inilah yang dibuat aplikasi:
[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] (Hapus baris barunya saat memasukkannya ke -filter_complex.) Outputnya 606×1080 dengan durasi yang sama dengan input.
Kode Rust aplikasi menjelaskan mengapa crop tidak dianimasikan di dalam satu shot, padahal ffmpeg bisa melakukannya:
/// 🔴 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> { Kapan selisih frame gagal
Metode ini menemukan apa yang bergerak, bukan siapa yang penting. Ia berhasil bila subjek adalah hal utama yang bergerak dan kamera diam. Ia gagal bila:
- kamera melakukan panning, sehingga setiap sel berubah dan pusatnya jatuh dekat tengah;
- latar belakangnya ramai, misalnya lalu lintas atau kerumunan;
- dua orang bergerak di sisi yang berlawanan, dan rata-ratanya berada di antara mereka.
Pantulan adalah kasus yang lebih ringan. Pada shot pertama klip ini, bayangan perempuan itu di jendela juga bergerak, tetapi tetap dekat dengannya, jadi framing masih menemukannya. Pendeteksi wajah atau orang menangani kasus sulit dengan lebih baik, dengan harga sebuah model. Jawaban aplikasi untuk kasus itu adalah override manual: pengguna menyeret satu titik ke subjek, dan analisis dilewati.
Aplikasi desktop yang dipakai di sini gratis di halaman unduh osFoundry.


