diff --git a/src/geometric_transformations.rs b/src/geometric_transformations.rs index a6394b52..28badcc1 100644 --- a/src/geometric_transformations.rs +++ b/src/geometric_transformations.rs @@ -317,6 +317,85 @@ where warp(image, &projection, interpolation, default) } +/// Rotates an image clockwise about its center by theta radians without cropping. +/// The output image has dimensions calculated to fit the entire rotated image. +/// Output pixels whose pre-image lies outside the input image are set to `default`. +pub fn rotate_about_center_no_crop

( + image: &Image

, + theta: f32, + interpolation: Interpolation, + default: P, +) -> Image

+where + P: Pixel + Send + Sync, +

::Subpixel: Send + Sync, +

::Subpixel: Into + Clamp, +{ + let (width, height) = image.dimensions(); + + let cos = theta.cos(); + let sin = theta.sin(); + + let new_width = (height as f32 * sin.abs() + width as f32 * cos.abs()).ceil() as u32; + let new_height = (height as f32 * cos.abs() + width as f32 * sin.abs()).ceil() as u32; + + let mut out_img = Image::new(new_width, new_height); + + rotate_about_center_into(image, theta, interpolation, default, &mut out_img); + + out_img +} + +/// Rotates an image clockwise about its center by theta radians, writing to a provided output. +/// Output pixels whose pre-image lies outside the input image are set to `default`. +fn rotate_about_center_into

( + image: &Image

, + theta: f32, + interpolation: Interpolation, + default: P, + out: &mut Image

, +) where + P: Pixel + Send + Sync, +

::Subpixel: Send + Sync, +

::Subpixel: Into + Clamp, +{ + let (w, h) = image.dimensions(); + let (ow, oh) = out.dimensions(); + rotate_into( + image, + (w as f32 / 2.0, h as f32 / 2.0), + (ow as f32 / 2.0, oh as f32 / 2.0), + theta, + interpolation, + default, + out, + ) +} + +/// Rotates an image clockwise about the provided center by theta radians, writing to a provided output. +/// Output pixels whose pre-image lies outside the input image are set to `default`. +fn rotate_into

( + image: &Image

, + center: (f32, f32), + out_center: (f32, f32), + theta: f32, + interpolation: Interpolation, + default: P, + out: &mut Image

, +) where + P: Pixel + Send + Sync, +

::Subpixel: Send + Sync, +

