Skip to content

Commit

Permalink
Initial public release
Browse files Browse the repository at this point in the history
  • Loading branch information
kornelski committed Nov 10, 2017
0 parents commit 1e47604
Show file tree
Hide file tree
Showing 22 changed files with 1,666 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dssim
Cargo.lock
target/
30 changes: 30 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[package]
authors = ["Kornel <[email protected]>"]
categories = ["multimedia::images", "command-line-utilities"]
description = "Measures structural similarity between images using a multi-scale variant of the SSIM algorithm."
documentation = "https://github.com/pornel/dssim#readme"
homepage = "https://kornel.ski/dssim"
include = ["README.md", "Cargo.toml", "tests/*.png", "src/*.rs"]
keywords = ["ssim", "image", "comparison"]
license = "AGPL-3.0"
name = "dssim"
readme = "README.md"
repository = "https://github.com/pornel/dssim.git"
version = "2.8.0"

[[bin]]
doctest = false
name = "dssim"
path = "src/main.rs"

[dependencies]
getopts = "0.2.15"
imgref = "1.3.0"
itertools = "0.7.2"
lodepng = "2.0.4"
rgb = "0.8.1"
unzip3 = "0.1.0"

[lib]
doctest = false
name = "dssim"
73 changes: 73 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# RGBA Structural Similarity

This tool computes (dis)similarity between two or more PNG images using an algorithm approximating human vision.

Comparison is done using [the SSIM algorithm](https://ece.uwaterloo.ca/~z70wang/research/ssim/) at multiple weighed resolutions.

The value returned is 1/SSIM-1, where 0 means identical image, and >0 (unbounded) is amount of difference. Values are not directly comparable with other tools. [See below](#interpreting-the-values) on interpreting the values.

## Features

* Comparison is done in in L\*a\*b\* color space (D65 white point, sRGB gamma) with chroma subsampling. Other implementations use "RGB" or grayscale without gamma correction.
* Supports alpha channel.
* No OpenCV or MATLAB needed.
- DSSIM [version 1.x](https://github.com/pornel/dssim/tree/dssim1-c) uses C (C99) and `libpng` or Cocoa on macOS.
- DSSIM version 2.x is easy to build with [Rust](https://www.rust-lang.org/).

## Usage

dssim file-original.png file-modified.png

Will output something like "0.02341" (smaller is better) followed by a filename.

You can supply multiple filenames to compare them all with the first file:

dssim file.png modified1.png modified2.png modified3.png

You can save an image visualising the difference between the files:

dssim -o difference.png file.png file-modified.png

The `dssim.c` file is also usable as a C library.

### Interpreting the values

The amount of difference goes from 0 to infinity. It's not a percentage.

If you're comparing two different image compression codecs, then ensure you either:

* compress images to the same file size, and then use DSSIM to compare which one is closests to the original, or
* compress images to the same DSSIM value, and compare file sizes to see how much file size gain each option gives.

[More about benchmarking image compression](https://kornel.ski/faircomparison).

When you quote results, please include DSSIM version, since the scale has changed between versions.
The version is printed when you run `dssim -h`.

## Build or Download

You need Rust

cargo build --release

Will give you `./target/release/dssim`.

## Accuracy

Scores for version 2.0 measured against [TID2013][1] database:

TID2013 Category | Spearman correlation
--- | ---
Noise | -0.930
Actual | -0.937
Simple | -0.945
Exotic | -0.842
New | -0.771
Color | -0.779
Full | -0.851

[1]: http://www.ponomarenko.info/tid2013.htm

## License

DSSIM is dual-licensed under [AGPL](LICENSE) or [commercial](https://supportedsource.org/projects/dssim) license.
238 changes: 238 additions & 0 deletions src/blur.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
const KERNEL: [f32; 9] = [
1./16., 2./16., 1./16.,
2./16., 4./16., 2./16.,
1./16., 2./16., 1./16.,
];

#[cfg(target_os = "macos")]
mod mac {
use std;
use ffi::vImage_Buffer;
use ffi::vImageConvolve_PlanarF;
use ffi::vImage_Flags::kvImageEdgeExtend;
use super::KERNEL;

pub fn blur(src: &[f32], tmp: &mut [f32], dst: &mut [f32], width: usize, height: usize) {
assert_eq!(src.len(), width * height);
assert_eq!(dst.len(), width * height);

let srcbuf = vImage_Buffer {
width: width as u64,
height: height as u64,
rowBytes: width * std::mem::size_of::<f32>(),
data: src.as_ptr(),
};
let mut dstbuf = vImage_Buffer {
width: width as u64,
height: height as u64,
rowBytes: width * std::mem::size_of::<f32>(),
data: dst.as_mut_ptr(),
};

do_blur(&srcbuf, tmp, &mut dstbuf, width, height);
}

pub fn blur_in_place(srcdst: &mut [f32], tmp: &mut [f32], width: usize, height: usize) {
assert_eq!(srcdst.len(), width * height);

let srcbuf = vImage_Buffer {
width: width as u64,
height: height as u64,
rowBytes: width * std::mem::size_of::<f32>(),
data: srcdst.as_ptr(),
};
let mut dstbuf = vImage_Buffer {
width: width as u64,
height: height as u64,
rowBytes: width * std::mem::size_of::<f32>(),
data: srcdst.as_mut_ptr(),
};

do_blur(&srcbuf, tmp, &mut dstbuf, width, height);
}

pub fn do_blur(srcbuf: &vImage_Buffer<*const f32>, tmp: &mut [f32], dstbuf: &mut vImage_Buffer<*mut f32>, width: usize, height: usize) {
assert_eq!(tmp.len(), width * height);

unsafe {
let mut tmpwrbuf = vImage_Buffer {
width: width as u64,
height: height as u64,
rowBytes: width * std::mem::size_of::<f32>(),
data: tmp.as_mut_ptr(),
};
let res = vImageConvolve_PlanarF(srcbuf, &mut tmpwrbuf, std::ptr::null_mut(), 0, 0, KERNEL.as_ptr(), 3, 3, 0., kvImageEdgeExtend);
assert_eq!(0, res);

let tmprbuf = vImage_Buffer {
width: width as u64,
height: height as u64,
rowBytes: width * std::mem::size_of::<f32>(),
data: tmp.as_ptr(),
};
let res = vImageConvolve_PlanarF(&tmprbuf, dstbuf, std::ptr::null_mut(), 0, 0, KERNEL.as_ptr(), 3, 3, 0., kvImageEdgeExtend);
assert_eq!(0, res);
}
}
}

#[cfg(not(target_os = "macos"))]
mod portable {
use std::cmp::min;
use super::KERNEL;

#[inline]
fn do3f(prev: &[f32], curr: &[f32], next: &[f32], i: usize) -> f32 {
debug_assert!(i > 0);

let c0 = i-1;
let c1 = i;
let c2 = i+1;

unsafe {
prev.get_unchecked(c0)*KERNEL[0] + prev.get_unchecked(c1)*KERNEL[1] + prev.get_unchecked(c2)*KERNEL[2] +
curr.get_unchecked(c0)*KERNEL[3] + curr.get_unchecked(c1)*KERNEL[4] + curr.get_unchecked(c2)*KERNEL[5] +
next.get_unchecked(c0)*KERNEL[6] + next.get_unchecked(c1)*KERNEL[7] + next.get_unchecked(c2)*KERNEL[8]
}
}

fn do3(prev: &[f32], curr: &[f32], next: &[f32], i: usize, width: usize) -> f32 {
let c0 = if i > 0 {i-1} else {0};
let c1 = i;
let c2 = min(i+1, width-1);

prev[c0]*KERNEL[0] + prev[c1]*KERNEL[1] + prev[c2]*KERNEL[2] +
curr[c0]*KERNEL[3] + curr[c1]*KERNEL[4] + curr[c2]*KERNEL[5] +
next[c0]*KERNEL[6] + next[c1]*KERNEL[7] + next[c2]*KERNEL[8]
}

pub fn blur(src: &[f32], tmp: &mut [f32], dst: &mut [f32], width: usize, height: usize) {
do_blur(src, tmp, width, height);
do_blur(tmp, dst, width, height);
}

pub fn do_blur(src: &[f32], dst: &mut [f32], width: usize, height: usize) {
assert!(width > 0);
assert!(width < 1<<24);
assert!(height > 0);
assert!(height < 1<<24);
assert!(src.len() >= width*height);
assert!(dst.len() >= width*height);

let mut prev = &src[0..width];
let mut curr = prev;
let mut next = prev;
for y in 0..height {
prev = curr;
curr = next;
next = if y+1 < height {&src[(y+1)*width..(y+2)*width]} else {curr};

let mut dstrow = &mut dst[y*width..y*width+width];

dstrow[0] = do3(prev, curr, next, 0, width);
for i in 1..width-1 {
dstrow[i] = do3f(prev, curr, next, i);
}
if width > 1 {
dstrow[width-1] = do3(prev, curr, next, width-1, width);
}
}
}

pub fn blur_in_place(srcdst: &mut [f32], tmp: &mut [f32], width: usize, height: usize) {
do_blur(srcdst, tmp, width, height);
do_blur(tmp, srcdst, width, height);
}
}


#[cfg(target_os = "macos")]
pub use self::mac::*;

#[cfg(not(target_os = "macos"))]
pub use self::portable::*;

#[test]
fn blur_zero() {
let src = vec![0.25];

let mut tmp = vec![-55.; 1];
let mut dst = vec![-99.; 1];

let mut src2 = src.clone();

blur(&src[..], &mut tmp[..], &mut dst[..], 1, 1);
blur_in_place(&mut src2[..], &mut tmp[..], 1, 1);

assert_eq!(src2, dst);
assert_eq!(0.25, dst[0]);
}

#[test]
fn blur_one() {
let src = vec![
0.,0.,0.,0.,0.,
0.,0.,0.,0.,0.,
0.,0.,1.,0.,0.,
0.,0.,0.,0.,0.,
0.,0.,0.,0.,0.,
];
let mut tmp = vec![-55.; 5*5];
let mut dst = vec![999.; 5*5];

let mut src2 = src.clone();

blur(&src[..], &mut tmp[..], &mut dst[..], 5, 5);
blur_in_place(&mut src2[..], &mut tmp[..], 5, 5);

assert_eq!(src2, dst);

assert_eq!(1./256., dst[0]);
assert_eq!(1./256., dst[5*5-1]);
let center = 1./16.*1./16. + 2./16.*2./16. + 1./16.*1./16. +
2./16.*2./16. + 4./16.*4./16. + 2./16.*2./16. +
1./16.*1./16. + 2./16.*2./16. + 1./16.*1./16.;
assert_eq!(center, dst[2*5+2]);

}

#[test]
fn blur_two() {
let src = vec![
0.,1.,1.,1.,
1.,1.,1.,1.,
1.,1.,1.,1.,
1.,1.,1.,1.,
];
let mut tmp = vec![-55.; 4*4];
let mut dst = vec![999.; 4*4];

let mut src2 = src.clone();

blur(&src[..], &mut tmp[..], &mut dst[..], 4, 4);
blur_in_place(&mut src2[..], &mut tmp[..], 4, 4);

assert_eq!(src2, dst);

let z00 = 0.*1./16. + 0.*2./16. + 1.*1./16. +
0.*2./16. + 0.*4./16. + 1.*2./16. +
1.*1./16. + 1.*2./16. + 1.*1./16.;
let z01 = 0.*1./16. + 1.*2./16. + 1.*1./16. +
0.*2./16. + 1.*4./16. + 1.*2./16. +
1.*1./16. + 1.*2./16. + 1.*1./16.;

let z10 = 0.*1./16. + 0.*2./16. + 1.*1./16. +
1.*2./16. + 1.*4./16. + 1.*2./16. +
1.*1./16. + 1.*2./16. + 1.*1./16.;
let z11 = 0.*1./16. + 1.*2./16. + 1.*1./16. +
1.*2./16. + 1.*4./16. + 1.*2./16. +
1.*1./16. + 1.*2./16. + 1.*1./16.;
let exp = z00*1./16. + z00*2./16. + z01*1./16. +
z00*2./16. + z00*4./16. + z01*2./16. +
z10*1./16. + z10*2./16. + z11*1./16.;

assert_eq!(1., dst[3]);
assert_eq!(1., dst[3 * 4]);
assert_eq!(1., dst[4 * 4 - 1]);
assert_eq!(exp, dst[0]);
}
Loading

0 comments on commit 1e47604

Please sign in to comment.