Browse Source

Merge remote-tracking branch 'origin/main' into bp/oldJpegCompression

pull/2266/head
Brian Popow 4 years ago
parent
commit
10bcfb7171
  1. 7
      src/ImageSharp/Advanced/IRowIntervalOperation{TBuffer}.cs
  2. 7
      src/ImageSharp/Advanced/IRowOperation{TBuffer}.cs
  3. 16
      src/ImageSharp/Advanced/ParallelRowIterator.Wrappers.cs
  4. 10
      src/ImageSharp/Advanced/ParallelRowIterator.cs
  5. 4
      src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
  6. 8
      src/ImageSharp/IO/BufferedReadStream.cs
  7. 5
      src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs
  8. 5
      src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor{TPixel}.cs
  9. 17
      src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs
  10. 6
      src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor{TPixel}.cs
  11. 5
      src/ImageSharp/Processing/Processors/Convolution/Convolution2DRowOperation{TPixel}.cs
  12. 18
      src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs
  13. 104
      src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessorHelpers.cs
  14. 10
      src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs
  15. 7
      src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs
  16. 7
      src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs
  17. 5
      src/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor{TPixel,TDelegate}.cs
  18. 5
      src/ImageSharp/Processing/Processors/Filters/FilterProcessor{TPixel}.cs
  19. 5
      src/ImageSharp/Processing/Processors/Filters/OpaqueProcessor{TPixel}.cs
  20. 46
      src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor.cs
  21. 226
      src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs
  22. 74
      src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs
  23. 64
      src/ImageSharp/Processing/Processors/Normalization/GrayscaleLevelsRowOperation{TPixel}.cs
  24. 6
      src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs
  25. 7
      src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs
  26. 3
      src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs
  27. 5
      src/ImageSharp/Processing/Processors/Overlays/GlowProcessor{TPixel}.cs
  28. 5
      src/ImageSharp/Processing/Processors/Overlays/VignetteProcessor{TPixel}.cs
  29. 5
      src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs
  30. 5
      src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs
  31. 11
      tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs
  32. 6
      tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs
  33. 422
      tests/ImageSharp.Tests/IO/BufferedReadStreamTests.cs
  34. 38
      tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs
  35. 81
      tests/ImageSharp.Tests/Processing/Normalization/MagickCompareTests.cs
  36. 70
      tests/ImageSharp.Tests/Processing/Processors/Convolution/ConvolutionProcessorHelpersTest.cs
  37. 1
      tests/ImageSharp.Tests/TestImages.cs
  38. 3
      tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs
  39. 3
      tests/Images/External/ReferenceOutput/HistogramEqualizationTests/AutoLevel_SeparateChannels_CompareToReferenceOutput_Rgba32_forest_bridge.png
  40. 3
      tests/Images/External/ReferenceOutput/HistogramEqualizationTests/AutoLevel_SynchronizedChannels_CompareToReferenceOutput_Rgba32_forest_bridge.png
  41. 3
      tests/Images/Input/Webp/issues/Issue2243.webp

7
src/ImageSharp/Advanced/IRowIntervalOperation{TBuffer}.cs

@ -12,6 +12,13 @@ namespace SixLabors.ImageSharp.Advanced;
public interface IRowIntervalOperation<TBuffer>
where TBuffer : unmanaged
{
/// <summary>
/// Return the minimal required number of items in the buffer passed on <see cref="Invoke" />.
/// </summary>
/// <param name="bounds">The bounds of the operation.</param>
/// <returns>The required buffer length.</returns>
int GetRequiredBufferLength(Rectangle bounds);
/// <summary>
/// Invokes the method passing the row interval and a buffer.
/// </summary>

7
src/ImageSharp/Advanced/IRowOperation{TBuffer}.cs

@ -10,6 +10,13 @@ namespace SixLabors.ImageSharp.Advanced;
public interface IRowOperation<TBuffer>
where TBuffer : unmanaged
{
/// <summary>
/// Return the minimal required number of items in the buffer passed on <see cref="Invoke" />.
/// </summary>
/// <param name="bounds">The bounds of the operation.</param>
/// <returns>The required buffer length.</returns>
int GetRequiredBufferLength(Rectangle bounds);
/// <summary>
/// Invokes the method passing the row and a buffer.
/// </summary>

16
src/ImageSharp/Advanced/ParallelRowIterator.Wrappers.cs

@ -63,7 +63,7 @@ public static partial class ParallelRowIterator
private readonly int minY;
private readonly int maxY;
private readonly int stepY;
private readonly int width;
private readonly int bufferLength;
private readonly MemoryAllocator allocator;
private readonly T action;
@ -72,14 +72,14 @@ public static partial class ParallelRowIterator
int minY,
int maxY,
int stepY,
int width,
int bufferLength,
MemoryAllocator allocator,
in T action)
{
this.minY = minY;
this.maxY = maxY;
this.stepY = stepY;
this.width = width;
this.bufferLength = bufferLength;
this.allocator = allocator;
this.action = action;
}
@ -96,7 +96,7 @@ public static partial class ParallelRowIterator
int yMax = Math.Min(yMin + this.stepY, this.maxY);
using IMemoryOwner<TBuffer> buffer = this.allocator.Allocate<TBuffer>(this.width);
using IMemoryOwner<TBuffer> buffer = this.allocator.Allocate<TBuffer>(this.bufferLength);
Span<TBuffer> span = buffer.Memory.Span;
@ -153,7 +153,7 @@ public static partial class ParallelRowIterator
private readonly int minY;
private readonly int maxY;
private readonly int stepY;
private readonly int width;
private readonly int bufferLength;
private readonly MemoryAllocator allocator;
private readonly T operation;
@ -162,14 +162,14 @@ public static partial class ParallelRowIterator
int minY,
int maxY,
int stepY,
int width,
int bufferLength,
MemoryAllocator allocator,
in T operation)
{
this.minY = minY;
this.maxY = maxY;
this.stepY = stepY;
this.width = width;
this.bufferLength = bufferLength;
this.allocator = allocator;
this.operation = operation;
}
@ -187,7 +187,7 @@ public static partial class ParallelRowIterator
int yMax = Math.Min(yMin + this.stepY, this.maxY);
var rows = new RowInterval(yMin, yMax);
using IMemoryOwner<TBuffer> buffer = this.allocator.Allocate<TBuffer>(this.width);
using IMemoryOwner<TBuffer> buffer = this.allocator.Allocate<TBuffer>(this.bufferLength);
Unsafe.AsRef(in this.operation).Invoke(in rows, buffer.Memory.Span);
}

10
src/ImageSharp/Advanced/ParallelRowIterator.cs

@ -118,11 +118,12 @@ public static partial class ParallelRowIterator
int maxSteps = DivideCeil(width * height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
MemoryAllocator allocator = parallelSettings.MemoryAllocator;
int bufferLength = Unsafe.AsRef(operation).GetRequiredBufferLength(rectangle);
// Avoid TPL overhead in this trivial case:
if (numOfSteps == 1)
{
using IMemoryOwner<TBuffer> buffer = allocator.Allocate<TBuffer>(width);
using IMemoryOwner<TBuffer> buffer = allocator.Allocate<TBuffer>(bufferLength);
Span<TBuffer> span = buffer.Memory.Span;
for (int y = top; y < bottom; y++)
@ -135,7 +136,7 @@ public static partial class ParallelRowIterator
int verticalStep = DivideCeil(height, numOfSteps);
var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = numOfSteps };
var wrappingOperation = new RowOperationWrapper<T, TBuffer>(top, bottom, verticalStep, width, allocator, in operation);
var wrappingOperation = new RowOperationWrapper<T, TBuffer>(top, bottom, verticalStep, bufferLength, allocator, in operation);
Parallel.For(
0,
@ -244,12 +245,13 @@ public static partial class ParallelRowIterator
int maxSteps = DivideCeil(width * height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
MemoryAllocator allocator = parallelSettings.MemoryAllocator;
int bufferLength = Unsafe.AsRef(operation).GetRequiredBufferLength(rectangle);
// Avoid TPL overhead in this trivial case:
if (numOfSteps == 1)
{
var rows = new RowInterval(top, bottom);
using IMemoryOwner<TBuffer> buffer = allocator.Allocate<TBuffer>(width);
using IMemoryOwner<TBuffer> buffer = allocator.Allocate<TBuffer>(bufferLength);
Unsafe.AsRef(operation).Invoke(in rows, buffer.Memory.Span);
@ -258,7 +260,7 @@ public static partial class ParallelRowIterator
int verticalStep = DivideCeil(height, numOfSteps);
var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = numOfSteps };
var wrappingOperation = new RowIntervalOperationWrapper<T, TBuffer>(top, bottom, verticalStep, width, allocator, in operation);
var wrappingOperation = new RowIntervalOperationWrapper<T, TBuffer>(top, bottom, verticalStep, bufferLength, allocator, in operation);
Parallel.For(
0,

4
src/ImageSharp/Formats/Webp/WebpDecoderCore.cs

@ -221,7 +221,9 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
}
else
{
WebpThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header");
// Ignore unknown chunks.
uint chunkSize = this.ReadChunkSize();
this.currentStream.Skip((int)chunkSize);
}
}

8
src/ImageSharp/IO/BufferedReadStream.cs

@ -49,7 +49,7 @@ internal sealed class BufferedReadStream : Stream
this.BaseStream = stream;
this.Length = stream.Length;
this.Position = (int)stream.Position;
this.readerPosition = stream.Position;
this.BufferSize = configuration.StreamProcessingBufferSize;
this.maxBufferIndex = this.BufferSize - 1;
this.readBuffer = ArrayPool<byte>.Shared.Rent(this.BufferSize);
@ -96,9 +96,8 @@ internal sealed class BufferedReadStream : Stream
else
{
// Base stream seek will throw for us if invalid.
this.BaseStream.Seek(value, SeekOrigin.Begin);
this.readerPosition = value;
this.readBufferIndex = this.BufferSize;
this.FillReadBuffer();
}
}
}
@ -147,6 +146,7 @@ internal sealed class BufferedReadStream : Stream
}
this.readerPosition++;
unsafe
{
return this.pinnedReadBuffer[this.readBufferIndex++];
@ -202,7 +202,7 @@ internal sealed class BufferedReadStream : Stream
if (this.readerPosition != baseStream.Position)
{
baseStream.Seek(this.readerPosition, SeekOrigin.Begin);
this.readerPosition = (int)baseStream.Position;
this.readerPosition = baseStream.Position;
}
// Reset to trigger full read on next attempt.

5
src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs

@ -85,6 +85,11 @@ internal class AdaptiveThresholdProcessor<TPixel> : ImageProcessor<TPixel>
this.clusterSize = clusterSize;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public int GetRequiredBufferLength(Rectangle bounds)
=> bounds.Width;
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y, Span<L8> span)

5
src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor{TPixel}.cs

@ -86,6 +86,11 @@ internal class BinaryThresholdProcessor<TPixel> : ImageProcessor<TPixel>
this.configuration = configuration;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public int GetRequiredBufferLength(Rectangle bounds)
=> bounds.Width;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Invoke(int y, Span<Rgb24> span)

17
src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs

@ -220,6 +220,11 @@ internal class BokehBlurProcessor<TPixel> : ImageProcessor<TPixel>
this.configuration = configuration;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public int GetRequiredBufferLength(Rectangle bounds)
=> bounds.Width;
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y, Span<Vector4> span)
@ -289,6 +294,11 @@ internal class BokehBlurProcessor<TPixel> : ImageProcessor<TPixel>
this.gamma = gamma;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public int GetRequiredBufferLength(Rectangle bounds)
=> bounds.Width;
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y, Span<Vector4> span)
@ -329,6 +339,13 @@ internal class BokehBlurProcessor<TPixel> : ImageProcessor<TPixel>
this.configuration = configuration;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public int GetRequiredBufferLength(Rectangle bounds)
{
return bounds.Width;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y, Span<Vector4> span)

6
src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor{TPixel}.cs

@ -64,10 +64,6 @@ internal class Convolution2DProcessor<TPixel> : ImageProcessor<TPixel>
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
// We use a rectangle 3x the interest width to allocate a buffer big enough
// for source and target bulk pixel conversion.
var operationBounds = new Rectangle(interest.X, interest.Y, interest.Width * 3, interest.Height);
using (var map = new KernelSamplingMap(allocator))
{
// Since the kernel sizes are identical we can use a single map.
@ -85,7 +81,7 @@ internal class Convolution2DProcessor<TPixel> : ImageProcessor<TPixel>
ParallelRowIterator.IterateRows<Convolution2DRowOperation<TPixel>, Vector4>(
this.Configuration,
operationBounds,
interest,
in operation);
}

5
src/ImageSharp/Processing/Processors/Convolution/Convolution2DRowOperation{TPixel}.cs

@ -46,6 +46,11 @@ internal readonly struct Convolution2DRowOperation<TPixel> : IRowOperation<Vecto
this.preserveAlpha = preserveAlpha;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public int GetRequiredBufferLength(Rectangle bounds)
=> 3 * bounds.Width;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Invoke(int y, Span<Vector4> span)

18
src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs

@ -70,10 +70,6 @@ internal class Convolution2PassProcessor<TPixel> : ImageProcessor<TPixel>
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
// We use a rectangle 2x the interest width to allocate a buffer big enough
// for source and target bulk pixel conversion.
var operationBounds = new Rectangle(interest.X, interest.Y, interest.Width * 2, interest.Height);
// We can create a single sampling map with the size as if we were using the non separated 2D kernel
// the two 1D kernels represent, and reuse it across both convolution steps, like in the bokeh blur.
using var mapXY = new KernelSamplingMap(this.Configuration.MemoryAllocator);
@ -92,7 +88,7 @@ internal class Convolution2PassProcessor<TPixel> : ImageProcessor<TPixel>
ParallelRowIterator.IterateRows<HorizontalConvolutionRowOperation, Vector4>(
this.Configuration,
operationBounds,
interest,
in horizontalOperation);
// Vertical convolution
@ -107,7 +103,7 @@ internal class Convolution2PassProcessor<TPixel> : ImageProcessor<TPixel>
ParallelRowIterator.IterateRows<VerticalConvolutionRowOperation, Vector4>(
this.Configuration,
operationBounds,
interest,
in verticalOperation);
}
@ -143,6 +139,11 @@ internal class Convolution2PassProcessor<TPixel> : ImageProcessor<TPixel>
this.preserveAlpha = preserveAlpha;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public int GetRequiredBufferLength(Rectangle bounds)
=> 2 * bounds.Width;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Invoke(int y, Span<Vector4> span)
@ -304,6 +305,11 @@ internal class Convolution2PassProcessor<TPixel> : ImageProcessor<TPixel>
this.preserveAlpha = preserveAlpha;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public int GetRequiredBufferLength(Rectangle bounds)
=> 2 * bounds.Width;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Invoke(int y, Span<Vector4> span)

104
src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessorHelpers.cs

@ -9,6 +9,7 @@ internal static class ConvolutionProcessorHelpers
/// Kernel radius is calculated using the minimum viable value.
/// See <see href="http://chemaguerra.com/gaussian-filter-radius/"/>.
/// </summary>
/// <param name="sigma">The weight of the blur.</param>
internal static int GetDefaultGaussianRadius(float sigma)
=> (int)MathF.Ceiling(sigma * 3);
@ -16,9 +17,11 @@ internal static class ConvolutionProcessorHelpers
/// Create a 1 dimensional Gaussian kernel using the Gaussian G(x) function.
/// </summary>
/// <returns>The convolution kernel.</returns>
/// <param name="size">The kernel size.</param>
/// <param name="weight">The weight of the blur.</param>
internal static float[] CreateGaussianBlurKernel(int size, float weight)
{
var kernel = new float[size];
float[] kernel = new float[size];
float sum = 0F;
float midpoint = (size - 1) / 2F;
@ -44,9 +47,11 @@ internal static class ConvolutionProcessorHelpers
/// Create a 1 dimensional Gaussian kernel using the Gaussian G(x) function
/// </summary>
/// <returns>The convolution kernel.</returns>
/// <param name="size">The kernel size.</param>
/// <param name="weight">The weight of the blur.</param>
internal static float[] CreateGaussianSharpenKernel(int size, float weight)
{
var kernel = new float[size];
float[] kernel = new float[size];
float sum = 0;
@ -83,4 +88,99 @@ internal static class ConvolutionProcessorHelpers
return kernel;
}
/// <summary>
/// Checks whether or not a given NxM matrix is linearly separable, and if so, it extracts the separable components.
/// These would be two 1D vectors, <paramref name="row"/> of size N and <paramref name="column"/> of size M.
/// This algorithm runs in O(NM).
/// </summary>
/// <param name="matrix">The input 2D matrix to analyze.</param>
/// <param name="row">The resulting 1D row vector, if possible.</param>
/// <param name="column">The resulting 1D column vector, if possible.</param>
/// <returns>Whether or not <paramref name="matrix"/> was linearly separable.</returns>
public static bool TryGetLinearlySeparableComponents(this DenseMatrix<float> matrix, out float[] row, out float[] column)
{
int height = matrix.Rows;
int width = matrix.Columns;
float[] tempX = new float[width];
float[] tempY = new float[height];
// This algorithm checks whether the input matrix is linearly separable and extracts two
// 1D components if possible. Note that for a given NxM matrix that is linearly separable,
// there exists an infinite number of possible solutions to the system of linear equations
// representing the possible 1D components that can produce the input matrix as a product.
// Let's assume we have a 3x3 input matrix to describe the logic. We have the following:
//
// | m11, m12, m13 | | c1 |
// M = | m21, m22, m23 |, and we want to find: R = | r1, r2, r3 | and C = | c2 |.
// | m31, m32, m33 | | c3 |
//
// We essentially get the following system of linear equations to solve:
//
// / a11 = r1c1
// | a12 = r2c1
// | a13 = r3c1
// | a21 = r1c2 a11 a12 a13 a11 a12 a13
// / a22 = r2c2, which gives us: ----- = ----- = ----- and ----- = ----- = -----.
// \ a23 = r3c2 a21 a22 a23 a31 a32 a33
// | a31 = r1c3
// | a32 = r2c3
// \ a33 = r3c3
//
// As we said, there are infinite solutions to this problem (provided the input matrix is in
// fact linearly separable), but we can look at the equalities above to find a way to define
// one specific solution that is very easy to calculate (and that is equivalent to all others
// anyway). In particular, we can see that in order for it to be linearly separable, the matrix
// needs to have each row linearly dependent on each other. That is, its rank is just 1. This
// means that we can express the whole matrix as a function of one row vector (any of the rows
// in the matrix), and a column vector that represents the ratio of each element in a given column
// j with the corresponding j-th item in the reference row. This same procedure extends naturally
// to lineary separable 2D matrices of any size, too. So we end up with the following generalized
// solution for a matrix M of size NxN (or MxN, that works too) and the R and C vectors:
//
// | m11, m12, m13, ..., m1N | | m11/m11 |
// | m21, m22, m23, ..., m2N | | m21/m11 |
// M = | m31, m32, m33, ..., m3N |, R = | m11, m12, m13, ..., m1N |, C = | m31/m11 |.
// | ... ... ... ... ... | | ... |
// | mN1, mN2, mN3, ..., mNN | | mN1/m11 |
//
// So what this algorithm does is just the following:
// 1) It calculates the C[i] value for each i-th row.
// 2) It checks that every j-th item in the row respects C[i] = M[i, j] / M[0, j]. If this is
// not true for any j-th item in any i-th row, then the matrix is not linearly separable.
// 3) It sets items in R and C to the values detailed above if the validation passed.
for (int y = 1; y < height; y++)
{
float ratio = matrix[y, 0] / matrix[0, 0];
for (int x = 1; x < width; x++)
{
if (Math.Abs(ratio - (matrix[y, x] / matrix[0, x])) > 0.0001f)
{
row = null;
column = null;
return false;
}
}
tempY[y] = ratio;
}
// The first row is used as a reference, to the ratio is just 1
tempY[0] = 1;
// The row component is simply the reference row in the input matrix.
// In this case, we're just using the first one for simplicity.
for (int x = 0; x < width; x++)
{
tempX[x] = matrix[0, x];
}
row = tempX;
column = tempY;
return true;
}
}

10
src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs

@ -57,9 +57,6 @@ internal class ConvolutionProcessor<TPixel> : ImageProcessor<TPixel>
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
// We use a rectangle 2x the interest width to allocate a buffer big enough
// for source and target bulk pixel conversion.
var operationBounds = new Rectangle(interest.X, interest.Y, interest.Width * 2, interest.Height);
using (var map = new KernelSamplingMap(allocator))
{
map.BuildSamplingOffsetMap(this.KernelXY, interest);
@ -67,7 +64,7 @@ internal class ConvolutionProcessor<TPixel> : ImageProcessor<TPixel>
var operation = new RowOperation(interest, targetPixels, source.PixelBuffer, map, this.KernelXY, this.Configuration, this.PreserveAlpha);
ParallelRowIterator.IterateRows<RowOperation, Vector4>(
this.Configuration,
operationBounds,
interest,
in operation);
}
@ -106,6 +103,11 @@ internal class ConvolutionProcessor<TPixel> : ImageProcessor<TPixel>
this.preserveAlpha = preserveAlpha;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public int GetRequiredBufferLength(Rectangle bounds)
=> 2 * bounds.Width;
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y, Span<Vector4> span)

7
src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs

@ -31,11 +31,6 @@ internal sealed class MedianBlurProcessor<TPixel> : ImageProcessor<TPixel>
Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
// We use a rectangle with width set wider, to allocate a buffer big enough
// for kernel source, channel buffers, source rows and target bulk pixel conversion.
int operationWidth = (2 * kernelSize * kernelSize) + interest.Width + (kernelSize * interest.Width);
Rectangle operationBounds = new(interest.X, interest.Y, operationWidth, interest.Height);
using KernelSamplingMap map = new(this.Configuration.MemoryAllocator);
map.BuildSamplingOffsetMap(kernelSize, kernelSize, interest, this.definition.BorderWrapModeX, this.definition.BorderWrapModeY);
@ -50,7 +45,7 @@ internal sealed class MedianBlurProcessor<TPixel> : ImageProcessor<TPixel>
ParallelRowIterator.IterateRows<MedianRowOperation<TPixel>, Vector4>(
this.Configuration,
operationBounds,
interest,
in operation);
Buffer2D<TPixel>.SwapOrCopyContent(source.PixelBuffer, targetPixels);

7
src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs

@ -43,9 +43,14 @@ internal readonly struct MedianRowOperation<TPixel> : IRowOperation<Vector4>
this.wChannelStart = this.zChannelStart + kernelCount;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public int GetRequiredBufferLength(Rectangle bounds)
=> (2 * this.kernelSize * this.kernelSize) + bounds.Width + (this.kernelSize * bounds.Width);
public void Invoke(int y, Span<Vector4> span)
{
// Span has kernelSize^2 followed by bound width.
// Span has kernelSize^2 twice, then bound width followed by kernelsize * bounds width.
int boundsX = this.bounds.X;
int boundsWidth = this.bounds.Width;
int kernelCount = this.kernelSize * this.kernelSize;

5
src/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor{TPixel,TDelegate}.cs

@ -83,6 +83,11 @@ internal sealed class PixelRowDelegateProcessor<TPixel, TDelegate> : ImageProces
this.rowProcessor = rowProcessor;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public int GetRequiredBufferLength(Rectangle bounds)
=> bounds.Width;
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y, Span<Vector4> span)

5
src/ImageSharp/Processing/Processors/Filters/FilterProcessor{TPixel}.cs

@ -66,6 +66,11 @@ internal class FilterProcessor<TPixel> : ImageProcessor<TPixel>
this.configuration = configuration;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public int GetRequiredBufferLength(Rectangle bounds)
=> bounds.Width;
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y, Span<Vector4> span)

5
src/ImageSharp/Processing/Processors/Filters/OpaqueProcessor{TPixel}.cs

@ -46,6 +46,11 @@ internal sealed class OpaqueProcessor<TPixel> : ImageProcessor<TPixel>
this.bounds = bounds;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public int GetRequiredBufferLength(Rectangle bounds)
=> bounds.Width;
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y, Span<Vector4> span)

46
src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor.cs

@ -0,0 +1,46 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Processing.Processors.Normalization;
/// <summary>
/// Applies a luminance histogram equilization to the image.
/// </summary>
public class AutoLevelProcessor : HistogramEqualizationProcessor
{
/// <summary>
/// Initializes a new instance of the <see cref="AutoLevelProcessor"/> class.
/// It uses the exact minimum and maximum values found in the luminance channel, as the BlackPoint and WhitePoint to linearly stretch the colors
/// (and histogram) of the image.
/// </summary>
/// <param name="luminanceLevels">The number of different luminance levels. Typical values are 256 for 8-bit grayscale images
/// or 65536 for 16-bit grayscale images.</param>
/// <param name="clipHistogram">Indicating whether to clip the histogram bins at a specific value.</param>
/// <param name="clipLimit">The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value.</param>
/// <param name="syncChannels">Whether to apply a synchronized luminance value to each color channel.</param>
public AutoLevelProcessor(
int luminanceLevels,
bool clipHistogram,
int clipLimit,
bool syncChannels)
: base(luminanceLevels, clipHistogram, clipLimit)
{
this.SyncChannels = syncChannels;
}
/// <summary>
/// Gets a value indicating whether to apply a synchronized luminance value to each color channel.
/// </summary>
public bool SyncChannels { get; }
/// <inheritdoc />
public override IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>(Configuration configuration, Image<TPixel> source, Rectangle sourceRectangle)
=> new AutoLevelProcessor<TPixel>(
configuration,
this.LuminanceLevels,
this.ClipHistogram,
this.ClipLimit,
this.SyncChannels,
source,
sourceRectangle);
}

226
src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs

@ -0,0 +1,226 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Normalization;
/// <summary>
/// Applies a luminance histogram equalization to the image.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal class AutoLevelProcessor<TPixel> : HistogramEqualizationProcessor<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
/// <summary>
/// Initializes a new instance of the <see cref="AutoLevelProcessor{TPixel}"/> class.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="luminanceLevels">
/// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images
/// or 65536 for 16-bit grayscale images.
/// </param>
/// <param name="clipHistogram">Indicating whether to clip the histogram bins at a specific value.</param>
/// <param name="clipLimit">The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value.</param>
/// <param name="source">The source <see cref="Image{TPixel}"/> for the current processor instance.</param>
/// <param name="sourceRectangle">The source area to process for the current processor instance.</param>
/// <param name="syncChannels">Whether to apply a synchronized luminance value to each color channel.</param>
public AutoLevelProcessor(
Configuration configuration,
int luminanceLevels,
bool clipHistogram,
int clipLimit,
bool syncChannels,
Image<TPixel> source,
Rectangle sourceRectangle)
: base(configuration, luminanceLevels, clipHistogram, clipLimit, source, sourceRectangle)
{
this.SyncChannels = syncChannels;
}
/// <summary>
/// Gets a value indicating whether to apply a synchronized luminance value to each color channel.
/// </summary>
private bool SyncChannels { get; }
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source)
{
MemoryAllocator memoryAllocator = this.Configuration.MemoryAllocator;
int numberOfPixels = source.Width * source.Height;
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
using IMemoryOwner<int> histogramBuffer = memoryAllocator.Allocate<int>(this.LuminanceLevels, AllocationOptions.Clean);
// Build the histogram of the grayscale levels.
var grayscaleOperation = new GrayscaleLevelsRowOperation<TPixel>(this.Configuration, interest, histogramBuffer, source.PixelBuffer, this.LuminanceLevels);
ParallelRowIterator.IterateRows<GrayscaleLevelsRowOperation<TPixel>, Vector4>(
this.Configuration,
interest,
in grayscaleOperation);
Span<int> histogram = histogramBuffer.GetSpan();
if (this.ClipHistogramEnabled)
{
this.ClipHistogram(histogram, this.ClipLimit);
}
using IMemoryOwner<int> cdfBuffer = memoryAllocator.Allocate<int>(this.LuminanceLevels, AllocationOptions.Clean);
// Calculate the cumulative distribution function, which will map each input pixel to a new value.
int cdfMin = CalculateCdf(
ref MemoryMarshal.GetReference(cdfBuffer.GetSpan()),
ref MemoryMarshal.GetReference(histogram),
histogram.Length - 1);
float numberOfPixelsMinusCdfMin = numberOfPixels - cdfMin;
if (this.SyncChannels)
{
var cdfOperation = new SynchronizedChannelsRowOperation(this.Configuration, interest, cdfBuffer, source.PixelBuffer, this.LuminanceLevels, numberOfPixelsMinusCdfMin);
ParallelRowIterator.IterateRows<SynchronizedChannelsRowOperation, Vector4>(
this.Configuration,
interest,
in cdfOperation);
}
else
{
var cdfOperation = new SeperateChannelsRowOperation(this.Configuration, interest, cdfBuffer, source.PixelBuffer, this.LuminanceLevels, numberOfPixelsMinusCdfMin);
ParallelRowIterator.IterateRows<SeperateChannelsRowOperation, Vector4>(
this.Configuration,
interest,
in cdfOperation);
}
}
/// <summary>
/// A <see langword="struct"/> implementing the cdf logic for synchronized color channels.
/// </summary>
private readonly struct SynchronizedChannelsRowOperation : IRowOperation<Vector4>
{
private readonly Configuration configuration;
private readonly Rectangle bounds;
private readonly IMemoryOwner<int> cdfBuffer;
private readonly Buffer2D<TPixel> source;
private readonly int luminanceLevels;
private readonly float numberOfPixelsMinusCdfMin;
[MethodImpl(InliningOptions.ShortMethod)]
public SynchronizedChannelsRowOperation(
Configuration configuration,
Rectangle bounds,
IMemoryOwner<int> cdfBuffer,
Buffer2D<TPixel> source,
int luminanceLevels,
float numberOfPixelsMinusCdfMin)
{
this.configuration = configuration;
this.bounds = bounds;
this.cdfBuffer = cdfBuffer;
this.source = source;
this.luminanceLevels = luminanceLevels;
this.numberOfPixelsMinusCdfMin = numberOfPixelsMinusCdfMin;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public int GetRequiredBufferLength(Rectangle bounds) => bounds.Width;
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y, Span<Vector4> span)
{
Span<Vector4> vectorBuffer = span.Slice(0, this.bounds.Width);
ref Vector4 vectorRef = ref MemoryMarshal.GetReference(vectorBuffer);
ref int cdfBase = ref MemoryMarshal.GetReference(this.cdfBuffer.GetSpan());
var sourceAccess = new PixelAccessor<TPixel>(this.source);
int levels = this.luminanceLevels;
float noOfPixelsMinusCdfMin = this.numberOfPixelsMinusCdfMin;
Span<TPixel> pixelRow = sourceAccess.GetRowSpan(y).Slice(this.bounds.X, this.bounds.Width);
PixelOperations<TPixel>.Instance.ToVector4(this.configuration, pixelRow, vectorBuffer);
for (int x = 0; x < this.bounds.Width; x++)
{
var vector = Unsafe.Add(ref vectorRef, x);
int luminance = ColorNumerics.GetBT709Luminance(ref vector, levels);
float scaledLuminance = Unsafe.Add(ref cdfBase, luminance) / noOfPixelsMinusCdfMin;
float scalingFactor = scaledLuminance * levels / luminance;
Vector4 scaledVector = new Vector4(scalingFactor * vector.X, scalingFactor * vector.Y, scalingFactor * vector.Z, vector.W);
Unsafe.Add(ref vectorRef, x) = scaledVector;
}
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, vectorBuffer, pixelRow);
}
}
/// <summary>
/// A <see langword="struct"/> implementing the cdf logic for separate color channels.
/// </summary>
private readonly struct SeperateChannelsRowOperation : IRowOperation<Vector4>
{
private readonly Configuration configuration;
private readonly Rectangle bounds;
private readonly IMemoryOwner<int> cdfBuffer;
private readonly Buffer2D<TPixel> source;
private readonly int luminanceLevels;
private readonly float numberOfPixelsMinusCdfMin;
[MethodImpl(InliningOptions.ShortMethod)]
public SeperateChannelsRowOperation(
Configuration configuration,
Rectangle bounds,
IMemoryOwner<int> cdfBuffer,
Buffer2D<TPixel> source,
int luminanceLevels,
float numberOfPixelsMinusCdfMin)
{
this.configuration = configuration;
this.bounds = bounds;
this.cdfBuffer = cdfBuffer;
this.source = source;
this.luminanceLevels = luminanceLevels;
this.numberOfPixelsMinusCdfMin = numberOfPixelsMinusCdfMin;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public int GetRequiredBufferLength(Rectangle bounds) => bounds.Width;
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y, Span<Vector4> span)
{
Span<Vector4> vectorBuffer = span.Slice(0, this.bounds.Width);
ref Vector4 vectorRef = ref MemoryMarshal.GetReference(vectorBuffer);
ref int cdfBase = ref MemoryMarshal.GetReference(this.cdfBuffer.GetSpan());
var sourceAccess = new PixelAccessor<TPixel>(this.source);
int levelsMinusOne = this.luminanceLevels - 1;
float noOfPixelsMinusCdfMin = this.numberOfPixelsMinusCdfMin;
Span<TPixel> pixelRow = sourceAccess.GetRowSpan(y);
PixelOperations<TPixel>.Instance.ToVector4(this.configuration, pixelRow, vectorBuffer);
for (int x = 0; x < this.bounds.Width; x++)
{
var vector = Unsafe.Add(ref vectorRef, x) * levelsMinusOne;
uint originalX = (uint)MathF.Round(vector.X);
float scaledX = Unsafe.Add(ref cdfBase, originalX) / noOfPixelsMinusCdfMin;
uint originalY = (uint)MathF.Round(vector.Y);
float scaledY = Unsafe.Add(ref cdfBase, originalY) / noOfPixelsMinusCdfMin;
uint originalZ = (uint)MathF.Round(vector.Z);
float scaledZ = Unsafe.Add(ref cdfBase, originalZ) / noOfPixelsMinusCdfMin;
Unsafe.Add(ref vectorRef, x) = new Vector4(scaledX, scaledY, scaledZ, vector.W);
}
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, vectorBuffer, pixelRow);
}
}
}

74
src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs

@ -51,8 +51,8 @@ internal class GlobalHistogramEqualizationProcessor<TPixel> : HistogramEqualizat
using IMemoryOwner<int> histogramBuffer = memoryAllocator.Allocate<int>(this.LuminanceLevels, AllocationOptions.Clean);
// Build the histogram of the grayscale levels.
var grayscaleOperation = new GrayscaleLevelsRowOperation(interest, histogramBuffer, source.PixelBuffer, this.LuminanceLevels);
ParallelRowIterator.IterateRows(
var grayscaleOperation = new GrayscaleLevelsRowOperation<TPixel>(this.Configuration, interest, histogramBuffer, source.PixelBuffer, this.LuminanceLevels);
ParallelRowIterator.IterateRows<GrayscaleLevelsRowOperation<TPixel>, Vector4>(
this.Configuration,
interest,
in grayscaleOperation);
@ -74,59 +74,19 @@ internal class GlobalHistogramEqualizationProcessor<TPixel> : HistogramEqualizat
float numberOfPixelsMinusCdfMin = numberOfPixels - cdfMin;
// Apply the cdf to each pixel of the image
var cdfOperation = new CdfApplicationRowOperation(interest, cdfBuffer, source.PixelBuffer, this.LuminanceLevels, numberOfPixelsMinusCdfMin);
ParallelRowIterator.IterateRows(
var cdfOperation = new CdfApplicationRowOperation(this.Configuration, interest, cdfBuffer, source.PixelBuffer, this.LuminanceLevels, numberOfPixelsMinusCdfMin);
ParallelRowIterator.IterateRows<CdfApplicationRowOperation, Vector4>(
this.Configuration,
interest,
in cdfOperation);
}
/// <summary>
/// A <see langword="struct"/> implementing the grayscale levels logic for <see cref="GlobalHistogramEqualizationProcessor{TPixel}"/>.
/// </summary>
private readonly struct GrayscaleLevelsRowOperation : IRowOperation
{
private readonly Rectangle bounds;
private readonly IMemoryOwner<int> histogramBuffer;
private readonly Buffer2D<TPixel> source;
private readonly int luminanceLevels;
[MethodImpl(InliningOptions.ShortMethod)]
public GrayscaleLevelsRowOperation(
Rectangle bounds,
IMemoryOwner<int> histogramBuffer,
Buffer2D<TPixel> source,
int luminanceLevels)
{
this.bounds = bounds;
this.histogramBuffer = histogramBuffer;
this.source = source;
this.luminanceLevels = luminanceLevels;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y)
{
ref int histogramBase = ref MemoryMarshal.GetReference(this.histogramBuffer.GetSpan());
Span<TPixel> pixelRow = this.source.DangerousGetRowSpan(y);
int levels = this.luminanceLevels;
for (int x = 0; x < this.bounds.Width; x++)
{
// TODO: We should bulk convert here.
var vector = pixelRow[x].ToVector4();
int luminance = ColorNumerics.GetBT709Luminance(ref vector, levels);
Interlocked.Increment(ref Unsafe.Add(ref histogramBase, luminance));
}
}
}
/// <summary>
/// A <see langword="struct"/> implementing the cdf application levels logic for <see cref="GlobalHistogramEqualizationProcessor{TPixel}"/>.
/// </summary>
private readonly struct CdfApplicationRowOperation : IRowOperation
private readonly struct CdfApplicationRowOperation : IRowOperation<Vector4>
{
private readonly Configuration configuration;
private readonly Rectangle bounds;
private readonly IMemoryOwner<int> cdfBuffer;
private readonly Buffer2D<TPixel> source;
@ -135,12 +95,14 @@ internal class GlobalHistogramEqualizationProcessor<TPixel> : HistogramEqualizat
[MethodImpl(InliningOptions.ShortMethod)]
public CdfApplicationRowOperation(
Configuration configuration,
Rectangle bounds,
IMemoryOwner<int> cdfBuffer,
Buffer2D<TPixel> source,
int luminanceLevels,
float numberOfPixelsMinusCdfMin)
{
this.configuration = configuration;
this.bounds = bounds;
this.cdfBuffer = cdfBuffer;
this.source = source;
@ -150,22 +112,30 @@ internal class GlobalHistogramEqualizationProcessor<TPixel> : HistogramEqualizat
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y)
public int GetRequiredBufferLength(Rectangle bounds) => bounds.Width;
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y, Span<Vector4> span)
{
Span<Vector4> vectorBuffer = span.Slice(0, this.bounds.Width);
ref Vector4 vectorRef = ref MemoryMarshal.GetReference(vectorBuffer);
ref int cdfBase = ref MemoryMarshal.GetReference(this.cdfBuffer.GetSpan());
Span<TPixel> pixelRow = this.source.DangerousGetRowSpan(y);
int levels = this.luminanceLevels;
float noOfPixelsMinusCdfMin = this.numberOfPixelsMinusCdfMin;
Span<TPixel> pixelRow = this.source.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToVector4(this.configuration, pixelRow, vectorBuffer);
for (int x = 0; x < this.bounds.Width; x++)
{
// TODO: We should bulk convert here.
ref TPixel pixel = ref pixelRow[x];
var vector = pixel.ToVector4();
var vector = Unsafe.Add(ref vectorRef, x);
int luminance = ColorNumerics.GetBT709Luminance(ref vector, levels);
float luminanceEqualized = Unsafe.Add(ref cdfBase, luminance) / noOfPixelsMinusCdfMin;
pixel.FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, vector.W));
Unsafe.Add(ref vectorRef, x) = new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, vector.W);
}
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, vectorBuffer, pixelRow);
}
}
}

64
src/ImageSharp/Processing/Processors/Normalization/GrayscaleLevelsRowOperation{TPixel}.cs

@ -0,0 +1,64 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Normalization;
/// <summary>
/// A <see langword="struct"/> implementing the grayscale levels logic as <see cref="IRowOperation{Vector4}"/>.
/// </summary>
internal readonly struct GrayscaleLevelsRowOperation<TPixel> : IRowOperation<Vector4>
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly Configuration configuration;
private readonly Rectangle bounds;
private readonly IMemoryOwner<int> histogramBuffer;
private readonly Buffer2D<TPixel> source;
private readonly int luminanceLevels;
[MethodImpl(InliningOptions.ShortMethod)]
public GrayscaleLevelsRowOperation(
Configuration configuration,
Rectangle bounds,
IMemoryOwner<int> histogramBuffer,
Buffer2D<TPixel> source,
int luminanceLevels)
{
this.configuration = configuration;
this.bounds = bounds;
this.histogramBuffer = histogramBuffer;
this.source = source;
this.luminanceLevels = luminanceLevels;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public int GetRequiredBufferLength(Rectangle bounds) => bounds.Width;
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y, Span<Vector4> span)
{
Span<Vector4> vectorBuffer = span.Slice(0, this.bounds.Width);
ref Vector4 vectorRef = ref MemoryMarshal.GetReference(vectorBuffer);
ref int histogramBase = ref MemoryMarshal.GetReference(this.histogramBuffer.GetSpan());
int levels = this.luminanceLevels;
Span<TPixel> pixelRow = this.source.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToVector4(this.configuration, pixelRow, vectorBuffer);
for (int x = 0; x < this.bounds.Width; x++)
{
var vector = Unsafe.Add(ref vectorRef, x);
int luminance = ColorNumerics.GetBT709Luminance(ref vector, levels);
Interlocked.Increment(ref Unsafe.Add(ref histogramBase, luminance));
}
}
}

6
src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs

@ -22,4 +22,10 @@ public enum HistogramEqualizationMethod : int
/// Adaptive histogram equalization using sliding window. Slower then the tile interpolation mode, but can yield to better results.
/// </summary>
AdaptiveSlidingWindow,
/// <summary>
/// Adjusts the brightness levels of a particular image by scaling the
/// minimum and maximum values to the full brightness range.
/// </summary>
AutoLevel
}

7
src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs

@ -42,4 +42,11 @@ public class HistogramEqualizationOptions
/// Defaults to 8.
/// </summary>
public int NumberOfTiles { get; set; } = 8;
/// <summary>
/// Gets or sets a value indicating whether to synchronize the scaling factor over all color channels.
/// This parameter is only applicable to AutoLevel and is ignored for all others.
/// Defaults to true.
/// </summary>
public bool SyncChannels { get; set; } = true;
}

3
src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs

@ -60,6 +60,9 @@ public abstract class HistogramEqualizationProcessor : IImageProcessor
HistogramEqualizationMethod.AdaptiveSlidingWindow
=> new AdaptiveHistogramEqualizationSlidingWindowProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimit, options.NumberOfTiles),
HistogramEqualizationMethod.AutoLevel
=> new AutoLevelProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimit, options.SyncChannels),
_ => new GlobalHistogramEqualizationProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimit),
};
}

5
src/ImageSharp/Processing/Processors/Overlays/GlowProcessor{TPixel}.cs

@ -93,6 +93,11 @@ internal class GlowProcessor<TPixel> : ImageProcessor<TPixel>
this.source = source;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public int GetRequiredBufferLength(Rectangle bounds)
=> bounds.Width;
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y, Span<float> span)
{

5
src/ImageSharp/Processing/Processors/Overlays/VignetteProcessor{TPixel}.cs

@ -101,6 +101,11 @@ internal class VignetteProcessor<TPixel> : ImageProcessor<TPixel>
this.source = source;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public int GetRequiredBufferLength(Rectangle bounds)
=> bounds.Width;
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y, Span<float> span)
{

5
src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs

@ -176,6 +176,11 @@ internal class AffineTransformProcessor<TPixel> : TransformProcessor<TPixel>, IR
this.xRadius = LinearTransformUtility.GetSamplingRadius(in sampler, source.Width, destination.Width);
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public int GetRequiredBufferLength(Rectangle bounds)
=> bounds.Width;
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows, Span<Vector4> span)
{

5
src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs

@ -176,6 +176,11 @@ internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel>
this.xRadius = LinearTransformUtility.GetSamplingRadius(in sampler, bounds.Width, destination.Width);
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public int GetRequiredBufferLength(Rectangle bounds)
=> bounds.Width;
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows, Span<Vector4> span)
{

11
tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs

@ -385,6 +385,17 @@ public class WebpDecoderTests
image.CompareToOriginal(provider, ReferenceDecoder);
}
// https://github.com/SixLabors/ImageSharp/issues/2243
[Theory]
[WithFile(Lossy.Issue2243, PixelTypes.Rgba32)]
public void WebpDecoder_CanDecode_Issue2243<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(WebpDecoder);
image.DebugSave(provider);
image.CompareToOriginal(provider, ReferenceDecoder);
}
[Theory]
[WithFile(Lossless.LossLessCorruptImage3, PixelTypes.Rgba32)]
public void WebpDecoder_ThrowImageFormatException_OnInvalidImages<TPixel>(TestImageProvider<TPixel> provider)

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

