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