spatialrust_camera/
distortion.rs1use spatialrust_math::Vec2;
2
3#[derive(Clone, Copy, Debug, Default, PartialEq)]
5pub struct BrownConrady {
6 pub k1: f64,
8 pub k2: f64,
10 pub p1: f64,
12 pub p2: f64,
14 pub k3: f64,
16}
17
18#[derive(Clone, Copy, Debug, Default, PartialEq)]
20pub struct KannalaBrandt4 {
21 pub k1: f64,
23 pub k2: f64,
25 pub k3: f64,
27 pub k4: f64,
29}
30
31impl KannalaBrandt4 {
32 #[must_use]
34 pub fn distort(self, point: Vec2<f64>) -> Vec2<f64> {
35 let radius = point.x.hypot(point.y);
36 if radius <= f64::EPSILON {
37 return point;
38 }
39 let theta = radius.atan();
40 let theta2 = theta * theta;
41 let distorted_theta = theta
42 * (1.0
43 + theta2 * (self.k1 + theta2 * (self.k2 + theta2 * (self.k3 + theta2 * self.k4))));
44 let scale = distorted_theta / radius;
45 Vec2 { x: point.x * scale, y: point.y * scale }
46 }
47
48 #[must_use]
50 pub fn undistort(self, point: Vec2<f64>) -> Vec2<f64> {
51 let distorted_radius = point.x.hypot(point.y);
52 if distorted_radius <= f64::EPSILON {
53 return point;
54 }
55 let mut theta = distorted_radius.min(std::f64::consts::FRAC_PI_2 - 1e-6);
56 for _ in 0..16 {
57 let theta2 = theta * theta;
58 let theta4 = theta2 * theta2;
59 let theta6 = theta4 * theta2;
60 let theta8 = theta4 * theta4;
61 let value = theta
62 * (1.0 + self.k1 * theta2 + self.k2 * theta4 + self.k3 * theta6 + self.k4 * theta8)
63 - distorted_radius;
64 let derivative = 1.0
65 + 3.0 * self.k1 * theta2
66 + 5.0 * self.k2 * theta4
67 + 7.0 * self.k3 * theta6
68 + 9.0 * self.k4 * theta8;
69 if derivative.abs() < 1e-12 {
70 break;
71 }
72 let step = value / derivative;
73 theta -= step;
74 if step.abs() < 1e-13 {
75 break;
76 }
77 }
78 let radius = theta.tan();
79 let scale = radius / distorted_radius;
80 Vec2 { x: point.x * scale, y: point.y * scale }
81 }
82}
83
84impl BrownConrady {
85 #[must_use]
87 pub fn is_identity(self) -> bool {
88 self == Self::default()
89 }
90
91 #[must_use]
93 pub fn distort(self, point: Vec2<f64>) -> Vec2<f64> {
94 let x = point.x;
95 let y = point.y;
96 let r2 = x.mul_add(x, y * y);
97 let radial = 1.0 + r2 * (self.k1 + r2 * (self.k2 + r2 * self.k3));
98 Vec2 {
99 x: x * radial + 2.0 * self.p1 * x * y + self.p2 * (r2 + 2.0 * x * x),
100 y: y * radial + self.p1 * (r2 + 2.0 * y * y) + 2.0 * self.p2 * x * y,
101 }
102 }
103
104 #[must_use]
109 pub fn undistort(self, distorted: Vec2<f64>) -> Vec2<f64> {
110 if self.is_identity() {
111 return distorted;
112 }
113 let mut estimate = distorted;
114 const STEP: f64 = 1e-7;
115 for _ in 0..12 {
116 let observed = self.distort(estimate);
117 let error = Vec2 { x: observed.x - distorted.x, y: observed.y - distorted.y };
118 if error.x.abs().max(error.y.abs()) < 1e-14 {
119 break;
120 }
121 let dx = self.distort(Vec2 { x: estimate.x + STEP, y: estimate.y });
122 let dy = self.distort(Vec2 { x: estimate.x, y: estimate.y + STEP });
123 let j00 = (dx.x - observed.x) / STEP;
124 let j10 = (dx.y - observed.y) / STEP;
125 let j01 = (dy.x - observed.x) / STEP;
126 let j11 = (dy.y - observed.y) / STEP;
127 let determinant = j00.mul_add(j11, -(j01 * j10));
128 if determinant.abs() < f64::EPSILON {
129 break;
130 }
131 let delta_x = (j11 * error.x - j01 * error.y) / determinant;
132 let delta_y = (-j10 * error.x + j00 * error.y) / determinant;
133 estimate.x -= delta_x;
134 estimate.y -= delta_y;
135 }
136 estimate
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::{BrownConrady, KannalaBrandt4};
143 use spatialrust_math::Vec2;
144
145 #[test]
146 fn distortion_roundtrip() {
147 let model = BrownConrady { k1: -0.2, k2: 0.03, p1: 0.001, p2: -0.002, k3: 0.0 };
148 for point in [Vec2 { x: -0.4, y: 0.3 }, Vec2 { x: 0.2, y: -0.1 }] {
149 let recovered = model.undistort(model.distort(point));
150 assert!((recovered.x - point.x).abs() < 1e-9);
151 assert!((recovered.y - point.y).abs() < 1e-9);
152 }
153 }
154
155 #[test]
156 fn fisheye_roundtrip() {
157 let model = KannalaBrandt4 { k1: 0.02, k2: -0.003, k3: 0.0004, k4: -0.00002 };
158 for point in [Vec2 { x: -0.8, y: 0.5 }, Vec2 { x: 0.2, y: -0.1 }] {
159 let recovered = model.undistort(model.distort(point));
160 assert!((recovered.x - point.x).abs() < 1e-10);
161 assert!((recovered.y - point.y).abs() < 1e-10);
162 }
163 }
164}