1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.internal.graphics.cam; 18 19 20 import android.annotation.NonNull; 21 import android.graphics.Color; 22 23 import com.android.internal.graphics.ColorUtils; 24 25 /** 26 * Collection of methods for transforming between color spaces. 27 * 28 * <p>Methods are named $xFrom$Y. For example, lstarFromInt() returns L* from an ARGB integer. 29 * 30 * <p>These methods, generally, convert colors between the L*a*b*, XYZ, and sRGB spaces. 31 * 32 * <p>L*a*b* is a perceptually accurate color space. This is particularly important in the L* 33 * dimension: it measures luminance and unlike lightness measures traditionally used in UI work via 34 * RGB or HSL, this luminance transitions smoothly, permitting creation of pleasing shades of a 35 * color, and more pleasing transitions between colors. 36 * 37 * <p>XYZ is commonly used as an intermediate color space for converting between one color space to 38 * another. For example, to convert RGB to L*a*b*, first RGB is converted to XYZ, then XYZ is 39 * convered to L*a*b*. 40 * 41 * <p>sRGB is a "specification originated from work in 1990s through cooperation by Hewlett-Packard 42 * and Microsoft, and it was designed to be a standard definition of RGB for the internet, which it 43 * indeed became...The standard is based on a sampling of computer monitors at the time...The whole 44 * idea of sRGB is that if everyone assumed that RGB meant the same thing, then the results would be 45 * consistent, and reasonably good. It worked." - Fairchild, Color Models and Systems: Handbook of 46 * Color Psychology, 2015 47 */ 48 public final class CamUtils { CamUtils()49 private CamUtils() { 50 } 51 52 // Transforms XYZ color space coordinates to 'cone'/'RGB' responses in CAM16. 53 static final float[][] XYZ_TO_CAM16RGB = { 54 {0.401288f, 0.650173f, -0.051461f}, 55 {-0.250268f, 1.204414f, 0.045854f}, 56 {-0.002079f, 0.048952f, 0.953127f} 57 }; 58 59 // Transforms 'cone'/'RGB' responses in CAM16 to XYZ color space coordinates. 60 static final float[][] CAM16RGB_TO_XYZ = { 61 {1.86206786f, -1.01125463f, 0.14918677f}, 62 {0.38752654f, 0.62144744f, -0.00897398f}, 63 {-0.01584150f, -0.03412294f, 1.04996444f} 64 }; 65 66 // Need this, XYZ coordinates in internal ColorUtils are private 67 68 // sRGB specification has D65 whitepoint - Stokes, Anderson, Chandrasekar, Motta - A Standard 69 // Default Color Space for the Internet: sRGB, 1996 70 static final float[] WHITE_POINT_D65 = {95.047f, 100.0f, 108.883f}; 71 72 // This is a more precise sRGB to XYZ transformation matrix than traditionally 73 // used. It was derived using Schlomer's technique of transforming the xyY 74 // primaries to XYZ, then applying a correction to ensure mapping from sRGB 75 // 1, 1, 1 to the reference white point, D65. 76 static final double[][] SRGB_TO_XYZ = 77 new double[][] { 78 new double[] {0.41233895, 0.35762064, 0.18051042}, 79 new double[] {0.2126, 0.7152, 0.0722}, 80 new double[] {0.01932141, 0.11916382, 0.95034478}, 81 }; 82 83 static final double[][] XYZ_TO_SRGB = 84 new double[][] { 85 new double[] { 86 3.2413774792388685, -1.5376652402851851, -0.49885366846268053, 87 }, 88 new double[] { 89 -0.9691452513005321, 1.8758853451067872, 0.04156585616912061, 90 }, 91 new double[] { 92 0.05562093689691305, -0.20395524564742123, 1.0571799111220335, 93 }, 94 }; 95 96 /** 97 * The signum function. 98 * 99 * @return 1 if num > 0, -1 if num < 0, and 0 if num = 0 100 */ signum(double num)101 public static int signum(double num) { 102 if (num < 0) { 103 return -1; 104 } else if (num == 0) { 105 return 0; 106 } else { 107 return 1; 108 } 109 } 110 111 /** 112 * Converts an L* value to an ARGB representation. 113 * 114 * @param lstar L* in L*a*b* 115 * @return ARGB representation of grayscale color with lightness matching L* 116 */ argbFromLstar(double lstar)117 public static int argbFromLstar(double lstar) { 118 double fy = (lstar + 16.0) / 116.0; 119 double fz = fy; 120 double fx = fy; 121 double kappa = 24389.0 / 27.0; 122 double epsilon = 216.0 / 24389.0; 123 boolean lExceedsEpsilonKappa = lstar > 8.0; 124 double y = lExceedsEpsilonKappa ? fy * fy * fy : lstar / kappa; 125 boolean cubeExceedEpsilon = fy * fy * fy > epsilon; 126 double x = cubeExceedEpsilon ? fx * fx * fx : lstar / kappa; 127 double z = cubeExceedEpsilon ? fz * fz * fz : lstar / kappa; 128 float[] whitePoint = WHITE_POINT_D65; 129 return argbFromXyz(x * whitePoint[0], y * whitePoint[1], z * whitePoint[2]); 130 } 131 132 /** Converts a color from ARGB to XYZ. */ argbFromXyz(double x, double y, double z)133 public static int argbFromXyz(double x, double y, double z) { 134 double[][] matrix = XYZ_TO_SRGB; 135 double linearR = matrix[0][0] * x + matrix[0][1] * y + matrix[0][2] * z; 136 double linearG = matrix[1][0] * x + matrix[1][1] * y + matrix[1][2] * z; 137 double linearB = matrix[2][0] * x + matrix[2][1] * y + matrix[2][2] * z; 138 int r = delinearized(linearR); 139 int g = delinearized(linearG); 140 int b = delinearized(linearB); 141 return argbFromRgb(r, g, b); 142 } 143 144 /** Converts a color from linear RGB components to ARGB format. */ argbFromLinrgb(double[] linrgb)145 public static int argbFromLinrgb(double[] linrgb) { 146 int r = delinearized(linrgb[0]); 147 int g = delinearized(linrgb[1]); 148 int b = delinearized(linrgb[2]); 149 return argbFromRgb(r, g, b); 150 } 151 152 /** Converts a color from linear RGB components to ARGB format. */ argbFromLinrgbComponents(double r, double g, double b)153 public static int argbFromLinrgbComponents(double r, double g, double b) { 154 return argbFromRgb(delinearized(r), delinearized(g), delinearized(b)); 155 } 156 157 /** 158 * Delinearizes an RGB component. 159 * 160 * @param rgbComponent 0.0 <= rgb_component <= 100.0, represents linear R/G/B channel 161 * @return 0 <= output <= 255, color channel converted to regular RGB space 162 */ delinearized(double rgbComponent)163 public static int delinearized(double rgbComponent) { 164 double normalized = rgbComponent / 100.0; 165 double delinearized = 0.0; 166 if (normalized <= 0.0031308) { 167 delinearized = normalized * 12.92; 168 } else { 169 delinearized = 1.055 * Math.pow(normalized, 1.0 / 2.4) - 0.055; 170 } 171 return clampInt(0, 255, (int) Math.round(delinearized * 255.0)); 172 } 173 174 /** 175 * Clamps an integer between two integers. 176 * 177 * @return input when min <= input <= max, and either min or max otherwise. 178 */ clampInt(int min, int max, int input)179 public static int clampInt(int min, int max, int input) { 180 if (input < min) { 181 return min; 182 } else if (input > max) { 183 return max; 184 } 185 186 return input; 187 } 188 189 /** Converts a color from RGB components to ARGB format. */ argbFromRgb(int red, int green, int blue)190 public static int argbFromRgb(int red, int green, int blue) { 191 return (255 << 24) | ((red & 255) << 16) | ((green & 255) << 8) | (blue & 255); 192 } 193 intFromLstar(float lstar)194 static int intFromLstar(float lstar) { 195 if (lstar < 1) { 196 return 0xff000000; 197 } else if (lstar > 99) { 198 return 0xffffffff; 199 } 200 201 // XYZ to LAB conversion routine, assume a and b are 0. 202 float fy = (lstar + 16.0f) / 116.0f; 203 204 // fz = fx = fy because a and b are 0 205 float fz = fy; 206 float fx = fy; 207 208 float kappa = 24389f / 27f; 209 float epsilon = 216f / 24389f; 210 boolean lExceedsEpsilonKappa = (lstar > 8.0f); 211 float yT = lExceedsEpsilonKappa ? fy * fy * fy : lstar / kappa; 212 boolean cubeExceedEpsilon = (fy * fy * fy) > epsilon; 213 float xT = cubeExceedEpsilon ? fx * fx * fx : (116f * fx - 16f) / kappa; 214 float zT = cubeExceedEpsilon ? fz * fz * fz : (116f * fx - 16f) / kappa; 215 216 return ColorUtils.XYZToColor(xT * CamUtils.WHITE_POINT_D65[0], 217 yT * CamUtils.WHITE_POINT_D65[1], zT * CamUtils.WHITE_POINT_D65[2]); 218 } 219 220 /** Returns L* from L*a*b*, perceptual luminance, from an ARGB integer (ColorInt). */ lstarFromInt(int argb)221 public static float lstarFromInt(int argb) { 222 return lstarFromY(yFromInt(argb)); 223 } 224 lstarFromY(float y)225 static float lstarFromY(float y) { 226 y = y / 100.0f; 227 final float e = 216.f / 24389.f; 228 float yIntermediate; 229 if (y <= e) { 230 return ((24389.f / 27.f) * y); 231 } else { 232 yIntermediate = (float) Math.cbrt(y); 233 } 234 return 116.f * yIntermediate - 16.f; 235 } 236 yFromInt(int argb)237 static float yFromInt(int argb) { 238 final float r = linearized(Color.red(argb)); 239 final float g = linearized(Color.green(argb)); 240 final float b = linearized(Color.blue(argb)); 241 double[][] matrix = SRGB_TO_XYZ; 242 double y = (r * matrix[1][0]) + (g * matrix[1][1]) + (b * matrix[1][2]); 243 return (float) y; 244 } 245 246 @NonNull xyzFromInt(int argb)247 static float[] xyzFromInt(int argb) { 248 final float r = linearized(Color.red(argb)); 249 final float g = linearized(Color.green(argb)); 250 final float b = linearized(Color.blue(argb)); 251 252 double[][] matrix = SRGB_TO_XYZ; 253 double x = (r * matrix[0][0]) + (g * matrix[0][1]) + (b * matrix[0][2]); 254 double y = (r * matrix[1][0]) + (g * matrix[1][1]) + (b * matrix[1][2]); 255 double z = (r * matrix[2][0]) + (g * matrix[2][1]) + (b * matrix[2][2]); 256 return new float[]{(float) x, (float) y, (float) z}; 257 } 258 259 /** 260 * Converts an L* value to a Y value. 261 * 262 * <p>L* in L*a*b* and Y in XYZ measure the same quantity, luminance. 263 * 264 * <p>L* measures perceptual luminance, a linear scale. Y in XYZ measures relative luminance, a 265 * logarithmic scale. 266 * 267 * @param lstar L* in L*a*b* 268 * @return Y in XYZ 269 */ yFromLstar(double lstar)270 public static double yFromLstar(double lstar) { 271 double ke = 8.0; 272 if (lstar > ke) { 273 return Math.pow((lstar + 16.0) / 116.0, 3.0) * 100.0; 274 } else { 275 return lstar / (24389.0 / 27.0) * 100.0; 276 } 277 } 278 linearized(int rgbComponent)279 static float linearized(int rgbComponent) { 280 float normalized = (float) rgbComponent / 255.0f; 281 282 if (normalized <= 0.04045f) { 283 return (normalized / 12.92f) * 100.0f; 284 } else { 285 return (float) Math.pow(((normalized + 0.055f) / 1.055f), 2.4f) * 100.0f; 286 } 287 } 288 } 289