Skip to main content

blad_cfa/
lib.rs

1//! Colour filter array decorrelation.
2//!
3//! A Bayer mosaic interleaves four colour channels on a 2×2 grid, so horizontally
4//! adjacent samples measure *different* colours. Predictive image coders assume
5//! neighbouring samples are correlated, and that assumption is violated on every step —
6//! which is why compressing a mosaic as if it were a grayscale image leaves a large
7//! amount on the table.
8//!
9//! Splitting the mosaic into four half-resolution planes, one per CFA position, restores
10//! the assumption: within a plane, every neighbour measures the same colour.
11//!
12//! Measured on a Hasselblad X1D 3FR (8384×6304, 16-bit, ISO 400), lossless JXL:
13//!
14//! | approach                    | ratio |
15//! |-----------------------------|-------|
16//! | mosaic as one grayscale     | 0.613 |
17//! | four CFA sub-planes         | 0.534 |
18//!
19//! **12.8% smaller, and faster** — the planes are quarter-size and encode in parallel.
20//! General-purpose compressors cannot do this because they do not know the data is a
21//! mosaic.
22//!
23//! # Why this operates on bytes
24//!
25//! The transform is a pure reordering: samples are moved, never read. So it needs
26//! neither the CFA pattern (RGGB vs BGGR vs …) nor the byte order — which keeps it
27//! exactly reversible for any sensor, and avoids materialising the image as `u16`.
28
29#[derive(Debug, thiserror::Error, PartialEq, Eq)]
30pub enum Error {
31    #[error("CFA split needs even dimensions, got {width}×{height}")]
32    OddDimensions { width: u32, height: u32 },
33    #[error("expected {expected} bytes of mosaic data, got {actual}")]
34    WrongLength { expected: usize, actual: usize },
35}
36
37pub type Result<T> = std::result::Result<T, Error>;
38
39/// The four CFA positions, in raster order: (0,0), (1,0), (0,1), (1,1).
40pub const PLANE_COUNT: usize = 4;
41
42/// Four half-resolution planes decomposed from a mosaic, as raw sample bytes in the
43/// source's own byte order.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct Planes {
46    /// Width of each plane (half the mosaic width).
47    pub width: u32,
48    /// Height of each plane (half the mosaic height).
49    pub height: u32,
50    pub bytes_per_sample: usize,
51    pub planes: [Vec<u8>; PLANE_COUNT],
52}
53
54/// Split a mosaic into four sub-planes.
55pub fn split(mosaic: &[u8], width: u32, height: u32, bytes_per_sample: usize) -> Result<Planes> {
56    if !width.is_multiple_of(2) || !height.is_multiple_of(2) {
57        return Err(Error::OddDimensions { width, height });
58    }
59    let bps = bytes_per_sample;
60    let expected = width as usize * height as usize * bps;
61    if mosaic.len() != expected {
62        return Err(Error::WrongLength {
63            expected,
64            actual: mosaic.len(),
65        });
66    }
67
68    let (w, h) = (width as usize, height as usize);
69    let (hw, hh) = (w / 2, h / 2);
70    let mut planes: [Vec<u8>; PLANE_COUNT] =
71        std::array::from_fn(|_| Vec::with_capacity(hw * hh * bps));
72
73    for y in 0..h {
74        let row = &mosaic[y * w * bps..(y + 1) * w * bps];
75        // Rows alternate between plane pair {0,1} (even y) and {2,3} (odd y).
76        let pair = (y % 2) * 2;
77        for x in 0..w {
78            planes[pair + (x % 2)].extend_from_slice(&row[x * bps..(x + 1) * bps]);
79        }
80    }
81
82    Ok(Planes {
83        width: hw as u32,
84        height: hh as u32,
85        bytes_per_sample: bps,
86        planes,
87    })
88}
89
90/// Re-interleave four sub-planes back into a mosaic.
91pub fn merge(planes: &Planes) -> Vec<u8> {
92    let bps = planes.bytes_per_sample;
93    let (hw, hh) = (planes.width as usize, planes.height as usize);
94    let (w, h) = (hw * 2, hh * 2);
95    let mut out = vec![0u8; w * h * bps];
96
97    for y in 0..h {
98        let plane_row = (y / 2) * hw * bps;
99        let pair = (y % 2) * 2;
100        for x in 0..w {
101            let src = &planes.planes[pair + (x % 2)];
102            let s = plane_row + (x / 2) * bps;
103            let d = (y * w + x) * bps;
104            out[d..d + bps].copy_from_slice(&src[s..s + bps]);
105        }
106    }
107    out
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    /// Deterministic pseudo-random bytes; no dev-dependency needed.
115    fn noise(n: usize, seed: u64) -> Vec<u8> {
116        let mut s = seed | 1;
117        (0..n)
118            .map(|_| {
119                s ^= s << 13;
120                s ^= s >> 7;
121                s ^= s << 17;
122                (s >> 24) as u8
123            })
124            .collect()
125    }
126
127    #[test]
128    fn round_trip_is_byte_exact() {
129        for (w, h) in [(8u32, 6u32), (64, 32), (2, 2), (1024, 8)] {
130            let src = noise(w as usize * h as usize * 2, u64::from(w * h));
131            let p = split(&src, w, h, 2).unwrap();
132            assert_eq!(src, merge(&p), "round trip failed for {w}×{h}");
133        }
134    }
135
136    #[test]
137    fn planes_are_quarter_size_and_partition_the_mosaic() {
138        let (w, h) = (8u32, 6u32);
139        let src = noise(w as usize * h as usize * 2, 7);
140        let p = split(&src, w, h, 2).unwrap();
141        assert_eq!((p.width, p.height), (4, 3));
142        let total: usize = p.planes.iter().map(|v| v.len()).sum();
143        assert_eq!(total, (w * h) as usize * 2);
144    }
145
146    #[test]
147    fn split_routes_positions_correctly() {
148        // 2×2 mosaic of 16-bit samples, raster order 1,2,3,4 (little-endian bytes).
149        let src: Vec<u8> = [1u16, 2, 3, 4]
150            .iter()
151            .flat_map(|v| v.to_le_bytes())
152            .collect();
153        let p = split(&src, 2, 2, 2).unwrap();
154        assert_eq!(p.planes[0], vec![1, 0]); // (0,0)
155        assert_eq!(p.planes[1], vec![2, 0]); // (1,0)
156        assert_eq!(p.planes[2], vec![3, 0]); // (0,1)
157        assert_eq!(p.planes[3], vec![4, 0]); // (1,1)
158    }
159
160    /// The transform must not depend on byte order: the same bytes in, the same bytes
161    /// out, whatever they happen to mean.
162    #[test]
163    fn byte_order_is_irrelevant() {
164        let src = noise(16 * 16 * 2, 3);
165        let a = split(&src, 16, 16, 2).unwrap();
166        let mut swapped = src.clone();
167        for c in swapped.chunks_exact_mut(2) {
168            c.swap(0, 1);
169        }
170        let b = split(&swapped, 16, 16, 2).unwrap();
171        for i in 0..PLANE_COUNT {
172            let unswapped: Vec<u8> = b.planes[i]
173                .chunks_exact(2)
174                .flat_map(|c| [c[1], c[0]])
175                .collect();
176            assert_eq!(a.planes[i], unswapped);
177        }
178    }
179
180    #[test]
181    fn odd_dimensions_rejected() {
182        let src = vec![0u8; 3 * 4 * 2];
183        assert_eq!(
184            split(&src, 3, 4, 2),
185            Err(Error::OddDimensions {
186                width: 3,
187                height: 4
188            })
189        );
190    }
191
192    #[test]
193    fn wrong_length_rejected() {
194        assert!(matches!(
195            split(&[0u8; 10], 4, 4, 2),
196            Err(Error::WrongLength { .. })
197        ));
198    }
199}