Skip to content

Commit

Permalink
Merge branch 'hsl' into 'main'
Browse files Browse the repository at this point in the history
Add HSL support

See merge request Wacton/Unicolour!5
  • Loading branch information
waacton committed Mar 21, 2022
2 parents 8beb2eb + 593e054 commit 741e963
Show file tree
Hide file tree
Showing 49 changed files with 1,325 additions and 940 deletions.
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Unicolour is a small set of utilities for working with colour:
A `Unicolour` encapsulates a single colour and its representation across different colour spaces. It supports:
- RGB
- HSB/HSV
- HSL
- CIE XYZ
- CIE LAB

Expand Down Expand Up @@ -38,12 +39,14 @@ var unicolour = Unicolour.FromHex("#FF1493");
var unicolour = Unicolour.FromRgb255(255, 20, 147);
var unicolour = Unicolour.FromRgb(1.0, 0.078, 0.576);
var unicolour = Unicolour.FromHsb(327.6, 0.922, 1.0);
var unicolour = Unicolour.FromHsl(327.6, 1.0, 0.539);
```

3. Get representation of colour in different colour spaces:
```c#
var rgb = unicolour.Rgb;
var hsb = unicolour.Hsb;
var hsl = unicolour.Hsl;
var xyz = unicolour.Xyz;
var lab = unicolour.Lab;
```
Expand All @@ -52,6 +55,7 @@ var lab = unicolour.Lab;
```c#
var interpolated = unicolour1.InterpolateRgb(unicolour2, 0.5);
var interpolated = unicolour1.InterpolateHsb(unicolour2, 0.5);
var interpolated = unicolour1.InterpolateHsl(unicolour2, 0.5);
```

5. Compare colours:
Expand All @@ -70,12 +74,12 @@ XYZ configuration only requires the reference white point.

```c#
var config = new Configuration(
new(0.7347, 0.2653), // RGB: red chromaticity coordinates
new(0.1152, 0.8264), // RGB: green chromaticity coordinates
new(0.1566, 0.0177), // RGB: blue chromaticity coordinates
new(0.7347, 0.2653), // RGB red chromaticity coordinates
new(0.1152, 0.8264), // RGB green chromaticity coordinates
new(0.1566, 0.0177), // RGB blue chromaticity coordinates
value => Companding.InverseGamma(value, 2.2), // RGB inverse companding function
WhitePoint.From(Illuminant.D50), // RGB white point
WhitePoint.From(Illuminant.D50), // XYZ white point
value => Companding.InverseGamma(value, 2.2)); // RGB inverse companding function
WhitePoint.From(Illuminant.D50)); // XYZ white point
var unicolour = Unicolour.FromRgb(config, 255, 20, 147);
```
Expand Down
62 changes: 27 additions & 35 deletions Unicolour.Tests/ConfigurationTests.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
namespace Wacton.Unicolour.Tests;

using NUnit.Framework;
using Wacton.Unicolour.Tests.Lookups;
using Wacton.Unicolour.Tests.Utils;

public static class ConfigurationTests
{
Expand Down Expand Up @@ -43,9 +43,9 @@ public static void StandardRgbD65ToXyzD65()
var unicolourWithConfig = Unicolour.FromRgb(Configuration.Default, 0.5, 0.25, 0.75);
var expectedColour = new TestColour
{
Xyz = (0.200757, 0.119618, 0.506757),
Lab = (41.1553, 51.4108, -56.4485),
// Luv = (41.1553, 16.3709, -86.7190)
Xyz = new(0.200757, 0.119618, 0.506757),
Lab = new(41.1553, 51.4108, -56.4485),
// Luv = new(41.1553, 16.3709, -86.7190)
};

Assert.That(rgbToXyzMatrix.Data, Is.EqualTo(expectedMatrixA).Within(0.0005));
Expand All @@ -61,9 +61,9 @@ public static void StandardRgbD65ToXyzD50()
Chromaticity.StandardRgbR,
Chromaticity.StandardRgbG,
Chromaticity.StandardRgbB,
Companding.InverseStandardRgb,
WhitePoint.From(Illuminant.D65, Observer.Standard2),
WhitePoint.From(Illuminant.D50, Observer.Standard2),
Companding.InverseStandardRgb);
WhitePoint.From(Illuminant.D50, Observer.Standard2));

// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
var expectedMatrix = new[,]
Expand All @@ -76,9 +76,9 @@ public static void StandardRgbD65ToXyzD50()
var unicolour = Unicolour.FromRgb(configuration, 0.5, 0.25, 0.75);
var expectedColour = new TestColour
{
Xyz = (0.187691, 0.115771, 0.381093),
Lab = (40.5359, 46.0847, -57.1158),
// Luv = (40.5359, 18.7523, -78.2057)
Xyz = new(0.187691, 0.115771, 0.381093),
Lab = new(40.5359, 46.0847, -57.1158),
// Luv = new(40.5359, 18.7523, -78.2057)
};

Assert.That(configuration.RgbToXyzMatrix.Data, Is.EqualTo(expectedMatrix).Within(0.0000001));
Expand All @@ -92,16 +92,14 @@ public static void AdobeRgbD65ToXyzD65()
AdobeChromaticityR,
AdobeChromaticityG,
AdobeChromaticityB,
WhitePoint.From(Illuminant.D65, Observer.Standard2),
WhitePoint.From(Illuminant.D65, Observer.Standard2),
value => Companding.InverseGamma(value, 2.19921875));
value => Companding.InverseGamma(value, 2.19921875), WhitePoint.From(Illuminant.D65, Observer.Standard2), WhitePoint.From(Illuminant.D65, Observer.Standard2));

var unicolour = Unicolour.FromRgb(configuration, 0.5, 0.25, 0.75);
var expectedColour = new TestColour
{
Xyz = (0.234243, 0.134410, 0.535559),
Lab = (43.4203, 57.3600, -55.4259),
// Luv = (43.4203, 25.4480, -87.3268)
Xyz = new(0.234243, 0.134410, 0.535559),
Lab = new(43.4203, 57.3600, -55.4259),
// Luv = new(43.4203, 25.4480, -87.3268)
};

AssertColour(unicolour, expectedColour);
Expand All @@ -114,16 +112,14 @@ public static void AdobeRgbD65ToXyzD50()
AdobeChromaticityR,
AdobeChromaticityG,
AdobeChromaticityB,
WhitePoint.From(Illuminant.D65, Observer.Standard2),
WhitePoint.From(Illuminant.D50, Observer.Standard2),
value => Companding.InverseGamma(value, 2.19921875));
value => Companding.InverseGamma(value, 2.19921875), WhitePoint.From(Illuminant.D65, Observer.Standard2), WhitePoint.From(Illuminant.D50, Observer.Standard2));

var unicolour = Unicolour.FromRgb(configuration, 0.5, 0.25, 0.75);
var expectedColour = new TestColour
{
Xyz = (0.221673, 0.130920, 0.402670),
Lab = (42.9015, 52.4152, -55.9013),
// Luv = (42.9015, 29.0751, -78.5576)
Xyz = new(0.221673, 0.130920, 0.402670),
Lab = new(42.9015, 52.4152, -55.9013),
// Luv = new(42.9015, 29.0751, -78.5576)
};

AssertColour(unicolour, expectedColour);
Expand All @@ -136,16 +132,14 @@ public static void WideGamutRgbD50ToXyzD65()
WideGamutChromaticityR,
WideGamutChromaticityG,
WideGamutChromaticityB,
WhitePoint.From(Illuminant.D50, Observer.Standard2),
WhitePoint.From(Illuminant.D65, Observer.Standard2),
value => Companding.InverseGamma(value, 2.19921875));
value => Companding.InverseGamma(value, 2.19921875), WhitePoint.From(Illuminant.D50, Observer.Standard2), WhitePoint.From(Illuminant.D65, Observer.Standard2));

var unicolour = Unicolour.FromRgb(configuration, 0.5, 0.25, 0.75);
var expectedColour = new TestColour
{
Xyz = (0.251993, 0.102404, 0.550393),
Lab = (38.2704, 87.2838, -65.7493),
// Luv = (38.2704, 47.3837, -99.6819)
Xyz = new(0.251993, 0.102404, 0.550393),
Lab = new(38.2704, 87.2838, -65.7493),
// Luv = new(38.2704, 47.3837, -99.6819)
};

AssertColour(unicolour, expectedColour);
Expand All @@ -158,24 +152,22 @@ public static void WideGamutRgbD50ToXyzD50()
WideGamutChromaticityR,
WideGamutChromaticityG,
WideGamutChromaticityB,
WhitePoint.From(Illuminant.D50, Observer.Standard2),
WhitePoint.From(Illuminant.D50, Observer.Standard2),
value => Companding.InverseGamma(value, 2.19921875));
value => Companding.InverseGamma(value, 2.19921875), WhitePoint.From(Illuminant.D50, Observer.Standard2), WhitePoint.From(Illuminant.D50, Observer.Standard2));

var unicolour = Unicolour.FromRgb(configuration, 0.5, 0.25, 0.75);
var expectedColour = new TestColour
{
Xyz = (0.238795, 0.099490, 0.413181),
Lab = (37.7508, 82.3084, -66.1402),
// Luv = (37.7508, 55.1488, -91.6044)
Xyz = new(0.238795, 0.099490, 0.413181),
Lab = new(37.7508, 82.3084, -66.1402),
// Luv = new(37.7508, 55.1488, -91.6044)
};

AssertColour(unicolour, expectedColour);
}

private static void AssertColour(Unicolour unicolour, TestColour expected)
{
Assert.That(unicolour.Xyz.Tuple, Is.EqualTo(expected.Xyz).Within(0.001));
Assert.That(unicolour.Lab.Tuple, Is.EqualTo(expected.Lab).Within(0.05));
AssertUtils.AssertColourTuple(unicolour.Xyz.Tuple, expected.Xyz!, 0.001);
AssertUtils.AssertColourTuple(unicolour.Lab.Tuple, expected.Lab!, 0.05);
}
}
2 changes: 1 addition & 1 deletion Unicolour.Tests/ContrastTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

using System;
using NUnit.Framework;
using Wacton.Unicolour.Tests.Lookups;
using Wacton.Unicolour.Tests.Utils;

public static class ContrastTests
{
Expand Down
132 changes: 71 additions & 61 deletions Unicolour.Tests/ConversionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,103 +3,113 @@ namespace Wacton.Unicolour.Tests;
using System;
using System.Drawing;
using NUnit.Framework;
using Wacton.Unicolour;
using Wacton.Unicolour.Tests.Lookups;
using Wacton.Unicolour.Tests.Utils;

public class ConversionTests
{
private const double RgbTolerance = 0.00000000001;
private const double HsbTolerance = 0.00000001;
private const double HslTolerance = 0.00000001;

[Test]
public void NamedRgbMatchesNamedHsb() => AssertUtils.AssertNamedColours(AssertRgbToHsbConversion);
public void NamedColoursMatchRgbConversion() => AssertUtils.AssertNamedColours(AssertRgbConversion);

[Test]
public void RgbSameAfterUnconversion()
public void RgbSameAfterDeconversion()
{
AssertUtils.AssertNamedColours(AssertRgbUnconversion);
AssertUtils.AssertRandomRgbColours(AssertRgbUnconversion);
AssertUtils.AssertRandomRgb255Colours(AssertRgbUnconversion);
AssertUtils.AssertNamedColours(AssertRgbDeconversion);
AssertUtils.AssertRandomRgbColours(AssertRgbDeconversion);
AssertUtils.AssertRandomRgb255Colours(AssertRgb255Deconversion);
}

[Test]
public void HsbSameAfterUnconversion()
public void HsbSameAfterDeconversion()
{
AssertUtils.AssertNamedColours(AssertHsbUnconversion);
AssertUtils.AssertRandomHsbColours(AssertHsbUnconversion);
AssertUtils.AssertNamedColours(AssertHsbDeconversion);
AssertUtils.AssertRandomHsbColours(AssertHsbDeconversion);
}

private static void AssertRgbToHsbConversion(TestColour namedColour)
[Test]
public void HslSameAfterDeconversion()
{
AssertUtils.AssertNamedColours(AssertHslDeconversion);
AssertUtils.AssertRandomHslColours(AssertHslDeconversion);
}

private static void AssertRgbConversion(TestColour namedColour)
{
var systemColour = ColorTranslator.FromHtml(namedColour.Hex!);
var rgb = new Rgb(systemColour.R / 255.0, systemColour.G / 255.0, systemColour.B / 255.0, Configuration.Default);
var hsb = Conversion.RgbToHsb(rgb);
var hsl = Conversion.RgbToHsl(rgb);

var expectedRoundedHsb = namedColour.Hsb;
var expectedRoundedHsl = namedColour.Hsl;

Assert.That(Math.Round(hsb.H), Is.EqualTo(expectedRoundedHsb.Value.h));
Assert.That(Math.Round(hsb.S, 2), Is.EqualTo(expectedRoundedHsb.Value.s));
Assert.That(Math.Round(hsb.B, 2), Is.EqualTo(expectedRoundedHsb.Value.b));
}

private static void AssertRgbUnconversion(TestColour namedColour)
{
var systemColour = ColorTranslator.FromHtml(namedColour.Hex!);
var originalRgb = new Rgb(systemColour.R / 255.0, systemColour.G / 255.0, systemColour.B / 255.0, Configuration.Default);
AssertRgbUnconversion(originalRgb);
}

private static void AssertRgbUnconversion(int r, int g, int b)
{
var originalRgb = new Rgb(r / 255.0, g / 255.0, b / 255.0, Configuration.Default);
AssertRgbUnconversion(originalRgb);
}

private static void AssertRgbUnconversion(double r, double g, double b)
{
var originalRgb = new Rgb(r, g, b, Configuration.Default);
AssertRgbUnconversion(originalRgb);
Assert.That(Math.Round(hsb.H), Is.EqualTo(expectedRoundedHsb!.First), namedColour.Name!);
Assert.That(Math.Round(hsb.S, 2), Is.EqualTo(expectedRoundedHsb.Second), namedColour.Name!);
Assert.That(Math.Round(hsb.B, 2), Is.EqualTo(expectedRoundedHsb.Third), namedColour.Name!);

// within 0.02 because it seems like some of wikipedia's HSL values have questionable rounding...
Assert.That(Math.Round(hsl.H), Is.EqualTo(expectedRoundedHsl!.First), namedColour.Name!);
Assert.That(Math.Round(hsl.S, 2), Is.EqualTo(expectedRoundedHsl.Second).Within(0.02), namedColour.Name!);
Assert.That(Math.Round(hsl.L, 2), Is.EqualTo(expectedRoundedHsl.Third).Within(0.02), namedColour.Name!);
}

private static void AssertRgbUnconversion(Rgb originalRgb)

private static void AssertRgbDeconversion(TestColour namedColour) => AssertRgbDeconversion(GetRgbTupleFromHex(namedColour.Hex!));
private static void AssertRgb255Deconversion(ColourTuple tuple) => AssertRgbDeconversion(GetNormalisedRgb255Tuple(tuple));
private static void AssertRgbDeconversion(ColourTuple tuple) => AssertRgbDeconversion(new Rgb(tuple.First, tuple.Second, tuple.Third, Configuration.Default));
private static void AssertRgbDeconversion(Rgb original)
{
var convertedRgb = Conversion.HsbToRgb(Conversion.RgbToHsb(originalRgb), Configuration.Default);
var deconverted = Conversion.HsbToRgb(Conversion.RgbToHsb(original), Configuration.Default);

Assert.That(convertedRgb.R, Is.EqualTo(originalRgb.R).Within(RgbTolerance));
Assert.That(convertedRgb.G, Is.EqualTo(originalRgb.G).Within(RgbTolerance));
Assert.That(convertedRgb.B, Is.EqualTo(originalRgb.B).Within(RgbTolerance));
Assert.That(convertedRgb.Tuple, Is.EqualTo(originalRgb.Tuple).Within(RgbTolerance));
Assert.That(deconverted.R, Is.EqualTo(original.R).Within(RgbTolerance));
Assert.That(deconverted.G, Is.EqualTo(original.G).Within(RgbTolerance));
Assert.That(deconverted.B, Is.EqualTo(original.B).Within(RgbTolerance));
AssertUtils.AssertColourTuple(deconverted.Tuple, original.Tuple, RgbTolerance);

Assert.That(convertedRgb.RLinear, Is.EqualTo(originalRgb.RLinear).Within(RgbTolerance));
Assert.That(convertedRgb.GLinear, Is.EqualTo(originalRgb.GLinear).Within(RgbTolerance));
Assert.That(convertedRgb.BLinear, Is.EqualTo(originalRgb.BLinear).Within(RgbTolerance));
Assert.That(convertedRgb.TupleLinear, Is.EqualTo(originalRgb.TupleLinear).Within(RgbTolerance));
Assert.That(deconverted.RLinear, Is.EqualTo(original.RLinear).Within(RgbTolerance));
Assert.That(deconverted.GLinear, Is.EqualTo(original.GLinear).Within(RgbTolerance));
Assert.That(deconverted.BLinear, Is.EqualTo(original.BLinear).Within(RgbTolerance));
AssertUtils.AssertColourTuple(deconverted.TupleLinear, original.TupleLinear, RgbTolerance);

Assert.That(convertedRgb.R255, Is.EqualTo(originalRgb.R255));
Assert.That(convertedRgb.G255, Is.EqualTo(originalRgb.G255));
Assert.That(convertedRgb.B255, Is.EqualTo(originalRgb.B255));
Assert.That(convertedRgb.Tuple255, Is.EqualTo(originalRgb.Tuple255));
Assert.That(deconverted.R255, Is.EqualTo(original.R255));
Assert.That(deconverted.G255, Is.EqualTo(original.G255));
Assert.That(deconverted.B255, Is.EqualTo(original.B255));
AssertUtils.AssertColourTuple(deconverted.Tuple255, original.Tuple255, RgbTolerance);
}

private static void AssertHsbDeconversion(TestColour namedColour) => AssertHsbDeconversion(namedColour.Hsb!);
private static void AssertHsbDeconversion(ColourTuple tuple) => AssertHsbDeconversion(new Hsb(tuple.First, tuple.Second, tuple.Third));
private static void AssertHsbDeconversion(Hsb original)
{
var deconverted = Conversion.RgbToHsb(Conversion.HsbToRgb(original, Configuration.Default));
Assert.That(deconverted.H, Is.EqualTo(original.H).Within(HsbTolerance));
Assert.That(deconverted.S, Is.EqualTo(original.S).Within(HsbTolerance));
Assert.That(deconverted.B, Is.EqualTo(original.B).Within(HsbTolerance));
AssertUtils.AssertColourTuple(deconverted.Tuple, original.Tuple, HsbTolerance, true);
}

private static void AssertHsbUnconversion(TestColour namedColour)
private static void AssertHslDeconversion(TestColour namedColour) => AssertHslDeconversion(namedColour.Hsl!);
private static void AssertHslDeconversion(ColourTuple tuple) => AssertHslDeconversion(new Hsl(tuple.First, tuple.Second, tuple.Third));
private static void AssertHslDeconversion(Hsl original)
{
var (h, s, b) = namedColour.Hsb.Value;
var originalHsb = new Hsb(h, s, b);
AssertHsbUnconversion(originalHsb);
var deconverted = Conversion.RgbToHsl(Conversion.HsbToRgb(Conversion.HslToHsb(original), Configuration.Default));
Assert.That(deconverted.H, Is.EqualTo(original.H).Within(HslTolerance));
Assert.That(deconverted.S, Is.EqualTo(original.S).Within(HslTolerance));
Assert.That(deconverted.L, Is.EqualTo(original.L).Within(HslTolerance));
AssertUtils.AssertColourTuple(deconverted.Tuple, original.Tuple, HslTolerance, true);
}

private static void AssertHsbUnconversion(double h, double s, double b)
private static ColourTuple GetRgbTupleFromHex(string hex)
{
var originalHsb = new Hsb(h, s, b);
AssertHsbUnconversion(originalHsb);
var (r255, g255, b255, _) = Wacton.Unicolour.Utils.ParseColourHex(hex);
return new(r255 / 255.0, g255 / 255.0, b255 / 255.0);
}

private static void AssertHsbUnconversion(Hsb originalHsb)
private static ColourTuple GetNormalisedRgb255Tuple(ColourTuple tuple)
{
var convertedHsb = Conversion.RgbToHsb(Conversion.HsbToRgb(originalHsb, Configuration.Default));
Assert.That(convertedHsb.H, Is.EqualTo(originalHsb.H).Within(HsbTolerance));
Assert.That(convertedHsb.S, Is.EqualTo(originalHsb.S).Within(HsbTolerance));
Assert.That(convertedHsb.B, Is.EqualTo(originalHsb.B).Within(HsbTolerance));
Assert.That(convertedHsb.Tuple, Is.EqualTo(originalHsb.Tuple).Within(HsbTolerance));
var (r255, g255, b255) = tuple;
return new(r255 / 255.0, g255 / 255.0, b255 / 255.0);
}
}
2 changes: 1 addition & 1 deletion Unicolour.Tests/DifferenceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

using System;
using NUnit.Framework;
using Wacton.Unicolour.Tests.Lookups;
using Wacton.Unicolour.Tests.Utils;

public static class DifferenceTests
{
Expand Down
Loading

0 comments on commit 741e963

Please sign in to comment.