1use spatialrust_math::{
4 solve_linear_system, symmetric_eigen3, LeastSquaresResult, Mat3, Vec2, Vec3,
5};
6
7use crate::{
8 CameraMatrix3, Essential3, Fundamental3, GeometricEstimate, Homography3, PointCorrespondence2,
9 RelativePose, RelativePoseEstimate, RobustEstimationOptions, TriangulatedPoint, VisionError,
10 VisionResult,
11};
12
13pub fn estimate_homography(correspondences: &[PointCorrespondence2]) -> VisionResult<Homography3> {
15 if correspondences.len() < 4 {
16 return Err(VisionError::InvalidParameter(
17 "homography estimation requires at least four correspondences".into(),
18 ));
19 }
20 let source = correspondences.iter().map(|pair| pair.source()).collect::<Vec<_>>();
21 let target = correspondences.iter().map(|pair| pair.target()).collect::<Vec<_>>();
22 let source_normalization = Normalization2::from_points(&source)?;
23 let target_normalization = Normalization2::from_points(&target)?;
24 let mut normal = vec![vec![0.0; 8]; 8];
25 let mut rhs = vec![0.0; 8];
26 for pair in correspondences {
27 let source = source_normalization.apply(pair.source());
28 let target = target_normalization.apply(pair.target());
29 accumulate_least_squares(
30 &mut normal,
31 &mut rhs,
32 &[source.x, source.y, 1.0, 0.0, 0.0, 0.0, -target.x * source.x, -target.x * source.y],
33 target.x,
34 );
35 accumulate_least_squares(
36 &mut normal,
37 &mut rhs,
38 &[0.0, 0.0, 0.0, source.x, source.y, 1.0, -target.y * source.x, -target.y * source.y],
39 target.y,
40 );
41 }
42 let LeastSquaresResult::Solved(solution) = solve_linear_system(normal, rhs) else {
43 return Err(VisionError::InvalidParameter(
44 "homography correspondences are degenerate".into(),
45 ));
46 };
47 let normalized = Mat3::from_rows(
48 [solution[0], solution[1], solution[2]],
49 [solution[3], solution[4], solution[5]],
50 [solution[6], solution[7], 1.0],
51 );
52 Homography3::try_new(
53 target_normalization.inverse.mul_mat3(normalized).mul_mat3(source_normalization.matrix),
54 )
55}
56
57pub fn estimate_fundamental(
62 correspondences: &[PointCorrespondence2],
63) -> VisionResult<Fundamental3> {
64 if correspondences.len() < 8 {
65 return Err(VisionError::InvalidParameter(
66 "fundamental estimation requires at least eight correspondences".into(),
67 ));
68 }
69 let source = correspondences.iter().map(|pair| pair.source()).collect::<Vec<_>>();
70 let target = correspondences.iter().map(|pair| pair.target()).collect::<Vec<_>>();
71 let source_normalization = Normalization2::from_points(&source)?;
72 let target_normalization = Normalization2::from_points(&target)?;
73 let mut normal = vec![vec![0.0; 9]; 9];
74 for pair in correspondences {
75 let source = source_normalization.apply(pair.source());
76 let target = target_normalization.apply(pair.target());
77 let row = [
78 target.x * source.x,
79 target.x * source.y,
80 target.x,
81 target.y * source.x,
82 target.y * source.y,
83 target.y,
84 source.x,
85 source.y,
86 1.0,
87 ];
88 for row_index in 0..9 {
89 for column in 0..9 {
90 normal[row_index][column] += row[row_index] * row[column];
91 }
92 }
93 }
94 let vector = smallest_symmetric_eigenvector(normal).ok_or_else(|| {
95 VisionError::InvalidParameter("fundamental correspondences are degenerate".into())
96 })?;
97 let normalized = enforce_rank_two(Mat3::from_rows(
98 [vector[0], vector[1], vector[2]],
99 [vector[3], vector[4], vector[5]],
100 [vector[6], vector[7], vector[8]],
101 ));
102 Fundamental3::try_new(
103 target_normalization
104 .matrix
105 .transpose()
106 .mul_mat3(normalized)
107 .mul_mat3(source_normalization.matrix),
108 )
109}
110
111pub fn estimate_homography_ransac(
113 correspondences: &[PointCorrespondence2],
114 options: RobustEstimationOptions,
115) -> VisionResult<GeometricEstimate<Homography3>> {
116 robust_estimate(correspondences, options, 4, estimate_homography, homography_residual)
117}
118
119pub fn estimate_fundamental_ransac(
121 correspondences: &[PointCorrespondence2],
122 options: RobustEstimationOptions,
123) -> VisionResult<GeometricEstimate<Fundamental3>> {
124 robust_estimate(correspondences, options, 8, estimate_fundamental, fundamental_residual)
125}
126
127pub fn estimate_essential(
129 correspondences: &[PointCorrespondence2],
130 source_camera: CameraMatrix3,
131 target_camera: CameraMatrix3,
132) -> VisionResult<Essential3> {
133 let normalized = normalize_correspondences(correspondences, source_camera, target_camera)?;
134 estimate_essential_normalized(&normalized)
135}
136
137pub fn estimate_essential_ransac(
142 correspondences: &[PointCorrespondence2],
143 source_camera: CameraMatrix3,
144 target_camera: CameraMatrix3,
145 options: RobustEstimationOptions,
146) -> VisionResult<GeometricEstimate<Essential3>> {
147 let normalized = normalize_correspondences(correspondences, source_camera, target_camera)?;
148 robust_estimate(&normalized, options, 8, estimate_essential_normalized, essential_residual)
149}
150
151pub fn triangulate_correspondence(
153 correspondence: PointCorrespondence2,
154 source_camera: CameraMatrix3,
155 target_camera: CameraMatrix3,
156 pose: RelativePose,
157) -> VisionResult<TriangulatedPoint> {
158 let source = source_camera.normalize_pixel(correspondence.source());
159 let target = target_camera.normalize_pixel(correspondence.target());
160 triangulate_normalized(
161 Vec2 { x: source.x, y: source.y },
162 Vec2 { x: target.x, y: target.y },
163 pose,
164 )
165 .ok_or_else(|| VisionError::InvalidParameter("triangulation is degenerate".into()))
166}
167
168pub fn recover_relative_pose(
170 essential: Essential3,
171 correspondences: &[PointCorrespondence2],
172 source_camera: CameraMatrix3,
173 target_camera: CameraMatrix3,
174) -> VisionResult<RelativePoseEstimate> {
175 if correspondences.is_empty() {
176 return Err(VisionError::InvalidParameter(
177 "pose recovery requires at least one correspondence".into(),
178 ));
179 }
180 let normalized = normalize_correspondences(correspondences, source_camera, target_camera)?;
181 let (mut left, mut right) = essential_singular_vectors(essential.matrix())?;
182 if determinant(left) < 0.0 {
183 negate_column(&mut left, 2);
184 }
185 if determinant(right) < 0.0 {
186 negate_column(&mut right, 2);
187 }
188 let w = Mat3::from_rows([0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]);
189 let rotations = [
190 proper_rotation(left.mul_mat3(w).mul_mat3(right.transpose())),
191 proper_rotation(left.mul_mat3(w.transpose()).mul_mat3(right.transpose())),
192 ];
193 let translation = column(left, 2).normalize();
194 let mut best: Option<(RelativePose, Vec<Option<TriangulatedPoint>>, usize, f64)> = None;
195 for rotation in rotations {
196 for translation in [translation, Vec3::new(-translation.x, -translation.y, -translation.z)]
197 {
198 let pose = RelativePose::try_new(rotation, translation)?;
199 let points = normalized
200 .iter()
201 .map(|pair| triangulate_normalized(pair.source(), pair.target(), pose))
202 .collect::<Vec<_>>();
203 let count = points.iter().flatten().filter(|point| point.has_positive_depth()).count();
204 let error = points
205 .iter()
206 .flatten()
207 .filter(|point| point.has_positive_depth())
208 .map(|point| point.reprojection_error())
209 .sum::<f64>();
210 if best.as_ref().map_or(true, |candidate| {
211 count > candidate.2 || (count == candidate.2 && error < candidate.3)
212 }) {
213 best = Some((pose, points, count, error));
214 }
215 }
216 }
217 let (pose, points, _, _) = best.expect("four essential-pose candidates");
218 Ok(RelativePoseEstimate::new(pose, points))
219}
220
221fn normalize_correspondences(
222 correspondences: &[PointCorrespondence2],
223 source_camera: CameraMatrix3,
224 target_camera: CameraMatrix3,
225) -> VisionResult<Vec<PointCorrespondence2>> {
226 correspondences
227 .iter()
228 .map(|pair| {
229 let source = source_camera.normalize_pixel(pair.source());
230 let target = target_camera.normalize_pixel(pair.target());
231 PointCorrespondence2::try_new(
232 Vec2 { x: source.x, y: source.y },
233 Vec2 { x: target.x, y: target.y },
234 )
235 })
236 .collect()
237}
238
239fn estimate_essential_normalized(
240 correspondences: &[PointCorrespondence2],
241) -> VisionResult<Essential3> {
242 let fundamental = estimate_fundamental(correspondences)?;
243 Essential3::try_new(project_essential(fundamental.matrix())?)
244}
245
246fn robust_estimate<Model: Copy>(
247 correspondences: &[PointCorrespondence2],
248 options: RobustEstimationOptions,
249 sample_size: usize,
250 estimate: fn(&[PointCorrespondence2]) -> VisionResult<Model>,
251 residual: fn(Model, PointCorrespondence2) -> f64,
252) -> VisionResult<GeometricEstimate<Model>> {
253 let options = options.validate()?;
254 if correspondences.len() < sample_size {
255 return Err(VisionError::InvalidParameter(format!(
256 "robust estimation requires at least {sample_size} correspondences"
257 )));
258 }
259 let mut rng = XorShift64::new(options.seed);
260 let mut best: Option<(Model, Vec<bool>, Vec<f64>, usize, f64)> = None;
261 let mut iteration_limit = options.max_iterations;
262 let mut iteration = 0;
263 while iteration < iteration_limit {
264 let indices = sample_unique(&mut rng, correspondences.len(), sample_size);
265 let sample = indices.iter().map(|&index| correspondences[index]).collect::<Vec<_>>();
266 if let Ok(model) = estimate(&sample) {
267 let residuals = correspondences
268 .iter()
269 .copied()
270 .map(|pair| residual(model, pair))
271 .collect::<Vec<_>>();
272 let inliers =
273 residuals.iter().map(|&value| value <= options.threshold).collect::<Vec<_>>();
274 let count = inliers.iter().filter(|&&value| value).count();
275 let error = residuals
276 .iter()
277 .zip(&inliers)
278 .filter_map(|(value, &is_inlier)| is_inlier.then_some(*value))
279 .sum::<f64>();
280 let improves = best.as_ref().map_or(true, |candidate| {
281 count > candidate.3 || (count == candidate.3 && error < candidate.4)
282 });
283 if improves {
284 if count >= sample_size {
285 let inlier_ratio = count as f64 / correspondences.len() as f64;
286 let success = inlier_ratio.powi(sample_size as i32).clamp(0.0, 1.0);
287 if success > 0.0 && success < 1.0 {
288 let required = ((1.0 - options.confidence).ln() / (1.0 - success).ln())
289 .ceil()
290 .max(1.0) as usize;
291 iteration_limit = iteration_limit.min(required.max(iteration + 1));
292 } else if success == 1.0 {
293 iteration_limit = iteration + 1;
294 }
295 }
296 best = Some((model, inliers, residuals, count, error));
297 }
298 }
299 iteration += 1;
300 }
301 let (_, best_inliers, _, count, _) = best.ok_or_else(|| {
302 VisionError::InvalidParameter("robust geometry estimation found no valid model".into())
303 })?;
304 if count < sample_size {
305 return Err(VisionError::InvalidParameter(
306 "robust geometry estimation found too few inliers".into(),
307 ));
308 }
309 let inlier_pairs = correspondences
310 .iter()
311 .zip(&best_inliers)
312 .filter_map(|(&pair, &is_inlier)| is_inlier.then_some(pair))
313 .collect::<Vec<_>>();
314 let refined = estimate(&inlier_pairs)?;
315 let residuals =
316 correspondences.iter().copied().map(|pair| residual(refined, pair)).collect::<Vec<_>>();
317 let inliers = residuals.iter().map(|&value| value <= options.threshold).collect();
318 GeometricEstimate::try_new(refined, correspondences.len(), inliers, residuals)
319}
320
321fn homography_residual(model: Homography3, pair: PointCorrespondence2) -> f64 {
322 let projected = model.matrix().mul_vec3(Vec3::new(pair.source().x, pair.source().y, 1.0));
323 if projected.z.abs() <= f64::EPSILON {
324 return f64::MAX;
325 }
326 let dx = projected.x / projected.z - pair.target().x;
327 let dy = projected.y / projected.z - pair.target().y;
328 dx.hypot(dy)
329}
330
331fn fundamental_residual(model: Fundamental3, pair: PointCorrespondence2) -> f64 {
332 epipolar_residual(model.matrix(), pair)
333}
334
335fn essential_residual(model: Essential3, pair: PointCorrespondence2) -> f64 {
336 epipolar_residual(model.matrix(), pair)
337}
338
339fn epipolar_residual(matrix: Mat3<f64>, pair: PointCorrespondence2) -> f64 {
340 let source = Vec3::new(pair.source().x, pair.source().y, 1.0);
341 let target = Vec3::new(pair.target().x, pair.target().y, 1.0);
342 let line_target = matrix.mul_vec3(source);
343 let line_source = matrix.transpose().mul_vec3(target);
344 let numerator = target.dot(line_target).abs();
345 let denominator = line_target.x * line_target.x
346 + line_target.y * line_target.y
347 + line_source.x * line_source.x
348 + line_source.y * line_source.y;
349 if denominator <= f64::EPSILON {
350 f64::MAX
351 } else {
352 numerator / denominator.sqrt()
353 }
354}
355
356fn project_essential(matrix: Mat3<f64>) -> VisionResult<Mat3<f64>> {
357 let (left, right) = essential_singular_vectors(matrix)?;
358 let covariance = matrix.transpose().mul_mat3(matrix);
359 let eigen = symmetric_eigen3(covariance);
360 let singular = [eigen.eigenvalues[2].max(0.0).sqrt(), eigen.eigenvalues[1].max(0.0).sqrt()];
361 let scale = 0.5 * (singular[0] + singular[1]);
362 let mut result = Mat3::from_rows([0.0; 3], [0.0; 3], [0.0; 3]);
363 for component in 0..2 {
364 let left_column = column(left, component);
365 let right_column = column(right, component);
366 let left_values = [left_column.x, left_column.y, left_column.z];
367 let right_values = [right_column.x, right_column.y, right_column.z];
368 for (row, &left_value) in left_values.iter().enumerate() {
369 for (column, &right_value) in right_values.iter().enumerate() {
370 result.m[row][column] += scale * left_value * right_value;
371 }
372 }
373 }
374 Ok(result)
375}
376
377fn essential_singular_vectors(matrix: Mat3<f64>) -> VisionResult<(Mat3<f64>, Mat3<f64>)> {
378 let eigen = symmetric_eigen3(matrix.transpose().mul_mat3(matrix));
379 let right0 = column(eigen.eigenvectors, 2).normalize();
380 let right1 = column(eigen.eigenvectors, 1).normalize();
381 let right2 = right0.cross(right1).normalize();
382 let sigma0 = eigen.eigenvalues[2].max(0.0).sqrt();
383 let sigma1 = eigen.eigenvalues[1].max(0.0).sqrt();
384 if sigma0 <= 1e-12 || sigma1 <= 1e-12 {
385 return Err(VisionError::InvalidParameter(
386 "essential matrix has fewer than two non-zero singular values".into(),
387 ));
388 }
389 let left0 = scale_vec(matrix.mul_vec3(right0), 1.0 / sigma0).normalize();
390 let mut left1 = scale_vec(matrix.mul_vec3(right1), 1.0 / sigma1);
391 left1 = (left1 - scale_vec(left0, left0.dot(left1))).normalize();
392 let left2 = left0.cross(left1).normalize();
393 Ok((matrix_from_columns(left0, left1, left2), matrix_from_columns(right0, right1, right2)))
394}
395
396fn triangulate_normalized(
397 source: Vec2<f64>,
398 target: Vec2<f64>,
399 pose: RelativePose,
400) -> Option<TriangulatedPoint> {
401 let rotation = pose.rotation();
402 let translation = pose.translation();
403 let rows = [
404 vec![-1.0, 0.0, source.x, 0.0],
405 vec![0.0, -1.0, source.y, 0.0],
406 vec![
407 target.x * rotation.m[2][0] - rotation.m[0][0],
408 target.x * rotation.m[2][1] - rotation.m[0][1],
409 target.x * rotation.m[2][2] - rotation.m[0][2],
410 target.x * translation.z - translation.x,
411 ],
412 vec![
413 target.y * rotation.m[2][0] - rotation.m[1][0],
414 target.y * rotation.m[2][1] - rotation.m[1][1],
415 target.y * rotation.m[2][2] - rotation.m[1][2],
416 target.y * translation.z - translation.y,
417 ],
418 ];
419 let mut normal = vec![vec![0.0; 4]; 4];
420 for row in rows {
421 for first in 0..4 {
422 for second in 0..4 {
423 normal[first][second] += row[first] * row[second];
424 }
425 }
426 }
427 let homogeneous = smallest_symmetric_eigenvector(normal)?;
428 if homogeneous[3].abs() <= 1e-12 {
429 return None;
430 }
431 let position = Vec3::new(
432 homogeneous[0] / homogeneous[3],
433 homogeneous[1] / homogeneous[3],
434 homogeneous[2] / homogeneous[3],
435 );
436 let target_position = rotation.mul_vec3(position) + translation;
437 if position.z.abs() <= 1e-12 || target_position.z.abs() <= 1e-12 {
438 return None;
439 }
440 let source_error =
441 (position.x / position.z - source.x).hypot(position.y / position.z - source.y);
442 let target_error = (target_position.x / target_position.z - target.x)
443 .hypot(target_position.y / target_position.z - target.y);
444 TriangulatedPoint::try_new(
445 position,
446 position.z,
447 target_position.z,
448 0.5 * (source_error + target_error),
449 )
450 .ok()
451}
452
453fn matrix_from_columns(first: Vec3<f64>, second: Vec3<f64>, third: Vec3<f64>) -> Mat3<f64> {
454 Mat3::from_rows(
455 [first.x, second.x, third.x],
456 [first.y, second.y, third.y],
457 [first.z, second.z, third.z],
458 )
459}
460
461fn column(matrix: Mat3<f64>, index: usize) -> Vec3<f64> {
462 Vec3::new(matrix.m[0][index], matrix.m[1][index], matrix.m[2][index])
463}
464
465fn scale_vec(vector: Vec3<f64>, scale: f64) -> Vec3<f64> {
466 Vec3::new(vector.x * scale, vector.y * scale, vector.z * scale)
467}
468
469fn determinant(matrix: Mat3<f64>) -> f64 {
470 matrix.m[0][0] * (matrix.m[1][1] * matrix.m[2][2] - matrix.m[1][2] * matrix.m[2][1])
471 - matrix.m[0][1] * (matrix.m[1][0] * matrix.m[2][2] - matrix.m[1][2] * matrix.m[2][0])
472 + matrix.m[0][2] * (matrix.m[1][0] * matrix.m[2][1] - matrix.m[1][1] * matrix.m[2][0])
473}
474
475fn negate_column(matrix: &mut Mat3<f64>, column: usize) {
476 for row in 0..3 {
477 matrix.m[row][column] = -matrix.m[row][column];
478 }
479}
480
481fn proper_rotation(mut matrix: Mat3<f64>) -> Mat3<f64> {
482 if determinant(matrix) < 0.0 {
483 for row in &mut matrix.m {
484 for value in row {
485 *value = -*value;
486 }
487 }
488 }
489 matrix
490}
491
492fn enforce_rank_two(matrix: Mat3<f64>) -> Mat3<f64> {
493 let covariance = matrix.transpose().mul_mat3(matrix);
494 let eigen = symmetric_eigen3(covariance);
495 let vector = Vec3::new(
496 eigen.eigenvectors.m[0][0],
497 eigen.eigenvectors.m[1][0],
498 eigen.eigenvectors.m[2][0],
499 );
500 let image = matrix.mul_vec3(vector);
501 let mut result = matrix;
502 for row in 0..3 {
503 for column in 0..3 {
504 result.m[row][column] -=
505 [image.x, image.y, image.z][row] * [vector.x, vector.y, vector.z][column];
506 }
507 }
508 result
509}
510
511fn accumulate_least_squares(normal: &mut [Vec<f64>], rhs: &mut [f64], row: &[f64], value: f64) {
512 for row_index in 0..row.len() {
513 rhs[row_index] += row[row_index] * value;
514 for column in 0..row.len() {
515 normal[row_index][column] += row[row_index] * row[column];
516 }
517 }
518}
519
520struct Normalization2 {
521 matrix: Mat3<f64>,
522 inverse: Mat3<f64>,
523}
524
525impl Normalization2 {
526 fn from_points(points: &[Vec2<f64>]) -> VisionResult<Self> {
527 let count = points.len() as f64;
528 let center = Vec2 {
529 x: points.iter().map(|point| point.x).sum::<f64>() / count,
530 y: points.iter().map(|point| point.y).sum::<f64>() / count,
531 };
532 let rms = (points
533 .iter()
534 .map(|point| {
535 let dx = point.x - center.x;
536 let dy = point.y - center.y;
537 dx * dx + dy * dy
538 })
539 .sum::<f64>()
540 / count)
541 .sqrt();
542 if !rms.is_finite() || rms <= f64::EPSILON {
543 return Err(VisionError::InvalidParameter(
544 "geometry points have zero spatial extent".into(),
545 ));
546 }
547 let scale = 2.0_f64.sqrt() / rms;
548 Ok(Self {
549 matrix: Mat3::from_rows(
550 [scale, 0.0, -scale * center.x],
551 [0.0, scale, -scale * center.y],
552 [0.0, 0.0, 1.0],
553 ),
554 inverse: Mat3::from_rows(
555 [1.0 / scale, 0.0, center.x],
556 [0.0, 1.0 / scale, center.y],
557 [0.0, 0.0, 1.0],
558 ),
559 })
560 }
561
562 fn apply(&self, point: Vec2<f64>) -> Vec2<f64> {
563 let normalized = self.matrix.mul_vec3(Vec3::new(point.x, point.y, 1.0));
564 Vec2 { x: normalized.x, y: normalized.y }
565 }
566}
567
568#[allow(clippy::needless_range_loop)]
569fn smallest_symmetric_eigenvector(mut matrix: Vec<Vec<f64>>) -> Option<Vec<f64>> {
570 let size = matrix.len();
571 if size == 0 || matrix.iter().any(|row| row.len() != size) {
572 return None;
573 }
574 let mut vectors = vec![vec![0.0; size]; size];
575 for (index, row) in vectors.iter_mut().enumerate() {
576 row[index] = 1.0;
577 }
578 for _ in 0..size * size * 32 {
579 let mut pivot = (0, 1);
580 let mut maximum = 0.0_f64;
581 for (row, values) in matrix.iter().enumerate() {
582 for (column, &value) in values.iter().enumerate().skip(row + 1) {
583 if value.abs() > maximum {
584 maximum = value.abs();
585 pivot = (row, column);
586 }
587 }
588 }
589 if maximum < 1e-12 {
590 break;
591 }
592 let (p, q) = pivot;
593 let angle = 0.5 * (2.0 * matrix[p][q]).atan2(matrix[q][q] - matrix[p][p]);
594 let (sine, cosine) = angle.sin_cos();
595 for row in 0..size {
596 if row != p && row != q {
597 let rp = matrix[row][p];
598 let rq = matrix[row][q];
599 matrix[row][p] = cosine * rp - sine * rq;
600 matrix[p][row] = matrix[row][p];
601 matrix[row][q] = sine * rp + cosine * rq;
602 matrix[q][row] = matrix[row][q];
603 }
604 }
605 let pp = matrix[p][p];
606 let qq = matrix[q][q];
607 let pq = matrix[p][q];
608 matrix[p][p] = cosine * cosine * pp - 2.0 * sine * cosine * pq + sine * sine * qq;
609 matrix[q][q] = sine * sine * pp + 2.0 * sine * cosine * pq + cosine * cosine * qq;
610 matrix[p][q] = 0.0;
611 matrix[q][p] = 0.0;
612 for row in &mut vectors {
613 let rp = row[p];
614 let rq = row[q];
615 row[p] = cosine * rp - sine * rq;
616 row[q] = sine * rp + cosine * rq;
617 }
618 }
619 let index =
620 (0..size).min_by(|&left, &right| matrix[left][left].total_cmp(&matrix[right][right]))?;
621 let mut vector = vectors.iter().map(|row| row[index]).collect::<Vec<_>>();
622 let norm = vector.iter().map(|value| value * value).sum::<f64>().sqrt();
623 if !norm.is_finite() || norm <= f64::EPSILON {
624 return None;
625 }
626 for value in &mut vector {
627 *value /= norm;
628 }
629 Some(vector)
630}
631
632struct XorShift64(u64);
633
634impl XorShift64 {
635 fn new(seed: u64) -> Self {
636 Self(if seed == 0 { 0x9e37_79b9_7f4a_7c15 } else { seed })
637 }
638
639 fn next(&mut self) -> u64 {
640 self.0 ^= self.0 << 13;
641 self.0 ^= self.0 >> 7;
642 self.0 ^= self.0 << 17;
643 self.0
644 }
645}
646
647fn sample_unique(rng: &mut XorShift64, population: usize, count: usize) -> Vec<usize> {
648 let mut sample = Vec::with_capacity(count);
649 while sample.len() < count {
650 let candidate = (rng.next() % population as u64) as usize;
651 if !sample.contains(&candidate) {
652 sample.push(candidate);
653 }
654 }
655 sample
656}
657
658#[cfg(test)]
659mod tests {
660 use super::{
661 estimate_essential, estimate_fundamental, estimate_homography, estimate_homography_ransac,
662 fundamental_residual, homography_residual, recover_relative_pose,
663 triangulate_correspondence,
664 };
665 use crate::{
666 CameraMatrix3, Essential3, PointCorrespondence2, RelativePose, RobustEstimationOptions,
667 };
668 use spatialrust_camera::CameraIntrinsics;
669 use spatialrust_math::{Mat3, Vec2, Vec3};
670
671 fn correspondence(source: (f64, f64), target: (f64, f64)) -> PointCorrespondence2 {
672 PointCorrespondence2::try_new(
673 Vec2 { x: source.0, y: source.1 },
674 Vec2 { x: target.0, y: target.1 },
675 )
676 .unwrap()
677 }
678
679 #[test]
680 fn homography_recovers_known_projective_mapping() {
681 let pairs = [
682 ((0.0, 0.0), (3.0, -2.0)),
683 ((10.0, 0.0), (23.0, -2.0)),
684 ((0.0, 5.0), (3.0, 13.0)),
685 ((10.0, 5.0), (23.0, 13.0)),
686 ((4.0, 2.0), (11.0, 4.0)),
687 ]
688 .map(|(source, target)| correspondence(source, target));
689 let model = estimate_homography(&pairs).unwrap();
690 assert!(pairs.iter().all(|&pair| homography_residual(model, pair) < 1e-9));
691 }
692
693 #[test]
694 fn homography_ransac_rejects_large_outliers_deterministically() {
695 let mut pairs = (0..20)
696 .map(|index| {
697 let x = f64::from(index % 5) * 8.0;
698 let y = f64::from(index / 5) * 7.0;
699 correspondence((x, y), (1.5 * x + 4.0, 0.75 * y - 3.0))
700 })
701 .collect::<Vec<_>>();
702 pairs.push(correspondence((5.0, 5.0), (500.0, -300.0)));
703 pairs.push(correspondence((9.0, 11.0), (-200.0, 400.0)));
704 let options = RobustEstimationOptions { threshold: 0.1, seed: 7, ..Default::default() };
705 let first = estimate_homography_ransac(&pairs, options).unwrap();
706 let second = estimate_homography_ransac(&pairs, options).unwrap();
707 assert_eq!(first, second);
708 assert_eq!(first.inlier_count(), 20);
709 }
710
711 #[test]
712 fn eight_point_model_satisfies_synthetic_epipolar_constraints() {
713 let pairs = (0..16)
714 .map(|index| {
715 let x = f64::from(index % 4) * 0.3 - 0.4;
716 let y = f64::from(index / 4) * 0.2 - 0.3;
717 let depth = 2.0 + f64::from(index) * 0.05;
718 let source = (x / depth, y / depth);
719 let target = ((x + 0.2) / depth, y / depth);
720 correspondence(source, target)
721 })
722 .collect::<Vec<_>>();
723 let model = estimate_fundamental(&pairs).unwrap();
724 assert!(pairs.iter().all(|&pair| fundamental_residual(model, pair) < 1e-7));
725 }
726
727 #[test]
728 fn calibrated_triangulation_and_pose_recovery_choose_positive_depth() {
729 let intrinsics = CameraIntrinsics::try_new(500.0, 500.0, 320.0, 240.0, 640, 480).unwrap();
730 let camera = CameraMatrix3::from_intrinsics(intrinsics);
731 let known_pose =
732 RelativePose::try_new(Mat3::<f64>::identity(), Vec3::new(0.2, 0.0, 0.0)).unwrap();
733 let points = (0..16)
734 .map(|index| {
735 Vec3::new(
736 f64::from(index % 4) * 0.25 - 0.4,
737 f64::from(index / 4) * 0.2 - 0.3,
738 2.0 + f64::from((index * index + 3 * index) % 17) * 0.07,
739 )
740 })
741 .collect::<Vec<_>>();
742 let pairs = points
743 .iter()
744 .map(|point| {
745 let target = *point + known_pose.translation();
746 correspondence(
747 (500.0 * point.x / point.z + 320.0, 500.0 * point.y / point.z + 240.0),
748 (500.0 * target.x / target.z + 320.0, 500.0 * target.y / target.z + 240.0),
749 )
750 })
751 .collect::<Vec<_>>();
752 let triangulated =
753 triangulate_correspondence(pairs[3], camera, camera, known_pose).unwrap();
754 assert!((triangulated.position().x - points[3].x).abs() < 1e-8);
755 assert!((triangulated.position().z - points[3].z).abs() < 1e-8);
756 assert!(triangulated.has_positive_depth());
757
758 let estimated = estimate_essential(&pairs, camera, camera).unwrap();
759 let maximum_residual = pairs
760 .iter()
761 .map(|pair| {
762 let source = camera.normalize_pixel(pair.source());
763 let target = camera.normalize_pixel(pair.target());
764 let normalized = correspondence((source.x, source.y), (target.x, target.y));
765 super::essential_residual(estimated, normalized)
766 })
767 .fold(0.0_f64, f64::max);
768 assert!(maximum_residual < 1e-7, "maximum residual {maximum_residual}");
769 let essential = Essential3::try_new(Mat3::from_rows(
770 [0.0, 0.0, 0.0],
771 [0.0, 0.0, -0.2],
772 [0.0, 0.2, 0.0],
773 ))
774 .unwrap();
775 let recovered = recover_relative_pose(essential, &pairs, camera, camera).unwrap();
776 assert_eq!(recovered.positive_depth_count(), pairs.len());
777 assert!(recovered.pose().translation().x.abs() > 0.99);
778 }
779}