Browse Source

Fix Color Filters (#806)

* Use Matrix5x4 and fix Hue Filter

* Update test references. Fix #802

* Increase tolerance to handle xplat variance.

* Rename to ColorMatrix
af/merge-core
James Jackson-South 7 years ago
committed by GitHub
parent
commit
bbb739ba93
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 37
      src/ImageSharp/Common/Helpers/Vector4Utils.cs
  2. 459
      src/ImageSharp/Primitives/ColorMatrix.cs
  3. 6
      src/ImageSharp/Processing/FilterExtensions.cs
  4. 358
      src/ImageSharp/Processing/KnownFilterMatrices.cs
  5. 36
      src/ImageSharp/Processing/Processors/Filters/FilterProcessor.cs
  6. 3
      tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs
  7. 6
      tests/ImageSharp.Tests/Helpers/Vector4UtilsTests.cs
  8. 270
      tests/ImageSharp.Tests/Primitives/ColorMatrixTests.cs
  9. 8
      tests/ImageSharp.Tests/Processing/Processors/Filters/BrightnessTest.cs
  10. 9
      tests/ImageSharp.Tests/Processing/Processors/Filters/ColorBlindnessTest.cs
  11. 20
      tests/ImageSharp.Tests/Processing/Processors/Filters/FilterTest.cs
  12. 28
      tests/ImageSharp.Tests/TestUtilities/ApproximateFloatComparer.cs
  13. 2
      tests/Images/External

37
src/ImageSharp/Common/Helpers/Vector4Utils.cs

@ -5,6 +5,7 @@ using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Primitives;
namespace SixLabors.ImageSharp
{
@ -70,5 +71,41 @@ namespace SixLabors.ImageSharp
UnPremultiply(ref v);
}
}
/// <summary>
/// Transforms a vector by the given matrix.
/// </summary>
/// <param name="vector">The source vector.</param>
/// <param name="matrix">The transformation matrix.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public static void Transform(ref Vector4 vector, ref ColorMatrix matrix)
{
float x = vector.X;
float y = vector.Y;
float z = vector.Z;
float w = vector.W;
vector.X = (x * matrix.M11) + (y * matrix.M21) + (z * matrix.M31) + (w * matrix.M41) + matrix.M51;
vector.Y = (x * matrix.M12) + (y * matrix.M22) + (z * matrix.M32) + (w * matrix.M42) + matrix.M52;
vector.Z = (x * matrix.M13) + (y * matrix.M23) + (z * matrix.M33) + (w * matrix.M43) + matrix.M53;
vector.W = (x * matrix.M14) + (y * matrix.M24) + (z * matrix.M34) + (w * matrix.M44) + matrix.M54;
}
/// <summary>
/// Bulk variant of <see cref="Transform(ref Vector4, ref ColorMatrix)"/>
/// </summary>
/// <param name="vectors">The span of vectors</param>
/// <param name="matrix">The transformation matrix.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public static void Transform(Span<Vector4> vectors, ref ColorMatrix matrix)
{
ref Vector4 baseRef = ref MemoryMarshal.GetReference(vectors);
for (int i = 0; i < vectors.Length; i++)
{
ref Vector4 v = ref Unsafe.Add(ref baseRef, i);
Transform(ref v, ref matrix);
}
}
}
}

459
src/ImageSharp/Primitives/ColorMatrix.cs

@ -0,0 +1,459 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
#pragma warning disable SA1117 // Parameters should be on same line or separate lines
using System;
using System.Globalization;
using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Primitives
{
/// <summary>
/// A structure encapsulating a 5x4 matrix used for transforming the color and alpha components of an image.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct ColorMatrix : IEquatable<ColorMatrix>
{
/// <summary>
/// Value at row 1, column 1 of the matrix.
/// </summary>
public float M11;
/// <summary>
/// Value at row 1, column 2 of the matrix.
/// </summary>
public float M12;
/// <summary>
/// Value at row 1, column 3 of the matrix.
/// </summary>
public float M13;
/// <summary>
/// Value at row 1, column 4 of the matrix.
/// </summary>
public float M14;
/// <summary>
/// Value at row 2, column 1 of the matrix.
/// </summary>
public float M21;
/// <summary>
/// Value at row 2, column 2 of the matrix.
/// </summary>
public float M22;
/// <summary>
/// Value at row 2, column 3 of the matrix.
/// </summary>
public float M23;
/// <summary>
/// Value at row 2, column 4 of the matrix.
/// </summary>
public float M24;
/// <summary>
/// Value at row 3, column 1 of the matrix.
/// </summary>
public float M31;
/// <summary>
/// Value at row 3, column 2 of the matrix.
/// </summary>
public float M32;
/// <summary>
/// Value at row 3, column 3 of the matrix.
/// </summary>
public float M33;
/// <summary>
/// Value at row 3, column 4 of the matrix.
/// </summary>
public float M34;
/// <summary>
/// Value at row 4, column 1 of the matrix.
/// </summary>
public float M41;
/// <summary>
/// Value at row 4, column 2 of the matrix.
/// </summary>
public float M42;
/// <summary>
/// Value at row 4, column 3 of the matrix.
/// </summary>
public float M43;
/// <summary>
/// Value at row 4, column 4 of the matrix.
/// </summary>
public float M44;
/// <summary>
/// Value at row 5, column 1 of the matrix.
/// </summary>
public float M51;
/// <summary>
/// Value at row 5, column 2 of the matrix.
/// </summary>
public float M52;
/// <summary>
/// Value at row 5, column 3 of the matrix.
/// </summary>
public float M53;
/// <summary>
/// Value at row 5, column 4 of the matrix.
/// </summary>
public float M54;
/// <summary>
/// Initializes a new instance of the <see cref="ColorMatrix"/> struct.
/// </summary>
/// <param name="m11">The value at row 1, column 1 of the matrix.</param>
/// <param name="m12">The value at row 1, column 2 of the matrix.</param>
/// <param name="m13">The value at row 1, column 3 of the matrix.</param>
/// <param name="m14">The value at row 1, column 4 of the matrix.</param>
/// <param name="m21">The value at row 2, column 1 of the matrix.</param>
/// <param name="m22">The value at row 2, column 2 of the matrix.</param>
/// <param name="m23">The value at row 2, column 3 of the matrix.</param>
/// <param name="m24">The value at row 2, column 4 of the matrix.</param>
/// <param name="m31">The value at row 3, column 1 of the matrix.</param>
/// <param name="m32">The value at row 3, column 2 of the matrix.</param>
/// <param name="m33">The value at row 3, column 3 of the matrix.</param>
/// <param name="m34">The value at row 3, column 4 of the matrix.</param>
/// <param name="m41">The value at row 4, column 1 of the matrix.</param>
/// <param name="m42">The value at row 4, column 2 of the matrix.</param>
/// <param name="m43">The value at row 4, column 3 of the matrix.</param>
/// <param name="m44">The value at row 4, column 4 of the matrix.</param>
/// <param name="m51">The value at row 5, column 1 of the matrix.</param>
/// <param name="m52">The value at row 5, column 2 of the matrix.</param>
/// <param name="m53">The value at row 5, column 3 of the matrix.</param>
/// <param name="m54">The value at row 5, column 4 of the matrix.</param>
public ColorMatrix(float m11, float m12, float m13, float m14,
float m21, float m22, float m23, float m24,
float m31, float m32, float m33, float m34,
float m41, float m42, float m43, float m44,
float m51, float m52, float m53, float m54)
{
this.M11 = m11;
this.M12 = m12;
this.M13 = m13;
this.M14 = m14;
this.M21 = m21;
this.M22 = m22;
this.M23 = m23;
this.M24 = m24;
this.M31 = m31;
this.M32 = m32;
this.M33 = m33;
this.M34 = m34;
this.M41 = m41;
this.M42 = m42;
this.M43 = m43;
this.M44 = m44;
this.M51 = m51;
this.M52 = m52;
this.M53 = m53;
this.M54 = m54;
}
/// <summary>
/// Gets the multiplicative identity matrix.
/// </summary>
public static ColorMatrix Identity { get; } =
new ColorMatrix(1F, 0F, 0F, 0F,
0F, 1F, 0F, 0F,
0F, 0F, 1F, 0F,
0F, 0F, 0F, 1F,
0F, 0F, 0F, 0F);
/// <summary>
/// Gets a value indicating whether the matrix is the identity matrix.
/// </summary>
public bool IsIdentity
{
get
{
// Check diagonal element first for early out.
return this.M11 == 1F && this.M22 == 1F && this.M33 == 1F && this.M44 == 1F
&& this.M12 == 0F && this.M13 == 0F && this.M14 == 0F
&& this.M21 == 0F && this.M23 == 0F && this.M24 == 0F
&& this.M31 == 0F && this.M32 == 0F && this.M34 == 0F
&& this.M41 == 0F && this.M42 == 0F && this.M43 == 0F
&& this.M51 == 0F && this.M52 == 0F && this.M53 == 0F && this.M54 == 0F;
}
}
/// <summary>
/// Adds two matrices together.
/// </summary>
/// <param name="value1">The first source matrix.</param>
/// <param name="value2">The second source matrix.</param>
/// <returns>The resulting matrix.</returns>
public static ColorMatrix operator +(ColorMatrix value1, ColorMatrix value2)
{
ColorMatrix m;
m.M11 = value1.M11 + value2.M11;
m.M12 = value1.M12 + value2.M12;
m.M13 = value1.M13 + value2.M13;
m.M14 = value1.M14 + value2.M14;
m.M21 = value1.M21 + value2.M21;
m.M22 = value1.M22 + value2.M22;
m.M23 = value1.M23 + value2.M23;
m.M24 = value1.M24 + value2.M24;
m.M31 = value1.M31 + value2.M31;
m.M32 = value1.M32 + value2.M32;
m.M33 = value1.M33 + value2.M33;
m.M34 = value1.M34 + value2.M34;
m.M41 = value1.M41 + value2.M41;
m.M42 = value1.M42 + value2.M42;
m.M43 = value1.M43 + value2.M43;
m.M44 = value1.M44 + value2.M44;
m.M51 = value1.M51 + value2.M51;
m.M52 = value1.M52 + value2.M52;
m.M53 = value1.M53 + value2.M53;
m.M54 = value1.M54 + value2.M54;
return m;
}
/// <summary>
/// Subtracts the second matrix from the first.
/// </summary>
/// <param name="value1">The first source matrix.</param>
/// <param name="value2">The second source matrix.</param>
/// <returns>The result of the subtraction.</returns>
public static ColorMatrix operator -(ColorMatrix value1, ColorMatrix value2)
{
ColorMatrix m;
m.M11 = value1.M11 - value2.M11;
m.M12 = value1.M12 - value2.M12;
m.M13 = value1.M13 - value2.M13;
m.M14 = value1.M14 - value2.M14;
m.M21 = value1.M21 - value2.M21;
m.M22 = value1.M22 - value2.M22;
m.M23 = value1.M23 - value2.M23;
m.M24 = value1.M24 - value2.M24;
m.M31 = value1.M31 - value2.M31;
m.M32 = value1.M32 - value2.M32;
m.M33 = value1.M33 - value2.M33;
m.M34 = value1.M34 - value2.M34;
m.M41 = value1.M41 - value2.M41;
m.M42 = value1.M42 - value2.M42;
m.M43 = value1.M43 - value2.M43;
m.M44 = value1.M44 - value2.M44;
m.M51 = value1.M51 - value2.M51;
m.M52 = value1.M52 - value2.M52;
m.M53 = value1.M53 - value2.M53;
m.M54 = value1.M54 - value2.M54;
return m;
}
/// <summary>
/// Returns a new matrix with the negated elements of the given matrix.
/// </summary>
/// <param name="value">The source matrix.</param>
/// <returns>The negated matrix.</returns>
public static unsafe ColorMatrix operator -(ColorMatrix value)
{
ColorMatrix m;
m.M11 = -value.M11;
m.M12 = -value.M12;
m.M13 = -value.M13;
m.M14 = -value.M14;
m.M21 = -value.M21;
m.M22 = -value.M22;
m.M23 = -value.M23;
m.M24 = -value.M24;
m.M31 = -value.M31;
m.M32 = -value.M32;
m.M33 = -value.M33;
m.M34 = -value.M34;
m.M41 = -value.M41;
m.M42 = -value.M42;
m.M43 = -value.M43;
m.M44 = -value.M44;
m.M51 = -value.M51;
m.M52 = -value.M52;
m.M53 = -value.M53;
m.M54 = -value.M54;
return m;
}
/// <summary>
/// Multiplies a matrix by another matrix.
/// </summary>
/// <param name="value1">The first source matrix.</param>
/// <param name="value2">The second source matrix.</param>
/// <returns>The result of the multiplication.</returns>
public static ColorMatrix operator *(ColorMatrix value1, ColorMatrix value2)
{
ColorMatrix m;
// First row
m.M11 = (value1.M11 * value2.M11) + (value1.M12 * value2.M21) + (value1.M13 * value2.M31) + (value1.M14 * value2.M41);
m.M12 = (value1.M11 * value2.M12) + (value1.M12 * value2.M22) + (value1.M13 * value2.M32) + (value1.M14 * value2.M42);
m.M13 = (value1.M11 * value2.M13) + (value1.M12 * value2.M23) + (value1.M13 * value2.M33) + (value1.M14 * value2.M43);
m.M14 = (value1.M11 * value2.M14) + (value1.M12 * value2.M24) + (value1.M13 * value2.M34) + (value1.M14 * value2.M44);
// Second row
m.M21 = (value1.M21 * value2.M11) + (value1.M22 * value2.M21) + (value1.M23 * value2.M31) + (value1.M24 * value2.M41);
m.M22 = (value1.M21 * value2.M12) + (value1.M22 * value2.M22) + (value1.M23 * value2.M32) + (value1.M24 * value2.M42);
m.M23 = (value1.M21 * value2.M13) + (value1.M22 * value2.M23) + (value1.M23 * value2.M33) + (value1.M24 * value2.M43);
m.M24 = (value1.M21 * value2.M14) + (value1.M22 * value2.M24) + (value1.M23 * value2.M34) + (value1.M24 * value2.M44);
// Third row
m.M31 = (value1.M31 * value2.M11) + (value1.M32 * value2.M21) + (value1.M33 * value2.M31) + (value1.M34 * value2.M41);
m.M32 = (value1.M31 * value2.M12) + (value1.M32 * value2.M22) + (value1.M33 * value2.M32) + (value1.M34 * value2.M42);
m.M33 = (value1.M31 * value2.M13) + (value1.M32 * value2.M23) + (value1.M33 * value2.M33) + (value1.M34 * value2.M43);
m.M34 = (value1.M31 * value2.M14) + (value1.M32 * value2.M24) + (value1.M33 * value2.M34) + (value1.M34 * value2.M44);
// Fourth row
m.M41 = (value1.M41 * value2.M11) + (value1.M42 * value2.M21) + (value1.M43 * value2.M31) + (value1.M44 * value2.M41);
m.M42 = (value1.M41 * value2.M12) + (value1.M42 * value2.M22) + (value1.M43 * value2.M32) + (value1.M44 * value2.M42);
m.M43 = (value1.M41 * value2.M13) + (value1.M42 * value2.M23) + (value1.M43 * value2.M33) + (value1.M44 * value2.M43);
m.M44 = (value1.M41 * value2.M14) + (value1.M42 * value2.M24) + (value1.M43 * value2.M34) + (value1.M44 * value2.M44);
// Fifth row
m.M51 = (value1.M51 * value2.M11) + (value1.M52 * value2.M21) + (value1.M53 * value2.M31) + (value1.M54 * value2.M41) + value2.M51;
m.M52 = (value1.M51 * value2.M12) + (value1.M52 * value2.M22) + (value1.M53 * value2.M32) + (value1.M54 * value2.M52) + value2.M52;
m.M53 = (value1.M51 * value2.M13) + (value1.M52 * value2.M23) + (value1.M53 * value2.M33) + (value1.M54 * value2.M53) + value2.M53;
m.M54 = (value1.M51 * value2.M14) + (value1.M52 * value2.M24) + (value1.M53 * value2.M34) + (value1.M54 * value2.M54) + value2.M54;
return m;
}
/// <summary>
/// Multiplies a matrix by a scalar value.
/// </summary>
/// <param name="value1">The source matrix.</param>
/// <param name="value2">The scaling factor.</param>
/// <returns>The scaled matrix.</returns>
public static ColorMatrix operator *(ColorMatrix value1, float value2)
{
ColorMatrix m;
m.M11 = value1.M11 * value2;
m.M12 = value1.M12 * value2;
m.M13 = value1.M13 * value2;
m.M14 = value1.M14 * value2;
m.M21 = value1.M21 * value2;
m.M22 = value1.M22 * value2;
m.M23 = value1.M23 * value2;
m.M24 = value1.M24 * value2;
m.M31 = value1.M31 * value2;
m.M32 = value1.M32 * value2;
m.M33 = value1.M33 * value2;
m.M34 = value1.M34 * value2;
m.M41 = value1.M41 * value2;
m.M42 = value1.M42 * value2;
m.M43 = value1.M43 * value2;
m.M44 = value1.M44 * value2;
m.M51 = value1.M51 * value2;
m.M52 = value1.M52 * value2;
m.M53 = value1.M53 * value2;
m.M54 = value1.M54 * value2;
return m;
}
/// <summary>
/// Returns a boolean indicating whether the given two matrices are equal.
/// </summary>
/// <param name="value1">The first matrix to compare.</param>
/// <param name="value2">The second matrix to compare.</param>
/// <returns>True if the given matrices are equal; False otherwise.</returns>
public static bool operator ==(ColorMatrix value1, ColorMatrix value2) => value1.Equals(value2);
/// <summary>
/// Returns a boolean indicating whether the given two matrices are not equal.
/// </summary>
/// <param name="value1">The first matrix to compare.</param>
/// <param name="value2">The second matrix to compare.</param>
/// <returns>True if the given matrices are equal; False otherwise.</returns>
public static bool operator !=(ColorMatrix value1, ColorMatrix value2) => !value1.Equals(value2);
/// <inheritdoc/>
public override bool Equals(object obj) => obj is ColorMatrix matrix && this.Equals(matrix);
/// <inheritdoc/>
public bool Equals(ColorMatrix other) =>
this.M11 == other.M11
&& this.M12 == other.M12
&& this.M13 == other.M13
&& this.M14 == other.M14
&& this.M21 == other.M21
&& this.M22 == other.M22
&& this.M23 == other.M23
&& this.M24 == other.M24
&& this.M31 == other.M31
&& this.M32 == other.M32
&& this.M33 == other.M33
&& this.M34 == other.M34
&& this.M41 == other.M41
&& this.M42 == other.M42
&& this.M43 == other.M43
&& this.M44 == other.M44
&& this.M51 == other.M51
&& this.M52 == other.M52
&& this.M53 == other.M53
&& this.M54 == other.M54;
/// <inheritdoc/>
public override int GetHashCode()
{
HashCode hash = default;
hash.Add(this.M11);
hash.Add(this.M12);
hash.Add(this.M13);
hash.Add(this.M14);
hash.Add(this.M21);
hash.Add(this.M22);
hash.Add(this.M23);
hash.Add(this.M24);
hash.Add(this.M31);
hash.Add(this.M32);
hash.Add(this.M33);
hash.Add(this.M34);
hash.Add(this.M41);
hash.Add(this.M42);
hash.Add(this.M43);
hash.Add(this.M44);
hash.Add(this.M51);
hash.Add(this.M52);
hash.Add(this.M53);
hash.Add(this.M54);
return hash.ToHashCode();
}
/// <inheritdoc/>
public override string ToString()
{
CultureInfo ci = CultureInfo.CurrentCulture;
return string.Format(ci, "{{ {{M11:{0} M12:{1} M13:{2} M14:{3}}} {{M21:{4} M22:{5} M23:{6} M24:{7}}} {{M31:{8} M32:{9} M33:{10} M34:{11}}} {{M41:{12} M42:{13} M43:{14} M44:{15}}} {{M51:{16} M52:{17} M53:{18} M54:{19}}} }}",
this.M11.ToString(ci), this.M12.ToString(ci), this.M13.ToString(ci), this.M14.ToString(ci),
this.M21.ToString(ci), this.M22.ToString(ci), this.M23.ToString(ci), this.M24.ToString(ci),
this.M31.ToString(ci), this.M32.ToString(ci), this.M33.ToString(ci), this.M34.ToString(ci),
this.M41.ToString(ci), this.M42.ToString(ci), this.M43.ToString(ci), this.M44.ToString(ci),
this.M51.ToString(ci), this.M52.ToString(ci), this.M53.ToString(ci), this.M54.ToString(ci));
}
}
}

