Skip to content

Commit

Permalink
feat(preprocessors): implemented resize operation
Browse files Browse the repository at this point in the history
  • Loading branch information
SalOne22 committed Mar 1, 2024
1 parent 60d9aa5 commit 7aa016e
Show file tree
Hide file tree
Showing 9 changed files with 960 additions and 5 deletions.
614 changes: 614 additions & 0 deletions Cargo.lock

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,10 @@ opt-level = "z"
[dependencies]
anyhow = "1.0.80"
clap = { version = "4.5.1", features = ["cargo", "string"] }
fast_image_resize = "3.0.4"
indoc = "2.0.4"
log = "0.4.20"
pretty_env_logger = "0.5.0"
rayon = "1.8.1"
zune-core = "0.4.12"
zune-image = "0.4.15"
15 changes: 15 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
use cli::cli;
use preprocessors::pipeline::PreprocessorPipeline;
use zune_image::{image::Image, pipelines::Pipeline};

mod cli;
mod codecs;
mod preprocessors;
mod utils;

fn main() {
pretty_env_logger::init();

let matches = cli().get_matches();

if let Some(threads) = matches.get_one::<u8>("threads") {
rayon::ThreadPoolBuilder::new()
.num_threads(*threads as usize)
.build_global()
.unwrap();
}

let mut pipeline = Pipeline::<Image>::new();

let preprocessor_pipeline = PreprocessorPipeline::from_matches(&matches);
}
21 changes: 17 additions & 4 deletions src/preprocessors/mod.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
use clap::{arg, value_parser, ArgGroup, Command};
use clap::{arg, value_parser, ArgAction, ArgGroup, Command};
use indoc::indoc;

use resize::ResizeValue;

use crate::preprocessors::resize::ResizeFilter;
use crate::preprocessors::resize::{ResizeFilter, ResizeFit};

pub mod pipeline;
mod resize;

