use crate::error::{Result, VisionError};
use scirs2_core::ndarray::Array3;
#[derive(Debug, Clone, PartialEq)]
pub struct PinholeCameraModel {
pub fx: f64,
pub fy: f64,
pub cx: f64,
pub cy: f64,
pub width: usize,
pub height: usize,
}
impl PinholeCameraModel {
pub fn new(fx: f64, fy: f64, cx: f64, cy: f64, width: usize, height: usize) -> Self {
Self {
fx,
fy,
cx,
cy,
width,
height,
}
}
pub fn from_calibration_matrix(k: &[[f64; 3]; 3], width: usize, height: usize) -> Self {
Self {
fx: k[0][0],
fy: k[1][1],
cx: k[0][2],
cy: k[1][2],
width,
height,
}
}
pub fn to_matrix(&self) -> [[f64; 3]; 3] {
[
[self.fx, 0.0, self.cx],
[0.0, self.fy, self.cy],
[0.0, 0.0, 1.0],
]
}
pub fn fov_x(&self) -> f64 {
2.0 * (self.width as f64 / (2.0 * self.fx)).atan()
}
pub fn fov_y(&self) -> f64 {
2.0 * (self.height as f64 / (2.0 * self.fy)).atan()
}
}
pub fn project(model: &PinholeCameraModel, point_3d: &[f64; 3]) -> Result<(f64, f64)> {
let z = point_3d[2];
if z <= 0.0 {
return Err(VisionError::InvalidParameter(
"point_3d Z must be positive (point must be in front of camera)".to_string(),
));
}
let u = model.fx * point_3d[0] / z + model.cx;
let v = model.fy * point_3d[1] / z + model.cy;
Ok((u, v))
}
pub fn unproject(model: &PinholeCameraModel, pixel: (f64, f64), depth: f64) -> [f64; 3] {
let (u, v) = pixel;
let x = (u - model.cx) * depth / model.fx;
let y = (v - model.cy) * depth / model.fy;
[x, y, depth]
}
#[derive(Debug, Clone, PartialEq)]
pub struct RadialDistortion {
pub k1: f64,
pub k2: f64,
pub k3: f64,
pub p1: f64,
pub p2: f64,
}
impl Default for RadialDistortion {
fn default() -> Self {
Self {
k1: 0.0,
k2: 0.0,
k3: 0.0,
p1: 0.0,
p2: 0.0,
}
}
}
impl RadialDistortion {
pub fn new(k1: f64, k2: f64, k3: f64, p1: f64, p2: f64) -> Self {
Self { k1, k2, k3, p1, p2 }
}
pub fn is_identity(&self) -> bool {
self.k1 == 0.0 && self.k2 == 0.0 && self.k3 == 0.0 && self.p1 == 0.0 && self.p2 == 0.0
}
}
pub fn distort(model: &RadialDistortion, point: (f64, f64)) -> (f64, f64) {
let (x, y) = point;
let r2 = x * x + y * y;
let r4 = r2 * r2;
let r6 = r4 * r2;
let radial = 1.0 + model.k1 * r2 + model.k2 * r4 + model.k3 * r6;
let x_d = x * radial + 2.0 * model.p1 * x * y + model.p2 * (r2 + 2.0 * x * x);
let y_d = y * radial + model.p1 * (r2 + 2.0 * y * y) + 2.0 * model.p2 * x * y;
(x_d, y_d)
}
pub fn undistort_point(
model: &RadialDistortion,
point: (f64, f64),
max_iters: usize,
tol: f64,
) -> (f64, f64) {
if model.is_identity() {
return point;
}
let (xd, yd) = point;
let mut x = xd;
let mut y = yd;
for _ in 0..max_iters.max(1) {
let (x_proj, y_proj) = distort(model, (x, y));
let ex = x_proj - xd;
let ey = y_proj - yd;
if ex * ex + ey * ey < tol * tol {
break;
}
x -= ex;
y -= ey;
}
(x, y)
}
pub fn undistort_image(
image: &Array3<f64>,
model: &PinholeCameraModel,
distortion: &RadialDistortion,
) -> Result<Array3<f64>> {
let (h, w, c) = image.dim();
if h != model.height || w != model.width {
return Err(VisionError::InvalidParameter(format!(
"image dimensions {}×{} do not match camera model {}×{}",
h, w, model.height, model.width
)));
}
let mut output = Array3::zeros((h, w, c));
for v in 0..h {
for u in 0..w {
let xn = (u as f64 - model.cx) / model.fx;
let yn = (v as f64 - model.cy) / model.fy;
let (xd, yd) = distort(distortion, (xn, yn));
let us = model.fx * xd + model.cx;
let vs = model.fy * yd + model.cy;
let u0 = us.floor() as i64;
let v0 = vs.floor() as i64;
let u1 = u0 + 1;
let v1 = v0 + 1;
let du = us - u0 as f64;
let dv = vs - v0 as f64;
let in_bounds = |uu: i64, vv: i64| uu >= 0 && uu < w as i64 && vv >= 0 && vv < h as i64;
for ch in 0..c {
let sample = |uu: i64, vv: i64| -> f64 {
if in_bounds(uu, vv) {
image[[vv as usize, uu as usize, ch]]
} else {
0.0
}
};
let val = sample(u0, v0) * (1.0 - du) * (1.0 - dv)
+ sample(u1, v0) * du * (1.0 - dv)
+ sample(u0, v1) * (1.0 - du) * dv
+ sample(u1, v1) * du * dv;
output[[v, u, ch]] = val;
}
}
}
Ok(output)
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array3;
#[test]
fn test_project_principal_ray() {
let cam = PinholeCameraModel::new(800.0, 800.0, 320.0, 240.0, 640, 480);
let (u, v) =
project(&cam, &[0.0, 0.0, 1.0]).expect("project should succeed for principal ray");
assert!((u - 320.0).abs() < 1e-9, "u={u}");
assert!((v - 240.0).abs() < 1e-9, "v={v}");
}
#[test]
fn test_project_negative_z_errors() {
let cam = PinholeCameraModel::new(800.0, 800.0, 320.0, 240.0, 640, 480);
assert!(project(&cam, &[0.0, 0.0, -1.0]).is_err());
assert!(project(&cam, &[0.0, 0.0, 0.0]).is_err());
}
#[test]
fn test_unproject_roundtrip() {
let cam = PinholeCameraModel::new(800.0, 800.0, 320.0, 240.0, 640, 480);
let depth = 3.5;
let (u, v) = project(&cam, &[0.5, -0.3, depth])
.expect("project should succeed for point in front of camera");
let pt = unproject(&cam, (u, v), depth);
assert!((pt[0] - 0.5).abs() < 1e-9, "X={}", pt[0]);
assert!((pt[1] - (-0.3)).abs() < 1e-9, "Y={}", pt[1]);
assert!((pt[2] - depth).abs() < 1e-9, "Z={}", pt[2]);
}
#[test]
fn test_distort_identity() {
let d = RadialDistortion::default();
let (xd, yd) = distort(&d, (0.3, 0.2));
assert!((xd - 0.3).abs() < 1e-12);
assert!((yd - 0.2).abs() < 1e-12);
}
#[test]
fn test_distort_nonzero() {
let d = RadialDistortion::new(0.1, 0.0, 0.0, 0.0, 0.0);
let x = 0.5;
let y = 0.0;
let (xd, _yd) = distort(&d, (x, y));
assert!((xd - x * 1.025).abs() < 1e-12, "xd={xd}");
}
#[test]
fn test_undistort_image_identity_distortion() {
let cam = PinholeCameraModel::new(400.0, 400.0, 160.0, 120.0, 320, 240);
let dist = RadialDistortion::default();
let img = Array3::from_elem((240, 320, 3), 128.0_f64);
let out = undistort_image(&img, &cam, &dist)
.expect("undistort_image should succeed with identity distortion");
assert_eq!(out.dim(), img.dim());
let err: f64 = (&out - &img).iter().map(|x| x.abs()).sum::<f64>() / (240 * 320 * 3) as f64;
assert!(err < 1.0, "mean error = {err}");
}
#[test]
fn test_undistort_image_wrong_size() {
let cam = PinholeCameraModel::new(400.0, 400.0, 160.0, 120.0, 320, 240);
let dist = RadialDistortion::default();
let img = Array3::<f64>::zeros((100, 100, 3));
assert!(undistort_image(&img, &cam, &dist).is_err());
}
#[test]
fn test_fov() {
let cam = PinholeCameraModel::new(800.0, 800.0, 320.0, 240.0, 640, 480);
let fov_x = cam.fov_x();
let fov_y = cam.fov_y();
assert!(fov_x > 0.0 && fov_x < std::f64::consts::PI);
assert!(fov_y > 0.0 && fov_y < std::f64::consts::PI);
}
#[test]
fn test_to_matrix_roundtrip() {
let cam = PinholeCameraModel::new(750.0, 760.0, 319.5, 239.5, 640, 480);
let k = cam.to_matrix();
assert!((k[0][0] - cam.fx).abs() < 1e-12);
assert!((k[1][1] - cam.fy).abs() < 1e-12);
assert!((k[0][2] - cam.cx).abs() < 1e-12);
assert!((k[1][2] - cam.cy).abs() < 1e-12);
}
}