Color appearance model library
Create colors using CAM16 hue and chroma, L*a*b* L*, and map them to RGB without distortion. Test: atest FrameworksCoreTests:com.android.internal.graphics.cam .CamTest, all passed. No link, atest/my build are broken and cannot upload results. Bug: 186640057 Change-Id: I64b605dd452d64e57be0047d2e4672917c01c2a8
This commit is contained in:
@@ -22,6 +22,8 @@ import android.annotation.IntRange;
|
||||
import android.annotation.NonNull;
|
||||
import android.graphics.Color;
|
||||
|
||||
import com.android.internal.graphics.cam.Cam;
|
||||
|
||||
/**
|
||||
* Copied from: frameworks/support/core-utils/java/android/support/v4/graphics/ColorUtils.java
|
||||
*
|
||||
@@ -332,6 +334,35 @@ public final class ColorUtils {
|
||||
return Color.rgb(r, g, b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the ARGB color to a color appearance model.
|
||||
*
|
||||
* The color appearance model is based on CAM16 hue and chroma, using L*a*b*'s L* as the
|
||||
* third dimension.
|
||||
*
|
||||
* @param color the ARGB color to convert. The alpha component is ignored.
|
||||
*/
|
||||
public static Cam colorToCAM(@ColorInt int color) {
|
||||
return Cam.fromInt(color);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a color appearance model representation to an ARGB color.
|
||||
*
|
||||
* Note: the returned color may have a lower chroma than requested. Whether a chroma is
|
||||
* available depends on luminance. For example, there's no such thing as a high chroma light
|
||||
* red, due to the limitations of our eyes and/or physics. If the requested chroma is
|
||||
* unavailable, the highest possible chroma at the requested luminance is returned.
|
||||
*
|
||||
* @param hue hue, in degrees, in CAM coordinates
|
||||
* @param chroma chroma in CAM coordinates.
|
||||
* @param lstar perceptual luminance, L* in L*a*b*
|
||||
*/
|
||||
@ColorInt
|
||||
public static int CAMToColor(float hue, float chroma, float lstar) {
|
||||
return Cam.getInt(hue, chroma, lstar);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the alpha component of {@code color} to be {@code alpha}.
|
||||
*/
|
||||
|
||||
509
core/java/com/android/internal/graphics/cam/Cam.java
Normal file
509
core/java/com/android/internal/graphics/cam/Cam.java
Normal file
@@ -0,0 +1,509 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.internal.graphics.cam;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
|
||||
import com.android.internal.graphics.ColorUtils;
|
||||
|
||||
/**
|
||||
* A color appearance model, based on CAM16, extended to use L* as the lightness dimension, and
|
||||
* coupled to a gamut mapping algorithm. Creates a color system, enables a digital design system.
|
||||
*/
|
||||
public class Cam {
|
||||
// The maximum difference between the requested L* and the L* returned.
|
||||
private static final float DL_MAX = 0.2f;
|
||||
// The maximum color distance, in CAM16-UCS, between a requested color and the color returned.
|
||||
private static final float DE_MAX = 1.0f;
|
||||
// When the delta between the floor & ceiling of a binary search for chroma is less than this,
|
||||
// the binary search terminates.
|
||||
private static final float CHROMA_SEARCH_ENDPOINT = 0.4f;
|
||||
// When the delta between the floor & ceiling of a binary search for J, lightness in CAM16,
|
||||
// is less than this, the binary search terminates.
|
||||
private static final float LIGHTNESS_SEARCH_ENDPOINT = 0.01f;
|
||||
|
||||
// CAM16 color dimensions, see getters for documentation.
|
||||
private final float mHue;
|
||||
private final float mChroma;
|
||||
private final float mJ;
|
||||
private final float mQ;
|
||||
private final float mM;
|
||||
private final float mS;
|
||||
|
||||
// Coordinates in UCS space. Used to determine color distance, like delta E equations in L*a*b*.
|
||||
private final float mJstar;
|
||||
private final float mAstar;
|
||||
private final float mBstar;
|
||||
|
||||
/** Hue in CAM16 */
|
||||
public float getHue() {
|
||||
return mHue;
|
||||
}
|
||||
|
||||
/** Chroma in CAM16 */
|
||||
public float getChroma() {
|
||||
return mChroma;
|
||||
}
|
||||
|
||||
/** Lightness in CAM16 */
|
||||
public float getJ() {
|
||||
return mJ;
|
||||
}
|
||||
|
||||
/**
|
||||
* Brightness in CAM16.
|
||||
*
|
||||
* <p>Prefer lightness, brightness is an absolute quantity. For example, a sheet of white paper
|
||||
* is much brighter viewed in sunlight than in indoor light, but it is the lightest object under
|
||||
* any lighting.
|
||||
*/
|
||||
public float getQ() {
|
||||
return mQ;
|
||||
}
|
||||
|
||||
/**
|
||||
* Colorfulness in CAM16.
|
||||
*
|
||||
* <p>Prefer chroma, colorfulness is an absolute quantity. For example, a yellow toy car is much
|
||||
* more colorful outside than inside, but it has the same chroma in both environments.
|
||||
*/
|
||||
public float getM() {
|
||||
return mM;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saturation in CAM16.
|
||||
*
|
||||
* <p>Colorfulness in proportion to brightness. Prefer chroma, saturation measures colorfulness
|
||||
* relative to the color's own brightness, where chroma is colorfulness relative to white.
|
||||
*/
|
||||
public float getS() {
|
||||
return mS;
|
||||
}
|
||||
|
||||
/** Lightness coordinate in CAM16-UCS */
|
||||
public float getJstar() {
|
||||
return mJstar;
|
||||
}
|
||||
|
||||
/** a* coordinate in CAM16-UCS */
|
||||
public float getAstar() {
|
||||
return mAstar;
|
||||
}
|
||||
|
||||
/** b* coordinate in CAM16-UCS */
|
||||
public float getBstar() {
|
||||
return mBstar;
|
||||
}
|
||||
|
||||
/** Construct a CAM16 color */
|
||||
Cam(float hue, float chroma, float j, float q, float m, float s, float jstar, float astar,
|
||||
float bstar) {
|
||||
mHue = hue;
|
||||
mChroma = chroma;
|
||||
mJ = j;
|
||||
mQ = q;
|
||||
mM = m;
|
||||
mS = s;
|
||||
mJstar = jstar;
|
||||
mAstar = astar;
|
||||
mBstar = bstar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a hue & chroma in CAM16, L* in L*a*b*, return an ARGB integer. The chroma of the color
|
||||
* returned may, and frequently will, be lower than requested. Assumes the color is viewed in
|
||||
* the
|
||||
* frame defined by the sRGB standard.
|
||||
*/
|
||||
public static int getInt(float hue, float chroma, float lstar) {
|
||||
return getInt(hue, chroma, lstar, Frame.DEFAULT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a color appearance model from a ARGB integer representing a color. It is assumed the
|
||||
* color was viewed in the frame defined in the sRGB standard.
|
||||
*/
|
||||
@NonNull
|
||||
public static Cam fromInt(int argb) {
|
||||
return fromIntInFrame(argb, Frame.DEFAULT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a color appearance model from a ARGB integer representing a color, specifying the
|
||||
* frame in which the color was viewed. Prefer Cam.fromInt.
|
||||
*/
|
||||
@NonNull
|
||||
public static Cam fromIntInFrame(int argb, @NonNull Frame frame) {
|
||||
// Transform ARGB int to XYZ
|
||||
float[] xyz = CamUtils.xyzFromInt(argb);
|
||||
|
||||
// Transform XYZ to 'cone'/'rgb' responses
|
||||
float[][] matrix = CamUtils.XYZ_TO_CAM16RGB;
|
||||
float rT = (xyz[0] * matrix[0][0]) + (xyz[1] * matrix[0][1]) + (xyz[2] * matrix[0][2]);
|
||||
float gT = (xyz[0] * matrix[1][0]) + (xyz[1] * matrix[1][1]) + (xyz[2] * matrix[1][2]);
|
||||
float bT = (xyz[0] * matrix[2][0]) + (xyz[1] * matrix[2][1]) + (xyz[2] * matrix[2][2]);
|
||||
|
||||
// Discount illuminant
|
||||
float rD = frame.getRgbD()[0] * rT;
|
||||
float gD = frame.getRgbD()[1] * gT;
|
||||
float bD = frame.getRgbD()[2] * bT;
|
||||
|
||||
// Chromatic adaptation
|
||||
float rAF = (float) Math.pow(frame.getFl() * Math.abs(rD) / 100.0, 0.42);
|
||||
float gAF = (float) Math.pow(frame.getFl() * Math.abs(gD) / 100.0, 0.42);
|
||||
float bAF = (float) Math.pow(frame.getFl() * Math.abs(bD) / 100.0, 0.42);
|
||||
float rA = Math.signum(rD) * 400.0f * rAF / (rAF + 27.13f);
|
||||
float gA = Math.signum(gD) * 400.0f * gAF / (gAF + 27.13f);
|
||||
float bA = Math.signum(bD) * 400.0f * bAF / (bAF + 27.13f);
|
||||
|
||||
// redness-greenness
|
||||
float a = (float) (11.0 * rA + -12.0 * gA + bA) / 11.0f;
|
||||
// yellowness-blueness
|
||||
float b = (float) (rA + gA - 2.0 * bA) / 9.0f;
|
||||
|
||||
// auxiliary components
|
||||
float u = (20.0f * rA + 20.0f * gA + 21.0f * bA) / 20.0f;
|
||||
float p2 = (40.0f * rA + 20.0f * gA + bA) / 20.0f;
|
||||
|
||||
// hue
|
||||
float atan2 = (float) Math.atan2(b, a);
|
||||
float atanDegrees = atan2 * 180.0f / (float) Math.PI;
|
||||
float hue =
|
||||
atanDegrees < 0
|
||||
? atanDegrees + 360.0f
|
||||
: atanDegrees >= 360 ? atanDegrees - 360.0f : atanDegrees;
|
||||
float hueRadians = hue * (float) Math.PI / 180.0f;
|
||||
|
||||
// achromatic response to color
|
||||
float ac = p2 * frame.getNbb();
|
||||
|
||||
// CAM16 lightness and brightness
|
||||
float j = 100.0f * (float) Math.pow(ac / frame.getAw(), frame.getC() * frame.getZ());
|
||||
float q =
|
||||
4.0f
|
||||
/ frame.getC()
|
||||
* (float) Math.sqrt(j / 100.0f)
|
||||
* (frame.getAw() + 4.0f)
|
||||
* frame.getFlRoot();
|
||||
|
||||
// CAM16 chroma, colorfulness, and saturation.
|
||||
float huePrime = (hue < 20.14) ? hue + 360 : hue;
|
||||
float eHue = 0.25f * (float) (Math.cos(huePrime * Math.PI / 180.0 + 2.0) + 3.8);
|
||||
float p1 = 50000.0f / 13.0f * eHue * frame.getNc() * frame.getNcb();
|
||||
float t = p1 * (float) Math.sqrt(a * a + b * b) / (u + 0.305f);
|
||||
float alpha =
|
||||
(float) Math.pow(t, 0.9) * (float) Math.pow(1.64 - Math.pow(0.29, frame.getN()),
|
||||
0.73);
|
||||
// CAM16 chroma, colorfulness, saturation
|
||||
float c = alpha * (float) Math.sqrt(j / 100.0);
|
||||
float m = c * frame.getFlRoot();
|
||||
float s = 50.0f * (float) Math.sqrt((alpha * frame.getC()) / (frame.getAw() + 4.0f));
|
||||
|
||||
// CAM16-UCS components
|
||||
float jstar = (1.0f + 100.0f * 0.007f) * j / (1.0f + 0.007f * j);
|
||||
float mstar = 1.0f / 0.0228f * (float) Math.log(1.0f + 0.0228f * m);
|
||||
float astar = mstar * (float) Math.cos(hueRadians);
|
||||
float bstar = mstar * (float) Math.sin(hueRadians);
|
||||
|
||||
return new Cam(hue, c, j, q, m, s, jstar, astar, bstar);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a CAM from lightness, chroma, and hue coordinates. It is assumed those coordinates
|
||||
* were measured in the sRGB standard frame.
|
||||
*/
|
||||
@NonNull
|
||||
private static Cam fromJch(float j, float c, float h) {
|
||||
return fromJchInFrame(j, c, h, Frame.DEFAULT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a CAM from lightness, chroma, and hue coordinates, and also specify the frame in which
|
||||
* the color is being viewed.
|
||||
*/
|
||||
@NonNull
|
||||
private static Cam fromJchInFrame(float j, float c, float h, Frame frame) {
|
||||
float q =
|
||||
4.0f
|
||||
/ frame.getC()
|
||||
* (float) Math.sqrt(j / 100.0)
|
||||
* (frame.getAw() + 4.0f)
|
||||
* frame.getFlRoot();
|
||||
float m = c * frame.getFlRoot();
|
||||
float alpha = c / (float) Math.sqrt(j / 100.0);
|
||||
float s = 50.0f * (float) Math.sqrt((alpha * frame.getC()) / (frame.getAw() + 4.0f));
|
||||
|
||||
float hueRadians = h * (float) Math.PI / 180.0f;
|
||||
float jstar = (1.0f + 100.0f * 0.007f) * j / (1.0f + 0.007f * j);
|
||||
float mstar = 1.0f / 0.0228f * (float) Math.log(1.0 + 0.0228 * m);
|
||||
float astar = mstar * (float) Math.cos(hueRadians);
|
||||
float bstar = mstar * (float) Math.sin(hueRadians);
|
||||
return new Cam(h, c, j, q, m, s, jstar, astar, bstar);
|
||||
}
|
||||
|
||||
/**
|
||||
* Distance in CAM16-UCS space between two colors.
|
||||
*
|
||||
* <p>Much like L*a*b* was designed to measure distance between colors, the CAM16 standard
|
||||
* defined a color space called CAM16-UCS to measure distance between CAM16 colors.
|
||||
*/
|
||||
public float distance(@NonNull Cam other) {
|
||||
float dJ = getJstar() - other.getJstar();
|
||||
float dA = getAstar() - other.getAstar();
|
||||
float dB = getBstar() - other.getBstar();
|
||||
double dEPrime = Math.sqrt(dJ * dJ + dA * dA + dB * dB);
|
||||
double dE = 1.41 * Math.pow(dEPrime, 0.63);
|
||||
return (float) dE;
|
||||
}
|
||||
|
||||
/** Returns perceived color as an ARGB integer, as viewed in standard sRGB frame. */
|
||||
public int viewedInSrgb() {
|
||||
return viewed(Frame.DEFAULT);
|
||||
}
|
||||
|
||||
/** Returns color perceived in a frame as an ARGB integer. */
|
||||
public int viewed(@NonNull Frame frame) {
|
||||
float alpha =
|
||||
(getChroma() == 0.0 || getJ() == 0.0)
|
||||
? 0.0f
|
||||
: getChroma() / (float) Math.sqrt(getJ() / 100.0);
|
||||
|
||||
float t =
|
||||
(float) Math.pow(alpha / Math.pow(1.64 - Math.pow(0.29, frame.getN()), 0.73),
|
||||
1.0 / 0.9);
|
||||
float hRad = getHue() * (float) Math.PI / 180.0f;
|
||||
|
||||
float eHue = 0.25f * (float) (Math.cos(hRad + 2.0) + 3.8);
|
||||
float ac = frame.getAw() * (float) Math.pow(getJ() / 100.0,
|
||||
1.0 / frame.getC() / frame.getZ());
|
||||
float p1 = eHue * (50000.0f / 13.0f) * frame.getNc() * frame.getNcb();
|
||||
float p2 = (ac / frame.getNbb());
|
||||
|
||||
float hSin = (float) Math.sin(hRad);
|
||||
float hCos = (float) Math.cos(hRad);
|
||||
|
||||
float gamma =
|
||||
23.0f * (p2 + 0.305f) * t / (23.0f * p1 + 11.0f * t * hCos + 108.0f * t * hSin);
|
||||
float a = gamma * hCos;
|
||||
float b = gamma * hSin;
|
||||
float rA = (460.0f * p2 + 451.0f * a + 288.0f * b) / 1403.0f;
|
||||
float gA = (460.0f * p2 - 891.0f * a - 261.0f * b) / 1403.0f;
|
||||
float bA = (460.0f * p2 - 220.0f * a - 6300.0f * b) / 1403.0f;
|
||||
|
||||
float rCBase = (float) Math.max(0, (27.13 * Math.abs(rA)) / (400.0 - Math.abs(rA)));
|
||||
float rC = Math.signum(rA) * (100.0f / frame.getFl()) * (float) Math.pow(rCBase,
|
||||
1.0 / 0.42);
|
||||
float gCBase = (float) Math.max(0, (27.13 * Math.abs(gA)) / (400.0 - Math.abs(gA)));
|
||||
float gC = Math.signum(gA) * (100.0f / frame.getFl()) * (float) Math.pow(gCBase,
|
||||
1.0 / 0.42);
|
||||
float bCBase = (float) Math.max(0, (27.13 * Math.abs(bA)) / (400.0 - Math.abs(bA)));
|
||||
float bC = Math.signum(bA) * (100.0f / frame.getFl()) * (float) Math.pow(bCBase,
|
||||
1.0 / 0.42);
|
||||
float rF = rC / frame.getRgbD()[0];
|
||||
float gF = gC / frame.getRgbD()[1];
|
||||
float bF = bC / frame.getRgbD()[2];
|
||||
|
||||
|
||||
float[][] matrix = CamUtils.CAM16RGB_TO_XYZ;
|
||||
float x = (rF * matrix[0][0]) + (gF * matrix[0][1]) + (bF * matrix[0][2]);
|
||||
float y = (rF * matrix[1][0]) + (gF * matrix[1][1]) + (bF * matrix[1][2]);
|
||||
float z = (rF * matrix[2][0]) + (gF * matrix[2][1]) + (bF * matrix[2][2]);
|
||||
|
||||
int argb = ColorUtils.XYZToColor(x, y, z);
|
||||
return argb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a hue & chroma in CAM16, L* in L*a*b*, and the frame in which the color will be
|
||||
* viewed,
|
||||
* return an ARGB integer.
|
||||
*
|
||||
* <p>The chroma of the color returned may, and frequently will, be lower than requested. This
|
||||
* is
|
||||
* a fundamental property of color that cannot be worked around by engineering. For example, a
|
||||
* red
|
||||
* hue, with high chroma, and high L* does not exist: red hues have a maximum chroma below 10
|
||||
* in
|
||||
* light shades, creating pink.
|
||||
*/
|
||||
public static int getInt(float hue, float chroma, float lstar, @NonNull Frame frame) {
|
||||
// This is a crucial routine for building a color system, CAM16 itself is not sufficient.
|
||||
//
|
||||
// * Why these dimensions?
|
||||
// Hue and chroma from CAM16 are used because they're the most accurate measures of those
|
||||
// quantities. L* from L*a*b* is used because it correlates with luminance, luminance is
|
||||
// used to measure contrast for a11y purposes, thus providing a key constraint on what
|
||||
// colors
|
||||
// can be used.
|
||||
//
|
||||
// * Why is this routine required to build a color system?
|
||||
// In all perceptually accurate color spaces (i.e. L*a*b* and later), `chroma` may be
|
||||
// impossible for a given `hue` and `lstar`.
|
||||
// For example, a high chroma light red does not exist - chroma is limited to below 10 at
|
||||
// light red shades, we call that pink. High chroma light green does exist, but not dark
|
||||
// Also, when converting from another color space to RGB, the color may not be able to be
|
||||
// represented in RGB. In those cases, the conversion process ends with RGB values
|
||||
// outside 0-255
|
||||
// The vast majority of color libraries surveyed simply round to 0 to 255. That is not an
|
||||
// option for this library, as it distorts the expected luminance, and thus the expected
|
||||
// contrast needed for a11y
|
||||
//
|
||||
// * What does this routine do?
|
||||
// Dealing with colors in one color space not fitting inside RGB is, loosely referred to as
|
||||
// gamut mapping or tone mapping. These algorithms are traditionally idiosyncratic, there is
|
||||
// no universal answer. However, because the intent of this library is to build a system for
|
||||
// digital design, and digital design uses luminance to measure contrast/a11y, we have one
|
||||
// very important constraint that leads to an objective algorithm: the L* of the returned
|
||||
// color _must_ match the requested L*.
|
||||
//
|
||||
// Intuitively, if the color must be distorted to fit into the RGB gamut, and the L*
|
||||
// requested *must* be fulfilled, than the hue or chroma of the returned color will need
|
||||
// to be different from the requested hue/chroma.
|
||||
//
|
||||
// After exploring both options, it was more intuitive that if the requested chroma could
|
||||
// not be reached, it used the highest possible chroma. The alternative was finding the
|
||||
// closest hue where the requested chroma could be reached, but that is not nearly as
|
||||
// intuitive, as the requested hue is so fundamental to the color description.
|
||||
|
||||
// If the color doesn't have meaningful chroma, return a gray with the requested Lstar.
|
||||
//
|
||||
// Yellows are very chromatic at L = 100, and blues are very chromatic at L = 0. All the
|
||||
// other hues are white at L = 100, and black at L = 0. To preserve consistency for users of
|
||||
// this system, it is better to simply return white at L* > 99, and black and L* < 0.
|
||||
if (chroma < 1.0 || Math.round(lstar) <= 0.0 || Math.round(lstar) >= 100.0) {
|
||||
return CamUtils.intFromLstar(lstar);
|
||||
}
|
||||
|
||||
hue = hue < 0 ? 0 : Math.min(360, hue);
|
||||
|
||||
// The highest chroma possible. Updated as binary search proceeds.
|
||||
float high = chroma;
|
||||
|
||||
// The guess for the current binary search iteration. Starts off at the highest chroma,
|
||||
// thus, if a color is possible at the requested chroma, the search can stop after one try.
|
||||
float mid = chroma;
|
||||
float low = 0.0f;
|
||||
boolean isFirstLoop = true;
|
||||
|
||||
Cam answer = null;
|
||||
|
||||
while (Math.abs(low - high) >= CHROMA_SEARCH_ENDPOINT) {
|
||||
// Given the current chroma guess, mid, and the desired hue, find J, lightness in
|
||||
// CAM16 color space, that creates a color with L* = `lstar` in the L*a*b* color space.
|
||||
Cam possibleAnswer = findCamByJ(hue, mid, lstar);
|
||||
|
||||
if (isFirstLoop) {
|
||||
if (possibleAnswer != null) {
|
||||
return possibleAnswer.viewed(frame);
|
||||
} else {
|
||||
// If this binary search iteration was the first iteration, and this point
|
||||
// has been reached, it means the requested chroma was not available at the
|
||||
// requested hue and L*.
|
||||
// Proceed to a traditional binary search that starts at the midpoint between
|
||||
// the requested chroma and 0.
|
||||
isFirstLoop = false;
|
||||
mid = low + (high - low) / 2.0f;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (possibleAnswer == null) {
|
||||
// There isn't a CAM16 J that creates a color with L* `lstar`. Try a lower chroma.
|
||||
high = mid;
|
||||
} else {
|
||||
answer = possibleAnswer;
|
||||
// It is possible to create a color. Try higher chroma.
|
||||
low = mid;
|
||||
}
|
||||
|
||||
mid = low + (high - low) / 2.0f;
|
||||
}
|
||||
|
||||
// There was no answer: meaning, for the desired hue, there was no chroma low enough to
|
||||
// generate a color with the desired L*.
|
||||
// All values of L* are possible when there is 0 chroma. Return a color with 0 chroma, i.e.
|
||||
// a shade of gray, with the desired L*.
|
||||
if (answer == null) {
|
||||
return CamUtils.intFromLstar(lstar);
|
||||
}
|
||||
|
||||
return answer.viewed(frame);
|
||||
}
|
||||
|
||||
// Find J, lightness in CAM16 color space, that creates a color with L* = `lstar` in the L*a*b*
|
||||
// color space.
|
||||
//
|
||||
// Returns null if no J could be found that generated a color with L* `lstar`.
|
||||
@Nullable
|
||||
private static Cam findCamByJ(float hue, float chroma, float lstar) {
|
||||
float low = 0.0f;
|
||||
float high = 100.0f;
|
||||
float mid = 0.0f;
|
||||
float bestdL = 1000.0f;
|
||||
float bestdE = 1000.0f;
|
||||
|
||||
Cam bestCam = null;
|
||||
while (Math.abs(low - high) > LIGHTNESS_SEARCH_ENDPOINT) {
|
||||
mid = low + (high - low) / 2;
|
||||
// Create the intended CAM color
|
||||
Cam camBeforeClip = Cam.fromJch(mid, chroma, hue);
|
||||
// Convert the CAM color to RGB. If the color didn't fit in RGB, during the conversion,
|
||||
// the initial RGB values will be outside 0 to 255. The final RGB values are clipped to
|
||||
// 0 to 255, distorting the intended color.
|
||||
int clipped = camBeforeClip.viewedInSrgb();
|
||||
float clippedLstar = CamUtils.lstarFromInt(clipped);
|
||||
float dL = Math.abs(lstar - clippedLstar);
|
||||
|
||||
// If the clipped color's L* is within error margin...
|
||||
if (dL < DL_MAX) {
|
||||
// ...check if the CAM equivalent of the clipped color is far away from intended CAM
|
||||
// color. For the intended color, use lightness and chroma from the clipped color,
|
||||
// and the intended hue. Callers are wondering what the lightness is, they know
|
||||
// chroma may be distorted, so the only concern here is if the hue slipped too far.
|
||||
Cam camClipped = Cam.fromInt(clipped);
|
||||
float dE = camClipped.distance(
|
||||
Cam.fromJch(camClipped.getJ(), camClipped.getChroma(), hue));
|
||||
if (dE <= DE_MAX) {
|
||||
bestdL = dL;
|
||||
bestdE = dE;
|
||||
bestCam = camClipped;
|
||||
}
|
||||
}
|
||||
|
||||
// If there's no error at all, there's no need to search more.
|
||||
//
|
||||
// Note: this happens much more frequently than expected, but this is a very delicate
|
||||
// property which relies on extremely precise sRGB <=> XYZ calculations, as well as fine
|
||||
// tuning of the constants that determine error margins and when the binary search can
|
||||
// terminate.
|
||||
if (bestdL == 0 && bestdE == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (clippedLstar < lstar) {
|
||||
low = mid;
|
||||
} else {
|
||||
high = mid;
|
||||
}
|
||||
}
|
||||
|
||||
return bestCam;
|
||||
}
|
||||
|
||||
}
|
||||
165
core/java/com/android/internal/graphics/cam/CamUtils.java
Normal file
165
core/java/com/android/internal/graphics/cam/CamUtils.java
Normal file
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.internal.graphics.cam;
|
||||
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.graphics.Color;
|
||||
|
||||
import com.android.internal.graphics.ColorUtils;
|
||||
|
||||
/**
|
||||
* Collection of methods for transforming between color spaces.
|
||||
*
|
||||
* <p>Methods are named $xFrom$Y. For example, lstarFromInt() returns L* from an ARGB integer.
|
||||
*
|
||||
* <p>These methods, generally, convert colors between the L*a*b*, XYZ, and sRGB spaces.
|
||||
*
|
||||
* <p>L*a*b* is a perceptually accurate color space. This is particularly important in the L*
|
||||
* dimension: it measures luminance and unlike lightness measures traditionally used in UI work via
|
||||
* RGB or HSL, this luminance transitions smoothly, permitting creation of pleasing shades of a
|
||||
* color, and more pleasing transitions between colors.
|
||||
*
|
||||
* <p>XYZ is commonly used as an intermediate color space for converting between one color space to
|
||||
* another. For example, to convert RGB to L*a*b*, first RGB is converted to XYZ, then XYZ is
|
||||
* convered to L*a*b*.
|
||||
*
|
||||
* <p>sRGB is a "specification originated from work in 1990s through cooperation by Hewlett-Packard
|
||||
* and Microsoft, and it was designed to be a standard definition of RGB for the internet, which it
|
||||
* indeed became...The standard is based on a sampling of computer monitors at the time...The whole
|
||||
* idea of sRGB is that if everyone assumed that RGB meant the same thing, then the results would be
|
||||
* consistent, and reasonably good. It worked." - Fairchild, Color Models and Systems: Handbook of
|
||||
* Color Psychology, 2015
|
||||
*/
|
||||
public final class CamUtils {
|
||||
private CamUtils() {
|
||||
}
|
||||
|
||||
// Transforms XYZ color space coordinates to 'cone'/'RGB' responses in CAM16.
|
||||
static final float[][] XYZ_TO_CAM16RGB = {
|
||||
{0.401288f, 0.650173f, -0.051461f},
|
||||
{-0.250268f, 1.204414f, 0.045854f},
|
||||
{-0.002079f, 0.048952f, 0.953127f}
|
||||
};
|
||||
|
||||
// Transforms 'cone'/'RGB' responses in CAM16 to XYZ color space coordinates.
|
||||
static final float[][] CAM16RGB_TO_XYZ = {
|
||||
{1.86206786f, -1.01125463f, 0.14918677f},
|
||||
{0.38752654f, 0.62144744f, -0.00897398f},
|
||||
{-0.01584150f, -0.03412294f, 1.04996444f}
|
||||
};
|
||||
|
||||
// Need this, XYZ coordinates in internal ColorUtils are private
|
||||
|
||||
// sRGB specification has D65 whitepoint - Stokes, Anderson, Chandrasekar, Motta - A Standard
|
||||
// Default Color Space for the Internet: sRGB, 1996
|
||||
static final float[] WHITE_POINT_D65 = {95.047f, 100.0f, 108.883f};
|
||||
|
||||
// This is a more precise sRGB to XYZ transformation matrix than traditionally
|
||||
// used. It was derived using Schlomer's technique of transforming the xyY
|
||||
// primaries to XYZ, then applying a correction to ensure mapping from sRGB
|
||||
// 1, 1, 1 to the reference white point, D65.
|
||||
static final float[][] SRGB_TO_XYZ = {
|
||||
{0.41233895f, 0.35762064f, 0.18051042f},
|
||||
{0.2126f, 0.7152f, 0.0722f},
|
||||
{0.01932141f, 0.11916382f, 0.95034478f}
|
||||
};
|
||||
|
||||
static int intFromLstar(float lstar) {
|
||||
if (lstar < 1) {
|
||||
return 0xff000000;
|
||||
} else if (lstar > 99) {
|
||||
return 0xffffffff;
|
||||
}
|
||||
|
||||
// XYZ to LAB conversion routine, assume a and b are 0.
|
||||
float fy = (lstar + 16.0f) / 116.0f;
|
||||
|
||||
// fz = fx = fy because a and b are 0
|
||||
float fz = fy;
|
||||
float fx = fy;
|
||||
|
||||
float kappa = 24389f / 27f;
|
||||
float epsilon = 216f / 24389f;
|
||||
boolean lExceedsEpsilonKappa = (lstar > 8.0f);
|
||||
float yT = lExceedsEpsilonKappa ? fy * fy * fy : lstar / kappa;
|
||||
boolean cubeExceedEpsilon = (fy * fy * fy) > epsilon;
|
||||
float xT = cubeExceedEpsilon ? fx * fx * fx : (116f * fx - 16f) / kappa;
|
||||
float zT = cubeExceedEpsilon ? fz * fz * fz : (116f * fx - 16f) / kappa;
|
||||
|
||||
return ColorUtils.XYZToColor(xT * CamUtils.WHITE_POINT_D65[0],
|
||||
yT * CamUtils.WHITE_POINT_D65[1], zT * CamUtils.WHITE_POINT_D65[2]);
|
||||
}
|
||||
|
||||
/** Returns L* from L*a*b*, perceptual luminance, from an ARGB integer (ColorInt). */
|
||||
public static float lstarFromInt(int argb) {
|
||||
return lstarFromY(yFromInt(argb));
|
||||
}
|
||||
|
||||
static float lstarFromY(float y) {
|
||||
y = y / 100.0f;
|
||||
final float e = 216.f / 24389.f;
|
||||
float yIntermediate;
|
||||
if (y <= e) {
|
||||
return ((24389.f / 27.f) * y);
|
||||
} else {
|
||||
yIntermediate = (float) Math.cbrt(y);
|
||||
}
|
||||
return 116.f * yIntermediate - 16.f;
|
||||
}
|
||||
|
||||
static float yFromInt(int argb) {
|
||||
final float r = linearized(Color.red(argb));
|
||||
final float g = linearized(Color.green(argb));
|
||||
final float b = linearized(Color.blue(argb));
|
||||
float[][] matrix = SRGB_TO_XYZ;
|
||||
float y = (r * matrix[1][0]) + (g * matrix[1][1]) + (b * matrix[1][2]);
|
||||
return y;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
static float[] xyzFromInt(int argb) {
|
||||
final float r = linearized(Color.red(argb));
|
||||
final float g = linearized(Color.green(argb));
|
||||
final float b = linearized(Color.blue(argb));
|
||||
|
||||
float[][] matrix = SRGB_TO_XYZ;
|
||||
float x = (r * matrix[0][0]) + (g * matrix[0][1]) + (b * matrix[0][2]);
|
||||
float y = (r * matrix[1][0]) + (g * matrix[1][1]) + (b * matrix[1][2]);
|
||||
float z = (r * matrix[2][0]) + (g * matrix[2][1]) + (b * matrix[2][2]);
|
||||
return new float[]{x, y, z};
|
||||
}
|
||||
|
||||
static float yFromLstar(float lstar) {
|
||||
float ke = 8.0f;
|
||||
if (lstar > ke) {
|
||||
return (float) Math.pow(((lstar + 16.0) / 116.0), 3) * 100f;
|
||||
} else {
|
||||
return lstar / (24389f / 27f) * 100f;
|
||||
}
|
||||
}
|
||||
|
||||
static float linearized(int rgbComponent) {
|
||||
float normalized = (float) rgbComponent / 255.0f;
|
||||
|
||||
if (normalized <= 0.04045f) {
|
||||
return (normalized / 12.92f) * 100.0f;
|
||||
} else {
|
||||
return (float) Math.pow(((normalized + 0.055f) / 1.055f), 2.4f) * 100.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
194
core/java/com/android/internal/graphics/cam/Frame.java
Normal file
194
core/java/com/android/internal/graphics/cam/Frame.java
Normal file
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.internal.graphics.cam;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.util.MathUtils;
|
||||
|
||||
/**
|
||||
* The frame, or viewing conditions, where a color was seen. Used, along with a color, to create a
|
||||
* color appearance model representing the color.
|
||||
*
|
||||
* <p>To convert a traditional color to a color appearance model, it requires knowing what
|
||||
* conditions the color was observed in. Our perception of color depends on, for example, the tone
|
||||
* of the light illuminating the color, how bright that light was, etc.
|
||||
*
|
||||
* <p>This class is modelled separately from the color appearance model itself because there are a
|
||||
* number of calculations during the color => CAM conversion process that depend only on the viewing
|
||||
* conditions. Caching those calculations in a Frame instance saves a significant amount of time.
|
||||
*/
|
||||
public final class Frame {
|
||||
// Standard viewing conditions assumed in RGB specification - Stokes, Anderson, Chandrasekar,
|
||||
// Motta - A Standard Default Color Space for the Internet: sRGB, 1996.
|
||||
//
|
||||
// White point = D65
|
||||
// Luminance of adapting field: 200 / Pi / 5, units are cd/m^2.
|
||||
// sRGB ambient illuminance = 64 lux (per sRGB spec). However, the spec notes this is
|
||||
// artificially low and based on monitors in 1990s. Use 200, the sRGB spec says this is the
|
||||
// real average, and a survey of lux values on Wikipedia confirms this is a comfortable
|
||||
// default: somewhere between a very dark overcast day and office lighting.
|
||||
// Per CAM16 introduction paper (Li et al, 2017) Ew = pi * lw, and La = lw * Yb/Yw
|
||||
// Ew = ambient environment luminance, in lux.
|
||||
// Yb/Yw is taken to be midgray, ~20% relative luminance (XYZ Y 18.4, CIELAB L* 50).
|
||||
// Therefore La = (Ew / pi) * .184
|
||||
// La = 200 / pi * .184
|
||||
// Image surround to 10 degrees = ~20% relative luminance = CIELAB L* 50
|
||||
//
|
||||
// Not from sRGB standard:
|
||||
// Surround = average, 2.0.
|
||||
// Discounting illuminant = false, doesn't occur for self-luminous displays
|
||||
public static final Frame DEFAULT =
|
||||
Frame.make(
|
||||
CamUtils.WHITE_POINT_D65,
|
||||
(float) (200.0f / Math.PI * CamUtils.yFromLstar(50.0f) / 100.f), 50.0f, 2.0f,
|
||||
false);
|
||||
|
||||
private final float mAw;
|
||||
private final float mNbb;
|
||||
private final float mNcb;
|
||||
private final float mC;
|
||||
private final float mNc;
|
||||
private final float mN;
|
||||
private final float[] mRgbD;
|
||||
private final float mFl;
|
||||
private final float mFlRoot;
|
||||
private final float mZ;
|
||||
|
||||
float getAw() {
|
||||
return mAw;
|
||||
}
|
||||
|
||||
float getN() {
|
||||
return mN;
|
||||
}
|
||||
|
||||
float getNbb() {
|
||||
return mNbb;
|
||||
}
|
||||
|
||||
float getNcb() {
|
||||
return mNcb;
|
||||
}
|
||||
|
||||
float getC() {
|
||||
return mC;
|
||||
}
|
||||
|
||||
float getNc() {
|
||||
return mNc;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
float[] getRgbD() {
|
||||
return mRgbD;
|
||||
}
|
||||
|
||||
float getFl() {
|
||||
return mFl;
|
||||
}
|
||||
|
||||
float getFlRoot() {
|
||||
return mFlRoot;
|
||||
}
|
||||
|
||||
float getZ() {
|
||||
return mZ;
|
||||
}
|
||||
|
||||
private Frame(float n, float aw, float nbb, float ncb, float c, float nc, float[] rgbD,
|
||||
float fl, float fLRoot, float z) {
|
||||
mN = n;
|
||||
mAw = aw;
|
||||
mNbb = nbb;
|
||||
mNcb = ncb;
|
||||
mC = c;
|
||||
mNc = nc;
|
||||
mRgbD = rgbD;
|
||||
mFl = fl;
|
||||
mFlRoot = fLRoot;
|
||||
mZ = z;
|
||||
}
|
||||
|
||||
/** Create a custom frame. */
|
||||
@NonNull
|
||||
public static Frame make(@NonNull float[] whitepoint, float adaptingLuminance,
|
||||
float backgroundLstar, float surround, boolean discountingIlluminant) {
|
||||
// Transform white point XYZ to 'cone'/'rgb' responses
|
||||
float[][] matrix = CamUtils.XYZ_TO_CAM16RGB;
|
||||
float[] xyz = whitepoint;
|
||||
float rW = (xyz[0] * matrix[0][0]) + (xyz[1] * matrix[0][1]) + (xyz[2] * matrix[0][2]);
|
||||
float gW = (xyz[0] * matrix[1][0]) + (xyz[1] * matrix[1][1]) + (xyz[2] * matrix[1][2]);
|
||||
float bW = (xyz[0] * matrix[2][0]) + (xyz[1] * matrix[2][1]) + (xyz[2] * matrix[2][2]);
|
||||
|
||||
// Scale input surround, domain (0, 2), to CAM16 surround, domain (0.8, 1.0)
|
||||
float f = 0.8f + (surround / 10.0f);
|
||||
// "Exponential non-linearity"
|
||||
float c = (f >= 0.9) ? MathUtils.lerp(0.59f, 0.69f, ((f - 0.9f) * 10.0f)) : MathUtils.lerp(
|
||||
0.525f, 0.59f, ((f - 0.8f) * 10.0f));
|
||||
// Calculate degree of adaptation to illuminant
|
||||
float d = discountingIlluminant ? 1.0f : f * (1.0f - ((1.0f / 3.6f) * (float) Math.exp(
|
||||
(-adaptingLuminance - 42.0f) / 92.0f)));
|
||||
// Per Li et al, if D is greater than 1 or less than 0, set it to 1 or 0.
|
||||
d = (d > 1.0) ? 1.0f : (d < 0.0) ? 0.0f : d;
|
||||
// Chromatic induction factor
|
||||
float nc = f;
|
||||
|
||||
// Cone responses to the whitepoint, adjusted for illuminant discounting.
|
||||
//
|
||||
// Why use 100.0 instead of the white point's relative luminance?
|
||||
//
|
||||
// Some papers and implementations, for both CAM02 and CAM16, use the Y
|
||||
// value of the reference white instead of 100. Fairchild's Color Appearance
|
||||
// Models (3rd edition) notes that this is in error: it was included in the
|
||||
// CIE 2004a report on CIECAM02, but, later parts of the conversion process
|
||||
// account for scaling of appearance relative to the white point relative
|
||||
// luminance. This part should simply use 100 as luminance.
|
||||
float[] rgbD = new float[]{d * (100.0f / rW) + 1.0f - d, d * (100.0f / gW) + 1.0f - d,
|
||||
d * (100.0f / bW) + 1.0f - d, };
|
||||
// Luminance-level adaptation factor
|
||||
float k = 1.0f / (5.0f * adaptingLuminance + 1.0f);
|
||||
float k4 = k * k * k * k;
|
||||
float k4F = 1.0f - k4;
|
||||
float fl = (k4 * adaptingLuminance) + (0.1f * k4F * k4F * (float) Math.cbrt(
|
||||
5.0 * adaptingLuminance));
|
||||
|
||||
// Intermediate factor, ratio of background relative luminance to white relative luminance
|
||||
float n = CamUtils.yFromLstar(backgroundLstar) / whitepoint[1];
|
||||
|
||||
// Base exponential nonlinearity
|
||||
// note Schlomer 2018 has a typo and uses 1.58, the correct factor is 1.48
|
||||
float z = 1.48f + (float) Math.sqrt(n);
|
||||
|
||||
// Luminance-level induction factors
|
||||
float nbb = 0.725f / (float) Math.pow(n, 0.2);
|
||||
float ncb = nbb;
|
||||
|
||||
// Discounted cone responses to the white point, adjusted for post-chromatic
|
||||
// adaptation perceptual nonlinearities.
|
||||
float[] rgbAFactors = new float[]{(float) Math.pow(fl * rgbD[0] * rW / 100.0, 0.42),
|
||||
(float) Math.pow(fl * rgbD[1] * gW / 100.0, 0.42), (float) Math.pow(
|
||||
fl * rgbD[2] * bW / 100.0, 0.42)};
|
||||
|
||||
float[] rgbA = new float[]{(400.0f * rgbAFactors[0]) / (rgbAFactors[0] + 27.13f),
|
||||
(400.0f * rgbAFactors[1]) / (rgbAFactors[1] + 27.13f),
|
||||
(400.0f * rgbAFactors[2]) / (rgbAFactors[2] + 27.13f), };
|
||||
|
||||
float aw = ((2.0f * rgbA[0]) + rgbA[1] + (0.05f * rgbA[2])) * nbb;
|
||||
|
||||
return new Frame(n, aw, nbb, ncb, c, nc, rgbD, fl, (float) Math.pow(fl, 0.25), z);
|
||||
}
|
||||
}
|
||||
196
core/tests/coretests/src/android/colormodel/CamTest.java
Normal file
196
core/tests/coretests/src/android/colormodel/CamTest.java
Normal file
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.internal.graphics.cam;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
@RunWith(JUnit4.class)
|
||||
public final class CamTest {
|
||||
static final int BLACK = 0xff000000;
|
||||
static final int WHITE = 0xffffffff;
|
||||
static final int MIDGRAY = 0xff777777;
|
||||
|
||||
static final int RED = 0xffff0000;
|
||||
static final int GREEN = 0xff00ff00;
|
||||
static final int BLUE = 0xff0000ff;
|
||||
|
||||
@Test
|
||||
public void camFromIntToInt() {
|
||||
Cam cam = Cam.fromInt(RED);
|
||||
int color = cam.viewed(Frame.DEFAULT);
|
||||
assertEquals(color, RED);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void yFromMidgray() {
|
||||
assertEquals(18.418f, CamUtils.yFromLstar(50.0f), 0.001);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void yFromBlack() {
|
||||
assertEquals(0.0f, CamUtils.yFromLstar(0.0f), 0.001);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void yFromWhite() {
|
||||
assertEquals(100.0f, CamUtils.yFromLstar(100.0f), 0.001);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void camFromRed() {
|
||||
Cam cam = Cam.fromInt(RED);
|
||||
assertEquals(46.445f, cam.getJ(), 0.001f);
|
||||
assertEquals(113.357f, cam.getChroma(), 0.001f);
|
||||
assertEquals(27.408f, cam.getHue(), 0.001f);
|
||||
assertEquals(89.494f, cam.getM(), 0.001f);
|
||||
assertEquals(91.889f, cam.getS(), 0.001f);
|
||||
assertEquals(105.988f, cam.getQ(), 0.001f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void camFromGreen() {
|
||||
Cam cam = Cam.fromInt(GREEN);
|
||||
assertEquals(79.331f, cam.getJ(), 0.001f);
|
||||
assertEquals(108.409f, cam.getChroma(), 0.001f);
|
||||
assertEquals(142.139f, cam.getHue(), 0.001f);
|
||||
assertEquals(85.587f, cam.getM(), 0.001f);
|
||||
assertEquals(78.604f, cam.getS(), 0.001f);
|
||||
assertEquals(138.520, cam.getQ(), 0.001f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void camFromBlue() {
|
||||
Cam cam = Cam.fromInt(BLUE);
|
||||
assertEquals(25.465f, cam.getJ(), 0.001f);
|
||||
assertEquals(87.230f, cam.getChroma(), 0.001f);
|
||||
assertEquals(282.788f, cam.getHue(), 0.001f);
|
||||
assertEquals(68.867f, cam.getM(), 0.001f);
|
||||
assertEquals(93.674f, cam.getS(), 0.001f);
|
||||
assertEquals(78.481f, cam.getQ(), 0.001f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void camFromBlack() {
|
||||
Cam cam = Cam.fromInt(BLACK);
|
||||
assertEquals(0.0f, cam.getJ(), 0.001f);
|
||||
assertEquals(0.0f, cam.getChroma(), 0.001f);
|
||||
assertEquals(0.0f, cam.getHue(), 0.001f);
|
||||
assertEquals(0.0f, cam.getM(), 0.001f);
|
||||
assertEquals(0.0f, cam.getS(), 0.001f);
|
||||
assertEquals(0.0f, cam.getQ(), 0.001f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void camFromWhite() {
|
||||
Cam cam = Cam.fromInt(WHITE);
|
||||
assertEquals(100.0f, cam.getJ(), 0.001f);
|
||||
assertEquals(2.869f, cam.getChroma(), 0.001f);
|
||||
assertEquals(209.492f, cam.getHue(), 0.001f);
|
||||
assertEquals(2.265f, cam.getM(), 0.001f);
|
||||
assertEquals(12.068f, cam.getS(), 0.001f);
|
||||
assertEquals(155.521, cam.getQ(), 0.001f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getRedFromGamutMap() {
|
||||
int colorToTest = RED;
|
||||
Cam cam = Cam.fromInt(colorToTest);
|
||||
int color = Cam.getInt(cam.getHue(), cam.getChroma(), CamUtils.lstarFromInt(colorToTest));
|
||||
assertEquals(colorToTest, color);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getGreenFromGamutMap() {
|
||||
int colorToTest = GREEN;
|
||||
Cam cam = Cam.fromInt(colorToTest);
|
||||
int color = Cam.getInt(cam.getHue(), cam.getChroma(), CamUtils.lstarFromInt(colorToTest));
|
||||
assertEquals(colorToTest, color);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getBlueFromGamutMap() {
|
||||
int colorToTest = BLUE;
|
||||
Cam cam = Cam.fromInt(colorToTest);
|
||||
int color = Cam.getInt(cam.getHue(), cam.getChroma(), CamUtils.lstarFromInt(colorToTest));
|
||||
assertEquals(colorToTest, color);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getWhiteFromGamutMap() {
|
||||
int colorToTest = WHITE;
|
||||
Cam cam = Cam.fromInt(colorToTest);
|
||||
int color = Cam.getInt(cam.getHue(), cam.getChroma(), CamUtils.lstarFromInt(colorToTest));
|
||||
assertEquals(colorToTest, color);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getBlackFromGamutMap() {
|
||||
int colorToTest = BLACK;
|
||||
Cam cam = Cam.fromInt(colorToTest);
|
||||
int color = Cam.getInt(cam.getHue(), cam.getChroma(), CamUtils.lstarFromInt(colorToTest));
|
||||
assertEquals(colorToTest, color);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getMidgrayFromGamutMap() {
|
||||
int colorToTest = MIDGRAY;
|
||||
Cam cam = Cam.fromInt(colorToTest);
|
||||
int color = Cam.getInt(cam.getHue(), cam.getChroma(), CamUtils.lstarFromInt(colorToTest));
|
||||
assertEquals(colorToTest, color);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getRandomGreenFromGamutMap() {
|
||||
int colorToTest = 0xff009200;
|
||||
Cam cam = Cam.fromInt(colorToTest);
|
||||
int color = Cam.getInt(cam.getHue(), cam.getChroma(), CamUtils.lstarFromInt(colorToTest));
|
||||
assertEquals(colorToTest, color);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void gamutMapArbitraryHCL() {
|
||||
int color = Cam.getInt(309.0f, 40.0f, 70.0f);
|
||||
Cam cam = Cam.fromInt(color);
|
||||
|
||||
assertEquals(308.759f, cam.getHue(), 0.001);
|
||||
assertEquals(40.148f, cam.getChroma(), 0.001);
|
||||
assertEquals(70.029f, CamUtils.lstarFromInt(color), 0.001f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ucsCoordinates() {
|
||||
Cam cam = Cam.fromInt(RED);
|
||||
|
||||
assertEquals(59.584f, cam.getJstar(), 0.001f);
|
||||
assertEquals(43.297f, cam.getAstar(), 0.001f);
|
||||
assertEquals(22.451f, cam.getBstar(), 0.001f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deltaEWhiteToBlack() {
|
||||
assertEquals(25.661f, Cam.fromInt(WHITE).distance(Cam.fromInt(BLACK)), 0.001f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deltaERedToBlue() {
|
||||
assertEquals(21.415f, Cam.fromInt(RED).distance(Cam.fromInt(BLUE)), 0.001f);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user