1use super::image::rgb_to_luma;
2use ndarray::{s, Array2, ArrayView2, ArrayView3};
3use nshare::ToNdarray2;
4
5#[derive(Debug, Clone)]
7pub struct IntensityMap {
8 map: Array2<f32>,
9 shape: (usize, usize),
10}
11
12const H: f32 = 0.005;
14const H_INV: f32 = 1.0 / H;
15const BORDER_SIZE: usize = 2;
16
17impl IntensityMap {
18 pub fn shape(&self) -> (usize, usize) {
20 self.shape
21 }
22
23 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 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 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 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 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 pub fn from_rgb_image(image: &ArrayView3<u8>) -> Self {
97 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 }
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 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 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}