#![allow(clippy::manual_range_contains)]
use getopts::Options;
#[cfg(feature = "threads")]
use rayon::prelude::*;
use std::env;
use std::path::PathBuf;
fn usage(argv0: &str) {
eprintln!("\
Usage: {argv0} original.png modified.png [modified.png...]\
\n or: {argv0} -o difference.png original.png modified.png\n\n\
Compares first image against subsequent images, and outputs\n\
1/SSIM-1 difference for each of them in order (0 = identical).\n\n\
Images must have identical size, but may have different gamma & depth.\n\
\nVersion {} https://kornel.ski/dssim\n", env!("CARGO_PKG_VERSION"));
}
fn to_byte(i: f32) -> u8 {
if i <= 0.0 {0}
else if i >= 255.0/256.0 {255}
else {(i * 256.0) as u8}
}
fn main() {
let args: Vec<String> = env::args().collect();
let (program, rest_args) = args.split_at(1);
let mut opts = Options::new();
opts.optopt("o", "", "set output file name", "NAME");
opts.optflag("h", "help", "print this help menu");
let matches = match opts.parse(rest_args) {
Ok(m) => m,
Err(err) => {
eprintln!("{err}");
std::process::exit(1);
},
};
if matches.opt_present("h") {
usage(&program[0]);
return;
}
let map_output_file_tmp = matches.opt_str("o");
let map_output_file = map_output_file_tmp.as_ref();
let files: Vec<PathBuf> = matches.free.iter().map(|p| p.into()).collect();
if files.len() < 2 {
eprintln!("You must specify at least 2 files to compare\n");
usage(&program[0]);
std::process::exit(1);
}
let mut attr = dssim::Dssim::new();
#[cfg(feature = "threads")]
let files_iter = files.par_iter();
#[cfg(not(feature = "threads"))]
let files_iter = files.iter();
let files = files_iter.map(|file| -> Result<_, String> {
let image = dssim::load_image(&attr, file).map_err(|e| format!("Can't load {}, because: {e}", file.display()))?;
Ok((file, image))
}).collect::<Result<Vec<_>,_>>();
let mut files = match files {
Ok(f) => f,
Err(err) => {
eprintln!("{err}");
std::process::exit(1);
},
};
let (file1, original) = files.remove(0);
for (file2, modified) in files {
if original.width() != modified.width() || original.height() != modified.height() {
eprintln!("Image {} has a different size ({}x{}) than {} ({}x{})\n",
file2.display(), modified.width(), modified.height(),
file1.display(), original.width(), original.height());
std::process::exit(1);
}
if map_output_file.is_some() {
attr.set_save_ssim_maps(8);
}
let (dssim, ssim_maps) = attr.compare(&original, modified);
println!("{dssim:.8}\t{}", file2.display());
if map_output_file.is_some() {
#[cfg(feature = "threads")]
let ssim_maps_iter = ssim_maps.par_iter();
#[cfg(not(feature = "threads"))]
let ssim_maps_iter = ssim_maps.iter();
ssim_maps_iter.enumerate().for_each(|(n, map_meta)| {
let avgssim = map_meta.ssim as f32;
let out: Vec<_> = map_meta.map.pixels().map(|ssim|{
let max = 1_f32 - ssim;
let maxsq = max * max;
rgb::RGBA8 {
r: to_byte(maxsq * 16.0),
g: to_byte(max * 3.0),
b: to_byte(max / ((1_f32 - avgssim) * 4_f32)),
a: 255,
}
}).collect();
let write_res = lodepng::encode32_file(format!("{}-{n}.png", map_output_file.unwrap()), &out, map_meta.map.width(), map_meta.map.height());
if write_res.is_err() {
eprintln!("Can't write {}: {write_res:?}", map_output_file.unwrap());
std::process::exit(1);
}
});
}
}
}
#[test]
fn image_gray() {
let attr = dssim::Dssim::new();
let g1 = dssim::load_image(&attr, "tests/gray1-rgba.png").unwrap();
let g2 = dssim::load_image(&attr, "tests/gray1-pal.png").unwrap();
let g3 = dssim::load_image(&attr, "tests/gray1-gray.png").unwrap();
let g4 = dssim::load_image(&attr, "tests/gray1.jpg").unwrap();
let (diff, _) = attr.compare(&g1, g2);
assert!(diff < 0.00001);
let (diff, _) = attr.compare(&g1, g3);
assert!(diff < 0.00001);
let (diff, _) = attr.compare(&g1, g4);
assert!(diff < 0.00006);
}
#[test]
fn image_gray_profile() {
let attr = dssim::Dssim::new();
let gp1 = dssim::load_image(&attr, "tests/gray-profile.png").unwrap();
let gp2 = dssim::load_image(&attr, "tests/gray-profile2.png").unwrap();
let gp3 = dssim::load_image(&attr, "tests/gray-profile.jpg").unwrap();
let (diff, _) = attr.compare(&gp1, gp2);
assert!(diff < 0.0003, "{}", diff);
let (diff, _) = attr.compare(&gp1, gp3);
assert!(diff < 0.0003, "{}", diff);
}
#[test]
fn image_load1() {
let attr = dssim::Dssim::new();
let prof_jpg = dssim::load_image(&attr, "tests/profile.jpg").unwrap();
let prof_png = dssim::load_image(&attr, "tests/profile.png").unwrap();
let (diff, _) = attr.compare(&prof_jpg, prof_png);
assert!(diff <= 0.002);
let strip_jpg = dssim::load_image(&attr, "tests/profile-stripped.jpg").unwrap();
let (diff, _) = attr.compare(&strip_jpg, prof_jpg);
assert!(diff > 0.008, "{}", diff);
let strip_png = dssim::load_image(&attr, "tests/profile-stripped.png").unwrap();
let (diff, _) = attr.compare(&strip_jpg, strip_png);
assert!(diff > 0.009, "{}", diff);
}
#[test]
fn rgblu_input() {
use dssim::{Dssim, RGBLU};
use imgref::{Img, ImgRef, ImgVec};
let ctx = Dssim::new();
let im: ImgVec<RGBLU> = Img::new(vec![rgb::RGB::new(0.,0.,0.)], 1, 1);
let imr: ImgRef<'_, RGBLU> = im.as_ref();
ctx.create_image(&imr);
}