Source code for compimg.similarity

"""Module with routines for computing similarity between images"""
import abc
import numpy as np
import compimg

from compimg import kernels
from compimg.pads import EdgePad
from compimg._internals import _decorators, _utilities

# Kernel that is used in the SSIM implementation presented by the authors in
# "Image Quality Assessment: From Error Visibility to Structural Similarity"
# by Wang et al.
_SSIM_GAUSSIAN_KERNEL_11X11 = np.array([
    [1.0576e-06, 7.8144e-06, 3.7022e-05, 0.00011246, 0.00021905, 0.00027356,
     0.00021905, 0.00011246, 3.7022e-05, 7.8144e-06, 1.0576e-06],
    [7.8144e-06, 5.7741e-05, 0.00027356, 0.00083101, 0.0016186, 0.0020214,
     0.0016186, 0.00083101, 0.00027356, 5.7741e-05, 7.8144e-06],
    [3.7022e-05, 0.00027356, 0.0012961, 0.0039371, 0.0076684, 0.0095766,
     0.0076684, 0.0039371, 0.0012961, 0.00027356, 3.7022e-05],
    [0.00011246, 0.00083101, 0.0039371, 0.01196, 0.023294, 0.029091, 0.023294,
     0.01196, 0.0039371, 0.00083101, 0.00011246],
    [0.00021905, 0.0016186, 0.0076684, 0.023294, 0.045371, 0.056662, 0.045371,
     0.023294, 0.0076684, 0.0016186, 0.00021905],
    [0.00027356, 0.0020214, 0.0095766, 0.029091, 0.056662, 0.070762, 0.056662,
     0.029091, 0.0095766, 0.0020214, 0.00027356],
    [0.00021905, 0.0016186, 0.0076684, 0.023294, 0.045371, 0.056662, 0.045371,
     0.023294, 0.0076684, 0.0016186, 0.00021905],
    [0.00011246, 0.00083101, 0.0039371, 0.01196, 0.023294, 0.029091, 0.023294,
     0.01196, 0.0039371, 0.00083101, 0.00011246],
    [3.7022e-05, 0.00027356, 0.0012961, 0.0039371, 0.0076684, 0.0095766,
     0.0076684, 0.0039371, 0.0012961, 0.00027356, 3.7022e-05],
    [7.8144e-06, 5.7741e-05, 0.00027356, 0.00083101, 0.0016186, 0.0020214,
     0.0016186, 0.00083101, 0.00027356, 5.7741e-05, 7.8144e-06],
    [1.0576e-06, 7.8144e-06, 3.7022e-05, 0.00011246, 0.00021905, 0.00027356,
     0.00021905, 0.00011246, 3.7022e-05, 7.8144e-06, 1.0576e-06]])


