diff --git a/backend/aim/metrics/m20/m20_color_harmony.py b/backend/aim/metrics/m20/m20_color_harmony.py index da3b521..710ddeb 100644 --- a/backend/aim/metrics/m20/m20_color_harmony.py +++ b/backend/aim/metrics/m20/m20_color_harmony.py @@ -22,7 +22,7 @@ References: - 1. Cohen-Or, D., Sorkine, O., Gal, R., Leyvand, T. and Xu, Y.Q. (2006). + 1. Cohen-Or, D., Sorkine, O., Gal, R., Leyvand, T., and Xu, Y.Q. (2006). Color Harmonization. ACM Transactions on Graphics, 25(3), 624-630. doi: https://doi.org/10.1145/1141911.1141933 diff --git a/legacy/aim_metrics/aim_metrics/accessibility/__init__.py b/backend/aim/metrics/m23/__init__.py similarity index 100% rename from legacy/aim_metrics/aim_metrics/accessibility/__init__.py rename to backend/aim/metrics/m23/__init__.py diff --git a/backend/aim/metrics/m23/m23_color_blindness.py b/backend/aim/metrics/m23/m23_color_blindness.py new file mode 100644 index 0000000..bb9ccdf --- /dev/null +++ b/backend/aim/metrics/m23/m23_color_blindness.py @@ -0,0 +1,536 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Metric: + Color blindness + + +Description: + A physiologically-based model for simulation of color vision deficiency. + + Notice: "Since there are no strong biological explanations yet to justify + the causes of tritanopia and tritanomaly, we simulate tritanomaly based on + the shift paradigm only (Eq. 19) as an approximation to the actual + phenomenon and restrain our model from trying to model tritanopia." + + +Source: + The code is imported and adopted from + https://github.com/DaltonLens/DaltonLens-Python. + + +Funding information and contact: + This work was funded by Technology Industries of Finland in a three-year + project grant on self-optimizing web services. The principal investigator + is Antti Oulasvirta (antti.oulasvirta@aalto.fi) of Aalto University. + + +References: + 1. Machado, G.M., Oliveira, M.M., and Fernandes, L.A.F. (2009). + A Physiologically-based Model for Simulation of Color Vision + Deficiency. IEEE Transactions on Visualization and Computer Graphics, + 15(6), 1291-1298. doi: https://doi.org/10.1109/TVCG.2009.113 + + +Change log: + v1.0 (2022-10-21) + * Initial implementation +""" + + +# ---------------------------------------------------------------------------- +# Imports +# ---------------------------------------------------------------------------- + +# Standard library modules +import base64 +import math +from io import BytesIO +from typing import Dict, List, Optional, Union + +# Third-party modules +import numpy as np +from PIL import Image +from pydantic import HttpUrl + +# First-party modules +from aim.common import image_utils +from aim.common.constants import GUI_TYPE_DESKTOP +from aim.metrics.interfaces import AIMMetricInterface + +# ---------------------------------------------------------------------------- +# Metadata +# ---------------------------------------------------------------------------- + +__author__ = "Amir Hossein Kargaran, Markku Laine" +__date__ = "2022-10-21" +__email__ = "markku.laine@aalto.fi" +__version__ = "1.0" + + +# ---------------------------------------------------------------------------- +# Metric +# ---------------------------------------------------------------------------- + + +class Metric(AIMMetricInterface): + """ + Metric: Color blindness. + """ + + # Private constants + # From https://www.inf.ufrgs.br/~oliveira/pubs_files/CVD_Simulation/CVD_Simulation.html#Tutorial + # Converted to a NumPy array using https://github.com/colour-science/colour/blob/develop/colour/blindness/datasets/machado2010.py + # The severity range is [0.0, 1.0] with a step of 0.1, but here the index is multiplied by 10 to make it an integer. + _DEFAULT_SEVERITY: Dict[str, float] = { + "protan": 1.0, + "deutan": 1.0, + "tritan": 1.0, + } # severity should be between 0.0 and 1.0 + _MACHADO_2009_MATRICES: Dict[str, Dict[int, np.ndarray]] = { + "protan": { + 0: np.array( + [ + [1.000000, 0.000000, -0.000000], + [0.000000, 1.000000, 0.000000], + [-0.000000, -0.000000, 1.000000], + ] + ), + 1: np.array( + [ + [0.856167, 0.182038, -0.038205], + [0.029342, 0.955115, 0.015544], + [-0.002880, -0.001563, 1.004443], + ] + ), + 2: np.array( + [ + [0.734766, 0.334872, -0.069637], + [0.051840, 0.919198, 0.028963], + [-0.004928, -0.004209, 1.009137], + ] + ), + 3: np.array( + [ + [0.630323, 0.465641, -0.095964], + [0.069181, 0.890046, 0.040773], + [-0.006308, -0.007724, 1.014032], + ] + ), + 4: np.array( + [ + [0.539009, 0.579343, -0.118352], + [0.082546, 0.866121, 0.051332], + [-0.007136, -0.011959, 1.019095], + ] + ), + 5: np.array( + [ + [0.458064, 0.679578, -0.137642], + [0.092785, 0.846313, 0.060902], + [-0.007494, -0.016807, 1.024301], + ] + ), + 6: np.array( + [ + [0.385450, 0.769005, -0.154455], + [0.100526, 0.829802, 0.069673], + [-0.007442, -0.022190, 1.029632], + ] + ), + 7: np.array( + [ + [0.319627, 0.849633, -0.169261], + [0.106241, 0.815969, 0.077790], + [-0.007025, -0.028051, 1.035076], + ] + ), + 8: np.array( + [ + [0.259411, 0.923008, -0.182420], + [0.110296, 0.804340, 0.085364], + [-0.006276, -0.034346, 1.040622], + ] + ), + 9: np.array( + [ + [0.203876, 0.990338, -0.194214], + [0.112975, 0.794542, 0.092483], + [-0.005222, -0.041043, 1.046265], + ] + ), + 10: np.array( + [ + [0.152286, 1.052583, -0.204868], + [0.114503, 0.786281, 0.099216], + [-0.003882, -0.048116, 1.051998], + ] + ), + }, + "deutan": { + 0: np.array( + [ + [1.000000, 0.000000, -0.000000], + [0.000000, 1.000000, 0.000000], + [-0.000000, -0.000000, 1.000000], + ] + ), + 1: np.array( + [ + [0.866435, 0.177704, -0.044139], + [0.049567, 0.939063, 0.011370], + [-0.003453, 0.007233, 0.996220], + ] + ), + 2: np.array( + [ + [0.760729, 0.319078, -0.079807], + [0.090568, 0.889315, 0.020117], + [-0.006027, 0.013325, 0.992702], + ] + ), + 3: np.array( + [ + [0.675425, 0.433850, -0.109275], + [0.125303, 0.847755, 0.026942], + [-0.007950, 0.018572, 0.989378], + ] + ), + 4: np.array( + [ + [0.605511, 0.528560, -0.134071], + [0.155318, 0.812366, 0.032316], + [-0.009376, 0.023176, 0.986200], + ] + ), + 5: np.array( + [ + [0.547494, 0.607765, -0.155259], + [0.181692, 0.781742, 0.036566], + [-0.010410, 0.027275, 0.983136], + ] + ), + 6: np.array( + [ + [0.498864, 0.674741, -0.173604], + [0.205199, 0.754872, 0.039929], + [-0.011131, 0.030969, 0.980162], + ] + ), + 7: np.array( + [ + [0.457771, 0.731899, -0.189670], + [0.226409, 0.731012, 0.042579], + [-0.011595, 0.034333, 0.977261], + ] + ), + 8: np.array( + [ + [0.422823, 0.781057, -0.203881], + [0.245752, 0.709602, 0.044646], + [-0.011843, 0.037423, 0.974421], + ] + ), + 9: np.array( + [ + [0.392952, 0.823610, -0.216562], + [0.263559, 0.690210, 0.046232], + [-0.011910, 0.040281, 0.971630], + ] + ), + 10: np.array( + [ + [0.367322, 0.860646, -0.227968], + [0.280085, 0.672501, 0.047413], + [-0.011820, 0.042940, 0.968881], + ] + ), + }, + "tritan": { + 0: np.array( + [ + [1.000000, 0.000000, -0.000000], + [0.000000, 1.000000, 0.000000], + [-0.000000, -0.000000, 1.000000], + ] + ), + 1: np.array( + [ + [0.926670, 0.092514, -0.019184], + [0.021191, 0.964503, 0.014306], + [0.008437, 0.054813, 0.936750], + ] + ), + 2: np.array( + [ + [0.895720, 0.133330, -0.029050], + [0.029997, 0.945400, 0.024603], + [0.013027, 0.104707, 0.882266], + ] + ), + 3: np.array( + [ + [0.905871, 0.127791, -0.033662], + [0.026856, 0.941251, 0.031893], + [0.013410, 0.148296, 0.838294], + ] + ), + 4: np.array( + [ + [0.948035, 0.089490, -0.037526], + [0.014364, 0.946792, 0.038844], + [0.010853, 0.193991, 0.795156], + ] + ), + 5: np.array( + [ + [1.017277, 0.027029, -0.044306], + [-0.006113, 0.958479, 0.047634], + [0.006379, 0.248708, 0.744913], + ] + ), + 6: np.array( + [ + [1.104996, -0.046633, -0.058363], + [-0.032137, 0.971635, 0.060503], + [0.001336, 0.317922, 0.680742], + ] + ), + 7: np.array( + [ + [1.193214, -0.109812, -0.083402], + [-0.058496, 0.979410, 0.079086], + [-0.002346, 0.403492, 0.598854], + ] + ), + 8: np.array( + [ + [1.257728, -0.139648, -0.118081], + [-0.078003, 0.975409, 0.102594], + [-0.003316, 0.501214, 0.502102], + ] + ), + 9: np.array( + [ + [1.278864, -0.125333, -0.153531], + [-0.084748, 0.957674, 0.127074], + [-0.000989, 0.601151, 0.399838], + ] + ), + 10: np.array( + [ + [1.255528, -0.076749, -0.178779], + [-0.078411, 0.930809, 0.147602], + [0.004733, 0.691367, 0.303900], + ] + ), + }, + } + + # Private methods + @staticmethod + def _linearRGB_from_sRGB(im: np.ndarray): + """ + Convert sRGB to linearRGB, removing the gamma correction. + Formula taken from Wikipedia https://en.wikipedia.org/wiki/SRGB + + Args: + im : The input sRGB image, normalized between [0, 1] + + Returns: + The output RGB image, array of shape (M, N, 3) with dtype float + """ + out: np.ndarray = np.zeros_like(im) + small_mask: np.ndarray = im < 0.04045 + large_mask: np.ndarray = np.logical_not(small_mask) + out[small_mask] = im[small_mask] / 12.92 + out[large_mask] = np.power((im[large_mask] + 0.055) / 1.055, 2.4) + return out + + @staticmethod + def _sRGB_from_linearRGB(im: np.ndarray): + """ + Convert linearRGB to sRGB, applying the gamma correction. + Formula taken from Wikipedia https://en.wikipedia.org/wiki/SRGB + + Args: + im : The input RGB image, normalized between [0, 1] + + Returns: + The output sRGB image, array of shape (M, N, 3) with dtype float + """ + out: np.ndarray = np.zeros_like(im) + # Make sure we're in range, otherwise gamma will go crazy. + im_clipped: np.ndarray = np.clip(im, 0.0, 1.0) + small_mask: np.ndarray = im_clipped < 0.0031308 + large_mask: np.ndarray = np.logical_not(small_mask) + out[small_mask] = im_clipped[small_mask] * 12.92 + out[large_mask] = ( + np.power(im_clipped[large_mask], 1.0 / 2.4) * 1.055 - 0.055 + ) + return out + + @staticmethod + def _as_float32(im: np.ndarray): + """ + Divide by 255 and cast the uint8 image to float32 + """ + return im.astype(np.float32) / 255.0 + + @staticmethod + def _as_uint8(im: np.ndarray): + """ + Multiply by 255 and cast the float image to uint8 + """ + return (np.clip(im, 0.0, 1.0) * 255.0).astype(np.uint8) + + @staticmethod + def _apply_color_matrix(im: np.ndarray, m: np.ndarray): + """ + Transform a color array with the given 3x3 matrix. + + Args: + im: Input image with array of shape (..., 3) + m: Color matrix to apply with array of shape (3, 3) + + Returns: + Output array with shape of (..., 3), where each input color vector was multiplied by m + """ + # Another option is np.einsum('ij, ...j', m, im), but it can be much + # slower, especially on float32 types because the matrix + # multiplication is heavily optimized. + # So the matmul is generally (much) faster, but we need to take the + # transpose of m as it gets applied on the right side. Indeed, for + # each column color vector v we wanted $v' = m . v$ . To flip the + # side we can use $m . v = (v^T . m^T)^T$ . The transposes on the 1d + # vector are implicit and can be ignored, so we just need to compute + # $v . m^T$. This is what numpy matmul will do for all the vectors + # thanks to its broadcasting rules that pick the last 2 dimensions of + # each array, so it will actually compute matrix multiplications of + # shape (M, 3) x (3, 3) with M the penultimate dimension of m. That + # will write a matrix of shape (M,3) with each row storing the result + # of $v' = v . M^T$. + return im @ m.T + + @classmethod + def _simulate_cvd_linear_rgb( + cls, + image_linear_rgb_float32: np.ndarray, + deficiency: str, + severity: float, + ): + """ + Simulate the appearance of a linear rgb image for the given color + vision deficiency. + + Args: + image_linear_rgb_float32: The input linear RGB image (np.ndarray), with values in [0, 1] + deficiency: The deficiency (str) to simulate + severity: The severity (float) between 0 (normal vision) and 1 (complete dichromacy) + + Returns: + The simulated rgb image (np.ndarray) with values in [0, 1] + """ + severity_lower: int = int(math.floor(severity * 10.0)) + severity_higher: int = min(severity_lower + 1, 10) + m1: np.ndarray = cls._MACHADO_2009_MATRICES[deficiency][severity_lower] + m2: np.ndarray = cls._MACHADO_2009_MATRICES[deficiency][ + severity_higher + ] + + # alpha = 0 => only m1, alpha = 1.0 => only m2 + alpha: float = severity - severity_lower / 10.0 + m: np.ndarray = alpha * m2 + (1.0 - alpha) * m1 + + return cls._apply_color_matrix(image_linear_rgb_float32, m) + + @classmethod + def _simulate_cvd( + cls, img_rgb: Image.Image, deficiency: str, severity: float + ): + """ + Simulate the appearance of an image for the given color vision deficiency. + + Args: + img_rgb: The input RGB image (Image.Image), with values in [0, 255] + deficiency: The deficiency (str) to simulate + severity: The severity (float) between 0 (normal vision) and 1 (complete dichromacy) + + Returns: + The simulated sRGB image (Image.Image) with values in [0, 255] + """ + # Get NumPy linear rgb image of input image + img_srgb_norm: np.ndarray = cls._as_float32(np.array(img_rgb)) + im_linear_rgb: np.ndarray = cls._linearRGB_from_sRGB(img_srgb_norm) + + # Compute simulated image + im_cvd_linear_rgb: np.ndarray = cls._simulate_cvd_linear_rgb( + im_linear_rgb, deficiency, severity + ) + im_cvd_float: np.ndarray = cls._sRGB_from_linearRGB(im_cvd_linear_rgb) + + # Convert NumPy array to Image.Image format + im_cvd_unit8: np.ndarray = cls._as_uint8(im_cvd_float) + img_cvd_srgb: Image.Image = Image.fromarray(im_cvd_unit8, mode="RGB") + return img_cvd_srgb + + # Public methods + @classmethod + def execute_metric( + cls, + gui_image: str, + gui_type: int = GUI_TYPE_DESKTOP, + gui_url: Optional[HttpUrl] = None, + ) -> Optional[List[Union[int, float, str]]]: + """ + Execute the metric. + + Args: + gui_image: GUI image (PNG) encoded in Base64 + + Kwargs: + gui_type: GUI type, desktop = 0 (default), mobile = 1 + gui_url: GUI URL (defaults to None) + + Returns: + Results (list of measures) + - Protanopia (str, image (PNG) encoded in Base64) + - Deuteranopia (str, image (PNG) encoded in Base64) + - Tritanopia (str, image (PNG) encoded in Base64) + """ + # Create PIL image + img: Image.Image = Image.open(BytesIO(base64.b64decode(gui_image))) + + # Convert image from ??? (e.g., RGBA) to RGB color space + img_rgb: Image.Image = img.convert("RGB") + + # Compute protanope + # Definition: A protan or a protanope is a person suffering from + # protanopia ("strong" protan) or protanomaly ("mild" protan). + protan_im: Image.Image = cls._simulate_cvd( + img_rgb, "protan", cls._DEFAULT_SEVERITY["protan"] + ) + protan_b64: str = image_utils.to_png_image_base64(protan_im) + + # Compute deuteranope + # Definition: A deutan or a deuteranope is a person suffering from + # deuteranopia ("strong" deutan) or deuteranomaly ("mild" deutan). + deutan_im: Image.Image = cls._simulate_cvd( + img_rgb, "deutan", cls._DEFAULT_SEVERITY["deutan"] + ) + deutan_b64: str = image_utils.to_png_image_base64(deutan_im) + + # Compute tritanope + # Definition: A tritan or a tritanope is a person suffering from + # tritanopia ("strong" tritan) or tritanomaly ("mild" tritan). + tritan_im: Image.Image = cls._simulate_cvd( + img_rgb, "tritan", cls._DEFAULT_SEVERITY["tritan"] + ) + tritan_b64: str = image_utils.to_png_image_base64(tritan_im) + + return [ + protan_b64, + deutan_b64, + tritan_b64, + ] diff --git a/backend/data/evaluations/ALEXA_500/quantiles.csv b/backend/data/evaluations/ALEXA_500/quantiles.csv index 0e41c64..c58ee68 100644 --- a/backend/data/evaluations/ALEXA_500/quantiles.csv +++ b/backend/data/evaluations/ALEXA_500/quantiles.csv @@ -1,4 +1,4 @@ -quantile,total_evaluation_time,read_image_time,m1_time,m1_0,m2_time,m2_0,m3_time,m3_0,m4_time,m4_0,m5_time,m5_0,m6_time,m6_0,m7_time,m7_0,m8_time,m8_0,m9_time,m10_time,m10_0,m11_time,m11_0,m12_time,m12_0,m13_time,m13_0,m14_time,m14_0,m14_1,m14_2,m14_3,m14_4,m14_5,m15_time,m15_0,m16_time,m16_0,m16_1,m16_2,m16_3,m16_4,m17_time,m17_0,m17_1,m17_2,m18_time,m18_0,m18_1,m19_time,m19_0,m20_time,m20_0 -0.25,268.2464,0.0099,0.0001,355148.5,0.0483,84198.5,0.0612,2713.5,0.0388,0.0296,0.0519,0.5786,12.5198,0.6033,16.9822,2.7153,22.319,3.771,8.4746,1.9521,0.5009,0.0908,809.5,4.318,132.5,0.0673,56.4449,0.2524,61.1937,21.203,0.1141,5.3333,-2.4527,8.4718,0.0758,26.0989,4.6302,17.4804,0.0738,0.1652,0.6448,0.1903,0.0724,29.5,34.5,47.5,0.199,4.728,1.7074,4.3362,12.2146,184.3299,259.2499 -0.5,289.8546,0.0112,0.0003,595978.0,0.0589,106498.0,0.0877,5569.0,0.0444,0.0398,0.0587,0.6387,12.8551,0.656,17.1648,3.1693,22.4567,4.6523,10.8732,2.165,0.5376,0.1865,1743.0,12.8617,259.0,0.0723,73.5964,0.2658,76.2311,27.7922,1.3217,8.0883,0.2638,12.2652,0.0822,38.7664,4.6928,248.575,0.1522,0.2207,0.8071,0.2527,0.0802,51.0,78.0,93.0,0.2077,5.2904,1.7875,12.5858,15.4308,189.9121,1149.9245 -0.75,323.4018,0.0132,0.0004,855812.0,0.0716,129990.0,0.1157,9764.0,0.0513,0.0495,0.0677,0.6856,13.1804,0.7113,17.383,3.4694,22.7282,5.5114,12.8472,2.4576,0.6127,0.2976,2551.0,29.6176,444.0,0.0782,85.9417,0.2804,85.946,32.749,3.7988,12.1717,2.569,17.3738,0.0903,54.1464,4.7937,352.5484,0.2469,0.2778,0.8906,0.3148,0.0899,76.5,131.5,163.5,0.2203,5.8194,1.8684,28.9288,18.5564,193.4328,3113.7351 +quantile,total_evaluation_time,read_image_time,m1_time,m1_0,m2_time,m2_0,m3_time,m3_0,m4_time,m4_0,m5_time,m5_0,m6_time,m6_0,m7_time,m7_0,m8_time,m8_0,m9_time,m10_time,m10_0,m11_time,m11_0,m12_time,m12_0,m13_time,m13_0,m14_time,m14_0,m14_1,m14_2,m14_3,m14_4,m14_5,m15_time,m15_0,m16_time,m16_0,m16_1,m16_2,m16_3,m16_4,m17_time,m17_0,m17_1,m17_2,m18_time,m18_0,m18_1,m19_time,m19_0,m20_time,m20_0,m23_time +0.25,269.3076,0.0099,0.0001,355148.5,0.0483,84198.5,0.0612,2713.5,0.0388,0.0296,0.0519,0.5786,12.5198,0.6033,16.9822,2.7153,22.319,3.771,8.4746,1.9521,0.5009,0.0908,809.5,4.318,132.5,0.0673,56.4449,0.2524,61.1937,21.203,0.1141,5.3333,-2.4527,8.4718,0.0758,26.0989,4.6302,17.4804,0.0738,0.1652,0.6448,0.1903,0.0724,29.5,34.5,47.5,0.199,4.728,1.7074,4.3362,12.2146,184.3299,259.2499,1.0612 +0.5,291.0994,0.0112,0.0003,595978.0,0.0589,106498.0,0.0877,5569.0,0.0444,0.0398,0.0587,0.6387,12.8551,0.656,17.1648,3.1693,22.4567,4.6523,10.8732,2.165,0.5376,0.1865,1743.0,12.8617,259.0,0.0723,73.5964,0.2658,76.2311,27.7922,1.3217,8.0883,0.2638,12.2652,0.0822,38.7664,4.6928,248.575,0.1522,0.2207,0.8071,0.2527,0.0802,51.0,78.0,93.0,0.2077,5.2904,1.7875,12.5858,15.4308,189.9121,1149.9245,1.2448 +0.75,324.8790,0.0132,0.0004,855812.0,0.0716,129990.0,0.1157,9764.0,0.0513,0.0495,0.0677,0.6856,13.1804,0.7113,17.383,3.4694,22.7282,5.5114,12.8472,2.4576,0.6127,0.2976,2551.0,29.6176,444.0,0.0782,85.9417,0.2804,85.946,32.749,3.7988,12.1717,2.569,17.3738,0.0903,54.1464,4.7937,352.5484,0.2469,0.2778,0.8906,0.3148,0.0899,76.5,131.5,163.5,0.2203,5.8194,1.8684,28.9288,18.5564,193.4328,3113.7351,1.4772 diff --git a/backend/data/tests/expected_results/m20_1_aim.png b/backend/data/tests/expected_results/m20_1_aim_bad_harmony.png similarity index 100% rename from backend/data/tests/expected_results/m20_1_aim.png rename to backend/data/tests/expected_results/m20_1_aim_bad_harmony.png diff --git a/backend/data/tests/expected_results/m20_2_aim.png b/backend/data/tests/expected_results/m20_2_aim_bad_harmony.png similarity index 100% rename from backend/data/tests/expected_results/m20_2_aim.png rename to backend/data/tests/expected_results/m20_2_aim_bad_harmony.png diff --git a/backend/data/tests/expected_results/m23_0_colored_crayons.png b/backend/data/tests/expected_results/m23_0_colored_crayons.png new file mode 100644 index 0000000..593d72a Binary files /dev/null and b/backend/data/tests/expected_results/m23_0_colored_crayons.png differ diff --git a/backend/data/tests/expected_results/m23_0_myhelsinki.fi_website.png b/backend/data/tests/expected_results/m23_0_myhelsinki.fi_website.png new file mode 100644 index 0000000..c4e0115 Binary files /dev/null and b/backend/data/tests/expected_results/m23_0_myhelsinki.fi_website.png differ diff --git a/backend/data/tests/expected_results/m23_0_rgbspan.png b/backend/data/tests/expected_results/m23_0_rgbspan.png new file mode 100644 index 0000000..3b774dd Binary files /dev/null and b/backend/data/tests/expected_results/m23_0_rgbspan.png differ diff --git a/backend/data/tests/expected_results/m23_1_colored_crayons.png b/backend/data/tests/expected_results/m23_1_colored_crayons.png new file mode 100644 index 0000000..f7ba27d Binary files /dev/null and b/backend/data/tests/expected_results/m23_1_colored_crayons.png differ diff --git a/backend/data/tests/expected_results/m23_1_myhelsinki.fi_website.png b/backend/data/tests/expected_results/m23_1_myhelsinki.fi_website.png new file mode 100644 index 0000000..1306b92 Binary files /dev/null and b/backend/data/tests/expected_results/m23_1_myhelsinki.fi_website.png differ diff --git a/backend/data/tests/expected_results/m23_1_rgbspan.png b/backend/data/tests/expected_results/m23_1_rgbspan.png new file mode 100644 index 0000000..1daa15e Binary files /dev/null and b/backend/data/tests/expected_results/m23_1_rgbspan.png differ diff --git a/backend/data/tests/expected_results/m23_2_colored_crayons.png b/backend/data/tests/expected_results/m23_2_colored_crayons.png new file mode 100644 index 0000000..2df2f23 Binary files /dev/null and b/backend/data/tests/expected_results/m23_2_colored_crayons.png differ diff --git a/backend/data/tests/expected_results/m23_2_myhelsinki.fi_website.png b/backend/data/tests/expected_results/m23_2_myhelsinki.fi_website.png new file mode 100644 index 0000000..a70269d Binary files /dev/null and b/backend/data/tests/expected_results/m23_2_myhelsinki.fi_website.png differ diff --git a/backend/data/tests/expected_results/m23_2_rgbspan.png b/backend/data/tests/expected_results/m23_2_rgbspan.png new file mode 100644 index 0000000..87b25f1 Binary files /dev/null and b/backend/data/tests/expected_results/m23_2_rgbspan.png differ diff --git a/backend/data/tests/input_values/colored_crayons.png b/backend/data/tests/input_values/colored_crayons.png new file mode 100644 index 0000000..131d1c2 Binary files /dev/null and b/backend/data/tests/input_values/colored_crayons.png differ diff --git a/backend/data/tests/input_values/rgbspan.png b/backend/data/tests/input_values/rgbspan.png new file mode 100644 index 0000000..2b3471a Binary files /dev/null and b/backend/data/tests/input_values/rgbspan.png differ diff --git a/backend/tests/metrics/test_m20.py b/backend/tests/metrics/test_m20.py index a948371..3a71ed9 100644 --- a/backend/tests/metrics/test_m20.py +++ b/backend/tests/metrics/test_m20.py @@ -46,8 +46,8 @@ "aim_bad_harmony.png", [ 1246.757656, - load_expected_result("m20_1_aim.png"), - load_expected_result("m20_2_aim.png"), + load_expected_result("m20_1_aim_bad_harmony.png"), + load_expected_result("m20_2_aim_bad_harmony.png"), ], ), ], diff --git a/backend/tests/metrics/test_m23.py b/backend/tests/metrics/test_m23.py new file mode 100644 index 0000000..3c09e51 --- /dev/null +++ b/backend/tests/metrics/test_m23.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Tests for the 'Color blindness' metric (m23). +""" + + +# ---------------------------------------------------------------------------- +# Imports +# ---------------------------------------------------------------------------- + +# Standard library modules +import pathlib +from typing import Any, List, Optional, Union + +# Third-party modules +import pytest + +# First-party modules +from aim.common import image_utils +from aim.metrics.m23.m23_color_blindness import Metric +from tests.common.constants import DATA_TESTS_INPUT_VALUES_DIR, IDIFF_TOLERANCE +from tests.common.utils import load_expected_result + +# ---------------------------------------------------------------------------- +# Metadata +# ---------------------------------------------------------------------------- + +__author__ = "Amir Hossein Kargaran, Markku Laine" +__date__ = "2022-10-21" +__email__ = "markku.laine@aalto.fi" +__version__ = "1.0" + + +# ---------------------------------------------------------------------------- +# Tests +# ---------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + ["input_value", "expected_results"], + [ + ( + "myhelsinki.fi_website.png", + [ + load_expected_result("m23_0_myhelsinki.fi_website.png"), + load_expected_result("m23_1_myhelsinki.fi_website.png"), + load_expected_result("m23_2_myhelsinki.fi_website.png"), + ], + ), + ( + "colored_crayons.png", + [ + load_expected_result("m23_0_colored_crayons.png"), + load_expected_result("m23_1_colored_crayons.png"), + load_expected_result("m23_2_colored_crayons.png"), + ], + ), + ( + "rgbspan.png", + [ + load_expected_result("m23_0_rgbspan.png"), + load_expected_result("m23_1_rgbspan.png"), + load_expected_result("m23_2_rgbspan.png"), + ], + ), + ], +) +def test_color_blindness_desktop( + input_value: str, expected_results: List[Any] +) -> None: + """ + Test Color blindness (desktop GUIs). + + Args: + input_value: GUI image file name + expected_results: Expected results (list of measures) + """ + # Build GUI image file path + gui_image_filepath: pathlib.Path = ( + pathlib.Path(DATA_TESTS_INPUT_VALUES_DIR) / input_value + ) + + # Read GUI image (PNG) + gui_image_png_base64: str = image_utils.read_image(gui_image_filepath) + + # Execute metric + result: Optional[List[Union[int, float, str]]] = Metric.execute_metric( + gui_image_png_base64 + ) + + # Test result + if ( + result is not None + and isinstance(result[0], str) + and isinstance(result[1], str) + and isinstance(result[2], str) + ): + assert ( + image_utils.idiff(result[0], expected_results[0]) + <= IDIFF_TOLERANCE + ) + assert ( + image_utils.idiff(result[1], expected_results[1]) + <= IDIFF_TOLERANCE + ) + assert ( + image_utils.idiff(result[2], expected_results[2]) + <= IDIFF_TOLERANCE + ) diff --git a/frontend/src/assets/results/m23_results.json b/frontend/src/assets/results/m23_results.json new file mode 100644 index 0000000..9492f4c --- /dev/null +++ b/frontend/src/assets/results/m23_results.json @@ -0,0 +1,429 @@ +[ + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {} +] diff --git a/legacy/aim_metrics/aim_metrics/accessibility/ac1_colour_blindness.py b/legacy/aim_metrics/aim_metrics/accessibility/ac1_colour_blindness.py deleted file mode 100644 index 3eb6f15..0000000 --- a/legacy/aim_metrics/aim_metrics/accessibility/ac1_colour_blindness.py +++ /dev/null @@ -1,157 +0,0 @@ -########################################################### -# Colour Blindness - Deuteranope, Protanope and Tratanope # -########################################################### -# -# V1.0 -# 29/05/2017 -# -# Implemented by: -# Thomas Langerak -# (hello@thomaslangerak.nl) -# -# Based on: -# https://moinmo.in/AccessibleMoin?action=AttachFile&do=get&target=daltonize.py -# By: -# Oliver Siemoneit (see ac1_original_header.txt) -# -# Supervisor: -# Antti Oulasvirta -# -# This work was funded by Technology Industries of Finland in a three-year -# project grant on self-optimizing web services. The principal investigator -# is Antti Oulasvirta of Aalto University (antti.oulasvirta@aalto.fi) -# -########### -# Summary # -########### -# -# (Taken from 1) -# Color vision deficiency (CVD) affects approximately 200 million people worldwide, compromising the ability of these -# individuals to effectively perform color and visualization-related tasks. This has a significant impact on their -# private and professional lives. We present a physiologically-based model for simulating color vision. Our model is -# based on the stage theory of human color vision and is derived from data reported in electrophysiological studies. -# It is the first model to consistently handle normal color vision, anomalous trichromacy, and dichromacy in a -# unified way. We have validated the proposed model through an experimental evaluation involving groups of color -# vision deficient individuals and normal color vision ones. Our model can provide insights and feedback on how to -# improve visualization experiences for individuals with CVD. It also provides a framework for testing hypotheses -# about some aspects of the retinal photoreceptors in color vision deficient individuals. -# -############# -# Technical # -############# -# -# Inputs: JPG image (base64) -# Returns: List of 3 items: Deuteranopia (base64), Protanopia (base64), Tritanopia (base64) - -############## -# References # -############## -# -# 1. Machado, G., Oliveira, M. and Fernandes, L. A Physiologically-based Model for Simulation of Color Vision -# Deficiency. IEEE Transactions on Visualization and Computer Graphics 15, 6 (2009), 1291-1298. -# -############## -# Change Log # -############## -# -############### -# Bugs/Issues # -############### -# -# Need to update the conversion matrixs (strength for deut and prot is 0.7 and trit is 1.0 -# Can make it a nested loop instead of doing three times the same things in the loop -# -import StringIO -import numpy -import base64 -from PIL import Image -from io import BytesIO - - -def execute(b64): - # Get image data - b64 = base64.b64decode(b64) - b64 = BytesIO(b64) - img = Image.open(b64) - im = img.convert('RGB') - RGB = numpy.asarray(im, dtype=float) - - # Transformation matrix for Deuteranope (a form of red/green color deficit) - deut_matrix = numpy.array( - [[0.457771, 0.731899, -0.189670], - [0.226409, 0.731012, 0.042579], - [-0.011595, 0.034333, 0.977261]] - ) - # Transformation matrix for Protanope (another form of red/green color deficit) - prot_matrix = numpy.array( - [[0.319627, 0.849633, -0.169261], - [0.106241, 0.815969, 0.077790], - [-0.007025, -0.028051, 1.035076]] - ) - # Transformation matrix for Tritanope (a blue/yellow deficit - very rare) - trit_matrix = numpy.array( - [[1.255528, -0.076749, -0.178779], - [-0.078411, 0.930809, 0.147602], - [0.004733, 0.691367, 0.303900]] - ) - - # Transform the image using each matrix - rgb_d = numpy.zeros_like(RGB) - rgb_p = numpy.zeros_like(RGB) - rgb_t = numpy.zeros_like(RGB) - for i in range(RGB.shape[0]): - for j in range(RGB.shape[1]): - rgb = RGB[i, j, :3] - rgb_d[i, j, :3] = numpy.dot(deut_matrix, rgb) - rgb_p[i, j, :3] = numpy.dot(prot_matrix, rgb) - rgb_t[i, j, :3] = numpy.dot(trit_matrix, rgb) - - - # Make sure that all numbers are within 0-255 due to conversions - for i in range(RGB.shape[0]): - for j in range(RGB.shape[1]): - rgb_d[i, j, 0] = max(0, rgb_d[i, j, 0]) - rgb_d[i, j, 0] = min(255, rgb_d[i, j, 0]) - rgb_d[i, j, 1] = max(0, rgb_d[i, j, 1]) - rgb_d[i, j, 1] = min(255, rgb_d[i, j, 1]) - rgb_d[i, j, 2] = max(0, rgb_d[i, j, 2]) - rgb_d[i, j, 2] = min(255, rgb_d[i, j, 2]) - - rgb_p[i, j, 0] = max(0, rgb_p[i, j, 0]) - rgb_p[i, j, 0] = min(255, rgb_p[i, j, 0]) - rgb_p[i, j, 1] = max(0, rgb_p[i, j, 1]) - rgb_p[i, j, 1] = min(255, rgb_p[i, j, 1]) - rgb_p[i, j, 2] = max(0, rgb_p[i, j, 2]) - rgb_p[i, j, 2] = min(255, rgb_p[i, j, 2]) - - rgb_t[i, j, 0] = max(0, rgb_t[i, j, 0]) - rgb_t[i, j, 0] = min(255, rgb_t[i, j, 0]) - rgb_t[i, j, 1] = max(0, rgb_t[i, j, 1]) - rgb_t[i, j, 1] = min(255, rgb_t[i, j, 1]) - rgb_t[i, j, 2] = max(0, rgb_t[i, j, 2]) - rgb_t[i, j, 2] = min(255, rgb_t[i, j, 2]) - - # Save as image into buffer - sim_d = rgb_d.astype('uint8') - sim_p = rgb_p.astype('uint8') - sim_t = rgb_t.astype('uint8') - im_d = Image.fromarray(sim_d, mode='RGB') - im_p = Image.fromarray(sim_p, mode='RGB') - im_t = Image.fromarray(sim_t, mode='RGB') - d_string = StringIO.StringIO() - p_string = StringIO.StringIO() - t_string = StringIO.StringIO() - im_d.save(d_string, format="PNG") - im_p.save(p_string, format="PNG") - im_t.save(t_string, format="PNG") - - # Encode it as base64 - d_b64 = base64.b64encode(d_string.getvalue()) - p_b64 = base64.b64encode(p_string.getvalue()) - t_b64 = base64.b64encode(t_string.getvalue()) - - d_string.close() - p_string.close() - t_string.close() - - return [d_b64, p_b64, t_b64] diff --git a/legacy/aim_metrics/aim_metrics/accessibility/ac1_original_header.txt b/legacy/aim_metrics/aim_metrics/accessibility/ac1_original_header.txt deleted file mode 100644 index 0739110..0000000 --- a/legacy/aim_metrics/aim_metrics/accessibility/ac1_original_header.txt +++ /dev/null @@ -1,41 +0,0 @@ -""" - MoinMoin - Daltonize ImageCorrection - Effect - - Daltonize image correction algorithm implemented according to - http://scien.stanford.edu/class/psych221/projects/05/ofidaner/colorblindness_project.htm - - Many thanks to Onur Fidaner, Poliang Lin and Nevran Ozguven for their work - on this topic and for releasing their complete research results to the public - (unlike the guys from http://www.vischeck.com/). This is of great help for a - lot of people! - - Please note: - Daltonize ImageCorrection needs - * Python Image Library (PIL) from http://www.pythonware.com/products/pil/ - * NumPy from http://numpy.scipy.org/ - - You can call Daltonize from the command-line with - "daltonize.py C:\image.png" - - Explanations: - * Normally this module is called from Moin.AttachFile.get_file - * @param filename, filepath is the filename/fullpath to an image in the attachment - dir of a page. - * @param color_deficit can either be - - 'd' for Deuteranope image correction - - 'p' for Protanope image correction - - 't' for Tritanope image correct - Idea: - * Since daltonizing an image takes quite some time and we don't want visually - impaired users to wait so long until the page is loaded, this module has a - command-line option built-in which could be called as a separate process - after a file upload of a non visually impaired user in "AttachFile", e.g - "spawnlp(os.NO_WAIT...)" - * "AttachFile": If an image attachment is deleted or overwritten by a new version - please make sure to delete the daltonized images and redaltonize them. - * But all in all: Concrete implementation of ImageCorrection needs further - thinking and discussion. This is only a first prototype as proof of concept. - - @copyright: 2007 by Oliver Siemoneit - @license: GNU GPL, see COPYING for details. -""" diff --git a/metrics.json b/metrics.json index eb63cb9..83c6880 100644 --- a/metrics.json +++ b/metrics.json @@ -3,7 +3,7 @@ { "name": "Accessibility", "icon": "universal-access", - "metrics": [] + "metrics": ["m23"] }, { "name": "Aesthetics", @@ -1396,7 +1396,7 @@ "speed": 0, "references": [ { - "title": "Cohen-Or, D., Sorkine, O., Gal, R., Leyvand, T. and Xu, Y.Q. (2006). Color Harmonization. ACM Transactions on Graphics, 25(3), 624-630. doi: https://doi.org/10.1145/1141911.1141933", + "title": "Cohen-Or, D., Sorkine, O., Gal, R., Leyvand, T., and Xu, Y.Q. (2006). Color Harmonization. ACM Transactions on Graphics, 25(3), 624-630. doi: https://doi.org/10.1145/1141911.1141933", "url": "https://doi.org/10.1145/1141911.1141933" } ], @@ -1446,6 +1446,43 @@ "description": "The hue histogram of the image before and after harmonization." } ] + }, + "m23": { + "id": "m23", + "name": "Color blindness", + "description": "A physiologically-based model for simulation of color vision deficiency.", + "evidence": 4, + "relevance": 5, + "speed": 2, + "references": [ + { + "title": "Machado, G.M., Oliveira, M.M., and Fernandes, L.A.F. (2009). A Physiologically-based Model for Simulation of Color Vision Deficiency. IEEE Transactions on Visualization and Computer Graphics, 15(6), 1291-1298. doi: https://doi.org/10.1109/TVCG.2009.113", + "url": "https://doi.org/10.1109/TVCG.2009.113" + } + ], + "results": [ + { + "id": "m23_0", + "index": 0, + "type": "b64", + "name": "Protanopia", + "description": "Red-green color blindness, lack of Long-wavelength (L, ~red) cone cells in the retina" + }, + { + "id": "m23_1", + "index": 1, + "type": "b64", + "name": "Deuteranopia", + "description": "Red-green color blindness, lack of Medium-wavelength (M, ~green) cone cells in the retina" + }, + { + "id": "m23_2", + "index": 2, + "type": "b64", + "name": "Tritanopia", + "description": "Blue-yellow color blindness, lack of Short-wavelength (S, ~blue) cone cells in the retina" + } + ] } } }