From 42ba0083230b5238eca67bb01c9b4f511df3c62c Mon Sep 17 00:00:00 2001 From: Star Brilliant Date: Mon, 21 Mar 2022 12:15:48 +0000 Subject: [PATCH] [ColorPicker] Increase precision of CIEXYZ format (#17041) * Increase precision of CIEXYZ conversion matrix The output has 4 decimal places, so the conversion matrix should be more than 6 digits to avoid round-off errors. * Match unit tests and docs with new CIEXYZ conversion matrix * Remove negative sign from zeros I generated the unit test results from other color-management systems. It seems that they sometimes output negative zeros for very small values. Let's just remove the negative signs for aesthetic. * Fix spelling mistakes in ColorConverterTest.cs * Explain how to obtain CIEXYZ unit test reference values * Explain the CIELAB output is D65 adapted version * Add words related to CIEXYZ conversion to spellcheck bypass list --- .github/actions/spell-check/expect.txt | 3 + .../ColorPickerUI/Helpers/ColorHelper.cs | 29 +++-- .../Helpers/ColorConverterTest.cs | 107 ++++++++++-------- 3 files changed, 80 insertions(+), 59 deletions(-) diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index b2ee5211bd..00bc393b29 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -178,6 +178,7 @@ bpp bricelam BRIGHTGREEN Browsable +brucelindbloom bsd bstr bti @@ -523,6 +524,7 @@ enum EOAC eol epicgames +Eqn ERASEBKGND EREOF EResize @@ -2033,6 +2035,7 @@ towupper tracelogging traies transcoded +transicc Transnistria TRAYMOUSEMESSAGE triaging diff --git a/src/modules/colorPicker/ColorPickerUI/Helpers/ColorHelper.cs b/src/modules/colorPicker/ColorPickerUI/Helpers/ColorHelper.cs index 7015201b23..c5b645e8be 100644 --- a/src/modules/colorPicker/ColorPickerUI/Helpers/ColorHelper.cs +++ b/src/modules/colorPicker/ColorPickerUI/Helpers/ColorHelper.cs @@ -162,8 +162,10 @@ namespace ColorPicker.Helpers /// /// Convert a given to a CIE XYZ color (XYZ) - /// The constants of the formula used come from this wikipedia page: + /// The constants of the formula matches this Wikipedia page, but at a higher precision: /// https://en.wikipedia.org/wiki/SRGB#The_reverse_transformation_(sRGB_to_CIE_XYZ) + /// This page provides a method to calculate the constants: + /// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html /// /// The to convert /// The X [0..1], Y [0..1] and Z [0..1] @@ -179,14 +181,14 @@ namespace ColorPicker.Helpers double bLinear = (b > 0.04045) ? Math.Pow((b + 0.055) / 1.055, 2.4) : (b / 12.92); return ( - (rLinear * 0.4124) + (gLinear * 0.3576) + (bLinear * 0.1805), - (rLinear * 0.2126) + (gLinear * 0.7152) + (bLinear * 0.0722), - (rLinear * 0.0193) + (gLinear * 0.1192) + (bLinear * 0.9505) + (rLinear * 0.41239079926595948) + (gLinear * 0.35758433938387796) + (bLinear * 0.18048078840183429), + (rLinear * 0.21263900587151036) + (gLinear * 0.71516867876775593) + (bLinear * 0.07219231536073372), + (rLinear * 0.01933081871559185) + (gLinear * 0.11919477979462599) + (bLinear * 0.95053215224966058) ); } /// - /// Convert a CIE XYZ color to a CIE LAB color (LAB) + /// Convert a CIE XYZ color to a CIE LAB color (LAB) adapted to sRGB D65 white point /// The constants of the formula used come from this wikipedia page: /// https://en.wikipedia.org/wiki/CIELAB_color_space#Converting_between_CIELAB_and_CIEXYZ_coordinates /// @@ -197,10 +199,19 @@ namespace ColorPicker.Helpers private static (double lightness, double chromaticityA, double chromaticityB) GetCIELABColorFromCIEXYZ(double x, double y, double z) { - // These values are based on the D65 Illuminant - x = x * 100 / 95.0489; - y = y * 100 / 100.0; - z = z * 100 / 108.8840; + // sRGB reference white (x=0.3127, y=0.3290, Y=1.0), actually CIE Standard Illuminant D65 truncated to 4 decimal places, + // then converted to XYZ using the formula: + // X = x * (Y / y) + // Y = Y + // Z = (1 - x - y) * (Y / y) + double x_n = 0.9504559270516717; + double y_n = 1.0; + double z_n = 1.0890577507598784; + + // Scale XYZ values relative to reference white + x /= x_n; + y /= y_n; + z /= z_n; // XYZ to CIELab transformation double delta = 6d / 29; diff --git a/src/modules/colorPicker/UnitTest-ColorPickerUI/Helpers/ColorConverterTest.cs b/src/modules/colorPicker/UnitTest-ColorPickerUI/Helpers/ColorConverterTest.cs index 68bdb9597c..b0dfa8eb89 100644 --- a/src/modules/colorPicker/UnitTest-ColorPickerUI/Helpers/ColorConverterTest.cs +++ b/src/modules/colorPicker/UnitTest-ColorPickerUI/Helpers/ColorConverterTest.cs @@ -305,35 +305,35 @@ namespace Microsoft.ColorPicker.UnitTests } [TestMethod] - [DataRow("FFFFFF", 100.00, 0.00, -0.01)] // white - [DataRow("808080", 53.59, 0.00, -0.01)] // gray + [DataRow("FFFFFF", 100.00, 0.00, 0.00)] // white + [DataRow("808080", 53.59, 0.00, 0.00)] // gray [DataRow("000000", 0.00, 0.00, 0.00)] // black - [DataRow("FF0000", 53.23, 80.11, 67.22)] // red + [DataRow("FF0000", 53.24, 80.09, 67.20)] // red [DataRow("008000", 46.23, -51.70, 49.90)] // green [DataRow("80FFFF", 93.16, -35.23, -10.87)] // cyan - [DataRow("8080FF", 59.20, 33.1, -63.47)] // blue - [DataRow("BF40BF", 50.10, 65.51, -41.49)] // magenta + [DataRow("8080FF", 59.20, 33.10, -63.46)] // blue + [DataRow("BF40BF", 50.10, 65.50, -41.48)] // magenta [DataRow("BFBF00", 75.04, -17.35, 76.03)] // yellow [DataRow("008000", 46.23, -51.70, 49.90)] // green - [DataRow("8080FF", 59.20, 33.1, -63.47)] // blue - [DataRow("BF40BF", 50.10, 65.51, -41.49)] // magenta - [DataRow("0048BA", 34.35, 27.94, -64.81)] // absolute zero + [DataRow("8080FF", 59.20, 33.10, -63.46)] // blue + [DataRow("BF40BF", 50.10, 65.50, -41.48)] // magenta + [DataRow("0048BA", 34.35, 27.94, -64.80)] // absolute zero [DataRow("B0BF1A", 73.91, -23.39, 71.15)] // acid green - [DataRow("D0FF14", 93.87, -40.21, 88.97)] // arctic lime - [DataRow("1B4D3E", 29.13, -20.97, 3.95)] // brunswick green + [DataRow("D0FF14", 93.87, -40.20, 88.97)] // arctic lime + [DataRow("1B4D3E", 29.13, -20.96, 3.95)] // brunswick green [DataRow("FFEF00", 93.01, -13.86, 91.48)] // canary yellow - [DataRow("FFA600", 75.16, 23.41, 79.11)] // cheese + [DataRow("FFA600", 75.16, 23.41, 79.10)] // cheese [DataRow("1A2421", 13.18, -5.23, 0.56)] // dark jungle green - [DataRow("003399", 25.77, 28.89, -59.10)] // dark powder blue - [DataRow("D70A53", 46.03, 71.91, 18.02)] // debian red - [DataRow("80FFD5", 92.09, -45.08, 9.28)] // fathom secret green - [DataRow("EFDFBB", 89.26, -0.13, 19.64)] // dutch white - [DataRow("5218FA", 36.65, 75.63, -97.71)] // han purple - [DataRow("FF496C", 59.07, 69.90, 21.79)] // infra red - [DataRow("545AA7", 41.20, 19.32, -42.35)] // liberty - [DataRow("E6A8D7", 75.91, 30.13, -14.80)] // light orchid - [DataRow("ADDFAD", 84.32, -25.67, 19.36)] // light moss green - [DataRow("E3F988", 94.25, -23.70, 51.57)] // mindaro + [DataRow("003399", 25.76, 28.89, -59.09)] // dark powder blue + [DataRow("D70A53", 46.03, 71.90, 18.03)] // debian red + [DataRow("80FFD5", 92.09, -45.08, 9.29)] // fathom secret green + [DataRow("EFDFBB", 89.26, -0.13, 19.65)] // dutch white + [DataRow("5218FA", 36.65, 75.63, -97.70)] // han purple + [DataRow("FF496C", 59.08, 69.89, 21.80)] // infra red + [DataRow("545AA7", 41.20, 19.32, -42.34)] // liberty + [DataRow("E6A8D7", 75.91, 30.13, -14.79)] // light orchid + [DataRow("ADDFAD", 84.32, -25.67, 19.37)] // light moss green + [DataRow("E3F988", 94.25, -23.70, 51.58)] // mindaro public void ColorRGBtoCIELABTest(string hexValue, double lightness, double chromaticityA, double chromaticityB) { if (string.IsNullOrWhiteSpace(hexValue)) @@ -360,37 +360,44 @@ namespace Microsoft.ColorPicker.UnitTests Assert.AreEqual(Math.Round(result.chromaticityB, 2), chromaticityB); } + // The following results are computed using LittleCMS2, an open-source color management engine, + // with the following command-line arguments: + // echo 0xFF 0xFF 0xFF | transicc -i "*sRGB" -o "*XYZ" -t 3 -d 0 + // where "0xFF 0xFF 0xFF" are filled in with the hexadecimal red/green/blue values; + // "-t 3" means using absolute colorimetric intent, in other words, disabling white point scaling; + // "-d 0" means disabling chromatic adaptation, otherwise it will output CIEXYZ-D50 instead of D65. + // + // If we have the same results as the reference output listed below, it means our algorithm is accurate. [TestMethod] - [DataRow("FFFFFF", 95.0500, 100.0000, 108.9000)] // white - [DataRow("808080", 20.5175, 21.5861, 23.5072)] // gray + [DataRow("FFFFFF", 95.0456, 100.0000, 108.9058)] // white + [DataRow("808080", 20.5166, 21.5861, 23.5085)] // gray [DataRow("000000", 0.0000, 0.0000, 0.0000)] // black - [DataRow("FF0000", 41.2400, 21.2600, 1.9300)] // red - [DataRow("008000", 7.7192, 15.4383, 2.5731)] // green - [DataRow("80FFFF", 62.7121, 83.3292, 107.3866)] // cyan - [DataRow("8080FF", 34.6713, 27.2475, 98.0397)] // blue - [DataRow("BF40BF", 32.7232, 18.5047, 51.1373)] // magenta - [DataRow("BFBF00", 40.1167, 48.3380, 7.2158)] // yellow - [DataRow("008000", 7.7192, 15.4383, 2.5731)] // green - [DataRow("80FFFF", 62.7121, 83.3292, 107.3866)] // cyan - [DataRow("8080FF", 34.6713, 27.2475, 98.0397)] // blue - [DataRow("BF40BF", 32.7232, 18.5047, 51.1373)] // magenta - [DataRow("0048BA", 11.1803, 8.1799, 47.4440)] // absolute zero - [DataRow("B0BF1A", 36.7218, 46.5663, 8.0300)] // acid green - [DataRow("D0FF14", 61.8987, 84.9804, 13.8023)] // arctic lime - [DataRow("1B4D3E", 3.9754, 5.8886, 5.4845)] // brunswick green - [DataRow("FFEF00", 72.1065, 82.9930, 12.2188)] // canary yellow - [DataRow("FFA600", 54.8762, 48.5324, 6.4754)] // cheese - [DataRow("1A2421", 1.3314, 1.5912, 1.6758)] // dark jungle green - [DataRow("003399", 6.9336, 4.6676, 30.6725)] // dark powder blue - [DataRow("D70A53", 29.6942, 15.2887, 9.5696)] // debian red - [DataRow("80FFD5", 56.6723, 80.9133, 75.5817)] // fathom secret green - [DataRow("EFDFBB", 70.9539, 74.7139, 57.6953)] // dutch white - [DataRow("5218FA", 21.0616, 9.3492, 91.1370)] // han purple - [DataRow("FF496C", 46.3293, 27.1078, 16.9779)] // infra red - [DataRow("545AA7", 14.2874, 11.9872, 38.1199)] // liberty - [DataRow("E6A8D7", 58.9015, 49.7346, 70.7853)] // light orchid - [DataRow("ADDFAD", 51.1641, 64.6767, 49.3224)] // light moss green - [DataRow("E3F988", 69.9982, 85.8598, 36.1759)] // mindaro + [DataRow("FF0000", 41.2391, 21.2639, 1.9331)] // red + [DataRow("008000", 7.7188, 15.4377, 2.5729)] // green + [DataRow("80FFFF", 62.7084, 83.3261, 107.3900)] // cyan + [DataRow("8080FF", 34.6688, 27.2469, 98.0434)] // blue + [DataRow("BF40BF", 32.7217, 18.5062, 51.1405)] // magenta + [DataRow("BFBF00", 40.1154, 48.3384, 7.2171)] // yellow + [DataRow("008000", 7.7188, 15.4377, 2.5729)] // green + [DataRow("8080FF", 34.6688, 27.2469, 98.0434)] // blue + [DataRow("BF40BF", 32.7217, 18.5062, 51.1405)] // magenta + [DataRow("0048BA", 11.1792, 8.1793, 47.4455)] // absolute zero + [DataRow("B0BF1A", 36.7205, 46.5663, 8.0311)] // acid green + [DataRow("D0FF14", 61.8965, 84.9797, 13.8037)] // arctic lime + [DataRow("1B4D3E", 3.9752, 5.8883, 5.4847)] // brunswick green + [DataRow("FFEF00", 72.1042, 82.9942, 12.2215)] // canary yellow + [DataRow("FFA600", 54.8747, 48.5351, 6.4783)] // cheese + [DataRow("1A2421", 1.3313, 1.5911, 1.6759)] // dark jungle green + [DataRow("003399", 6.9329, 4.6672, 30.6735)] // dark powder blue + [DataRow("D70A53", 29.6934, 15.2913, 9.5719)] // debian red + [DataRow("80FFD5", 56.6693, 80.9105, 75.5840)] // fathom secret green + [DataRow("EFDFBB", 70.9510, 74.7146, 57.6991)] // dutch white + [DataRow("5218FA", 21.0597, 9.3488, 91.1403)] // han purple + [DataRow("FF496C", 46.3280, 27.1114, 16.9814)] // infra red + [DataRow("545AA7", 14.2864, 11.9869, 38.1214)] // liberty + [DataRow("E6A8D7", 58.8989, 49.7359, 70.7897)] // light orchid + [DataRow("ADDFAD", 51.1617, 64.6757, 49.3246)] // light moss green + [DataRow("E3F988", 69.9955, 85.8597, 36.1785)] // mindaro public void ColorRGBtoCIEXYZTest(string hexValue, double x, double y, double z) { if (string.IsNullOrWhiteSpace(hexValue))