ff_filter/effects/stabilizer.rs
1//! Video stabilization — two-pass motion analysis and correction.
2
3#![allow(unsafe_code)]
4
5use std::path::Path;
6
7use crate::FilterError;
8
9/// Options for the first stabilization pass (motion analysis).
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct AnalyzeOptions {
12 /// Motion shakiness level 1–10 (default: 5).
13 pub shakiness: u8,
14 /// Detection accuracy 1–15 (default: 15, highest quality).
15 pub accuracy: u8,
16 /// Step size for motion search in pixels 1–32 (default: 6).
17 pub stepsize: u8,
18}
19
20impl Default for AnalyzeOptions {
21 fn default() -> Self {
22 Self {
23 shakiness: 5,
24 accuracy: 15,
25 stepsize: 6,
26 }
27 }
28}
29
30/// Interpolation algorithm used by [`Stabilizer::transform`].
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum Interpolation {
33 /// Bilinear interpolation (faster, default).
34 Bilinear,
35 /// Bicubic interpolation (higher quality, slower).
36 Bicubic,
37}
38
39/// Options for the second stabilization pass (transform application).
40#[derive(Debug, Clone, PartialEq)]
41pub struct StabilizeOptions {
42 /// Temporal smoothing radius in frames 0–500 (default: 10).
43 pub smoothing: u16,
44 /// Fill stabilization borders with black instead of previous-frame content
45 /// (default: true).
46 pub crop_black: bool,
47 /// Zoom factor: 0.0 = no zoom, positive = fixed zoom-in (default: 0.0).
48 pub zoom: f32,
49 /// Optimal zoom: 0 = disabled, 1 = auto-static, 2 = adaptive (default: 0).
50 pub optzoom: u8,
51 /// Pixel interpolation algorithm (default: [`Interpolation::Bilinear`]).
52 pub interpol: Interpolation,
53}
54
55impl Default for StabilizeOptions {
56 fn default() -> Self {
57 Self {
58 smoothing: 10,
59 crop_black: true,
60 zoom: 0.0,
61 optzoom: 0,
62 interpol: Interpolation::Bilinear,
63 }
64 }
65}
66
67impl StabilizeOptions {
68 /// Set the fixed zoom-in factor (0.0 = no zoom).
69 #[must_use]
70 pub fn zoom(mut self, z: f32) -> Self {
71 self.zoom = z;
72 self
73 }
74
75 /// Set the auto-zoom mode: 0 = disabled, 1 = static, 2 = adaptive.
76 ///
77 /// Values outside 0–2 are clamped.
78 #[must_use]
79 pub fn optzoom(mut self, mode: u8) -> Self {
80 self.optzoom = mode.clamp(0, 2);
81 self
82 }
83
84 /// Set the sub-pixel interpolation algorithm used during frame warping.
85 #[must_use]
86 pub fn interpol(mut self, i: Interpolation) -> Self {
87 self.interpol = i;
88 self
89 }
90}
91
92/// Two-pass video stabilization using `FFmpeg`'s `vidstabdetect` /
93/// `vidstabtransform` filters.
94///
95/// **Pass 1**: [`Stabilizer::analyze`] — motion analysis, produces a `.trf` file.
96/// **Pass 2**: [`Stabilizer::transform`] — correction, consumes the `.trf` file.
97pub struct Stabilizer;
98
99impl Stabilizer {
100 /// Analyze motion in `input` and write the transform file to `output_trf`.
101 ///
102 /// Runs a self-contained `FFmpeg` filter graph:
103 /// `movie → vidstabdetect → buffersink`.
104 /// The resulting `.trf` file is consumed by [`Stabilizer::transform`] in pass 2.
105 ///
106 /// # Errors
107 ///
108 /// Returns [`FilterError::Ffmpeg`] if:
109 /// - `vidstabdetect` is not available in the linked `FFmpeg` build.
110 /// - The input file is unreadable or does not exist.
111 /// - The filter graph cannot be configured or the `.trf` file cannot be written.
112 pub fn analyze(
113 input: &Path,
114 output_trf: &Path,
115 opts: &AnalyzeOptions,
116 ) -> Result<(), FilterError> {
117 // SAFETY: analyze_vidstab_unsafe manages all raw pointer lifetimes
118 // under the avfilter ownership rules: graph allocated with
119 // avfilter_graph_alloc(), built and configured, drained via
120 // av_buffersink_get_frame(), then freed before returning.
121 // All CString values are kept alive for the duration of the graph build.
122 unsafe { super::effects_inner::analyze_vidstab_unsafe(input, output_trf, opts) }
123 }
124
125 /// Apply motion transforms from the `.trf` file produced by [`Stabilizer::analyze`].
126 ///
127 /// Reads `input`, applies `vidstabtransform`, and writes the stabilized video
128 /// to `output` (re-encoded with the best available H.264 encoder).
129 ///
130 /// # Errors
131 ///
132 /// Returns [`FilterError::Ffmpeg`] if:
133 /// - `vidstabtransform` is not available in the linked `FFmpeg` build.
134 /// - `trf_path` does not exist or is unreadable.
135 /// - The input file is unreadable or does not exist.
136 /// - The output file cannot be created or encoded.
137 pub fn transform(
138 input: &Path,
139 trf_path: &Path,
140 output: &Path,
141 opts: &StabilizeOptions,
142 ) -> Result<(), FilterError> {
143 // SAFETY: transform_vidstab_unsafe manages all raw pointer lifetimes:
144 // - avfilter graph is allocated, built, drained, then freed.
145 // - AVCodecContext is allocated, opened, flushed, then freed.
146 // - AVFormatContext is allocated, written to, trailer flushed, then freed.
147 // All CString values are kept alive for the duration of each operation.
148 unsafe { super::effects_inner::transform_vidstab_unsafe(input, trf_path, output, opts) }
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn analyze_options_default_should_have_expected_values() {
158 let opts = AnalyzeOptions::default();
159 assert_eq!(opts.shakiness, 5);
160 assert_eq!(opts.accuracy, 15);
161 assert_eq!(opts.stepsize, 6);
162 }
163
164 #[test]
165 fn stabilize_options_default_should_have_expected_values() {
166 let opts = StabilizeOptions::default();
167 assert_eq!(opts.smoothing, 10);
168 assert!(opts.crop_black);
169 assert!((opts.zoom - 0.0_f32).abs() < f32::EPSILON);
170 assert_eq!(opts.optzoom, 0);
171 assert_eq!(opts.interpol, Interpolation::Bilinear);
172 }
173
174 #[test]
175 fn zoom_builder_should_set_zoom_field() {
176 let opts = StabilizeOptions::default().zoom(1.5);
177 assert!((opts.zoom - 1.5_f32).abs() < f32::EPSILON);
178 }
179
180 #[test]
181 fn optzoom_builder_should_set_optzoom_field() {
182 let opts = StabilizeOptions::default().optzoom(1);
183 assert_eq!(opts.optzoom, 1);
184 }
185
186 #[test]
187 fn optzoom_builder_should_clamp_above_maximum_to_two() {
188 let opts = StabilizeOptions::default().optzoom(5);
189 assert_eq!(opts.optzoom, 2);
190 }
191
192 #[test]
193 fn interpol_builder_should_set_interpol_field() {
194 let opts = StabilizeOptions::default().interpol(Interpolation::Bicubic);
195 assert_eq!(opts.interpol, Interpolation::Bicubic);
196 }
197}