almeida_estimator/
lib.rs

1//! # Implementation of "Robust Estimation of Camera Motion Using Optical Flow Models".
2//!
3//! The following estimator produces relatively accurate rotation estimates, with no translational
4//! output.
5//!
6//! This is not a blind reimplementation of the paper - it contains several improvements to the
7//! methods used.
8
9use nalgebra as na;
10use ofps::prelude::v1::*;
11use rand::seq::SliceRandom;
12
13ofps::define_descriptor!(almeida, Estimator, |_| Ok(Box::new(
14    AlmeidaEstimator::default()
15)));
16
17const EPS: f32 = 0.001 * std::f32::consts::PI / 180.0;
18const ALPHA: f32 = 0.5;
19
20pub trait MotionModel {
21    fn roll(&self, coords: na::Point2<f32>, eps: f32) -> na::Vector2<f32>;
22
23    fn pitch(&self, coords: na::Point2<f32>, eps: f32) -> na::Vector2<f32>;
24
25    fn yaw(&self, coords: na::Point2<f32>, eps: f32) -> na::Vector2<f32>;
26
27    fn point_angle(&self, p: na::Point2<f32>) -> na::Vector2<f32>;
28}
29
30impl MotionModel for StandardCamera {
31    fn roll(&self, coords: na::Point2<f32>, eps: f32) -> na::Vector2<f32> {
32        // We use different camera axis compared to what nalgebra considers as RPY
33        self.delta(coords, na::Matrix4::from_euler_angles(0.0, eps, 0.0))
34    }
35
36    fn pitch(&self, coords: na::Point2<f32>, eps: f32) -> na::Vector2<f32> {
37        self.delta(coords, na::Matrix4::from_euler_angles(eps, 0.0, 0.0))
38    }
39
40    fn yaw(&self, coords: na::Point2<f32>, eps: f32) -> na::Vector2<f32> {
41        self.delta(coords, na::Matrix4::from_euler_angles(0.0, 0.0, -eps))
42    }
43
44    fn point_angle(&self, p: na::Point2<f32>) -> na::Vector2<f32> {
45        StandardCamera::point_angle(self, p)
46    }
47}
48
49/// Motion estimator built on a research paper titled "Robust Estimation
50/// of Camera Motion Using Optical Flow Models".
51///
52/// Authors:
53///
54/// Jurandy Almeida, Rodrigo Minetto, Tiago A. Almeida, Ricardo da S. Torres, and Neucimar J. Leite.
55///
56/// This estimator only produces rotational output, and no translation.
57pub struct AlmeidaEstimator {
58    /// True if ransac is used. False to perform least
59    /// squares minimisation solution.
60    use_ransac: bool,
61    /// Number of iterations for ransac.
62    num_iters: usize,
63    /// Target angle error in degrees for the sample to be considered as inlier.
64    inlier_angle: f32,
65    /// Number of samples per each ransac iteration.
66    ransac_samples: usize,
67}
68
69impl Default for AlmeidaEstimator {
70    fn default() -> Self {
71        Self {
72            use_ransac: true,
73            num_iters: 200,
74            inlier_angle: 0.05,
75            ransac_samples: 1000,
76        }
77    }
78}
79
80impl Properties for AlmeidaEstimator {
81    fn props_mut(&mut self) -> Vec<(&str, PropertyMut)> {
82        vec![
83            ("Use ransac", PropertyMut::bool(&mut self.use_ransac)),
84            (
85                "Ransac iters",
86                PropertyMut::usize(&mut self.num_iters, 1, 500),
87            ),
88            (
89                "Inlier threshold",
90                PropertyMut::float(&mut self.inlier_angle, 0.01, 1.0),
91            ),
92            (
93                "Ransac samples",
94                PropertyMut::usize(&mut self.ransac_samples, 100, 16000),
95            ),
96        ]
97    }
98}
99
100impl Estimator for AlmeidaEstimator {
101    fn estimate(
102        &mut self,
103        motion_vectors: &[MotionEntry],
104        camera: &StandardCamera,
105        _move_magnitude: Option<f32>,
106    ) -> Result<(na::UnitQuaternion<f32>, na::Vector3<f32>)> {
107        let rot = if self.use_ransac {
108            solve_ypr_ransac(
109                motion_vectors,
110                camera,
111                self.num_iters,
112                self.inlier_angle,
113                self.ransac_samples,
114            )
115        } else {
116            solve_ypr_given(motion_vectors, camera)
117        };
118
119        Ok((rot, na::Vector3::default()))
120    }
121}
122
123fn solve_ypr_given(input: &[MotionEntry], camera: &StandardCamera) -> na::UnitQuaternion<f32> {
124    let dot = |a: usize, b: usize| move |vecs: &[na::Vector2<f32>]| vecs[a].dot(&vecs[b]);
125
126    fn dot_map<T: Fn(&[na::Vector2<f32>]) -> f32>(
127        motion: &[(na::Point2<f32>, [na::Vector2<f32>; 4])],
128    ) -> (impl Fn(T) -> f32 + '_) {
129        move |dot| motion.iter().map(|(_, v)| dot(v)).sum::<f32>()
130    }
131
132    let limit = (15.0 / ALPHA).ceil() as usize;
133
134    let mut rotation = na::UnitQuaternion::identity();
135
136    // Iterative optimisation loop.
137    for i in 0..limit {
138        let alpha = if i == limit - 1 { 1.0 } else { ALPHA };
139
140        let rotm = rotation.to_homogeneous();
141
142        let motion = input
143            .iter()
144            .copied()
145            .map(|(pos, motion)| {
146                let delta = camera.delta(pos, rotm);
147                (
148                    pos + delta,
149                    [
150                        motion - delta,
151                        camera.roll(pos, EPS),
152                        camera.pitch(pos, EPS),
153                        camera.yaw(pos, EPS),
154                    ],
155                )
156            })
157            .collect::<Vec<_>>();
158
159        let a = na::Matrix3::from_iterator(
160            [
161                dot(1, 1),
162                dot(1, 2),
163                dot(1, 3),
164                dot(2, 1),
165                dot(2, 2),
166                dot(2, 3),
167                dot(3, 1),
168                dot(3, 2),
169                dot(3, 3),
170            ]
171            .iter()
172            .map(dot_map(&motion)),
173        );
174
175        let b = na::Vector3::from_iterator(
176            [dot(1, 0), dot(2, 0), dot(3, 0)]
177                .iter()
178                .map(dot_map(&motion)),
179        );
180
181        let decomp = a.lu();
182
183        let model = decomp.solve(&b).unwrap_or_default();
184
185        let model = model * EPS * alpha;
186
187        // Apply rotation in YRP order, as it is more correct.
188
189        let roll = na::UnitQuaternion::from_euler_angles(0.0, model.x, 0.0);
190        let pitch = na::UnitQuaternion::from_euler_angles(model.y, 0.0, 0.0);
191        let yaw = na::UnitQuaternion::from_euler_angles(0.0, 0.0, -model.z);
192
193        let rot = pitch * roll * yaw;
194
195        rotation *= rot;
196    }
197
198    // We estimated how points rotate, not how the camera rotates - take inverse.
199    rotation.inverse()
200}
201
202fn solve_ypr_ransac(
203    field: &[MotionEntry],
204    camera: &StandardCamera,
205    num_iters: usize,
206    target_delta: f32,
207    num_samples: usize,
208) -> na::UnitQuaternion<f32> {
209    let mut best_inliers = vec![];
210    let target_delta = target_delta.to_radians();
211
212    let rng = &mut rand::thread_rng();
213
214    for _ in 0..num_iters {
215        let samples = field.choose_multiple(rng, 3).copied().collect::<Vec<_>>();
216
217        let fit = solve_ypr_given(&samples, camera);
218
219        let motion = field
220            .choose_multiple(rng, num_samples)
221            .copied()
222            .collect::<Vec<_>>();
223
224        let mat = fit.inverse().to_homogeneous();
225
226        let inliers = motion
227            .iter()
228            .copied()
229            .map(|(pos, vec)| {
230                let delta = camera.delta(pos, mat);
231                ((pos, vec), (pos + delta, vec - delta))
232            })
233            .filter(|(_, (sample, vec))| {
234                let angle = camera.point_angle(*sample);
235                let cosang = na::matrix![angle.x.cos(); angle.y.cos()];
236                vec.component_mul(&cosang).magnitude_squared() <= target_delta * target_delta
237            })
238            .map(|(a, _)| a)
239            .collect::<Vec<_>>();
240
241        if inliers.len() > best_inliers.len() {
242            best_inliers = inliers;
243        }
244    }
245
246    if best_inliers.len() >= 3 {
247        solve_ypr_given(&best_inliers, camera)
248    } else {
249        Default::default()
250    }
251}
252
253#[cfg(test)]
254pub mod tests {
255    use super::*;
256
257    fn get_grid(
258        grid_cnt_x: usize,
259        grid_cnt_y: usize,
260        camera: &StandardCamera,
261    ) -> Vec<na::Point3<f32>> {
262        let mut new_points = vec![];
263
264        for x in 0..grid_cnt_x {
265            for y in 0..grid_cnt_y {
266                let x = x as f32 / grid_cnt_x as f32;
267                let y = y as f32 / grid_cnt_y as f32;
268                new_points.push(na::Point2::new(x, y));
269            }
270        }
271
272        let view = calc_view(Default::default(), Default::default());
273
274        new_points
275            .into_iter()
276            .map(|p| camera.unproject(p, view))
277            .collect()
278    }
279
280    fn calc_view(rot: na::UnitQuaternion<f32>, pos: na::Point3<f32>) -> na::Matrix4<f32> {
281        na::Matrix4::look_at_rh(
282            &pos,
283            &(pos + rot.transform_vector(&na::Vector3::new(0.0, -1.0, 0.0))),
284            &rot.transform_vector(&na::Vector3::new(0.0, 0.0, 1.0)),
285        )
286    }
287
288    fn project_grid<'a>(
289        grid: impl IntoIterator<Item = &'a na::Point3<f32>>,
290        camera: &StandardCamera,
291        view: na::Matrix4<f32>,
292    ) -> Vec<na::Point2<f32>> {
293        grid.into_iter().map(|&p| camera.project(p, view)).collect()
294    }
295
296    fn calc_field(
297        p1: impl IntoIterator<Item = na::Point2<f32>>,
298        p2: impl IntoIterator<Item = na::Point2<f32>>,
299    ) -> Vec<MotionEntry> {
300        let mid = na::Point2::new(0.5, 0.5);
301        p1.into_iter()
302            .zip(p2.into_iter())
303            .filter(|(p1, p2)| (p1 - mid).magnitude() <= 0.71 || (p2 - mid).magnitude() <= 0.71)
304            .map(|(p1, p2)| (p1, p2 - p1))
305            .collect()
306    }
307
308    fn test_rot(mut estimator: AlmeidaEstimator) {
309        let camera = StandardCamera::new(1.0, 90.0);
310
311        let grid = get_grid(50, 50, &camera);
312
313        for rot in [0.01f32, 0.1, 1.0, 10.0] {
314            let angles = [
315                (0.0, 0.0, 0.0),
316                (rot, 0.0, 0.0),
317                (0.0, rot, 0.0),
318                (0.0, 0.0, rot),
319                (rot, rot, 0.0),
320                (rot, 0.0, rot),
321                (0.0, rot, rot),
322                (rot, rot, rot),
323            ];
324
325            for q in angles.iter().map(|&(r, p, y)| {
326                na::UnitQuaternion::from_euler_angles(
327                    r.to_radians(),
328                    p.to_radians(),
329                    y.to_radians(),
330                )
331            }) {
332                let p1 = project_grid(
333                    &grid,
334                    &camera,
335                    calc_view(Default::default(), Default::default()),
336                );
337                let p2 = project_grid(&grid, &camera, calc_view(q, Default::default()));
338
339                let field = calc_field(p1, p2);
340
341                let (r, _) = estimator.estimate(&field, &camera, None).unwrap();
342
343                let delta = q.angle_to(&r).to_degrees();
344
345                println!("E: {}", delta / rot);
346
347                assert!(
348                    delta < 0.1 * rot,
349                    "{:?} vs {:?}: {} > {}",
350                    q.euler_angles(),
351                    r.euler_angles(),
352                    delta,
353                    0.1 * rot
354                );
355            }
356        }
357    }
358
359    #[test]
360    fn test_rotation_default() {
361        let mut estimator = AlmeidaEstimator::default();
362        estimator.use_ransac = false;
363        test_rot(estimator);
364    }
365
366    #[test]
367    fn test_rotation_ransac() {
368        let mut estimator = AlmeidaEstimator::default();
369        estimator.use_ransac = true;
370        estimator.num_iters = 100;
371        test_rot(estimator);
372    }
373}