6
src/ImageSharp/Processing/FilterExtensions.cs

@ -1,8 +1,8 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Primitives;
using SixLabors.ImageSharp.Processing.Processors.Filters;
using SixLabors.Primitives;
@ -20,7 +20,7 @@ namespace SixLabors.ImageSharp.Processing
/// <param name="source">The image this method extends.</param>
/// <param name="matrix">The filter color matrix</param>
/// <returns>The <see cref="Image{TPixel}"/>.</returns>
public static IImageProcessingContext<TPixel> Filter<TPixel>(this IImageProcessingContext<TPixel> source, Matrix4x4 matrix)
public static IImageProcessingContext<TPixel> Filter<TPixel>(this IImageProcessingContext<TPixel> source, ColorMatrix matrix)
where TPixel : struct, IPixel<TPixel>
=> source.ApplyProcessor(new FilterProcessor<TPixel>(matrix));
@ -34,7 +34,7 @@ namespace SixLabors.ImageSharp.Processing
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param>
/// <returns>The <see cref="Image{TPixel}"/>.</returns>
public static IImageProcessingContext<TPixel> Filter<TPixel>(this IImageProcessingContext<TPixel> source, Matrix4x4 matrix, Rectangle rectangle)
public static IImageProcessingContext<TPixel> Filter<TPixel>(this IImageProcessingContext<TPixel> source, ColorMatrix matrix, Rectangle rectangle)
where TPixel : struct, IPixel<TPixel>
=> source.ApplyProcessor(new FilterProcessor<TPixel>(matrix), rectangle);
}

358
src/ImageSharp/Processing/KnownFilterMatrices.cs

