Source code for compare

#!/usr/bin/env python
# coding: utf-8
#
#  Copyright (c) 2012, Adam Turcotte (adam.turcotte@gmail.com)
#                      Nicolas Robidoux (nicolas.robidoux@gmail.com)
#  License: BSD 2-Clause License
#
#  This file is part of the
#  EXQUIRES (EXtensible QUantitative Image RESampling) test suite
#

"""Print the result of calling a difference metric on two image files.

  **Difference Metrics:**

    =========== =================================================
    NAME        DESCRIPTION
    =========== =================================================
    srgb_1      :math:`\ell_1` norm in sRGB colour space
    srgb_2      :math:`\ell_2` norm in sRGB colour space
    srgb_4      :math:`\ell_4` norm in sRGB colour space
    srgb_inf    :math:`\ell_\infty` norm in sRGB colour space
    cmc_1       :math:`\ell_1` norm using the CMC(1:1) colour difference
    cmc_2       :math:`\ell_2` norm using the CMC(1:1) colour difference
    cmc_4       :math:`\ell_4` norm using the CMC(1:1) colour difference
    cmc_inf     :math:`\ell_\infty` norm using the CMC(1:1) colour difference
    xyz_1       :math:`\ell_1` norm in XYZ colour space
    xyz_2       :math:`\ell_2` norm in XYZ colour space
    xyz_4       :math:`\ell_4` norm in XYZ colour space
    xyz_inf     :math:`\ell_\infty` norm in XYZ colour space
    blur_1      MSSIM-inspired :math:`\ell_1` norm
    blur_2      MSSIM-inspired :math:`\ell_2` norm
    blur_4      MSSIM-inspired :math:`\ell_4` norm
    blur_inf    MSSIM-inspired :math:`\ell_\infty` norm
    mssim       Mean Structural Similarity Index (MSSIM)
    =========== =================================================

"""

import inspect
import os
from math import exp

from exquires import parsing


