Skip to content

Commit

Permalink
Merge branch 'gamut-mapping' into 'main'
Browse files Browse the repository at this point in the history
Add gamut mapping

See merge request Wacton/Unicolour!34
  • Loading branch information
waacton committed Oct 31, 2023
2 parents 99be8c2 + 75ac48e commit d55329c
Show file tree
Hide file tree
Showing 19 changed files with 383 additions and 114 deletions.
26 changes: 18 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ Unicolour is a .NET library written in C# for working with colour:
- Colour mixing / colour interpolation
- Colour comparison
- Colour properties
- Colour gamut mapping

## Overview 🧭
A `Unicolour` encapsulates a single colour and its representation across different colour spaces. It supports:
- RGB
- HSB/HSV
Expand Down Expand Up @@ -65,6 +67,9 @@ Simulation of colour vision deficiency (CVD) / colour blindness is supported for
- Tritanopia (no blue perception)
- Achromatopsia (no colour perception)

If a colour is outwith the display gamut, the closest in-gamut colour can be obtained through
gamut mapping according to CSS specifications.

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 Down Expand Up @@ -102,17 +107,17 @@ Targets [.NET Standard 2.0](https://docs.microsoft.com/en-us/dotnet/standard/net
| HCT | `Unicolour.FromHct()` | `.Hct` | `.MixHct()` |

## How to use 🌈
1. Install the package from [NuGet](https://www.nuget.org/packages/Wacton.Unicolour/)
##### 1. Install the package from [NuGet](https://www.nuget.org/packages/Wacton.Unicolour/)
```
dotnet add package Wacton.Unicolour
```

2. Import the package:
##### 2. Import the package:
```c#
using Wacton.Unicolour;
```

3. Create a `Unicolour` from values:
##### 3. Create a `Unicolour` from values:
```c#
var unicolour = Unicolour.FromHex("#FF1493");
var unicolour = Unicolour.FromRgb255(255, 20, 147);
Expand All @@ -138,7 +143,7 @@ var unicolour = Unicolour.FromCam16(62.47, 42.60, -1.36);
var unicolour = Unicolour.FromHct(358.2, 100.38, 55.96);
```

4. Get representations of a colour in different colour spaces:
##### 4. Get representations of a colour in different colour spaces:
```c#
var rgb = unicolour.Rgb;
var hsb = unicolour.Hsb;
Expand All @@ -162,14 +167,14 @@ var cam16 = unicolour.Cam16;
var hct = unicolour.Hct;
```

5. Get properties of a colour
##### 5. Get properties of a colour
```c#
var hex = unicolour.Hex;
var relativeLuminance = unicolour.RelativeLuminance;
var temperature = unicolour.Temperature;
```

6. Mix colours (interpolate between them):
##### 6. Mix colours (interpolate between them):
```c#
var mixed = unicolour1.MixRgb(unicolour2, 0.5);
var mixed = unicolour1.MixHsb(unicolour2, 0.5);
Expand All @@ -193,7 +198,7 @@ var mixed = unicolour1.MixCam16(unicolour2, 0.5);
var mixed = unicolour1.MixHct(unicolour2, 0.5);
```

7. Compare colours:
##### 7. Compare colours:
```c#
var contrast = unicolour1.Contrast(unicolour2);
var difference = unicolour1.DeltaE76(unicolour2);
Expand All @@ -207,7 +212,12 @@ var difference = unicolour1.DeltaECam02(unicolour2);
var difference = unicolour1.DeltaECam16(unicolour2);
```

8. Simulate colour vision deficiency:
##### 8. Map to display gamut:
```c#
var mapped = unicolour.MapToGamut();
```

##### 9. Simulate colour vision deficiency:
```c#
var protanopia = unicolour.SimulateProtanopia();
var deuteranopia = unicolour.SimulateDeuteranopia();
Expand Down
4 changes: 2 additions & 2 deletions Unicolour.Example/Gradient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace Wacton.Unicolour.Example;

internal static class Gradient
{
const bool ConstrainUndisplayableColours = true; // if false, undisplayable colours will render as transparent
private const bool RenderOutOfGamutAsTransparent = false;

internal delegate Unicolour Mix(Unicolour start, Unicolour end, double distance);

Expand Down Expand Up @@ -61,7 +61,7 @@ private static void SetLabel(Image<Rgba32> image, string text, Unicolour colour)
private static Rgba32 AsRgba32(Unicolour unicolour)
{
var (r, g, b) = unicolour.Rgb.Byte255.ConstrainedTriplet;
var a = ConstrainUndisplayableColours || unicolour.IsDisplayable ? 255 : 0;
var a = unicolour.IsInDisplayGamut || !RenderOutOfGamutAsTransparent ? 255 : 0;
return new Rgba32((byte) r, (byte) g, (byte) b, (byte) a);
}
}
102 changes: 41 additions & 61 deletions Unicolour.Tests/ConfigureRgbTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
*/
public class ConfigureRgbTests
{
private const double Tolerance = 0.01;
private const double Tolerance = 0.001;

// https://en.wikipedia.org/wiki/Adobe_RGB_color_space
private static readonly Chromaticity AdobeChromaticityR = new(0.64, 0.33);
Expand All @@ -24,24 +24,28 @@ public class ConfigureRgbTests

private const double Gamma = 2.19921875;

private static readonly Dictionary<ColourTriplet, ColourTriplet> StandardRgbToDisplayP3Lookup = new()
private static readonly Configuration StandardRgbConfig = new(RgbConfiguration.StandardRgb, XyzConfiguration.D65);
private static readonly Configuration DisplayP3Config = new(RgbConfiguration.DisplayP3, XyzConfiguration.D65);
private static readonly Configuration Rec2020Config = new(RgbConfiguration.Rec2020, XyzConfiguration.D65);

private static readonly List<TestCaseData> StandardRgbToDisplayP3Lookup = new()
{
{ new(1.0, 0.0, 0.0), new(0.9175, 0.2003, 0.1386) },
{ new(0.0, 1.0, 0.0), new(0.4587, 0.9853, 0.2983) },
{ new(0.0, 0.0, 1.0), new(0.0000, 0.0000, 0.9596) },
{ new(1.0930, -0.5435, -0.2538), new(1.0, 0.0, 0.0) },
{ new(-2.9057, 1.0183, -1.0162), new(0.0, 1.0, 0.0) },
{ new(0.00, 0.00, 1.04), new(0.0, 0.0, 1.0) }
new TestCaseData(new ColourTriplet(1, 0, 0), new ColourTriplet(0.9175, 0.2003, 0.1386)),
new TestCaseData(new ColourTriplet(0, 1, 0), new ColourTriplet(0.4584, 0.9853, 0.2983)),
new TestCaseData(new ColourTriplet(0, 0, 1), new ColourTriplet(0.0000, 0.0000, 0.9596)),
new TestCaseData(new ColourTriplet(1.0931, -0.2267, -0.1501), new ColourTriplet(1, 0, 0)),
new TestCaseData(new ColourTriplet(-0.5116, 1.0183, -0.3107), new ColourTriplet(0, 1, 0)),
new TestCaseData(new ColourTriplet(0.0000, 0.0000, 1.0420), new ColourTriplet(0, 0, 1))
};

private static readonly Dictionary<ColourTriplet, ColourTriplet> StandardRgbToRec2020Lookup = new()
private static readonly List<TestCaseData> StandardRgbToRec2020Lookup = new()
{
{ new(1.0, 0.0, 0.0), new(0.7920, 0.2310, 0.0738) },
{ new(0.0, 1.0, 0.0), new(0.5675, 0.9593, 0.2690) },
{ new(0.0, 0.0, 1.0), new(0.1683, 0.0511, 0.9468) },
{ new(1.2482, -1.6094, -0.2346), new(1.0, 0.0, 0.0) },
{ new(-7.5910, 1.0563, -1.2998), new(0.0, 1.0, 0.0) },
{ new(-0.9409, -0.1079, 1.0505), new(0.0, 0.0, 1.0) }
new TestCaseData(new ColourTriplet(1, 0, 0), new ColourTriplet(0.7920, 0.2310, 0.0738)),
new TestCaseData(new ColourTriplet(0, 1, 0), new ColourTriplet(0.5675, 0.9593, 0.2690)),
new TestCaseData(new ColourTriplet(0, 0, 1), new ColourTriplet(0.1684, 0.0511, 0.9468)),
new TestCaseData(new ColourTriplet(1.2482, -0.3879, -0.1435), new ColourTriplet(1, 0, 0)),
new TestCaseData(new ColourTriplet(-0.7904, 1.0563, -0.3502), new ColourTriplet(0, 1, 0)),
new TestCaseData(new ColourTriplet(-0.2992, -0.0886, 1.0505), new ColourTriplet(0, 0, 1))
};

[Test]
Expand Down Expand Up @@ -189,61 +193,37 @@ public void XyzD50ToWideGamutRgbD50()
AssertUtils.AssertTriplet<Rgb>(unicolourXyz, expectedRgb, Tolerance);
AssertUtils.AssertTriplet<Rgb>(unicolourLab, expectedRgb, Tolerance);
}

[Test]
public void ConvertStandardRgbToDisplayP3()
[TestCaseSource(nameof(StandardRgbToDisplayP3Lookup))]
public void ConvertStandardRgbToDisplayP3(ColourTriplet standardRgbTriplet, ColourTriplet displayP3Triplet)
{
var standardRgbConfig = new Configuration(RgbConfiguration.StandardRgb, XyzConfiguration.D65);
var displayP3Config = new Configuration(RgbConfiguration.DisplayP3, XyzConfiguration.D65);

foreach (var (standardRgbTriplet, displayP3Triplet) in StandardRgbToDisplayP3Lookup)
{
var standardRgb = Unicolour.FromRgb(standardRgbConfig, standardRgbTriplet.Tuple);
var displayP3 = standardRgb.ConvertToConfiguration(displayP3Config);
AssertUtils.AssertTriplet<Rgb>(displayP3, displayP3Triplet, Tolerance);
}
var standardRgb = Unicolour.FromRgb(StandardRgbConfig, standardRgbTriplet.Tuple);
var displayP3 = standardRgb.ConvertToConfiguration(DisplayP3Config);
AssertUtils.AssertTriplet<Rgb>(displayP3, displayP3Triplet, Tolerance);
}

[Test]
public void ConvertDisplayP3ToStandardRgb()
[TestCaseSource(nameof(StandardRgbToDisplayP3Lookup))]
public void ConvertDisplayP3ToStandardRgb(ColourTriplet standardRgbTriplet, ColourTriplet displayP3Triplet)
{
var standardRgbConfig = new Configuration(RgbConfiguration.StandardRgb, XyzConfiguration.D65);
var displayP3Config = new Configuration(RgbConfiguration.DisplayP3, XyzConfiguration.D65);

foreach (var (standardRgbTriplet, displayP3Triplet) in StandardRgbToDisplayP3Lookup)
{
var displayP3 = Unicolour.FromRgb(displayP3Config, displayP3Triplet.Tuple);
var standardRgb = displayP3.ConvertToConfiguration(standardRgbConfig);
AssertUtils.AssertTriplet<Rgb>(standardRgb, standardRgbTriplet, Tolerance);
}
var displayP3 = Unicolour.FromRgb(DisplayP3Config, displayP3Triplet.Tuple);
var standardRgb = displayP3.ConvertToConfiguration(StandardRgbConfig);
AssertUtils.AssertTriplet<Rgb>(standardRgb, standardRgbTriplet, Tolerance);
}

[Test]
public void ConvertStandardRgbToRec2020()
[TestCaseSource(nameof(StandardRgbToRec2020Lookup))]
public void ConvertStandardRgbToRec2020(ColourTriplet standardRgbTriplet, ColourTriplet rec2020Triplet)
{
var standardRgbConfig = new Configuration(RgbConfiguration.StandardRgb, XyzConfiguration.D65);
var rec2020Config = new Configuration(RgbConfiguration.Rec2020, XyzConfiguration.D65);

foreach (var (standardRgbTriplet, rec2020Triplet) in StandardRgbToRec2020Lookup)
{
var standardRgb = Unicolour.FromRgb(standardRgbConfig, standardRgbTriplet.Tuple);
var rec2020 = standardRgb.ConvertToConfiguration(rec2020Config);
AssertUtils.AssertTriplet<Rgb>(rec2020, rec2020Triplet, Tolerance);
}
var standardRgb = Unicolour.FromRgb(StandardRgbConfig, standardRgbTriplet.Tuple);
var rec2020 = standardRgb.ConvertToConfiguration(Rec2020Config);
AssertUtils.AssertTriplet<Rgb>(rec2020, rec2020Triplet, Tolerance);
}

[Test]
public void ConvertRec2020ToStandardRgb()
[TestCaseSource(nameof(StandardRgbToRec2020Lookup))]
public void ConvertRec2020ToStandardRgb(ColourTriplet standardRgbTriplet, ColourTriplet rec2020Triplet)
{
var standardRgbConfig = new Configuration(RgbConfiguration.StandardRgb, XyzConfiguration.D65);
var rec2020Config = new Configuration(RgbConfiguration.Rec2020, XyzConfiguration.D65);

foreach (var (standardRgbTriplet, rec2020Triplet) in StandardRgbToRec2020Lookup)
{
var rec2020 = Unicolour.FromRgb(rec2020Config, rec2020Triplet.Tuple);
var standardRgb = rec2020.ConvertToConfiguration(standardRgbConfig);
AssertUtils.AssertTriplet<Rgb>(standardRgb, standardRgbTriplet, Tolerance);
}
var rec2020 = Unicolour.FromRgb(Rec2020Config, rec2020Triplet.Tuple);
var standardRgb = rec2020.ConvertToConfiguration(StandardRgbConfig);
AssertUtils.AssertTriplet<Rgb>(standardRgb, standardRgbTriplet, Tolerance);
}

[TestCase(Illuminant.A)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@

using NUnit.Framework;

public class DisplayableColourTests
public class DisplayGamutTests
{
[TestCase(0.0, 0.0, 0.0)]
[TestCase(0.5, 0.5, 0.5)]
[TestCase(1.0, 1.0, 1.0)]
[TestCase(double.Epsilon, double.Epsilon, double.Epsilon)]
public void DisplayableRgb(double r, double g, double b)
public void InRgbGamut(double r, double g, double b)
{
var unicolour = Unicolour.FromRgb(r, g, b);
Assert.That(unicolour.IsDisplayable, Is.True);
Assert.That(unicolour.IsInDisplayGamut, Is.True);
}

[TestCase(-0.00001, 0.0, 0.0)]
Expand All @@ -35,9 +35,9 @@ public void DisplayableRgb(double r, double g, double b)
[TestCase(double.NaN, 0.5, 0.5)]
[TestCase(0.5, double.NaN, 0.5)]
[TestCase(0.5, 0.5, double.NaN)]
public void UndisplayableRgb(double r, double g, double b)
public void OutRgbGamut(double r, double g, double b)
{
var unicolour = Unicolour.FromRgb(r, g, b);
Assert.That(unicolour.IsDisplayable, Is.False);
Assert.That(unicolour.IsInDisplayGamut, Is.False);
}
}
2 changes: 1 addition & 1 deletion Unicolour.Tests/EqualityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@ private static void AssertUnicoloursEqual(Unicolour unicolour1, Unicolour unicol
AssertEqual(unicolour1.Hct, unicolour2.Hct);
AssertEqual(unicolour1.Alpha, unicolour2.Alpha);
AssertEqual(unicolour1.Hex, unicolour2.Hex);
AssertEqual(unicolour1.IsDisplayable, unicolour2.IsDisplayable);
AssertEqual(unicolour1.IsInDisplayGamut, unicolour2.IsInDisplayGamut);
AssertEqual(unicolour1.RelativeLuminance, unicolour2.RelativeLuminance);
AssertEqual(unicolour1.Description, unicolour2.Description);
AssertEqual(unicolour1.Temperature, unicolour2.Temperature);
Expand Down
Loading

0 comments on commit d55329c

Please sign in to comment.