mirror of https://github.com/SixLabors/ImageSharp
46 changed files with 703 additions and 287 deletions
@ -0,0 +1,62 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Numerics; |
|||
|
|||
namespace SixLabors.ImageSharp.ColorProfiles; |
|||
|
|||
/// <summary>
|
|||
/// Provides standard YCbCr matrices for RGB to YCbCr conversion.
|
|||
/// </summary>
|
|||
public static class KnownYCbCrMatrices |
|||
{ |
|||
#pragma warning disable SA1137 // Elements should have the same indentation
|
|||
#pragma warning disable SA1117 // Parameters should be on same line or separate lines
|
|||
/// <summary>
|
|||
/// ITU-R BT.601 (SD video standard).
|
|||
/// </summary>
|
|||
public static readonly YCbCrMatrix BT601 = new( |
|||
new Matrix4x4( |
|||
0.299000F, 0.587000F, 0.114000F, 0F, |
|||
-0.168736F, -0.331264F, 0.500000F, 0F, |
|||
0.500000F, -0.418688F, -0.081312F, 0F, |
|||
0F, 0F, 0F, 1F), |
|||
new Matrix4x4( |
|||
1.000000F, 0.000000F, 1.402000F, 0F, |
|||
1.000000F, -0.344136F, -0.714136F, 0F, |
|||
1.000000F, 1.772000F, 0.000000F, 0F, |
|||
0F, 0F, 0F, 1F), |
|||
new Vector3(0F, 0.5F, 0.5F)); |
|||
|
|||
/// <summary>
|
|||
/// ITU-R BT.709 (HD video, sRGB standard).
|
|||
/// </summary>
|
|||
public static readonly YCbCrMatrix BT709 = new( |
|||
new Matrix4x4( |
|||
0.212600F, 0.715200F, 0.072200F, 0F, |
|||
-0.114572F, -0.385428F, 0.500000F, 0F, |
|||
0.500000F, -0.454153F, -0.045847F, 0F, |
|||
0F, 0F, 0F, 1F), |
|||
new Matrix4x4( |
|||
1.000000F, 0.000000F, 1.574800F, 0F, |
|||
1.000000F, -0.187324F, -0.468124F, 0F, |
|||
1.000000F, 1.855600F, 0.000000F, 0F, |
|||
0F, 0F, 0F, 1F), |
|||
new Vector3(0F, 0.5F, 0.5F)); |
|||
|
|||
/// <summary>
|
|||
/// ITU-R BT.2020 (UHD/4K video standard).
|
|||
/// </summary>
|
|||
public static readonly YCbCrMatrix BT2020 = new( |
|||
new Matrix4x4( |
|||
0.262700F, 0.678000F, 0.059300F, 0F, |
|||
-0.139630F, -0.360370F, 0.500000F, 0F, |
|||
0.500000F, -0.459786F, -0.040214F, 0F, |
|||
0F, 0F, 0F, 1F), |
|||
new Matrix4x4( |
|||
1.000000F, 0.000000F, 1.474600F, 0F, |
|||
1.000000F, -0.164553F, -0.571353F, 0F, |
|||
1.000000F, 1.881400F, 0.000000F, 0F, |
|||
0F, 0F, 0F, 1F), |
|||
new Vector3(0F, 0.5F, 0.5F)); |
|||
} |
|||
@ -1,27 +0,0 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Numerics; |
|||
|
|||
namespace SixLabors.ImageSharp.ColorProfiles; |
|||
|
|||
/// <summary>
|
|||
/// Provides standard Y (luma) coefficient sets for weighted RGB conversions.
|
|||
/// </summary>
|
|||
public static class KnownYCoefficients |
|||
{ |
|||
/// <summary>
|
|||
/// ITU-R BT.601 (SD video standard).
|
|||
/// </summary>
|
|||
public static readonly Vector3 BT601 = new(0.299F, 0.587F, 0.114F); |
|||
|
|||
/// <summary>
|
|||
/// ITU-R BT.709 (HD video, sRGB standard).
|
|||
/// </summary>
|
|||
public static readonly Vector3 BT709 = new(0.2126F, 0.7152F, 0.0722F); |
|||
|
|||
/// <summary>
|
|||
/// ITU-R BT.2020 (UHD/4K video standard).
|
|||
/// </summary>
|
|||
public static readonly Vector3 BT2020 = new(0.2627F, 0.6780F, 0.0593F); |
|||
} |
|||
@ -0,0 +1,58 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Numerics; |
|||
using SixLabors.ImageSharp.ColorProfiles.WorkingSpaces; |
|||
|
|||
namespace SixLabors.ImageSharp.ColorProfiles; |
|||
|
|||
/// <summary>
|
|||
/// <para>
|
|||
/// Represents a YCbCr color matrix containing forward and inverse transformation matrices,
|
|||
/// and the chrominance offsets to apply for full-range encoding
|
|||
/// </para>
|
|||
/// <para>
|
|||
/// These matrices must be selected to match the characteristics of the associated <see cref="RgbWorkingSpace"/>,
|
|||
/// including its transfer function (gamma or companding) and chromaticity coordinates. Using mismatched matrices and
|
|||
/// working spaces will produce incorrect conversions.
|
|||
/// </para>
|
|||
/// </summary>
|
|||
public readonly struct YCbCrMatrix |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="YCbCrMatrix"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="forward">
|
|||
/// The forward transformation matrix from RGB to YCbCr. The matrix must include the
|
|||
/// standard chrominance offsets in the fourth column, such as <c>(0, 0.5, 0.5)</c>.
|
|||
/// </param>
|
|||
/// <param name="inverse">
|
|||
/// The inverse transformation matrix from YCbCr to RGB. This matrix expects that
|
|||
/// chrominance offsets have already been subtracted prior to application.
|
|||
/// </param>
|
|||
/// <param name="offset">
|
|||
/// The chrominance offsets to be added after the forward conversion,
|
|||
/// and subtracted before the inverse conversion. Usually <c>(0, 0.5, 0.5)</c>.
|
|||
/// </param>
|
|||
public YCbCrMatrix(Matrix4x4 forward, Matrix4x4 inverse, Vector3 offset) |
|||
{ |
|||
this.Forward = forward; |
|||
this.Inverse = inverse; |
|||
this.Offset = offset; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the matrix used to convert gamma-encoded RGB to YCbCr.
|
|||
/// </summary>
|
|||
public Matrix4x4 Forward { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the matrix used to convert YCbCr back to gamma-encoded RGB.
|
|||
/// </summary>
|
|||
public Matrix4x4 Inverse { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the chrominance offset vector to apply during encoding (add) or decoding (subtract).
|
|||
/// </summary>
|
|||
public Vector3 Offset { get; } |
|||
} |
|||
@ -0,0 +1,216 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Numerics; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Runtime.InteropServices; |
|||
|
|||
namespace SixLabors.ImageSharp.ColorProfiles; |
|||
|
|||
/// <summary>
|
|||
/// Represents a YCCK (luminance, blue chroma, red chroma, black) color.
|
|||
/// YCCK is not a true color space but a reversible transform of CMYK, where the CMY components
|
|||
/// are converted to YCbCr using the ITU-R BT.601 standard, and the K (black) component is preserved separately.
|
|||
/// </summary>
|
|||
[StructLayout(LayoutKind.Sequential)] |
|||
public readonly struct YccK : IColorProfile<YccK, Rgb> |
|||
{ |
|||
private static readonly Vector4 Min = Vector4.Zero; |
|||
private static readonly Vector4 Max = Vector4.One; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="YccK"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="y">The y luminance component.</param>
|
|||
/// <param name="cb">The cb chroma component.</param>
|
|||
/// <param name="cr">The cr chroma component.</param>
|
|||
/// <param name="k">The keyline black component.</param>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public YccK(float y, float cb, float cr, float k) |
|||
: this(new Vector4(y, cb, cr, k)) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="YccK"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="vector">The vector representing the c, m, y, k components.</param>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public YccK(Vector4 vector) |
|||
{ |
|||
vector = Vector4.Clamp(vector, Min, Max); |
|||
this.Y = vector.X; |
|||
this.Cb = vector.Y; |
|||
this.Cr = vector.Z; |
|||
this.K = vector.W; |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
|
|||
private YccK(Vector4 vector, bool _) |
|||
#pragma warning restore SA1313 // Parameter names should begin with lower-case letter
|
|||
{ |
|||
this.Y = vector.X; |
|||
this.Cb = vector.Y; |
|||
this.Cr = vector.Z; |
|||
this.K = vector.W; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the Y luminance component.
|
|||
/// <remarks>A value ranging between 0 and 1.</remarks>
|
|||
/// </summary>
|
|||
public float Y { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the C (blue) chroma component.
|
|||
/// <remarks>A value ranging between 0 and 1.</remarks>
|
|||
/// </summary>
|
|||
public float Cb { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the C (red) chroma component.
|
|||
/// <remarks>A value ranging between 0 and 1.</remarks>
|
|||
/// </summary>
|
|||
public float Cr { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the keyline black color component.
|
|||
/// <remarks>A value ranging between 0 and 1.</remarks>
|
|||
/// </summary>
|
|||
public float K { get; } |
|||
|
|||
/// <summary>
|
|||
/// Compares two <see cref="YccK"/> objects for equality.
|
|||
/// </summary>
|
|||
/// <param name="left">The <see cref="YccK"/> on the left side of the operand.</param>
|
|||
/// <param name="right">The <see cref="YccK"/> 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 ==(YccK left, YccK right) => left.Equals(right); |
|||
|
|||
/// <summary>
|
|||
/// Compares two <see cref="YccK"/> objects for inequality.
|
|||
/// </summary>
|
|||
/// <param name="left">The <see cref="YccK"/> on the left side of the operand.</param>
|
|||
/// <param name="right">The <see cref="YccK"/> 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 !=(YccK left, YccK right) => !left.Equals(right); |
|||
|
|||
/// <inheritdoc/>
|
|||
public Vector4 ToScaledVector4() |
|||
{ |
|||
Vector4 v4 = default; |
|||
v4 += this.AsVector4Unsafe(); |
|||
return v4; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public static YccK FromScaledVector4(Vector4 source) |
|||
=> new(source, true); |
|||
|
|||
/// <inheritdoc/>
|
|||
public static void ToScaledVector4(ReadOnlySpan<YccK> source, Span<Vector4> destination) |
|||
{ |
|||
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); |
|||
MemoryMarshal.Cast<YccK, Vector4>(source).CopyTo(destination); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public static void FromScaledVector4(ReadOnlySpan<Vector4> source, Span<YccK> destination) |
|||
{ |
|||
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); |
|||
MemoryMarshal.Cast<Vector4, YccK>(source).CopyTo(destination); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public Rgb ToProfileConnectingSpace(ColorConversionOptions options) |
|||
{ |
|||
Matrix4x4 m = options.YCbCrMatrix.Inverse; |
|||
Vector3 offset = options.YCbCrMatrix.Offset; |
|||
Vector3 normalized = this.AsVector3Unsafe() - offset; |
|||
|
|||
float r = Vector3.Dot(normalized, new Vector3(m.M11, m.M12, m.M13)); |
|||
float g = Vector3.Dot(normalized, new Vector3(m.M21, m.M22, m.M23)); |
|||
float b = Vector3.Dot(normalized, new Vector3(m.M31, m.M32, m.M33)); |
|||
|
|||
Vector3 rgb = new Vector3(r, g, b) * (1F - this.K); |
|||
return Rgb.FromScaledVector3(rgb); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public static YccK FromProfileConnectingSpace(ColorConversionOptions options, in Rgb source) |
|||
{ |
|||
Matrix4x4 m = options.YCbCrMatrix.Forward; |
|||
Vector3 offset = options.YCbCrMatrix.Offset; |
|||
|
|||
Vector3 rgb = source.AsVector3Unsafe(); |
|||
float k = 1F - MathF.Max(rgb.X, MathF.Max(rgb.Y, rgb.Z)); |
|||
|
|||
if (k >= 1F - Constants.Epsilon) |
|||
{ |
|||
return new YccK(new Vector4(0F, 0.5F, 0.5F, 1F), true); |
|||
} |
|||
|
|||
rgb /= 1F - k; |
|||
|
|||
float y = Vector3.Dot(rgb, new Vector3(m.M11, m.M12, m.M13)); |
|||
float cb = Vector3.Dot(rgb, new Vector3(m.M21, m.M22, m.M23)); |
|||
float cr = Vector3.Dot(rgb, new Vector3(m.M31, m.M32, m.M33)); |
|||
|
|||
return new YccK(new Vector4(y, cb, cr, k) + new Vector4(offset, 0F)); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<YccK> source, Span<Rgb> destination) |
|||
{ |
|||
// TODO: We can possibly optimize this by using SIMD
|
|||
for (int i = 0; i < source.Length; i++) |
|||
{ |
|||
destination[i] = source[i].ToProfileConnectingSpace(options); |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<Rgb> source, Span<YccK> 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); |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource() |
|||
=> ChromaticAdaptionWhitePointSource.RgbWorkingSpace; |
|||
|
|||
/// <inheritdoc/>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public override int GetHashCode() |
|||
=> HashCode.Combine(this.Y, this.Cb, this.Cr, this.K); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override string ToString() |
|||
=> FormattableString.Invariant($"YccK({this.Y:#0.##}, {this.Cb:#0.##}, {this.Cr:#0.##}, {this.K:#0.##})"); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool Equals(object? obj) |
|||
=> obj is YccK other && this.Equals(other); |
|||
|
|||
/// <inheritdoc/>
|
|||
public bool Equals(YccK other) |
|||
=> this.AsVector4Unsafe() == other.AsVector4Unsafe(); |
|||
|
|||
private Vector3 AsVector3Unsafe() => Unsafe.As<YccK, Vector3>(ref Unsafe.AsRef(in this)); |
|||
|
|||
private Vector4 AsVector4Unsafe() => Unsafe.As<YccK, Vector4>(ref Unsafe.AsRef(in this)); |
|||
} |
|||
@ -0,0 +1,80 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.ColorProfiles; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.ColorProfiles; |
|||
|
|||
/// <summary>
|
|||
/// Tests <see cref="Rgb"/>-<see cref="YccK"/> conversions.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Test data generated mathematically
|
|||
/// </remarks>
|
|||
public class RgbAndYccKConversionTests |
|||
{ |
|||
private static readonly ApproximateColorProfileComparer Comparer = new(.001F); |
|||
|
|||
[Theory] |
|||
[InlineData(1, .5F, .5F, 0, 1, 1, 1)] |
|||
[InlineData(0, .5F, .5F, 1, 0, 0, 0)] |
|||
[InlineData(.5F, .5F, .5F, 0, .5F, .5F, .5F)] |
|||
public void Convert_YccK_To_Rgb(float y, float cb, float cr, float k, float r, float g, float b) |
|||
{ |
|||
// Arrange
|
|||
YccK input = new(y, cb, cr, k); |
|||
Rgb expected = new(r, g, b); |
|||
ColorProfileConverter converter = new(); |
|||
|
|||
Span<YccK> inputSpan = new YccK[5]; |
|||
inputSpan.Fill(input); |
|||
|
|||
Span<Rgb> actualSpan = new Rgb[5]; |
|||
|
|||
// Act
|
|||
Rgb actual = converter.Convert<YccK, Rgb>(input); |
|||
converter.Convert<YccK, Rgb>(inputSpan, actualSpan); |
|||
|
|||
// Assert
|
|||
Assert.Equal(expected, actual, Comparer); |
|||
|
|||
for (int i = 0; i < actualSpan.Length; i++) |
|||
{ |
|||
Assert.Equal(expected, actualSpan[i], Comparer); |
|||
} |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(1, 1, 1, 1, .5F, .5F, 0)] |
|||
[InlineData(0, 0, 0, 0, .5F, .5F, 1)] |
|||
[InlineData(.5F, .5F, .5F, 1, .5F, .5F, .5F)] |
|||
public void Convert_Rgb_To_YccK(float r, float g, float b, float y, float cb, float cr, float k) |
|||
{ |
|||
// Multiple YccK representations can decode to the same RGB value.
|
|||
// For example, (Y=1.0, Cb=0.5, Cr=0.5, K=0.5) and (Y=0.5, Cb=0.5, Cr=0.5, K=0.0) both yield RGB (0.5, 0.5, 0.5).
|
|||
// This is expected because YccK is not a unique encoding — K modulates RGB after YCbCr decoding.
|
|||
// Round-tripping RGB -> YccK -> RGB is stable, but YccK -> RGB -> YccK is not injective.
|
|||
|
|||
// Arrange
|
|||
Rgb input = new(r, g, b); |
|||
YccK expected = new(y, cb, cr, k); |
|||
ColorProfileConverter converter = new(); |
|||
|
|||
Span<Rgb> inputSpan = new Rgb[5]; |
|||
inputSpan.Fill(input); |
|||
|
|||
Span<YccK> actualSpan = new YccK[5]; |
|||
|
|||
// Act
|
|||
YccK actual = converter.Convert<Rgb, YccK>(input); |
|||
converter.Convert<Rgb, YccK>(inputSpan, actualSpan); |
|||
|
|||
// Assert
|
|||
Assert.Equal(expected, actual, Comparer); |
|||
|
|||
for (int i = 0; i < actualSpan.Length; i++) |
|||
{ |
|||
Assert.Equal(expected, actualSpan[i], Comparer); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,44 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.ColorProfiles; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.ColorProfiles; |
|||
|
|||
/// <summary>
|
|||
/// Tests the <see cref="YccK"/> struct.
|
|||
/// </summary>
|
|||
[Trait("Color", "Conversion")] |
|||
public class YccKTests |
|||
{ |
|||
[Fact] |
|||
public void YccKConstructorAssignsFields() |
|||
{ |
|||
const float y = .75F; |
|||
const float cb = .5F; |
|||
const float cr = .25F; |
|||
const float k = .125F; |
|||
|
|||
YccK ycckValue = new(y, cb, cr, k); |
|||
Assert.Equal(y, ycckValue.Y); |
|||
Assert.Equal(cb, ycckValue.Cb); |
|||
Assert.Equal(cr, ycckValue.Cr); |
|||
Assert.Equal(k, ycckValue.K); |
|||
} |
|||
|
|||
[Fact] |
|||
public void YccKEquality() |
|||
{ |
|||
YccK x = default; |
|||
YccK y = new(1F, 1F, 1F, 1F); |
|||
Assert.True(default == default(YccK)); |
|||
Assert.False(default != default(YccK)); |
|||
Assert.Equal(default, default(YccK)); |
|||
Assert.Equal(new YccK(1, 1, 1, 1), new YccK(1, 1, 1, 1)); |
|||
Assert.Equal(new YccK(.5F, .5F, .5F, .5F), new YccK(.5F, .5F, .5F, .5F)); |
|||
|
|||
Assert.False(x.Equals(y)); |
|||
Assert.False(x.Equals((object)y)); |
|||
Assert.False(x.GetHashCode().Equals(y.GetHashCode())); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue