pub mod intrinsics;
pub use intrinsics::{CameraExtrinsics, CameraIntrinsics, StereoCameraSystem};
use crate::error::{Result, VisionError};
use scirs2_core::ndarray::{Array1, Array2};
#[derive(Debug, Clone, PartialEq)]
pub struct PinholeCamera {
pub fx: f64,
pub fy: f64,
pub cx: f64,
pub cy: f64,
pub k1: f64,
pub k2: f64,
pub p1: f64,
pub p2: f64,
}
impl PinholeCamera {
pub fn new(fx: f64, fy: f64, cx: f64, cy: f64, k1: f64, k2: f64, p1: f64, p2: f64) -> Self {
Self {
fx,
fy,
cx,
cy,
k1,
k2,
p1,
p2,
}
}
pub fn ideal(fx: f64, fy: f64, cx: f64, cy: f64) -> Self {
Self::new(fx, fy, cx, cy, 0.0, 0.0, 0.0, 0.0)
}
pub fn project(&self, point_3d: &[f64; 3]) -> Result<[f64; 2]> {
let z = point_3d[2];
if z <= 0.0 {
return Err(VisionError::InvalidParameter(
"point_3d[2] (Z) must be positive".to_string(),
));
}
let xn = point_3d[0] / z;
let yn = point_3d[1] / z;
Ok([self.fx * xn + self.cx, self.fy * yn + self.cy])
}
pub fn project_distorted(&self, point_3d: &[f64; 3]) -> Result<[f64; 2]> {
let z = point_3d[2];
if z <= 0.0 {
return Err(VisionError::InvalidParameter(
"point_3d[2] (Z) must be positive".to_string(),
));
}
let xn = point_3d[0] / z;
let yn = point_3d[1] / z;
let (xd, yd) = apply_distortion(self.k1, self.k2, self.p1, self.p2, xn, yn);
Ok([self.fx * xd + self.cx, self.fy * yd + self.cy])
}
pub fn backproject(&self, pixel: &[f64; 2], depth: f64) -> [f64; 3] {
let xn = (pixel[0] - self.cx) / self.fx;
let yn = (pixel[1] - self.cy) / self.fy;
[xn * depth, yn * depth, depth]
}
pub fn undistort_point(&self, pixel: &[f64; 2]) -> [f64; 2] {
let mut xn = (pixel[0] - self.cx) / self.fx;
let mut yn = (pixel[1] - self.cy) / self.fy;
for _ in 0..20 {
let r2 = xn * xn + yn * yn;
let rad = 1.0 + self.k1 * r2 + self.k2 * r2 * r2;
let dx = 2.0 * self.p1 * xn * yn + self.p2 * (r2 + 2.0 * xn * xn);
let dy = self.p1 * (r2 + 2.0 * yn * yn) + 2.0 * self.p2 * xn * yn;
let xd_curr = xn * rad + dx;
let yd_curr = yn * rad + dy;
let xd_obs = (pixel[0] - self.cx) / self.fx;
let yd_obs = (pixel[1] - self.cy) / self.fy;
xn += (xd_obs - xd_curr) / rad.max(1e-8);
yn += (yd_obs - yd_curr) / rad.max(1e-8);
}
[self.fx * xn + self.cx, self.fy * yn + self.cy]
}
pub fn intrinsic_matrix(&self) -> Array2<f64> {
let mut k = Array2::<f64>::zeros((3, 3));
k[[0, 0]] = self.fx;
k[[0, 2]] = self.cx;
k[[1, 1]] = self.fy;
k[[1, 2]] = self.cy;
k[[2, 2]] = 1.0;
k
}
pub fn distortion_coeffs(&self) -> [f64; 4] {
[self.k1, self.k2, self.p1, self.p2]
}
}
#[inline]
fn apply_distortion(k1: f64, k2: f64, p1: f64, p2: f64, xn: f64, yn: f64) -> (f64, f64) {
let r2 = xn * xn + yn * yn;
let rad = 1.0 + k1 * r2 + k2 * r2 * r2;
let xd = xn * rad + 2.0 * p1 * xn * yn + p2 * (r2 + 2.0 * xn * xn);
let yd = yn * rad + p1 * (r2 + 2.0 * yn * yn) + 2.0 * p2 * xn * yn;
(xd, yd)
}
#[derive(Debug, Clone)]
pub struct StereoPair {
pub left: PinholeCamera,
pub right: PinholeCamera,
pub rotation: Array2<f64>,
pub translation: Array1<f64>,
}
impl StereoPair {
pub fn new(
left: PinholeCamera,
right: PinholeCamera,
rotation: Array2<f64>,
translation: Array1<f64>,
) -> Result<Self> {
if rotation.shape() != [3, 3] {
return Err(VisionError::InvalidParameter(
"rotation must be a 3×3 matrix".to_string(),
));
}
if translation.len() != 3 {
return Err(VisionError::InvalidParameter(
"translation must have length 3".to_string(),
));
}
Ok(Self {
left,
right,
rotation,
translation,
})
}
pub fn baseline(&self) -> f64 {
self.translation.iter().map(|&v| v * v).sum::<f64>().sqrt()
}
pub fn project_both(&self, point_left: &[f64; 3]) -> Result<([f64; 2], [f64; 2])> {
let px_l = self.left.project(point_left)?;
let r = &self.rotation;
let t = &self.translation;
let x = r[[0, 0]] * point_left[0]
+ r[[0, 1]] * point_left[1]
+ r[[0, 2]] * point_left[2]
+ t[0];
let y = r[[1, 0]] * point_left[0]
+ r[[1, 1]] * point_left[1]
+ r[[1, 2]] * point_left[2]
+ t[1];
let z = r[[2, 0]] * point_left[0]
+ r[[2, 1]] * point_left[1]
+ r[[2, 2]] * point_left[2]
+ t[2];
let px_r = self.right.project(&[x, y, z])?;
Ok((px_l, px_r))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_project_principal_axis() {
let cam = PinholeCamera::ideal(800.0, 800.0, 320.0, 240.0);
let px = cam
.project(&[0.0, 0.0, 1.0])
.expect("project should succeed for point in front of camera");
assert!((px[0] - 320.0).abs() < 1e-9, "u = {}", px[0]);
assert!((px[1] - 240.0).abs() < 1e-9, "v = {}", px[1]);
}
#[test]
fn test_project_behind_camera() {
let cam = PinholeCamera::ideal(800.0, 800.0, 320.0, 240.0);
assert!(cam.project(&[0.0, 0.0, 0.0]).is_err());
assert!(cam.project(&[0.0, 0.0, -1.0]).is_err());
}
#[test]
fn test_backproject_roundtrip() {
let cam = PinholeCamera::ideal(800.0, 800.0, 320.0, 240.0);
let pt3d = [1.0, -0.5, 3.0];
let px = cam
.project(&pt3d)
.expect("project should succeed for valid 3D point");
let back = cam.backproject(&px, pt3d[2]);
assert!((back[0] - pt3d[0]).abs() < 1e-9, "X = {}", back[0]);
assert!((back[1] - pt3d[1]).abs() < 1e-9, "Y = {}", back[1]);
assert!((back[2] - pt3d[2]).abs() < 1e-9, "Z = {}", back[2]);
}
#[test]
fn test_distorted_on_axis() {
let cam = PinholeCamera::new(800.0, 800.0, 320.0, 240.0, 0.2, 0.05, 0.001, 0.001);
let ideal = cam
.project(&[0.0, 0.0, 1.0])
.expect("project should succeed for principal axis");
let dist = cam
.project_distorted(&[0.0, 0.0, 1.0])
.expect("project_distorted should succeed for principal axis");
assert!((ideal[0] - dist[0]).abs() < 1e-12);
assert!((ideal[1] - dist[1]).abs() < 1e-12);
}
#[test]
fn test_distorted_off_axis() {
let cam = PinholeCamera::new(800.0, 800.0, 320.0, 240.0, 0.2, 0.0, 0.0, 0.0);
let ideal = cam
.project(&[1.0, 1.0, 2.0])
.expect("project should succeed for off-axis point");
let dist = cam
.project_distorted(&[1.0, 1.0, 2.0])
.expect("project_distorted should succeed for off-axis point");
assert!(
(dist[0] - 320.0).abs() > (ideal[0] - 320.0).abs(),
"dist.u={}, ideal.u={}",
dist[0],
ideal[0]
);
}
#[test]
fn test_undistort_point_principal_axis() {
let cam = PinholeCamera::new(800.0, 800.0, 320.0, 240.0, 0.1, 0.02, 0.0, 0.0);
let undist = cam.undistort_point(&[320.0, 240.0]);
assert!((undist[0] - 320.0).abs() < 1e-9);
assert!((undist[1] - 240.0).abs() < 1e-9);
}
#[test]
fn test_intrinsic_matrix() {
let cam = PinholeCamera::ideal(400.0, 500.0, 200.0, 150.0);
let k = cam.intrinsic_matrix();
assert_eq!(k.shape(), &[3, 3]);
assert!((k[[0, 0]] - 400.0).abs() < 1e-12);
assert!((k[[1, 1]] - 500.0).abs() < 1e-12);
assert!((k[[0, 2]] - 200.0).abs() < 1e-12);
assert!((k[[1, 2]] - 150.0).abs() < 1e-12);
assert!((k[[2, 2]] - 1.0).abs() < 1e-12);
assert!((k[[0, 1]]).abs() < 1e-12); }
#[test]
fn test_stereo_pair_baseline() {
use scirs2_core::ndarray::{Array1, Array2};
let cam = PinholeCamera::ideal(800.0, 800.0, 320.0, 240.0);
let r = Array2::<f64>::eye(3);
let t = Array1::from_vec(vec![-0.1, 0.0, 0.0]);
let stereo = StereoPair::new(cam.clone(), cam, r, t)
.expect("StereoPair::new should succeed with valid inputs");
assert!((stereo.baseline() - 0.1).abs() < 1e-9);
}
}