use super::dlt;
use crate::Error;
use vision_calibration_core::{
Camera, Estimator, FxFyCxCySkew, IdentitySensor, Iso3, NoDistortion, Pinhole, Pt2, Pt3,
RansacOptions, Real, ransac_fit,
};
pub fn dlt_ransac(
world: &[Pt3],
image: &[Pt2],
k: &FxFyCxCySkew<Real>,
opts: &RansacOptions,
) -> Result<(Iso3, Vec<usize>), Error> {
let n = world.len();
if n < 6 || image.len() != n {
return Err(Error::InsufficientData { need: 6, got: n });
}
#[derive(Clone)]
struct PnpDatum {
pw: Pt3,
pi: Pt2,
k: FxFyCxCySkew<Real>,
}
struct PnpEst;
impl Estimator for PnpEst {
type Datum = PnpDatum;
type Model = Iso3;
const MIN_SAMPLES: usize = 6;
fn fit(data: &[Self::Datum], sample_indices: &[usize]) -> Option<Self::Model> {
let mut world = Vec::with_capacity(sample_indices.len());
let mut image = Vec::with_capacity(sample_indices.len());
for &idx in sample_indices {
world.push(data[idx].pw);
image.push(data[idx].pi);
}
let k = data[0].k;
dlt::dlt(&world, &image, &k).ok()
}
fn residual(model: &Self::Model, datum: &Self::Datum) -> f64 {
let cam = Camera::new(Pinhole, NoDistortion, IdentitySensor, datum.k);
let pw = datum.pw;
let pc = model.transform_point(&pw);
let Some(proj) = cam.project_point(&pc) else {
return f64::INFINITY;
};
let du = proj.x - datum.pi.x;
let dv = proj.y - datum.pi.y;
(du * du + dv * dv).sqrt()
}
fn is_degenerate(_data: &[Self::Datum], sample_indices: &[usize]) -> bool {
sample_indices.len() < Self::MIN_SAMPLES
}
}
let data: Vec<PnpDatum> = world
.iter()
.cloned()
.zip(image.iter().cloned())
.map(|(pw, pi)| PnpDatum { pw, pi, k: *k })
.collect();
let res = ransac_fit::<PnpEst>(&data, opts);
if !res.success {
return Err(Error::numerical(
"ransac failed to find a consensus PnP solution",
));
}
let pose = res.model.expect("success guarantees a model");
Ok((pose, res.inliers))
}
#[cfg(test)]
mod tests {
use super::*;
use nalgebra::{Isometry3, Rotation3, Translation3};
use vision_calibration_core::{Camera, IdentitySensor, NoDistortion, Pinhole};
#[test]
fn ransac_handles_outliers() {
let k = FxFyCxCySkew {
fx: 800.0,
fy: 780.0,
cx: 640.0,
cy: 360.0,
skew: 0.0,
};
let cam = Camera::new(Pinhole, NoDistortion, IdentitySensor, k);
let rot = Rotation3::from_euler_angles(0.1, -0.05, 0.2);
let t = Translation3::new(0.1, -0.05, 1.0);
let iso_gt = Isometry3::from_parts(t, rot.into());
let mut world = Vec::new();
let mut image = Vec::new();
for z in 0..2 {
for y in 0..3 {
for x in 0..4 {
let pw = Pt3::new(x as Real * 0.1, y as Real * 0.1, 0.5 + z as Real * 0.1);
let pc = iso_gt.transform_point(&pw);
let uv = cam.project_point(&pc).unwrap();
world.push(pw);
image.push(uv);
}
}
}
let inlier_count = world.len();
for i in 0..4 {
world.push(Pt3::new(0.5 + i as Real * 0.2, -0.3, 1.2));
image.push(Pt2::new(
1200.0 + i as Real * 50.0,
-100.0 - i as Real * 25.0,
));
}
let opts = RansacOptions {
max_iters: 500,
thresh: 1.0,
min_inliers: inlier_count.saturating_sub(2),
confidence: 0.99,
seed: 77,
refit_on_inliers: true,
};
let (est, inliers) = dlt_ransac(&world, &image, &k, &opts).unwrap();
assert!(inliers.len() >= inlier_count.saturating_sub(2));
assert!(inliers.len() < world.len());
let dt = (est.translation.vector - iso_gt.translation.vector).norm();
let r_est = est.rotation.to_rotation_matrix();
let r_gt = iso_gt.rotation.to_rotation_matrix();
let r_diff = r_est.transpose() * r_gt;
let trace = r_diff.matrix().trace();
let cos_theta = ((trace - 1.0) * 0.5).clamp(-1.0, 1.0);
let ang = cos_theta.acos();
assert!(dt < 1e-3, "translation error too large: {}", dt);
assert!(ang < 1e-3, "rotation error too large: {}", ang);
}
}