use crate::math::projection::project_pinhole;
use nalgebra::{DVectorView, Matrix3, Quaternion, RealField, SVector, UnitQuaternion, Vector3};
#[derive(Debug, Clone, Copy)]
pub(crate) struct ObservationData {
pub pw: [f64; 3],
pub uv: [f64; 2],
pub w: f64,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct RobotPoseData {
pub robot_se3: [f64; 7],
pub mode: crate::ir::HandEyeMode,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct HandEyeRobotDeltaData {
pub robot: RobotPoseData,
pub obs: ObservationData,
}
pub(crate) fn reproj_residual_pinhole4_se3_generic<T: RealField>(
intr: DVectorView<'_, T>,
pose: DVectorView<'_, T>,
pw: [f64; 3],
uv: [f64; 2],
w: f64,
) -> SVector<T, 2> {
debug_assert!(intr.len() >= 4, "intrinsics must have 4 params");
debug_assert!(pose.len() == 7, "pose must have 7 params");
let fx = intr[0].clone();
let fy = intr[1].clone();
let cx = intr[2].clone();
let cy = intr[3].clone();
let qx = pose[0].clone();
let qy = pose[1].clone();
let qz = pose[2].clone();
let qw = pose[3].clone();
let tx = pose[4].clone();
let ty = pose[5].clone();
let tz = pose[6].clone();
let quat = Quaternion::new(qw, qx, qy, qz);
let rot = UnitQuaternion::from_quaternion(quat);
let t = Vector3::new(tx, ty, tz);
let pw_t = Vector3::new(
T::from_f64(pw[0]).unwrap(),
T::from_f64(pw[1]).unwrap(),
T::from_f64(pw[2]).unwrap(),
);
let pc = rot.transform_vector(&pw_t) + t;
let proj = project_pinhole(fx, fy, cx, cy, pc);
let sqrt_w = T::from_f64(w.sqrt()).unwrap();
let u_meas = T::from_f64(uv[0]).unwrap();
let v_meas = T::from_f64(uv[1]).unwrap();
let ru = (u_meas - proj.x.clone()) * sqrt_w.clone();
let rv = (v_meas - proj.y.clone()) * sqrt_w;
SVector::<T, 2>::new(ru, rv)
}
fn distort_brown_conrady_generic<T: RealField>(
x: T,
y: T,
k1: T,
k2: T,
k3: T,
p1: T,
p2: T,
) -> (T, T) {
let r2 = x.clone() * x.clone() + y.clone() * y.clone();
let r4 = r2.clone() * r2.clone();
let r6 = r4.clone() * r2.clone();
let radial = T::one() + k1 * r2.clone() + k2 * r4 + k3 * r6;
let two = T::one() + T::one();
let x2 = x.clone() * x.clone();
let y2 = y.clone() * y.clone();
let xy = x.clone() * y.clone();
let x_tan =
two.clone() * p1.clone() * xy.clone() + p2.clone() * (r2.clone() + two.clone() * x2);
let y_tan = p1 * (r2 + two.clone() * y2) + two * p2 * xy;
(x.clone() * radial.clone() + x_tan, y * radial + y_tan)
}
fn skew_matrix<T: RealField>(w: &Vector3<T>) -> Matrix3<T> {
Matrix3::new(
T::zero(),
-w.z.clone(),
w.y.clone(),
w.z.clone(),
T::zero(),
-w.x.clone(),
-w.y.clone(),
w.x.clone(),
T::zero(),
)
}
fn se3_exp<T: RealField>(xi: DVectorView<'_, T>) -> (UnitQuaternion<T>, Vector3<T>) {
debug_assert!(xi.len() == 6, "se3 tangent must have 6 params");
let w = Vector3::new(xi[0].clone(), xi[1].clone(), xi[2].clone());
let v = Vector3::new(xi[3].clone(), xi[4].clone(), xi[5].clone());
let theta = w.norm();
let eps = T::from_f64(1e-9).unwrap();
let w_hat = skew_matrix(&w);
let w_hat2 = w_hat.clone() * w_hat.clone();
let (b, c) = if theta.clone() <= eps {
let half = T::from_f64(0.5).unwrap();
let sixth = T::from_f64(1.0 / 6.0).unwrap();
(half, sixth)
} else {
let theta2 = theta.clone() * theta.clone();
let theta3 = theta2.clone() * theta.clone();
let sin_theta = theta.clone().sin();
let cos_theta = theta.clone().cos();
let b = (T::one() - cos_theta) / theta2;
let c = (theta - sin_theta) / theta3;
(b, c)
};
let v_mat = Matrix3::identity() + w_hat * b + w_hat2 * c;
let t = v_mat * v;
let rot = UnitQuaternion::from_scaled_axis(w);
(rot, t)
}
pub(crate) fn reproj_residual_pinhole4_dist5_se3_generic<T: RealField>(
intr: DVectorView<'_, T>,
dist: DVectorView<'_, T>,
pose: DVectorView<'_, T>,
pw: [f64; 3],
uv: [f64; 2],
w: f64,
) -> SVector<T, 2> {
debug_assert!(intr.len() >= 4, "intrinsics must have 4 params");
debug_assert!(dist.len() >= 5, "distortion must have 5 params");
debug_assert!(pose.len() == 7, "pose must have 7 params");
let fx = intr[0].clone();
let fy = intr[1].clone();
let cx = intr[2].clone();
let cy = intr[3].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 qx = pose[0].clone();
let qy = pose[1].clone();
let qz = pose[2].clone();
let qw = pose[3].clone();
let tx = pose[4].clone();
let ty = pose[5].clone();
let tz = pose[6].clone();
let quat = Quaternion::new(qw, qx, qy, qz);
let rot = UnitQuaternion::from_quaternion(quat);
let t = Vector3::new(tx, ty, tz);
let pw_t = Vector3::new(
T::from_f64(pw[0]).unwrap(),
T::from_f64(pw[1]).unwrap(),
T::from_f64(pw[2]).unwrap(),
);
let pc = rot.transform_vector(&pw_t) + t;
let eps = T::from_f64(1e-12).unwrap();
let z_safe = if pc.z.clone() > eps.clone() {
pc.z.clone()
} else {
eps
};
let x_norm = pc.x.clone() / z_safe.clone();
let y_norm = pc.y.clone() / z_safe;
let (x_dist, y_dist) = distort_brown_conrady_generic(x_norm, y_norm, k1, k2, k3, p1, p2);
let u_proj = fx * x_dist + cx;
let v_proj = fy * y_dist + cy;
let sqrt_w = T::from_f64(w.sqrt()).unwrap();
let u_meas = T::from_f64(uv[0]).unwrap();
let v_meas = T::from_f64(uv[1]).unwrap();
let ru = (u_meas - u_proj) * sqrt_w.clone();
let rv = (v_meas - v_proj) * sqrt_w;
SVector::<T, 2>::new(ru, rv)
}
pub(crate) fn tilt_projection_matrix_generic<T: RealField>(
tau_x: T,
tau_y: T,
) -> nalgebra::Matrix3<T> {
let s_tx = tau_x.clone().sin();
let c_tx = tau_x.cos();
let s_ty = tau_y.clone().sin();
let c_ty = tau_y.cos();
let zero = T::zero();
let one = T::one();
let rot_x = nalgebra::Matrix3::new(
one.clone(),
zero.clone(),
zero.clone(),
zero.clone(),
c_tx.clone(),
s_tx.clone(),
zero.clone(),
-s_tx.clone(),
c_tx,
);
let rot_y = nalgebra::Matrix3::new(
c_ty.clone(),
zero.clone(),
-s_ty.clone(),
zero.clone(),
one.clone(),
zero.clone(),
s_ty.clone(),
zero.clone(),
c_ty,
);
let rot_xy = rot_y * rot_x;
let r22 = rot_xy[(2, 2)].clone();
let r02 = rot_xy[(0, 2)].clone();
let r12 = rot_xy[(1, 2)].clone();
nalgebra::Matrix3::new(
r22.clone(),
zero.clone(),
-r02,
zero.clone(),
r22.clone(),
-r12,
zero.clone(),
zero.clone(),
one,
) * rot_xy
}
pub(crate) fn apply_scheimpflug_generic<T: RealField>(
x_norm: T,
y_norm: T,
tau_x: T,
tau_y: T,
) -> (T, T) {
let h = tilt_projection_matrix_generic(tau_x, tau_y);
let p = nalgebra::Vector3::new(x_norm, y_norm, T::one());
let p_tilted = h * p;
let eps = T::from_f64(1e-12).unwrap();
let z_safe = if p_tilted.z.clone() > eps.clone() {
p_tilted.z.clone()
} else {
eps
};
(
p_tilted.x.clone() / z_safe.clone(),
p_tilted.y.clone() / z_safe,
)
}
pub(crate) fn apply_scheimpflug_inverse_generic<T: RealField>(
x_sensor: T,
y_sensor: T,
tau_x: T,
tau_y: T,
) -> (T, T) {
let h = tilt_projection_matrix_generic(tau_x, tau_y);
let h_inv = match h.try_inverse() {
Some(inv) => inv,
None => return (x_sensor, y_sensor),
};
let p = h_inv * Vector3::new(x_sensor, y_sensor, T::one());
let eps = T::from_f64(1e-12).unwrap();
let z_safe = if p.z.clone().abs() > eps.clone() {
p.z.clone()
} else {
eps
};
(p.x.clone() / z_safe.clone(), p.y.clone() / z_safe)
}
pub(crate) fn reproj_residual_pinhole4_dist5_scheimpflug2_se3_generic<T: RealField>(
intr: DVectorView<'_, T>,
dist: DVectorView<'_, T>,
sensor: DVectorView<'_, T>,
pose: DVectorView<'_, T>,
pw: [f64; 3],
uv: [f64; 2],
w: f64,
) -> SVector<T, 2> {
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");
let fx = intr[0].clone();
let fy = intr[1].clone();
let cx = intr[2].clone();
let cy = intr[3].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 tau_x = sensor[0].clone();
let tau_y = sensor[1].clone();
let qx = pose[0].clone();
let qy = pose[1].clone();
let qz = pose[2].clone();
let qw = pose[3].clone();
let tx = pose[4].clone();
let ty = pose[5].clone();
let tz = pose[6].clone();
let quat = Quaternion::new(qw, qx, qy, qz);
let rot = UnitQuaternion::from_quaternion(quat);
let t = Vector3::new(tx, ty, tz);
let pw_t = Vector3::new(
T::from_f64(pw[0]).unwrap(),
T::from_f64(pw[1]).unwrap(),
T::from_f64(pw[2]).unwrap(),
);
let pc = rot.transform_vector(&pw_t) + t;
let eps = T::from_f64(1e-12).unwrap();
let z_safe = if pc.z.clone() > eps.clone() {
pc.z.clone()
} else {
eps
};
let x_norm = pc.x.clone() / z_safe.clone();
let y_norm = pc.y.clone() / z_safe;
let (x_dist, y_dist) = distort_brown_conrady_generic(x_norm, y_norm, k1, k2, k3, p1, p2);
let (x_sensor, y_sensor) = apply_scheimpflug_generic(x_dist, y_dist, tau_x, tau_y);
let u_proj = fx * x_sensor + cx;
let v_proj = fy * y_sensor + cy;
let sqrt_w = T::from_f64(w.sqrt()).unwrap();
let u_meas = T::from_f64(uv[0]).unwrap();
let v_meas = T::from_f64(uv[1]).unwrap();
let ru = (u_meas - u_proj) * sqrt_w.clone();
let rv = (v_meas - v_proj) * sqrt_w;
SVector::<T, 2>::new(ru, rv)
}
pub(crate) fn reproj_residual_pinhole4_dist5_two_se3_generic<T: RealField>(
intr: DVectorView<'_, T>,
dist: DVectorView<'_, T>,
extr: DVectorView<'_, T>,
pose: DVectorView<'_, T>,
obs: &ObservationData,
) -> SVector<T, 2> {
use nalgebra::{Quaternion, UnitQuaternion, Vector3};
let extr_q = UnitQuaternion::from_quaternion(Quaternion::new(
extr[3].clone(),
extr[0].clone(),
extr[1].clone(),
extr[2].clone(),
));
let extr_t = Vector3::new(extr[4].clone(), extr[5].clone(), extr[6].clone());
let pose_q = UnitQuaternion::from_quaternion(Quaternion::new(
pose[3].clone(),
pose[0].clone(),
pose[1].clone(),
pose[2].clone(),
));
let pose_t = Vector3::new(pose[4].clone(), pose[5].clone(), pose[6].clone());
let pw_t = Vector3::new(
T::from_f64(obs.pw[0]).unwrap(),
T::from_f64(obs.pw[1]).unwrap(),
T::from_f64(obs.pw[2]).unwrap(),
);
let p_rig = pose_q.transform_vector(&pw_t) + pose_t;
let p_camera = extr_q.inverse_transform_vector(&(p_rig - extr_t));
let eps = T::from_f64(1e-12).unwrap();
let z_safe = if p_camera.z.clone() > eps.clone() {
p_camera.z.clone()
} else {
eps
};
let x_norm = p_camera.x.clone() / z_safe.clone();
let y_norm = p_camera.y.clone() / z_safe;
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 (xd, yd) = distort_brown_conrady_generic(x_norm, y_norm, k1, k2, k3, p1, p2);
let fx = intr[0].clone();
let fy = intr[1].clone();
let cx = intr[2].clone();
let cy = intr[3].clone();
let u_pred = fx * xd + cx;
let v_pred = fy * yd + cy;
let sqrt_w = T::from_f64(obs.w.sqrt()).unwrap();
let u_meas = T::from_f64(obs.uv[0]).unwrap();
let v_meas = T::from_f64(obs.uv[1]).unwrap();
SVector::<T, 2>::new(
(u_meas - u_pred) * sqrt_w.clone(),
(v_meas - v_pred) * sqrt_w,
)
}
pub(crate) fn reproj_residual_pinhole4_dist5_handeye_generic<T: RealField>(
intr: DVectorView<'_, T>,
dist: DVectorView<'_, T>,
extr: DVectorView<'_, T>,
handeye: DVectorView<'_, T>,
target: DVectorView<'_, T>,
robot_data: &RobotPoseData,
obs: &ObservationData,
) -> SVector<T, 2> {
use nalgebra::{Quaternion, UnitQuaternion, Vector3};
let extr_q = UnitQuaternion::from_quaternion(Quaternion::new(
extr[3].clone(),
extr[0].clone(),
extr[1].clone(),
extr[2].clone(),
));
let extr_t = Vector3::new(extr[4].clone(), extr[5].clone(), extr[6].clone());
let handeye_q = UnitQuaternion::from_quaternion(Quaternion::new(
handeye[3].clone(),
handeye[0].clone(),
handeye[1].clone(),
handeye[2].clone(),
));
let handeye_t = Vector3::new(handeye[4].clone(), handeye[5].clone(), handeye[6].clone());
let target_q = UnitQuaternion::from_quaternion(Quaternion::new(
target[3].clone(),
target[0].clone(),
target[1].clone(),
target[2].clone(),
));
let target_t = Vector3::new(target[4].clone(), target[5].clone(), target[6].clone());
let robot_q: UnitQuaternion<T> = UnitQuaternion::from_quaternion(Quaternion::new(
T::from_f64(robot_data.robot_se3[3]).unwrap(),
T::from_f64(robot_data.robot_se3[0]).unwrap(),
T::from_f64(robot_data.robot_se3[1]).unwrap(),
T::from_f64(robot_data.robot_se3[2]).unwrap(),
));
let robot_t = Vector3::new(
T::from_f64(robot_data.robot_se3[4]).unwrap(),
T::from_f64(robot_data.robot_se3[5]).unwrap(),
T::from_f64(robot_data.robot_se3[6]).unwrap(),
);
let pw_t = Vector3::new(
T::from_f64(obs.pw[0]).unwrap(),
T::from_f64(obs.pw[1]).unwrap(),
T::from_f64(obs.pw[2]).unwrap(),
);
let p_camera = match robot_data.mode {
crate::ir::HandEyeMode::EyeInHand => {
let p_base = target_q.transform_vector(&pw_t) + target_t.clone();
let p_gripper = robot_q.inverse_transform_vector(&(p_base - robot_t.clone()));
let p_rig = handeye_q.inverse_transform_vector(&(p_gripper - handeye_t.clone()));
extr_q.inverse_transform_vector(&(p_rig - extr_t.clone()))
}
crate::ir::HandEyeMode::EyeToHand => {
let p_gripper = target_q.transform_vector(&pw_t) + target_t.clone();
let p_base = robot_q.transform_vector(&p_gripper) + robot_t.clone();
let p_rig = handeye_q.transform_vector(&p_base) + handeye_t.clone();
extr_q.inverse_transform_vector(&(p_rig - extr_t.clone()))
}
};
let eps = T::from_f64(1e-12).unwrap();
let z_safe = if p_camera.z.clone() > eps.clone() {
p_camera.z.clone()
} else {
eps
};
let x_norm = p_camera.x.clone() / z_safe.clone();
let y_norm = p_camera.y.clone() / z_safe;
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 (xd, yd) = distort_brown_conrady_generic(x_norm, y_norm, k1, k2, k3, p1, p2);
let fx = intr[0].clone();
let fy = intr[1].clone();
let cx = intr[2].clone();
let cy = intr[3].clone();
let u_pred = fx * xd + cx;
let v_pred = fy * yd + cy;
let sqrt_w = T::from_f64(obs.w.sqrt()).unwrap();
let u_meas = T::from_f64(obs.uv[0]).unwrap();
let v_meas = T::from_f64(obs.uv[1]).unwrap();
SVector::<T, 2>::new(
(u_meas - u_pred) * sqrt_w.clone(),
(v_meas - v_pred) * sqrt_w,
)
}
pub(crate) fn reproj_residual_pinhole4_dist5_handeye_robot_delta_generic<T: RealField>(
intr: DVectorView<'_, T>,
dist: DVectorView<'_, T>,
extr: DVectorView<'_, T>,
handeye: DVectorView<'_, T>,
target: DVectorView<'_, T>,
robot_delta: DVectorView<'_, T>,
data: &HandEyeRobotDeltaData,
) -> SVector<T, 2> {
use nalgebra::{Quaternion, UnitQuaternion, Vector3};
let robot_data = &data.robot;
let obs = &data.obs;
let extr_q = UnitQuaternion::from_quaternion(Quaternion::new(
extr[3].clone(),
extr[0].clone(),
extr[1].clone(),
extr[2].clone(),
));
let extr_t = Vector3::new(extr[4].clone(), extr[5].clone(), extr[6].clone());
let handeye_q = UnitQuaternion::from_quaternion(Quaternion::new(
handeye[3].clone(),
handeye[0].clone(),
handeye[1].clone(),
handeye[2].clone(),
));
let handeye_t = Vector3::new(handeye[4].clone(), handeye[5].clone(), handeye[6].clone());
let target_q = UnitQuaternion::from_quaternion(Quaternion::new(
target[3].clone(),
target[0].clone(),
target[1].clone(),
target[2].clone(),
));
let target_t = Vector3::new(target[4].clone(), target[5].clone(), target[6].clone());
let robot_q: UnitQuaternion<T> = UnitQuaternion::from_quaternion(Quaternion::new(
T::from_f64(robot_data.robot_se3[3]).unwrap(),
T::from_f64(robot_data.robot_se3[0]).unwrap(),
T::from_f64(robot_data.robot_se3[1]).unwrap(),
T::from_f64(robot_data.robot_se3[2]).unwrap(),
));
let robot_t = Vector3::new(
T::from_f64(robot_data.robot_se3[4]).unwrap(),
T::from_f64(robot_data.robot_se3[5]).unwrap(),
T::from_f64(robot_data.robot_se3[6]).unwrap(),
);
let (delta_q, delta_t) = se3_exp(robot_delta);
let robot_q = delta_q.clone() * robot_q;
let robot_t = delta_q.transform_vector(&robot_t) + delta_t;
let pw_t = Vector3::new(
T::from_f64(obs.pw[0]).unwrap(),
T::from_f64(obs.pw[1]).unwrap(),
T::from_f64(obs.pw[2]).unwrap(),
);
let p_camera = match robot_data.mode {
crate::ir::HandEyeMode::EyeInHand => {
let p_base = target_q.transform_vector(&pw_t) + target_t.clone();
let p_gripper = robot_q.inverse_transform_vector(&(p_base - robot_t.clone()));
let p_rig = handeye_q.inverse_transform_vector(&(p_gripper - handeye_t.clone()));
extr_q.inverse_transform_vector(&(p_rig - extr_t.clone()))
}
crate::ir::HandEyeMode::EyeToHand => {
let p_gripper = target_q.transform_vector(&pw_t) + target_t.clone();
let p_base = robot_q.transform_vector(&p_gripper) + robot_t.clone();
let p_rig = handeye_q.transform_vector(&p_base) + handeye_t.clone();
extr_q.inverse_transform_vector(&(p_rig - extr_t.clone()))
}
};
let eps = T::from_f64(1e-12).unwrap();
let z_safe = if p_camera.z.clone() > eps.clone() {
p_camera.z.clone()
} else {
eps
};
let x_norm = p_camera.x.clone() / z_safe.clone();
let y_norm = p_camera.y.clone() / z_safe;
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 (xd, yd) = distort_brown_conrady_generic(x_norm, y_norm, k1, k2, k3, p1, p2);
let fx = intr[0].clone();
let fy = intr[1].clone();
let cx = intr[2].clone();
let cy = intr[3].clone();
let u_pred = fx * xd + cx;
let v_pred = fy * yd + cy;
let sqrt_w = T::from_f64(obs.w.sqrt()).unwrap();
let u_meas = T::from_f64(obs.uv[0]).unwrap();
let v_meas = T::from_f64(obs.uv[1]).unwrap();
SVector::<T, 2>::new(
(u_meas - u_pred) * sqrt_w.clone(),
(v_meas - v_pred) * sqrt_w,
)
}
#[cfg(test)]
mod tests {
use super::*;
use nalgebra::DVector;
#[test]
fn distortion_changes_projection() {
let intr = DVector::from_row_slice(&[800.0, 800.0, 640.0, 360.0]);
let dist_zero = DVector::from_row_slice(&[0.0, 0.0, 0.0, 0.0, 0.0]);
let dist_barrel = DVector::from_row_slice(&[-0.3, 0.1, 0.0, 0.0, 0.0]);
let pose = DVector::from_row_slice(&[0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0]);
let pw = [0.5, 0.5, 1.0];
let uv = [1000.0, 700.0];
let r1: SVector<f64, 2> = reproj_residual_pinhole4_dist5_se3_generic(
intr.as_view(),
dist_zero.as_view(),
pose.as_view(),
pw,
uv,
1.0,
);
let r2: SVector<f64, 2> = reproj_residual_pinhole4_dist5_se3_generic(
intr.as_view(),
dist_barrel.as_view(),
pose.as_view(),
pw,
uv,
1.0,
);
let diff = (r1[0] - r2[0]).abs();
assert!(
diff > 1.0,
"Expected residuals to differ by >1.0, got diff={diff}"
);
}
#[test]
fn zero_distortion_matches_no_distortion() {
let intr = DVector::from_row_slice(&[800.0, 780.0, 640.0, 360.0]);
let dist_zero = DVector::from_row_slice(&[0.0, 0.0, 0.0, 0.0, 0.0]);
let pose = DVector::from_row_slice(&[0.0, 0.0, 0.0, 1.0, 0.2, 0.1, 0.8]);
let pw = [0.05, -0.03, 0.8];
let uv = [650.0, 355.0];
let w = 2.0;
let r_nodist: SVector<f64, 2> =
reproj_residual_pinhole4_se3_generic(intr.as_view(), pose.as_view(), pw, uv, w);
let r_zerodist: SVector<f64, 2> = reproj_residual_pinhole4_dist5_se3_generic(
intr.as_view(),
dist_zero.as_view(),
pose.as_view(),
pw,
uv,
w,
);
let diff_u = (r_nodist[0] - r_zerodist[0]).abs();
let diff_v = (r_nodist[1] - r_zerodist[1]).abs();
assert!(diff_u < 1e-6, "u residuals should match, diff={diff_u}");
assert!(diff_v < 1e-6, "v residuals should match, diff={diff_v}");
}
}