You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
162 lines
6.4 KiB
Python
162 lines
6.4 KiB
Python
"""
|
|
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.')
|
|
|