-
Notifications
You must be signed in to change notification settings - Fork 592
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
RFC: Rethink GenericImage #1118
Comments
Should we make a project for planning for |
I haven't used it before, but seems like it is worth a try |
I would like to propose something that looks more like the |
Could you sketch out a bit more of what that might look like? In particular, what would be the type signature of the Iterator::next() function equivalent on |
(*Note: moved from a discussion on an alternative container that should implement the trait) I'm no longer sure if the trait is the correct public interface. We've already had plenty of examples in My main question was why we are relying on a trait at all? All C libraries for imageprocessing can do without it, so does |
One of the key differences between the two traits is that ContiguousImage defines the exact memory layout of the image data. As a result, it makes specialization based on layout irrelevant. It is true that algorithms would potentially need an extra switch based of color type that could be avoided in the static typing scenario, but the API is such that it could be done once per image (instead of say once per pixel) which means the overhead should be imperceptible. The net result is that a downstream developer (or even the imageproc crate) could write their own ContigiousImageExt trait with additional image processing algorithms that either was implemented in terms of the provided ones, or matched on the color type and then operated on the backing buffer directly, and wouldn't pay any performance penalty for doing so
The only reason to have a trait would be so that dynamic and static color typed images (i.e. DynamicImage and ImageBuffer) could have the same methods. But really the core of the proposal is that all image algorithms should be implemented for dynamically typed images with a well defined layout. This is a property that the python libraries, and I believe some/most C libraries do have. Using a trait to accomplish it is secondary
I don't think that would actually be much of a difference. Wrapping the values of the required methods from ContigiousImage into XxxView and then implementing AsRef / AsMut would be essentially equivelent to implementing ContigiousImage directly. |
Imposing specific invariants on some data is what a type is for, not a trait. Unless we are talking about making an Furthermore, #1084 and planar images are (imo) evidence that we can not feasibly work with a single memory layout. Adding multiple traits doesn't work since that will involve specialization to make use of them but adding different types for these different memory layouts does work, and allows well-defined extension traits as well.
The same is true if these are types and downstream crates add a trait that add a couple of methods to the types.
Indeed, that was the point I was trying to make. If presented with two largely equivalent solutions, the simpler one should be chosen. |
I think it is OK not to have first class support for weird memory layouts. Just providing conversion methods should be enough for most use cases (including for the author of #1084). Though I'm not sure I follow your point about needing an unsafe trait to guarantee memory layout: the buffer returned by ContingiousImage::data() will be assumed to be a packed array of pixels by downstream consumers and the user will just get garbled output/panics if the function is implemented wrong, but never actual UB. I agree with you about trying to get simplicity, but as I think through the type based version I'm less sure it is actually simpler. For the trait version it is just two types and a trait that provides methods on them, but I'm a bit fuzzy what the other would look like. Could you sketch out exactly how the various derefs and View / ViewMut types would look like? |
The following is wrong with wanting to provide a safe trait that tries to enforce a particular layout of image data: // No guarantee that this corresponds to `ColorType` or the dimensions!
// Need to recheck on every access in a generic implementation using the
// trait. We know for a fact from imageproc that the compiler will not always
// elide these checks.
fn data_mut(&mut self) -> &mut [u64];
// An implementation could overwrite this!
// Same problem as `Deref` and `AsRef` and `Error::type_id` on which you
// can not unsafely rely.
fn as_bytes(&self) -> &[u8] { bytemuck::bytes_of(self.data()) }
// Either `DynamicImage` is very open or it is hard to give a performant
// implementation of this. What about in-place conversion? It would introduce
// a new `Sized` bound or is not always implementable.
fn convert_to(&self, new: PixelType) -> DynamicImage { ... }
// Why exactly `DynamicImage`? And if we have one specific choice here then
// what is the benefit over mutable inherent methods on `DynamicImage`?
fn resize(&self, ...) -> DynamicImage { ... }
We already have The trait could be similar to trait AsPixels<P: Pixel> {
fn as_pixels(&self) -> ViewMut<&'_ mut [P::Subpixel], P>;
} This solves the problem with The remaining question is whether or not Also a short remark on |
Ah, you were thinking of "enforcing layout" much more strongly than I was. I was just thinking that if someone chooses to implement the trait in a way that returns bogus/inconsistent values for the methods, then we get panics/breakage (but not UB). This is true of other traits too, like Hash/Eq which the documentation points out can result in weird behavior if implemented wrong. Though you make a good point that it would be nice to validate the fields once and then be able to have unsafe code rely on them being in a consistent state.
The problem with using ViewMut directly is that it is parameterized on Pixel, which would prevent DynamicImage from implementing AsPixels.
Wouldn't this design require a method call on every use of AsPixels? let mut img: DynamicImage = load_image_somehow();
img.as_pixels().blur(...)?;
img.as_pixels().rotate(...)?; |
I'd argue that declaring a single in-memory layout for the core of the crate drastically reduces complexity. But I agree that should be a separate discussion from defining PixelType. |
It would be like the relationship of
Also already partially solved on a concrete type. The current |
This would be very unpleasant for writing any downstream generic code. Essentially any operation on a dynamic image would be a match with separate branches for each possibility. This is a current annoyance of the crate, and something I'd like to resolve by just making the color/pixel type a field instead of a type parameter in most places.
What about something like this then? trait ContigiousImage {
// Single methods which ensures invariants are met. If you have a DynamicView/DynamicViewMut.
// then these are no-ops, otherwise they are cheap checks that the layout is right
fn as_view(&self) -> DynamicView;
fn as_view_mut(&mut self) -> DynamicViewMut;
// Provided methods that can be called directly on any anything that implements this trait.
// Internally they all call as_view / as_view_mut and then perform the operation on that
fn blur(&mut self, radius: f32) { ... }
fn rotate(&mut self, angle: f32) { ... }
} |
Any performant code will need slices of concrete types, there are no cache efficient alternatives. The current usability crisis comes from the fact that there are not enough algorithmic traits for even basic operations since even those traits can not work efficiently on trait PixelOblivious {
fn apply<P: Pixel>(&mut self, view: ViewMut<P>);
}
struct Blur(f32);
impl PixelOblivious for Blur {
fn apply<P>(&mut self, view: ViewMut<P>) {
imageops::blur(view, self.0);
}
}
impl DynamicImage {
fn apply(&mut self, mut alg: PixelOblivious) {
match self.color_type {
ColorType::Rgb8 => alg.apply(self.as_view::<Rgb<u8>>().unwrap()),
…
}
}
pub fn blur(&mut self, amount: f32) {
self.apply(Blur(amount));
}
} I'm not oposed to having |
This makes |
If DynamicImage was the only type in the crate that supported image algorithms this would be fine with me. But right now you can run image algorithms on anything that implements GenericImage, and DynamicImages get left out. My proposal is inspired by thinking "what if we had a similar trait that worked with statically and dynamically typed images". Adding boilerplate code to DynamicImage for each possible algorithm doesn't really solve this, because downstream users would have to write the same boilerplate (without the help of PixelOblivious).
But why even bother with implementations in other places? AsDynamicViewMut (or whatever we end up calling it) could just be the only place things are implemented and the main types could each implement. DynamicViewMut in particular should have no trouble implementing the trait |
It feels very much as if Implementing the exact same thing on
Adding it to one specific struct is powerful as Rust does not have Higher-Ranked-traits, it also creates a unified vocabulary. I see this as similar to how a dynamic amount of borrowed data should almost always be passed via a slice, and not as This option also avoids binary bloat from monomorphization. |
We have to make these decisions at some point. In particular it isn't really possible to write generic code without this. In fact, some of the existing algorithm implementations are problematic precisely because they assume facts about layout that aren't actually guaranteed.
The lack of a Deref implementation is the big issue. Nobody would tolerate it if you had to do |
True, and in light of Maybe I tried to look at this issue of traits backwards. It is not quite that we should have no traits, in fact operations such as impl<T: GenericImage> Blur for T {
// This is too inflexible to get the best performance in all cases.
// * It hinders downstream impls that must not intersect `GenericImage`.
// This is already a HUGE issue in `imageproc`.
// * Internal performance suffers as we don't have specialization.
} But if we only look at concrete impls then the |
I missed this thread and just posted #1159. I think it's relevant to this topic. |
Right now, most of the image algorithms are implemented for the GenericImage trait. I see a couple problems with this design:
I propose we instead implement the various algorithms directly within a single trait that both statically and dynamically typed images can implement. We'll lose the ability for downstream users to add new pixel types (though that capability was dubious in practice since we really don't let users actually describe the meaning of pixel types), but should end up with a design that is both cleaner and hopefully more correct.
The text was updated successfully, but these errors were encountered: