use image::{GrayImage, ImageBuffer, Luma, Pixel};
use crate::float_image::FloatImageView;
#[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
use std::arch::wasm32::*;
pub fn build_pyramid(image: &GrayImage, levels: usize) -> Vec<GrayImage> {
let mut pyramid = Vec::new();
build_pyramid_into(image, levels, &mut pyramid);
pyramid
}
pub fn build_pyramid_into(image: &GrayImage, levels: usize, pyramid: &mut Vec<GrayImage>) {
ensure_level(pyramid, 0, image.width(), image.height());
pyramid[0].copy_from_slice(image.as_raw());
let mut produced = 1;
for level in 1..levels {
let (prev_w, prev_h) = pyramid[level - 1].dimensions();
if prev_w < 2 || prev_h < 2 {
break;
}
let (new_w, new_h) = (prev_w / 2, prev_h / 2);
ensure_level(pyramid, level, new_w, new_h);
let (head, tail) = pyramid.split_at_mut(level);
let previous_level = &head[level - 1];
let new_image = &mut tail[0];
downsample_2x2_into(
previous_level.as_raw(),
prev_w as usize,
new_w as usize,
new_h as usize,
new_image,
);
produced += 1;
}
pyramid.truncate(produced);
}
pub(crate) fn build_float_pyramid_into(
image: FloatImageView<'_>,
levels: usize,
pyramid: &mut Vec<ImageBuffer<Luma<f32>, Vec<f32>>>,
) {
let (width, height) = image.dimensions();
ensure_level(pyramid, 0, width, height);
pyramid[0].copy_from_slice(image.pixels());
let mut produced = 1;
for level in 1..levels {
let (prev_w, prev_h) = pyramid[level - 1].dimensions();
if prev_w < 2 || prev_h < 2 {
break;
}
let (new_w, new_h) = (prev_w / 2, prev_h / 2);
ensure_level(pyramid, level, new_w, new_h);
let (head, tail) = pyramid.split_at_mut(level);
downsample_2x2_f32(
head[level - 1].as_raw(),
prev_w as usize,
new_w as usize,
new_h as usize,
&mut tail[0],
);
produced += 1;
}
pyramid.truncate(produced);
}
fn downsample_2x2_f32(src: &[f32], prev_w: usize, new_w: usize, new_h: usize, dst: &mut [f32]) {
for (y, out) in dst.chunks_exact_mut(new_w).take(new_h).enumerate() {
let (top, _) = src[2 * y * prev_w..][..2 * new_w].as_chunks::<2>();
let (bottom, _) = src[(2 * y + 1) * prev_w..][..2 * new_w].as_chunks::<2>();
for (value, (top, bottom)) in out.iter_mut().zip(top.iter().zip(bottom)) {
*value = ((top[0] + top[1]) + (bottom[0] + bottom[1])) * 0.25;
}
}
}
fn downsample_2x2_into(src: &[u8], prev_w: usize, new_w: usize, new_h: usize, dst: &mut [u8]) {
#[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
{
unsafe { downsample_2x2_simd128(src, prev_w, new_w, new_h, dst) }
}
#[cfg(not(all(target_arch = "wasm32", target_feature = "simd128")))]
{
downsample_2x2_scalar(src, prev_w, new_w, new_h, dst);
}
}
#[cfg(not(all(target_arch = "wasm32", target_feature = "simd128")))]
fn downsample_2x2_scalar(src: &[u8], prev_w: usize, new_w: usize, new_h: usize, dst: &mut [u8]) {
for y in 0..new_h {
let r0 = (2 * y) * prev_w;
let r1 = r0 + prev_w;
let out_row = y * new_w;
for x in 0..new_w {
let px = 2 * x;
unsafe {
let s = *src.get_unchecked(r0 + px) as u32
+ *src.get_unchecked(r0 + px + 1) as u32
+ *src.get_unchecked(r1 + px) as u32
+ *src.get_unchecked(r1 + px + 1) as u32;
*dst.get_unchecked_mut(out_row + x) = (s / 4) as u8;
}
}
}
}
#[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
#[target_feature(enable = "simd128")]
unsafe fn downsample_2x2_simd128(
src: &[u8],
prev_w: usize,
new_w: usize,
new_h: usize,
dst: &mut [u8],
) {
let chunks = new_w / 16;
for y in 0..new_h {
let r0 = (2 * y) * prev_w;
let r1 = r0 + prev_w;
let out_row = y * new_w;
for c in 0..chunks {
let out_x = c * 16;
let in_x = out_x * 2; let (s0a, s0b, s1a, s1b) = unsafe {
(
v128_load(src.as_ptr().add(r0 + in_x) as *const v128),
v128_load(src.as_ptr().add(r0 + in_x + 16) as *const v128),
v128_load(src.as_ptr().add(r1 + in_x) as *const v128),
v128_load(src.as_ptr().add(r1 + in_x + 16) as *const v128),
)
};
let lo = u16x8_add(
u16x8_extadd_pairwise_u8x16(s0a),
u16x8_extadd_pairwise_u8x16(s1a),
);
let hi = u16x8_add(
u16x8_extadd_pairwise_u8x16(s0b),
u16x8_extadd_pairwise_u8x16(s1b),
);
let packed = u8x16_narrow_i16x8(u16x8_shr(lo, 2), u16x8_shr(hi, 2));
unsafe {
v128_store(dst.as_mut_ptr().add(out_row + out_x) as *mut v128, packed);
}
}
for x in (chunks * 16)..new_w {
let px = 2 * x;
unsafe {
let s = *src.get_unchecked(r0 + px) as u32
+ *src.get_unchecked(r0 + px + 1) as u32
+ *src.get_unchecked(r1 + px) as u32
+ *src.get_unchecked(r1 + px + 1) as u32;
*dst.get_unchecked_mut(out_row + x) = (s / 4) as u8;
}
}
}
}
fn ensure_level<P>(
pyramid: &mut Vec<ImageBuffer<P, Vec<P::Subpixel>>>,
index: usize,
width: u32,
height: u32,
) where
P: Pixel,
P::Subpixel: Default,
{
if index == pyramid.len() {
pyramid.push(ImageBuffer::new(width, height));
} else if pyramid[index].dimensions() != (width, height) {
let mut pixels = std::mem::take(&mut pyramid[index]).into_raw();
pixels.resize(width as usize * height as usize, Default::default());
pyramid[index] = ImageBuffer::from_raw(width, height, pixels)
.expect("level buffer has exactly width * height pixels");
}
}
pub(crate) fn level_dimensions(
width: u32,
height: u32,
levels: usize,
) -> impl Iterator<Item = (u32, u32)> {
std::iter::successors(Some((width, height)), |&(w, h)| {
(w >= 2 && h >= 2).then_some((w / 2, h / 2))
})
.take(levels.max(1))
}
pub(crate) fn pyramid_level_count(width: u32, height: u32, levels: usize) -> usize {
level_dimensions(width, height, levels).count()
}
pub(crate) fn reserve_pyramid<P>(
pyramid: &mut Vec<ImageBuffer<P, Vec<P::Subpixel>>>,
width: u32,
height: u32,
levels: usize,
) where
P: Pixel,
P::Subpixel: Default,
{
for (index, (w, h)) in level_dimensions(width, height, levels).enumerate() {
ensure_level(pyramid, index, w, h);
}
}
#[cfg(test)]
mod tests {
use super::*;
use image::Luma;
fn make_image(width: u32, height: u32) -> GrayImage {
let mut img = GrayImage::new(width, height);
for y in 0..height {
for x in 0..width {
let v = ((x * 37 + y * 19 + (x ^ y) * 7) & 0xff) as u8;
img.put_pixel(x, y, Luma([v]));
}
}
img
}
fn reference_half(prev: &GrayImage) -> GrayImage {
let (pw, ph) = prev.dimensions();
let (nw, nh) = (pw / 2, ph / 2);
let mut out = GrayImage::new(nw, nh);
for y in 0..nh {
for x in 0..nw {
let (px, py) = (2 * x, 2 * y);
let s = prev.get_pixel(px, py)[0] as u32
+ prev.get_pixel(px + 1, py)[0] as u32
+ prev.get_pixel(px, py + 1)[0] as u32
+ prev.get_pixel(px + 1, py + 1)[0] as u32;
out.put_pixel(x, y, Luma([(s / 4) as u8]));
}
}
out
}
#[test]
fn float_pyramid_holds_exact_means_with_the_8_bit_level_sizes() {
for (w, h) in [(64u32, 48u32), (65, 49), (37, 71), (3, 3), (1, 9)] {
let img = make_image(w, h);
let pixels: Vec<f32> = img.as_raw().iter().map(|&v| v as f32).collect();
let view = FloatImageView::new(&pixels, w, h).unwrap();
let mut pyramid = Vec::new();
build_float_pyramid_into(view, 5, &mut pyramid);
let sizes: Vec<_> = pyramid.iter().map(|level| level.dimensions()).collect();
let expected: Vec<_> = build_pyramid(&img, 5)
.iter()
.map(|level| level.dimensions())
.collect();
assert_eq!(sizes, expected);
assert_eq!(pyramid[0].as_raw(), &pixels);
for pair in pyramid.windows(2) {
let (fine, coarse) = (&pair[0], &pair[1]);
for (x, y, value) in coarse.enumerate_pixels() {
let p = |dx, dy| fine.get_pixel(2 * x + dx, 2 * y + dy)[0] as f64;
let mean = (p(0, 0) + p(1, 0) + p(0, 1) + p(1, 1)) / 4.0;
assert!((value[0] as f64 - mean).abs() <= mean.abs() * 1e-6);
}
}
}
}
#[test]
fn pyramid_matches_naive_reference() {
for (w, h) in [(64u32, 48u32), (65, 49), (37, 71), (16, 16), (3, 3)] {
let img = make_image(w, h);
let pyr = build_pyramid(&img, 4);
assert_eq!(pyr[0], img);
let mut expected = img.clone();
for level in pyr.iter().skip(1) {
expected = reference_half(&expected);
assert_eq!(level, &expected, "level mismatch at {w}x{h}");
}
}
}
}