""" All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or its licensors. For complete copyright and license terms please see the LICENSE at the root of this distribution (the "License"). All use of this software is governed by the License, or, if provided, by the license below or the license accompanying this file. Do not remove or modify any license notices. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Python implementation of Quaternion Structural Similarity by Amir Kolaman and Orly Yadid-Pecht as seen in IEEE Transaction on Image Processing Vol 21 No 4 April 2012. """ import imageio import numpy import os from scipy import ndimage def _quaternion_matrix_conj(q): q_out = numpy.zeros(q.shape) q_out[:, :, 0] = q[:, :, 0] q_out[:, :, 1] = -q[:, :, 1] q_out[:, :, 2] = -q[:, :, 2] q_out[:, :, 3] = -q[:, :, 3] return q_out def _quaternion_matrix_dot(q1, q2): return numpy.sqrt(numpy.sum(numpy.multiply(q1, q2), 2)) def _quaternion_matrix_norm(q): return _quaternion_matrix_dot(q, q) def _quaternion_matrix_mult(q1, q2): """q = (q1[0] * q2[0] - q1[1] * q2[1] - q1[2] * q2[2] - q1[3] * q2[3], q1[0] * q2[1] + q1[1] * q2[0] - q1[2] * q2[3] + q1[3] * q2[2], q1[0] * q2[2] + q1[1] * q2[3] + q1[2] * q2[0] - q1[3] * q2[1], q1[0] * q2[3] - q1[1] * q2[2] + q1[2] * q2[1] + q1[3] * q2[0])""" # add error checking for q1 and q2 being same size q = numpy.zeros(q1.shape) q[:, :, 0] = numpy.multiply(q1[:, :, 0], q2[:, :, 0]) - numpy.multiply(q1[:, :, 1], q2[:, :, 1]) \ - numpy.multiply(q1[:, :, 2], q2[:, :, 2]) - numpy.multiply(q1[:, :, 3], q2[:, :, 3]) q[:, :, 1] = numpy.multiply(q1[:, :, 0], q2[:, :, 1]) + numpy.multiply(q1[:, :, 1], q2[:, :, 0]) \ - numpy.multiply(q1[:, :, 2], q2[:, :, 3]) + numpy.multiply(q1[:, :, 3], q2[:, :, 2]) q[:, :, 2] = numpy.multiply(q1[:, :, 0], q2[:, :, 2]) + numpy.multiply(q1[:, :, 1], q2[:, :, 3]) \ + numpy.multiply(q1[:, :, 2], q2[:, :, 0]) - numpy.multiply(q1[:, :, 3], q2[:, :, 1]) q[:, :, 3] = numpy.multiply(q1[:, :, 0], q2[:, :, 3]) - numpy.multiply(q1[:, :, 1], q2[:, :, 2]) \ + numpy.multiply(q1[:, :, 2], q2[:, :, 1]) + numpy.multiply(q1[:, :, 3], q2[:, :, 0]) return q def _quaternion_matrix_div(q1, q2): q2_norm = _quaternion_matrix_norm(q2) q = _quaternion_matrix_mult(q1, _quaternion_matrix_conj(q2)) return numpy.divide(q, numpy.dstack([q2_norm] * 4)) def qssim(screenshot, goldenimage, channel_max=255, diff_path='.'): """ Returns the mean quaternion similarity index between two images. For images that are the same the expected result is 1.000. By default we are assuming a channel max of 255(8bit channels) There are a series of tuning parameters that are taken from the 2004 paper by Wang et al Image Quality Assesment: From Error Visibility to Structural Similarity. :param screenshot: Screenshot filename to test :param goldenimage: Golden image to test against. :param channel_max: Maximum channel value. :param diff_path: Target path where diff image should be stored. :return: Mean quaternion similarity from 0.00->1.00 (identical). """ # load image and copy it into a 4 channel array to treat rgb like quaternions img1 = imageio.imread(screenshot) img2 = imageio.imread(goldenimage) # Avoid later precision issues img1 = img1.astype(numpy.float64) / channel_max img2 = img2.astype(numpy.float64) / channel_max im1_size = img1.shape im2_size = img2.shape # check im1 and im2 are same size hue1 = numpy.zeros((im1_size[0], im1_size[1], im1_size[2] + 1)) hue2 = numpy.zeros((im1_size[0], im1_size[1], im1_size[2] + 1)) mu1 = numpy.zeros((im1_size[0], im1_size[1], im1_size[2] + 1)) mu2 = numpy.zeros((im1_size[0], im1_size[1], im1_size[2] + 1)) offset1 = numpy.zeros((im1_size[0], im1_size[1], im1_size[2] + 1)) offset2 = numpy.zeros((im1_size[0], im1_size[1], im1_size[2] + 1)) hue1[:, :, 1:4] = img1 hue2[:, :, 1:4] = img2 mu1[:, :, 1:4] = img1 mu2[:, :, 1:4] = img2 # Algorithm tuning parameters. Can me modified as needed. sigma = 1.5 # These parameters are just to prevent divide by zero issues. L = 1 K1 = 0.01 K2 = 0.03 C1 = (K1 * L) ** 2 C2 = (K2 * L) ** 2 offset1[:, :, 0] = C1 offset2[:, :, 0] = C2 # blur each color channel of both images mu1 = ndimage.filters.gaussian_filter1d(mu1, sigma, 0) mu1 = ndimage.filters.gaussian_filter1d(mu1, sigma, 1) mu2 = ndimage.filters.gaussian_filter1d(mu2, sigma, 0) mu2 = ndimage.filters.gaussian_filter1d(mu2, sigma, 1) mu1_sq = _quaternion_matrix_mult(mu1, _quaternion_matrix_conj(mu1)) mu2_sq = _quaternion_matrix_mult(mu2, _quaternion_matrix_conj(mu2)) mu12 = _quaternion_matrix_mult(mu1, _quaternion_matrix_conj(mu2)) hue1_sq = _quaternion_matrix_mult(hue1 - mu1, _quaternion_matrix_conj(hue1 - mu1)) hue2_sq = _quaternion_matrix_mult(hue2 - mu2, _quaternion_matrix_conj(hue2 - mu2)) hue12 = _quaternion_matrix_mult(hue1 - mu1, _quaternion_matrix_conj(hue2 - mu2)) sigma1 = ndimage.filters.gaussian_filter1d(hue1_sq, sigma, 0) sigma1 = ndimage.filters.gaussian_filter1d(sigma1, sigma, 1) sigma2 = ndimage.filters.gaussian_filter1d(hue2_sq, sigma, 0) sigma2 = ndimage.filters.gaussian_filter1d(sigma2, sigma, 1) sigma12 = ndimage.filters.gaussian_filter1d(hue12, sigma, 0) sigma12 = ndimage.filters.gaussian_filter1d(sigma12, sigma, 1) numerator1 = 2 * mu12 + offset1 numerator2 = 2 * sigma12 + offset2 denominator1 = mu1_sq + mu2_sq + offset1 denominator2 = sigma1 + sigma2 + offset2 part1 = _quaternion_matrix_norm(numerator1) / denominator1[:, :, 0] part2 = _quaternion_matrix_norm(numerator2) / denominator2[:, :, 0] qssim_map = numpy.multiply(part1, part2) extension = os.path.splitext(screenshot)[1] screenshot_name = os.path.basename(screenshot) diff_name = '.'.join(screenshot_name.split('.')[:-1]) + "_diff" + extension diff_full_path = os.path.join(diff_path, diff_name) imageio.imwrite(diff_full_path, (qssim_map * channel_max).astype(numpy.uint8)) return ndimage.mean(numpy.abs(qssim_map)) if __name__ == "__main__": """ If this is run by accident, inform user that this is a module, not a separate script. """ print('qssim.py is not a standalone script.')