Skip to content

Commit

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

See merge request Wacton/Unicolour!8
  • Loading branch information
waacton committed Apr 5, 2022
2 parents 63e1ab4 + 0e32db0 commit fe2b388
Show file tree
Hide file tree
Showing 38 changed files with 794 additions and 307 deletions.
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ A `Unicolour` encapsulates a single colour and its representation across differe
- HSL
- CIE XYZ
- CIE LAB
- Oklab

Unicolour uses sRGB as the default RGB model and standard illuminant D65 (2° observer) as the default white point of the XYZ colour space.
These [can be overridden](#advanced-configuration-) using the `Configuration` parameter.
Expand All @@ -22,8 +23,6 @@ This library was initially written for personal projects since existing librarie
The goal of this library is to be intuitive and easy to use; performance is not a priority.
It is also [extensively tested](Unicolour.Tests) against known colour values and other .NET libraries.

More functionality will be added over time.

Targets .NET Standard 2.0 for use in .NET 5.0+, .NET Core 2.0+ and .NET Framework 4.6.1+ applications.

## How to use 🎨
Expand All @@ -43,6 +42,7 @@ var unicolour = Unicolour.FromHsb(327.6, 0.922, 1.0);
var unicolour = Unicolour.FromHsl(327.6, 1.0, 0.539);
var unicolour = Unicolour.FromXyz(0.47, 0.24, 0.3);
var unicolour = Unicolour.FromLab(55.96, +84.54, -5.7);
var unicolour = Unicolour.FromOklab(0.65, 0.26, -0.01);
```

3. Get representation of colour in different colour spaces:
Expand All @@ -52,6 +52,7 @@ var hsb = unicolour.Hsb;
var hsl = unicolour.Hsl;
var xyz = unicolour.Xyz;
var lab = unicolour.Lab;
var oklab = unicolour.Oklab;
```

4. Interpolate between colours:
Expand All @@ -61,6 +62,7 @@ var interpolated = unicolour1.InterpolateHsb(unicolour2, 0.5);
var interpolated = unicolour1.InterpolateHsl(unicolour2, 0.5);
var interpolated = unicolour1.InterpolateXyz(unicolour2, 0.5);
var interpolated = unicolour1.InterpolateLab(unicolour2, 0.5);
var interpolated = unicolour1.InterpolateOklab(unicolour2, 0.5);
```

5. Compare colours:
Expand All @@ -69,7 +71,8 @@ var contrast = unicolour1.Contrast(unicolour2);
var difference = unicolour1.DeltaE76(unicolour2);
```

See also the [example code](Unicolour.Example/Program.cs), which uses `Unicolour` to generate gradients through different colour spaces.
See also the [example code](Unicolour.Example/Program.cs), which uses `Unicolour` to generate gradients through different colour spaces:
![Gradients generate from Unicolour](Unicolour.Example/gradients.png)

## Advanced configuration 💡
A `Configuration` parameter can be used to change the RGB model (e.g. Adobe RGB, wide-gamut RGB)
Expand Down
73 changes: 43 additions & 30 deletions Unicolour.Example/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,53 +5,66 @@
using SixLabors.ImageSharp.Processing;
using Wacton.Unicolour;

var startColour = Unicolour.FromHsb(260, 1.0, 0.33);
var endColour = Unicolour.FromHsb(30, 0.66, 1.0);
var backgroundRgba32 = AsRgba32(Unicolour.FromHex("#404046"));
var textRgba32 = AsRgba32(Unicolour.FromHex("#E8E8FF"));
const int gradientWidth = 800;
const int gradientHeight = 100;

FontCollection collection = new();
var fontFamily = collection.Add("Inconsolata-Regular.ttf");
var font = fontFamily.CreateFont(32);
var font = fontFamily.CreateFont(24);
var textRgba32 = AsRgba32(Unicolour.FromHex("#E8E8FF"));

var gradientWidth = 1600;
var gradientHeight = 200;
var labels = new List<string> {"RGB", "HSB", "HSL", "XYZ", "LAB", "OKLAB"};
var purple = Unicolour.FromHsb(260, 1.0, 0.33);
var orange = Unicolour.FromHsb(30, 0.66, 1.0);
var black = Unicolour.FromRgb(0, 0, 0);
var cyan = Unicolour.FromRgb255(0, 255, 255);

var image = new Image<Rgba32>(gradientWidth, gradientHeight * 5);
image.Mutate(x => x.BackgroundColor(backgroundRgba32));
var image = new Image<Rgba32>(gradientWidth * 2, gradientHeight * labels.Count);
Draw(purple, orange, 0);
Draw(black, cyan, 1);

for (var x = 0; x < gradientWidth; x++)
void Draw(Unicolour start, Unicolour end, int column)
{
var distance = x / (double)(gradientWidth - 1);
var viaRgb = startColour.InterpolateRgb(endColour, distance);
var viaHsb = startColour.InterpolateHsb(endColour, distance);
var viaHsl = startColour.InterpolateHsl(endColour, distance);
var viaXyz = startColour.InterpolateXyz(endColour, distance);
var viaLab = startColour.InterpolateLab(endColour, distance);
SetPixels(x, viaRgb, viaHsb, viaHsl, viaXyz, viaLab);
for (var pixelIndex = 0; pixelIndex < gradientWidth; pixelIndex++)
{
var distance = pixelIndex / (double)(gradientWidth - 1);
var unicolours = new List<Unicolour>
{
start.InterpolateRgb(end, distance),
start.InterpolateHsb(end, distance),
start.InterpolateHsl(end, distance),
start.InterpolateXyz(end, distance),
start.InterpolateLab(end, distance),
start.InterpolateOklab(end, distance)
};

SetPixels(column, pixelIndex, unicolours);
}

for (var i = 0; i < labels.Count; i++)
{
var label = labels[i];
var textLocation = TextLocation(column, i);
image.Mutate(context => context.DrawText(label, font, textRgba32, textLocation));
}
}

image.Save("gradients.png");

void SetPixels(int x, Unicolour viaRgb, Unicolour viaHsb, Unicolour viaHsl, Unicolour viaXyz, Unicolour viaLab)
void SetPixels(int column, int pixelIndex, List<Unicolour> unicolours)
{
for (var y = 0; y < gradientHeight; y++)
{
image[x, y] = AsRgba32(viaRgb);
image[x, y + 200] = AsRgba32(viaHsb);
image[x, y + 400] = AsRgba32(viaHsl);
image[x, y + 600] = AsRgba32(viaXyz);
image[x, y + 800] = AsRgba32(viaLab);
for (var i = 0; i < unicolours.Count; i++)
{
var x = gradientWidth * column + pixelIndex;
image[x, y + gradientHeight * i] = AsRgba32(unicolours[i]);
}
}

PointF TextLocation(float targetY) => new(16, targetY + 16);
image.Mutate(context => context.DrawText("RGB", font, textRgba32, TextLocation(0)));
image.Mutate(context => context.DrawText("HSB", font, textRgba32, TextLocation(200)));
image.Mutate(context => context.DrawText("HSL", font, textRgba32, TextLocation(400)));
image.Mutate(context => context.DrawText("XYZ", font, textRgba32, TextLocation(600)));
image.Mutate(context => context.DrawText("LAB", font, textRgba32, TextLocation(800)));
}

PointF TextLocation(float column, float row) => new(gradientWidth * column + 16, gradientHeight * row + 16);

Rgba32 AsRgba32(Unicolour unicolour)
{
var rgb = unicolour.Rgb;
Expand Down
Binary file added Unicolour.Example/gradients.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 9 additions & 9 deletions Unicolour.Tests/ConfigurationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ public static class ConfigurationTests
[Test]
public static void StandardRgbD65ToXyzD65()
{
var rgbToXyzMatrix = Configuration.Default.RgbToXyzMatrix;

// https://en.wikipedia.org/wiki/SRGB#From_sRGB_to_CIE_XYZ
var expectedMatrixA = new[,]
{
Expand All @@ -39,6 +37,7 @@ public static void StandardRgbD65ToXyzD65()
{0.0193339, 0.1191920, 0.9503041}
};

var rgbToXyzMatrix = Matrices.RgbToXyzMatrix(Configuration.Default);
var unicolourNoConfig = Unicolour.FromRgb(0.5, 0.25, 0.75);
var unicolourWithConfig = Unicolour.FromRgb(Configuration.Default, 0.5, 0.25, 0.75);
var expectedColour = new TestColour
Expand All @@ -57,8 +56,6 @@ public static void StandardRgbD65ToXyzD65()
[Test]
public static void XyzD65ToStandardRgbD65()
{
var xyzToRgbMatrix = Configuration.Default.XyzToRgbMatrix;

// https://en.wikipedia.org/wiki/SRGB#From_CIE_XYZ_to_sRGB
var expectedMatrixA = new[,]
{
Expand All @@ -75,6 +72,7 @@ public static void XyzD65ToStandardRgbD65()
{0.0556434, -0.2040259, 1.0572252}
};

var xyzToRgbMatrix = Matrices.RgbToXyzMatrix(Configuration.Default).Inverse();
var unicolourXyzNoConfig = Unicolour.FromXyz(0.200757, 0.119618, 0.506757);
var unicolourXyzWithConfig = Unicolour.FromXyz(Configuration.Default, 0.200757, 0.119618, 0.506757);
var unicolourLabNoConfig = Unicolour.FromLab(41.1553, 51.4108, -56.4485);
Expand Down Expand Up @@ -109,6 +107,7 @@ public static void StandardRgbD65ToXyzD50()
{0.0139322, 0.0971045, 0.7141733}
};

var rgbToXyzMatrix = Matrices.RgbToXyzMatrix(configuration);
var unicolour = Unicolour.FromRgb(configuration, 0.5, 0.25, 0.75);
var expectedColour = new TestColour
{
Expand All @@ -117,7 +116,7 @@ public static void StandardRgbD65ToXyzD50()
// Luv = new(40.5359, 18.7523, -78.2057)
};

Assert.That(configuration.RgbToXyzMatrix.Data, Is.EqualTo(expectedMatrix).Within(0.0000001));
Assert.That(rgbToXyzMatrix.Data, Is.EqualTo(expectedMatrix).Within(0.0000001));
AssertColour(unicolour, expectedColour);
}

Expand All @@ -141,11 +140,12 @@ public static void XyzD50ToStandardRgbD65()
{ 0.0719453, -0.2289914, 1.4052427}
};

var xyzToRgbMatrix = Matrices.RgbToXyzMatrix(configuration).Inverse();
var unicolourXyz = Unicolour.FromXyz(configuration, 0.187691, 0.115771, 0.381093);
var unicolourLab = Unicolour.FromLab(configuration, 40.5359, 46.0847, -57.1158);
var expectedColour = new TestColour { Rgb = new(0.5, 0.25, 0.75) };

Assert.That(configuration.XyzToRgbMatrix.Data, Is.EqualTo(expectedMatrix).Within(0.0000001));
Assert.That(xyzToRgbMatrix.Data, Is.EqualTo(expectedMatrix).Within(0.0000001));
AssertColour(unicolourXyz, expectedColour);
AssertColour(unicolourLab, expectedColour);
}
Expand Down Expand Up @@ -324,8 +324,8 @@ public static void XyzD50ToWideGamutRgbD50()

