Browse Source

Add Add LUV and LCHLUV

pull/2739/head
James Jackson-South 2 years ago
parent
commit
e24146b049
  1. 4
      src/ImageSharp/ColorProfiles/CieConstants.cs
  2. 191
      src/ImageSharp/ColorProfiles/CieLchuv.cs
  3. 211
      src/ImageSharp/ColorProfiles/CieLuv.cs
  4. 10
      src/ImageSharp/ColorProfiles/YCbCr.cs
  5. 8
      src/ImageSharp/Common/Helpers/Numerics.cs
  6. 12
      tests/ImageSharp.Tests/ColorProfiles/ApproximateColorProfileComparer.cs
  7. 89
      tests/ImageSharp.Tests/ColorProfiles/CieLchuvAndCieLuvConversionTests.cs

4
src/ImageSharp/ColorProfiles/CieConstants.cs

@ -12,10 +12,10 @@ internal static class CieConstants
/// <summary>
/// 216F / 24389F
/// </summary>
public const float Epsilon = 0.008856452F;
public const float Epsilon = 216f / 24389f;
/// <summary>
/// 24389F / 27F
/// </summary>
public const float Kappa = 903.2963F;
public const float Kappa = 24389f / 27f;
}

191
src/ImageSharp/ColorProfiles/CieLchuv.cs

@ -0,0 +1,191 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.ColorProfiles;
/// <summary>
/// Represents the CIE L*C*h°, cylindrical form of the CIE L*u*v* 1976 color.
/// <see href="https://en.wikipedia.org/wiki/CIELAB_color_space#Cylindrical_representation:_CIELCh_or_CIEHLC"/>
/// </summary>
public readonly struct CieLchuv : IColorProfile<CieLchuv, CieXyz>
{
private static readonly Vector3 Min = new(0, -200, 0);
private static readonly Vector3 Max = new(100, 200, 360);
/// <summary>
/// Initializes a new instance of the <see cref="CieLchuv"/> struct.
/// </summary>
/// <param name="l">The lightness dimension.</param>
/// <param name="c">The chroma, relative saturation.</param>
/// <param name="h">The hue in degrees.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public CieLchuv(float l, float c, float h)
: this(new Vector3(l, c, h))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CieLchuv"/> struct.
/// </summary>
/// <param name="vector">The vector representing the l, c, h components.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public CieLchuv(Vector3 vector)
: this()
{
vector = Vector3.Clamp(vector, Min, Max);
this.L = vector.X;
this.C = vector.Y;
this.H = vector.Z;
}
/// <summary>
/// Gets the lightness dimension.
/// <remarks>A value ranging between 0 (black), 100 (diffuse white) or higher (specular white).</remarks>
/// </summary>
public readonly float L { get; }
/// <summary>
/// Gets the a chroma component.
/// <remarks>A value ranging from 0 to 200.</remarks>
/// </summary>
public readonly float C { get; }
/// <summary>
/// Gets the h° hue component in degrees.
/// <remarks>A value ranging from 0 to 360.</remarks>
/// </summary>
public readonly float H { get; }
/// <summary>
/// Gets the reference white point of this color
/// </summary>
public readonly CieXyz WhitePoint { get; }
/// <summary>
/// Compares two <see cref="CieLchuv"/> objects for equality.
/// </summary>
/// <param name="left">The <see cref="CieLchuv"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="CieLchuv"/> on the right side of the operand.</param>
/// <returns>
/// True if the current left is equal to the <paramref name="right"/> parameter; otherwise, false.
/// </returns>
public static bool operator ==(CieLchuv left, CieLchuv right) => left.Equals(right);
/// <summary>
/// Compares two <see cref="CieLchuv"/> objects for inequality
/// </summary>
/// <param name="left">The <see cref="CieLchuv"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="CieLchuv"/> on the right side of the operand.</param>
/// <returns>
/// True if the current left is unequal to the <paramref name="right"/> parameter; otherwise, false.
/// </returns>
public static bool operator !=(CieLchuv left, CieLchuv right) => !left.Equals(right);
/// <inheritdoc/>
public static CieLchuv FromProfileConnectingSpace(ColorConversionOptions options, in CieXyz source)
{
CieLuv luv = CieLuv.FromProfileConnectingSpace(options, source);
// Conversion algorithm described here:
// https://en.wikipedia.org/wiki/CIELUV#Cylindrical_representation_.28CIELCH.29
float l = luv.L, u = luv.U, v = luv.V;
float c = MathF.Sqrt((u * u) + (v * v));
float hRadians = MathF.Atan2(v, u);
float hDegrees = GeometryUtilities.RadianToDegree(hRadians);
// Wrap the angle round at 360.
hDegrees %= 360;
// Make sure it's not negative.
while (hDegrees < 0)
{
hDegrees += 360;
}
return new CieLchuv(l, c, hDegrees);
}
/// <inheritdoc/>
public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<CieXyz> source, Span<CieLchuv> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
for (int i = 0; i < source.Length; i++)
{
CieXyz xyz = source[i];
destination[i] = FromProfileConnectingSpace(options, in xyz);
}
}
/// <inheritdoc/>
public CieXyz ToProfileConnectingSpace(ColorConversionOptions options)
{
// Conversion algorithm described here:
// https://en.wikipedia.org/wiki/CIELUV#Cylindrical_representation_.28CIELCH.29
float l = this.L, c = this.C, hDegrees = this.H;
float hRadians = GeometryUtilities.DegreeToRadian(hDegrees);
float u = c * MathF.Cos(hRadians);
float v = c * MathF.Sin(hRadians);
CieLuv luv = new(l, u, v);
return luv.ToProfileConnectingSpace(options);
}
/// <inheritdoc/>
public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<CieLchuv> source, Span<CieXyz> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
for (int i = 0; i < source.Length; i++)
{
CieLchuv lch = source[i];
destination[i] = lch.ToProfileConnectingSpace(options);
}
}
/// <inheritdoc/>
public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource()
=> ChromaticAdaptionWhitePointSource.WhitePoint;
/// <inheritdoc/>
public override int GetHashCode()
=> HashCode.Combine(this.L, this.C, this.H, this.WhitePoint);
/// <inheritdoc/>
public override string ToString()
=> FormattableString.Invariant($"CieLchuv({this.L:#0.##}, {this.C:#0.##}, {this.H:#0.##})");
/// <inheritdoc/>
public override bool Equals(object? obj)
=> obj is CieLchuv other && this.Equals(other);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(CieLchuv other)
=> this.L.Equals(other.L)
&& this.C.Equals(other.C)
&& this.H.Equals(other.H)
&& this.WhitePoint.Equals(other.WhitePoint);
/// <summary>
/// Computes the saturation of the color (chroma normalized by lightness)
/// </summary>
/// <remarks>
/// A value ranging from 0 to 100.
/// </remarks>
/// <returns>The <see cref="float"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public float Saturation()
{
float result = 100 * (this.C / this.L);
if (float.IsNaN(result))
{
return 0;
}
return result;
}
}

