diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzRgb.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzRgb.cs new file mode 100644 index 0000000000..b0ec427c5d --- /dev/null +++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzRgb.cs @@ -0,0 +1,52 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.ColorProfiles; + +internal static class ColorProfileConverterExtensionsCieXyzRgb +{ + public static TTo Convert(this ColorProfileConverter converter, TFrom source) + where TFrom : struct, IColorProfile + where TTo : struct, IColorProfile + { + ColorConversionOptions options = converter.Options; + + // Convert to input PCS + CieXyz pcsFrom = source.ToProfileConnectingSpace(options); + + // Adapt to target white point + pcsFrom = VonKriesChromaticAdaptation.Transform(options, in pcsFrom); + + // Convert between PCS + Rgb pcsTo = Rgb.FromProfileConnectingSpace(options, in pcsFrom); + + // Convert to output from PCS + return TTo.FromProfileConnectingSpace(options, pcsTo); + } + + public static void Convert(this ColorProfileConverter converter, ReadOnlySpan source, Span destination) + where TFrom : struct, IColorProfile + where TTo : struct, IColorProfile + { + ColorConversionOptions options = converter.Options; + + // Convert to input PCS. + using IMemoryOwner pcsFromOwner = options.MemoryAllocator.Allocate(source.Length); + Span pcsFrom = pcsFromOwner.GetSpan(); + TFrom.ToProfileConnectionSpace(options, source, pcsFrom); + + // Adapt to target white point + VonKriesChromaticAdaptation.Transform(options, pcsFrom, pcsFrom); + + // Convert between PCS. + using IMemoryOwner pcsToOwner = options.MemoryAllocator.Allocate(source.Length); + Span pcsTo = pcsToOwner.GetSpan(); + Rgb.FromProfileConnectionSpace(options, pcsFrom, pcsTo); + + // Convert to output from PCS + TTo.FromProfileConnectionSpace(options, pcsTo, destination); + } +} diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieXyz.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieXyz.cs new file mode 100644 index 0000000000..f98d4bfc31 --- /dev/null +++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieXyz.cs @@ -0,0 +1,52 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.ColorProfiles; + +internal static class ColorProfileConverterExtensionsRgbCieXyz +{ + public static TTo Convert(this ColorProfileConverter converter, TFrom source) + where TFrom : struct, IColorProfile + where TTo : struct, IColorProfile + { + ColorConversionOptions options = converter.Options; + + // Convert to input PCS + Rgb pcsFrom = source.ToProfileConnectingSpace(options); + + // Convert between PCS + CieXyz pcsTo = pcsFrom.ToProfileConnectingSpace(options); + + // Adapt to target white point + pcsTo = VonKriesChromaticAdaptation.Transform(options, in pcsTo); + + // Convert to output from PCS + return TTo.FromProfileConnectingSpace(options, pcsTo); + } + + public static void Convert(this ColorProfileConverter converter, ReadOnlySpan source, Span destination) + where TFrom : struct, IColorProfile + where TTo : struct, IColorProfile + { + ColorConversionOptions options = converter.Options; + + // Convert to input PCS. + using IMemoryOwner pcsFromOwner = options.MemoryAllocator.Allocate(source.Length); + Span pcsFrom = pcsFromOwner.GetSpan(); + TFrom.ToProfileConnectionSpace(options, source, pcsFrom); + + // Convert between PCS. + using IMemoryOwner pcsToOwner = options.MemoryAllocator.Allocate(source.Length); + Span pcsTo = pcsToOwner.GetSpan(); + Rgb.ToProfileConnectionSpace(options, pcsFrom, pcsTo); + + // Adapt to target white point + VonKriesChromaticAdaptation.Transform(options, pcsTo, pcsTo); + + // Convert to output from PCS + TTo.FromProfileConnectionSpace(options, pcsTo, destination); + } +} diff --git a/src/ImageSharp/ColorProfiles/Rgb.cs b/src/ImageSharp/ColorProfiles/Rgb.cs index bccae1132b..21575dd85c 100644 --- a/src/ImageSharp/ColorProfiles/Rgb.cs +++ b/src/ImageSharp/ColorProfiles/Rgb.cs @@ -7,7 +7,10 @@ using SixLabors.ImageSharp.ColorProfiles.WorkingSpaces; namespace SixLabors.ImageSharp.ColorProfiles; -internal readonly struct Rgb : IProfileConnectingSpace +/// +/// Represents an RGB (red, green, blue) color profile. +/// +public readonly struct Rgb : IProfileConnectingSpace { /// /// Initializes a new instance of the struct. diff --git a/src/ImageSharp/ColorProfiles/YCbCr.cs b/src/ImageSharp/ColorProfiles/YCbCr.cs new file mode 100644 index 0000000000..07062b32ae --- /dev/null +++ b/src/ImageSharp/ColorProfiles/YCbCr.cs @@ -0,0 +1,158 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.ColorProfiles; + +/// +/// Represents an YCbCr (luminance, blue chroma, red chroma) color as defined in the ITU-T T.871 specification for the JFIF use with Jpeg. +/// +/// +/// +public readonly struct YCbCr : IColorProfile +{ + private static readonly Vector3 Min = Vector3.Zero; + private static readonly Vector3 Max = new(255); + + /// + /// Initializes a new instance of the struct. + /// + /// The y luminance component. + /// The cb chroma component. + /// The cr chroma component. + [MethodImpl(InliningOptions.ShortMethod)] + public YCbCr(float y, float cb, float cr) + : this(new Vector3(y, cb, cr)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The vector representing the y, cb, cr components. + [MethodImpl(InliningOptions.ShortMethod)] + public YCbCr(Vector3 vector) + { + vector = Vector3.Clamp(vector, Min, Max); + this.Y = vector.X; + this.Cb = vector.Y; + this.Cr = vector.Z; + } + + /// + /// Gets the Y luminance component. + /// A value ranging between 0 and 255. + /// + public readonly float Y { get; } + + /// + /// Gets the Cb chroma component. + /// A value ranging between 0 and 255. + /// + public readonly float Cb { get; } + + /// + /// Gets the Cr chroma component. + /// A value ranging between 0 and 255. + /// + public readonly float Cr { get; } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + public static bool operator ==(YCbCr left, YCbCr right) => left.Equals(right); + + /// + /// Compares two objects for inequality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + [MethodImpl(InliningOptions.ShortMethod)] + public static bool operator !=(YCbCr left, YCbCr right) => !left.Equals(right); + + /// + public static YCbCr FromProfileConnectingSpace(ColorConversionOptions options, in Rgb source) + { + Vector3 rgb = source.ToScaledVector3() * Max; + float r = rgb.X; + float g = rgb.Y; + float b = rgb.Z; + + float y = (0.299F * r) + (0.587F * g) + (0.114F * b); + float cb = 128F + ((-0.168736F * r) - (0.331264F * g) + (0.5F * b)); + float cr = 128F + ((0.5F * r) - (0.418688F * g) - (0.081312F * b)); + + return new YCbCr(y, cb, cr); + } + + /// + public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // TODO: We can optimize this by using SIMD + for (int i = 0; i < source.Length; i++) + { + Rgb rgb = source[i]; + destination[i] = FromProfileConnectingSpace(options, in rgb); + } + } + + /// + public Rgb ToProfileConnectingSpace(ColorConversionOptions options) + { + float y = this.Y; + float cb = this.Cb - 128F; + float cr = this.Cr - 128F; + + float r = MathF.Round(y + (1.402F * cr), MidpointRounding.AwayFromZero); + float g = MathF.Round(y - (0.344136F * cb) - (0.714136F * cr), MidpointRounding.AwayFromZero); + float b = MathF.Round(y + (1.772F * cb), MidpointRounding.AwayFromZero); + + return Rgb.FromScaledVector3(new Vector3(r, g, b) / Max); + } + + /// + public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // TODO: We can optimize this by using SIMD + for (int i = 0; i < source.Length; i++) + { + YCbCr ycbcr = source[i]; + destination[i] = ycbcr.ToProfileConnectingSpace(options); + } + } + + /// + public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource() + => ChromaticAdaptionWhitePointSource.RgbWorkingSpace; + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public override int GetHashCode() => HashCode.Combine(this.Y, this.Cb, this.Cr); + + /// + public override string ToString() => FormattableString.Invariant($"YCbCr({this.Y}, {this.Cb}, {this.Cr})"); + + /// + public override bool Equals(object? obj) => obj is YCbCr other && this.Equals(other); + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public bool Equals(YCbCr other) + => this.Y.Equals(other.Y) + && this.Cb.Equals(other.Cb) + && this.Cr.Equals(other.Cr); +} diff --git a/tests/ImageSharp.Tests/ColorProfiles/ApproximateColorProfileComparer.cs b/tests/ImageSharp.Tests/ColorProfiles/ApproximateColorProfileComparer.cs index 84319b66c1..921148318f 100644 --- a/tests/ImageSharp.Tests/ColorProfiles/ApproximateColorProfileComparer.cs +++ b/tests/ImageSharp.Tests/ColorProfiles/ApproximateColorProfileComparer.cs @@ -14,7 +14,8 @@ internal readonly struct ApproximateColorProfileComparer : IEqualityComparer, IEqualityComparer, IEqualityComparer, - IEqualityComparer + IEqualityComparer, + IEqualityComparer { private readonly float epsilon; @@ -34,6 +35,8 @@ internal readonly struct ApproximateColorProfileComparer : public bool Equals(Rgb x, Rgb y) => this.Equals(x.R, y.R) && this.Equals(x.G, y.G) && this.Equals(x.B, y.B); + 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 int GetHashCode([DisallowNull] CieLab obj) => obj.GetHashCode(); public int GetHashCode([DisallowNull] CieXyz obj) => obj.GetHashCode(); @@ -44,6 +47,8 @@ internal readonly struct ApproximateColorProfileComparer : public int GetHashCode([DisallowNull] Rgb obj) => obj.GetHashCode(); + public int GetHashCode([DisallowNull] YCbCr obj) => obj.GetHashCode(); + private bool Equals(float x, float y) { float d = x - y; diff --git a/tests/ImageSharp.Tests/ColorProfiles/RgbAndYCbCrConversionTest.cs b/tests/ImageSharp.Tests/ColorProfiles/RgbAndYCbCrConversionTest.cs new file mode 100644 index 0000000000..05c17e89eb --- /dev/null +++ b/tests/ImageSharp.Tests/ColorProfiles/RgbAndYCbCrConversionTest.cs @@ -0,0 +1,76 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.ColorProfiles; + +namespace SixLabors.ImageSharp.Tests.ColorProfiles.Conversion; + +/// +/// Tests - conversions. +/// +/// +/// Test data generated mathematically +/// +public class RgbAndYCbCrConversionTest +{ + private static readonly ApproximateColorProfileComparer Comparer = new(.001F); + + [Theory] + [InlineData(255, 128, 128, 1, 1, 1)] + [InlineData(0, 128, 128, 0, 0, 0)] + [InlineData(128, 128, 128, 0.502, 0.502, 0.502)] + public void Convert_YCbCr_To_Rgb(float y, float cb, float cr, float r, float g, float b) + { + // Arrange + YCbCr input = new(y, cb, cr); + Rgb expected = new(r, g, b); + ColorProfileConverter converter = new(); + + Span inputSpan = new YCbCr[5]; + inputSpan.Fill(input); + + Span actualSpan = new Rgb[5]; + + // Act + Rgb actual = converter.Convert(input); + converter.Convert(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, 128, 128)] + [InlineData(1, 1, 1, 255, 128, 128)] + [InlineData(0.5, 0.5, 0.5, 127.5, 128, 128)] + [InlineData(1, 0, 0, 76.245, 84.972, 255)] + public void Convert_Rgb_To_YCbCr(float r, float g, float b, float y, float cb, float cr) + { + // Arrange + Rgb input = new(r, g, b); + YCbCr expected = new(y, cb, cr); + ColorProfileConverter converter = new(); + + Span inputSpan = new Rgb[5]; + inputSpan.Fill(input); + + Span actualSpan = new YCbCr[5]; + + // Act + YCbCr actual = converter.Convert(input); + converter.Convert(inputSpan, actualSpan); + + // Assert + Assert.Equal(expected, actual, Comparer); + + for (int i = 0; i < actualSpan.Length; i++) + { + Assert.Equal(expected, actualSpan[i], Comparer); + } + } +}