Skip to content

Commit

Permalink
[Color Picker] Feature/lab color representation (#12935)
Browse files Browse the repository at this point in the history
* Created logic to convrt RGB to CIELAB (with intermediate step to CIEXYZ)

* Added CIELAB to the available color representation types

* Created tests for the color conversion from RGB to LAB (and for RGB to XYZ)

* Update ColorPickerViewModel to keep the L*a*b* format the same

* Improved variable names & comment

* Remove url from color converting website to avoid unnecessary license issues

* Removed typo of the wrong variable

* Added expected words into dictionary

* Added links to explain used formulas

* Added CIE XYZ color space

* Added 'SRGB' to the dictionary

* Updated the range for the X and Z value in the CIE XYZ color space comments

* Fixed XYZ to LAB calculations

* Changed output format for CIELAb

Changed L*a*b*(L,a,b) to CIELab(L,a,b)

* Changed output in tests

* Fixed tests

* Added extra accuracy

* Add decimal places to cielab and ciexyz formats

Co-authored-by: Jaime Bernardo <jaime@janeasystems.com>
  • Loading branch information
RubenFricke and jaimecbernardo committed Sep 24, 2021
1 parent 88e2426 commit 3358fd9
Show file tree
Hide file tree
Showing 8 changed files with 259 additions and 1 deletion.
7 changes: 7 additions & 0 deletions .github/actions/spell-check/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -230,10 +230,13 @@ CHILDACTIVATE
CHILDWINDOW
chrdavis
chrisharris
chromaticities
chrono
Chrzan
chrzan
CHT
cielab
CIEXYZ
CImage
cinttypes
cla
Expand Down Expand Up @@ -289,6 +292,7 @@ comhost
cominterop
commandline
commctrl
companding
Compat
COMPOSITIONFULL
comsupp
Expand Down Expand Up @@ -403,6 +407,7 @@ ddd
ddee
ddf
Deact
debian
DECLAR
declspec
decltype
Expand Down Expand Up @@ -1265,6 +1270,7 @@ mii
MIIM
millis
mimetype
mindaro
Minimizeallwindows
MINIMIZEBOX
miniz
Expand Down Expand Up @@ -1958,6 +1964,7 @@ SRCCOPY
sre
sregex
SResize
SRGB
srme
srre
srw
Expand Down
71 changes: 71 additions & 0 deletions src/modules/colorPicker/ColorPickerUI/Helpers/ColorHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,77 @@ internal static (string hue, double whiteness, double blackness) ConvertToNatura
return (GetNaturalColorFromHue(color.GetHue()), min, 1 - max);
}

/// <summary>
/// Convert a given <see cref="Color"/> to a CIE LAB color (LAB)
/// </summary>
/// <param name="color">The <see cref="Color"/> to convert</param>
/// <returns>The lightness [0..100] and two chromaticities [-128..127]</returns>
internal static (double lightness, double chromaticityA, double chromaticityB) ConvertToCIELABColor(Color color)
{
var xyz = ConvertToCIEXYZColor(color);
var lab = GetCIELABColorFromCIEXYZ(xyz.x, xyz.y, xyz.z);

return lab;
}

/// <summary>
/// Convert a given <see cref="Color"/> to a CIE XYZ color (XYZ)
/// The constants of the formula used come from this wikipedia page:
/// https://en.wikipedia.org/wiki/SRGB#The_reverse_transformation_(sRGB_to_CIE_XYZ)
/// </summary>
/// <param name="color">The <see cref="Color"/> to convert</param>
/// <returns>The X [0..1], Y [0..1] and Z [0..1]</returns>
internal static (double x, double y, double z) ConvertToCIEXYZColor(Color color)
{
double r = color.R / 255d;
double g = color.G / 255d;
double b = color.B / 255d;

// inverse companding, gamma correction must be undone
double rLinear = (r > 0.04045) ? Math.Pow((r + 0.055) / 1.055, 2.4) : (r / 12.92);
double gLinear = (g > 0.04045) ? Math.Pow((g + 0.055) / 1.055, 2.4) : (g / 12.92);
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)
);
}

