Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gradient enum and Kernel trait V2 #601

Closed
wants to merge 9 commits into from
Closed

Conversation

ripytide
Copy link
Contributor

@ripytide ripytide commented May 10, 2024

This is a version 2 PR of #596

This time I've added a Kernel trait to allow heap OR stack allocated kernel usage using OwnedKernel and BorrowedKernel respectively. I also rolled back the 1D kernel function back to taking &[K].

Summary:

  • Made more functions taking 2D kernels the impl Kernel<K> trait for Heap or Stack kernels
  • Moved HORIZONTAL_SOBEL and similar constants to OwnedKernel::sobel_horizontal_3x3() and made them generic for any `T: From
  • Removed horizontal_* and vertical_* functions since they can all be achieved via composition such as filter(image, OwnedKernel::sobel_horizontal_3x3()) which is more explicit and rusty.
  • Bumped MSRV to 1.75.0 for the enumerate() method of Kernel which uses return position impl traits.
  • Made Kernel::filter a free-standing function which is more consistent with the rest of the library crate::filter::filter()
  • Added a filter_clamped() function
  • Removed the filter3x3() function as it is no different from filter_clamped()

let gradient = map_pixels(&gradient, |_, _, p| Luma([(p[0].abs() / scale) as u8]));
gradient.save(&output_dir.join(file_name)).unwrap();
) {
let horizontal_gradients = filter3x3(input, gradient_kernel.kernel1::<i16>());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know why we should change the filter3x3 signature. Just Kernel::filter

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know what you mean?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can just call kernel.filter(input), why do you want to change filter3x3? It's very popular function and will not compile after merge for most users

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason is because of #520 where users are struggling to discover the filter() method because it is a associated method rather than a free-standing function like every other function in the library. This is very inconsistent and so it would be better to just use filter3x3(image, kernel) or filter(image, kernel) instead of kernel.filter(image).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can do it without breaking changes!
What about pub fn filter<'a>(image: &Image<P>, kernel: &Kernel<'a, K>, f: impl FnMut(&mut Q::Subpixel, K)) -> Image<Q>?

We can also implement Deref, so it's possible to call filter3x3(&img, &kernel)

impl<'a, T> Deref for Kernel<'a, T> {
    type Target = [T];
    fn deref(&self) -> &Self::Target {
        self.data
    }
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not difficult to find Kernel methods, we just don't have examples. Also there is no clamped_filter method, which is easier to use

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a filter_clamped() method in this PR for convenience.

pub fn filter<'a>(image: &Image

, kernel: &Kernel<'a, K>, f: impl FnMut(&mut Q::Subpixel, K)) -> Image

This is already possible by the filter() method in this PR

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But you have a trait

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes the trait is used in the filter_clamped() function. Its signature is simply: pub fn filter_clamped<P, K, S>(image: &Image<P>, kernel: impl Kernel<K>) -> Image<ChannelMap<P, S>>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OwnedKernel and BorrowedKernel are the main two types which now implement the Kernel trait.

Copy link
Contributor

@cospectrum cospectrum left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Breaking changes, and there are only 4 days left before the release.

@ripytide
Copy link
Contributor Author

I'm in no rush to get this merged before the next release, the breaking changes are allowable since we're in pre 1.0 stage, and justified given gradients() and filter() are much more flexible now than before and GradientKernel is more ergonomic in my opinion.

@cospectrum
Copy link
Contributor

cospectrum commented May 10, 2024

impl Kernel<'static, i32> {
    pub fn vertical_sobel() -> Self {
        Self::new(&crate::gradients::VERTICAL_SOBEL, 3, 3)
    }
}

@cospectrum
Copy link
Contributor

cospectrum commented May 10, 2024

Creating traits and additional Kernel structs is too much for a small issue like #520. I don't think the owner will be convinced, but I have suggested a possible solution.

@ripytide
Copy link
Contributor Author

The point is that consistency is important and having some important methods be free-standing and other associated with types is inconsistent. This PR fixes that. The traits are simply much more flexible as they allow heap OR stack allocated kernels which is necessary for dynamically sized kernels such as for the GradientKernel which is 3x3 or 2x2.

@cospectrum
Copy link
Contributor

which is necessary for dynamically sized kernels such as for the GradientKernel which is 3x3 or 2x2

No, it's not

#[derive(Debug, Copy, Clone)]
pub enum Gradient {
    /// The 3x3 Sobel kernel
    /// See <https://en.wikipedia.org/wiki/Sobel_operator>
    Sobel,
    /// The 3x3 Scharr kernel
    Scharr,
    /// The 3x3 Prewitt kernel
    Prewitt,
    /// The 2x2 Roberts kernel
    /// See <https://en.wikipedia.org/wiki/Roberts_cross>
    Roberts,
}

impl Gradient {
    pub fn vertical_kernel(&self) -> Kernel<'static, i32> {
        match self {
            Gradient::Sobel => Kernel::new(&VERTICAL_SOBEL, 3, 3),
            Gradient::Scharr => Kernel::new(&VERTICAL_SCHARR, 3, 3),
            Gradient::Prewitt => Kernel::new(&VERTICAL_PREWITT, 3, 3),
            Gradient::Roberts => Kernel::new(&VERTICAL_ROBERTS, 2, 2),
        }
    }
}

@ripytide
Copy link
Contributor Author

ripytide commented May 10, 2024

Ok, it's not necessary for the compile-time dynamic GradientKernel enum, but it is necessary for run-time dynamically sized kernels, like for example a cli script that took the kernel size as a command-line argument and then did a convolution based on that kernel size.

How would you implement this?

pub fn guass_kernel(radius: u32) -> BorrowedKernel<'static, i32> {
    //impossible without leaking memory
}

@cospectrum
Copy link
Contributor

I would just return Vec<i32> or Image<Luma<T>>.
Maybe you should create an issue and ask @theotherphil about some changes in Kernel api? This way you won't waste your time writing code that won't be accepted.

@ripytide
Copy link
Contributor Author

Vec<i32> is missing dimensions, Image cannot be used with any of the filter functions both are inferior to OwnedKernel + the Kernel trait.

A PR is easier to discuss changes over than an issue as it shows the full extent of a change required.

@cospectrum
Copy link
Contributor

Maybe we can add from_image and that's it.

impl<'a, T> Kernel<'a, T>
where T: Primitive
{
    /// Create kernel from image
    pub fn from_image(image: &'a Image<Luma<T>>) -> Self {
        Self::new(image.as_ref(), image.width(), image.height())
    }
}

@ripytide
Copy link
Contributor Author

ripytide commented May 10, 2024

But that still doesn't allow the implementation of guass_kernel().

pub fn guass_kernel(radius: u32) -> BorrowedKernel<'static, i32> {
    let image = (0..radius).cartesian_product(0..radius);
    BorrowedKernel::from_image(&image) // borrow checker issue
}

@cospectrum
Copy link
Contributor

cospectrum commented May 10, 2024

So, the question is whether we want struct OwnedKernel or not.

@ripytide
Copy link
Contributor Author

ripytide commented May 10, 2024

Why not add it @cospectrum? Owned structs can also be stored in other structs/enums more easily as they don't have leaky lifetimes.

@cospectrum
Copy link
Contributor

I don't have any strong arguments for or against. your example make sense, but there is one point: in the library itself there is not yet a single place where we could use this OwnedKernel. That is, it will be an export-only struct.

@theotherphil
Copy link
Contributor

Thanks for your work on exploring options to improve filter ergonomics.

Unfortunately these changes add too much user facing complexity for the benefits they bring.

Could the discoverability issue of Kernel::filter be fixed by just making this associated function freestanding?

@ripytide
Copy link
Contributor Author

Could the complexity of using a Kernel trait can be mitigated by adding examples to methods taking kernel arguments? I disagree that the ergonomics and flexibility is not worth the complexity.

Dynamically sized kernels seems to me to be a fairly reasonable usecase, for example, opencv has the ksize parameter (https://docs.opencv.org/4.x/d4/d86/group__imgproc__filter.html#gaabe8c836e97159a9193fb0b11ac52cf1) which would be made much easier to implement using the OwnedKernel.

@cospectrum
Copy link
Contributor

cospectrum commented May 11, 2024

I don't really understand what the problem is with the Kernel::filter. 2 ppl in the issie saw filter3x3 function and wanted filter_nxm(image: &GrayImage, n: usize, m: usize, kernel: &[T]), they didn't want OwnedKernel or Kernel trait. And the trait will cause much more trouble to the user than just a structure with a method, btw.
clamped_filter is what they need.

@ripytide
Copy link
Contributor Author

ripytide commented May 12, 2024

Wait a minute, @cospectrum

No, it's not

#[derive(Debug, Copy, Clone)]
pub enum Gradient {
    /// The 3x3 Sobel kernel
    /// See <https://en.wikipedia.org/wiki/Sobel_operator>
    Sobel,
    /// The 3x3 Scharr kernel
    Scharr,
    /// The 3x3 Prewitt kernel
    Prewitt,
    /// The 2x2 Roberts kernel
    /// See <https://en.wikipedia.org/wiki/Roberts_cross>
    Roberts,
}

impl Gradient {
    pub fn vertical_kernel(&self) -> Kernel<'static, i32> {
        match self {
            Gradient::Sobel => Kernel::new(&VERTICAL_SOBEL, 3, 3),
            Gradient::Scharr => Kernel::new(&VERTICAL_SCHARR, 3, 3),
            Gradient::Prewitt => Kernel::new(&VERTICAL_PREWITT, 3, 3),
            Gradient::Roberts => Kernel::new(&VERTICAL_ROBERTS, 2, 2),
        }
    }
}

This doesn't work for any type T, only i32, whereas my implementation works for any T: From<i8>

@ripytide
Copy link
Contributor Author

You would have to write a constant for every single type which would be super annoying to use.

@ripytide
Copy link
Contributor Author

ripytide commented May 12, 2024

I've now removed the GradientKernel enum and moved all the Kernel constructors to associated methods of OwnedKernel.

@cospectrum
Copy link
Contributor

Wait a minute, @cospectrum

No, it's not

#[derive(Debug, Copy, Clone)]
pub enum Gradient {
    /// The 3x3 Sobel kernel
    /// See <https://en.wikipedia.org/wiki/Sobel_operator>
    Sobel,
    /// The 3x3 Scharr kernel
    Scharr,
    /// The 3x3 Prewitt kernel
    Prewitt,
    /// The 2x2 Roberts kernel
    /// See <https://en.wikipedia.org/wiki/Roberts_cross>
    Roberts,
}

impl Gradient {
    pub fn vertical_kernel(&self) -> Kernel<'static, i32> {
        match self {
            Gradient::Sobel => Kernel::new(&VERTICAL_SOBEL, 3, 3),
            Gradient::Scharr => Kernel::new(&VERTICAL_SCHARR, 3, 3),
            Gradient::Prewitt => Kernel::new(&VERTICAL_PREWITT, 3, 3),
            Gradient::Roberts => Kernel::new(&VERTICAL_ROBERTS, 2, 2),
        }
    }
}

This doesn't work for any type T, only i32, whereas my implementation works for any T: From<i8>

But we don't need any type.

@ripytide
Copy link
Contributor Author

Not yet, but what if users have different image pixel formats: u8, i8, u16, etc.. it would be very wasteful to always cast to i32. Would it not be better if the kernels could be created in any generic format from i8?

@cospectrum
Copy link
Contributor

A dynamic cast from one type to another will still be a waste of resources. I would expect more from the library. For example, generate all static arrays using macros.

@ripytide
Copy link
Contributor Author

Despite the fact that kernel creation is incomparably tiny when compared with image convolution? That seems like a heck of performance/complexity tradeoff. A lot of the casts can also be optimized by the compiler since the generic will always be known at compile time.

@ripytide
Copy link
Contributor Author

I'm going to close this and work on a V3 that discards the disagreeable things like the Kernel trait and just focuses on the minimal other things that have been largely agreed on.

@ripytide ripytide closed this May 19, 2024
@ripytide ripytide mentioned this pull request May 19, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants