use crate::factors::reprojection_model::apply_scheimpflug_inverse_generic;
use nalgebra::{DVectorView, Quaternion, RealField, SVector, UnitQuaternion, Vector2, Vector3};
fn undistort_pixel_to_normalized<T: RealField>(
intr: DVectorView<'_, T>,
dist: DVectorView<'_, T>,
sensor: DVectorView<'_, T>,
laser_pixel: [f64; 2],
) -> (T, T) {
debug_assert!(intr.len() >= 4, "intrinsics must have 4 params");
debug_assert!(dist.len() >= 5, "distortion must have 5 params");
debug_assert!(sensor.len() >= 2, "sensor must have 2 params");
let fx = intr[0].clone();
let fy = intr[1].clone();
let cx = intr[2].clone();
let cy = intr[3].clone();
let tau_x = sensor[0].clone();
let tau_y = sensor[1].clone();
let k1 = dist[0].clone();
let k2 = dist[1].clone();
let k3 = dist[2].clone();
let p1 = dist[3].clone();
let p2 = dist[4].clone();
let u_px = T::from_f64(laser_pixel[0]).unwrap();
let v_px = T::from_f64(laser_pixel[1]).unwrap();
let x_s = (u_px - cx) / fx;
let y_s = (v_px - cy) / fy;
let (x_d, y_d) = apply_scheimpflug_inverse_generic(x_s, y_s, tau_x, tau_y);
let mut x_u = x_d.clone();
let mut y_u = y_d.clone();
for _ in 0..5 {
let r2 = x_u.clone() * x_u.clone() + y_u.clone() * y_u.clone();
let r4 = r2.clone() * r2.clone();
let r6 = r4.clone() * r2.clone();
let radial = T::one() + k1.clone() * r2.clone() + k2.clone() * r4.clone() + k3.clone() * r6;
let xy = x_u.clone() * y_u.clone();
let two = T::from_f64(2.0).unwrap();
let dx_t = two.clone() * p1.clone() * xy.clone()
+ p2.clone() * (r2.clone() + two.clone() * x_u.clone() * x_u.clone());
let dy_t = p1.clone() * (r2.clone() + two.clone() * y_u.clone() * y_u.clone())
+ two.clone() * p2.clone() * xy;
x_u = (x_d.clone() - dx_t) / radial.clone();
y_u = (y_d.clone() - dy_t) / radial;
}
(x_u, y_u)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn laser_plane_pixel_residual_generic<T: RealField>(
intr: DVectorView<'_, T>,
dist: DVectorView<'_, T>,
sensor: DVectorView<'_, T>,
pose: DVectorView<'_, T>,
plane_normal: DVectorView<'_, T>,
plane_distance: DVectorView<'_, T>,
laser_pixel: [f64; 2],
w: f64,
) -> SVector<T, 1> {
debug_assert!(intr.len() >= 4, "intrinsics must have 4 params");
debug_assert!(dist.len() >= 5, "distortion must have 5 params");
debug_assert!(sensor.len() >= 2, "sensor must have 2 params");
debug_assert!(pose.len() == 7, "pose must have 7 params (SE3)");
debug_assert!(plane_normal.len() == 3, "plane normal must have 3 params");
debug_assert!(
plane_distance.len() == 1,
"plane distance must have 1 param"
);
let pose_qx = pose[0].clone();
let pose_qy = pose[1].clone();
let pose_qz = pose[2].clone();
let pose_qw = pose[3].clone();
let pose_tx = pose[4].clone();
let pose_ty = pose[5].clone();
let pose_tz = pose[6].clone();
let pose_quat = Quaternion::new(pose_qw, pose_qx, pose_qy, pose_qz);
let pose_rot = UnitQuaternion::from_quaternion(pose_quat);
let pose_t = Vector3::new(pose_tx, pose_ty, pose_tz);
let (x_u, y_u) = undistort_pixel_to_normalized(intr, dist, sensor, laser_pixel);
let ray_dir_camera = Vector3::new(x_u, y_u, T::one()).normalize();
let ray_dir_target = pose_rot.inverse_transform_vector(&ray_dir_camera);
let ray_origin_target = pose_rot.inverse_transform_vector(&(-pose_t.clone()));
let eps = T::from_f64(1e-9).unwrap();
if ray_dir_target.z.clone().abs() < eps {
let large_residual = T::from_f64(1e6).unwrap();
return SVector::<T, 1>::new(large_residual);
}
let t_intersect = -ray_origin_target.z.clone() / ray_dir_target.z.clone();
if t_intersect < -eps {
let large_residual = T::from_f64(1e6).unwrap();
return SVector::<T, 1>::new(large_residual);
}
let pt_target = ray_origin_target + ray_dir_target * t_intersect;
let pt_camera = pose_rot.transform_vector(&pt_target) + pose_t;
let n_c = Vector3::new(
plane_normal[0].clone(),
plane_normal[1].clone(),
plane_normal[2].clone(),
);
let d_c = plane_distance[0].clone();
let dist_to_plane = n_c.dot(&pt_camera) + d_c;
let sqrt_w = T::from_f64(w.sqrt()).unwrap();
let residual = dist_to_plane * sqrt_w;
SVector::<T, 1>::new(residual)
}
fn compute_line_origin<T: RealField>(
n1: &Vector3<T>,
d1: &T,
n2: &Vector3<T>,
d2: &T,
v: &Vector3<T>,
) -> Vector3<T> {
let abs_vx = v.x.clone().abs();
let abs_vy = v.y.clone().abs();
let abs_vz = v.z.clone().abs();
if abs_vz >= abs_vx.clone() && abs_vz >= abs_vy.clone() {
let det = n1.x.clone() * n2.y.clone() - n1.y.clone() * n2.x.clone();
let eps = T::from_f64(1e-12).unwrap();
if det.clone().abs() < eps {
return Vector3::new(T::zero(), T::zero(), T::zero());
}
let x = ((-d1.clone()) * n2.y.clone() - (-d2.clone()) * n1.y.clone()) / det.clone();
let y = (n1.x.clone() * (-d2.clone()) - n2.x.clone() * (-d1.clone())) / det;
Vector3::new(x, y, T::zero())
} else if abs_vy >= abs_vx {
let det = n1.x.clone() * n2.z.clone() - n1.z.clone() * n2.x.clone();
let eps = T::from_f64(1e-12).unwrap();
if det.clone().abs() < eps {
return Vector3::new(T::zero(), T::zero(), T::zero());
}
let x = ((-d1.clone()) * n2.z.clone() - (-d2.clone()) * n1.z.clone()) / det.clone();
let z = (n1.x.clone() * (-d2.clone()) - n2.x.clone() * (-d1.clone())) / det;
Vector3::new(x, T::zero(), z)
} else {
let det = n1.y.clone() * n2.z.clone() - n1.z.clone() * n2.y.clone();
let eps = T::from_f64(1e-12).unwrap();
if det.clone().abs() < eps {
return Vector3::new(T::zero(), T::zero(), T::zero());
}
let y = ((-d1.clone()) * n2.z.clone() - (-d2.clone()) * n1.z.clone()) / det.clone();
let z = (n1.y.clone() * (-d2.clone()) - n2.y.clone() * (-d1.clone())) / det;
Vector3::new(T::zero(), y, z)
}
}
fn project_line_to_normalized_plane<T: RealField>(
p0: &Vector3<T>,
v: &Vector3<T>,
) -> (Vector2<T>, Vector2<T>) {
let p0_norm = Vector2::new(p0.x.clone() / p0.z.clone(), p0.y.clone() / p0.z.clone());
let z2_inv = T::one() / (p0.z.clone() * p0.z.clone());
let v_norm_x = (v.x.clone() * p0.z.clone() - p0.x.clone() * v.z.clone()) * z2_inv.clone();
let v_norm_y = (v.y.clone() * p0.z.clone() - p0.y.clone() * v.z.clone()) * z2_inv;
let v_norm_len =
(v_norm_x.clone() * v_norm_x.clone() + v_norm_y.clone() * v_norm_y.clone()).sqrt();
let eps = T::from_f64(1e-12).unwrap();
if v_norm_len.clone() < eps {
return (p0_norm, Vector2::new(T::one(), T::zero()));
}
let v_norm = Vector2::new(v_norm_x / v_norm_len.clone(), v_norm_y / v_norm_len);
(p0_norm, v_norm)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn laser_line_dist_normalized_generic<T: RealField>(
intr: DVectorView<'_, T>,
dist: DVectorView<'_, T>,
sensor: DVectorView<'_, T>,
pose: DVectorView<'_, T>,
plane_normal: DVectorView<'_, T>,
plane_distance: DVectorView<'_, T>,
laser_pixel: [f64; 2],
w: f64,
) -> SVector<T, 1> {
debug_assert!(intr.len() >= 4, "intrinsics must have 4 params");
debug_assert!(dist.len() >= 5, "distortion must have 5 params");
debug_assert!(sensor.len() >= 2, "sensor must have 2 params");
debug_assert!(pose.len() == 7, "pose must have 7 params (SE3)");
debug_assert!(plane_normal.len() == 3, "plane normal must have 3 params");
debug_assert!(
plane_distance.len() == 1,
"plane distance must have 1 param"
);
let fx = intr[0].clone();
let fy = intr[1].clone();
let pose_qx = pose[0].clone();
let pose_qy = pose[1].clone();
let pose_qz = pose[2].clone();
let pose_qw = pose[3].clone();
let pose_tx = pose[4].clone();
let pose_ty = pose[5].clone();
let pose_tz = pose[6].clone();
let pose_quat = Quaternion::new(pose_qw, pose_qx, pose_qy, pose_qz);
let pose_rot = UnitQuaternion::from_quaternion(pose_quat);
let pose_t = Vector3::new(pose_tx, pose_ty, pose_tz);
let n_c = Vector3::new(
plane_normal[0].clone(),
plane_normal[1].clone(),
plane_normal[2].clone(),
);
let d_c = plane_distance[0].clone();
let rot_matrix = pose_rot.to_rotation_matrix();
let n_target_c = rot_matrix.matrix().column(2).into_owned();
let d_target_c = -n_target_c.dot(&pose_t);
let v_3d = n_c.cross(&n_target_c);
let v_norm_3d = v_3d.norm();
let epsilon = T::from_f64(1e-9).unwrap();
if v_norm_3d < epsilon {
let large_residual = T::from_f64(1e6).unwrap();
return SVector::<T, 1>::new(large_residual);
}
let v_3d_unit = v_3d / v_norm_3d;
let p0_3d = compute_line_origin(&n_c, &d_c, &n_target_c, &d_target_c, &v_3d_unit);
let p0_z_abs = p0_3d.z.clone().abs();
if p0_z_abs < epsilon {
let large_residual = T::from_f64(1e6).unwrap();
return SVector::<T, 1>::new(large_residual);
}
let (p0_norm, v_norm) = project_line_to_normalized_plane(&p0_3d, &v_3d_unit);
let (x_u, y_u) = undistort_pixel_to_normalized(intr, dist, sensor, laser_pixel);
let to_pixel_x = x_u - p0_norm.x.clone();
let to_pixel_y = y_u - p0_norm.y.clone();
let cross = v_norm.x.clone() * to_pixel_y - v_norm.y.clone() * to_pixel_x;
let dist_normalized = cross.abs();
let f_geom = (fx * fy).sqrt();
let dist_pixels = dist_normalized * f_geom;
let sqrt_w = T::from_f64(w.sqrt()).unwrap();
let residual = dist_pixels * sqrt_w;
SVector::<T, 1>::new(residual)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::factors::reprojection_model::apply_scheimpflug_generic;
use nalgebra::DVector;
#[test]
fn laser_residual_smoke_test() {
let intr = DVector::from_vec(vec![800.0, 800.0, 640.0, 360.0]);
let dist = DVector::from_vec(vec![0.0, 0.0, 0.0, 0.0, 0.0]); let sensor = DVector::from_vec(vec![0.0, 0.0]);
let pose = DVector::from_vec(vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]);
let plane_normal = DVector::from_vec(vec![0.0, 0.0, 1.0]);
let plane_distance = DVector::from_vec(vec![-0.5]);
let pixel = [640.0, 360.0];
let w = 1.0;
let residual = laser_plane_pixel_residual_generic(
intr.as_view(),
dist.as_view(),
sensor.as_view(),
pose.as_view(),
plane_normal.as_view(),
plane_distance.as_view(),
pixel,
w,
);
assert!(
(residual[0] + 0.5_f64).abs() < 0.1,
"residual: {}",
residual[0]
);
}
#[test]
fn laser_line_dist_normalized_smoke_test() {
let intr = DVector::from_vec(vec![800.0, 800.0, 640.0, 360.0]);
let dist = DVector::from_vec(vec![0.0, 0.0, 0.0, 0.0, 0.0]); let sensor = DVector::from_vec(vec![0.0, 0.0]);
let pose = DVector::from_vec(vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.5]);
let normal = nalgebra::Vector3::new(0.1, 0.0, 1.0).normalize();
let plane_normal = DVector::from_vec(vec![normal.x, normal.y, normal.z]);
let plane_distance = DVector::from_vec(vec![-0.4]);
let pixel = [640.0, 360.0];
let w = 1.0;
let residual = laser_line_dist_normalized_generic(
intr.as_view(),
dist.as_view(),
sensor.as_view(),
pose.as_view(),
plane_normal.as_view(),
plane_distance.as_view(),
pixel,
w,
);
let res: f64 = residual[0];
assert!(
res.is_finite(),
"residual should be finite (not NaN/Inf): {}",
res
);
assert!(
res.abs() < 10000.0,
"residual magnitude unreasonable: {}",
res
);
}
#[test]
fn undistort_center_pixel_is_zero() {
let intr = DVector::from_vec(vec![800.0, 800.0, 640.0, 360.0]);
let dist = DVector::from_vec(vec![0.0, 0.0, 0.0, 0.0, 0.0]);
let sensor = DVector::from_vec(vec![0.0, 0.0]);
let (x_u, y_u): (f64, f64) = undistort_pixel_to_normalized(
intr.as_view(),
dist.as_view(),
sensor.as_view(),
[640.0, 360.0],
);
assert!(x_u.abs() < 1e-12_f64, "x_u should be 0 for principal point");
assert!(y_u.abs() < 1e-12_f64, "y_u should be 0 for principal point");
}
#[test]
fn undistort_inverts_scheimpflug_for_zero_distortion() {
let intr = DVector::from_vec(vec![800.0, 820.0, 640.0, 360.0]);
let dist = DVector::from_vec(vec![0.0, 0.0, 0.0, 0.0, 0.0]);
let sensor = DVector::from_vec(vec![0.02, -0.01]);
let x_norm = 0.1_f64;
let y_norm = -0.05_f64;
let (x_sensor, y_sensor) = apply_scheimpflug_generic(x_norm, y_norm, sensor[0], sensor[1]);
let u = intr[0] * x_sensor + intr[2];
let v = intr[1] * y_sensor + intr[3];
let (x_u, y_u): (f64, f64) =
undistort_pixel_to_normalized(intr.as_view(), dist.as_view(), sensor.as_view(), [u, v]);
assert!((x_u - x_norm).abs() < 1e-8, "x_u mismatch: {}", x_u);
assert!((y_u - y_norm).abs() < 1e-8, "y_u mismatch: {}", y_u);
}
#[test]
fn test_line_projection_geometry() {
use nalgebra::Vector3;
let p0 = Vector3::new(1.0, 0.5, 1.0);
let v = Vector3::new(1.0, 0.0, 0.0);
let (p0_norm, v_norm) = project_line_to_normalized_plane(&p0, &v);
assert!((p0_norm.x - 1.0_f64).abs() < 1e-10);
assert!((p0_norm.y - 0.5_f64).abs() < 1e-10);
assert!((v_norm.x - 1.0_f64).abs() < 1e-10);
assert!(v_norm.y.abs() < 1e-10_f64);
}
#[test]
fn test_compute_line_origin_simple() {
use nalgebra::Vector3;
let n1 = Vector3::new(0.0, 0.0, 1.0); let d1 = 0.0;
let n2 = Vector3::new(1.0, 0.0, 0.0); let d2 = 0.0;
let v = n1.cross(&n2);
let p0 = compute_line_origin(&n1, &d1, &n2, &d2, &v);
let plane1_dist: f64 = n1.dot(&p0) + d1;
let plane2_dist: f64 = n2.dot(&p0) + d2;
assert!(plane1_dist.abs() < 1e-10);
assert!(plane2_dist.abs() < 1e-10);
assert!(p0.x.abs() < 1e-10);
assert!(p0.z.abs() < 1e-10);
}
}