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}