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.
o3de/Tools/LyTestTools/ly_test_tools/image/screenshot_compare_qssim.py

158 lines
6.1 KiB
Python

"""
Copyright (c) Contributors to the Open 3D Engine Project.
For complete copyright and license terms please see the LICENSE at the root of this distribution.
SPDX-License-Identifier: Apache-2.0 OR MIT
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.')