use kornia_algebra::{Mat3AF32, Vec2F32, Vec3AF32};
use kornia_imgproc::calibration::distortion::PolynomialDistortion;
use crate::pnp::epnp::{solve_epnp, EPnPParams};
use crate::ransac::{Estimator, Match2d3d};
#[derive(Debug, Clone, Copy)]
pub struct EPnPModel {
pub rotation: Mat3AF32,
pub translation: Vec3AF32,
}
#[derive(Debug, Clone)]
pub struct EPnPEstimator {
pub k: Mat3AF32,
pub distortion: Option<PolynomialDistortion>,
pub params: EPnPParams,
}
impl EPnPEstimator {
pub fn new(k: Mat3AF32) -> Self {
Self {
k,
distortion: None,
params: EPnPParams::default(),
}
}
pub fn with_distortion(mut self, distortion: PolynomialDistortion) -> Self {
self.distortion = Some(distortion);
self
}
pub fn with_params(mut self, params: EPnPParams) -> Self {
self.params = params;
self
}
}
impl Estimator for EPnPEstimator {
type Model = EPnPModel;
type Sample = Match2d3d;
const SAMPLE_SIZE: usize = 4;
fn fit(&self, samples: &[Self::Sample], out: &mut Vec<Self::Model>) {
if samples.len() < Self::SAMPLE_SIZE {
return;
}
let n = samples.len();
let mut world = Vec::with_capacity(n);
let mut image = Vec::with_capacity(n);
for s in samples {
world.push(Vec3AF32::new(
s.object.x as f32,
s.object.y as f32,
s.object.z as f32,
));
image.push(Vec2F32::new(s.image.x as f32, s.image.y as f32));
}
if let Ok(result) = solve_epnp(
&world,
&image,
&self.k,
self.distortion.as_ref(),
&self.params,
) {
out.push(EPnPModel {
rotation: result.rotation,
translation: result.translation,
});
}
}
fn residual(&self, model: &Self::Model, sample: &Self::Sample) -> f64 {
let p = Vec3AF32::new(
sample.object.x as f32,
sample.object.y as f32,
sample.object.z as f32,
);
let pc = model.rotation * p + model.translation;
if pc.z.abs() < 1e-6 {
return f64::INFINITY;
}
let xn = pc.x / pc.z;
let yn = pc.y / pc.z;
let arr = self.k.to_cols_array();
let fx = arr[0] as f64;
let fy = arr[4] as f64;
let cx = arr[6] as f64;
let cy = arr[7] as f64;
let u = fx * xn as f64 + cx;
let v = fy * yn as f64 + cy;
let du = u - sample.image.x;
let dv = v - sample.image.y;
du * du + dv * dv
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pnp::refine::LMRefineParams;
use kornia_algebra::{Vec2F64, Vec3F64};
#[test]
fn fits_and_scores_clean_correspondences() {
let fx = 600.0_f32;
let fy = 600.0_f32;
let cx = 320.0_f32;
let cy = 240.0_f32;
let k = Mat3AF32::from_cols(
Vec3AF32::new(fx, 0.0, 0.0),
Vec3AF32::new(0.0, fy, 0.0),
Vec3AF32::new(cx, cy, 1.0),
);
let world_pts = [
Vec3F64::new(-0.4, -0.3, 5.0),
Vec3F64::new(0.3, -0.2, 4.5),
Vec3F64::new(-0.2, 0.4, 6.0),
Vec3F64::new(0.5, 0.3, 5.5),
Vec3F64::new(-0.1, -0.5, 4.8),
Vec3F64::new(0.2, 0.1, 5.2),
];
let angle = 0.05_f64;
let r = [
[angle.cos(), 0.0, -angle.sin()],
[0.0, 1.0, 0.0],
[angle.sin(), 0.0, angle.cos()],
];
let t = [0.1_f64, -0.05, 0.2];
let matches: Vec<Match2d3d> = world_pts
.iter()
.map(|p| {
let pc = [
r[0][0] * p.x + r[0][1] * p.y + r[0][2] * p.z + t[0],
r[1][0] * p.x + r[1][1] * p.y + r[1][2] * p.z + t[1],
r[2][0] * p.x + r[2][1] * p.y + r[2][2] * p.z + t[2],
];
let u = fx as f64 * pc[0] / pc[2] + cx as f64;
let v = fy as f64 * pc[1] / pc[2] + cy as f64;
Match2d3d::new(*p, Vec2F64::new(u, v))
})
.collect();
let est = EPnPEstimator::new(k).with_params(EPnPParams {
refine_lm: Some(LMRefineParams::default()),
..Default::default()
});
let mut models = Vec::new();
est.fit(&matches, &mut models);
assert_eq!(models.len(), 1, "expected exactly one EPnP solution");
for m in &matches {
let r2 = est.residual(&models[0], m);
assert!(r2.is_finite(), "non-finite reprojection²: {r2}");
}
}
#[test]
fn under_min_samples_yields_no_model() {
let k = Mat3AF32::from_cols(
Vec3AF32::new(500.0, 0.0, 0.0),
Vec3AF32::new(0.0, 500.0, 0.0),
Vec3AF32::new(320.0, 240.0, 1.0),
);
let est = EPnPEstimator::new(k);
let mut models = Vec::new();
est.fit(&[], &mut models);
assert!(models.is_empty());
}
}