#!/usr/bin/python # # Copyright 2016 The TensorFlow Authors. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================== """Python implementation of MS-SSIM. Usage: python msssim.py --original_image=original.png --compared_image=distorted.png """ import numpy as np from scipy import signal from scipy.ndimage.filters import convolve import tensorflow as tf tf.flags.DEFINE_string('original_image', None, 'Path to PNG image.') tf.flags.DEFINE_string('compared_image', None, 'Path to PNG image.') FLAGS = tf.flags.FLAGS def _FSpecialGauss(size, sigma): """Function to mimic the 'fspecial' gaussian MATLAB function.""" radius = size // 2 offset = 0.0 start, stop = -radius, radius + 1 if size % 2 == 0: offset = 0.5 stop -= 1 x, y = np.mgrid[offset + start:stop, offset + start:stop] assert len(x) == size g = np.exp(-((x**2 + y**2)/(2.0 * sigma**2))) return g / g.sum() def _SSIMForMultiScale(img1, img2, max_val=255, filter_size=11, filter_sigma=1.5, k1=0.01, k2=0.03): """Return the Structural Similarity Map between `img1` and `img2`. This function attempts to match the functionality of ssim_index_new.m by Zhou Wang: http://www.cns.nyu.edu/~lcv/ssim/msssim.zip Arguments: img1: Numpy array holding the first RGB image batch. img2: Numpy array holding the second RGB image batch. max_val: the dynamic range of the images (i.e., the difference between the maximum the and minimum allowed values). filter_size: Size of blur kernel to use (will be reduced for small images). filter_sigma: Standard deviation for Gaussian blur kernel (will be reduced for small images). k1: Constant used to maintain stability in the SSIM calculation (0.01 in the original paper). k2: Constant used to maintain stability in the SSIM calculation (0.03 in the original paper). Returns: Pair containing the mean SSIM and contrast sensitivity between `img1` and `img2`. Raises: RuntimeError: If input images don't have the same shape or don't have four dimensions: [batch_size, height, width, depth]. """ if img1.shape != img2.shape: raise RuntimeError('Input images must have the same shape (%s vs. %s).', img1.shape, img2.shape) if img1.ndim != 4: raise RuntimeError('Input images must have four dimensions, not %d', img1.ndim) img1 = img1.astype(np.float64) img2 = img2.astype(np.float64) _, height, width, _ = img1.shape # Filter size can't be larger than height or width of images. size = min(filter_size, height, width) # Scale down sigma if a smaller filter size is used. sigma = size * filter_sigma / filter_size if filter_size else 0 if filter_size: window = np.reshape(_FSpecialGauss(size, sigma), (1, size, size, 1)) mu1 = signal.fftconvolve(img1, window, mode='valid') mu2 = signal.fftconvolve(img2, window, mode='valid') sigma11 = signal.fftconvolve(img1 * img1, window, mode='valid') sigma22 = signal.fftconvolve(img2 * img2, window, mode='valid') sigma12 = signal.fftconvolve(img1 * img2, window, mode='valid') else: # Empty blur kernel so no need to convolve. mu1, mu2 = img1, img2 sigma11 = img1 * img1 sigma22 = img2 * img2 sigma12 = img1 * img2 mu11 = mu1 * mu1 mu22 = mu2 * mu2 mu12 = mu1 * mu2 sigma11 -= mu11 sigma22 -= mu22 sigma12 -= mu12 # Calculate intermediate values used by both ssim and cs_map. c1 = (k1 * max_val) ** 2 c2 = (k2 * max_val) ** 2 v1 = 2.0 * sigma12 + c2 v2 = sigma11 + sigma22 + c2 ssim = np.mean((((2.0 * mu12 + c1) * v1) / ((mu11 + mu22 + c1) * v2))) cs = np.mean(v1 / v2) return ssim, cs def MultiScaleSSIM(img1, img2, max_val=255, filter_size=11, filter_sigma=1.5, k1=0.01, k2=0.03, weights=None): """Return the MS-SSIM score between `img1` and `img2`. This function implements Multi-Scale Structural Similarity (MS-SSIM) Image Quality Assessment according to Zhou Wang's paper, "Multi-scale structural similarity for image quality assessment" (2003). Link: https://ece.uwaterloo.ca/~z70wang/publications/msssim.pdf Author's MATLAB implementation: http://www.cns.nyu.edu/~lcv/ssim/msssim.zip Arguments: img1: Numpy array holding the first RGB image batch. img2: Numpy array holding the second RGB image batch. max_val: the dynamic range of the images (i.e., the difference between the maximum the and minimum allowed values). filter_size: Size of blur kernel to use (will be reduced for small images). filter_sigma: Standard deviation for Gaussian blur kernel (will be reduced for small images). k1: Constant used to maintain stability in the SSIM calculation (0.01 in the original paper). k2: Constant used to maintain stability in the SSIM calculation (0.03 in the original paper). weights: List of weights for each level; if none, use five levels and the weights from the original paper. Returns: MS-SSIM score between `img1` and `img2`. Raises: RuntimeError: If input images don't have the same shape or don't have four dimensions: [batch_size, height, width, depth]. """ if img1.shape != img2.shape: raise RuntimeError('Input images must have the same shape (%s vs. %s).', img1.shape, img2.shape) if img1.ndim != 4: raise RuntimeError('Input images must have four dimensions, not %d', img1.ndim) # Note: default weights don't sum to 1.0 but do match the paper / matlab code. weights = np.array(weights if weights else [0.0448, 0.2856, 0.3001, 0.2363, 0.1333]) levels = weights.size downsample_filter = np.ones((1, 2, 2, 1)) / 4.0 im1, im2 = [x.astype(np.float64) for x in [img1, img2]] mssim = np.array([]) mcs = np.array([]) for _ in range(levels): ssim, cs = _SSIMForMultiScale( im1, im2, max_val=max_val, filter_size=filter_size, filter_sigma=filter_sigma, k1=k1, k2=k2) mssim = np.append(mssim, ssim) mcs = np.append(mcs, cs) filtered = [convolve(im, downsample_filter, mode='reflect') for im in [im1, im2]] im1, im2 = [x[:, ::2, ::2, :] for x in filtered] return (np.prod(mcs[0:levels-1] ** weights[0:levels-1]) * (mssim[levels-1] ** weights[levels-1])) def main(_): if FLAGS.original_image is None or FLAGS.compared_image is None: print('\nUsage: python msssim.py --original_image=original.png ' '--compared_image=distorted.png\n\n') return if not tf.gfile.Exists(FLAGS.original_image): print('\nCannot find --original_image.\n') return if not tf.gfile.Exists(FLAGS.compared_image): print('\nCannot find --compared_image.\n') return with tf.gfile.FastGFile(FLAGS.original_image) as image_file: img1_str = image_file.read('rb') with tf.gfile.FastGFile(FLAGS.compared_image) as image_file: img2_str = image_file.read('rb') input_img = tf.placeholder(tf.string) decoded_image = tf.expand_dims(tf.image.decode_png(input_img, channels=3), 0) with tf.Session() as sess: img1 = sess.run(decoded_image, feed_dict={input_img: img1_str}) img2 = sess.run(decoded_image, feed_dict={input_img: img2_str}) print((MultiScaleSSIM(img1, img2, max_val=255))) if __name__ == '__main__': tf.app.run()