Implement less abstracted nearest filtering.
Add some tests that try to compare image's Nearest with my implementation. According the PSNR they're very different, but to a human the look the same.
This commit is contained in:
parent
77d69221d1
commit
7f64a4d2f6
@ -2,12 +2,13 @@ use criterion::BenchmarkId;
|
|||||||
use criterion::Throughput;
|
use criterion::Throughput;
|
||||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
|
|
||||||
use image::imageops::FilterType;
|
use image::imageops;
|
||||||
use image::GenericImageView;
|
use image::GenericImageView;
|
||||||
|
|
||||||
use photosync::library::load_image;
|
use photosync::library::load_image;
|
||||||
use photosync::library::resize;
|
use photosync::library::resize;
|
||||||
use photosync::library::save_to_jpeg_bytes;
|
use photosync::library::save_to_jpeg_bytes;
|
||||||
|
use photosync::library::FilterType;
|
||||||
|
|
||||||
pub fn criterion_benchmark(c: &mut Criterion) {
|
pub fn criterion_benchmark(c: &mut Criterion) {
|
||||||
const TEST_IMAGE_PATH: &'static str = "testdata/image.jpg";
|
const TEST_IMAGE_PATH: &'static str = "testdata/image.jpg";
|
||||||
@ -28,9 +29,10 @@ pub fn criterion_benchmark(c: &mut Criterion) {
|
|||||||
{
|
{
|
||||||
let (w, h) = size;
|
let (w, h) = size;
|
||||||
for filter in [
|
for filter in [
|
||||||
|
FilterType::Builtin(imageops::Nearest),
|
||||||
|
FilterType::Builtin(imageops::CatmullRom),
|
||||||
|
FilterType::Builtin(imageops::Lanczos3),
|
||||||
FilterType::Nearest,
|
FilterType::Nearest,
|
||||||
FilterType::CatmullRom,
|
|
||||||
FilterType::Lanczos3,
|
|
||||||
]
|
]
|
||||||
.iter()
|
.iter()
|
||||||
{
|
{
|
||||||
@ -62,7 +64,7 @@ pub fn criterion_benchmark(c: &mut Criterion) {
|
|||||||
),
|
),
|
||||||
size,
|
size,
|
||||||
|b, size| {
|
|b, size| {
|
||||||
let small_img = resize(&img, *size, FilterType::Lanczos3);
|
let small_img = resize(&img, *size, FilterType::Builtin(imageops::Lanczos3));
|
||||||
b.iter(|| black_box(save_to_jpeg_bytes(&small_img)))
|
b.iter(|| black_box(save_to_jpeg_bytes(&small_img)))
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -80,7 +82,7 @@ pub fn criterion_benchmark(c: &mut Criterion) {
|
|||||||
|b, size| {
|
|b, size| {
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
let img = load_image(TEST_IMAGE_PATH).expect("failed to load test image");
|
let img = load_image(TEST_IMAGE_PATH).expect("failed to load test image");
|
||||||
let small_img = resize(&img, *size, FilterType::Lanczos3);
|
let small_img = resize(&img, *size, FilterType::Builtin(imageops::Lanczos3));
|
||||||
black_box(save_to_jpeg_bytes(&small_img))
|
black_box(save_to_jpeg_bytes(&small_img))
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
155
src/library.rs
155
src/library.rs
@ -6,8 +6,9 @@ use std::path::PathBuf;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use google_photoslibrary1 as photos;
|
use google_photoslibrary1 as photos;
|
||||||
use image::imageops::FilterType;
|
use image::imageops;
|
||||||
use image::DynamicImage;
|
use image::DynamicImage;
|
||||||
|
use image::GenericImage;
|
||||||
use image::GenericImageView;
|
use image::GenericImageView;
|
||||||
use image::ImageFormat;
|
use image::ImageFormat;
|
||||||
use image::ImageResult;
|
use image::ImageResult;
|
||||||
@ -39,6 +40,53 @@ where
|
|||||||
.decode()
|
.decode()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub enum FilterType {
|
||||||
|
Builtin(imageops::FilterType),
|
||||||
|
Nearest,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// fill_size computes the largest rectangle that fits in src with the aspect ratio of dst.
|
||||||
|
fn fill_size(src: (u32, u32), dst: (u32, u32)) -> (u32, u32) {
|
||||||
|
debug_assert!(src.0 >= dst.0);
|
||||||
|
debug_assert!(src.1 >= dst.1);
|
||||||
|
let x_scale = src.0 as f32 / dst.0 as f32;
|
||||||
|
let y_scale = src.1 as f32 / dst.1 as f32;
|
||||||
|
if x_scale > y_scale {
|
||||||
|
// Height will fill, width will crop.
|
||||||
|
(
|
||||||
|
(dst.0 as f32 * y_scale) as u32,
|
||||||
|
(dst.1 as f32 * y_scale) as u32,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Width will fill, height will crop.
|
||||||
|
(
|
||||||
|
(dst.0 as f32 * x_scale) as u32,
|
||||||
|
(dst.1 as f32 * x_scale) as u32,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resize_to_fill_nearest(w: u32, h: u32, img: &DynamicImage) -> DynamicImage {
|
||||||
|
let mut dst = DynamicImage::new_rgb8(w, h);
|
||||||
|
let (src_w, src_h) = img.dimensions();
|
||||||
|
let (crop_w, crop_h) = fill_size((src_w, src_h), (w, h));
|
||||||
|
let off_x = (src_w - crop_w) / 2;
|
||||||
|
let off_y = (src_h - crop_h) / 2;
|
||||||
|
let src = img.view(off_x, off_y, crop_w, crop_h);
|
||||||
|
let x_scale = crop_w as f32 / w as f32;
|
||||||
|
let y_scale = crop_h as f32 / h as f32;
|
||||||
|
|
||||||
|
for y in 0..h {
|
||||||
|
for x in 0..w {
|
||||||
|
let x_idx = (x as f32 * x_scale).round() as u32;
|
||||||
|
let y_idx = (y as f32 * y_scale).round() as u32;
|
||||||
|
dst.put_pixel(x, y, src.get_pixel(x_idx, y_idx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dst
|
||||||
|
}
|
||||||
|
|
||||||
pub fn resize(
|
pub fn resize(
|
||||||
img: &DynamicImage,
|
img: &DynamicImage,
|
||||||
dimensions: (Option<u32>, Option<u32>),
|
dimensions: (Option<u32>, Option<u32>),
|
||||||
@ -52,7 +100,10 @@ pub fn resize(
|
|||||||
(None, Some(h)) => (orig_w * h / orig_h, h),
|
(None, Some(h)) => (orig_w * h / orig_h, h),
|
||||||
(None, None) => (orig_w, orig_h),
|
(None, None) => (orig_w, orig_h),
|
||||||
};
|
};
|
||||||
img.resize_to_fill(w, h, filter)
|
match filter {
|
||||||
|
FilterType::Builtin(filter) => img.resize_to_fill(w, h, filter),
|
||||||
|
FilterType::Nearest => resize_to_fill_nearest(w, h, img),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save_to_jpeg_bytes(img: &DynamicImage) -> ImageResult<Vec<u8>> {
|
pub fn save_to_jpeg_bytes(img: &DynamicImage) -> ImageResult<Vec<u8>> {
|
||||||
@ -234,15 +285,17 @@ impl Library {
|
|||||||
// Cache miss, fill cache and return.
|
// Cache miss, fill cache and return.
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
info!("cache MISS {}", key);
|
info!("cache MISS {}", key);
|
||||||
let bytes =
|
let bytes = match self.generate_thumbnail(
|
||||||
match self.generate_thumbnail(media_items_id, dimensions, FilterType::Lanczos3)
|
media_items_id,
|
||||||
{
|
dimensions,
|
||||||
Ok(bytes) => bytes,
|
FilterType::Builtin(imageops::FilterType::Lanczos3),
|
||||||
Err(e) => {
|
) {
|
||||||
error!("Failed to generate thumbnail for {}: {}", media_items_id, e);
|
Ok(bytes) => bytes,
|
||||||
return None;
|
Err(e) => {
|
||||||
}
|
error!("Failed to generate thumbnail for {}: {}", media_items_id, e);
|
||||||
};
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
match db.put(key.as_bytes(), &bytes) {
|
match db.put(key.as_bytes(), &bytes) {
|
||||||
Ok(_) => Some(bytes),
|
Ok(_) => Some(bytes),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@ -265,6 +318,86 @@ mod test {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use tempdir::TempDir;
|
use tempdir::TempDir;
|
||||||
|
|
||||||
|
fn compare_images(lhs: DynamicImage, rhs: DynamicImage) {
|
||||||
|
let lhs = lhs.to_rgb();
|
||||||
|
let rhs = rhs.to_rgb();
|
||||||
|
// Based on https://en.wikipedia.org/wiki/Peak_signal-to-noise_ratio#Definition
|
||||||
|
//
|
||||||
|
let mut mse: [i64; 3] = [0, 0, 0];
|
||||||
|
for (l, r) in lhs.pixels().zip(rhs.pixels()) {
|
||||||
|
let image::Rgb(l_pix) = l;
|
||||||
|
let image::Rgb(r_pix) = r;
|
||||||
|
|
||||||
|
{
|
||||||
|
for i in 0..3 {
|
||||||
|
let d = l_pix[i] as i64 - r_pix[i] as i64;
|
||||||
|
let d2 = d * d;
|
||||||
|
mse[i] += d2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// assert_eq!(l_pix, r_pix, "{:?} != {:?} @ {} x {} ", l_pix, r_pix, x, y);
|
||||||
|
}
|
||||||
|
let (w, h) = lhs.dimensions();
|
||||||
|
let mn = (w * h) as i64;
|
||||||
|
mse.iter_mut().for_each(|i| *i = *i / mn);
|
||||||
|
let psnr: Vec<_> = mse
|
||||||
|
.iter()
|
||||||
|
.map(|i| 20. * 255_f32.log10() - 10. * (*i as f32).log10())
|
||||||
|
.collect();
|
||||||
|
// Uncomment to explore differences
|
||||||
|
/*
|
||||||
|
lhs.save("/tmp/lhs.png").expect("failed to write lhs.png");
|
||||||
|
rhs.save("/tmp/rhs.png").expect("failed to write rhs.png");
|
||||||
|
assert!(false, "MSE {:?} PSNR {:?} dB", mse, psnr);
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fill_sizes() {
|
||||||
|
let srcs = vec![(400, 300), (300, 400)];
|
||||||
|
|
||||||
|
let dsts = vec![(225, 300), (300, 225), (100, 100)];
|
||||||
|
|
||||||
|
let want = vec![
|
||||||
|
(225, 300),
|
||||||
|
(400, 300),
|
||||||
|
(300, 300),
|
||||||
|
(300, 400),
|
||||||
|
(300, 225),
|
||||||
|
(300, 300),
|
||||||
|
];
|
||||||
|
let mut i = 0;
|
||||||
|
for s in &srcs {
|
||||||
|
for d in &dsts {
|
||||||
|
let w = want[i];
|
||||||
|
dbg!(s, d, w);
|
||||||
|
let got = fill_size(*s, *d);
|
||||||
|
assert_eq!(
|
||||||
|
got, w,
|
||||||
|
"{}. src {:?} dst {:?} want {:?} got {:?}",
|
||||||
|
i, s, d, w, got
|
||||||
|
);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resize_to_fill_nearest() {
|
||||||
|
let w = 256;
|
||||||
|
let h = 256;
|
||||||
|
const TEST_IMAGE_PATH: &'static str = "testdata/image.jpg";
|
||||||
|
let img = load_image(TEST_IMAGE_PATH).expect("failed to load test image");
|
||||||
|
let reference = resize(
|
||||||
|
&img,
|
||||||
|
(Some(w), Some(h)),
|
||||||
|
FilterType::Builtin(imageops::FilterType::Nearest),
|
||||||
|
);
|
||||||
|
let got = resize(&img, (Some(w), Some(h)), FilterType::Nearest);
|
||||||
|
compare_images(reference, got);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn clean_db() {
|
fn clean_db() {
|
||||||
let td = TempDir::new("photosync_test").expect("failed to create temporary directory");
|
let td = TempDir::new("photosync_test").expect("failed to create temporary directory");
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user