Skip to content

Commit 1e47604

Browse files
committed
Initial public release
0 parents  commit 1e47604

22 files changed

+1666
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
dssim
2+
Cargo.lock
3+
target/

Cargo.toml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
[package]
2+
authors = ["Kornel <[email protected]>"]
3+
categories = ["multimedia::images", "command-line-utilities"]
4+
description = "Measures structural similarity between images using a multi-scale variant of the SSIM algorithm."
5+
documentation = "https://github.com/pornel/dssim#readme"
6+
homepage = "https://kornel.ski/dssim"
7+
include = ["README.md", "Cargo.toml", "tests/*.png", "src/*.rs"]
8+
keywords = ["ssim", "image", "comparison"]
9+
license = "AGPL-3.0"
10+
name = "dssim"
11+
readme = "README.md"
12+
repository = "https://github.com/pornel/dssim.git"
13+
version = "2.8.0"
14+
15+
[[bin]]
16+
doctest = false
17+
name = "dssim"
18+
path = "src/main.rs"
19+
20+
[dependencies]
21+
getopts = "0.2.15"
22+
imgref = "1.3.0"
23+
itertools = "0.7.2"
24+
lodepng = "2.0.4"
25+
rgb = "0.8.1"
26+
unzip3 = "0.1.0"
27+
28+
[lib]
29+
doctest = false
30+
name = "dssim"

