Browse Source

Merge pull request #710 from SixLabors/af/improved-parallelization

Optimized parallel pixel processing
af/merge-core
James Jackson-South 8 years ago
committed by GitHub
parent
commit
41d14dd9b8
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 178
      src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawImageProcessor.cs
  2. 223
      src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor.cs
  3. 61
      src/ImageSharp/Common/Helpers/ParallelFor.cs
  4. 71
      src/ImageSharp/Common/ParallelUtils/ParallelExecutionSettings.cs
  5. 146
      src/ImageSharp/Common/ParallelUtils/ParallelHelper.cs
  6. 1
      src/ImageSharp/Configuration.cs
  7. 50
      src/ImageSharp/ImageFrame{TPixel}.cs
  8. 3
      src/ImageSharp/ImageSharp.csproj.DotSettings
  9. 20
      src/ImageSharp/Memory/Buffer2DExtensions.cs
  10. 45
      src/ImageSharp/Memory/RowInterval.cs
  11. 43
      src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor.cs
  12. 119
      src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor.cs
  13. 61
      src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor.cs
  14. 94
      src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor.cs
  15. 42
      src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor.cs
  16. 110
      src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor.cs
  17. 28
      src/ImageSharp/Processing/Processors/Filters/FilterProcessor.cs
  18. 38
      src/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor.cs
  19. 59
      src/ImageSharp/Processing/Processors/Overlays/GlowProcessor.cs
  20. 61
      src/ImageSharp/Processing/Processors/Overlays/VignetteProcessor.cs
  21. 186
      src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs
  22. 32
      src/ImageSharp/Processing/Processors/Transforms/CropProcessor.cs
  23. 64
      src/ImageSharp/Processing/Processors/Transforms/FlipProcessor.cs
  24. 216
      src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs
  25. 148
      src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs
  26. 86
      src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs
  27. 115
      tests/ImageSharp.Benchmarks/Codecs/CopyPixels.cs
  28. 38
      tests/ImageSharp.Benchmarks/Samplers/Glow.cs
  29. 255
      tests/ImageSharp.Tests/Drawing/FillPatternTests.cs
  30. 44
      tests/ImageSharp.Tests/Drawing/FillSolidBrushTests.cs
  31. 338
      tests/ImageSharp.Tests/Helpers/ParallelHelperTests.cs
  32. 38
      tests/ImageSharp.Tests/Helpers/RowIntervalTests.cs
  33. 2
      tests/ImageSharp.Tests/Processing/Processors/Convolution/DetectEdgesTest.cs
  34. 24
      tests/ImageSharp.Tests/Processing/Processors/Transforms/CropTest.cs
  35. 19
      tests/ImageSharp.Tests/Processing/Processors/Transforms/FlipTests.cs
  36. 23
      tests/ImageSharp.Tests/Processing/Processors/Transforms/RotateTests.cs
  37. 2
      tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs
  38. 19
      tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs
  39. 2
      tests/Images/External

178
src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawImageProcessor.cs

@ -1,95 +1,101 @@
// Copyright (c) Six Labors and contributors. // Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System; using System;
using System.Buffers; using System.Buffers;
using System.Threading.Tasks; using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.Memory; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Primitives; using SixLabors.Memory;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Drawing
{ namespace SixLabors.ImageSharp.Processing.Processors.Drawing
/// <summary> {
/// Combines two images together by blending the pixels. /// <summary>
/// </summary> /// Combines two images together by blending the pixels.
/// </summary>
/// <typeparam name="TPixelDst">The pixel format of destination image.</typeparam> /// <typeparam name="TPixelDst">The pixel format of destination image.</typeparam>
/// <typeparam name="TPixelSrc">The pixel format os source image.</typeparam> /// <typeparam name="TPixelSrc">The pixel format of source image.</typeparam>
internal class DrawImageProcessor<TPixelDst, TPixelSrc> : ImageProcessor<TPixelDst> internal class DrawImageProcessor<TPixelDst, TPixelSrc> : ImageProcessor<TPixelDst>
where TPixelDst : struct, IPixel<TPixelDst> where TPixelDst : struct, IPixel<TPixelDst>
where TPixelSrc : struct, IPixel<TPixelSrc> where TPixelSrc : struct, IPixel<TPixelSrc>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DrawImageProcessor{TPixelDst, TPixelSrc}"/> class. /// Initializes a new instance of the <see cref="DrawImageProcessor{TPixelDst, TPixelSrc}"/> class.
/// </summary> /// </summary>
/// <param name="image">The image to blend with the currently processing image.</param> /// <param name="image">The image to blend with the currently processing image.</param>
/// <param name="location">The location to draw the blended image.</param> /// <param name="location">The location to draw the blended image.</param>
/// <param name="colorBlendingMode">The blending mode to use when drawing the image.</param> /// <param name="colorBlendingMode">The blending mode to use when drawing the image.</param>
/// <param name="alphaCompositionMode">The Alpha blending mode to use when drawing the image.</param> /// <param name="alphaCompositionMode">The Alpha blending mode to use when drawing the image.</param>
/// <param name="opacity">The opacity of the image to blend. Must be between 0 and 1.</param> /// <param name="opacity">The opacity of the image to blend. Must be between 0 and 1.</param>
public DrawImageProcessor(Image<TPixelSrc> image, Point location, PixelColorBlendingMode colorBlendingMode, PixelAlphaCompositionMode alphaCompositionMode, float opacity) public DrawImageProcessor(Image<TPixelSrc> image, Point location, PixelColorBlendingMode colorBlendingMode, PixelAlphaCompositionMode alphaCompositionMode, float opacity)
{ {
Guard.MustBeBetweenOrEqualTo(opacity, 0, 1, nameof(opacity)); Guard.MustBeBetweenOrEqualTo(opacity, 0, 1, nameof(opacity));
this.Image = image; this.Image = image;
this.Opacity = opacity; this.Opacity = opacity;
this.Blender = PixelOperations<TPixelDst>.Instance.GetPixelBlender(colorBlendingMode, alphaCompositionMode); this.Blender = PixelOperations<TPixelDst>.Instance.GetPixelBlender(colorBlendingMode, alphaCompositionMode);
this.Location = location; this.Location = location;
} }
/// <summary> /// <summary>
/// Gets the image to blend /// Gets the image to blend
/// </summary> /// </summary>
public Image<TPixelSrc> Image { get; } public Image<TPixelSrc> Image { get; }
/// <summary> /// <summary>
/// Gets the opacity of the image to blend /// Gets the opacity of the image to blend
/// </summary> /// </summary>
public float Opacity { get; } public float Opacity { get; }
/// <summary> /// <summary>
/// Gets the pixel blender /// Gets the pixel blender
/// </summary> /// </summary>
public PixelBlender<TPixelDst> Blender { get; } public PixelBlender<TPixelDst> Blender { get; }
/// <summary> /// <summary>
/// Gets the location to draw the blended image /// Gets the location to draw the blended image
/// </summary> /// </summary>
public Point Location { get; } public Point Location { get; }
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixelDst> source, Rectangle sourceRectangle, Configuration configuration) protected override void OnFrameApply(ImageFrame<TPixelDst> source, Rectangle sourceRectangle, Configuration configuration)
{ {
Image<TPixelSrc> targetImage = this.Image; Image<TPixelSrc> targetImage = this.Image;
PixelBlender<TPixelDst> blender = this.Blender; PixelBlender<TPixelDst> blender = this.Blender;
int locationY = this.Location.Y; int locationY = this.Location.Y;
// Align start/end positions. // Align start/end positions.
Rectangle bounds = targetImage.Bounds(); Rectangle bounds = targetImage.Bounds();
int minX = Math.Max(this.Location.X, sourceRectangle.X); int minX = Math.Max(this.Location.X, sourceRectangle.X);
int maxX = Math.Min(this.Location.X + bounds.Width, sourceRectangle.Width); int maxX = Math.Min(this.Location.X + bounds.Width, sourceRectangle.Width);
int targetX = minX - this.Location.X; int targetX = minX - this.Location.X;
int minY = Math.Max(this.Location.Y, sourceRectangle.Y); int minY = Math.Max(this.Location.Y, sourceRectangle.Y);
int maxY = Math.Min(this.Location.Y + bounds.Height, sourceRectangle.Bottom); int maxY = Math.Min(this.Location.Y + bounds.Height, sourceRectangle.Bottom);
int width = maxX - minX; int width = maxX - minX;
MemoryAllocator memoryAllocator = this.Image.GetConfiguration().MemoryAllocator; MemoryAllocator memoryAllocator = this.Image.GetConfiguration().MemoryAllocator;
ParallelFor.WithConfiguration( var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY);
minY,
maxY, ParallelHelper.IterateRows(
configuration, workingRect,
y => configuration,
{ rows =>
Span<TPixelDst> background = source.GetPixelRowSpan(y).Slice(minX, width); {
Span<TPixelSrc> foreground = targetImage.GetPixelRowSpan(y - locationY).Slice(targetX, width); for (int y = rows.Min; y < rows.Max; y++)
blender.Blend<TPixelSrc>(memoryAllocator, background, background, foreground, this.Opacity); {
}); Span<TPixelDst> background = source.GetPixelRowSpan(y).Slice(minX, width);
} Span<TPixelSrc> foreground =
} targetImage.GetPixelRowSpan(y - locationY).Slice(targetX, width);
blender.Blend<TPixelSrc>(memoryAllocator, background, background, foreground, this.Opacity);
}
});
}
}
} }

223
src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor.cs

@ -1,107 +1,116 @@
// Copyright (c) Six Labors and contributors. // Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System; using System;
using System.Buffers; using System.Buffers;
using System.Threading.Tasks; using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.Memory; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Primitives; using SixLabors.Memory;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Drawing
{ namespace SixLabors.ImageSharp.Processing.Processors.Drawing
/// <summary> {
/// Using the brush as a source of pixels colors blends the brush color with source. /// <summary>
/// </summary> /// Using the brush as a source of pixels colors blends the brush color with source.
/// <typeparam name="TPixel">The pixel format.</typeparam> /// </summary>
internal class FillProcessor<TPixel> : ImageProcessor<TPixel> /// <typeparam name="TPixel">The pixel format.</typeparam>
where TPixel : struct, IPixel<TPixel> internal class FillProcessor<TPixel> : ImageProcessor<TPixel>
{ where TPixel : struct, IPixel<TPixel>
/// <summary> {
/// The brush. /// <summary>
/// </summary> /// The brush.
private readonly IBrush<TPixel> brush; /// </summary>
private readonly GraphicsOptions options; private readonly IBrush<TPixel> brush;
private readonly GraphicsOptions options;
/// <summary>
/// Initializes a new instance of the <see cref="FillProcessor{TPixel}"/> class. /// <summary>
/// </summary> /// Initializes a new instance of the <see cref="FillProcessor{TPixel}"/> class.
/// <param name="brush">The brush to source pixel colors from.</param> /// </summary>
/// <param name="options">The options</param> /// <param name="brush">The brush to source pixel colors from.</param>
public FillProcessor(IBrush<TPixel> brush, GraphicsOptions options) /// <param name="options">The options</param>
{ public FillProcessor(IBrush<TPixel> brush, GraphicsOptions options)
this.brush = brush; {
this.options = options; this.brush = brush;
} this.options = options;
}
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration) /// <inheritdoc/>
{ protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration)
int startX = sourceRectangle.X; {
int endX = sourceRectangle.Right; int startX = sourceRectangle.X;
int startY = sourceRectangle.Y; int endX = sourceRectangle.Right;
int endY = sourceRectangle.Bottom; int startY = sourceRectangle.Y;
int endY = sourceRectangle.Bottom;
// Align start/end positions.
int minX = Math.Max(0, startX); // Align start/end positions.
int maxX = Math.Min(source.Width, endX); int minX = Math.Max(0, startX);
int minY = Math.Max(0, startY); int maxX = Math.Min(source.Width, endX);
int maxY = Math.Min(source.Height, endY); int minY = Math.Max(0, startY);
int maxY = Math.Min(source.Height, endY);
int width = maxX - minX;
int width = maxX - minX;
// If there's no reason for blending, then avoid it.
if (this.IsSolidBrushWithoutBlending(out SolidBrush<TPixel> solidBrush)) var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY);
{
ParallelFor.WithConfiguration( // If there's no reason for blending, then avoid it.
minY, if (this.IsSolidBrushWithoutBlending(out SolidBrush<TPixel> solidBrush))
maxY, {
configuration, ParallelExecutionSettings parallelSettings = configuration.GetParallelSettings().MultiplyMinimumPixelsPerTask(4);
y =>
{ ParallelHelper.IterateRows(
source.GetPixelRowSpan(y).Slice(minX, width).Fill(solidBrush.Color); workingRect,
}); parallelSettings,
} rows =>
else {
{ for (int y = rows.Min; y < rows.Max; y++)
// Reset offset if necessary. {
if (minX > 0) source.GetPixelRowSpan(y).Slice(minX, width).Fill(solidBrush.Color);
{ }
startX = 0; });
} }
else
if (minY > 0) {
{ // Reset offset if necessary.
startY = 0; if (minX > 0)
} {
startX = 0;
using (IMemoryOwner<float> amount = source.MemoryAllocator.Allocate<float>(width)) }
using (BrushApplicator<TPixel> applicator = this.brush.CreateApplicator(
source, if (minY > 0)
sourceRectangle, {
this.options)) startY = 0;
{ }
amount.GetSpan().Fill(1f);
using (IMemoryOwner<float> amount = source.MemoryAllocator.Allocate<float>(width))
ParallelFor.WithConfiguration( using (BrushApplicator<TPixel> applicator = this.brush.CreateApplicator(
minY, source,
maxY, sourceRectangle,
configuration, this.options))
y => {
{ amount.GetSpan().Fill(1f);
int offsetY = y - startY;
int offsetX = minX - startX; ParallelHelper.IterateRows(
workingRect,
applicator.Apply(amount.GetSpan(), offsetX, offsetY); configuration,
}); rows =>
} {
} for (int y = rows.Min; y < rows.Max; y++)
} {
int offsetY = y - startY;
private bool IsSolidBrushWithoutBlending(out SolidBrush<TPixel> solidBrush) int offsetX = minX - startX;
{
applicator.Apply(amount.GetSpan(), offsetX, offsetY);
}
});
}
}
}
private bool IsSolidBrushWithoutBlending(out SolidBrush<TPixel> solidBrush)
{
solidBrush = this.brush as SolidBrush<TPixel>; solidBrush = this.brush as SolidBrush<TPixel>;
if (solidBrush == null) if (solidBrush == null)
@ -109,7 +118,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing
return false; return false;
} }
return this.options.IsOpaqueColorWithoutBlending(solidBrush.Color); return this.options.IsOpaqueColorWithoutBlending(solidBrush.Color);
} }
} }
} }

61
src/ImageSharp/Common/Helpers/ParallelFor.cs

@ -1,61 +0,0 @@
using System;
using System.Buffers;
using System.Threading.Tasks;
using SixLabors.Memory;
namespace SixLabors.ImageSharp
{
/// <summary>
/// Utility methods for Parallel.For() execution. Use this instead of raw <see cref="Parallel"/> calls!
/// </summary>
internal static class ParallelFor
{
/// <summary>
/// Helper method to execute Parallel.For using the settings in <paramref name="configuration"/>
/// </summary>
public static void WithConfiguration(int fromInclusive, int toExclusive, Configuration configuration, Action<int> body)
{
Parallel.For(fromInclusive, toExclusive, configuration.GetParallelOptions(), body);
}
/// <summary>
/// Helper method to execute Parallel.For with temporary worker buffer shared between executing tasks.
/// The buffer is not guaranteed to be clean!
/// </summary>
/// <typeparam name="T">The value type of the buffer</typeparam>
/// <param name="fromInclusive">The start index, inclusive.</param>
/// <param name="toExclusive">The end index, exclusive.</param>
/// <param name="configuration">The <see cref="Configuration"/> used for getting the <see cref="MemoryAllocator"/> and <see cref="ParallelOptions"/></param>
/// <param name="bufferLength">The length of the requested parallel buffer</param>
/// <param name="body">The delegate that is invoked once per iteration.</param>
public static void WithTemporaryBuffer<T>(
int fromInclusive,
int toExclusive,
Configuration configuration,
int bufferLength,
Action<int, IMemoryOwner<T>> body)
where T : struct
{
MemoryAllocator memoryAllocator = configuration.MemoryAllocator;
ParallelOptions parallelOptions = configuration.GetParallelOptions();
IMemoryOwner<T> InitBuffer()
{
return memoryAllocator.Allocate<T>(bufferLength);
}
void CleanUpBuffer(IMemoryOwner<T> buffer)
{
buffer.Dispose();
}
IMemoryOwner<T> BodyFunc(int i, ParallelLoopState state, IMemoryOwner<T> buffer)
{
body(i, buffer);
return buffer;
}
Parallel.For(fromInclusive, toExclusive, parallelOptions, InitBuffer, BodyFunc, CleanUpBuffer);
}
}
}

71
src/ImageSharp/Common/ParallelUtils/ParallelExecutionSettings.cs

@ -0,0 +1,71 @@
// Copyright(c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Threading.Tasks;
using SixLabors.Memory;
namespace SixLabors.ImageSharp.ParallelUtils
{
/// <summary>
/// Defines execution settings for methods in <see cref="ParallelHelper"/>.
/// </summary>
internal readonly struct ParallelExecutionSettings
{
/// <summary>
/// Default value for <see cref="MinimumPixelsProcessedPerTask"/>.
/// </summary>
public const int DefaultMinimumPixelsProcessedPerTask = 4096;
/// <summary>
/// Initializes a new instance of the <see cref="ParallelExecutionSettings"/> struct.
/// </summary>
public ParallelExecutionSettings(
int maxDegreeOfParallelism,
int minimumPixelsProcessedPerTask,
MemoryAllocator memoryAllocator)
{
this.MaxDegreeOfParallelism = maxDegreeOfParallelism;
this.MinimumPixelsProcessedPerTask = minimumPixelsProcessedPerTask;
this.MemoryAllocator = memoryAllocator;
}
/// <summary>
/// Initializes a new instance of the <see cref="ParallelExecutionSettings"/> struct.
/// </summary>
public ParallelExecutionSettings(int maxDegreeOfParallelism, MemoryAllocator memoryAllocator)
: this(maxDegreeOfParallelism, DefaultMinimumPixelsProcessedPerTask, memoryAllocator)
{
}
/// <summary>
/// Gets the MemoryAllocator
/// </summary>
public MemoryAllocator MemoryAllocator { get; }
/// <summary>
/// Gets the value used for initializing <see cref="ParallelOptions.MaxDegreeOfParallelism"/> when using TPL.
/// </summary>
public int MaxDegreeOfParallelism { get; }
/// <summary>
/// Gets the minimum number of pixels being processed by a single task when parallelizing operations with TPL.
/// Launching tasks for pixel regions below this limit is not worth the overhead.
/// Initialized with <see cref="DefaultMinimumPixelsProcessedPerTask"/> by default,
/// the optimum value is operation specific. (The cheaper the operation, the larger the value is.)
/// </summary>
public int MinimumPixelsProcessedPerTask { get; }
/// <summary>
/// Creates a new instance of <see cref="ParallelExecutionSettings"/>
/// having <see cref="MinimumPixelsProcessedPerTask"/> multiplied by <paramref name="multiplier"/>
/// </summary>
public ParallelExecutionSettings MultiplyMinimumPixelsPerTask(int multiplier)
{
return new ParallelExecutionSettings(
this.MaxDegreeOfParallelism,
this.MinimumPixelsProcessedPerTask * multiplier,
this.MemoryAllocator);
}
}
}

146
src/ImageSharp/Common/ParallelUtils/ParallelHelper.cs

@ -0,0 +1,146 @@
// Copyright(c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Memory;
using SixLabors.Memory;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.ParallelUtils
{
/// <summary>
/// Utility methods for batched processing of pixel row intervals.
/// Parallel execution is optimized for image processing.
/// Use this instead of direct <see cref="Parallel"/> calls!
/// </summary>
internal static class ParallelHelper
{
/// <summary>
/// Get the default <see cref="ParallelExecutionSettings"/> for a <see cref="Configuration"/>
/// </summary>
public static ParallelExecutionSettings GetParallelSettings(this Configuration configuration)
{
return new ParallelExecutionSettings(configuration.MaxDegreeOfParallelism, configuration.MemoryAllocator);
}
/// <summary>
/// Iterate through the rows of a rectangle in optimized batches defined by <see cref="RowInterval"/>-s.
/// </summary>
public static void IterateRows(Rectangle rectangle, Configuration configuration, Action<RowInterval> body)
{
ParallelExecutionSettings parallelSettings = configuration.GetParallelSettings();
IterateRows(rectangle, parallelSettings, body);
}
/// <summary>
/// Iterate through the rows of a rectangle in optimized batches defined by <see cref="RowInterval"/>-s.
/// </summary>
public static void IterateRows(
Rectangle rectangle,
in ParallelExecutionSettings parallelSettings,
Action<RowInterval> body)
{
DebugGuard.MustBeGreaterThan(rectangle.Width, 0, nameof(rectangle));
int maxSteps = DivideCeil(rectangle.Width * rectangle.Height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
// Avoid TPL overhead in this trivial case:
if (numOfSteps == 1)
{
var rows = new RowInterval(rectangle.Top, rectangle.Bottom);
body(rows);
return;
}
int verticalStep = DivideCeil(rectangle.Height, numOfSteps);
var parallelOptions = new ParallelOptions() { MaxDegreeOfParallelism = numOfSteps };
Parallel.For(
0,
numOfSteps,
parallelOptions,
i =>
{
int yMin = rectangle.Top + (i * verticalStep);
int yMax = Math.Min(yMin + verticalStep, rectangle.Bottom);
var rows = new RowInterval(yMin, yMax);
body(rows);
});
}
/// <summary>
/// Iterate through the rows of a rectangle in optimized batches defined by <see cref="RowInterval"/>-s
/// instantiating a temporary buffer for each <paramref name="body"/> invocation.
/// </summary>
public static void IterateRowsWithTempBuffer<T>(
Rectangle rectangle,
in ParallelExecutionSettings parallelSettings,
Action<RowInterval, Memory<T>> body)
where T : struct
{
int maxSteps = DivideCeil(rectangle.Width * rectangle.Height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
MemoryAllocator memoryAllocator = parallelSettings.MemoryAllocator;
// Avoid TPL overhead in this trivial case:
if (numOfSteps == 1)
{
var rows = new RowInterval(rectangle.Top, rectangle.Bottom);
using (IMemoryOwner<T> buffer = memoryAllocator.Allocate<T>(rectangle.Width))
{
body(rows, buffer.Memory);
}
return;
}
int verticalStep = DivideCeil(rectangle.Height, numOfSteps);
var parallelOptions = new ParallelOptions() { MaxDegreeOfParallelism = numOfSteps };
Parallel.For(
0,
numOfSteps,
parallelOptions,
i =>
{
int yMin = rectangle.Top + (i * verticalStep);
int yMax = Math.Min(yMin + verticalStep, rectangle.Bottom);
var rows = new RowInterval(yMin, yMax);
using (IMemoryOwner<T> buffer = memoryAllocator.Allocate<T>(rectangle.Width))
{
body(rows, buffer.Memory);
}
});
}
/// <summary>
/// Iterate through the rows of a rectangle in optimized batches defined by <see cref="RowInterval"/>-s
/// instantiating a temporary buffer for each <paramref name="body"/> invocation.
/// </summary>
public static void IterateRowsWithTempBuffer<T>(
Rectangle rectangle,
Configuration configuration,
Action<RowInterval, Memory<T>> body)
where T : struct
{
IterateRowsWithTempBuffer(rectangle, configuration.GetParallelSettings(), body);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int DivideCeil(int dividend, int divisor) => 1 + ((dividend - 1) / divisor);
}
}

1
src/ImageSharp/Configuration.cs

@ -59,6 +59,7 @@ namespace SixLabors.ImageSharp
/// <summary> /// <summary>
/// Gets or sets the maximum number of concurrent tasks enabled in ImageSharp algorithms /// Gets or sets the maximum number of concurrent tasks enabled in ImageSharp algorithms
/// configured with this <see cref="Configuration"/> instance. /// configured with this <see cref="Configuration"/> instance.
/// Initialized with <see cref="Environment.ProcessorCount"/> by default.
/// </summary> /// </summary>
public int MaxDegreeOfParallelism public int MaxDegreeOfParallelism
{ {

50
src/ImageSharp/ImageFrame{TPixel}.cs

@ -9,6 +9,7 @@ using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.MetaData; using SixLabors.ImageSharp.MetaData;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory; using SixLabors.Memory;
using SixLabors.Primitives; using SixLabors.Primitives;
@ -288,20 +289,24 @@ namespace SixLabors.ImageSharp
var target = new ImageFrame<TPixel2>(configuration, this.Width, this.Height, this.MetaData.DeepClone()); var target = new ImageFrame<TPixel2>(configuration, this.Width, this.Height, this.MetaData.DeepClone());
ParallelFor.WithTemporaryBuffer( ParallelHelper.IterateRowsWithTempBuffer<Vector4>(
0, this.Bounds(),
this.Height,
configuration, configuration,
this.Width, (rows, tempRowBuffer) =>
(int y, IMemoryOwner<Vector4> tempRowBuffer) => {
{ for (int y = rows.Min; y < rows.Max; y++)
Span<TPixel> sourceRow = this.GetPixelRowSpan(y); {
Span<TPixel2> targetRow = target.GetPixelRowSpan(y); Span<TPixel> sourceRow = this.GetPixelRowSpan(y);
Span<Vector4> tempRowSpan = tempRowBuffer.GetSpan(); Span<TPixel2> targetRow = target.GetPixelRowSpan(y);
Span<Vector4> tempRowSpan = tempRowBuffer.Span;
PixelOperations<TPixel>.Instance.ToScaledVector4(sourceRow, tempRowSpan, sourceRow.Length);
PixelOperations<TPixel2>.Instance.PackFromScaledVector4(tempRowSpan, targetRow, targetRow.Length); PixelOperations<TPixel>.Instance.ToScaledVector4(sourceRow, tempRowSpan, sourceRow.Length);
}); PixelOperations<TPixel2>.Instance.PackFromScaledVector4(
tempRowSpan,
targetRow,
targetRow.Length);
}
});
return target; return target;
} }
@ -313,15 +318,16 @@ namespace SixLabors.ImageSharp
/// <param name="value">The value to initialize the bitmap with.</param> /// <param name="value">The value to initialize the bitmap with.</param>
internal void Clear(ParallelOptions parallelOptions, TPixel value) internal void Clear(ParallelOptions parallelOptions, TPixel value)
{ {
Parallel.For( Span<TPixel> span = this.GetPixelSpan();
0,
this.Height, if (value.Equals(default))
parallelOptions, {
y => span.Clear();
{ }
Span<TPixel> targetRow = this.GetPixelRowSpan(y); else
targetRow.Fill(value); {
}); span.Fill(value);
}
} }
/// <inheritdoc/> /// <inheritdoc/>

3
src/ImageSharp/ImageSharp.csproj.DotSettings

@ -0,0 +1,3 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=common/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=common_005Cexceptions/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

20
src/ImageSharp/Memory/Buffer2DExtensions.cs

@ -96,15 +96,14 @@ namespace SixLabors.ImageSharp.Memory
/// <param name="buffer">The <see cref="Buffer2D{T}"/></param> /// <param name="buffer">The <see cref="Buffer2D{T}"/></param>
/// <param name="rectangle">The rectangle subarea</param> /// <param name="rectangle">The rectangle subarea</param>
/// <returns>The <see cref="BufferArea{T}"/></returns> /// <returns>The <see cref="BufferArea{T}"/></returns>
public static BufferArea<T> GetArea<T>(this Buffer2D<T> buffer, Rectangle rectangle) public static BufferArea<T> GetArea<T>(this Buffer2D<T> buffer, in Rectangle rectangle)
where T : struct => new BufferArea<T>(buffer, rectangle); where T : struct => new BufferArea<T>(buffer, rectangle);
public static BufferArea<T> GetArea<T>(this Buffer2D<T> buffer, int x, int y, int width, int height) public static BufferArea<T> GetArea<T>(this Buffer2D<T> buffer, int x, int y, int width, int height)
where T : struct where T : struct => new BufferArea<T>(buffer, new Rectangle(x, y, width, height));
{
var rectangle = new Rectangle(x, y, width, height); public static BufferArea<T> GetAreaBetweenRows<T>(this Buffer2D<T> buffer, int minY, int maxY)
return new BufferArea<T>(buffer, rectangle); where T : struct => new BufferArea<T>(buffer, new Rectangle(0, minY, buffer.Width, maxY - minY));
}
/// <summary> /// <summary>
/// Return a <see cref="BufferArea{T}"/> to the whole area of 'buffer' /// Return a <see cref="BufferArea{T}"/> to the whole area of 'buffer'
@ -114,5 +113,14 @@ namespace SixLabors.ImageSharp.Memory
/// <returns>The <see cref="BufferArea{T}"/></returns> /// <returns>The <see cref="BufferArea{T}"/></returns>
public static BufferArea<T> GetArea<T>(this Buffer2D<T> buffer) public static BufferArea<T> GetArea<T>(this Buffer2D<T> buffer)
where T : struct => new BufferArea<T>(buffer); where T : struct => new BufferArea<T>(buffer);
/// <summary>
/// Gets a span for all the pixels in <paramref name="buffer"/> defined by <paramref name="rows"/>
/// </summary>
public static Span<T> GetMultiRowSpan<T>(this Buffer2D<T> buffer, in RowInterval rows)
where T : struct
{
return buffer.Span.Slice(rows.Min * buffer.Width, rows.Height * buffer.Width);
}
} }
} }

45
src/ImageSharp/Memory/RowInterval.cs

@ -0,0 +1,45 @@
// Copyright(c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Memory
{
/// <summary>
/// Represents an interval of rows in a <see cref="Rectangle"/> and/or <see cref="Buffer2D{T}"/>
/// </summary>
internal readonly struct RowInterval
{
/// <summary>
/// Initializes a new instance of the <see cref="RowInterval"/> struct.
/// </summary>
public RowInterval(int min, int max)
{
DebugGuard.MustBeLessThan(min, max, nameof(min));
this.Min = min;
this.Max = max;
}
/// <summary>
/// Gets the INCLUSIVE minimum
/// </summary>
public int Min { get; }
/// <summary>
/// Gets the EXCLUSIVE maximum
/// </summary>
public int Max { get; }
/// <summary>
/// Gets the difference (<see cref="Max"/> - <see cref="Min"/>)
/// </summary>
public int Height => this.Max - this.Min;
/// <inheritdoc />
public override string ToString()
{
return $"RowInterval [{this.Min}->{this.Max}[";
}
}
}

43
src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor.cs

@ -2,10 +2,10 @@
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System; using System;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors;
using SixLabors.Primitives; using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Binarization namespace SixLabors.ImageSharp.Processing.Processors.Binarization
@ -56,7 +56,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization
public TPixel LowerColor { get; set; } public TPixel LowerColor { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration) protected override void OnFrameApply(
ImageFrame<TPixel> source,
Rectangle sourceRectangle,
Configuration configuration)
{ {
float threshold = this.Threshold * 255F; float threshold = this.Threshold * 255F;
TPixel upper = this.UpperColor; TPixel upper = this.UpperColor;
@ -70,25 +73,29 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization
bool isAlphaOnly = typeof(TPixel) == typeof(Alpha8); bool isAlphaOnly = typeof(TPixel) == typeof(Alpha8);
ParallelFor.WithConfiguration( var workingRect = Rectangle.FromLTRB(startX, startY, endX, endY);
startY,
endY, ParallelHelper.IterateRows(
workingRect,
configuration, configuration,
y => rows =>
{ {
Span<TPixel> row = source.GetPixelRowSpan(y); for (int y = rows.Min; y < rows.Max; y++)
Rgba32 rgba = default;
for (int x = startX; x < endX; x++)
{ {
ref TPixel color = ref row[x]; Span<TPixel> row = source.GetPixelRowSpan(y);
color.ToRgba32(ref rgba); Rgba32 rgba = default;
for (int x = startX; x < endX; x++)
{
ref TPixel color = ref row[x];
color.ToRgba32(ref rgba);
// Convert to grayscale using ITU-R Recommendation BT.709 if required // Convert to grayscale using ITU-R Recommendation BT.709 if required
float luminance = isAlphaOnly float luminance = isAlphaOnly
? rgba.A ? rgba.A
: (.2126F * rgba.R) + (.7152F * rgba.G) + (.0722F * rgba.B); : (.2126F * rgba.R) + (.7152F * rgba.G) + (.0722F * rgba.B);
color = luminance >= threshold ? upper : lower; color = luminance >= threshold ? upper : lower;
}
} }
}); });
} }

119
src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor.cs

@ -3,12 +3,12 @@
using System; using System;
using System.Numerics; using System.Numerics;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Primitives; using SixLabors.ImageSharp.Primitives;
using SixLabors.Memory;
using SixLabors.Primitives; using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Convolution namespace SixLabors.ImageSharp.Processing.Processors.Convolution
@ -42,7 +42,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
public DenseMatrix<float> KernelY { get; } public DenseMatrix<float> KernelY { get; }
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration) protected override void OnFrameApply(
ImageFrame<TPixel> source,
Rectangle sourceRectangle,
Configuration configuration)
{ {
int kernelYHeight = this.KernelY.Rows; int kernelYHeight = this.KernelY.Rows;
int kernelYWidth = this.KernelY.Columns; int kernelYWidth = this.KernelY.Columns;
@ -58,71 +61,77 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
int maxY = endY - 1; int maxY = endY - 1;
int maxX = endX - 1; int maxX = endX - 1;
using (Buffer2D<TPixel> targetPixels = configuration.MemoryAllocator.Allocate2D<TPixel>(source.Width, source.Height)) using (Buffer2D<TPixel> targetPixels =
configuration.MemoryAllocator.Allocate2D<TPixel>(source.Width, source.Height))
{ {
source.CopyTo(targetPixels); source.CopyTo(targetPixels);
ParallelFor.WithConfiguration( var workingRectangle = Rectangle.FromLTRB(startX, startY, endX, endY);
startY,
endY,
configuration,
y =>
{
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
Span<TPixel> targetRow = targetPixels.GetRowSpan(y);
for (int x = startX; x < endX; x++) ParallelHelper.IterateRows(
workingRectangle,
configuration,
rows =>
{ {
float rX = 0; for (int y = rows.Min; y < rows.Max; y++)
float gX = 0;
float bX = 0;
float rY = 0;
float gY = 0;
float bY = 0;
// Apply each matrix multiplier to the color components for each pixel.
for (int fy = 0; fy < kernelYHeight; fy++)
{ {
int fyr = fy - radiusY; Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
int offsetY = y + fyr; Span<TPixel> targetRow = targetPixels.GetRowSpan(y);
offsetY = offsetY.Clamp(0, maxY); for (int x = startX; x < endX; x++)
Span<TPixel> sourceOffsetRow = source.GetPixelRowSpan(offsetY);
for (int fx = 0; fx < kernelXWidth; fx++)
{ {
int fxr = fx - radiusX; float rX = 0;
int offsetX = x + fxr; float gX = 0;
float bX = 0;
offsetX = offsetX.Clamp(0, maxX); float rY = 0;
Vector4 currentColor = sourceOffsetRow[offsetX].ToVector4().Premultiply(); float gY = 0;
float bY = 0;
if (fy < kernelXHeight)
// Apply each matrix multiplier to the color components for each pixel.
for (int fy = 0; fy < kernelYHeight; fy++)
{ {
Vector4 kx = this.KernelX[fy, fx] * currentColor; int fyr = fy - radiusY;
rX += kx.X; int offsetY = y + fyr;
gX += kx.Y;
bX += kx.Z; offsetY = offsetY.Clamp(0, maxY);
Span<TPixel> sourceOffsetRow = source.GetPixelRowSpan(offsetY);
for (int fx = 0; fx < kernelXWidth; fx++)
{
int fxr = fx - radiusX;
int offsetX = x + fxr;
offsetX = offsetX.Clamp(0, maxX);
Vector4 currentColor = sourceOffsetRow[offsetX].ToVector4().Premultiply();
if (fy < kernelXHeight)
{
Vector4 kx = this.KernelX[fy, fx] * currentColor;
rX += kx.X;
gX += kx.Y;
bX += kx.Z;
}
if (fx < kernelYWidth)
{
Vector4 ky = this.KernelY[fy, fx] * currentColor;
rY += ky.X;
gY += ky.Y;
bY += ky.Z;
}
}
} }
if (fx < kernelYWidth) float red = MathF.Sqrt((rX * rX) + (rY * rY));
{ float green = MathF.Sqrt((gX * gX) + (gY * gY));
Vector4 ky = this.KernelY[fy, fx] * currentColor; float blue = MathF.Sqrt((bX * bX) + (bY * bY));
rY += ky.X;
gY += ky.Y; ref TPixel pixel = ref targetRow[x];
bY += ky.Z; pixel.PackFromVector4(
} new Vector4(red, green, blue, sourceRow[x].ToVector4().W).UnPremultiply());
} }
} }
});
float red = MathF.Sqrt((rX * rX) + (rY * rY));
float green = MathF.Sqrt((gX * gX) + (gY * gY));
float blue = MathF.Sqrt((bX * bX) + (bY * bY));
ref TPixel pixel = ref targetRow[x];
pixel.PackFromVector4(new Vector4(red, green, blue, sourceRow[x].ToVector4().W).UnPremultiply());
}
});
Buffer2D<TPixel>.SwapOrCopyContent(source.PixelBuffer, targetPixels); Buffer2D<TPixel>.SwapOrCopyContent(source.PixelBuffer, targetPixels);
} }

61
src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor.cs

@ -6,6 +6,7 @@ using System.Numerics;
using System.Threading.Tasks; using System.Threading.Tasks;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Primitives; using SixLabors.ImageSharp.Primitives;
using SixLabors.ImageSharp.Processing.Processors; using SixLabors.ImageSharp.Processing.Processors;
@ -82,43 +83,47 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
int maxY = endY - 1; int maxY = endY - 1;
int maxX = endX - 1; int maxX = endX - 1;
ParallelFor.WithConfiguration( var workingRectangle = Rectangle.FromLTRB(startX, startY, endX, endY);
startY,
endY,
configuration,
y =>
{
Span<TPixel> targetRow = targetPixels.GetRowSpan(y);
for (int x = startX; x < endX; x++) ParallelHelper.IterateRows(
workingRectangle,
configuration,
rows =>
{ {
Vector4 destination = default; for (int y = rows.Min; y < rows.Max; y++)
// Apply each matrix multiplier to the color components for each pixel.
for (int fy = 0; fy < kernelHeight; fy++)
{ {
int fyr = fy - radiusY; Span<TPixel> targetRow = targetPixels.GetRowSpan(y);
int offsetY = y + fyr;
offsetY = offsetY.Clamp(0, maxY); for (int x = startX; x < endX; x++)
Span<TPixel> row = sourcePixels.GetRowSpan(offsetY);
for (int fx = 0; fx < kernelWidth; fx++)
{ {
int fxr = fx - radiusX; Vector4 destination = default;
int offsetX = x + fxr;
// Apply each matrix multiplier to the color components for each pixel.
for (int fy = 0; fy < kernelHeight; fy++)
{
int fyr = fy - radiusY;
int offsetY = y + fyr;
offsetX = offsetX.Clamp(0, maxX); offsetY = offsetY.Clamp(0, maxY);
Span<TPixel> row = sourcePixels.GetRowSpan(offsetY);
Vector4 currentColor = row[offsetX].ToVector4().Premultiply(); for (int fx = 0; fx < kernelWidth; fx++)
destination += kernel[fy, fx] * currentColor; {
int fxr = fx - radiusX;
int offsetX = x + fxr;
offsetX = offsetX.Clamp(0, maxX);
Vector4 currentColor = row[offsetX].ToVector4().Premultiply();
destination += kernel[fy, fx] * currentColor;
}
}
ref TPixel pixel = ref targetRow[x];
pixel.PackFromVector4(destination.UnPremultiply());
} }
} }
});
ref TPixel pixel = ref targetRow[x];
pixel.PackFromVector4(destination.UnPremultiply());
}
});
} }
} }
} }

94
src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor.cs

@ -6,6 +6,7 @@ using System.Numerics;
using System.Threading.Tasks; using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Primitives; using SixLabors.ImageSharp.Primitives;
using SixLabors.ImageSharp.Processing.Processors; using SixLabors.ImageSharp.Processing.Processors;
@ -52,50 +53,55 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
{ {
source.CopyTo(targetPixels); source.CopyTo(targetPixels);
ParallelFor.WithConfiguration( var workingRect = Rectangle.FromLTRB(startX, startY, endX, endY);
startY,
endY, ParallelHelper.IterateRows(
configuration, workingRect,
y => configuration,
{ rows =>
Span<TPixel> sourceRow = source.GetPixelRowSpan(y); {
Span<TPixel> targetRow = targetPixels.GetRowSpan(y); for (int y = rows.Min; y < rows.Max; y++)
{
for (int x = startX; x < endX; x++) Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
{ Span<TPixel> targetRow = targetPixels.GetRowSpan(y);
float red = 0;
float green = 0; for (int x = startX; x < endX; x++)
float blue = 0; {
float red = 0;
// Apply each matrix multiplier to the color components for each pixel. float green = 0;
for (int fy = 0; fy < kernelLength; fy++) float blue = 0;
{
int fyr = fy - radius; // Apply each matrix multiplier to the color components for each pixel.
int offsetY = y + fyr; for (int fy = 0; fy < kernelLength; fy++)
{
offsetY = offsetY.Clamp(0, maxY); int fyr = fy - radius;
Span<TPixel> sourceOffsetRow = source.GetPixelRowSpan(offsetY); int offsetY = y + fyr;
for (int fx = 0; fx < kernelLength; fx++) offsetY = offsetY.Clamp(0, maxY);
{ Span<TPixel> sourceOffsetRow = source.GetPixelRowSpan(offsetY);
int fxr = fx - radius;
int offsetX = x + fxr; for (int fx = 0; fx < kernelLength; fx++)
{
offsetX = offsetX.Clamp(0, maxX); int fxr = fx - radius;
int offsetX = x + fxr;
Vector4 currentColor = sourceOffsetRow[offsetX].ToVector4().Premultiply();
currentColor *= this.KernelXY[fy, fx]; offsetX = offsetX.Clamp(0, maxX);
red += currentColor.X; Vector4 currentColor = sourceOffsetRow[offsetX].ToVector4().Premultiply();
green += currentColor.Y; currentColor *= this.KernelXY[fy, fx];
blue += currentColor.Z;
} red += currentColor.X;
} green += currentColor.Y;
blue += currentColor.Z;
ref TPixel pixel = ref targetRow[x]; }
pixel.PackFromVector4(new Vector4(red, green, blue, sourceRow[x].ToVector4().W).UnPremultiply()); }
}
}); ref TPixel pixel = ref targetRow[x];
pixel.PackFromVector4(
new Vector4(red, green, blue, sourceRow[x].ToVector4().W).UnPremultiply());
}
}
});
Buffer2D<TPixel>.SwapOrCopyContent(source.PixelBuffer, targetPixels); Buffer2D<TPixel>.SwapOrCopyContent(source.PixelBuffer, targetPixels);
} }

42
src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor.cs

@ -8,6 +8,7 @@ using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Primitives; using SixLabors.ImageSharp.Primitives;
using SixLabors.ImageSharp.Processing.Processors.Filters; using SixLabors.ImageSharp.Processing.Processors.Filters;
@ -124,6 +125,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
shiftY = 0; shiftY = 0;
} }
var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY);
// Additional runs. // Additional runs.
// ReSharper disable once ForCanBeConvertedToForeach // ReSharper disable once ForCanBeConvertedToForeach
for (int i = 1; i < kernels.Length; i++) for (int i = 1; i < kernels.Length; i++)
@ -135,30 +138,35 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
Buffer2D<TPixel> passPixels = pass.PixelBuffer; Buffer2D<TPixel> passPixels = pass.PixelBuffer;
Buffer2D<TPixel> targetPixels = source.PixelBuffer; Buffer2D<TPixel> targetPixels = source.PixelBuffer;
ParallelFor.WithConfiguration( ParallelHelper.IterateRows(
minY, workingRect,
maxY,
configuration, configuration,
y => rows =>
{ {
int offsetY = y - shiftY; for (int y = rows.Min; y < rows.Max; y++)
{
int offsetY = y - shiftY;
ref TPixel passPixelsBase = ref MemoryMarshal.GetReference(passPixels.GetRowSpan(offsetY)); ref TPixel passPixelsBase =
ref TPixel targetPixelsBase = ref MemoryMarshal.GetReference(targetPixels.GetRowSpan(offsetY)); ref MemoryMarshal.GetReference(passPixels.GetRowSpan(offsetY));
ref TPixel targetPixelsBase =
ref MemoryMarshal.GetReference(targetPixels.GetRowSpan(offsetY));
for (int x = minX; x < maxX; x++) for (int x = minX; x < maxX; x++)
{ {
int offsetX = x - shiftX; int offsetX = x - shiftX;
// Grab the max components of the two pixels // Grab the max components of the two pixels
ref TPixel currentPassPixel = ref Unsafe.Add(ref passPixelsBase, offsetX); ref TPixel currentPassPixel = ref Unsafe.Add(ref passPixelsBase, offsetX);
ref TPixel currentTargetPixel = ref Unsafe.Add(ref targetPixelsBase, offsetX); ref TPixel currentTargetPixel =
ref Unsafe.Add(ref targetPixelsBase, offsetX);
var pixelValue = Vector4.Max( var pixelValue = Vector4.Max(
currentPassPixel.ToVector4(), currentPassPixel.ToVector4(),
currentTargetPixel.ToVector4()); currentTargetPixel.ToVector4());
currentTargetPixel.PackFromVector4(pixelValue); currentTargetPixel.PackFromVector4(pixelValue);
}
} }
}); });
} }

110
src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor.cs

