#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2016,2017,2018 Jérémie DECOCK (http://www.jdhp.org)
# This script is provided under the terms and conditions of the MIT license:
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
__all__ = ['normalize_array',
'mse',
'nrmse',
#'number_of_clusters',
#'metric1',
#'metric2',
#'metric3',
#'metric4',
'psnr',
'ssim']
import numpy as np
#from pywi.image.pixel_clusters import filter_pixels_clusters_stats
from skimage.measure import compare_ssim as ski_ssim
from skimage.measure import compare_psnr as ski_psnr
#from skimage.measure import compare_nrmse as ski_nrmse
###############################################################################
# EXCEPTIONS #
###############################################################################
class RefBasedMetricError(Exception):
"""Exceptions common to the pywi.benchmark.metrics.refbased module"""
pass
class EmptyOutputImageError(RefBasedMetricError):
"""Exception raised when the output image only have null pixels"""
def __init__(self):
super(EmptyOutputImageError, self).__init__("Empty output image error")
class EmptyReferenceImageError(RefBasedMetricError):
"""Exception raised when the reference image only have null pixels"""
def __init__(self):
super(EmptyReferenceImageError, self).__init__("Empty reference image error")
###############################################################################
# TOOL FUNCTIONS #
###############################################################################
[docs]def normalize_array(array):
r"""Normalize the given array such that its values fit between 0.0
and 1.0.
It applies
.. math::
\text{normalize}(\boldsymbol{S}) = \frac{ \boldsymbol{S} - \text{min}(\boldsymbol{S}) }{ \text{max}(\boldsymbol{S}) - \text{min}(\boldsymbol{S}) }
where :math:`\boldsymbol{S}` is the input array (an image).
Parameters
----------
image : Numpy array
The image to normalize (whatever its shape)
Returns
-------
Numpy array
The normalized version of the input image (keeping the same dimension
and shape)
"""
# Copy and cast images to prevent tricky bugs
# See https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.astype.html#numpy-ndarray-astype
input_array = array.astype('float64', copy=True)
min_value = np.nanmin(input_array)
max_value = np.nanmax(input_array)
output_array = (input_array - min_value) / float(max_value - min_value)
return output_array
###############################################################################
# METRIC FUNCTIONS #
###############################################################################
# Mean-Squared Error (MSE) ####################################################
[docs]def mse(image, reference_image):
r"""Compute the score of ``image`` regarding ``reference_image``
with the *Mean-Squared Error* (MSE) metric.
It applies
.. math::
\text{MSE}(\hat{\boldsymbol{S}}, \boldsymbol{S}^*) = \left\langle \left( \hat{\boldsymbol{S}} - \boldsymbol{S}^* \right)^{\circ 2} \right\rangle
with:
- :math:`\hat{\boldsymbol{S}}` the algorithm's output image (i.e. the
*cleaned* image);
- :math:`\boldsymbol{S}^*` the reference image (i.e. the *clean* image);
- :math:`\langle \boldsymbol{S} \rangle` the average of matrix
:math:`\boldsymbol{S}`;
- :math:`\boldsymbol{S}^{\circ 2}` the
`Hadamar power <https://en.wikipedia.org/wiki/Hadamard_product_(matrices)#Analogous_operations>`_
(i.e. the element wise square) of matrix :math:`\boldsymbol{S}`.
See http://scikit-image.org/docs/dev/api/skimage.measure.html#compare-mse
for more information.
Note
----
This function is not well-suited to high dynamic range images handled with
this project (errors are correlated with energy levels).
Parameters
----------
image: 2D ndarray
The cleaned image returned by the image cleanning algorithm to assess.
reference_image: 2D ndarray
The actual clean image (the best result that can be expected for the
image cleaning algorithm).
Returns
-------
float
The score of the image cleaning algorithm for the given image.
"""
# Copy and cast images to prevent tricky bugs
# See https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.astype.html#numpy-ndarray-astype
image = image.astype('float64', copy=True)
reference_image = reference_image.astype('float64', copy=True)
score = np.nanmean(np.square(image - reference_image))
return float(score)
# Normalized Root Mean-Squared Error (NRMSE) ##################################
[docs]def nrmse(image, reference_image):
r"""Compute the score of ``image`` regarding ``reference_image``
with the *Normalized Root Mean-Squared Error* (NRMSE) metric.
It applies
.. math::
\text{NRMSE}(\hat{\boldsymbol{S}}, \boldsymbol{S}^*) = \frac{\sqrt{\text{MSE}}}{\sqrt{ \left\langle \hat{\boldsymbol{S}} \circ \boldsymbol{S}^* \right\rangle }}
with:
- :math:`\hat{\boldsymbol{S}}` the algorithm's output image (i.e. the
*cleaned* image);
- :math:`\boldsymbol{S}^*` the reference image (i.e. the *clean* image);
- :math:`\langle \boldsymbol{S} \rangle` the average of matrix
:math:`\boldsymbol{S}`;
- :math:`\circ` the
`Hadamar product <https://en.wikipedia.org/wiki/Hadamard_product_(matrices)>`_
(i.e. the element wise product operator).
See http://scikit-image.org/docs/dev/api/skimage.measure.html#compare-nrmse and
https://en.wikipedia.org/wiki/Root-mean-square_deviation for more information.
Parameters
----------
image: 2D ndarray
The cleaned image returned by the image cleanning algorithm to assess.
reference_image: 2D ndarray
The actual clean image (the best result that can be expected for the
image cleaning algorithm).
Returns
-------
float
The score of the image cleaning algorithm for the given image.
"""
# Copy and cast images to prevent tricky bugs
# See https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.astype.html#numpy-ndarray-astype
image = image.astype('float64', copy=True)
reference_image = reference_image.astype('float64', copy=True)
#if ('nrmse_normalize_type' in kwargs) and (kwargs['nrmse_normalize_type'].lower() == 'euclidian'):
# denom =
# TODO: see https://github.com/scikit-image/scikit-image/blob/master/skimage/measure/simple_metrics.py#L82
_mse = mse(image, reference_image)
denom = np.sqrt(np.nanmean((reference_image * image), dtype=np.float64))
if denom == 0:
score = float('nan')
else:
score = float(np.sqrt(_mse) / denom)
return score
## Unusual Normalized Root Mean-Squared Error (uNRMSE) #########################
#
#def metric1(image, reference_image):
# r"""Compute the score of ``image`` regarding ``reference_image``
# with a (unusually) normalized version of the *Root Mean-Squared Error*
# (RMSE) metric.
#
# It applies
#
# .. math::
#
# \text{uNRMSE}(\hat{\boldsymbol{S}}, \boldsymbol{S}^*) = \left\langle \left( \left( \hat{\boldsymbol{S}}_n - \boldsymbol{S}^*_n \right)^{\circ 2} \right)^{\circ \frac{1}{2}} \right\rangle
#
# with:
#
# - :math:`\hat{\boldsymbol{S}}_n`
# the algorithm's normalized output image (i.e. the *cleaned* image),
# (using :func:`normalize_array`);
# - :math:`\boldsymbol{S}^*_n`
# the normalized reference image (i.e. the *clean* image)
# (using :func:`normalize_array`);
# - :math:`\langle \boldsymbol{S} \rangle` the average of matrix
# :math:`\boldsymbol{S}`;
# - :math:`\boldsymbol{S}^{\circ 2}` the
# `Hadamar power <https://en.wikipedia.org/wiki/Hadamard_product_(matrices)#Analogous_operations>`_
# (i.e. the element wise square) of matrix :math:`\boldsymbol{S}`.
#
# Note
# ----
# This function is not robust to noise on extreme values.
#
# Parameters
# ----------
# image: 2D ndarray
# The cleaned image returned by the image cleanning algorithm to assess.
# reference_image: 2D ndarray
# The actual clean image (the best result that can be expected for the
# image cleaning algorithm).
#
# Returns
# -------
# float
# The score of the image cleaning algorithm for the given image.
#
#
# """
#
# # Copy and cast images to prevent tricky bugs
# # See https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.astype.html#numpy-ndarray-astype
# image = image.astype('float64', copy=True)
# reference_image = reference_image.astype('float64', copy=True)
#
# image = normalize_array(image)
# reference_image = normalize_array(reference_image)
#
# score = np.nanmean(np.square(image - reference_image))
#
# return float(score)
#
#
## Mean Pixel Difference 2 #####################################################
#
#def metric2(image, reference_image):
# r"""Compute the score of ``image`` regarding ``reference_image``
# with the :math:`\mathcal{E}_{\text{shape}}` metric.
#
# It applies
#
# .. math::
#
# f(\hat{\boldsymbol{S}}, \boldsymbol{S}^*) = \left\langle \text{abs} \left( \frac{\hat{\boldsymbol{S}}}{\sum_i \hat{\boldsymbol{S}}_i} - \frac{\boldsymbol{S}^*}{\sum_i \boldsymbol{S}^*_i} \right) \right\rangle
#
# with:
#
# - :math:`\hat{\boldsymbol{S}}` the algorithm's output image
# (i.e. the *cleaned* image);
# - :math:`\boldsymbol{S}^*` the reference image (i.e. the *clean* image);
# - :math:`\langle \boldsymbol{S} \rangle` the average of matrix
# :math:`\boldsymbol{S}`.
#
# Parameters
# ----------
# image: 2D ndarray
# The cleaned image returned by the image cleanning algorithm to assess.
# reference_image: 2D ndarray
# The actual clean image (the best result that can be expected for the
# image cleaning algorithm).
#
# Returns
# -------
# float
# The score of the image cleaning algorithm for the given image.
# """
#
# # Copy and cast images to prevent tricky bugs
# # See https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.astype.html#numpy-ndarray-astype
# image = image.astype('float64', copy=True)
# reference_image = reference_image.astype('float64', copy=True)
#
# sum_output_image = float(np.nansum(image))
# sum_reference_image = float(np.nansum(reference_image))
#
# if sum_output_image <= 0: # TODO
# raise EmptyOutputImageError()
#
# if sum_reference_image <= 0: # TODO
# raise EmptyReferenceImageError()
#
# mark = np.nanmean(np.abs((image / sum_output_image) - (reference_image / sum_reference_image)))
#
# return float(mark)
#
#
## Relative Total Counts Difference (mpdspd) ###################################
#
#def metric3(image, reference_image):
# r"""Compute the score of ``image`` regarding ``reference_image``
# with the :math:`\mathcal{E}^+_{\text{energy}}`
# (a.k.a. *relative total counts difference*) metric.
#
# It applies
#
# .. math::
#
# f(\hat{\boldsymbol{S}}, \boldsymbol{S}^*) = \frac{ \text{abs} \left( \sum_i \hat{\boldsymbol{S}}_i - \sum_i \boldsymbol{S}^*_i \right) }{ \sum_i \boldsymbol{S}^*_i }
#
# with :math:`\hat{\boldsymbol{S}}` the algorithm's output image
# (i.e. the *cleaned* image)
# and :math:`\boldsymbol{S}^*` the reference image
# (i.e. the *clean* image).
#
# Parameters
# ----------
# image: 2D ndarray
# The cleaned image returned by the image cleanning algorithm to assess.
# reference_image: 2D ndarray
# The actual clean image (the best result that can be expected for the
# image cleaning algorithm).
#
# Returns
# -------
# float
# The score of the image cleaning algorithm for the given image.
# """
#
# # Copy and cast images to prevent tricky bugs
# # See https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.astype.html#numpy-ndarray-astype
# image = image.astype('float64', copy=True)
# reference_image = reference_image.astype('float64', copy=True)
#
# sum_output_image = float(np.nansum(image))
# sum_reference_image = float(np.nansum(reference_image))
#
# if sum_reference_image <= 0: # TODO
# raise EmptyReferenceImageError()
#
# mark = np.abs(sum_output_image - sum_reference_image) / sum_reference_image
#
# return float(mark)
#
#
## Signed Relative Total Counts Difference (sspd) ##############################
#
#def metric4(image, reference_image):
# r"""Compute the score of ``image`` regarding ``reference_image``
# with the :math:`\mathcal{E}_{\text{energy}}`
# (a.k.a. *signed relative total counts difference*) metric.
#
# It applies
#
# .. math::
#
# f(\hat{\boldsymbol{S}}, \boldsymbol{S}^*) = \frac{ \sum_i \hat{\boldsymbol{S}}_i - \sum_i \boldsymbol{S}^*_i }{ \sum_i \boldsymbol{S}^*_i }
#
# with :math:`\hat{\boldsymbol{S}}` the algorithm's output image
# (i.e. the *cleaned* image)
# and :math:`\boldsymbol{S}^*` the reference image
# (i.e. the *clean* image).
#
# Parameters
# ----------
# image: 2D ndarray
# The cleaned image returned by the image cleanning algorithm to assess.
# reference_image: 2D ndarray
# The actual clean image (the best result that can be expected for the
# image cleaning algorithm).
#
# Returns
# -------
# float
# The score of the image cleaning algorithm for the given image.
# """
#
# # Copy and cast images to prevent tricky bugs
# # See https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.astype.html#numpy-ndarray-astype
# image = image.astype('float64', copy=True)
# reference_image = reference_image.astype('float64', copy=True)
#
# sum_output_image = float(np.nansum(image))
# sum_reference_image = float(np.nansum(reference_image))
#
# if sum_reference_image <= 0: # TODO
# raise EmptyReferenceImageError()
#
# mark = (sum_output_image - sum_reference_image) / sum_reference_image
#
# return float(mark)
# Structural Similarity Index Measure (SSIM) ##################################
[docs]def ssim(image, reference_image):
r"""Compute the score of ``image`` regarding ``reference_image``
with the *Structural Similarity Index Measure* (SSIM) metric.
See [1]_, [2]_, [3]_ and [4]_ for more information.
The SSIM index is calculated on various windows of an image.
The measure between two windows :math:`x` and :math:`y` of common size
:math:`N.N` is:
.. math::
\hbox{SSIM}(x,y) = \frac{(2\mu_x\mu_y + c_1)(2\sigma_{xy} + c_2)}{(\mu_x^2 + \mu_y^2 + c_1)(\sigma_x^2 + \sigma_y^2 + c_2)}
with:
* :math:`\scriptstyle\mu_x` the average of :math:`\scriptstyle x`;
* :math:`\scriptstyle\mu_y` the average of :math:`\scriptstyle y`;
* :math:`\scriptstyle\sigma_x^2` the variance of :math:`\scriptstyle x`;
* :math:`\scriptstyle\sigma_y^2` the variance of :math:`\scriptstyle y`;
* :math:`\scriptstyle \sigma_{xy}` the covariance of :math:`\scriptstyle x` and :math:`\scriptstyle y`;
* :math:`\scriptstyle c_1 = (k_1L)^2`, :math:`\scriptstyle c_2 = (k_2L)^2` two variables to stabilize the division with weak denominator;
* :math:`\scriptstyle L` the dynamic range of the pixel-values (typically this is :math:`\scriptstyle 2^{\#bits\ per\ pixel}-1`);
* :math:`\scriptstyle k_1 = 0.01` and :math:`\scriptstyle k_2 = 0.03` by default.
The SSIM index satisfies the condition of symmetry:
.. math::
\text{SSIM}(x, y) = \text{SSIM}(y, x)
Parameters
----------
image: 2D ndarray
The cleaned image returned by the image cleanning algorithm to assess.
reference_image: 2D ndarray
The actual clean image (the best result that can be expected for the
image cleaning algorithm).
Returns
-------
float
The score of the image cleaning algorithm for the given image.
References
----------
.. [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, 600-612.
https://ece.uwaterloo.ca/~z70wang/publications/ssim.pdf,
DOI:10.1.1.11.2477
.. [2] Avanaki, A. N. (2009). Exact global histogram specification
optimized for structural similarity. Optical Review, 16, 613-621.
http://arxiv.org/abs/0901.0065,
DOI:10.1007/s10043-009-0119-z
.. [3] http://scikit-image.org/docs/dev/api/skimage.measure.html#compare-ssim
.. [4] https://en.wikipedia.org/wiki/Structural_similarity
"""
# Copy and cast images to prevent tricky bugs
# See https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.astype.html#numpy-ndarray-astype
image = image.astype('float64', copy=True)
reference_image = reference_image.astype('float64', copy=True)
# TODO: the following two lines may be wrong...
image[np.isnan(image)] = 0
reference_image[np.isnan(reference_image)] = 0
ssim_val, ssim_image = ski_ssim(image, reference_image, full=True, gaussian_weights=True, sigma=0.5)
return float(ssim_val)
# Peak Signal-to-Noise Ratio (PSNR) ###########################################
[docs]def psnr(image, reference_image):
r"""Compute the score of ``image`` regarding ``reference_image``
with the *Peak Signal-to-Noise Ratio* (PSNR) metric.
See [5]_ and [6]_ for more information.
Parameters
----------
image: 2D ndarray
The cleaned image returned by the image cleanning algorithm to assess.
reference_image: 2D ndarray
The actual clean image (the best result that can be expected for the
image cleaning algorithm).
Returns
-------
float
The score of the image cleaning algorithm for the given image.
References
----------
.. [5] http://scikit-image.org/docs/dev/api/skimage.measure.html#skimage.measure.compare_psnr
.. [6] https://en.wikipedia.org/wiki/Peak_signal-to-noise_ratio
"""
# Copy and cast images to prevent tricky bugs
# See https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.astype.html#numpy-ndarray-astype
image = image.astype('float64', copy=True)
reference_image = reference_image.astype('float64', copy=True)
# TODO: the following two lines may be wrong...
image[np.isnan(image)] = 0
reference_image[np.isnan(reference_image)] = 0
#psnr_val = ski_psnr(image, reference_image, dynamic_range=1e3)
psnr_val = ski_psnr(image, reference_image, data_range=1e3)
return float(psnr_val)
# Clusters of pixels ##########################################################
#def number_of_clusters(image, reference_image):
# delta_pe, delta_abs_pe, delta_num_pixels = filter_pixels_clusters_stats(image)
#
# score_dict = collections.OrderedDict((
# ('kill_isolated_pixels_delta_pe', delta_pe),
# ('kill_isolated_pixels_delta_abs_pe', delta_abs_pe),
# ('kill_isolated_pixels_delta_num_pixels', delta_num_pixels)
# ))
#
# Score = collections.namedtuple('Score', score_dict.keys())
#
# return Score(**score_dict)