cv_geom/lib.rs
1//! This crate contains computational geometry algorithms for [Rust CV](https://github.com/rust-cv/).
2//!
3//! ## Triangulation
4//!
5//! In this problem we know the relative pose of cameras and the [`Bearing`] of the same feature
6//! observed in each camera frame. We want to find the point of intersection from all cameras.
7//!
8//! - `p` the point we are trying to triangulate
9//! - `a` the normalized keypoint on camera A
10//! - `b` the normalized keypoint on camera B
11//! - `O` the optical center of a camera
12//! - `@` the virtual image plane
13//!
14//! ```text
15//! @
16//! @
17//! p--------b--------O
18//! / @
19//! / @
20//! / @
21//! / @
22//! @@@@@@@a@@@@@
23//! /
24//! /
25//! /
26//! O
27//! ```
28
29#![no_std]
30
31use cv_core::nalgebra::{zero, Matrix3x4, Matrix4, RowVector4};
32use cv_core::{
33 Bearing, CameraPoint, CameraToCamera, Pose, TriangulatorObservances, TriangulatorRelative,
34 WorldPoint, WorldToCamera,
35};
36
37/// This solves triangulation problems by simply minimizing the squared reprojection error of all observances.
38///
39/// This is a quick triangulator to execute and is fairly robust.
40///
41/// ```
42/// use cv_core::nalgebra::{Vector3, Point3, Rotation3, Unit};
43/// use cv_core::{TriangulatorRelative, CameraToCamera, CameraPoint, Pose, Projective};
44/// use cv_geom::MinSquaresTriangulator;
45///
46/// let point = CameraPoint::from_point(Point3::new(0.3, 0.1, 2.0));
47/// let pose = CameraToCamera::from_parts(Vector3::new(0.1, 0.1, 0.1), Rotation3::new(Vector3::new(0.1, 0.1, 0.1)));
48/// let bearing_a = point.bearing();
49/// let bearing_b = pose.transform(point).bearing();
50/// let triangulated = MinSquaresTriangulator::new().triangulate_relative(pose, bearing_a, bearing_b).unwrap();
51/// let distance = (point.point().unwrap().coords - triangulated.point().unwrap().coords).norm();
52/// assert!(distance < 1e-6);
53/// ```
54#[derive(Copy, Clone, Debug)]
55pub struct MinSquaresTriangulator {
56 epsilon: f64,
57 max_iterations: usize,
58}
59
60impl MinSquaresTriangulator {
61 /// Creates a `MinSquaresTriangulator` with default values.
62 ///
63 /// Same as calling [`Default::default`].
64 pub fn new() -> Self {
65 Default::default()
66 }
67
68 /// Set the epsilon used in the symmetric eigen solver.
69 ///
70 /// Default is `1e-9`.
71 pub fn epsilon(self, epsilon: f64) -> Self {
72 Self { epsilon, ..self }
73 }
74
75 /// Set the maximum number of iterations for the symmetric eigen solver.
76 ///
77 /// Default is `100`.
78 pub fn max_iterations(self, max_iterations: usize) -> Self {
79 Self {
80 max_iterations,
81 ..self
82 }
83 }
84}
85
86impl Default for MinSquaresTriangulator {
87 fn default() -> Self {
88 Self {
89 epsilon: 1e-9,
90 max_iterations: 100,
91 }
92 }
93}
94
95impl TriangulatorObservances for MinSquaresTriangulator {
96 fn triangulate_observances<B: Bearing>(
97 &self,
98 pairs: impl IntoIterator<Item = (WorldToCamera, B)>,
99 ) -> Option<WorldPoint> {
100 let mut a: Matrix4<f64> = zero();
101
102 for (pose, bearing) in pairs {
103 // Get the normalized bearing.
104 let bearing = bearing.bearing().into_inner();
105 // Get the pose as a 3x4 matrix.
106 let rot = pose.0.rotation.matrix();
107 let trans = pose.0.translation.vector;
108 let pose = Matrix3x4::<f64>::from_columns(&[
109 rot.column(0),
110 rot.column(1),
111 rot.column(2),
112 trans.column(0),
113 ]);
114 // Set up the least squares problem.
115 let term = pose - bearing * bearing.transpose() * pose;
116 a += term.transpose() * term;
117 }
118
119 let se = a.try_symmetric_eigen(self.epsilon, self.max_iterations)?;
120
121 se.eigenvalues
122 .iter()
123 .enumerate()
124 .min_by_key(|&(_, &n)| float_ord::FloatOrd(n))
125 .map(|(ix, _)| se.eigenvectors.column(ix).into_owned())
126 .map(|v| if v.w.is_sign_negative() { -v } else { v })
127 .map(Into::into)
128 }
129}
130
131/// Based on algorithm 12 from "Multiple View Geometry in Computer Vision, Second Edition"
132#[derive(Copy, Clone, Debug)]
133pub struct RelativeDltTriangulator {
134 epsilon: f64,
135 max_iterations: usize,
136}
137
138impl RelativeDltTriangulator {
139 /// Creates a `RelativeDltTriangulator` with default values.
140 ///
141 /// Same as calling [`Default::default`].
142 pub fn new() -> Self {
143 Default::default()
144 }
145
146 /// Set the epsilon used in the SVD solver.
147 ///
148 /// Default is `1e-9`.
149 pub fn epsilon(self, epsilon: f64) -> Self {
150 Self { epsilon, ..self }
151 }
152
153 /// Set the maximum number of iterations for the SVD solver.
154 ///
155 /// Default is `100`.
156 pub fn max_iterations(self, max_iterations: usize) -> Self {
157 Self {
158 max_iterations,
159 ..self
160 }
161 }
162}
163
164impl Default for RelativeDltTriangulator {
165 fn default() -> Self {
166 Self {
167 epsilon: 1e-9,
168 max_iterations: 100,
169 }
170 }
171}
172
173impl TriangulatorRelative for RelativeDltTriangulator {
174 fn triangulate_relative<A: Bearing, B: Bearing>(
175 &self,
176 relative_pose: CameraToCamera,
177 a: A,
178 b: B,
179 ) -> Option<CameraPoint> {
180 let pose = relative_pose.homogeneous();
181 let a = a.bearing_unnormalized();
182 let b = b.bearing_unnormalized();
183 let mut design = Matrix4::zeros();
184 design
185 .row_mut(0)
186 .copy_from(&RowVector4::new(-a.z, 0.0, a.x, 0.0));
187 design
188 .row_mut(1)
189 .copy_from(&RowVector4::new(0.0, -a.z, a.y, 0.0));
190 design
191 .row_mut(2)
192 .copy_from(&(b.x * pose.row(2) - b.z * pose.row(0)));
193 design
194 .row_mut(3)
195 .copy_from(&(b.y * pose.row(2) - b.z * pose.row(1)));
196
197 let svd = design.try_svd(false, true, self.epsilon, self.max_iterations)?;
198
199 // Extract the null-space vector from V* corresponding to the smallest
200 // singular value and then normalize it back from heterogeneous coordinates.
201 svd.singular_values
202 .iter()
203 .enumerate()
204 .min_by_key(|&(_, &n)| float_ord::FloatOrd(n))
205 .map(|(ix, _)| svd.v_t.unwrap().row(ix).transpose().into_owned())
206 .map(|v| if v.w.is_sign_negative() { -v } else { v })
207 .map(Into::into)
208 }
209}