use crate::Error;
use nalgebra::DMatrix;
use serde::{Deserialize, Serialize};
use vision_calibration_core::{BrownConrady5, Mat3, Pt2, Real, Vec2, Vec3, View};
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct DistortionFitOptions {
pub fix_tangential: bool,
pub fix_k3: bool,
pub iters: u32,
}
impl Default for DistortionFitOptions {
fn default() -> Self {
Self {
fix_tangential: false,
fix_k3: true, iters: 8,
}
}
}
pub struct MetaHomography {
pub homography: Mat3,
}
pub type DistortionView = View<MetaHomography>;
pub fn estimate_distortion_from_homographies(
intrinsics: &Mat3,
views: &[DistortionView],
opts: DistortionFitOptions,
) -> Result<BrownConrady5<Real>, Error> {
let total_points: usize = views.iter().map(|v| v.obs.points_2d.len()).sum();
let n_params = match (opts.fix_tangential, opts.fix_k3) {
(true, true) => 2, (true, false) => 3, (false, true) => 4, (false, false) => 5, };
#[allow(clippy::manual_div_ceil)] let min_points = (n_params + 1) / 2 + 2; if total_points < min_points {
return Err(Error::InsufficientData {
need: min_points,
got: total_points,
});
}
let k_inv = intrinsics.try_inverse().ok_or(Error::Singular)?;
let mut a = DMatrix::<Real>::zeros(2 * total_points, n_params);
let mut b = nalgebra::DVector::<Real>::zeros(2 * total_points);
let mut row_idx = 0;
for view in views {
for (board_pt, pixel_obs) in view.obs.points_3d.iter().zip(&view.obs.points_2d) {
let board_h = Vec3::new(board_pt.x, board_pt.y, 1.0);
let pixel_ideal_h = view.meta.homography * board_h;
let pixel_ideal = Pt2::new(
pixel_ideal_h.x / pixel_ideal_h.z,
pixel_ideal_h.y / pixel_ideal_h.z,
);
let n_ideal_h = k_inv * Vec3::new(pixel_ideal.x, pixel_ideal.y, 1.0);
let n_ideal = Vec2::new(n_ideal_h.x / n_ideal_h.z, n_ideal_h.y / n_ideal_h.z);
let n_obs_h = k_inv * Vec3::new(pixel_obs.x, pixel_obs.y, 1.0);
let n_obs = Vec2::new(n_obs_h.x / n_obs_h.z, n_obs_h.y / n_obs_h.z);
let residual = n_obs - n_ideal;
let x = n_ideal.x;
let y = n_ideal.y;
let r2 = x * x + y * y;
let r4 = r2 * r2;
let r6 = r4 * r2;
let mut col_idx = 0;
a[(row_idx, col_idx)] = x * r2;
a[(row_idx + 1, col_idx)] = y * r2;
col_idx += 1;
a[(row_idx, col_idx)] = x * r4;
a[(row_idx + 1, col_idx)] = y * r4;
col_idx += 1;
if !opts.fix_k3 {
a[(row_idx, col_idx)] = x * r6;
a[(row_idx + 1, col_idx)] = y * r6;
col_idx += 1;
}
if !opts.fix_tangential {
let xy = x * y;
let x2 = x * x;
let y2 = y * y;
a[(row_idx, col_idx)] = 2.0 * xy;
a[(row_idx + 1, col_idx)] = r2 + 2.0 * y2;
col_idx += 1;
a[(row_idx, col_idx)] = r2 + 2.0 * x2;
a[(row_idx + 1, col_idx)] = 2.0 * xy;
}
b[row_idx] = residual.x;
b[row_idx + 1] = residual.y;
row_idx += 2;
}
}
let mut max_r2 = 0.0;
for view in views {
for board_pt in &view.obs.points_2d {
let board_h = Vec3::new(board_pt.x, board_pt.y, 1.0);
let pixel_ideal_h = view.meta.homography * board_h;
let pixel_ideal = Pt2::new(
pixel_ideal_h.x / pixel_ideal_h.z,
pixel_ideal_h.y / pixel_ideal_h.z,
);
let n_ideal_h = k_inv * Vec3::new(pixel_ideal.x, pixel_ideal.y, 1.0);
let n_ideal = Vec2::new(n_ideal_h.x / n_ideal_h.z, n_ideal_h.y / n_ideal_h.z);
let r2 = n_ideal.x * n_ideal.x + n_ideal.y * n_ideal.y;
if r2 > max_r2 {
max_r2 = r2;
}
}
}
if max_r2 < 1e-6 {
return Err(Error::invalid_input(
"degenerate configuration for distortion estimation",
));
}
let svd = a.svd(true, true);
let x = svd
.solve(&b, 1e-10)
.map_err(|_| Error::numerical("svd failed during distortion estimation"))?;
let mut col_idx = 0;
let k1 = x[col_idx];
col_idx += 1;
let k2 = x[col_idx];
col_idx += 1;
let k3 = if opts.fix_k3 {
0.0
} else {
let val = x[col_idx];
col_idx += 1;
val
};
let (p1, p2) = if opts.fix_tangential {
(0.0, 0.0)
} else {
let p1 = x[col_idx];
col_idx += 1;
let p2 = x[col_idx];
(p1, p2)
};
Ok(BrownConrady5 {
k1,
k2,
k3,
p1,
p2,
iters: opts.iters,
})
}
#[cfg(test)]
mod tests {
use super::*;
use nalgebra::{Isometry3, Matrix3, Rotation3, Translation3, Vector3};
use vision_calibration_core::{CorrespondenceView, DistortionModel, Pt3};
fn make_kmtx() -> Mat3 {
Matrix3::new(800.0, 0.0, 640.0, 0.0, 800.0, 360.0, 0.0, 0.0, 1.0)
}
fn synthetic_homography_with_distortion(
kmtx: &Mat3,
dist: &BrownConrady5<Real>,
rot: Rotation3<Real>,
t: Vector3<Real>,
board_points: &[Pt3],
) -> (Mat3, Vec<Pt2>) {
let iso = Isometry3::from_parts(Translation3::from(t), rot.into());
let mut pixels = Vec::new();
for bp in board_points {
let p3d = iso.transform_point(bp);
if p3d.z <= 0.0 {
continue;
}
let n_undist = Pt2::new(p3d.x / p3d.z, p3d.y / p3d.z);
let n_dist = dist.distort(&n_undist);
let pixel_h = kmtx * Vec3::new(n_dist.x, n_dist.y, 1.0);
pixels.push(Pt2::new(pixel_h.x / pixel_h.z, pixel_h.y / pixel_h.z));
}
let binding = iso.rotation.to_rotation_matrix();
let r_mat = binding.matrix();
let r1 = r_mat.column(0);
let r2 = r_mat.column(1);
let mut hmtx = Mat3::zeros();
hmtx.set_column(0, &(kmtx * r1));
hmtx.set_column(1, &(kmtx * r2));
hmtx.set_column(2, &(kmtx * t));
(hmtx, pixels)
}
#[test]
fn synthetic_radial_only_recovers_k1_k2() {
let kmtx = make_kmtx();
let dist_gt = BrownConrady5 {
k1: -0.2,
k2: 0.05,
k3: 0.0,
p1: 0.0,
p2: 0.0,
iters: 8,
};
let mut board_points = Vec::new();
for i in 0..7 {
for j in 0..7 {
board_points.push(Pt3::new(i as Real * 30.0, j as Real * 30.0, 0.0));
}
}
let poses = vec![
(
Rotation3::from_euler_angles(0.1, 0.0, 0.05),
Vector3::new(100.0, -50.0, 1000.0),
),
(
Rotation3::from_euler_angles(-0.05, 0.15, -0.1),
Vector3::new(-50.0, 100.0, 1200.0),
),
(
Rotation3::from_euler_angles(0.2, -0.1, 0.0),
Vector3::new(0.0, 0.0, 900.0),
),
];
let mut views = Vec::new();
for (rot, t) in poses {
let (h, pixels) =
synthetic_homography_with_distortion(&kmtx, &dist_gt, rot, t, &board_points);
views.push(DistortionView::new(
CorrespondenceView::new(board_points.clone(), pixels.clone()).unwrap(),
MetaHomography { homography: h },
));
}
let opts = DistortionFitOptions {
fix_tangential: true,
fix_k3: true,
iters: 8,
};
let dist_est = estimate_distortion_from_homographies(&kmtx, &views, opts).unwrap();
println!("Ground truth: k1={}, k2={}", dist_gt.k1, dist_gt.k2);
println!("Estimated: k1={}, k2={}", dist_est.k1, dist_est.k2);
assert!((dist_est.k1 - dist_gt.k1).abs() < 0.1, "k1 error too large");
assert!(
(dist_est.k2 - dist_gt.k2).abs() < 0.03,
"k2 error too large"
);
assert_eq!(dist_est.k3, 0.0);
assert_eq!(dist_est.p1, 0.0);
assert_eq!(dist_est.p2, 0.0);
}
#[test]
fn tangential_distortion_estimation_reasonable() {
let kmtx = make_kmtx();
let dist_gt = BrownConrady5 {
k1: -0.15,
k2: 0.02,
k3: 0.0,
p1: 0.001,
p2: -0.002,
iters: 8,
};
let mut board_points = Vec::new();
for i in 0..7 {
for j in 0..7 {
board_points.push(Pt3::new(i as Real * 30.0, j as Real * 30.0, 0.0));
}
}
let poses = vec![
(
Rotation3::from_euler_angles(0.1, 0.0, 0.05),
Vector3::new(100.0, -50.0, 1000.0),
),
(
Rotation3::from_euler_angles(-0.05, 0.15, -0.1),
Vector3::new(-50.0, 100.0, 1200.0),
),
(
Rotation3::from_euler_angles(0.2, -0.1, 0.0),
Vector3::new(0.0, 0.0, 900.0),
),
(
Rotation3::from_euler_angles(0.0, 0.2, 0.1),
Vector3::new(80.0, 80.0, 1100.0),
),
];
let mut views = Vec::new();
for (rot, t) in poses {
let (h, pixels) =
synthetic_homography_with_distortion(&kmtx, &dist_gt, rot, t, &board_points);
views.push(DistortionView::new(
CorrespondenceView::new(board_points.clone(), pixels.clone()).unwrap(),
MetaHomography { homography: h },
));
}
let opts = DistortionFitOptions {
fix_tangential: false,
fix_k3: true,
iters: 8,
};
let dist_est = estimate_distortion_from_homographies(&kmtx, &views, opts).unwrap();
println!(
"Ground truth: k1={}, k2={}, p1={}, p2={}",
dist_gt.k1, dist_gt.k2, dist_gt.p1, dist_gt.p2
);
println!(
"Estimated: k1={}, k2={}, p1={}, p2={}",
dist_est.k1, dist_est.k2, dist_est.p1, dist_est.p2
);
assert!(
dist_est.k1.signum() == dist_gt.k1.signum(),
"k1 sign mismatch"
);
assert!(dist_est.p1.abs() < 0.01, "p1 order of magnitude reasonable");
assert!(dist_est.p2.abs() < 0.01, "p2 order of magnitude reasonable");
}
}