1use 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 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
49pub struct AlmeidaEstimator {
58 use_ransac: bool,
61 num_iters: usize,
63 inlier_angle: f32,
65 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 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 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 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}