diff --git a/Cargo.toml b/Cargo.toml index 5e69d552..f0f90c56 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,6 @@ exclude = [".github/*", "examples/*", "tests/*"] [features] default = ["rayon", "image/default"] -property-testing = ["quickcheck"] display-window = ["sdl2"] rayon = ["dep:rayon", "image/rayon"] @@ -33,7 +32,6 @@ rand = { version = "0.8.5", default-features = false, features = [ ] } rand_distr = { version = "0.4.3", default-features = false } rayon = { version = "1.8.0", optional = true, default-features = false } -quickcheck = { version = "1.0.3", optional = true, default-features = false } sdl2 = { version = "0.36", optional = true, default-features = false, features = [ "bundled", ] } @@ -45,7 +43,6 @@ getrandom = { version = "0.2", default-features = false, features = ["js"] } [dev-dependencies] assert_approx_eq = "1.1.0" proptest = "1.4.0" -quickcheck = "1.0.3" wasm-bindgen-test = "0.3.38" [package.metadata.docs.rs] diff --git a/README.md b/README.md index ecc989db..f1ce6493 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ from not using linear color spaces. This library provides both single-threaded and multi-threaded variations of several functions by using [rayon](https://github.com/rayon-rs/rayon). -Depending on image size and the amount of work performed per pixel the parallel versions may not +Depending on image size and the amount of work performed per pixel the parallel versions may not always be faster - we recommend benchmarking for your specific use-case. ## Crate Features @@ -52,13 +52,11 @@ always be faster - we recommend benchmarking for your specific use-case. - `katexit`: enables latex in documentation via [katexit](https://github.com/termoshtt/katexit) -- `property-testing`: enables `quickcheck` -- `quickcheck`: exposes helper types and methods to enable property testing - via [quickcheck](https://github.com/BurntSushi/quickcheck) - `display-window`: enables `sdl2` - `sdl2`: enables the displaying of images (using `imageproc::window`) with [sdl2](https://github.com/Rust-SDL2/rust-sdl2) ## How to contribute -See [CONTRIBUTING.md](CONTRIBUTING.md). \ No newline at end of file +See [CONTRIBUTING.md](CONTRIBUTING.md). + diff --git a/src/distance_transform.rs b/src/distance_transform.rs index 86fd473f..1226ff40 100644 --- a/src/distance_transform.rs +++ b/src/distance_transform.rs @@ -443,34 +443,10 @@ fn intersection(f: &S, p: usize, q: usize) -> f64 { mod tests { use super::*; use crate::definitions::Image; - use crate::property_testing::GrayTestImage; - use crate::utils::pixel_diff_summary; use image::{GrayImage, Luma}; - use quickcheck::{quickcheck, Arbitrary, Gen, TestResult}; use std::cmp::max; use std::f64; - /// Avoid generating garbage floats during certain calculations below. - #[derive(Debug, Clone)] - struct BoundedFloat(f64); - - impl Arbitrary for BoundedFloat { - fn arbitrary(g: &mut Gen) -> Self { - let mut f; - - loop { - f = f64::arbitrary(g); - - if f.is_normal() { - f = f.clamp(-1_000_000.0, 1_000_000.0); - break; - } - } - - BoundedFloat(f) - } - } - #[test] fn test_distance_transform_saturation() { // A single foreground pixel in the top-left @@ -486,54 +462,10 @@ mod tests { assert_pixels_eq!(distances, expected); } - impl Sink for Vec { - fn put(&mut self, idx: usize, value: f64) { - self[idx] = value; - } - fn len(&self) -> usize { - self.len() - } - } - - fn distance_transform_1d(f: &Vec) -> Vec { - let mut r = vec![0.0; f.len()]; - let mut e = LowerEnvelope::new(f.len()); - distance_transform_1d_mut(f, &mut r, &mut e); - r - } - - #[test] - fn test_distance_transform_1d_constant() { - let f = vec![0.0, 0.0, 0.0]; - let dists = distance_transform_1d(&f); - assert_eq!(dists, &[0.0, 0.0, 0.0]); - } - - #[test] - fn test_distance_transform_1d_descending_gradient() { - let f = vec![7.0, 5.0, 3.0, 1.0]; - let dists = distance_transform_1d(&f); - assert_eq!(dists, &[6.0, 4.0, 2.0, 1.0]); - } - - #[test] - fn test_distance_transform_1d_ascending_gradient() { - let f = vec![1.0, 3.0, 5.0, 7.0]; - let dists = distance_transform_1d(&f); - assert_eq!(dists, &[1.0, 2.0, 4.0, 6.0]); - } - - #[test] - fn test_distance_transform_1d_with_infinities() { - let f = vec![f64::INFINITY, f64::INFINITY, 5.0, f64::INFINITY]; - let dists = distance_transform_1d(&f); - assert_eq!(dists, &[9.0, 6.0, 5.0, 6.0]); - } - // Simple implementation of 1d distance transform which performs an // exhaustive search. Used to valid the more complicated lower-envelope // implementation against. - fn distance_transform_1d_reference(f: &[f64]) -> Vec { + pub fn distance_transform_1d_reference(f: &[f64]) -> Vec { let mut ret = vec![0.0; f.len()]; for q in 0..f.len() { ret[q] = (0..f.len()) @@ -546,20 +478,16 @@ mod tests { ret } - #[cfg_attr(miri, ignore = "slow")] - #[test] - fn test_distance_transform_1d_matches_reference_implementation() { - fn prop(f: Vec) -> bool { - let v: Vec = f.into_iter().map(|n| n.0).collect(); - let expected = distance_transform_1d_reference(&v); - let actual = distance_transform_1d(&v); - expected == actual - } - - quickcheck(prop as fn(Vec) -> bool); + pub fn distance_transform_1d(f: &Vec) -> Vec { + let mut r = vec![0.0; f.len()]; + let mut e = LowerEnvelope::new(f.len()); + distance_transform_1d_mut(f, &mut r, &mut e); + r } - fn euclidean_squared_distance_transform_reference(image: &Image>) -> Image> { + pub fn euclidean_squared_distance_transform_reference( + image: &Image>, + ) -> Image> { let (width, height) = image.dimensions(); let mut dists = Image::new(width, height); @@ -586,18 +514,41 @@ mod tests { dists } - #[cfg_attr(miri, ignore = "slow")] - #[test] - fn test_euclidean_squared_distance_transform_matches_reference_implementation() { - fn prop(image: GrayTestImage) -> TestResult { - let expected = euclidean_squared_distance_transform_reference(&image.0); - let actual = euclidean_squared_distance_transform(&image.0); - match pixel_diff_summary(&actual, &expected) { - None => TestResult::passed(), - Some(err) => TestResult::error(err), - } + impl Sink for Vec { + fn put(&mut self, idx: usize, value: f64) { + self[idx] = value; } - quickcheck(prop as fn(GrayTestImage) -> TestResult); + fn len(&self) -> usize { + self.len() + } + } + + #[test] + fn test_distance_transform_1d_constant() { + let f = vec![0.0, 0.0, 0.0]; + let dists = distance_transform_1d(&f); + assert_eq!(dists, &[0.0, 0.0, 0.0]); + } + + #[test] + fn test_distance_transform_1d_descending_gradient() { + let f = vec![7.0, 5.0, 3.0, 1.0]; + let dists = distance_transform_1d(&f); + assert_eq!(dists, &[6.0, 4.0, 2.0, 1.0]); + } + + #[test] + fn test_distance_transform_1d_ascending_gradient() { + let f = vec![1.0, 3.0, 5.0, 7.0]; + let dists = distance_transform_1d(&f); + assert_eq!(dists, &[1.0, 2.0, 4.0, 6.0]); + } + + #[test] + fn test_distance_transform_1d_with_infinities() { + let f = vec![f64::INFINITY, f64::INFINITY, 5.0, f64::INFINITY]; + let dists = distance_transform_1d(&f); + assert_eq!(dists, &[9.0, 6.0, 5.0, 6.0]); } #[test] @@ -623,6 +574,34 @@ mod tests { } } +#[cfg(not(miri))] +#[cfg(test)] +mod proptests { + use super::tests::euclidean_squared_distance_transform_reference; + use super::tests::{distance_transform_1d, distance_transform_1d_reference}; + use super::*; + use crate::proptest_utils::arbitrary_image; + use proptest::prelude::*; + + proptest! { + #[test] + fn test_distance_transform_1d_matches_reference_implementation(f in proptest::collection::vec(-10_000_000.0..10_000_000.0, 0..50)) { + let actual = distance_transform_1d(&f); + let expected = distance_transform_1d_reference(&f); + + assert_eq!(actual, expected); + } + + #[test] + fn test_euclidean_squared_distance_transform_matches_reference_implementation(image in arbitrary_image::>(0..10, 0..10)) { + let expected = euclidean_squared_distance_transform_reference(&image); + let actual = euclidean_squared_distance_transform(&image); + + assert_eq!(actual, expected) + } + } +} + #[cfg(not(miri))] #[cfg(test)] mod benches { diff --git a/src/filter/median.rs b/src/filter/median.rs index f52a9bd2..2ebe249b 100644 --- a/src/filter/median.rs +++ b/src/filter/median.rs @@ -414,13 +414,13 @@ mod benches { bench_median_filter!(bench_median_filter_s100_rx8_ry1, side: 100, x_radius: 8,y_radius: 1); } +#[cfg(not(miri))] #[cfg(test)] -mod tests { +mod proptests { use super::*; - use crate::property_testing::GrayTestImage; - use crate::utils::pixel_diff_summary; + use crate::proptest_utils::arbitrary_image; use image::{GrayImage, Luma}; - use quickcheck::{quickcheck, TestResult}; + use proptest::prelude::*; use std::cmp::{max, min}; // Reference implementation of median filter - written to be as simple as possible, @@ -470,20 +470,13 @@ mod tests { sorted[mid] } - #[cfg_attr(miri, ignore = "slow")] - #[test] - fn test_median_filter_matches_reference_implementation() { - fn prop(image: GrayTestImage, x_radius: u32, y_radius: u32) -> TestResult { - let x_radius = x_radius % 5; - let y_radius = y_radius % 5; - let expected = reference_median_filter(&image.0, x_radius, y_radius); - let actual = median_filter(&image.0, x_radius, y_radius); - - match pixel_diff_summary(&actual, &expected) { - None => TestResult::passed(), - Some(err) => TestResult::error(err), - } + proptest! { + #[test] + fn test_median_filter_matches_reference_implementation(image in arbitrary_image::>(0..10, 0..10), x_radius in 0_u32..5, y_radius in 0_u32..5) { + let expected = reference_median_filter(&image, x_radius, y_radius); + let actual = median_filter(&image, x_radius, y_radius); + + assert_eq!(actual, expected); } - quickcheck(prop as fn(GrayTestImage, u32, u32) -> TestResult); } } diff --git a/src/filter/mod.rs b/src/filter/mod.rs index f04abbce..35ac9e1f 100644 --- a/src/filter/mod.rs +++ b/src/filter/mod.rs @@ -582,7 +582,7 @@ mod tests { // I think the interesting edge cases here are determined entirely // by the relative sizes of the kernel and the image side length, so // I'm just enumerating over small values instead of generating random - // examples via quickcheck. + // examples via proptesting. for height in 0..5 { for width in 0..5 { for kernel_length in 0..15 { diff --git a/src/integral_image.rs b/src/integral_image.rs index 71bb3172..39fe947f 100644 --- a/src/integral_image.rs +++ b/src/integral_image.rs @@ -465,10 +465,7 @@ pub fn column_running_sum(image: &GrayImage, column: u32, buffer: &mut [u32], pa mod tests { use super::*; use crate::definitions::Image; - use crate::property_testing::GrayTestImage; - use crate::utils::pixel_diff_summary; use image::{GenericImage, Luma}; - use quickcheck::{quickcheck, TestResult}; #[test] fn test_integral_image_gray() { @@ -555,7 +552,7 @@ mod tests { } /// Simple implementation of integral_image to validate faster versions against. - fn integral_image_ref(image: &I) -> Image> + pub fn integral_image_ref(image: &I) -> Image> where I: GenericImage>, { @@ -579,19 +576,25 @@ mod tests { out } +} - #[cfg_attr(miri, ignore = "slow")] - #[test] - fn test_integral_image_matches_reference_implementation() { - fn prop(image: GrayTestImage) -> TestResult { - let expected = integral_image_ref(&image.0); - let actual = integral_image(&image.0); - match pixel_diff_summary(&actual, &expected) { - None => TestResult::passed(), - Some(err) => TestResult::error(err), - } +#[cfg(not(miri))] +#[cfg(test)] +mod proptests { + use super::tests::integral_image_ref; + use super::*; + use crate::proptest_utils::arbitrary_image; + use image::Luma; + use proptest::prelude::*; + + proptest! { + #[test] + fn test_integral_image_matches_reference_implementation(image in arbitrary_image::>(0..10, 0..10)) { + let expected = integral_image_ref(&image); + let actual = integral_image(&image); + + assert_eq!(expected, actual); } - quickcheck(prop as fn(GrayTestImage) -> TestResult); } } diff --git a/src/lib.rs b/src/lib.rs index 0c277736..1926747d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,8 +42,6 @@ pub mod morphology; pub mod noise; pub mod pixelops; pub mod point; -#[cfg(any(feature = "property-testing", test))] -pub mod property_testing; pub mod rect; pub mod region_labelling; pub mod seam_carving; diff --git a/src/property_testing.rs b/src/property_testing.rs deleted file mode 100644 index 5346ea66..00000000 --- a/src/property_testing.rs +++ /dev/null @@ -1,128 +0,0 @@ -//! Utilities to help with writing property-based tests -//! (e.g. [quickcheck] tests) for image processing functions. -//! -//! [quickcheck]: https://github.com/BurntSushi/quickcheck - -use crate::definitions::Image; -use image::{GenericImage, Luma, Pixel, Primitive, Rgb}; -use quickcheck::{Arbitrary, Gen}; -use rand_distr::{Distribution, Standard}; -use std::fmt; - -/// Wrapper for image buffers to allow us to write an Arbitrary instance. -#[derive(Clone)] -pub struct TestBuffer(pub Image); - -/// 8bpp grayscale `TestBuffer`. -pub type GrayTestImage = TestBuffer>; - -/// 24bpp RGB `TestBuffer`. -pub type RgbTestImage = TestBuffer>; - -impl Arbitrary for TestBuffer -where - ::Subpixel: Send, -{ - fn arbitrary(g: &mut Gen) -> Self { - let (width, height) = small_image_dimensions(g); - let mut image = Image::new(width, height); - for y in 0..height { - for x in 0..width { - let pix: T = ArbitraryPixel::arbitrary(g); - image.put_pixel(x, y, pix); - } - } - TestBuffer(image) - } - - fn shrink(&self) -> Box>> { - Box::new(shrink(&self.0).map(TestBuffer)) - } -} - -/// Workaround for not being able to define Arbitrary instances for pixel types -/// defines in other modules. -pub trait ArbitraryPixel { - /// Generate an arbitrary instance of this pixel type. - fn arbitrary(g: &mut Gen) -> Self; -} - -fn shrink(image: &I) -> Box>> -where - I: GenericImage, - I::Pixel: 'static, -{ - let mut subs = vec![]; - - let w = image.width(); - let h = image.height(); - - if w > 0 { - let left = copy_sub(image, 0, 0, w - 1, h); - subs.push(left); - let right = copy_sub(image, 1, 0, w - 1, h); - subs.push(right); - } - if h > 0 { - let top = copy_sub(image, 0, 0, w, h - 1); - subs.push(top); - let bottom = copy_sub(image, 0, 1, w, h - 1); - subs.push(bottom); - } - - Box::new(subs.into_iter()) -} - -fn copy_sub(image: &I, x: u32, y: u32, width: u32, height: u32) -> Image -where - I: GenericImage, -{ - let mut out = Image::new(width, height); - for dy in 0..height { - let oy = y + dy; - for dx in 0..width { - let ox = x + dx; - out.put_pixel(dx, dy, image.get_pixel(ox, oy)); - } - } - out -} - -impl fmt::Debug for TestBuffer { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "width: {}, height: {}, data: {:?}", - self.0.width(), - self.0.height(), - self.0.enumerate_pixels().collect::>() - ) - } -} - -fn small_image_dimensions(g: &mut Gen) -> (u32, u32) { - let dims: (u8, u8) = Arbitrary::arbitrary(g); - ((dims.0 % 10) as u32, (dims.1 % 10) as u32) -} - -impl ArbitraryPixel for Rgb -where - Standard: Distribution, -{ - fn arbitrary(g: &mut Gen) -> Self { - let red: T = T::arbitrary(g); - let green: T = T::arbitrary(g); - let blue: T = T::arbitrary(g); - Rgb([red, green, blue]) - } -} - -impl ArbitraryPixel for Luma -where - Standard: Distribution, -{ - fn arbitrary(g: &mut Gen) -> Self { - let val: T = T::arbitrary(g); - Luma([val]) - } -} diff --git a/src/suppress.rs b/src/suppress.rs index 0431d646..4b97a2fe 100644 --- a/src/suppress.rs +++ b/src/suppress.rs @@ -175,10 +175,7 @@ where mod tests { use super::{local_maxima, suppress_non_maximum}; use crate::definitions::{Image, Position, Score}; - use crate::property_testing::GrayTestImage; - use crate::utils::pixel_diff_summary; use image::{GenericImage, GrayImage, Luma, Primitive}; - use quickcheck::{quickcheck, TestResult}; use std::cmp; #[derive(PartialEq, Debug, Copy, Clone)] @@ -274,7 +271,7 @@ mod tests { /// Reference implementation of suppress_non_maximum. Used to validate /// the (presumably faster) actual implementation. - fn suppress_non_maximum_reference(image: &I, radius: u32) -> Image> + pub fn suppress_non_maximum_reference(image: &I, radius: u32) -> Image> where I: GenericImage>, C: Primitive + Ord, @@ -320,19 +317,6 @@ mod tests { out } - #[test] - fn test_suppress_non_maximum_matches_reference_implementation() { - fn prop(image: GrayTestImage) -> TestResult { - let expected = suppress_non_maximum_reference(&image.0, 3); - let actual = suppress_non_maximum(&image.0, 3); - match pixel_diff_summary(&actual, &expected) { - None => TestResult::passed(), - Some(err) => TestResult::error(err), - } - } - quickcheck(prop as fn(GrayTestImage) -> TestResult); - } - #[test] fn test_step() { assert_eq!((0u32..5).step_by(4).collect::>(), vec![0, 4]); @@ -341,6 +325,26 @@ mod tests { } } +#[cfg(not(miri))] +#[cfg(test)] +mod proptests { + use super::suppress_non_maximum; + use super::tests::suppress_non_maximum_reference; + use crate::proptest_utils::arbitrary_image; + use image::Luma; + use proptest::prelude::*; + + proptest! { + #[test] + fn test_suppress_non_maximum_matches_reference_implementation(image in arbitrary_image::>(0..10, 0..10)) { + let expected = suppress_non_maximum_reference(&image, 3); + let actual = suppress_non_maximum(&image, 3); + + assert_eq!(expected, actual); + } + } +} + #[cfg(not(miri))] #[cfg(test)] mod benches {