1pub trait CameraModel: Copy + Send + Sync + 'static {
19 const IS_RECTIFIED: bool;
24
25 fn distort(&self, xn: f64, yn: f64) -> [f64; 2];
28
29 fn undistort(&self, xd: f64, yd: f64) -> [f64; 2];
32
33 fn distort_jacobian(&self, xn: f64, yn: f64) -> [[f64; 2]; 2];
39}
40
41#[derive(Clone, Copy, Debug, Default)]
51pub struct PinholeModel;
52
53impl CameraModel for PinholeModel {
54 const IS_RECTIFIED: bool = true;
55
56 #[inline]
57 fn distort(&self, xn: f64, yn: f64) -> [f64; 2] {
58 [xn, yn]
59 }
60
61 #[inline]
62 fn undistort(&self, xd: f64, yd: f64) -> [f64; 2] {
63 [xd, yd]
64 }
65
66 #[inline]
67 fn distort_jacobian(&self, _xn: f64, _yn: f64) -> [[f64; 2]; 2] {
68 [[1.0, 0.0], [0.0, 1.0]]
69 }
70}
71
72#[cfg(feature = "non_rectified")]
77#[derive(Clone, Copy, Debug)]
89pub struct BrownConradyModel {
90 pub k1: f64,
92 pub k2: f64,
94 pub p1: f64,
96 pub p2: f64,
98 pub k3: f64,
100}
101
102#[cfg(feature = "non_rectified")]
103impl BrownConradyModel {
104 pub fn from_coeffs(coeffs: &[f64]) -> Result<Self, &'static str> {
109 if coeffs.len() != 5 {
110 return Err("BrownConrady requires exactly 5 coefficients: [k1, k2, p1, p2, k3]");
111 }
112 Ok(Self {
113 k1: coeffs[0],
114 k2: coeffs[1],
115 p1: coeffs[2],
116 p2: coeffs[3],
117 k3: coeffs[4],
118 })
119 }
120}
121
122#[cfg(feature = "non_rectified")]
123impl CameraModel for BrownConradyModel {
124 const IS_RECTIFIED: bool = false;
125
126 fn distort(&self, xn: f64, yn: f64) -> [f64; 2] {
127 let r2 = xn * xn + yn * yn;
128 let r4 = r2 * r2;
129 let r6 = r2 * r4;
130 let radial = 1.0 + self.k1 * r2 + self.k2 * r4 + self.k3 * r6;
131 let xd = xn * radial + 2.0 * self.p1 * xn * yn + self.p2 * (r2 + 2.0 * xn * xn);
132 let yd = yn * radial + self.p1 * (r2 + 2.0 * yn * yn) + 2.0 * self.p2 * xn * yn;
133 [xd, yd]
134 }
135
136 fn undistort(&self, xd: f64, yd: f64) -> [f64; 2] {
137 let mut xu = xd;
140 let mut yu = yd;
141 for _ in 0..5 {
142 let r2 = xu * xu + yu * yu;
143 let r4 = r2 * r2;
144 let r6 = r2 * r4;
145 let radial = 1.0 + self.k1 * r2 + self.k2 * r4 + self.k3 * r6;
146 let dx = 2.0 * self.p1 * xu * yu + self.p2 * (r2 + 2.0 * xu * xu);
147 let dy = self.p1 * (r2 + 2.0 * yu * yu) + 2.0 * self.p2 * xu * yu;
148 let xu_new = (xd - dx) / radial.max(1e-8);
149 let yu_new = (yd - dy) / radial.max(1e-8);
150 let converged = (xu_new - xu).abs() < 1e-12 && (yu_new - yu).abs() < 1e-12;
151 xu = xu_new;
152 yu = yu_new;
153 if converged {
154 break;
155 }
156 }
157 [xu, yu]
158 }
159
160 #[allow(clippy::similar_names)]
161 fn distort_jacobian(&self, xn: f64, yn: f64) -> [[f64; 2]; 2] {
162 let r2 = xn * xn + yn * yn;
163 let r4 = r2 * r2;
164 let r6 = r2 * r4;
165 let radial = 1.0 + self.k1 * r2 + self.k2 * r4 + self.k3 * r6;
166 let d_radial_dr2 = self.k1 + 2.0 * self.k2 * r2 + 3.0 * self.k3 * r4;
168
169 let dxd_dxn =
174 radial + 2.0 * xn * xn * d_radial_dr2 + 2.0 * self.p1 * yn + 6.0 * self.p2 * xn;
175
176 let dxd_dyn = 2.0 * xn * yn * d_radial_dr2 + 2.0 * self.p1 * xn + 2.0 * self.p2 * yn;
180
181 let dyd_dxn = 2.0 * xn * yn * d_radial_dr2 + 2.0 * self.p1 * xn + 2.0 * self.p2 * yn;
186
187 let dyd_dyn =
191 radial + 2.0 * yn * yn * d_radial_dr2 + 6.0 * self.p1 * yn + 2.0 * self.p2 * xn;
192
193 [[dxd_dxn, dxd_dyn], [dyd_dxn, dyd_dyn]]
194 }
195}
196
197#[cfg(feature = "non_rectified")]
202#[derive(Clone, Copy, Debug)]
215pub struct KannalaBrandtModel {
216 pub k1: f64,
218 pub k2: f64,
220 pub k3: f64,
222 pub k4: f64,
224}
225
226#[cfg(feature = "non_rectified")]
227impl KannalaBrandtModel {
228 pub fn from_coeffs(coeffs: &[f64]) -> Result<Self, &'static str> {
233 if coeffs.len() != 4 {
234 return Err("KannalaBrandt requires exactly 4 coefficients: [k1, k2, k3, k4]");
235 }
236 Ok(Self {
237 k1: coeffs[0],
238 k2: coeffs[1],
239 k3: coeffs[2],
240 k4: coeffs[3],
241 })
242 }
243
244 #[inline]
247 fn angle_poly(&self, theta: f64) -> (f64, f64) {
248 let t2 = theta * theta;
249 let t4 = t2 * t2;
250 let t6 = t2 * t4;
251 let t8 = t4 * t4;
252 let theta_d = theta * (1.0 + self.k1 * t2 + self.k2 * t4 + self.k3 * t6 + self.k4 * t8);
253 let dtheta_d =
254 1.0 + 3.0 * self.k1 * t2 + 5.0 * self.k2 * t4 + 7.0 * self.k3 * t6 + 9.0 * self.k4 * t8;
255 (theta_d, dtheta_d)
256 }
257}
258
259#[cfg(feature = "non_rectified")]
260impl CameraModel for KannalaBrandtModel {
261 const IS_RECTIFIED: bool = false;
262
263 fn distort(&self, xn: f64, yn: f64) -> [f64; 2] {
264 let r = (xn * xn + yn * yn).sqrt();
265 if r < 1e-8 {
266 return [xn, yn];
267 }
268 let theta = r.atan();
270 let (theta_d, _) = self.angle_poly(theta);
271 let scale = theta_d / r;
272 [xn * scale, yn * scale]
273 }
274
275 fn undistort(&self, xd: f64, yd: f64) -> [f64; 2] {
276 let r_d = (xd * xd + yd * yd).sqrt();
277 if r_d < 1e-8 {
278 return [xd, yd];
279 }
280 let mut theta = r_d; for _ in 0..10 {
283 let (theta_d, d_theta_d) = self.angle_poly(theta);
284 let f = theta_d - r_d;
285 if f.abs() < 1e-12 {
286 break;
287 }
288 theta -= f / d_theta_d.max(1e-8);
289 theta = theta.max(0.0);
290 }
291 let r_undist = theta.tan();
293 let scale = r_undist / r_d;
294 [xd * scale, yd * scale]
295 }
296
297 #[allow(clippy::similar_names)]
298 fn distort_jacobian(&self, xn: f64, yn: f64) -> [[f64; 2]; 2] {
299 let r2 = xn * xn + yn * yn;
300 let r = r2.sqrt();
301 if r < 1e-8 {
302 return [[1.0, 0.0], [0.0, 1.0]];
304 }
305
306 let theta = r.atan();
307 let (theta_d, dtheta_d_dtheta) = self.angle_poly(theta);
308
309 let one_plus_r2 = 1.0 + r2;
318 let dthetad_dxn = dtheta_d_dtheta * xn / (r * one_plus_r2);
319 let dthetad_dyn = dtheta_d_dtheta * yn / (r * one_plus_r2);
320
321 let dscale_dxn = (dthetad_dxn * r - theta_d * (xn / r)) / r2;
322 let dscale_dyn = (dthetad_dyn * r - theta_d * (yn / r)) / r2;
323
324 let dxd_dxn = theta_d / r + xn * dscale_dxn;
326 let dxd_dyn = xn * dscale_dyn;
327 let dyd_dxn = yn * dscale_dxn;
328 let dyd_dyn = theta_d / r + yn * dscale_dyn;
329
330 [[dxd_dxn, dxd_dyn], [dyd_dxn, dyd_dyn]]
331 }
332}
333
334#[cfg(test)]
335#[allow(clippy::expect_used, clippy::unwrap_used)]
336mod tests {
337 use super::*;
338
339 #[test]
340 fn pinhole_is_identity() {
341 let m = PinholeModel;
342 let [xd, yd] = m.distort(0.3, -0.2);
343 assert!((xd - 0.3).abs() < f64::EPSILON);
344 assert!((yd - (-0.2)).abs() < f64::EPSILON);
345 let [xu, yu] = m.undistort(0.3, -0.2);
346 assert!((xu - 0.3).abs() < f64::EPSILON);
347 assert!((yu - (-0.2)).abs() < f64::EPSILON);
348 }
349
350 #[cfg(feature = "non_rectified")]
351 mod non_rectified {
352 use super::*;
353
354 fn check_roundtrip<C: CameraModel>(model: &C, xn: f64, yn: f64, tol: f64) {
356 let [xd, yd] = model.distort(xn, yn);
357 let [xu, yu] = model.undistort(xd, yd);
358 assert!((xu - xn).abs() < tol, "xn round-trip failed: {xu} vs {xn}");
359 assert!((yu - yn).abs() < tol, "yn round-trip failed: {yu} vs {yn}");
360 }
361
362 #[allow(clippy::similar_names)]
364 fn check_jacobian<C: CameraModel>(model: &C, xn: f64, yn: f64) {
365 let eps = 1e-6;
366 let jac = model.distort_jacobian(xn, yn);
367
368 let [x0, y0] = model.distort(xn, yn);
369 let [x1, _] = model.distort(xn + eps, yn);
370 let [_, y1] = model.distort(xn, yn + eps);
371 let [x2, _] = model.distort(xn, yn + eps);
372 let [_, y2] = model.distort(xn + eps, yn);
373
374 let num_dxd_dxn = (x1 - x0) / eps;
375 let num_dxd_dyn = (x2 - x0) / eps;
376 let num_dyd_dxn = (y2 - y0) / eps;
377 let num_dyd_dyn = (y1 - y0) / eps;
378
379 assert!(
380 (jac[0][0] - num_dxd_dxn).abs() < 1e-5,
381 "∂xd/∂xn: analytic={} numeric={num_dxd_dxn}",
382 jac[0][0]
383 );
384 assert!(
385 (jac[0][1] - num_dxd_dyn).abs() < 1e-5,
386 "∂xd/∂yn: analytic={} numeric={num_dxd_dyn}",
387 jac[0][1]
388 );
389 assert!(
390 (jac[1][0] - num_dyd_dxn).abs() < 1e-5,
391 "∂yd/∂xn: analytic={} numeric={num_dyd_dxn}",
392 jac[1][0]
393 );
394 assert!(
395 (jac[1][1] - num_dyd_dyn).abs() < 1e-5,
396 "∂yd/∂yn: analytic={} numeric={num_dyd_dyn}",
397 jac[1][1]
398 );
399 }
400
401 #[test]
402 fn brown_conrady_roundtrip() {
403 let m = BrownConradyModel {
404 k1: -0.3,
405 k2: 0.1,
406 p1: 0.001,
407 p2: -0.002,
408 k3: 0.0,
409 };
410 for &(xn, yn) in &[(0.1, 0.2), (-0.3, 0.15), (0.0, 0.4)] {
411 check_roundtrip(&m, xn, yn, 1e-7);
412 }
413 }
414
415 #[test]
416 fn brown_conrady_jacobian() {
417 let m = BrownConradyModel {
418 k1: -0.3,
419 k2: 0.1,
420 p1: 0.001,
421 p2: -0.002,
422 k3: 0.0,
423 };
424 for &(xn, yn) in &[(0.1, 0.2), (-0.3, 0.15), (0.05, -0.05)] {
425 check_jacobian(&m, xn, yn);
426 }
427 }
428
429 #[test]
430 fn kannala_brandt_roundtrip() {
431 let m = KannalaBrandtModel {
432 k1: 0.1,
433 k2: -0.01,
434 k3: 0.001,
435 k4: 0.0,
436 };
437 for &(xn, yn) in &[(0.1, 0.2), (-0.3, 0.15), (0.5, 0.5)] {
438 check_roundtrip(&m, xn, yn, 1e-7);
439 }
440 }
441
442 #[test]
443 fn kannala_brandt_jacobian() {
444 let m = KannalaBrandtModel {
445 k1: 0.1,
446 k2: -0.01,
447 k3: 0.001,
448 k4: 0.0,
449 };
450 for &(xn, yn) in &[(0.1, 0.2), (-0.3, 0.15), (0.3, -0.3)] {
451 check_jacobian(&m, xn, yn);
452 }
453 }
454
455 #[test]
456 fn brown_conrady_from_coeffs_validates_length() {
457 assert!(BrownConradyModel::from_coeffs(&[0.0; 4]).is_err());
458 assert!(BrownConradyModel::from_coeffs(&[0.0; 5]).is_ok());
459 assert!(BrownConradyModel::from_coeffs(&[0.0; 6]).is_err());
460 }
461
462 #[test]
463 fn kannala_brandt_from_coeffs_validates_length() {
464 assert!(KannalaBrandtModel::from_coeffs(&[0.0; 3]).is_err());
465 assert!(KannalaBrandtModel::from_coeffs(&[0.0; 4]).is_ok());
466 assert!(KannalaBrandtModel::from_coeffs(&[0.0; 5]).is_err());
467 }
468 }
469}