From 0511e6b81240ce1807297705d0fe73cfd8ff3e61 Mon Sep 17 00:00:00 2001 From: Rubens Fernandes Date: Tue, 5 Jan 2016 10:24:19 +1030 Subject: [PATCH 1/3] Not finished. Former-commit-id: 72ffa4557a16eb5fecc2eff4a0877304c760eb89 Former-commit-id: 884424c2d761c372c4bc720474fb5268c0fc7ed3 Former-commit-id: 3a228da3c7a44aa86c5a030a6be873c55ffe4bf6 --- src/ImageProcessor/Colors/ColorTransforms.cs | 31 +++ .../Colors/Colorspaces/CieLab.cs | 218 ++++++++++++++++++ 2 files changed, 249 insertions(+) create mode 100644 src/ImageProcessor/Colors/Colorspaces/CieLab.cs diff --git a/src/ImageProcessor/Colors/ColorTransforms.cs b/src/ImageProcessor/Colors/ColorTransforms.cs index 5c7dc99ef..fc3a01a78 100644 --- a/src/ImageProcessor/Colors/ColorTransforms.cs +++ b/src/ImageProcessor/Colors/ColorTransforms.cs @@ -173,6 +173,37 @@ namespace ImageProcessor return new Color(r, g, b); } + /// + /// Allows the implicit conversion of an instance of to a + /// . + /// + /// The instance of to convert. + /// + /// An instance of . + /// + public static implicit operator Color(CieLab cieLabColor) + { + // First convert back to XYZ... + float y = (cieLabColor.L + 16F) / 116F; + float x = cieLabColor.A / 500F + y; + float z = y - cieLabColor.B / 200F; + + float x3 = (float)Math.Pow(x, 3); + float y3 = (float)Math.Pow(y, 3); + float z3 = (float)Math.Pow(z, 3); + + y = (y3 > 0.008856F) ? y3 : (y - 16F / 116F) / 7.787F; + x = (x3 > 0.008856F) ? x3 : (x - 16F / 116F) / 7.787F; + z = (z3 > 0.008856F) ? z3 : (z - 16F / 116F) / 7.787F; + + // Then XYZ to RGB + float r = (x * 3.240969941904521F) + (y * -1.537383177570093F) + (z * -0.498610760293F); + float g = (x * -0.96924363628087F) + (y * 1.87596750150772F) + (z * 0.041555057407175F); + float b = (x * 0.055630079696993F) + (y * -0.20397695888897F) + (z * 1.056971514242878F); + + return Color.InverseCompand(new Color(r, g, b)); + } + /// /// Gets the color component from the given values. /// diff --git a/src/ImageProcessor/Colors/Colorspaces/CieLab.cs b/src/ImageProcessor/Colors/Colorspaces/CieLab.cs new file mode 100644 index 000000000..978e0f0ae --- /dev/null +++ b/src/ImageProcessor/Colors/Colorspaces/CieLab.cs @@ -0,0 +1,218 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessor +{ + using System; + using System.ComponentModel; + using System.Numerics; + + /// + /// Represents an CIE LAB 1976 color. + /// + public struct CieLab : IEquatable + { + /// + /// Represents a that has L, A, B values set to zero. + /// + public static readonly CieLab Empty = default(CieLab); + + /// + /// The epsilon for comparing floating point numbers. + /// + private const float Epsilon = 0.0001f; + + /// + /// The backing vector for SIMD support. + /// + private Vector3 backingVector; + + /// + /// Initializes a new instance of the struct. + /// + /// The lightness dimension. + /// The a (green - magenta) component. + /// The b (blue - yellow) component. + public CieLab(float l, float a, float b) + : this() + { + this.backingVector.X = ClampL(l); + this.backingVector.Y = ClampAB(a); + this.backingVector.Z = ClampAB(b); + } + + /// + /// Gets the lightness dimension. + /// A value ranging between 0 (black), 100 (diffuse white) or higher (specular white). + /// + public float L => this.backingVector.X; + + /// + /// Gets the a color component. + /// Negative is green, positive magenta. + /// + public float A => this.backingVector.Y; + + /// + /// Gets the b color component. + /// Negative is blue, positive is yellow + /// + public float B => this.backingVector.Z; + + /// + /// Gets a value indicating whether this is empty. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public bool IsEmpty => this.backingVector.Equals(default(Vector4)); + + /// + /// Allows the implicit conversion of an instance of to a + /// . + /// + /// + /// The instance of to convert. + /// + /// + /// An instance of . + /// + public static implicit operator CieLab(Color color) + { + // First convert to CIE XYZ + color = Color.InverseCompand(color.Limited); + + float x = (color.R * 0.41239079926595F) + (color.G * 0.35758433938387F) + (color.B * 0.18048078840183F); + float y = (color.R * 0.21263900587151F) + (color.G * 0.71516867876775F) + (color.B * 0.072192315360733F); + float z = (color.R * 0.019330818715591F) + (color.G * 0.11919477979462F) + (color.B * 0.95053215224966F); + + x *= 100F; + y *= 100F; + z *= 100F; + + // Now to LAB + x /= 95.047F; + y /= 100F; + z /= 108.883F; + + x = x > 0.008856 ? (float) Math.Pow(x, 1F / 3F) : (7.787F * x) + (16F / 116F); + y = y > 0.008856 ? (float) Math.Pow(y, 1F / 3F) : (7.787F * y) + (16F / 116F); + z = z > 0.008856 ? (float) Math.Pow(z, 1F / 3F) : (7.787F * z) + (16F / 116F); + + float l = (116 * y) - 16; + float a = 500 * (x - y); + float b = 200 * (y - z); + + return new CieLab(l, a, b); + } + + /// + /// Compares two objects for equality. + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + public static bool operator ==(CieLab left, CieLab right) + { + return left.Equals(right); + } + + /// + /// Compares two objects for inequality + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + public static bool operator !=(CieLab left, CieLab right) + { + return !left.Equals(right); + } + + /// + public override bool Equals(object obj) + { + if (obj is CieLab) + { + CieLab color = (CieLab)obj; + + return this.backingVector == color.backingVector; + } + + return false; + } + + /// + public override int GetHashCode() + { + return GetHashCode(this); + } + + /// + public override string ToString() + { + if (this.IsEmpty) + { + return "CieLab [Empty]"; + } + + return $"CieLab [ L={this.L:#0.##}, A={this.A:#0.##}, B={this.B:#0.##}]"; + } + + /// + public bool Equals(CieLab other) + { + return this.backingVector.Equals(other.backingVector); + } + + /// + /// Checks the range for lightness. + /// + /// + /// The value to check. + /// + /// + /// The sanitized . + /// + private static float ClampL(float value) + { + return value.Clamp(0, 100); + } + + /// + /// Checks the range for components A or B. + /// + /// + /// The value to check. + /// + /// + /// The sanitized . + /// + private static float ClampAB(float value) + { + return value.Clamp(-100, 100); + } + + /// + /// Returns the hash code for this instance. + /// + /// + /// The instance of to return the hash code for. + /// + /// + /// A 32-bit signed integer that is the hash code for this instance. + /// + private static int GetHashCode(CieLab color) => color.backingVector.GetHashCode(); + } +} From cb690286ec7b5907dd2662253ff039fe8b063825 Mon Sep 17 00:00:00 2001 From: Rubens Fernandes Date: Tue, 5 Jan 2016 20:52:45 +1030 Subject: [PATCH 2/3] Tests included. Having a hard time making the unit test results match any web converter to a high degree. I am using the Colormine coefs, but since their calculations use double and I am using float, I am still in trouble with a few values. Former-commit-id: e70cd5f9e2ca95e8c7f349fd7eed8669eb64a169 Former-commit-id: 75df1cb3a3c8bf5915cc7d29bf053d9b3b82a42b Former-commit-id: b0582b9a280ccd32912e14b7df7f99063589b27e --- src/ImageProcessor/Colors/ColorTransforms.cs | 14 ++-- .../Colors/Colorspaces/CieLab.cs | 12 +-- .../Colors/ColorConversionTests.cs | 81 +++++++++++++++++++ 3 files changed, 94 insertions(+), 13 deletions(-) diff --git a/src/ImageProcessor/Colors/ColorTransforms.cs b/src/ImageProcessor/Colors/ColorTransforms.cs index fc3a01a78..7643db0b1 100644 --- a/src/ImageProcessor/Colors/ColorTransforms.cs +++ b/src/ImageProcessor/Colors/ColorTransforms.cs @@ -188,20 +188,20 @@ namespace ImageProcessor float x = cieLabColor.A / 500F + y; float z = y - cieLabColor.B / 200F; - float x3 = (float)Math.Pow(x, 3); - float y3 = (float)Math.Pow(y, 3); - float z3 = (float)Math.Pow(z, 3); + float x3 = x * x * x; + float y3 = y * y * y; + float z3 = z * z * z; y = (y3 > 0.008856F) ? y3 : (y - 16F / 116F) / 7.787F; x = (x3 > 0.008856F) ? x3 : (x - 16F / 116F) / 7.787F; z = (z3 > 0.008856F) ? z3 : (z - 16F / 116F) / 7.787F; // Then XYZ to RGB - float r = (x * 3.240969941904521F) + (y * -1.537383177570093F) + (z * -0.498610760293F); - float g = (x * -0.96924363628087F) + (y * 1.87596750150772F) + (z * 0.041555057407175F); - float b = (x * 0.055630079696993F) + (y * -0.20397695888897F) + (z * 1.056971514242878F); + float r = (x * 3.2406F) + (y * -1.5372F) + (z * -0.4986F); + float g = (x * -0.9689F) + (y * 1.8758F) + (z * 0.0415F); + float b = (x * 0.0557F) + (y * -0.2040F) + (z * 1.0570F); - return Color.InverseCompand(new Color(r, g, b)); + return Color.Compand(new Color(r, g, b)); } /// diff --git a/src/ImageProcessor/Colors/Colorspaces/CieLab.cs b/src/ImageProcessor/Colors/Colorspaces/CieLab.cs index 978e0f0ae..567937edb 100644 --- a/src/ImageProcessor/Colors/Colorspaces/CieLab.cs +++ b/src/ImageProcessor/Colors/Colorspaces/CieLab.cs @@ -82,9 +82,9 @@ namespace ImageProcessor // First convert to CIE XYZ color = Color.InverseCompand(color.Limited); - float x = (color.R * 0.41239079926595F) + (color.G * 0.35758433938387F) + (color.B * 0.18048078840183F); - float y = (color.R * 0.21263900587151F) + (color.G * 0.71516867876775F) + (color.B * 0.072192315360733F); - float z = (color.R * 0.019330818715591F) + (color.G * 0.11919477979462F) + (color.B * 0.95053215224966F); + float x = (color.R * 0.4124F) + (color.G * 0.3576F) + (color.B * 0.1805F); + float y = (color.R * 0.2126F) + (color.G * 0.7152F) + (color.B * 0.0722F); + float z = (color.R * 0.0193F) + (color.G * 0.1192F) + (color.B * 0.9505F); x *= 100F; y *= 100F; @@ -99,9 +99,9 @@ namespace ImageProcessor y = y > 0.008856 ? (float) Math.Pow(y, 1F / 3F) : (7.787F * y) + (16F / 116F); z = z > 0.008856 ? (float) Math.Pow(z, 1F / 3F) : (7.787F * z) + (16F / 116F); - float l = (116 * y) - 16; - float a = 500 * (x - y); - float b = 200 * (y - z); + float l = (116F * y) - 16F; + float a = 500F * (x - y); + float b = 200F * (y - z); return new CieLab(l, a, b); } diff --git a/tests/ImageProcessor.Tests/Colors/ColorConversionTests.cs b/tests/ImageProcessor.Tests/Colors/ColorConversionTests.cs index 6551b17c7..e68a7e74e 100644 --- a/tests/ImageProcessor.Tests/Colors/ColorConversionTests.cs +++ b/tests/ImageProcessor.Tests/Colors/ColorConversionTests.cs @@ -334,5 +334,86 @@ namespace ImageProcessor.Tests Assert.Equal(color4, (Color)cmyk4); } } + + /// + /// Tests the implicit conversion from to . + /// Comparison values obtained from + /// http://colormine.org/convert/rgb-to-lab + /// http://au.mathworks.com/help/images/ref/rgb2lab.html + /// http://www.colorhexa.com/00ffff + /// L seems to match quite well, A and B tend to drift from the converter results a bit more + /// + [Fact] + public void ColorToCieLab() + { + // White + Color color = new Color(1, 1, 1); + CieLab cielab = color; + + Assert.Equal(100, cielab.L, 1); + Assert.Equal(0, cielab.A, 1); + Assert.Equal(0, cielab.B, 1); + + // Black + Color color2 = new Color(0, 0, 0); + CieLab cielab2 = color2; + Assert.Equal(0, cielab2.L, 1); + Assert.Equal(0, cielab2.A, 1); + Assert.Equal(0, cielab2.B, 1); + + //// Grey + Color color3 = new Color(128 / 255f, 128 / 255f, 128 / 255f); + CieLab cielab3 = color3; + Assert.Equal(53.6, cielab3.L, 1); + Assert.Equal(0, cielab3.A, 1); + Assert.Equal(0, cielab3.B, 1); + + //// Cyan + Color color4 = new Color(0, 1, 1); + CieLab cielab4 = color4; + Assert.Equal(91.1, cielab4.L, 1); + Assert.Equal(-48.1, cielab4.A, 1); + Assert.Equal(-14.1, cielab4.B, 1); + } + + /// + /// Tests the implicit conversion from to . + /// + [Fact] + public void CieLabToColor() + { + // Dark moderate pink. + CieLab cielab = new CieLab(36.5492f, 33.3173f, -12.0615f); + Color color = cielab; + + Assert.Equal(color.R, 128 / 255f, 1); + Assert.Equal(color.G, 64 / 255f, 2); + Assert.Equal(color.B, 106 / 255f, 1); + + // Ochre + CieLab cielab2 = new CieLab(58.1758f, 27.3399f, 56.8240f); + Color color2 = cielab2; + + Assert.Equal(color2.R, 204 / 255f, 1); + Assert.Equal(color2.G, 119 / 255f, 2); + Assert.Equal(color2.B, 34 / 255f, 1); + + //// White + CieLab cielab3 = new CieLab(0, 0, 0); + Color color3 = cielab3; + + Assert.Equal(color3.R, 0f, 1); + Assert.Equal(color3.G, 0f, 1); + Assert.Equal(color3.B, 0f, 1); + + //// Check others. + Random random = new Random(0); + for (int i = 0; i < 1000; i++) + { + Color color4 = new Color(random.Next(1), random.Next(1), random.Next(1)); + CieLab cielab4 = color4; + Assert.Equal(color4, (Color)cielab4); + } + } } } From b7c2a210237402eb7b030e1bd20760f105b007ed Mon Sep 17 00:00:00 2001 From: Rubens Fernandes Date: Tue, 5 Jan 2016 22:51:29 +1030 Subject: [PATCH 3/3] Now tests match ColorMine. Former-commit-id: 230fc9b5be97d89cd2192a142d6b6d3f0292db1c Former-commit-id: bbf2282ee1f10635708894b4fbc6623f77d6d62d Former-commit-id: 6b394493a6b33248d6cd63ee55fa8b1200f85b3f --- src/ImageProcessor/Colors/ColorTransforms.cs | 9 +++- .../Colors/Colorspaces/CieLab.cs | 18 +++---- .../Colors/ColorConversionTests.cs | 47 +++++++++---------- 3 files changed, 37 insertions(+), 37 deletions(-) diff --git a/src/ImageProcessor/Colors/ColorTransforms.cs b/src/ImageProcessor/Colors/ColorTransforms.cs index 7643db0b1..e085f35e8 100644 --- a/src/ImageProcessor/Colors/ColorTransforms.cs +++ b/src/ImageProcessor/Colors/ColorTransforms.cs @@ -192,11 +192,16 @@ namespace ImageProcessor float y3 = y * y * y; float z3 = z * z * z; - y = (y3 > 0.008856F) ? y3 : (y - 16F / 116F) / 7.787F; x = (x3 > 0.008856F) ? x3 : (x - 16F / 116F) / 7.787F; + y = (cieLabColor.L > 0.008856F * 903.3F) ? y3 : (cieLabColor.L / 903.3F); z = (z3 > 0.008856F) ? z3 : (z - 16F / 116F) / 7.787F; - // Then XYZ to RGB + x *= 0.95047F; + //y *= 1F; + z *= 1.08883F; + + // Then XYZ to RGB (multiplication by 100 was done above already) + float r = (x * 3.2406F) + (y * -1.5372F) + (z * -0.4986F); float g = (x * -0.9689F) + (y * 1.8758F) + (z * 0.0415F); float b = (x * 0.0557F) + (y * -0.2040F) + (z * 1.0570F); diff --git a/src/ImageProcessor/Colors/Colorspaces/CieLab.cs b/src/ImageProcessor/Colors/Colorspaces/CieLab.cs index 567937edb..6098d40f8 100644 --- a/src/ImageProcessor/Colors/Colorspaces/CieLab.cs +++ b/src/ImageProcessor/Colors/Colorspaces/CieLab.cs @@ -86,20 +86,16 @@ namespace ImageProcessor float y = (color.R * 0.2126F) + (color.G * 0.7152F) + (color.B * 0.0722F); float z = (color.R * 0.0193F) + (color.G * 0.1192F) + (color.B * 0.9505F); - x *= 100F; - y *= 100F; - z *= 100F; - // Now to LAB - x /= 95.047F; - y /= 100F; - z /= 108.883F; + x /= 0.95047F; + //y /= 1F; + z /= 1.08883F; - x = x > 0.008856 ? (float) Math.Pow(x, 1F / 3F) : (7.787F * x) + (16F / 116F); - y = y > 0.008856 ? (float) Math.Pow(y, 1F / 3F) : (7.787F * y) + (16F / 116F); - z = z > 0.008856 ? (float) Math.Pow(z, 1F / 3F) : (7.787F * z) + (16F / 116F); + x = x > 0.008856F ? (float) Math.Pow(x, 1F / 3F) : (903.3F * x + 16F) / 116F; + y = y > 0.008856F ? (float) Math.Pow(y, 1F / 3F) : (903.3F * y + 16F) / 116F; + z = z > 0.008856F ? (float) Math.Pow(z, 1F / 3F) : (903.3F * z + 16F) / 116F; - float l = (116F * y) - 16F; + float l = Math.Max(0, (116F * y) - 16F); float a = 500F * (x - y); float b = 200F * (y - z); diff --git a/tests/ImageProcessor.Tests/Colors/ColorConversionTests.cs b/tests/ImageProcessor.Tests/Colors/ColorConversionTests.cs index e68a7e74e..b219cd123 100644 --- a/tests/ImageProcessor.Tests/Colors/ColorConversionTests.cs +++ b/tests/ImageProcessor.Tests/Colors/ColorConversionTests.cs @@ -339,9 +339,6 @@ namespace ImageProcessor.Tests /// Tests the implicit conversion from to . /// Comparison values obtained from /// http://colormine.org/convert/rgb-to-lab - /// http://au.mathworks.com/help/images/ref/rgb2lab.html - /// http://www.colorhexa.com/00ffff - /// L seems to match quite well, A and B tend to drift from the converter results a bit more /// [Fact] public void ColorToCieLab() @@ -350,35 +347,37 @@ namespace ImageProcessor.Tests Color color = new Color(1, 1, 1); CieLab cielab = color; - Assert.Equal(100, cielab.L, 1); - Assert.Equal(0, cielab.A, 1); - Assert.Equal(0, cielab.B, 1); + Assert.Equal(100, cielab.L, 3); + Assert.Equal(0.005, cielab.A, 3); + Assert.Equal(-0.010, cielab.B, 3); // Black Color color2 = new Color(0, 0, 0); CieLab cielab2 = color2; - Assert.Equal(0, cielab2.L, 1); - Assert.Equal(0, cielab2.A, 1); - Assert.Equal(0, cielab2.B, 1); + Assert.Equal(0, cielab2.L, 3); + Assert.Equal(0, cielab2.A, 3); + Assert.Equal(0, cielab2.B, 3); //// Grey Color color3 = new Color(128 / 255f, 128 / 255f, 128 / 255f); CieLab cielab3 = color3; - Assert.Equal(53.6, cielab3.L, 1); - Assert.Equal(0, cielab3.A, 1); - Assert.Equal(0, cielab3.B, 1); + Assert.Equal(53.585, cielab3.L, 3); + Assert.Equal(0.003, cielab3.A, 3); + Assert.Equal(-0.006, cielab3.B, 3); //// Cyan Color color4 = new Color(0, 1, 1); CieLab cielab4 = color4; - Assert.Equal(91.1, cielab4.L, 1); - Assert.Equal(-48.1, cielab4.A, 1); - Assert.Equal(-14.1, cielab4.B, 1); + Assert.Equal(91.117, cielab4.L, 3); + Assert.Equal(-48.080, cielab4.A, 3); + Assert.Equal(-14.138, cielab4.B, 3); } /// /// Tests the implicit conversion from to . /// + /// Comparison values obtained from + /// http://colormine.org/convert/rgb-to-lab [Fact] public void CieLabToColor() { @@ -386,25 +385,25 @@ namespace ImageProcessor.Tests CieLab cielab = new CieLab(36.5492f, 33.3173f, -12.0615f); Color color = cielab; - Assert.Equal(color.R, 128 / 255f, 1); - Assert.Equal(color.G, 64 / 255f, 2); - Assert.Equal(color.B, 106 / 255f, 1); + Assert.Equal(color.R, 128 / 255f, 3); + Assert.Equal(color.G, 64 / 255f, 3); + Assert.Equal(color.B, 106 / 255f, 3); // Ochre CieLab cielab2 = new CieLab(58.1758f, 27.3399f, 56.8240f); Color color2 = cielab2; - Assert.Equal(color2.R, 204 / 255f, 1); - Assert.Equal(color2.G, 119 / 255f, 2); - Assert.Equal(color2.B, 34 / 255f, 1); + Assert.Equal(color2.R, 204 / 255f, 3); + Assert.Equal(color2.G, 119 / 255f, 3); + Assert.Equal(color2.B, 34 / 255f, 3); //// White CieLab cielab3 = new CieLab(0, 0, 0); Color color3 = cielab3; - Assert.Equal(color3.R, 0f, 1); - Assert.Equal(color3.G, 0f, 1); - Assert.Equal(color3.B, 0f, 1); + Assert.Equal(color3.R, 0f, 3); + Assert.Equal(color3.G, 0f, 3); + Assert.Equal(color3.B, 0f, 3); //// Check others. Random random = new Random(0);