align3d/
intensity_map.rs

1use super::image::rgb_to_luma;
2use ndarray::{s, Array2, ArrayView2, ArrayView3};
3use nshare::ToNdarray2;
4
5/// Stores a grayscale image with float and interpolation operations.
6#[derive(Debug, Clone)]
7pub struct IntensityMap {
8    map: Array2<f32>,
9    shape: (usize, usize),
10}
11
12// The H is gradient divisor constant.
13const H: f32 = 0.005;
14const H_INV: f32 = 1.0 / H;
15const BORDER_SIZE: usize = 2;
16
17impl IntensityMap {
18    /// Returns the shape of the map (height, width).
19    pub fn shape(&self) -> (usize, usize) {
20        self.shape
21    }
22
23    /// Creates a map with all zeros.
24    pub fn zeros(shape: (usize, usize)) -> Self {
25        Self {
26            map: Array2::zeros((shape.0 + BORDER_SIZE, shape.1 + BORDER_SIZE)),
27            shape,
28        }
29    }
30
31    /// Fills the map with the values from a given image.
32    /// It will try to reuse allocated map.
33    ///
34    /// # Arguments
35    /// * image: The image data to be converted in a intensity map.
36    ///   Its values are divided by 255.0.
37    pub fn fill(&mut self, image: &ArrayView2<u8>) {
38        let (in_height, in_width) = {
39            let dim = image.shape();
40            (dim[0], dim[1])
41        };
42
43        let (map_grid_height, map_grid_width) = self.map.dim();
44
45        if in_height >= map_grid_height && in_width >= map_grid_width {
46            self.map = Array2::zeros((in_height + BORDER_SIZE, in_width + BORDER_SIZE));
47        }
48
49        self.shape = (in_height, in_width);
50
51        // Fills the image.
52        self.map
53            .slice_mut(s!(..in_height, ..in_width))
54            .iter_mut()
55            .zip(image.iter())
56            .for_each(|(dst, src)| {
57                *dst = *src as f32 / 255.0;
58            });
59
60        // Fills the border X:
61        for col in 0..in_width - 1 {
62            let border = self.map[(in_height - 1, col)];
63            for k in 0..2 {
64                self.map[(in_height + k, col)] = border;
65            }
66        }
67
68        for row in 0..in_height - 1 {
69            let border = self.map[(row, in_width - 1)];
70            for k in 0..2 {
71                self.map[(row, in_width + k)] = border;
72            }
73        }
74
75        let last_elem = image[(in_height - 1, in_width - 1)] as f32 / 255.0;
76        for k in 0..2 {
77            self.map[(in_height + k, in_width + k)] = last_elem;
78        }
79    }
80
81    /// Constructor to create a map filled with an image.
82    /// See `fill`.
83    pub fn from_luma_image(image: &ArrayView2<u8>) -> Self {
84        let shape = {
85            let sh = image.shape();
86            (sh[0], sh[1])
87        };
88
89        let mut map = Self::zeros(shape);
90        map.fill(image);
91        map
92    }
93
94    /// Constructor to create a map filled with a RGBimage.
95    /// See `fill`.
96    pub fn from_rgb_image(image: &ArrayView3<u8>) -> Self {
97        // TODO: remove unnecessary copies.
98
99        let luma = match image.dim() {
100            (width, height, 3) => {
101                let mut luma = Array2::<u8>::zeros((width, height));
102                for y in 0..height {
103                    for x in 0..width {
104                        let r = image[[x, y, 0]];
105                        let g = image[[x, y, 1]];
106                        let b = image[[x, y, 2]];
107                        luma[[x, y]] = (rgb_to_luma(r, g, b) * 255.0) as u8;
108                    }
109                }
110                luma
111
112                //let shape = image.shape();
113                //let color = image.view().into_shape((shape[1] * shape[2], 3)).unwrap();
114                //color
115                //    .axis_iter(Axis(0))
116                //    .map(|rgb| (rgb_to_luma(rgb[0], rgb[1], rgb[2]) * 255.0) as u8)
117                //    .collect::<Array1<u8>>()
118                //    .into_shape((shape[1], shape[2]))
119                //    .unwrap()
120            }
121            (3, width, height) => {
122                let mut luma = Array2::<u8>::zeros((width, height));
123                for y in 0..height {
124                    for x in 0..width {
125                        let r = image[[0, x, y]];
126                        let g = image[[1, x, y]];
127                        let b = image[[2, x, y]];
128                        luma[[x, y]] = (rgb_to_luma(r, g, b) * 255.0) as u8;
129                    }
130                }
131                luma
132            }
133            _ => panic!("Invalid image shape"),
134        };
135
136        Self::from_luma_image(&luma.view())
137    }
138
139    /// Returns the intensity value with bilinear interpolation if
140    /// u or v are not round numbers.
141    ///
142    /// # Arguments:
143    ///
144    /// * `u`: The "x" coordinate. Range is [0..width].
145    /// * `v`: The "y" coordinate. Range is [0..height].
146    ///
147    /// # Returns:
148    ///
149    /// Bilinear interpolated value.
150    pub fn bilinear(&self, u: f32, v: f32) -> f32 {
151        let ui = u as usize;
152        let vi = v as usize;
153
154        let u_frac = u - ui as f32;
155        let v_frac = v - vi as f32;
156
157        let (val00, val10, val01, val11) = {
158            (
159                self.map[(vi, ui)],
160                self.map[(vi, ui + 1)],
161                self.map[[vi + 1, ui]],
162                self.map[(vi + 1, ui + 1)],
163            )
164        };
165
166        let u0_interp = val00 * (1.0 - u_frac) + val10 * u_frac;
167        let u1_interp = val01 * (1.0 - u_frac) + val11 * u_frac;
168        u0_interp * (1.0 - v_frac) + u1_interp * v_frac
169    }
170
171    /// Returns the intensity value with bilinear interpolation if
172    /// u or v are not round numbers, and `u` and `v` numerical gradients.
173    ///
174    /// # Arguments:
175    ///
176    /// * `u`: The "x" coordinate. Range is [0..1].
177    /// * `v`: The "y" coordinate. Range is [0..1].
178    ///
179    /// # Returns:
180    ///
181    /// * Bilinear interpolated value.
182    /// * `u`'s gradient.
183    /// * `v`'s gradient.
184    pub fn bilinear_grad(&self, u: f32, v: f32) -> (f32, f32, f32) {
185        let ui = u as usize;
186        let vi = v as usize;
187
188        let u_frac = u - ui as f32;
189        let v_frac = v - vi as f32;
190
191        let (val00, val10, val01, val11) = (
192            self.map[(vi, ui)],
193            self.map[(vi, ui + 1)],
194            self.map[(vi + 1, ui)],
195            self.map[(vi + 1, ui + 1)],
196        );
197
198        let u0_interp = val00 * (1.0 - u_frac) + val10 * u_frac;
199        let u1_interp = val01 * (1.0 - u_frac) + val11 * u_frac;
200
201        let value = u0_interp * (1.0 - v_frac) + u1_interp * v_frac;
202
203        let uh = self.bilinear(u + H, v);
204        let vh = self.bilinear(u, v + H);
205
206        let grad_u = (uh - value) * H_INV;
207        let grad_v = (vh - value) * H_INV;
208
209        (value, grad_u, grad_v)
210    }
211}
212
213impl ToNdarray2 for IntensityMap {
214    type Out = Array2<f32>;
215    fn into_ndarray2(self) -> Self::Out {
216        self.map
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use ndarray::Array2;
223    use rstest::rstest;
224
225    use super::IntensityMap;
226    use crate::unit_test::bloei_luma8;
227
228    #[rstest]
229    fn border_should_repeat(bloei_luma8: Array2<u8>) {
230        let map = IntensityMap::from_luma_image(&bloei_luma8.view());
231        let width = bloei_luma8.shape()[1];
232        let height = bloei_luma8.shape()[0];
233        assert_eq!(
234            map.bilinear(0.0, (height - 1) as f32 + 0.25),
235            bloei_luma8[(height - 1, 0)] as f32 / 255.0
236        );
237        assert_eq!(
238            map.bilinear((width - 1) as f32 + 0.25, 0.0),
239            bloei_luma8[(0, width - 1)] as f32 / 255.0
240        );
241    }
242
243    #[rstest]
244    fn round_uv_should_match_image(bloei_luma8: Array2<u8>) {
245        let map = IntensityMap::from_luma_image(&bloei_luma8.view());
246
247        for (y, x) in [(20, 0), (33, 44), (12, 48)] {
248            assert_eq!(
249                map.bilinear(x as f32, y as f32),
250                bloei_luma8[(y, x)] as f32 / 255.0
251            );
252        }
253    }
254
255    #[rstest]
256    fn values(bloei_luma8: Array2<u8>) {
257        let map = IntensityMap::from_luma_image(&bloei_luma8.view());
258        for ((y, x), img_value) in bloei_luma8.indexed_iter() {
259            let (value, _du, _dv) = map.bilinear_grad(x as f32, y as f32);
260            assert_eq!(*img_value as f32 / 255.0, value);
261        }
262    }
263}