README.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# RGBA Structural Similarity
2+
3+
This tool computes (dis)similarity between two or more PNG images using an algorithm approximating human vision.
4+
5+
Comparison is done using [the SSIM algorithm](https://ece.uwaterloo.ca/~z70wang/research/ssim/) at multiple weighed resolutions.
6+
7+
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.
8+
9+
## Features
10+
11+
* 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.
12+
* Supports alpha channel.
13+
* No OpenCV or MATLAB needed.
14+
- DSSIM [version 1.x](https://github.com/pornel/dssim/tree/dssim1-c) uses C (C99) and `libpng` or Cocoa on macOS.
15+
- DSSIM version 2.x is easy to build with [Rust](https://www.rust-lang.org/).
16+
17+
## Usage
18+
19+
dssim file-original.png file-modified.png
20+
21+
Will output something like "0.02341" (smaller is better) followed by a filename.
22+
23+
You can supply multiple filenames to compare them all with the first file:
24+
25+
dssim file.png modified1.png modified2.png modified3.png
26+
27+
You can save an image visualising the difference between the files:
28+
29+
dssim -o difference.png file.png file-modified.png
30+
31+
The `dssim.c` file is also usable as a C library.
32+
33+
### Interpreting the values
34+
35+
The amount of difference goes from 0 to infinity. It's not a percentage.
36+
37+
If you're comparing two different image compression codecs, then ensure you either:
38+
39+
* compress images to the same file size, and then use DSSIM to compare which one is closests to the original, or
40+
* compress images to the same DSSIM value, and compare file sizes to see how much file size gain each option gives.
41+
42+
[More about benchmarking image compression](https://kornel.ski/faircomparison).
43+
44+
When you quote results, please include DSSIM version, since the scale has changed between versions.
45+
The version is printed when you run `dssim -h`.
46+
47+
## Build or Download
48+
49+
You need Rust
50+
51+
cargo build --release
52+
53+
Will give you `./target/release/dssim`.
54+
55+
## Accuracy
56+
57+
Scores for version 2.0 measured against [TID2013][1] database:
58+
59+
TID2013 Category | Spearman correlation
60+
--- | ---
61+
Noise | -0.930
62+
Actual | -0.937
63+
Simple | -0.945
64+
Exotic | -0.842
65+
New | -0.771
66+
Color | -0.779
67+
Full | -0.851
68+
69+
[1]: http://www.ponomarenko.info/tid2013.htm
70+
71+
## License
72+
73+
DSSIM is dual-licensed under [AGPL](LICENSE) or [commercial](https://supportedsource.org/projects/dssim) license.

src/blur.rs

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
const KERNEL: [f32; 9] = [
2+
1./16., 2./16., 1./16.,
3+
2./16., 4./16., 2./16.,
4+
1./16., 2./16., 1./16.,
5+
];
6+
7+
#[cfg(target_os = "macos")]
8+
mod mac {
9+
use std;
10+
use ffi::vImage_Buffer;
11+
use ffi::vImageConvolve_PlanarF;
12+
use ffi::vImage_Flags::kvImageEdgeExtend;
13+
use super::KERNEL;
14+
15+
pub fn blur(src: &[f32], tmp: &mut [f32], dst: &mut [f32], width: usize, height: usize) {
16+
assert_eq!(src.len(), width * height);
17+
assert_eq!(dst.len(), width * height);
18+
19+
let srcbuf = vImage_Buffer {
20+
width: width as u64,
21+
height: height as u64,
22+
rowBytes: width * std::mem::size_of::<f32>(),
23+
data: src.as_ptr(),
24+
};
25+
let mut dstbuf = vImage_Buffer {
26+
width: width as u64,
27+
height: height as u64,
28+
rowBytes: width * std::mem::size_of::<f32>(),
29+
data: dst.as_mut_ptr(),
30+
};
31+
32+
do_blur(&srcbuf, tmp, &mut dstbuf, width, height);
33+
}
34+
35+
pub fn blur_in_place(srcdst: &mut [f32], tmp: &mut [f32], width: usize, height: usize) {
36+
assert_eq!(srcdst.len(), width * height);
37+
38+
let srcbuf = vImage_Buffer {
39+
width: width as u64,
40+
height: height as u64,
41+
rowBytes: width * std::mem::size_of::<f32>(),
42+
data: srcdst.as_ptr(),
43+
};
44+
let mut dstbuf = vImage_Buffer {
45+
width: width as u64,
46+
height: height as u64,
47+
rowBytes: width * std::mem::size_of::<f32>(),
48+
data: srcdst.as_mut_ptr(),
49+
};
50+
51+
do_blur(&srcbuf, tmp, &mut dstbuf, width, height);
52+
}
53+
54+
pub fn do_blur(srcbuf: &vImage_Buffer<*const f32>, tmp: &mut [f32], dstbuf: &mut vImage_Buffer<*mut f32>, width: usize, height: usize) {
55+
assert_eq!(tmp.len(), width * height);
56+
57+
unsafe {
58+
let mut tmpwrbuf = vImage_Buffer {
59+
width: width as u64,
60+
height: height as u64,
61+
rowBytes: width * std::mem::size_of::<f32>(),
62+
data: tmp.as_mut_ptr(),
63+
};
64+
let res = vImageConvolve_PlanarF(srcbuf, &mut tmpwrbuf, std::ptr::null_mut(), 0, 0, KERNEL.as_ptr(), 3, 3, 0., kvImageEdgeExtend);
65+
assert_eq!(0, res);
66+
67+
let tmprbuf = vImage_Buffer {
68+
width: width as u64,
69+
height: height as u64,
70+
rowBytes: width * std::mem::size_of::<f32>(),
71+
data: tmp.as_ptr(),
72+
};
73+
let res = vImageConvolve_PlanarF(&tmprbuf, dstbuf, std::ptr::null_mut(), 0, 0, KERNEL.as_ptr(), 3, 3, 0., kvImageEdgeExtend);
74+
assert_eq!(0, res);
75+
}
76+
}
77+
}
78+
79+
#[cfg(not(target_os = "macos"))]
80+
mod portable {
81+
use std::cmp::min;
82+
use super::KERNEL;
83+
84+
#[inline]
85+
fn do3f(prev: &[f32], curr: &[f32], next: &[f32], i: usize) -> f32 {
86+
debug_assert!(i > 0);
87+
88+
let c0 = i-1;
89+
let c1 = i;
90+
let c2 = i+1;
91+
92+
unsafe {
93+
prev.get_unchecked(c0)*KERNEL[0] + prev.get_unchecked(c1)*KERNEL[1] + prev.get_unchecked(c2)*KERNEL[2] +
94+
curr.get_unchecked(c0)*KERNEL[3] + curr.get_unchecked(c1)*KERNEL[4] + curr.get_unchecked(c2)*KERNEL[5] +
95+
next.get_unchecked(c0)*KERNEL[6] + next.get_unchecked(c1)*KERNEL[7] + next.get_unchecked(c2)*KERNEL[8]
96+
}
97+
}
98+
99+
fn do3(prev: &[f32], curr: &[f32], next: &[f32], i: usize, width: usize) -> f32 {
100+
let c0 = if i > 0 {i-1} else {0};
101+
let c1 = i;
102+
let c2 = min(i+1, width-1);
103+
104+
prev[c0]*KERNEL[0] + prev[c1]*KERNEL[1] + prev[c2]*KERNEL[2] +
105+
curr[c0]*KERNEL[3] + curr[c1]*KERNEL[4] + curr[c2]*KERNEL[5] +
106+
next[c0]*KERNEL[6] + next[c1]*KERNEL[7] + next[c2]*KERNEL[8]
107+
}
108+
109+
pub fn blur(src: &[f32], tmp: &mut [f32], dst: &mut [f32], width: usize, height: usize) {
110+
do_blur(src, tmp, width, height);
111+
do_blur(tmp, dst, width, height);
112+
}
113+
114+
pub fn do_blur(src: &[f32], dst: &mut [f32], width: usize, height: usize) {
115+
assert!(width > 0);
116+
assert!(width < 1<<24);
117+
assert!(height > 0);
118+
assert!(height < 1<<24);
119+
assert!(src.len() >= width*height);
120+
assert!(dst.len() >= width*height);
121+
122+
let mut prev = &src[0..width];
123+
let mut curr = prev;
124+
let mut next = prev;
125+
for y in 0..height {
126+
prev = curr;
127+
curr = next;
128+
next = if y+1 < height {&src[(y+1)*width..(y+2)*width]} else {curr};
129+
130+
let mut dstrow = &mut dst[y*width..y*width+width];
131+
132+
dstrow[0] = do3(prev, curr, next, 0, width);
133+
for i in 1..width-1 {
134+
dstrow[i] = do3f(prev, curr, next, i);
135+
}
136+
if width > 1 {
137+
dstrow[width-1] = do3(prev, curr, next, width-1, width);
138+
}
139+
}
140+
}
141+
142+
pub fn blur_in_place(srcdst: &mut [f32], tmp: &mut [f32], width: usize, height: usize) {
143+
do_blur(srcdst, tmp, width, height);
144+
do_blur(tmp, srcdst, width, height);
145+
}
146+
}
147+
148+
149+
#[cfg(target_os = "macos")]
150+
pub use self::mac::*;
151+
152+
#[cfg(not(target_os = "macos"))]
153+
pub use self::portable::*;
154+
155+
#[test]
156+
fn blur_zero() {
157+
let src = vec![0.25];
158+
159+
let mut tmp = vec![-55.; 1];
160+
let mut dst = vec![-99.; 1];
161+
162+
let mut src2 = src.clone();
163+
164+
blur(&src[..], &mut tmp[..], &mut dst[..], 1, 1);
165+
blur_in_place(&mut src2[..], &mut tmp[..], 1, 1);
166+
167+
assert_eq!(src2, dst);
168+
assert_eq!(0.25, dst[0]);
169+
}
170+
171+
#[test]
172+
fn blur_one() {
173+
let src = vec![
174+
0.,0.,0.,0.,0.,
175+
0.,0.,0.,0.,0.,
176+
0.,0.,1.,0.,0.,
177+
0.,0.,0.,0.,0.,
178+
0.,0.,0.,0.,0.,
179+
];
180+
let mut tmp = vec![-55.; 5*5];
181+
let mut dst = vec![999.; 5*5];
182+
183+
let mut src2 = src.clone();
184+
185+
blur(&src[..], &mut tmp[..], &mut dst[..], 5, 5);
186+
blur_in_place(&mut src2[..], &mut tmp[..], 5, 5);
187+
188+
assert_eq!(src2, dst);
189+
190+
assert_eq!(1./256., dst[0]);
191+
assert_eq!(1./256., dst[5*5-1]);
192+
let center = 1./16.*1./16. + 2./16.*2./16. + 1./16.*1./16. +
193+
2./16.*2./16. + 4./16.*4./16. + 2./16.*2./16. +
194+
1./16.*1./16. + 2./16.*2./16. + 1./16.*1./16.;
195+
assert_eq!(center, dst[2*5+2]);
196+
197+
}
198+
199+
#[test]
200+
fn blur_two() {
201+
let src = vec![
202+
0.,1.,1.,1.,
203+
1.,1.,1.,1.,
204+
1.,1.,1.,1.,
205+
1.,1.,1.,1.,
206+
];
207+
let mut tmp = vec![-55.; 4*4];
208+
let mut dst = vec![999.; 4*4];
209+
210+
let mut src2 = src.clone();
211+
212+
blur(&src[..], &mut tmp[..], &mut dst[..], 4, 4);
213+
blur_in_place(&mut src2[..], &mut tmp[..], 4, 4);
214+
215+
assert_eq!(src2, dst);
216+
217+
let z00 = 0.*1./16. + 0.*2./16. + 1.*1./16. +
218+
0.*2./16. + 0.*4./16. + 1.*2./16. +
219+
1.*1./16. + 1.*2./16. + 1.*1./16.;
220+
let z01 = 0.*1./16. + 1.*2./16. + 1.*1./16. +
221+
0.*2./16. + 1.*4./16. + 1.*2./16. +
222+
1.*1./16. + 1.*2./16. + 1.*1./16.;
223+
224+
let z10 = 0.*1./16. + 0.*2./16. + 1.*1./16. +
225+
1.*2./16. + 1.*4./16. + 1.*2./16. +
226+
1.*1./16. + 1.*2./16. + 1.*1./16.;
227+
let z11 = 0.*1./16. + 1.*2./16. + 1.*1./16. +
228+
1.*2./16. + 1.*4./16. + 1.*2./16. +
229+
1.*1./16. + 1.*2./16. + 1.*1./16.;
230+
let exp = z00*1./16. + z00*2./16. + z01*1./16. +
231+
z00*2./16. + z00*4./16. + z01*2./16. +
232+
z10*1./16. + z10*2./16. + z11*1./16.;
233+
234+
assert_eq!(1., dst[3]);
235+
assert_eq!(1., dst[3 * 4]);
236+
assert_eq!(1., dst[4 * 4 - 1]);
237+
assert_eq!(exp, dst[0]);
238+
}

0 commit comments

Comments
 (0)