@ -3,11 +3,11 @@
using System; using System;
using System.Numerics; using System.Numerics;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
using SixLabors.Primitives; using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Effects namespace SixLabors.ImageSharp.Processing.Processors.Effects
@ -49,7 +49,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Effects
public int BrushSize { get; } public int BrushSize { get; }
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration) protected override void OnFrameApply(
ImageFrame<TPixel> source,
Rectangle sourceRectangle,
Configuration configuration)
{ {
if (this.BrushSize <= 0 || this.BrushSize > source.Height || this.BrushSize > source.Width) if (this.BrushSize <= 0 || this.BrushSize > source.Height || this.BrushSize > source.Width)
{ {
@ -70,69 +73,74 @@ namespace SixLabors.ImageSharp.Processing.Processors.Effects
{ {
source.CopyTo(targetPixels); source.CopyTo(targetPixels);
ParallelFor.WithConfiguration( var workingRect = Rectangle.FromLTRB(startX, startY, endX, endY);
startY, ParallelHelper.IterateRows(
maxY, workingRect,
configuration, configuration,
y => rows =>
{
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
Span<TPixel> targetRow = targetPixels.GetRowSpan(y);
for (int x = startX; x < endX; x++)
{ {
int maxIntensity = 0; for (int y = rows.Min; y < rows.Max; y++)
int maxIndex = 0; {
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
Span<TPixel> targetRow = targetPixels.GetRowSpan(y);
int[] intensityBin = new int[levels]; for (int x = startX; x < endX; x++)
float[] redBin = new float[levels]; {
float[] blueBin = new float[levels]; int maxIntensity = 0;
float[] greenBin = new float[levels]; int maxIndex = 0;
for (int fy = 0; fy <= radius; fy++) int[] intensityBin = new int[levels];
{ float[] redBin = new float[levels];
int fyr = fy - radius; float[] blueBin = new float[levels];
int offsetY = y + fyr; float[] greenBin = new float[levels];
offsetY = offsetY.Clamp(0, maxY); for (int fy = 0; fy <= radius; fy++)
{
int fyr = fy - radius;
int offsetY = y + fyr;
Span<TPixel> sourceOffsetRow = source.GetPixelRowSpan(offsetY); offsetY = offsetY.Clamp(0, maxY);
for (int fx = 0; fx <= radius; fx++) Span<TPixel> sourceOffsetRow = source.GetPixelRowSpan(offsetY);
{
int fxr = fx - radius;
int offsetX = x + fxr;
offsetX = offsetX.Clamp(0, maxX);
var vector = sourceOffsetRow[offsetX].ToVector4(); for (int fx = 0; fx <= radius; fx++)
{
int fxr = fx - radius;
int offsetX = x + fxr;
offsetX = offsetX.Clamp(0, maxX);
float sourceRed = vector.X; var vector = sourceOffsetRow[offsetX].ToVector4();
float sourceBlue = vector.Z;
float sourceGreen = vector.Y;
int currentIntensity = (int)MathF.Round((sourceBlue + sourceGreen + sourceRed) / 3F * (levels - 1)); float sourceRed = vector.X;
float sourceBlue = vector.Z;
float sourceGreen = vector.Y;
intensityBin[currentIntensity]++; int currentIntensity = (int)MathF.Round(
blueBin[currentIntensity] += sourceBlue; (sourceBlue + sourceGreen + sourceRed) / 3F * (levels - 1));
greenBin[currentIntensity] += sourceGreen;
redBin[currentIntensity] += sourceRed;
if (intensityBin[currentIntensity] > maxIntensity) intensityBin[currentIntensity]++;
{ blueBin[currentIntensity] += sourceBlue;
maxIntensity = intensityBin[currentIntensity]; greenBin[currentIntensity] += sourceGreen;
maxIndex = currentIntensity; redBin[currentIntensity] += sourceRed;
}
}
float red = MathF.Abs(redBin[maxIndex] / maxIntensity); if (intensityBin[currentIntensity] > maxIntensity)
float green = MathF.Abs(greenBin[maxIndex] / maxIntensity); {
float blue = MathF.Abs(blueBin[maxIndex] / maxIntensity); maxIntensity = intensityBin[currentIntensity];
maxIndex = currentIntensity;
}
}
ref TPixel pixel = ref targetRow[x]; float red = MathF.Abs(redBin[maxIndex] / maxIntensity);
pixel.PackFromVector4(new Vector4(red, green, blue, sourceRow[x].ToVector4().W)); float green = MathF.Abs(greenBin[maxIndex] / maxIntensity);
float blue = MathF.Abs(blueBin[maxIndex] / maxIntensity);
ref TPixel pixel = ref targetRow[x];
pixel.PackFromVector4(
new Vector4(red, green, blue, sourceRow[x].ToVector4().W));
}
}
} }
} });
});
Buffer2D<TPixel>.SwapOrCopyContent(source.PixelBuffer, targetPixels); Buffer2D<TPixel>.SwapOrCopyContent(source.PixelBuffer, targetPixels);
} }

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

@ -5,6 +5,7 @@ using System;
using System.Numerics; using System.Numerics;
using System.Threading.Tasks; using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Primitives; using SixLabors.Primitives;
@ -35,25 +36,24 @@ namespace SixLabors.ImageSharp.Processing.Processors.Filters
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration) protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration)
{ {
var interest = Rectangle.Intersect(sourceRectangle, source.Bounds()); var interest = Rectangle.Intersect(sourceRectangle, source.Bounds());
int startY = interest.Y;
int endY = interest.Bottom;
int startX = interest.X;
int endX = interest.Right;
Matrix4x4 matrix = this.Matrix; Matrix4x4 matrix = this.Matrix;
ParallelFor.WithConfiguration( ParallelHelper.IterateRows(
startY, interest,
endY,
configuration, configuration,
y => rows =>
{ {
Span<TPixel> row = source.GetPixelRowSpan(y); for (int y = rows.Min; y < rows.Max; y++)
for (int x = startX; x < endX; x++)
{ {
ref TPixel pixel = ref row[x]; Span<TPixel> row = source.GetPixelRowSpan(y);
var vector = Vector4.Transform(pixel.ToVector4(), matrix);
pixel.PackFromVector4(vector); for (int x = interest.X; x < interest.Right; x++)
{
ref TPixel pixel = ref row[x];
var vector = Vector4.Transform(pixel.ToVector4(), matrix);
pixel.PackFromVector4(vector);
}
} }
}); });
} }

38
src/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor.cs

@ -6,6 +6,7 @@ using System.Buffers;
using System.Threading.Tasks; using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory; using SixLabors.Memory;
using SixLabors.Primitives; using SixLabors.Primitives;
@ -67,6 +68,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays
int width = maxX - minX; int width = maxX - minX;
var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY);
using (IMemoryOwner<TPixel> colors = source.MemoryAllocator.Allocate<TPixel>(width)) using (IMemoryOwner<TPixel> colors = source.MemoryAllocator.Allocate<TPixel>(width))
using (IMemoryOwner<float> amount = source.MemoryAllocator.Allocate<float>(width)) using (IMemoryOwner<float> amount = source.MemoryAllocator.Allocate<float>(width))
{ {
@ -74,25 +77,30 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays
Span<TPixel> colorSpan = colors.GetSpan(); Span<TPixel> colorSpan = colors.GetSpan();
Span<float> amountSpan = amount.GetSpan(); Span<float> amountSpan = amount.GetSpan();
// TODO: Use Span.Fill? colorSpan.Fill(this.Color);
for (int i = 0; i < width; i++) amountSpan.Fill(this.GraphicsOptions.BlendPercentage);
{
colorSpan[i] = this.Color;
amountSpan[i] = this.GraphicsOptions.BlendPercentage;
}
PixelBlender<TPixel> blender = PixelOperations<TPixel>.Instance.GetPixelBlender(this.GraphicsOptions); PixelBlender<TPixel> blender = PixelOperations<TPixel>.Instance.GetPixelBlender(this.GraphicsOptions);
ParallelFor.WithConfiguration(
minY, ParallelHelper.IterateRows(
maxY, workingRect,
configuration, configuration,
y => rows =>
{ {
Span<TPixel> destination = source.GetPixelRowSpan(y - startY).Slice(minX - startX, width); for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> destination =
source.GetPixelRowSpan(y - startY).Slice(minX - startX, width);
// This switched color & destination in the 2nd and 3rd places because we are applying the target color under the current one // This switched color & destination in the 2nd and 3rd places because we are applying the target color under the current one
blender.Blend(source.MemoryAllocator, destination, colors.GetSpan(), destination, amount.GetSpan()); blender.Blend(
}); source.MemoryAllocator,
destination,
colors.GetSpan(),
destination,
amount.GetSpan());
}
});
} }
} }
} }

59
src/ImageSharp/Processing/Processors/Overlays/GlowProcessor.cs

@ -7,6 +7,7 @@ using System.Numerics;
using System.Threading.Tasks; using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Primitives; using SixLabors.ImageSharp.Primitives;
using SixLabors.Memory; using SixLabors.Memory;
@ -84,6 +85,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration) protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration)
{ {
// TODO: can we simplify the rectangle calculation?
int startY = sourceRectangle.Y; int startY = sourceRectangle.Y;
int endY = sourceRectangle.Bottom; int endY = sourceRectangle.Bottom;
int startX = sourceRectangle.X; int startX = sourceRectangle.X;
@ -113,36 +115,43 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays
} }
int width = maxX - minX; int width = maxX - minX;
int offsetX = minX - startX;
var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY);
using (IMemoryOwner<TPixel> rowColors = source.MemoryAllocator.Allocate<TPixel>(width)) using (IMemoryOwner<TPixel> rowColors = source.MemoryAllocator.Allocate<TPixel>(width))
{ {
// Be careful! Do not capture rowColorsSpan in the lambda below! rowColors.GetSpan().Fill(glowColor);
Span<TPixel> rowColorsSpan = rowColors.GetSpan();
for (int i = 0; i < width; i++)
{
rowColorsSpan[i] = glowColor;
}
ParallelFor.WithTemporaryBuffer<float>( ParallelHelper.IterateRowsWithTempBuffer<float>(
minY, workingRect,
maxY,
configuration, configuration,
width, (rows, amounts) =>
(y, amounts) =>
{
Span<float> amountsSpan = amounts.GetSpan();
int offsetY = y - startY;
int offsetX = minX - startX;
for (int i = 0; i < width; i++)
{ {
float distance = Vector2.Distance(center, new Vector2(i + offsetX, offsetY)); Span<float> amountsSpan = amounts.Span;
amountsSpan[i] = (this.GraphicsOptions.BlendPercentage * (1 - (.95F * (distance / maxDistance)))).Clamp(0, 1);
} for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> destination = source.GetPixelRowSpan(offsetY).Slice(offsetX, width); int offsetY = y - startY;
this.blender.Blend(source.MemoryAllocator, destination, destination, rowColors.GetSpan(), amountsSpan); for (int i = 0; i < width; i++)
}); {
float distance = Vector2.Distance(center, new Vector2(i + offsetX, offsetY));
amountsSpan[i] =
(this.GraphicsOptions.BlendPercentage * (1 - (.95F * (distance / maxDistance))))
.Clamp(0, 1);
}
Span<TPixel> destination = source.GetPixelRowSpan(offsetY).Slice(offsetX, width);
this.blender.Blend(
source.MemoryAllocator,
destination,
destination,
rowColors.GetSpan(),
amountsSpan);
}
});
} }
} }
} }

61
src/ImageSharp/Processing/Processors/Overlays/VignetteProcessor.cs

@ -7,6 +7,7 @@ using System.Numerics;
using System.Threading.Tasks; using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Primitives; using SixLabors.ImageSharp.Primitives;
using SixLabors.Memory; using SixLabors.Memory;
@ -115,43 +116,43 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays
} }
int width = maxX - minX; int width = maxX - minX;
int offsetX = minX - startX;
var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY);
using (IMemoryOwner<TPixel> rowColors = source.MemoryAllocator.Allocate<TPixel>(width)) using (IMemoryOwner<TPixel> rowColors = source.MemoryAllocator.Allocate<TPixel>(width))
{ {
// Be careful! Do not capture rowColorsSpan in the lambda below! rowColors.GetSpan().Fill(vignetteColor);
Span<TPixel> rowColorsSpan = rowColors.GetSpan();
for (int i = 0; i < width; i++)
{
rowColorsSpan[i] = vignetteColor;
}
ParallelFor.WithTemporaryBuffer<float>( ParallelHelper.IterateRowsWithTempBuffer<float>(
minY, workingRect,
maxY,
configuration, configuration,
width, (rows, amounts) =>
(y, amounts) =>
{ {
Span<float> amountsSpan = amounts.GetSpan(); Span<float> amountsSpan = amounts.Span;
int offsetY = y - startY;
int offsetX = minX - startX; for (int y = rows.Min; y < rows.Max; y++)
for (int i = 0; i < width; i++)
{ {
float distance = Vector2.Distance(centre, new Vector2(i + offsetX, offsetY)); int offsetY = y - startY;
amountsSpan[i] =
(this.GraphicsOptions.BlendPercentage * (.9F * (distance / maxDistance))).Clamp( for (int i = 0; i < width; i++)
0, {
1); float distance = Vector2.Distance(centre, new Vector2(i + offsetX, offsetY));
amountsSpan[i] =
(this.GraphicsOptions.BlendPercentage * (.9F * (distance / maxDistance))).Clamp(
0,
1);
}
Span<TPixel> destination = source.GetPixelRowSpan(offsetY).Slice(offsetX, width);
this.blender.Blend(
source.MemoryAllocator,
destination,
destination,
rowColors.GetSpan(),
amountsSpan);
} }
Span<TPixel> destination = source.GetPixelRowSpan(offsetY).Slice(offsetX, width);
this.blender.Blend(
source.MemoryAllocator,
destination,
destination,
rowColors.GetSpan(),
amountsSpan);
}); });
} }
} }

186
src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs

@ -10,6 +10,7 @@ using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory; using SixLabors.Memory;
using SixLabors.Primitives; using SixLabors.Primitives;
@ -78,23 +79,25 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
if (this.Sampler is NearestNeighborResampler) if (this.Sampler is NearestNeighborResampler)
{ {
ParallelFor.WithConfiguration( ParallelHelper.IterateRows(
0, targetBounds,
height,
configuration, configuration,
y => rows =>
{
Span<TPixel> destRow = destination.GetPixelRowSpan(y);
for (int x = 0; x < width; x++)
{ {
var point = Point.Transform(new Point(x, y), matrix); for (int y = rows.Min; y < rows.Max; y++)
if (sourceBounds.Contains(point.X, point.Y))
{ {
destRow[x] = source[point.X, point.Y]; Span<TPixel> destRow = destination.GetPixelRowSpan(y);
for (int x = 0; x < width; x++)
{
var point = Point.Transform(new Point(x, y), matrix);
if (sourceBounds.Contains(point.X, point.Y))
{
destRow[x] = source[point.X, point.Y];
}
}
} }
} });
});
return; return;
} }
@ -116,86 +119,107 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
using (Buffer2D<float> yBuffer = memoryAllocator.Allocate2D<float>(yLength, height)) using (Buffer2D<float> yBuffer = memoryAllocator.Allocate2D<float>(yLength, height))
using (Buffer2D<float> xBuffer = memoryAllocator.Allocate2D<float>(xLength, height)) using (Buffer2D<float> xBuffer = memoryAllocator.Allocate2D<float>(xLength, height))
{ {
ParallelFor.WithConfiguration( ParallelHelper.IterateRows(
0, targetBounds,
height,
configuration, configuration,
y => rows =>
{ {
ref TPixel destRowRef = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y)); for (int y = rows.Min; y < rows.Max; y++)
ref float ySpanRef = ref MemoryMarshal.GetReference(yBuffer.GetRowSpan(y));
ref float xSpanRef = ref MemoryMarshal.GetReference(xBuffer.GetRowSpan(y));
for (int x = 0; x < width; x++)
{ {
// Use the single precision position to calculate correct bounding pixels ref TPixel destRowRef = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y));
// otherwise we get rogue pixels outside of the bounds. ref float ySpanRef = ref MemoryMarshal.GetReference(yBuffer.GetRowSpan(y));
var point = Vector2.Transform(new Vector2(x, y), matrix); ref float xSpanRef = ref MemoryMarshal.GetReference(xBuffer.GetRowSpan(y));
// Clamp sampling pixel radial extents to the source image edges
Vector2 maxXY = point + radius;
Vector2 minXY = point - radius;
// max, maxY, minX, minY
var extents = new Vector4(
MathF.Floor(maxXY.X + .5F),
MathF.Floor(maxXY.Y + .5F),
MathF.Ceiling(minXY.X - .5F),
MathF.Ceiling(minXY.Y - .5F));
int right = (int)extents.X;
int bottom = (int)extents.Y;
int left = (int)extents.Z;
int top = (int)extents.W;
extents = Vector4.Clamp(extents, Vector4.Zero, maxSource);
int maxX = (int)extents.X;
int maxY = (int)extents.Y;
int minX = (int)extents.Z;
int minY = (int)extents.W;
if (minX == maxX || minY == maxY)
{
continue;
}
// It appears these have to be calculated on-the-fly. for (int x = 0; x < width; x++)
// Precalulating transformed weights would require prior knowledge of every transformed pixel location
// since they can be at sub-pixel positions on both axis.
// I've optimized where I can but am always open to suggestions.
if (yScale > 1 && xScale > 1)
{
CalculateWeightsDown(top, bottom, minY, maxY, point.Y, sampler, yScale, ref ySpanRef, yLength);
CalculateWeightsDown(left, right, minX, maxX, point.X, sampler, xScale, ref xSpanRef, xLength);
}
else
{ {
CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ref ySpanRef); // Use the single precision position to calculate correct bounding pixels
CalculateWeightsScaleUp(minX, maxX, point.X, sampler, ref xSpanRef); // otherwise we get rogue pixels outside of the bounds.
} var point = Vector2.Transform(new Vector2(x, y), matrix);
// Clamp sampling pixel radial extents to the source image edges
Vector2 maxXY = point + radius;
Vector2 minXY = point - radius;
// max, maxY, minX, minY
var extents = new Vector4(
MathF.Floor(maxXY.X + .5F),
MathF.Floor(maxXY.Y + .5F),
MathF.Ceiling(minXY.X - .5F),
MathF.Ceiling(minXY.Y - .5F));
int right = (int)extents.X;
int bottom = (int)extents.Y;
int left = (int)extents.Z;
int top = (int)extents.W;
extents = Vector4.Clamp(extents, Vector4.Zero, maxSource);
int maxX = (int)extents.X;
int maxY = (int)extents.Y;
int minX = (int)extents.Z;
int minY = (int)extents.W;
if (minX == maxX || minY == maxY)
{
continue;
}
// Now multiply the results against the offsets // It appears these have to be calculated on-the-fly.
Vector4 sum = Vector4.Zero; // Precalculating transformed weights would require prior knowledge of every transformed pixel location
for (int yy = 0, j = minY; j <= maxY; j++, yy++) // since they can be at sub-pixel positions on both axis.
{ // I've optimized where I can but am always open to suggestions.
float yWeight = Unsafe.Add(ref ySpanRef, yy); if (yScale > 1 && xScale > 1)
{
CalculateWeightsDown(
top,
bottom,
minY,
maxY,
point.Y,
sampler,
yScale,
ref ySpanRef,
yLength);
CalculateWeightsDown(
left,
right,
minX,
maxX,
point.X,
sampler,
xScale,
ref xSpanRef,
xLength);
}
else
{
CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ref ySpanRef);
CalculateWeightsScaleUp(minX, maxX, point.X, sampler, ref xSpanRef);
}
for (int xx = 0, i = minX; i <= maxX; i++, xx++) // Now multiply the results against the offsets
Vector4 sum = Vector4.Zero;
for (int yy = 0, j = minY; j <= maxY; j++, yy++)
{ {
float xWeight = Unsafe.Add(ref xSpanRef, xx); float yWeight = Unsafe.Add(ref ySpanRef, yy);
var vector = source[i, j].ToVector4();
for (int xx = 0, i = minX; i <= maxX; i++, xx++)
{
float xWeight = Unsafe.Add(ref xSpanRef, xx);
var vector = source[i, j].ToVector4();
// Values are first premultiplied to prevent darkening of edge pixels // Values are first premultiplied to prevent darkening of edge pixels
Vector4 multiplied = vector.Premultiply(); Vector4 multiplied = vector.Premultiply();
sum += multiplied * xWeight * yWeight; sum += multiplied * xWeight * yWeight;
}
} }
}
ref TPixel dest = ref Unsafe.Add(ref destRowRef, x); ref TPixel dest = ref Unsafe.Add(ref destRowRef, x);
// Reverse the premultiplication // Reverse the premultiplication
dest.PackFromVector4(sum.UnPremultiply()); dest.PackFromVector4(sum.UnPremultiply());
}
} }
}); });
} }

