- **BREAKING**: The type of the CropBox fields has been changed to f64. Now you can use

fractional size and position of crop box.
- **BREAKING**: The type of `centering` argument of `ImageView::set_crop_box_to_fit_dst_size()`
  and `DynamicImageView::set_crop_box_to_fit_dst_size()` methods has been changed to `Optional<(f64, f64)>`.
- **BREAKING**: The `crop_box` argument of `ImageViewMut::crop()` and `DynamicImageViewMut::crop()`
  methods has been replaced with separate `left`, `top`, `width` and `height` arguments.
This commit is contained in:
Kirill Kuzminykh
2024-02-04 22:51:35 +03:00
parent 6654b8ae61
commit 19d526af74
6 changed files with 169 additions and 92 deletions
+6
View File
@@ -14,6 +14,12 @@
in native Rust for `U8`, `U8x2`, `U8x3` and `U8x4` images.
- Changed order of convolution passes for `U8`, `U8x2`, `U8x3` and `U8x4` images.
Now the vertical pass is the first and the horizontal pass is the second.
- **BREAKING**: The type of the `CropBox` fields has been changed to `f64`. Now you can use
fractional size and position of crop box.
- **BREAKING**: The type of `centering` argument of `ImageView::set_crop_box_to_fit_dst_size()`
and `DynamicImageView::set_crop_box_to_fit_dst_size()` methods has been changed to `Optional<(f64, f64)>`.
- **BREAKING**: The `crop_box` argument of `ImageViewMut::crop()` and `DynamicImageViewMut::crop()`
methods has been replaced with separate `left`, `top`, `width` and `height` arguments.
## [2.7.3] - 2023-05-07
+12 -3
View File
@@ -127,7 +127,7 @@ impl<'a> DynamicImageView<'a> {
&mut self,
dst_width: NonZeroU32,
dst_height: NonZeroU32,
centering: Option<(f32, f32)>,
centering: Option<(f64, f64)>,
) {
dynamic_map!(self, |typed_image| typed_image
.set_crop_box_to_fit_dst_size(dst_width, dst_height, centering))
@@ -148,8 +148,17 @@ impl<'a> DynamicImageViewMut<'a> {
}
/// Create cropped version of the view.
pub fn crop(self, crop_box: CropBox) -> Result<Self, CropBoxError> {
Ok(dynamic_mut_map!(self, typed_image => typed_image.crop(crop_box)?))
pub fn crop(
self,
left: u32,
top: u32,
width: NonZeroU32,
height: NonZeroU32,
) -> Result<Self, CropBoxError> {
Ok(dynamic_mut_map!(
self,
typed_image => typed_image.crop(left, top, width, height)?
))
}
}
+75 -52
View File
@@ -6,14 +6,14 @@ use std::slice;
use crate::pixels::{GetCount, IntoPixelComponent, PixelComponent, PixelExt};
use crate::{CropBoxError, DifferentDimensionsError, ImageBufferError, ImageRowsError, PixelType};
/// Parameters of crop box that may be used with [`ImageView`]
/// A crop box parameters that may be used with [`ImageView`]
/// and [`DynamicImageView`](crate::DynamicImageView)
#[derive(Debug, Clone, Copy)]
pub struct CropBox {
pub left: u32,
pub top: u32,
pub width: NonZeroU32,
pub height: NonZeroU32,
pub left: f64,
pub top: f64,
pub width: f64,
pub height: f64,
}
/// Generic immutable image view.
@@ -42,10 +42,10 @@ where
width,
height,
crop_box: CropBox {
left: 0,
top: 0,
width,
height,
left: 0.,
top: 0.,
width: width.get() as _,
height: height.get() as _,
},
rows,
})
@@ -70,10 +70,10 @@ where
width,
height,
crop_box: CropBox {
left: 0,
top: 0,
width,
height,
left: 0.,
top: 0.,
width: width.get() as _,
height: height.get() as _,
},
rows,
})
@@ -97,10 +97,10 @@ where
width,
height,
crop_box: CropBox {
left: 0,
top: 0,
width,
height,
left: 0.,
top: 0.,
width: width.get() as _,
height: height.get() as _,
},
rows,
})
@@ -123,12 +123,19 @@ where
}
pub fn set_crop_box(&mut self, crop_box: CropBox) -> Result<(), CropBoxError> {
if crop_box.left >= self.width.get() || crop_box.top >= self.height.get() {
let width_f = self.width().get() as f64;
let height_f = self.height().get() as f64;
if crop_box.left < 0.
|| crop_box.top < 0.
|| crop_box.left >= width_f
|| crop_box.top >= height_f
{
return Err(CropBoxError::PositionIsOutOfImageBoundaries);
}
let right = crop_box.left + crop_box.width.get();
let bottom = crop_box.top + crop_box.height.get();
if right > self.width.get() || bottom > self.height.get() {
let right = crop_box.left + crop_box.width;
let bottom = crop_box.top + crop_box.height;
if right > width_f || bottom > height_f {
return Err(CropBoxError::SizeIsOutOfImageBoundaries);
}
self.crop_box = crop_box;
@@ -152,7 +159,7 @@ where
&mut self,
dst_width: NonZeroU32,
dst_height: NonZeroU32,
centering: Option<(f32, f32)>,
centering: Option<(f64, f64)>,
) {
// This function based on code of ImageOps.fit() from Pillow package.
// https://github.com/python-pillow/Pillow/blob/master/src/PIL/ImageOps.py
@@ -163,15 +170,15 @@ where
};
// calculate aspect ratios
let width = self.width.get() as f32;
let height = self.height.get() as f32;
let width = self.width.get() as f64;
let height = self.height.get() as f64;
let image_ratio = width / height;
let required_ration = dst_width.get() as f32 / dst_height.get() as f32;
let required_ration = dst_width.get() as f64 / dst_height.get() as f64;
let crop_width;
let crop_height;
// figure out if the sides or top/bottom will be cropped off
if (image_ratio - required_ration).abs() < f32::EPSILON {
if (image_ratio - required_ration).abs() < f64::EPSILON {
// The image is already the needed ratio
crop_width = width;
crop_height = height;
@@ -189,10 +196,10 @@ where
let crop_top = (height - crop_height) * centering.1;
self.set_crop_box(CropBox {
left: crop_left.round() as u32,
top: crop_top.round() as u32,
width: NonZeroU32::new(crop_width.round() as u32).unwrap(),
height: NonZeroU32::new(crop_height.round() as u32).unwrap(),
left: crop_left,
top: crop_top,
width: crop_width,
height: crop_height,
})
.unwrap();
}
@@ -259,11 +266,11 @@ where
#[inline(always)]
pub(crate) fn iter_cropped_rows<'s>(&'s self) -> impl Iterator<Item = &'a [P]> + 's {
let first_row = self.crop_box.top as usize;
let last_row = first_row + self.crop_box.height.get() as usize;
let last_row = first_row + self.crop_box.height as usize;
let rows = unsafe { self.rows.get_unchecked(first_row..last_row) };
let first_col = self.crop_box.left as usize;
let last_col = first_col + self.crop_box.width.get() as usize;
let last_col = first_col + self.crop_box.width as usize;
rows.iter()
// Safety guaranteed by method 'set_crop_box'
.map(move |row| unsafe { row.get_unchecked(first_col..last_col) })
@@ -379,7 +386,17 @@ where
src_view: &ImageView<P>,
) -> Result<(), DifferentDimensionsError> {
let src_crop_box = src_view.crop_box();
if self.width != src_crop_box.width || self.height != src_crop_box.height {
if src_crop_box.left != src_crop_box.left.round()
|| src_crop_box.top != src_crop_box.top.round()
|| src_crop_box.width != src_crop_box.width.round()
|| src_crop_box.height != src_crop_box.height.round()
{
// The crop box has fractional part in some his part
return Err(DifferentDimensionsError);
}
if self.width.get() != src_crop_box.width as u32
|| self.height.get() != src_crop_box.height as u32
{
return Err(DifferentDimensionsError);
}
self.rows
@@ -390,26 +407,32 @@ where
}
/// Create cropped version of the view.
pub fn crop(self, crop_box: CropBox) -> Result<Self, CropBoxError> {
if crop_box.left >= self.width.get() || crop_box.top >= self.height.get() {
pub fn crop(
self,
left: u32,
top: u32,
width: NonZeroU32,
height: NonZeroU32,
) -> Result<Self, CropBoxError> {
if left >= self.width.get() || top >= self.height.get() {
return Err(CropBoxError::PositionIsOutOfImageBoundaries);
}
let right = crop_box.left + crop_box.width.get();
let bottom = crop_box.top + crop_box.height.get();
let right = left + width.get();
let bottom = top + height.get();
if right > self.width.get() || bottom > self.height.get() {
return Err(CropBoxError::SizeIsOutOfImageBoundaries);
}
let row_range = (crop_box.left as usize)..(right as usize);
let row_range = (left as usize)..(right as usize);
let rows = self
.rows
.into_iter()
.skip(crop_box.top as usize)
.take(crop_box.height.get() as usize)
.skip(top as usize)
.take(height.get() as usize)
.map(|row| unsafe { row.get_unchecked_mut(row_range.clone()) })
.collect();
Ok(Self {
width: crop_box.width,
height: crop_box.height,
width,
height,
rows,
})
}
@@ -430,10 +453,10 @@ where
width: view.width,
height: view.height,
crop_box: CropBox {
left: 0,
top: 0,
width: view.width,
height: view.height,
left: 0.,
top: 0.,
width: view.width.get() as _,
height: view.height.get() as _,
},
rows,
}
@@ -511,12 +534,12 @@ mod tests {
let image_view: ImageViewMut<crate::pixels::U8> =
ImageViewMut::from_buffer(image.width(), image.height(), image.buffer_mut()).unwrap();
let cropped_view = image_view
.crop(CropBox {
left: 10,
top: 10,
width: NonZeroU32::new(44).unwrap(),
height: NonZeroU32::new(12).unwrap(),
})
.crop(
10,
10,
NonZeroU32::new(44).unwrap(),
NonZeroU32::new(12).unwrap(),
)
.unwrap();
assert_eq!(cropped_view.width().get(), 44);
assert_eq!(cropped_view.height().get(), 12);
+19 -19
View File
@@ -266,17 +266,17 @@ where
{
let crop_box = src_image.crop_box();
let dst_width = dst_image.width().get();
let x_scale = crop_box.width.get() as f64 / dst_width as f64;
let y_scale = crop_box.height.get() as f64 / dst_image.height().get() as f64;
let x_scale = crop_box.width / dst_width as f64;
let y_scale = crop_box.height / dst_image.height().get() as f64;
// Pretabulate horizontal pixel positions
let x_in_start = crop_box.left as f64 + x_scale * 0.5;
let x_in_start = crop_box.left + x_scale * 0.5;
let max_src_x = src_image.width().get() as usize;
let x_in_tab: Vec<usize> = (0..dst_width)
.map(|x| ((x_in_start + x_scale * x as f64) as usize).min(max_src_x))
.collect();
let y_in_start = crop_box.top as f64 + y_scale * 0.5;
let y_in_start = crop_box.top + y_scale * 0.5;
let src_rows =
src_image.iter_rows_with_step(y_in_start, y_scale, dst_image.height().get() as usize);
@@ -303,26 +303,28 @@ fn resample_convolution<P>(
let dst_height = dst_image.height();
let (filter_fn, filter_support) = convolution::get_filter_func(filter_type);
let need_horizontal = dst_width != crop_box.width;
let need_horizontal =
dst_width.get() as f64 != crop_box.width || crop_box.left != crop_box.left.round();
let horiz_coeffs = need_horizontal.then(|| {
test_log!("compute horizontal convolution coefficients");
convolution::precompute_coefficients(
src_image.width(),
crop_box.left as f64,
crop_box.left as f64 + crop_box.width.get() as f64,
crop_box.left,
crop_box.left + crop_box.width,
dst_width,
filter_fn,
filter_support,
)
});
let need_vertical = dst_height != crop_box.height;
let need_vertical =
dst_height.get() as f64 != crop_box.height || crop_box.top != crop_box.top.round();
let vert_coeffs = need_vertical.then(|| {
test_log!("compute vertical convolution coefficients");
convolution::precompute_coefficients(
src_image.height(),
crop_box.top as f64,
crop_box.top as f64 + crop_box.height.get() as f64,
crop_box.top,
crop_box.top + crop_box.height,
dst_height,
filter_fn,
filter_support,
@@ -397,7 +399,7 @@ fn resample_convolution<P>(
P::horiz_convolution(
src_image,
dst_image,
crop_box.top,
crop_box.top as u32, // crop_box.top is exactly an integer if vertical pass is not required
horiz_coeffs,
cpu_extensions,
);
@@ -406,7 +408,7 @@ fn resample_convolution<P>(
P::vert_convolution(
src_image,
dst_image,
crop_box.left,
crop_box.left as u32, // crop_box.left is exactly an integer if horizontal pass is not required
vert_coeffs,
cpu_extensions,
);
@@ -429,19 +431,17 @@ fn resample_super_sampling<P>(
let crop_box = src_image.crop_box();
let dst_width = dst_image.width().get();
let dst_height = dst_image.height().get();
let width_scale = crop_box.width.get() as f32 / dst_width as f32;
let height_scale = crop_box.height.get() as f32 / dst_height as f32;
let width_scale = crop_box.width / dst_width as f64;
let height_scale = crop_box.height / dst_height as f64;
// It makes sense to resize the image in two steps only if the image
// size is greater than the required size by multiplicity times.
let factor = width_scale.min(height_scale) / multiplicity as f32;
let factor = width_scale.min(height_scale) / multiplicity as f64;
if factor > 1.2 {
// First step is resizing the source image by fastest algorithm.
// The temporary image will be about ``multiplicity`` times larger
// than required.
let tmp_width =
NonZeroU32::new((crop_box.width.get() as f32 / factor).round() as u32).unwrap();
let tmp_height =
NonZeroU32::new((crop_box.height.get() as f32 / factor).round() as u32).unwrap();
let tmp_width = NonZeroU32::new((crop_box.width / factor).round() as u32).unwrap();
let tmp_height = NonZeroU32::new((crop_box.height / factor).round() as u32).unwrap();
let mut tmp_img = get_temp_image_from_buffer(temp_buffer, tmp_width, tmp_height);
resample_nearest(src_image, &mut tmp_img.dst_view());
+1 -6
View File
@@ -69,12 +69,7 @@ fn crop_view_mut() {
let mut cropped_dst_view = dst_image
.view_mut()
.crop(fr::CropBox {
left: 10,
top: 10,
width: nonzero(44),
height: nonzero(12),
})
.crop(10, 10, nonzero(44), nonzero(12))
.unwrap();
assert_eq!(cropped_dst_view.width().get(), 44);
assert_eq!(cropped_dst_view.height().get(), 12);
+56 -12
View File
@@ -61,10 +61,10 @@ fn resize_to_same_size_after_cropping() {
let mut src_view = src_image.view();
src_view
.set_crop_box(CropBox {
top: 10,
left: 10,
width,
height,
top: 10.,
left: 10.,
width: width.get() as _,
height: height.get() as _,
})
.unwrap();
@@ -110,10 +110,10 @@ fn resize_to_same_width<const C: usize>(
let mut src_view = src_image.view();
src_view
.set_crop_box(CropBox {
left: 10,
top: 0,
width,
height: src_height,
left: 10.,
top: 0.,
width: width.get() as _,
height: src_height.get() as _,
})
.unwrap();
@@ -164,10 +164,10 @@ fn resize_to_same_height<const C: usize>(
let mut src_view = src_image.view();
src_view
.set_crop_box(CropBox {
left: 0,
top: 10,
width: src_width,
height,
left: 0.,
top: 10.,
width: src_width.get() as _,
height: height.get() as _,
})
.unwrap();
@@ -811,6 +811,50 @@ mod not_u8x4 {
);
}
}
#[test]
fn fractional_cropping() {
let mut src_buf = [0, 0, 0, 0, 255, 0, 0, 0, 0];
let src_image =
Image::from_slice_u8(nonzero(3), nonzero(3), &mut src_buf, PixelType::U8).unwrap();
let mut dst_image = Image::new(nonzero(1), nonzero(1), PixelType::U8);
let mut resizer = Resizer::new(ResizeAlg::Convolution(FilterType::Box));
// Resize without cropping
resizer
.resize(&src_image.view(), &mut dst_image.view_mut())
.unwrap();
assert_eq!(dst_image.buffer()[0], (255.0f32 / 9.0).round() as u8);
// Resize with fractional cropping
let mut src_view = src_image.view();
src_view
.set_crop_box(CropBox {
left: 0.5,
top: 0.5,
width: 2.,
height: 2.,
})
.unwrap();
resizer
.resize(&src_view, &mut dst_image.view_mut())
.unwrap();
assert_eq!(dst_image.buffer()[0], (255.0f32 / 4.0).round() as u8);
// Resize with integer cropping
src_view
.set_crop_box(CropBox {
left: 1.,
top: 1.,
width: 1.,
height: 1.,
})
.unwrap();
resizer
.resize(&src_view, &mut dst_image.view_mut())
.unwrap();
assert_eq!(dst_image.buffer()[0], 255);
}
}
mod u8x4 {