@ -2,19 +2,28 @@
// Licensed under the Apache License, Version 2.0.
using System;
using System.Numerics;
using SixLabors.ImageSharp.Primitives;
// Many of these matrices are tranlated from Chromium project where
// SkScalar[] is memory-mapped to a row-major matrix.
// The following translates to our column-major form:
//
// | 0| 1| 2| 3| 4| |0|5|10|15| |M11|M12|M13|M14|
// | 5| 6| 7| 8| 9| |1|6|11|16| |M21|M22|M23|M24|
// |10|11|12|13|14| = |2|7|12|17| = |M31|M32|M33|M34|
// |15|16|17|18|19| |3|8|13|18| |M41|M42|M43|M44|
// |4|9|14|19| |M51|M52|M53|M54|
namespace SixLabors.ImageSharp.Processing
{
/// <summary>
/// A collection of known <see cref="Matrix4x4"/> values for composing filters
/// A collection of known <see cref="ColorMatrix"/> values for composing filters
/// </summary>
public static class KnownFilterMatrices
{
/// <summary>
/// Gets a filter recreating Achromatomaly (Color desensitivity) color blindness
/// </summary>
public static Matrix4x4 AchromatomalyFilter { get; } = new Matrix4x4
public static ColorMatrix AchromatomalyFilter { get; } = new ColorMatrix
{
M11 = .618F,
M12 = .163F,
@ -31,7 +40,7 @@ namespace SixLabors.ImageSharp.Processing
/// <summary>
/// Gets a filter recreating Achromatopsia (Monochrome) color blindness.
/// </summary>
public static Matrix4x4 AchromatopsiaFilter { get; } = new Matrix4x4
public static ColorMatrix AchromatopsiaFilter { get; } = new ColorMatrix
{
M11 = .299F,
M12 = .299F,
@ -42,97 +51,97 @@ namespace SixLabors.ImageSharp.Processing
M31 = .114F,
M32 = .114F,
M33 = .114F,
M44 = 1
M44 = 1F
};
/// <summary>
/// Gets a filter recreating Deuteranomaly (Green-Weak) color blindness.
/// </summary>
public static Matrix4x4 DeuteranomalyFilter { get; } = new Matrix4x4
public static ColorMatrix DeuteranomalyFilter { get; } = new ColorMatrix
{
M11 = 0.8F,
M12 = 0.258F,
M21 = 0.2F,
M22 = 0.742F,
M23 = 0.142F,
M33 = 0.858F,
M44 = 1
M11 = .8F,
M12 = .258F,
M21 = .2F,
M22 = .742F,
M23 = .142F,
M33 = .858F,
M44 = 1F
};
/// <summary>
/// Gets a filter recreating Deuteranopia (Green-Blind) color blindness.
/// </summary>
public static Matrix4x4 DeuteranopiaFilter { get; } = new Matrix4x4
public static ColorMatrix DeuteranopiaFilter { get; } = new ColorMatrix
{
M11 = 0.625F,
M12 = 0.7F,
M21 = 0.375F,
M22 = 0.3F,
M23 = 0.3F,
M33 = 0.7F,
M44 = 1
M11 = .625F,
M12 = .7F,
M21 = .375F,
M22 = .3F,
M23 = .3F,
M33 = .7F,
M44 = 1F
};
/// <summary>
/// Gets a filter recreating Protanomaly (Red-Weak) color blindness.
/// </summary>
public static Matrix4x4 ProtanomalyFilter { get; } = new Matrix4x4
public static ColorMatrix ProtanomalyFilter { get; } = new ColorMatrix
{
M11 = 0.817F,
M12 = 0.333F,
M21 = 0.183F,
M22 = 0.667F,
M23 = 0.125F,
M33 = 0.875F,
M44 = 1
M11 = .817F,
M12 = .333F,
M21 = .183F,
M22 = .667F,
M23 = .125F,
M33 = .875F,
M44 = 1F
};
/// <summary>
/// Gets a filter recreating Protanopia (Red-Blind) color blindness.
/// </summary>
public static Matrix4x4 ProtanopiaFilter { get; } = new Matrix4x4
public static ColorMatrix ProtanopiaFilter { get; } = new ColorMatrix
{
M11 = 0.567F,
M12 = 0.558F,
M21 = 0.433F,
M22 = 0.442F,
M23 = 0.242F,
M33 = 0.758F,
M44 = 1
M11 = .567F,
M12 = .558F,
M21 = .433F,
M22 = .442F,
M23 = .242F,
M33 = .758F,
M44 = 1F
};
/// <summary>
/// Gets a filter recreating Tritanomaly (Blue-Weak) color blindness.
/// </summary>
public static Matrix4x4 TritanomalyFilter { get; } = new Matrix4x4
public static ColorMatrix TritanomalyFilter { get; } = new ColorMatrix
{
M11 = 0.967F,
M21 = 0.33F,
M22 = 0.733F,
M23 = 0.183F,
M32 = 0.267F,
M33 = 0.817F,
M44 = 1
M11 = .967F,
M21 = .33F,
M22 = .733F,
M23 = .183F,
M32 = .267F,
M33 = .817F,
M44 = 1F
};
/// <summary>
/// Gets a filter recreating Tritanopia (Blue-Blind) color blindness.
/// </summary>
public static Matrix4x4 TritanopiaFilter { get; } = new Matrix4x4
public static ColorMatrix TritanopiaFilter { get; } = new ColorMatrix
{
M11 = 0.95F,
M21 = 0.05F,
M22 = 0.433F,
M23 = 0.475F,
M32 = 0.567F,
M33 = 0.525F,
M44 = 1
M11 = .95F,
M21 = .05F,
M22 = .433F,
M23 = .475F,
M32 = .567F,
M33 = .525F,
M44 = 1F
};
/// <summary>
/// Gets an approximated black and white filter
/// </summary>
public static Matrix4x4 BlackWhiteFilter { get; } = new Matrix4x4()
public static ColorMatrix BlackWhiteFilter { get; } = new ColorMatrix()
{
M11 = 1.5F,
M12 = 1.5F,
@ -143,24 +152,24 @@ namespace SixLabors.ImageSharp.Processing
M31 = 1.5F,
M32 = 1.5F,
M33 = 1.5F,
M41 = -1F,
M42 = -1F,
M43 = -1F,
M44 = 1
M44 = 1F,
M51 = -1F,
M52 = -1F,
M53 = -1F,
};
/// <summary>
/// Gets a filter recreating an old Kodachrome camera effect.
/// </summary>
public static Matrix4x4 KodachromeFilter { get; } = new Matrix4x4
public static ColorMatrix KodachromeFilter { get; } = new ColorMatrix
{
M11 = 0.7297023F,
M22 = 0.6109577F,
M33 = 0.597218F,
M41 = 0.105F,
M42 = 0.145F,
M43 = 0.155F,
M44 = 1
M11 = .7297023F,
M22 = .6109577F,
M33 = .597218F,
M44 = 1F,
M51 = .105F,
M52 = .145F,
M53 = .155F,
}
* CreateSaturateFilter(1.2F) * CreateContrastFilter(1.35F);
@ -168,15 +177,15 @@ namespace SixLabors.ImageSharp.Processing
/// <summary>
/// Gets a filter recreating an old Lomograph camera effect.
/// </summary>
public static Matrix4x4 LomographFilter { get; } = new Matrix4x4
public static ColorMatrix LomographFilter { get; } = new ColorMatrix
{
M11 = 1.5F,
M22 = 1.45F,
M33 = 1.16F,
M41 = -.1F,
M42 = -.02F,
M43 = -.07F,
M44 = 1
M44 = 1F,
M51 = -.1F,
M52 = -.02F,
M53 = -.07F,
}
* CreateSaturateFilter(1.1F) * CreateContrastFilter(1.33F);
@ -184,21 +193,21 @@ namespace SixLabors.ImageSharp.Processing
/// <summary>
/// Gets a filter recreating an old Polaroid camera effect.
/// </summary>
public static Matrix4x4 PolaroidFilter { get; } = new Matrix4x4
public static ColorMatrix PolaroidFilter { get; } = new ColorMatrix
{
M11 = 1.538F,
M12 = -0.062F,
M13 = -0.262F,
M21 = -0.022F,
M12 = -.062F,
M13 = -.262F,
M21 = -.022F,
M22 = 1.578F,
M23 = -0.022F,
M23 = -.022F,
M31 = .216F,
M32 = -.16F,
M33 = 1.5831F,
M41 = 0.02F,
M42 = -0.05F,
M43 = -0.05F,
M44 = 1
M44 = 1F,
M51 = .02F,
M52 = -.05F,
M53 = -.05F
};
/// <summary>
@ -209,18 +218,18 @@ namespace SixLabors.ImageSharp.Processing
/// Other values are linear multipliers on the effect. Values of an amount over 1 are allowed, providing brighter results.
/// </remarks>
/// <param name="amount">The proportion of the conversion. Must be greater than or equal to 0.</param>
/// <returns>The <see cref="Matrix4x4"/></returns>
public static Matrix4x4 CreateBrightnessFilter(float amount)
/// <returns>The <see cref="ColorMatrix"/></returns>
public static ColorMatrix CreateBrightnessFilter(float amount)
{
Guard.MustBeGreaterThanOrEqualTo(amount, 0, nameof(amount));
// See https://cs.chromium.org/chromium/src/cc/paint/render_surface_filters.cc
return new Matrix4x4
return new ColorMatrix
{
M11 = amount,
M22 = amount,
M33 = amount,
M44 = 1
M44 = 1F
};
}
@ -232,23 +241,23 @@ namespace SixLabors.ImageSharp.Processing
/// Other values are linear multipliers on the effect. Values of an amount over 1 are allowed, providing results with more contrast.
/// </remarks>
/// <param name="amount">The proportion of the conversion. Must be greater than or equal to 0.</param>
/// <returns>The <see cref="Matrix4x4"/></returns>
public static Matrix4x4 CreateContrastFilter(float amount)
/// <returns>The <see cref="ColorMatrix"/></returns>
public static ColorMatrix CreateContrastFilter(float amount)
{
Guard.MustBeGreaterThanOrEqualTo(amount, 0, nameof(amount));
// See https://cs.chromium.org/chromium/src/cc/paint/render_surface_filters.cc
float contrast = (-.5F * amount) + .5F;
return new Matrix4x4
return new ColorMatrix
{
M11 = amount,
M22 = amount,
M33 = amount,
M41 = contrast,
M42 = contrast,
M43 = contrast,
M44 = 1
M44 = 1F,
M51 = contrast,
M52 = contrast,
M53 = contrast
};
}
@ -257,26 +266,27 @@ namespace SixLabors.ImageSharp.Processing
/// <see href="https://en.wikipedia.org/wiki/Luma_%28video%29#Rec._601_luma_versus_Rec._709_luma_coefficients"/>
/// </summary>
/// <param name="amount">The proportion of the conversion. Must be between 0 and 1.</param>
/// <returns>The <see cref="Matrix4x4"/></returns>
public static Matrix4x4 CreateGrayscaleBt601Filter(float amount)
/// <returns>The <see cref="ColorMatrix"/></returns>
public static ColorMatrix CreateGrayscaleBt601Filter(float amount)
{
Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount));
Guard.MustBeBetweenOrEqualTo(amount, 0, 1F, nameof(amount));
amount = 1F - amount;
// https://cs.chromium.org/chromium/src/cc/paint/render_surface_filters.cc
return new Matrix4x4
{
M11 = .299F + (.701F * amount),
M12 = .299F - (.299F * amount),
M13 = .299F - (.299F * amount),
M21 = .587F - (.587F * amount),
M22 = .587F + (.413F * amount),
M23 = .587F - (.587F * amount),
M31 = .114F - (.114F * amount),
M32 = .114F - (.114F * amount),
M33 = .114F + (.886F * amount),
M44 = 1
};
ColorMatrix m = default;
m.M11 = .299F + (.701F * amount);
m.M21 = .587F - (.587F * amount);
m.M31 = 1F - (m.M11 + m.M21);
m.M12 = .299F - (.299F * amount);
m.M22 = .587F + (.2848F * amount);
m.M32 = 1F - (m.M12 + m.M22);
m.M13 = .299F - (.299F * amount);
m.M23 = .587F - (.587F * amount);
m.M33 = 1F - (m.M13 + m.M23);
m.M44 = 1F;
return m;
}
/// <summary>
@ -284,34 +294,36 @@ namespace SixLabors.ImageSharp.Processing
/// <see href="https://en.wikipedia.org/wiki/Rec._709#Luma_coefficients"/>
/// </summary>
/// <param name="amount">The proportion of the conversion. Must be between 0 and 1.</param>
/// <returns>The <see cref="Matrix4x4"/></returns>
public static Matrix4x4 CreateGrayscaleBt709Filter(float amount)
/// <returns>The <see cref="ColorMatrix"/></returns>
public static ColorMatrix CreateGrayscaleBt709Filter(float amount)
{
Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount));
Guard.MustBeBetweenOrEqualTo(amount, 0, 1F, nameof(amount));
amount = 1F - amount;
// https://cs.chromium.org/chromium/src/cc/paint/render_surface_filters.cc
return new Matrix4x4
{
M11 = .2126F + (.7874F * amount),
M12 = .2126F - (.2126F * amount),
M13 = .2126F - (.2126F * amount),
M21 = .7152F - (.7152F * amount),
M22 = .7152F + (.2848F * amount),
M23 = .7152F - (.7152F * amount),
M31 = .0722F - (.0722F * amount),
M32 = .0722F - (.0722F * amount),
M33 = .0722F + (.9278F * amount),
M44 = 1
};
ColorMatrix m = default;
m.M11 = .2126F + (.7874F * amount);
m.M21 = .7152F - (.7152F * amount);
m.M31 = 1F - (m.M11 + m.M21);
m.M12 = .2126F - (.2126F * amount);
m.M22 = .7152F + (.2848F * amount);
m.M32 = 1F - (m.M12 + m.M22);
m.M13 = .2126F - (.2126F * amount);
m.M23 = .7152F - (.7152F * amount);
m.M33 = 1F - (m.M13 + m.M23);
m.M44 = 1F;
return m;
}
/// <summary>
/// Create a hue filter matrix using the given angle in degrees.
/// </summary>
/// <param name="degrees">The angle of rotation in degrees.</param>
/// <returns>The <see cref="Matrix4x4"/></returns>
public static Matrix4x4 CreateHueFilter(float degrees)
/// <returns>The <see cref="ColorMatrix"/></returns>
public static ColorMatrix CreateHueFilter(float degrees)
{
// Wrap the angle round at 360.
degrees %= 360;
@ -329,18 +341,20 @@ namespace SixLabors.ImageSharp.Processing
// The matrix is set up to preserve the luminance of the image.
// See http://graficaobscura.com/matrix/index.html
// Number are taken from https://msdn.microsoft.com/en-us/library/jj192162(v=vs.85).aspx
return new Matrix4x4
return new ColorMatrix
{
M11 = .213F + (cosRadian * .787F) - (sinRadian * .213F),
M12 = .213F - (cosRadian * .213F) - (sinRadian * 0.143F),
M13 = .213F - (cosRadian * .213F) - (sinRadian * .787F),
M21 = .715F - (cosRadian * .715F) - (sinRadian * .715F),
M22 = .715F + (cosRadian * .285F) + (sinRadian * 0.140F),
M23 = .715F - (cosRadian * .715F) + (sinRadian * .715F),
M31 = .072F - (cosRadian * .072F) + (sinRadian * .928F),
M32 = .072F - (cosRadian * .072F) - (sinRadian * 0.283F),
M12 = .213F - (cosRadian * .213F) + (sinRadian * .143F),
M22 = .715F + (cosRadian * .285F) + (sinRadian * .140F),
M32 = .072F - (cosRadian * .072F) - (sinRadian * .283F),
M13 = .213F - (cosRadian * .213F) - (sinRadian * .787F),
M23 = .715F - (cosRadian * .715F) + (sinRadian * .715F),
M33 = .072F + (cosRadian * .928F) + (sinRadian * .072F),
M44 = 1
M44 = 1F
};
}
@ -348,23 +362,23 @@ namespace SixLabors.ImageSharp.Processing
/// Create an invert filter matrix using the given amount.
/// </summary>
/// <param name="amount">The proportion of the conversion. Must be between 0 and 1.</param>
/// <returns>The <see cref="Matrix4x4"/></returns>
public static Matrix4x4 CreateInvertFilter(float amount)
/// <returns>The <see cref="ColorMatrix"/></returns>
public static ColorMatrix CreateInvertFilter(float amount)
{
Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount));
// See https://cs.chromium.org/chromium/src/cc/paint/render_surface_filters.cc
float invert = 1F - (2F * amount);
return new Matrix4x4
return new ColorMatrix
{
M11 = invert,
M22 = invert,
M33 = invert,
M41 = amount,
M42 = amount,
M43 = amount,
M44 = 1
M44 = 1F,
M51 = amount,
M52 = amount,
M53 = amount,
};
}
@ -372,17 +386,17 @@ namespace SixLabors.ImageSharp.Processing
/// Create an opacity filter matrix using the given amount.
/// </summary>
/// <param name="amount">The proportion of the conversion. Must be between 0 and 1.</param>
/// <returns>The <see cref="Matrix4x4"/></returns>
public static Matrix4x4 CreateOpacityFilter(float amount)
/// <returns>The <see cref="ColorMatrix"/></returns>
public static ColorMatrix CreateOpacityFilter(float amount)
{
Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount));
// See https://cs.chromium.org/chromium/src/cc/paint/render_surface_filters.cc
return new Matrix4x4
return new ColorMatrix
{
M11 = 1,
M22 = 1,
M33 = 1,
M11 = 1F,
M22 = 1F,
M33 = 1F,
M44 = amount
};
}
@ -395,25 +409,27 @@ namespace SixLabors.ImageSharp.Processing
/// Other values are linear multipliers on the effect. Values of amount over 1 are allowed, providing super-saturated results
/// </remarks>
/// <param name="amount">The proportion of the conversion. Must be greater than or equal to 0.</param>
/// <returns>The <see cref="Matrix4x4"/></returns>
public static Matrix4x4 CreateSaturateFilter(float amount)
/// <returns>The <see cref="ColorMatrix"/></returns>
public static ColorMatrix CreateSaturateFilter(float amount)
{
Guard.MustBeGreaterThanOrEqualTo(amount, 0, nameof(amount));
// See https://cs.chromium.org/chromium/src/cc/paint/render_surface_filters.cc
return new Matrix4x4
{
M11 = .213F + (.787F * amount),
M12 = .213F - (.213F * amount),
M13 = .213F - (.213F * amount),
M21 = .715F - (.715F * amount),
M22 = .715F + (.285F * amount),
M23 = .715F - (.715F * amount),
M31 = 1F - ((.213F + (.787F * amount)) + (.715F - (.715F * amount))),
M32 = 1F - ((.213F - (.213F * amount)) + (.715F + (.285F * amount))),
M33 = 1F - ((.213F - (.213F * amount)) + (.715F - (.715F * amount))),
M44 = 1
};
ColorMatrix m = default;
m.M11 = .213F + (.787F * amount);
m.M21 = .715F - (.715F * amount);
m.M31 = 1F - (m.M11 + m.M21);
m.M12 = .213F - (.213F * amount);
m.M22 = .715F + (.285F * amount);
m.M32 = 1F - (m.M12 + m.M22);
m.M13 = .213F - (.213F * amount);
m.M23 = .715F - (.715F * amount);
m.M33 = 1F - (m.M13 + m.M23);
m.M44 = 1F;
return m;
}
/// <summary>
@ -421,25 +437,27 @@ namespace SixLabors.ImageSharp.Processing
/// The formula used matches the svg specification. <see href="http://www.w3.org/TR/filter-effects/#sepiaEquivalent"/>
/// </summary>
/// <param name="amount">The proportion of the conversion. Must be between 0 and 1.</param>
/// <returns>The <see cref="Matrix4x4"/></returns>
public static Matrix4x4 CreateSepiaFilter(float amount)
/// <returns>The <see cref="ColorMatrix"/></returns>
public static ColorMatrix CreateSepiaFilter(float amount)
{
Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount));
amount = 1F - amount;
// See https://cs.chromium.org/chromium/src/cc/paint/render_surface_filters.cc
return new Matrix4x4
return new ColorMatrix
{
M11 = .393F + (.607F * amount),
M12 = .349F - (.349F * amount),
M13 = .272F - (.272F * amount),
M21 = .769F - (.769F * amount),
M22 = .686F + (.314F * amount),
M23 = .534F - (.534F * amount),
M31 = .189F - (.189F * amount),
M12 = .349F - (.349F * amount),
M22 = .686F + (.314F * amount),
M32 = .168F - (.168F * amount),
M13 = .272F - (.272F * amount),
M23 = .534F - (.534F * amount),
M33 = .131F + (.869F * amount),
M44 = 1
M44 = 1F
};
}
}

36
src/ImageSharp/Processing/Processors/Filters/FilterProcessor.cs

@ -3,16 +3,16 @@
using System;
using System.Numerics;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Primitives;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Filters
{
/// <summary>
/// Provides methods that accept a <see cref="Matrix4x4"/> matrix to apply free-form filters to images.
/// Provides methods that accept a <see cref="ColorMatrix"/> matrix to apply free-form filters to images.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal class FilterProcessor<TPixel> : ImageProcessor<TPixel>
@ -22,38 +22,36 @@ namespace SixLabors.ImageSharp.Processing.Processors.Filters
/// Initializes a new instance of the <see cref="FilterProcessor{TPixel}"/> class.
/// </summary>
/// <param name="matrix">The matrix used to apply the image filter</param>
public FilterProcessor(Matrix4x4 matrix)
{
this.Matrix = matrix;
}
public FilterProcessor(ColorMatrix matrix) => this.Matrix = matrix;
/// <summary>
/// Gets the <see cref="Matrix4x4"/> used to apply the image filter.
/// Gets the <see cref="ColorMatrix"/> used to apply the image filter.
/// </summary>
public Matrix4x4 Matrix { get; }
public ColorMatrix Matrix { get; }
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration)
{
var interest = Rectangle.Intersect(sourceRectangle, source.Bounds());
int startX = interest.X;
Matrix4x4 matrix = this.Matrix;
ColorMatrix matrix = this.Matrix;
ParallelHelper.IterateRows(
ParallelHelper.IterateRowsWithTempBuffer<Vector4>(
interest,
configuration,
rows =>
(rows, vectorBuffer) =>
{
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> row = source.GetPixelRowSpan(y);
for (int x = interest.X; x < interest.Right; x++)
{
ref TPixel pixel = ref row[x];
var vector = Vector4.Transform(pixel.ToVector4(), matrix);
pixel.FromVector4(vector);
}
Span<Vector4> vectorSpan = vectorBuffer.Span;
int length = vectorSpan.Length;
Span<TPixel> rowSpan = source.GetPixelRowSpan(y).Slice(startX, length);
PixelOperations<TPixel>.Instance.ToVector4(configuration, rowSpan, vectorSpan);
Vector4Utils.Transform(vectorSpan, ref matrix);
PixelOperations<TPixel>.Instance.FromVector4(configuration, vectorSpan, rowSpan);
}
});
}