::Subpixel: Into + Clamp, +{ + let (cx, cy) = center; + let (ocx, ocy) = out_center; + let projection = Projection::translate(ocx, ocy) + * Projection::rotate(theta) + * Projection::translate(-cx, -cy); + + warp_into(image, &projection, interpolation, default, out); +} + /// Rotates an image 90 degrees clockwise. /// /// # Examples @@ -882,7 +961,8 @@ pub enum Interpolation { #[cfg(test)] mod tests { use super::*; - use image::Luma; + use image::{GrayImage, Luma}; + use std::f32::consts::PI; #[test] fn test_rotate_nearest_zero_radians() { @@ -901,7 +981,7 @@ mod tests { } #[test] - fn text_rotate_nearest_quarter_turn_clockwise() { + fn test_rotate_nearest_quarter_turn_clockwise() { let image = gray_image!( 00, 01, 02; 10, 11, 12); @@ -917,7 +997,7 @@ mod tests { } #[test] - fn text_rotate_nearest_half_turn_anticlockwise() { + fn test_rotate_nearest_half_turn_anticlockwise() { let image = gray_image!( 00, 01, 02; 10, 11, 12); diff --git a/tests/data/truth/elephant_rotate_no_crop_bicubic.png b/tests/data/truth/elephant_rotate_no_crop_bicubic.png new file mode 100644 index 00000000..c7c89919 Binary files /dev/null and b/tests/data/truth/elephant_rotate_no_crop_bicubic.png differ diff --git a/tests/data/truth/elephant_rotate_no_crop_bicubic_rgba.png b/tests/data/truth/elephant_rotate_no_crop_bicubic_rgba.png new file mode 100644 index 00000000..317a133f Binary files /dev/null and b/tests/data/truth/elephant_rotate_no_crop_bicubic_rgba.png differ diff --git a/tests/data/truth/elephant_rotate_no_crop_bilinear.png b/tests/data/truth/elephant_rotate_no_crop_bilinear.png new file mode 100644 index 00000000..d601a11f Binary files /dev/null and b/tests/data/truth/elephant_rotate_no_crop_bilinear.png differ diff --git a/tests/data/truth/elephant_rotate_no_crop_bilinear_rgba.png b/tests/data/truth/elephant_rotate_no_crop_bilinear_rgba.png new file mode 100644 index 00000000..cac81b3e Binary files /dev/null and b/tests/data/truth/elephant_rotate_no_crop_bilinear_rgba.png differ diff --git a/tests/data/truth/elephant_rotate_no_crop_nearest.png b/tests/data/truth/elephant_rotate_no_crop_nearest.png new file mode 100644 index 00000000..b7e8b83e Binary files /dev/null and b/tests/data/truth/elephant_rotate_no_crop_nearest.png differ diff --git a/tests/data/truth/elephant_rotate_no_crop_nearest_default_red_rgba.png b/tests/data/truth/elephant_rotate_no_crop_nearest_default_red_rgba.png new file mode 100644 index 00000000..c1cc1027 Binary files /dev/null and b/tests/data/truth/elephant_rotate_no_crop_nearest_default_red_rgba.png differ diff --git a/tests/data/truth/elephant_rotate_no_crop_nearest_rgba.png b/tests/data/truth/elephant_rotate_no_crop_nearest_rgba.png new file mode 100644 index 00000000..817f6c9b Binary files /dev/null and b/tests/data/truth/elephant_rotate_no_crop_nearest_rgba.png differ diff --git a/tests/regression.rs b/tests/regression.rs index a8e478df..af0c6667 100644 --- a/tests/regression.rs +++ b/tests/regression.rs @@ -25,6 +25,7 @@ use imageproc::contrast::ThresholdType; use imageproc::definitions::Image; use imageproc::filter::bilateral::GaussianEuclideanColorDistance; use imageproc::filter::bilateral_filter; +use imageproc::geometric_transformations::rotate_about_center_no_crop; use imageproc::kernel::{self}; use imageproc::{ definitions::{Clamp, HasBlack, HasWhite}, @@ -142,6 +143,23 @@ fn test_rotate_nearest_rgb() { ); } +#[test] +fn test_rotate_no_crop_nearest_rgb() { + fn rotate_nearest_about_center_no_crop(image: &RgbImage) -> RgbImage { + rotate_about_center_no_crop( + image, + std::f32::consts::PI / 4f32, + Interpolation::Nearest, + Rgb::black(), + ) + } + compare_to_truth( + "elephant.png", + "elephant_rotate_no_crop_nearest.png", + rotate_nearest_about_center_no_crop, + ); +} + #[test] fn test_rotate_nearest_rgba() { fn rotate_nearest_about_center(image: &RgbaImage) -> RgbaImage { @@ -159,6 +177,23 @@ fn test_rotate_nearest_rgba() { ); } +#[test] +fn test_rotate_no_crop_nearest_rgba() { + fn rotate_nearest_about_center_no_crop(image: &RgbaImage) -> RgbaImage { + rotate_about_center_no_crop( + image, + std::f32::consts::PI / 4f32, + Interpolation::Nearest, + Rgba::black(), + ) + } + compare_to_truth( + "elephant_rgba.png", + "elephant_rotate_no_crop_nearest_rgba.png", + rotate_nearest_about_center_no_crop, + ); +} + #[test] fn test_equalize_histogram_grayscale() { use imageproc::contrast::equalize_histogram; @@ -183,6 +218,24 @@ fn test_rotate_bilinear_rgb() { ); } +#[test] +fn test_rotate_bilinear_no_crop_rgb() { + fn rotate_bilinear_about_center_no_crop(image: &RgbImage) -> RgbImage { + rotate_about_center_no_crop( + image, + std::f32::consts::PI / 4f32, + Interpolation::Bilinear, + Rgb::black(), + ) + } + compare_to_truth_with_tolerance( + "elephant.png", + "elephant_rotate_no_crop_bilinear.png", + rotate_bilinear_about_center_no_crop, + 2, + ); +} + #[test] fn test_rotate_bilinear_rgba() { fn rotate_bilinear_about_center(image: &RgbaImage) -> RgbaImage { @@ -201,6 +254,24 @@ fn test_rotate_bilinear_rgba() { ); } +#[test] +fn test_rotate_no_crop_bilinear_rgba() { + fn rotate_bilinear_about_center_no_crop(image: &RgbaImage) -> RgbaImage { + rotate_about_center_no_crop( + image, + std::f32::consts::PI / 4f32, + Interpolation::Bilinear, + Rgba::black(), + ) + } + compare_to_truth_with_tolerance( + "elephant_rgba.png", + "elephant_rotate_no_crop_bilinear_rgba.png", + rotate_bilinear_about_center_no_crop, + 2, + ); +} + #[test] fn test_rotate_bicubic_rgb() { fn rotate_bicubic_about_center(image: &RgbImage) -> RgbImage { @@ -219,6 +290,24 @@ fn test_rotate_bicubic_rgb() { ); } +#[test] +fn test_rotate_no_crop_bicubic_rgb() { + fn rotate_bicubic_about_center_no_crop(image: &RgbImage) -> RgbImage { + rotate_about_center_no_crop( + image, + std::f32::consts::PI / 4f32, + Interpolation::Bicubic, + Rgb::black(), + ) + } + compare_to_truth_with_tolerance( + "elephant.png", + "elephant_rotate_no_crop_bicubic.png", + rotate_bicubic_about_center_no_crop, + 2, + ); +} + #[test] fn test_rotate_bicubic_rgba() { fn rotate_bicubic_about_center(image: &RgbaImage) -> RgbaImage { @@ -237,6 +326,42 @@ fn test_rotate_bicubic_rgba() { ); } +#[test] +fn test_rotate_no_crop_bicubic_rgba() { + fn rotate_bicubic_about_center_no_crop(image: &RgbaImage) -> RgbaImage { + rotate_about_center_no_crop( + image, + std::f32::consts::PI / 4f32, + Interpolation::Bicubic, + Rgba::black(), + ) + } + compare_to_truth_with_tolerance( + "elephant_rgba.png", + "elephant_rotate_no_crop_bicubic_rgba.png", + rotate_bicubic_about_center_no_crop, + 2, + ); +} + +#[test] +fn test_rotate_no_crop_default_color() { + fn rotate_nearest_about_center_no_crop_default_red(image: &RgbaImage) -> RgbaImage { + rotate_about_center_no_crop( + image, + std::f32::consts::PI / 4f32, + Interpolation::Nearest, + Rgba([255, 0, 0, 255]), + ) + } + compare_to_truth_with_tolerance( + "elephant_rgba.png", + "elephant_rotate_no_crop_nearest_default_red_rgba.png", + rotate_nearest_about_center_no_crop_default_red, + 2, + ) +} + #[test] fn test_affine_nearest_rgb() { fn affine_nearest(image: &RgbImage) -> RgbImage {