1use crate::imageview::{ImageBuffer, ImageView};
9#[cfg(feature = "tracing")]
10use tracing::instrument;
11
12pub struct PyramidBuffers {
19 levels: Vec<ImageBuffer>,
20}
21
22impl Default for PyramidBuffers {
23 fn default() -> Self {
24 Self::new()
25 }
26}
27
28impl PyramidBuffers {
29 pub fn new() -> Self {
31 Self { levels: Vec::new() }
32 }
33
34 pub fn with_capacity(num_levels: u8) -> Self {
36 Self {
37 levels: Vec::with_capacity(num_levels.saturating_sub(1) as usize),
38 }
39 }
40
41 fn ensure_level_shape(&mut self, idx: usize, w: usize, h: usize) {
42 if idx >= self.levels.len() {
43 self.levels.resize_with(idx + 1, || ImageBuffer::new(w, h));
44 }
45
46 let level = &mut self.levels[idx];
47 if level.width != w || level.height != h {
48 *level = ImageBuffer::new(w, h);
49 }
50 }
51}
52
53#[non_exhaustive]
55pub struct PyramidLevel<'a> {
56 pub img: ImageView<'a>,
59 pub scale: f32,
63}
64
65#[non_exhaustive]
72pub struct Pyramid<'a> {
73 pub levels: Vec<PyramidLevel<'a>>,
75}
76
77#[derive(Clone, Debug)]
79#[non_exhaustive]
80pub struct PyramidParams {
81 pub num_levels: u8,
83 pub min_size: usize,
85}
86
87impl Default for PyramidParams {
88 fn default() -> Self {
89 Self {
90 num_levels: 1,
91 min_size: 128,
92 }
93 }
94}
95
96#[cfg_attr(
104 feature = "tracing",
105 instrument(
106 level = "info",
107 skip(base, params, buffers),
108 fields(levels = params.num_levels, min_size = params.min_size)
109 )
110)]
111pub fn build_pyramid<'a>(
112 base: ImageView<'a>,
113 params: &PyramidParams,
114 buffers: &'a mut PyramidBuffers,
115) -> Pyramid<'a> {
116 if params.num_levels == 0 || base.width < params.min_size || base.height < params.min_size {
117 return Pyramid { levels: Vec::new() };
118 }
119
120 #[derive(Clone, Copy)]
121 enum LevelSource {
122 Base,
123 Buffer(usize),
124 }
125
126 let mut sources: Vec<(LevelSource, f32)> = Vec::with_capacity(params.num_levels as usize);
127 sources.push((LevelSource::Base, 1.0));
128
129 let mut current_src = LevelSource::Base;
130 let mut current_w = base.width;
131 let mut current_h = base.height;
132 let mut scale = 1.0f32;
133
134 for level_idx in 1..params.num_levels {
135 let w2 = current_w / 2;
136 let h2 = current_h / 2;
137
138 if w2 == 0 || h2 == 0 || w2 < params.min_size || h2 < params.min_size {
139 break;
140 }
141
142 let buf_idx = (level_idx - 1) as usize;
143 buffers.ensure_level_shape(buf_idx, w2, h2);
144
145 let (src_img, dst): (ImageView<'_>, &mut ImageBuffer) = match current_src {
146 LevelSource::Base => (base, &mut buffers.levels[buf_idx]),
147 LevelSource::Buffer(src_idx) => {
148 debug_assert!(src_idx < buf_idx);
149 let (head, tail) = buffers.levels.split_at_mut(buf_idx);
150 (head[src_idx].as_view(), &mut tail[0])
151 }
152 };
153
154 downsample_2x_box(src_img, dst);
155
156 scale *= 0.5;
157 current_src = LevelSource::Buffer(buf_idx);
158 current_w = w2;
159 current_h = h2;
160 sources.push((current_src, scale));
161 }
162
163 let mut levels = Vec::with_capacity(sources.len());
164 for (source, lvl_scale) in sources {
165 let img = match source {
166 LevelSource::Base => base,
167 LevelSource::Buffer(idx) => buffers.levels[idx].as_view(),
168 };
169 levels.push(PyramidLevel {
170 img,
171 scale: lvl_scale,
172 });
173 }
174
175 Pyramid { levels }
176}
177
178#[inline]
183fn downsample_2x_box(src: ImageView<'_>, dst: &mut ImageBuffer) {
184 #[cfg(all(feature = "par_pyramid", feature = "rayon", feature = "simd"))]
185 return downsample_2x_box_parallel_simd(src, dst);
186
187 #[cfg(all(feature = "par_pyramid", feature = "rayon", not(feature = "simd")))]
188 return downsample_2x_box_parallel_scalar(src, dst);
189
190 #[cfg(all(feature = "par_pyramid", not(feature = "rayon"), feature = "simd"))]
191 return downsample_2x_box_simd(src, dst);
192
193 #[cfg(all(feature = "par_pyramid", not(feature = "rayon"), not(feature = "simd")))]
194 return downsample_2x_box_scalar(src, dst);
195
196 #[cfg(not(feature = "par_pyramid"))]
197 return downsample_2x_box_scalar(src, dst);
198}
199
200#[inline]
201#[cfg_attr(
202 all(feature = "par_pyramid", any(feature = "rayon", feature = "simd")),
203 allow(dead_code)
204)]
205fn downsample_2x_box_scalar(src: ImageView<'_>, dst: &mut ImageBuffer) {
206 debug_assert_eq!(src.width / 2, dst.width);
207 debug_assert_eq!(src.height / 2, dst.height);
208
209 let src_w = src.width;
210 let dst_w = dst.width;
211 let dst_h = dst.height;
212
213 for y in 0..dst_h {
214 let row0 = (y * 2) * src_w;
215 let row1 = row0 + src_w;
216
217 downsample_row_scalar(
218 &src.data[row0..row0 + src_w],
219 &src.data[row1..row1 + src_w],
220 &mut dst.data[y * dst_w..(y + 1) * dst_w],
221 );
222 }
223}
224
225#[cfg(all(feature = "par_pyramid", not(feature = "rayon"), feature = "simd"))]
226fn downsample_2x_box_simd(src: ImageView<'_>, dst: &mut ImageBuffer) {
227 debug_assert_eq!(src.width / 2, dst.width);
228 debug_assert_eq!(src.height / 2, dst.height);
229
230 let src_w = src.width;
231 let dst_w = dst.width;
232 let dst_h = dst.height;
233
234 for y_out in 0..dst_h {
235 let y0 = 2 * y_out;
236 let y1 = y0 + 1;
237
238 let row0 = &src.data[y0 * src_w..(y0 + 1) * src_w];
239 let row1 = &src.data[y1 * src_w..(y1 + 1) * src_w];
240
241 let dst_row = &mut dst.data[y_out * dst_w..(y_out + 1) * dst_w];
242
243 downsample_row_simd(row0, row1, dst_row);
244 }
245}
246
247#[cfg(all(feature = "par_pyramid", feature = "rayon", not(feature = "simd")))]
248fn downsample_2x_box_parallel_scalar(src: ImageView<'_>, dst: &mut ImageBuffer) {
249 use rayon::prelude::*;
250
251 debug_assert_eq!(src.width / 2, dst.width);
252 debug_assert_eq!(src.height / 2, dst.height);
253
254 let src_w = src.width;
255 let dst_w = dst.width;
256
257 dst.data
258 .par_chunks_mut(dst_w)
259 .enumerate()
260 .for_each(|(y_out, dst_row)| {
261 let y0 = 2 * y_out;
262 let y1 = y0 + 1;
263
264 let row0 = &src.data[y0 * src_w..(y0 + 1) * src_w];
265 let row1 = &src.data[y1 * src_w..(y1 + 1) * src_w];
266
267 downsample_row_scalar(row0, row1, dst_row);
268 });
269}
270
271#[cfg(all(feature = "par_pyramid", feature = "rayon", feature = "simd"))]
272fn downsample_2x_box_parallel_simd(src: ImageView<'_>, dst: &mut ImageBuffer) {
273 use rayon::prelude::*;
274
275 debug_assert_eq!(src.width / 2, dst.width);
276 debug_assert_eq!(src.height / 2, dst.height);
277
278 let src_w = src.width;
279 let dst_w = dst.width;
280
281 dst.data
282 .par_chunks_mut(dst_w)
283 .enumerate()
284 .for_each(|(y_out, dst_row)| {
285 let y0 = 2 * y_out;
286 let y1 = y0 + 1;
287
288 let row0 = &src.data[y0 * src_w..(y0 + 1) * src_w];
289 let row1 = &src.data[y1 * src_w..(y1 + 1) * src_w];
290
291 downsample_row_simd(row0, row1, dst_row);
292 });
293}
294
295#[inline]
296fn downsample_row_scalar(row0: &[u8], row1: &[u8], dst_row: &mut [u8]) {
297 let dst_w = dst_row.len();
298
299 for (x, item) in dst_row.iter_mut().enumerate().take(dst_w) {
300 let sx = x * 2;
301 let p00 = row0[sx] as u16;
302 let p01 = row0[sx + 1] as u16;
303 let p10 = row1[sx] as u16;
304 let p11 = row1[sx + 1] as u16;
305 let sum = p00 + p01 + p10 + p11;
306 *item = ((sum + 2) >> 2) as u8;
307 }
308}
309
310#[cfg(all(feature = "par_pyramid", feature = "simd"))]
311fn downsample_row_simd(row0: &[u8], row1: &[u8], dst_row: &mut [u8]) {
312 use std::ops::Shr;
313 use std::simd::num::SimdUint;
314 use std::simd::{u16x16, u8x16};
315
316 const LANES: usize = 16;
317 let mut x_out = 0usize;
318
319 while x_out + LANES <= dst_row.len() {
320 let mut p00 = [0u8; LANES];
321 let mut p01 = [0u8; LANES];
322 let mut p10 = [0u8; LANES];
323 let mut p11 = [0u8; LANES];
324
325 for lane in 0..LANES {
326 let x = x_out + lane;
327 let sx = 2 * x;
328 p00[lane] = row0[sx];
329 p01[lane] = row0[sx + 1];
330 p10[lane] = row1[sx];
331 p11[lane] = row1[sx + 1];
332 }
333
334 let p00v = u8x16::from_array(p00).cast::<u16>();
335 let p01v = u8x16::from_array(p01).cast::<u16>();
336 let p10v = u8x16::from_array(p10).cast::<u16>();
337 let p11v = u8x16::from_array(p11).cast::<u16>();
338
339 let sum = p00v + p01v + p10v + p11v;
340 let avg = (sum + u16x16::splat(2)).shr(2);
341 let out = avg.cast::<u8>();
342
343 dst_row[x_out..x_out + LANES].copy_from_slice(out.as_array());
344 x_out += LANES;
345 }
346
347 if x_out < dst_row.len() {
349 downsample_row_scalar(row0, row1, &mut dst_row[x_out..]);
350 }
351}
352
353#[cfg(test)]
354mod tests {
355 use super::*;
356
357 fn reference_downsample(src: &ImageBuffer) -> ImageBuffer {
358 let mut dst = ImageBuffer::new(src.width / 2, src.height / 2);
359 downsample_2x_box_scalar(src.as_view(), &mut dst);
360 dst
361 }
362
363 fn gray_to_buffer(w: u32, h: u32, data: Vec<u8>) -> ImageBuffer {
364 ImageBuffer {
365 width: w as usize,
366 height: h as usize,
367 data,
368 }
369 }
370
371 fn make_checker(w: u32, h: u32, a: u8, b: u8) -> ImageBuffer {
372 let mut data = vec![0u8; (w * h) as usize];
373 for y in 0..h {
374 for x in 0..w {
375 data[(y * w + x) as usize] = if (x + y) % 2 == 0 { a } else { b };
376 }
377 }
378 gray_to_buffer(w, h, data)
379 }
380
381 #[test]
382 fn downsample_matches_reference() {
383 let mut src = ImageBuffer::new(8, 8);
384 for (i, p) in src.data.iter_mut().enumerate() {
385 *p = (i % 251) as u8;
386 }
387 let mut dst = ImageBuffer::new(4, 4);
388 downsample_2x_box(src.as_view(), &mut dst);
389 let expected = reference_downsample(&src);
390 assert_eq!(dst.data, expected.data);
391 }
392
393 #[test]
394 fn downsample_matches_reference_on_checker() {
395 let src = make_checker(16, 14, 0, 255);
396 let mut dst = ImageBuffer::new(src.width / 2, src.height / 2);
397 downsample_2x_box(src.as_view(), &mut dst);
398 let expected = reference_downsample(&src);
399 assert_eq!(dst.data, expected.data);
400 }
401
402 #[test]
403 fn build_pyramid_single_level() {
404 let img = ImageBuffer::new(64, 64);
405 let params = PyramidParams {
406 num_levels: 1,
407 min_size: 16,
408 };
409 let mut buffers = PyramidBuffers::new();
410 let pyramid = build_pyramid(img.as_view(), ¶ms, &mut buffers);
411 assert_eq!(pyramid.levels.len(), 1);
412 assert_eq!(pyramid.levels[0].scale, 1.0);
413 }
414
415 #[test]
416 fn build_pyramid_multiple_levels() {
417 let img = ImageBuffer::new(128, 128);
418 let params = PyramidParams {
419 num_levels: 4,
420 min_size: 16,
421 };
422 let mut buffers = PyramidBuffers::new();
423 let pyramid = build_pyramid(img.as_view(), ¶ms, &mut buffers);
424 assert_eq!(pyramid.levels.len(), 4);
425 assert_eq!(pyramid.levels[0].img.width, 128);
426 assert_eq!(pyramid.levels[1].img.width, 64);
427 assert_eq!(pyramid.levels[2].img.width, 32);
428 assert_eq!(pyramid.levels[3].img.width, 16);
429 assert!((pyramid.levels[3].scale - 0.125).abs() < 1e-6);
430 }
431
432 #[test]
433 fn build_pyramid_stops_at_min_size() {
434 let img = ImageBuffer::new(64, 64);
435 let params = PyramidParams {
436 num_levels: 10,
437 min_size: 32,
438 };
439 let mut buffers = PyramidBuffers::new();
440 let pyramid = build_pyramid(img.as_view(), ¶ms, &mut buffers);
441 assert_eq!(pyramid.levels.len(), 2); }
443}