32
src/ImageSharp/Processing/Processors/Transforms/CropProcessor.cs

@ -4,8 +4,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Primitives; using SixLabors.Primitives;
@ -53,21 +53,23 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
return; return;
} }
int minY = Math.Max(this.CropRectangle.Y, sourceRectangle.Y); var rect = Rectangle.Intersect(this.CropRectangle, sourceRectangle);
int maxY = Math.Min(this.CropRectangle.Bottom, sourceRectangle.Bottom);
int minX = Math.Max(this.CropRectangle.X, sourceRectangle.X);
int maxX = Math.Min(this.CropRectangle.Right, sourceRectangle.Right);
ParallelFor.WithConfiguration( // Copying is cheap, we should process more pixels per task:
minY, ParallelExecutionSettings parallelSettings = configuration.GetParallelSettings().MultiplyMinimumPixelsPerTask(4);
maxY,
configuration, ParallelHelper.IterateRows(
y => rect,
{ parallelSettings,
Span<TPixel> sourceRow = source.GetPixelRowSpan(y).Slice(minX); rows =>
Span<TPixel> targetRow = destination.GetPixelRowSpan(y - minY); {
sourceRow.Slice(0, maxX - minX).CopyTo(targetRow); for (int y = rows.Min; y < rows.Max; y++)
}); {
Span<TPixel> sourceRow = source.GetPixelRowSpan(y).Slice(rect.Left);
Span<TPixel> targetRow = destination.GetPixelRowSpan(y - rect.Top);
sourceRow.Slice(0, rect.Width).CopyTo(targetRow);
}
});
} }
} }
} }

64
src/ImageSharp/Processing/Processors/Transforms/FlipProcessor.cs

@ -2,9 +2,11 @@
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System; using System;
using System.Buffers;
using System.Threading.Tasks; using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory; using SixLabors.Memory;
using SixLabors.Primitives; using SixLabors.Primitives;
@ -55,27 +57,20 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
private void FlipX(ImageFrame<TPixel> source, Configuration configuration) private void FlipX(ImageFrame<TPixel> source, Configuration configuration)
{ {
int height = source.Height; int height = source.Height;
int halfHeight = (int)Math.Ceiling(source.Height * .5F);
using (Buffer2D<TPixel> targetPixels = configuration.MemoryAllocator.Allocate2D<TPixel>(source.Size())) using (IMemoryOwner<TPixel> tempBuffer = configuration.MemoryAllocator.Allocate<TPixel>(source.Width))
{ {
ParallelFor.WithConfiguration( Span<TPixel> temp = tempBuffer.Memory.Span;
0,
halfHeight,
configuration,
y =>
{
int newY = height - y - 1;
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
Span<TPixel> altSourceRow = source.GetPixelRowSpan(newY);
Span<TPixel> targetRow = targetPixels.GetRowSpan(y);
Span<TPixel> altTargetRow = targetPixels.GetRowSpan(newY);
sourceRow.CopyTo(altTargetRow);
altSourceRow.CopyTo(targetRow);
});
Buffer2D<TPixel>.SwapOrCopyContent(source.PixelBuffer, targetPixels); for (int yTop = 0; yTop < height / 2; yTop++)
{
int yBottom = height - yTop - 1;
Span<TPixel> topRow = source.GetPixelRowSpan(yBottom);
Span<TPixel> bottomRow = source.GetPixelRowSpan(yTop);
topRow.CopyTo(temp);
bottomRow.CopyTo(topRow);
temp.CopyTo(bottomRow);
}
} }
} }
@ -86,31 +81,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <param name="configuration">The configuration.</param> /// <param name="configuration">The configuration.</param>
private void FlipY(ImageFrame<TPixel> source, Configuration configuration) private void FlipY(ImageFrame<TPixel> source, Configuration configuration)
{ {
int width = source.Width; ParallelHelper.IterateRows(
int height = source.Height; source.Bounds(),
int halfWidth = (int)Math.Ceiling(width * .5F); configuration,
rows =>
using (Buffer2D<TPixel> targetPixels = configuration.MemoryAllocator.Allocate2D<TPixel>(source.Size())) {
{ for (int y = rows.Min; y < rows.Max; y++)
ParallelFor.WithConfiguration(
0,
height,
configuration,
y =>
{ {
Span<TPixel> sourceRow = source.GetPixelRowSpan(y); source.GetPixelRowSpan(y).Reverse();
Span<TPixel> targetRow = targetPixels.GetRowSpan(y); }
});
for (int x = 0; x < halfWidth; x++)
{
int newX = width - x - 1;
targetRow[x] = sourceRow[newX];
targetRow[newX] = sourceRow[x];
}
});
Buffer2D<TPixel>.SwapOrCopyContent(source.PixelBuffer, targetPixels);
}
} }
} }
} }

216
src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs

@ -10,6 +10,7 @@ using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory; using SixLabors.Memory;
using SixLabors.Primitives; using SixLabors.Primitives;
@ -75,28 +76,30 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
if (this.Sampler is NearestNeighborResampler) if (this.Sampler is NearestNeighborResampler)
{ {
ParallelFor.WithConfiguration( ParallelHelper.IterateRows(
0, targetBounds,
height,
configuration, configuration,
y => rows =>
{
Span<TPixel> destRow = destination.GetPixelRowSpan(y);
for (int x = 0; x < width; x++)
{ {
var v3 = Vector3.Transform(new Vector3(x, y, 1), matrix); for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> destRow = destination.GetPixelRowSpan(y);
float z = MathF.Max(v3.Z, Epsilon); for (int x = 0; x < width; x++)
int px = (int)MathF.Round(v3.X / z); {
int py = (int)MathF.Round(v3.Y / z); var v3 = Vector3.Transform(new Vector3(x, y, 1), matrix);
if (sourceBounds.Contains(px, py)) float z = MathF.Max(v3.Z, Epsilon);
{ int px = (int)MathF.Round(v3.X / z);
destRow[x] = source[px, py]; int py = (int)MathF.Round(v3.Y / z);
if (sourceBounds.Contains(px, py))
{
destRow[x] = source[px, py];
}
}
} }
} });
});
return; return;
} }
@ -121,92 +124,113 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
using (Buffer2D<float> yBuffer = memoryAllocator.Allocate2D<float>(yLength, height)) using (Buffer2D<float> yBuffer = memoryAllocator.Allocate2D<float>(yLength, height))
using (Buffer2D<float> xBuffer = memoryAllocator.Allocate2D<float>(xLength, height)) using (Buffer2D<float> xBuffer = memoryAllocator.Allocate2D<float>(xLength, height))
{ {
ParallelFor.WithConfiguration( ParallelHelper.IterateRows(
0, targetBounds,
height,
configuration, configuration,
y => rows =>
{
ref TPixel destRowRef = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y));
ref float ySpanRef = ref MemoryMarshal.GetReference(yBuffer.GetRowSpan(y));
ref float xSpanRef = ref MemoryMarshal.GetReference(xBuffer.GetRowSpan(y));
for (int x = 0; x < width; x++)
{ {
// Use the single precision position to calculate correct bounding pixels for (int y = rows.Min; y < rows.Max; y++)
// otherwise we get rogue pixels outside of the bounds.
var v3 = Vector3.Transform(new Vector3(x, y, 1), matrix);
float z = MathF.Max(v3.Z, Epsilon);
// Using Vector4 with dummy 0-s, because Vector2 SIMD implementation is not reliable:
Vector4 point = new Vector4(v3.X, v3.Y, 0, 0) / z;
// Clamp sampling pixel radial extents to the source image edges
Vector4 maxXY = point + radius;
Vector4 minXY = point - radius;
// max, maxY, minX, minY
var extents = new Vector4(
MathF.Floor(maxXY.X + .5F),
MathF.Floor(maxXY.Y + .5F),
MathF.Ceiling(minXY.X - .5F),
MathF.Ceiling(minXY.Y - .5F));
int right = (int)extents.X;
int bottom = (int)extents.Y;
int left = (int)extents.Z;
int top = (int)extents.W;
extents = Vector4.Clamp(extents, Vector4.Zero, maxSource);
int maxX = (int)extents.X;
int maxY = (int)extents.Y;
int minX = (int)extents.Z;
int minY = (int)extents.W;
if (minX == maxX || minY == maxY)
{
continue;
}
// It appears these have to be calculated on-the-fly.
// Precalulating transformed weights would require prior knowledge of every transformed pixel location
// since they can be at sub-pixel positions on both axis.
// I've optimized where I can but am always open to suggestions.
if (yScale > 1 && xScale > 1)
{
CalculateWeightsDown(top, bottom, minY, maxY, point.Y, sampler, yScale, ref ySpanRef, yLength);
CalculateWeightsDown(left, right, minX, maxX, point.X, sampler, xScale, ref xSpanRef, xLength);
}
else
{ {
CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ref ySpanRef); ref TPixel destRowRef = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y));
CalculateWeightsScaleUp(minX, maxX, point.X, sampler, ref xSpanRef); ref float ySpanRef = ref MemoryMarshal.GetReference(yBuffer.GetRowSpan(y));
} ref float xSpanRef = ref MemoryMarshal.GetReference(xBuffer.GetRowSpan(y));
// Now multiply the results against the offsets for (int x = 0; x < width; x++)
Vector4 sum = Vector4.Zero;
for (int yy = 0, j = minY; j <= maxY; j++, yy++)
{
float yWeight = Unsafe.Add(ref ySpanRef, yy);
for (int xx = 0, i = minX; i <= maxX; i++, xx++)
{ {
float xWeight = Unsafe.Add(ref xSpanRef, xx); // Use the single precision position to calculate correct bounding pixels
var vector = source[i, j].ToVector4(); // otherwise we get rogue pixels outside of the bounds.
var v3 = Vector3.Transform(new Vector3(x, y, 1), matrix);
// Values are first premultiplied to prevent darkening of edge pixels float z = MathF.Max(v3.Z, Epsilon);
Vector4 multiplied = vector.Premultiply();
sum += multiplied * xWeight * yWeight; // Using Vector4 with dummy 0-s, because Vector2 SIMD implementation is not reliable:
Vector4 point = new Vector4(v3.X, v3.Y, 0, 0) / z;
// Clamp sampling pixel radial extents to the source image edges
Vector4 maxXY = point + radius;
Vector4 minXY = point - radius;
// max, maxY, minX, minY
var extents = new Vector4(
MathF.Floor(maxXY.X + .5F),
MathF.Floor(maxXY.Y + .5F),
MathF.Ceiling(minXY.X - .5F),
MathF.Ceiling(minXY.Y - .5F));
int right = (int)extents.X;
int bottom = (int)extents.Y;
int left = (int)extents.Z;
int top = (int)extents.W;
extents = Vector4.Clamp(extents, Vector4.Zero, maxSource);
int maxX = (int)extents.X;
int maxY = (int)extents.Y;
int minX = (int)extents.Z;
int minY = (int)extents.W;
if (minX == maxX || minY == maxY)
{
continue;
}
// It appears these have to be calculated on-the-fly.
// Precalulating transformed weights would require prior knowledge of every transformed pixel location
// since they can be at sub-pixel positions on both axis.
// I've optimized where I can but am always open to suggestions.
if (yScale > 1 && xScale > 1)
{
CalculateWeightsDown(
top,
bottom,
minY,
maxY,
point.Y,
sampler,
yScale,
ref ySpanRef,
yLength);
CalculateWeightsDown(
left,
right,
minX,
maxX,
point.X,
sampler,
xScale,
ref xSpanRef,
xLength);
}
else
{
CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ref ySpanRef);
CalculateWeightsScaleUp(minX, maxX, point.X, sampler, ref xSpanRef);
}
// Now multiply the results against the offsets
Vector4 sum = Vector4.Zero;
for (int yy = 0, j = minY; j <= maxY; j++, yy++)
{
float yWeight = Unsafe.Add(ref ySpanRef, yy);
for (int xx = 0, i = minX; i <= maxX; i++, xx++)
{
float xWeight = Unsafe.Add(ref xSpanRef, xx);
var vector = source[i, j].ToVector4();
// Values are first premultiplied to prevent darkening of edge pixels
Vector4 multiplied = vector.Premultiply();
sum += multiplied * xWeight * yWeight;
}
}
ref TPixel dest = ref Unsafe.Add(ref destRowRef, x);
// Reverse the premultiplication
dest.PackFromVector4(sum.UnPremultiply());
} }
} }
});
ref TPixel dest = ref Unsafe.Add(ref destRowRef, x);
// Reverse the premultiplication
dest.PackFromVector4(sum.UnPremultiply());
}
});
} }
} }

148
src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs

@ -11,6 +11,7 @@ using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory; using SixLabors.Memory;
using SixLabors.Primitives; using SixLabors.Primitives;
@ -267,26 +268,31 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
if (this.Sampler is NearestNeighborResampler) if (this.Sampler is NearestNeighborResampler)
{ {
var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY);
// Scaling factors // Scaling factors
float widthFactor = sourceRectangle.Width / (float)this.ResizeRectangle.Width; float widthFactor = sourceRectangle.Width / (float)this.ResizeRectangle.Width;
float heightFactor = sourceRectangle.Height / (float)this.ResizeRectangle.Height; float heightFactor = sourceRectangle.Height / (float)this.ResizeRectangle.Height;
ParallelFor.WithConfiguration( ParallelHelper.IterateRows(
minY, workingRect,
maxY,
configuration, configuration,
y => rows =>
{
// Y coordinates of source points
Span<TPixel> sourceRow = source.GetPixelRowSpan((int)(((y - startY) * heightFactor) + sourceY));
Span<TPixel> targetRow = destination.GetPixelRowSpan(y);
for (int x = minX; x < maxX; x++)
{ {
// X coordinates of source points for (int y = rows.Min; y < rows.Max; y++)
targetRow[x] = sourceRow[(int)(((x - startX) * widthFactor) + sourceX)]; {
} // Y coordinates of source points
}); Span<TPixel> sourceRow =
source.GetPixelRowSpan((int)(((y - startY) * heightFactor) + sourceY));
Span<TPixel> targetRow = destination.GetPixelRowSpan(y);
for (int x = minX; x < maxX; x++)
{
// X coordinates of source points
targetRow[x] = sourceRow[(int)(((x - startX) * widthFactor) + sourceX)];
}
}
});
return; return;
} }
@ -300,72 +306,88 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{ {
firstPassPixels.MemorySource.Clear(); firstPassPixels.MemorySource.Clear();
ParallelFor.WithTemporaryBuffer( var processColsRect = new Rectangle(0, 0, source.Width, sourceRectangle.Bottom);
0,
sourceRectangle.Bottom, ParallelHelper.IterateRowsWithTempBuffer<Vector4>(
processColsRect,
configuration, configuration,
source.Width, (rows, tempRowBuffer) =>
(int y, IMemoryOwner<Vector4> tempRowBuffer) =>
{ {
ref Vector4 firstPassRow = ref MemoryMarshal.GetReference(firstPassPixels.GetRowSpan(y)); for (int y = rows.Min; y < rows.Max; y++)
Span<TPixel> sourceRow = source.GetPixelRowSpan(y); {
Span<Vector4> tempRowSpan = tempRowBuffer.GetSpan(); ref Vector4 firstPassRow =
ref MemoryMarshal.GetReference(firstPassPixels.GetRowSpan(y));
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
Span<Vector4> tempRowSpan = tempRowBuffer.Span;
PixelOperations<TPixel>.Instance.ToVector4(sourceRow, tempRowSpan, sourceRow.Length); PixelOperations<TPixel>.Instance.ToVector4(sourceRow, tempRowSpan, sourceRow.Length);
if (this.Compand) if (this.Compand)
{
for (int x = minX; x < maxX; x++)
{ {
WeightsWindow window = this.horizontalWeights.Weights[x - startX]; for (int x = minX; x < maxX; x++)
Unsafe.Add(ref firstPassRow, x) = window.ComputeExpandedWeightedRowSum(tempRowSpan, sourceX); {
WeightsWindow window = this.horizontalWeights.Weights[x - startX];
Unsafe.Add(ref firstPassRow, x) =
window.ComputeExpandedWeightedRowSum(tempRowSpan, sourceX);
}
} }
} else
else
{
for (int x = minX; x < maxX; x++)
{ {
WeightsWindow window = this.horizontalWeights.Weights[x - startX]; for (int x = minX; x < maxX; x++)
Unsafe.Add(ref firstPassRow, x) = window.ComputeWeightedRowSum(tempRowSpan, sourceX); {
WeightsWindow window = this.horizontalWeights.Weights[x - startX];
Unsafe.Add(ref firstPassRow, x) =
window.ComputeWeightedRowSum(tempRowSpan, sourceX);
}
} }
} }
}); });
var processRowsRect = Rectangle.FromLTRB(0, minY, width, maxY);
// Now process the rows. // Now process the rows.
ParallelFor.WithConfiguration( ParallelHelper.IterateRows(
minY, processRowsRect,
maxY,
configuration, configuration,
y => rows =>
{
// Ensure offsets are normalized for cropping and padding.
WeightsWindow window = this.verticalWeights.Weights[y - startY];
ref TPixel targetRow = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y));
if (this.Compand)
{
for (int x = 0; x < width; x++)
{
// Destination color components
Vector4 destinationVector = window.ComputeWeightedColumnSum(firstPassPixels, x, sourceY);
destinationVector = destinationVector.Compress();
ref TPixel pixel = ref Unsafe.Add(ref targetRow, x);
pixel.PackFromVector4(destinationVector);
}
}
else
{ {
for (int x = 0; x < width; x++) for (int y = rows.Min; y < rows.Max; y++)
{ {
// Destination color components // Ensure offsets are normalized for cropping and padding.
Vector4 destinationVector = window.ComputeWeightedColumnSum(firstPassPixels, x, sourceY); WeightsWindow window = this.verticalWeights.Weights[y - startY];
ref TPixel targetRow = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y));
ref TPixel pixel = ref Unsafe.Add(ref targetRow, x); if (this.Compand)
pixel.PackFromVector4(destinationVector); {
for (int x = 0; x < width; x++)
{
// Destination color components
Vector4 destinationVector = window.ComputeWeightedColumnSum(
firstPassPixels,
x,
sourceY);
destinationVector = destinationVector.Compress();
ref TPixel pixel = ref Unsafe.Add(ref targetRow, x);
pixel.PackFromVector4(destinationVector);
}
}
else
{
for (int x = 0; x < width; x++)
{
// Destination color components
Vector4 destinationVector = window.ComputeWeightedColumnSum(
firstPassPixels,
x,
sourceY);
ref TPixel pixel = ref Unsafe.Add(ref targetRow, x);
pixel.PackFromVector4(destinationVector);
}
}
} }
} });
});
} }
} }

86
src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs

@ -5,6 +5,7 @@ using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.MetaData.Profiles.Exif; using SixLabors.ImageSharp.MetaData.Profiles.Exif;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Transforms; using SixLabors.ImageSharp.Processing.Processors.Transforms;
using SixLabors.Primitives; using SixLabors.Primitives;
@ -147,25 +148,27 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
int height = source.Height; int height = source.Height;
Rectangle destinationBounds = destination.Bounds(); Rectangle destinationBounds = destination.Bounds();
ParallelFor.WithConfiguration( ParallelHelper.IterateRows(
0, source.Bounds(),
height,
configuration, configuration,
y => rows =>
{
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
for (int x = 0; x < width; x++)
{ {
int newX = height - y - 1; for (int y = rows.Min; y < rows.Max; y++)
newX = height - newX - 1;
int newY = width - x - 1;
if (destinationBounds.Contains(newX, newY))
{ {
destination[newX, newY] = sourceRow[x]; Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
for (int x = 0; x < width; x++)
{
int newX = height - y - 1;
newX = height - newX - 1;
int newY = width - x - 1;
if (destinationBounds.Contains(newX, newY))
{
destination[newX, newY] = sourceRow[x];
}
}
} }
} });
});
} }
/// <summary> /// <summary>
@ -179,20 +182,22 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
int width = source.Width; int width = source.Width;
int height = source.Height; int height = source.Height;
ParallelFor.WithConfiguration( ParallelHelper.IterateRows(
0, source.Bounds(),
height,
configuration, configuration,
y => rows =>
{
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
Span<TPixel> targetRow = destination.GetPixelRowSpan(height - y - 1);
for (int x = 0; x < width; x++)
{ {
targetRow[width - x - 1] = sourceRow[x]; for (int y = rows.Min; y < rows.Max; y++)
} {
}); Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
Span<TPixel> targetRow = destination.GetPixelRowSpan(height - y - 1);
for (int x = 0; x < width; x++)
{
targetRow[width - x - 1] = sourceRow[x];
}
}
});
} }
/// <summary> /// <summary>
@ -207,22 +212,25 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
int height = source.Height; int height = source.Height;
Rectangle destinationBounds = destination.Bounds(); Rectangle destinationBounds = destination.Bounds();
ParallelFor.WithConfiguration( ParallelHelper.IterateRows(
0, source.Bounds(),
height,
configuration, configuration,
y => rows =>
{
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
int newX = height - y - 1;
for (int x = 0; x < width; x++)
{ {
if (destinationBounds.Contains(newX, x)) for (int y = rows.Min; y < rows.Max; y++)
{ {
destination[newX, x] = sourceRow[x]; Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
int newX = height - y - 1;
for (int x = 0; x < width; x++)
{
// TODO: Optimize this:
if (destinationBounds.Contains(newX, x))
{
destination[newX, x] = sourceRow[x];
}
}
} }
} });
});
} }
} }
} }

115
tests/ImageSharp.Benchmarks/Codecs/CopyPixels.cs

@ -1,115 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Benchmarks.Codecs
{
public class CopyPixels : BenchmarkBase
{
[Benchmark(Baseline = true, Description = "PixelAccessor Copy by indexer")]
public Rgba32 CopyByPixelAccesor()
{
using (var source = new Image<Rgba32>(1024, 768))
using (var target = new Image<Rgba32>(1024, 768))
{
Buffer2D<Rgba32> sourcePixels = source.GetRootFramePixelBuffer();
Buffer2D<Rgba32> targetPixels = target.GetRootFramePixelBuffer();
ParallelFor.WithConfiguration(
0,
source.Height,
Configuration.Default,
y =>
{
for (int x = 0; x < source.Width; x++)
{
targetPixels[x, y] = sourcePixels[x, y];
}
});
return targetPixels[0, 0];
}
}
[Benchmark(Description = "PixelAccessor Copy by Span")]
public Rgba32 CopyByPixelAccesorSpan()
{
using (var source = new Image<Rgba32>(1024, 768))
using (var target = new Image<Rgba32>(1024, 768))
{
Buffer2D<Rgba32> sourcePixels = source.GetRootFramePixelBuffer();
Buffer2D<Rgba32> targetPixels = target.GetRootFramePixelBuffer();
ParallelFor.WithConfiguration(
0,
source.Height,
Configuration.Default,
y =>
{
Span<Rgba32> sourceRow = sourcePixels.GetRowSpan(y);
Span<Rgba32> targetRow = targetPixels.GetRowSpan(y);
for (int x = 0; x < source.Width; x++)
{
targetRow[x] = sourceRow[x];
}
});
return targetPixels[0, 0];
}
}
[Benchmark(Description = "Copy by indexer")]
public Rgba32 Copy()
{
using (var source = new Image<Rgba32>(1024, 768))
using (var target = new Image<Rgba32>(1024, 768))
{
ParallelFor.WithConfiguration(
0,
source.Height,
Configuration.Default,
y =>
{
for (int x = 0; x < source.Width; x++)
{
target[x, y] = source[x, y];
}
});
return target[0, 0];
}
}
[Benchmark(Description = "Copy by Span")]
public Rgba32 CopySpan()
{
using (var source = new Image<Rgba32>(1024, 768))
using (var target = new Image<Rgba32>(1024, 768))
{
ParallelFor.WithConfiguration(
0,
source.Height,
Configuration.Default,
y =>
{
Span<Rgba32> sourceRow = source.Frames.RootFrame.GetPixelRowSpan(y);
Span<Rgba32> targetRow = target.Frames.RootFrame.GetPixelRowSpan(y);
for (int x = 0; x < source.Width; x++)
{
targetRow[x] = sourceRow[x];
}
});
return target[0, 0];
}
}
}
}

38
tests/ImageSharp.Benchmarks/Samplers/Glow.cs

@ -9,6 +9,7 @@ using System.Threading.Tasks;
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors; using SixLabors.ImageSharp.Processing.Processors;
using SixLabors.ImageSharp.Processing.Processors.Overlays; using SixLabors.ImageSharp.Processing.Processors.Overlays;
@ -112,26 +113,29 @@ namespace SixLabors.ImageSharp.Benchmarks
Buffer2D<TPixel> sourcePixels = source.PixelBuffer; Buffer2D<TPixel> sourcePixels = source.PixelBuffer;
rowColors.GetSpan().Fill(glowColor); rowColors.GetSpan().Fill(glowColor);
ParallelFor.WithConfiguration( var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY);
minY, ParallelHelper.IterateRows(
maxY, workingRect,
configuration, configuration,
y => rows =>
{ {
int offsetY = y - startY; for (int y = rows.Min; y < rows.Max; y++)
for (int x = minX; x < maxX; x++)
{ {
int offsetX = x - startX; int offsetY = y - startY;
float distance = Vector2.Distance(centre, new Vector2(offsetX, offsetY));
Vector4 sourceColor = sourcePixels[offsetX, offsetY].ToVector4(); for (int x = minX; x < maxX; x++)
TPixel packed = default(TPixel); {
packed.PackFromVector4( int offsetX = x - startX;
PremultipliedLerp( float distance = Vector2.Distance(centre, new Vector2(offsetX, offsetY));
sourceColor, Vector4 sourceColor = sourcePixels[offsetX, offsetY].ToVector4();
glowColor.ToVector4(), TPixel packed = default(TPixel);
1 - (.95F * (distance / maxDistance)))); packed.PackFromVector4(
sourcePixels[offsetX, offsetY] = packed; PremultipliedLerp(
sourceColor,
glowColor.ToVector4(),
1 - (.95F * (distance / maxDistance))));
sourcePixels[offsetX, offsetY] = packed;
}
} }
}); });
} }

255
tests/ImageSharp.Tests/Drawing/FillPatternTests.cs

