Skip to main content

rawshift_image/transforms/
black_level.rs

1//! Black level subtraction for raw sensor data.
2//!
3//! Subtracts the per-channel black level (pedestal) from raw CFA data.
4//! Black levels are stored per 2×2 Bayer position in [`RawImage::black_levels`].
5
6use crate::core::image::RawImage;
7
8/// Subtract per-channel black levels from raw CFA data in place.
9///
10/// Each pixel's black level is determined by its position in the 2×2 Bayer
11/// pattern: `black_levels[(y % 2) * 2 + (x % 2)]`.
12///
13/// This is a no-op if all black levels are zero.
14pub fn apply_black_level(raw: &mut RawImage) {
15    let bl = *raw.black_levels();
16    if bl[0] == 0 && bl[1] == 0 && bl[2] == 0 && bl[3] == 0 {
17        return;
18    }
19
20    let width = raw.width() as usize;
21    for (i, pixel) in raw.data.iter_mut().enumerate() {
22        let x = i % width;
23        let y = i / width;
24        let bl_idx = (y % 2) * 2 + (x % 2);
25        *pixel = pixel.saturating_sub(bl[bl_idx]);
26    }
27}
28
29#[cfg(test)]
30mod tests {
31    use super::*;
32    use crate::core::image::{CfaPattern, Rect, Size};
33
34    fn make_raw(width: u32, height: u32, data: Vec<u16>, black_levels: [u16; 4]) -> RawImage {
35        let size = Size::new(width, height);
36        let active = Rect::from_coords(0, 0, width, height);
37        let mut raw = RawImage::new(size, active, 14, CfaPattern::Rggb);
38        raw.data = data;
39        raw.set_black_levels(black_levels);
40        raw
41    }
42
43    #[test]
44    fn no_op_when_all_zero() {
45        let original = vec![100, 200, 300, 400];
46        let mut raw = make_raw(2, 2, original.clone(), [0, 0, 0, 0]);
47        apply_black_level(&mut raw);
48        assert_eq!(raw.data, original);
49    }
50
51    #[test]
52    fn uniform_black_level() {
53        let mut raw = make_raw(2, 2, vec![100, 200, 300, 400], [50, 50, 50, 50]);
54        apply_black_level(&mut raw);
55        assert_eq!(raw.data, vec![50, 150, 250, 350]);
56    }
57
58    #[test]
59    fn per_channel_subtraction() {
60        // 2×2 image, each pixel at a different Bayer position
61        // bl_idx mapping: (0,0)=0, (1,0)=1, (0,1)=2, (1,1)=3
62        let mut raw = make_raw(2, 2, vec![1000, 1000, 1000, 1000], [10, 20, 30, 40]);
63        apply_black_level(&mut raw);
64        assert_eq!(raw.data, vec![990, 980, 970, 960]);
65    }
66
67    #[test]
68    fn saturating_subtraction_does_not_underflow() {
69        let mut raw = make_raw(2, 1, vec![5, 10], [100, 100, 100, 100]);
70        apply_black_level(&mut raw);
71        assert_eq!(raw.data, vec![0, 0]);
72    }
73
74    #[test]
75    fn test_black_level_all_channels() {
76        // 2x2 image: each pixel at a distinct CFA position
77        // bl_idx: (0,0)->0, (1,0)->1, (0,1)->2, (1,1)->3
78        let data = vec![1000u16, 2000, 3000, 4000];
79        let black_levels = [100u16, 200, 300, 400];
80        let mut raw = make_raw(2, 2, data, black_levels);
81        apply_black_level(&mut raw);
82        assert_eq!(raw.data[0], 900, "(0,0) R channel");
83        assert_eq!(raw.data[1], 1800, "(1,0) G channel");
84        assert_eq!(raw.data[2], 2700, "(0,1) G channel");
85        assert_eq!(raw.data[3], 3600, "(1,1) B channel");
86    }
87
88    #[test]
89    fn test_black_level_clamps_at_zero() {
90        // Pixel value less than or equal to black level should become 0
91        let data = vec![50u16, 100, 150, 200];
92        let black_levels = [100u16, 100, 100, 100];
93        let mut raw = make_raw(2, 2, data, black_levels);
94        apply_black_level(&mut raw);
95        assert_eq!(raw.data[0], 0, "50 - 100 should clamp to 0");
96        assert_eq!(raw.data[1], 0, "100 - 100 should equal 0");
97        assert_eq!(raw.data[2], 50, "150 - 100 should be 50");
98        assert_eq!(raw.data[3], 100, "200 - 100 should be 100");
99    }
100
101    #[test]
102    fn test_black_level_preserves_above_white() {
103        // Values above the typical white level are unchanged by black level subtraction
104        // (black_level subtraction doesn't clip to white level)
105        let data = vec![65000u16, 65535, 50000, 40000];
106        let black_levels = [100u16, 100, 100, 100];
107        let mut raw = make_raw(2, 2, data, black_levels);
108        apply_black_level(&mut raw);
109        assert_eq!(
110            raw.data[0], 64900,
111            "high value should have black subtracted"
112        );
113        assert_eq!(raw.data[1], 65435, "max value minus black level");
114        assert_eq!(raw.data[2], 49900);
115        assert_eq!(raw.data[3], 39900);
116    }
117}