spatialrust_camera/
model.rs1use crate::BrownConrady;
2use spatialrust_math::{Vec2, Vec3};
3
4#[derive(Clone, Debug, PartialEq, thiserror::Error)]
6pub enum CameraError {
7 #[error("camera focal lengths must be finite and positive")]
9 InvalidFocalLength,
10 #[error("camera principal point must be finite")]
12 InvalidPrincipalPoint,
13 #[error("point depth must be finite and positive, found {0}")]
15 InvalidDepth(f64),
16 #[error("pixel coordinates must be finite")]
18 InvalidPixel,
19}
20
21#[derive(Clone, Copy, Debug, PartialEq)]
23pub struct CameraIntrinsics {
24 pub fx: f64,
26 pub fy: f64,
28 pub cx: f64,
30 pub cy: f64,
32 pub width: usize,
34 pub height: usize,
36}
37
38impl CameraIntrinsics {
39 pub fn try_new(
41 fx: f64,
42 fy: f64,
43 cx: f64,
44 cy: f64,
45 width: usize,
46 height: usize,
47 ) -> Result<Self, CameraError> {
48 if !fx.is_finite() || !fy.is_finite() || fx <= 0.0 || fy <= 0.0 {
49 return Err(CameraError::InvalidFocalLength);
50 }
51 if !cx.is_finite() || !cy.is_finite() {
52 return Err(CameraError::InvalidPrincipalPoint);
53 }
54 Ok(Self { fx, fy, cx, cy, width, height })
55 }
56}
57
58#[derive(Clone, Copy, Debug, PartialEq)]
60pub struct PinholeCamera {
61 pub intrinsics: CameraIntrinsics,
63 pub distortion: BrownConrady,
65}
66
67impl PinholeCamera {
68 #[must_use]
70 pub const fn new(intrinsics: CameraIntrinsics) -> Self {
71 Self {
72 intrinsics,
73 distortion: BrownConrady { k1: 0.0, k2: 0.0, p1: 0.0, p2: 0.0, k3: 0.0 },
74 }
75 }
76
77 #[must_use]
79 pub const fn with_distortion(mut self, distortion: BrownConrady) -> Self {
80 self.distortion = distortion;
81 self
82 }
83
84 pub fn project(&self, point: Vec3<f64>) -> Result<Vec2<f64>, CameraError> {
86 if !point.z.is_finite() || point.z <= 0.0 {
87 return Err(CameraError::InvalidDepth(point.z));
88 }
89 let normalized = Vec2 { x: point.x / point.z, y: point.y / point.z };
90 let distorted = self.distortion.distort(normalized);
91 Ok(Vec2 {
92 x: self.intrinsics.fx.mul_add(distorted.x, self.intrinsics.cx),
93 y: self.intrinsics.fy.mul_add(distorted.y, self.intrinsics.cy),
94 })
95 }
96
97 pub fn unproject(&self, pixel: Vec2<f64>, depth: f64) -> Result<Vec3<f64>, CameraError> {
99 if !depth.is_finite() || depth <= 0.0 {
100 return Err(CameraError::InvalidDepth(depth));
101 }
102 if !pixel.x.is_finite() || !pixel.y.is_finite() {
103 return Err(CameraError::InvalidPixel);
104 }
105 let distorted = Vec2 {
106 x: (pixel.x - self.intrinsics.cx) / self.intrinsics.fx,
107 y: (pixel.y - self.intrinsics.cy) / self.intrinsics.fy,
108 };
109 let normalized = self.distortion.undistort(distorted);
110 Ok(Vec3::new(normalized.x * depth, normalized.y * depth, depth))
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::{CameraIntrinsics, PinholeCamera};
117 use crate::BrownConrady;
118 use spatialrust_math::Vec3;
119
120 #[test]
121 fn project_unproject_roundtrip_with_distortion() {
122 let intrinsics = CameraIntrinsics::try_new(525.0, 520.0, 319.5, 239.5, 640, 480).unwrap();
123 let camera = PinholeCamera::new(intrinsics).with_distortion(BrownConrady {
124 k1: -0.15,
125 k2: 0.02,
126 p1: 0.001,
127 p2: -0.001,
128 k3: 0.0,
129 });
130 let point = Vec3::new(0.4, -0.2, 2.5);
131 let pixel = camera.project(point).unwrap();
132 let recovered = camera.unproject(pixel, point.z).unwrap();
133 assert!((recovered.x - point.x).abs() < 1e-8);
134 assert!((recovered.y - point.y).abs() < 1e-8);
135 assert_eq!(recovered.z, point.z);
136 }
137}