diff --git a/README.md b/README.md index 4962faf5b3..09262eb572 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ If you prefer, you can compile ImageSharp yourself (please do and help!) - Using [Visual Studio 2022](https://visualstudio.microsoft.com/vs/) - Make sure you have the latest version installed - - Make sure you have [the .NET 6 SDK](https://www.microsoft.com/net/core#windows) installed + - Make sure you have [the .NET 7 SDK](https://www.microsoft.com/net/core#windows) installed Alternatively, you can work from command line and/or with a lightweight editor on **both Linux/Unix and Windows**: diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykArm64.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykArm64.cs new file mode 100644 index 0000000000..11122d3b89 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykArm64.cs @@ -0,0 +1,95 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.Arm; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components; + +internal abstract partial class JpegColorConverterBase +{ + internal sealed class CmykArm64 : JpegColorConverterArm64 + { + public CmykArm64(int precision) + : base(JpegColorSpace.Cmyk, precision) + { + } + + /// + public override void ConvertToRgbInplace(in ComponentValues values) + { + ref Vector128 c0Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector128 c1Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + ref Vector128 c2Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + ref Vector128 c3Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); + + // Used for the color conversion + var scale = Vector128.Create(1 / (this.MaximumValue * this.MaximumValue)); + + nint n = (nint)(uint)values.Component0.Length / Vector128.Count; + for (nint i = 0; i < n; i++) + { + ref Vector128 c = ref Unsafe.Add(ref c0Base, i); + ref Vector128 m = ref Unsafe.Add(ref c1Base, i); + ref Vector128 y = ref Unsafe.Add(ref c2Base, i); + Vector128 k = Unsafe.Add(ref c3Base, i); + + k = AdvSimd.Multiply(k, scale); + c = AdvSimd.Multiply(c, k); + m = AdvSimd.Multiply(m, k); + y = AdvSimd.Multiply(y, k); + } + } + + /// + public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) + => ConvertFromRgb(in values, this.MaximumValue, rLane, gLane, bLane); + + public static void ConvertFromRgb(in ComponentValues values, float maxValue, Span rLane, Span gLane, Span bLane) + { + ref Vector128 destC = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector128 destM = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + ref Vector128 destY = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + ref Vector128 destK = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); + + ref Vector128 srcR = + ref Unsafe.As>(ref MemoryMarshal.GetReference(rLane)); + ref Vector128 srcG = + ref Unsafe.As>(ref MemoryMarshal.GetReference(gLane)); + ref Vector128 srcB = + ref Unsafe.As>(ref MemoryMarshal.GetReference(bLane)); + + var scale = Vector128.Create(maxValue); + + nint n = (nint)(uint)values.Component0.Length / Vector128.Count; + for (nint i = 0; i < n; i++) + { + Vector128 ctmp = AdvSimd.Subtract(scale, Unsafe.Add(ref srcR, i)); + Vector128 mtmp = AdvSimd.Subtract(scale, Unsafe.Add(ref srcG, i)); + Vector128 ytmp = AdvSimd.Subtract(scale, Unsafe.Add(ref srcB, i)); + Vector128 ktmp = AdvSimd.Min(ctmp, AdvSimd.Min(mtmp, ytmp)); + + Vector128 kMask = AdvSimd.Not(AdvSimd.CompareEqual(ktmp, scale)); + + ctmp = AdvSimd.And(AdvSimd.Arm64.Divide(AdvSimd.Subtract(ctmp, ktmp), AdvSimd.Subtract(scale, ktmp)), kMask); + mtmp = AdvSimd.And(AdvSimd.Arm64.Divide(AdvSimd.Subtract(mtmp, ktmp), AdvSimd.Subtract(scale, ktmp)), kMask); + ytmp = AdvSimd.And(AdvSimd.Arm64.Divide(AdvSimd.Subtract(ytmp, ktmp), AdvSimd.Subtract(scale, ktmp)), kMask); + + Unsafe.Add(ref destC, i) = AdvSimd.Subtract(scale, AdvSimd.Multiply(ctmp, scale)); + Unsafe.Add(ref destM, i) = AdvSimd.Subtract(scale, AdvSimd.Multiply(mtmp, scale)); + Unsafe.Add(ref destY, i) = AdvSimd.Subtract(scale, AdvSimd.Multiply(ytmp, scale)); + Unsafe.Add(ref destK, i) = AdvSimd.Subtract(scale, ktmp); + } + } + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterArm64.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterArm64.cs new file mode 100644 index 0000000000..d6d4d6ef93 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterArm64.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.Arm; +using System.Runtime.Intrinsics.X86; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components; + +internal abstract partial class JpegColorConverterBase +{ + /// + /// abstract base for implementations + /// based on instructions. + /// + /// + /// Converters of this family would expect input buffers lengths to be + /// divisible by 8 without a remainder. + /// This is guaranteed by real-life data as jpeg stores pixels via 8x8 blocks. + /// DO NOT pass test data of invalid size to these converters as they + /// potentially won't do a bound check and return a false positive result. + /// + internal abstract class JpegColorConverterArm64 : JpegColorConverterBase + { + protected JpegColorConverterArm64(JpegColorSpace colorSpace, int precision) + : base(colorSpace, precision) + { + } + + public static bool IsSupported => AdvSimd.Arm64.IsSupported; + + public sealed override bool IsAvailable => IsSupported; + + public sealed override int ElementsPerBatch => Vector128.Count; + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.cs index 66f0e9f5a5..c6ad623104 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.cs @@ -176,6 +176,11 @@ private static JpegColorConverterBase GetCmykConverter(int precision) return new CmykAvx(precision); } + if (JpegColorConverterArm64.IsSupported) + { + return new CmykArm64(precision); + } + if (JpegColorConverterVector.IsSupported) { return new CmykVector(precision); diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/CmykColorConversion.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/CmykColorConversion.cs index 6ad20ce679..51cd02bc7a 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/CmykColorConversion.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/CmykColorConversion.cs @@ -37,4 +37,12 @@ public void SimdVectorAvx() new JpegColorConverterBase.CmykAvx(8).ConvertToRgbInplace(values); } + + [Benchmark] + public void SimdVectorArm64() + { + var values = new JpegColorConverterBase.ComponentValues(this.Input, 0); + + new JpegColorConverterBase.CmykArm64(8).ConvertToRgbInplace(values); + } } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs index a939f1b687..44675aaea2 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs @@ -20,7 +20,7 @@ public class JpegColorConverterTests private const int TestBufferLength = 40; - private const HwIntrinsics IntrinsicsConfig = HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX; + private const HwIntrinsics IntrinsicsConfig = HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2; private static readonly ApproximateColorSpaceComparer ColorSpaceComparer = new(epsilon: Precision); @@ -36,7 +36,7 @@ public JpegColorConverterTests(ITestOutputHelper output) [Fact] public void GetConverterThrowsExceptionOnInvalidColorSpace() { - var invalidColorSpace = (JpegColorSpace)(-1); + JpegColorSpace invalidColorSpace = (JpegColorSpace)(-1); Assert.Throws(() => JpegColorConverterBase.GetConverter(invalidColorSpace, 8)); } @@ -61,7 +61,7 @@ public void GetConverterThrowsExceptionOnInvalidPrecision() [InlineData(JpegColorSpace.YCbCr, 12)] internal void GetConverterReturnsValidConverter(JpegColorSpace colorSpace, int precision) { - var converter = JpegColorConverterBase.GetConverter(colorSpace, precision); + JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(colorSpace, precision); Assert.NotNull(converter); Assert.True(converter.IsAvailable); @@ -75,10 +75,10 @@ internal void GetConverterReturnsValidConverter(JpegColorSpace colorSpace, int p [InlineData(JpegColorSpace.Cmyk, 4)] [InlineData(JpegColorSpace.RGB, 3)] [InlineData(JpegColorSpace.YCbCr, 3)] - internal void ConvertWithSelectedConverter(JpegColorSpace colorSpace, int componentCount) + internal void ConvertToRgbWithSelectedConverter(JpegColorSpace colorSpace, int componentCount) { - var converter = JpegColorConverterBase.GetConverter(colorSpace, 8); - ValidateConversion( + JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(colorSpace, 8); + ValidateConversionToRgb( converter, componentCount, 1); @@ -87,13 +87,13 @@ internal void ConvertWithSelectedConverter(JpegColorSpace colorSpace, int compon [Theory] [MemberData(nameof(Seeds))] public void FromYCbCrBasic(int seed) => - this.TestConverter(new JpegColorConverterBase.YCbCrScalar(8), 3, seed); + this.TestConversionToRgb(new JpegColorConverterBase.YCbCrScalar(8), 3, seed); [Theory] [MemberData(nameof(Seeds))] public void FromYCbCrVector(int seed) { - var converter = new JpegColorConverterBase.YCbCrVector(8); + JpegColorConverterBase.YCbCrVector converter = new(8); if (!converter.IsAvailable) { @@ -108,22 +108,23 @@ public void FromYCbCrVector(int seed) IntrinsicsConfig); static void RunTest(string arg) => - ValidateConversion( + ValidateConversionToRgb( new JpegColorConverterBase.YCbCrVector(8), 3, - FeatureTestRunner.Deserialize(arg)); + FeatureTestRunner.Deserialize(arg), + new JpegColorConverterBase.YCbCrScalar(8)); } [Theory] [MemberData(nameof(Seeds))] public void FromCmykBasic(int seed) => - this.TestConverter(new JpegColorConverterBase.CmykScalar(8), 4, seed); + this.TestConversionToRgb(new JpegColorConverterBase.CmykScalar(8), 4, seed); [Theory] [MemberData(nameof(Seeds))] public void FromCmykVector(int seed) { - var converter = new JpegColorConverterBase.CmykVector(8); + JpegColorConverterBase.CmykVector converter = new(8); if (!converter.IsAvailable) { @@ -138,22 +139,23 @@ public void FromCmykVector(int seed) IntrinsicsConfig); static void RunTest(string arg) => - ValidateConversion( + ValidateConversionToRgb( new JpegColorConverterBase.CmykVector(8), 4, - FeatureTestRunner.Deserialize(arg)); + FeatureTestRunner.Deserialize(arg), + new JpegColorConverterBase.CmykScalar(8)); } [Theory] [MemberData(nameof(Seeds))] public void FromGrayscaleBasic(int seed) => - this.TestConverter(new JpegColorConverterBase.GrayscaleScalar(8), 1, seed); + this.TestConversionToRgb(new JpegColorConverterBase.GrayscaleScalar(8), 1, seed); [Theory] [MemberData(nameof(Seeds))] public void FromGrayscaleVector(int seed) { - var converter = new JpegColorConverterBase.GrayScaleVector(8); + JpegColorConverterBase.GrayScaleVector converter = new(8); if (!converter.IsAvailable) { @@ -168,22 +170,23 @@ public void FromGrayscaleVector(int seed) IntrinsicsConfig); static void RunTest(string arg) => - ValidateConversion( + ValidateConversionToRgb( new JpegColorConverterBase.GrayScaleVector(8), 1, - FeatureTestRunner.Deserialize(arg)); + FeatureTestRunner.Deserialize(arg), + new JpegColorConverterBase.GrayscaleScalar(8)); } [Theory] [MemberData(nameof(Seeds))] public void FromRgbBasic(int seed) => - this.TestConverter(new JpegColorConverterBase.RgbScalar(8), 3, seed); + this.TestConversionToRgb(new JpegColorConverterBase.RgbScalar(8), 3, seed); [Theory] [MemberData(nameof(Seeds))] public void FromRgbVector(int seed) { - var converter = new JpegColorConverterBase.RgbVector(8); + JpegColorConverterBase.RgbVector converter = new(8); if (!converter.IsAvailable) { @@ -198,22 +201,23 @@ public void FromRgbVector(int seed) IntrinsicsConfig); static void RunTest(string arg) => - ValidateConversion( + ValidateConversionToRgb( new JpegColorConverterBase.RgbVector(8), 3, - FeatureTestRunner.Deserialize(arg)); + FeatureTestRunner.Deserialize(arg), + new JpegColorConverterBase.RgbScalar(8)); } [Theory] [MemberData(nameof(Seeds))] public void FromYccKBasic(int seed) => - this.TestConverter(new JpegColorConverterBase.YccKScalar(8), 4, seed); + this.TestConversionToRgb(new JpegColorConverterBase.YccKScalar(8), 4, seed); [Theory] [MemberData(nameof(Seeds))] public void FromYccKVector(int seed) { - var converter = new JpegColorConverterBase.YccKVector(8); + JpegColorConverterBase.YccKVector converter = new(8); if (!converter.IsAvailable) { @@ -228,41 +232,119 @@ public void FromYccKVector(int seed) IntrinsicsConfig); static void RunTest(string arg) => - ValidateConversion( + ValidateConversionToRgb( new JpegColorConverterBase.YccKVector(8), 4, - FeatureTestRunner.Deserialize(arg)); + FeatureTestRunner.Deserialize(arg), + new JpegColorConverterBase.YccKScalar(8)); } [Theory] [MemberData(nameof(Seeds))] public void FromYCbCrAvx2(int seed) => - this.TestConverter(new JpegColorConverterBase.YCbCrAvx(8), 3, seed); + this.TestConversionToRgb(new JpegColorConverterBase.YCbCrAvx(8), + 3, + seed, + new JpegColorConverterBase.YCbCrScalar(8)); + + [Theory] + [MemberData(nameof(Seeds))] + public void FromRgbToYCbCrAvx2(int seed) => + this.TestConversionFromRgb(new JpegColorConverterBase.YCbCrAvx(8), + 3, + seed, + new JpegColorConverterBase.YCbCrScalar(8), + precísion: 2); [Theory] [MemberData(nameof(Seeds))] public void FromCmykAvx2(int seed) => - this.TestConverter(new JpegColorConverterBase.CmykAvx(8), 4, seed); + this.TestConversionToRgb(new JpegColorConverterBase.CmykAvx(8), + 4, + seed, + new JpegColorConverterBase.CmykScalar(8)); + + [Theory] + [MemberData(nameof(Seeds))] + public void FromRgbToCmykAvx2(int seed) => + this.TestConversionFromRgb(new JpegColorConverterBase.CmykAvx(8), + 4, + seed, + new JpegColorConverterBase.CmykScalar(8), + precísion: 4); + + [Theory] + [MemberData(nameof(Seeds))] + public void FromCmykArm(int seed) => + this.TestConversionToRgb( new JpegColorConverterBase.CmykArm64(8), + 4, + seed, + new JpegColorConverterBase.CmykScalar(8)); + + [Theory] + [MemberData(nameof(Seeds))] + public void FromRgbToCmykArm(int seed) => + this.TestConversionFromRgb(new JpegColorConverterBase.CmykArm64(8), + 4, + seed, + new JpegColorConverterBase.CmykScalar(8), + precísion: 4); [Theory] [MemberData(nameof(Seeds))] public void FromGrayscaleAvx2(int seed) => - this.TestConverter(new JpegColorConverterBase.GrayscaleAvx(8), 1, seed); + this.TestConversionToRgb(new JpegColorConverterBase.GrayscaleAvx(8), + 1, + seed, + new JpegColorConverterBase.GrayscaleScalar(8)); + + [Theory] + [MemberData(nameof(Seeds))] + public void FromRgbToGrayscaleAvx2(int seed) => + this.TestConversionFromRgb(new JpegColorConverterBase.GrayscaleAvx(8), + 1, + seed, + new JpegColorConverterBase.GrayscaleScalar(8), + precísion: 3); [Theory] [MemberData(nameof(Seeds))] public void FromRgbAvx2(int seed) => - this.TestConverter(new JpegColorConverterBase.RgbAvx(8), 3, seed); + this.TestConversionToRgb(new JpegColorConverterBase.RgbAvx(8), + 3, + seed, + new JpegColorConverterBase.RgbScalar(8)); + + [Theory] + [MemberData(nameof(Seeds))] + public void FromRgbArm(int seed) => + this.TestConversionToRgb(new JpegColorConverterBase.RgbArm(8), + 3, + seed, + new JpegColorConverterBase.RgbScalar(8)); [Theory] [MemberData(nameof(Seeds))] public void FromYccKAvx2(int seed) => - this.TestConverter(new JpegColorConverterBase.YccKAvx(8), 4, seed); + this.TestConversionToRgb( new JpegColorConverterBase.YccKAvx(8), + 4, + seed, + new JpegColorConverterBase.YccKScalar(8)); - private void TestConverter( + [Theory] + [MemberData(nameof(Seeds))] + public void FromRgbToYccKAvx2(int seed) => + this.TestConversionFromRgb(new JpegColorConverterBase.YccKAvx(8), + 4, + seed, + new JpegColorConverterBase.YccKScalar(8), + precísion: 4); + + private void TestConversionToRgb( JpegColorConverterBase converter, int componentCount, - int seed) + int seed, + JpegColorConverterBase baseLineConverter = null) { if (!converter.IsAvailable) { @@ -271,10 +353,33 @@ private void TestConverter( return; } - ValidateConversion( + ValidateConversionToRgb( converter, componentCount, - seed); + seed, + baseLineConverter); + } + + private void TestConversionFromRgb( + JpegColorConverterBase converter, + int componentCount, + int seed, + JpegColorConverterBase baseLineConverter, + int precísion) + { + if (!converter.IsAvailable) + { + this.Output.WriteLine( + $"Skipping test - {converter.GetType().Name} is not supported on current hardware."); + return; + } + + ValidateConversionFromRgb( + converter, + componentCount, + seed, + baseLineConverter, + precísion); } private static JpegColorConverterBase.ComponentValues CreateRandomValues( @@ -303,24 +408,117 @@ private static JpegColorConverterBase.ComponentValues CreateRandomValues( return new JpegColorConverterBase.ComponentValues(buffers, 0); } - private static void ValidateConversion( + private static float[] CreateRandomValues(int length, Random rnd) + { + float[] values = new float[length]; + + for (int j = 0; j < values.Length; j++) + { + values[j] = (float)rnd.NextDouble() * MaxColorChannelValue; + } + + return values; + } + + private static void ValidateConversionToRgb( JpegColorConverterBase converter, int componentCount, - int seed) + int seed, + JpegColorConverterBase baseLineConverter = null) { JpegColorConverterBase.ComponentValues original = CreateRandomValues(TestBufferLength, componentCount, seed); - JpegColorConverterBase.ComponentValues values = new( + JpegColorConverterBase.ComponentValues actual = new( original.ComponentCount, original.Component0.ToArray(), original.Component1.ToArray(), original.Component2.ToArray(), original.Component3.ToArray()); - converter.ConvertToRgbInplace(values); + converter.ConvertToRgbInplace(actual); for (int i = 0; i < TestBufferLength; i++) { - Validate(converter.ColorSpace, original, values, i); + Validate(converter.ColorSpace, original, actual, i); + } + + // Compare conversion result to a baseline, should be the scalar version. + if (baseLineConverter != null) + { + JpegColorConverterBase.ComponentValues expected = new( + original.ComponentCount, + original.Component0.ToArray(), + original.Component1.ToArray(), + original.Component2.ToArray(), + original.Component3.ToArray()); + baseLineConverter.ConvertToRgbInplace(expected); + if (componentCount == 1) + { + Assert.True(expected.Component0.SequenceEqual(actual.Component0)); + } + + if (componentCount == 2) + { + Assert.True(expected.Component1.SequenceEqual(actual.Component1)); + } + + if (componentCount == 3) + { + Assert.True(expected.Component2.SequenceEqual(actual.Component2)); + } + + if (componentCount == 4) + { + Assert.True(expected.Component3.SequenceEqual(actual.Component3)); + } + } + } + + private static void ValidateConversionFromRgb( + JpegColorConverterBase converter, + int componentCount, + int seed, + JpegColorConverterBase baseLineConverter, + int precision = 4) + { + // arrange + JpegColorConverterBase.ComponentValues actual = CreateRandomValues(TestBufferLength, componentCount, seed); + JpegColorConverterBase.ComponentValues expected = CreateRandomValues(TestBufferLength, componentCount, seed); + Random rnd = new(seed); + float[] rLane = CreateRandomValues(TestBufferLength, rnd); + float[] gLane = CreateRandomValues(TestBufferLength, rnd); + float[] bLane = CreateRandomValues(TestBufferLength, rnd); + + // act + converter.ConvertFromRgb(actual, rLane, gLane, bLane); + baseLineConverter.ConvertFromRgb(expected, rLane, gLane, bLane); + + // assert + if (componentCount == 1) + { + CompareSequenceWithTolerance(expected.Component0, actual.Component0, precision); + } + + if (componentCount == 2) + { + CompareSequenceWithTolerance(expected.Component1, actual.Component1, precision); + } + + if (componentCount == 3) + { + CompareSequenceWithTolerance(expected.Component2, actual.Component2, precision); + } + + if (componentCount == 4) + { + CompareSequenceWithTolerance(expected.Component3, actual.Component3, precision); + } + } + + private static void CompareSequenceWithTolerance(Span expected, Span actual, int precision) + { + for (int i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i], actual[i], precision: precision); } } @@ -358,9 +556,9 @@ private static void ValidateYCbCr(in JpegColorConverterBase.ComponentValues valu float y = values.Component0[i]; float cb = values.Component1[i]; float cr = values.Component2[i]; - var expected = ColorSpaceConverter.ToRgb(new YCbCr(y, cb, cr)); + Rgb expected = ColorSpaceConverter.ToRgb(new YCbCr(y, cb, cr)); - var actual = new Rgb(result.Component0[i], result.Component1[i], result.Component2[i]); + Rgb actual = new(result.Component0[i], result.Component1[i], result.Component2[i]); bool equal = ColorSpaceComparer.Equals(expected, actual); Assert.True(equal, $"Colors {expected} and {actual} are not equal at index {i}");