-
Notifications
You must be signed in to change notification settings - Fork 10
/
ssim.jl
199 lines (164 loc) · 8.18 KB
/
ssim.jl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
"""
SSIM([kernel], [(α, β, γ)]; crop=false) <: FullReferenceIQI
assess(iqi::SSIM, img, ref)
assess_ssim(img, ref; crop=false)
Structural similarity (SSIM) index is an image quality assessment method based
on degradation of structural information.
The SSIM index is composed of three components: luminance, contrast, and
structure; `ssim = 𝐿ᵅ * 𝐶ᵝ * 𝑆ᵞ`, where `W := (α, β, γ)` controls relative
importance of each components. By default `W = (1.0, 1.0, 1.0)`.
In practice, a mean version SSIM is used. At each pixel, SSIM is calculated
locally with neighborhoods weighted by `kernel`, returning a ssim map;
`ssim` is actaully `mean(ssim_map)`.
By default `kernel = KernelFactors.gaussian(1.5, 11)`.
!!! tip
The default parameters comes from [1]. For benchmark usage, it is recommended to not
change the parameters, because most other SSIM implementations follows the same settings.
Keyword `crop` controls whether the boundary of ssim map should be dropped; you need
to set it `true` to reproduce the result in [1].
# Example
`assess_ssim(img, ref)` should be sufficient to get a benchmark for algorithms. One
could also instantiate a customed SSIM, then pass it to `assess` or use it as a
function. For example:
```julia
iqi = SSIM(KernelFactors.gaussian(2.5, 17), (1.0, 1.0, 2.0))
assess(iqi, img, ref)
iqi(img, ref) # both usages are equivalent
```
!!! info
SSIM is defined only for gray images. How `RGB` and other `Color3` images are handled may vary
in different implementations. For `RGB` images, channels are handled seperately when calculating
𝐿, 𝐶, and 𝑆. Generic `Color3` images are converted to `RGB` first before calculation.
# Reference
[1] Wang, Z., Bovik, A. C., Sheikh, H. R., & Simoncelli, E. P. (2004). Image quality assessment: from error visibility to structural similarity. _IEEE transactions on image processing_, 13(4), 600-612.
[2] Wang, Z., Bovik, A. C., Sheikh, H. R., & Simoncelli, E. P. (2003). The SSIM Index for Image Quality Assessment. Retrived May 30, 2019, from http://www.cns.nyu.edu/~lcv/ssim/
"""
struct SSIM{A<:AbstractVector} <: FullReferenceIQI
kernel::A
W::NTuple{3, Float64}
crop::Bool
function SSIM(kernel::AbstractVector=SSIM_KERNEL, W::NTuple=SSIM_W; crop=false)
ndims(kernel) == 1 || throw(ArgumentError("only 1-d kernel is valid"))
issymetric(kernel) || @warn "SSIM kernel is assumed to be symmetric"
all(W .>= 0) || throw(ArgumentError("(α, β, γ) should be non-negative, instead it's $(W)"))
kernel = OffsetArrays.centered(kernel)
new{typeof(kernel)}(kernel, W, crop)
end
end
# default values from [1]
# kernel generated from ImageFiltering.KernelFactors.gaussian(1.5, 11)
# don't use ImageFiltering directly here because we lazy-import that dependency
const SSIM_KERNEL = OffsetArray([0.00102838008447911,
0.007598758135239185,
0.03600077212843083,
0.10936068950970002,
0.2130055377112537,
0.26601172486179436,
0.2130055377112537,
0.10936068950970002,
0.03600077212843083,
0.007598758135239185,
0.00102838008447911,], -5:5)
const SSIM_W = (1.0, 1.0, 1.0) # (α, β, γ)
Base.:(==)(ia::SSIM, ib::SSIM) = ia.kernel == ib.kernel && ia.W == ib.W && ia.crop == ib.crop
# api
# By default we don't crop the padding boundary to match the ssim result from
# MATLAB Image Processing Toolbox, which is used more broadly than the original
# implementaion [2] (written in MATLAB as well).
# -- Johnny Chen <johnnychen94@hotmail.com>
(iqi::SSIM)(x, ref) = mean(_ssim_map(iqi, x, ref))
@doc (@doc SSIM)
assess_ssim(x, ref; crop=false) = SSIM(;crop=crop)(x, ref)
# Parameters `(K₁, K₂)` are used to avoid instability when denominator is very
# close to zero. Different from origianl implementation [2], we don't make it
# public since the ssim result is insensitive to these parameters according to
# [1].
# -- Johnny Chen <johnnychen94@hotmail.com>
const SSIM_K = (0.01, 0.03)
# SSIM is defined only for gray images,
# RGB images are treated as 3d gray images,
# other Color3 images are converted to RGB first.
function _ssim_map(iqi::SSIM,
x::Union{GenericGrayImage, AbstractArray{<:AbstractRGB}},
ref::Union{GenericGrayImage, AbstractArray{<:AbstractRGB}},
peakval = 1.0,
K = SSIM_K)
if size(x) ≠ size(ref)
err = ArgumentError("images should be the same size, instead they're $(size(x))-$(size(ref))")
throw(err)
end
if axes(x) ≠ axes(ref)
x = OffsetArrays.no_offset_view(x)
ref = OffsetArrays.no_offset_view(ref)
end
α, β, γ = iqi.W
C₁, C₂ = @. (peakval * K)^2
C₃ = C₂/2
T = promote_type(float(eltype(ref)), float(eltype(x)))
x = of_eltype(T, x)
ref = of_eltype(T, ref)
if [α, β, γ] ≈ [1.0, 1.0, 1.0]
ssim_map = __ssim_map_fast(x, ref, iqi.kernel, C₁, C₂; crop=iqi.crop)
else
l, c, s = __ssim_map_general(x, ref, iqi.kernel, C₁, C₂, C₃; crop=iqi.crop)
# ensure that negative numbers in s are not being raised to powers less than 1
# this is a non-standard implementation and it could vary from implementaion to implementaion
γ < 1.0 && (s = max.(s, zero(eltype(s))))
ssim_map = @. l^α * c^β * s^γ # equation (12) in [1]
end
return ssim_map
end
function _ssim_map(iqi::SSIM,
x::AbstractArray{<:Color3},
ref::AbstractArray{<:Color3},
peakval = 1.0, K = SSIM_K)
C = ccolor(RGB, floattype(eltype(x)))
_ssim_map(iqi, of_eltype(C, x), of_eltype(C, ref), peakval, K)
end
# helpers
function issymetric(kernel)
origin = first(axes(kernel, 1))
center = (length(kernel)-1) ÷ 2 + origin
kernel[origin:center] ≈ kernel[end:-1:center]
end
function __ssim_map_fast(x, ref, window, C₁, C₂; crop)
μx², μxy, μy², σx², σxy, σy² = _ssim_statistics(x, ref, window; crop=crop)
# this is a special case when [α, β, γ] ≈ [1.0, 1.0, 1.0]
# equation (13) in [1]
return @. ((2μxy + C₁)*(2σxy + C₂))/((μx²+μy²+C₁)*(σx² + σy² + C₂))
end
function __ssim_map_general(x, ref, window, C₁, C₂, C₃; crop)
μx², μxy, μy², σx², σxy, σy² = _ssim_statistics(x, ref, window; crop = crop)
T = eltype(σx²)
σx_σy = @. sqrt( max(σx²*σy², zero(T) ))
l = @. (2μxy + C₁)/(μx² + μy²) # equation (6) in [1]
c = @. (2σx_σy + C₂)/(σx² + σy² + C₂) # equation (9) in [1]
s = @. (σxy + C₃)/(σx_σy + C₃) # equation (10) in [1]
# MS-SSIM needs these, so we don't multiply them together here
return l, c, s
end
function _ssim_statistics(x::GenericImage, ref::GenericImage, window; crop)
# For RGB and other Color3 images, we don't slide the window at the color channel.
# In other words, these characters will be calculated channelwisely
region = map(axes(x)) do a
o = length(window) ÷ 2
# Even if crop=true, it crops only when image is larger than window
length(a) > length(window) ? (first(a)+o:last(a)-o) : a
end
R = crop ? CartesianIndices(region) : CartesianIndices(x)
window = ImageFiltering.kernelfactors(Tuple(repeated(window, ndims(ref))))
# don't slide the window in the channel dimension
μx = view(ImageFiltering.imfilter(x, window, "symmetric"), R) # equation (14) in [1]
μy = view(ImageFiltering.imfilter(ref, window, "symmetric"), R) # equation (14) in [1]
μx² = _mul.(μx, μx)
μy² = _mul.(μy, μy)
μxy = _mul.(μx, μy)
σx² = view(ImageFiltering.imfilter(_mul.(x, x ), window, "symmetric"), R) .- μx² # equation (15) in [1]
σy² = view(ImageFiltering.imfilter(_mul.(ref, ref), window, "symmetric"), R) .- μy² # equation (15) in [1]
σxy = view(ImageFiltering.imfilter(_mul.(x, ref), window, "symmetric"), R) .- μxy # equation (16) in [1]
# after that, channel dimension can be treated generically so we expand them here
return channelview.((μx², μxy, μy², σx², σxy, σy²))
end
# _ssim_statistics does not expected to work with other Color3 types
@inline _mul(x::Number, y::Number) = x * y
@inline _mul(x::C, y::C) where C <: Union{AbstractGray, AbstractRGB} = mapc(*, x, y)