1use super::motion::Motion;
5use ffcine_core::geom::{self, Mat3};
6
7#[derive(Debug, Clone, Copy, PartialEq, Default)]
10pub struct Pose {
11 pub tx: f64,
12 pub ty: f64,
13 pub angle: f64,
14 pub log_scale: f64,
15}
16
17impl Pose {
18 fn to_mat3(self, centre: [f64; 2]) -> Mat3 {
19 geom::similarity(centre, self.tx, self.ty, self.angle, self.log_scale.exp())
20 }
21
22 fn components(self) -> [f64; 4] {
23 [self.tx, self.ty, self.angle, self.log_scale]
24 }
25
26 fn from_components([tx, ty, angle, log_scale]: [f64; 4]) -> Self {
27 Self {
28 tx,
29 ty,
30 angle,
31 log_scale,
32 }
33 }
34}
35
36pub fn camera_path(motions: &[Motion], centre: [f64; 2]) -> Vec<Pose> {
41 let mut abs = geom::IDENTITY;
42 let mut out = Vec::with_capacity(motions.len());
43 let mut prev_angle = 0.0;
44 for (k, m) in motions.iter().enumerate() {
45 if k > 0 {
46 let inv = geom::inverse(&m.to_mat3(centre)).unwrap_or(geom::IDENTITY);
48 abs = geom::mul(&inv, &abs);
49 }
50 let (tx, ty, angle, scale) = geom::decompose_similarity(&abs, centre);
51 let tau = std::f64::consts::TAU;
53 let angle = angle + tau * ((prev_angle - angle) / tau).round();
54 prev_angle = angle;
55 out.push(Pose {
56 tx,
57 ty,
58 angle,
59 log_scale: scale.ln(),
60 });
61 }
62 out
63}
64
65pub fn gaussian(path: &[Pose], smoothing: u32) -> Vec<Pose> {
69 if smoothing == 0 {
70 return path.to_vec();
71 }
72 let mu = f64::from(smoothing);
73 let sigma2 = (mu / 2.0).powi(2);
74 let s = 2 * smoothing as usize + 1;
75 let kernel: Vec<f64> = (0..s)
76 .map(|i| (-(i as f64 - mu).powi(2) / sigma2).exp())
77 .collect();
78 let mu = smoothing as usize;
79 (0..path.len())
80 .map(|i| {
81 let (mut sum, mut wsum) = ([0.0; 4], 0.0);
82 for (k, &w) in kernel.iter().enumerate() {
83 let Some(idx) = (i + k).checked_sub(mu).filter(|&j| j < path.len()) else {
84 continue;
85 };
86 wsum += w;
87 for (acc, v) in sum.iter_mut().zip(path[idx].components()) {
88 *acc += w * v;
89 }
90 }
91 Pose::from_components(sum.map(|v| v / wsum))
92 })
93 .collect()
94}
95
96#[derive(Debug, Clone, Copy, PartialEq)]
98pub enum Zoom {
99 Auto { max: Option<f64> },
102 Fixed(f64),
104}
105
106impl Default for Zoom {
107 fn default() -> Self {
108 Self::Auto { max: None }
109 }
110}
111
112pub fn corrections(actual: &[Pose], smoothed: &[Pose], centre: [f64; 2]) -> Vec<Mat3> {
115 actual
116 .iter()
117 .zip(smoothed)
118 .map(|(a, s)| {
119 let s_inv = geom::inverse(&s.to_mat3(centre)).unwrap_or(geom::IDENTITY);
120 geom::mul(&a.to_mat3(centre), &s_inv)
121 })
122 .collect()
123}
124
125pub fn min_zoom(warps: &[Mat3], width: f64, height: f64) -> f64 {
130 let c = [width / 2.0, height / 2.0];
131 let corners = [[0.0, 0.0], [width, 0.0], [0.0, height], [width, height]];
132 let mut z: f64 = 1.0;
133 for m in warps {
134 let p0 = geom::apply(m, c);
135 if !(0.0..=width).contains(&p0[0]) || !(0.0..=height).contains(&p0[1]) {
136 return f64::INFINITY;
137 }
138 let mut u_max: f64 = 1.0;
141 for k in corners {
142 let d = [k[0] - c[0], k[1] - c[1]];
143 let v = [
144 m[0][0] * d[0] + m[0][1] * d[1],
145 m[1][0] * d[0] + m[1][1] * d[1],
146 ];
147 for (p, v, hi) in [(p0[0], v[0], width), (p0[1], v[1], height)] {
148 if v > 0.0 {
149 u_max = u_max.min((hi - p) / v);
150 } else if v < 0.0 {
151 u_max = u_max.min(p / -v);
152 }
153 }
154 }
155 z = z.max(1.0 / u_max);
156 }
157 z
158}
159
160pub fn choose_zoom(zoom: Zoom, needed: f64) -> f64 {
162 match zoom {
163 Zoom::Auto { max } => max.map_or(needed, |m| needed.min(m)),
164 Zoom::Fixed(z) => z,
165 }
166}
167
168pub fn apply_zoom(warps: &mut [Mat3], centre: [f64; 2], zoom: f64) {
170 let s = geom::scale_about(centre, 1.0 / zoom);
171 for m in warps {
172 *m = geom::mul(m, &s);
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 const C: [f64; 2] = [960.0, 540.0];
181
182 fn motion(tx: f64, ty: f64, angle: f64, scale: f64) -> Motion {
183 Motion {
184 tx,
185 ty,
186 angle,
187 scale,
188 confidence: 1.0,
189 }
190 }
191
192 #[test]
193 fn path_composes_motions() {
194 let m = vec![motion(-2.0, 0.0, 0.0, 1.0); 5];
196 let p = camera_path(&m, C);
197 assert_eq!(p.len(), 5);
198 assert!((p[4].tx - 8.0).abs() < 1e-9 && p[4].ty.abs() < 1e-9);
199 let m = vec![motion(0.0, 0.0, -0.01, 1.0); 4];
201 let p = camera_path(&m, C);
202 assert!((p[3].angle - 0.03).abs() < 1e-12 && p[3].tx.abs() < 1e-9);
203 }
204
205 #[test]
206 fn gaussian_matches_vidstab_kernel() {
207 let flat = vec![
209 Pose {
210 tx: 3.0,
211 ty: -1.0,
212 angle: 0.1,
213 log_scale: 0.01
214 };
215 30
216 ];
217 for (a, b) in gaussian(&flat, 10).iter().zip(&flat) {
218 assert!((a.tx - b.tx).abs() < 1e-12 && (a.angle - b.angle).abs() < 1e-12);
219 }
220 let mut imp = vec![Pose::default(); 41];
222 imp[20].tx = 1.0;
223 let out = gaussian(&imp, 4);
224 let k: Vec<f64> = (0..9)
225 .map(|i| (-(f64::from(i) - 4.0).powi(2) / 4.0).exp())
226 .collect();
227 let sum: f64 = k.iter().sum();
228 for d in 0..=4 {
229 assert!((out[20 + d].tx - k[4 + d] / sum).abs() < 1e-12);
230 }
231 assert!(out[25].tx.abs() < 1e-15);
232 let pan: Vec<Pose> = (0..60)
234 .map(|i| Pose {
235 tx: f64::from(i) * 2.0,
236 ..Pose::default()
237 })
238 .collect();
239 let out = gaussian(&pan, 20);
240 assert!((out[30].tx - 60.0).abs() < 1e-9);
241 }
242
243 #[test]
244 fn corrections_cancel_shake_and_zoom_hides_borders() {
245 let m: Vec<Motion> = (0..50)
247 .map(|i| {
248 let s = if i % 2 == 0 { 1.0 } else { -1.0 };
249 motion(8.0 * s, 0.0, 0.0035 * 2.0 * s, 1.0)
250 })
251 .collect();
252 let actual = camera_path(&m, C);
253 let smooth = gaussian(&actual, 15);
254 let w = corrections(&actual, &smooth, C);
255 for (i, wi) in w.iter().enumerate() {
257 let (tx, ty, a, _) = geom::decompose_similarity(wi, C);
258 let d = actual[i].tx - smooth[i].tx;
259 assert!(
260 d.abs() > 3.0 && (tx - d).abs() < 0.1,
261 "frame {i}: {tx} vs {d}"
262 );
263 assert!((ty - (actual[i].ty - smooth[i].ty)).abs() < 0.1);
264 assert!((a - (actual[i].angle - smooth[i].angle)).abs() < 1e-6);
265 }
266 let z = min_zoom(&w, 1920.0, 1080.0);
267 assert!(z > 1.0 && z < 1.03, "{z}");
268 let mut zw = w.clone();
269 apply_zoom(&mut zw, C, z);
270 let tol = 1e-6;
271 for m in &zw {
272 for k in [[0.0, 0.0], [1920.0, 0.0], [0.0, 1080.0], [1920.0, 1080.0]] {
273 let [x, y] = geom::apply(m, k);
274 assert!((-tol..=1920.0 + tol).contains(&x) && (-tol..=1080.0 + tol).contains(&y));
275 }
276 }
277 let mut zw = w;
279 apply_zoom(&mut zw, C, z * 0.999);
280 let out = zw.iter().any(|m| {
281 [[0.0, 0.0], [1920.0, 0.0], [0.0, 1080.0], [1920.0, 1080.0]]
282 .iter()
283 .any(|&k| {
284 let [x, y] = geom::apply(m, k);
285 !(0.0..=1920.0).contains(&x) || !(0.0..=1080.0).contains(&y)
286 })
287 });
288 assert!(out);
289 assert!((choose_zoom(Zoom::Auto { max: Some(1.001) }, z) - 1.001).abs() < 1e-12);
290 assert!((choose_zoom(Zoom::Fixed(1.1), z) - 1.1).abs() < 1e-12);
291 assert!((min_zoom(&[geom::IDENTITY], 1920.0, 1080.0) - 1.0).abs() < 1e-12);
292 }
293}