| /* |
| * Copyright (c) 2002-2018, the original author or authors. |
| * |
| * This software is distributable under the BSD license. See the terms of the |
| * BSD license in the documentation provided with this software. |
| * |
| * https://opensource.org/licenses/BSD-3-Clause |
| */ |
| package jdk.internal.org.jline.utils; |
| |
| import java.io.BufferedReader; |
| import java.io.IOError; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.nio.charset.StandardCharsets; |
| import java.util.LinkedHashMap; |
| import java.util.Map; |
| import java.util.stream.Stream; |
| |
| import static jdk.internal.org.jline.terminal.TerminalBuilder.PROP_COLOR_DISTANCE; |
| |
| public class Colors { |
| |
| /** |
| * Default 256 colors palette |
| */ |
| public static final int[] DEFAULT_COLORS_256 = { |
| 0x000000, 0x800000, 0x008000, 0x808000, 0x000080, 0x800080, 0x008080, 0xc0c0c0, |
| 0x808080, 0xff0000, 0x00ff00, 0xffff00, 0x0000ff, 0xff00ff, 0x00ffff, 0xffffff, |
| |
| 0x000000, 0x00005f, 0x000087, 0x0000af, 0x0000d7, 0x0000ff, 0x005f00, 0x005f5f, |
| 0x005f87, 0x005faf, 0x005fd7, 0x005fff, 0x008700, 0x00875f, 0x008787, 0x0087af, |
| 0x0087d7, 0x0087ff, 0x00af00, 0x00af5f, 0x00af87, 0x00afaf, 0x00afd7, 0x00afff, |
| 0x00d700, 0x00d75f, 0x00d787, 0x00d7af, 0x00d7d7, 0x00d7ff, 0x00ff00, 0x00ff5f, |
| 0x00ff87, 0x00ffaf, 0x00ffd7, 0x00ffff, 0x5f0000, 0x5f005f, 0x5f0087, 0x5f00af, |
| 0x5f00d7, 0x5f00ff, 0x5f5f00, 0x5f5f5f, 0x5f5f87, 0x5f5faf, 0x5f5fd7, 0x5f5fff, |
| 0x5f8700, 0x5f875f, 0x5f8787, 0x5f87af, 0x5f87d7, 0x5f87ff, 0x5faf00, 0x5faf5f, |
| 0x5faf87, 0x5fafaf, 0x5fafd7, 0x5fafff, 0x5fd700, 0x5fd75f, 0x5fd787, 0x5fd7af, |
| 0x5fd7d7, 0x5fd7ff, 0x5fff00, 0x5fff5f, 0x5fff87, 0x5fffaf, 0x5fffd7, 0x5fffff, |
| 0x870000, 0x87005f, 0x870087, 0x8700af, 0x8700d7, 0x8700ff, 0x875f00, 0x875f5f, |
| 0x875f87, 0x875faf, 0x875fd7, 0x875fff, 0x878700, 0x87875f, 0x878787, 0x8787af, |
| 0x8787d7, 0x8787ff, 0x87af00, 0x87af5f, 0x87af87, 0x87afaf, 0x87afd7, 0x87afff, |
| 0x87d700, 0x87d75f, 0x87d787, 0x87d7af, 0x87d7d7, 0x87d7ff, 0x87ff00, 0x87ff5f, |
| 0x87ff87, 0x87ffaf, 0x87ffd7, 0x87ffff, 0xaf0000, 0xaf005f, 0xaf0087, 0xaf00af, |
| 0xaf00d7, 0xaf00ff, 0xaf5f00, 0xaf5f5f, 0xaf5f87, 0xaf5faf, 0xaf5fd7, 0xaf5fff, |
| 0xaf8700, 0xaf875f, 0xaf8787, 0xaf87af, 0xaf87d7, 0xaf87ff, 0xafaf00, 0xafaf5f, |
| 0xafaf87, 0xafafaf, 0xafafd7, 0xafafff, 0xafd700, 0xafd75f, 0xafd787, 0xafd7af, |
| 0xafd7d7, 0xafd7ff, 0xafff00, 0xafff5f, 0xafff87, 0xafffaf, 0xafffd7, 0xafffff, |
| 0xd70000, 0xd7005f, 0xd70087, 0xd700af, 0xd700d7, 0xd700ff, 0xd75f00, 0xd75f5f, |
| 0xd75f87, 0xd75faf, 0xd75fd7, 0xd75fff, 0xd78700, 0xd7875f, 0xd78787, 0xd787af, |
| 0xd787d7, 0xd787ff, 0xd7af00, 0xd7af5f, 0xd7af87, 0xd7afaf, 0xd7afd7, 0xd7afff, |
| 0xd7d700, 0xd7d75f, 0xd7d787, 0xd7d7af, 0xd7d7d7, 0xd7d7ff, 0xd7ff00, 0xd7ff5f, |
| 0xd7ff87, 0xd7ffaf, 0xd7ffd7, 0xd7ffff, 0xff0000, 0xff005f, 0xff0087, 0xff00af, |
| 0xff00d7, 0xff00ff, 0xff5f00, 0xff5f5f, 0xff5f87, 0xff5faf, 0xff5fd7, 0xff5fff, |
| 0xff8700, 0xff875f, 0xff8787, 0xff87af, 0xff87d7, 0xff87ff, 0xffaf00, 0xffaf5f, |
| 0xffaf87, 0xffafaf, 0xffafd7, 0xffafff, 0xffd700, 0xffd75f, 0xffd787, 0xffd7af, |
| 0xffd7d7, 0xffd7ff, 0xffff00, 0xffff5f, 0xffff87, 0xffffaf, 0xffffd7, 0xffffff, |
| |
| 0x080808, 0x121212, 0x1c1c1c, 0x262626, 0x303030, 0x3a3a3a, 0x444444, 0x4e4e4e, |
| 0x585858, 0x626262, 0x6c6c6c, 0x767676, 0x808080, 0x8a8a8a, 0x949494, 0x9e9e9e, |
| 0xa8a8a8, 0xb2b2b2, 0xbcbcbc, 0xc6c6c6, 0xd0d0d0, 0xdadada, 0xe4e4e4, 0xeeeeee, |
| }; |
| |
| /** D50 illuminant for CAM color spaces */ |
| public static final double[] D50 = new double[] { 96.422f, 100.0f, 82.521f }; |
| /** D65 illuminant for CAM color spaces */ |
| public static final double[] D65 = new double[] { 95.047, 100.0, 108.883 }; |
| |
| /** Average surrounding for CAM color spaces */ |
| public static final double[] averageSurrounding = new double[] { 1.0, 0.690, 1.0 }; |
| /** Dim surrounding for CAM color spaces */ |
| public static final double[] dimSurrounding = new double[] { 0.9, 0.590, 0.9 }; |
| /** Dark surrounding for CAM color spaces */ |
| public static final double[] darkSurrounding = new double[] { 0.8, 0.525, 0.8 }; |
| |
| /** sRGB encoding environment */ |
| public static final double[] sRGB_encoding_environment = vc(D50, 64.0, 64/5, dimSurrounding); |
| /** sRGB typical environment */ |
| public static final double[] sRGB_typical_environment = vc(D50, 200.0, 200/5, averageSurrounding); |
| /** Adobe RGB environment */ |
| public static final double[] AdobeRGB_environment = vc(D65, 160.0, 160/5, averageSurrounding); |
| |
| private static int[] COLORS_256 = DEFAULT_COLORS_256; |
| |
| private static Map<String, Integer> COLOR_NAMES; |
| |
| public static void setRgbColors(int[] colors) { |
| if (colors == null || colors.length != 256) { |
| throw new IllegalArgumentException(); |
| } |
| COLORS_256 = colors; |
| } |
| |
| public static int rgbColor(int col) { |
| return COLORS_256[col]; |
| } |
| |
| public static Integer rgbColor(String name) { |
| if (COLOR_NAMES == null) { |
| Map<String, Integer> colors = new LinkedHashMap<>(); |
| try (InputStream is = InfoCmp.class.getResourceAsStream("colors.txt"); |
| BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { |
| br.lines().map(String::trim) |
| .filter(s -> !s.startsWith("#")) |
| .filter(s -> !s.isEmpty()) |
| .forEachOrdered(s -> { |
| colors.put(s, colors.size()); |
| }); |
| COLOR_NAMES = colors; |
| } catch (IOException e) { |
| throw new IOError(e); |
| } |
| } |
| return COLOR_NAMES.get(name); |
| } |
| |
| public static int roundColor(int col, int max) { |
| return roundColor(col, max, null); |
| } |
| |
| public static int roundColor(int col, int max, String dist) { |
| if (col >= max) { |
| int c = COLORS_256[col]; |
| col = roundColor(c, COLORS_256, max, dist); |
| } |
| return col; |
| } |
| |
| public static int roundRgbColor(int r, int g, int b, int max) { |
| return roundColor((r << 16) + (g << 8) + b, COLORS_256, max, (String) null); |
| } |
| |
| private static int roundColor(int color, int[] colors, int max, String dist) { |
| return roundColor(color, colors, max, getDistance(dist)); |
| } |
| |
| private interface Distance { |
| double compute(int c1, int c2); |
| } |
| |
| private static int roundColor(int color, int[] colors, int max, Distance distance) { |
| double best_distance = Integer.MAX_VALUE; |
| int best_index = Integer.MAX_VALUE; |
| for (int idx = 0; idx < max; idx++) { |
| double d = distance.compute(color, colors[idx]); |
| if (d <= best_distance) { |
| best_index = idx; |
| best_distance = d; |
| } |
| } |
| return best_index; |
| } |
| |
| private static Distance getDistance(String dist) { |
| if (dist == null) { |
| dist = System.getProperty(PROP_COLOR_DISTANCE, "cie76"); |
| } |
| return doGetDistance(dist); |
| } |
| |
| private static Distance doGetDistance(String dist) { |
| if (dist.equals("rgb")) { |
| return (p1, p2) -> { |
| // rgb: see https://www.compuphase.com/cmetric.htm |
| double[] c1 = rgb(p1); |
| double[] c2 = rgb(p2); |
| double rmean = (c1[0] + c2[0]) / 2.0; |
| double[] w = { 2.0 + rmean, 4.0, 3.0 - rmean }; |
| return scalar(c1, c2, w); |
| }; |
| } |
| if (dist.matches("rgb\\(([0-9]+(\\.[0-9]+)?),([0-9]+(\\.[0-9]+)?),([0-9]+(\\.[0-9]+)?)\\)")) { |
| return (p1, p2) -> scalar(rgb(p1), rgb(p2), getWeights(dist)); |
| } |
| if (dist.equals("lab") || dist.equals("cie76")) { |
| return (p1, p2) -> scalar(rgb2cielab(p1), rgb2cielab(p2)); |
| } |
| if (dist.matches("lab\\(([0-9]+(\\.[0-9]+)?),([0-9]+(\\.[0-9]+)?)\\)")) { |
| double[] w = getWeights(dist); |
| return (p1, p2) -> scalar(rgb2cielab(p1), rgb2cielab(p2), new double[] { w[0], w[1], w[1] }); |
| } |
| if (dist.equals("cie94")) { |
| return (p1, p2) -> cie94(rgb2cielab(p1), rgb2cielab(p2)); |
| } |
| if (dist.equals("cie00") || dist.equals("cie2000")) { |
| return (p1, p2) -> cie00(rgb2cielab(p1), rgb2cielab(p2)); |
| } |
| if (dist.equals("cam02")) { |
| return (p1, p2) -> cam02(p1, p2, sRGB_typical_environment); |
| } |
| if (dist.equals("camlab")) { |
| return (p1, p2) -> { |
| double[] c1 = camlab(p1, sRGB_typical_environment); |
| double[] c2 = camlab(p2, sRGB_typical_environment); |
| return scalar(c1, c2); |
| }; |
| } |
| if (dist.matches("camlab\\(([0-9]+(\\.[0-9]+)?),([0-9]+(\\.[0-9]+)?)\\)")) { |
| return (p1, p2) -> { |
| double[] c1 = camlab(p1, sRGB_typical_environment); |
| double[] c2 = camlab(p2, sRGB_typical_environment); |
| double[] w = getWeights(dist); |
| return scalar(c1, c2, new double[] { w[0], w[1], w[1] }); |
| }; |
| } |
| if (dist.matches("camlch")) { |
| return (p1, p2) -> { |
| double[] c1 = camlch(p1, sRGB_typical_environment); |
| double[] c2 = camlch(p2, sRGB_typical_environment); |
| return camlch(c1, c2); |
| }; |
| } |
| if (dist.matches("camlch\\(([0-9]+(\\.[0-9]+)?),([0-9]+(\\.[0-9]+)?),([0-9]+(\\.[0-9]+)?)\\)")) { |
| return (p1, p2) -> { |
| double[] c1 = camlch(p1, sRGB_typical_environment); |
| double[] c2 = camlch(p2, sRGB_typical_environment); |
| double[] w = getWeights(dist); |
| return camlch(c1, c2, w); |
| }; |
| } |
| throw new IllegalArgumentException("Unsupported distance function: " + dist); |
| } |
| |
| private static double[] getWeights(String dist) { |
| String[] weights = dist.substring(dist.indexOf('(') + 1, dist.length() - 1).split(","); |
| return Stream.of(weights).mapToDouble(Double::parseDouble).toArray(); |
| } |
| |
| private static double scalar(double[] c1, double[] c2, double[] w) { |
| return sqr((c1[0] - c2[0]) * w[0]) |
| + sqr((c1[1] - c2[1]) * w[1]) |
| + sqr((c1[2] - c2[2]) * w[2]); |
| } |
| |
| private static double scalar(double[] c1, double[] c2) { |
| return sqr(c1[0] - c2[0]) |
| + sqr(c1[1] - c2[1]) |
| + sqr(c1[2] - c2[2]); |
| } |
| |
| private static final int L = 0; |
| private static final int A = 1; |
| private static final int B = 2; |
| private static final int X = 0; |
| private static final int Y = 1; |
| private static final int Z = 2; |
| private static final double kl = 2.0; |
| private static final double kc = 1.0; |
| private static final double kh = 1.0; |
| private static final double k1 = 0.045; |
| private static final double k2 = 0.015; |
| |
| private static double cie94(double[] lab1, double[] lab2) { |
| double dl = lab1[L] - lab2[L]; |
| double da = lab1[A] - lab2[A]; |
| double db = lab1[B] - lab2[B]; |
| double c1 = Math.sqrt(lab1[A] * lab1[A] + lab1[B] * lab1[B]); |
| double c2 = Math.sqrt(lab2[A] * lab2[A] + lab2[B] * lab2[B]); |
| double dc = c1 - c2; |
| double dh = da * da + db * db - dc * dc; |
| dh = dh < 0.0 ? 0.0 : Math.sqrt(dh); |
| double sl = 1.0; |
| double sc = 1.0 + k1 * c1; |
| double sh = 1.0 + k2 * c1; |
| double dLKlsl = dl / (kl * sl); |
| double dCkcsc = dc / (kc * sc); |
| double dHkhsh = dh / (kh * sh); |
| return dLKlsl * dLKlsl + dCkcsc * dCkcsc + dHkhsh * dHkhsh; |
| } |
| |
| private static double cie00(double[] lab1, double[] lab2) { |
| double c_star_1_ab = Math.sqrt(lab1[A] * lab1[A] + lab1[B] * lab1[B]); |
| double c_star_2_ab = Math.sqrt(lab2[A] * lab2[A] + lab2[B] * lab2[B]); |
| double c_star_average_ab = (c_star_1_ab + c_star_2_ab) / 2.0; |
| double c_star_average_ab_pot_3 = c_star_average_ab * c_star_average_ab * c_star_average_ab; |
| double c_star_average_ab_pot_7 = c_star_average_ab_pot_3 * c_star_average_ab_pot_3 * c_star_average_ab; |
| double G = 0.5 * (1.0 - Math.sqrt(c_star_average_ab_pot_7 / (c_star_average_ab_pot_7 + 6103515625.0))); //25^7 |
| double a1_prime = (1.0 + G) * lab1[A]; |
| double a2_prime = (1.0 + G) * lab2[A]; |
| double C_prime_1 = Math.sqrt(a1_prime * a1_prime + lab1[B] * lab1[B]); |
| double C_prime_2 = Math.sqrt(a2_prime * a2_prime + lab2[B] * lab2[B]); |
| double h_prime_1 = (Math.toDegrees(Math.atan2(lab1[B], a1_prime)) + 360.0) % 360.0; |
| double h_prime_2 = (Math.toDegrees(Math.atan2(lab2[B], a2_prime)) + 360.0) % 360.0; |
| double delta_L_prime = lab2[L] - lab1[L]; |
| double delta_C_prime = C_prime_2 - C_prime_1; |
| double h_bar = Math.abs(h_prime_1 - h_prime_2); |
| double delta_h_prime; |
| if (C_prime_1 * C_prime_2 == 0.0) { |
| delta_h_prime = 0.0; |
| } else if (h_bar <= 180.0) { |
| delta_h_prime = h_prime_2 - h_prime_1; |
| } else if (h_prime_2 <= h_prime_1) { |
| delta_h_prime = h_prime_2 - h_prime_1 + 360.0; |
| } else { |
| delta_h_prime = h_prime_2 - h_prime_1 - 360.0; |
| } |
| double delta_H_prime = 2.0 * Math.sqrt(C_prime_1 * C_prime_2) * Math.sin(Math.toRadians(delta_h_prime / 2.0)); |
| double L_prime_average = (lab1[L] + lab2[L]) / 2.0; |
| double C_prime_average = (C_prime_1 + C_prime_2) / 2.0; |
| double h_prime_average; |
| if (C_prime_1 * C_prime_2 == 0.0) { |
| h_prime_average = 0.0; |
| } else if (h_bar <= 180.0) { |
| h_prime_average = (h_prime_1 + h_prime_2) / 2.0; |
| } else if ((h_prime_1 + h_prime_2) < 360.0) { |
| h_prime_average = (h_prime_1 + h_prime_2 + 360.0) / 2.0; |
| } else { |
| h_prime_average = (h_prime_1 + h_prime_2 - 360.0) / 2.0; |
| } |
| double L_prime_average_minus_50 = L_prime_average - 50.0; |
| double L_prime_average_minus_50_square = L_prime_average_minus_50 * L_prime_average_minus_50; |
| double T = 1.0 |
| - 0.17 * Math.cos(Math.toRadians(h_prime_average - 30.0)) |
| + 0.24 * Math.cos(Math.toRadians(h_prime_average * 2.0)) |
| + 0.32 * Math.cos(Math.toRadians(h_prime_average * 3.0 + 6.0)) |
| - 0.20 * Math.cos(Math.toRadians(h_prime_average * 4.0 - 63.0)); |
| double S_L = 1.0 + ((0.015 * L_prime_average_minus_50_square) / Math.sqrt(20.0 + L_prime_average_minus_50_square)); |
| double S_C = 1.0 + 0.045 * C_prime_average; |
| double S_H = 1.0 + 0.015 * T * C_prime_average; |
| double h_prime_average_minus_275_div_25 = (h_prime_average - 275.0) / (25.0); |
| double h_prime_average_minus_275_div_25_square = h_prime_average_minus_275_div_25 * h_prime_average_minus_275_div_25; |
| double delta_theta = 30.0 * Math.exp(-h_prime_average_minus_275_div_25_square); |
| double C_prime_average_pot_3 = C_prime_average * C_prime_average * C_prime_average; |
| double C_prime_average_pot_7 = C_prime_average_pot_3 * C_prime_average_pot_3 * C_prime_average; |
| double R_C = 2.0 * Math.sqrt(C_prime_average_pot_7 / (C_prime_average_pot_7 + 6103515625.0)); //25^7 |
| double R_T = - Math.sin(Math.toRadians(2.0 * delta_theta)) * R_C; |
| double dLKlsl = delta_L_prime / (kl * S_L); |
| double dCkcsc = delta_C_prime / (kc * S_C); |
| double dHkhsh = delta_H_prime / (kh * S_H); |
| return dLKlsl * dLKlsl + dCkcsc * dCkcsc + dHkhsh * dHkhsh + R_T * dCkcsc * dHkhsh; |
| } |
| |
| private static double cam02(int p1, int p2, double[] vc) { |
| double[] c1 = jmh2ucs(camlch(p1, vc)); |
| double[] c2 = jmh2ucs(camlch(p2, vc)); |
| return scalar(c1, c2); |
| } |
| |
| private static double[] jmh2ucs(double[] lch) { |
| double sJ = ((1.0 + 100 * 0.007) * lch[0]) / (1.0 + 0.007 * lch[0]); |
| double sM = ((1.0 / 0.0228) * Math.log(1.0 + 0.0228 * lch[1])); |
| double a = sM * Math.cos(Math.toRadians(lch[2])); |
| double b = sM * Math.sin(Math.toRadians(lch[2])); |
| return new double[] {sJ, a, b }; |
| } |
| |
| static double camlch(double[] c1, double[] c2) { |
| return camlch(c1, c2, new double[] { 1.0, 1.0, 1.0 }); |
| } |
| |
| static double camlch(double[] c1, double[] c2, double[] w) { |
| // normalize weights to correlate range |
| double lightnessWeight = w[0] / 100.0; |
| double colorfulnessWeight = w[1] / 120.0; |
| double hueWeight = w[2] / 360.0; |
| // calculate sort-of polar distance |
| double dl = (c1[0] - c2[0]) * lightnessWeight; |
| double dc = (c1[1] - c2[1]) * colorfulnessWeight; |
| double dh = hueDifference(c1[2], c2[2], 360.0) * hueWeight; |
| return dl * dl + dc * dc + dh * dh; |
| } |
| |
| private static double hueDifference(double hue1, double hue2, double c) { |
| double difference = (hue2 - hue1) % c; |
| double ch = c / 2; |
| if (difference > ch) |
| difference -= c; |
| if (difference < -ch) |
| difference += c; |
| return difference; |
| } |
| |
| private static double[] rgb(int color) { |
| int r = (color >> 16) & 0xFF; |
| int g = (color >> 8) & 0xFF; |
| int b = (color >> 0) & 0xFF; |
| return new double[] { r / 255.0, g / 255.0, b / 255.0 }; |
| } |
| |
| static double[] rgb2xyz(int color) { |
| return rgb2xyz(rgb(color)); |
| } |
| |
| static double[] rgb2cielab(int color) { |
| return rgb2cielab(rgb(color)); |
| } |
| |
| static double[] camlch(int color) { |
| return camlch(color, sRGB_typical_environment); |
| } |
| |
| static double[] camlch(int color, double[] vc) { |
| return xyz2camlch(rgb2xyz(color), vc); |
| } |
| |
| static double[] camlab(int color) { |
| return camlab(color, sRGB_typical_environment); |
| } |
| |
| static double[] camlab(int color, double[] vc) { |
| return lch2lab(camlch(color, vc)); |
| } |
| |
| static double[] lch2lab(double[] lch) { |
| double toRad = Math.PI / 180; |
| return new double[] { lch[0], lch[1] * Math.cos(lch[2] * toRad), lch[1] * Math.sin(lch[2] * toRad) }; |
| } |
| |
| private static double[] xyz2camlch(double[] xyz, double[] vc) { |
| double[] XYZ = new double[] {xyz[0] * 100.0, xyz[1] * 100.0, xyz[2] * 100.0}; |
| double[] cam = forwardTransform(XYZ, vc); |
| return new double[] { cam[J], cam[M], cam[h] }; |
| } |
| |
| /** Lightness */ |
| public static final int J = 0; |
| /** Brightness */ |
| public static final int Q = 1; |
| /** Chroma */ |
| public static final int C = 2; |
| /** Colorfulness */ |
| public static final int M = 3; |
| /** Saturation */ |
| public static final int s = 4; |
| /** Hue Composition / Hue Quadrature */ |
| public static final int H = 5; |
| /** Hue */ |
| public static final int h = 6; |
| |
| |
| /** CIECAM02 appearance correlates */ |
| private static double[] forwardTransform(double[] XYZ, double[] vc) { |
| // calculate sharpened cone response |
| double[] RGB = forwardPreAdaptationConeResponse(XYZ); |
| // calculate corresponding (sharpened) cone response considering various luminance level and surround conditions in D |
| double[] RGB_c = forwardPostAdaptationConeResponse(RGB, vc); |
| // calculate HPE equal area cone fundamentals |
| double[] RGBPrime = CAT02toHPE(RGB_c); |
| // calculate response-compressed postadaptation cone response |
| double[] RGBPrime_a = forwardResponseCompression(RGBPrime, vc); |
| // calculate achromatic response |
| double A = (2.0 * RGBPrime_a[0] + RGBPrime_a[1] + RGBPrime_a[2] / 20.0 - 0.305) * vc[VC_N_BB]; |
| // calculate lightness |
| double J = 100.0 * Math.pow(A / vc[VC_A_W], vc[VC_Z] * vc[VC_C]); |
| // calculate redness-greenness and yellowness-blueness color opponent values |
| double a = RGBPrime_a[0] + (-12.0 * RGBPrime_a[1] + RGBPrime_a[2]) / 11.0; |
| double b = (RGBPrime_a[0] + RGBPrime_a[1] - 2.0 * RGBPrime_a[2]) / 9.0; |
| // calculate hue angle |
| double h = (Math.toDegrees(Math.atan2(b, a)) + 360.0) % 360.0; |
| // calculate eccentricity |
| double e = ((12500.0 / 13.0) * vc[VC_N_C] * vc[VC_N_CB]) * (Math.cos(Math.toRadians(h) + 2.0) + 3.8); |
| // get t |
| double t = e * Math.sqrt(Math.pow(a, 2.0) + Math.pow(b, 2.0)) / (RGBPrime_a[0] + RGBPrime_a[1] + 1.05 * RGBPrime_a[2]); |
| // calculate brightness |
| double Q = (4.0 / vc[VC_C]) * Math.sqrt(J / 100.0) * (vc[VC_A_W] + 4.0) * Math.pow(vc[VC_F_L], 0.25); |
| // calculate the correlates of chroma, colorfulness, and saturation |
| double C = Math.signum(t) * Math.pow(Math.abs(t), 0.9) * Math.sqrt(J / 100.0) * Math.pow(1.64- Math.pow(0.29, vc[VC_N]), 0.73); |
| double M = C * Math.pow(vc[VC_F_L], 0.25); |
| double s = 100.0 * Math.sqrt(M / Q); |
| // calculate hue composition |
| double H = calculateH(h); |
| return new double[] { J, Q, C, M, s, H, h }; |
| } |
| |
| private static double calculateH(double h) { |
| if (h < 20.14) |
| h = h + 360; |
| double i; |
| if (h >= 20.14 && h < 90.0) { // index i = 1 |
| i = (h - 20.14) / 0.8; |
| return 100.0 * i / (i + (90 - h) / 0.7); |
| } else if (h < 164.25) { // index i = 2 |
| i = (h - 90) / 0.7; |
| return 100.0 + 100.0 * i / (i + (164.25 - h) / 1); |
| } else if (h < 237.53) { // index i = 3 |
| i = (h - 164.25) / 1.0; |
| return 200.0 + 100.0 * i / (i + (237.53 - h) / 1.2); |
| } else if (h <= 380.14) { // index i = 4 |
| i = (h - 237.53) / 1.2; |
| double H = 300.0 + 100.0 * i / (i + (380.14 - h) / 0.8); |
| // don't use 400 if we can use 0 |
| if (H <= 400.0 && H >= 399.999) |
| H = 0; |
| return H; |
| } else { |
| throw new IllegalArgumentException("h outside assumed range 0..360: " + Double.toString(h)); |
| } |
| } |
| |
| private static double[] forwardResponseCompression(double[] RGB, double[] vc) { |
| double[] result = new double[3]; |
| for(int channel = 0; channel < RGB.length; channel++) { |
| if(RGB[channel] >= 0) { |
| double n = Math.pow(vc[VC_F_L] * RGB[channel] / 100.0, 0.42); |
| result[channel] = 400.0 * n / (n + 27.13) + 0.1; |
| } else { |
| double n = Math.pow(-1.0 * vc[VC_F_L] * RGB[channel] / 100.0, 0.42); |
| result[channel] = -400.0 * n / (n + 27.13) + 0.1; |
| } |
| } |
| return result; |
| } |
| |
| private static double[] forwardPostAdaptationConeResponse(double[] RGB, double[] vc) { |
| return new double[] { vc[VC_D_RGB_R] * RGB[0], vc[VC_D_RGB_G] * RGB[1], vc[VC_D_RGB_B] * RGB[2] }; |
| } |
| |
| public static double[] CAT02toHPE(double[] RGB) { |
| double[] RGBPrime = new double[3]; |
| RGBPrime[0] = 0.7409792 * RGB[0] + 0.2180250 * RGB[1] + 0.0410058 * RGB[2]; |
| RGBPrime[1] = 0.2853532 * RGB[0] + 0.6242014 * RGB[1] + 0.0904454 * RGB[2]; |
| RGBPrime[2] = -0.0096280 * RGB[0] - 0.0056980 * RGB[1] + 1.0153260 * RGB[2]; |
| return RGBPrime; |
| } |
| |
| private static double[] forwardPreAdaptationConeResponse(double[] XYZ) { |
| double[] RGB = new double[3]; |
| RGB[0] = 0.7328 * XYZ[0] + 0.4296 * XYZ[1] - 0.1624 * XYZ[2]; |
| RGB[1] = -0.7036 * XYZ[0] + 1.6975 * XYZ[1] + 0.0061 * XYZ[2]; |
| RGB[2] = 0.0030 * XYZ[0] + 0.0136 * XYZ[1] + 0.9834 * XYZ[2]; |
| return RGB; |
| } |
| |
| static final int SUR_F = 0; |
| static final int SUR_C = 1; |
| static final int SUR_N_C = 2; |
| |
| static final int VC_X_W = 0; |
| static final int VC_Y_W = 1; |
| static final int VC_Z_W = 2; |
| static final int VC_L_A = 3; |
| static final int VC_Y_B = 4; |
| static final int VC_F = 5; |
| static final int VC_C = 6; |
| static final int VC_N_C = 7; |
| |
| static final int VC_Z = 8; |
| static final int VC_N = 9; |
| static final int VC_N_BB = 10; |
| static final int VC_N_CB = 11; |
| static final int VC_A_W = 12; |
| static final int VC_F_L = 13; |
| static final int VC_D_RGB_R = 14; |
| static final int VC_D_RGB_G = 15; |
| static final int VC_D_RGB_B = 16; |
| |
| static double[] vc(double[] xyz_w, double L_A, double Y_b, double[] surrounding) { |
| double[] vc = new double[17]; |
| vc[VC_X_W] = xyz_w[0]; |
| vc[VC_Y_W] = xyz_w[1]; |
| vc[VC_Z_W] = xyz_w[2]; |
| vc[VC_L_A] = L_A; |
| vc[VC_Y_B] = Y_b; |
| vc[VC_F] = surrounding[SUR_F]; |
| vc[VC_C] = surrounding[SUR_C]; |
| vc[VC_N_C] = surrounding[SUR_N_C]; |
| |
| double[] RGB_w = forwardPreAdaptationConeResponse(xyz_w); |
| double D = Math.max(0.0, Math.min(1.0, vc[VC_F] * (1.0 - (1.0 / 3.6) * Math.pow(Math.E, (-L_A - 42.0) / 92.0)))); |
| double Yw = xyz_w[1]; |
| double[] RGB_c = new double[] { |
| (D * Yw / RGB_w[0]) + (1.0 - D), |
| (D * Yw / RGB_w[1]) + (1.0 - D), |
| (D * Yw / RGB_w[2]) + (1.0 - D), |
| }; |
| |
| // calculate increase in brightness and colorfulness caused by brighter viewing environments |
| double L_Ax5 = 5.0 * L_A; |
| double k = 1.0 / (L_Ax5 + 1.0); |
| double kpow4 = Math.pow(k, 4.0); |
| vc[VC_F_L] = 0.2 * kpow4 * (L_Ax5) + 0.1 * Math.pow(1.0 - kpow4, 2.0) * Math.pow(L_Ax5, 1.0/3.0); |
| |
| // calculate response compression on J and C caused by background lightness. |
| vc[VC_N] = Y_b / Yw; |
| vc[VC_Z] = 1.48 + Math.sqrt(vc[VC_N]); |
| |
| vc[VC_N_BB] = 0.725 * Math.pow(1.0 / vc[VC_N], 0.2); |
| vc[VC_N_CB] = vc[VC_N_BB]; // chromatic contrast factors (calculate increase in J, Q, and C caused by dark backgrounds) |
| |
| // calculate achromatic response to white |
| double[] RGB_wc = new double[] {RGB_c[0] * RGB_w[0], RGB_c[1] * RGB_w[1], RGB_c[2] * RGB_w[2]}; |
| double[] RGBPrime_w = CAT02toHPE(RGB_wc); |
| double[] RGBPrime_aw = new double[3]; |
| for(int channel = 0; channel < RGBPrime_w.length; channel++) { |
| if(RGBPrime_w[channel] >= 0) { |
| double n = Math.pow(vc[VC_F_L] * RGBPrime_w[channel] / 100.0, 0.42); |
| RGBPrime_aw[channel] = 400.0 * n / (n + 27.13) + 0.1; |
| } else { |
| double n = Math.pow(-1.0 * vc[VC_F_L] * RGBPrime_w[channel] / 100.0, 0.42); |
| RGBPrime_aw[channel] = -400.0 * n / (n + 27.13) + 0.1; |
| } |
| } |
| vc[VC_A_W] = (2.0 * RGBPrime_aw[0] + RGBPrime_aw[1] + RGBPrime_aw[2] / 20.0 - 0.305) * vc[VC_N_BB]; |
| vc[VC_D_RGB_R] = RGB_c[0]; |
| vc[VC_D_RGB_G] = RGB_c[1]; |
| vc[VC_D_RGB_B] = RGB_c[2]; |
| return vc; |
| } |
| |
| public static double[] rgb2cielab(double[] rgb) { |
| return xyz2lab(rgb2xyz(rgb)); |
| } |
| |
| private static double[] rgb2xyz(double[] rgb) { |
| double vr = pivotRgb(rgb[0]); |
| double vg = pivotRgb(rgb[1]); |
| double vb = pivotRgb(rgb[2]); |
| // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html |
| double x = vr * 0.4124564 + vg * 0.3575761 + vb * 0.1804375; |
| double y = vr * 0.2126729 + vg * 0.7151522 + vb * 0.0721750; |
| double z = vr * 0.0193339 + vg * 0.1191920 + vb * 0.9503041; |
| return new double[] { x, y, z }; |
| } |
| |
| private static double pivotRgb(double n) { |
| return n > 0.04045 ? Math.pow((n + 0.055) / 1.055, 2.4) : n / 12.92; |
| } |
| |
| private static double[] xyz2lab(double[] xyz) { |
| double fx = pivotXyz(xyz[0]); |
| double fy = pivotXyz(xyz[1]); |
| double fz = pivotXyz(xyz[2]); |
| double l = 116.0 * fy - 16.0; |
| double a = 500.0 * (fx - fy); |
| double b = 200.0 * (fy - fz); |
| return new double[] { l, a, b }; |
| } |
| |
| private static final double epsilon = 216.0 / 24389.0; |
| private static final double kappa = 24389.0 / 27.0; |
| private static double pivotXyz(double n) { |
| return n > epsilon ? Math.cbrt(n) : (kappa * n + 16) / 116; |
| } |
| |
| private static double sqr(double n) { |
| return n * n; |
| } |
| |
| } |