Skip to main content

ff_filter/effects/
stabilizer.rs

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