3
tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs

@ -57,8 +57,7 @@ namespace SixLabors.ImageSharp.Tests
{
using (Image<Rgba32> image = file.CreateImage())
{
var encoder = new PngEncoder { Quantizer = new WuQuantizer(KnownDiffusers.JarvisJudiceNinke, 256), ColorType = PngColorType.Palette };
image.Save($"{path}/{file.FileName}.png", encoder);
image.Save($"{path}/{file.FileName}");
}
}
}

6
tests/ImageSharp.Tests/Helpers/Vector4UtilsTests.cs

@ -11,6 +11,8 @@ namespace SixLabors.ImageSharp.Tests.Helpers
{
public class Vector4UtilsTests
{
private readonly ApproximateFloatComparer ApproximateFloatComparer = new ApproximateFloatComparer(1e-6f);
[Theory]
[InlineData(0)]
[InlineData(1)]
@ -23,7 +25,7 @@ namespace SixLabors.ImageSharp.Tests.Helpers
Vector4Utils.Premultiply(source);
Assert.Equal(expected, source, new ApproximateFloatComparer(1e-6f));
Assert.Equal(expected, source, this.ApproximateFloatComparer);
}
[Theory]
@ -38,7 +40,7 @@ namespace SixLabors.ImageSharp.Tests.Helpers
Vector4Utils.UnPremultiply(source);
Assert.Equal(expected, source, new ApproximateFloatComparer(1e-6f));
Assert.Equal(expected, source, this.ApproximateFloatComparer);
}
}
}

270
tests/ImageSharp.Tests/Primitives/ColorMatrixTests.cs

