Skip to main content

mtb_align/
gray.rs

1/// A single-channel 8-bit image: row-major samples carried with their dimensions.
2#[derive(Clone)]
3pub struct Gray {
4    data: Vec<u8>,
5    width: usize,
6    height: usize,
7}
8
9impl Gray {
10    /// Panics unless `data.len() == width * height`.
11    pub fn from_vec(data: Vec<u8>, width: usize, height: usize) -> Self {
12        assert_eq!(
13            data.len(),
14            width * height,
15            "{width}x{height} needs {} samples, got {}",
16            width * height,
17            data.len()
18        );
19        Self {
20            data,
21            width,
22            height,
23        }
24    }
25
26    /// Interleaved 8-bit RGB flattened with the paper's weights, which sum to
27    /// exactly 256. Ward notes the green channel alone works as well.
28    ///
29    /// Panics unless `rgb.len() == width * height * 3`.
30    pub fn from_rgb(rgb: &[u8], width: usize, height: usize) -> Self {
31        assert_eq!(
32            rgb.len(),
33            width * height * 3,
34            "{width}x{height} needs {} samples, got {}",
35            width * height * 3,
36            rgb.len()
37        );
38
39        let data = rgb
40            .chunks_exact(3)
41            .map(|pixel| {
42                let (red, green, blue) = (pixel[0] as u32, pixel[1] as u32, pixel[2] as u32);
43                ((54 * red + 183 * green + 19 * blue) / 256) as u8
44            })
45            .collect();
46
47        Self {
48            data,
49            width,
50            height,
51        }
52    }
53
54    pub fn width(&self) -> usize {
55        self.width
56    }
57
58    pub fn height(&self) -> usize {
59        self.height
60    }
61
62    pub fn as_slice(&self) -> &[u8] {
63        &self.data
64    }
65
66    pub fn sample(&self, x: usize, y: usize) -> u8 {
67        self.data[y * self.width + x]
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn from_vec_carries_samples_and_dimensions() {
77        let gray = Gray::from_vec(vec![1, 2, 3, 4, 5, 6], 3, 2);
78
79        assert_eq!(gray.width(), 3);
80        assert_eq!(gray.height(), 2);
81        assert_eq!(gray.as_slice(), &[1, 2, 3, 4, 5, 6]);
82    }
83
84    #[test]
85    #[should_panic(expected = "3x2 needs 6 samples, got 5")]
86    fn from_vec_rejects_a_length_that_contradicts_the_dimensions() {
87        Gray::from_vec(vec![1, 2, 3, 4, 5], 3, 2);
88    }
89
90    #[test]
91    fn sample_reads_row_major() {
92        let gray = Gray::from_vec(vec![1, 2, 3, 4, 5, 6], 3, 2);
93
94        assert_eq!(gray.sample(0, 0), 1);
95        assert_eq!(gray.sample(2, 0), 3);
96        assert_eq!(gray.sample(0, 1), 4);
97        assert_eq!(gray.sample(2, 1), 6);
98    }
99
100    #[test]
101    fn from_rgb_maps_white_to_full_scale_and_black_to_zero() {
102        let gray = Gray::from_rgb(&[255, 255, 255, 0, 0, 0], 2, 1);
103
104        assert_eq!(gray.as_slice(), &[255, 0]);
105    }
106
107    #[test]
108    fn from_rgb_weights_the_channels_as_the_paper_does() {
109        let gray = Gray::from_rgb(&[255, 0, 0, 0, 255, 0, 0, 0, 255], 3, 1);
110
111        assert_eq!(gray.as_slice(), &[53, 182, 18]);
112    }
113
114    #[test]
115    #[should_panic(expected = "2x1 needs 6 samples, got 5")]
116    fn from_rgb_rejects_a_length_that_contradicts_the_dimensions() {
117        Gray::from_rgb(&[1, 2, 3, 4, 5], 2, 1);
118    }
119}