[docs]class Metrics(object): """This class contains error metrics to be used on sRGB images. The :math:`\ell_1`, :math:`\ell_2`, :math:`\ell_4`, and :math:`\ell_\infty` metrics are normalized by L, the largest possible pixel value of the input images (the lowest is assumed to be 0). The range of output for these metrics is [0, 100]. The MSSIM metric produces output in the range [-1, 1], but it is unlikely that a negative value will be produced, as the image features must differ greatly. For instance, a pure white image compared with a pure black image produces a result slightly greater than 0. The CMC and XYZ errors can be slightly outside the range [0, 100], but this will not occur for most image pairs. .. note:: By default, a :class:`Metrics` object is configured to operate on 16-bit images. :param image1: first image to compare (reference image) :param image2: second image to compare (test image) :param L: highest possible pixel value (default=65535) :type image1: `path` :type image2: `path` :type L: `integer` """ def __init__(self, image1, image2, maxval=65535): """Create a new :class:`Metrics` object.""" vipscc = __import__('vipsCC', globals(), locals(), ['VImage', 'VMask'], -1) self.vmask = vipscc.VMask self.im1 = vipscc.VImage.VImage(image1) self.im2 = vipscc.VImage.VImage(image2) self.maxval = maxval self.srgb_profile = os.path.join(os.path.dirname(__file__), 'sRGB_IEC61966-2-1_black_scaled.icc') self.intent = 1 # IM_INTENT_RELATIVE_COLORIMETRIC
[docs] def srgb_1(self): """Compute :math:`\ell_1` error in sRGB colour space. The equation for the :math:`\ell_1` error, aka Average Absolute Error (AAE), is .. math:: :label: l_1 \ell_1(x,y) = \\frac{1}{N} \sum_{i=1}^{N} |x_i - y_i| where :math:`x` and :math:`y` are the images to compare, each consisting of :math:`N` pixels. :return: :math:`\ell_1` error :rtype: `float` """ diff = self.im1.subtract(self.im2).abs().avg() / self.maxval return diff * 100
[docs] def srgb_2(self): """Compute :math:`\ell_2` error in sRGB colour space. The equation for the :math:`\ell_2` error, aka Root Mean Squared Error (RMSE), is .. math:: :label: l_2 \ell_2(x,y) = \sqrt{\\frac{1}{N} \sum_{i=1}^{N} (x_i - y_i)^2} where :math:`x` and :math:`y` are the images to compare, each consisting of :math:`N` pixels. :return: :math:`\ell_2` error :rtype: `float` """ diff = self.im1.subtract(self.im2).pow(2).avg() ** 0.5 / self.maxval return diff * 100
[docs] def srgb_4(self): """Compute :math:`\ell_4` error in sRGB colour space. The equation for the :math:`\ell_4` error is .. math:: :label: l_4 \ell_4(x,y) = \sqrt[4]{\\frac{1}{N} \sum_{i=1}^{N} (x_i - y_i)^4} where :math:`x` and :math:`y` are the images to compare, each consisting of :math:`N` pixels. :return: :math:`\ell_4` error :rtype: `float` """ diff = self.im1.subtract(self.im2).pow(4).avg() ** 0.25 / self.maxval return diff * 100
[docs] def srgb_inf(self): """Compute :math:`\ell_\infty` error in sRGB colour space. The equation for the :math:`\ell_\infty` error, aka Maximum Absolute Error (MAE), is .. math:: :label: l_inf \ell_\infty(x,y) = \max_{1 \le i \le N} |x_i - y_i| where :math:`x` and :math:`y` are the images to compare, each consisting of :math:`N` pixels. :return: :math:`\ell_\infty` error :rtype: `float` """ diff = self.im1.subtract(self.im2).abs().max() / self.maxval return diff * 100
[docs] def mssim(self): """Compute the Mean Structural Similarity Index (MSSIM). The equation for SSIM is .. math:: :label: ssim 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)} where :math:`\mu_x` and :math:`\mu_y` are the sample means, :math:`\sigma_x` and :math:`\sigma_y` are the standard deviations, and :math:`\sigma_{xy}` is the correlation coefficient between images :math:`x` and :math:`y`. Once the SSIM map is computed, the border is trimmed by 5 pixels and the mean is returned. This version is slightly more efficient than the method proposed by Wang et. al. because it reduces the number of Gaussian blurs from 5 to 4. .. note:: The images are converted to grayscale before applying Gaussian blur. The grayscale conversion is equivalent to taking the Y channel in YIQ colour space. :return: mean SSIM :rtype: `float` """ # Compute the SSIM constants from the highest possible pixel value. const1 = (0.01 * self.maxval) ** 2 const_sum = const1 + (0.03 * self.maxval) ** 2 # Create the Gaussian blur mask. blur = self.vmask.VDMask(11, 1, 1.0, 0, _get_blurlist()) # Compute a mask for converting the image to grayscale. # Note that the result is equivalent to the Y channel of YIQ. rgb2gray = self.vmask.VDMask(3, 1, 1, 0, [0.299, 0.587, 0.114]) # Convert the image to grayscale using Matlab's approach. im1_g = self.im1.recomb(rgb2gray) im2_g = self.im2.recomb(rgb2gray) # Apply Gaussian blur to the grayscale images. im1_b = im1_g.convsep(blur) im2_b = im2_g.convsep(blur) # Compute the SSIM map. tmp1 = im1_g.multiply(im2_g).convsep(blur).lin(2, const_sum) tmp2 = im1_g.pow(2).add(im2_g.pow(2)).convsep(blur).lin(1, const_sum) tmp3 = im1_b.multiply(im2_b).lin(2, const1) tmp4 = im2_b.subtract(im1_b).pow(2).add(tmp3) tmp5 = tmp3.multiply(tmp1.subtract(tmp3)) ssim = tmp5.divide(tmp2.subtract(tmp4).multiply(tmp4)) # Crop the SSIM map and return the average. return ssim.extract_area(5, 5, im1_g.Xsize() - 10, im1_g.Ysize() - 10).avg()
[docs] def blur_1(self): """Compute MSSIM-inspired :math:`\ell_1` error. This method performs the same greyscale conversion, Gaussian blur, and cropping as MSSIM, but returns the :math:`\ell_1` error of the cropped image. See :eq:`l_1` for details on how the blurred images are compared. .. note:: The images are converted to grayscale before applying Gaussian blur. The grayscale conversion is equivalent to taking the Y channel in YIQ colour space. :return: MSSIM-inspired :math:`\ell_1` error :rtype: `float` """ # Create the Gaussian blur mask. blur = self.vmask.VDMask(11, 1, 1.0, 0, _get_blurlist()) # Compute a mask for converting the image to grayscale. # Note that the result is equivalent to the Y channel of YIQ. rgb2gray = self.vmask.VDMask(3, 1, 1, 0, [0.299, 0.587, 0.114]) # Convert the image to grayscale using Matlab's approach. im1_g = self.im1.recomb(rgb2gray) im2_g = self.im2.recomb(rgb2gray) # Apply Gaussian blur, crop the difference and return the l_1 error. diff = im1_g.convsep(blur).subtract(im2_g.convsep(blur)) crop = diff.extract_area(5, 5, im1_g.Xsize() - 10, im1_g.Ysize() - 10) return (crop.abs().avg() / self.maxval) * 100
[docs] def blur_2(self): """Compute MSSIM-inspired :math:`\ell_2` error. This method performs the same greyscale conversion, Gaussian blur, and cropping as MSSIM, but returns the :math:`\ell_2` error of the cropped image. See :eq:`l_2` for details on how the blurred images are compared. .. note:: The images are converted to grayscale before applying Gaussian blur. The grayscale conversion is equivalent to taking the Y channel in YIQ colour space. :return: MSSIM-inspired :math:`\ell_2` error :rtype: `float` """ # Create the Gaussian blur mask. blur = self.vmask.VDMask(11, 1, 1.0, 0, _get_blurlist()) # Compute a mask for converting the image to grayscale. # Note that the result is equivalent to the Y channel of YIQ. rgb2gray = self.vmask.VDMask(3, 1, 1, 0, [0.299, 0.587, 0.114]) # Convert the image to grayscale using Matlab's approach. im1_g = self.im1.recomb(rgb2gray) im2_g = self.im2.recomb(rgb2gray) # Apply Gaussian blur, crop the difference and return the l_2 error. diff = im1_g.convsep(blur).subtract(im2_g.convsep(blur)) crop = diff.extract_area(5, 5, im1_g.Xsize() - 10, im1_g.Ysize() - 10) return (crop.pow(2).avg() ** 0.5 / self.maxval) * 100
[docs] def blur_4(self): """Compute MSSIM-inspired :math:`\ell_4` error. This method performs the same greyscale conversion, Gaussian blur, and cropping as MSSIM, but returns the :math:`\ell_4` error of the cropped image. See :eq:`l_4` for details on how the blurred images are compared. .. note:: The images are converted to grayscale before applying Gaussian blur. The grayscale conversion is equivalent to taking the Y channel in YIQ colour space. :return: MSSIM-inspired :math:`\ell_4` error :rtype: `float` """ # Create the Gaussian blur mask. blur = self.vmask.VDMask(11, 1, 1.0, 0, _get_blurlist()) # Compute a mask for converting the image to grayscale. # Note that the result is equivalent to the Y channel of YIQ. rgb2gray = self.vmask.VDMask(3, 1, 1, 0, [0.299, 0.587, 0.114]) # Convert the image to grayscale using Matlab's approach. im1_g = self.im1.recomb(rgb2gray) im2_g = self.im2.recomb(rgb2gray) # Apply Gaussian blur, crop the difference and return the l_4 error. diff = im1_g.convsep(blur).subtract(im2_g.convsep(blur)) crop = diff.extract_area(5, 5, im1_g.Xsize() - 10, im1_g.Ysize() - 10) return (crop.pow(4).avg() ** 0.25 / self.maxval) * 100
[docs] def blur_inf(self): """Compute MSSIM-inspired :math:`\ell_\infty` error. This method performs the same greyscale conversion, Gaussian blur, and cropping as MSSIM, but returns the :math:`\ell_\infty` error of the cropped image. See :eq:`l_inf` for details on how the blurred images are compared. .. note:: The images are converted to grayscale before applying Gaussian blur. The grayscale conversion is equivalent to taking the Y channel in YIQ colour space. :return: MSSIM-inspired :math:`\ell_\infty` error :rtype: `float` """ # Create the Gaussian blur mask. blur = self.vmask.VDMask(11, 1, 1.0, 0, _get_blurlist()) # Compute a mask for converting the image to grayscale. # Note that the result is equivalent to the Y channel of YIQ. rgb2gray = self.vmask.VDMask(3, 1, 1, 0, [0.299, 0.587, 0.114]) # Convert the image to grayscale using Matlab's approach. im1_g = self.im1.recomb(rgb2gray) im2_g = self.im2.recomb(rgb2gray) # Apply Gaussian blur, crop the difference and return the l_inf error. diff = im1_g.convsep(blur).subtract(im2_g.convsep(blur)) crop = diff.extract_area(5, 5, im1_g.Xsize() - 10, im1_g.Ysize() - 10) return (crop.abs().max() / self.maxval) * 100
[docs] def cmc_1(self): """Compute :math:`\ell_1` error in Uniform Colour Space (UCS). This method imports the images into Lab colour space, then calculates delta-E CMC(1:1) and returns the average. See :eq:`l_1` for details on how the standard :math:`\ell_1` norm is computed. :return: :math:`\ell_1` error in Uniform Colour Space (UCS) :rtype: `float` """ lab1 = self.im1.icc_import(self.srgb_profile, self.intent) lab2 = self.im2.icc_import(self.srgb_profile, self.intent) return lab1.dECMC_fromLab(lab2).avg()
[docs] def cmc_2(self): """Compute :math:`\ell_2` error in Uniform Colour Space (UCS). This method imports the images into Lab colour space, then calculates delta-E CMC(1:1) and returns the :math:`\ell_2` norm. See :eq:`l_2` for details on how the standard :math:`\ell_2` norm is computed. :return: :math:`\ell_2` error in Uniform Colour Space (UCS) :rtype: `float` """ lab1 = self.im1.icc_import(self.srgb_profile, self.intent) lab2 = self.im2.icc_import(self.srgb_profile, self.intent) return lab1.dECMC_fromLab(lab2).pow(2).avg() ** 0.5
[docs] def cmc_4(self): """Compute :math:`\ell_4` error in Uniform Colour Space (UCS). This method imports the images into Lab colour space, then calculates delta-E CMC(1:1) and returns the :math:`\ell_4` norm. See :eq:`l_4` for details on how the standard :math:`\ell_4` norm is computed. :return: :math:`\ell_4` error in Uniform Colour Space (UCS) :rtype: `float` """ lab1 = self.im1.icc_import(self.srgb_profile, self.intent) lab2 = self.im2.icc_import(self.srgb_profile, self.intent) return lab1.dECMC_fromLab(lab2).pow(4).avg() ** 0.25
[docs] def cmc_inf(self): """Compute :math:`\ell_\infty` error in Uniform Colour Space (UCS). This method imports the images into Lab colour space, then calculates delta-E CMC(1:1) and returns the :math:`\ell_\infty` norm. See :eq:`l_inf` for details on how the standard :math:`\ell_\infty` norm is computed. :return: :math:`\ell_\infty` error in Uniform Colour Space (UCS) :rtype: `float` """ lab1 = self.im1.icc_import(self.srgb_profile, self.intent) lab2 = self.im2.icc_import(self.srgb_profile, self.intent) return lab1.dECMC_fromLab(lab2).max()
[docs] def xyz_1(self): """Compute :math:`\ell_1` error in XYZ Colour Space. This method imports the images into XYZ colour space, then calculates the :math:`\ell_1` error. See :eq:`l_1` for details on how the standard :math:`\ell_1` norm is computed. :return: :math:`\ell_1` error in XYZ Colour Space :rtype: `float` """ xyz1 = self.im1.icc_import(self.srgb_profile, self.intent).Lab2XYZ() xyz2 = self.im2.icc_import(self.srgb_profile, self.intent).Lab2XYZ() return xyz1.subtract(xyz2).abs().avg()
[docs] def xyz_2(self): """Compute :math:`\ell_2` error in XYZ Colour Space. This method imports the images into XYZ colour space, then calculates the :math:`\ell_2` error. See :eq:`l_2` for details on how the standard :math:`\ell_2` norm is computed. :return: :math:`\ell_2` error in XYZ Colour Space :rtype: `float` """ xyz1 = self.im1.icc_import(self.srgb_profile, self.intent).Lab2XYZ() xyz2 = self.im2.icc_import(self.srgb_profile, self.intent).Lab2XYZ() return xyz1.subtract(xyz2).pow(2).avg() ** 0.5
[docs] def xyz_4(self): """Compute :math:`\ell_4` error in XYZ Colour Space. This method imports the images into XYZ colour space, then calculates the :math:`\ell_4` error. See :eq:`l_4` for details on how the standard :math:`\ell_4` norm is computed. :return: :math:`\ell_4` error in XYZ Colour Space :rtype: `float` """ xyz1 = self.im1.icc_import(self.srgb_profile, self.intent).Lab2XYZ() xyz2 = self.im2.icc_import(self.srgb_profile, self.intent).Lab2XYZ() return xyz1.subtract(xyz2).pow(4).avg() ** 0.25
[docs] def xyz_inf(self): """Compute :math:`\ell_\infty` error in XYZ Colour Space. This method imports the images into XYZ colour space, then calculates the :math:`\ell_\infty` error. See :eq:`l_inf` for details on how the standard :math:`\ell_\infty` norm is computed. :return: :math:`\ell_\infty` error in XYZ Colour Space :rtype: `float` """ xyz1 = self.im1.icc_import(self.srgb_profile, self.intent).Lab2XYZ() xyz2 = self.im2.icc_import(self.srgb_profile, self.intent).Lab2XYZ() return xyz1.subtract(xyz2).abs().max()
[docs]def _get_blurlist(): """Private method to return a Gaussian blur mask. .. note:: This is a private function called by :meth:`~Metrics.blur_1`, :meth:`~Metrics.blur_2`, :meth:`~Metrics.blur_4`, :meth:`~Metrics.blur_inf`, and :meth:`~Metrics.mssim`. """ # Compute the raw Gaussian blur coefficients. blur_sigma = 1.5 blur_divisor = 2 * blur_sigma * blur_sigma rawblur1 = exp(-1 / blur_divisor) rawblur2 = exp(-4 / blur_divisor) rawblur3 = exp(-9 / blur_divisor) rawblur4 = exp(-16 / blur_divisor) rawblur5 = exp(-25 / blur_divisor) # Normalize the raw Gaussian blur coefficients. rawblursum = rawblur1 + rawblur2 + rawblur3 + rawblur4 + rawblur5 blur_normalizer = 2 * rawblursum + 1 blur0 = 1 / blur_normalizer blur1 = rawblur1 / blur_normalizer blur2 = rawblur2 / blur_normalizer blur3 = rawblur3 / blur_normalizer blur4 = rawblur4 / blur_normalizer blur5 = rawblur5 / blur_normalizer # Return the Gaussian blur mask as a list. return [blur5, blur4, blur3, blur2, blur1, blur0, blur1, blur2, blur3, blur4, blur5]
[docs]def main(): """Run :ref:`exquires-compare`.""" # Obtain a list of error metrics that can be called. metrics = [] methods = inspect.getmembers(Metrics, predicate=inspect.ismethod) for method in methods[1:]: metrics.append(method[0]) # Define the command-line argument parser. parser = parsing.ExquiresParser(description=__doc__) parser.add_argument('metric', type=str, metavar='METRIC', choices=metrics, help='the difference metric to use') parser.add_argument('image1', type=str, metavar='IMAGE_1', help='the first image to compare') parser.add_argument('image2', type=str, metavar='IMAGE_2', help='the second image to compare') parser.add_argument('-m', '--maxval', type=int, metavar='MAX_LEVEL', default=65535, help='the maximum pixel value (default: 65535)') # Attempt to parse the command-line arguments. args = parser.parse_args() # Attempt to call the chosen metric on the specified images. vipscc = __import__('vipsCC', globals(), locals(), ['VError'], -1) try: # Print the result with 15 digits after the decimal. metric = Metrics(args.image1, args.image2, args.maxval) print '%.15f' % getattr(metric, args.metric)() except vipscc.VError.VError, error: parser.error(str(error))
if __name__ == '__main__': main()