[docs]class SimilarityMetric(abc.ABC): """ Abstract class for all similarity metrics. """
[docs] @abc.abstractmethod def compare(self, image: np.ndarray, reference: np.ndarray) -> float: """ Performs comparison. :param image: Image that is being compared. :param reference: Image that we compare to. :return: Numerical result of the comparison. """
[docs]class MSE(SimilarityMetric): """ Mean squared error. """
[docs] @_decorators._raise_when_arrays_have_different_dtypes @_decorators._raise_when_arrays_have_different_shapes def compare(self, image: np.ndarray, reference: np.ndarray) -> float: image = image.astype(compimg.config.intermediate_dtype, copy=False) reference = reference.astype(compimg.config.intermediate_dtype, copy=False) return np.mean(((reference - image) ** 2))
[docs]class RMSE(SimilarityMetric): """ Root mean squared error. """
[docs] @_decorators._raise_when_arrays_have_different_dtypes @_decorators._raise_when_arrays_have_different_shapes def compare(self, image: np.ndarray, reference: np.ndarray) -> float: image = image.astype(compimg.config.intermediate_dtype, copy=False) reference = reference.astype(compimg.config.intermediate_dtype, copy=False) return np.sqrt(MSE().compare(image, reference))
[docs]class MAE(SimilarityMetric): """ Mean absolute error. """
[docs] @_decorators._raise_when_arrays_have_different_dtypes @_decorators._raise_when_arrays_have_different_shapes def compare(self, image: np.ndarray, reference: np.ndarray) -> float: image = image.astype(compimg.config.intermediate_dtype, copy=False) reference = reference.astype(compimg.config.intermediate_dtype, copy=False) return np.mean(np.abs(reference - image))
[docs]class PSNR(SimilarityMetric): """ Peak signal-to-noise ratio according to https://en.wikipedia.org/wiki/Peak_signal-to-noise_ratio. """
[docs] @_decorators._raise_when_arrays_have_different_dtypes @_decorators._raise_when_arrays_have_different_shapes def compare(self, image: np.ndarray, reference: np.ndarray) -> float: image_original_dtype = image.dtype image = image.astype(compimg.config.intermediate_dtype, copy=False) reference = reference.astype(compimg.config.intermediate_dtype, copy=False) mse = MSE().compare(image, reference) if mse == 0.0: return float("inf") _, max_pixel_value = _utilities._get_image_dtype_range( image_original_dtype) psnr = 20 * np.log10(max_pixel_value) - 10 * np.log10(mse) return psnr
[docs]class SSIM(SimilarityMetric): """ Structural similarity index according to the paper from 2004 "Image Quality Assessment: From Error Visibility to Structural Similarity" by Wang et al. """ def __init__(self, k1: float = 0.01, k2: float = 0.03): self._k1 = k1 self._k2 = k2
[docs] @_decorators._raise_when_arrays_have_different_dtypes @_decorators._raise_when_arrays_have_different_shapes def compare(self, image: np.ndarray, reference: np.ndarray) -> float: # This implementations is based on # https://docs.opencv.org/2.4/doc/tutorials/highgui/video-input-psnr-ssim/video-input-psnr-ssim.html convolve = kernels._unobtrusive_convolve _, max_pixel_value = _utilities._get_image_dtype_range(image.dtype) C1 = (self._k1 * max_pixel_value) ** 2 C2 = (self._k2 * max_pixel_value) ** 2 image = image.astype(compimg.config.intermediate_dtype) reference = reference.astype(compimg.config.intermediate_dtype) x = image y = reference y_squared = reference * reference x_squared = image * image x_times_y = image * reference x_mean = convolve(x, _SSIM_GAUSSIAN_KERNEL_11X11) y_mean = convolve(y, _SSIM_GAUSSIAN_KERNEL_11X11) x_mean_squared = x_mean * x_mean y_mean_squared = y_mean * y_mean sigma_x_squared = convolve(x_squared, _SSIM_GAUSSIAN_KERNEL_11X11) sigma_x_squared -= x_mean_squared sigma_y_squared = convolve(y_squared, _SSIM_GAUSSIAN_KERNEL_11X11) sigma_y_squared -= y_mean_squared sigma_x_y = convolve(x_times_y, _SSIM_GAUSSIAN_KERNEL_11X11) sigma_x_y -= x_mean * y_mean t1 = 2 * x_mean * y_mean + C1 t2 = 2 * sigma_x_y + C2 t3 = t1 * t2 t1 = x_mean_squared + y_mean_squared + C1 t2 = sigma_x_squared + sigma_y_squared + C2 t1 = t1 * t2 ssim_map = t3 / t1 return np.mean(ssim_map)
[docs]class GSSIM(SimilarityMetric): """ Gradient-Based Structural similarity index according to the paper "GRADIENT-BASED STRUCTURAL SIMILARITY FOR IMAGE QUALITY ASSESSMENT" by Chen et al. """ def __init__(self, k1: float = 0.01, k2: float = 0.03): self._k1 = k1 self._k2 = k2
[docs] @_decorators._raise_when_arrays_have_different_dtypes @_decorators._raise_when_arrays_have_different_shapes def compare(self, image: np.ndarray, reference: np.ndarray) -> float: convolve = kernels._unobtrusive_convolve _, max_pixel_value = _utilities._get_image_dtype_range(image.dtype) C1 = (self._k1 * max_pixel_value) ** 2 C2 = (self._k2 * max_pixel_value) ** 2 C3 = C2 / 2.0 image = image.astype(compimg.config.intermediate_dtype) reference = reference.astype(compimg.config.intermediate_dtype) x = image y = reference x_mean = convolve(x, _SSIM_GAUSSIAN_KERNEL_11X11) y_mean = convolve(y, _SSIM_GAUSSIAN_KERNEL_11X11) x_mean_squared = x_mean * x_mean y_mean_squared = y_mean * y_mean sobel_image = self._apply_sobel(image) sobel_reference = self._apply_sobel(reference) # sx means sobel_x sx_squared = sobel_image * sobel_image sy_squared = sobel_reference * sobel_reference sxy = sobel_reference * sobel_image sx_mean = convolve(sobel_image, _SSIM_GAUSSIAN_KERNEL_11X11) sy_mean = convolve(sobel_reference, _SSIM_GAUSSIAN_KERNEL_11X11) sx_mean_squared = sx_mean * sx_mean sy_mean_squared = sy_mean * sy_mean sigma_sx_squared = convolve(sx_squared, _SSIM_GAUSSIAN_KERNEL_11X11) sigma_sx_squared -= sx_mean_squared sigma_sy_squared = convolve(sy_squared, _SSIM_GAUSSIAN_KERNEL_11X11) sigma_sy_squared -= sy_mean_squared sigma_sxy = convolve(sxy, _SSIM_GAUSSIAN_KERNEL_11X11) sigma_sxy -= sx_mean * sy_mean luminance = (2 * x_mean * y_mean + C1) / ( x_mean_squared + y_mean_squared + C1) contrast = (2 * np.sqrt(sigma_sx_squared) * np.sqrt( sigma_sy_squared) + C2) / ( sigma_sx_squared + sigma_sy_squared + C2) structure = (sigma_sxy + C3) / ( np.sqrt(sigma_sx_squared) * np.sqrt(sigma_sy_squared) + C3) return np.mean(luminance * contrast * structure)
def _apply_sobel(self, array: np.ndarray) -> np.ndarray: convolve = kernels._unobtrusive_convolve array = EdgePad(1).apply(array) array1 = convolve(array, kernels.HORIZONTAL_SOBEL_3x3) array2 = convolve(array, kernels.VERTICAL_SOBEL_3x3) return array1 + array2