@ -413,6 +413,9 @@ public class ParallelRowIteratorTests
public TestRowIntervalOperation(Action<RowInterval> action)
=> this.action = action;
public int GetRequiredBufferLength(Rectangle bounds)
=> bounds.Width;
public void Invoke(in RowInterval rows) => this.action(rows);
}
@ -424,6 +427,9 @@ public class ParallelRowIteratorTests
public TestRowIntervalOperation(RowIntervalAction<TBuffer> action)
=> this.action = action;
public int GetRequiredBufferLength(Rectangle bounds)
=> bounds.Width;
public void Invoke(in RowInterval rows, Span<TBuffer> span)
=> this.action(rows, span);
}

422
tests/ImageSharp.Tests/IO/BufferedReadStreamTests.cs

@ -10,12 +10,10 @@ public class BufferedReadStreamTests
private readonly Configuration configuration;
public BufferedReadStreamTests()
{
this.configuration = Configuration.CreateDefaultInstance();
}
=> this.configuration = Configuration.CreateDefaultInstance();
public static readonly TheoryData<int> BufferSizes =
new TheoryData<int>()
new()
{
1, 2, 4, 8,
16, 97, 503,
@ -28,21 +26,19 @@ public class BufferedReadStreamTests
public void BufferedStreamCanReadSingleByteFromOrigin(int bufferSize)
{
this.configuration.StreamProcessingBufferSize = bufferSize;
using (MemoryStream stream = this.CreateTestStream(bufferSize * 3))
using MemoryStream stream = CreateTestStream(bufferSize * 3);
byte[] expected = stream.ToArray();
using (BufferedReadStream reader = new(this.configuration, stream))
{
byte[] expected = stream.ToArray();
using (var reader = new BufferedReadStream(this.configuration, stream))
{
Assert.Equal(expected[0], reader.ReadByte());
// We've read a whole chunk but increment by 1 in our reader.
Assert.True(stream.Position >= bufferSize);
Assert.Equal(1, reader.Position);
}
Assert.Equal(expected[0], reader.ReadByte());
// Position of the stream should be reset on disposal.
Assert.Equal(1, stream.Position);
// We've read a whole chunk but increment by 1 in our reader.
Assert.True(stream.Position >= bufferSize);
Assert.Equal(1, reader.Position);
}
// Position of the stream should be reset on disposal.
Assert.Equal(1, stream.Position);
}
[Theory]
@ -50,23 +46,21 @@ public class BufferedReadStreamTests
public void BufferedStreamCanReadSingleByteFromOffset(int bufferSize)
{
this.configuration.StreamProcessingBufferSize = bufferSize;
using (MemoryStream stream = this.CreateTestStream(bufferSize * 3))
using MemoryStream stream = CreateTestStream(bufferSize * 3);
byte[] expected = stream.ToArray();
int offset = expected.Length / 2;
using (BufferedReadStream reader = new(this.configuration, stream))
{
byte[] expected = stream.ToArray();
int offset = expected.Length / 2;
using (var reader = new BufferedReadStream(this.configuration, stream))
{
reader.Position = offset;
reader.Position = offset;
Assert.Equal(expected[offset], reader.ReadByte());
// We've read a whole chunk but increment by 1 in our reader.
Assert.Equal(bufferSize + offset, stream.Position);
Assert.Equal(offset + 1, reader.Position);
}
Assert.Equal(expected[offset], reader.ReadByte());
Assert.Equal(offset + 1, stream.Position);
// We've read a whole chunk but increment by 1 in our reader.
Assert.Equal(bufferSize + offset, stream.Position);
Assert.Equal(offset + 1, reader.Position);
}
Assert.Equal(offset + 1, stream.Position);
}
[Theory]
@ -74,36 +68,34 @@ public class BufferedReadStreamTests
public void BufferedStreamCanReadSubsequentSingleByteCorrectly(int bufferSize)
{
this.configuration.StreamProcessingBufferSize = bufferSize;
using (MemoryStream stream = this.CreateTestStream(bufferSize * 3))
using MemoryStream stream = CreateTestStream(bufferSize * 3);
byte[] expected = stream.ToArray();
int i;
using (BufferedReadStream reader = new(this.configuration, stream))
{
byte[] expected = stream.ToArray();
int i;
using (var reader = new BufferedReadStream(this.configuration, stream))
for (i = 0; i < expected.Length; i++)
{
for (i = 0; i < expected.Length; i++)
Assert.Equal(expected[i], reader.ReadByte());
Assert.Equal(i + 1, reader.Position);
if (i < bufferSize)
{
Assert.Equal(expected[i], reader.ReadByte());
Assert.Equal(i + 1, reader.Position);
if (i < bufferSize)
{
Assert.Equal(stream.Position, bufferSize);
}
else if (i >= bufferSize && i < bufferSize * 2)
{
// We should have advanced to the second chunk now.
Assert.Equal(stream.Position, bufferSize * 2);
}
else
{
// We should have advanced to the third chunk now.
Assert.Equal(stream.Position, bufferSize * 3);
}
Assert.Equal(stream.Position, bufferSize);
}
else if (i >= bufferSize && i < bufferSize * 2)
{
// We should have advanced to the second chunk now.
Assert.Equal(stream.Position, bufferSize * 2);
}
else
{
// We should have advanced to the third chunk now.
Assert.Equal(stream.Position, bufferSize * 3);
}
}
Assert.Equal(i, stream.Position);
}
Assert.Equal(i, stream.Position);
}
[Theory]
@ -111,21 +103,17 @@ public class BufferedReadStreamTests
public void BufferedStreamCanReadMultipleBytesFromOrigin(int bufferSize)
{
this.configuration.StreamProcessingBufferSize = bufferSize;
using (MemoryStream stream = this.CreateTestStream(bufferSize * 3))
{
var buffer = new byte[2];
byte[] expected = stream.ToArray();
using (var reader = new BufferedReadStream(this.configuration, stream))
{
Assert.Equal(2, reader.Read(buffer, 0, 2));
Assert.Equal(expected[0], buffer[0]);
Assert.Equal(expected[1], buffer[1]);
// We've read a whole chunk but increment by the buffer length in our reader.
Assert.True(stream.Position >= bufferSize);
Assert.Equal(buffer.Length, reader.Position);
}
}
using MemoryStream stream = CreateTestStream(bufferSize * 3);
byte[] buffer = new byte[2];
byte[] expected = stream.ToArray();
using BufferedReadStream reader = new(this.configuration, stream);
Assert.Equal(2, reader.Read(buffer, 0, 2));
Assert.Equal(expected[0], buffer[0]);
Assert.Equal(expected[1], buffer[1]);
// We've read a whole chunk but increment by the buffer length in our reader.
Assert.True(stream.Position >= bufferSize);
Assert.Equal(buffer.Length, reader.Position);
}
[Theory]
@ -133,49 +121,45 @@ public class BufferedReadStreamTests
public void BufferedStreamCanReadSubsequentMultipleByteCorrectly(int bufferSize)
{
this.configuration.StreamProcessingBufferSize = bufferSize;
using (MemoryStream stream = this.CreateTestStream(bufferSize * 3))
using MemoryStream stream = CreateTestStream(bufferSize * 3);
const int increment = 2;
byte[] buffer = new byte[2];
byte[] expected = stream.ToArray();
using BufferedReadStream reader = new(this.configuration, stream);
for (int i = 0, o = 0; i < expected.Length / increment; i++, o += increment)
{
const int increment = 2;
var buffer = new byte[2];
byte[] expected = stream.ToArray();
using (var reader = new BufferedReadStream(this.configuration, stream))
// Check values are correct.
Assert.Equal(increment, reader.Read(buffer, 0, increment));
Assert.Equal(expected[o], buffer[0]);
Assert.Equal(expected[o + 1], buffer[1]);
Assert.Equal(o + increment, reader.Position);
// These tests ensure that we are correctly reading
// our buffer in chunks of the given size.
int offset = i * increment;
// First chunk.
if (offset < bufferSize)
{
for (int i = 0, o = 0; i < expected.Length / increment; i++, o += increment)
{
// Check values are correct.
Assert.Equal(increment, reader.Read(buffer, 0, increment));
Assert.Equal(expected[o], buffer[0]);
Assert.Equal(expected[o + 1], buffer[1]);
Assert.Equal(o + increment, reader.Position);
// These tests ensure that we are correctly reading
// our buffer in chunks of the given size.
int offset = i * increment;
// First chunk.
if (offset < bufferSize)
{
// We've read an entire chunk once and are
// now reading from that chunk.
Assert.True(stream.Position >= bufferSize);
continue;
}
// Second chunk
if (offset < bufferSize * 2)
{
Assert.True(stream.Position > bufferSize);
// Odd buffer size with even increments can
// jump to the third chunk on final read.
Assert.True(stream.Position <= bufferSize * 3);
continue;
}
// Third chunk
Assert.True(stream.Position > bufferSize * 2);
}
// We've read an entire chunk once and are
// now reading from that chunk.
Assert.True(stream.Position >= bufferSize);
continue;
}
// Second chunk
if (offset < bufferSize * 2)
{
Assert.True(stream.Position > bufferSize);
// Odd buffer size with even increments can
// jump to the third chunk on final read.
Assert.True(stream.Position <= bufferSize * 3);
continue;
}
// Third chunk
Assert.True(stream.Position > bufferSize * 2);
}
}
@ -184,49 +168,45 @@ public class BufferedReadStreamTests
public void BufferedStreamCanReadSubsequentMultipleByteSpanCorrectly(int bufferSize)
{
this.configuration.StreamProcessingBufferSize = bufferSize;
using (MemoryStream stream = this.CreateTestStream(bufferSize * 3))
using MemoryStream stream = CreateTestStream(bufferSize * 3);
const int increment = 2;
Span<byte> buffer = new byte[2];
byte[] expected = stream.ToArray();
using BufferedReadStream reader = new(this.configuration, stream);
for (int i = 0, o = 0; i < expected.Length / increment; i++, o += increment)
{
const int increment = 2;
Span<byte> buffer = new byte[2];
byte[] expected = stream.ToArray();
using (var reader = new BufferedReadStream(this.configuration, stream))
// Check values are correct.
Assert.Equal(increment, reader.Read(buffer, 0, increment));
Assert.Equal(expected[o], buffer[0]);
Assert.Equal(expected[o + 1], buffer[1]);
Assert.Equal(o + increment, reader.Position);
// These tests ensure that we are correctly reading
// our buffer in chunks of the given size.
int offset = i * increment;
// First chunk.
if (offset < bufferSize)
{
for (int i = 0, o = 0; i < expected.Length / increment; i++, o += increment)
{
// Check values are correct.
Assert.Equal(increment, reader.Read(buffer, 0, increment));
Assert.Equal(expected[o], buffer[0]);
Assert.Equal(expected[o + 1], buffer[1]);
Assert.Equal(o + increment, reader.Position);
// These tests ensure that we are correctly reading
// our buffer in chunks of the given size.
int offset = i * increment;
// First chunk.
if (offset < bufferSize)
{
// We've read an entire chunk once and are
// now reading from that chunk.
Assert.True(stream.Position >= bufferSize);
continue;
}
// Second chunk
if (offset < bufferSize * 2)
{
Assert.True(stream.Position > bufferSize);
// Odd buffer size with even increments can
// jump to the third chunk on final read.
Assert.True(stream.Position <= bufferSize * 3);
continue;
}
// Third chunk
Assert.True(stream.Position > bufferSize * 2);
}
// We've read an entire chunk once and are
// now reading from that chunk.
Assert.True(stream.Position >= bufferSize);
continue;
}
// Second chunk
if (offset < bufferSize * 2)
{
Assert.True(stream.Position > bufferSize);
// Odd buffer size with even increments can
// jump to the third chunk on final read.
Assert.True(stream.Position <= bufferSize * 3);
continue;
}
// Third chunk
Assert.True(stream.Position > bufferSize * 2);
}
}
@ -235,34 +215,28 @@ public class BufferedReadStreamTests
public void BufferedStreamCanSkip(int bufferSize)
{
this.configuration.StreamProcessingBufferSize = bufferSize;
using (MemoryStream stream = this.CreateTestStream(bufferSize * 4))
{
byte[] expected = stream.ToArray();
using (var reader = new BufferedReadStream(this.configuration, stream))
{
int skip = 1;
int plusOne = 1;
int skip2 = bufferSize;
using MemoryStream stream = CreateTestStream(bufferSize * 4);
byte[] expected = stream.ToArray();
using BufferedReadStream reader = new(this.configuration, stream);
const int skip = 1;
const int plusOne = 1;
int skip2 = bufferSize;
// Skip
reader.Skip(skip);
Assert.Equal(skip, reader.Position);
Assert.Equal(stream.Position, reader.Position);
// Skip
reader.Skip(skip);
Assert.Equal(skip, reader.Position);
// Read
Assert.Equal(expected[skip], reader.ReadByte());
// Read
Assert.Equal(expected[skip], reader.ReadByte());
// Skip Again
reader.Skip(skip2);
// Skip Again
reader.Skip(skip2);
// First Skip + First Read + Second Skip
int position = skip + plusOne + skip2;
// First Skip + First Read + Second Skip
int position = skip + plusOne + skip2;
Assert.Equal(position, reader.Position);
Assert.Equal(stream.Position, reader.Position);
Assert.Equal(expected[position], reader.ReadByte());
}
}
Assert.Equal(position, reader.Position);
Assert.Equal(expected[position], reader.ReadByte());
}
[Theory]
@ -272,23 +246,21 @@ public class BufferedReadStreamTests
this.configuration.StreamProcessingBufferSize = bufferSize;
// Create a stream smaller than the default buffer length
using (MemoryStream stream = this.CreateTestStream(Math.Max(1, bufferSize / 4)))
using MemoryStream stream = CreateTestStream(Math.Max(1, bufferSize / 4));
byte[] expected = stream.ToArray();
int offset = expected.Length / 2;
using (BufferedReadStream reader = new(this.configuration, stream))
{
byte[] expected = stream.ToArray();
int offset = expected.Length / 2;
using (var reader = new BufferedReadStream(this.configuration, stream))
{
reader.Position = offset;
reader.Position = offset;
Assert.Equal(expected[offset], reader.ReadByte());
Assert.Equal(expected[offset], reader.ReadByte());
// We've read a whole length of the stream but increment by 1 in our reader.
Assert.Equal(Math.Max(1, bufferSize / 4), stream.Position);
Assert.Equal(offset + 1, reader.Position);
}
Assert.Equal(offset + 1, stream.Position);
// We've read a whole length of the stream but increment by 1 in our reader.
Assert.Equal(Math.Max(1, bufferSize / 4), stream.Position);
Assert.Equal(offset + 1, reader.Position);
}
Assert.Equal(offset + 1, stream.Position);
}
[Theory]
@ -296,16 +268,12 @@ public class BufferedReadStreamTests
public void BufferedStreamReadsCanReadAllAsSingleByteFromOrigin(int bufferSize)
{
this.configuration.StreamProcessingBufferSize = bufferSize;
using (MemoryStream stream = this.CreateTestStream(bufferSize * 3))
using MemoryStream stream = CreateTestStream(bufferSize * 3);
byte[] expected = stream.ToArray();
using BufferedReadStream reader = new(this.configuration, stream);
for (int i = 0; i < expected.Length; i++)
{
byte[] expected = stream.ToArray();
using (var reader = new BufferedReadStream(this.configuration, stream))
{
for (int i = 0; i < expected.Length; i++)
{
Assert.Equal(expected[i], reader.ReadByte());
}
}
Assert.Equal(expected[i], reader.ReadByte());
}
}
@ -314,13 +282,9 @@ public class BufferedReadStreamTests
public void BufferedStreamThrowsOnNegativePosition(int bufferSize)
{
this.configuration.StreamProcessingBufferSize = bufferSize;
using (MemoryStream stream = this.CreateTestStream(bufferSize))
{
using (var reader = new BufferedReadStream(this.configuration, stream))
{
Assert.Throws<ArgumentOutOfRangeException>(() => reader.Position = -stream.Length);
}
}
using MemoryStream stream = CreateTestStream(bufferSize);
using BufferedReadStream reader = new(this.configuration, stream);
Assert.Throws<ArgumentOutOfRangeException>(() => reader.Position = -stream.Length);
}
[Theory]
@ -328,13 +292,9 @@ public class BufferedReadStreamTests
public void BufferedStreamCanSetPositionToEnd(int bufferSize)
{
this.configuration.StreamProcessingBufferSize = bufferSize;
using (MemoryStream stream = this.CreateTestStream(bufferSize * 2))
{
using (var reader = new BufferedReadStream(this.configuration, stream))
{
reader.Position = reader.Length;
}
}
using MemoryStream stream = CreateTestStream(bufferSize * 2);
using BufferedReadStream reader = new(this.configuration, stream);
reader.Position = reader.Length;
}
[Theory]
@ -342,20 +302,46 @@ public class BufferedReadStreamTests
public void BufferedStreamCanSetPositionPastTheEnd(int bufferSize)
{
this.configuration.StreamProcessingBufferSize = bufferSize;
using (MemoryStream stream = this.CreateTestStream(bufferSize * 2))
using MemoryStream stream = CreateTestStream(bufferSize * 2);
using BufferedReadStream reader = new(this.configuration, stream);
reader.Position = reader.Length + 1;
Assert.Equal(stream.Length + 1, stream.Position);
}
[Fact]
public void BufferedStreamCanSetPositionMultipleTimes()
{
Configuration configuration = new()
{
using (var reader = new BufferedReadStream(this.configuration, stream))
{
reader.Position = reader.Length + 1;
Assert.Equal(stream.Length + 1, stream.Position);
}
StreamProcessingBufferSize = 16
};
byte[] buffer = new byte[255];
for (int i = 0; i < buffer.Length; i++)
{
buffer[i] = (byte)i;
}
BufferedReadStream bufferedStream = new(configuration, new MemoryStream(buffer));
// Read more then fits into the buffer.
for (int i = 0; i < 20; i++)
{
bufferedStream.ReadByte();
}
// Set the Position twice.
bufferedStream.Position = 10;
bufferedStream.Position = 3;
int actual = bufferedStream.ReadByte();
Assert.Equal(3, actual);
}
private MemoryStream CreateTestStream(int length)
private static MemoryStream CreateTestStream(int length)
{
var buffer = new byte[length];
var random = new Random();
byte[] buffer = new byte[length];
Random random = new();
random.NextBytes(buffer);
return new EvilStream(buffer);
@ -371,8 +357,6 @@ public class BufferedReadStreamTests
}
public override int Read(byte[] buffer, int offset, int count)
{
return base.Read(buffer, offset, 1);
}
=> base.Read(buffer, offset, 1);
}
}

38
tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs

@ -134,6 +134,44 @@ public class HistogramEqualizationTests
}
}
[Theory]
[WithFile(TestImages.Jpeg.Baseline.ForestBridgeDifferentComponentsQuality, PixelTypes.Rgba32)]
public void AutoLevel_SeparateChannels_CompareToReferenceOutput<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{
var options = new HistogramEqualizationOptions
{
Method = HistogramEqualizationMethod.AutoLevel,
LuminanceLevels = 256,
SyncChannels = false
};
image.Mutate(x => x.HistogramEqualization(options));
image.DebugSave(provider);
image.CompareToReferenceOutput(ValidatorComparer, provider, extension: "png");
}
}
[Theory]
[WithFile(TestImages.Jpeg.Baseline.ForestBridgeDifferentComponentsQuality, PixelTypes.Rgba32)]
public void AutoLevel_SynchronizedChannels_CompareToReferenceOutput<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{
var options = new HistogramEqualizationOptions
{
Method = HistogramEqualizationMethod.AutoLevel,
LuminanceLevels = 256,
SyncChannels = true
};
image.Mutate(x => x.HistogramEqualization(options));
image.DebugSave(provider);
image.CompareToReferenceOutput(ValidatorComparer, provider, extension: "png");
}
}
/// <summary>
/// This is regression test for a bug with the calculation of the y-start positions,
/// where it could happen that one too much start position was calculated in some cases.

