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

157 lines
6.0 KiB
Python

"""
Copyright (c) Contributors to the Open 3D Engine Project
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.')