use crate::stereo::disparity::DisparityMap;
use crate::stereo::reconstruction::PointCloud;
#[derive(Debug, Clone)]
pub struct CameraIntrinsics {
pub fx: f64,
pub fy: f64,
pub cx: f64,
pub cy: f64,
pub distortion: [f64; 5],
}
impl CameraIntrinsics {
pub fn ideal(fx: f64, fy: f64, cx: f64, cy: f64) -> Self {
Self {
fx,
fy,
cx,
cy,
distortion: [0.0; 5],
}
}
pub fn undistort_point(&self, u: f64, v: f64) -> (f64, f64) {
let xn = (u - self.cx) / self.fx;
let yn = (v - self.cy) / self.fy;
let r2 = xn * xn + yn * yn;
let [k1, k2, p1, p2, k3] = self.distortion;
let radial = 1.0 + k1 * r2 + k2 * r2 * r2 + k3 * r2 * r2 * r2;
let xd = xn * radial + 2.0 * p1 * xn * yn + p2 * (r2 + 2.0 * xn * xn);
let yd = yn * radial + p1 * (r2 + 2.0 * yn * yn) + 2.0 * p2 * xn * yn;
(xd * self.fx + self.cx, yd * self.fy + self.cy)
}
}
#[derive(Debug, Clone)]
pub struct StereoCalibration {
pub left: CameraIntrinsics,
pub right: CameraIntrinsics,
pub rotation: [[f64; 3]; 3],
pub translation: [f64; 3],
pub baseline: f64,
}
impl StereoCalibration {
pub fn new(
left: CameraIntrinsics,
right: CameraIntrinsics,
rotation: [[f64; 3]; 3],
translation: [f64; 3],
) -> Self {
let baseline = translation[0].abs();
Self {
left,
right,
rotation,
translation,
baseline,
}
}
pub fn from_baseline(fx: f64, fy: f64, cx: f64, cy: f64, baseline: f64) -> Self {
let cam = CameraIntrinsics::ideal(fx, fy, cx, cy);
Self {
left: cam.clone(),
right: cam,
rotation: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
translation: [-baseline, 0.0, 0.0],
baseline,
}
}
pub fn q_matrix(&self) -> [[f64; 4]; 4] {
let f = self.left.fx;
let cx = self.left.cx;
let cy = self.left.cy;
let cx_prime = self.right.cx;
let b = self.baseline;
[
[1.0, 0.0, 0.0, -cx],
[0.0, 1.0, 0.0, -cy],
[0.0, 0.0, 0.0, f],
[0.0, 0.0, -1.0 / b, (cx - cx_prime) / b],
]
}
pub fn reproject_to_3d(&self, disparity: &DisparityMap) -> PointCloud {
let q = self.q_matrix();
let mut points = Vec::new();
for row in 0..disparity.height {
for col in 0..disparity.width {
let d = disparity.get(row, col) as f64;
if d <= 0.0 {
continue;
}
let xu = col as f64;
let yu = row as f64;
let wx = q[0][0] * xu + q[0][3];
let wy = q[1][1] * yu + q[1][3];
let wz = q[2][3]; let ww = q[3][2] * d + q[3][3];
if ww.abs() > 1e-10 {
let x3d = wx / ww;
let y3d = wy / ww;
let z3d = (wz / ww).abs(); points.push([x3d as f32, y3d as f32, z3d as f32]);
}
}
}
PointCloud {
points,
colors: None,
normals: None,
}
}
pub fn depth_to_disparity(&self, z: f64) -> f64 {
if z.abs() < 1e-10 {
return 0.0;
}
self.baseline * self.left.fx / z
}
pub fn disparity_to_depth(&self, disparity: f64) -> f64 {
if disparity.abs() < 1e-10 {
return 0.0;
}
self.baseline * self.left.fx / disparity
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_baseline_baseline_field() {
let cal = StereoCalibration::from_baseline(500.0, 500.0, 320.0, 240.0, 0.12);
assert!((cal.baseline - 0.12).abs() < 1e-9);
}
#[test]
fn test_q_matrix_dimensions() {
let cal = StereoCalibration::from_baseline(500.0, 500.0, 320.0, 240.0, 0.1);
let q = cal.q_matrix();
assert_eq!(q.len(), 4);
assert_eq!(q[0].len(), 4);
assert!((q[2][3] - 500.0).abs() < 1e-9, "Q[2][3]={}", q[2][3]);
assert!((q[3][2] - (-1.0 / 0.1)).abs() < 1e-6, "Q[3][2]={}", q[3][2]);
}
#[test]
fn test_reproject_to_3d_basic() {
let cal = StereoCalibration::from_baseline(500.0, 500.0, 32.0, 24.0, 0.1);
let mut disp = DisparityMap::new(64, 48, 0, 32);
disp.set(24, 32, 10.0);
let pc = cal.reproject_to_3d(&disp);
assert_eq!(pc.len(), 1);
let p = pc.points[0];
assert!(p[2] > 0.0, "z should be positive (in front of camera)");
}
#[test]
fn test_depth_disparity_roundtrip() {
let cal = StereoCalibration::from_baseline(600.0, 600.0, 320.0, 240.0, 0.15);
let z = 3.0;
let d = cal.depth_to_disparity(z);
let z2 = cal.disparity_to_depth(d);
assert!((z - z2).abs() < 1e-6, "roundtrip z={z} → d={d} → z={z2}");
}
#[test]
fn test_undistort_point_zero_distortion() {
let cam = CameraIntrinsics::ideal(500.0, 500.0, 320.0, 240.0);
let (u2, v2) = cam.undistort_point(350.0, 270.0);
assert!((u2 - 350.0).abs() < 1e-4, "u={u2}");
assert!((v2 - 270.0).abs() < 1e-4, "v={v2}");
}
}