@ -0,0 +1,270 @@
using System;
using System.Globalization;
using SixLabors.ImageSharp.Primitives;
using SixLabors.ImageSharp.Processing;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Primitives
{
public class ColorMatrixTests
{
private readonly ApproximateFloatComparer ApproximateFloatComparer = new ApproximateFloatComparer(1e-6f);
[Fact]
public void ColorMatrixIdentityIsCorrect()
{
ColorMatrix val = default;
val.M11 = val.M22 = val.M33 = val.M44 = 1F;
Assert.Equal(val, ColorMatrix.Identity, this.ApproximateFloatComparer);
}
[Fact]
public void ColorMatrixCanDetectIdentity()
{
ColorMatrix m = ColorMatrix.Identity;
Assert.True(m.IsIdentity);
m.M12 = 1F;
Assert.False(m.IsIdentity);
}
[Fact]
public void ColorMatrixEquality()
{
ColorMatrix m = KnownFilterMatrices.CreateHueFilter(45F);
ColorMatrix m2 = KnownFilterMatrices.CreateHueFilter(45F);
object obj = m2;
Assert.True(m.Equals(obj));
Assert.True(m.Equals(m2));
Assert.True(m == m2);
Assert.False(m != m2);
}
[Fact]
public void ColorMatrixMultiply()
{
ColorMatrix value1 = this.CreateAllTwos();
ColorMatrix value2 = this.CreateAllThrees();
ColorMatrix m;
// First row
m.M11 = (value1.M11 * value2.M11) + (value1.M12 * value2.M21) + (value1.M13 * value2.M31) + (value1.M14 * value2.M41);
m.M12 = (value1.M11 * value2.M12) + (value1.M12 * value2.M22) + (value1.M13 * value2.M32) + (value1.M14 * value2.M42);
m.M13 = (value1.M11 * value2.M13) + (value1.M12 * value2.M23) + (value1.M13 * value2.M33) + (value1.M14 * value2.M43);
m.M14 = (value1.M11 * value2.M14) + (value1.M12 * value2.M24) + (value1.M13 * value2.M34) + (value1.M14 * value2.M44);
// Second row
m.M21 = (value1.M21 * value2.M11) + (value1.M22 * value2.M21) + (value1.M23 * value2.M31) + (value1.M24 * value2.M41);
m.M22 = (value1.M21 * value2.M12) + (value1.M22 * value2.M22) + (value1.M23 * value2.M32) + (value1.M24 * value2.M42);
m.M23 = (value1.M21 * value2.M13) + (value1.M22 * value2.M23) + (value1.M23 * value2.M33) + (value1.M24 * value2.M43);
m.M24 = (value1.M21 * value2.M14) + (value1.M22 * value2.M24) + (value1.M23 * value2.M34) + (value1.M24 * value2.M44);
// Third row
m.M31 = (value1.M31 * value2.M11) + (value1.M32 * value2.M21) + (value1.M33 * value2.M31) + (value1.M34 * value2.M41);
m.M32 = (value1.M31 * value2.M12) + (value1.M32 * value2.M22) + (value1.M33 * value2.M32) + (value1.M34 * value2.M42);
m.M33 = (value1.M31 * value2.M13) + (value1.M32 * value2.M23) + (value1.M33 * value2.M33) + (value1.M34 * value2.M43);
m.M34 = (value1.M31 * value2.M14) + (value1.M32 * value2.M24) + (value1.M33 * value2.M34) + (value1.M34 * value2.M44);
// Fourth row
m.M41 = (value1.M41 * value2.M11) + (value1.M42 * value2.M21) + (value1.M43 * value2.M31) + (value1.M44 * value2.M41);
m.M42 = (value1.M41 * value2.M12) + (value1.M42 * value2.M22) + (value1.M43 * value2.M32) + (value1.M44 * value2.M42);
m.M43 = (value1.M41 * value2.M13) + (value1.M42 * value2.M23) + (value1.M43 * value2.M33) + (value1.M44 * value2.M43);
m.M44 = (value1.M41 * value2.M14) + (value1.M42 * value2.M24) + (value1.M43 * value2.M34) + (value1.M44 * value2.M44);
// Fifth row
m.M51 = (value1.M51 * value2.M11) + (value1.M52 * value2.M21) + (value1.M53 * value2.M31) + (value1.M54 * value2.M41) + value2.M51;
m.M52 = (value1.M51 * value2.M12) + (value1.M52 * value2.M22) + (value1.M53 * value2.M32) + (value1.M54 * value2.M52) + value2.M52;
m.M53 = (value1.M51 * value2.M13) + (value1.M52 * value2.M23) + (value1.M53 * value2.M33) + (value1.M54 * value2.M53) + value2.M53;
m.M54 = (value1.M51 * value2.M14) + (value1.M52 * value2.M24) + (value1.M53 * value2.M34) + (value1.M54 * value2.M54) + value2.M54;
Assert.Equal(m, value1 * value2, this.ApproximateFloatComparer);
}
[Fact]
public void ColorMatrixMultiplyScalar()
{
ColorMatrix m = this.CreateAllTwos();
Assert.Equal(this.CreateAllFours(), m * 2, this.ApproximateFloatComparer);
}
[Fact]
public void ColorMatrixSubtract()
{
ColorMatrix m = this.CreateAllOnes() + this.CreateAllTwos();
Assert.Equal(this.CreateAllThrees(), m);
}
[Fact]
public void ColorMatrixNegate()
{
ColorMatrix m = this.CreateAllOnes() * -1F;
Assert.Equal(m, -this.CreateAllOnes());
}
[Fact]
public void ColorMatrixAdd()
{
ColorMatrix m = this.CreateAllOnes() + this.CreateAllTwos();
Assert.Equal(this.CreateAllThrees(), m);
}
[Fact]
public void ColorMatrixHashCode()
{
#if NETCOREAPP2_1
ColorMatrix m = KnownFilterMatrices.CreateBrightnessFilter(.5F);
HashCode hash = default;
hash.Add(m.M11);
hash.Add(m.M12);
hash.Add(m.M13);
hash.Add(m.M14);
hash.Add(m.M21);
hash.Add(m.M22);
hash.Add(m.M23);
hash.Add(m.M24);
hash.Add(m.M31);
hash.Add(m.M32);
hash.Add(m.M33);
hash.Add(m.M34);
hash.Add(m.M41);
hash.Add(m.M42);
hash.Add(m.M43);
hash.Add(m.M44);
hash.Add(m.M51);
hash.Add(m.M52);
hash.Add(m.M53);
hash.Add(m.M54);
Assert.Equal(hash.ToHashCode(), m.GetHashCode());
#endif
}
[Fact]
public void ColorMatrixToString()
{
ColorMatrix m = KnownFilterMatrices.CreateBrightnessFilter(.5F);
CultureInfo ci = CultureInfo.CurrentCulture;
string expected = string.Format(ci, "{{ {{M11:{0} M12:{1} M13:{2} M14:{3}}} {{M21:{4} M22:{5} M23:{6} M24:{7}}} {{M31:{8} M32:{9} M33:{10} M34:{11}}} {{M41:{12} M42:{13} M43:{14} M44:{15}}} {{M51:{16} M52:{17} M53:{18} M54:{19}}} }}",
m.M11.ToString(ci), m.M12.ToString(ci), m.M13.ToString(ci), m.M14.ToString(ci),
m.M21.ToString(ci), m.M22.ToString(ci), m.M23.ToString(ci), m.M24.ToString(ci),
m.M31.ToString(ci), m.M32.ToString(ci), m.M33.ToString(ci), m.M34.ToString(ci),
m.M41.ToString(ci), m.M42.ToString(ci), m.M43.ToString(ci), m.M44.ToString(ci),
m.M51.ToString(ci), m.M52.ToString(ci), m.M53.ToString(ci), m.M54.ToString(ci));
Assert.Equal(expected, m.ToString());
}
private ColorMatrix CreateAllOnes()
{
return new ColorMatrix
{
M11 = 1F,
M12 = 1F,
M13 = 1F,
M14 = 1F,
M21 = 1F,
M22 = 1F,
M23 = 1F,
M24 = 1F,
M31 = 1F,
M32 = 1F,
M33 = 1F,
M34 = 1F,
M41 = 1F,
M42 = 1F,
M43 = 1F,
M44 = 1F,
M51 = 1F,
M52 = 1F,
M53 = 1F,
M54 = 1F
};
}
private ColorMatrix CreateAllTwos()
{
return new ColorMatrix
{
M11 = 2F,
M12 = 2F,
M13 = 2F,
M14 = 2F,
M21 = 2F,
M22 = 2F,
M23 = 2F,
M24 = 2F,
M31 = 2F,
M32 = 2F,
M33 = 2F,
M34 = 2F,
M41 = 2F,
M42 = 2F,
M43 = 2F,
M44 = 2F,
M51 = 2F,
M52 = 2F,
M53 = 2F,
M54 = 2F
};
}
private ColorMatrix CreateAllThrees()
{
return new ColorMatrix
{
M11 = 3F,
M12 = 3F,
M13 = 3F,
M14 = 3F,
M21 = 3F,
M22 = 3F,
M23 = 3F,
M24 = 3F,
M31 = 3F,
M32 = 3F,
M33 = 3F,
M34 = 3F,
M41 = 3F,
M42 = 3F,
M43 = 3F,
M44 = 3F,
M51 = 3F,
M52 = 3F,
M53 = 3F,
M54 = 3F
};
}
private ColorMatrix CreateAllFours()
{
return new ColorMatrix
{
M11 = 4F,
M12 = 4F,
M13 = 4F,
M14 = 4F,
M21 = 4F,
M22 = 4F,
M23 = 4F,
M24 = 4F,
M31 = 4F,
M32 = 4F,
M33 = 4F,
M34 = 4F,
M41 = 4F,
M42 = 4F,
M43 = 4F,
M44 = 4F,
M51 = 4F,
M52 = 4F,
M53 = 4F,
M54 = 4F
};
}
}
}

