"Resizing" an image rgb values vector

Hey there :wave:

I'm kind of stuck here. I have an image of size 2560x1600 converted into a vector which consists of a repeating pattern [r,g,b,r,g,b,r,g,b...] (through this library) which I want to "reduce" to one of length 12 (4x1).

This would be done to get a sort of average colour per area for those 4 sections.

I was wondering if someone has an idea of how to approach this or if there is a library/algorithm I could use since so far all my approaches have failed.

I'm assuming that's 4x3, not 4x1? And how exactly do you want to "reduce" he values? How are the sections defined? Are they the 4 quadrants of the image or something else?

Ah no its 4 pixels, each having their r,g,b components so 12 values in total. Each pixel represents a quarter of the image horizontally.

Reduce is as in getting the average for each zone. Its done the same way image editors do it when resizing the image. So

Becomes
image
(Save for the resizing back up so it can be seen)

Are you having difficulty breaking up the input image or performing the average?


Edit: I misunderstood what you wanted (I thought you wanted 4 quadrants like "upper left", not slices). You could modify this idea to get quarter-rows though.

For the former, you basically need a way to process each half-row as part of a specific quadrant. You could do this by:

  • Split the slice into a top half and a bottom half
  • Create iterators over the left-half and right-half of each row from those
  • Optionally map each half-row iterator to a pixel iterator

And then you could average the pixels from each quadrant's iterator.


For the latter, color theory is a deep topic and there's no single approach to averaging colors. The most common are probably:

  • Do a naive arithmetic mean of the values
    • Don't do this, SRGB is not a linear space and you'll get poor results
    • But the poor results may match what some other software does...
  • Average the sum of squares, then take the square root of that
  • Convert to a linear colorspace like L*AB that more closely models human vision, and take an arithmetic mean in that space instead

Here's a quickly-thrown-together, completely untested playground to illustrate both sections above. Perhaps it's enough to point you in the right direction. (Edit: Be mindful of my misunderstanding.)

However, if you can find a library made for this purpose, that's likely a better approach than rolling your own. Quick and/or simple "image similarity" libraries often use something like this, for example, and are thus a part of many "image manager" projects.

1 Like

I'm not sure how strict your requirements are but if the goal is mainly to resize the image, there is functionality for this built into the image crate. Here I'm trying its resize function with a Lanczos filter, as well as its thumbnail function which uses linear resampling and is presumably speedier.

use image::io::Reader as ImageReader;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let img = ImageReader::open("image.jpg")?.decode()?;
    let resized = image::imageops::resize(&img, 4, 1, image::imageops::FilterType::Lanczos3);
    resized.save("lanczos.png")?;

    let thumbnail = image::imageops::thumbnail(&img, 4, 1);
    thumbnail.save("thumbnail.png")?;
    Ok(())
}

This gives me these files:

lanczos.png
lanczos
thumbnail.png
thumbnail

Above, I read an image file, but image is a very general-purpose crate and can be used on pixel data stored in memory as well. This can be RGB, RGBA, or even BGRA (which I bring up since it appears this is the format returned by scrap):

use image::buffer::ConvertBuffer;

type BgraImage<V> = image::ImageBuffer<image::Bgra<u8>, V>;

// scrap::Frame derefs to [u8], so this will create a BgraImage<&[u8]>
let bgra_img = BgraImage::from_raw(width, height, &*frame).expect("size error?!");
let rgb_img: image::RgbImage = bgra_img.convert();

If you want to do the averaging part yourself, image can still help with gathering the pixels that belong to each section. You could use GenericImage::sub_image to extract a portion of the image, and GenericImageView::pixels() to iterate over its pixels.

use image::{GenericImage, GenericImageView};

(0..4).map(|i| {
    // (this is a bit sloppy and might leave out a few pixels for widths not divisible by 4 🤷)
    let region = img.sub_image((width/4)*i, 0, width/4, height);
    let average = average_color(region.pixels());
    // do something with 'average'
})

fn average_color(pixels: impl Iterator<Item=image::Rgb<u8>>) -> image::Rgb<u8> {
    // define this yourself...
}
4 Likes

Reading @ExpHP's reply made me realize I misinterpreted how you want to split up the image. I'll add a note to my reply above too.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.