private static void AssertColour(Unicolour unicolour, TestColour expected)
{
if (expected.Rgb != null) AssertUtils.AssertColourTuple(unicolour.Rgb.Tuple, expected.Rgb!, 0.01);
if (expected.Xyz != null) AssertUtils.AssertColourTuple(unicolour.Xyz.Tuple, expected.Xyz!, 0.001);
if (expected.Lab != null) AssertUtils.AssertColourTuple(unicolour.Lab.Tuple, expected.Lab!, 0.05);
if (expected.Rgb != null) AssertUtils.AssertColourTriplet(unicolour.Rgb.Triplet, expected.Rgb!, 0.01);
if (expected.Xyz != null) AssertUtils.AssertColourTriplet(unicolour.Xyz.Triplet, expected.Xyz!, 0.001);
if (expected.Lab != null) AssertUtils.AssertColourTriplet(unicolour.Lab.Triplet, expected.Lab!, 0.05);
}
}
53 changes: 32 additions & 21 deletions Unicolour.Tests/ConversionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public class ConversionTests
private const double HslTolerance = 0.00000001;
private const double XyzTolerance = 0.00000001;
private const double LabTolerance = 0.00000001;
private const double OklabTolerance = 0.000001;

[Test]
// no point doing this test starting with Wikipedia's HSB / HSL values since they're rounded
Expand Down Expand Up @@ -44,6 +45,9 @@ public void HslSameAfterDeconversion()