impl Preprocessors for Command {
fn preprocessors(self) -> Self {
self.group(ArgGroup::new("preprocessors").multiple(true))
self.group(
ArgGroup::new("preprocessors")
.args(["resize", "quantization"])
.multiple(true)
)
.next_help_heading("Preprocessors")
.arg(
arg!(--resize <RESIZE> "Resize the image(s) according to the specified criteria")
Expand All @@ -20,20 +25,28 @@ impl Preprocessors for Command {
- 150%: Adjust image size by this percentage
- 100x100: Resize image to these dimensions
- 200x_: Adjust image dimensions while maintaining the aspect ratio based on the specified dimension"})
.value_parser(value_parser!(ResizeValue)),
.value_parser(value_parser!(ResizeValue))
.action(ArgAction::Append),
)
.arg(
arg!(--filter <FILTER> "Filter that used when resizing an image")
.value_parser(value_parser!(ResizeFilter))
.default_value("lanczos3")
.requires("resize"),
)
.arg(
arg!(--fit <FIT> "Specifies how to fit image")
.value_parser(value_parser!(ResizeFit))
.default_value("stretch")
.requires("resize"),
)
.arg(
arg!(--quantization [QUALITY] "Enables quantization with optional quality")
.long_help(indoc! {"Enables quantization with optional quality
If quality is not provided default 75% quality is used"})
.value_parser(value_parser!(u8).range(1..=100))
.action(ArgAction::Append)
.default_missing_value("75")
)
.arg(
Expand Down
22 changes: 22 additions & 0 deletions src/preprocessors/pipeline.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use std::collections::BTreeMap;

use clap::ArgMatches;
use zune_image::traits::OperationsTrait;

use super::resize::Resize;

pub struct PreprocessorPipeline(BTreeMap<usize, Box<dyn OperationsTrait>>);

impl PreprocessorPipeline {
pub fn from_matches(matches: &ArgMatches) -> Self {
let mut pipeline: BTreeMap<usize, Box<dyn OperationsTrait>> = BTreeMap::new();

if let Some(resize) = Resize::from_matches(matches) {
resize.for_each(|(resize, index)| {
pipeline.insert(index, Box::new(resize));
});
}

Self(pipeline)
}
}
18 changes: 18 additions & 0 deletions src/preprocessors/resize/filter.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use anyhow::anyhow;
use clap::{builder::PossibleValue, ValueEnum};
use fast_image_resize as fr;

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ResizeFilter {
Nearest,
Box,
Bilinear,
Hamming,
Expand All @@ -14,6 +16,7 @@ pub enum ResizeFilter {
impl ValueEnum for ResizeFilter {
fn value_variants<'a>() -> &'a [Self] {
&[
ResizeFilter::Nearest,
ResizeFilter::Box,
ResizeFilter::Bilinear,
ResizeFilter::Hamming,
Expand All @@ -25,6 +28,7 @@ impl ValueEnum for ResizeFilter {

fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
Some(match self {
ResizeFilter::Nearest => PossibleValue::new("nearest").help("Simplest filter, for each destination pixel gets nearest source pixel"),
ResizeFilter::Box => PossibleValue::new("box").help("Each pixel contributes equally to destination. For upscaling, like Nearest."),
ResizeFilter::Bilinear => PossibleValue::new("bilinear").help("Uses linear interpolation among contributing pixels for output"),
ResizeFilter::Hamming => PossibleValue::new("hamming").help("Provides quality akin to bicubic for downscaling, sharper than Bilinear, but not optimal for upscaling"),
Expand Down Expand Up @@ -56,3 +60,17 @@ impl std::str::FromStr for ResizeFilter {
Err(anyhow!("invalid variant: {s}"))
}
}

impl From<ResizeFilter> for fr::ResizeAlg {
fn from(value: ResizeFilter) -> Self {
match value {
ResizeFilter::Nearest => fr::ResizeAlg::Nearest,
ResizeFilter::Box => fr::ResizeAlg::Convolution(fr::FilterType::Box),
ResizeFilter::Bilinear => fr::ResizeAlg::Convolution(fr::FilterType::Bilinear),
ResizeFilter::Hamming => fr::ResizeAlg::Convolution(fr::FilterType::Hamming),
ResizeFilter::CatmullRom => fr::ResizeAlg::Convolution(fr::FilterType::CatmullRom),
ResizeFilter::Mitchell => fr::ResizeAlg::Convolution(fr::FilterType::Mitchell),
ResizeFilter::Lanczos3 => fr::ResizeAlg::Convolution(fr::FilterType::Lanczos3),
}
}
}
45 changes: 45 additions & 0 deletions src/preprocessors/resize/fit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use anyhow::anyhow;
use clap::{builder::PossibleValue, ValueEnum};

#[derive(Clone, Copy)]
pub enum ResizeFit {
Stretch,
Cover,
}

impl ValueEnum for ResizeFit {
fn value_variants<'a>() -> &'a [Self] {
&[ResizeFit::Stretch, ResizeFit::Cover]
}

fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
Some(match self {
ResizeFit::Stretch => PossibleValue::new("stretch")
.help("Stretches resulting image to fit new dimensions"),
ResizeFit::Cover => PossibleValue::new("cover")
.help("Resulting image is sized to maintain its aspect ratio. Clipping to fit"),
})
}
}

impl std::fmt::Display for ResizeFit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.to_possible_value()
.expect("no values are skipped")
.get_name()
.fmt(f)
}
}

impl std::str::FromStr for ResizeFit {
type Err = anyhow::Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
for variant in Self::value_variants() {
if variant.to_possible_value().unwrap().matches(s, false) {
return Ok(*variant);
}
}
Err(anyhow!("invalid variant: {s}"))
}
}
153 changes: 153 additions & 0 deletions src/preprocessors/resize/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,158 @@
mod filter;
mod fit;
mod value;

use std::{io::Write, num::NonZeroU32};

pub use self::fit::ResizeFit;
use clap::ArgMatches;
use fast_image_resize as fr;
pub use filter::ResizeFilter;
use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
pub use value::ResizeValue;
use zune_core::colorspace::ColorSpace;
use zune_image::{
channel::Channel, codecs::qoi::zune_core::bit_depth::BitType, errors::ImageErrors,
traits::OperationsTrait,
};

pub struct Resize {
value: ResizeValue,
filter: ResizeFilter,
fit: ResizeFit,
}

impl Resize {
pub fn from_matches(matches: &ArgMatches) -> Option<impl Iterator<Item = (Self, usize)> + '_> {
if let Some(resize_processors) = matches.get_occurrences::<ResizeValue>("resize") {
let resize_filter = matches.get_one::<ResizeFilter>("filter");
let resize_fit = matches.get_one::<ResizeFit>("fit");

return Some(
resize_processors
.flatten()
.zip(matches.indices_of("resize").unwrap())
.map(move |(value, index)| {
(
Self {
value: *value,
filter: *resize_filter.unwrap_or(&ResizeFilter::Lanczos3),
fit: *resize_fit.unwrap_or(&ResizeFit::Stretch),
},
index,
)
}),
);
}

None
}
}

impl OperationsTrait for Resize {
fn name(&self) -> &'static str {
"Resize"
}

fn execute_impl(
&self,
image: &mut zune_image::image::Image,
) -> Result<(), zune_image::errors::ImageErrors> {
let (width, height) = image.dimensions();
let depth = image.depth().bit_type();

let width = NonZeroU32::new(width as u32).unwrap();
let height = NonZeroU32::new(height as u32).unwrap();

let (dst_width, dst_height) = self.value.map_dimensions(width.get(), height.get());

let new_length = dst_width * dst_height * image.depth().size_of() as u32;

let dst_width = NonZeroU32::new(dst_width).ok_or(ImageErrors::OperationsError(
zune_image::errors::ImageOperationsErrors::Generic("width cannot be zero"),
))?;
let dst_height = NonZeroU32::new(dst_height).ok_or(ImageErrors::OperationsError(
zune_image::errors::ImageOperationsErrors::Generic("height cannot be zero"),
))?;

if image.colorspace() != ColorSpace::RGBA {
return Err(zune_image::errors::ImageErrors::OperationsError(
zune_image::errors::ImageOperationsErrors::WrongColorspace(
ColorSpace::RGBA,
image.colorspace(),
),
));
}

image.channels_mut(false).par_iter_mut().try_for_each(
|old_channel| -> Result<(), ImageErrors> {
let mut new_channel = Channel::new_with_bit_type(new_length as usize, depth);

let mut src_image = fr::Image::from_slice_u8(
width,
height,
old_channel.reinterpret_as_mut()?,
match depth {
BitType::U8 => fr::PixelType::U8x4,
BitType::U16 => fr::PixelType::U16x4,
BitType::F32 => fr::PixelType::F32,
d => {
return Err(ImageErrors::OperationsError(
zune_image::errors::ImageOperationsErrors::UnsupportedType(
"resize", d,
),
))
}
},
)
.map_err(|e| {
ImageErrors::OperationsError(
zune_image::errors::ImageOperationsErrors::GenericString(e.to_string()),
)
})?;

match self.fit {
ResizeFit::Stretch => {}
ResizeFit::Cover => {
src_image.view().set_crop_box_to_fit_dst_size(
dst_width,
dst_height,
Some((0.5, 0.5)),
);
}
}

let alpha_mul_div = fr::MulDiv::default();
alpha_mul_div
.multiply_alpha_inplace(&mut src_image.view_mut())
.unwrap();

let mut dst_image = fr::Image::new(dst_width, dst_height, src_image.pixel_type());

let mut dst_view = dst_image.view_mut();

let mut resizer = fr::Resizer::new(self.filter.into());

resizer.resize(&src_image.view(), &mut dst_view).unwrap();

alpha_mul_div.divide_alpha_inplace(&mut dst_view).unwrap();

new_channel
.reinterpret_as_mut()?
.write(dst_image.buffer())?;

**old_channel = new_channel;

Ok(())
},
)?;

image.set_dimensions(dst_width.get() as usize, dst_height.get() as usize);

Ok(())
}

fn supported_types(&self) -> &'static [zune_image::codecs::qoi::zune_core::bit_depth::BitType] {
&[BitType::U8, BitType::U16, BitType::F32]
}
}
Loading

0 comments on commit 7aa016e

Please sign in to comment.