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