• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 import android.annotation.NonNull;
20 import android.ravenwood.annotation.RavenwoodKeepWholeClass;
21 import android.util.MathUtils;
22 
23 import com.android.internal.annotations.VisibleForTesting;
24 
25 /**
26  * The frame, or viewing conditions, where a color was seen. Used, along with a color, to create a
27  * color appearance model representing the color.
28  *
29  * <p>To convert a traditional color to a color appearance model, it requires knowing what
30  * conditions the color was observed in. Our perception of color depends on, for example, the tone
31  * of the light illuminating the color, how bright that light was, etc.
32  *
33  * <p>This class is modelled separately from the color appearance model itself because there are a
34  * number of calculations during the color => CAM conversion process that depend only on the viewing
35  * conditions. Caching those calculations in a Frame instance saves a significant amount of time.
36  */
37 @RavenwoodKeepWholeClass
38 public final class Frame {
39     // Standard viewing conditions assumed in RGB specification - Stokes, Anderson, Chandrasekar,
40     // Motta - A Standard Default Color Space for the Internet: sRGB, 1996.
41     //
42     // White point = D65
43     // Luminance of adapting field: 200 / Pi / 5, units are cd/m^2.
44     //   sRGB ambient illuminance = 64 lux (per sRGB spec). However, the spec notes this is
45     //     artificially low and based on monitors in 1990s. Use 200, the sRGB spec says this is the
46     //     real average, and a survey of lux values on Wikipedia confirms this is a comfortable
47     //     default: somewhere between a very dark overcast day and office lighting.
48     //   Per CAM16 introduction paper (Li et al, 2017) Ew = pi * lw, and La = lw * Yb/Yw
49     //   Ew = ambient environment luminance, in lux.
50     //   Yb/Yw is taken to be midgray, ~20% relative luminance (XYZ Y 18.4, CIELAB L* 50).
51     //   Therefore La = (Ew / pi) * .184
52     //   La = 200 / pi * .184
53     // Image surround to 10 degrees = ~20% relative luminance = CIELAB L* 50
54     //
55     // Not from sRGB standard:
56     // Surround = average, 2.0.
57     // Discounting illuminant = false, doesn't occur for self-luminous displays
58     public static final Frame DEFAULT =
59             Frame.make(
60                     CamUtils.WHITE_POINT_D65,
61                     (float) (200.0f / Math.PI * CamUtils.yFromLstar(50.0f) / 100.f), 50.0f, 2.0f,
62                     false);
63 
64     private final float mAw;
65     private final float mNbb;
66     private final float mNcb;
67     private final float mC;
68     private final float mNc;
69     private final float mN;
70     private final float[] mRgbD;
71     private final float mFl;
72     private final float mFlRoot;
73     private final float mZ;
74 
75     @VisibleForTesting
getAw()76     public float getAw() {
77         return mAw;
78     }
79 
80     @VisibleForTesting
getN()81     public float getN() {
82         return mN;
83     }
84 
85     @VisibleForTesting
getNbb()86     public float getNbb() {
87         return mNbb;
88     }
89 
getNcb()90     float getNcb() {
91         return mNcb;
92     }
93 
getC()94     float getC() {
95         return mC;
96     }
97 
getNc()98     float getNc() {
99         return mNc;
100     }
101 
102     @VisibleForTesting
103     @NonNull
getRgbD()104     public float[] getRgbD() {
105         return mRgbD;
106     }
107 
getFl()108     float getFl() {
109         return mFl;
110     }
111 
112     @VisibleForTesting
113     @NonNull
getFlRoot()114     public float getFlRoot() {
115         return mFlRoot;
116     }
117 
getZ()118     float getZ() {
119         return mZ;
120     }
121 
Frame(float n, float aw, float nbb, float ncb, float c, float nc, float[] rgbD, float fl, float fLRoot, float z)122     private Frame(float n, float aw, float nbb, float ncb, float c, float nc, float[] rgbD,
123             float fl, float fLRoot, float z) {
124         mN = n;
125         mAw = aw;
126         mNbb = nbb;
127         mNcb = ncb;
128         mC = c;
129         mNc = nc;
130         mRgbD = rgbD;
131         mFl = fl;
132         mFlRoot = fLRoot;
133         mZ = z;
134     }
135 
136     /** Create a custom frame. */
137     @NonNull
make(@onNull float[] whitepoint, float adaptingLuminance, float backgroundLstar, float surround, boolean discountingIlluminant)138     public static Frame make(@NonNull float[] whitepoint, float adaptingLuminance,
139             float backgroundLstar, float surround, boolean discountingIlluminant) {
140         // Transform white point XYZ to 'cone'/'rgb' responses
141         float[][] matrix = CamUtils.XYZ_TO_CAM16RGB;
142         float[] xyz = whitepoint;
143         float rW = (xyz[0] * matrix[0][0]) + (xyz[1] * matrix[0][1]) + (xyz[2] * matrix[0][2]);
144         float gW = (xyz[0] * matrix[1][0]) + (xyz[1] * matrix[1][1]) + (xyz[2] * matrix[1][2]);
145         float bW = (xyz[0] * matrix[2][0]) + (xyz[1] * matrix[2][1]) + (xyz[2] * matrix[2][2]);
146 
147         // Scale input surround, domain (0, 2), to CAM16 surround, domain (0.8, 1.0)
148         float f = 0.8f + (surround / 10.0f);
149         // "Exponential non-linearity"
150         float c = (f >= 0.9) ? MathUtils.lerp(0.59f, 0.69f, ((f - 0.9f) * 10.0f)) : MathUtils.lerp(
151                 0.525f, 0.59f, ((f - 0.8f) * 10.0f));
152         // Calculate degree of adaptation to illuminant
153         float d = discountingIlluminant ? 1.0f : f * (1.0f - ((1.0f / 3.6f) * (float) Math.exp(
154                 (-adaptingLuminance - 42.0f) / 92.0f)));
155         // Per Li et al, if D is greater than 1 or less than 0, set it to 1 or 0.
156         d = (d > 1.0) ? 1.0f : (d < 0.0) ? 0.0f : d;
157         // Chromatic induction factor
158         float nc = f;
159 
160         // Cone responses to the whitepoint, adjusted for illuminant discounting.
161         //
162         // Why use 100.0 instead of the white point's relative luminance?
163         //
164         // Some papers and implementations, for both CAM02 and CAM16, use the Y
165         // value of the reference white instead of 100. Fairchild's Color Appearance
166         // Models (3rd edition) notes that this is in error: it was included in the
167         // CIE 2004a report on CIECAM02, but, later parts of the conversion process
168         // account for scaling of appearance relative to the white point relative
169         // luminance. This part should simply use 100 as luminance.
170         float[] rgbD = new float[]{d * (100.0f / rW) + 1.0f - d, d * (100.0f / gW) + 1.0f - d,
171                 d * (100.0f / bW) + 1.0f - d, };
172         // Luminance-level adaptation factor
173         float k = 1.0f / (5.0f * adaptingLuminance + 1.0f);
174         float k4 = k * k * k * k;
175         float k4F = 1.0f - k4;
176         float fl = (k4 * adaptingLuminance) + (0.1f * k4F * k4F * (float) Math.cbrt(
177                 5.0 * adaptingLuminance));
178 
179         // Intermediate factor, ratio of background relative luminance to white relative luminance
180         float n = (float) CamUtils.yFromLstar(backgroundLstar) / whitepoint[1];
181 
182         // Base exponential nonlinearity
183         // note Schlomer 2018 has a typo and uses 1.58, the correct factor is 1.48
184         float z = 1.48f + (float) Math.sqrt(n);
185 
186         // Luminance-level induction factors
187         float nbb = 0.725f / (float) Math.pow(n, 0.2);
188         float ncb = nbb;
189 
190         // Discounted cone responses to the white point, adjusted for post-chromatic
191         // adaptation perceptual nonlinearities.
192         float[] rgbAFactors = new float[]{(float) Math.pow(fl * rgbD[0] * rW / 100.0, 0.42),
193                 (float) Math.pow(fl * rgbD[1] * gW / 100.0, 0.42), (float) Math.pow(
194                 fl * rgbD[2] * bW / 100.0, 0.42)};
195 
196         float[] rgbA = new float[]{(400.0f * rgbAFactors[0]) / (rgbAFactors[0] + 27.13f),
197                 (400.0f * rgbAFactors[1]) / (rgbAFactors[1] + 27.13f),
198                 (400.0f * rgbAFactors[2]) / (rgbAFactors[2] + 27.13f), };
199 
200         float aw = ((2.0f * rgbA[0]) + rgbA[1] + (0.05f * rgbA[2])) * nbb;
201 
202         return new Frame(n, aw, nbb, ncb, c, nc, rgbD, fl, (float) Math.pow(fl, 0.25), z);
203     }
204 }
205