From 0bd406feeb9c916a26f7713014dfe3abad5d98a6 Mon Sep 17 00:00:00 2001 From: caleb Date: Sun, 22 Oct 2023 00:36:21 +0300 Subject: [PATCH] zune: Move filters to zune-imageprocs and add opencl Just like 9e5d10c234a36ee4f2a0856fd9f0dcb929379465 but for some reason was dropped during refactorings(I may have done a git-reset --- README.md | 82 ++++++ crates/zune-bin/Cargo.toml | 6 +- crates/zune-bin/src/cmd_args.rs | 10 +- crates/zune-bin/src/cmd_parsers/filters.rs | 23 +- crates/zune-bin/src/cmd_parsers/operations.rs | 38 +-- crates/zune-image/Cargo.toml | 8 +- crates/zune-image/src/channel.rs | 20 +- crates/zune-image/src/codecs.rs | 2 +- crates/zune-image/src/codecs/jpeg.rs | 4 +- .../zune-image/src/core_filters/colorspace.rs | 60 ++-- .../src/core_filters/colorspace/grayscale.rs | 2 +- .../src/deinterleave/deinterleave_impls.rs | 2 +- crates/zune-image/src/filters.rs | 35 --- crates/zune-image/src/filters/transpose.rs | 4 +- crates/zune-image/src/image.rs | 41 ++- crates/zune-image/src/lib.rs | 1 - crates/zune-image/src/metadata.rs | 8 + crates/zune-imageprocs/Cargo.toml | 9 +- crates/zune-imageprocs/src/auto_orient.rs | 107 +++++++ crates/zune-imageprocs/src/box_blur.rs | 127 ++++++++- crates/zune-imageprocs/src/brighten.rs | 72 +++++ crates/zune-imageprocs/src/contrast.rs | 55 ++++ crates/zune-imageprocs/src/convolve.rs | 203 +++++++++++-- crates/zune-imageprocs/src/crop.rs | 107 +++++++ crates/zune-imageprocs/src/exposure.rs | 82 ++++++ crates/zune-imageprocs/src/flip.rs | 121 ++++++++ crates/zune-imageprocs/src/flop.rs | 62 +++- crates/zune-imageprocs/src/gamma.rs | 96 +++++++ crates/zune-imageprocs/src/gaussian_blur.rs | 150 ++++++++++ crates/zune-imageprocs/src/invert.rs | 62 +++- crates/zune-imageprocs/src/lib.rs | 3 + crates/zune-imageprocs/src/median.rs | 118 +++++++- crates/zune-imageprocs/src/mirror.rs | 77 ++++- crates/zune-imageprocs/src/pad.rs | 19 +- crates/zune-imageprocs/src/premul_alpha.rs | 151 +++++++++- crates/zune-imageprocs/src/prewitt.rs | 24 +- crates/zune-imageprocs/src/resize.rs | 108 +++++++ crates/zune-imageprocs/src/rotate.rs | 60 ++++ crates/zune-imageprocs/src/scharr.rs | 134 +++++++++ crates/zune-imageprocs/src/sobel.rs | 134 ++++++++- crates/zune-imageprocs/src/spatial.rs | 124 ++++++++ .../zune-imageprocs/src/stretch_contrast.rs | 107 ++++++- crates/zune-imageprocs/src/threshold.rs | 83 +++++- crates/zune-imageprocs/src/traits.rs | 1 + crates/zune-imageprocs/src/transpose.rs | 98 +++++++ crates/zune-imageprocs/src/transpose/sse41.rs | 89 ++++++ crates/zune-imageprocs/src/transpose/tests.rs | 29 ++ crates/zune-imageprocs/src/unsharpen.rs | 160 +++++++++++ crates/zune-opencl/Cargo.toml | 12 + crates/zune-opencl/src/lib.rs | 17 ++ crates/zune-opencl/src/ocl_img.rs | 7 + crates/zune-opencl/src/ocl_sobel.rs | 195 +++++++++++++ .../zune-opencl/src/open_cl/ocl_brighten.cl | 54 ++++ crates/zune-opencl/src/open_cl/ocl_sobel.cl | 140 +++++++++ crates/zune-python/Cargo.toml | 3 +- crates/zune-python/src/lib.rs | 14 +- crates/zune-python/src/py_enums.rs | 144 +++++----- crates/zune-python/src/py_functions.rs | 10 +- crates/zune-python/src/py_image.rs | 266 ++++++++++-------- .../src/py_image/numpy_bindings.rs | 8 +- crates/zune-wasm/Cargo.toml | 1 + crates/zune-wasm/src/lib.rs | 17 +- 62 files changed, 3622 insertions(+), 384 deletions(-) create mode 100644 README.md delete mode 100644 crates/zune-image/src/filters.rs create mode 100644 crates/zune-imageprocs/src/auto_orient.rs create mode 100644 crates/zune-imageprocs/src/exposure.rs create mode 100644 crates/zune-opencl/Cargo.toml create mode 100644 crates/zune-opencl/src/lib.rs create mode 100644 crates/zune-opencl/src/ocl_img.rs create mode 100644 crates/zune-opencl/src/ocl_sobel.rs create mode 100644 crates/zune-opencl/src/open_cl/ocl_brighten.cl create mode 100644 crates/zune-opencl/src/open_cl/ocl_sobel.cl diff --git a/README.md b/README.md new file mode 100644 index 00000000..15787013 --- /dev/null +++ b/README.md @@ -0,0 +1,82 @@ +# zune-image + +This workspace features a set of small,independent and performant image codecs that can be used +for decoding and sometimes encoding images in a variety of formats. + +The set of codecs aim to have the following features in order of priority + +- Performance: Performance should be on par with or better than reference libraries. For example, + `zune-jpeg` should easily replace `libjpeg-turbo` without any noticeable speed loss. +- Safety: Minimal `unsafe` code, with the sole exception of SIMD intrinsics which currently require `unsafe`. +- Robustness: All decoders should be fuzz tested and found bugs fixed promptly. +- Ease of use: Consistent API across decoders and encoders. + Anyone, even your grandma should be able to decode supported formats +- Fast compile times: No dependencies on huge crates. Minimal and relatively well commented code. + +## Formats + +| Image Format | Decoder | Encoder | `no_std` Support | +|--------------|---------------|----------------|------------------| +| jpeg | zune-jpeg | [jpeg-encoder] | Yes | +| png | zune-png | - | Yes | +| ppm | zune-ppm | zune-ppm | Yes | +| qoi | zune-qoi | zune-qoi | Yes | +| farbfeld | zune-farbfeld | zune-farbfeld | Yes | +| psd | zune-psd | - | Yes | +| jpeg-xl | - | zune-jpegxl | Yes [^1] | +| hdr | zune-hdr | zune-hdr | No [^2] | + +- [^1] You lose threading capabilities. +- [^2] Lack of existence of `floor` and `exp` in the `core` library. + +## Safety + +This workspace **allows only 1 type of unsafe:** platform specific intrinsics (e.g. SIMD), and only where speed really +matters. + +All other types are **explicitly forbidden.** + +## Repository structure + - `crates` Contain main image code, each crate is prefixed with `zune-`. + The crates are divided into image formats, like `zune-png` deals with `png` decoding + - `zune-imageprocs` deals with image processing routines, etc etc + - `tests`: Image testing routines, they mainly read from `test-images` + - `benchmarks`: Benchmarking routines, they test the library routines with other popular image libraries. + - `fuzz-corpus` : Some interesting image files used for fuzzing. + - `test-images`: Images for testing various aspects of the decoder + - `docs`: Documentation on various parts of the library +## Why yet another image library + +Rust already has a good image library i.e https://github.com/image-rs/image + +But I'll let the overall speed of operations (decoding, applying image operations like blurring) speak for itself when +compared to other implementations. + +## Benchmarks. + +Library benchmarks are available [online] and also reproducible offline + +To reproduce benchmarks you can run the following commands + +Tested, on Linux, but should work for most operating systems + +```shell +git clone https://github.com/etemesi254/zune-image +cd ./zune-image +cargo bench --workspace +``` + +This will create a criterion directory in target which will contain benchmark +results of most image decoding operations. + + +[online]:https://etemesi254.github.io/posts/Zune-Benchmarks/ + +## Fuzzing + +Most decoders are tested in CI to ensure new changes do not introduce regressions. + +Critical decoders are fuzz tested in CI once every day to catch any potential issue/bug. + + +[jpeg-encoder]: https://github.com/vstroebel/jpeg-encoder diff --git a/crates/zune-bin/Cargo.toml b/crates/zune-bin/Cargo.toml index 3329b862..f431c31b 100644 --- a/crates/zune-bin/Cargo.toml +++ b/crates/zune-bin/Cargo.toml @@ -8,11 +8,13 @@ edition = "2021" [dependencies] log = "0.4.17" -zune-image = { version = "0.4.0", path = "../zune-image", features = ["all"] } +zune-image = { version = "0.4", path = "../zune-image", features = ["all"] } zune-core = { path = "../zune-core", version = "0.4" } -memmap2 = "0.9.0" +zune-imageprocs = { path = "../zune-imageprocs" } +memmap2 = "0.5.7" serde_json = "1.0.93" serde = "1.0.152" +zune-opencl = { path = "../zune-opencl" } [dependencies.simple_logger] version = "4.0.0" diff --git a/crates/zune-bin/src/cmd_args.rs b/crates/zune-bin/src/cmd_args.rs index f55860b8..e810cf0e 100644 --- a/crates/zune-bin/src/cmd_args.rs +++ b/crates/zune-bin/src/cmd_args.rs @@ -401,9 +401,15 @@ fn add_filters() -> (Vec, ArgGroup) { .help("Perform a 2D NxN convolution. N can be either of 3,5 or 7") .group(GROUP) .help_heading(GROUP) - .num_args(..=9) + .num_args(..=49) .action(ArgAction::Append) - .value_parser(value_parser!(f32)) + .value_parser(value_parser!(f32)), + Arg::new("ocl-sobel") + .long("ocl-sobel") + .help("Perform a 3x3 sobel convolution operation using opencl filters") + .action(ArgAction::SetTrue) + .help_heading(GROUP) + .group(GROUP) ]; args.sort_unstable_by(|x, y| x.get_id().cmp(y.get_id())); let arg_group = ArgGroup::new(GROUP) diff --git a/crates/zune-bin/src/cmd_parsers/filters.rs b/crates/zune-bin/src/cmd_parsers/filters.rs index e667b750..a34f1a74 100644 --- a/crates/zune-bin/src/cmd_parsers/filters.rs +++ b/crates/zune-bin/src/cmd_parsers/filters.rs @@ -8,15 +8,17 @@ use clap::ArgMatches; use log::debug; -use zune_image::filters::box_blur::BoxBlur; -use zune_image::filters::convolve::Convolve; -use zune_image::filters::gaussian_blur::GaussianBlur; -use zune_image::filters::scharr::Scharr; -use zune_image::filters::sobel::Sobel; -use zune_image::filters::statistics::{StatisticOperations, StatisticsOps}; -use zune_image::filters::unsharpen::Unsharpen; use zune_image::traits::IntoImage; use zune_image::workflow::WorkFlow; +use zune_imageprocs::box_blur::BoxBlur; +use zune_imageprocs::convolve::Convolve; +use zune_imageprocs::gaussian_blur::GaussianBlur; +use zune_imageprocs::scharr::Scharr; +use zune_imageprocs::sobel::Sobel; +use zune_imageprocs::spatial::StatisticsOps; +use zune_imageprocs::spatial_ops::StatisticOperations; +use zune_imageprocs::unsharpen::Unsharpen; +use zune_opencl::ocl_sobel::OclSobel; pub fn parse_options( workflow: &mut WorkFlow, argument: &str, args: &ArgMatches @@ -77,7 +79,12 @@ pub fn parse_options( .map(|x| **x) .collect(); - workflow.add_operation(Box::new(Convolve::new(values))) + workflow.add_operation(Box::new(Convolve::new(values, 1.0))) + } else if argument == "ocl-sobel" { + let ocl = OclSobel::try_new().map_err(|x| format!("{:?}", x))?; + debug!("Added ocl-sobel argument"); + + workflow.add_operation(Box::new(ocl)); } Ok(()) diff --git a/crates/zune-bin/src/cmd_parsers/operations.rs b/crates/zune-bin/src/cmd_parsers/operations.rs index 4389027c..bb23aa5d 100644 --- a/crates/zune-bin/src/cmd_parsers/operations.rs +++ b/crates/zune-bin/src/cmd_parsers/operations.rs @@ -12,24 +12,24 @@ use zune_core::bit_depth::BitDepth; use zune_core::colorspace::ColorSpace; use zune_image::core_filters::colorspace::ColorspaceConv; use zune_image::core_filters::depth::Depth; -use zune_image::filters::brighten::Brighten; -use zune_image::filters::contrast::Contrast; -use zune_image::filters::crop::Crop; -use zune_image::filters::exposure::Exposure; -use zune_image::filters::flip::{Flip, VerticalFlip}; -use zune_image::filters::flop::Flop; -use zune_image::filters::gamma::Gamma; -use zune_image::filters::invert::Invert; -use zune_image::filters::median::Median; -use zune_image::filters::mirror::{Mirror, MirrorMode}; -use zune_image::filters::orientation::AutoOrient; -use zune_image::filters::resize::{Resize, ResizeMethod}; -use zune_image::filters::statistics::{StatisticOperations, StatisticsOps}; -use zune_image::filters::stretch_contrast::StretchContrast; -use zune_image::filters::threshold::{Threshold, ThresholdMethod}; -use zune_image::filters::transpose::Transpose; use zune_image::traits::IntoImage; use zune_image::workflow::WorkFlow; +use zune_imageprocs::brighten::Brighten; +use zune_imageprocs::contrast::Contrast; +use zune_imageprocs::crop::Crop; +use zune_imageprocs::exposure::Exposure; +use zune_imageprocs::flip::{Flip, VerticalFlip}; +use zune_imageprocs::flop::Flop; +use zune_imageprocs::gamma::Gamma; +use zune_imageprocs::invert::Invert; +use zune_imageprocs::median::Median; +use zune_imageprocs::mirror::{Mirror, MirrorMode}; +use zune_imageprocs::resize::{Resize, ResizeMethod}; +use zune_imageprocs::spatial::StatisticsOps; +use zune_imageprocs::spatial_ops::StatisticOperations; +use zune_imageprocs::stretch_contrast::StretchContrast; +use zune_imageprocs::threshold::{Threshold, ThresholdMethod}; +use zune_imageprocs::transpose::Transpose; use crate::cmd_args::arg_parsers::IColorSpace; @@ -116,9 +116,9 @@ pub fn parse_options( ) } else if argument == "stretch_contrast" { let values = args - .get_many::(argument) + .get_many::(argument) .unwrap() - .collect::>(); + .collect::>(); let lower = *values[0]; @@ -180,7 +180,7 @@ pub fn parse_options( workflow.add_operation(Box::new(ColorspaceConv::new(colorspace))) } else if argument == "auto-orient" { debug!("Add auto orient operation"); - workflow.add_operation(Box::new(AutoOrient)) + //workflow.add_operation(Box::new(AutoOrient)) } else if argument == "exposure" { let exposure = *args.get_one::(argument).unwrap(); diff --git a/crates/zune-image/Cargo.toml b/crates/zune-image/Cargo.toml index 43e687f6..2699f85d 100644 --- a/crates/zune-image/Cargo.toml +++ b/crates/zune-image/Cargo.toml @@ -22,19 +22,17 @@ serde-support = ["zune-core/serde", "serde"] image_formats = ["jpeg", "ppm", "png", "psd", "farbfeld", "qoi", "jpeg-xl", "hdr", "bmp"] # External crates that help us handle metadata metadata = ["kamadak-exif"] -# Image filters -filters = ["zune-imageprocs"] # Every supported thing default = ["all"] # Whether to use threads or not for some operations threads = ["zune-jpegxl/threads"] # Simd support -simd = ["zune-jpeg/x86", "zune-png/sse", "zune-imageprocs/avx2", "zune-imageprocs/sse2", "zune-imageprocs/sse3", "zune-imageprocs/sse41"] +simd = ["zune-jpeg/x86", "zune-png/sse"] -all = ["image_formats", "serde-support", "metadata", "threads", "filters", "simd", "log"] +all = ["image_formats", "serde-support", "metadata", "threads", "simd", "log"] [dependencies] -zune-imageprocs = { path = "../zune-imageprocs", optional = true } +#zune-imageprocs = { path = "../zune-imageprocs", optional = true } # Core primitives zune-core = { path = "../zune-core", version = "0.4" } # Images diff --git a/crates/zune-image/src/channel.rs b/crates/zune-image/src/channel.rs index 13f8b9ca..52008beb 100644 --- a/crates/zune-image/src/channel.rs +++ b/crates/zune-image/src/channel.rs @@ -244,7 +244,7 @@ impl Channel { /// - length: The new lenghth of the array /// - type_id: The type id of the type this is supposed to store /// - pub(crate) fn new_with_length_and_type(length: usize, type_id: TypeId) -> Channel { + pub fn new_with_length_and_type(length: usize, type_id: TypeId) -> Channel { let mut channel = Channel::new_with_capacity_and_type(length, type_id); channel.length = length; @@ -568,6 +568,24 @@ impl Channel { Ok(()) } + + /// Return the raw memory layout of the channel as `&[u8]` + /// + /// # Safety + /// This is unsafe just as a remainder that the memory is just + /// a bag of bytes and may not be just `&[u8]`. + pub unsafe fn alias(&self) -> &[u8] { + std::slice::from_raw_parts(self.ptr, self.length) + } + + /// Return the raw memory layout of the channel as `mut &[u8]` + /// + /// # Safety + /// This is unsafe just as a remainder that the memory is just + /// a bag of bytes and may not be just `mut &[u8]`. + pub unsafe fn alias_mut(&mut self) -> &mut [u8] { + std::slice::from_raw_parts_mut(self.ptr, self.length) + } } impl Drop for Channel { diff --git a/crates/zune-image/src/codecs.rs b/crates/zune-image/src/codecs.rs index 8858a2f9..02237576 100644 --- a/crates/zune-image/src/codecs.rs +++ b/crates/zune-image/src/codecs.rs @@ -104,7 +104,7 @@ impl ImageFormat { /// Return true if an image format has an encoder that can convert the image /// into that format pub fn has_encoder(self) -> bool { - return self.get_encoder().is_some(); + self.get_encoder().is_some() } pub fn has_decoder(self) -> bool { diff --git a/crates/zune-image/src/codecs/jpeg.rs b/crates/zune-image/src/codecs/jpeg.rs index 7e506dda..fa6165fb 100644 --- a/crates/zune-image/src/codecs/jpeg.rs +++ b/crates/zune-image/src/codecs/jpeg.rs @@ -186,11 +186,11 @@ impl EncoderTrait for JpegEncoder { Ok(encoded_data) } else { - return Err(ImgEncodeErrors::UnsupportedColorspace( + Err(ImgEncodeErrors::UnsupportedColorspace( image.get_colorspace(), self.supported_colorspaces() ) - .into()); + .into()) } } diff --git a/crates/zune-image/src/core_filters/colorspace.rs b/crates/zune-image/src/core_filters/colorspace.rs index 2a848dfa..9dcaba26 100644 --- a/crates/zune-image/src/core_filters/colorspace.rs +++ b/crates/zune-image/src/core_filters/colorspace.rs @@ -118,17 +118,15 @@ fn rgb_to_grayscale( if preserve_alpha && colorspace.has_alpha() { frame.set_channels(vec![out, channel[3].clone()]); out_colorspace = ColorSpace::LumaA; + } else if to.has_alpha() { + // add alpha channel + let mut alpha_out = Channel::new_with_length::(size); + alpha_out.reinterpret_as_mut::().unwrap().fill(u8::MAX); + frame.set_channels(vec![out, alpha_out]); + out_colorspace = ColorSpace::Luma; } else { - if to.has_alpha() { - // add alpha channel - let mut alpha_out = Channel::new_with_length::(size); - alpha_out.reinterpret_as_mut::().unwrap().fill(u8::MAX); - frame.set_channels(vec![out, alpha_out]); - out_colorspace = ColorSpace::Luma; - } else { - frame.set_channels(vec![out]); - out_colorspace = ColorSpace::Luma; - } + frame.set_channels(vec![out]); + out_colorspace = ColorSpace::Luma; } } BitType::U16 => { @@ -142,20 +140,18 @@ fn rgb_to_grayscale( if preserve_alpha && colorspace.has_alpha() { frame.set_channels(vec![out, channel[3].clone()]); out_colorspace = ColorSpace::LumaA; + } else if to.has_alpha() { + // add alpha channel + let mut alpha_out = Channel::new_with_length::(size); + alpha_out + .reinterpret_as_mut::() + .unwrap() + .fill(u16::MAX); + frame.set_channels(vec![out, alpha_out]); + out_colorspace = ColorSpace::Luma; } else { - if to.has_alpha() { - // add alpha channel - let mut alpha_out = Channel::new_with_length::(size); - alpha_out - .reinterpret_as_mut::() - .unwrap() - .fill(u16::MAX); - frame.set_channels(vec![out, alpha_out]); - out_colorspace = ColorSpace::Luma; - } else { - frame.set_channels(vec![out]); - out_colorspace = ColorSpace::Luma; - } + frame.set_channels(vec![out]); + out_colorspace = ColorSpace::Luma; } } BitType::F32 => { @@ -175,17 +171,15 @@ fn rgb_to_grayscale( if preserve_alpha && colorspace.has_alpha() { frame.set_channels(vec![out, channel[3].clone()]); out_colorspace = ColorSpace::LumaA; + } else if to.has_alpha() { + // add alpha channel + let mut alpha_out = Channel::new_with_length::(size); + alpha_out.reinterpret_as_mut::().unwrap().fill(1.0); + frame.set_channels(vec![out, alpha_out]); + out_colorspace = ColorSpace::Luma; } else { - if to.has_alpha() { - // add alpha channel - let mut alpha_out = Channel::new_with_length::(size); - alpha_out.reinterpret_as_mut::().unwrap().fill(1.0); - frame.set_channels(vec![out, alpha_out]); - out_colorspace = ColorSpace::Luma; - } else { - frame.set_channels(vec![out]); - out_colorspace = ColorSpace::Luma; - } + frame.set_channels(vec![out]); + out_colorspace = ColorSpace::Luma; } } d => return Err(ImageErrors::ImageOperationNotImplemented("colorspace", d)) diff --git a/crates/zune-image/src/core_filters/colorspace/grayscale.rs b/crates/zune-image/src/core_filters/colorspace/grayscale.rs index eb108cfa..e09d4153 100644 --- a/crates/zune-image/src/core_filters/colorspace/grayscale.rs +++ b/crates/zune-image/src/core_filters/colorspace/grayscale.rs @@ -51,7 +51,7 @@ pub fn rgb_to_grayscale_f32(r: &[f32], g: &[f32], b: &[f32], out: &mut [f32], ma convert_rgb_to_grayscale_scalar_f32(r, g, b, out, max_value); } -#[cfg(all(feature = "benchmarks"))] +#[cfg(feature = "benchmarks")] #[cfg(test)] mod benchmarks { extern crate test; diff --git a/crates/zune-image/src/deinterleave/deinterleave_impls.rs b/crates/zune-image/src/deinterleave/deinterleave_impls.rs index d5117805..e63b793e 100644 --- a/crates/zune-image/src/deinterleave/deinterleave_impls.rs +++ b/crates/zune-image/src/deinterleave/deinterleave_impls.rs @@ -146,7 +146,7 @@ pub fn de_interleave_four_channels_f32( scalar::de_interleave_four_channels_scalar(source, c1, c2, c3, c4); } -#[cfg(all(feature = "benchmarks"))] +#[cfg(feature = "benchmarks")] #[cfg(test)] mod benchmarks { extern crate test; diff --git a/crates/zune-image/src/filters.rs b/crates/zune-image/src/filters.rs deleted file mode 100644 index fbc772e7..00000000 --- a/crates/zune-image/src/filters.rs +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) 2023. - * - * This software is free software; You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license - */ - -//! Contains image manipulation algorithms -//! -//! This contains structs that implement `OperationsTrait` -//! meaning they can manipulate images -#![cfg(feature = "filters")] -pub mod box_blur; -pub mod brighten; -pub mod contrast; -pub mod convolve; -pub mod crop; -pub mod exposure; -pub mod flip; -pub mod flop; -pub mod gamma; -pub mod gaussian_blur; -pub mod invert; -pub mod median; -pub mod mirror; -pub mod orientation; -pub mod premul_alpha; -pub mod resize; -pub mod rotate; -pub mod scharr; -pub mod sobel; -pub mod statistics; -pub mod stretch_contrast; -pub mod threshold; -pub mod transpose; -pub mod unsharpen; diff --git a/crates/zune-image/src/filters/transpose.rs b/crates/zune-image/src/filters/transpose.rs index cc0f8b8f..c9f87d19 100644 --- a/crates/zune-image/src/filters/transpose.rs +++ b/crates/zune-image/src/filters/transpose.rs @@ -7,7 +7,7 @@ */ use zune_core::bit_depth::BitType; -use zune_imageprocs::transpose::{transpose_generic, transpose_u16, transpose_u8}; +use zune_imageprocs::transpose::{transpose_f32, transpose_u16, transpose_u8}; use crate::channel::Channel; use crate::errors::ImageErrors; @@ -61,7 +61,7 @@ impl OperationsTrait for Transpose { ); } BitType::F32 => { - transpose_generic::( + transpose_f32( channel.reinterpret_as()?, out_channel.reinterpret_as_mut()?, width, diff --git a/crates/zune-image/src/image.rs b/crates/zune-image/src/image.rs index 1ac9c27b..ad0c2316 100644 --- a/crates/zune-image/src/image.rs +++ b/crates/zune-image/src/image.rs @@ -91,10 +91,14 @@ impl Image { self.metadata.set_depth(depth) } - pub const fn get_metadata(&self) -> &ImageMetadata { + pub const fn metadata(&self) -> &ImageMetadata { &self.metadata } + pub fn metadata_mut(&mut self) -> &mut ImageMetadata { + &mut self.metadata + } + /// Return an immutable reference to all image frames /// /// # Returns @@ -166,13 +170,13 @@ impl Image { } } pub fn flatten_to_u8(&self) -> Vec> { - return if self.get_depth() == BitDepth::Eight { + if self.get_depth() == BitDepth::Eight { self.flatten_frames::() } else { let mut im_clone = self.clone(); Depth::new(BitDepth::Eight).execute(&mut im_clone).unwrap(); im_clone.flatten_frames::() - }; + } } pub(crate) fn to_u8_be(&self) -> Vec> { let colorspace = self.get_colorspace(); @@ -204,7 +208,19 @@ impl Image { frame.write_rgba(colorspace, out).unwrap(); } } - pub(crate) fn set_dimensions(&mut self, width: usize, height: usize) { + /// Set new image dimensions + /// + /// # Warning + /// + /// This is potentially dangerous and should be used only when + /// the underlying channels have been modified. + /// + /// # Arguments: + /// - width: The new image width + /// - height: The new imag height. + /// + /// Modifies the image in place + pub fn set_dimensions(&mut self, width: usize, height: usize) { self.metadata.set_dimensions(width, height); } @@ -384,7 +400,6 @@ impl Image { /// # Panics /// - If calculating image dimensions will overflow [`usize`] /// - /// - If image `depth.size_of()` is not 2 /// /// - If pixels length is not equal to expected length pub fn from_u16(pixels: &[u16], width: usize, height: usize, colorspace: ColorSpace) -> Image { @@ -402,6 +417,22 @@ impl Image { Image::new(pixels, BitDepth::Sixteen, width, height, colorspace) } + /// Create an image from raw f32 pixels + /// + /// Pixels are expected to be interleaved according to number of components in the colorspace + /// + /// e.g if image is RGBA, pixels should be in the form of `[R,G,B,A,R,G,B,A]` + /// + ///The bit depth will be treated as [BitDepth::Float32](zune_core::bit_depth::BitDepth::Float32) + /// + /// # Returns + /// An [`Image`](crate::image::Image) struct + /// + /// + /// # Panics + /// - If calculating image dimensions will overflow [`usize`] + /// + /// - If pixels length is not equal to expected length pub fn from_f32(pixels: &[f32], width: usize, height: usize, colorspace: ColorSpace) -> Image { let expected_len = checked_mul(width, height, 1, colorspace.num_components()); assert_eq!( diff --git a/crates/zune-image/src/lib.rs b/crates/zune-image/src/lib.rs index 6a083616..e0ac4e40 100644 --- a/crates/zune-image/src/lib.rs +++ b/crates/zune-image/src/lib.rs @@ -63,7 +63,6 @@ pub mod codecs; pub mod core_filters; pub mod deinterleave; pub mod errors; -pub mod filters; pub mod frame; pub mod image; pub mod metadata; diff --git a/crates/zune-image/src/metadata.rs b/crates/zune-image/src/metadata.rs index 4394bb64..9f56a9fb 100644 --- a/crates/zune-image/src/metadata.rs +++ b/crates/zune-image/src/metadata.rs @@ -150,4 +150,12 @@ impl ImageMetadata { pub const fn get_image_format(&self) -> Option { self.format } + + pub const fn alpha(&self) -> AlphaState { + self.alpha + } + + pub fn set_alpha(&mut self, alpha_state: AlphaState) { + self.alpha = alpha_state; + } } diff --git a/crates/zune-imageprocs/Cargo.toml b/crates/zune-imageprocs/Cargo.toml index 775dfcd1..d7a6ead8 100644 --- a/crates/zune-imageprocs/Cargo.toml +++ b/crates/zune-imageprocs/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [dependencies] zune-core = { path = "../zune-core", version = "0.4" } - +zune-image = { path = "../zune-image",version = "0.4" } [features] avx2 = [] @@ -15,10 +15,11 @@ sse2 = [] sse3 = [] sse41 = [] ## Needs nightly, disabled by default -benchmarks=[] +benchmarks = [] log = ["zune-core/log"] -default = ["avx2", "sse2", "sse3", "sse41"] +threads=[] +default = ["avx2", "sse2", "sse3", "sse41","threads"] [dev-dependencies] -nanorand={version="0.7.0",default-features=false,features=["wyrand"]} # testing purposes. \ No newline at end of file +nanorand = { version = "0.7.0", default-features = false, features = ["wyrand"] } # testing purposes. \ No newline at end of file diff --git a/crates/zune-imageprocs/src/auto_orient.rs b/crates/zune-imageprocs/src/auto_orient.rs new file mode 100644 index 00000000..3d9da134 --- /dev/null +++ b/crates/zune-imageprocs/src/auto_orient.rs @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2023. + * + * This software is free software; + * + * You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license + */ + +//! Perform auto orientation of the image +use zune_core::bit_depth::BitType; +use zune_image::errors::ImageErrors; +use zune_image::image::Image; +use zune_image::traits::OperationsTrait; + +/// Auto orient the image based on the exif metadata +/// +/// This operation is a no-op if `metadata` feature is not specified +/// in the crate level docs +/// +/// This operation is also a no-op if the image does not have +/// exif metadata +pub struct AutoOrient; + +impl OperationsTrait for AutoOrient { + fn get_name(&self) -> &'static str { + "Auto orient" + } + + fn execute_impl(&self, _image: &mut Image) -> Result<(), ImageErrors> { + // check if we have exif orientation metadata and transform it + // to be this orientation + #[cfg(feature = "metadata")] + { + use exif::{Tag, Value}; + + if let Some(data) = image.metadata.exif.clone() { + for field in data { + // look for the orientation tag + if field.tag == Tag::Orientation { + match &field.value { + Value::Short(bytes) => { + if bytes.is_empty() { + warn!("The exif value is empty, cannot orient"); + return Ok(()); + } + match bytes[0] { + 1 => (), // orientation is okay + 2 => { + Flop::new().execute_impl(image)?; + } + + 3 => { + Flip::new().execute_impl(image)?; + } + 4 => { + // swap top with bottom + // 180 degree rotation + Rotate::new(180.0).execute_impl(image)?; + } + 5 => { + Transpose::new().execute_impl(image)?; + } + 6 => { + Transpose::new().execute_impl(image)?; + Flop::new().execute_impl(image)?; + } + 7 => { + Transpose::new().execute_impl(image)?; + Flip::new().execute_impl(image)?; + } + 8 => { + Transpose::new().execute_impl(image)?; + Rotate::new(180.0).execute_impl(image)?; + } + + _ => { + warn!( + "Unknown exif orientation tag {:?}, ignoring it", + &field.value + ); + } + } + } + _ => { + warn!("Invalid exif orientation type, ignoring it"); + } + } + } + } + } + // update exif + if let Some(data) = &mut image.metadata.exif { + for field in data { + // set orientation to do nothing + if field.tag == Tag::Orientation { + field.value = Value::Byte(vec![1]); + } + } + } + } + Ok(()) + } + + fn supported_types(&self) -> &'static [BitType] { + &[BitType::U16, BitType::U8, BitType::F32] + } +} diff --git a/crates/zune-imageprocs/src/box_blur.rs b/crates/zune-imageprocs/src/box_blur.rs index 2f460e2e..9b89beb6 100644 --- a/crates/zune-imageprocs/src/box_blur.rs +++ b/crates/zune-imageprocs/src/box_blur.rs @@ -8,12 +8,128 @@ use std::f32; -use zune_core::log::warn; +use zune_core::bit_depth::BitType; +use zune_core::log::{trace, warn}; +use zune_image::errors::ImageErrors; +use zune_image::image::Image; +use zune_image::traits::OperationsTrait; use crate::mathops::{compute_mod_u32, fastdiv_u32}; use crate::traits::NumOps; use crate::transpose; +/// Perform a box blur +/// +/// Radius is a measure of how many +/// pixels to include in the box blur. +/// +/// The greater the radius, the more pronounced the box blur +/// +/// This operation is multithreaded capable +#[derive(Default)] +pub struct BoxBlur { + radius: usize +} + +impl BoxBlur { + /// Create a new blur operation. + /// + /// # Arguments + /// - radius: The radius of the blur, larger the value the more pronounced the blur + #[must_use] + pub fn new(radius: usize) -> BoxBlur { + BoxBlur { radius } + } +} + +impl OperationsTrait for BoxBlur { + fn get_name(&self) -> &'static str { + "Box blur" + } + + fn execute_impl(&self, image: &mut Image) -> Result<(), ImageErrors> { + let (width, height) = image.get_dimensions(); + + let depth = image.get_depth(); + + #[cfg(feature = "threads")] + { + trace!("Running box blur in multithreaded mode"); + std::thread::scope(|s| { + let mut errors = vec![]; + // blur each channel on a separate thread + for channel in image.get_channels_mut(false) { + let result = s.spawn(|| match depth.bit_type() { + BitType::U16 => { + let mut scratch_space = vec![0; width * height]; + let data = channel.reinterpret_as_mut::()?; + box_blur_u16(data, &mut scratch_space, width, height, self.radius); + Ok(()) + } + BitType::U8 => { + let mut scratch_space = vec![0; width * height]; + let data = channel.reinterpret_as_mut::()?; + box_blur_u8(data, &mut scratch_space, width, height, self.radius); + Ok(()) + } + + BitType::F32 => { + let mut scratch_space = vec![0.0; width * height]; + let data = channel.reinterpret_as_mut::()?; + box_blur_f32(data, &mut scratch_space, width, height, self.radius); + Ok(()) + } + d => return Err(ImageErrors::ImageOperationNotImplemented("box_blur", d)) + }); + errors.push(result); + } + errors + .into_iter() + .map(|x| x.join().unwrap()) + .collect::, ImageErrors>>() + })?; + } + #[cfg(not(feature = "threads"))] + { + trace!("Running box blur in single threaded mode"); + + match depth.bit_type() { + BitType::U16 => { + let mut scratch_space = vec![0; width * height]; + + for channel in image.get_channels_mut(false) { + let data = channel.reinterpret_as_mut::()?; + box_blur_u16(data, &mut scratch_space, width, height, self.radius); + } + } + BitType::U8 => { + let mut scratch_space = vec![0; width * height]; + + for channel in image.get_channels_mut(false) { + let data = channel.reinterpret_as_mut::()?; + box_blur_u8(data, &mut scratch_space, width, height, self.radius); + } + } + + BitType::F32 => { + let mut scratch_space = vec![0.0; width * height]; + + for channel in image.get_channels_mut(false) { + let data = channel.reinterpret_as_mut::()?; + box_blur_f32(data, &mut scratch_space, width, height, self.radius); + } + } + d => return Err(ImageErrors::ImageOperationNotImplemented("box_blur", d)) + } + } + + Ok(()) + } + fn supported_types(&self) -> &'static [BitType] { + &[BitType::U8, BitType::U16, BitType::F32] + } +} + pub fn box_blur_u16( in_out_image: &mut [u16], scratch_space: &mut [u16], width: usize, height: usize, radius: usize ) { @@ -147,11 +263,14 @@ where } } -#[allow(clippy::cast_possible_truncation, clippy::too_many_lines)] +#[allow( + clippy::cast_possible_truncation, + clippy::too_many_lines, + clippy::cast_precision_loss +)] pub(crate) fn box_blur_f32_inner( in_image: &[f32], out_image: &mut [f32], width: usize, radius: usize ) { - let radius = radius; if width <= 1 || radius <= 1 { // repeated here for the optimizer return; @@ -214,7 +333,7 @@ pub(crate) fn box_blur_f32_inner( } } -#[cfg(all(feature = "benchmarks"))] +#[cfg(feature = "benchmarks")] #[cfg(test)] mod benchmarks { extern crate test; diff --git a/crates/zune-imageprocs/src/brighten.rs b/crates/zune-imageprocs/src/brighten.rs index 37bbc0d9..3d40bac5 100644 --- a/crates/zune-imageprocs/src/brighten.rs +++ b/crates/zune-imageprocs/src/brighten.rs @@ -23,8 +23,80 @@ //! //! The `a+c` is saturating on the maximum value for the type //! +use zune_core::bit_depth::BitType; +use zune_core::colorspace::ColorSpace; +use zune_image::errors::ImageErrors; +use zune_image::image::Image; +use zune_image::traits::OperationsTrait; + use crate::traits::NumOps; +#[derive(Default)] +pub struct Brighten { + value: f32 +} + +impl Brighten { + #[must_use] + pub fn new(value: f32) -> Brighten { + Brighten { value } + } +} + +impl OperationsTrait for Brighten { + fn get_name(&self) -> &'static str { + "Brighten" + } + + #[allow( + clippy::cast_sign_loss, + clippy::cast_precision_loss, + clippy::cast_possible_truncation + )] + fn execute_impl(&self, image: &mut Image) -> Result<(), ImageErrors> { + let max_val = image.get_depth().max_value(); + let depth = image.get_depth(); + + for channel in image.get_channels_mut(true) { + match depth.bit_type() { + BitType::U8 => brighten( + channel.reinterpret_as_mut::().unwrap(), + self.value.clamp(0., 255.) as u8, + u8::try_from(max_val.clamp(0, 255)).unwrap() + ), + BitType::U16 => brighten( + channel.reinterpret_as_mut::().unwrap(), + self.value as u16, + max_val + ), + BitType::F32 => brighten_f32( + channel.reinterpret_as_mut::().unwrap(), + self.value, + f32::from(max_val) + ), + d => { + return Err(ImageErrors::ImageOperationNotImplemented( + self.get_name(), + d + )) + } + } + } + Ok(()) + } + fn supported_colorspaces(&self) -> &'static [ColorSpace] { + &[ + ColorSpace::RGBA, + ColorSpace::RGB, + ColorSpace::LumaA, + ColorSpace::Luma + ] + } + + fn supported_types(&self) -> &'static [BitType] { + &[BitType::U8, BitType::U16, BitType::F32] + } +} /// Brighten operation /// /// # Arguments diff --git a/crates/zune-imageprocs/src/contrast.rs b/crates/zune-imageprocs/src/contrast.rs index 24b1cf0a..22255211 100644 --- a/crates/zune-imageprocs/src/contrast.rs +++ b/crates/zune-imageprocs/src/contrast.rs @@ -27,6 +27,61 @@ //! R' = F(R-128)+128 //! ``` +use zune_core::bit_depth::BitType; +use zune_core::colorspace::ColorSpace; +use zune_image::errors::ImageErrors; +use zune_image::image::Image; +use zune_image::traits::OperationsTrait; + +/// Adjust the contrast of an image +/// +/// +#[derive(Default)] +pub struct Contrast { + contrast: f32 +} + +impl Contrast { + #[must_use] + pub fn new(contrast: f32) -> Contrast { + Contrast { contrast } + } +} + +impl OperationsTrait for Contrast { + fn get_name(&self) -> &'static str { + "contrast" + } + + fn execute_impl(&self, image: &mut Image) -> Result<(), ImageErrors> { + let depth = image.get_depth(); + + for channel in image.get_channels_mut(true) { + match depth.bit_type() { + BitType::U8 => contrast_u8(channel.reinterpret_as_mut::()?, self.contrast), + d => { + return Err(ImageErrors::ImageOperationNotImplemented( + self.get_name(), + d + )) + } + } + } + Ok(()) + } + fn supported_colorspaces(&self) -> &'static [ColorSpace] { + &[ + ColorSpace::RGBA, + ColorSpace::RGB, + ColorSpace::LumaA, + ColorSpace::Luma + ] + } + fn supported_types(&self) -> &'static [BitType] { + &[BitType::U8] + } +} + /// Calculate the contrast of an image /// /// # Arguments diff --git a/crates/zune-imageprocs/src/convolve.rs b/crates/zune-imageprocs/src/convolve.rs index 1e9e062b..ae591a71 100644 --- a/crates/zune-imageprocs/src/convolve.rs +++ b/crates/zune-imageprocs/src/convolve.rs @@ -6,17 +6,166 @@ * You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license */ +use zune_core::bit_depth::BitType; +use zune_core::log::trace; +use zune_image::channel::Channel; +use zune_image::errors::ImageErrors; +use zune_image::image::Image; +use zune_image::traits::OperationsTrait; + use crate::pad::{pad, PadMethod}; use crate::traits::NumOps; use crate::utils::z_prefetch; -fn convolve_3x3_inner(in_array: &[T; 9], weights: &[f32; 9]) -> T +/// Convolve an image +#[derive(Default)] +pub struct Convolve { + weights: Vec, + scale: f32 +} + +impl Convolve { + /// Create a new convolve matrix, this supports 3x3,5x5 and 7x7 matrices + /// + /// The operation will return an error if the weights length isn't 9(3x3),25(5x5) or 49(7x7) + #[must_use] + pub fn new(weights: Vec, scale: f32) -> Convolve { + Convolve { weights, scale } + } +} + +impl OperationsTrait for Convolve { + fn get_name(&self) -> &'static str { + "2D convolution" + } + #[allow(clippy::too_many_lines)] + fn execute_impl(&self, image: &mut Image) -> Result<(), ImageErrors> { + let (width, height) = image.get_dimensions(); + let depth = image.get_depth(); + + #[cfg(feature = "threads")] + { + trace!("Running convolve in multithreaded mode"); + + std::thread::scope(|s| { + let mut errors = vec![]; + for channel in image.get_channels_mut(true) { + let scope = s.spawn(|| { + // Hello + let mut out_channel = Channel::new_with_bit_type( + width * height * depth.size_of(), + depth.bit_type() + ); + + match depth.bit_type() { + BitType::U8 => { + convolve( + channel.reinterpret_as::()?, + out_channel.reinterpret_as_mut::()?, + width, + height, + &self.weights, + self.scale + )?; + } + BitType::U16 => { + convolve( + channel.reinterpret_as::()?, + out_channel.reinterpret_as_mut::()?, + width, + height, + &self.weights, + self.scale + )?; + } + BitType::F32 => { + convolve( + channel.reinterpret_as::()?, + out_channel.reinterpret_as_mut::()?, + width, + height, + &self.weights, + self.scale + )?; + } + d => { + return Err(ImageErrors::ImageOperationNotImplemented( + self.get_name(), + d + )) + } + } + + *channel = out_channel; + Ok(()) + }); + errors.push(scope); + } + errors + .into_iter() + .map(|x| x.join().unwrap()) + .collect::, ImageErrors>>() + })?; + } + #[cfg(not(feature = "threads"))] + { + for channel in image.get_channels_mut(true) { + let mut out_channel = + Channel::new_with_bit_type(width * height * depth.size_of(), depth.bit_type()); + + match depth.bit_type() { + BitType::U8 => { + convolve( + channel.reinterpret_as::()?, + out_channel.reinterpret_as_mut::()?, + width, + height, + &self.weights, + self.scale + )?; + } + BitType::U16 => { + convolve( + channel.reinterpret_as::()?, + out_channel.reinterpret_as_mut::()?, + width, + height, + &self.weights, + self.scale + )?; + } + BitType::F32 => { + convolve( + channel.reinterpret_as::()?, + out_channel.reinterpret_as_mut::()?, + width, + height, + &self.weights, + self.scale + )?; + } + d => { + return Err(ImageErrors::ImageOperationNotImplemented( + self.get_name(), + d + )) + } + } + *channel = out_channel; + } + } + Ok(()) + } + fn supported_types(&self) -> &'static [BitType] { + &[BitType::U8, BitType::U16, BitType::F32] + } +} + +fn convolve_3x3_inner(in_array: &[T; 9], weights: &[f32; 9], scale: f32) -> T where T: NumOps + Copy + Default, f32: From { - let scale = 1.0; // 9.0; - T::from_f32( in_array .iter() @@ -28,12 +177,11 @@ where .zclamp(T::min_val(), T::max_val()) } -fn convolve_5x5_inner(in_array: &[T; 25], weights: &[f32; 25]) -> T +fn convolve_5x5_inner(in_array: &[T; 25], weights: &[f32; 25], scale: f32) -> T where T: NumOps + Copy + Default, f32: From { - let scale = 1.0; // / 25.0; T::from_f32( in_array .iter() @@ -45,12 +193,11 @@ where .zclamp(T::min_val(), T::max_val()) } -fn convolve_7x7_inner(in_array: &[T; 49], weights: &[f32; 49]) -> T +fn convolve_7x7_inner(in_array: &[T; 49], weights: &[f32; 49], scale: f32) -> T where T: NumOps + Copy + Default, f32: From { - let scale = 1.0 / 49.0; T::from_f32( in_array .iter() @@ -64,7 +211,8 @@ where /// Convolve a matrix pub fn convolve_3x3( - in_channel: &[T], out_channel: &mut [T], width: usize, height: usize, weights: &[f32; 9] + in_channel: &[T], out_channel: &mut [T], width: usize, height: usize, weights: &[f32; 9], + scale: f32 ) where T: NumOps + Copy + Default, f32: From @@ -79,12 +227,14 @@ pub fn convolve_3x3( width, height, convolve_3x3_inner, - weights + weights, + scale ); } pub fn convolve_5x5( - in_channel: &[T], out_channel: &mut [T], width: usize, height: usize, weights: &[f32; 25] + in_channel: &[T], out_channel: &mut [T], width: usize, height: usize, weights: &[f32; 25], + scale: f32 ) where T: NumOps + Copy + Default, f32: From @@ -99,12 +249,14 @@ pub fn convolve_5x5( width, height, convolve_5x5_inner, - weights + weights, + scale ); } pub fn convolve_7x7( - in_channel: &[T], out_channel: &mut [T], width: usize, height: usize, weights: &[f32; 49] + in_channel: &[T], out_channel: &mut [T], width: usize, height: usize, weights: &[f32; 49], + scale: f32 ) where T: NumOps + Copy + Default, f32: From @@ -119,13 +271,15 @@ pub fn convolve_7x7( width, height, convolve_7x7_inner, - weights + weights, + scale ); } /// Selects a convolve matrix pub fn convolve( - in_channel: &[T], out_channel: &mut [T], width: usize, height: usize, weights: &[f32] + in_channel: &[T], out_channel: &mut [T], width: usize, height: usize, weights: &[f32], + scale: f32 ) -> Result<(), &'static str> where T: NumOps + Copy + Default, @@ -137,7 +291,8 @@ where out_channel, width, height, - weights.try_into().unwrap() + weights.try_into().unwrap(), + scale ); } else if weights.len() == 25 { convolve_5x5::( @@ -145,7 +300,8 @@ where out_channel, width, height, - weights.try_into().unwrap() + weights.try_into().unwrap(), + scale ); } else if weights.len() == 49 { convolve_7x7::( @@ -153,7 +309,8 @@ where out_channel, width, height, - weights.try_into().unwrap() + weights.try_into().unwrap(), + scale ); } else { return Err("Not implemented, only works for 3x3, 5x5 and 7x7 arrays"); @@ -166,10 +323,10 @@ where #[allow(non_snake_case)] fn spatial_NxN( in_channel: &[T], out_channel: &mut [T], width: usize, height: usize, function: F, - values: &[f32; OUT_SIZE] + values: &[f32; OUT_SIZE], scale: f32 ) where T: Default + Copy, - F: Fn(&[T; OUT_SIZE], &[f32; OUT_SIZE]) -> T + F: Fn(&[T; OUT_SIZE], &[f32; OUT_SIZE], f32) -> T { let old_width = width; let height = (RADIUS * 2) + height; @@ -201,7 +358,7 @@ fn spatial_NxN( i += radius_size; } - let result = function(&local_storage, values); + let result = function(&local_storage, values, scale); out_channel[iy * old_width + ix] = result; } @@ -221,7 +378,7 @@ mod tests { let mut data = vec![0u8; width * height]; let mut out = vec![13; width * height]; nanorand::WyRand::new().fill(&mut data); - convolve_3x3(&data, &mut out, width, height, &[0.0; 9]); + convolve_3x3(&data, &mut out, width, height, &[0.0; 9], 1.); assert!(out.iter().all(|x| *x == 0)); } @@ -231,7 +388,7 @@ mod tests { let mut data = vec![0u8; width * height]; let mut out = vec![13; width * height]; nanorand::WyRand::new().fill(&mut data); - convolve_5x5(&data, &mut out, width, height, &[0.0; 25]); + convolve_5x5(&data, &mut out, width, height, &[0.0; 25], 1.); assert!(out.iter().all(|x| *x == 0)); } @@ -241,7 +398,7 @@ mod tests { let mut data = vec![0u8; width * height]; let mut out = vec![13; width * height]; nanorand::WyRand::new().fill(&mut data); - convolve_7x7(&data, &mut out, width, height, &[0.0; 49]); + convolve_7x7(&data, &mut out, width, height, &[0.0; 49], 1.); assert!(out.iter().all(|x| *x == 0)); } } diff --git a/crates/zune-imageprocs/src/crop.rs b/crates/zune-imageprocs/src/crop.rs index af0c4292..a1c9e383 100644 --- a/crates/zune-imageprocs/src/crop.rs +++ b/crates/zune-imageprocs/src/crop.rs @@ -48,6 +48,113 @@ //! Rust iterators are fun!! //! //! +//! + +use zune_core::bit_depth::BitType; +use zune_image::channel::Channel; +use zune_image::errors::ImageErrors; +use zune_image::image::Image; +use zune_image::traits::OperationsTrait; + +/// Crop out a part of an image +/// +/// This creates a smaller image from a bigger image +pub struct Crop { + x: usize, + y: usize, + width: usize, + height: usize +} + +impl Crop { + /// Create a new crop operation + /// + /// # Arguments + /// - width: The width of the new cropped out image + /// - height: The height of the new cropped out image. + /// - x: How far from the x origin the image should start from + /// - y: How far from the y origin the image should start from + /// + /// Origin is defined as the image top left corner. + #[must_use] + pub fn new(width: usize, height: usize, x: usize, y: usize) -> Crop { + Crop { + x, + y, + width, + height + } + } +} + +impl OperationsTrait for Crop { + fn get_name(&self) -> &'static str { + "Crop" + } + + fn execute_impl(&self, image: &mut Image) -> Result<(), ImageErrors> { + let new_dims = self.width * self.height * image.get_depth().size_of(); + let (old_width, _) = image.get_dimensions(); + let depth = image.get_depth().bit_type(); + + for channel in image.get_channels_mut(false) { + let mut new_vec = Channel::new_with_length_and_type(new_dims, channel.get_type_id()); + + // since crop is just bytewise copies, we can use the lowest common denominator for it + // and it will still work + match depth { + BitType::U8 => { + crop::( + channel.reinterpret_as()?, + old_width, + new_vec.reinterpret_as_mut()?, + self.width, + self.height, + self.x, + self.y + ); + } + BitType::U16 => { + crop::( + channel.reinterpret_as()?, + old_width, + new_vec.reinterpret_as_mut()?, + self.width, + self.height, + self.x, + self.y + ); + } + BitType::F32 => { + crop::( + channel.reinterpret_as()?, + old_width, + new_vec.reinterpret_as_mut()?, + self.width, + self.height, + self.x, + self.y + ); + } + d => { + return Err(ImageErrors::ImageOperationNotImplemented( + self.get_name(), + d + )) + } + } + *channel = new_vec; + } + + // safety: We just changed size of array + image.set_dimensions(self.width, self.height); + + Ok(()) + } + fn supported_types(&self) -> &'static [BitType] { + &[BitType::U8, BitType::U16, BitType::F32] + } +} /// Crop an image channel /// diff --git a/crates/zune-imageprocs/src/exposure.rs b/crates/zune-imageprocs/src/exposure.rs new file mode 100644 index 00000000..0b8aa119 --- /dev/null +++ b/crates/zune-imageprocs/src/exposure.rs @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2023. + * + * This software is free software; + * + * You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license + */ + +use zune_core::bit_depth::BitType; +use zune_image::errors::ImageErrors; +use zune_image::image::Image; +use zune_image::traits::OperationsTrait; + +/// Adjust exposure of image +pub struct Exposure { + exposure: f32, + black: f32 +} + +impl Exposure { + /// Create a new exposure filter + /// + /// # Arguments + /// - exposure: Set the exposure correction, + /// allowed range is from -3.0 to 3.0. Default should be zero + /// + /// - black: Set black level correction: Allowed range from -1.0 to 1.0. Default is zero + #[must_use] + pub fn new(exposure: f32, black: f32) -> Exposure { + Exposure { exposure, black } + } +} + +impl OperationsTrait for Exposure { + fn get_name(&self) -> &'static str { + "Exposure" + } + + #[allow( + clippy::cast_sign_loss, + clippy::cast_lossless, + clippy::cast_possible_truncation + )] + fn execute_impl(&self, image: &mut Image) -> Result<(), ImageErrors> { + let bit_type = image.get_depth().bit_type(); + + for channel in image.get_channels_mut(true) { + match bit_type { + BitType::U8 => { + let raw_px = channel.reinterpret_as_mut::()?; + for x in raw_px.iter_mut() { + *x = ((f32::from(*x) - self.black) * self.exposure).clamp(0., 255.0) as _; + } + } + BitType::U16 => { + let raw_px = channel.reinterpret_as_mut::()?; + for x in raw_px.iter_mut() { + *x = ((f32::from(*x) - self.black) * self.exposure).clamp(0., 65535.0) as _; + } + } + BitType::F32 => { + let raw_px = channel.reinterpret_as_mut::()?; + raw_px + .iter_mut() + .for_each(|x| *x = (*x - self.black) * self.exposure); + } + d => { + return Err(ImageErrors::ImageOperationNotImplemented( + self.get_name(), + d + )) + } + } + } + + Ok(()) + } + + fn supported_types(&self) -> &'static [BitType] { + &[BitType::U8, BitType::U16, BitType::F32] + } +} diff --git a/crates/zune-imageprocs/src/flip.rs b/crates/zune-imageprocs/src/flip.rs index 4b2440d1..38587089 100644 --- a/crates/zune-imageprocs/src/flip.rs +++ b/crates/zune-imageprocs/src/flip.rs @@ -6,6 +6,127 @@ * You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license */ +use zune_core::bit_depth::BitType; +use zune_image::errors::ImageErrors; +use zune_image::image::Image; +use zune_image::traits::OperationsTrait; + +/// Creates a vertical mirror image by reflecting +/// the pixels around the central x-axis. +/// +/// +/// ```text +/// +///old image new image +/// ┌─────────┐ ┌──────────┐ +/// │a b c d e│ │j i h g f │ +/// │f g h i j│ │e d c b a │ +/// └─────────┘ └──────────┘ +/// ``` +#[derive(Default)] +pub struct Flip; + +impl Flip { + /// Create a new flip operation + #[must_use] + pub fn new() -> Flip { + Self + } +} + +impl OperationsTrait for Flip { + fn get_name(&self) -> &'static str { + "Flip" + } + + fn execute_impl(&self, image: &mut Image) -> Result<(), ImageErrors> { + let depth = image.get_depth(); + + for inp in image.get_channels_mut(false) { + match depth.bit_type() { + BitType::U8 => { + flip(inp.reinterpret_as_mut::()?); + } + BitType::U16 => { + flip(inp.reinterpret_as_mut::()?); + } + BitType::F32 => { + flip(inp.reinterpret_as_mut::()?); + } + d => { + return Err(ImageErrors::ImageOperationNotImplemented( + self.get_name(), + d + )) + } + } + } + + Ok(()) + } + fn supported_types(&self) -> &'static [BitType] { + &[BitType::U8, BitType::U16, BitType::F32] + } +} + +/// Flip the image vertically,( rotate image by 180 degrees) +/// +/// ```text +/// +///old image new image +/// ┌─────────┐ ┌──────────┐ +/// │a b c d e│ │f g h i j │ +/// │f g h i j│ │a b c d e │ +/// └─────────┘ └──────────┘ +/// ``` +/// +#[derive(Default)] +pub struct VerticalFlip; + +impl VerticalFlip { + /// Create a new VerticalFlip operation + #[must_use] + pub fn new() -> VerticalFlip { + Self + } +} + +impl OperationsTrait for VerticalFlip { + fn get_name(&self) -> &'static str { + "Vertical Flip" + } + + fn execute_impl(&self, image: &mut Image) -> Result<(), ImageErrors> { + let depth = image.get_depth(); + let width = image.get_dimensions().0; + + for inp in image.get_channels_mut(false) { + match depth.bit_type() { + BitType::U8 => { + vertical_flip(inp.reinterpret_as_mut::()?, width); + } + BitType::U16 => { + vertical_flip(inp.reinterpret_as_mut::()?, width); + } + BitType::F32 => { + vertical_flip(inp.reinterpret_as_mut::()?, width); + } + d => { + return Err(ImageErrors::ImageOperationNotImplemented( + self.get_name(), + d + )) + } + } + } + + Ok(()) + } + fn supported_types(&self) -> &'static [BitType] { + &[BitType::U8, BitType::U16, BitType::F32] + } +} + /// Flip an image /// /// ```text diff --git a/crates/zune-imageprocs/src/flop.rs b/crates/zune-imageprocs/src/flop.rs index b3785476..15f4f651 100644 --- a/crates/zune-imageprocs/src/flop.rs +++ b/crates/zune-imageprocs/src/flop.rs @@ -6,6 +6,66 @@ * You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license */ +use zune_core::bit_depth::BitType; +use zune_image::errors::ImageErrors; +use zune_image::image::Image; +use zune_image::traits::OperationsTrait; + +/// Creates a horizontal mirror image by reflecting the pixels around the central y-axis +///```text +///old image new image +///┌─────────┐ ┌──────────┐ +///│a b c d e│ │e d b c a │ +///│f g h i j│ │j i h g f │ +///└─────────┘ └──────────┘ +///``` +#[derive(Default)] +pub struct Flop; + +impl Flop { + /// Create a new flop implementation + #[must_use] + pub fn new() -> Flop { + Self + } +} + +impl OperationsTrait for Flop { + fn get_name(&self) -> &'static str { + "Flop" + } + + fn execute_impl(&self, image: &mut Image) -> Result<(), ImageErrors> { + let (width, _) = image.get_dimensions(); + let depth = image.get_depth(); + + for channel in image.get_channels_mut(false) { + match depth.bit_type() { + BitType::U8 => { + flop(channel.reinterpret_as_mut::()?, width); + } + BitType::U16 => { + flop(channel.reinterpret_as_mut::()?, width); + } + BitType::F32 => { + flop(channel.reinterpret_as_mut::()?, width); + } + d => { + return Err(ImageErrors::ImageOperationNotImplemented( + self.get_name(), + d + )) + } + } + } + + Ok(()) + } + fn supported_types(&self) -> &'static [BitType] { + &[BitType::U8, BitType::U16, BitType::F32] + } +} + /// Flop an image /// ///```text @@ -33,7 +93,7 @@ pub fn flop(in_out_image: &mut [T], width: usize) { } } -#[cfg(all(feature = "benchmarks"))] +#[cfg(feature = "benchmarks")] #[cfg(test)] mod benchmarks { extern crate test; diff --git a/crates/zune-imageprocs/src/gamma.rs b/crates/zune-imageprocs/src/gamma.rs index 41e3d954..8f9364ce 100644 --- a/crates/zune-imageprocs/src/gamma.rs +++ b/crates/zune-imageprocs/src/gamma.rs @@ -6,8 +6,104 @@ * You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license */ +use zune_core::bit_depth::BitType; +use zune_core::log::trace; +use zune_image::errors::ImageErrors; +use zune_image::image::Image; +use zune_image::traits::OperationsTrait; + use crate::traits::NumOps; +/// Gamma adjust an image +/// +/// This currently only supports 8 and 16 bit depth images since it applies an optimization +/// that works for those depths. +/// +/// This operation is internally multithreaded, where supported +#[derive(Default)] +pub struct Gamma { + value: f32 +} + +impl Gamma { + /// Create a new gamma correction operation. + /// + /// # Arguments + /// value: Ranges typical range is from 0.8-2.3 + #[must_use] + pub fn new(value: f32) -> Gamma { + Gamma { value } + } +} + +impl OperationsTrait for Gamma { + fn get_name(&self) -> &'static str { + "Gamma Correction" + } + + fn execute_impl(&self, image: &mut Image) -> Result<(), ImageErrors> { + let max_value = image.get_depth().max_value(); + + let depth = image.get_depth(); + #[cfg(not(feature = "threads"))] + { + trace!("Running gamma correction in single threaded mode"); + + for channel in image.get_channels_mut(false) { + match depth.bit_type() { + BitType::U16 => { + gamma(channel.reinterpret_as_mut::()?, self.value, max_value) + } + BitType::U8 => { + gamma(channel.reinterpret_as_mut::()?, self.value, max_value) + } + d => { + return Err(ImageErrors::ImageOperationNotImplemented( + self.get_name(), + d + )) + } + } + } + } + #[cfg(feature = "threads")] + { + trace!("Running gamma correction in multithreaded mode"); + + std::thread::scope(|s| { + let mut errors = vec![]; + for channel in image.get_channels_mut(false) { + let t = s.spawn(|| match depth.bit_type() { + BitType::U16 => { + gamma(channel.reinterpret_as_mut::()?, self.value, max_value); + Ok(()) + } + BitType::U8 => { + gamma(channel.reinterpret_as_mut::()?, self.value, max_value); + Ok(()) + } + d => { + return Err(ImageErrors::ImageOperationNotImplemented( + self.get_name(), + d + )) + } + }); + errors.push(t); + } + errors + .into_iter() + .map(|x| x.join().unwrap()) + .collect::, ImageErrors>>() + })?; + } + Ok(()) + } + fn supported_types(&self) -> &'static [BitType] { + &[BitType::U8, BitType::U16] + } +} + #[allow( clippy::cast_sign_loss, clippy::cast_possible_truncation, diff --git a/crates/zune-imageprocs/src/gaussian_blur.rs b/crates/zune-imageprocs/src/gaussian_blur.rs index e46e4aba..eb2008dc 100644 --- a/crates/zune-imageprocs/src/gaussian_blur.rs +++ b/crates/zune-imageprocs/src/gaussian_blur.rs @@ -14,8 +14,158 @@ //! //! For the math behind it see +use zune_core::bit_depth::BitType; +use zune_core::log::trace; +use zune_image::errors::ImageErrors; +use zune_image::image::Image; +use zune_image::traits::OperationsTrait; + use crate::transpose; +#[derive(Default)] +pub struct GaussianBlur { + sigma: f32 +} + +impl GaussianBlur { + /// Create a new gaussian blur filter + /// + /// # Arguments + /// - sigma: How much to blur by. + #[must_use] + pub fn new(sigma: f32) -> GaussianBlur { + GaussianBlur { sigma } + } +} + +impl OperationsTrait for GaussianBlur { + fn get_name(&self) -> &'static str { + "Gaussian blur" + } + + #[allow(clippy::too_many_lines)] + fn execute_impl(&self, image: &mut Image) -> Result<(), ImageErrors> { + let (width, height) = image.get_dimensions(); + let depth = image.get_depth(); + + #[cfg(not(feature = "threads"))] + { + trace!("Running gaussian blur in single threaded mode"); + + match depth.bit_type() { + BitType::U8 => { + let mut temp = vec![0; width * height]; + + for channel in image.get_channels_mut(false) { + gaussian_blur_u8( + channel.reinterpret_as_mut::()?, + &mut temp, + width, + height, + self.sigma + ); + } + } + BitType::U16 => { + let mut temp = vec![0; width * height]; + + for channel in image.get_channels_mut(false) { + gaussian_blur_u16( + channel.reinterpret_as_mut::()?, + &mut temp, + width, + height, + self.sigma + ); + } + } + BitType::F32 => { + let mut temp = vec![0.0; width * height]; + for channel in image.get_channels_mut(false) { + gaussian_blur_f32( + channel.reinterpret_as_mut()?, + &mut temp, + width, + height, + self.sigma + ); + } + } + d => { + return Err(ImageErrors::ImageOperationNotImplemented( + self.get_name(), + d + )) + } + } + } + + #[cfg(feature = "threads")] + { + trace!("Running gaussian blur in multithreaded mode"); + std::thread::scope(|s| { + let mut errors = vec![]; + // blur each channel on a separate thread + for channel in image.get_channels_mut(false) { + let result = s.spawn(|| match depth.bit_type() { + BitType::U8 => { + let mut temp = vec![0; width * height]; + + gaussian_blur_u8( + channel.reinterpret_as_mut::()?, + &mut temp, + width, + height, + self.sigma + ); + Ok(()) + } + BitType::U16 => { + let mut temp = vec![0; width * height]; + + gaussian_blur_u16( + channel.reinterpret_as_mut::()?, + &mut temp, + width, + height, + self.sigma + ); + Ok(()) + } + BitType::F32 => { + let mut temp = vec![0.0; width * height]; + + gaussian_blur_f32( + channel.reinterpret_as_mut()?, + &mut temp, + width, + height, + self.sigma + ); + Ok(()) + } + d => { + return Err(ImageErrors::ImageOperationNotImplemented( + self.get_name(), + d + )) + } + }); + errors.push(result); + } + errors + .into_iter() + .map(|x| x.join().unwrap()) + .collect::, ImageErrors>>() + })?; + } + + Ok(()) + } + fn supported_types(&self) -> &'static [BitType] { + &[BitType::U8, BitType::U16] + } +} /// Create different box radius for each gaussian kernel function. #[allow( clippy::cast_possible_truncation, diff --git a/crates/zune-imageprocs/src/invert.rs b/crates/zune-imageprocs/src/invert.rs index 71def2e2..570b9ea8 100644 --- a/crates/zune-imageprocs/src/invert.rs +++ b/crates/zune-imageprocs/src/invert.rs @@ -8,8 +8,68 @@ use std::ops::Sub; +use zune_core::bit_depth::BitType; +use zune_core::colorspace::ColorSpace; +use zune_image::errors::ImageErrors; +use zune_image::image::Image; +use zune_image::traits::OperationsTrait; + use crate::traits::NumOps; +/// Invert an image pixel. +/// +/// The operation is similar to `T::max_val()-pixel`, where +/// `T::max_val()` is the maximum value for that bit-depth +/// (255 for [`u8`],65535 for [`u16`], 1 for [`f32`]) +/// +#[derive(Default)] +pub struct Invert; + +impl Invert { + /// Create a new invert operation + #[must_use] + pub fn new() -> Invert { + Self + } +} + +impl OperationsTrait for Invert { + fn get_name(&self) -> &'static str { + "Invert" + } + fn execute_impl(&self, image: &mut Image) -> Result<(), ImageErrors> { + let depth = image.get_depth().bit_type(); + + for channel in image.get_channels_mut(true) { + match depth { + BitType::U8 => invert(channel.reinterpret_as_mut::().unwrap()), + BitType::U16 => invert(channel.reinterpret_as_mut::().unwrap()), + BitType::F32 => invert(channel.reinterpret_as_mut::().unwrap()), + d => { + return Err(ImageErrors::ImageOperationNotImplemented( + self.get_name(), + d + )) + } + } + } + + Ok(()) + } + + fn supported_colorspaces(&self) -> &'static [ColorSpace] { + &[ + ColorSpace::RGB, + ColorSpace::RGBA, + ColorSpace::LumaA, + ColorSpace::Luma + ] + } + fn supported_types(&self) -> &'static [BitType] { + &[BitType::U8, BitType::U16, BitType::F32] + } +} + ///Invert a pixel /// /// The formula for inverting a 8 bit pixel @@ -23,7 +83,7 @@ where } } -#[cfg(all(feature = "benchmarks"))] +#[cfg(feature = "benchmarks")] #[cfg(test)] mod benchmarks { extern crate test; diff --git a/crates/zune-imageprocs/src/lib.rs b/crates/zune-imageprocs/src/lib.rs index c0fa927f..357d90df 100644 --- a/crates/zune-imageprocs/src/lib.rs +++ b/crates/zune-imageprocs/src/lib.rs @@ -28,6 +28,7 @@ clippy::wildcard_imports )] +mod auto_orient; pub mod box_blur; pub mod brighten; pub mod contrast; @@ -56,3 +57,5 @@ pub mod traits; pub mod transpose; pub mod unsharpen; mod utils; + +pub mod exposure; diff --git a/crates/zune-imageprocs/src/median.rs b/crates/zune-imageprocs/src/median.rs index 4b3b70d5..f466b8c7 100644 --- a/crates/zune-imageprocs/src/median.rs +++ b/crates/zune-imageprocs/src/median.rs @@ -8,8 +8,124 @@ use std::fmt::Debug; +use zune_core::bit_depth::BitType; +use zune_core::log::trace; +use zune_image::channel::Channel; +use zune_image::errors::ImageErrors; +use zune_image::image::Image; +use zune_image::traits::OperationsTrait; + //use crate::spatial::spatial_mut; +/// Median returns a new image in which each pixel is the median of its neighbors. +/// +/// The parameter radius corresponds to the radius of the neighbor area to be searched, +/// +/// for example a radius of R will result in a search window length of 2R+1 for each dimension. +#[derive(Default)] +pub struct Median { + radius: usize +} + +impl Median { + #[must_use] + pub fn new(radius: usize) -> Median { + Median { radius } + } +} + +impl OperationsTrait for Median { + fn get_name(&self) -> &'static str { + "Median Filter" + } + + fn execute_impl(&self, image: &mut Image) -> Result<(), ImageErrors> { + let (width, height) = image.get_dimensions(); + + let depth = image.get_depth(); + #[cfg(not(feature = "threads"))] + { + trace!("Running median filter single threaded mode"); + + for channel in image.get_channels_mut(false) { + let mut new_channel = Channel::new_with_bit_type(channel.len(), depth.bit_type()); + + match depth.bit_type() { + BitType::U16 => median( + channel.reinterpret_as::()?, + new_channel.reinterpret_as_mut::()?, + self.radius, + width, + height + ), + BitType::U8 => median( + channel.reinterpret_as::().unwrap(), + new_channel.reinterpret_as_mut::()?, + self.radius, + width, + height + ), + d => { + return Err(ImageErrors::ImageOperationNotImplemented( + self.get_name(), + d + )) + } + } + *channel = new_channel; + } + } + #[cfg(feature = "threads")] + { + trace!("Running median filter multithreaded mode"); + + std::thread::scope(|s| { + let mut errors = vec![]; + for channel in image.get_channels_mut(true) { + let result = s.spawn(|| { + let mut new_channel = + Channel::new_with_bit_type(channel.len(), depth.bit_type()); + + match depth.bit_type() { + BitType::U16 => median( + channel.reinterpret_as::()?, + new_channel.reinterpret_as_mut::()?, + self.radius, + width, + height + ), + BitType::U8 => median( + channel.reinterpret_as::()?, + new_channel.reinterpret_as_mut::()?, + self.radius, + width, + height + ), + d => { + return Err(ImageErrors::ImageOperationNotImplemented( + self.get_name(), + d + )) + } + } + *channel = new_channel; + Ok(()) + }); + errors.push(result); + } + errors + .into_iter() + .map(|x| x.join().unwrap()) + .collect::, ImageErrors>>() + })?; + } + Ok(()) + } + fn supported_types(&self) -> &'static [BitType] { + &[BitType::U8, BitType::U16] + } +} + pub fn find_median(array: &mut [T]) -> T { array.sort_unstable(); let middle = array.len() / 2; @@ -23,6 +139,6 @@ pub fn find_median(array: &mut [T]) -> T { pub fn median( _in_channel: &[T], _out_channel: &mut [T], _radius: usize, _width: usize, _height: usize ) { - panic!(); + //panic!(); //spatial_mut(in_channel, out_channel, radius, width, height, find_median); } diff --git a/crates/zune-imageprocs/src/mirror.rs b/crates/zune-imageprocs/src/mirror.rs index a9bbf661..8ef8170b 100644 --- a/crates/zune-imageprocs/src/mirror.rs +++ b/crates/zune-imageprocs/src/mirror.rs @@ -6,7 +6,12 @@ * You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license */ -/// Supported mirror modes +use zune_core::bit_depth::BitType; +use zune_image::errors::ImageErrors; +use zune_image::image::Image; +use zune_image::traits::OperationsTrait; + +/// Supported mirror modes #[derive(Copy, Clone, Eq, PartialEq)] pub enum MirrorMode { /// @@ -46,6 +51,76 @@ pub enum MirrorMode { /// ``` West } + +/// Rearrange the pixels along a certain axis. +/// +/// To see the effect of this +/// see the image [mirror-modes](zune_imageprocs::mirror::MirrorMode) documentation +/// for each used mode +pub struct Mirror { + mode: MirrorMode +} + +impl Mirror { + /// Create a new mirror filter + #[must_use] + pub fn new(mode: MirrorMode) -> Mirror { + Self { mode } + } +} + +impl OperationsTrait for Mirror { + fn get_name(&self) -> &'static str { + "Mirror" + } + + fn execute_impl(&self, image: &mut Image) -> Result<(), ImageErrors> { + let (width, height) = image.get_dimensions(); + let depth = image.get_depth(); + + for channel in image.get_channels_mut(false) { + match depth.bit_type() { + BitType::U8 => { + mirror( + channel.reinterpret_as_mut::()?, + width, + height, + self.mode + ); + } + + BitType::U16 => { + mirror( + channel.reinterpret_as_mut::()?, + width, + height, + self.mode + ); + } + BitType::F32 => { + mirror( + channel.reinterpret_as_mut::()?, + width, + height, + self.mode + ); + } + d => { + return Err(ImageErrors::ImageOperationNotImplemented( + self.get_name(), + d + )) + } + } + } + + Ok(()) + } + fn supported_types(&self) -> &'static [BitType] { + &[BitType::U8, BitType::U16, BitType::F32] + } +} + /// Mirror an image by duplicating pixels from one edge to the other half /// /// E.g a mirror along the east direction looks like diff --git a/crates/zune-imageprocs/src/pad.rs b/crates/zune-imageprocs/src/pad.rs index 5ef6da6a..2dc1ab31 100644 --- a/crates/zune-imageprocs/src/pad.rs +++ b/crates/zune-imageprocs/src/pad.rs @@ -119,6 +119,15 @@ fn replicate( // ▼ │ │ // └─────────────────────────────────────┘ // + + // fill top row + let first_row = &pixels[0..width]; + for out in out_pixels.chunks_exact_mut(padded_w).take(pad_y) { + out[0..start].fill(first_row[0]); + out[start..end].copy_from_slice(first_row); + out[end..].fill(*first_row.last().unwrap_or(&T::default())); + } + // fill middle rows for (out, in_pix) in out_pixels .chunks_exact_mut(padded_w) .skip(pad_y) @@ -129,10 +138,18 @@ fn replicate( out[start..end].copy_from_slice(in_pix); out[end..].fill(*in_pix.last().unwrap_or(&T::default())); } + + // fill bottom row + let last_row = pixels.rchunks_exact(width).next().unwrap(); + for out in out_pixels.rchunks_exact_mut(padded_w).take(pad_y) { + out[0..start].fill(last_row[0]); + out[start..end].copy_from_slice(last_row); + out[end..].fill(*last_row.last().unwrap_or(&T::default())); + } out_pixels } -#[cfg(all(feature = "benchmarks"))] +#[cfg(feature = "benchmarks")] #[cfg(test)] mod benchmarks { extern crate test; diff --git a/crates/zune-imageprocs/src/premul_alpha.rs b/crates/zune-imageprocs/src/premul_alpha.rs index df78fc04..bd477e67 100644 --- a/crates/zune-imageprocs/src/premul_alpha.rs +++ b/crates/zune-imageprocs/src/premul_alpha.rs @@ -41,13 +41,152 @@ //! - Iterate over source channel and alpha, //! - Lookup special constant (`c` ) for the alpha value //! - Multiply that constant `c` with channel value and take top bits -//! [`fastdiv_u32`](crate::mathops::fastdiv_u32) +//! [`fastdiv_u32`](fastdiv_u32) //! - +use zune_core::bit_depth::{BitDepth, BitType}; +use zune_core::log::warn; +use zune_image::errors::ImageErrors; +use zune_image::image::Image; +use zune_image::metadata::AlphaState; +use zune_image::traits::OperationsTrait; + use crate::mathops::{compute_mod_u32, fastdiv_u32}; mod sse; +/// Carry out alpha pre-multiply and un-premultiply +/// +/// The type of transform is specified. +/// +/// Note that some operations are lossy, +/// due to the nature of the operation multiplying and dividing values. +/// Where alpha is to big to fit into target integer, or zero, there will +/// be loss of image quality. +#[derive(Copy, Clone)] +pub struct PremultiplyAlpha { + to: AlphaState +} + +impl PremultiplyAlpha { + /// Create a new alpha pre-multiplication operation. + /// + /// It can be used to convert from pre-multiplied alpha to + /// normal alpha or vice-versa + #[must_use] + pub fn new(to: AlphaState) -> PremultiplyAlpha { + PremultiplyAlpha { to } + } +} + +impl OperationsTrait for PremultiplyAlpha { + fn get_name(&self) -> &'static str { + "pre-multiply alpha" + } + + fn execute_impl(&self, image: &mut Image) -> Result<(), ImageErrors> { + if !image.get_colorspace().has_alpha() { + warn!("Image colorspace indicates no alpha channel, this operation is a no-op"); + return Ok(()); + } + + let colorspaces = image.get_colorspace(); + let alpha_state = image.metadata().alpha(); + + if alpha_state == self.to { + warn!("Alpha is already in required mode, exiting"); + return Ok(()); + } + + let bit_type = image.get_depth(); + + for image_frame in image.get_frames_mut() { + // read colorspace + // split between alpha and color channels + let (color_channels, alpha) = image_frame + .get_channels_mut(colorspaces, false) + .split_at_mut(colorspaces.num_components() - 1); + + assert_eq!(alpha.len(), 1); + + // create static tables + let u8_table = create_unpremul_table_u8(); + let mut u16_table = vec![]; + + if bit_type == BitDepth::Sixteen { + u16_table = create_unpremul_table_u16(); + } + for channel in color_channels { + // from alpha channel, read + match (alpha_state, self.to) { + (AlphaState::NonPreMultiplied, AlphaState::PreMultiplied) => match bit_type { + BitDepth::Eight => { + premultiply_u8( + channel.reinterpret_as_mut()?, + alpha[0].reinterpret_as()? + ); + } + BitDepth::Sixteen => { + premultiply_u16( + channel.reinterpret_as_mut()?, + alpha[0].reinterpret_as()? + ); + } + + BitDepth::Float32 => premultiply_f32( + channel.reinterpret_as_mut()?, + alpha[0].reinterpret_as()? + ), + d => { + return Err(ImageErrors::ImageOperationNotImplemented( + self.get_name(), + d.bit_type() + )) + } + }, + (AlphaState::PreMultiplied, AlphaState::NonPreMultiplied) => match bit_type { + BitDepth::Eight => { + unpremultiply_u8( + channel.reinterpret_as_mut()?, + alpha[0].reinterpret_as()?, + &u8_table + ); + } + BitDepth::Sixteen => { + unpremultiply_u16( + channel.reinterpret_as_mut()?, + alpha[0].reinterpret_as()?, + &u16_table + ); + } + + BitDepth::Float32 => unpremultiply_f32( + channel.reinterpret_as_mut()?, + alpha[0].reinterpret_as()? + ), + d => { + return Err(ImageErrors::ImageOperationNotImplemented( + self.get_name(), + d.bit_type() + )) + } + }, + (_, _) => return Err(ImageErrors::GenericStr("Could not pre-multiply alpha")) + } + } + } + + // update metadata + image.metadata_mut().set_alpha(self.to); + + Ok(()) + } + + fn supported_types(&self) -> &'static [BitType] { + &[BitType::F32, BitType::U16, BitType::U8] + } +} + /// Create the fastdiv table for u8 division /// /// Useful for speeding up un-pre-multiplying alpha @@ -129,10 +268,11 @@ pub fn unpremultiply_u8(input: &mut [u8], alpha: &[u8], premul_table: &[u128; 25 input.iter_mut().zip(alpha).for_each(|(color, al)| { let associated_alpha = premul_table[usize::from(*al)]; - *color = fastdiv_u32( + *color = u8::try_from(fastdiv_u32( u32::from(*color) * MAX_VALUE + (u32::from(*al) / 2), associated_alpha - ) as u8; + )) + .unwrap_or(u8::MAX); }); } @@ -164,10 +304,11 @@ pub fn unpremultiply_u16(input: &mut [u16], alpha: &[u16], premul_table: &[u128] input.iter_mut().zip(alpha).for_each(|(color, al)| { let associated_alpha = premul_table[usize::from(*al)]; - *color = fastdiv_u32( + *color = u16::try_from(fastdiv_u32( u32::from(*color) * MAX_VALUE + (u32::from(*al) / 2), associated_alpha - ) as u16; + )) + .unwrap_or(u16::MAX); }); } diff --git a/crates/zune-imageprocs/src/prewitt.rs b/crates/zune-imageprocs/src/prewitt.rs index 67892186..d11b3a3e 100644 --- a/crates/zune-imageprocs/src/prewitt.rs +++ b/crates/zune-imageprocs/src/prewitt.rs @@ -20,23 +20,23 @@ fn prewitt_inner_f32(c: &[T; 9]) -> T f32: std::convert::From { // matrix - // -1, 0, 3, - // -10, 0, 10, - // -3, 0, 3 + // +1, 0, -1, + // +1, 0, -1, + // +1, 0, -1 // let mut sum_a = 0.0; - sum_a += (f32::from(c[0]) * -03.) + (f32::from(c[2]) * 03.); - sum_a += (f32::from(c[3]) * -10.) + (f32::from(c[5]) * 10.); - sum_a += (f32::from(c[6]) * -03.) + (f32::from(c[7]) * 30.); + sum_a += (f32::from(c[0]) * 1.) + (f32::from(c[2]) * -1.); + sum_a += (f32::from(c[3]) * 1.) + (f32::from(c[5]) * -1.); + sum_a += (f32::from(c[6]) * 1.) + (f32::from(c[7]) * -1.); // matrix - // -03,-10,-03, - // 0, 0, 0, - // 03, 10, 03 + // 1, 1, 1, + // 0, 0, 0, + // -1, -1, -1 let mut sum_b = 0.0; - sum_b += (f32::from(c[0]) * -03.) + (f32::from(c[1]) * -10.); - sum_b += (f32::from(c[2]) * -03.) + (f32::from(c[6]) * 03.); - sum_b += (f32::from(c[7]) * 10.) + (f32::from(c[8]) * 03.); + sum_b += (f32::from(c[0]) * 1.) + (f32::from(c[1]) * 1.); + sum_b += (f32::from(c[2]) * 1.) + (f32::from(c[6]) * -1.); + sum_b += (f32::from(c[7]) * -1.) + (f32::from(c[8]) * -1.); T::from_f32(((sum_a * sum_a) + (sum_b * sum_b)).sqrt()) } diff --git a/crates/zune-imageprocs/src/resize.rs b/crates/zune-imageprocs/src/resize.rs index e92f3e6a..9593c631 100644 --- a/crates/zune-imageprocs/src/resize.rs +++ b/crates/zune-imageprocs/src/resize.rs @@ -6,8 +6,116 @@ * You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license */ +use zune_core::bit_depth::BitType; +use zune_image::channel::Channel; +use zune_image::errors::ImageErrors; +use zune_image::image::Image; +use zune_image::traits::OperationsTrait; + use crate::traits::NumOps; +/// Resize an image to a new width and height +/// using the resize method specified +#[derive(Copy, Clone)] +pub struct Resize { + new_width: usize, + new_height: usize, + method: ResizeMethod +} + +impl Resize { + /// Create a new resize operation + /// + /// # Argument + /// - new_width: The new image width + /// - new_height: The new image height. + /// - method: The resize method to use + #[must_use] + pub fn new(new_width: usize, new_height: usize, method: ResizeMethod) -> Resize { + Resize { + new_width, + new_height, + method + } + } +} + +impl OperationsTrait for Resize { + fn get_name(&self) -> &'static str { + "Resize" + } + + fn execute_impl(&self, image: &mut Image) -> Result<(), ImageErrors> { + let (old_w, old_h) = image.get_dimensions(); + let depth = image.get_depth().bit_type(); + + let new_length = self.new_width * self.new_height * image.get_depth().size_of(); + + match depth { + BitType::U8 => { + for old_channel in image.get_channels_mut(false) { + let mut new_channel = Channel::new_with_bit_type(new_length, depth); + + resize::( + old_channel.reinterpret_as().unwrap(), + new_channel.reinterpret_as_mut().unwrap(), + self.method, + old_w, + old_h, + self.new_width, + self.new_height + ); + *old_channel = new_channel; + } + } + BitType::U16 => { + for old_channel in image.get_channels_mut(true) { + let mut new_channel = Channel::new_with_bit_type(new_length, depth); + + resize::( + old_channel.reinterpret_as().unwrap(), + new_channel.reinterpret_as_mut().unwrap(), + self.method, + old_w, + old_h, + self.new_width, + self.new_height + ); + *old_channel = new_channel; + } + } + BitType::F32 => { + for old_channel in image.get_channels_mut(true) { + let mut new_channel = Channel::new_with_bit_type(new_length, depth); + + resize::( + old_channel.reinterpret_as().unwrap(), + new_channel.reinterpret_as_mut().unwrap(), + self.method, + old_w, + old_h, + self.new_width, + self.new_height + ); + *old_channel = new_channel; + } + } + d => { + return Err(ImageErrors::ImageOperationNotImplemented( + self.get_name(), + d + )) + } + } + + image.set_dimensions(self.new_width, self.new_height); + + Ok(()) + } + fn supported_types(&self) -> &'static [BitType] { + &[BitType::U8, BitType::U16] + } +} mod bilinear; #[derive(Copy, Clone, Debug)] diff --git a/crates/zune-imageprocs/src/rotate.rs b/crates/zune-imageprocs/src/rotate.rs index 8d6eaa93..9dbe71c3 100644 --- a/crates/zune-imageprocs/src/rotate.rs +++ b/crates/zune-imageprocs/src/rotate.rs @@ -6,6 +6,66 @@ * You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license */ +use zune_core::bit_depth::BitType; +use zune_image::errors::ImageErrors; +use zune_image::image::Image; +use zune_image::traits::OperationsTrait; + +pub struct Rotate { + angle: f32 +} + +impl Rotate { + #[must_use] + pub fn new(angle: f32) -> Rotate { + Rotate { angle } + } +} + +impl OperationsTrait for Rotate { + fn get_name(&self) -> &'static str { + "Rotate" + } + + fn execute_impl(&self, image: &mut Image) -> Result<(), ImageErrors> { + let im_type = image.get_depth().bit_type(); + + let (width, _) = image.get_dimensions(); + + for channel in image.get_channels_mut(false) { + match im_type { + BitType::U8 => { + if (self.angle - 180.0).abs() < f32::EPSILON { + rotate_180::(channel.reinterpret_as_mut()?, width); + } + } + BitType::U16 => { + if (self.angle - 180.0).abs() < f32::EPSILON { + rotate_180::(channel.reinterpret_as_mut()?, width); + } + } + BitType::F32 => { + if (self.angle - 180.0).abs() < f32::EPSILON { + rotate_180::(channel.reinterpret_as_mut()?, width); + } + } + d => { + return Err(ImageErrors::ImageOperationNotImplemented( + self.get_name(), + d + )) + } + }; + } + + Ok(()) + } + + fn supported_types(&self) -> &'static [BitType] { + &[BitType::U8, BitType::U16, BitType::F32] + } +} + pub fn rotate(angle: f32, width: usize, in_image: &[T], out_image: &mut [T]) { let angle = angle % 360.0; diff --git a/crates/zune-imageprocs/src/scharr.rs b/crates/zune-imageprocs/src/scharr.rs index fdc9d348..c7de0990 100644 --- a/crates/zune-imageprocs/src/scharr.rs +++ b/crates/zune-imageprocs/src/scharr.rs @@ -6,10 +6,144 @@ * You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license */ +use zune_core::bit_depth::BitType; +use zune_image::channel::Channel; +use zune_image::errors::ImageErrors; +use zune_image::image::Image; +use zune_image::traits::OperationsTrait; + use crate::pad::{pad, PadMethod}; use crate::spatial::spatial_NxN; use crate::traits::NumOps; +/// Perform a scharr image derivative. +/// +/// This operation calculates the gradient of the image, +/// which represents how quickly pixel values change from +/// one point to another in both the horizontal and vertical directions. +/// The magnitude and direction of the gradient can be used to detect edges in an image. +/// +/// The matrix for scharr is +/// +/// Gx matrix +/// ```text +/// -3, 0, 3, +/// -10, 0, 10, +/// -3, 0, 3 +/// ``` +/// Gy matrix +/// ```text +/// -3,-10,-3, +/// 0, 0, 0, +/// 3, 10, 3 +/// ``` +/// +/// The window is a 3x3 window. +#[derive(Default, Copy, Clone)] +pub struct Scharr; + +impl Scharr { + /// Create a new scharr filter + #[must_use] + pub fn new() -> Scharr { + Self + } +} + +impl OperationsTrait for Scharr { + fn get_name(&self) -> &'static str { + "Scharr" + } + fn execute_impl(&self, image: &mut Image) -> Result<(), ImageErrors> { + let depth = image.get_depth().bit_type(); + let (width, height) = image.get_dimensions(); + + #[cfg(not(feature = "threads"))] + { + for channel in image.get_channels_mut(true) { + let mut out_channel = Channel::new_with_bit_type(channel.len(), depth); + match depth { + BitType::U8 => scharr_int::( + channel.reinterpret_as()?, + out_channel.reinterpret_as_mut()?, + width, + height + ), + BitType::U16 => scharr_int::( + channel.reinterpret_as()?, + out_channel.reinterpret_as_mut()?, + width, + height + ), + BitType::F32 => scharr_float::( + channel.reinterpret_as()?, + out_channel.reinterpret_as_mut()?, + width, + height + ), + d => { + return Err(ImageErrors::ImageOperationNotImplemented( + self.get_name(), + d + )) + } + } + *channel = out_channel; + } + } + #[cfg(feature = "threads")] + { + std::thread::scope(|s| { + let mut t_results = vec![]; + for channel in image.get_channels_mut(true) { + let result = s.spawn(|| { + let mut out_channel = Channel::new_with_bit_type(channel.len(), depth); + match depth { + BitType::U8 => scharr_int::( + channel.reinterpret_as()?, + out_channel.reinterpret_as_mut()?, + width, + height + ), + BitType::U16 => scharr_int::( + channel.reinterpret_as()?, + out_channel.reinterpret_as_mut()?, + width, + height + ), + BitType::F32 => scharr_float::( + channel.reinterpret_as()?, + out_channel.reinterpret_as_mut()?, + width, + height + ), + d => { + return Err(ImageErrors::ImageOperationNotImplemented( + self.get_name(), + d + )) + } + } + *channel = out_channel; + Ok(()) + }); + t_results.push(result); + } + + t_results + .into_iter() + .map(|x| x.join().unwrap()) + .collect::, ImageErrors>>() + })?; + } + + Ok(()) + } + + fn supported_types(&self) -> &'static [BitType] { + &[BitType::U8, BitType::U16, BitType::F32] + } +} /// Calculate scharr for f32 images /// /// # Arguments diff --git a/crates/zune-imageprocs/src/sobel.rs b/crates/zune-imageprocs/src/sobel.rs index 29daeaea..82988d48 100644 --- a/crates/zune-imageprocs/src/sobel.rs +++ b/crates/zune-imageprocs/src/sobel.rs @@ -6,11 +6,143 @@ * You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license */ -//! Scharr operations +//! sobel operations +use zune_core::bit_depth::BitType; +use zune_image::channel::Channel; +use zune_image::errors::ImageErrors; +use zune_image::image::Image; +use zune_image::traits::OperationsTrait; + use crate::pad::{pad, PadMethod}; use crate::spatial::spatial_NxN; use crate::traits::NumOps; +/// Perform a sobel image derivative. +/// +/// This operation calculates the gradient of the image, +/// which represents how quickly pixel values change from +/// one point to another in both the horizontal and vertical directions. +/// The magnitude and direction of the gradient can be used to detect edges in an image. +/// +/// The matrix for sobel is +/// +/// Gx matrix +/// ```text +/// -1, 0, 1, +/// -2, 0, 2, +/// -1, 0, 1 +/// ``` +/// Gy matrix +/// ```text +/// -1,-2,-1, +/// 0, 0, 0, +/// 1, 2, 1 +/// ``` +/// +/// The window is a 3x3 window. +#[derive(Default, Copy, Clone)] +pub struct Sobel; + +impl Sobel { + #[must_use] + pub fn new() -> Sobel { + Self + } +} + +impl OperationsTrait for Sobel { + fn get_name(&self) -> &'static str { + "Sobel" + } + fn execute_impl(&self, image: &mut Image) -> Result<(), ImageErrors> { + let depth = image.get_depth().bit_type(); + let (width, height) = image.get_dimensions(); + + #[cfg(not(feature = "threads"))] + { + for channel in image.get_channels_mut(true) { + let mut out_channel = Channel::new_with_bit_type(channel.len(), depth); + match depth { + BitType::U8 => sobel_int::( + channel.reinterpret_as()?, + out_channel.reinterpret_as_mut()?, + width, + height + ), + BitType::U16 => sobel_int::( + channel.reinterpret_as()?, + out_channel.reinterpret_as_mut()?, + width, + height + ), + BitType::F32 => sobel_float::( + channel.reinterpret_as()?, + out_channel.reinterpret_as_mut()?, + width, + height + ), + d => { + return Err(ImageErrors::ImageOperationNotImplemented( + self.get_name(), + d + )) + } + } + *channel = out_channel; + } + } + #[cfg(feature = "threads")] + { + std::thread::scope(|s| { + let mut t_results = vec![]; + for channel in image.get_channels_mut(true) { + let result = s.spawn(|| { + let mut out_channel = Channel::new_with_bit_type(channel.len(), depth); + match depth { + BitType::U8 => sobel_int::( + channel.reinterpret_as()?, + out_channel.reinterpret_as_mut()?, + width, + height + ), + BitType::U16 => sobel_int::( + channel.reinterpret_as()?, + out_channel.reinterpret_as_mut()?, + width, + height + ), + BitType::F32 => sobel_float::( + channel.reinterpret_as()?, + out_channel.reinterpret_as_mut()?, + width, + height + ), + d => { + return Err(ImageErrors::ImageOperationNotImplemented( + self.get_name(), + d + )) + } + } + *channel = out_channel; + Ok(()) + }); + t_results.push(result); + } + t_results + .into_iter() + .map(|x| x.join().unwrap()) + .collect::, ImageErrors>>() + })?; + } + + Ok(()) + } + + fn supported_types(&self) -> &'static [BitType] { + &[BitType::U8, BitType::U16, BitType::F32] + } +} /// Calculate sobel for f32 images /// /// # Arguments diff --git a/crates/zune-imageprocs/src/spatial.rs b/crates/zune-imageprocs/src/spatial.rs index 4723c437..96ca285f 100644 --- a/crates/zune-imageprocs/src/spatial.rs +++ b/crates/zune-imageprocs/src/spatial.rs @@ -5,7 +5,131 @@ * * You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license */ +/// Statistic operations on images. +/// +/// The parameter radius corresponds to the radius of the neighbor area the statistic is applied, +/// larger radius means more compute time. +/// +/// for example a radius of R will result in a search window length of 2R+1 for each dimension. +pub struct StatisticsOps { + radius: usize, + operation: StatisticOperations +} + +impl StatisticsOps { + #[must_use] + pub fn new(radius: usize, operation: StatisticOperations) -> StatisticsOps { + StatisticsOps { radius, operation } + } +} + +impl OperationsTrait for StatisticsOps { + fn get_name(&self) -> &'static str { + "StatisticsOps Filter" + } + + fn execute_impl(&self, image: &mut Image) -> Result<(), ImageErrors> { + let (width, height) = image.get_dimensions(); + + let depth = image.get_depth(); + #[cfg(not(feature = "threads"))] + { + trace!("Running erode filter in single threaded mode"); + + for channel in image.get_channels_mut(true) { + let mut new_channel = Channel::new_with_bit_type(channel.len(), depth.bit_type()); + + match depth.bit_type() { + BitType::U16 => spatial_ops( + channel.reinterpret_as::()?, + new_channel.reinterpret_as_mut::()?, + self.radius, + width, + height, + self.operation + ), + BitType::U8 => spatial_ops( + channel.reinterpret_as::()?, + new_channel.reinterpret_as_mut::()?, + self.radius, + width, + height, + self.operation + ), + d => { + return Err(ImageErrors::ImageOperationNotImplemented( + self.get_name(), + d + )) + } + } + *channel = new_channel; + } + } + #[cfg(feature = "threads")] + { + trace!( + "Running statistics filter for {:?} in multithreaded mode", + self.operation + ); + + std::thread::scope(|s| { + let mut errors = vec![]; + for channel in image.get_channels_mut(false) { + let result = s.spawn(|| { + let mut new_channel = + Channel::new_with_bit_type(channel.len(), depth.bit_type()); + + match depth.bit_type() { + BitType::U16 => spatial_ops( + channel.reinterpret_as::()?, + new_channel.reinterpret_as_mut::()?, + self.radius, + width, + height, + self.operation + ), + BitType::U8 => spatial_ops( + channel.reinterpret_as::()?, + new_channel.reinterpret_as_mut::()?, + self.radius, + width, + height, + self.operation + ), + d => { + return Err(ImageErrors::ImageOperationNotImplemented( + self.get_name(), + d + )) + } + } + *channel = new_channel; + Ok(()) + }); + errors.push(result); + } + errors + .into_iter() + .map(|x| x.join().unwrap()) + .collect::, ImageErrors>>() + })?; + } + Ok(()) + } + fn supported_types(&self) -> &'static [BitType] { + &[BitType::U8, BitType::U16] + } +} + +use zune_core::bit_depth::BitType; +use zune_core::log::trace; +use zune_image::channel::Channel; +use zune_image::errors::ImageErrors; +use zune_image::image::Image; +use zune_image::traits::OperationsTrait; +use crate::spatial_ops::{spatial_ops, StatisticOperations}; use crate::utils::z_prefetch; /// spatial goes through each pixel on an image collecting its neighbors and picking one diff --git a/crates/zune-imageprocs/src/stretch_contrast.rs b/crates/zune-imageprocs/src/stretch_contrast.rs index 65f449a3..dc620c66 100644 --- a/crates/zune-imageprocs/src/stretch_contrast.rs +++ b/crates/zune-imageprocs/src/stretch_contrast.rs @@ -6,8 +6,79 @@ * You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license */ +/// Linearly stretches the contrast in an image in place, +/// sending lower to image minimum and upper to image maximum. +#[derive(Default)] +pub struct StretchContrast { + lower: f32, + upper: f32 +} + +impl StretchContrast { + /// Create a new stretch contrast filter + /// + /// # Arguments + /// - lower: Lower minimum value for which pixels below this are clamped to the value + /// - upper: Upper maximum value for which pixels above are clamped to the value + #[must_use] + pub fn new(lower: f32, upper: f32) -> StretchContrast { + StretchContrast { lower, upper } + } +} + +impl OperationsTrait for StretchContrast { + fn get_name(&self) -> &'static str { + "Stretch Contrast" + } + + #[allow( + clippy::cast_precision_loss, + clippy::cast_possible_truncation, + clippy::cast_sign_loss + )] + fn execute_impl(&self, image: &mut Image) -> Result<(), ImageErrors> { + let depth = image.get_depth(); + + for channel in image.get_channels_mut(true) { + match depth.bit_type() { + BitType::U8 => stretch_contrast( + channel.reinterpret_as_mut::()?, + self.lower as u8, + self.upper as u8, + u32::from(depth.max_value()) + )?, + BitType::U16 => stretch_contrast( + channel.reinterpret_as_mut::()?, + self.lower as _, + self.upper as _, + u32::from(depth.max_value()) + )?, + BitType::F32 => stretch_contrast_f32( + channel.reinterpret_as_mut::()?, + self.lower as _, + self.upper as _ + )?, + d => { + return Err(ImageErrors::ImageOperationNotImplemented( + self.get_name(), + d + )) + } + } + } + Ok(()) + } + fn supported_types(&self) -> &'static [BitType] { + &[BitType::U8, BitType::U16] + } +} use std::ops::Sub; +use zune_core::bit_depth::BitType; +use zune_image::errors::ImageErrors; +use zune_image::image::Image; +use zune_image::traits::OperationsTrait; + use crate::mathops::{compute_mod_u32, fastdiv_u32}; use crate::traits::NumOps; @@ -63,12 +134,32 @@ where Ok(()) } +pub fn stretch_contrast_f32(image: &mut [f32], lower: f32, upper: f32) -> Result<(), &'static str> { + if upper < lower { + return Err("upper must be strictly greater than lower"); + } + let inv_range = 1. / (upper - lower); + for pixel in image.iter_mut() { + if *pixel > upper { + *pixel = f32::max_val(); + } else if *pixel <= lower { + *pixel = f32::min_val(); + } else { + *pixel = (f32::max_val() - *pixel) * inv_range; + } + } + + Ok(()) +} + #[cfg(feature = "benchmarks")] #[cfg(test)] mod benchmarks { extern crate test; - use crate::stretch_contrast::stretch_contrast; + use nanorand::Rng; + + use crate::stretch_contrast::{stretch_contrast, stretch_contrast_f32}; #[bench] fn bench_stretch_contrast(b: &mut test::Bencher) { @@ -76,9 +167,23 @@ mod benchmarks { let height = 800; let dimensions = width * height; let mut in_vec = vec![255_u16; dimensions]; + nanorand::WyRand::new().fill(&mut in_vec); b.iter(|| { stretch_contrast(&mut in_vec, 3, 10, 65535).unwrap(); }); } + + #[bench] + fn bench_stretch_contrast_f32(b: &mut test::Bencher) { + let width = 800; + let height = 800; + let dimensions = width * height; + let mut in_vec = vec![0.0; dimensions]; + nanorand::WyRand::new().fill(&mut in_vec); + + b.iter(|| { + stretch_contrast_f32(&mut in_vec, 0.5, 0.8).unwrap(); + }); + } } diff --git a/crates/zune-imageprocs/src/threshold.rs b/crates/zune-imageprocs/src/threshold.rs index 55707c45..f8bf630b 100644 --- a/crates/zune-imageprocs/src/threshold.rs +++ b/crates/zune-imageprocs/src/threshold.rs @@ -6,6 +6,12 @@ * You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license */ +use zune_core::bit_depth::BitType; +use zune_core::log::warn; +use zune_image::errors::ImageErrors; +use zune_image::image::Image; +use zune_image::traits::OperationsTrait; + use crate::traits::NumOps; #[derive(Copy, Clone, Debug)] @@ -29,6 +35,81 @@ impl ThresholdMethod { } } +/// Apply a fixed level threshold to an image. +/// +/// +/// # Methods +/// The library supports threshold methods derived from opencv , see [here](https://docs.opencv.org/4.x/d7/d1b/group__imgproc__misc.html#gaa9e58d2860d4afa658ef70a9b1115576) +/// for the definitions +/// +/// - [Binary](ThresholdMethod::Binary) => max if src(x,y) > thresh 0 otherwise +/// - [BinaryInv](ThresholdMethod::BinaryInv) => 0 if src(x,y) > thresh max otherwise +/// - [ThreshTrunc](ThresholdMethod::ThreshTrunc) => thresh if src(x,y) > thresh src(x,y) otherwise +/// - [ThreshToZero](ThresholdMethod::ThreshToZero) => src(x,y) if src(x,y) > thresh 0 otherwise +/// +/// See https://en.wikipedia.org/wiki/Thresholding_(image_processing) +pub struct Threshold { + method: ThresholdMethod, + threshold: f32 +} + +impl Threshold { + /// Create a new threshold filter + /// + /// # Arguments + /// - threshold: f32 The maximum value, this is type casted to the appropriate bit depth + /// for 8 bit images it saturates at u8::MAX, for 16 bit images at u16::MAX, for float images + /// the value is treated as is + /// - method: Threshold method to use, matches opencv methods + #[must_use] + pub fn new(threshold: f32, method: ThresholdMethod) -> Threshold { + Threshold { method, threshold } + } +} + +impl OperationsTrait for Threshold { + fn get_name(&self) -> &'static str { + "Threshold" + } + + #[allow( + clippy::cast_sign_loss, + clippy::cast_precision_loss, + clippy::cast_possible_truncation + )] + fn execute_impl(&self, image: &mut Image) -> Result<(), ImageErrors> { + if !image.get_colorspace().is_grayscale() { + warn!("Threshold works well with grayscale images, results may be something you don't expect"); + } + + let depth = image.get_depth(); + for channel in image.get_channels_mut(true) { + match depth.bit_type() { + BitType::U16 => threshold( + channel.reinterpret_as_mut::()?, + self.threshold.clamp(0., 65535.) as u16, + self.method + ), + BitType::U8 => threshold( + channel.reinterpret_as_mut::()?, + self.threshold.clamp(0., 255.) as u8, + self.method + ), + BitType::F32 => threshold( + channel.reinterpret_as_mut::()?, + self.threshold, + self.method + ), + d => return Err(ImageErrors::ImageOperationNotImplemented("threshold", d)) + } + } + + Ok(()) + } + fn supported_types(&self) -> &'static [BitType] { + &[BitType::U8, BitType::U16, BitType::F32] + } +} #[rustfmt::skip] pub fn threshold(in_channel: &mut [T], threshold: T, method: ThresholdMethod) where @@ -69,7 +150,7 @@ pub fn threshold(in_channel: &mut [T], threshold: T, method: ThresholdMethod) } } -#[cfg(all(feature = "benchmarks"))] +#[cfg(feature = "benchmarks")] #[cfg(test)] mod benchmarks { extern crate test; diff --git a/crates/zune-imageprocs/src/traits.rs b/crates/zune-imageprocs/src/traits.rs index 2e672761..c54e1c92 100644 --- a/crates/zune-imageprocs/src/traits.rs +++ b/crates/zune-imageprocs/src/traits.rs @@ -206,6 +206,7 @@ impl NumOps for f32 { 1.0 } + #[allow(clippy::cast_sign_loss)] fn to_usize(self) -> usize { self as _ } diff --git a/crates/zune-imageprocs/src/transpose.rs b/crates/zune-imageprocs/src/transpose.rs index 570073c0..09e4fda4 100644 --- a/crates/zune-imageprocs/src/transpose.rs +++ b/crates/zune-imageprocs/src/transpose.rs @@ -8,7 +8,12 @@ use std::sync::Once; +use zune_core::bit_depth::BitType; use zune_core::log::trace; +use zune_image::channel::Channel; +use zune_image::errors::ImageErrors; +use zune_image::image::Image; +use zune_image::traits::OperationsTrait; use crate::transpose::scalar::transpose_scalar; @@ -18,6 +23,79 @@ mod tests; static START: Once = Once::new(); +/// Transpose an image +/// +/// This mirrors the image along the image top left to bottom-right +/// diagonal +/// +/// Done by swapping X and Y indices of the array representation +#[derive(Default)] +pub struct Transpose; + +impl Transpose { + #[must_use] + pub fn new() -> Transpose { + Transpose + } +} + +impl OperationsTrait for Transpose { + fn get_name(&self) -> &'static str { + "Transpose" + } + + fn execute_impl(&self, image: &mut Image) -> Result<(), ImageErrors> { + let (width, height) = image.get_dimensions(); + let out_dim = width * height * image.get_depth().size_of(); + + let depth = image.get_depth(); + + for channel in image.get_channels_mut(false) { + let mut out_channel = Channel::new_with_bit_type(out_dim, depth.bit_type()); + + match depth.bit_type() { + BitType::U8 => { + transpose_u8( + channel.reinterpret_as::()?, + out_channel.reinterpret_as_mut::()?, + width, + height + ); + } + BitType::U16 => { + transpose_u16( + channel.reinterpret_as::()?, + out_channel.reinterpret_as_mut::()?, + width, + height + ); + } + BitType::F32 => { + transpose_float( + channel.reinterpret_as()?, + out_channel.reinterpret_as_mut()?, + width, + height + ); + } + d => { + return Err(ImageErrors::ImageOperationNotImplemented( + self.get_name(), + d + )) + } + }; + *channel = out_channel; + } + + image.set_dimensions(height, width); + + Ok(()) + } + fn supported_types(&self) -> &'static [BitType] { + &[BitType::U8, BitType::U16, BitType::F32] + } +} pub fn transpose_u16(in_matrix: &[u16], out_matrix: &mut [u16], width: usize, height: usize) { #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] { @@ -59,6 +137,26 @@ pub fn transpose_u8(in_matrix: &[u8], out_matrix: &mut [u8], width: usize, heigh }); transpose_scalar(in_matrix, out_matrix, width, height); } +pub fn transpose_float(in_matrix: &[f32], out_matrix: &mut [f32], width: usize, height: usize) { + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + { + #[cfg(feature = "sse41")] + { + use crate::transpose::sse41::transpose_sse_float; + + if is_x86_feature_detected!("sse4.1") { + START.call_once(|| { + trace!("Using SSE4.1 transpose u8 algorithm"); + }); + unsafe { return transpose_sse_float(in_matrix, out_matrix, width, height) } + } + } + } + START.call_once(|| { + trace!("Using scalar transpose u8 algorithm"); + }); + transpose_scalar(in_matrix, out_matrix, width, height); +} pub fn transpose_generic( in_matrix: &[T], out_matrix: &mut [T], width: usize, height: usize diff --git a/crates/zune-imageprocs/src/transpose/sse41.rs b/crates/zune-imageprocs/src/transpose/sse41.rs index ab233afc..9781be9d 100644 --- a/crates/zune-imageprocs/src/transpose/sse41.rs +++ b/crates/zune-imageprocs/src/transpose/sse41.rs @@ -375,3 +375,92 @@ pub unsafe fn transpose_sse41_u8( } } } + +unsafe fn transpose_sse_float_4x4_inner(in_matrix: &[f32], out: &mut [f32], in_stride: usize, out_stride: usize){ + assert!((3 * out_stride) <= out.len()); + + assert!((3 * in_stride) <= in_matrix.len()); + + let mut row0 = _mm_loadu_ps(in_matrix.get_unchecked(in_stride * 0..).as_ptr().cast()); + let mut row1 = _mm_loadu_ps(in_matrix.get_unchecked(in_stride * 1..).as_ptr().cast()); + let mut row2 = _mm_loadu_ps(in_matrix.get_unchecked(in_stride * 2..).as_ptr().cast()); + let mut row3 = _mm_loadu_ps(in_matrix.get_unchecked(in_stride * 3..).as_ptr().cast()); + + _MM_TRANSPOSE4_PS(&mut row0,&mut row1,&mut row2,&mut row3); + + + _mm_storeu_ps(out.get_unchecked_mut(out_stride * 0..).as_mut_ptr().cast(), row0); + _mm_storeu_ps(out.get_unchecked_mut(out_stride * 1..).as_mut_ptr().cast(), row1); + _mm_storeu_ps(out.get_unchecked_mut(out_stride * 2..).as_mut_ptr().cast(), row2); + _mm_storeu_ps(out.get_unchecked_mut(out_stride * 3..).as_mut_ptr().cast(), row3); + +} + + +pub unsafe fn transpose_sse_float( + in_matrix: &[f32], out_matrix: &mut [f32], width: usize, height: usize +) { + const SMALL_WIDTH_THRESHOLD: usize = 4; + + let dimensions = width * height; + + assert_eq!( + in_matrix.len(), + dimensions, + "In matrix dimensions do not match width and height" + ); + + assert_eq!( + out_matrix.len(), + dimensions, + "Out matrix dimensions do not match width and height" + ); + + if width < SMALL_WIDTH_THRESHOLD { + return crate::transpose::transpose_scalar(in_matrix, out_matrix, width, height); + } + + // We want to figure out how many times we can divide the width into + // 4 since inner loop transposes by 4 + let width_iterations = width / 4; + let sin_height = 4 * width; + + for (i, in_width_stride) in in_matrix.chunks_exact(sin_height).enumerate() { + for j in 0..width_iterations { + let out_height_stride = &mut out_matrix[(j * height * 4) + (i * 4)..]; + + transpose_sse_float_4x4_inner( + &in_width_stride[(j * 4)..], + out_height_stride, + width, + height + ); + } + } + // Deal with the part that hasn't been copied + // + // + //┌──────────┬─────┐ + //│ │ │ + //│ │ │ + //│ Done │ B │ + //│ │ │ + //│ │ │ + //├──────────┘-----│ + //│ C │ + //└────────────────┘ + // Everything in region b and C isn't done + let rem_w = width - (width & 3); + let rem_h = height - (height & 3); + + for i in rem_h..height { + for j in 0..width { + out_matrix[(j * height) + i] = in_matrix[(i * width) + j]; + } + } + for i in rem_w..width { + for j in 0..height { + out_matrix[(i * height) + j] = in_matrix[(j * width) + i]; + } + } +} \ No newline at end of file diff --git a/crates/zune-imageprocs/src/transpose/tests.rs b/crates/zune-imageprocs/src/transpose/tests.rs index c5836080..82856e65 100644 --- a/crates/zune-imageprocs/src/transpose/tests.rs +++ b/crates/zune-imageprocs/src/transpose/tests.rs @@ -33,3 +33,32 @@ fn test_transpose_sse_scalar_identical() { assert_eq!(a, b); } } + + +#[test] +fn test_transpose_sse_float_scalar_identical() { + use nanorand::Rng; + + use crate::transpose; + + let mut rng = nanorand::WyRand::new(); + + let width: usize = 42; + let height: usize = 25; + + let mut in_matrix: Vec = vec![0.0; width * height]; + rng.fill(&mut in_matrix); + + let mut sse_out = vec![0.0; width * height]; + let mut scalar_out = vec![34.0; width * height]; + unsafe { + transpose::sse41::transpose_sse_float(&in_matrix, &mut sse_out, width, height); + } + transpose::scalar::transpose_scalar(&in_matrix, &mut scalar_out, width, height); + for (a, b) in scalar_out + .chunks_exact(height) + .zip(sse_out.chunks_exact(height)) + { + assert_eq!(a, b); + } +} \ No newline at end of file diff --git a/crates/zune-imageprocs/src/unsharpen.rs b/crates/zune-imageprocs/src/unsharpen.rs index 55182b46..819921dd 100644 --- a/crates/zune-imageprocs/src/unsharpen.rs +++ b/crates/zune-imageprocs/src/unsharpen.rs @@ -6,8 +6,168 @@ * You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license */ +use zune_core::bit_depth::BitType; +use zune_core::log::trace; +use zune_image::errors::ImageErrors; +use zune_image::image::Image; +use zune_image::traits::OperationsTrait; + use crate::gaussian_blur::{gaussian_blur_u16, gaussian_blur_u8}; +/// Perform an unsharpen mask +/// +/// This uses the result of a gaussian filter and thresholding to +/// perform the mask calculation +#[derive(Default)] +pub struct Unsharpen { + sigma: f32, + threshold: u16, + percentage: u8 +} + +impl Unsharpen { + /// Create a new unsharp mask + /// + /// # Arguments + /// - sigma: This value is passed to the gaussian filter,consult [it's documentation](crate::filters::gaussian_blur::GaussianBlur) + /// on how to use it + /// + /// - threshold: If the result of the blur and the initial image is greater than this, add the difference, otherwise + /// skip + /// - percentage: `threshold*percentage` + /// + #[must_use] + pub fn new(sigma: f32, threshold: u16, percentage: u8) -> Unsharpen { + Unsharpen { + sigma, + threshold, + percentage + } + } +} + +impl OperationsTrait for Unsharpen { + fn get_name(&self) -> &'static str { + "Unsharpen" + } + + fn execute_impl(&self, image: &mut Image) -> Result<(), ImageErrors> { + let (width, height) = image.get_dimensions(); + + let depth = image.get_depth(); + + #[cfg(not(feature = "threads"))] + { + trace!("Running unsharpen in single threaded mode"); + + match depth.bit_type() { + BitType::U16 => { + let mut blur_buffer = vec![0; width * height]; + let mut blur_scratch = vec![0; width * height]; + + for channel in image.get_channels_mut(true) { + unsharpen_u16( + channel.reinterpret_as_mut::()?, + &mut blur_buffer, + &mut blur_scratch, + self.sigma, + self.threshold, + self.percentage as u16, + width, + height + ); + } + } + + BitType::U8 => { + let mut blur_buffer = vec![0; width * height]; + let mut blur_scratch = vec![0; width * height]; + + for channel in image.get_channels_mut(true) { + unsharpen_u8( + channel.reinterpret_as_mut::()?, + &mut blur_buffer, + &mut blur_scratch, + self.sigma, + self.threshold as u8, + self.percentage, + width, + height + ); + } + } + d => { + return Err(ImageErrors::ImageOperationNotImplemented( + self.get_name(), + d + )) + } + } + } + #[cfg(feature = "threads")] + { + trace!("Running unsharpen in multithreaded mode"); + std::thread::scope(|s| { + let mut errors = vec![]; + // blur each channel on a separate thread + for channel in image.get_channels_mut(true) { + let result = s.spawn(|| match depth.bit_type() { + BitType::U16 => { + let mut blur_buffer = vec![0; width * height]; + let mut blur_scratch = vec![0; width * height]; + + unsharpen_u16( + channel.reinterpret_as_mut::()?, + &mut blur_buffer, + &mut blur_scratch, + self.sigma, + self.threshold, + u16::from(self.percentage), + width, + height + ); + Ok(()) + } + + BitType::U8 => { + let mut blur_buffer = vec![0; width * height]; + let mut blur_scratch = vec![0; width * height]; + + unsharpen_u8( + channel.reinterpret_as_mut::()?, + &mut blur_buffer, + &mut blur_scratch, + self.sigma, + u8::try_from(self.threshold.clamp(0, 255)).unwrap_or(u8::MAX), + self.percentage, + width, + height + ); + Ok(()) + } + d => { + return Err(ImageErrors::ImageOperationNotImplemented( + self.get_name(), + d + )) + } + }); + errors.push(result); + } + errors + .into_iter() + .map(|x| x.join().unwrap()) + .collect::, ImageErrors>>() + })?; + } + + Ok(()) + } + fn supported_types(&self) -> &'static [BitType] { + &[BitType::U8, BitType::U16] + } +} + /// Sharpen an image /// /// The underlying algorithm applies a gaussian blur diff --git a/crates/zune-opencl/Cargo.toml b/crates/zune-opencl/Cargo.toml new file mode 100644 index 00000000..32274a79 --- /dev/null +++ b/crates/zune-opencl/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "zune-opencl" +version = "0.4.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +ocl = "0.19.4" +zune-image = { path = "../zune-image" } +zune-core = { path = "../zune-core" } +bytemuck = "1.13.1" \ No newline at end of file diff --git a/crates/zune-opencl/src/lib.rs b/crates/zune-opencl/src/lib.rs new file mode 100644 index 00000000..5462f222 --- /dev/null +++ b/crates/zune-opencl/src/lib.rs @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2023. + * + * This software is free software; + * + * You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license + */ + +use zune_image::errors::ImageErrors; + +mod ocl_img; +pub mod ocl_sobel; + +fn propagate_ocl_error(error: ocl::Error) -> ImageErrors { + let message = format!("OCL_ERROR:\n{}", error); + ImageErrors::GenericString(message) +} diff --git a/crates/zune-opencl/src/ocl_img.rs b/crates/zune-opencl/src/ocl_img.rs new file mode 100644 index 00000000..663586e9 --- /dev/null +++ b/crates/zune-opencl/src/ocl_img.rs @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2023. + * + * This software is free software; + * + * You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license + */ diff --git a/crates/zune-opencl/src/ocl_sobel.rs b/crates/zune-opencl/src/ocl_sobel.rs new file mode 100644 index 00000000..3563363a --- /dev/null +++ b/crates/zune-opencl/src/ocl_sobel.rs @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2023. + * + * This software is free software; + * + * You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license + */ + +use std::sync::Mutex; + +use ocl::{OclPrm, ProQue}; +use zune_core::bit_depth::BitType; +use zune_image::channel::Channel; +use zune_image::errors::ImageErrors; +use zune_image::image::Image; + +use crate::propagate_ocl_error; + +unsafe fn ocl_deriv_generic( + ocl_pq: &ocl::ProQue, name: &'static str, ref_channel: &Channel, mut_channel: &mut Channel, + dims: (usize, usize) +) -> Result<(), ImageErrors> { + // Create input and output buffers + let input_image: ocl::Buffer = ocl_pq + .buffer_builder() + .flags(ocl::MemFlags::READ_ONLY) + .build() + .map_err(propagate_ocl_error)?; + + input_image + .write(ref_channel.reinterpret_as()?) + .enq() + .map_err(propagate_ocl_error)?; + // define output image + let output_image: ocl::Buffer = ocl_pq + .buffer_builder() + .flags(ocl::MemFlags::WRITE_ONLY) + .build() + .map_err(propagate_ocl_error)?; + + // Define the constant value + // Enqueue the kernel + ocl_pq + .kernel_builder(name) + .arg(&input_image) + .arg(&output_image) + .arg(dims.0 as i32) + .arg(dims.1 as i32) + .build() + .map_err(propagate_ocl_error)? + .enq() + .map_err(propagate_ocl_error)?; + + output_image + .read(mut_channel.reinterpret_as_mut()?) + .enq() + .map_err(propagate_ocl_error)?; + + Ok(()) +} + +/// Sobel OpenCL filter. +/// +/// # Warning +/// This filter may be slower than the normal `Sobel` filter +/// please and it may not work on all +/// platforms. +/// +/// Please test/benchmark before using this filter +/// +/// +/// # Example +/// ``` +/// use zune_core::colorspace::ColorSpace; +/// use zune_image::traits::OperationsTrait; +/// use zune_opencl::ocl_sobel::OclSobel; +/// // create an image with color type RGB 100x1000 +/// let mut image = zune_image::image::Image::fill(100_u8, +/// ColorSpace::RGB, 100, 100).unwrap(); +/// // execute +/// OclSobel::try_new().unwrap().execute(&mut image).unwrap(); +/// +/// ``` +pub struct OclSobel { + // protect by mutex in oder to get + // interior mutability, we need to set_dims + // in execute_impl which takes an immutable + // reference + pq: std::sync::Mutex +} + +impl OclSobel { + /// Try to create a new sobel filter. + /// + /// This invokes the opencl compiler and it's done + /// outside init to allow OclSobel to be reused on multiple images + /// without recompiling the kernel. + /// + /// # Returns. + /// - Ok(OclSobel): OpenCL sobel kernel runner. + /// - Err(e): Compiling opencl kernel raised an error. or for some reason + /// we can't build + pub fn try_new() -> Result { + let ocl_pq = ProQue::builder() + .src(include_str!("./open_cl/ocl_sobel.cl")) + .build() + .map_err(propagate_ocl_error)?; + + Ok(OclSobel { + pq: Mutex::new(ocl_pq) + }) + } +} + +impl zune_image::traits::OperationsTrait for OclSobel { + fn get_name(&self) -> &'static str { + "OCL Sobel" + } + + fn execute_impl(&self, image: &mut Image) -> Result<(), ImageErrors> { + let depth = image.get_depth(); + let dims = image.get_dimensions(); + let b_type = image.get_depth(); + + let mut ocl_pq = self.pq.lock().map_err(|x| { + let message = format!("Could not unlock mutex:\n{}", x); + ImageErrors::GenericString(message) + })?; + + ocl_pq.set_dims(dims); + + for channel in image.get_channels_mut(true) { + let mut mut_channel = Channel::new_with_bit_type(channel.len(), b_type.bit_type()); + unsafe { + match depth.bit_type() { + BitType::U8 => { + ocl_deriv_generic::( + &ocl_pq, + "SobelFilterU8", + channel, + &mut mut_channel, + dims + )?; + } + BitType::U16 => { + ocl_deriv_generic::( + &ocl_pq, + "SobelFilterU16", + channel, + &mut mut_channel, + dims + )?; + } + BitType::F32 => { + ocl_deriv_generic::( + &ocl_pq, + "SobelFilterF32", + channel, + &mut mut_channel, + dims + )?; + } + + d => { + return Err(ImageErrors::ImageOperationNotImplemented( + self.get_name(), + d + )) + } + } + } + *channel = mut_channel; + } + + Ok(()) + } + + fn supported_types(&self) -> &'static [BitType] { + &[BitType::U8, BitType::U16, BitType::F32] + } +} + +#[test] +fn test_ocr_sobel() { + use zune_core::colorspace::ColorSpace; + use zune_image::traits::OperationsTrait; + // test for all supported bit types and whether they work. + let mut image = Image::fill(100_u8, ColorSpace::RGB, 100, 100).unwrap(); + let ocl_sobel = OclSobel::try_new().unwrap(); + + for d_type in ocl_sobel.supported_types() { + image.convert_depth(d_type.to_depth()).unwrap(); + ocl_sobel.clone_and_execute(&image).unwrap(); + } +} diff --git a/crates/zune-opencl/src/open_cl/ocl_brighten.cl b/crates/zune-opencl/src/open_cl/ocl_brighten.cl new file mode 100644 index 00000000..fac4ca7a --- /dev/null +++ b/crates/zune-opencl/src/open_cl/ocl_brighten.cl @@ -0,0 +1,54 @@ +__kernel void brighten_u8( + global uchar* inputImage, + const uchar num, + const int width, + const int height) +{ + int x = get_global_id(0); + int y = get_global_id(1); + + if (x < width && y < height) + { + int index = y * width + x; + uchar pixel = inputImage[index]; + uchar result = clamp(pixel + num, 0, 255); + + inputImage[index] = result; + } +} +__kernel void brighten_u16( + global ushort* inputImage, + const ushort num, + const int width, + const int height) +{ + int x = get_global_id(0); + int y = get_global_id(1); + + if (x < width && y < height) + { + int index = y * width + x; + ushort pixel = inputImage[index]; + ushort result = clamp(pixel + num, 0, 65535); + + inputImage[index] = result; + } +} +__kernel void brighten_f32( + global float* inputImage, + const float num, + const int width, + const int height) +{ + int x = get_global_id(0); + int y = get_global_id(1); + + if (x < width && y < height) + { + int index = y * width + x; + float pixel = inputImage[index]; + float result = clamp(pixel + num, 0.0f, 1.0f); + + inputImage[index] = result; + } +} \ No newline at end of file diff --git a/crates/zune-opencl/src/open_cl/ocl_sobel.cl b/crates/zune-opencl/src/open_cl/ocl_sobel.cl new file mode 100644 index 00000000..b49507de --- /dev/null +++ b/crates/zune-opencl/src/open_cl/ocl_sobel.cl @@ -0,0 +1,140 @@ + +__kernel void SobelFilterU8( + global uchar* inputImage, + global uchar* outputImage, + const int width, + const int height + ) +{ + // This is the currently focused pixel and is the output pixel + // location + int2 ImageCoordinate = (int2)(get_global_id(0), get_global_id(1)); + + if (ImageCoordinate.x < width && ImageCoordinate.y < height) + { + // clamp to ensure that reads and writes never go out of place + int x = min(max(ImageCoordinate.x,1),width-1); + int y = min(max(ImageCoordinate.y,1),height-1); + + // Read the 8 pixels around the currently focused pixel + uint Pixel00 = (uint)inputImage[((y - 1) * width) + (x - 1)]; + uint Pixel01 = (uint)inputImage[((y - 1) * width) + (x + 0)]; + uint Pixel02 = (uint)inputImage[((y - 1) * width) + (x + 1)]; + + uint Pixel10 = (uint)inputImage[((y + 0) * width) + (x - 1)]; + uint Pixel11 = (uint)inputImage[((y + 0) * width) + (x + 0)]; + uint Pixel12 = (uint)inputImage[((y + 0) * width) + (x + 1)]; + + uint Pixel20 = (uint)inputImage[((y + 1) * width) + (x - 1)]; + uint Pixel21 = (uint)inputImage[((y + 1) * width) + (x + 0)]; + uint Pixel22 = (uint)inputImage[((y + 1) * width) + (x + 1)]; + + // This is equivalent to looping through the 9 pixels + // under this convolution and applying the appropriate + // filter, here we've already applied the filter coefficients + // since they are static + uint Gx = Pixel00 + (2 * Pixel10) + Pixel20 - + Pixel02 - (2 * Pixel12) - Pixel22; + + uint Gy = Pixel00 + (2 * Pixel01) + Pixel02 - + Pixel20 - (2 * Pixel21) - Pixel22; + + // Compute the gradient magnitude + uint OutColor = (uint)sqrt((float)(Gx * Gx + Gy * Gy)); // R + + // Write the RGB value to the output image + outputImage[((ImageCoordinate.y + 0) * width) + (ImageCoordinate.x + 0)]= OutColor; + } +} + + +__kernel void SobelFilterU16( + global ushort* inputImage, + global ushort* outputImage, + const int width, + const int height + ) +{ + int2 ImageCoordinate = (int2)(get_global_id(0), get_global_id(1)); + + // Make sure we are within the image bounds + if (ImageCoordinate.x < width && ImageCoordinate.y < height) + { + // clamp to ensure that reads and writes never go out of place + int x = min(max(ImageCoordinate.x,1),width-1); + int y = min(max(ImageCoordinate.y,1),height-1); + + // Read the 6 pixels around the currently focused pixel + uint Pixel00 = (uint)inputImage[((y - 1) * width) + (x - 1)]; + uint Pixel01 = (uint)inputImage[((y - 1) * width) + (x + 0)]; + uint Pixel02 = (uint)inputImage[((y - 1) * width) + (x + 1)]; + + uint Pixel10 = (uint)inputImage[((y + 0) * width) + (x - 1)]; + uint Pixel11 = (uint)inputImage[((y + 0) * width) + (x + 0)]; + uint Pixel12 = (uint)inputImage[((y + 0) * width) + (x + 1)]; + + uint Pixel20 = (uint)inputImage[((y + 1) * width) + (x - 1)]; + uint Pixel21 = (uint)inputImage[((y + 1) * width) + (x + 0)]; + uint Pixel22 = (uint)inputImage[((y + 1) * width) + (x + 1)]; + + uint Gx = Pixel00 + (2 * Pixel10) + Pixel20 - + Pixel02 - (2 * Pixel12) - Pixel22; + + uint Gy = Pixel00 + (2 * Pixel01) + Pixel02 - + Pixel20 - (2 * Pixel21) - Pixel22; + + // Compute the gradient magnitude + uint OutColor = (uint)sqrt((float)(Gx * Gx + Gy * Gy)); // R + + // Write the RGB value to the output image + outputImage[((y + 0) * width) + (x + 0)]= OutColor; + } +} + +__kernel void SobelFilterF32( + global float* inputImage, + global float* outputImage, + const int width, + const int height + ) +{ + // This is the currently focused pixel and is the output pixel + // location + int2 ImageCoordinate = (int2)(get_global_id(0), get_global_id(1)); + + if (ImageCoordinate.x < width && ImageCoordinate.y < height) + { + // clamp to ensure that reads and writes never go out of place + int x = min(max(ImageCoordinate.x,1),width-1); + int y = min(max(ImageCoordinate.y,1),height-1); + + // Read the 8 pixels around the currently focused pixel + float Pixel00 = inputImage[((y - 1) * width) + (x - 1)]; + float Pixel01 = inputImage[((y - 1) * width) + (x + 0)]; + float Pixel02 = inputImage[((y - 1) * width) + (x + 1)]; + + float Pixel10 = inputImage[((y + 0) * width) + (x - 1)]; + float Pixel11 = inputImage[((y + 0) * width) + (x + 0)]; + float Pixel12 = inputImage[((y + 0) * width) + (x + 1)]; + + float Pixel20 = inputImage[((y + 1) * width) + (x - 1)]; + float Pixel21 = inputImage[((y + 1) * width) + (x + 0)]; + float Pixel22 = inputImage[((y + 1) * width) + (x + 1)]; + + // This is equivalent to looping through the 9 pixels + // under this convolution and applying the appropriate + // filter, here we've already applied the filter coefficients + // since they are static + float Gx = Pixel00 + (2 * Pixel10) + Pixel20 - + Pixel02 - (2 * Pixel12) - Pixel22; + + float Gy = Pixel00 + (2 * Pixel01) + Pixel02 - + Pixel20 - (2 * Pixel21) - Pixel22; + + // Compute the gradient magnitude + float OutColor = sqrt((float)(Gx * Gx + Gy * Gy)); // R + + // Write the RGB value to the output image + outputImage[((y + 0) * width) + (x + 0)]= OutColor; + } +} \ No newline at end of file diff --git a/crates/zune-python/Cargo.toml b/crates/zune-python/Cargo.toml index 50712a6a..011c47fc 100644 --- a/crates/zune-python/Cargo.toml +++ b/crates/zune-python/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] -name = "zune_image" +name = "zil" crate-type = ["cdylib"] [dependencies] @@ -14,4 +14,5 @@ zune-png = { path = "../zune-png" } zune-jpeg = { path = "../zune-jpeg" } zune-image = { path = "../zune-image" } zune-core = { path = "../zune-core" } +zune-imageprocs = { path = "../zune-imageprocs",features = ["threads"] } numpy = "0.20.0" \ No newline at end of file diff --git a/crates/zune-python/src/lib.rs b/crates/zune-python/src/lib.rs index 973f339f..bbd34c9b 100644 --- a/crates/zune-python/src/lib.rs +++ b/crates/zune-python/src/lib.rs @@ -10,7 +10,7 @@ use py_functions::*; use py_image::*; use pyo3::prelude::*; -use crate::py_enums::{PyImageColorSpace, PyImageDepth, PyImageFormats, PyImageThresholdType}; +use crate::py_enums::{ZImageColorSpace, ZImageDepth, ZImageFormats, ZImageThresholdType}; mod py_enums; mod py_functions; @@ -18,13 +18,13 @@ mod py_image; /// A Python module implemented in Rust. #[pymodule] -#[pyo3(name = "zune_image")] +#[pyo3(name = "zil")] fn zune_image(_py: Python, m: &PyModule) -> PyResult<()> { - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; m.add_function(wrap_pyfunction!(guess_format, m)?)?; m.add_function(wrap_pyfunction!(decode_image, m)?)?; diff --git a/crates/zune-python/src/py_enums.rs b/crates/zune-python/src/py_enums.rs index 06a56cf0..123d131a 100644 --- a/crates/zune-python/src/py_enums.rs +++ b/crates/zune-python/src/py_enums.rs @@ -11,23 +11,23 @@ use zune_core::bit_depth::BitDepth; use zune_core::colorspace::ColorSpace; use zune_image::codecs::ImageFormat; use zune_image::errors::ImageErrors; -use zune_image::filters::threshold::ThresholdMethod; +use zune_imageprocs::threshold::ThresholdMethod; #[pyclass] -pub struct PyImageErrors { +pub struct ZImageErrors { pub(crate) error: zune_image::errors::ImageErrors } -impl From for PyImageErrors { +impl From for ZImageErrors { fn from(value: ImageErrors) -> Self { - PyImageErrors { error: value } + ZImageErrors { error: value } } } #[pyclass] #[allow(non_camel_case_types)] #[derive(Copy, Clone)] -pub enum PyImageFormats { +pub enum ZImageFormats { PNG, JPEG, BMP, @@ -40,19 +40,19 @@ pub enum PyImageFormats { Unknown } -impl PyImageFormats { +impl ZImageFormats { pub fn to_imageformat(self) -> ImageFormat { match self { - PyImageFormats::PNG => ImageFormat::PNG, - PyImageFormats::JPEG => ImageFormat::JPEG, - PyImageFormats::BMP => ImageFormat::BMP, - PyImageFormats::PPM => ImageFormat::PPM, - PyImageFormats::PSD => ImageFormat::PSD, - PyImageFormats::FarbFeld => ImageFormat::Farbfeld, - PyImageFormats::Qoi => ImageFormat::QOI, - PyImageFormats::JPEG_XL => ImageFormat::JPEG_XL, - PyImageFormats::HDR => ImageFormat::HDR, - PyImageFormats::Unknown => ImageFormat::Unknown + ZImageFormats::PNG => ImageFormat::PNG, + ZImageFormats::JPEG => ImageFormat::JPEG, + ZImageFormats::BMP => ImageFormat::BMP, + ZImageFormats::PPM => ImageFormat::PPM, + ZImageFormats::PSD => ImageFormat::PSD, + ZImageFormats::FarbFeld => ImageFormat::Farbfeld, + ZImageFormats::Qoi => ImageFormat::QOI, + ZImageFormats::JPEG_XL => ImageFormat::JPEG_XL, + ZImageFormats::HDR => ImageFormat::HDR, + ZImageFormats::Unknown => ImageFormat::Unknown } } /// Return true if an image format has an encoder @@ -67,27 +67,27 @@ impl PyImageFormats { } } -impl From for PyImageFormats { +impl From for ZImageFormats { fn from(value: ImageFormat) -> Self { return match value { - ImageFormat::JPEG => PyImageFormats::JPEG, - ImageFormat::PNG => PyImageFormats::PNG, - ImageFormat::PPM => PyImageFormats::PPM, - ImageFormat::PSD => PyImageFormats::PSD, - ImageFormat::Farbfeld => PyImageFormats::FarbFeld, - ImageFormat::QOI => PyImageFormats::Qoi, - ImageFormat::JPEG_XL => PyImageFormats::JPEG_XL, - ImageFormat::HDR => PyImageFormats::HDR, - ImageFormat::BMP => PyImageFormats::BMP, - ImageFormat::Unknown => PyImageFormats::Unknown, - _ => PyImageFormats::Unknown + ImageFormat::JPEG => ZImageFormats::JPEG, + ImageFormat::PNG => ZImageFormats::PNG, + ImageFormat::PPM => ZImageFormats::PPM, + ImageFormat::PSD => ZImageFormats::PSD, + ImageFormat::Farbfeld => ZImageFormats::FarbFeld, + ImageFormat::QOI => ZImageFormats::Qoi, + ImageFormat::JPEG_XL => ZImageFormats::JPEG_XL, + ImageFormat::HDR => ZImageFormats::HDR, + ImageFormat::BMP => ZImageFormats::BMP, + ImageFormat::Unknown => ZImageFormats::Unknown, + _ => ZImageFormats::Unknown }; } } #[pyclass] #[derive(Copy, Clone)] -pub enum PyImageColorSpace { +pub enum ZImageColorSpace { RGB, RGBA, Luma, @@ -100,69 +100,69 @@ pub enum PyImageColorSpace { Unknown } -impl PyImageColorSpace { +impl ZImageColorSpace { pub(crate) fn to_colorspace(self) -> ColorSpace { match self { - PyImageColorSpace::RGB => ColorSpace::RGB, - PyImageColorSpace::RGBA => ColorSpace::RGBA, - PyImageColorSpace::Luma => ColorSpace::Luma, - PyImageColorSpace::LumaA => ColorSpace::LumaA, - PyImageColorSpace::Unexposed => ColorSpace::Unknown, - PyImageColorSpace::YCbCr => ColorSpace::YCbCr, - PyImageColorSpace::BGR => ColorSpace::BGR, - PyImageColorSpace::BGRA => ColorSpace::BGRA, - PyImageColorSpace::CMYK => ColorSpace::CMYK, - PyImageColorSpace::Unknown => ColorSpace::Unknown + ZImageColorSpace::RGB => ColorSpace::RGB, + ZImageColorSpace::RGBA => ColorSpace::RGBA, + ZImageColorSpace::Luma => ColorSpace::Luma, + ZImageColorSpace::LumaA => ColorSpace::LumaA, + ZImageColorSpace::Unexposed => ColorSpace::Unknown, + ZImageColorSpace::YCbCr => ColorSpace::YCbCr, + ZImageColorSpace::BGR => ColorSpace::BGR, + ZImageColorSpace::BGRA => ColorSpace::BGRA, + ZImageColorSpace::CMYK => ColorSpace::CMYK, + ZImageColorSpace::Unknown => ColorSpace::Unknown } } } -impl From for PyImageColorSpace { +impl From for ZImageColorSpace { fn from(value: ColorSpace) -> Self { return match value { - ColorSpace::RGB => PyImageColorSpace::RGB, - ColorSpace::RGBA => PyImageColorSpace::RGBA, - ColorSpace::YCbCr => PyImageColorSpace::YCbCr, - ColorSpace::Luma => PyImageColorSpace::Luma, - ColorSpace::LumaA => PyImageColorSpace::LumaA, - ColorSpace::YCCK => PyImageColorSpace::Unexposed, - ColorSpace::CMYK => PyImageColorSpace::CMYK, - ColorSpace::BGR => PyImageColorSpace::BGR, - ColorSpace::BGRA => PyImageColorSpace::BGRA, - ColorSpace::Unknown => PyImageColorSpace::Unknown, - _ => PyImageColorSpace::Unknown + ColorSpace::RGB => ZImageColorSpace::RGB, + ColorSpace::RGBA => ZImageColorSpace::RGBA, + ColorSpace::YCbCr => ZImageColorSpace::YCbCr, + ColorSpace::Luma => ZImageColorSpace::Luma, + ColorSpace::LumaA => ZImageColorSpace::LumaA, + ColorSpace::YCCK => ZImageColorSpace::Unexposed, + ColorSpace::CMYK => ZImageColorSpace::CMYK, + ColorSpace::BGR => ZImageColorSpace::BGR, + ColorSpace::BGRA => ZImageColorSpace::BGRA, + ColorSpace::Unknown => ZImageColorSpace::Unknown, + _ => ZImageColorSpace::Unknown }; } } #[pyclass] #[derive(Copy, Clone, Debug)] -pub enum PyImageDepth { - Eight, - Sixteen, +pub enum ZImageDepth { + U8, + U16, F32, Unknown } -impl PyImageDepth { +impl ZImageDepth { pub(crate) fn to_depth(self) -> BitDepth { match self { - PyImageDepth::Eight => BitDepth::Eight, - PyImageDepth::Sixteen => BitDepth::Sixteen, - PyImageDepth::F32 => BitDepth::Float32, - PyImageDepth::Unknown => BitDepth::Unknown + ZImageDepth::U8 => BitDepth::Eight, + ZImageDepth::U16 => BitDepth::Sixteen, + ZImageDepth::F32 => BitDepth::Float32, + ZImageDepth::Unknown => BitDepth::Unknown } } } -impl From for PyImageDepth { +impl From for ZImageDepth { fn from(value: BitDepth) -> Self { match value { - BitDepth::Eight => PyImageDepth::Eight, - BitDepth::Sixteen => PyImageDepth::Sixteen, - BitDepth::Float32 => PyImageDepth::F32, - BitDepth::Unknown => PyImageDepth::Unknown, - _ => PyImageDepth::Unknown + BitDepth::Eight => ZImageDepth::U8, + BitDepth::Sixteen => ZImageDepth::U16, + BitDepth::Float32 => ZImageDepth::F32, + BitDepth::Unknown => ZImageDepth::Unknown, + _ => ZImageDepth::Unknown } } } @@ -170,20 +170,20 @@ impl From for PyImageDepth { /// Different threshold arguments for the threshold parameter #[pyclass] #[derive(Copy, Clone, Debug)] -pub enum PyImageThresholdType { +pub enum ZImageThresholdType { Binary, BinaryInv, ThreshTrunc, ThreshToZero } -impl PyImageThresholdType { +impl ZImageThresholdType { pub(crate) fn to_threshold(self) -> ThresholdMethod { match self { - PyImageThresholdType::Binary => ThresholdMethod::Binary, - PyImageThresholdType::BinaryInv => ThresholdMethod::BinaryInv, - PyImageThresholdType::ThreshTrunc => ThresholdMethod::ThreshTrunc, - PyImageThresholdType::ThreshToZero => ThresholdMethod::ThreshToZero + ZImageThresholdType::Binary => ThresholdMethod::Binary, + ZImageThresholdType::BinaryInv => ThresholdMethod::BinaryInv, + ZImageThresholdType::ThreshTrunc => ThresholdMethod::ThreshTrunc, + ZImageThresholdType::ThreshToZero => ThresholdMethod::ThreshToZero } } } diff --git a/crates/zune-python/src/py_functions.rs b/crates/zune-python/src/py_functions.rs index 33ac23e0..3bd849d7 100644 --- a/crates/zune-python/src/py_functions.rs +++ b/crates/zune-python/src/py_functions.rs @@ -8,16 +8,18 @@ use pyo3::prelude::*; -use crate::py_enums::PyImageFormats; +use crate::py_enums::ZImageFormats; /// Guess an image format from bytes /// /// # Arguments /// bytes: An array of bytes consisting of an encoded image #[pyfunction] -pub fn guess_format(bytes: &[u8]) -> PyResult { +pub fn guess_format(bytes: &[u8]) -> PyResult { match zune_image::codecs::guess_format(bytes) { - Some((format, _)) => Ok(PyImageFormats::from(format)), - None => Ok(PyImageFormats::Unknown) + Some((format, _)) => Ok(ZImageFormats::from(format)), + None => Ok(ZImageFormats::Unknown) } } + + diff --git a/crates/zune-python/src/py_image.rs b/crates/zune-python/src/py_image.rs index b1d191bb..471208ed 100644 --- a/crates/zune-python/src/py_image.rs +++ b/crates/zune-python/src/py_image.rs @@ -9,44 +9,44 @@ mod numpy_bindings; use std::fs::read; -use numpy::PyArray3; +use numpy::{PyUntypedArray}; use pyo3::exceptions::PyException; use pyo3::prelude::*; -use zune_image::filters::box_blur::BoxBlur; -use zune_image::filters::crop::Crop; -use zune_image::filters::exposure::Exposure; -use zune_image::filters::flip::Flip; -use zune_image::filters::flop::Flop; -use zune_image::filters::gamma::Gamma; -use zune_image::filters::gaussian_blur::GaussianBlur; -use zune_image::filters::invert::Invert; -use zune_image::filters::orientation::AutoOrient; -use zune_image::filters::sobel::Sobel; -use zune_image::filters::stretch_contrast::StretchContrast; -use zune_image::filters::threshold::Threshold; -use zune_image::filters::transpose::Transpose; +use zune_core::bit_depth::BitType; +use zune_imageprocs::box_blur::BoxBlur; +use zune_imageprocs::crop::Crop; +use zune_imageprocs::exposure::Exposure; +use zune_imageprocs::flip::Flip; +use zune_imageprocs::flop::Flop; +use zune_imageprocs::gamma::Gamma; +use zune_imageprocs::gaussian_blur::GaussianBlur; +use zune_imageprocs::invert::Invert; +use zune_imageprocs::sobel::Sobel; +use zune_imageprocs::stretch_contrast::StretchContrast; +use zune_imageprocs::threshold::Threshold; +use zune_imageprocs::transpose::Transpose; use zune_image::image::Image; use zune_image::traits::OperationsTrait; use zune_png::zune_core::options::DecoderOptions; use crate::py_enums::{ - PyImageColorSpace, PyImageDepth, PyImageErrors, PyImageFormats, PyImageThresholdType + ZImageColorSpace, ZImageDepth, ZImageErrors, ZImageFormats, ZImageThresholdType, }; #[pyclass] #[derive(Clone)] -pub struct PyImage { - image: Image +pub struct ZImage { + image: Image, } -impl PyImage { - pub(crate) fn new(image: Image) -> PyImage { - return PyImage { image }; +impl ZImage { + pub(crate) fn new(image: Image) -> ZImage { + return ZImage { image }; } } #[pymethods] -impl PyImage { +impl ZImage { /// Get the image or the first frame of an image /// as a Python list. /// @@ -60,10 +60,10 @@ impl PyImage { pub fn to_u8_2d(&self) -> Vec> { return self.image.flatten_to_u8(); } - pub fn format(&self) -> PyImageFormats { - match self.image.get_metadata().get_image_format() { - Some(format) => PyImageFormats::from(format), - None => PyImageFormats::Unknown + pub fn format(&self) -> ZImageFormats { + match self.image.metadata().get_image_format() { + Some(format) => ZImageFormats::from(format), + None => ZImageFormats::Unknown } } /// Get the image dimensions as a tuple of width and height @@ -93,9 +93,9 @@ impl PyImage { /// - The current image colorspace /// /// # See - /// - [convert_colorspace](PyImage::convert_colorspace) : Convert from one colorspace to another - pub fn colorspace(&self) -> PyImageColorSpace { - PyImageColorSpace::from(self.image.get_colorspace()) + /// - [convert_colorspace](ZImage::convert_colorspace) : Convert from one colorspace to another + pub fn colorspace(&self) -> ZImageColorSpace { + ZImageColorSpace::from(self.image.get_colorspace()) } /// Convert from one colorspace to another /// @@ -108,11 +108,11 @@ impl PyImage { /// - If `in_place=False`: An image copy on success on error, returns error that occurred #[pyo3(signature = (to, in_place = false))] pub fn convert_colorspace( - &mut self, to: PyImageColorSpace, in_place: bool - ) -> PyResult> { + &mut self, to: ZImageColorSpace, in_place: bool, + ) -> PyResult> { let color = to.to_colorspace(); - let exec = |image: &mut PyImage| -> PyResult<()> { + let exec = |image: &mut ZImage| -> PyResult<()> { if let Err(e) = image.image.convert_color(color) { return Err(PyErr::new::(format!( "Error converting: {:?}", @@ -142,8 +142,8 @@ impl PyImage { /// - Sixteen: u16 (2 bytes per pixel) /// - F32: Float f32 (4 bytes per pixel, float type) /// - pub fn depth(&self) -> PyImageDepth { - PyImageDepth::from(self.image.get_depth()) + pub fn depth(&self) -> ZImageDepth { + ZImageDepth::from(self.image.get_depth()) } /// Save an image to a format /// @@ -157,7 +157,7 @@ impl PyImage { /// /// # Returns /// - Nothing on success, or Exception on error - pub fn save(&self, file: String, format: PyImageFormats) -> PyResult<()> { + pub fn save(&self, file: String, format: ZImageFormats) -> PyResult<()> { if let Err(e) = self.image.save_to(file, format.to_imageformat()) { return Err(PyErr::new::(format!( "Error encoding: {:?}", @@ -181,12 +181,12 @@ impl PyImage { /// #[pyo3(signature = (width, height, x, y, in_place = false))] pub fn crop( - &mut self, width: usize, height: usize, x: usize, y: usize, in_place: bool - ) -> PyResult> { - let exec = |image: &mut PyImage| -> PyResult<()> { + &mut self, width: usize, height: usize, x: usize, y: usize, in_place: bool, + ) -> PyResult> { + let exec = |image: &mut ZImage| -> PyResult<()> { Crop::new(width, height, x, y) .execute(&mut image.image) - .map_err(|x| PyImageErrors::from(x))?; + .map_err(|x| ZImageErrors::from(x))?; Ok(()) }; return if in_place { @@ -206,11 +206,11 @@ impl PyImage { /// - inplace: Whether to transpose the image in place or generate a clone /// and transpose the new clone #[pyo3(signature = (in_place = false))] - pub fn transpose(&mut self, in_place: bool) -> PyResult> { - let exec = |image: &mut PyImage| -> PyResult<()> { + pub fn transpose(&mut self, in_place: bool) -> PyResult> { + let exec = |image: &mut ZImage| -> PyResult<()> { Transpose::new() .execute(&mut image.image) - .map_err(|x| PyImageErrors::from(x))?; + .map_err(|x| ZImageErrors::from(x))?; Ok(()) }; return if in_place { @@ -240,10 +240,10 @@ impl PyImage { /// - If `in_place=True`: Nothing on success, on error returns error that occurred /// - If `in_place=False`: An image copy on success on error, returns error that occurred #[pyo3(signature = (to, in_place = false))] - pub fn convert_depth(&mut self, to: PyImageDepth, in_place: bool) -> PyResult> { + pub fn convert_depth(&mut self, to: ZImageDepth, in_place: bool) -> PyResult> { let color = to.to_depth(); - let exec = |image: &mut PyImage| -> PyResult<()> { + let exec = |image: &mut ZImage| -> PyResult<()> { if let Err(e) = image.image.convert_depth(color) { return Err(PyErr::new::(format!( "Error converting: {:?}", @@ -277,12 +277,11 @@ impl PyImage { /// # Returns /// - If `in_place=True`: Nothing on success, on error returns error that occurred /// - If `in_place=False`: An image copy on success on error, returns error that occurred - - #[pyo3(signature = (value, method = PyImageThresholdType::Binary, in_place = false))] + #[pyo3(signature = (value, method = ZImageThresholdType::Binary, in_place = false))] pub fn threshold( - &mut self, value: f32, method: PyImageThresholdType, in_place: bool - ) -> PyResult> { - let exec = |image: &mut PyImage| -> PyResult<()> { + &mut self, value: f32, method: ZImageThresholdType, in_place: bool, + ) -> PyResult> { + let exec = |image: &mut ZImage| -> PyResult<()> { if let Err(e) = Threshold::new(value, method.to_threshold()).execute(&mut image.image) { return Err(PyErr::new::(format!( "Error converting: {:?}", @@ -311,8 +310,8 @@ impl PyImage { /// - If `in_place=True`: Nothing on success, on error returns error that occurred /// - If `in_place=False`: An image copy on success on error, returns error that occurred #[pyo3(signature = (in_place = false))] - pub fn invert(&mut self, in_place: bool) -> PyResult> { - let exec = |image: &mut PyImage| -> PyResult<()> { + pub fn invert(&mut self, in_place: bool) -> PyResult> { + let exec = |image: &mut ZImage| -> PyResult<()> { if let Err(e) = Invert::new().execute(&mut image.image) { return Err(PyErr::new::(format!( "Error converting: {:?}", @@ -342,8 +341,8 @@ impl PyImage { /// - If `in_place=True`: Nothing on success, on error returns error that occurred /// - If `in_place=False`: An image copy on success on error, returns error that occurred #[pyo3(signature = (radius, in_place = false))] - pub fn box_blur(&mut self, radius: usize, in_place: bool) -> PyResult> { - let exec = |image: &mut PyImage| -> PyResult<()> { + pub fn box_blur(&mut self, radius: usize, in_place: bool) -> PyResult> { + let exec = |image: &mut ZImage| -> PyResult<()> { if let Err(e) = BoxBlur::new(radius).execute(&mut image.image) { return Err(PyErr::new::(format!( "Error converting: {:?}", @@ -378,9 +377,9 @@ impl PyImage { /// - If `in_place=False`: An image copy on success on error, returns error that occurred #[pyo3(signature = (exposure, black_point = 0.0, in_place = false))] pub fn exposure( - &mut self, exposure: f32, black_point: f32, in_place: bool - ) -> PyResult> { - let exec = |image: &mut PyImage| -> PyResult<()> { + &mut self, exposure: f32, black_point: f32, in_place: bool, + ) -> PyResult> { + let exec = |image: &mut ZImage| -> PyResult<()> { if let Err(e) = Exposure::new(exposure, black_point).execute(&mut image.image) { return Err(PyErr::new::(format!( "Error converting: {:?}", @@ -417,8 +416,8 @@ impl PyImage { /// - If `in_place=True`: Nothing on success, on error returns error that occurred /// - If `in_place=False`: An image copy on success on error, returns error that occurred #[pyo3(signature = (in_place = false))] - pub fn flip(&mut self, in_place: bool) -> PyResult> { - let exec = |image: &mut PyImage| -> PyResult<()> { + pub fn flip(&mut self, in_place: bool) -> PyResult> { + let exec = |image: &mut ZImage| -> PyResult<()> { if let Err(e) = Flip.execute(&mut image.image) { return Err(PyErr::new::(format!( "Error converting: {:?}", @@ -454,8 +453,8 @@ impl PyImage { /// - If `in_place=True`: Nothing on success, on error returns error that occurred /// - If `in_place=False`: An image copy on success on error, returns error that occurred #[pyo3(signature = (in_place = false))] - pub fn flop(&mut self, in_place: bool) -> PyResult> { - let exec = |image: &mut PyImage| -> PyResult<()> { + pub fn flop(&mut self, in_place: bool) -> PyResult> { + let exec = |image: &mut ZImage| -> PyResult<()> { if let Err(e) = Flop.execute(&mut image.image) { return Err(PyErr::new::(format!( "Error converting: {:?}", @@ -487,8 +486,8 @@ impl PyImage { /// - If `in_place=True`: Nothing on success, on error returns error that occurred /// - If `in_place=False`: An image copy on success on error, returns error that occurred #[pyo3(signature = (gamma, in_place = false))] - pub fn gamma(&mut self, gamma: f32, in_place: bool) -> PyResult> { - let exec = |image: &mut PyImage| -> PyResult<()> { + pub fn gamma(&mut self, gamma: f32, in_place: bool) -> PyResult> { + let exec = |image: &mut ZImage| -> PyResult<()> { if let Err(e) = Gamma::new(gamma).execute(&mut image.image) { return Err(PyErr::new::(format!( "Error converting: {:?}", @@ -519,8 +518,8 @@ impl PyImage { /// - If `in_place=True`: Nothing on success, on error returns error that occurred /// - If `in_place=False`: An image copy on success on error, returns error that occurred #[pyo3(signature = (sigma, in_place = false))] - pub fn gaussian_blur(&mut self, sigma: f32, in_place: bool) -> PyResult> { - let exec = |image: &mut PyImage| -> PyResult<()> { + pub fn gaussian_blur(&mut self, sigma: f32, in_place: bool) -> PyResult> { + let exec = |image: &mut ZImage| -> PyResult<()> { if let Err(e) = GaussianBlur::new(sigma).execute(&mut image.image) { return Err(PyErr::new::(format!( "Error converting: {:?}", @@ -541,33 +540,33 @@ impl PyImage { } } - /// Auto orient the image based on the exif metadata - /// - /// - /// This operation is also a no-op if the image does not have - /// exif metadata - #[pyo3(signature = (in_place = false))] - pub fn auto_orient(&mut self, in_place: bool) -> PyResult> { - let exec = |image: &mut PyImage| -> PyResult<()> { - if let Err(e) = AutoOrient.execute(&mut image.image) { - return Err(PyErr::new::(format!( - "Error converting: {:?}", - e - ))); - } - Ok(()) - }; - - if in_place { - exec(self)?; - Ok(None) - } else { - let mut im_clone = self.clone(); - exec(&mut im_clone)?; - - Ok(Some(im_clone)) - } - } + // /// Auto orient the image based on the exif metadata + // /// + // /// + // /// This operation is also a no-op if the image does not have + // /// exif metadata + // #[pyo3(signature = (in_place = false))] + // pub fn auto_orient(&mut self, in_place: bool) -> PyResult> { + // let exec = |image: &mut ZImage| -> PyResult<()> { + // if let Err(e) = AutoOrient.execute(&mut image.image) { + // return Err(PyErr::new::(format!( + // "Error converting: {:?}", + // e + // ))); + // } + // Ok(()) + // }; + // + // if in_place { + // exec(self)?; + // Ok(None) + // } else { + // let mut im_clone = self.clone(); + // exec(&mut im_clone)?; + // + // Ok(Some(im_clone)) + // } + // } /// Calculate the sobel derivative of an image /// @@ -589,8 +588,8 @@ impl PyImage { /// # Arguments /// - in-place: Whether to carry the operation in place or clone and operate on the copy #[pyo3(signature = (in_place = false))] - pub fn sobel(&mut self, in_place: bool) -> PyResult> { - let exec = |image: &mut PyImage| -> PyResult<()> { + pub fn sobel(&mut self, in_place: bool) -> PyResult> { + let exec = |image: &mut ZImage| -> PyResult<()> { if let Err(e) = Sobel.execute(&mut image.image) { return Err(PyErr::new::(format!( "Error converting: {:?}", @@ -631,8 +630,8 @@ impl PyImage { /// # Arguments /// - in-place: Whether to carry the operation in place or clone and operate on the copy #[pyo3(signature = (in_place = false))] - pub fn scharr(&mut self, in_place: bool) -> PyResult> { - let exec = |image: &mut PyImage| -> PyResult<()> { + pub fn scharr(&mut self, in_place: bool) -> PyResult> { + let exec = |image: &mut ZImage| -> PyResult<()> { if let Err(e) = Sobel.execute(&mut image.image) { return Err(PyErr::new::(format!( "Error converting: {:?}", @@ -656,19 +655,21 @@ impl PyImage { /// Linearly stretches the contrast in an image in place, /// sending lower to image minimum and upper to image maximum. /// - /// # Arguments + /// Arguments: + /// /// - lower: Lower minimum value for which pixels below this are clamped to the value /// - upper: Upper maximum value for which pixels above are clamped to the value /// /// - /// # Returns + /// Returns: + /// /// - If `in_place=True`: Nothing on success, on error returns error that occurred /// - If `in_place=False`: An image copy on success on error, returns error that occurred #[pyo3(signature = (lower, upper, in_place = false))] pub fn stretch_contrast( - &mut self, lower: u16, upper: u16, in_place: bool - ) -> PyResult> { - let exec = |image: &mut PyImage| -> PyResult<()> { + &mut self, lower: f32, upper: f32, in_place: bool, + ) -> PyResult> { + let exec = |image: &mut ZImage| -> PyResult<()> { if let Err(e) = StretchContrast::new(lower, upper).execute(&mut image.image) { return Err(PyErr::new::(format!( "Error converting: {:?}", @@ -688,22 +689,59 @@ impl PyImage { Ok(Some(im_clone)) } } - pub fn to_numpy_u8<'py>(&self, py: Python<'py>) -> PyResult<&'py PyArray3> { - self.to_numpy_generic(py, PyImageDepth::Eight) - } - pub fn to_numpy_u16<'py>(&self, py: Python<'py>) -> PyResult<&'py PyArray3> { - self.to_numpy_generic(py, PyImageDepth::Sixteen) - } - pub fn to_numpy_f32<'py>(&self, py: Python<'py>) -> PyResult<&'py PyArray3> { - self.to_numpy_generic(py, PyImageDepth::F32) + /// Convert the image bytes to a numpy array + /// + /// The array will always be a 3-D numpy array of + /// `[width,height,colorspace_components]` dimensions/ + /// This means that e.g for a 256x256 rgb image the result will be `[256,256,3]` dimensions + /// + /// Colorspace is important in determining output. + /// + /// RGB colorspace is arranged as `R`,`G`,`B` , BGR is arranged as `B`,`G`,`R` + /// + /// + /// Array type: + /// + /// The array type is determined by the image depths/ image bit-type + /// + /// The following mappings are considered. + /// + /// - ZImageDepth::Eight -> dtype=uint8 + /// - ZImageDepth::Sixteen -> dtype=uint16 + /// - ZimageDepth::F32 -> dtype=float32 + /// + /// + /// Returns: + /// + /// A numpy representation of the image if okay. + /// + /// An error in case something went wrong + pub fn to_numpy<'py>(&self, py: Python<'py>) -> PyResult<&'py PyUntypedArray> { + match self.image.get_depth().bit_type() { + BitType::U8 => { + Ok(self.to_numpy_generic::(py, ZImageDepth::U8)?.as_untyped()) + } + BitType::U16 => { + Ok(self.to_numpy_generic::(py, ZImageDepth::U16)?.as_untyped()) + } + BitType::F32 => { + Ok(self.to_numpy_generic::(py, ZImageDepth::F32)?.as_untyped()) + } + d => { + Err(PyErr::new::(format!( + "Error converting to depth {:?}", + d + ))) + } + } } } #[pyfunction] -pub fn decode_image(bytes: &[u8]) -> PyResult { +pub fn decode_image(bytes: &[u8]) -> PyResult { let im_result = Image::read(bytes, DecoderOptions::new_fast()); return match im_result { - Ok(result) => Ok(PyImage::new(result)), + Ok(result) => Ok(ZImage::new(result)), Err(err) => Err(PyErr::new::(format!( "Error decoding: {:?}", err @@ -711,18 +749,18 @@ pub fn decode_image(bytes: &[u8]) -> PyResult { }; } -impl From for pyo3::PyErr { - fn from(value: PyImageErrors) -> Self { +impl From for pyo3::PyErr { + fn from(value: ZImageErrors) -> Self { PyErr::new::(format!("{:?}", value.error)) } } /// Decode a file path containing an image #[pyfunction] -pub fn decode_file(file: String) -> PyResult { +pub fn decode_file(file: String) -> PyResult { return match read(file) { - Ok(bytes) => Ok(PyImage::new( - Image::read(bytes, DecoderOptions::new_fast()).map_err(|x| PyImageErrors::from(x))? + Ok(bytes) => Ok(ZImage::new( + Image::read(bytes, DecoderOptions::new_fast()).map_err(|x| ZImageErrors::from(x))? )), Err(e) => Err(PyErr::new::(format!("{}", e))) }; diff --git a/crates/zune-python/src/py_image/numpy_bindings.rs b/crates/zune-python/src/py_image/numpy_bindings.rs index 54de6f8d..661e837d 100644 --- a/crates/zune-python/src/py_image/numpy_bindings.rs +++ b/crates/zune-python/src/py_image/numpy_bindings.rs @@ -10,12 +10,12 @@ use numpy::PyArray3; use pyo3::exceptions::PyException; use pyo3::{PyErr, PyResult, Python}; -use crate::py_enums::PyImageDepth; -use crate::py_image::PyImage; +use crate::py_enums::ZImageDepth; +use crate::py_image::ZImage; -impl PyImage { +impl ZImage { pub(crate) fn to_numpy_generic<'py, T>( - &self, py: Python<'py>, expected: PyImageDepth + &self, py: Python<'py>, expected: ZImageDepth ) -> PyResult<&'py PyArray3> where T: Copy + Default + 'static + numpy::Element + Send diff --git a/crates/zune-wasm/Cargo.toml b/crates/zune-wasm/Cargo.toml index 9eb7376d..bf1befd9 100644 --- a/crates/zune-wasm/Cargo.toml +++ b/crates/zune-wasm/Cargo.toml @@ -15,6 +15,7 @@ default = ["console_error_panic_hook"] wasm-bindgen = "0.2.63" zune-image = { path = "../zune-image", features = ["image_formats"] } zune-core = { path = "../zune-core", version = "0.4" } +zune-imageprocs = { path = "../zune-imageprocs" } # The `console_error_panic_hook` crate provides better debugging of panics by # logging them with `console.error`. This is great for development, but requires # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for diff --git a/crates/zune-wasm/src/lib.rs b/crates/zune-wasm/src/lib.rs index 8e1f8303..d20c6ec5 100644 --- a/crates/zune-wasm/src/lib.rs +++ b/crates/zune-wasm/src/lib.rs @@ -14,15 +14,16 @@ use zune_core::log::{debug, error, info}; // use zune_core::colorspace::ColorSpace; use zune_image::codecs::ImageFormat; use zune_image::core_filters::depth::Depth; -use zune_image::filters::brighten::Brighten; -use zune_image::filters::contrast::Contrast; -use zune_image::filters::gamma::Gamma; -use zune_image::filters::invert::Invert; -use zune_image::filters::statistics::{StatisticOperations, StatisticsOps}; -use zune_image::filters::stretch_contrast::StretchContrast; -use zune_image::filters::threshold::{Threshold, ThresholdMethod}; use zune_image::image::Image; use zune_image::traits::OperationsTrait; +use zune_imageprocs::brighten::Brighten; +use zune_imageprocs::contrast::Contrast; +use zune_imageprocs::gamma::Gamma; +use zune_imageprocs::invert::Invert; +use zune_imageprocs::spatial::StatisticsOps; +use zune_imageprocs::spatial_ops::StatisticOperations; +use zune_imageprocs::stretch_contrast::StretchContrast; +use zune_imageprocs::threshold::{Threshold, ThresholdMethod}; use crate::enums::{WasmColorspace, WasmImageDecodeFormats}; use crate::utils::set_panic_hook; @@ -110,7 +111,7 @@ impl WasmImage { } /// Apply a contrast operation to the image - pub fn stretch_contrast(&mut self, lower: u16, upper: u16) { + pub fn stretch_contrast(&mut self, lower: f32, upper: f32) { let ops = StretchContrast::new(lower, upper); self.execute_ops(&ops); }