211
src/ImageSharp/ColorProfiles/CieLuv.cs

@ -0,0 +1,211 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.ColorProfiles;
/// <summary>
/// The CIE 1976 (L*, u*, v*) color space, commonly known by its abbreviation CIELUV, is a color space adopted by the International
/// Commission on Illumination (CIE) in 1976, as a simple-to-compute transformation of the 1931 CIE XYZ color space, but which
/// attempted perceptual uniformity
/// <see href="https://en.wikipedia.org/wiki/CIELUV"/>
/// </summary>
public readonly struct CieLuv : IColorProfile<CieLuv, CieXyz>
{
/// <summary>
/// Initializes a new instance of the <see cref="CieLuv"/> struct.
/// </summary>
/// <param name="l">The lightness dimension.</param>
/// <param name="u">The blue-yellow chromaticity coordinate of the given white point.</param>
/// <param name="v">The red-green chromaticity coordinate of the given white point.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public CieLuv(float l, float u, float v)
{
// Not clamping as documentation about this space only indicates "usual" ranges
this.L = l;
this.U = u;
this.V = v;
}
/// <summary>
/// Gets the lightness dimension
/// <remarks>A value usually ranging between 0 and 100.</remarks>
/// </summary>
public readonly float L { get; }
/// <summary>
/// Gets the blue-yellow chromaticity coordinate of the given white point.
/// <remarks>A value usually ranging between -100 and 100.</remarks>
/// </summary>
public readonly float U { get; }
/// <summary>
/// Gets the red-green chromaticity coordinate of the given white point.
/// <remarks>A value usually ranging between -100 and 100.</remarks>
/// </summary>
public readonly float V { get; }
/// <summary>
/// Gets the reference white point of this color
/// </summary>
public readonly CieXyz WhitePoint { get; }
/// <summary>
/// Compares two <see cref="CieLuv"/> objects for equality.
/// </summary>
/// <param name="left">The <see cref="CieLuv"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="CieLuv"/> on the right side of the operand.</param>
/// <returns>
/// True if the current left is equal to the <paramref name="right"/> parameter; otherwise, false.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator ==(CieLuv left, CieLuv right) => left.Equals(right);
/// <summary>
/// Compares two <see cref="CieLuv"/> objects for inequality.
/// </summary>
/// <param name="left">The <see cref="CieLuv"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="CieLuv"/> on the right side of the operand.</param>
/// <returns>
/// True if the current left is unequal to the <paramref name="right"/> parameter; otherwise, false.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(CieLuv left, CieLuv right) => !left.Equals(right);
/// <inheritdoc/>
public static CieLuv FromProfileConnectingSpace(ColorConversionOptions options, in CieXyz source)
{
// Use doubles here for accuracy.
// Conversion algorithm described here:
// http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_Luv.html
CieXyz whitePoint = options.TargetWhitePoint;
double yr = source.Y / whitePoint.Y;
double den = source.X + (15 * source.Y) + (3 * source.Z);
double up = den > 0 ? ComputeU(in source) : 0;
double vp = den > 0 ? ComputeV(in source) : 0;
double upr = ComputeU(in whitePoint);
double vpr = ComputeV(in whitePoint);
const double e = 1 / 3d;
double l = yr > CieConstants.Epsilon
? ((116 * Math.Pow(yr, e)) - 16d)
: (CieConstants.Kappa * yr);
if (double.IsNaN(l))
{
l = 0;
}
double u = 13 * l * (up - upr);
double v = 13 * l * (vp - vpr);
if (double.IsNaN(u) || u == -0d)
{
u = 0;
}
if (double.IsNaN(v) || u == -0d)
{
v = 0;
}
return new CieLuv((float)l, (float)u, (float)v);
}
/// <inheritdoc/>
public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<CieXyz> source, Span<CieLuv> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
for (int i = 0; i < source.Length; i++)
{
CieXyz xyz = source[i];
destination[i] = FromProfileConnectingSpace(options, in xyz);
}
}
/// <inheritdoc/>
public CieXyz ToProfileConnectingSpace(ColorConversionOptions options)
{
// Use doubles here for accuracy.
// Conversion algorithm described here:
// http://www.brucelindbloom.com/index.html?Eqn_Luv_to_XYZ.html
CieXyz whitePoint = options.WhitePoint;
double l = this.L, u = this.U, v = this.V;
double u0 = ComputeU(in whitePoint);
double v0 = ComputeV(in whitePoint);
double y = l > CieConstants.Kappa * CieConstants.Epsilon
? Numerics.Pow3((l + 16) / 116d)
: l / CieConstants.Kappa;
double a = ((52 * l / (u + (13 * l * u0))) - 1) / 3;
double b = -5 * y;
const double c = -1 / 3d;
double d = y * ((39 * l / (v + (13 * l * v0))) - 5);
double x = (d - b) / (a - c);
double z = (x * a) + b;
if (double.IsNaN(x))
{
x = 0;
}
if (double.IsNaN(y))
{
y = 0;
}
if (double.IsNaN(z))
{
z = 0;
}
return new CieXyz((float)x, (float)y, (float)z);
}
/// <inheritdoc/>
public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<CieLuv> source, Span<CieXyz> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
for (int i = 0; i < source.Length; i++)
{
CieLuv luv = source[i];
destination[i] = luv.ToProfileConnectingSpace(options);
}
}
/// <inheritdoc/>
public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource()
=> ChromaticAdaptionWhitePointSource.WhitePoint;
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(this.L, this.U, this.V, this.WhitePoint);
/// <inheritdoc/>
public override string ToString() => FormattableString.Invariant($"CieLuv({this.L:#0.##}, {this.U:#0.##}, {this.V:#0.##})");
/// <inheritdoc/>
public override bool Equals(object? obj) => obj is CieLuv other && this.Equals(other);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(CieLuv other)
=> this.L.Equals(other.L)
&& this.U.Equals(other.U)
&& this.V.Equals(other.V)
&& this.WhitePoint.Equals(other.WhitePoint);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double ComputeU(in CieXyz source)
=> (4 * source.X) / (source.X + (15 * source.Y) + (3 * source.Z));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double ComputeV(in CieXyz source)
=> (9 * source.Y) / (source.X + (15 * source.Y) + (3 * source.Z));
}

