• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2021 Google LLC
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.google.ux.material.libmonet.hct;
18 
19 import static java.lang.Math.max;
20 
21 import com.google.ux.material.libmonet.utils.ColorUtils;
22 
23 /**
24  * CAM16, a color appearance model. Colors are not just defined by their hex code, but rather, a hex
25  * code and viewing conditions.
26  *
27  * <p>CAM16 instances also have coordinates in the CAM16-UCS space, called J*, a*, b*, or jstar,
28  * astar, bstar in code. CAM16-UCS is included in the CAM16 specification, and should be used when
29  * measuring distances between colors.
30  *
31  * <p>In traditional color spaces, a color can be identified solely by the observer's measurement of
32  * the color. Color appearance models such as CAM16 also use information about the environment where
33  * the color was observed, known as the viewing conditions.
34  *
35  * <p>For example, white under the traditional assumption of a midday sun white point is accurately
36  * measured as a slightly chromatic blue by CAM16. (roughly, hue 203, chroma 3, lightness 100)
37  */
38 public final class Cam16 {
39   // Transforms XYZ color space coordinates to 'cone'/'RGB' responses in CAM16.
40   static final double[][] XYZ_TO_CAM16RGB = {
41     {0.401288, 0.650173, -0.051461},
42     {-0.250268, 1.204414, 0.045854},
43     {-0.002079, 0.048952, 0.953127}
44   };
45 
46   // Transforms 'cone'/'RGB' responses in CAM16 to XYZ color space coordinates.
47   static final double[][] CAM16RGB_TO_XYZ = {
48     {1.8620678, -1.0112547, 0.14918678},
49     {0.38752654, 0.62144744, -0.00897398},
50     {-0.01584150, -0.03412294, 1.0499644}
51   };
52 
53   // CAM16 color dimensions, see getters for documentation.
54   private final double hue;
55   private final double chroma;
56   private final double j;
57   private final double q;
58   private final double m;
59   private final double s;
60 
61   // Coordinates in UCS space. Used to determine color distance, like delta E equations in L*a*b*.
62   private final double jstar;
63   private final double astar;
64   private final double bstar;
65 
66   // Avoid allocations during conversion by pre-allocating an array.
67   private final double[] tempArray = new double[] {0.0, 0.0, 0.0};
68 
69   /**
70    * CAM16 instances also have coordinates in the CAM16-UCS space, called J*, a*, b*, or jstar,
71    * astar, bstar in code. CAM16-UCS is included in the CAM16 specification, and is used to measure
72    * distances between colors.
73    */
distance(Cam16 other)74   public double distance(Cam16 other) {
75     double dJ = getJstar() - other.getJstar();
76     double dA = getAstar() - other.getAstar();
77     double dB = getBstar() - other.getBstar();
78     double dEPrime = Math.sqrt(dJ * dJ + dA * dA + dB * dB);
79     double dE = 1.41 * Math.pow(dEPrime, 0.63);
80     return dE;
81   }
82 
83   /** Hue in CAM16 */
getHue()84   public double getHue() {
85     return hue;
86   }
87 
88   /** Chroma in CAM16 */
getChroma()89   public double getChroma() {
90     return chroma;
91   }
92 
93   /** Lightness in CAM16 */
getJ()94   public double getJ() {
95     return j;
96   }
97 
98   /**
99    * Brightness in CAM16.
100    *
101    * <p>Prefer lightness, brightness is an absolute quantity. For example, a sheet of white paper is
102    * much brighter viewed in sunlight than in indoor light, but it is the lightest object under any
103    * lighting.
104    */
getQ()105   public double getQ() {
106     return q;
107   }
108 
109   /**
110    * Colorfulness in CAM16.
111    *
112    * <p>Prefer chroma, colorfulness is an absolute quantity. For example, a yellow toy car is much
113    * more colorful outside than inside, but it has the same chroma in both environments.
114    */
getM()115   public double getM() {
116     return m;
117   }
118 
119   /**
120    * Saturation in CAM16.
121    *
122    * <p>Colorfulness in proportion to brightness. Prefer chroma, saturation measures colorfulness
123    * relative to the color's own brightness, where chroma is colorfulness relative to white.
124    */
getS()125   public double getS() {
126     return s;
127   }
128 
129   /** Lightness coordinate in CAM16-UCS */
getJstar()130   public double getJstar() {
131     return jstar;
132   }
133 
134   /** a* coordinate in CAM16-UCS */
getAstar()135   public double getAstar() {
136     return astar;
137   }
138 
139   /** b* coordinate in CAM16-UCS */
getBstar()140   public double getBstar() {
141     return bstar;
142   }
143 
144   /**
145    * All of the CAM16 dimensions can be calculated from 3 of the dimensions, in the following
146    * combinations: - {j or q} and {c, m, or s} and hue - jstar, astar, bstar Prefer using a static
147    * method that constructs from 3 of those dimensions. This constructor is intended for those
148    * methods to use to return all possible dimensions.
149    *
150    * @param hue for example, red, orange, yellow, green, etc.
151    * @param chroma informally, colorfulness / color intensity. like saturation in HSL, except
152    *     perceptually accurate.
153    * @param j lightness
154    * @param q brightness; ratio of lightness to white point's lightness
155    * @param m colorfulness
156    * @param s saturation; ratio of chroma to white point's chroma
157    * @param jstar CAM16-UCS J coordinate
158    * @param astar CAM16-UCS a coordinate
159    * @param bstar CAM16-UCS b coordinate
160    */
Cam16( double hue, double chroma, double j, double q, double m, double s, double jstar, double astar, double bstar)161   private Cam16(
162       double hue,
163       double chroma,
164       double j,
165       double q,
166       double m,
167       double s,
168       double jstar,
169       double astar,
170       double bstar) {
171     this.hue = hue;
172     this.chroma = chroma;
173     this.j = j;
174     this.q = q;
175     this.m = m;
176     this.s = s;
177     this.jstar = jstar;
178     this.astar = astar;
179     this.bstar = bstar;
180   }
181 
182   /**
183    * Create a CAM16 color from a color, assuming the color was viewed in default viewing conditions.
184    *
185    * @param argb ARGB representation of a color.
186    */
fromInt(int argb)187   public static Cam16 fromInt(int argb) {
188     return fromIntInViewingConditions(argb, ViewingConditions.DEFAULT);
189   }
190 
191   /**
192    * Create a CAM16 color from a color in defined viewing conditions.
193    *
194    * @param argb ARGB representation of a color.
195    * @param viewingConditions Information about the environment where the color was observed.
196    */
197   // The RGB => XYZ conversion matrix elements are derived scientific constants. While the values
198   // may differ at runtime due to floating point imprecision, keeping the values the same, and
199   // accurate, across implementations takes precedence.
200   @SuppressWarnings("FloatingPointLiteralPrecision")
fromIntInViewingConditions(int argb, ViewingConditions viewingConditions)201   static Cam16 fromIntInViewingConditions(int argb, ViewingConditions viewingConditions) {
202     // Transform ARGB int to XYZ
203     int red = (argb & 0x00ff0000) >> 16;
204     int green = (argb & 0x0000ff00) >> 8;
205     int blue = (argb & 0x000000ff);
206     double redL = ColorUtils.linearized(red);
207     double greenL = ColorUtils.linearized(green);
208     double blueL = ColorUtils.linearized(blue);
209     double x = 0.41233895 * redL + 0.35762064 * greenL + 0.18051042 * blueL;
210     double y = 0.2126 * redL + 0.7152 * greenL + 0.0722 * blueL;
211     double z = 0.01932141 * redL + 0.11916382 * greenL + 0.95034478 * blueL;
212 
213     return fromXyzInViewingConditions(x, y, z, viewingConditions);
214   }
215 
fromXyzInViewingConditions( double x, double y, double z, ViewingConditions viewingConditions)216   static Cam16 fromXyzInViewingConditions(
217       double x, double y, double z, ViewingConditions viewingConditions) {
218     // Transform XYZ to 'cone'/'rgb' responses
219     double[][] matrix = XYZ_TO_CAM16RGB;
220     double rT = (x * matrix[0][0]) + (y * matrix[0][1]) + (z * matrix[0][2]);
221     double gT = (x * matrix[1][0]) + (y * matrix[1][1]) + (z * matrix[1][2]);
222     double bT = (x * matrix[2][0]) + (y * matrix[2][1]) + (z * matrix[2][2]);
223 
224     // Discount illuminant
225     double rD = viewingConditions.getRgbD()[0] * rT;
226     double gD = viewingConditions.getRgbD()[1] * gT;
227     double bD = viewingConditions.getRgbD()[2] * bT;
228 
229     // Chromatic adaptation
230     double rAF = Math.pow(viewingConditions.getFl() * Math.abs(rD) / 100.0, 0.42);
231     double gAF = Math.pow(viewingConditions.getFl() * Math.abs(gD) / 100.0, 0.42);
232     double bAF = Math.pow(viewingConditions.getFl() * Math.abs(bD) / 100.0, 0.42);
233     double rA = Math.signum(rD) * 400.0 * rAF / (rAF + 27.13);
234     double gA = Math.signum(gD) * 400.0 * gAF / (gAF + 27.13);
235     double bA = Math.signum(bD) * 400.0 * bAF / (bAF + 27.13);
236 
237     // redness-greenness
238     double a = (11.0 * rA + -12.0 * gA + bA) / 11.0;
239     // yellowness-blueness
240     double b = (rA + gA - 2.0 * bA) / 9.0;
241 
242     // auxiliary components
243     double u = (20.0 * rA + 20.0 * gA + 21.0 * bA) / 20.0;
244     double p2 = (40.0 * rA + 20.0 * gA + bA) / 20.0;
245 
246     // hue
247     double atan2 = Math.atan2(b, a);
248     double atanDegrees = Math.toDegrees(atan2);
249     double hue =
250         atanDegrees < 0
251             ? atanDegrees + 360.0
252             : atanDegrees >= 360 ? atanDegrees - 360.0 : atanDegrees;
253     double hueRadians = Math.toRadians(hue);
254 
255     // achromatic response to color
256     double ac = p2 * viewingConditions.getNbb();
257 
258     // CAM16 lightness and brightness
259     double j =
260         100.0
261             * Math.pow(
262                 ac / viewingConditions.getAw(),
263                 viewingConditions.getC() * viewingConditions.getZ());
264     double q =
265         4.0
266             / viewingConditions.getC()
267             * Math.sqrt(j / 100.0)
268             * (viewingConditions.getAw() + 4.0)
269             * viewingConditions.getFlRoot();
270 
271     // CAM16 chroma, colorfulness, and saturation.
272     double huePrime = (hue < 20.14) ? hue + 360 : hue;
273     double eHue = 0.25 * (Math.cos(Math.toRadians(huePrime) + 2.0) + 3.8);
274     double p1 = 50000.0 / 13.0 * eHue * viewingConditions.getNc() * viewingConditions.getNcb();
275     double t = p1 * Math.hypot(a, b) / (u + 0.305);
276     double alpha =
277         Math.pow(1.64 - Math.pow(0.29, viewingConditions.getN()), 0.73) * Math.pow(t, 0.9);
278     // CAM16 chroma, colorfulness, saturation
279     double c = alpha * Math.sqrt(j / 100.0);
280     double m = c * viewingConditions.getFlRoot();
281     double s =
282         50.0 * Math.sqrt((alpha * viewingConditions.getC()) / (viewingConditions.getAw() + 4.0));
283 
284     // CAM16-UCS components
285     double jstar = (1.0 + 100.0 * 0.007) * j / (1.0 + 0.007 * j);
286     double mstar = 1.0 / 0.0228 * Math.log1p(0.0228 * m);
287     double astar = mstar * Math.cos(hueRadians);
288     double bstar = mstar * Math.sin(hueRadians);
289 
290     return new Cam16(hue, c, j, q, m, s, jstar, astar, bstar);
291   }
292 
293   /**
294    * @param j CAM16 lightness
295    * @param c CAM16 chroma
296    * @param h CAM16 hue
297    */
fromJch(double j, double c, double h)298   static Cam16 fromJch(double j, double c, double h) {
299     return fromJchInViewingConditions(j, c, h, ViewingConditions.DEFAULT);
300   }
301 
302   /**
303    * @param j CAM16 lightness
304    * @param c CAM16 chroma
305    * @param h CAM16 hue
306    * @param viewingConditions Information about the environment where the color was observed.
307    */
fromJchInViewingConditions( double j, double c, double h, ViewingConditions viewingConditions)308   private static Cam16 fromJchInViewingConditions(
309       double j, double c, double h, ViewingConditions viewingConditions) {
310     double q =
311         4.0
312             / viewingConditions.getC()
313             * Math.sqrt(j / 100.0)
314             * (viewingConditions.getAw() + 4.0)
315             * viewingConditions.getFlRoot();
316     double m = c * viewingConditions.getFlRoot();
317     double alpha = c / Math.sqrt(j / 100.0);
318     double s =
319         50.0 * Math.sqrt((alpha * viewingConditions.getC()) / (viewingConditions.getAw() + 4.0));
320 
321     double hueRadians = Math.toRadians(h);
322     double jstar = (1.0 + 100.0 * 0.007) * j / (1.0 + 0.007 * j);
323     double mstar = 1.0 / 0.0228 * Math.log1p(0.0228 * m);
324     double astar = mstar * Math.cos(hueRadians);
325     double bstar = mstar * Math.sin(hueRadians);
326     return new Cam16(h, c, j, q, m, s, jstar, astar, bstar);
327   }
328 
329   /**
330    * Create a CAM16 color from CAM16-UCS coordinates.
331    *
332    * @param jstar CAM16-UCS lightness.
333    * @param astar CAM16-UCS a dimension. Like a* in L*a*b*, it is a Cartesian coordinate on the Y
334    *     axis.
335    * @param bstar CAM16-UCS b dimension. Like a* in L*a*b*, it is a Cartesian coordinate on the X
336    *     axis.
337    */
fromUcs(double jstar, double astar, double bstar)338   public static Cam16 fromUcs(double jstar, double astar, double bstar) {
339 
340     return fromUcsInViewingConditions(jstar, astar, bstar, ViewingConditions.DEFAULT);
341   }
342 
343   /**
344    * Create a CAM16 color from CAM16-UCS coordinates in defined viewing conditions.
345    *
346    * @param jstar CAM16-UCS lightness.
347    * @param astar CAM16-UCS a dimension. Like a* in L*a*b*, it is a Cartesian coordinate on the Y
348    *     axis.
349    * @param bstar CAM16-UCS b dimension. Like a* in L*a*b*, it is a Cartesian coordinate on the X
350    *     axis.
351    * @param viewingConditions Information about the environment where the color was observed.
352    */
fromUcsInViewingConditions( double jstar, double astar, double bstar, ViewingConditions viewingConditions)353   public static Cam16 fromUcsInViewingConditions(
354       double jstar, double astar, double bstar, ViewingConditions viewingConditions) {
355 
356     double m = Math.hypot(astar, bstar);
357     double m2 = Math.expm1(m * 0.0228) / 0.0228;
358     double c = m2 / viewingConditions.getFlRoot();
359     double h = Math.atan2(bstar, astar) * (180.0 / Math.PI);
360     if (h < 0.0) {
361       h += 360.0;
362     }
363     double j = jstar / (1. - (jstar - 100.) * 0.007);
364     return fromJchInViewingConditions(j, c, h, viewingConditions);
365   }
366 
367   /**
368    * ARGB representation of the color. Assumes the color was viewed in default viewing conditions,
369    * which are near-identical to the default viewing conditions for sRGB.
370    */
toInt()371   public int toInt() {
372     return viewed(ViewingConditions.DEFAULT);
373   }
374 
375   /**
376    * ARGB representation of the color, in defined viewing conditions.
377    *
378    * @param viewingConditions Information about the environment where the color will be viewed.
379    * @return ARGB representation of color
380    */
viewed(ViewingConditions viewingConditions)381   int viewed(ViewingConditions viewingConditions) {
382     double[] xyz = xyzInViewingConditions(viewingConditions, tempArray);
383     return ColorUtils.argbFromXyz(xyz[0], xyz[1], xyz[2]);
384   }
385 
xyzInViewingConditions(ViewingConditions viewingConditions, double[] returnArray)386   double[] xyzInViewingConditions(ViewingConditions viewingConditions, double[] returnArray) {
387     double alpha =
388         (getChroma() == 0.0 || getJ() == 0.0) ? 0.0 : getChroma() / Math.sqrt(getJ() / 100.0);
389 
390     double t =
391         Math.pow(
392             alpha / Math.pow(1.64 - Math.pow(0.29, viewingConditions.getN()), 0.73), 1.0 / 0.9);
393     double hRad = Math.toRadians(getHue());
394 
395     double eHue = 0.25 * (Math.cos(hRad + 2.0) + 3.8);
396     double ac =
397         viewingConditions.getAw()
398             * Math.pow(getJ() / 100.0, 1.0 / viewingConditions.getC() / viewingConditions.getZ());
399     double p1 = eHue * (50000.0 / 13.0) * viewingConditions.getNc() * viewingConditions.getNcb();
400     double p2 = (ac / viewingConditions.getNbb());
401 
402     double hSin = Math.sin(hRad);
403     double hCos = Math.cos(hRad);
404 
405     double gamma = 23.0 * (p2 + 0.305) * t / (23.0 * p1 + 11.0 * t * hCos + 108.0 * t * hSin);
406     double a = gamma * hCos;
407     double b = gamma * hSin;
408     double rA = (460.0 * p2 + 451.0 * a + 288.0 * b) / 1403.0;
409     double gA = (460.0 * p2 - 891.0 * a - 261.0 * b) / 1403.0;
410     double bA = (460.0 * p2 - 220.0 * a - 6300.0 * b) / 1403.0;
411 
412     double rCBase = max(0, (27.13 * Math.abs(rA)) / (400.0 - Math.abs(rA)));
413     double rC =
414         Math.signum(rA) * (100.0 / viewingConditions.getFl()) * Math.pow(rCBase, 1.0 / 0.42);
415     double gCBase = max(0, (27.13 * Math.abs(gA)) / (400.0 - Math.abs(gA)));
416     double gC =
417         Math.signum(gA) * (100.0 / viewingConditions.getFl()) * Math.pow(gCBase, 1.0 / 0.42);
418     double bCBase = max(0, (27.13 * Math.abs(bA)) / (400.0 - Math.abs(bA)));
419     double bC =
420         Math.signum(bA) * (100.0 / viewingConditions.getFl()) * Math.pow(bCBase, 1.0 / 0.42);
421     double rF = rC / viewingConditions.getRgbD()[0];
422     double gF = gC / viewingConditions.getRgbD()[1];
423     double bF = bC / viewingConditions.getRgbD()[2];
424 
425     double[][] matrix = CAM16RGB_TO_XYZ;
426     double x = (rF * matrix[0][0]) + (gF * matrix[0][1]) + (bF * matrix[0][2]);
427     double y = (rF * matrix[1][0]) + (gF * matrix[1][1]) + (bF * matrix[1][2]);
428     double z = (rF * matrix[2][0]) + (gF * matrix[2][1]) + (bF * matrix[2][2]);
429 
430     if (returnArray != null) {
431       returnArray[0] = x;
432       returnArray[1] = y;
433       returnArray[2] = z;
434       return returnArray;
435     } else {
436       return new double[] {x, y, z};
437     }
438   }
439 }
440