more image scaling filters

This commit is contained in:
bendn 2023-10-28 15:18:27 +07:00
parent 831f05de5b
commit 2c88066426
No known key found for this signature in database
GPG key ID: 0D9D3A2A3B2A93D6
8 changed files with 343 additions and 48 deletions

View file

@ -1,6 +1,6 @@
[package]
name = "fimg"
version = "0.4.19"
version = "0.4.20"
authors = ["bend-n <bend.n@outlook.com>"]
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

View file

@ -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

21
benches/scaling.rs Normal file
View file

@ -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],
];

View file

@ -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};

View file

@ -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<const N: usize>(
image: Image<&[u8], N>,
width: u32,
height: u32,
) -> Image<Vec<u8>, 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
);
}

140
src/scale/algorithms.rs Normal file
View file

@ -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<const N: usize>(
i: Image<&[u8], N>,
w: NonZeroU32,
h: NonZeroU32,
) -> Image<std::boxed::Box<[u8]>, N>
where
ChannelCount<N>: ToImageView<N>,
{
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::<N>::wrap(i), &mut dst.view_mut())
};
// SAFETY: ctor
unsafe { Image::new(dst.width(), dst.height(), dst.into_vec().into()) }
}
#[inline]
fn scale_transparent<const N: usize>(
i: Image<&mut [u8], N>,
w: NonZeroU32,
h: NonZeroU32,
) -> Image<std::boxed::Box<[u8]>, N>
where
ChannelCount<N>: AlphaDiv<N>,
{
Self::scale_opaque(i.as_ref(), w, h)
}
}
macro_rules! alg {
($for:ident) => {
impl ScalingAlgorithm for $for {
fn scale_opaque<const N: usize>(
i: Image<&[u8], N>,
w: NonZeroU32,
h: NonZeroU32,
) -> Image<std::boxed::Box<[u8]>, N>
where
ChannelCount<N>: ToImageView<N>,
{
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::<N>::wrap(i), &mut dst.view_mut())
};
// SAFETY: ctor
unsafe { Image::new(dst.width(), dst.height(), dst.into_vec().into()) }
}
fn scale_transparent<const N: usize>(
i: Image<&mut [u8], N>,
w: NonZeroU32,
h: NonZeroU32,
) -> Image<std::boxed::Box<[u8]>, N>
where
ChannelCount<N>: AlphaDiv<N>,
{
let mut dst = fr::Image::new(w, h);
// SAFETY: yes
unsafe {
fr::Resizer::new(fr::ResizeAlg::Convolution(fr::FilterType::$for))
.resize(&ChannelCount::<N>::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);
/// [MitchellNetravali](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<const N: usize>(
image: Image<&[u8], N>,
width: u32,
height: u32,
) -> Image<Vec<u8>, 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
}
}

79
src/scale/mod.rs Normal file
View file

@ -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::<Lanczos3>(2144, 1424);
//! ```
use crate::Image;
mod algorithms;
pub mod traits;
pub use algorithms::*;
macro_rules! transparent {
($n: literal, $name: ident) => {
impl<T: AsMut<[u8]> + AsRef<[u8]>> Image<T, $n> {
/// Scale a
#[doc = stringify!($name)]
/// image with a given scaling algorithm.
pub fn scale<A: traits::ScalingAlgorithm>(
&mut self,
width: u32,
height: u32,
) -> Image<std::boxed::Box<[u8]>, $n> {
A::scale_transparent(
self.as_mut(),
width.try_into().unwrap(),
height.try_into().unwrap(),
)
}
}
};
}
macro_rules! opaque {
($n: literal, $name: ident) => {
impl<T: AsMut<[u8]> + AsRef<[u8]>> Image<T, $n> {
/// Scale a
#[doc = stringify!($name)]
/// image with a given scaling algorithm.
pub fn scale<A: traits::ScalingAlgorithm>(
&self,
width: u32,
height: u32,
) -> Image<std::boxed::Box<[u8]>, $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::<Nearest>(268, 178).buffer,
&*Image::<_, 3>::open("tdata/small_cat.png").buffer
);
}

91
src/scale/traits.rs Normal file
View file

@ -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<const N: usize>(
i: Image<&[u8], N>,
w: NonZeroU32,
h: NonZeroU32,
) -> Image<Box<[u8]>, N>
where
ChannelCount<N>: ToImageView<N>;
/// Ya/Rgba scale
fn scale_transparent<const N: usize>(
i: Image<&mut [u8], N>,
w: NonZeroU32,
h: NonZeroU32,
) -> Image<Box<[u8]>, N>
where
ChannelCount<N>: AlphaDiv<N>;
}
/// helper
pub trait ToImageView<const N: usize>: Sealed {
#[doc(hidden)]
type P: fr::PixelExt + fr::Convolution;
#[doc(hidden)]
fn wrap(i: Image<&[u8], N>) -> fr::ImageView<Self::P>;
}
/// helper
pub trait AlphaDiv<const N: usize>: Sealed + ToImageView<N> {
#[doc(hidden)]
type P: fr::PixelExt + fr::Convolution + fr::AlphaMulDiv;
#[doc(hidden)]
fn handle(i: Image<&mut [u8], N>) -> fr::Image<'_, <Self as AlphaDiv<N>>::P>;
}
/// Generic helper for [`Image`] and [`fr::Image`] transfers.
pub struct ChannelCount<const N: usize> {}
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<Self::P> {
// 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<<Self as AlphaDiv<$n>>::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);