rawshift_image/transforms/
lens_correction.rs1use crate::core::image::RgbImage;
10
11pub fn apply_warp_rectilinear(image: &mut RgbImage, k: [f64; 4], cx: f64, cy: f64) {
34 apply_warp_rectilinear_tangential(image, k, [0.0, 0.0], cx, cy);
35}
36
37pub fn apply_warp_rectilinear_tangential(
56 image: &mut RgbImage,
57 k: [f64; 4],
58 p: [f64; 2],
59 cx: f64,
60 cy: f64,
61) {
62 let width = image.width() as usize;
63 let height = image.height() as usize;
64
65 if width == 0 || height == 0 {
66 return;
67 }
68
69 let norm = 0.5 * width.min(height) as f64;
71
72 let cx_px = cx * width as f64;
73 let cy_px = cy * height as f64;
74
75 let src = image.data.clone();
77
78 for y in 0..height {
79 for x in 0..width {
80 let xn = (x as f64 - cx_px) / norm;
82 let yn = (y as f64 - cy_px) / norm;
83
84 let r2 = xn * xn + yn * yn;
85 let r4 = r2 * r2;
86 let r6 = r4 * r2;
87 let r8 = r4 * r4;
88
89 let factor = 1.0 + k[0] * r2 + k[1] * r4 + k[2] * r6 + k[3] * r8;
91
92 let xs = xn * factor + 2.0 * p[0] * xn * yn + p[1] * (r2 + 2.0 * xn * xn);
94 let ys = yn * factor + p[0] * (r2 + 2.0 * yn * yn) + 2.0 * p[1] * xn * yn;
95
96 let src_x = xs * norm + cx_px;
98 let src_y = ys * norm + cy_px;
99
100 let dst = (y * width + x) * 3;
102 for ch in 0..3usize {
103 image.data[dst + ch] =
104 bilinear_sample(&src, width, height, ch, src_x as f32, src_y as f32);
105 }
106 }
107 }
108}
109
110#[inline]
116fn bilinear_sample(
117 data: &[u16],
118 width: usize,
119 height: usize,
120 channel: usize,
121 sx: f32,
122 sy: f32,
123) -> u16 {
124 let sx = sx.clamp(0.0, (width as f32) - 1.0);
126 let sy = sy.clamp(0.0, (height as f32) - 1.0);
127
128 let x0 = sx.floor() as usize;
129 let y0 = sy.floor() as usize;
130 let x1 = (x0 + 1).min(width - 1);
131 let y1 = (y0 + 1).min(height - 1);
132
133 let fx = sx - sx.floor();
134 let fy = sy - sy.floor();
135
136 let p00 = data[(y0 * width + x0) * 3 + channel] as f32;
137 let p10 = data[(y0 * width + x1) * 3 + channel] as f32;
138 let p01 = data[(y1 * width + x0) * 3 + channel] as f32;
139 let p11 = data[(y1 * width + x1) * 3 + channel] as f32;
140
141 let top = p00 * (1.0 - fx) + p10 * fx;
142 let bot = p01 * (1.0 - fx) + p11 * fx;
143 let result = top * (1.0 - fy) + bot * fy;
144
145 result.round() as u16
146}
147
148#[cfg(test)]
151mod tests {
152 use super::*;
153
154 fn make_rgb(width: u32, height: u32, fill: u16) -> RgbImage {
155 let n = (width as usize) * (height as usize) * 3;
156 RgbImage::new(width, height, vec![fill; n])
157 }
158
159 fn make_gradient(width: u32, height: u32) -> RgbImage {
160 let n = (width as usize) * (height as usize) * 3;
161 let data: Vec<u16> = (0..n).map(|i| (i as u16).wrapping_mul(7)).collect();
162 RgbImage::new(width, height, data)
163 }
164
165 #[test]
168 fn test_warp_zero_coefficients_unchanged() {
169 let w = 8u32;
172 let h = 8u32;
173 let original = make_gradient(w, h);
174 let mut img = original.clone();
175
176 apply_warp_rectilinear(&mut img, [0.0; 4], 0.5, 0.5);
177
178 assert_eq!(
179 img.data, original.data,
180 "zero coefficients must leave image unchanged"
181 );
182 }
183
184 #[test]
185 fn test_warp_output_size_unchanged() {
186 let w = 16u32;
187 let h = 10u32;
188 let mut img = make_gradient(w, h);
189
190 apply_warp_rectilinear(&mut img, [-0.01, 0.0, 0.0, 0.0], 0.5, 0.5);
191
192 assert_eq!(img.width(), w, "width must not change after warp");
193 assert_eq!(img.height(), h, "height must not change after warp");
194 assert_eq!(img.data.len(), (w as usize) * (h as usize) * 3);
195 }
196
197 #[test]
198 fn test_warp_center_pixel_unchanged() {
199 let w = 11u32; let h = 11u32;
205 let mut img = make_rgb(w, h, 32768);
206
207 let cx_idx = (h as usize / 2) * (w as usize) + (w as usize / 2);
208
209 apply_warp_rectilinear(&mut img, [-0.05, 0.002, 0.0, 0.0], 0.5, 0.5);
210
211 assert_eq!(img.data[cx_idx * 3], 32768, "R of centre pixel");
213 assert_eq!(img.data[cx_idx * 3 + 1], 32768, "G of centre pixel");
214 assert_eq!(img.data[cx_idx * 3 + 2], 32768, "B of centre pixel");
215 }
216
217 #[test]
218 fn test_warp_no_crash_small_image() {
219 let mut img = make_rgb(2, 2, 1000);
221 apply_warp_rectilinear(&mut img, [-0.1, 0.05, -0.01, 0.001], 0.5, 0.5);
222 assert_eq!(img.width(), 2);
223 assert_eq!(img.height(), 2);
224 }
225
226 #[test]
227 fn test_warp_no_crash_1x1_image() {
228 let mut img = make_rgb(1, 1, 5000);
229 apply_warp_rectilinear(&mut img, [-0.1, 0.0, 0.0, 0.0], 0.5, 0.5);
230 assert_eq!(img.data[0], 5000);
231 }
232
233 #[test]
236 fn test_warp_tangential_zero_unchanged() {
237 let original = make_gradient(8, 8);
238 let mut img = original.clone();
239
240 apply_warp_rectilinear_tangential(&mut img, [0.0; 4], [0.0; 2], 0.5, 0.5);
241
242 assert_eq!(
243 img.data, original.data,
244 "all-zero tangential coefficients must leave image unchanged"
245 );
246 }
247
248 #[test]
249 fn test_warp_tangential_output_size_unchanged() {
250 let w = 12u32;
251 let h = 8u32;
252 let mut img = make_gradient(w, h);
253
254 apply_warp_rectilinear_tangential(
255 &mut img,
256 [-0.01, 0.0, 0.0, 0.0],
257 [0.001, -0.001],
258 0.5,
259 0.5,
260 );
261
262 assert_eq!(img.width(), w);
263 assert_eq!(img.height(), h);
264 }
265
266 #[test]
267 fn test_warp_uniform_image_stays_uniform() {
268 let mut img = make_rgb(10, 10, 8000);
271 apply_warp_rectilinear(&mut img, [-0.05, 0.02, -0.005, 0.001], 0.5, 0.5);
272 assert!(
273 img.data.iter().all(|&v| v == 8000),
274 "uniform image must remain uniform"
275 );
276 }
277}