etemesi254 / zune-image

A fast and memory efficient image library in Rust
Other
330 stars 30 forks source link

How do you copy pixels in a rect area from one `Image` to another? #163

Closed hillin closed 8 months ago

hillin commented 8 months ago

In image-rs we can do this with the copy_from method.

Sorry but I didn't find any hint in the docs or code. If zune-image does not have this yet, this issue acts as a feature request too.

etemesi254 commented 8 months ago

Hi, you can assume that this copy_from is a simple crop operation. With that in mind and if you are willing to add a dependency , zune-imageprocs , you can use the crop filter

fn main() {
    //image dimensions
    let im_width = 1000;
    let im_height = 1000;

    let mut image = Image::fill(255_u8, ColorSpace::RGBA, im_width, im_height);
    let x = 10;
    let y = 10;

    let new_width = im_width - x;
    let new_height = im_height - y;
    // creates a new image, and applies the operation on it,
    // better if you don't want to modify the original image
    let new_image = Crop::new(new_width, new_height, x, y).clone_and_execute(&image).unwrap();
    // modifies in place
    // better if memory is a concern
    Crop::new(new_width, new_height, x, y).execute(&mut image).unwrap();
}

Above, x and y correspond to the same parameters as copy_from in image-rs.

Crop docs https://docs.rs/zune-imageprocs/latest/zune_imageprocs/crop/index.html

Hope that helps.

Feel free to ask any question if there is another concern

hillin commented 8 months ago

Thanks @etemesi254 !

The copy_from does a little bit more than a simple cropping: it copies image A to image B at a given position on image B, overwrites the pixels that image A covers, while keeping other pixels on image B untouched. In another word, we "draw" image A on image B.

Also notice that image-rs has the concept of "view", which is a virtual image that is projected to a rectangular region of a "real" image (bitmap). Unlike cropping, no new image is allocated when creating a view. In the example above, image A can be a view of another image A', hence we can copy a region of image A' to the specified position on image B, by combining these two operations.

etemesi254 commented 8 months ago

Then that's a really bad function name in my opinion then.

What you are trying to achieve is to composite one image over another, which most libraries call it composite. This takes into account alpha channels. Now this is way more complicated and to get it correctly is difficult, see https://www.youtube.com/watch?v=XobSAXZaKJ8

But we still support it, via the Composite filter, although not yet fully complete, it can perform the same emulation as before.

It wasn't documented so sorry for that.

Here's an example of me putting a logo on bottom right

use zune_core::colorspace::ColorSpace;
use zune_image::image::Image;
use zune_image::traits::OperationsTrait;

use zune_imageprocs::composite::{Composite, CompositeMethod};
use zune_imageprocs::utils::Gravity;

fn composite_over() {
       // logo image
        let mut src_image = Image::open("logo.png").unwrap();
        // background image
        let mut dst_image = Image::open("wallpaper.jpg").unwrap();

        // Convert to RGBA since the logo has a transparent background,
       //  the composite operation needs images with the same colorspace
       //  if the colorspaces differ this is an error
        src_image.convert_color(ColorSpace::RGBA).unwrap();
        dst_image.convert_color(ColorSpace::RGBA).unwrap();

       // It is recommended to pre-multiply before blending, so do that
        PremultiplyAlpha::new(AlphaState::PreMultiplied)
            .execute(&mut src_image)
            .unwrap();

       // it's called try_new because one value, either Gravity or Dimensions have to be specified
       // not both at the same time
        let composite = Composite::try_new(
            &src_image,
            CompositeMethod::Over,
            None,
            Some(Gravity::BottomRight)
        ).unwrap();
        // run the operation.
        composite.execute(&mut dst_image).unwrap();
        // save image, the save operation will take care of converting it to an RGB, jpeg doesn't support RGBA
        dst_image.save("./composite.jpg").unwrap();
    }

Also notice that image-rs has the concept of "view", which is a virtual image that is projected to a rectangular region of a "real" image (bitmap). Unlike cropping, no new image is allocated when creating a view. In the example above, image A can be a view of another image A', hence we can copy a region of image A' to the specified position on image B, by combining these two operations.

I think this is a sub-image, currently unimplemented as it complicates other things, but it can be added if there is a need

hillin commented 8 months ago

Great! Composite should definitely do the job.

We can say Composite generalizes what image-rs' copy_from does (by supporting alpha blending, or even bit blitting - I don't know), but to my understanding there is still some difference here: the copy in the bad naming actually implies that it's a fast block copy operation (which may prove to be difficult to implement in zune-image as it has a different memory model as far as I'm concerned).

Anyways, I'll try the Composite approach! Thanks for the help!

etemesi254 commented 8 months ago

We can say Composite generalizes what image-rs' copy_from does (by supporting alpha blending, or even bit blitting - I don't know), but to my understanding there is still some difference here: the copy in the bad naming actually implies that it's a fast block copy operation (which may prove to be difficult to implement in zune-image as it has a different memory model as far as I'm concerned).

The operation can be done quickly, that isn't hard to do

Doing it correctly is more difficult

I chose to do it correctly over quickly

hillin commented 8 months ago

With the help of Composite (plus the half-completed Resize), we have successfully migrated from image-rs to zune-image.

Along the way we created a benchmark here: https://github.com/hillin/image-rs-vs-zune-image. It measures the performance of one simple task, which is an extremely hot path in our project: render a rectangular region(a) from image A, onto another rectangular region(b) on image B. This involves cropping the region a from A, resize it to fit the region b and composite it to B.

With image-rs we use GenericImageView::view to crop (actually, create a sub-image), imageops::resize for resizing, and GenericImage::copy_from to composite. In zune-image these are replaced with zune_imageprocs::crop::Crop, zune_imageprocs::resize::Resize and zune_imageprocs::composite::Composite respectively.

Benchmark result on i9-9900K (3.60 GHz) + 32GB RAM, using criterion:

// load is loading a 512x512 lenna.jpg
image-rs load           time:   [2.0388 ms 2.0668 ms 2.0973 ms]
zune-image load         time:   [1.1924 ms 1.2063 ms 1.2206 ms]

image-rs render         time:   [3.0728 ms 3.1115 ms 3.1531 ms]
zune-image render       time:   [1.7630 ms 1.7722 ms 1.7814 ms]

The benchmark showed that zune-image is remarkably fast, 45% faster than image-rs on average. Great job on this amazing lib!

etemesi254 commented 8 months ago

Thanks, hope the multiple APIs didn't bog you down.

The architecture is that way for easy optimizations of each operation.

hillin commented 8 months ago

The API design is not a problem at all and I'm quite happy with them. Keep the great job up!