ringgrid/pixelmap/
cameramodel.rs1use super::{PixelMapper, RadialTangentialDistortion, UndistortConfig};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
6pub struct CameraIntrinsics {
7 pub fx: f64,
9 pub fy: f64,
11 pub cx: f64,
13 pub cy: f64,
15}
16
17impl CameraIntrinsics {
18 pub fn is_valid(self) -> bool {
20 self.fx.is_finite()
21 && self.fy.is_finite()
22 && self.cx.is_finite()
23 && self.cy.is_finite()
24 && self.fx.abs() > 1e-12
25 && self.fy.abs() > 1e-12
26 }
27
28 pub fn pixel_to_normalized(self, pixel_xy: [f64; 2]) -> Option<[f64; 2]> {
30 if !self.is_valid() {
31 return None;
32 }
33 let x = (pixel_xy[0] - self.cx) / self.fx;
34 let y = (pixel_xy[1] - self.cy) / self.fy;
35 if x.is_finite() && y.is_finite() {
36 Some([x, y])
37 } else {
38 None
39 }
40 }
41
42 pub fn normalized_to_pixel(self, normalized_xy: [f64; 2]) -> [f64; 2] {
44 [
45 self.fx * normalized_xy[0] + self.cx,
46 self.fy * normalized_xy[1] + self.cy,
47 ]
48 }
49}
50
51#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
79pub struct CameraModel {
80 pub intrinsics: CameraIntrinsics,
82 pub distortion: RadialTangentialDistortion,
84}
85
86impl CameraModel {
87 pub fn distort_pixel(self, undistorted_pixel_xy: [f64; 2]) -> Option<[f64; 2]> {
89 let xn = self.intrinsics.pixel_to_normalized(undistorted_pixel_xy)?;
90 let xd = self.distortion.distort_normalized(xn);
91 let pix = self.intrinsics.normalized_to_pixel(xd);
92 if pix[0].is_finite() && pix[1].is_finite() {
93 Some(pix)
94 } else {
95 None
96 }
97 }
98
99 pub fn undistort_pixel(self, distorted_pixel_xy: [f64; 2]) -> Option<[f64; 2]> {
101 self.undistort_pixel_with(distorted_pixel_xy, UndistortConfig::default())
102 }
103
104 pub fn undistort_pixel_with(
106 self,
107 distorted_pixel_xy: [f64; 2],
108 cfg: UndistortConfig,
109 ) -> Option<[f64; 2]> {
110 let xd = self.intrinsics.pixel_to_normalized(distorted_pixel_xy)?;
111 let mut x = xd[0];
112 let mut y = xd[1];
113
114 for _ in 0..cfg.max_iters.max(1) {
115 let r2 = x * x + y * y;
116 let r4 = r2 * r2;
117 let r6 = r4 * r2;
118 let radial =
119 1.0 + self.distortion.k1 * r2 + self.distortion.k2 * r4 + self.distortion.k3 * r6;
120 if !radial.is_finite() || radial.abs() < 1e-12 {
121 return None;
122 }
123
124 let dx_tan = 2.0 * self.distortion.p1 * x * y + self.distortion.p2 * (r2 + 2.0 * x * x);
125 let dy_tan = self.distortion.p1 * (r2 + 2.0 * y * y) + 2.0 * self.distortion.p2 * x * y;
126 let x_next = (xd[0] - dx_tan) / radial;
127 let y_next = (xd[1] - dy_tan) / radial;
128
129 if !x_next.is_finite() || !y_next.is_finite() {
130 return None;
131 }
132
133 let dx = x_next - x;
134 let dy = y_next - y;
135 x = x_next;
136 y = y_next;
137
138 if (dx * dx + dy * dy).sqrt() <= cfg.eps.max(0.0) {
139 break;
140 }
141 }
142
143 let out = self.intrinsics.normalized_to_pixel([x, y]);
144 if out[0].is_finite() && out[1].is_finite() {
145 Some(out)
146 } else {
147 None
148 }
149 }
150}
151
152impl PixelMapper for CameraModel {
153 fn image_to_working_pixel(&self, image_xy: [f64; 2]) -> Option<[f64; 2]> {
154 self.undistort_pixel(image_xy)
155 }
156
157 fn working_to_image_pixel(&self, working_xy: [f64; 2]) -> Option<[f64; 2]> {
158 self.distort_pixel(working_xy)
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 fn sample_camera() -> CameraModel {
167 CameraModel {
168 intrinsics: CameraIntrinsics {
169 fx: 900.0,
170 fy: 920.0,
171 cx: 640.0,
172 cy: 480.0,
173 },
174 distortion: RadialTangentialDistortion {
175 k1: -0.12,
176 k2: 0.03,
177 p1: 0.001,
178 p2: -0.0008,
179 k3: 0.0,
180 },
181 }
182 }
183
184 #[test]
185 fn intrinsics_validation_rejects_zero_focal() {
186 let k = CameraIntrinsics {
187 fx: 0.0,
188 fy: 500.0,
189 cx: 0.0,
190 cy: 0.0,
191 };
192 assert!(!k.is_valid());
193 assert!(k.pixel_to_normalized([100.0, 100.0]).is_none());
194 }
195
196 #[test]
197 fn zero_distortion_roundtrip_is_exact() {
198 let cam = CameraModel {
199 intrinsics: CameraIntrinsics {
200 fx: 800.0,
201 fy: 820.0,
202 cx: 640.0,
203 cy: 480.0,
204 },
205 distortion: RadialTangentialDistortion::default(),
206 };
207 let p = [300.25, 210.75];
208 let d = cam.distort_pixel(p).unwrap();
209 let u = cam.undistort_pixel(d).unwrap();
210 assert!((u[0] - p[0]).abs() < 1e-12);
211 assert!((u[1] - p[1]).abs() < 1e-12);
212 }
213
214 #[test]
215 fn roundtrip_with_distortion_is_stable() {
216 let cam = sample_camera();
217 let p = [250.0, 180.0];
218 let d = cam.distort_pixel(p).unwrap();
219 let u = cam.undistort_pixel(d).unwrap();
220 assert!((u[0] - p[0]).abs() < 1e-5, "x={}, p={}", u[0], p[0]);
221 assert!((u[1] - p[1]).abs() < 1e-5, "y={}, p={}", u[1], p[1]);
222 }
223}