/// <summary>
/// Convert a CIE XYZ color <see cref="double"/> to a CIE LAB color (LAB)
/// 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
/// </summary>
/// <param name="x">The <see cref="x"/> represents a mix of the three CIE RGB curves</param>
/// <param name="y">The <see cref="y"/> represents the luminance</param>
/// <param name="z">The <see cref="z"/> is quasi-equal to blue (of CIE RGB)</param>
/// <returns>The lightness [0..100] and two chromaticities [-128..127]</returns>
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;

// XYZ to CIELab transformation
double delta = 6d / 29;
double m = (1d / 3) * Math.Pow(delta, -2);
double t = Math.Pow(delta, 3);

double fx = (x > t) ? Math.Pow(x, 1.0 / 3.0) : (x * m) + (16.0 / 116.0);
double fy = (y > t) ? Math.Pow(y, 1.0 / 3.0) : (y * m) + (16.0 / 116.0);
double fz = (z > t) ? Math.Pow(z, 1.0 / 3.0) : (z * m) + (16.0 / 116.0);

double l = (116 * fy) - 16;
double a = 500 * (fx - fy);
double b = 200 * (fy - fz);

return (l, a, b);
}

/// <summary>
/// Return the natural color for the given hue value
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ internal static string GetStringRepresentation(Color color, ColorRepresentationT
ColorRepresentationType.HWB => ColorToHWB(color),
ColorRepresentationType.NCol => ColorToNCol(color),
ColorRepresentationType.RGB => ColorToRGB(color),
ColorRepresentationType.CIELAB => ColorToCIELAB(color),
ColorRepresentationType.CIEXYZ => ColorToCIEXYZ(color),

// Fall-back value, when "_userSettings.CopiedColorRepresentation.Value" is incorrect
_ => ColorToHex(color),
Expand Down Expand Up @@ -191,11 +193,46 @@ private static string ColorToNCol(Color color)
/// <summary>
/// Return a <see cref="string"/> representation of a RGB color
/// </summary>
/// <param name="color">The see cref="Color"/> for the RGB color presentation</param>
/// <param name="color">The <see cref="Color"/> for the RGB color presentation</param>
/// <returns>A <see cref="string"/> representation of a RGB color</returns>
private static string ColorToRGB(Color color)
=> $"rgb({color.R.ToString(CultureInfo.InvariantCulture)}"
+ $", {color.G.ToString(CultureInfo.InvariantCulture)}"
+ $", {color.B.ToString(CultureInfo.InvariantCulture)})";

/// <summary>
/// Returns a <see cref="string"/> representation of a CIE LAB color
/// </summary>
/// <param name="color">The <see cref="Color"/> for the CIE LAB color presentation</param>
/// <returns>A <see cref="string"/> representation of a CIE LAB color</returns>
private static string ColorToCIELAB(Color color)
{
var (lightness, chromaticityA, chromaticityB) = ColorHelper.ConvertToCIELABColor(color);
lightness = Math.Round(lightness, 2);
chromaticityA = Math.Round(chromaticityA, 2);
chromaticityB = Math.Round(chromaticityB, 2);

return $"CIELab({lightness.ToString(CultureInfo.InvariantCulture)}" +
$", {chromaticityA.ToString(CultureInfo.InvariantCulture)}" +
$", {chromaticityB.ToString(CultureInfo.InvariantCulture)})";
}

/// <summary>
/// Returns a <see cref="string"/> representation of a CIE XYZ color
/// </summary>
/// <param name="color">The <see cref="Color"/> for the CIE XYZ color presentation</param>
/// <returns>A <see cref="string"/> representation of a CIE XYZ color</returns>
private static string ColorToCIEXYZ(Color color)
{
var (x, y, z) = ColorHelper.ConvertToCIEXYZColor(color);

x = Math.Round(x * 100, 4);
y = Math.Round(y * 100, 4);
z = Math.Round(z * 100, 4);

return $"xyz({x.ToString(CultureInfo.InvariantCulture)}" +
$", {y.ToString(CultureInfo.InvariantCulture)}" +
$", {z.ToString(CultureInfo.InvariantCulture)})";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,18 @@ private void SetupAllColorRepresentations()
FormatName = ColorRepresentationType.NCol.ToString(),
Convert = (Color color) => { return ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.NCol); },
});
_allColorRepresentations.Add(
new ColorFormatModel()
{
FormatName = ColorRepresentationType.CIELAB.ToString(),
Convert = (Color color) => { return ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.CIELAB); },
});
_allColorRepresentations.Add(
new ColorFormatModel()
{
FormatName = ColorRepresentationType.CIEXYZ.ToString(),
Convert = (Color color) => { return ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.CIEXYZ); },
});

