1use spatialrust_math::{
4 solve_linear_system, symmetric_eigen3, LeastSquaresResult, Mat3, Vec2, Vec3,
5};
6
7use crate::{
8 AbsolutePose, CameraMatrix3, GeometricEstimate, ObjectImageCorrespondence,
9 PointCorrespondence2, RobustEstimationOptions, VisionError, VisionResult,
10};
11
12pub fn solve_pnp(
19 correspondences: &[ObjectImageCorrespondence],
20 camera: CameraMatrix3,
21) -> VisionResult<AbsolutePose> {
22 if correspondences.len() < 4 {
23 return Err(VisionError::InvalidParameter(
24 "PnP requires at least four object-image correspondences".into(),
25 ));
26 }
27 let initial = estimate_pnp_dlt(correspondences, camera)?;
28 refine_pnp(correspondences, camera, initial)
29}
30
31pub fn solve_pnp_ransac(
36 correspondences: &[ObjectImageCorrespondence],
37 camera: CameraMatrix3,
38 options: RobustEstimationOptions,
39) -> VisionResult<GeometricEstimate<AbsolutePose>> {
40 let options = options.validate()?;
41 const SAMPLE: usize = 6;
42 if correspondences.len() < SAMPLE {
43 return Err(VisionError::InvalidParameter(
44 "robust PnP requires at least six correspondences".into(),
45 ));
46 }
47 let mut rng = XorShift64::new(options.seed);
48 let mut best: Option<PnpConsensus> = None;
49 let mut iteration_limit = options.max_iterations;
50 let mut iteration = 0;
51 while iteration < iteration_limit {
52 let indices = sample_unique(&mut rng, correspondences.len(), SAMPLE);
53 let sample = indices.iter().map(|&index| correspondences[index]).collect::<Vec<_>>();
54 if let Ok(model) =
55 estimate_pnp_dlt(&sample, camera).and_then(|pose| refine_pnp(&sample, camera, pose))
56 {
57 let residuals = correspondences
58 .iter()
59 .copied()
60 .map(|pair| pnp_residual(model, pair, camera))
61 .collect::<Vec<_>>();
62 let inliers =
63 residuals.iter().map(|&value| value <= options.threshold).collect::<Vec<_>>();
64 let count = inliers.iter().filter(|&&value| value).count();
65 let error = residuals
66 .iter()
67 .zip(&inliers)
68 .filter_map(|(value, &is_inlier)| is_inlier.then_some(*value))
69 .sum::<f64>();
70 let improves = best.as_ref().map_or(true, |candidate| {
71 count > candidate.3 || (count == candidate.3 && error < candidate.4)
72 });
73 if improves {
74 if count >= SAMPLE {
75 let inlier_ratio = count as f64 / correspondences.len() as f64;
76 let success = inlier_ratio.powi(SAMPLE as i32).clamp(0.0, 1.0);
77 if success > 0.0 && success < 1.0 {
78 let required = ((1.0 - options.confidence).ln() / (1.0 - success).ln())
79 .ceil()
80 .max(1.0) as usize;
81 iteration_limit = iteration_limit.min(required.max(iteration + 1));
82 } else if success == 1.0 {
83 iteration_limit = iteration + 1;
84 }
85 }
86 best = Some((model, inliers, residuals, count, error));
87 }
88 }
89 iteration += 1;
90 }
91 let best = best
92 .ok_or_else(|| VisionError::InvalidParameter("robust PnP found no valid model".into()))?;
93 if best.3 < 4 {
94 return Err(VisionError::InvalidParameter("robust PnP found too few inliers".into()));
95 }
96 let inlier_pairs = correspondences
97 .iter()
98 .zip(&best.1)
99 .filter_map(|(&pair, &is_inlier)| is_inlier.then_some(pair))
100 .collect::<Vec<_>>();
101 let refinement = solve_pnp(&inlier_pairs, camera);
102 finish_pnp_consensus(best, refinement, correspondences, camera, options.threshold)
103}
104
105type PnpConsensus = (AbsolutePose, Vec<bool>, Vec<f64>, usize, f64);
107
108fn finish_pnp_consensus(
109 mut best: PnpConsensus,
110 refinement: VisionResult<AbsolutePose>,
111 correspondences: &[ObjectImageCorrespondence],
112 camera: CameraMatrix3,
113 threshold: f64,
114) -> VisionResult<GeometricEstimate<AbsolutePose>> {
115 if let Ok(refined) = refinement {
116 let (count, error) =
117 rescore_pnp(refined, correspondences, camera, threshold, &mut best.1, &mut best.2);
118 if count > best.3 || (count == best.3 && error < best.4) {
119 best.0 = refined;
120 best.3 = count;
121 best.4 = error;
122 } else {
123 rescore_pnp(best.0, correspondences, camera, threshold, &mut best.1, &mut best.2);
127 }
128 }
129 GeometricEstimate::try_new(best.0, correspondences.len(), best.1, best.2)
130}
131
132fn rescore_pnp(
133 pose: AbsolutePose,
134 correspondences: &[ObjectImageCorrespondence],
135 camera: CameraMatrix3,
136 threshold: f64,
137 inliers: &mut [bool],
138 residuals: &mut [f64],
139) -> (usize, f64) {
140 debug_assert_eq!(correspondences.len(), inliers.len());
141 debug_assert_eq!(correspondences.len(), residuals.len());
142 let mut count = 0;
143 let mut error = 0.0;
144 for ((&pair, inlier), residual) in
145 correspondences.iter().zip(inliers.iter_mut()).zip(residuals.iter_mut())
146 {
147 *residual = pnp_residual(pose, pair, camera);
148 *inlier = *residual <= threshold;
149 if *inlier {
150 count += 1;
151 error += *residual;
152 }
153 }
154 (count, error)
155}
156
157pub fn project_object_point(
159 pose: AbsolutePose,
160 camera: CameraMatrix3,
161 object: Vec3<f64>,
162) -> VisionResult<Vec2<f64>> {
163 let camera_point = pose.transform_point(object);
164 if camera_point.z <= 1e-12 {
165 return Err(VisionError::InvalidParameter(
166 "projected point lies behind or on the camera plane".into(),
167 ));
168 }
169 let normalized =
170 Vec3::new(camera_point.x / camera_point.z, camera_point.y / camera_point.z, 1.0);
171 let pixel = camera.matrix().mul_vec3(normalized);
172 Ok(Vec2 { x: pixel.x / pixel.z, y: pixel.y / pixel.z })
173}
174
175fn estimate_pnp_dlt(
176 correspondences: &[ObjectImageCorrespondence],
177 camera: CameraMatrix3,
178) -> VisionResult<AbsolutePose> {
179 if correspondences.len() < 4 {
180 return Err(VisionError::InvalidParameter(
181 "PnP DLT requires at least four correspondences".into(),
182 ));
183 }
184 if let Some(pose) = planar_initialization(correspondences, camera)? {
185 return Ok(pose);
186 }
187 let mut normal = vec![vec![0.0; 12]; 12];
188 for pair in correspondences {
189 let object = pair.object();
190 let image = pair.image();
191 let rows = [
192 [
193 object.x,
194 object.y,
195 object.z,
196 1.0,
197 0.0,
198 0.0,
199 0.0,
200 0.0,
201 -image.x * object.x,
202 -image.x * object.y,
203 -image.x * object.z,
204 -image.x,
205 ],
206 [
207 0.0,
208 0.0,
209 0.0,
210 0.0,
211 object.x,
212 object.y,
213 object.z,
214 1.0,
215 -image.y * object.x,
216 -image.y * object.y,
217 -image.y * object.z,
218 -image.y,
219 ],
220 ];
221 for row in rows {
222 for first in 0..12 {
223 for second in 0..12 {
224 normal[first][second] += row[first] * row[second];
225 }
226 }
227 }
228 }
229 let vector = smallest_symmetric_eigenvector(normal).ok_or_else(|| {
230 VisionError::InvalidParameter("PnP correspondences are degenerate".into())
231 })?;
232 let projection = Mat3::from_rows(
233 [vector[0], vector[1], vector[2]],
234 [vector[4], vector[5], vector[6]],
235 [vector[8], vector[9], vector[10]],
236 );
237 let translation_part = Vec3::new(vector[3], vector[7], vector[11]);
238 let calibrated = camera.inverse().mul_mat3(projection);
239 let calibrated_t = camera.inverse().mul_vec3(translation_part);
240 let (rotation, scale) = orthonormalize_rotation(calibrated)?;
241 let translation =
242 Vec3::new(calibrated_t.x / scale, calibrated_t.y / scale, calibrated_t.z / scale);
243 let pose = AbsolutePose::try_new(rotation, translation)?;
245 let positive =
246 correspondences.iter().filter(|pair| pose.transform_point(pair.object()).z > 0.0).count();
247 if positive * 2 < correspondences.len() {
248 AbsolutePose::try_new(
249 Mat3::from_rows(
250 [-rotation.m[0][0], -rotation.m[0][1], -rotation.m[0][2]],
251 [-rotation.m[1][0], -rotation.m[1][1], -rotation.m[1][2]],
252 [-rotation.m[2][0], -rotation.m[2][1], -rotation.m[2][2]],
253 ),
254 Vec3::new(-translation.x, -translation.y, -translation.z),
255 )
256 } else {
257 Ok(pose)
258 }
259}
260
261fn planar_initialization(
264 correspondences: &[ObjectImageCorrespondence],
265 camera: CameraMatrix3,
266) -> VisionResult<Option<AbsolutePose>> {
267 let origin = correspondences[0].object();
268 let extent = correspondences
269 .iter()
270 .map(|pair| {
271 let p = pair.object() - origin;
272 p.x.abs().max(p.y.abs()).max(p.z.abs())
273 })
274 .fold(0.0_f64, f64::max);
275 if !extent.is_finite() || extent == 0.0 {
276 return Err(VisionError::InvalidParameter("PnP object extent is zero or overflows".into()));
277 }
278 let mut mean = Vec3::new(0.0, 0.0, 0.0);
279 for pair in correspondences {
280 mean = mean + scale_vec(pair.object() - origin, 1.0 / extent);
281 }
282 mean = scale_vec(mean, 1.0 / correspondences.len() as f64);
283 let centroid = origin + scale_vec(mean, extent);
284 let mut covariance = [[0.0; 3]; 3];
285 for pair in correspondences {
286 let p = scale_vec(pair.object() - origin, 1.0 / extent) - mean;
287 let components = [p.x, p.y, p.z];
288 for row in 0..3 {
289 for column in 0..3 {
290 covariance[row][column] += components[row] * components[column];
291 }
292 }
293 }
294 if covariance.iter().flatten().any(|value| !value.is_finite())
295 || ![centroid.x, centroid.y, centroid.z].into_iter().all(f64::is_finite)
296 {
297 return Err(VisionError::InvalidParameter("PnP object covariance overflows".into()));
298 }
299 let eigen = symmetric_eigen3(Mat3::from_rows(covariance[0], covariance[1], covariance[2]));
300 if eigen.eigenvalues[1] <= eigen.eigenvalues[2] * 1e-10 {
301 return Err(VisionError::InvalidParameter("PnP object points are collinear".into()));
302 }
303 if eigen.eigenvalues[0] > eigen.eigenvalues[2] * 1e-6 {
304 return Ok(None);
305 }
306 let vectors = eigen.eigenvectors;
307 let u = Vec3::new(vectors.m[0][2], vectors.m[1][2], vectors.m[2][2]);
308 let v = Vec3::new(vectors.m[0][1], vectors.m[1][1], vectors.m[2][1]);
309 let n = u.cross(v);
310 let basis = Mat3::from_rows([u.x, v.x, n.x], [u.y, v.y, n.y], [u.z, v.z, n.z]);
311 let pairs = correspondences
312 .iter()
313 .map(|pair| {
314 let p = scale_vec(pair.object() - origin, 1.0 / extent) - mean;
315 let pixel = camera.normalize_pixel(pair.image());
316 PointCorrespondence2::try_new(
317 Vec2 { x: p.dot(u), y: p.dot(v) },
318 Vec2 { x: pixel.x, y: pixel.y },
319 )
320 })
321 .collect::<VisionResult<Vec<_>>>()?;
322 let homography = crate::multiview::estimate_homography(&pairs)?.matrix();
323 let a = Vec3::new(homography.m[0][0], homography.m[1][0], homography.m[2][0]);
324 let b = Vec3::new(homography.m[0][1], homography.m[1][1], homography.m[2][1]);
325 let c = Vec3::new(homography.m[0][2], homography.m[1][2], homography.m[2][2]);
326 let scale = (a.length() + b.length()) * 0.5;
327 if !scale.is_finite() || scale <= f64::EPSILON {
328 return Err(VisionError::InvalidParameter(
329 "PnP planar homography has invalid scale".into(),
330 ));
331 }
332 let mut best: Option<(AbsolutePose, usize, f64)> = None;
333 for sign in [1.0, -1.0] {
334 let first = scale_vec(a, sign / scale);
335 let second = scale_vec(b, sign / scale);
336 let third = first.cross(second);
337 let approximate = Mat3::from_rows(
338 [first.x, second.x, third.x],
339 [first.y, second.y, third.y],
340 [first.z, second.z, third.z],
341 );
342 let (camera_basis, _) = orthonormalize_rotation(approximate)?;
343 let rotation = camera_basis.mul_mat3(basis.transpose());
344 let translation = scale_vec(c, sign * extent / scale) - rotation.mul_vec3(centroid);
345 let pose = AbsolutePose::try_new(rotation, translation)?;
346 let positive = correspondences
347 .iter()
348 .filter(|pair| pose.transform_point(pair.object()).z > 0.0)
349 .count();
350 let error =
351 correspondences.iter().map(|pair| pnp_residual(pose, *pair, camera)).sum::<f64>();
352 if best.as_ref().map_or(true, |previous| {
353 positive > previous.1 || (positive == previous.1 && error < previous.2)
354 }) {
355 best = Some((pose, positive, error));
356 }
357 }
358 let (pose, positive, _) = best.expect("two planar signs evaluated");
359 if positive == 0 {
360 return Err(VisionError::InvalidParameter("PnP planar pose has no positive depth".into()));
361 }
362 Ok(Some(pose))
363}
364
365fn refine_pnp(
366 correspondences: &[ObjectImageCorrespondence],
367 camera: CameraMatrix3,
368 mut pose: AbsolutePose,
369) -> VisionResult<AbsolutePose> {
370 for _ in 0..20 {
371 let mut normal = vec![vec![0.0; 6]; 6];
372 let mut rhs = vec![0.0; 6];
373 let mut residual_sum = 0.0;
374 for pair in correspondences {
375 let camera_point = pose.transform_point(pair.object());
376 if camera_point.z <= 1e-12 {
377 continue;
378 }
379 let predicted = project_object_point(pose, camera, pair.object())?;
380 let error = [predicted.x - pair.image().x, predicted.y - pair.image().y];
381 residual_sum += error[0] * error[0] + error[1] * error[1];
382 let jacobian = projection_jacobian(pose, camera, pair.object(), camera_point)?;
383 for row in 0..2 {
384 for col in 0..6 {
385 rhs[col] -= jacobian[row][col] * error[row];
386 for other in 0..6 {
387 normal[col][other] += jacobian[row][col] * jacobian[row][other];
388 }
389 }
390 }
391 }
392 let LeastSquaresResult::Solved(delta) = solve_linear_system(normal, rhs) else {
393 break;
394 };
395 let update = Vec3::new(delta[0], delta[1], delta[2]);
396 if update.length() + Vec3::new(delta[3], delta[4], delta[5]).length() < 1e-10 {
397 break;
398 }
399 let rotated = exp_so3(update).mul_mat3(pose.rotation());
400 let (rotation, _) = orthonormalize_rotation(rotated)?;
401 let translation = pose.translation() + Vec3::new(delta[3], delta[4], delta[5]);
402 pose = AbsolutePose::try_new(rotation, translation)?;
403 if residual_sum < 1e-18 {
404 break;
405 }
406 }
407 Ok(pose)
408}
409
410fn projection_jacobian(
411 pose: AbsolutePose,
412 camera: CameraMatrix3,
413 object: Vec3<f64>,
414 camera_point: Vec3<f64>,
415) -> VisionResult<[[f64; 6]; 2]> {
416 let z = camera_point.z;
417 let z2 = z * z;
418 let fx = camera.matrix().m[0][0];
419 let fy = camera.matrix().m[1][1];
420 let du_dx = fx / z;
422 let du_dy = 0.0;
423 let du_dz = -fx * camera_point.x / z2;
424 let dv_dx = 0.0;
425 let dv_dy = fy / z;
426 let dv_dz = -fy * camera_point.y / z2;
427 let rotated = pose.rotation().mul_vec3(object);
429 let dx_domega = [
430 Vec3::new(0.0, -rotated.z, rotated.y),
431 Vec3::new(rotated.z, 0.0, -rotated.x),
432 Vec3::new(-rotated.y, rotated.x, 0.0),
433 ];
434 let mut jacobian = [[0.0; 6]; 2];
435 for axis in 0..3 {
436 let d = dx_domega[axis];
437 jacobian[0][axis] = du_dx * d.x + du_dy * d.y + du_dz * d.z;
438 jacobian[1][axis] = dv_dx * d.x + dv_dy * d.y + dv_dz * d.z;
439 }
440 jacobian[0][3] = du_dx;
441 jacobian[0][4] = du_dy;
442 jacobian[0][5] = du_dz;
443 jacobian[1][3] = dv_dx;
444 jacobian[1][4] = dv_dy;
445 jacobian[1][5] = dv_dz;
446 Ok(jacobian)
447}
448
449fn pnp_residual(pose: AbsolutePose, pair: ObjectImageCorrespondence, camera: CameraMatrix3) -> f64 {
450 match project_object_point(pose, camera, pair.object()) {
451 Ok(pixel) => (pixel.x - pair.image().x).hypot(pixel.y - pair.image().y),
452 Err(_) => f64::MAX,
453 }
454}
455
456fn orthonormalize_rotation(matrix: Mat3<f64>) -> VisionResult<(Mat3<f64>, f64)> {
457 let eigen = symmetric_eigen3(matrix.transpose().mul_mat3(matrix));
458 let scale = ((eigen.eigenvalues[0].max(0.0).sqrt()
459 + eigen.eigenvalues[1].max(0.0).sqrt()
460 + eigen.eigenvalues[2].max(0.0).sqrt())
461 / 3.0)
462 .max(1e-12);
463 let right = eigen.eigenvectors;
464 let mut left_cols = [Vec3::new(0.0, 0.0, 0.0); 3];
465 for (column, left_col) in left_cols.iter_mut().enumerate() {
466 let right_col = Vec3::new(right.m[0][column], right.m[1][column], right.m[2][column]);
467 let sigma = eigen.eigenvalues[column].max(0.0).sqrt().max(1e-12);
468 *left_col = scale_vec(matrix.mul_vec3(right_col), 1.0 / (sigma * scale));
469 }
470 left_cols[0] = left_cols[0].normalize();
471 left_cols[1] =
472 (left_cols[1] - scale_vec(left_cols[0], left_cols[0].dot(left_cols[1]))).normalize();
473 left_cols[2] = left_cols[0].cross(left_cols[1]).normalize();
474 let right0 = Vec3::new(right.m[0][0], right.m[1][0], right.m[2][0]).normalize();
475 let mut right1 = Vec3::new(right.m[0][1], right.m[1][1], right.m[2][1]);
476 right1 = (right1 - scale_vec(right0, right0.dot(right1))).normalize();
477 let right2 = right0.cross(right1).normalize();
478 let mut rotation = Mat3::from_rows(
479 [
480 left_cols[0].x * right0.x + left_cols[1].x * right1.x + left_cols[2].x * right2.x,
481 left_cols[0].x * right0.y + left_cols[1].x * right1.y + left_cols[2].x * right2.y,
482 left_cols[0].x * right0.z + left_cols[1].x * right1.z + left_cols[2].x * right2.z,
483 ],
484 [
485 left_cols[0].y * right0.x + left_cols[1].y * right1.x + left_cols[2].y * right2.x,
486 left_cols[0].y * right0.y + left_cols[1].y * right1.y + left_cols[2].y * right2.y,
487 left_cols[0].y * right0.z + left_cols[1].y * right1.z + left_cols[2].y * right2.z,
488 ],
489 [
490 left_cols[0].z * right0.x + left_cols[1].z * right1.x + left_cols[2].z * right2.x,
491 left_cols[0].z * right0.y + left_cols[1].z * right1.y + left_cols[2].z * right2.y,
492 left_cols[0].z * right0.z + left_cols[1].z * right1.z + left_cols[2].z * right2.z,
493 ],
494 );
495 if determinant(rotation) < 0.0 {
496 for row in &mut rotation.m {
497 for value in row {
498 *value = -*value;
499 }
500 }
501 }
502 Ok((rotation, if determinant(matrix) < 0.0 { -scale } else { scale }))
503}
504
505fn exp_so3(omega: Vec3<f64>) -> Mat3<f64> {
506 let theta = omega.length();
507 if theta < 1e-12 {
508 return Mat3::from_rows(
509 [1.0, -omega.z, omega.y],
510 [omega.z, 1.0, -omega.x],
511 [-omega.y, omega.x, 1.0],
512 );
513 }
514 let axis = omega.normalize();
515 let skew =
516 Mat3::from_rows([0.0, -axis.z, axis.y], [axis.z, 0.0, -axis.x], [-axis.y, axis.x, 0.0]);
517 let skew2 = skew.mul_mat3(skew);
518 let mut result = Mat3::<f64>::identity();
519 let s = theta.sin();
520 let c = 1.0 - theta.cos();
521 for row in 0..3 {
522 for column in 0..3 {
523 result.m[row][column] += s * skew.m[row][column] + c * skew2.m[row][column];
524 }
525 }
526 result
527}
528
529#[allow(clippy::needless_range_loop)]
530fn smallest_symmetric_eigenvector(mut matrix: Vec<Vec<f64>>) -> Option<[f64; 12]> {
531 let size = matrix.len();
532 if size != 12 || matrix.iter().any(|row| row.len() != size) {
533 return None;
534 }
535 let mut vectors = vec![vec![0.0; size]; size];
536 for (index, row) in vectors.iter_mut().enumerate() {
537 row[index] = 1.0;
538 }
539 for _ in 0..size * size * 32 {
540 let mut pivot = (0, 1);
541 let mut maximum = 0.0_f64;
542 for (row, values) in matrix.iter().enumerate() {
543 for (column, &value) in values.iter().enumerate().skip(row + 1) {
544 if value.abs() > maximum {
545 maximum = value.abs();
546 pivot = (row, column);
547 }
548 }
549 }
550 if maximum < 1e-12 {
551 break;
552 }
553 let (p, q) = pivot;
554 let angle = 0.5 * (2.0 * matrix[p][q]).atan2(matrix[q][q] - matrix[p][p]);
555 let (sine, cosine) = angle.sin_cos();
556 for row in 0..size {
557 if row != p && row != q {
558 let rp = matrix[row][p];
559 let rq = matrix[row][q];
560 matrix[row][p] = cosine * rp - sine * rq;
561 matrix[p][row] = matrix[row][p];
562 matrix[row][q] = sine * rp + cosine * rq;
563 matrix[q][row] = matrix[row][q];
564 }
565 }
566 let pp = matrix[p][p];
567 let qq = matrix[q][q];
568 let pq = matrix[p][q];
569 matrix[p][p] = cosine * cosine * pp - 2.0 * sine * cosine * pq + sine * sine * qq;
570 matrix[q][q] = sine * sine * pp + 2.0 * sine * cosine * pq + cosine * cosine * qq;
571 matrix[p][q] = 0.0;
572 matrix[q][p] = 0.0;
573 for row in &mut vectors {
574 let rp = row[p];
575 let rq = row[q];
576 row[p] = cosine * rp - sine * rq;
577 row[q] = sine * rp + cosine * rq;
578 }
579 }
580 let index =
581 (0..size).min_by(|&left, &right| matrix[left][left].total_cmp(&matrix[right][right]))?;
582 let mut vector = vectors.iter().map(|row| row[index]).collect::<Vec<_>>();
583 let norm = vector.iter().map(|value| value * value).sum::<f64>().sqrt();
584 if !norm.is_finite() || norm <= f64::EPSILON {
585 return None;
586 }
587 for value in &mut vector {
588 *value /= norm;
589 }
590 let mut out = [0.0; 12];
591 out.copy_from_slice(&vector);
592 Some(out)
593}
594
595fn sample_unique(rng: &mut XorShift64, upper: usize, count: usize) -> Vec<usize> {
596 let mut selected = Vec::with_capacity(count);
597 while selected.len() < count {
598 let value = rng.next_usize(upper);
599 if !selected.contains(&value) {
600 selected.push(value);
601 }
602 }
603 selected
604}
605
606fn scale_vec(vector: Vec3<f64>, scale: f64) -> Vec3<f64> {
607 Vec3::new(vector.x * scale, vector.y * scale, vector.z * scale)
608}
609
610fn determinant(matrix: Mat3<f64>) -> f64 {
611 matrix.m[0][0] * (matrix.m[1][1] * matrix.m[2][2] - matrix.m[1][2] * matrix.m[2][1])
612 - matrix.m[0][1] * (matrix.m[1][0] * matrix.m[2][2] - matrix.m[1][2] * matrix.m[2][0])
613 + matrix.m[0][2] * (matrix.m[1][0] * matrix.m[2][1] - matrix.m[1][1] * matrix.m[2][0])
614}
615
616struct XorShift64 {
617 state: u64,
618}
619
620impl XorShift64 {
621 fn new(seed: u64) -> Self {
622 Self { state: seed | 1 }
623 }
624
625 fn next_u64(&mut self) -> u64 {
626 self.state ^= self.state << 13;
627 self.state ^= self.state >> 7;
628 self.state ^= self.state << 17;
629 self.state
630 }
631
632 fn next_usize(&mut self, upper: usize) -> usize {
633 (self.next_u64() as usize) % upper.max(1)
634 }
635}
636
637#[cfg(test)]
638mod tests {
639 use super::{project_object_point, solve_pnp, solve_pnp_ransac};
640 use crate::{AbsolutePose, CameraMatrix3, ObjectImageCorrespondence, RobustEstimationOptions};
641 use spatialrust_camera::CameraIntrinsics;
642 use spatialrust_math::{Mat3, Vec2, Vec3};
643
644 fn camera() -> CameraMatrix3 {
645 let intrinsics = CameraIntrinsics::try_new(500.0, 500.0, 320.0, 240.0, 640, 480).unwrap();
646 CameraMatrix3::from_intrinsics(intrinsics)
647 }
648
649 fn sample_pose() -> AbsolutePose {
650 AbsolutePose::try_new(
651 Mat3::from_rows(
652 [0.936_293_4, -0.275_095_9, 0.218_350_8],
653 [0.289_629_5, 0.956_425_1, -0.036_957_0],
654 [-0.198_669_3, 0.097_843_4, 0.975_170_3],
655 ),
656 Vec3::new(0.15, -0.05, 2.5),
657 )
658 .unwrap()
659 }
660
661 #[test]
662 fn solve_pnp_recovers_known_pose() {
663 let camera = camera();
664 let pose = sample_pose();
665 let objects = [
666 Vec3::new(0.0, 0.0, 0.0),
667 Vec3::new(0.4, 0.0, 0.0),
668 Vec3::new(0.0, 0.3, 0.0),
669 Vec3::new(0.0, 0.0, 0.2),
670 Vec3::new(0.25, 0.2, 0.1),
671 Vec3::new(-0.1, 0.15, -0.05),
672 Vec3::new(0.1, -0.2, 0.05),
673 Vec3::new(-0.2, -0.1, 0.15),
674 ];
675 let pairs = objects
676 .into_iter()
677 .map(|object| {
678 let image = project_object_point(pose, camera, object).unwrap();
679 ObjectImageCorrespondence::try_new(object, image).unwrap()
680 })
681 .collect::<Vec<_>>();
682 let estimated = solve_pnp(&pairs, camera).unwrap();
683 for (expected, actual) in
684 pose.rotation().m.iter().flatten().zip(estimated.rotation().m.iter().flatten())
685 {
686 assert!((expected - actual).abs() < 2e-3);
687 }
688 assert!((pose.translation().x - estimated.translation().x).abs() < 2e-3);
689 assert!((pose.translation().y - estimated.translation().y).abs() < 2e-3);
690 assert!((pose.translation().z - estimated.translation().z).abs() < 2e-3);
691 }
692
693 #[test]
694 fn planar_initialization_recovers_arbitrary_plane_at_multiple_scales() {
695 let camera = camera();
696 let base = sample_pose();
697 let u = Vec3::new(0.8, 0.6, 0.0);
698 let v = Vec3::new(0.0, 0.0, 1.0);
699 let n = u.cross(v);
700 for scale in [1e-4, 1.0, 1e4] {
701 for thickness in [0.0, 1e-4] {
702 let pose = AbsolutePose::try_new(
703 base.rotation(),
704 super::scale_vec(base.translation(), scale),
705 )
706 .unwrap();
707 let pairs = (0..25)
708 .map(|index| {
709 let object = super::scale_vec(
710 super::scale_vec(u, (index % 5) as f64 * 0.2 - 0.4)
711 + super::scale_vec(v, (index / 5) as f64 * 0.2 - 0.4)
712 + super::scale_vec(n, thickness * ((index % 3) as f64 - 1.0))
713 + Vec3::new(0.3, -0.2, 0.1),
714 scale,
715 );
716 ObjectImageCorrespondence::try_new(
717 object,
718 project_object_point(pose, camera, object).unwrap(),
719 )
720 .unwrap()
721 })
722 .collect::<Vec<_>>();
723 let estimated = solve_pnp(&pairs, camera).unwrap();
724 for (a, b) in
725 estimated.rotation().m.iter().flatten().zip(pose.rotation().m.iter().flatten())
726 {
727 assert!((a - b).abs() < 1e-5, "scale={scale}, thickness={thickness}");
728 }
729 let error = estimated.translation() - pose.translation();
730 assert!(error.length() / scale < 1e-5);
731 }
732 }
733 }
734
735 #[test]
736 fn planar_four_points_work_and_collinear_points_are_rejected() {
737 let camera = camera();
738 let pose = sample_pose();
739 let objects = [
740 Vec3::new(-0.4, -0.3, 0.0),
741 Vec3::new(0.4, -0.3, 0.0),
742 Vec3::new(-0.4, 0.3, 0.0),
743 Vec3::new(0.4, 0.3, 0.0),
744 ];
745 let pairs = objects
746 .iter()
747 .map(|&object| {
748 ObjectImageCorrespondence::try_new(
749 object,
750 project_object_point(pose, camera, object).unwrap(),
751 )
752 .unwrap()
753 })
754 .collect::<Vec<_>>();
755 let estimated = solve_pnp(&pairs, camera).unwrap();
756 assert!((estimated.translation() - pose.translation()).length() < 1e-5);
757 let line = (0..8)
758 .map(|index| {
759 let object = Vec3::new(index as f64 * 0.1, 0.0, 0.0);
760 ObjectImageCorrespondence::try_new(
761 object,
762 project_object_point(pose, camera, object).unwrap(),
763 )
764 .unwrap()
765 })
766 .collect::<Vec<_>>();
767 assert!(solve_pnp(&line, camera).is_err());
768 }
769
770 #[test]
771 fn planar_ransac_recovers_pose_with_incorrect_image_pairs() {
772 let camera = camera();
773 let pose = sample_pose();
774 let pairs = (0..30)
775 .map(|index| {
776 let object =
777 Vec3::new((index % 6) as f64 * 0.2 - 0.5, (index / 6) as f64 * 0.2 - 0.4, 0.0);
778 let mut image = project_object_point(pose, camera, object).unwrap();
779 if index < 6 {
780 image.x += 80.0;
781 image.y -= 60.0;
782 }
783 ObjectImageCorrespondence::try_new(object, image).unwrap()
784 })
785 .collect::<Vec<_>>();
786 let estimate = solve_pnp_ransac(
787 &pairs,
788 camera,
789 RobustEstimationOptions {
790 threshold: 2.0,
791 max_iterations: 300,
792 seed: 7,
793 ..RobustEstimationOptions::default()
794 },
795 )
796 .unwrap();
797 assert_eq!(estimate.inlier_count(), 24);
798 assert!((estimate.model().translation() - pose.translation()).length() < 1e-5);
799 }
800
801 #[test]
802 fn solve_pnp_ransac_rejects_outliers() {
803 let camera = camera();
804 let pose = sample_pose();
805 let mut pairs = (0..20)
806 .map(|index| {
807 let object = Vec3::new(
808 (index % 5) as f64 * 0.1 - 0.2,
809 (index / 5) as f64 * 0.1 - 0.15,
810 (index % 3) as f64 * 0.05,
811 );
812 let image = project_object_point(pose, camera, object).unwrap();
813 ObjectImageCorrespondence::try_new(object, image).unwrap()
814 })
815 .collect::<Vec<_>>();
816 for pair in pairs.iter_mut().take(4) {
817 *pair = ObjectImageCorrespondence::try_new(
818 pair.object(),
819 Vec2 { x: pair.image().x + 80.0, y: pair.image().y - 60.0 },
820 )
821 .unwrap();
822 }
823 let estimate = solve_pnp_ransac(
824 &pairs,
825 camera,
826 RobustEstimationOptions {
827 threshold: 2.0,
828 confidence: 0.99,
829 max_iterations: 500,
830 seed: 7,
831 },
832 )
833 .unwrap();
834 assert!(estimate.inlier_count() >= 14);
835 let recovered = *estimate.model();
836 assert!((pose.translation().z - recovered.translation().z).abs() < 5e-2);
837 }
838
839 #[test]
840 fn robust_pose_survives_failed_inlier_reinitialization() {
841 let camera = CameraMatrix3::from_intrinsics(
845 CameraIntrinsics::try_new(525.0, 525.0, 319.5, 239.5, 640, 480).unwrap(),
846 );
847 let truth = AbsolutePose::try_new(
848 Mat3::from_rows(
849 [0.9983758533596424, -0.03588494159791196, -0.04424846206277005],
850 [0.034957430317503015, 0.999156210831159, -0.021560250998475538],
851 [0.044984814037660234, 0.019978421460035138, 0.9987878799735018],
852 ),
853 Vec3::new(0.01, -0.004, 0.007),
854 )
855 .unwrap();
856 let mut rng = super::XorShift64::new(5);
857 let uniform = |rng: &mut super::XorShift64| {
858 (rng.next_u64() >> 11) as f64 * (1.0 / ((1_u64 << 53) as f64))
859 };
860 let pairs = (0..90)
861 .map(|index| {
862 let x = -0.95 + 1.9 * uniform(&mut rng);
863 let y = -0.7 + 1.4 * uniform(&mut rng);
864 let z = 2.0 + 0.2 * x + 0.1 * y + 0.002 * (uniform(&mut rng) - 0.5);
865 let object = Vec3::new(x, y, z);
866 let mut image = project_object_point(truth, camera, object).unwrap();
867 image.x += 0.5 * (uniform(&mut rng) - 0.5);
868 image.y += 0.5 * (uniform(&mut rng) - 0.5);
869 if index % 3 == 0 {
870 image.x = 40.0 + 560.0 * uniform(&mut rng);
871 image.y = 40.0 + 400.0 * uniform(&mut rng);
872 }
873 ObjectImageCorrespondence::try_new(object, image).unwrap()
874 })
875 .collect::<Vec<_>>();
876 let estimate = solve_pnp_ransac(
877 &pairs,
878 camera,
879 RobustEstimationOptions { threshold: 2.0, ..Default::default() },
880 )
881 .expect("optional refitting must not discard a valid robust hypothesis");
882 assert!(estimate.inlier_count() >= 58);
883 assert_eq!(estimate.inliers().len(), pairs.len());
884 assert_eq!(estimate.residuals().len(), pairs.len());
885 for ((pair, &inlier), &residual) in
886 pairs.iter().zip(estimate.inliers()).zip(estimate.residuals())
887 {
888 let expected = super::pnp_residual(*estimate.model(), *pair, camera);
889 assert_eq!(residual, expected);
890 assert_eq!(inlier, expected <= 2.0);
891 }
892 }
893
894 fn consensus_fixture() -> (CameraMatrix3, Vec<ObjectImageCorrespondence>) {
895 let camera = camera();
896 let pairs = (0..32)
897 .map(|index| {
898 let object = Vec3::new(
899 (index % 8) as f64 * 0.04 - 0.14,
900 (index / 8) as f64 * 0.06 - 0.09,
901 (index % 3) as f64 * 0.1,
902 );
903 let image = Vec2 {
904 x: 500.0 * (object.x / (object.z + 3.0)) + 320.0,
905 y: 500.0 * (object.y / (object.z + 3.0)) + 240.0,
906 };
907 ObjectImageCorrespondence::try_new(object, image).unwrap()
908 })
909 .collect();
910 (camera, pairs)
911 }
912
913 fn translated_pose(x: f64, z: f64) -> AbsolutePose {
914 AbsolutePose::try_new(Mat3::<f64>::identity(), Vec3::new(x, 0.0, z)).unwrap()
915 }
916
917 fn consensus_for(
918 pose: AbsolutePose,
919 pairs: &[ObjectImageCorrespondence],
920 camera: CameraMatrix3,
921 ) -> super::PnpConsensus {
922 let mut inliers = vec![false; pairs.len()];
923 let mut residuals = vec![0.0; pairs.len()];
924 let (count, error) =
925 super::rescore_pnp(pose, pairs, camera, 2.0, &mut inliers, &mut residuals);
926 (pose, inliers, residuals, count, error)
927 }
928
929 #[test]
930 fn optional_refit_error_or_worse_pose_preserves_owned_rows() {
931 let (camera, pairs) = consensus_fixture();
932 let original_pairs = pairs.clone();
933 let selected = translated_pose(0.003, 3.0);
934 for candidate in [
935 Err(crate::VisionError::InvalidParameter("optional refit failed".into())),
936 Ok(translated_pose(0.1, 3.0)), Ok(translated_pose(0.006, 3.0)), Ok(translated_pose(0.0, -3.0)), ] {
940 let best = consensus_for(selected, &pairs, camera);
941 let decisions = best.1.as_ptr();
942 let residuals = best.2.as_ptr();
943 let estimate =
944 super::finish_pnp_consensus(best, candidate, &pairs, camera, 2.0).unwrap();
945 assert_eq!(*estimate.model(), selected);
946 assert_eq!(estimate.inlier_count(), pairs.len());
947 assert_eq!(estimate.inliers().as_ptr(), decisions);
948 assert_eq!(estimate.residuals().as_ptr(), residuals);
949 for (&residual, pair) in estimate.residuals().iter().zip(&pairs) {
950 let expected = 500.0 * 0.003 / (pair.object().z + 3.0);
951 assert!((residual - expected).abs() < 1e-12);
952 }
953 }
954 assert_eq!(pairs, original_pairs);
955 }
956
957 #[test]
958 fn equal_count_lower_error_refit_is_retained() {
959 let (camera, pairs) = consensus_fixture();
960 let best = consensus_for(translated_pose(0.003, 3.0), &pairs, camera);
961 let refined = translated_pose(0.0, 3.0);
962 let estimate = super::finish_pnp_consensus(best, Ok(refined), &pairs, camera, 2.0).unwrap();
963 assert_eq!(*estimate.model(), refined);
964 assert_eq!(estimate.inlier_count(), pairs.len());
965 assert!(estimate.residuals().iter().all(|&value| value < 1e-12));
966 }
967
968 #[test]
969 fn more_inliers_win_even_with_a_larger_inlier_residual_sum() {
970 let (camera, mut pairs) = consensus_fixture();
971 for pair in pairs.iter_mut().skip(30) {
972 let mut image = pair.image();
973 image.x += 2.2;
974 *pair = ObjectImageCorrespondence::try_new(pair.object(), image).unwrap();
975 }
976 let best = consensus_for(translated_pose(0.0, 3.0), &pairs, camera);
977 assert_eq!(best.3, 30);
978 assert!(best.4 < 1e-12);
979 let refined = translated_pose(0.0048, 3.0);
980 let estimate = super::finish_pnp_consensus(best, Ok(refined), &pairs, camera, 2.0).unwrap();
981 assert_eq!(*estimate.model(), refined);
982 assert_eq!(estimate.inlier_count(), 32);
983 assert!(estimate.residuals().iter().sum::<f64>() > 20.0);
984 }
985
986 #[test]
987 fn exact_consensus_ties_keep_the_selected_pose() {
988 let camera = camera();
989 let pair = ObjectImageCorrespondence::try_new(
990 Vec3::new(0.0, 0.0, 1.0),
991 Vec2 { x: 320.0, y: 240.0 },
992 )
993 .unwrap();
994 let pairs = vec![pair; 8];
995 let selected = translated_pose(0.0, 3.0);
996 let tied = translated_pose(0.0, 4.0);
997 let best = consensus_for(selected, &pairs, camera);
998 let estimate = super::finish_pnp_consensus(best, Ok(tied), &pairs, camera, 2.0).unwrap();
999 assert_eq!(*estimate.model(), selected);
1000 assert!(estimate.residuals().iter().all(|&value| value == 0.0));
1001 }
1002
1003 #[test]
1004 fn missing_robust_hypothesis_still_returns_an_error() {
1005 let camera = camera();
1006 let pair = ObjectImageCorrespondence::try_new(
1007 Vec3::new(0.0, 0.0, 1.0),
1008 Vec2 { x: 320.0, y: 240.0 },
1009 )
1010 .unwrap();
1011 assert!(solve_pnp_ransac(
1012 &[pair; 8],
1013 camera,
1014 RobustEstimationOptions { max_iterations: 8, ..Default::default() },
1015 )
1016 .is_err());
1017 }
1018}