[Test]
public void LabSameAfterDeconversion() => AssertUtils.AssertRandomLabColours(AssertLabDeconversion);

[Test]
public void OklabSameAfterDeconversion() => AssertUtils.AssertRandomOklabColours(AssertOklabDeconversion);

private static void AssertRgbConversion(TestColour namedColour)
{
Expand All @@ -65,66 +69,73 @@ private static void AssertRgbConversion(TestColour namedColour)
Assert.That(Math.Round(hsl.L, 2), Is.EqualTo(expectedRoundedHsl.Third).Within(0.02), namedColour.Name!);
}

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(TestColour namedColour) => AssertRgbDeconversion(GetRgbTripletFromHex(namedColour.Hex!));
private static void AssertRgb255Deconversion(ColourTriplet triplet) => AssertRgbDeconversion(GetNormalisedRgb255Triplet(triplet));
private static void AssertRgbDeconversion(ColourTriplet triplet) => AssertRgbDeconversion(new Rgb(triplet.First, triplet.Second, triplet.Third, Configuration.Default));
private static void AssertRgbDeconversion(Rgb original)
{
var deconvertedViaHsb = Conversion.HsbToRgb(Conversion.RgbToHsb(original), Configuration.Default);
AssertUtils.AssertColourTuple(deconvertedViaHsb.Tuple, original.Tuple, RgbTolerance);
AssertUtils.AssertColourTuple(deconvertedViaHsb.TupleLinear, original.TupleLinear, RgbTolerance);
AssertUtils.AssertColourTuple(deconvertedViaHsb.Tuple255, original.Tuple255, RgbTolerance);
AssertUtils.AssertColourTriplet(deconvertedViaHsb.Triplet, original.Triplet, RgbTolerance);
AssertUtils.AssertColourTriplet(deconvertedViaHsb.TripletLinear, original.TripletLinear, RgbTolerance);
AssertUtils.AssertColourTriplet(deconvertedViaHsb.Triplet255, original.Triplet255, RgbTolerance);

var deconvertedViaXyz = Conversion.XyzToRgb(Conversion.RgbToXyz(original, Configuration.Default), Configuration.Default);
AssertUtils.AssertColourTuple(deconvertedViaXyz.Tuple, original.Tuple, RgbTolerance);
AssertUtils.AssertColourTuple(deconvertedViaXyz.TupleLinear, original.TupleLinear, RgbTolerance);
AssertUtils.AssertColourTuple(deconvertedViaXyz.Tuple255, original.Tuple255, RgbTolerance);
AssertUtils.AssertColourTriplet(deconvertedViaXyz.Triplet, original.Triplet, RgbTolerance);
AssertUtils.AssertColourTriplet(deconvertedViaXyz.TripletLinear, original.TripletLinear, RgbTolerance);
AssertUtils.AssertColourTriplet(deconvertedViaXyz.Triplet255, original.Triplet255, 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(ColourTriplet triplet) => AssertHsbDeconversion(new Hsb(triplet.First, triplet.Second, triplet.Third));
private static void AssertHsbDeconversion(Hsb original)
{
var deconvertedViaRgb = Conversion.RgbToHsb(Conversion.HsbToRgb(original, Configuration.Default));
AssertUtils.AssertColourTuple(deconvertedViaRgb.Tuple, original.Tuple, HsbTolerance, true);
AssertUtils.AssertColourTriplet(deconvertedViaRgb.Triplet, original.Triplet, HsbTolerance, true);

var deconvertedViaHsl = Conversion.HslToHsb(Conversion.HsbToHsl(original));
AssertUtils.AssertColourTuple(deconvertedViaHsl.Tuple, original.Tuple, HsbTolerance, true);
AssertUtils.AssertColourTriplet(deconvertedViaHsl.Triplet, original.Triplet, HsbTolerance, true);
}

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(ColourTriplet triplet) => AssertHslDeconversion(new Hsl(triplet.First, triplet.Second, triplet.Third));
private static void AssertHslDeconversion(Hsl original)
{
var deconverted = Conversion.RgbToHsl(Conversion.HsbToRgb(Conversion.HslToHsb(original), Configuration.Default));
AssertUtils.AssertColourTuple(deconverted.Tuple, original.Tuple, HslTolerance, true);
AssertUtils.AssertColourTriplet(deconverted.Triplet, original.Triplet, HslTolerance, true);
}

private static void AssertXyzDeconversion(ColourTuple tuple) => AssertXyzDeconversion(new Xyz(tuple.First, tuple.Second, tuple.Third));
private static void AssertXyzDeconversion(ColourTriplet triplet) => AssertXyzDeconversion(new Xyz(triplet.First, triplet.Second, triplet.Third));
private static void AssertXyzDeconversion(Xyz original)
{
// note: cannot test deconversion via RGB space as XYZ <-> RGB is not 1:1
var deconverted = Conversion.LabToXyz(Conversion.XyzToLab(original, Configuration.Default), Configuration.Default);
AssertUtils.AssertColourTuple(deconverted.Tuple, original.Tuple, XyzTolerance);
AssertUtils.AssertColourTriplet(deconverted.Triplet, original.Triplet, XyzTolerance);
}

private static void AssertLabDeconversion(ColourTuple tuple) => AssertLabDeconversion(new Lab(tuple.First, tuple.Second, tuple.Third));
private static void AssertLabDeconversion(ColourTriplet triplet) => AssertLabDeconversion(new Lab(triplet.First, triplet.Second, triplet.Third));
private static void AssertLabDeconversion(Lab original)
{
// note: cannot test deconversion via RGB space as XYZ <-> RGB is not 1:1
var deconverted = Conversion.XyzToLab(Conversion.LabToXyz(original, Configuration.Default), Configuration.Default);
AssertUtils.AssertColourTuple(deconverted.Tuple, original.Tuple, LabTolerance);
AssertUtils.AssertColourTriplet(deconverted.Triplet, original.Triplet, LabTolerance);
}

private static void AssertOklabDeconversion(ColourTriplet triplet) => AssertOklabDeconversion(new Oklab(triplet.First, triplet.Second, triplet.Third));
private static void AssertOklabDeconversion(Oklab original)
{
var deconverted = Conversion.XyzToOklab(Conversion.OklabToXyz(original, Configuration.Default), Configuration.Default);
AssertUtils.AssertColourTriplet(deconverted.Triplet, original.Triplet, OklabTolerance);
}

private static ColourTuple GetRgbTupleFromHex(string hex)
private static ColourTriplet GetRgbTripletFromHex(string hex)
{
var (r255, g255, b255, _) = Wacton.Unicolour.Utils.ParseColourHex(hex);
return new(r255 / 255.0, g255 / 255.0, b255 / 255.0);
}

private static ColourTuple GetNormalisedRgb255Tuple(ColourTuple tuple)
private static ColourTriplet GetNormalisedRgb255Triplet(ColourTriplet triplet)
{
var (r255, g255, b255) = tuple;
var (r255, g255, b255) = triplet;
return new(r255 / 255.0, g255 / 255.0, b255 / 255.0);
}
}
Loading

0 comments on commit fe2b388

Please sign in to comment.