10
src/ImageSharp/ColorProfiles/YCbCr.cs

@ -22,7 +22,7 @@ public readonly struct YCbCr : IColorProfile<YCbCr, Rgb>
/// <param name="y">The y luminance component.</param>
/// <param name="cb">The cb chroma component.</param>
/// <param name="cr">The cr chroma component.</param>
[MethodImpl(InliningOptions.ShortMethod)]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public YCbCr(float y, float cb, float cr)
: this(new Vector3(y, cb, cr))
{
@ -32,7 +32,7 @@ public readonly struct YCbCr : IColorProfile<YCbCr, Rgb>
/// Initializes a new instance of the <see cref="YCbCr"/> struct.
/// </summary>
/// <param name="vector">The vector representing the y, cb, cr components.</param>
[MethodImpl(InliningOptions.ShortMethod)]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public YCbCr(Vector3 vector)
{
vector = Vector3.Clamp(vector, Min, Max);
@ -77,7 +77,7 @@ public readonly struct YCbCr : IColorProfile<YCbCr, Rgb>
/// <returns>
/// True if the current left is unequal to the <paramref name="right"/> parameter; otherwise, false.
/// </returns>
[MethodImpl(InliningOptions.ShortMethod)]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(YCbCr left, YCbCr right) => !left.Equals(right);
/// <inheritdoc/>
@ -140,7 +140,7 @@ public readonly struct YCbCr : IColorProfile<YCbCr, Rgb>
=> ChromaticAdaptionWhitePointSource.RgbWorkingSpace;
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int GetHashCode() => HashCode.Combine(this.Y, this.Cb, this.Cr);
/// <inheritdoc/>
@ -150,7 +150,7 @@ public readonly struct YCbCr : IColorProfile<YCbCr, Rgb>
public override bool Equals(object? obj) => obj is YCbCr other && this.Equals(other);
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(YCbCr other)
=> this.Y.Equals(other.Y)
&& this.Cb.Equals(other.Cb)

8
src/ImageSharp/Common/Helpers/Numerics.cs

@ -141,6 +141,14 @@ internal static class Numerics
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static float Pow3(float x) => x * x * x;
/// <summary>
/// Returns a specified number raised to the power of 3
/// </summary>
/// <param name="x">A double-precision floating-point number</param>
/// <returns>The number <paramref name="x" /> raised to the power of 3.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double Pow3(double x) => x * x * x;
/// <summary>
/// Implementation of 1D Gaussian G(x) function
/// </summary>

12
tests/ImageSharp.Tests/ColorProfiles/ApproximateColorProfileComparer.cs

@ -15,7 +15,9 @@ internal readonly struct ApproximateColorProfileComparer :
IEqualityComparer<Lms>,
IEqualityComparer<CieLch>,
IEqualityComparer<Rgb>,
IEqualityComparer<YCbCr>
IEqualityComparer<YCbCr>,
IEqualityComparer<CieLchuv>,
IEqualityComparer<CieLuv>
{
private readonly float epsilon;
@ -37,6 +39,10 @@ internal readonly struct ApproximateColorProfileComparer :
public bool Equals(YCbCr x, YCbCr y) => this.Equals(x.Y, y.Y) && this.Equals(x.Cb, y.Cb) && this.Equals(x.Cr, y.Cr);
public bool Equals(CieLchuv x, CieLchuv y) => this.Equals(x.L, y.L) && this.Equals(x.C, y.C) && this.Equals(x.H, y.H);
public bool Equals(CieLuv x, CieLuv y) => this.Equals(x.L, y.L) && this.Equals(x.U, y.U) && this.Equals(x.V, y.V);
public int GetHashCode([DisallowNull] CieLab obj) => obj.GetHashCode();
public int GetHashCode([DisallowNull] CieXyz obj) => obj.GetHashCode();
@ -49,6 +55,10 @@ internal readonly struct ApproximateColorProfileComparer :
public int GetHashCode([DisallowNull] YCbCr obj) => obj.GetHashCode();
public int GetHashCode([DisallowNull] CieLchuv obj) => obj.GetHashCode();
public int GetHashCode([DisallowNull] CieLuv obj) => obj.GetHashCode();
private bool Equals(float x, float y)
{
float d = x - y;

89
tests/ImageSharp.Tests/ColorProfiles/CieLchuvAndCieLuvConversionTests.cs

@ -0,0 +1,89 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.ColorProfiles;
namespace SixLabors.ImageSharp.Tests.ColorProfiles.Conversion;
/// <summary>
/// Tests <see cref="CieLuv"/>-<see cref="CieLchuv"/> conversions.
/// </summary>
/// <remarks>
/// Test data generated using:
/// <see href="http://www.brucelindbloom.com/index.html?ColorCalculator.html"/>
/// </remarks>
public class CieLchuvAndCieLuvConversionTests
{
private static readonly ApproximateColorProfileComparer Comparer = new(.0001F);
[Theory]
[InlineData(0, 0, 0, 0, 0, 0)]
[InlineData(54.2917, 106.8391, 40.8526, 54.2917, 80.8125, 69.8851)]
[InlineData(100, 0, 0, 100, 0, 0)]
[InlineData(100, 50, 180, 100, -50, 0)]
[InlineData(10, 36.0555, 56.3099, 10, 20, 30)]
[InlineData(10, 36.0555, 123.6901, 10, -20, 30)]
[InlineData(10, 36.0555, 303.6901, 10, 20, -30)]
[InlineData(10, 36.0555, 236.3099, 10, -20, -30)]
public void Convert_CieLchuv_to_CieLuv(float l, float c, float h, float l2, float u, float v)
{
// Arrange
CieLchuv input = new(l, c, h);
CieLuv expected = new(l2, u, v);
ColorConversionOptions options = new() { WhitePoint = Illuminants.D65, TargetWhitePoint = Illuminants.D65 };
ColorProfileConverter converter = new(options);
Span<CieLchuv> inputSpan = new CieLchuv[5];
inputSpan.Fill(input);
Span<CieLuv> actualSpan = new CieLuv[5];
// Act
CieLuv actual = converter.Convert<CieLchuv, CieLuv>(input);
converter.Convert<CieLchuv, CieLuv>(inputSpan, actualSpan);
// Assert
Assert.Equal(expected, actual, Comparer);
for (int i = 0; i < actualSpan.Length; i++)
{
Assert.Equal(expected, actualSpan[i], Comparer);
}
}
[Theory]
[InlineData(0, 0, 0, 0, 0, 0)]
[InlineData(54.2917, 80.8125, 69.8851, 54.2917, 106.8391, 40.8526)]
[InlineData(100, 0, 0, 100, 0, 0)]
[InlineData(100, -50, 0, 100, 50, 180)]
[InlineData(10, 20, 30, 10, 36.0555, 56.3099)]
[InlineData(10, -20, 30, 10, 36.0555, 123.6901)]
[InlineData(10, 20, -30, 10, 36.0555, 303.6901)]
[InlineData(10, -20, -30, 10, 36.0555, 236.3099)]
[InlineData(37.3511, 24.1720, 16.0684, 37.3511, 29.0255, 33.6141)]
public void Convert_CieLuv_to_CieLchuv(float l, float u, float v, float l2, float c, float h)
{
// Arrange
CieLuv input = new(l, u, v);
CieLchuv expected = new(l2, c, h);
ColorConversionOptions options = new() { WhitePoint = Illuminants.D65, TargetWhitePoint = Illuminants.D65 };
ColorProfileConverter converter = new(options);
Span<CieLuv> inputSpan = new CieLuv[5];
inputSpan.Fill(input);
Span<CieLchuv> actualSpan = new CieLchuv[5];
// Act
CieLchuv actual = converter.Convert<CieLuv, CieLchuv>(input);
converter.Convert<CieLuv, CieLchuv>(inputSpan, actualSpan);
// Assert
Assert.Equal(expected, actual, Comparer);
for (int i = 0; i < actualSpan.Length; i++)
{
Assert.Equal(expected, actualSpan[i], Comparer);
}
}
}
Loading…
Cancel
Save