rawshift_image/transforms/
ca_correction.rs1use crate::core::image::RgbImage;
10
11pub fn apply_ca_correction(image: &mut RgbImage, red_scale: f32, blue_scale: f32) {
33 let width = image.width() as usize;
34 let height = image.height() as usize;
35
36 if width == 0 || height == 0 {
37 return;
38 }
39
40 let cx = (width as f32 - 1.0) * 0.5;
41 let cy = (height as f32 - 1.0) * 0.5;
42
43 let input = image.data.clone();
44
45 let scales = [(0usize, red_scale), (2usize, blue_scale)];
46
47 for (channel, scale) in scales {
48 if (scale - 1.0_f32).abs() < 1e-6 {
49 continue; }
51
52 let inv_scale = 1.0_f32 / scale;
53
54 for y in 0..height {
55 for x in 0..width {
56 let sx = cx + (x as f32 - cx) * inv_scale;
57 let sy = cy + (y as f32 - cy) * inv_scale;
58
59 let value = bilinear_sample(&input, width, height, channel, sx, sy);
60 image.data[(y * width + x) * 3 + channel] = value;
61 }
62 }
63 }
64}
65
66#[inline]
71fn bilinear_sample(
72 data: &[u16],
73 width: usize,
74 height: usize,
75 channel: usize,
76 sx: f32,
77 sy: f32,
78) -> u16 {
79 let sx = sx.clamp(0.0, (width as f32) - 1.0);
81 let sy = sy.clamp(0.0, (height as f32) - 1.0);
82
83 let x0 = sx.floor() as usize;
84 let y0 = sy.floor() as usize;
85 let x1 = (x0 + 1).min(width - 1);
86 let y1 = (y0 + 1).min(height - 1);
87
88 let fx = sx - sx.floor();
89 let fy = sy - sy.floor();
90
91 let p00 = data[(y0 * width + x0) * 3 + channel] as f32;
92 let p10 = data[(y0 * width + x1) * 3 + channel] as f32;
93 let p01 = data[(y1 * width + x0) * 3 + channel] as f32;
94 let p11 = data[(y1 * width + x1) * 3 + channel] as f32;
95
96 let top = p00 * (1.0 - fx) + p10 * fx;
97 let bot = p01 * (1.0 - fx) + p11 * fx;
98 let result = top * (1.0 - fy) + bot * fy;
99
100 result.round() as u16
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 fn make_rgb(width: u32, height: u32, fill: u16) -> RgbImage {
108 let n = (width as usize) * (height as usize) * 3;
109 RgbImage::new(width, height, vec![fill; n])
110 }
111
112 #[test]
113 fn test_ca_correction_scale_1_unchanged() {
114 let w = 8u32;
116 let h = 8u32;
117 let n = (w as usize) * (h as usize) * 3;
118 let data: Vec<u16> = (0..n).map(|i| (i as u16).wrapping_mul(7)).collect();
120 let mut img = RgbImage::new(w, h, data.clone());
121 apply_ca_correction(&mut img, 1.0, 1.0);
122 assert_eq!(img.data, data, "scale 1.0 should leave image unchanged");
123 }
124
125 #[test]
126 fn test_ca_correction_no_crash_small() {
127 let mut img = make_rgb(2, 2, 1000);
129 apply_ca_correction(&mut img, 0.999, 1.001);
130 assert_eq!(img.width(), 2);
132 assert_eq!(img.height(), 2);
133 }
134
135 #[test]
136 fn test_ca_correction_output_size_unchanged() {
137 let w = 16u32;
138 let h = 10u32;
139 let mut img = make_rgb(w, h, 500);
140 apply_ca_correction(&mut img, 1.002, 0.998);
141 assert_eq!(img.width(), w, "width must not change");
142 assert_eq!(img.height(), h, "height must not change");
143 assert_eq!(img.data.len(), (w as usize) * (h as usize) * 3);
144 }
145
146 #[test]
147 fn test_ca_correction_uniform_image_unchanged() {
148 let mut img = make_rgb(12, 12, 8000);
150 apply_ca_correction(&mut img, 1.005, 0.995);
151 assert!(img.data.iter().all(|&v| v == 8000));
152 }
153
154 #[test]
155 fn test_ca_correction_green_channel_unmodified() {
156 let w = 8u32;
158 let h = 8u32;
159 let n = (w as usize) * (h as usize) * 3;
160 let data: Vec<u16> = (0..n).map(|i| i as u16).collect();
161 let original_green: Vec<u16> = data.chunks_exact(3).map(|px| px[1]).collect();
162 let mut img = RgbImage::new(w, h, data);
163 apply_ca_correction(&mut img, 1.005, 0.995);
164 let corrected_green: Vec<u16> = img.data.chunks_exact(3).map(|px| px[1]).collect();
165 assert_eq!(
166 original_green, corrected_green,
167 "G channel must not be modified"
168 );
169 }
170
171 #[test]
172 fn test_ca_correction_1x1_image() {
173 let mut img = make_rgb(1, 1, 1234);
174 apply_ca_correction(&mut img, 1.01, 0.99);
175 assert_eq!(img.data[0], 1234);
177 assert_eq!(img.data[2], 1234);
178 }
179}