-
Notifications
You must be signed in to change notification settings - Fork 145
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
Conversation
examples/gradients.rs
Outdated
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>()); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)
.
There was a problem hiding this comment.
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
}
}
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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>>
There was a problem hiding this comment.
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.
There was a problem hiding this 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.
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 |
impl Kernel<'static, i32> {
pub fn vertical_sobel() -> Self {
Self::new(&crate::gradients::VERTICAL_SOBEL, 3, 3)
}
} |
Creating |
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 |
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),
}
}
} |
Ok, it's not necessary for the compile-time dynamic How would you implement this? pub fn guass_kernel(radius: u32) -> BorrowedKernel<'static, i32> {
//impossible without leaking memory
} |
I would just return |
A PR is easier to discuss changes over than an issue as it shows the full extent of a change required. |
Maybe we can add 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())
}
} |
But that still doesn't allow the implementation of pub fn guass_kernel(radius: u32) -> BorrowedKernel<'static, i32> {
let image = (0..radius).cartesian_product(0..radius);
BorrowedKernel::from_image(&image) // borrow checker issue
} |
So, the question is whether we want struct |
Why not add it @cospectrum? Owned structs can also be stored in other structs/enums more easily as they don't have leaky lifetimes. |
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 |
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 |
Could the complexity of using a Dynamically sized kernels seems to me to be a fairly reasonable usecase, for example, |
I don't really understand what the problem is with the |
Wait a minute, @cospectrum
#[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 |
You would have to write a constant for every single type which would be super annoying to use. |
I've now removed the |
But we don't need any type. |
Not yet, but what if users have different image pixel formats: |
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. |
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. |
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. |
This is a version 2 PR of #596
This time I've added a
Kernel
trait to allow heap OR stack allocated kernel usage usingOwnedKernel
andBorrowedKernel
respectively. I also rolled back the 1D kernel function back to taking&[K]
.Summary:
impl Kernel<K>
trait for Heap or Stack kernelsHORIZONTAL_SOBEL
and similar constants toOwnedKernel::sobel_horizontal_3x3()
and made them generic for any `T: Fromhorizontal_*
andvertical_*
functions since they can all be achieved via composition such asfilter(image, OwnedKernel::sobel_horizontal_3x3())
which is more explicit and rusty.enumerate()
method ofKernel
which uses return position impl traits.Kernel::filter
a free-standing function which is more consistent with the rest of the librarycrate::filter::filter()
filter_clamped()
functionfilter3x3()
function as it is no different fromfilter_clamped()