Skip to main content

rawshift_image/processing/
color.rs

1//! Color processing primitives.
2//!
3//! This module provides functions for color correction in the raw processing pipeline:
4//! - White balance correction
5//! - Color matrix application (camera RGB to sRGB)
6//! - Gamma correction
7//!
8//! All functions operate on 16-bit RGB data in the range [0, 65535].
9
10use crate::core::image::{RawImage, RgbImage};
11
12/// Apply white balance to a raw Bayer CFA image.
13///
14/// Multiplies each pixel by the coefficient corresponding to its CFA channel
15/// (determined by the pixel's position within the 2x2 Bayer pattern).
16/// Data is clamped to the u16 range [0, 65535].
17///
18/// # Arguments
19/// * `image` - The raw image to modify in place
20/// * `coeffs` - (red_scale, green_scale, blue_scale) multipliers
21pub fn apply_white_balance_raw(image: &mut RawImage, coeffs: (f32, f32, f32)) {
22    let (r_scale, g_scale, b_scale) = coeffs;
23    let pattern = image.cfa_pattern().to_array();
24
25    // Build a 2x2 gain lookup from the CFA pattern.
26    // Pattern values: 0=Red, 1=Green, 2=Blue
27    let scale_for = |color: u8| -> f32 {
28        match color {
29            0 => r_scale,
30            2 => b_scale,
31            _ => g_scale,
32        }
33    };
34    let gains: [f32; 4] = [
35        scale_for(pattern[0]),
36        scale_for(pattern[1]),
37        scale_for(pattern[2]),
38        scale_for(pattern[3]),
39    ];
40
41    let width = image.width() as usize;
42    for (idx, pixel) in image.data.iter_mut().enumerate() {
43        let x = idx % width;
44        let y = idx / width;
45        let gain = gains[(y % 2) * 2 + (x % 2)];
46        let val = *pixel as f32 * gain;
47        *pixel = clamp_u16(val);
48    }
49}
50
51/// Apply white balance to an RGB image.
52///
53/// Multiplies each channel by the corresponding coefficient.
54/// Data is clamped to the u16 range [0, 65535].
55///
56/// # Arguments
57/// * `image` - The image to modify in place
58/// * `coeffs` - (red_scale, green_scale, blue_scale) multipliers
59///
60/// # Example
61/// ```ignore
62/// // Typical daylight white balance for Sony sensors
63/// apply_white_balance(&mut image, (2.35, 1.0, 1.65));
64/// ```
65pub fn apply_white_balance(image: &mut RgbImage, coeffs: (f32, f32, f32)) {
66    let (r_scale, g_scale, b_scale) = coeffs;
67
68    // Process pixel triplets
69    for chunk in image.data.chunks_exact_mut(3) {
70        // Red
71        let r = chunk[0] as f32 * r_scale;
72        chunk[0] = clamp_u16(r);
73
74        // Green
75        let g = chunk[1] as f32 * g_scale;
76        chunk[1] = clamp_u16(g);
77
78        // Blue
79        let b = chunk[2] as f32 * b_scale;
80        chunk[2] = clamp_u16(b);
81    }
82}
83
84/// Apply a 3x3 color matrix to an RGB image.
85///
86/// The matrix transforms from camera RGB to output color space (typically sRGB).
87/// Matrix is row-major: `[R_row, G_row, B_row]` where each row has 3 elements.
88///
89/// ```text
90/// [ R_out ]   [ m0 m1 m2 ] [ R_in ]
91/// [ G_out ] = [ m3 m4 m5 ] [ G_in ]
92/// [ B_out ]   [ m6 m7 m8 ] [ B_in ]
93/// ```
94///
95/// # Arguments
96/// * `image` - The image to modify in place
97/// * `matrix` - 3x3 row-major color transformation matrix
98pub fn apply_color_matrix(image: &mut RgbImage, matrix: &[f32; 9]) {
99    for chunk in image.data.chunks_exact_mut(3) {
100        let r = chunk[0] as f32;
101        let g = chunk[1] as f32;
102        let b = chunk[2] as f32;
103
104        let r_out = r * matrix[0] + g * matrix[1] + b * matrix[2];
105        let g_out = r * matrix[3] + g * matrix[4] + b * matrix[5];
106        let b_out = r * matrix[6] + g * matrix[7] + b * matrix[8];
107
108        chunk[0] = clamp_u16(r_out);
109        chunk[1] = clamp_u16(g_out);
110        chunk[2] = clamp_u16(b_out);
111    }
112}
113
114/// Pre-computed gamma correction lookup table.
115///
116/// Caches the gamma curve for efficient repeated application.
117/// Use this when applying the same gamma to multiple images.
118///
119/// # Example
120/// ```ignore
121/// let lut = GammaLut::new(2.2);
122/// lut.apply(&mut image1);
123/// lut.apply(&mut image2); // Reuses the same LUT
124/// ```
125pub struct GammaLut {
126    table: Box<[u16; 65536]>,
127    gamma: f32,
128}
129
130impl GammaLut {
131    /// Create a new gamma lookup table.
132    ///
133    /// # Arguments
134    /// * `gamma` - Gamma value (typically 2.2 for sRGB)
135    #[must_use]
136    pub fn new(gamma: f32) -> Self {
137        let mut table = Box::new([0u16; 65536]);
138        let inv_gamma = 1.0 / gamma;
139
140        for (i, v) in table.iter_mut().enumerate() {
141            let normalized = i as f32 / 65535.0;
142            let corrected = normalized.powf(inv_gamma);
143            *v = clamp_u16(corrected * 65535.0);
144        }
145
146        Self { table, gamma }
147    }
148
149    /// Get the gamma value this LUT was created with.
150    #[must_use]
151    pub fn gamma(&self) -> f32 {
152        self.gamma
153    }
154
155    /// Apply gamma correction using the cached lookup table.
156    pub fn apply(&self, image: &mut RgbImage) {
157        for pixel in &mut image.data {
158            *pixel = self.table[*pixel as usize];
159        }
160    }
161}
162
163/// Apply gamma correction to an RGB image.
164///
165/// Applies the formula: `V_out = V_in ^ (1 / gamma)`
166/// Input and output are in the range [0, 65535].
167///
168/// For repeated application of the same gamma, consider using [`GammaLut`]
169/// which caches the lookup table.
170///
171/// # Arguments
172/// * `image` - The image to modify in place
173/// * `gamma` - Gamma value (typically 2.2 for sRGB)
174pub fn apply_gamma(image: &mut RgbImage, gamma: f32) {
175    // Fast path: gamma 1.0 is identity
176    if (gamma - 1.0).abs() < 0.001 {
177        return;
178    }
179
180    let lut = GammaLut::new(gamma);
181    lut.apply(image);
182}
183
184/// Clamp a floating-point value to the u16 range [0, 65535].
185///
186/// # Arguments
187/// * `val` - The value to clamp
188///
189/// # Returns
190/// The value clamped to [0, 65535] and cast to u16.
191#[inline(always)]
192pub fn clamp_u16(val: f32) -> u16 {
193    val.clamp(0.0, 65535.0) as u16
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199    use crate::core::image::{CfaPattern, Rect, Size};
200
201    fn create_test_raw_image(width: u32, height: u32, pattern: CfaPattern) -> RawImage {
202        let size = Size::new(width, height);
203        let active = Rect::from_coords(0, 0, width, height);
204        RawImage::new(size, active, 14, pattern)
205    }
206
207    #[test]
208    fn test_white_balance_raw_identity() {
209        let mut image = create_test_raw_image(4, 4, CfaPattern::Rggb);
210        // Fill with uniform value
211        for pixel in &mut image.data {
212            *pixel = 1000;
213        }
214        apply_white_balance_raw(&mut image, (1.0, 1.0, 1.0));
215
216        for &pixel in &image.data {
217            assert_eq!(pixel, 1000);
218        }
219    }
220
221    #[test]
222    fn test_white_balance_raw_rggb() {
223        let mut image = create_test_raw_image(4, 4, CfaPattern::Rggb);
224        for pixel in &mut image.data {
225            *pixel = 1000;
226        }
227        apply_white_balance_raw(&mut image, (2.0, 1.0, 0.5));
228
229        // RGGB: row 0: R G R G, row 1: G B G B, ...
230        // (0,0) = R -> 2000
231        assert_eq!(image.data[0], 2000);
232        // (1,0) = G -> 1000
233        assert_eq!(image.data[1], 1000);
234        // (2,0) = R -> 2000
235        assert_eq!(image.data[2], 2000);
236        // (0,1) = G -> 1000
237        assert_eq!(image.data[4], 1000);
238        // (1,1) = B -> 500
239        assert_eq!(image.data[5], 500);
240        // (2,1) = G -> 1000
241        assert_eq!(image.data[6], 1000);
242        // (3,1) = B -> 500
243        assert_eq!(image.data[7], 500);
244    }
245
246    #[test]
247    fn test_white_balance_raw_bggr() {
248        let mut image = create_test_raw_image(2, 2, CfaPattern::Bggr);
249        for pixel in &mut image.data {
250            *pixel = 1000;
251        }
252        apply_white_balance_raw(&mut image, (3.0, 1.0, 2.0));
253
254        // BGGR: (0,0)=B (1,0)=G (0,1)=G (1,1)=R
255        assert_eq!(image.data[0], 2000); // B * 2.0
256        assert_eq!(image.data[1], 1000); // G * 1.0
257        assert_eq!(image.data[2], 1000); // G * 1.0
258        assert_eq!(image.data[3], 3000); // R * 3.0
259    }
260
261    #[test]
262    fn test_white_balance_raw_grbg() {
263        let mut image = create_test_raw_image(2, 2, CfaPattern::Grbg);
264        for pixel in &mut image.data {
265            *pixel = 1000;
266        }
267        apply_white_balance_raw(&mut image, (2.0, 1.0, 3.0));
268
269        // GRBG: (0,0)=G (1,0)=R (0,1)=B (1,1)=G
270        assert_eq!(image.data[0], 1000); // G
271        assert_eq!(image.data[1], 2000); // R
272        assert_eq!(image.data[2], 3000); // B
273        assert_eq!(image.data[3], 1000); // G
274    }
275
276    #[test]
277    fn test_white_balance_raw_gbrg() {
278        let mut image = create_test_raw_image(2, 2, CfaPattern::Gbrg);
279        for pixel in &mut image.data {
280            *pixel = 1000;
281        }
282        apply_white_balance_raw(&mut image, (2.0, 1.0, 3.0));
283
284        // GBRG: (0,0)=G (1,0)=B (0,1)=R (1,1)=G
285        assert_eq!(image.data[0], 1000); // G
286        assert_eq!(image.data[1], 3000); // B
287        assert_eq!(image.data[2], 2000); // R
288        assert_eq!(image.data[3], 1000); // G
289    }
290
291    #[test]
292    fn test_white_balance_raw_clamps() {
293        let mut image = create_test_raw_image(2, 2, CfaPattern::Rggb);
294        image.data[0] = 60000; // R position
295        image.data[1] = 30000; // G position
296        image.data[2] = 60000; // R position
297        image.data[3] = 30000; // G position
298        apply_white_balance_raw(&mut image, (2.0, 2.0, 2.0));
299
300        assert_eq!(image.data[0], 65535); // 60000 * 2 clamped
301        assert_eq!(image.data[1], 60000); // 30000 * 2
302    }
303
304    fn create_test_image(width: u32, height: u32, r: u16, g: u16, b: u16) -> RgbImage {
305        let mut data = Vec::with_capacity((width * height * 3) as usize);
306        for _ in 0..(width * height) {
307            data.push(r);
308            data.push(g);
309            data.push(b);
310        }
311        RgbImage::new(width, height, data)
312    }
313
314    #[test]
315    fn test_clamp_u16() {
316        assert_eq!(clamp_u16(0.0), 0);
317        assert_eq!(clamp_u16(100.5), 100);
318        assert_eq!(clamp_u16(65535.0), 65535);
319        assert_eq!(clamp_u16(-100.0), 0);
320        assert_eq!(clamp_u16(100000.0), 65535);
321    }
322
323    #[test]
324    fn test_white_balance_identity() {
325        let mut image = create_test_image(2, 2, 1000, 2000, 3000);
326        apply_white_balance(&mut image, (1.0, 1.0, 1.0));
327
328        // Identity transform should leave values unchanged
329        for i in 0..4 {
330            assert_eq!(image.data[i * 3], 1000);
331            assert_eq!(image.data[i * 3 + 1], 2000);
332            assert_eq!(image.data[i * 3 + 2], 3000);
333        }
334    }
335
336    #[test]
337    fn test_white_balance_scaling() {
338        let mut image = create_test_image(2, 2, 1000, 2000, 3000);
339        apply_white_balance(&mut image, (2.0, 1.0, 0.5));
340
341        for i in 0..4 {
342            assert_eq!(image.data[i * 3], 2000); // R * 2.0
343            assert_eq!(image.data[i * 3 + 1], 2000); // G * 1.0
344            assert_eq!(image.data[i * 3 + 2], 1500); // B * 0.5
345        }
346    }
347
348    #[test]
349    fn test_white_balance_clamps() {
350        let mut image = create_test_image(1, 1, 60000, 30000, 1000);
351        apply_white_balance(&mut image, (2.0, 2.0, 0.0));
352
353        assert_eq!(image.data[0], 65535); // Clipped to max
354        assert_eq!(image.data[1], 60000); // 30000 * 2
355        assert_eq!(image.data[2], 0); // Clipped to 0
356    }
357
358    #[test]
359    fn test_color_matrix_identity() {
360        let identity_matrix: [f32; 9] = [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0];
361        let mut image = create_test_image(2, 2, 1000, 2000, 3000);
362        apply_color_matrix(&mut image, &identity_matrix);
363
364        // Identity matrix should leave values unchanged
365        for i in 0..4 {
366            assert_eq!(image.data[i * 3], 1000);
367            assert_eq!(image.data[i * 3 + 1], 2000);
368            assert_eq!(image.data[i * 3 + 2], 3000);
369        }
370    }
371
372    #[test]
373    fn test_color_matrix_swap_channels() {
374        // Matrix that swaps R and B
375        let swap_matrix: [f32; 9] = [0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0];
376        let mut image = create_test_image(1, 1, 1000, 2000, 3000);
377        apply_color_matrix(&mut image, &swap_matrix);
378
379        assert_eq!(image.data[0], 3000); // R_out = B_in
380        assert_eq!(image.data[1], 2000); // G_out = G_in
381        assert_eq!(image.data[2], 1000); // B_out = R_in
382    }
383
384    #[test]
385    fn test_gamma_identity() {
386        let mut image = create_test_image(2, 2, 1000, 2000, 3000);
387        let original = image.data.clone();
388        apply_gamma(&mut image, 1.0);
389
390        // Gamma 1.0 should be identity (fast path)
391        assert_eq!(image.data, original);
392    }
393
394    #[test]
395    fn test_gamma_22() {
396        let mut image = create_test_image(1, 1, 0, 32768, 65535);
397        apply_gamma(&mut image, 2.2);
398
399        // Black should stay black
400        assert_eq!(image.data[0], 0);
401        // White should stay white
402        assert_eq!(image.data[2], 65535);
403        // Mid-tone should be brighter (gamma correction raises values)
404        assert!(
405            image.data[1] > 32768,
406            "Mid-tone {} should be > 32768",
407            image.data[1]
408        );
409    }
410
411    #[test]
412    fn test_gamma_lut_new() {
413        let lut = GammaLut::new(2.2);
414        assert_eq!(lut.gamma(), 2.2);
415    }
416
417    #[test]
418    fn test_gamma_lut_apply() {
419        let lut = GammaLut::new(2.2);
420        let mut image = create_test_image(1, 1, 0, 32768, 65535);
421        lut.apply(&mut image);
422
423        assert_eq!(image.data[0], 0);
424        assert_eq!(image.data[2], 65535);
425        assert!(
426            image.data[1] > 32768,
427            "Mid-tone should be brighter after gamma"
428        );
429    }
430
431    #[test]
432    fn test_gamma_lut_reuse() {
433        // Test that reusing a LUT produces consistent results
434        let lut = GammaLut::new(2.2);
435
436        let mut image1 = create_test_image(1, 1, 1000, 2000, 3000);
437        let mut image2 = create_test_image(1, 1, 1000, 2000, 3000);
438
439        lut.apply(&mut image1);
440        lut.apply(&mut image2);
441
442        assert_eq!(image1.data, image2.data);
443    }
444}