#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Defines full-referene image quality metricsself.
These methods require a ground truth in order to make a quality assessment.
.. moduleauthor:: Daniel J Ching <carterbox@users.noreply.github.com>
"""
__author__ = "Daniel Ching"
__copyright__ = "Copyright (c) 2016, UChicago Argonne, LLC."
__docformat__ = 'restructuredtext en'
__all__ = [
'pcc',
'ImageQuality',
'ssim',
'msssim',
]
import warnings
import numpy as np
from scipy import ndimage
warnings.filterwarnings(
'ignore',
'From scipy 0\.13\.0, the output shape of zoom\(\) '
'is calculated with round\(\) instead of int\(\)'
)
[docs]def pcc(A, B, masks=None):
"""Return the Pearson product-moment correlation coefficients (PCC).
Parameters
-------------
A, B : ndarray
The two images to be compared
masks : list of ndarrays, optional
If supplied, the data under each mask is computed separately.
Returns
----------------
covariances : array, list of arrays
"""
covariances = []
if masks is None:
data = np.vstack((np.ravel(A), np.ravel(B)))
return np.corrcoef(data)
for m in masks:
weights = m[m > 0]
masked_B = B[m > 0]
masked_A = A[m > 0]
data = np.vstack((masked_A, masked_B))
# covariances.append(np.cov(data,aweights=weights))
covariances.append(np.corrcoef(data))
return covariances
[docs]class ImageQuality(object):
"""Store information about image quality.
Attributes
----------
img0 : array
img1 : array
Stacks of reference and deformed images.
metrics : dict
A dictionary with image quality information organized by scale.
``metric[scale] = (mean_quality, quality_map)``
method : string
The metric used to calculate the quality
"""
def __init__(self, original, reconstruction):
self.img0 = original.astype(np.float)
self.img1 = reconstruction.astype(np.float)
self.scales = None
self.mets = None
self.maps = None
self.method = ''
[docs] def quality(self, method="MSSSIM", L=1.0, **kwargs):
"""Compute the full-reference image quality of each image pair.
Available methods include SSIM :cite:`wang:02`, MSSSIM :cite:`wang:03`,
VIFp :cite:`Sheikh:15`
Parameters
----------
method : string, optional, (default: MSSSIM)
The quality metric desired for this comparison.
Options include: SSIM, MSSSIM, VIFp
L : scalar
The dynamic range of the data. This value is 1 for float
representations and 2^bitdepth for integer representations.
"""
dictionary = {
"SSIM": ssim,
"MSSSIM": msssim,
"VIFp": vifp,
# "FSIM": fsim # FSIM :cite:`zhang:11`.
}
try:
method_func = dictionary[method]
except KeyError:
ValueError("That method is not implemented.")
self.method = method
if self.img0.ndim > 2:
self.mets = list()
self.maps = list()
for i in range(self.img0.shape[2]):
scales, mets, maps = method_func(
self.img0[:, :, i], self.img1[:, :, i], L=L, **kwargs
)
self.scales = scales
self.mets.append(mets)
self.maps.append(maps)
self.mets = np.stack(self.mets, axis=1)
newmaps = []
for level in range(len(self.maps[0])):
this_level = []
for m in self.maps:
this_level.append(m[level])
this_level = np.stack(this_level, axis=2)
newmaps.append(this_level)
self.maps = newmaps
else:
self.scales, self.mets, self.maps = method_func(
self.img0, self.img1, L=L, **kwargs
)
def _join_metrics(A, B):
"""Join two image metric dictionaries."""
for key in list(B.keys()):
if key in A:
A[key][0] = np.concatenate((A[key][0], B[key][0]))
A[key][1] = np.concatenate(
(np.atleast_3d(A[key][1]), np.atleast_3d(B[key][1])), axis=2
)
else:
A[key] = B[key]
return A
def vifp(img0, img1, nlevels=5, sigma=1.2, L=None):
"""Return the Visual Information Fidelity (VIFp) of two images.
in a multiscale pixel domain with scalar.
Parameters
----------
img0 : array
img1 : array
Two images for comparison.
nlevels : scalar
The number of levels to measure quality.
sigma : scalar
The size of the quality filter at the smallest scale.
Returns
-------
metrics : dict
A dictionary with image quality information organized by scale.
``metric[scale] = (mean_quality, quality_map)``
The valid range for VIFp is (0, 1].
.. centered:: COPYRIGHT NOTICE
Copyright (c) 2005 The University of Texas at Austin. All rights reserved.
Permission is hereby granted, without written agreement and without license
or royalty fees, to use, copy, modify, and distribute this code (the source
files) and its documentation for any purpose, provided that the copyright
notice in its entirety appear in all copies of this code, and the original
source of this code, Laboratory for Image and Video Engineering (LIVE,
http://live.ece.utexas.edu) at the University of Texas at Austin (UT
Austin, http://www.utexas.edu), is acknowledged in any publication that
reports research using this code. The research is to be cited in the
bibliography as: H. R. Sheikh and A. C. Bovik, "Image Information and
Visual Quality", IEEE Transactions on Image Processing, (to appear). IN NO
EVENT SHALL THE UNIVERSITY OF TEXAS AT AUSTIN BE LIABLE TO ANY PARTY FOR
DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT
OF THE USE OF THIS DATABASE AND ITS DOCUMENTATION, EVEN IF THE UNIVERSITY
OF TEXAS AT AUSTIN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. THE
UNIVERSITY OF TEXAS AT AUSTIN SPECIFICALLY DISCLAIMS ANY WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS FOR A PARTICULAR PURPOSE. THE DATABASE PROVIDED HEREUNDER IS ON
AN "AS IS" BASIS, AND THE UNIVERSITY OF TEXAS AT AUSTIN HAS NO OBLIGATION
TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
.. centered:: END COPYRIGHT NOTICE
"""
_full_reference_input_check(img0, img1, sigma, nlevels, L)
sigmaN_sq = 2 # used to tune response
eps = 1e-10
scales = np.zeros(nlevels)
mets = np.zeros(nlevels)
maps = [None] * nlevels
for level in range(0, nlevels):
# Downsample (using ndimage.zoom to prevent sampling bias)
if (level > 0):
img0 = ndimage.zoom(img0, 0.5)
img1 = ndimage.zoom(img1, 0.5)
mu0 = ndimage.gaussian_filter(img0, sigma)
mu1 = ndimage.gaussian_filter(img1, sigma)
sigma0_sq = ndimage.gaussian_filter((img0 - mu0)**2, sigma)
sigma1_sq = ndimage.gaussian_filter((img1 - mu1)**2, sigma)
sigma01 = ndimage.gaussian_filter((img0 - mu0) * (img1 - mu1), sigma)
g = sigma01 / (sigma0_sq + eps)
sigmav_sq = sigma1_sq - g * sigma01
# Calculate VIF
numator = np.log2(1 + g**2 * sigma0_sq / (sigmav_sq + sigmaN_sq))
denator = np.sum(np.log2(1 + sigma0_sq / sigmaN_sq))
vifmap = numator / denator
vifp = np.sum(vifmap)
# Normalize the map because we want values between 1 and 0
vifmap *= vifmap.size
scale = sigma * 2**level
scales[level] = scale
mets[level] = vifp
maps[level] = vifmap
return scales, mets, maps
# def fsim(img0, img1, nlevels=5, nwavelets=16, L=None):
# """FSIM Index with automatic downsampling, Version 1.0
#
# An implementation of the algorithm for calculating the Feature SIMilarity
# (FSIM) index was ported to Python. This implementation only considers the
# luminance component of images. For multichannel images, convert to
# grayscale first. Dynamic range should be 0-255.
#
# Parameters
# ----------
# img0 : array
# img1 : array
# Two images for comparison.
# nlevels : scalar
# The number of levels to measure quality.
# nwavelets : scalar
# The number of wavelets to use in the phase congruency calculation.
#
# Returns
# -------
# metrics : dict
# A dictionary with image quality information organized by scale.
# ``metric[scale] = (mean_quality, quality_map)``
# The valid range for FSIM is (0, 1].
#
#
# References
# ----------
# Lin Zhang, Lei Zhang, Xuanqin Mou, and David Zhang,"FSIM: a feature
# similarity index for image qualtiy assessment", IEEE Transactions on Image
# Processing, vol. 20, no. 8, pp. 2378-2386, 2011.
#
# .. centered:: COPYRIGHT NOTICE
#
# Copyright (c) 2010 Lin ZHANG, Lei Zhang, Xuanqin Mou and David Zhang.
# All Rights Reserved.
#
# Permission to use, copy, or modify this software and its documentation
# for educational and research purposes only and without fee is here
# granted, provided that this copyright notice and the original authors'
# names appear on all copies and supporting documentation. This program
# shall not be used, rewritten, or adapted as the basis of a commercial
# software or hardware product without first obtaining permission of the
# authors. The authors make no representations about the suitability of
# this software for any purpose. It is provided "as is" without express
# or implied warranty.
#
# .. centered:: END COPYRIGHT NOTICE
# """
# _full_reference_input_check(img0, img1, 1.2, nlevels, L)
# if nwavelets < 1:
# raise ValueError('There must be at least one wavelet level.')
#
# Y1 = img0
# Y2 = img1
#
# scales = np.zeros(nlevels)
# mets = np.zeros(nlevels)
# maps = [None] * nlevels
#
# for level in range(0, nlevels):
# # sigma = 1.2 is approximately correct because the width of the scharr
# # and min wavelet filter (phase congruency filter) is 3.
# sigma = 1.2 * 2**level
#
# F = 2 # Downsample (using ndimage.zoom to prevent sampling bias)
# Y1 = ndimage.zoom(Y1, 1/F)
# Y2 = ndimage.zoom(Y2, 1/F)
#
# # Calculate the phase congruency maps
# [PC1, Orient1, ft1, T1] = phase.phasecongmono(Y1, nscale=nwavelets)
# [PC2, Orient2, ft2, T2] = phase.phasecongmono(Y2, nscale=nwavelets)
#
# # Calculate the gradient magnitude map using Scharr filters
# dx = np.array([[3., 0., -3.],
# [10., 0., -10.],
# [3., 0., -3.]]) / 16
# dy = np.array([[3., 10., 3.],
# [0., 0., 0.],
# [-3., -10., -3.]]) / 16
#
# IxY1 = ndimage.filters.convolve(Y1, dx)
# IyY1 = ndimage.filters.convolve(Y1, dy)
# gradientMap1 = np.sqrt(IxY1**2 + IyY1**2)
#
# IxY2 = ndimage.filters.convolve(Y2, dx)
# IyY2 = ndimage.filters.convolve(Y2, dy)
# gradientMap2 = np.sqrt(IxY2**2 + IyY2**2)
#
# # Calculate the FSIM
# T1 = 0.85 # fixed and depends on dynamic range of PC values
# T2 = 160 # fixed and depends on dynamic range of GM values
# PCSimMatrix = (2 * PC1 * PC2 + T1) / (PC1**2 + PC2**2 + T1)
# gradientSimMatrix = ((2 * gradientMap1 * gradientMap2 + T2) /
# (gradientMap1**2 + gradientMap2**2 + T2))
# PCm = np.maximum(PC1, PC2)
# FSIMmap = gradientSimMatrix * PCSimMatrix
# FSIM = np.sum(FSIMmap * PCm) / np.sum(PCm)
#
# scales[level] = sigma
# mets[level] = FSIM
# maps[level] = FSIMmap
#
# return scales, mets, maps
[docs]def msssim(
img0,
img1,
nlevels=5,
sigma=1.2,
L=1.0,
K=(0.01, 0.03),
alpha=4,
beta_gamma=None
):
"""Multi-Scale Structural SIMilarity index (MS-SSIM).
Parameters
----------
img0 : array
img1 : array
Two images for comparison.
nlevels : int
The max number of levels to analyze
sigma : float
Sets the standard deviation of the gaussian filter. This setting
determines the minimum scale at which quality is assessed.
L : scalar
The dynamic range of the data. This value is 1 for float
representations and 2^bitdepth for integer representations.
K : 2-tuple
A list of two constants which help prevent division by zero.
alpha : float
The exponent which weights the contribution of the luminance term.
beta_gamma : list
The exponent which weights the contribution of the contrast and
structure terms at each level.
Returns
-------
metrics : dict
A dictionary with image quality information organized by scale.
``metric[scale] = (mean_quality, quality_map)``
The valid range for SSIM is [-1, 1].
References
----------
Multi-scale Structural Similarity Index (MS-SSIM)
Z. Wang, E. P. Simoncelli and A. C. Bovik, "Multi-scale structural
similarity for image quality assessment," Invited Paper, IEEE Asilomar
Conference on Signals, Systems and Computers, Nov. 2003
"""
_full_reference_input_check(img0, img1, sigma, nlevels, L)
# The relative imporance of each level as determined by human experiment
if beta_gamma is None:
beta_gamma = np.array([0.0448, 0.2856, 0.3001, 0.2363, 0.1333]) * 4
assert nlevels < 6, "Not enough beta_gamma weights for more than 5 levels"
scales = np.zeros(nlevels)
maps = [None] * nlevels
original_shape = np.array(img0.shape)
for level in range(0, nlevels):
scale, ssim_mean, ssim_map = ssim(
img0,
img1,
sigma=sigma,
L=L,
K=K,
scale=sigma,
alpha=0 if level < (nlevels - 1) else alpha,
beta_gamma=beta_gamma[level]
)
# Always take the direct ratio between original and downsampled maps
# to prevent resizing mismatch for odd sizes
ratio = original_shape / np.array(ssim_map.shape)
scales[level] = scale * ratio[0]
maps[level] = ndimage.zoom(ssim_map, ratio, prefilter=False, order=0)
if level == nlevels - 1:
break
# Downsample (using ndimage.zoom to prevent sampling bias)
# Images become half the size
img0 = ndimage.zoom(img0, 0.5)
img1 = ndimage.zoom(img1, 0.5)
ms_ssim_map = np.nanprod(maps, axis=0)
ms_ssim_mean = np.nanmean(ms_ssim_map)
return scales, ms_ssim_mean, ms_ssim_map
[docs]def ssim(
img1,
img2,
sigma=1.2,
L=1,
K=(0.01, 0.03),
scale=None,
alpha=4,
beta_gamma=4
):
"""Return the Structural SIMilarity index (SSIM) of two images.
A modified version of the Structural SIMilarity index (SSIM) based on an
implementation by Helder C. R. de Oliveira, based on the implementation by
Antoine Vacavant, ISIT lab, antoine.vacavant@iut.u-clermont1.fr
http://isit.u-clermont1.fr/~anvacava
Attributes
----------
img1 : array
img2 : array
Two images for comparison.
sigma : float
Sets the standard deviation of the gaussian filter. This setting
determines the minimum scale at which quality is assessed.
L : scalar
The dynamic range of the data. The difference between the
minimum and maximum of the data: 2^bitdepth for integer
representations.
K : 2-tuple
A list of two constants which help prevent division by zero.
alpha : float
The exponent which weights the contribution of the luminance term.
beta_gamma : list
The exponent which weights the contribution of the contrast and
structure terms at each level.
Returns
-------
metrics : dict
A dictionary with image quality information organized by scale.
``metric[scale] = (mean_quality, quality_map)``
The valid range for SSIM is [-1, 1].
References
----------
Z. Wang, A. C. Bovik, H. R. Sheikh and E. P. Simoncelli. Image quality
assessment: From error visibility to structural similarity. IEEE
Transactions on Image Processing, 13(4):600--612, 2004.
Z. Wang and A. C. Bovik. Mean squared error: Love it or leave it? - A new
look at signal fidelity measures. IEEE Signal Processing Magazine,
26(1):98--117, 2009.
Silvestre-Blanes, J., & Pérez-Lloréns, R. (2011, September). SSIM and their
dynamic range for image quality assessment. In ELMAR, 2011 Proceedings
(pp. 93-96). IEEE.
"""
_full_reference_input_check(img1, img2, sigma, 1, L)
if scale is not None and scale <= 0:
raise ValueError("Scale cannot be negative or zero.")
assert L > 0, "L, the dynamic range must be larger than 0."
c_1 = (K[0] * L)**2
c_2 = (K[1] * L)**2
c_3 = c_2 * 0.5
# Means obtained by Gaussian filtering of inputs
mu_1 = ndimage.filters.gaussian_filter(img1, sigma)
mu_2 = ndimage.filters.gaussian_filter(img2, sigma)
# Squares of means
mu_1_sq = mu_1**2
mu_2_sq = mu_2**2
mu_1_mu_2 = mu_1 * mu_2
# Variances obtained by Gaussian filtering of inputs' squares
sigma_1_sq = ndimage.filters.gaussian_filter(img1**2, sigma) - mu_1_sq
sigma_2_sq = ndimage.filters.gaussian_filter(img2**2, sigma) - mu_2_sq
# Covariance
sigma_12 = ndimage.filters.gaussian_filter(img1 * img2, sigma) - mu_1_mu_2
# Standard deviations multiplied
sigma_1_sigma_2 = np.sqrt(sigma_1_sq * sigma_2_sq)
# Division by zero is prevented by adding c_1 and c_2
numerator1 = 2 * mu_1_mu_2 + c_1 # luminance
denominator1 = mu_1_sq + mu_2_sq + c_1 # luminace
numerator2 = 2 * sigma_1_sigma_2 + c_2 # contrast
denominator2 = sigma_1_sq + sigma_2_sq + c_2 # constrast
numerator3 = sigma_12 + c_3 # structure
denominator3 = sigma_1_sigma_2 + c_3 # structure
if (c_1 > 0) and (c_2 > 0):
with np.errstate(invalid='ignore'):
ssim_map = (
(numerator1 / denominator1)**alpha *
((numerator2 * numerator3) /
(denominator2 * denominator3))**beta_gamma
)
else:
ssim_map = np.ones(numerator1.shape)
index = (denominator1 * denominator2 > 0)
ssim_map[index] = (
(numerator1[index] / denominator1[index])**alpha *
((numerator2[index] * numerator3[index]) /
(denominator2[index] * denominator3[index]))**beta_gamma
)
# Sometimes c_1 and c_2 don't do their job of stabilizing the result
with np.errstate(invalid='ignore'):
ssim_map[ssim_map > 1] = 1
ssim_map[ssim_map < -1] = -1
ssim_mean = np.nanmean(ssim_map)
if scale is None:
scale = sigma
return scale, ssim_mean, ssim_map
def _full_reference_input_check(img0, img1, sigma, nlevels, L):
"""Checks full reference quality measures for valid inputs."""
if nlevels <= 0:
raise ValueError('nlevels must be >= 1.')
if sigma < 1.2:
raise ValueError('sigma < 1.2 is effective meaningless.')
if np.min(img0.shape) / (2**(nlevels - 1)) < sigma * 2:
raise ValueError(
"{nlevels} levels makes {shape} smaller than a filter"
" size of 2 * {sigma}".format(
nlevels=nlevels, shape=img0.shape, sigma=sigma
)
)
if L is not None and L < 1:
raise ValueError("Dynamic range must be >= 1.")
if img0.shape != img1.shape:
raise ValueError(
"original and reconstruction should be the " + "same shape"
)