Cykooz / fast_image_resize

Rust library for fast image resizing with using of SIMD instructions.
Apache License 2.0
285 stars 25 forks source link

Equivalent to OpenCV `cv2.INTER_LINEAR` #32

Closed bedapisl closed 2 months ago

bedapisl commented 3 months ago

Hello is it possible to achieve exactly same results with this library as if I was using cv2.resize with interpolation=cv2.INTER_LINEAR from OpenCV?

I tried using Resizer::new(Convolution(Bilinear)) but the pixel values in the resulting image are slightly different.

Thanks

Cykooz commented 3 months ago

Do you ask about downscaling or upscaling?

Cykooz commented 3 months ago

As I understand, OpenCV uses "convolution" with fixed kernel size. In this case, downscaling very big image into very small one looks like a result of nearest "interpolation".

fast_image_resize uses convolution with adaptive kernel size. It requires more computations but makes more better result. Look at results of downscaling big image into small one with use of different OpenCV interpolations.

bedapisl commented 3 months ago

Downscaling is more important but I would ideally I would like both.

Yes, I see the results are visually better. But my goal is to reproduce a pipeline written in Python that uses OpenCV. I need exactly same results as in the OpenCV, no matter which ones look better.

Cykooz commented 3 months ago

I can try to implement a fixed kernel size, but I'm not sure if that would be enough to get exactly the same results. The result also depends on how a particular solution handles rounding of numbers, accuracy of intermediate calculations and other things.

bedapisl commented 2 months ago

Hello, can I ask you about some rough time estimate for this? Alternatively if you don't have time for it, I can try implementing it myself and send it to you for review.

Cykooz commented 2 months ago

I think I can do it for about 5-7 days. But I don't know what name to give these resizing methods. ConvolutionWithFixedKernalSize? Interpolation? Do you have any ideas?

bedapisl commented 2 months ago

I think both names you suggested make sense, I don't have any other idea.

Cykooz commented 2 months ago

I implemented Interpolation resize algorithm. I did the simple test to compare the result of Interpolation(FilterType::Bilinear) with the result of INTER_LINEAR from OpenCV. They look the same.

bedapisl commented 2 months ago

Perfect thanks. I will try it later.

Cykooz commented 2 months ago

I will publish this version on crates.org within 1-2 days.

Cykooz commented 2 months ago

I released version 4.2.0

bedapisl commented 2 months ago

Hello, I have tested the solution. The results are closer to these of OpenCV, but there are still some small +1/-1 differences. These are probably due to rounding as you said in your previous comment.

I am posting a reproducible example:

create_image.py

import cv2
import numpy as np

def create_image():
    width = 640
    height = 360

    np.random.seed(0)
    image = np.random.randint(0, 256, (height, width), dtype='uint8')

    cv2.imwrite("test_image.png", image)

def main():
    create_image()

if __name__ == "__main__":
    create_image()

resize_python.py

import cv2
import numpy as np

def resize_image(
    image: np.ndarray
) -> np.ndarray:
    new_height = 144
    new_width = 256

    scaled_image = cv2.resize(image, (new_width, new_height), interpolation=cv2.INTER_LINEAR)

    return scaled_image

def main():
    image = cv2.imread("test_image.png")
    resized = resize_image(image)
    print(resized[0:200,0,0].tolist())

if __name__ == "__main__":
    main()

main.rs

use std::fs;
use std::io::{BufWriter, Write};

use image::codecs::png::PngEncoder;
use image::{ImageEncoder, ImageReader};

use fast_image_resize::{FilterType, ResizeAlg, ResizeOptions, Resizer};
use fast_image_resize::images::Image;

fn main() {

    // Load test image
    let img = ImageReader::open("../test_image.png")
        .unwrap()
        .decode()
        .unwrap();

    let src_image = Image::from_vec_u8(img.width(), img.height(), img.into_bytes(), fast_image_resize::PixelType::U8).unwrap();

    // Create an empty destination image
    let new_height: usize = 144;
    let new_width: usize = 256;

    let mut dst_image = Image::from_vec_u8(new_width as u32, new_height as u32, vec![0; new_width*new_height as usize], fast_image_resize::PixelType::U8).unwrap();

    // Create a resizer
    let mut resizer = Resizer::new();
    let resize_options = ResizeOptions::new().resize_alg(ResizeAlg::Interpolation(FilterType::Bilinear));

    // Resize
    resizer.resize(
        &src_image,
        &mut dst_image,
        &resize_options,
    ).unwrap();

    // Print some pixels
    let pixels: Vec<u8> = dst_image.copy().into_vec().into_iter().step_by(new_width).collect();

    let pixel_strings: Vec<String> = pixels.into_iter().map(|pixel_value| pixel_value.to_string()).collect();
    println!("{}", pixel_strings.join(", "));

    // Save image
    let mut result_buf = BufWriter::new(Vec::new());
    PngEncoder::new(&mut result_buf)
        .write_image(
            dst_image.buffer(),
            new_width as u32,
            new_height as u32,
            image::ExtendedColorType::L8,
        )
        .unwrap();

    fs::write("../rust_output_image.png", result_buf.buffer()).expect("Unable to write file");
}

After running create_image.py I get following output from resize_image.py`:

[80, 113, 153, 116, 141, 74, 207, 216, 128, 62, 163, 138, 196, 59, 167, 194, 207, 188, 46, 149, 225, 191, 107, 143, 86, 62, 74, 103, 83, 156, 106, 192, 73, 149, 41, 103, 170, 150, 170, 76, 52, 149, 115, 129, 182, 81, 112, 122, 103, 147, 149, 90, 127, 128, 52, 125, 214, 146, 137, 174, 109, 150, 157, 103, 74, 133, 39, 208, 178, 167, 77, 178, 92, 158, 199, 189, 153, 153, 152, 112, 180, 75, 80, 144, 122, 84, 79, 228, 41, 177, 40, 60, 127, 159, 164, 128, 114, 181, 60, 55, 145, 153, 134, 95, 137, 67, 157, 130, 36, 151, 154, 68, 47, 93, 121, 77, 149, 185, 83, 136, 165, 49, 75, 208, 157, 168, 89, 67, 121, 143, 154, 151, 132, 72, 165, 131, 175, 97, 76, 85, 139, 182, 48, 166]

and a sligthly different output from running the rust code:

81, 113, 153, 117, 142, 74, 207, 216, 128, 62, 163, 138, 196, 59, 167, 195, 207, 188, 46, 150, 225, 191, 108, 144, 86, 62, 75, 103, 84, 157, 106, 192, 73, 150, 41, 103, 170, 151, 170, 76, 53, 149, 115, 130, 182, 81, 112, 122, 103, 147, 149, 90, 128, 129, 53, 125, 214, 147, 137, 174, 109, 150, 157, 103, 75, 134, 39, 209, 178, 167, 77, 179, 92, 158, 199, 189, 154, 154, 152, 112, 180, 75, 80, 145, 122, 84, 79, 228, 41, 177, 40, 60, 127, 159, 165, 128, 114, 181, 60, 55, 146, 154, 135, 96, 137, 67, 157, 130, 36, 152, 154, 69, 47, 93, 122, 78, 150, 185, 83, 136, 165, 49, 75, 208, 157, 168, 90, 67, 122, 143, 154, 151, 132, 73, 165, 132, 175, 97, 76, 86, 139, 182, 48, 166

The differences are at most 1.

For my usecase it would be the best to have a complete compatibility but I guess that is not a goal of this library. I don't know yet whether I will use the Interpolation or try some other approach. Anyway thanks for implementing it.