From 2c8806642665a4f6a36e9379f7b8989bcc5c1fa4 Mon Sep 17 00:00:00 2001 From: bendn Date: Sat, 28 Oct 2023 15:18:27 +0700 Subject: [PATCH] more image scaling filters --- Cargo.toml | 13 +++- README.md | 2 +- benches/scaling.rs | 21 ++++++ src/lib.rs | 3 +- src/scale.rs | 42 ------------ src/scale/algorithms.rs | 140 ++++++++++++++++++++++++++++++++++++++++ src/scale/mod.rs | 79 +++++++++++++++++++++++ src/scale/traits.rs | 91 ++++++++++++++++++++++++++ 8 files changed, 343 insertions(+), 48 deletions(-) create mode 100644 benches/scaling.rs delete mode 100644 src/scale.rs create mode 100644 src/scale/algorithms.rs create mode 100644 src/scale/mod.rs create mode 100644 src/scale/traits.rs diff --git a/Cargo.toml b/Cargo.toml index add8cb9..dfa3cdf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fimg" -version = "0.4.19" +version = "0.4.20" authors = ["bend-n "] license = "MIT" edition = "2021" @@ -14,9 +14,10 @@ png = { version = "0.17", features = ["unstable"], optional = true } fontdue = { version = "0.7.3", optional = true } vecto = "0.1.0" umath = "0.0.7" +fr = { version = "0.1.1", package = "fer", optional = true } [dev-dependencies] -iai = { path = "../iai" } +iai = { git = "https://github.com/bend-n/iai.git" } [[bench]] name = "overlays" @@ -33,15 +34,21 @@ name = "affine_transformations" path = "benches/affine_transformations.rs" harness = false +[[bench]] +name = "scaling" +path = "benches/scaling.rs" +harness = false + [[bench]] name = "tile" path = "benches/tile.rs" harness = false [features] +scale = ["fr"] save = ["png"] text = ["fontdue"] -default = ["save"] +default = ["save", "scale"] [profile.release] debug = 2 diff --git a/README.md b/README.md index bf212c0..bc312b3 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ quick simple image operations - [x] rotation - [x] flipping - [x] image tiling -- [x] nearest neighbor scaling +- [x] image scaling - [x] triangle drawing - [x] simple line drawing - [x] box drawing diff --git a/benches/scaling.rs b/benches/scaling.rs new file mode 100644 index 0000000..389998f --- /dev/null +++ b/benches/scaling.rs @@ -0,0 +1,21 @@ +use fimg::{scale::*, Image}; + +macro_rules! bench { + ($([$a: ident, $alg:ident]),+ $(,)?) => { + $(fn $a() { + let img: Image<_, 3> = Image::open("tdata/cat.png"); + iai::black_box(img.scale::<$alg>(267, 178)); + })+ + + iai::main!($($a,)+); + }; +} +bench![ + [nearest, Nearest], + [bilinear, Bilinear], + [boxs, Box], + [lanczos3, Lanczos3], + [catmull, CatmullRom], + [mitchell, Mitchell], + [hamming, Hamming], +]; diff --git a/src/lib.rs b/src/lib.rs index fac8791..4489c17 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,8 +14,6 @@ array_chunks )] #![warn( - clippy::missing_docs_in_private_items, - clippy::multiple_unsafe_ops_per_block, clippy::undocumented_unsafe_blocks, clippy::missing_const_for_fn, clippy::missing_safety_doc, @@ -34,6 +32,7 @@ pub mod cloner; mod drawing; pub(crate) mod math; mod overlay; +#[cfg(feature = "scale")] pub mod scale; use cloner::ImageCloner; pub use overlay::{ClonerOverlay, ClonerOverlayAt, Overlay, OverlayAt}; diff --git a/src/scale.rs b/src/scale.rs deleted file mode 100644 index 36165f7..0000000 --- a/src/scale.rs +++ /dev/null @@ -1,42 +0,0 @@ -//! holds scaling operations, at current only the Nearest Neighbor -use crate::Image; - -/// [Nearest Neighbor](https://en.wikipedia.org/wiki/Nearest-neighbor_interpolation) image scaling algorithm implementation. -/// Use [`Nearest::scale`]. -pub struct Nearest; -impl Nearest { - /// Resize a image. - /// # Safety - /// - /// `image` must be as big or bigger than `width`, `height. - #[must_use = "function does not modify the original image"] - pub unsafe fn scale( - image: Image<&[u8], N>, - width: u32, - height: u32, - ) -> Image, N> { - let x_scale = image.width() as f32 / width as f32; - let y_scale = image.height() as f32 / height as f32; - let mut out = Image::alloc(width, height); - for y in 0..height { - for x in 0..width { - let x1 = ((x as f32 + 0.5) * x_scale).floor() as u32; - let y1 = ((y as f32 + 0.5) * y_scale).floor() as u32; - // SAFETY: i asked the caller to make sure its ok - let px = unsafe { image.pixel(x1, y1) }; - // SAFETY: were looping over the width and height of out. its ok. - unsafe { out.set_pixel(x, y, px) }; - } - } - out - } -} - -#[test] -fn test_nearest() { - let i = Image::<_, 3>::open("tdata/cat.png"); - assert_eq!( - unsafe { Nearest::scale(i.as_ref(), 268, 178) }.buffer, - Image::<_, 3>::open("tdata/small_cat.png").buffer - ); -} diff --git a/src/scale/algorithms.rs b/src/scale/algorithms.rs new file mode 100644 index 0000000..d1dd6cf --- /dev/null +++ b/src/scale/algorithms.rs @@ -0,0 +1,140 @@ +use super::{traits::*, *}; +use std::num::NonZeroU32; + +/// [Nearest Neighbor](https://en.wikipedia.org/wiki/Nearest-neighbor_interpolation) image scaling algorithm. +pub struct Nearest; + +impl ScalingAlgorithm for Nearest { + /// Can be used on non opaque too! (Nearest is special like that). + fn scale_opaque( + i: Image<&[u8], N>, + w: NonZeroU32, + h: NonZeroU32, + ) -> Image, N> + where + ChannelCount: ToImageView, + { + let mut dst = fr::Image::new(w, h); + // SAFETY: swear, the pixel types are the same + unsafe { + fr::Resizer::new(fr::ResizeAlg::Nearest) + .resize(&ChannelCount::::wrap(i), &mut dst.view_mut()) + }; + + // SAFETY: ctor + unsafe { Image::new(dst.width(), dst.height(), dst.into_vec().into()) } + } + + #[inline] + fn scale_transparent( + i: Image<&mut [u8], N>, + w: NonZeroU32, + h: NonZeroU32, + ) -> Image, N> + where + ChannelCount: AlphaDiv, + { + Self::scale_opaque(i.as_ref(), w, h) + } +} + +macro_rules! alg { + ($for:ident) => { + impl ScalingAlgorithm for $for { + fn scale_opaque( + i: Image<&[u8], N>, + w: NonZeroU32, + h: NonZeroU32, + ) -> Image, N> + where + ChannelCount: ToImageView, + { + let mut dst = fr::Image::new(w, h); + // SAFETY: swear, the pixel types are the same + unsafe { + fr::Resizer::new(fr::ResizeAlg::Convolution(fr::FilterType::$for)) + .resize(&ChannelCount::::wrap(i), &mut dst.view_mut()) + }; + + // SAFETY: ctor + unsafe { Image::new(dst.width(), dst.height(), dst.into_vec().into()) } + } + + fn scale_transparent( + i: Image<&mut [u8], N>, + w: NonZeroU32, + h: NonZeroU32, + ) -> Image, N> + where + ChannelCount: AlphaDiv, + { + let mut dst = fr::Image::new(w, h); + // SAFETY: yes + unsafe { + fr::Resizer::new(fr::ResizeAlg::Convolution(fr::FilterType::$for)) + .resize(&ChannelCount::::handle(i).view(), &mut dst.view_mut()) + } + + // SAFETY: ctor + unsafe { Image::new(dst.width(), dst.height(), dst.into_vec().into()) } + } + } + }; +} + +/// [Lanczos](https://en.wikipedia.org/wiki/Lanczos_resampling) scaling with a filter size (*a*) of 3. +pub struct Lanczos3 {} +alg!(Lanczos3); + +/// [Catmull-Rom](https://en.wikipedia.org/wiki/Centripetal_Catmull%E2%80%93Rom_spline) bicubic filtering. +pub struct CatmullRom {} +alg!(CatmullRom); + +/// Linear interpolation. +pub struct Bilinear {} +alg!(Bilinear); + +/// The opposite of [`Nearest`]. +pub struct Box {} +alg!(Box); + +/// Hamming filtering has the same performance as a [`Bilinear`] filter, while +/// providing image (downscaling) quality comparable to bicubic filters like +/// [`CatmullRom`] or [`Mitchell`]. Creates a sharper image than [`Bilinear`] filtering, +/// and doesn't have dislocations on local level like [`Box`] suffers from. +/// Not recommended for upscaling. +pub struct Hamming {} +alg!(Hamming); + +/// [Mitchell–Netravali](https://en.wikipedia.org/wiki/Mitchell%E2%80%93Netravali_filters) bicubic filtering. +pub struct Mitchell {} +alg!(Mitchell); + +impl Nearest { + /// Resize a image. + /// # Safety + /// + /// `image` must be as big or bigger than `width`, `height. + #[must_use = "function does not modify the original image"] + #[deprecated = "use Image::scale instead (note that Image::scale does not support any N. if there is a N you would like to see supported, please open a issue)"] + pub unsafe fn scale( + image: Image<&[u8], N>, + width: u32, + height: u32, + ) -> Image, N> { + let x_scale = image.width() as f32 / width as f32; + let y_scale = image.height() as f32 / height as f32; + let mut out = Image::alloc(width, height); + for y in 0..height { + for x in 0..width { + let x1 = ((x as f32 + 0.5) * x_scale).floor() as u32; + let y1 = ((y as f32 + 0.5) * y_scale).floor() as u32; + // SAFETY: i asked the caller to make sure its ok + let px = unsafe { image.pixel(x1, y1) }; + // SAFETY: were looping over the width and height of out. its ok. + unsafe { out.set_pixel(x, y, px) }; + } + } + out + } +} diff --git a/src/scale/mod.rs b/src/scale/mod.rs new file mode 100644 index 0000000..ca68970 --- /dev/null +++ b/src/scale/mod.rs @@ -0,0 +1,79 @@ +//! holds scaling operations. +//! +//! choose from the wide expanse of options (ordered fastest to slowest): +//! +//! - [`Nearest`]: quickest, dumbest, jaggedest, scaling algorithm +//! - [`Box`]: you want slightly less pixels than nearest? here you go! kinda blurry though. +//! - [`Bilinear`]: _smooth_ scaling algorithm. rather fuzzy. +//! - [`Hamming`]: solves the [`Box`] problems. clearer image. +//! - [`CatmullRom`]: about the same as [`Hamming`], just a little slower. +//! - [`Mitchell`]: honestly, cant see the difference from [`CatmullRom`]. +//! - [`Lanczos3`]: prettiest scaling algorithm. highly recommend. +//! +//! usage: +//! ``` +//! # use fimg::{Image, scale::Lanczos3}; +//! let i = Image::<_, 3>::open("tdata/small_cat.png"); +//! let scaled = i.scale::(2144, 1424); +//! ``` +use crate::Image; + +mod algorithms; +pub mod traits; +pub use algorithms::*; + +macro_rules! transparent { + ($n: literal, $name: ident) => { + impl + AsRef<[u8]>> Image { + /// Scale a + #[doc = stringify!($name)] + /// image with a given scaling algorithm. + pub fn scale( + &mut self, + width: u32, + height: u32, + ) -> Image, $n> { + A::scale_transparent( + self.as_mut(), + width.try_into().unwrap(), + height.try_into().unwrap(), + ) + } + } + }; +} + +macro_rules! opaque { + ($n: literal, $name: ident) => { + impl + AsRef<[u8]>> Image { + /// Scale a + #[doc = stringify!($name)] + /// image with a given scaling algorithm. + pub fn scale( + &self, + width: u32, + height: u32, + ) -> Image, $n> { + A::scale_opaque( + self.as_ref(), + width.try_into().unwrap(), + height.try_into().unwrap(), + ) + } + } + }; +} + +opaque!(1, Y); +transparent!(2, YA); +opaque!(3, RGB); +transparent!(4, RGBA); + +#[test] +fn test_nearest() { + let i = Image::<_, 3>::open("tdata/cat.png"); + assert_eq!( + &*i.scale::(268, 178).buffer, + &*Image::<_, 3>::open("tdata/small_cat.png").buffer + ); +} diff --git a/src/scale/traits.rs b/src/scale/traits.rs new file mode 100644 index 0000000..66d1ddb --- /dev/null +++ b/src/scale/traits.rs @@ -0,0 +1,91 @@ +//! implementation detail for scaling. look into if you want to add a algorithm +use std::num::NonZeroU32; + +#[doc(hidden)] +mod seal { + #[doc(hidden)] + pub trait Sealed {} +} + +use seal::Sealed; + +use crate::Image; +impl Sealed for ChannelCount<1> {} +impl Sealed for ChannelCount<2> {} +impl Sealed for ChannelCount<3> {} +impl Sealed for ChannelCount<4> {} + +/// How to scale a image +pub trait ScalingAlgorithm { + /// Y/Rgb scale + fn scale_opaque( + i: Image<&[u8], N>, + w: NonZeroU32, + h: NonZeroU32, + ) -> Image, N> + where + ChannelCount: ToImageView; + /// Ya/Rgba scale + fn scale_transparent( + i: Image<&mut [u8], N>, + w: NonZeroU32, + h: NonZeroU32, + ) -> Image, N> + where + ChannelCount: AlphaDiv; +} + +/// helper +pub trait ToImageView: Sealed { + #[doc(hidden)] + type P: fr::PixelExt + fr::Convolution; + #[doc(hidden)] + fn wrap(i: Image<&[u8], N>) -> fr::ImageView; +} + +/// helper +pub trait AlphaDiv: Sealed + ToImageView { + #[doc(hidden)] + type P: fr::PixelExt + fr::Convolution + fr::AlphaMulDiv; + #[doc(hidden)] + fn handle(i: Image<&mut [u8], N>) -> fr::Image<'_, >::P>; +} + +/// Generic helper for [`Image`] and [`fr::Image`] transfers. +pub struct ChannelCount {} + +macro_rules! tiv { + ($n:literal, $which:ident) => { + impl ToImageView<$n> for ChannelCount<$n> { + type P = fr::$which; + fn wrap(i: Image<&[u8], $n>) -> fr::ImageView { + // SAFETY: same conds + unsafe { fr::ImageView::new(i.width, i.height, i.buffer()) } + } + } + }; +} + +tiv!(1, U8); +tiv!(2, U8x2); +tiv!(3, U8x3); +tiv!(4, U8x4); + +macro_rules! adiv { + ($n:literal, $which:ident) => { + impl AlphaDiv<$n> for ChannelCount<$n> { + type P = fr::$which; + fn handle(i: Image<&mut [u8], $n>) -> fr::Image<>::P> { + // SAFETY: we kinda have the same conditions + let mut i = unsafe { fr::Image::from_slice_u8(i.width, i.height, i.take_buffer()) }; + // SAFETY: mhm + unsafe { fr::MulDiv::default().multiply_alpha_inplace(&mut i.view_mut()) }; + + i + } + } + }; +} + +adiv!(2, U8x2); +adiv!(4, U8x4);