mirror of https://github.com/SixLabors/ImageSharp
7 changed files with 517 additions and 8 deletions
@ -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; |
|||
} |
|||
} |
|||
@ -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)); |
|||
} |
|||
@ -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…
Reference in new issue