From bb496e61a5ddfabc97e78940b807c4a137314725 Mon Sep 17 00:00:00 2001 From: Bill Thiede Date: Thu, 25 Jun 2020 18:23:04 -0700 Subject: [PATCH] Initial commit with simplify function that RLE scales images. --- .gitignore | 1 + Cargo.lock | 334 ++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 14 +++ src/lib.rs | 202 +++++++++++++++++++++++++++++++ src/main.rs | 1 + 5 files changed, 552 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/lib.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..f394fee --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,334 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "adler32" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b077b825e468cc974f0020d4082ee6e03132512f207ef1a02fd5d00d1f32d" + +[[package]] +name = "autocfg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "bytemuck" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37fa13df2292ecb479ec23aa06f4507928bef07839be9ef15281411076629431" + +[[package]] +name = "byteorder" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "color_quant" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dbbb57365263e881e805dc77d94697c9118fd94d8da011240555aa7b23445bd" + +[[package]] +name = "crc32fast" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f02af974daeee82218205558e51ec8768b48cf524bd01d550abe5573a608285" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", + "maybe-uninit", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "lazy_static", + "maybe-uninit", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-queue" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "maybe-uninit", +] + +[[package]] +name = "crossbeam-utils" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" +dependencies = [ + "autocfg", + "cfg-if", + "lazy_static", +] + +[[package]] +name = "deflate" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e5d2a2273fed52a7f947ee55b092c4057025d7a3e04e5ecdbd25d6c3fb1bd7" +dependencies = [ + "adler32", + "byteorder", +] + +[[package]] +name = "either" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" + +[[package]] +name = "gif" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471d90201b3b223f3451cd4ad53e34295f16a1df17b1edf3736d47761c3981af" +dependencies = [ + "color_quant", + "lzw", +] + +[[package]] +name = "hermit-abi" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9586eedd4ce6b3c498bc3b4dd92fc9f11166aa908a914071953768066c67909" +dependencies = [ + "libc", +] + +[[package]] +name = "image" +version = "0.23.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b0553fec6407d63fe2975b794dfb099f3f790bdc958823851af37b26404ab4" +dependencies = [ + "bytemuck", + "byteorder", + "gif", + "jpeg-decoder", + "num-iter", + "num-rational", + "num-traits", + "png", + "scoped_threadpool", + "tiff", +] + +[[package]] +name = "jpeg-decoder" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b47b4c4e017b01abdc5bcc126d2d1002e5a75bbe3ce73f9f4f311a916363704" +dependencies = [ + "byteorder", + "rayon", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9457b06509d27052635f90d6466700c65095fdf75409b3fbdd903e988b886f49" + +[[package]] +name = "log" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "lzw" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d947cbb889ed21c2a84be6ffbaebf5b4e0f4340638cba0444907e38b56be084" + +[[package]] +name = "maybe-uninit" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" + +[[package]] +name = "memoffset" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4fc2c02a7e374099d4ee95a193111f72d2110197fe200272371758f6c3643d8" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" +dependencies = [ + "adler32", +] + +[[package]] +name = "num-integer" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6e6b7c748f995c4c29c5f5ae0248536e04a5739927c74ec0fa564805094b9f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5b4d7360f362cfb50dde8143501e6940b22f644be75a4cc90b2d81968908138" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "perler" +version = "0.1.0" +dependencies = [ + "image", + "lazy_static", + "log", +] + +[[package]] +name = "png" +version = "0.16.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ccdd66f6fe4b2433b07e4728e9a013e43233120427046e93ceb709c3a439bf" +dependencies = [ + "bitflags", + "crc32fast", + "deflate", + "miniz_oxide", +] + +[[package]] +name = "rayon" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f02856753d04e03e26929f820d0a0a337ebe71f849801eea335d464b349080" +dependencies = [ + "autocfg", + "crossbeam-deque", + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e92e15d89083484e11353891f1af602cc661426deb9564c298b270c726973280" +dependencies = [ + "crossbeam-deque", + "crossbeam-queue", + "crossbeam-utils", + "lazy_static", + "num_cpus", +] + +[[package]] +name = "scoped_threadpool" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "tiff" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3b8a87c4da944c3f27e5943289171ac71a6150a79ff6bacfff06d159dfff2f" +dependencies = [ + "byteorder", + "lzw", + "miniz_oxide", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d98977d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "perler" +version = "0.1.0" +authors = ["Bill Thiede "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +image = "0.23.6" +log = "0.4.8" + +[dev-dependencies] +lazy_static = "1.4.0" diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..efaee83 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,202 @@ +use image::{DynamicImage, GenericImage, GenericImageView}; +use log::debug; + +pub fn simplify(input: &DynamicImage) -> DynamicImage { + let (w, h) = input.dimensions(); + // The dimensions method returns the images width and height. + debug!("dimensions {}x{}", w, h); + + let mut smallest_run = usize::MAX; + // Search for longest vertical runs + for y in 0..h { + let mut last_pix = input.get_pixel(0, y); + let mut run = 1; + for x in 1..w { + let pix = input.get_pixel(x, y); + debug!("xy {}x{} pix {:?} last pix {:?}", x, y, &pix.0, &last_pix.0); + if last_pix == pix { + run += 1; + } else { + smallest_run = smallest_run.min(run); + debug!( + "xy {}x{} pix {:?} last pix {:?} run {}", + x, y, &pix.0, &last_pix.0, &run + ); + run = 1; + } + last_pix = pix; + } + smallest_run = smallest_run.min(run); + } + debug!("Smallest vertical run {}", smallest_run); + + // Search for longest vertical runs + for x in 0..w { + let mut last_pix = input.get_pixel(x, 0); + let mut run = 1; + for y in 1..h { + let pix = input.get_pixel(x, y); + debug!("xy {}x{} pix {:?} last pix {:?}", x, y, &pix.0, &last_pix.0); + if last_pix == pix { + run += 1; + } else { + smallest_run = smallest_run.min(run); + debug!( + "xy {}x{} pix {:?} last pix {:?} run {}", + x, y, &pix.0, &last_pix.0, &run + ); + run = 1; + } + last_pix = pix; + } + smallest_run = smallest_run.min(run); + } + + debug!("Smallest run {}", smallest_run); + let scale = smallest_run as u32; + let s_w = w / scale; + let s_h = h / scale; + let mut out = DynamicImage::new_rgba8(s_w, s_h); + debug!("Creating image {}x{}", s_w, s_h); + for y in 0..s_h { + for x in 0..s_w { + let x_orig = x * scale; + let y_orig = y * scale; + let p = input.get_pixel(x_orig, y_orig); + debug!( + "getting {}x{} {:?} and storing in {}x{}", + x_orig, y_orig, p.0, x, y + ); + out.put_pixel(x, y, p); + } + } + + out +} + +#[cfg(test)] +mod tests { + use super::*; + + use image::{GenericImage, Rgba}; + use lazy_static::lazy_static; + + lazy_static! { + static ref WHITE: Rgba = [255, 255, 255, 255].into(); + static ref BLACK: Rgba = [0, 0, 0, 255].into(); + } + + struct DebugImage<'r>(&'r DynamicImage); + use std::fmt; + impl<'r> fmt::Display for DebugImage<'r> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let img = &self.0; + let (w, h) = img.dimensions(); + for y in 0..h { + let p = img.get_pixel(0, y); + write!(f, "[{:^3}", p.0[0])?; + for x in 1..w { + let p = img.get_pixel(x, y); + write!(f, " {:^3}", p.0[0])?; + } + write!(f, "]\n")?; + } + Ok(()) + } + } + + fn mk_horiz(w: u32, h: u32, scale: u32) -> DynamicImage { + let mut img = DynamicImage::new_rgba8(w * scale, h * scale); + for y in 0..h * scale { + for x in 0..w * scale { + if (y / scale) % 2 == 0 { + img.put_pixel(x, y, *WHITE); + } else { + img.put_pixel(x, y, *BLACK); + } + } + } + img + } + + fn mk_vert(w: u32, h: u32, scale: u32) -> DynamicImage { + let mut img = DynamicImage::new_rgba8(w * scale, h * scale); + for y in 0..h * scale { + for x in 0..w * scale { + if (x / scale) % 2 == 0 { + img.put_pixel(x, y, *WHITE); + } else { + img.put_pixel(x, y, *BLACK); + } + } + } + img + } + + fn mk_check(w: u32, h: u32, scale: u32) -> DynamicImage { + let mut img = DynamicImage::new_rgba8(w * scale, h * scale); + for y in 0..h * scale { + for x in 0..w * scale { + if (x / scale) % 2 == 0 || (y / scale) % 2 == 0 { + img.put_pixel(x, y, *WHITE); + } else { + img.put_pixel(x, y, *BLACK); + } + } + } + img + } + + fn compare_images(lhs: DynamicImage, rhs: DynamicImage) { + let lhs = &lhs.to_rgba(); + let rhs = &rhs.to_rgba(); + let l_dim = lhs.dimensions(); + let r_dim = rhs.dimensions(); + assert_eq!( + l_dim, r_dim, + "image sizes don't match {:?} != {:?}", + l_dim, r_dim + ); + + // Based on https://en.wikipedia.org/wiki/Peak_signal-to-noise_ratio#Definition + let mut mse: [i64; 3] = [0, 0, 0]; + let (w, _) = l_dim; + let w = w as usize; + for (c, (l, r)) in lhs.pixels().zip(rhs.pixels()).enumerate() { + let image::Rgba(l_pix) = l; + let image::Rgba(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; + } + } + let x = c / w; + let y = c % w; + if l_pix != r_pix { + lhs.save("/tmp/lhs.png").expect("Failed to save lhs image"); + rhs.save("/tmp/rhs.png").expect("Failed to save rhs image"); + assert_eq!(l_pix, r_pix, "{:?} != {:?} @ {} x {} ", l_pix, r_pix, x, y); + } + } + } + + #[test] + fn test_simplify() { + for (input, want) in vec![ + (mk_horiz(3, 3, 5), mk_horiz(3, 3, 1)), + (mk_vert(3, 3, 5), mk_vert(3, 3, 1)), + (mk_check(3, 3, 5), mk_check(3, 3, 1)), + (mk_horiz(3, 9, 5), mk_horiz(3, 9, 1)), + (mk_vert(9, 3, 5), mk_vert(9, 3, 1)), + ] { + let got = simplify(&input); + println!("input:\n{}", DebugImage(&input)); + println!("want:\n{}", DebugImage(&want)); + println!("got:\n{}", DebugImage(&got)); + compare_images(want, got); + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..f328e4d --- /dev/null +++ b/src/main.rs @@ -0,0 +1 @@ +fn main() {}