_userSettings.VisibleColorFormats.CollectionChanged += VisibleColorFormats_CollectionChanged;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,119 @@ public void ColorRGBtoNColTest(string hexValue, string hue, double whiteness, do
Assert.AreEqual(result.blackness * 100d, blackness, 0.5d);
}

[TestMethod]
[DataRow("FFFFFF", 100.00, 0.00, -0.01)] // white
[DataRow("808080", 53.59, 0.00, -0.01)] // gray
[DataRow("000000", 0.00, 0.00, 0.00)] // black
[DataRow("FF0000", 53.23, 80.11, 67.22)] // 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("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("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("FFEF00", 93.01, -13.86, 91.48)] // canary yellow
[DataRow("FFA600", 75.16, 23.41, 79.11)] // 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
public void ColorRGBtoCIELABTest(string hexValue, double lightness, double chromaticityA, double chromaticityB)
{
if (string.IsNullOrWhiteSpace(hexValue))
{
Assert.IsNotNull(hexValue);
}

Assert.IsTrue(hexValue.Length >= 6);

var red = int.Parse(hexValue.Substring(0, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
var green = int.Parse(hexValue.Substring(2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
var blue = int.Parse(hexValue.Substring(4, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture);

var color = Color.FromArgb(255, red, green, blue);
var result = ColorHelper.ConvertToCIELABColor(color);

// lightness[0..100]
Assert.AreEqual(Math.Round(result.lightness, 2), lightness);

// chromaticityA[-128..127]
Assert.AreEqual(Math.Round(result.chromaticityA, 2), chromaticityA);

// chromaticityB[-128..127]
Assert.AreEqual(Math.Round(result.chromaticityB, 2), chromaticityB);
}

[TestMethod]
[DataRow("FFFFFF", 95.0500, 100.0000, 108.9000)] // white
[DataRow("808080", 20.5175, 21.5861, 23.5072)] // 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
public void ColorRGBtoCIEXYZTest(string hexValue, double x, double y, double z)
{
if (string.IsNullOrWhiteSpace(hexValue))
{
Assert.IsNotNull(hexValue);
}

Assert.IsTrue(hexValue.Length >= 6);

var red = int.Parse(hexValue.Substring(0, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
var green = int.Parse(hexValue.Substring(2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
var blue = int.Parse(hexValue.Substring(4, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture);

var color = Color.FromArgb(255, red, green, blue);
var result = ColorHelper.ConvertToCIEXYZColor(color);

// x[0..0.95047]
Assert.AreEqual(Math.Round(result.x * 100, 4), x);

// y[0..1]
Assert.AreEqual(Math.Round(result.y * 100, 4), y);

// z[0..1.08883]
Assert.AreEqual(Math.Round(result.z * 100, 4), z);
}

[TestMethod]
public void ColorRGBtoCMYKZeroDivTest()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public class ColorRepresentationHelperTest
[DataRow(ColorRepresentationType.HSV, "hsv(0, 0%, 0%)")]
[DataRow(ColorRepresentationType.HWB, "hwb(0, 0%, 100%)")]
[DataRow(ColorRepresentationType.RGB, "rgb(0, 0, 0)")]
[DataRow(ColorRepresentationType.CIELAB, "CIELab(0, 0, 0)")]
[DataRow(ColorRepresentationType.CIEXYZ, "xyz(0, 0, 0)")]

public void GetStringRepresentationTest(ColorRepresentationType type, string expected)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,15 @@ public enum ColorRepresentationType
/// Color presentation as natural color (hue, whiteness[0%..100%], blackness[0%..100%])
/// </summary>
NCol = 8,

/// <summary>
/// Color presentation as CIELAB color space, also referred to as CIELAB(L[0..100], A[-128..127], B[-128..127])
/// </summary>
CIELAB = 9,

/// <summary>
/// Color presentation as CIEXYZ color space (X[0..95], Y[0..100], Z[0..109]
/// </summary>
CIEXYZ = 10,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ public ColorPickerViewModel(ISettingsUtils settingsUtils, ISettingsRepository<Ge
{ ColorRepresentationType.HWB, "HWB - hwb(100, 50%, 75%)" },
{ ColorRepresentationType.NCol, "NCol - R10, 50%, 75%" },
{ ColorRepresentationType.RGB, "RGB - rgb(100, 50, 75)" },
{ ColorRepresentationType.CIELAB, "CIE LAB - CIELab(76, 21, 80)" },
{ ColorRepresentationType.CIEXYZ, "CIE XYZ - xyz(56, 50, 7)" },
};

GeneralSettingsConfig = settingsRepository.SettingsConfig;
Expand Down Expand Up @@ -218,6 +220,8 @@ private void InitializeColorFormats()
var hsiFormatName = ColorRepresentationType.HSI.ToString();
var hwbFormatName = ColorRepresentationType.HWB.ToString();
var ncolFormatName = ColorRepresentationType.NCol.ToString();
var cielabFormatName = ColorRepresentationType.CIELAB.ToString();
var ciexyzFormatName = ColorRepresentationType.CIEXYZ.ToString();

formatsUnordered.Add(new ColorFormatModel(hexFormatName, "#EF68FF", visibleFormats.ContainsKey(hexFormatName) && visibleFormats[hexFormatName]));
formatsUnordered.Add(new ColorFormatModel(rgbFormatName, "rgb(239, 104, 255)", visibleFormats.ContainsKey(rgbFormatName) && visibleFormats[rgbFormatName]));
Expand All @@ -228,6 +232,8 @@ private void InitializeColorFormats()
formatsUnordered.Add(new ColorFormatModel(hsiFormatName, "hsi(100, 50%, 75%)", visibleFormats.ContainsKey(hsiFormatName) && visibleFormats[hsiFormatName]));
formatsUnordered.Add(new ColorFormatModel(hwbFormatName, "hwb(100, 50%, 75%)", visibleFormats.ContainsKey(hwbFormatName) && visibleFormats[hwbFormatName]));
formatsUnordered.Add(new ColorFormatModel(ncolFormatName, "R10, 50%, 75%", visibleFormats.ContainsKey(ncolFormatName) && visibleFormats[ncolFormatName]));
formatsUnordered.Add(new ColorFormatModel(cielabFormatName, "CIELab(66, 72, -52)", visibleFormats.ContainsKey(cielabFormatName) && visibleFormats[cielabFormatName]));
formatsUnordered.Add(new ColorFormatModel(ciexyzFormatName, "xyz(59, 35, 98)", visibleFormats.ContainsKey(ciexyzFormatName) && visibleFormats[ciexyzFormatName]));

foreach (var storedColorFormat in _colorPickerSettings.Properties.VisibleColorFormats)
{
Expand Down

0 comments on commit 3358fd9

Please sign in to comment.