@ -54,172 +54,225 @@ namespace SixLabors.ImageSharp.Tests.Drawing
[Fact] [Fact]
public void ImageShouldBeFloodFilledWithPercent10() public void ImageShouldBeFloodFilledWithPercent10()
{ {
this.Test("Percent10", Rgba32.Blue, Brushes.Percent10(Rgba32.HotPink, Rgba32.LimeGreen), this.Test(
"Percent10",
Rgba32.Blue,
Brushes.Percent10(Rgba32.HotPink, Rgba32.LimeGreen),
new[,] new[,]
{ {
{ Rgba32.HotPink , Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen}, { Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen },
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen}, { Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen },
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink , Rgba32.LimeGreen}, { Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen },
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen} { Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen }
}); });
} }
[Fact] [Fact]
public void ImageShouldBeFloodFilledWithPercent10Transparent() public void ImageShouldBeFloodFilledWithPercent10Transparent()
{ {
Test("Percent10_Transparent", Rgba32.Blue, Brushes.Percent10(Rgba32.HotPink), this.Test(
new Rgba32[,] { "Percent10_Transparent",
{ Rgba32.HotPink , Rgba32.Blue, Rgba32.Blue, Rgba32.Blue}, Rgba32.Blue,
{ Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue}, Brushes.Percent10(Rgba32.HotPink),
{ Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink , Rgba32.Blue}, new Rgba32[,]
{ Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue} {
}); { Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue },
{ Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue },
{ Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue },
{ Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue }
});
} }
[Fact] [Fact]
public void ImageShouldBeFloodFilledWithPercent20() public void ImageShouldBeFloodFilledWithPercent20()
{ {
Test("Percent20", Rgba32.Blue, Brushes.Percent20(Rgba32.HotPink, Rgba32.LimeGreen), this.Test(
new Rgba32[,] { "Percent20",
{ Rgba32.HotPink , Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen}, Rgba32.Blue,
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink , Rgba32.LimeGreen}, Brushes.Percent20(Rgba32.HotPink, Rgba32.LimeGreen),
{ Rgba32.HotPink , Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen}, new Rgba32[,]
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink , Rgba32.LimeGreen} {
}); { Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen },
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen },
{ Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen },
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen }
});
} }
[Fact] [Fact]
public void ImageShouldBeFloodFilledWithPercent20_transparent() public void ImageShouldBeFloodFilledWithPercent20_transparent()
{ {
Test("Percent20_Transparent", Rgba32.Blue, Brushes.Percent20(Rgba32.HotPink), this.Test(
new Rgba32[,] { "Percent20_Transparent",
{ Rgba32.HotPink , Rgba32.Blue, Rgba32.Blue, Rgba32.Blue}, Rgba32.Blue,
{ Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink , Rgba32.Blue}, Brushes.Percent20(Rgba32.HotPink),
{ Rgba32.HotPink , Rgba32.Blue, Rgba32.Blue, Rgba32.Blue}, new Rgba32[,]
{ Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink , Rgba32.Blue} {
}); { Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue },
{ Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue },
{ Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue },
{ Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue }
});
} }
[Fact] [Fact]
public void ImageShouldBeFloodFilledWithHorizontal() public void ImageShouldBeFloodFilledWithHorizontal()
{ {
Test("Horizontal", Rgba32.Blue, Brushes.Horizontal(Rgba32.HotPink, Rgba32.LimeGreen), this.Test(
new Rgba32[,] { "Horizontal",
{ Rgba32.LimeGreen , Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen}, Rgba32.Blue,
{ Rgba32.HotPink, Rgba32.HotPink, Rgba32.HotPink , Rgba32.HotPink}, Brushes.Horizontal(Rgba32.HotPink, Rgba32.LimeGreen),
{ Rgba32.LimeGreen , Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen}, new Rgba32[,]
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen , Rgba32.LimeGreen} {
}); { Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen },
{ Rgba32.HotPink, Rgba32.HotPink, Rgba32.HotPink, Rgba32.HotPink },
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen },
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen }
});
} }
[Fact] [Fact]
public void ImageShouldBeFloodFilledWithHorizontal_transparent() public void ImageShouldBeFloodFilledWithHorizontal_transparent()
{ {
Test("Horizontal_Transparent", Rgba32.Blue, Brushes.Horizontal(Rgba32.HotPink), this.Test(
new Rgba32[,] { "Horizontal_Transparent",
{ Rgba32.Blue , Rgba32.Blue, Rgba32.Blue, Rgba32.Blue}, Rgba32.Blue,
{ Rgba32.HotPink, Rgba32.HotPink, Rgba32.HotPink , Rgba32.HotPink}, Brushes.Horizontal(Rgba32.HotPink),
{ Rgba32.Blue , Rgba32.Blue, Rgba32.Blue, Rgba32.Blue}, new Rgba32[,]
{ Rgba32.Blue, Rgba32.Blue, Rgba32.Blue , Rgba32.Blue} {
}); { Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue },
{ Rgba32.HotPink, Rgba32.HotPink, Rgba32.HotPink, Rgba32.HotPink },
{ Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue },
{ Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue }
});
} }
[Fact] [Fact]
public void ImageShouldBeFloodFilledWithMin() public void ImageShouldBeFloodFilledWithMin()
{ {
Test("Min", Rgba32.Blue, Brushes.Min(Rgba32.HotPink, Rgba32.LimeGreen), this.Test(
new Rgba32[,] { "Min",
{ Rgba32.LimeGreen , Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen}, Rgba32.Blue,
{ Rgba32.LimeGreen , Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen}, Brushes.Min(Rgba32.HotPink, Rgba32.LimeGreen),
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen , Rgba32.LimeGreen}, new Rgba32[,]
{ Rgba32.HotPink, Rgba32.HotPink, Rgba32.HotPink , Rgba32.HotPink} {
}); { Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen },
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen },
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen },
{ Rgba32.HotPink, Rgba32.HotPink, Rgba32.HotPink, Rgba32.HotPink }
});
} }
[Fact] [Fact]
public void ImageShouldBeFloodFilledWithMin_transparent() public void ImageShouldBeFloodFilledWithMin_transparent()
{ {
Test("Min_Transparent", Rgba32.Blue, Brushes.Min(Rgba32.HotPink), this.Test(
new Rgba32[,] { "Min_Transparent",
{ Rgba32.Blue , Rgba32.Blue, Rgba32.Blue, Rgba32.Blue}, Rgba32.Blue,
{ Rgba32.Blue , Rgba32.Blue, Rgba32.Blue, Rgba32.Blue}, Brushes.Min(Rgba32.HotPink),
{ Rgba32.Blue, Rgba32.Blue, Rgba32.Blue , Rgba32.Blue}, new Rgba32[,]
{ Rgba32.HotPink, Rgba32.HotPink, Rgba32.HotPink , Rgba32.HotPink}, {
}); { Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue },
{ Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue },
{ Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue },
{ Rgba32.HotPink, Rgba32.HotPink, Rgba32.HotPink, Rgba32.HotPink },
});
} }
[Fact] [Fact]
public void ImageShouldBeFloodFilledWithVertical() public void ImageShouldBeFloodFilledWithVertical()
{ {
Test("Vertical", Rgba32.Blue, Brushes.Vertical(Rgba32.HotPink, Rgba32.LimeGreen), this.Test(
new Rgba32[,] { "Vertical",
{ Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen}, Rgba32.Blue,
{ Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen}, Brushes.Vertical(Rgba32.HotPink, Rgba32.LimeGreen),
{ Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen}, new Rgba32[,]
{ Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen} {
}); { Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen },
{ Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen },
{ Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen },
{ Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen }
});
} }
[Fact] [Fact]
public void ImageShouldBeFloodFilledWithVertical_transparent() public void ImageShouldBeFloodFilledWithVertical_transparent()
{ {
Test("Vertical_Transparent", Rgba32.Blue, Brushes.Vertical(Rgba32.HotPink), this.Test(
new Rgba32[,] { "Vertical_Transparent",
{ Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue}, Rgba32.Blue,
{ Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue}, Brushes.Vertical(Rgba32.HotPink),
{ Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue}, new Rgba32[,]
{ Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue} {
}); { Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue },
{ Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue },
{ Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue },
{ Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue }
});
} }
[Fact] [Fact]
public void ImageShouldBeFloodFilledWithForwardDiagonal() public void ImageShouldBeFloodFilledWithForwardDiagonal()
{ {
Test("ForwardDiagonal", Rgba32.Blue, Brushes.ForwardDiagonal(Rgba32.HotPink, Rgba32.LimeGreen), this.Test(
new Rgba32[,] { "ForwardDiagonal",
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink}, Rgba32.Blue,
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen}, Brushes.ForwardDiagonal(Rgba32.HotPink, Rgba32.LimeGreen),
{ Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen}, new Rgba32[,]
{ Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen} {
}); { Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink },
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen },
{ Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen },
{ Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen }
});
} }
[Fact] [Fact]
public void ImageShouldBeFloodFilledWithForwardDiagonal_transparent() public void ImageShouldBeFloodFilledWithForwardDiagonal_transparent()
{ {
Test("ForwardDiagonal_Transparent", Rgba32.Blue, Brushes.ForwardDiagonal(Rgba32.HotPink), this.Test(
new Rgba32[,] { "ForwardDiagonal_Transparent",
{ Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink}, Rgba32.Blue,
{ Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue}, Brushes.ForwardDiagonal(Rgba32.HotPink),
{ Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue}, new Rgba32[,]
{ Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue} {
}); { Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink },
{ Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue },
{ Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue },
{ Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue }
});
} }
[Fact] [Fact]
public void ImageShouldBeFloodFilledWithBackwardDiagonal() public void ImageShouldBeFloodFilledWithBackwardDiagonal()
{ {
Test("BackwardDiagonal", Rgba32.Blue, Brushes.BackwardDiagonal(Rgba32.HotPink, Rgba32.LimeGreen), this.Test(
new Rgba32[,] { "BackwardDiagonal",
{ Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen}, Rgba32.Blue,
{ Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen}, Brushes.BackwardDiagonal(Rgba32.HotPink, Rgba32.LimeGreen),
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen}, new Rgba32[,]
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink} {
}); { Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen },
{ Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen },
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen },
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink }
});
} }
[Fact] [Fact]
public void ImageShouldBeFloodFilledWithBackwardDiagonal_transparent() public void ImageShouldBeFloodFilledWithBackwardDiagonal_transparent()
{ {
Test("BackwardDiagonal_Transparent", Rgba32.Blue, Brushes.BackwardDiagonal(Rgba32.HotPink), this.Test(
new Rgba32[,] { "BackwardDiagonal_Transparent",
{ Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue}, Rgba32.Blue,
{ Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue}, Brushes.BackwardDiagonal(Rgba32.HotPink),
{ Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue}, new Rgba32[,]
{ Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink} {
}); { Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue },
{ Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue },
{ Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue },
{ Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink }
});
} }
} }
} }

44
tests/ImageSharp.Tests/Drawing/FillSolidBrushTests.cs

@ -1,20 +1,21 @@
// Copyright (c) Six Labors and contributors. // Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Primitives; using SixLabors.ImageSharp.Primitives;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using SixLabors.Primitives;
using SixLabors.Shapes; using SixLabors.Shapes;
using Xunit; using Xunit;
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Tests.Drawing namespace SixLabors.ImageSharp.Tests.Drawing
{ {
using System;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using SixLabors.Primitives;
[GroupOutput("Drawing")] [GroupOutput("Drawing")]
public class FillSolidBrushTests public class FillSolidBrushTests
{ {
@ -55,7 +56,9 @@ namespace SixLabors.ImageSharp.Tests.Drawing
[Theory] [Theory]
[WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, "Blue")] [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, "Blue")]
[WithSolidFilledImages(16, 16, "Yellow", PixelTypes.Rgba32, "Khaki")] [WithSolidFilledImages(16, 16, "Yellow", PixelTypes.Rgba32, "Khaki")]
public void WhenColorIsOpaque_OverridePreviousColor<TPixel>(TestImageProvider<TPixel> provider, string newColorName) public void WhenColorIsOpaque_OverridePreviousColor<TPixel>(
TestImageProvider<TPixel> provider,
string newColorName)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
using (Image<TPixel> image = provider.GetImage()) using (Image<TPixel> image = provider.GetImage())
@ -63,7 +66,11 @@ namespace SixLabors.ImageSharp.Tests.Drawing
TPixel color = TestUtils.GetPixelOfNamedColor<TPixel>(newColorName); TPixel color = TestUtils.GetPixelOfNamedColor<TPixel>(newColorName);
image.Mutate(c => c.Fill(color)); image.Mutate(c => c.Fill(color));
image.DebugSave(provider, newColorName, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); image.DebugSave(
provider,
newColorName,
appendPixelTypeToFileName: false,
appendSourceFileOrDescription: false);
image.ComparePixelBufferTo(color); image.ComparePixelBufferTo(color);
} }
} }
@ -84,7 +91,12 @@ namespace SixLabors.ImageSharp.Tests.Drawing
[Theory] [Theory]
[WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 5, 7, 3, 8)] [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 5, 7, 3, 8)]
[WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 8, 5, 6, 4)] [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 8, 5, 6, 4)]
public void FillRegion_WorksOnWrappedMemoryImage<TPixel>(TestImageProvider<TPixel> provider, int x0, int y0, int w, int h) public void FillRegion_WorksOnWrappedMemoryImage<TPixel>(
TestImageProvider<TPixel> provider,
int x0,
int y0,
int w,
int h)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
FormattableString testDetails = $"(x{x0},y{y0},w{w},h{h})"; FormattableString testDetails = $"(x{x0},y{y0},w{w},h{h})";
@ -105,27 +117,22 @@ namespace SixLabors.ImageSharp.Tests.Drawing
{ false, "Blue", 1.0f, PixelColorBlendingMode.Normal, 0.5f }, { false, "Blue", 1.0f, PixelColorBlendingMode.Normal, 0.5f },
{ false, "Green", 0.5f, PixelColorBlendingMode.Normal, 0.3f }, { false, "Green", 0.5f, PixelColorBlendingMode.Normal, 0.3f },
{ false, "HotPink", 0.8f, PixelColorBlendingMode.Normal, 0.8f }, { false, "HotPink", 0.8f, PixelColorBlendingMode.Normal, 0.8f },
{ false, "Blue", 0.5f, PixelColorBlendingMode.Multiply, 1.0f }, { false, "Blue", 0.5f, PixelColorBlendingMode.Multiply, 1.0f },
{ false, "Blue", 1.0f, PixelColorBlendingMode.Multiply, 0.5f }, { false, "Blue", 1.0f, PixelColorBlendingMode.Multiply, 0.5f },
{ false, "Green", 0.5f, PixelColorBlendingMode.Multiply, 0.3f }, { false, "Green", 0.5f, PixelColorBlendingMode.Multiply, 0.3f },
{ false, "HotPink", 0.8f, PixelColorBlendingMode.Multiply, 0.8f }, { false, "HotPink", 0.8f, PixelColorBlendingMode.Multiply, 0.8f },
{ false, "Blue", 0.5f, PixelColorBlendingMode.Add, 1.0f }, { false, "Blue", 0.5f, PixelColorBlendingMode.Add, 1.0f },
{ false, "Blue", 1.0f, PixelColorBlendingMode.Add, 0.5f }, { false, "Blue", 1.0f, PixelColorBlendingMode.Add, 0.5f },
{ false, "Green", 0.5f, PixelColorBlendingMode.Add, 0.3f }, { false, "Green", 0.5f, PixelColorBlendingMode.Add, 0.3f },
{ false, "HotPink", 0.8f, PixelColorBlendingMode.Add, 0.8f }, { false, "HotPink", 0.8f, PixelColorBlendingMode.Add, 0.8f },
{ true, "Blue", 0.5f, PixelColorBlendingMode.Normal, 1.0f }, { true, "Blue", 0.5f, PixelColorBlendingMode.Normal, 1.0f },
{ true, "Blue", 1.0f, PixelColorBlendingMode.Normal, 0.5f }, { true, "Blue", 1.0f, PixelColorBlendingMode.Normal, 0.5f },
{ true, "Green", 0.5f, PixelColorBlendingMode.Normal, 0.3f }, { true, "Green", 0.5f, PixelColorBlendingMode.Normal, 0.3f },
{ true, "HotPink", 0.8f, PixelColorBlendingMode.Normal, 0.8f }, { true, "HotPink", 0.8f, PixelColorBlendingMode.Normal, 0.8f },
{ true, "Blue", 0.5f, PixelColorBlendingMode.Multiply, 1.0f }, { true, "Blue", 0.5f, PixelColorBlendingMode.Multiply, 1.0f },
{ true, "Blue", 1.0f, PixelColorBlendingMode.Multiply, 0.5f }, { true, "Blue", 1.0f, PixelColorBlendingMode.Multiply, 0.5f },
{ true, "Green", 0.5f, PixelColorBlendingMode.Multiply, 0.3f }, { true, "Green", 0.5f, PixelColorBlendingMode.Multiply, 0.3f },
{ true, "HotPink", 0.8f, PixelColorBlendingMode.Multiply, 0.8f }, { true, "HotPink", 0.8f, PixelColorBlendingMode.Multiply, 0.8f },
{ true, "Blue", 0.5f, PixelColorBlendingMode.Add, 1.0f }, { true, "Blue", 0.5f, PixelColorBlendingMode.Add, 1.0f },
{ true, "Blue", 1.0f, PixelColorBlendingMode.Add, 0.5f }, { true, "Blue", 1.0f, PixelColorBlendingMode.Add, 0.5f },
{ true, "Green", 0.5f, PixelColorBlendingMode.Add, 0.3f }, { true, "Green", 0.5f, PixelColorBlendingMode.Add, 0.3f },
@ -155,8 +162,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing
var options = new GraphicsOptions(false) var options = new GraphicsOptions(false)
{ {
ColorBlendingMode = blenderMode, ColorBlendingMode = blenderMode, BlendPercentage = blendPercentage
BlendPercentage = blendPercentage
}; };
if (triggerFillRegion) if (triggerFillRegion)
@ -185,11 +191,13 @@ namespace SixLabors.ImageSharp.Tests.Drawing
appendPixelTypeToFileName: false, appendPixelTypeToFileName: false,
appendSourceFileOrDescription: false); appendSourceFileOrDescription: false);
PixelBlender<TPixel> blender = PixelOperations<TPixel>.Instance.GetPixelBlender(blenderMode, PixelAlphaCompositionMode.SrcOver); PixelBlender<TPixel> blender = PixelOperations<TPixel>.Instance.GetPixelBlender(
blenderMode,
PixelAlphaCompositionMode.SrcOver);
TPixel expectedPixel = blender.Blend(bgColor, fillColor, blendPercentage); TPixel expectedPixel = blender.Blend(bgColor, fillColor, blendPercentage);
image.ComparePixelBufferTo(expectedPixel); image.ComparePixelBufferTo(expectedPixel);
} }
} }
} }
} }

338
tests/ImageSharp.Tests/Helpers/ParallelHelperTests.cs

