Skip to content

Commit

Permalink
zune: Move filters to zune-imageprocs and add opencl
Browse files Browse the repository at this point in the history
Just like 9e5d10c

but for some reason was dropped during refactorings(I may have done a git-reset
  • Loading branch information
etemesi254 committed Oct 21, 2023
1 parent 9230981 commit 0bd406f
Show file tree
Hide file tree
Showing 62 changed files with 3,622 additions and 384 deletions.
82 changes: 82 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
6 changes: 4 additions & 2 deletions crates/zune-bin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
10 changes: 8 additions & 2 deletions crates/zune-bin/src/cmd_args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -401,9 +401,15 @@ fn add_filters() -> (Vec<Arg>, 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)
Expand Down
23 changes: 15 additions & 8 deletions crates/zune-bin/src/cmd_parsers/filters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T: IntoImage>(
workflow: &mut WorkFlow<T>, argument: &str, args: &ArgMatches
Expand Down Expand Up @@ -77,7 +79,12 @@ pub fn parse_options<T: IntoImage>(
.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(())
Expand Down
38 changes: 19 additions & 19 deletions crates/zune-bin/src/cmd_parsers/operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -116,9 +116,9 @@ pub fn parse_options<T: IntoImage>(
)
} else if argument == "stretch_contrast" {
let values = args
.get_many::<u16>(argument)
.get_many::<f32>(argument)
.unwrap()
.collect::<Vec<&u16>>();
.collect::<Vec<&f32>>();

let lower = *values[0];

Expand Down Expand Up @@ -180,7 +180,7 @@ pub fn parse_options<T: IntoImage>(
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::<f32>(argument).unwrap();

Expand Down
8 changes: 3 additions & 5 deletions crates/zune-image/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 19 additions & 1 deletion crates/zune-image/src/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion crates/zune-image/src/codecs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions crates/zune-image/src/codecs/jpeg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}

Expand Down
60 changes: 27 additions & 33 deletions crates/zune-image/src/core_filters/colorspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<u8>(size);
alpha_out.reinterpret_as_mut::<u8>().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::<u8>(size);
alpha_out.reinterpret_as_mut::<u8>().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 => {
Expand All @@ -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::<u16>(size);
alpha_out
.reinterpret_as_mut::<u16>()
.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::<u16>(size);
alpha_out
.reinterpret_as_mut::<u16>()
.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 => {
Expand All @@ -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::<f32>(size);
alpha_out.reinterpret_as_mut::<f32>().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::<f32>(size);
alpha_out.reinterpret_as_mut::<f32>().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))
Expand Down
2 changes: 1 addition & 1 deletion crates/zune-image/src/core_filters/colorspace/grayscale.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion crates/zune-image/src/deinterleave/deinterleave_impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 0bd406f

Please sign in to comment.