8
tests/ImageSharp.Tests/Processing/Processors/Filters/BrightnessTest.cs

@ -8,10 +8,13 @@ using Xunit;
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects
{
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
[GroupOutput("Filters")]
public class BrightnessTest
{
private readonly ImageComparer imageComparer = ImageComparer.Tolerant(0.007F);
public static readonly TheoryData<float> BrightnessValues
= new TheoryData<float>
{
@ -22,9 +25,6 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects
[Theory]
[WithTestPatternImages(nameof(BrightnessValues), 48, 48, PixelTypes.Rgba32)]
public void ApplyBrightnessFilter<TPixel>(TestImageProvider<TPixel> provider, float value)
where TPixel : struct, IPixel<TPixel>
{
provider.RunValidatingProcessorTest(ctx => ctx.Brightness(value), value);
}
where TPixel : struct, IPixel<TPixel> => provider.RunValidatingProcessorTest(ctx => ctx.Brightness(value), value, this.imageComparer);
}
}

9
tests/ImageSharp.Tests/Processing/Processors/Filters/ColorBlindnessTest.cs

@ -2,17 +2,19 @@
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters
{
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
[GroupOutput("Filters")]
public class ColorBlindnessTest
{
private readonly ImageComparer imageComparer = ImageComparer.Tolerant(0.03F);
public static readonly TheoryData<ColorBlindnessMode> ColorBlindnessFilters
= new TheoryData<ColorBlindnessMode>
{
@ -29,9 +31,6 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters
[Theory]
[WithTestPatternImages(nameof(ColorBlindnessFilters), 48, 48, PixelTypes.Rgba32)]
public void ApplyColorBlindnessFilter<TPixel>(TestImageProvider<TPixel> provider, ColorBlindnessMode colorBlindness)
where TPixel : struct, IPixel<TPixel>
{
provider.RunValidatingProcessorTest(x => x.ColorBlindness(colorBlindness), colorBlindness.ToString());
}
where TPixel : struct, IPixel<TPixel> => provider.RunValidatingProcessorTest(x => x.ColorBlindness(colorBlindness), colorBlindness.ToString(), this.imageComparer);
}
}

20
tests/ImageSharp.Tests/Processing/Processors/Filters/FilterTest.cs

@ -1,16 +1,13 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using SixLabors.Primitives;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters
{
using SixLabors.ImageSharp.Primitives;
using SixLabors.ImageSharp.Processing;
[GroupOutput("Filters")]
@ -25,7 +22,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters
public void ApplyFilter<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
Matrix4x4 m = CreateCombinedTestFilterMatrix();
ColorMatrix m = CreateCombinedTestFilterMatrix();
provider.RunValidatingProcessorTest(x => x.Filter(m), comparer: ValidatorComparer);
}
@ -35,18 +32,17 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters
public void ApplyFilterInBox<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
Matrix4x4 m = CreateCombinedTestFilterMatrix();
ColorMatrix m = CreateCombinedTestFilterMatrix();
provider.RunRectangleConstrainedValidatingProcessorTest((x, b) => x.Filter(m, b), comparer: ValidatorComparer);
}
private static Matrix4x4 CreateCombinedTestFilterMatrix()
private static ColorMatrix CreateCombinedTestFilterMatrix()
{
Matrix4x4 brightness = KnownFilterMatrices.CreateBrightnessFilter(0.9F);
Matrix4x4 hue = KnownFilterMatrices.CreateHueFilter(180F);
Matrix4x4 saturation = KnownFilterMatrices.CreateSaturateFilter(1.5F);
Matrix4x4 m = brightness * hue * saturation;
return m;
ColorMatrix brightness = KnownFilterMatrices.CreateBrightnessFilter(0.9F);
ColorMatrix hue = KnownFilterMatrices.CreateHueFilter(180F);
ColorMatrix saturation = KnownFilterMatrices.CreateSaturateFilter(1.5F);
return brightness * hue * saturation;
}
}

28
tests/ImageSharp.Tests/TestUtilities/ApproximateFloatComparer.cs

@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Numerics;
using SixLabors.ImageSharp.Primitives;
namespace SixLabors.ImageSharp.Tests
{
@ -11,8 +12,9 @@ namespace SixLabors.ImageSharp.Tests
/// </summary>
internal readonly struct ApproximateFloatComparer :
IEqualityComparer<float>,
IEqualityComparer<Vector2>,
IEqualityComparer<Vector4>,
IEqualityComparer<Vector2>
IEqualityComparer<ColorMatrix>
{
private readonly float Epsilon;
@ -20,7 +22,7 @@ namespace SixLabors.ImageSharp.Tests
/// Initializes a new instance of the <see cref="ApproximateFloatComparer"/> class.
/// </summary>
/// <param name="epsilon">The comparison error difference epsilon to use.</param>
public ApproximateFloatComparer(float epsilon = 1f) => this.Epsilon = epsilon;
public ApproximateFloatComparer(float epsilon = 1F) => this.Epsilon = epsilon;
/// <inheritdoc/>
public bool Equals(float x, float y)
@ -34,17 +36,29 @@ namespace SixLabors.ImageSharp.Tests
public int GetHashCode(float obj) => obj.GetHashCode();
/// <inheritdoc/>
public bool Equals(Vector4 a, Vector4 b) => this.Equals(a.X, b.X) && this.Equals(a.Y, b.Y) && this.Equals(a.Z, b.Z) && this.Equals(a.W, b.W);
public bool Equals(Vector2 x, Vector2 y) => this.Equals(x.X, y.X) && this.Equals(x.Y, y.Y);
/// <inheritdoc/>
public int GetHashCode(Vector4 obj) => obj.GetHashCode();
public int GetHashCode(Vector2 obj) => obj.GetHashCode();
/// <inheritdoc/>
public bool Equals(Vector4 x, Vector4 y) => this.Equals(x.X, y.X) && this.Equals(x.Y, y.Y) && this.Equals(x.Z, y.Z) && this.Equals(x.W, y.W);
/// <inheritdoc/>
public bool Equals(Vector2 a, Vector2 b) => this.Equals(a.X, b.X) && this.Equals(a.Y, b.Y);
public int GetHashCode(Vector4 obj) => obj.GetHashCode();
public int GetHashCode(Vector2 obj)
/// <inheritdoc/>
public bool Equals(ColorMatrix x, ColorMatrix y)
{
throw new System.NotImplementedException();
return
this.Equals(x.M11, y.M11) && this.Equals(x.M12, y.M12) && this.Equals(x.M13, y.M13) && this.Equals(x.M14, y.M14)
&& this.Equals(x.M21, y.M21) && this.Equals(x.M22, y.M22) && this.Equals(x.M23, y.M23) && this.Equals(x.M24, y.M24)
&& this.Equals(x.M31, y.M31) && this.Equals(x.M32, y.M32) && this.Equals(x.M33, y.M33) && this.Equals(x.M34, y.M34)
&& this.Equals(x.M41, y.M41) && this.Equals(x.M42, y.M42) && this.Equals(x.M43, y.M43) && this.Equals(x.M44, y.M44)
&& this.Equals(x.M51, y.M51) && this.Equals(x.M52, y.M52) && this.Equals(x.M53, y.M53) && this.Equals(x.M54, y.M54);
}
/// <inheritdoc/>
public int GetHashCode(ColorMatrix obj) => obj.GetHashCode();
}
}

2
tests/Images/External

@ -1 +1 @@
Subproject commit 7ada45bc3484f40e28a50817386ca93f293acd11
Subproject commit 74995302e3c9a5913f1cf09d71b15f888d0ec022
Loading…
Cancel
Save