81
tests/ImageSharp.Tests/Processing/Normalization/MagickCompareTests.cs

@ -0,0 +1,81 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Normalization;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using ImageMagick;
namespace SixLabors.ImageSharp.Tests.Processing.Normalization;
// ReSharper disable InconsistentNaming
[Trait("Category", "Processors")]
public class MagickCompareTests
{
[Theory]
[WithFile(TestImages.Jpeg.Baseline.ForestBridgeDifferentComponentsQuality, PixelTypes.Rgba32)]
public void AutoLevel_CompareToMagick<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, ImageSharp.PixelFormats.IPixel<TPixel>
{
Image<TPixel> imageFromMagick;
using (Stream stream = LoadAsStream(provider))
{
var magickImage = new MagickImage(stream);
// Apply Auto Level using the Grey (BT.709) channel.
magickImage.AutoLevel(Channels.Gray);
imageFromMagick = ConvertImageFromMagick<TPixel>(magickImage);
}
using (Image<TPixel> image = provider.GetImage())
{
var options = new HistogramEqualizationOptions
{
Method = HistogramEqualizationMethod.AutoLevel,
LuminanceLevels = 256,
SyncChannels = true
};
image.Mutate(x => x.HistogramEqualization(options));
image.DebugSave(provider);
ExactImageComparer.Instance.CompareImages(imageFromMagick, image);
}
}
private Stream LoadAsStream<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, ImageSharp.PixelFormats.IPixel<TPixel>
{
string path = TestImageProvider<TPixel>.GetFilePathOrNull(provider);
if (path == null)
{
throw new InvalidOperationException("CompareToMagick() works only with file providers!");
}
var testFile = TestFile.Create(path);
return new FileStream(testFile.FullPath, FileMode.Open);
}
private Image<TPixel> ConvertImageFromMagick<TPixel>(MagickImage magickImage)
where TPixel : unmanaged, ImageSharp.PixelFormats.IPixel<TPixel>
{
Configuration configuration = Configuration.Default.Clone();
configuration.PreferContiguousImageBuffers = true;
var result = new Image<TPixel>(configuration, magickImage.Width, magickImage.Height);
Assert.True(result.DangerousTryGetSinglePixelMemory(out Memory<TPixel> resultPixels));
using (IUnsafePixelCollection<ushort> pixels = magickImage.GetPixelsUnsafe())
{
byte[] data = pixels.ToByteArray(PixelMapping.RGBA);
PixelOperations<TPixel>.Instance.FromRgba32Bytes(
configuration,
data,
resultPixels.Span,
resultPixels.Length);
}
return result;
}
}

70
tests/ImageSharp.Tests/Processing/Processors/Convolution/ConvolutionProcessorHelpersTest.cs

@ -0,0 +1,70 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Processing.Processors.Convolution;
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Convolution;
[GroupOutput("Convolution")]
public class ConvolutionProcessorHelpersTest
{
[Theory]
[InlineData(3)]
[InlineData(5)]
[InlineData(9)]
[InlineData(22)]
[InlineData(33)]
[InlineData(80)]
public void VerifyGaussianKernelDecomposition(int radius)
{
int kernelSize = (radius * 2) + 1;
float sigma = radius / 3F;
float[] kernel = ConvolutionProcessorHelpers.CreateGaussianBlurKernel(kernelSize, sigma);
DenseMatrix<float> matrix = DotProduct(kernel, kernel);
bool result = matrix.TryGetLinearlySeparableComponents(out float[] row, out float[] column);
Assert.True(result);
Assert.NotNull(row);
Assert.NotNull(column);
Assert.Equal(row.Length, matrix.Rows);
Assert.Equal(column.Length, matrix.Columns);
float[,] dotProduct = DotProduct(row, column);
for (int y = 0; y < column.Length; y++)
{
for (int x = 0; x < row.Length; x++)
{
Assert.True(Math.Abs(matrix[y, x] - dotProduct[y, x]) < 0.0001F);
}
}
}
[Fact]
public void VerifyNonSeparableMatrix()
{
bool result = LaplacianKernels.LaplacianOfGaussianXY.TryGetLinearlySeparableComponents(
out float[] row,
out float[] column);
Assert.False(result);
Assert.Null(row);
Assert.Null(column);
}
private static DenseMatrix<float> DotProduct(float[] row, float[] column)
{
float[,] matrix = new float[column.Length, row.Length];
for (int x = 0; x < row.Length; x++)
{
for (int y = 0; y < column.Length; y++)
{
matrix[y, x] = row[x] * column[y];
}
}
return matrix;
}
}

1
tests/ImageSharp.Tests/TestImages.cs

@ -736,6 +736,7 @@ public static class TestImages
// Issues
public const string Issue1594 = "Webp/issues/Issue1594.webp";
public const string Issue2243 = "Webp/issues/Issue2243.webp";
}
}

3
tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs

@ -756,6 +756,9 @@ public static class TestImageExtensions
this.source = source;
}
public int GetRequiredBufferLength(Rectangle bounds)
=> bounds.Width;
public void Invoke(in RowInterval rows, Span<Vector4> span)
{
for (int y = rows.Min; y < rows.Max; y++)

3
tests/Images/External/ReferenceOutput/HistogramEqualizationTests/AutoLevel_SeparateChannels_CompareToReferenceOutput_Rgba32_forest_bridge.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:aada4a2ccf45de24f2a591a18d9bc0260ceb3829e104fee6982061013ed87282
size 14107709

3
tests/Images/External/ReferenceOutput/HistogramEqualizationTests/AutoLevel_SynchronizedChannels_CompareToReferenceOutput_Rgba32_forest_bridge.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dca9b5b890d3a79b0002b7093d254d484ada4207e5010d1f0c6248d4dd6e22db
size 13909894

3
tests/Images/Input/Webp/issues/Issue2243.webp

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c1aae55fae66f3f9469ad5e28eb8134a04b1c5d746acf0a4a19d0f63ca0581cd
size 55068
Loading…
Cancel
Save