@ -0,0 +1,338 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Numerics;
using System.Threading;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.Memory;
using SixLabors.Primitives;
using Xunit;
using Xunit.Abstractions;
namespace SixLabors.ImageSharp.Tests.Helpers
{
public class ParallelHelperTests
{
private readonly ITestOutputHelper Output;
public ParallelHelperTests(ITestOutputHelper output)
{
this.Output = output;
}
/// <summary>
/// maxDegreeOfParallelism, minY, maxY, expectedStepLength, expectedLastStepLength
/// </summary>
public static TheoryData<int, int, int, int, int> IterateRows_OverMinimumPixelsLimit_Data =
new TheoryData<int, int, int, int, int>()
{
{ 1, 0, 100, -1, 100 },
{ 2, 0, 9, 5, 4 },
{ 4, 0, 19, 5, 4 },
{ 2, 10, 19, 5, 4 },
{ 4, 0, 200, 50, 50 },
{ 4, 123, 323, 50, 50 },
{ 4, 0, 1201, 301, 298 },
{ 8, 10, 236, 29, 23 }
};
[Theory]
[MemberData(nameof(IterateRows_OverMinimumPixelsLimit_Data))]
public void IterateRows_OverMinimumPixelsLimit_IntervalsAreCorrect(
int maxDegreeOfParallelism,
int minY,
int maxY,
int expectedStepLength,
int expectedLastStepLength)
{
var parallelSettings = new ParallelExecutionSettings(
maxDegreeOfParallelism,
1,
Configuration.Default.MemoryAllocator);
var rectangle = new Rectangle(0, minY, 10, maxY - minY);
int actualNumberOfSteps = 0;
ParallelHelper.IterateRows(
rectangle,
parallelSettings,
rows =>
{
Assert.True(rows.Min >= minY);
Assert.True(rows.Max <= maxY);
int step = rows.Max - rows.Min;
int expected = rows.Max < maxY ? expectedStepLength : expectedLastStepLength;
Interlocked.Increment(ref actualNumberOfSteps);
Assert.Equal(expected, step);
});
Assert.Equal(maxDegreeOfParallelism, actualNumberOfSteps);
}
[Theory]
[MemberData(nameof(IterateRows_OverMinimumPixelsLimit_Data))]
public void IterateRows_OverMinimumPixelsLimit_ShouldVisitAllRows(
int maxDegreeOfParallelism,
int minY,
int maxY,
int expectedStepLength,
int expectedLastStepLength)
{
var parallelSettings = new ParallelExecutionSettings(
maxDegreeOfParallelism,
1,
Configuration.Default.MemoryAllocator);
var rectangle = new Rectangle(0, minY, 10, maxY - minY);
int[] expectedData = Enumerable.Repeat(0, minY).Concat(Enumerable.Range(minY, maxY - minY)).ToArray();
int[] actualData = new int[maxY];
ParallelHelper.IterateRows(
rectangle,
parallelSettings,
rows =>
{
for (int y = rows.Min; y < rows.Max; y++)
{
actualData[y] = y;
}
});
Assert.Equal(expectedData, actualData);
}
[Theory]
[MemberData(nameof(IterateRows_OverMinimumPixelsLimit_Data))]
public void IterateRowsWithTempBuffer_OverMinimumPixelsLimit(
int maxDegreeOfParallelism,
int minY,
int maxY,
int expectedStepLength,
int expectedLastStepLength)
{
var parallelSettings = new ParallelExecutionSettings(
maxDegreeOfParallelism,
1,
Configuration.Default.MemoryAllocator);
var rectangle = new Rectangle(0, minY, 10, maxY - minY);
var bufferHashes = new ConcurrentBag<int>();
int actualNumberOfSteps = 0;
ParallelHelper.IterateRowsWithTempBuffer(
rectangle,
parallelSettings,
(RowInterval rows, Memory<Vector4> buffer) =>
{
Assert.True(rows.Min >= minY);
Assert.True(rows.Max <= maxY);
bufferHashes.Add(buffer.GetHashCode());
int step = rows.Max - rows.Min;
int expected = rows.Max < maxY ? expectedStepLength : expectedLastStepLength;
Interlocked.Increment(ref actualNumberOfSteps);
Assert.Equal(expected, step);
});
Assert.Equal(maxDegreeOfParallelism, actualNumberOfSteps);
int numberOfDifferentBuffers = bufferHashes.Distinct().Count();
Assert.Equal(actualNumberOfSteps, numberOfDifferentBuffers);
}
[Theory]
[MemberData(nameof(IterateRows_OverMinimumPixelsLimit_Data))]
public void IterateRowsWithTempBuffer_OverMinimumPixelsLimit_ShouldVisitAllRows(
int maxDegreeOfParallelism,
int minY,
int maxY,
int expectedStepLength,
int expectedLastStepLength)
{
var parallelSettings = new ParallelExecutionSettings(
maxDegreeOfParallelism,
1,
Configuration.Default.MemoryAllocator);
var rectangle = new Rectangle(0, minY, 10, maxY - minY);
int[] expectedData = Enumerable.Repeat(0, minY).Concat(Enumerable.Range(minY, maxY - minY)).ToArray();
int[] actualData = new int[maxY];
ParallelHelper.IterateRowsWithTempBuffer(
rectangle,
parallelSettings,
(RowInterval rows, Memory<Vector4> buffer) =>
{
for (int y = rows.Min; y < rows.Max; y++)
{
actualData[y] = y;
}
});
Assert.Equal(expectedData, actualData);
}
public static TheoryData<int, int, int, int, int, int, int> IterateRows_WithEffectiveMinimumPixelsLimit_Data =
new TheoryData<int, int, int, int, int, int, int>()
{
{ 2, 200, 50, 2, 1, -1, 2 },
{ 2, 200, 200, 1, 1, -1, 1 },
{ 4, 200, 100, 4, 2, 2, 2 },
{ 4, 300, 100, 8, 3, 3, 2 },
{ 2, 5000, 1, 4500, 1, -1, 4500 },
{ 2, 5000, 1, 5000, 1, -1, 5000 },
{ 2, 5000, 1, 5001, 2, 2501, 2500 },
};
[Theory]
[MemberData(nameof(IterateRows_WithEffectiveMinimumPixelsLimit_Data))]
public void IterateRows_WithEffectiveMinimumPixelsLimit(
int maxDegreeOfParallelism,
int minimumPixelsProcessedPerTask,
int width,
int height,
int expectedNumberOfSteps,
int expectedStepLength,
int expectedLastStepLength)
{
var parallelSettings = new ParallelExecutionSettings(
maxDegreeOfParallelism,
minimumPixelsProcessedPerTask,
Configuration.Default.MemoryAllocator);
var rectangle = new Rectangle(0, 0, width, height);
int actualNumberOfSteps = 0;
ParallelHelper.IterateRows(
rectangle,
parallelSettings,
rows =>
{
Assert.True(rows.Min >= 0);
Assert.True(rows.Max <= height);
int step = rows.Max - rows.Min;
int expected = rows.Max < height ? expectedStepLength : expectedLastStepLength;
Interlocked.Increment(ref actualNumberOfSteps);
Assert.Equal(expected, step);
});
Assert.Equal(expectedNumberOfSteps, actualNumberOfSteps);
}
[Theory]
[MemberData(nameof(IterateRows_WithEffectiveMinimumPixelsLimit_Data))]
public void IterateRowsWithTempBuffer_WithEffectiveMinimumPixelsLimit(
int maxDegreeOfParallelism,
int minimumPixelsProcessedPerTask,
int width,
int height,
int expectedNumberOfSteps,
int expectedStepLength,
int expectedLastStepLength)
{
var parallelSettings = new ParallelExecutionSettings(
maxDegreeOfParallelism,
minimumPixelsProcessedPerTask,
Configuration.Default.MemoryAllocator);
var rectangle = new Rectangle(0, 0, width, height);
int actualNumberOfSteps = 0;
ParallelHelper.IterateRowsWithTempBuffer(
rectangle,
parallelSettings,
(RowInterval rows, Memory<Vector4> buffer) =>
{
Assert.True(rows.Min >= 0);
Assert.True(rows.Max <= height);
int step = rows.Max - rows.Min;
int expected = rows.Max < height ? expectedStepLength : expectedLastStepLength;
Interlocked.Increment(ref actualNumberOfSteps);
Assert.Equal(expected, step);
});
Assert.Equal(expectedNumberOfSteps, actualNumberOfSteps);
}
public static readonly TheoryData<int, int, int, int, int, int, int> IterateRectangularBuffer_Data =
new TheoryData<int, int, int, int, int, int, int>()
{
{ 8, 582, 453, 10, 10, 291, 226 }, // boundary data from DetectEdgesTest.DetectEdges_InBox
{ 2, 582, 453, 10, 10, 291, 226 },
{ 16, 582, 453, 10, 10, 291, 226 },
{ 16, 582, 453, 10, 10, 1, 226 },
{ 16, 1, 453, 0, 10, 1, 226 },
};
[Theory]
[MemberData(nameof(IterateRectangularBuffer_Data))]
public void IterateRectangularBuffer(
int maxDegreeOfParallelism,
int bufferWidth,
int bufferHeight,
int rectX,
int rectY,
int rectWidth,
int rectHeight)
{
MemoryAllocator memoryAllocator = Configuration.Default.MemoryAllocator;
using (Buffer2D<Point> expected = memoryAllocator.Allocate2D<Point>(bufferWidth, bufferHeight, AllocationOptions.Clean))
using (Buffer2D<Point> actual = memoryAllocator.Allocate2D<Point>(bufferWidth, bufferHeight, AllocationOptions.Clean))
{
var rect = new Rectangle(rectX, rectY, rectWidth, rectHeight);
void FillRow(int y, Buffer2D<Point> buffer)
{
for (int x = rect.Left; x < rect.Right; x++)
{
buffer[x, y] = new Point(x, y);
}
}
// Fill Expected data:
for (int y = rectY; y < rect.Bottom; y++)
{
FillRow(y, expected);
}
// Fill actual data using IterateRows:
var settings = new ParallelExecutionSettings(maxDegreeOfParallelism, memoryAllocator);
ParallelHelper.IterateRows(rect, settings,
rows =>
{
this.Output.WriteLine(rows.ToString());
for (int y = rows.Min; y < rows.Max; y++)
{
FillRow(y, actual);
}
});
// Assert:
TestImageExtensions.CompareBuffers(expected.Span, actual.Span);
}
}
}
}

38
tests/ImageSharp.Tests/Helpers/RowIntervalTests.cs

@ -0,0 +1,38 @@
using System;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory;
using SixLabors.Memory;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Helpers
{
public class RowIntervalTests
{
[Theory]
[InlineData(10, 20, 5, 10)]
[InlineData(1, 10, 0, 10)]
[InlineData(1, 10, 5, 8)]
[InlineData(1, 1, 0, 1)]
[InlineData(10, 20, 9, 10)]
[InlineData(10, 20, 0, 1)]
public void GetMultiRowSpan(int width, int height, int min, int max)
{
using (Buffer2D<int> buffer = Configuration.Default.MemoryAllocator.Allocate2D<int>(width, height))
{
var rows = new RowInterval(min, max);
Span<int> span = buffer.GetMultiRowSpan(rows);
ref int expected0 = ref buffer.Span[min * width];
int expectedLength = (max - min) * width;
ref int actual0 = ref span[0];
Assert.Equal(span.Length, expectedLength);
Assert.True(Unsafe.AreSame(ref expected0, ref actual0));
}
}
}
}

2
tests/ImageSharp.Tests/Processing/Processors/Convolution/DetectEdgesTest.cs

@ -10,8 +10,6 @@ using Xunit;
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Convolution namespace SixLabors.ImageSharp.Tests.Processing.Processors.Convolution
{ {
using SixLabors.ImageSharp.Advanced;
public class DetectEdgesTest : FileTestBase public class DetectEdgesTest : FileTestBase
{ {
private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.0456F); private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.0456F);

24
tests/ImageSharp.Tests/Processing/Processors/Transforms/CropTest.cs

@ -1,24 +1,34 @@
// Copyright (c) Six Labors and contributors. // Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using SixLabors.Primitives;
using Xunit; using Xunit;
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
{ {
[GroupOutput("Transforms")]
public class CropTest : FileTestBase public class CropTest : FileTestBase
{ {
[Theory] [Theory]
[WithFileCollection(nameof(DefaultFiles), DefaultPixelType)] [WithTestPatternImages(70, 30, PixelTypes.Rgba32, 0, 0, 70, 30)]
public void ImageShouldCrop<TPixel>(TestImageProvider<TPixel> provider) [WithTestPatternImages(50, 50, PixelTypes.Rgba32, -1, -1, 100, 200)]
[WithTestPatternImages(30, 70, PixelTypes.Rgba32, 7, 13, 20, 50)]
public void Crop<TPixel>(TestImageProvider<TPixel> provider, int x, int y, int w, int h)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
using (Image<TPixel> image = provider.GetImage()) var rect = new Rectangle(x, y, w, h);
{ FormattableString info = $"X{x}Y{y}.W{w}H{h}";
image.Mutate(x => x.Crop(image.Width / 2, image.Height / 2)); provider.RunValidatingProcessorTest(
image.DebugSave(provider); ctx => ctx.Crop(rect),
} info,
appendPixelTypeToFileName: false,
comparer: ImageComparer.Exact);
} }
} }
} }

19
tests/ImageSharp.Tests/Processing/Processors/Transforms/FlipTests.cs

@ -5,23 +5,24 @@ using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing;
using Xunit; using Xunit;
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
{ {
using SixLabors.ImageSharp.Processing; [GroupOutput("Transforms")]
public class FlipTests public class FlipTests
{ {
public static readonly TheoryData<FlipMode> FlipValues public static readonly TheoryData<FlipMode> FlipValues =
= new TheoryData<FlipMode> new TheoryData<FlipMode>
{ {
{ FlipMode.None }, FlipMode.None,
{ FlipMode.Vertical }, FlipMode.Vertical,
{ FlipMode.Horizontal }, FlipMode.Horizontal,
}; };
[Theory] [Theory]
[WithTestPatternImages(nameof(FlipValues), 20, 37, PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(FlipValues), 53, 37, PixelTypes.Rgba32)] [WithTestPatternImages(nameof(FlipValues), 53, 37, PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(FlipValues), 17, 32, PixelTypes.Rgba32)] [WithTestPatternImages(nameof(FlipValues), 17, 32, PixelTypes.Rgba32)]
public void Flip<TPixel>(TestImageProvider<TPixel> provider, FlipMode flipMode) public void Flip<TPixel>(TestImageProvider<TPixel> provider, FlipMode flipMode)

23
tests/ImageSharp.Tests/Processing/Processors/Transforms/RotateTests.cs

@ -7,7 +7,8 @@ using Xunit;
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
{ {
public class RotateTests : FileTestBase [GroupOutput("Transforms")]
public class RotateTests
{ {
public static readonly TheoryData<float> RotateAngles public static readonly TheoryData<float> RotateAngles
= new TheoryData<float> = new TheoryData<float>
@ -25,29 +26,21 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
}; };
[Theory] [Theory]
[WithTestPatternImages(nameof(RotateAngles), 100, 50, DefaultPixelType)] [WithTestPatternImages(nameof(RotateAngles), 100, 50, PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(RotateAngles), 50, 100, DefaultPixelType)] [WithTestPatternImages(nameof(RotateAngles), 50, 100, PixelTypes.Rgba32)]
public void Rotate_WithAngle<TPixel>(TestImageProvider<TPixel> provider, float value) public void Rotate_WithAngle<TPixel>(TestImageProvider<TPixel> provider, float value)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
using (Image<TPixel> image = provider.GetImage()) provider.RunValidatingProcessorTest(ctx => ctx.Rotate(value), value, appendPixelTypeToFileName: false);
{
image.Mutate(x => x.Rotate(value));
image.DebugSave(provider, value);
}
} }
[Theory] [Theory]
[WithTestPatternImages(nameof(RotateEnumValues), 100, 50, DefaultPixelType)] [WithTestPatternImages(nameof(RotateEnumValues), 100, 50, PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(RotateEnumValues), 50, 100, DefaultPixelType)] [WithTestPatternImages(nameof(RotateEnumValues), 50, 100, PixelTypes.Rgba32)]
public void Rotate_WithRotateTypeEnum<TPixel>(TestImageProvider<TPixel> provider, RotateMode value) public void Rotate_WithRotateTypeEnum<TPixel>(TestImageProvider<TPixel> provider, RotateMode value)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
using (Image<TPixel> image = provider.GetImage()) provider.RunValidatingProcessorTest(ctx => ctx.Rotate(value), value, appendPixelTypeToFileName: false);
{
image.Mutate(x => x.Rotate(value));
image.DebugSave(provider, value);
}
} }
} }
} }

2
tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs

@ -20,7 +20,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
private static readonly ImageComparer TolerantComparer = ImageComparer.TolerantPercentage(0.5f, 3); private static readonly ImageComparer TolerantComparer = ImageComparer.TolerantPercentage(0.5f, 3);
private ITestOutputHelper Output { get; } private ITestOutputHelper Output { get; }
public static readonly TheoryData<string> ResamplerNames = new TheoryData<string> public static readonly TheoryData<string> ResamplerNames = new TheoryData<string>
{ {
nameof(KnownResamplers.Bicubic), nameof(KnownResamplers.Bicubic),

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

@ -441,14 +441,23 @@ namespace SixLabors.ImageSharp.Tests
{ {
Span<TPixel> actualPixels = image.GetPixelSpan(); Span<TPixel> actualPixels = image.GetPixelSpan();
Assert.True(expectedPixels.Length == actualPixels.Length, "Buffer sizes are not equal!"); CompareBuffers(expectedPixels, actualPixels);
for (int i = 0; i < expectedPixels.Length; i++) return image;
}
public static void CompareBuffers<T>(Span<T> expected, Span<T> actual)
where T : struct, IEquatable<T>
{
Assert.True(expected.Length == actual.Length, "Buffer sizes are not equal!");
for (int i = 0; i < expected.Length; i++)
{ {
Assert.True(expectedPixels[i].Equals(actualPixels[i]), $"Pixels are different on position {i}!"); T x = expected[i];
} T a = actual[i];
return image; Assert.True(x.Equals(a), $"Buffers differ at position {i}! Expected: {x} | Actual: {a}");
}
} }
/// <summary> /// <summary>

2
tests/Images/External

@ -1 +1 @@
Subproject commit 6abc3bc0ac253a24c9e88e68d7b7d853350a85da Subproject commit 7f4d2d64c6b820ca2b6827e6a8540a1013305ccf
Loading…
Cancel
Save