Skip to content

Commit

Permalink
Merge branch 'linear-rgb' into 'main'
Browse files Browse the repository at this point in the history
Expose Linear RGB colour space

See merge request Wacton/Unicolour!36
  • Loading branch information
waacton committed Nov 1, 2023
2 parents ab0c19d + 4e60473 commit 43b4397
Show file tree
Hide file tree
Showing 41 changed files with 1,062 additions and 446 deletions.
67 changes: 36 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Unicolour is a .NET library written in C# for working with colour:
## Overview 🧭
A `Unicolour` encapsulates a single colour and its representation across different colour spaces. It supports:
- RGB
- Linear RGB
- HSB/HSV
- HSL
- HWB
Expand Down Expand Up @@ -81,30 +82,31 @@ It is also [extensively tested](Unicolour.Tests), including verification of roun
Targets [.NET Standard 2.0](https://docs.microsoft.com/en-us/dotnet/standard/net-standard?tabs=net-standard-2-0) for use in .NET 5.0+, .NET Core 2.0+ and .NET Framework 4.6.1+ applications.

## Quickstart ⚡
| Colour space | Construction | Access | Interpolation |
|-----------------------------------------|--------------------------|----------------|----------------|
| RGB (Hex) | `Unicolour.FromHex()` | `.Hex` | `.MixRgb()` |
| RGB (0-255) | `Unicolour.FromRgb255()` | `.Rgb.Byte255` | `.MixRgb()` |
| RGB | `Unicolour.FromRgb()` | `.Rgb` | `.MixRgb()` |
| HSB/HSV | `Unicolour.FromHsb()` | `.Hsb` | `.MixHsb()` |
| HSL | `Unicolour.FromHsl()` | `.Hsl` | `.MixHsl()` |
| HWB | `Unicolour.FromHwb()` | `.Hwb` | `.MixHwb()` |
| CIEXYZ | `Unicolour.FromXyz()` | `.Xyz` | `.MixXyz()` |
| CIExyY | `Unicolour.FromXyy()` | `.Xyy` | `.MixXyy()` |
| CIELAB | `Unicolour.FromLab()` | `.Lab` | `.MixLab()` |
| CIELCh<sub>ab</sub> | `Unicolour.FromLchab()` | `.Lchab` | `.MixLchab()` |
| CIELUV | `Unicolour.FromLuv()` | `.Luv` | `.MixLuv()` |
| CIELCh<sub>uv</sub> | `Unicolour.FromLchuv()` | `.Lchuv` | `.MixLchuv()` |
| HSLuv | `Unicolour.FromHsluv()` | `.Hsluv` | `.MixHsluv()` |
| HPLuv | `Unicolour.FromHpluv()` | `.Hpluv` | `.MixHpluv()` |
| IC<sub>T</sub>C<sub>P</sub> | `Unicolour.FromIctcp()` | `.Ictcp` | `.MixIctcp()` |
| J<sub>z</sub>a<sub>z</sub>b<sub>z</sub> | `Unicolour.FromJzazbz()` | `.Jzazbz` | `.MixJzazbz()` |
| J<sub>z</sub>C<sub>z</sub>h<sub>z</sub> | `Unicolour.FromJzczhz()` | `.Jzczhz` | `.MixJzczhz()` |
| Oklab | `Unicolour.FromOklab()` | `.Oklab` | `.MixOklab()` |
| Oklch | `Unicolour.FromOklch()` | `.Oklch` | `.MixOklch()` |
| CIECAM02 | `Unicolour.FromCam02()` | `.Cam02` | `.MixCam02()` |
| CAM16 | `Unicolour.FromCam16()` | `.Cam16` | `.MixCam16()` |
| HCT | `Unicolour.FromHct()` | `.Hct` | `.MixHct()` |
| Colour space | Construction | Access | Interpolation |
|-----------------------------------------|-----------------------------|----------------|-------------------|
| RGB (Hex) | `Unicolour.FromHex()` | `.Hex` | `.MixRgb()` |
| RGB (0-255) | `Unicolour.FromRgb255()` | `.Rgb.Byte255` | `.MixRgb()` |
| RGB | `Unicolour.FromRgb()` | `.Rgb` | `.MixRgb()` |
| Linear RGB | `Unicolour.FromRgbLinear()` | `.RgbLinear` | `.MixRgbLinear()` |
| HSB/HSV | `Unicolour.FromHsb()` | `.Hsb` | `.MixHsb()` |
| HSL | `Unicolour.FromHsl()` | `.Hsl` | `.MixHsl()` |
| HWB | `Unicolour.FromHwb()` | `.Hwb` | `.MixHwb()` |
| CIEXYZ | `Unicolour.FromXyz()` | `.Xyz` | `.MixXyz()` |
| CIExyY | `Unicolour.FromXyy()` | `.Xyy` | `.MixXyy()` |
| CIELAB | `Unicolour.FromLab()` | `.Lab` | `.MixLab()` |
| CIELCh<sub>ab</sub> | `Unicolour.FromLchab()` | `.Lchab` | `.MixLchab()` |
| CIELUV | `Unicolour.FromLuv()` | `.Luv` | `.MixLuv()` |
| CIELCh<sub>uv</sub> | `Unicolour.FromLchuv()` | `.Lchuv` | `.MixLchuv()` |
| HSLuv | `Unicolour.FromHsluv()` | `.Hsluv` | `.MixHsluv()` |
| HPLuv | `Unicolour.FromHpluv()` | `.Hpluv` | `.MixHpluv()` |
| IC<sub>T</sub>C<sub>P</sub> | `Unicolour.FromIctcp()` | `.Ictcp` | `.MixIctcp()` |
| J<sub>z</sub>a<sub>z</sub>b<sub>z</sub> | `Unicolour.FromJzazbz()` | `.Jzazbz` | `.MixJzazbz()` |
| J<sub>z</sub>C<sub>z</sub>h<sub>z</sub> | `Unicolour.FromJzczhz()` | `.Jzczhz` | `.MixJzczhz()` |
| Oklab | `Unicolour.FromOklab()` | `.Oklab` | `.MixOklab()` |
| Oklch | `Unicolour.FromOklch()` | `.Oklch` | `.MixOklch()` |
| CIECAM02 | `Unicolour.FromCam02()` | `.Cam02` | `.MixCam02()` |
| CAM16 | `Unicolour.FromCam16()` | `.Cam16` | `.MixCam16()` |
| HCT | `Unicolour.FromHct()` | `.Hct` | `.MixHct()` |

## How to use 🌈
1. Install the package from [NuGet](https://www.nuget.org/packages/Wacton.Unicolour/)
Expand All @@ -121,18 +123,19 @@ using Wacton.Unicolour;
```c#
var unicolour = Unicolour.FromHex("#FF1493");
var unicolour = Unicolour.FromRgb255(255, 20, 147);
var unicolour = Unicolour.FromRgb(1.0, 0.08, 0.58);
var unicolour = Unicolour.FromHsb(327.6, 0.922, 1.0);
var unicolour = Unicolour.FromHsl(327.6, 1.0, 0.539);
var unicolour = Unicolour.FromHwb(327.6, 0.078, 0.0);
var unicolour = Unicolour.FromRgb(1.00, 0.08, 0.58);
var unicolour = Unicolour.FromRgbLinear(1.00, 0.01, 0.29);
var unicolour = Unicolour.FromHsb(327.6, 0.922, 1.000);
var unicolour = Unicolour.FromHsl(327.6, 1.000, 0.539);
var unicolour = Unicolour.FromHwb(327.6, 0.078, 0.000);
var unicolour = Unicolour.FromXyz(0.4676, 0.2387, 0.2974);
var unicolour = Unicolour.FromXyy(0.4658, 0.2378, 0.2387);
var unicolour = Unicolour.FromLab(55.96, 84.54, -5.7);
var unicolour = Unicolour.FromLchab(55.96, 84.73, 356.1);
var unicolour = Unicolour.FromLuv(55.96, 131.47, -24.35);
var unicolour = Unicolour.FromLchuv(55.96, 133.71, 349.5);
var unicolour = Unicolour.FromHsluv(349.5, 100, 56);
var unicolour = Unicolour.FromHpluv(349.5, 303.2, 56);
var unicolour = Unicolour.FromHsluv(349.5, 100.0, 56.0);
var unicolour = Unicolour.FromHpluv(349.5, 303.2, 56.0);
var unicolour = Unicolour.FromIctcp(0.38, 0.12, 0.19);
var unicolour = Unicolour.FromJzazbz(0.106, 0.107, 0.005);
var unicolour = Unicolour.FromJzczhz(0.106, 0.107, 2.6);
Expand All @@ -146,6 +149,7 @@ var unicolour = Unicolour.FromHct(358.2, 100.38, 55.96);
4. Get colour space representations
```c#
var rgb = unicolour.Rgb;
var rgbLinear = unicolour.RgbLinear;
var hsb = unicolour.Hsb;
var hsl = unicolour.Hsl;
var hwb = unicolour.Hwb;
Expand Down Expand Up @@ -178,6 +182,7 @@ var inGamut = unicolour.IsInDisplayGamut;
6. Mix colours (interpolate between them)
```c#
var mixed = unicolour1.MixRgb(unicolour2, 0.5);
var mixed = unicolour1.MixRgbLinear(unicolour2, 0.5);
var mixed = unicolour1.MixHsb(unicolour2, 0.5);
var mixed = unicolour1.MixHsl(unicolour2, 0.5);
var mixed = unicolour1.MixHwb(unicolour2, 0.5);
Expand Down Expand Up @@ -213,7 +218,7 @@ var difference = unicolour1.DeltaECam02(unicolour2);
var difference = unicolour1.DeltaECam16(unicolour2);
```

8. Map to display gamut
8. Map colour to display gamut
```c#
var mapped = unicolour.MapToGamut();
```
Expand Down
4 changes: 2 additions & 2 deletions Unicolour.Console/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ static Table GetTable(Unicolour unicolour)
var table = new Table
{
Border = TableBorder.Rounded,
BorderStyle = new Style(new Color((byte)rgb255.R, (byte)rgb255.G, (byte)rgb255.B)),
BorderStyle = new Style(new Color((byte)rgb255.R, (byte)rgb255.G, (byte)rgb255.B))
};

table.AddColumn(new TableColumn("Space").Width(col1Width));
Expand All @@ -53,7 +53,7 @@ static Table GetTable(Unicolour unicolour)
table.AddRow("Hex", $"{unicolour.Hex}");
table.AddRow("Rgb 255", $"{unicolour.Rgb.Byte255}");
table.AddRow("Rgb", $"{unicolour.Rgb}");
table.AddRow("Rgb Lin.", $"{unicolour.Rgb.Linear}");
table.AddRow("Rgb Lin.", $"{unicolour.RgbLinear}");
table.AddRow("Hsl", $"{unicolour.Hsl}");
table.AddRow("Hsb", $"{unicolour.Hsb}");
table.AddRow("Hwb", $"{unicolour.Hwb}");
Expand Down
42 changes: 22 additions & 20 deletions Unicolour.Example/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ void GenerateColourSpaceGradients()
{
const int columns = 3;
const int columnWidth = 800;
const int rows = 20;
const int rows = 21;
const int rowHeight = 100;

var purple = Unicolour.FromHsb(260, 1.0, 0.33);
Expand Down Expand Up @@ -37,6 +37,7 @@ void GenerateColourSpaceGradients()
Image<Rgba32> DrawColumn(Unicolour[] colourPoints)
{
var rgb = Gradient.Draw(("RGB", light), columnWidth, rowHeight, colourPoints, (start, end, distance) => start.MixRgb(end, distance));
var rgbLinear = Gradient.Draw(("RGB Linear", light), columnWidth, rowHeight, colourPoints, (start, end, distance) => start.MixRgbLinear(end, distance));
var hsb = Gradient.Draw(("HSB", light), columnWidth, rowHeight, colourPoints, (start, end, distance) => start.MixHsb(end, distance));
var hsl = Gradient.Draw(("HSL", light), columnWidth, rowHeight, colourPoints, (start, end, distance) => start.MixHsl(end, distance));
var hwb = Gradient.Draw(("HWB", light), columnWidth, rowHeight, colourPoints, (start, end, distance) => start.MixHwb(end, distance));
Expand All @@ -60,25 +61,26 @@ Image<Rgba32> DrawColumn(Unicolour[] colourPoints)
var columnImage = new Image<Rgba32>(columnWidth, rowHeight * rows);
columnImage.Mutate(context => context
.DrawImage(rgb, new Point(0, rowHeight * 0), 1f)
.DrawImage(hsb, new Point(0, rowHeight * 1), 1f)
.DrawImage(hsl, new Point(0, rowHeight * 2), 1f)
.DrawImage(hwb, new Point(0, rowHeight * 3), 1f)
.DrawImage(xyz, new Point(0, rowHeight * 4), 1f)
.DrawImage(xyy, new Point(0, rowHeight * 5), 1f)
.DrawImage(lab, new Point(0, rowHeight * 6), 1f)
.DrawImage(lchab, new Point(0, rowHeight * 7), 1f)
.DrawImage(luv, new Point(0, rowHeight * 8), 1f)
.DrawImage(lchuv, new Point(0, rowHeight * 9), 1f)
.DrawImage(hsluv, new Point(0, rowHeight * 10), 1f)
.DrawImage(hpluv, new Point(0, rowHeight * 11), 1f)
.DrawImage(ictcp, new Point(0, rowHeight * 12), 1f)
.DrawImage(jzazbz, new Point(0, rowHeight * 13), 1f)
.DrawImage(jzczhz, new Point(0, rowHeight * 14), 1f)
.DrawImage(oklab, new Point(0, rowHeight * 15), 1f)
.DrawImage(oklch, new Point(0, rowHeight * 16), 1f)
.DrawImage(cam02, new Point(0, rowHeight * 17), 1f)
.DrawImage(cam16, new Point(0, rowHeight * 18), 1f)
.DrawImage(hct, new Point(0, rowHeight * 19), 1f)
.DrawImage(rgbLinear, new Point(0, rowHeight * 1), 1f)
.DrawImage(hsb, new Point(0, rowHeight * 2), 1f)
.DrawImage(hsl, new Point(0, rowHeight * 3), 1f)
.DrawImage(hwb, new Point(0, rowHeight * 4), 1f)
.DrawImage(xyz, new Point(0, rowHeight * 5), 1f)
.DrawImage(xyy, new Point(0, rowHeight * 6), 1f)
.DrawImage(lab, new Point(0, rowHeight * 7), 1f)
.DrawImage(lchab, new Point(0, rowHeight * 8), 1f)
.DrawImage(luv, new Point(0, rowHeight * 9), 1f)
.DrawImage(lchuv, new Point(0, rowHeight * 10), 1f)
.DrawImage(hsluv, new Point(0, rowHeight * 11), 1f)
.DrawImage(hpluv, new Point(0, rowHeight * 12), 1f)
.DrawImage(ictcp, new Point(0, rowHeight * 13), 1f)
.DrawImage(jzazbz, new Point(0, rowHeight * 14), 1f)
.DrawImage(jzczhz, new Point(0, rowHeight * 15), 1f)
.DrawImage(oklab, new Point(0, rowHeight * 16), 1f)
.DrawImage(oklch, new Point(0, rowHeight * 17), 1f)
.DrawImage(cam02, new Point(0, rowHeight * 18), 1f)
.DrawImage(cam16, new Point(0, rowHeight * 19), 1f)
.DrawImage(hct, new Point(0, rowHeight * 20), 1f)
);

return columnImage;
Expand Down
Binary file modified 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.
2 changes: 1 addition & 1 deletion Unicolour.Tests/AlphaTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public class AlphaTests
[TestCase(double.NaN, 0, "00")]
public void OutRange(double value, int value255, string hex) => AssertAlpha(value, value255, hex);

private void AssertAlpha(double value, int value255, string hex)
private static void AssertAlpha(double value, int value255, string hex)
{
var alpha = new Alpha(value);
Assert.That(alpha.A, Is.EqualTo(value));
Expand Down
11 changes: 11 additions & 0 deletions Unicolour.Tests/ConfigureCamTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace Wacton.Unicolour.Tests;

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

Expand Down Expand Up @@ -48,4 +49,14 @@ public void XyzWhitePointRoundTripViaCam16(Illuminant xyzIlluminant)
var cam = Cam16.FromXyz(xyz, CamConfiguration.StandardRgb, xyzConfig);
AssertUtils.AssertTriplet(cam.Triplet, expectedCam.Triplet, 0.00000000001);
}

[Test]
public void InvalidSurround()
{
const Surround badSurround = (Surround)int.MaxValue;
var camConfig = new CamConfiguration(WhitePoint.StandardRgb, 0, 0, badSurround);
Assert.Throws<ArgumentOutOfRangeException>(() => { _ = camConfig.F; });
Assert.Throws<ArgumentOutOfRangeException>(() => { _ = camConfig.C; });
Assert.Throws<ArgumentOutOfRangeException>(() => { _ = camConfig.Nc; });
}
}
11 changes: 3 additions & 8 deletions Unicolour.Tests/ConfigureIctcpTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,8 @@ public class ConfigureIctcpTests
private static readonly WhitePoint D65WhitePoint = WhitePoint.From(Illuminant.D65);
private static readonly ColourTriplet XyzWhite = new(D65WhitePoint.X / 100.0, D65WhitePoint.Y / 100.0, D65WhitePoint.Z / 100.0);

// TODO: simplify tests if ever support Unicolour.FromLinearRgb()
// Linear Rec2020 RGB as used in https://github.com/colour-science/colour#31224ictcp-colour-encoding
private static double Gamma(double e) => Companding.Rec2020.FromLinear(e);
private static readonly ColourTriplet TestLinearRgb = new(0.45620519, 0.03081071, 0.04091952);
private static readonly ColourTriplet TestRgb = new(Gamma(TestLinearRgb.First), Gamma(TestLinearRgb.Second), Gamma(TestLinearRgb.Third));

private static readonly Configuration Config100 = new(RgbConfiguration.Rec2020, XyzConfiguration.D65, ictcpScalar: 100);
private static readonly Configuration Config1 = new(RgbConfiguration.Rec2020, XyzConfiguration.D65, ictcpScalar: 1);
Expand Down Expand Up @@ -44,8 +41,7 @@ public void Rec2020RgbToIctcp100()
[Test] // matches the behaviour of python-based "colour-science/colour" (https://github.com/colour-science/colour#31224ictcp-colour-encoding)
public void Rec2020RgbToIctcp1()
{
var unicolour = Unicolour.FromRgb(Config1, TestRgb.Tuple);
AssertUtils.AssertTriplet(unicolour.Rgb.Linear.Triplet, TestLinearRgb, 0.00000000001);
var unicolour = Unicolour.FromRgbLinear(Config1, TestLinearRgb.Tuple);
AssertUtils.AssertTriplet(unicolour.Ictcp.Triplet, new(0.07351364, 0.00475253, 0.09351596), 0.00001);

var white = Unicolour.FromXyz(Config1, XyzWhite.Tuple);
Expand All @@ -57,8 +53,7 @@ public void Rec2020RgbToIctcp1()
[Test] // matches the behaviour of javascript-based "color.js" (https://github.com/LeaVerou/color.js / https://colorjs.io/apps/picker)
public void Rec2020RgbToIctcp203()
{
var unicolour = Unicolour.FromRgb(Config203, TestRgb.Tuple);
AssertUtils.AssertTriplet(unicolour.Rgb.Linear.Triplet, TestLinearRgb, 0.00000000001);
var unicolour = Unicolour.FromRgbLinear(Config203, TestLinearRgb.Tuple);
AssertUtils.AssertTriplet(unicolour.Ictcp.Triplet, new(0.39224991, -0.0001166, 0.28389029), 0.0001);

var white = Unicolour.FromXyz(Config203, XyzWhite.Tuple);
Expand All @@ -70,7 +65,7 @@ public void Rec2020RgbToIctcp203()
[Test]
public void ConvertTestColour()
{
var initial100 = Unicolour.FromRgb(Config100, TestRgb.Tuple);
var initial100 = Unicolour.FromRgbLinear(Config100, TestLinearRgb.Tuple);
var convertedTo1 = initial100.ConvertToConfiguration(Config1);
var convertedTo203 = convertedTo1.ConvertToConfiguration(Config203);
var convertedTo100 = convertedTo203.ConvertToConfiguration(Config100);
Expand Down
16 changes: 8 additions & 8 deletions Unicolour.Tests/ConfigureRgbTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ public void XyzD65ToStandardRgbD65()
};

// testing default config values; other tests explicitly construct configs
var xyzToRgbMatrix = Rgb.RgbToXyzMatrix(RgbConfiguration.StandardRgb, XyzConfiguration.D65).Inverse();
Assert.That(xyzToRgbMatrix.Data, Is.EqualTo(expectedMatrixA).Within(0.0005));
Assert.That(xyzToRgbMatrix.Data, Is.EqualTo(expectedMatrixB).Within(0.0000001));
var xyzToRgbLinearMatrix = RgbLinear.RgbLinearToXyzMatrix(RgbConfiguration.StandardRgb, XyzConfiguration.D65).Inverse();
Assert.That(xyzToRgbLinearMatrix.Data, Is.EqualTo(expectedMatrixA).Within(0.0005));
Assert.That(xyzToRgbLinearMatrix.Data, Is.EqualTo(expectedMatrixB).Within(0.0000001));

var unicolourXyz = Unicolour.FromXyz(Configuration.Default, 0.200757, 0.119618, 0.506757);
var unicolourXyzNoConfig = Unicolour.FromXyz(0.200757, 0.119618, 0.506757);
Expand Down Expand Up @@ -104,8 +104,8 @@ public void XyzD50ToStandardRgbD65()
{ 0.0719453, -0.2289914, 1.4052427 }
};

var xyzToRgbMatrix = Rgb.RgbToXyzMatrix(config.Rgb, config.Xyz).Inverse();
Assert.That(xyzToRgbMatrix.Data, Is.EqualTo(expectedMatrix).Within(0.0000001));
var xyzToRgbLinearMatrix = RgbLinear.RgbLinearToXyzMatrix(config.Rgb, config.Xyz).Inverse();
Assert.That(xyzToRgbLinearMatrix.Data, Is.EqualTo(expectedMatrix).Within(0.0000001));

var unicolourXyz = Unicolour.FromXyz(config, 0.187691, 0.115771, 0.381093);
var unicolourLab = Unicolour.FromLab(config, 40.5359, 46.0847, -57.1158);
Expand Down Expand Up @@ -240,11 +240,11 @@ public void XyzWhitePointRoundTrip(Illuminant xyzIlluminant)
{
var initialXyzConfig = new XyzConfiguration(RgbConfiguration.StandardRgb.WhitePoint);
var initialXyz = new Xyz(0.4676, 0.2387, 0.2974);
var expectedRgb = Rgb.FromXyz(initialXyz, RgbConfiguration.StandardRgb, initialXyzConfig);
var expectedRgb = RgbLinear.FromXyz(initialXyz, RgbConfiguration.StandardRgb, initialXyzConfig);

var xyzConfig = new XyzConfiguration(WhitePoint.From(xyzIlluminant));
var xyz = Rgb.ToXyz(expectedRgb, RgbConfiguration.StandardRgb, xyzConfig);
var rgb = Rgb.FromXyz(xyz, RgbConfiguration.StandardRgb, xyzConfig);
var xyz = RgbLinear.ToXyz(expectedRgb, RgbConfiguration.StandardRgb, xyzConfig);
var rgb = RgbLinear.FromXyz(xyz, RgbConfiguration.StandardRgb, xyzConfig);
AssertUtils.AssertTriplet(rgb.Triplet, expectedRgb.Triplet, 0.00000000001);
}
}
Loading

0 comments on commit 43b4397

Please sign in to comment.