Skip to main content

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}