- Commit
- c8c8976a20630694e2fd2f88904e7bec022b6523
- Parent
- 4193a04e494e6fb5203a986666667d767b683839
- Author
- Pablo <pablo-pie@riseup.net>
- Date
Improved dowmsampling of thumbnails
We now use linear interpolation instead of nearest-neighbord
Custum build of stapix for tikz.pablopie.xyz
Improved dowmsampling of thumbnails
We now use linear interpolation instead of nearest-neighbord
2 files changed, 310 insertions, 190 deletions
| Status | Name | Changes | Insertions | Deletions |
| Modified | .gitignore | 2 files changed | 1 | 0 |
| Modified | src/image.rs | 2 files changed | 309 | 190 |
diff --git a/.gitignore b/.gitignore @@ -1,4 +1,5 @@ /target /examples/site .project.gf +profile/ tags
diff --git a/src/image.rs b/src/image.rs @@ -14,6 +14,11 @@ //! feed the YUV pixel data directly to `libwebp` instead of first converting //! it to and then converting it back to YUV (which is what `image` would //! have done). +//! +//! ## Possible improvements +//! +//! * SIMD optimizations (doesn't seem necessary for now) +//! * use the GPU for downsampling (doesn't seem worth the coplexity/overhead) #![allow(clippy::identity_op)] #![allow(clippy::needless_range_loop)] @@ -59,7 +64,6 @@ enum PixelBuffer { /// Parses the Y Cb Cr data from a JPEG, downsampling to fit `TARGET_HEIGHT` /// along the way -// NOTE: using an approximation of nearest neighbor for efficiency pub fn parse_jpeg(path: &Path, target_height: usize) -> Result<Image, ()> { let options = DecoderOptions::default() .jpeg_set_out_colorspace(ColorSpace::YCbCr); @@ -81,9 +85,10 @@ pub fn parse_jpeg(path: &Path, target_height: usize) -> Result{ let src_width = info.width as usize; let src_height = info.height as usize; + let target_width = src_width * target_height / src_height; let (width, height) = if src_height > target_height { - (src_width * target_height / src_height, target_height) + (target_width, target_height) } else { (src_width, src_height) }; @@ -92,49 +97,32 @@ pub fn parse_jpeg(path: &Path, target_height: usize) -> Result{ let uv_height = height.div_ceil(2); assert_expected_pixels_len(src_width, src_height, 3, pixels.len()); - let y_len = width * height; - let uv_len = uv_width * uv_height; let colorspace = options.jpeg_get_out_colorspace(); debug_assert!(colorspace == ColorSpace::YCbCr, "unexpected colorspace when parsing JPEG: {colorspace:?}"); // ========================================================================== - let mut y = vec![MaybeUninit::uninit(); y_len]; - let mut cb = vec![MaybeUninit::uninit(); uv_len]; - let mut cr = vec![MaybeUninit::uninit(); uv_len]; - - if height == target_height { - // TODO: [optmize]: can we use SIMD here? - for dst_i in 0..y_len { - let dst_x = dst_i % width; - let dst_y = dst_i / width; - - let src_x = dst_x * src_width / width; - let src_y = dst_y * src_height / height; - - let src_i = src_y * src_width + src_x; - y[dst_i] = MaybeUninit::new(pixels[src_i*3]); - } - } else { - for i in 0..y_len { y[i] = MaybeUninit::new(pixels[i*3]); } - } - - // TODO: [optmize]: can we use SIMD here? - for dst_i in 0..uv_len { - let dst_x = dst_i % uv_width; - let dst_y = dst_i / uv_width; - - let src_x = dst_x * src_width / uv_width; - let src_y = dst_y * src_height / uv_height; + let [y] = unsafe { + load_and_downsample_channels_linear_checked::<1>( + &pixels, + src_width, src_height, + 3, + target_width, target_height, + ) + }; - let src_i = src_y * src_width + src_x; - cb[dst_i] = MaybeUninit::new(pixels[src_i*3+1]); - cr[dst_i] = MaybeUninit::new(pixels[src_i*3+2]); - } + let [cb, cr] = unsafe { + load_and_downsample_channels_linear::<2>( + &pixels, + src_width, src_height, + 3, 1, + uv_width, uv_height, + ) + }; // ========================================================================== - let y = unsafe { mem::transmute::<Vec<MaybeUninit<u8>>, Vec<u8>>(y) }; + let y = unsafe { mem::transmute::<Vec<MaybeUninit<u8>>, Vec<u8>>(y) }; let cb = unsafe { mem::transmute::<Vec<MaybeUninit<u8>>, Vec<u8>>(cb) }; let cr = unsafe { mem::transmute::<Vec<MaybeUninit<u8>>, Vec<u8>>(cr) }; @@ -181,9 +169,10 @@ pub fn parse_png(path: &Path, target_height: usize) -> Result{ let (src_width, src_height) = decoder .dimensions() .expect("should already have decoded PNG headers"); + let target_width = src_width * target_height / src_height; - let (width, height) = if src_height > target_height { - (src_width * target_height / src_height, target_height) + let (width, height) = if src_height >= target_height { + (target_width, target_height) } else { (src_width, src_height) }; @@ -213,178 +202,81 @@ pub fn parse_png(path: &Path, target_height: usize) -> Result{ assert_expected_pixels_len(src_width, src_height, bps, pixels.len()); // ========================================================================== - // handle grayscale input - - struct GrayscaleData { - y: Vec<u8>, - a: Option<Vec<u8>>, - uv_stride: usize, - uv_len: usize, - } - - #[inline] - #[allow(clippy::too_many_arguments)] - fn parse_ya_channels( - width: usize, height: usize, - src_width: usize, src_height: usize, - pixels: Vec<u8>, - target_height: usize, - bps: usize, has_alpha: bool, - ) -> GrayscaleData { - let uv_width = width.div_ceil(2); - let uv_height = height.div_ceil(2); - let y_len = width * height; - let uv_len = uv_width * uv_height; - - if !has_alpha && height < target_height { - return GrayscaleData { - y: pixels, - a: None, - uv_stride: uv_width, - uv_len, - }; - } - - if has_alpha && height < target_height { - let mut y = vec![MaybeUninit::uninit(); y_len]; - let mut a = vec![MaybeUninit::uninit(); y_len]; + // handle RGB/RGBA input - // TODO: [optimize]: can we optmize this? - for i in 0..y_len { - y[i] = MaybeUninit::new(pixels[i*2+0]); - a[i] = MaybeUninit::new(pixels[i*2+1]); + if !is_grayscale { + let bgra_data = if src_height < target_height { + unsafe { load_bgra_from_rgba(&pixels, src_width, src_height, has_alpha) } + } else { + unsafe { + load_and_downsample_bgra_from_rgba( + &pixels, + src_width, src_height, + has_alpha, + target_width, target_height, + ) } + }; - let y = unsafe { mem::transmute::<Vec<MaybeUninit<u8>>, Vec<u8>>(y) }; - let a = unsafe { mem::transmute::<Vec<MaybeUninit<u8>>, Vec<u8>>(a) }; - return GrayscaleData { y, a: Some(a), uv_stride: uv_width, uv_len, }; - } - - let mut y = vec![MaybeUninit::uninit(); y_len]; - for dst_i in 0..y_len { - let dst_x = dst_i % width; - let dst_y = dst_i / width; - - let src_x = dst_x * src_width / width; - let src_y = dst_y * src_height / height; + return Ok(Image { width, height, pixels: PixelBuffer::BGRA(bgra_data), }); + } - let src_i = src_y * src_width + src_x; - y[dst_i] = MaybeUninit::new(pixels[src_i*bps]); - } - let y = unsafe { mem::transmute::<Vec<MaybeUninit<u8>>, Vec<u8>>(y) }; + // ========================================================================== + // handle grayscale input - if !has_alpha { - return GrayscaleData { y, a: None, uv_stride: uv_width, uv_len, }; - } + let uv_width = width.div_ceil(2); + let uv_height = height.div_ceil(2); + let uv_stride = uv_width; + let uv_len = uv_width * uv_height; - let mut a = vec![MaybeUninit::uninit(); y_len]; - for dst_i in 0..y_len { - let dst_x = dst_i % width; - let dst_y = dst_i / width; + let cb = vec![128; uv_len]; + let cr = vec![128; uv_len]; - let src_x = dst_x * src_width / width; - let src_y = dst_y * src_height / height; + if !has_alpha { + if src_height < target_height { + return Ok(Image { + width, + height, - let src_i = src_y * src_width + src_x; - a[dst_i] = MaybeUninit::new(pixels[src_i*2+1]); + pixels: PixelBuffer::YCbCr { y: pixels, cb, cr, a: None, uv_stride, } + }); } - let a = unsafe { mem::transmute::<Vec<MaybeUninit<u8>>, Vec<u8>>(a) }; - - GrayscaleData { y, a: Some(a), uv_stride: uv_width, uv_len, } - } - if is_grayscale { - let GrayscaleData { y, a, uv_stride, uv_len, } = parse_ya_channels( - width, height, - src_width, src_height, - pixels, - target_height, - bps, has_alpha, - ); + let [y] = unsafe { + load_and_downsample_channels_linear::<1>( + &pixels, + src_width, src_height, + 1, 0, + target_width, target_height, + ) + }; + let y = unsafe { mem::transmute::<Vec<MaybeUninit<u8>>, Vec<u8>>(y) }; return Ok(Image { width, height, - pixels: PixelBuffer::YCbCr { - y, a, cb: vec![128; uv_len], cr: vec![128; uv_len], - uv_stride, - }, + pixels: PixelBuffer::YCbCr {y, a: None, cb, cr, uv_stride, } }); } - // ========================================================================== - // handle RGB/RGBA input - - #[inline] - #[allow(clippy::too_many_arguments)] - fn parse_bgra_channels( - width: usize, height: usize, - src_width: usize, src_height: usize, - pixels: &[u8], - target_height: usize, - bps: usize, has_alpha: bool, - ) -> Vec<MaybeUninit<u8>> { - let len = width * height; - let mut bgra_data = vec![MaybeUninit::uninit(); len * bps]; - - if height < target_height { - for i in 0..len { - bgra_data[i*bps+0] = MaybeUninit::new(pixels[i*bps+2]); - bgra_data[i*bps+1] = MaybeUninit::new(pixels[i*bps+1]); - bgra_data[i*bps+2] = MaybeUninit::new(pixels[i*bps+0]); - } - - if has_alpha { - for i in 0..len { - bgra_data[i*4+3] = MaybeUninit::new(pixels[i*4+3]); - } - } - - return bgra_data; - } - - for dst_i in 0..len { - let dst_x = dst_i % width; - let dst_y = dst_i / width; - - let src_x = dst_x * src_width / width; - let src_y = dst_y * src_height / height; - - let src_i = src_y * src_width + src_x; - bgra_data[dst_i*bps+0] = MaybeUninit::new(pixels[src_i*bps+2]); - bgra_data[dst_i*bps+1] = MaybeUninit::new(pixels[src_i*bps+1]); - bgra_data[dst_i*bps+2] = MaybeUninit::new(pixels[src_i*bps+0]); - } - - if has_alpha { - for dst_i in 0..len { - let dst_x = dst_i % width; - let dst_y = dst_i / width; - - let src_x = dst_x * src_width / width; - let src_y = dst_y * src_height / height; - - let src_i = src_y * src_width + src_x; - bgra_data[dst_i*4+3] = MaybeUninit::new(pixels[src_i*4+3]); - } - } - - bgra_data - } - - let bgra_data = parse_bgra_channels( - width, height, - src_width, src_height, - &pixels, - target_height, - bps, has_alpha, - ); - let bgra_data = unsafe { - mem::transmute::<Vec<MaybeUninit<u8>>, Vec<u8>>(bgra_data) + let [y, a] = unsafe { + load_and_downsample_channels_linear_checked::<2>( + &pixels, + src_width, src_height, + 2, + target_width, target_height, + ) }; + let y = unsafe { mem::transmute::<Vec<MaybeUninit<u8>>, Vec<u8>>(y) }; + let a = unsafe { mem::transmute::<Vec<MaybeUninit<u8>>, Vec<u8>>(a) }; + + Ok(Image { + width, + height, - Ok(Image { width, height, pixels: PixelBuffer::BGRA(bgra_data), }) + pixels: PixelBuffer::YCbCr { y, a: Some(a), cb, cr, uv_stride, }, + }) } pub fn encode_webp(img: &Image, output_path: &Path) -> Result<(), ()> { @@ -477,6 +369,233 @@ pub fn encode_webp(img: &Image, output_path: &Path) -> Result<(), ()> { } #[inline] +unsafe fn load_and_downsample_channels_linear<const CHANNELS: usize>( + pixels: &[u8], + src_width: usize, src_height: usize, + bps: usize, first_channel_offset: usize, + target_width: usize, target_height: usize, +) -> [Vec<MaybeUninit<u8>>; CHANNELS] { + debug_assert!(CHANNELS > 0); + + let mut result: [Vec<MaybeUninit<u8>>; CHANNELS] = { + [const { Vec::new() }; CHANNELS] + }; + + for j in 0..CHANNELS { + result[j].resize(target_width * target_height, MaybeUninit::uninit()); + } + + let mut dst_i = 0; + for dst_y in 0..target_height { + let bottom = ( dst_y * src_height).div_ceil(target_height); + let top = ((dst_y + 1) * src_height).div_ceil(target_height); + + for dst_x in 0..target_width { + let left = ( dst_x * src_width).div_ceil(target_width); + let right = ((dst_x + 1) * src_width).div_ceil(target_width); + + // TODO: handle this + assert!(top != bottom && left != right); + let area: u16 = ((right - left)*(top - bottom)) as u16; + + let mut acc: [u16; CHANNELS] = [0; CHANNELS]; + let mut i = bottom * src_width + left; + for _ in bottom..top { + for _ in left..right { + for j in 0..CHANNELS { + acc[j] += pixels[i*bps+first_channel_offset+j] as u16; + } + i += 1; + } + i += src_width + left; + i -= right; + } + + for j in 0..CHANNELS { + result[j][dst_i] = MaybeUninit::new((acc[j]/area) as u8); + } + + dst_i += 1; + } + } + + result +} + +#[inline] +unsafe fn load_channels<const CHANNELS: usize>( + pixels: &[u8], + src_width: usize, src_height: usize, + bps: usize, +) -> [Vec<MaybeUninit<u8>>; CHANNELS] { + debug_assert!(CHANNELS > 0); + + let mut result = [const { Vec::new() }; CHANNELS]; + + for j in 0..CHANNELS { + result[j].resize(src_width * src_height, MaybeUninit::uninit()); + } + + for j in 0..CHANNELS { + for i in 0..src_width*src_height { + result[j][i] = MaybeUninit::new(pixels[i*bps+j]); + } + } + + result +} + +#[inline] +unsafe fn load_and_downsample_channels_linear_checked<const CHANNELS: usize>( + pixels: &[u8], + src_width: usize, src_height: usize, + bps: usize, + target_width: usize, target_height: usize, +) -> [Vec<MaybeUninit<u8>>; CHANNELS] { + if src_height < target_height { + load_channels::<CHANNELS>(pixels, src_width, src_height, bps) + } else { + load_and_downsample_channels_linear::<CHANNELS>( + pixels, + src_width, src_height, + bps, 0, + target_width, target_height, + ) + } +} + +#[inline] +unsafe fn load_and_downsample_bgra_from_rgba( + pixels: &[u8], + src_width: usize, src_height: usize, + has_alpha: bool, + target_width: usize, target_height: usize, +) -> Vec<u8> { + if has_alpha { + return load_and_downsample_bgra_from_rgba_internal::<true>( + pixels, + src_width, src_height, + target_width, target_height, + ); + } else { + return load_and_downsample_bgra_from_rgba_internal::<false>( + pixels, + src_width, src_height, + target_width, target_height, + ); + } + + #[inline] + unsafe fn load_and_downsample_bgra_from_rgba_internal<const HAS_ALPHA: bool>( + pixels: &[u8], + src_width: usize, src_height: usize, + target_width: usize, target_height: usize, + ) -> Vec<u8> { + let bps = if HAS_ALPHA { 4 } else { 3 }; + + let len = target_width * target_height; + let mut result = vec![MaybeUninit::uninit(); len * 4]; + + let mut dst_i = 0; + for dst_y in 0..target_height { + let bottom = ( dst_y * src_height).div_ceil(target_height); + let top = ((dst_y + 1) * src_height).div_ceil(target_height); + + for dst_x in 0..target_width { + let left = ( dst_x * src_width).div_ceil(target_width); + let right = ((dst_x + 1) * src_width).div_ceil(target_width); + + // TODO: handle this + assert!(top != bottom && left != right); + + let area: u16 = ((right - left)*(top - bottom)) as u16; + + #[allow(unused_variables)] + let mut acc_a: u16 = 0; + let mut acc_r: u16 = 0; + let mut acc_g: u16 = 0; + let mut acc_b: u16 = 0; + + let mut i = bottom * src_width + left; + for _ in bottom..top { + for _ in left..right { + acc_r += pixels[i*bps+0] as u16; + acc_g += pixels[i*bps+1] as u16; + acc_b += pixels[i*bps+2] as u16; + // NOTE: compile time branches should never branch at runtime + #[allow(unused_assignments)] + if HAS_ALPHA { acc_a += pixels[i*bps+3] as u16; } + i += 1; + } + i += src_width + left; + i -= right; + } + + result[dst_i*4+0] = MaybeUninit::new((acc_b/area) as u8); + result[dst_i*4+1] = MaybeUninit::new((acc_g/area) as u8); + result[dst_i*4+2] = MaybeUninit::new((acc_r/area) as u8); + + // NOTE: compile time branches should never branch at runtime + if HAS_ALPHA { + result[dst_i*4+3] = MaybeUninit::new((acc_a/area) as u8); + } else { + result[dst_i*4+3] = MaybeUninit::new(0xff); + } + + dst_i += 1; + } + } + + mem::transmute::<Vec<MaybeUninit<u8>>, Vec<u8>>(result) + } +} + +#[inline] +unsafe fn load_bgra_from_rgba( + pixels: &[u8], + src_width: usize, src_height: usize, + has_alpha: bool, +) -> Vec<u8> { + if has_alpha { + return load_bgra_from_rgba_internal::<true>( + pixels, src_width, src_height, + ); + } else { + return load_bgra_from_rgba_internal::<false>( + pixels, src_width, src_height, + ); + } + + #[inline] + unsafe fn load_bgra_from_rgba_internal<const HAS_ALPHA: bool>( + pixels: &[u8], + src_width: usize, src_height: usize, + ) -> Vec<u8> { + let bps = if HAS_ALPHA { 4 } else { 3 }; + + let len = src_width * src_height; + let mut result = vec![MaybeUninit::uninit(); len * 4]; + + for i in 0..len { + let r = pixels[i*bps+0]; + let g = pixels[i*bps+1]; + let b = pixels[i*bps+2]; + + // NOTE: compile time branches should not actually branch at runtime + let mut a = 0xff; + if HAS_ALPHA { a = pixels[i*bps+3]; } + + result[i*4+0] = MaybeUninit::new(b); + result[i*4+1] = MaybeUninit::new(g); + result[i*4+2] = MaybeUninit::new(r); + result[i*4+3] = MaybeUninit::new(a); + } + + mem::transmute::<Vec<MaybeUninit<u8>>, Vec<u8>>(result) + } +} + +#[inline] fn assert_expected_pixels_len( width: usize, height: usize,