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.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Drawing
{
/// <summary>
/// Combines two images together by blending the pixels.
/// </summary>
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Drawing
{
/// <summary>
/// Combines two images together by blending the pixels.
/// </summary>
/// <typeparam name="TPixelDst">The pixel format of destination image.</typeparam>
/// <typeparam name="TPixelSrc">The pixel format os source image.</typeparam>
internal class DrawImageProcessor<TPixelDst, TPixelSrc> : ImageProcessor<TPixelDst>
/// <typeparam name="TPixelSrc">The pixel format of source image.</typeparam>
internal class DrawImageProcessor<TPixelDst, TPixelSrc> : ImageProcessor<TPixelDst>
where TPixelDst : struct, IPixel<TPixelDst>
where TPixelSrc : struct, IPixel<TPixelSrc>
where TPixelSrc : struct, IPixel<TPixelSrc>
{
/// <summary>
/// Initializes a new instance of the <see cref="DrawImageProcessor{TPixelDst, TPixelSrc}"/> class.
/// </summary>
/// <param name="image">The image to blend with the currently processing image.</param>
/// <summary>
/// Initializes a new instance of the <see cref="DrawImageProcessor{TPixelDst, TPixelSrc}"/> class.
/// </summary>
/// <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="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="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)
{
Guard.MustBeBetweenOrEqualTo(opacity, 0, 1, nameof(opacity));
this.Image = image;
this.Opacity = opacity;
this.Blender = PixelOperations<TPixelDst>.Instance.GetPixelBlender(colorBlendingMode, alphaCompositionMode);
this.Location = location;
}
/// <summary>
/// Gets the image to blend
/// </summary>
public Image<TPixelSrc> Image { get; }
/// <summary>
/// Gets the opacity of the image to blend
/// </summary>
public float Opacity { get; }
/// <summary>
/// Gets the pixel blender
/// </summary>
public PixelBlender<TPixelDst> Blender { get; }
/// <summary>
/// Gets the location to draw the blended image
/// </summary>
public Point Location { get; }
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixelDst> source, Rectangle sourceRectangle, Configuration configuration)
{
Image<TPixelSrc> targetImage = this.Image;
PixelBlender<TPixelDst> blender = this.Blender;
int locationY = this.Location.Y;
// Align start/end positions.
Rectangle bounds = targetImage.Bounds();
int minX = Math.Max(this.Location.X, sourceRectangle.X);
int maxX = Math.Min(this.Location.X + bounds.Width, sourceRectangle.Width);
int targetX = minX - this.Location.X;
int minY = Math.Max(this.Location.Y, sourceRectangle.Y);
int maxY = Math.Min(this.Location.Y + bounds.Height, sourceRectangle.Bottom);
int width = maxX - minX;
/// <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)
{
Guard.MustBeBetweenOrEqualTo(opacity, 0, 1, nameof(opacity));
this.Image = image;
this.Opacity = opacity;
this.Blender = PixelOperations<TPixelDst>.Instance.GetPixelBlender(colorBlendingMode, alphaCompositionMode);
this.Location = location;
}
/// <summary>
/// Gets the image to blend
/// </summary>
public Image<TPixelSrc> Image { get; }
/// <summary>
/// Gets the opacity of the image to blend
/// </summary>
public float Opacity { get; }
/// <summary>
/// Gets the pixel blender
/// </summary>
public PixelBlender<TPixelDst> Blender { get; }
/// <summary>
/// Gets the location to draw the blended image
/// </summary>
public Point Location { get; }
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixelDst> source, Rectangle sourceRectangle, Configuration configuration)
{
Image<TPixelSrc> targetImage = this.Image;
PixelBlender<TPixelDst> blender = this.Blender;
int locationY = this.Location.Y;
// Align start/end positions.
Rectangle bounds = targetImage.Bounds();
int minX = Math.Max(this.Location.X, sourceRectangle.X);
int maxX = Math.Min(this.Location.X + bounds.Width, sourceRectangle.Width);
int targetX = minX - this.Location.X;
int minY = Math.Max(this.Location.Y, sourceRectangle.Y);
int maxY = Math.Min(this.Location.Y + bounds.Height, sourceRectangle.Bottom);
int width = maxX - minX;
MemoryAllocator memoryAllocator = this.Image.GetConfiguration().MemoryAllocator;
ParallelFor.WithConfiguration(
minY,
maxY,
configuration,
y =>
{
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);
});
}
}
var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY);
ParallelHelper.IterateRows(
workingRect,
configuration,
rows =>
{
for (int y = rows.Min; y < rows.Max; y++)
{
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.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Drawing
{
/// <summary>
/// Using the brush as a source of pixels colors blends the brush color with source.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal class FillProcessor<TPixel> : ImageProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// The brush.
/// </summary>
private readonly IBrush<TPixel> brush;
private readonly GraphicsOptions options;
/// <summary>
/// Initializes a new instance of the <see cref="FillProcessor{TPixel}"/> class.
/// </summary>
/// <param name="brush">The brush to source pixel colors from.</param>
/// <param name="options">The options</param>
public FillProcessor(IBrush<TPixel> brush, GraphicsOptions options)
{
this.brush = brush;
this.options = options;
}
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration)
{
int startX = sourceRectangle.X;
int endX = sourceRectangle.Right;
int startY = sourceRectangle.Y;
int endY = sourceRectangle.Bottom;
// Align start/end positions.
int minX = Math.Max(0, startX);
int maxX = Math.Min(source.Width, endX);
int minY = Math.Max(0, startY);
int maxY = Math.Min(source.Height, endY);
int width = maxX - minX;
// If there's no reason for blending, then avoid it.
if (this.IsSolidBrushWithoutBlending(out SolidBrush<TPixel> solidBrush))
{
ParallelFor.WithConfiguration(
minY,
maxY,
configuration,
y =>
{
source.GetPixelRowSpan(y).Slice(minX, width).Fill(solidBrush.Color);
});
}
else
{
// Reset offset if necessary.
if (minX > 0)
{
startX = 0;
}
if (minY > 0)
{
startY = 0;
}
using (IMemoryOwner<float> amount = source.MemoryAllocator.Allocate<float>(width))
using (BrushApplicator<TPixel> applicator = this.brush.CreateApplicator(
source,
sourceRectangle,
this.options))
{
amount.GetSpan().Fill(1f);
ParallelFor.WithConfiguration(
minY,
maxY,
configuration,
y =>
{
int offsetY = y - startY;
int offsetX = minX - startX;
applicator.Apply(amount.GetSpan(), offsetX, offsetY);
});
}
}
}
private bool IsSolidBrushWithoutBlending(out SolidBrush<TPixel> solidBrush)
{
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Drawing
{
/// <summary>
/// Using the brush as a source of pixels colors blends the brush color with source.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal class FillProcessor<TPixel> : ImageProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// The brush.
/// </summary>
private readonly IBrush<TPixel> brush;
private readonly GraphicsOptions options;
/// <summary>
/// Initializes a new instance of the <see cref="FillProcessor{TPixel}"/> class.
/// </summary>
/// <param name="brush">The brush to source pixel colors from.</param>
/// <param name="options">The options</param>
public FillProcessor(IBrush<TPixel> brush, GraphicsOptions options)
{
this.brush = brush;
this.options = options;
}
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration)
{
int startX = sourceRectangle.X;
int endX = sourceRectangle.Right;
int startY = sourceRectangle.Y;
int endY = sourceRectangle.Bottom;
// Align start/end positions.
int minX = Math.Max(0, startX);
int maxX = Math.Min(source.Width, endX);
int minY = Math.Max(0, startY);
int maxY = Math.Min(source.Height, endY);
int width = maxX - minX;
var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY);
// If there's no reason for blending, then avoid it.
if (this.IsSolidBrushWithoutBlending(out SolidBrush<TPixel> solidBrush))
{
ParallelExecutionSettings parallelSettings = configuration.GetParallelSettings().MultiplyMinimumPixelsPerTask(4);
ParallelHelper.IterateRows(
workingRect,
parallelSettings,
rows =>
{
for (int y = rows.Min; y < rows.Max; y++)
{
source.GetPixelRowSpan(y).Slice(minX, width).Fill(solidBrush.Color);
}
});
}
else
{
// Reset offset if necessary.
if (minX > 0)
{
startX = 0;
}
if (minY > 0)
{
startY = 0;
}
using (IMemoryOwner<float> amount = source.MemoryAllocator.Allocate<float>(width))
using (BrushApplicator<TPixel> applicator = this.brush.CreateApplicator(
source,
sourceRectangle,
this.options))
{
amount.GetSpan().Fill(1f);
ParallelHelper.IterateRows(
workingRect,
configuration,
rows =>
{
for (int y = rows.Min; y < rows.Max; y++)
{
int offsetY = y - startY;
int offsetX = minX - startX;
applicator.Apply(amount.GetSpan(), offsetX, offsetY);
}
});
}
}
}
private bool IsSolidBrushWithoutBlending(out SolidBrush<TPixel> solidBrush)
{
solidBrush = this.brush as SolidBrush<TPixel>;
if (solidBrush == null)
@ -109,7 +118,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing
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>
/// Gets or sets the maximum number of concurrent tasks enabled in ImageSharp algorithms
/// configured with this <see cref="Configuration"/> instance.
/// Initialized with <see cref="Environment.ProcessorCount"/> by default.
/// </summary>
public int MaxDegreeOfParallelism
{

50
src/ImageSharp/ImageFrame{TPixel}.cs

@ -9,6 +9,7 @@ using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.MetaData;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
using SixLabors.Primitives;
@ -288,20 +289,24 @@ namespace SixLabors.ImageSharp
var target = new ImageFrame<TPixel2>(configuration, this.Width, this.Height, this.MetaData.DeepClone());
ParallelFor.WithTemporaryBuffer(
0,
this.Height,
ParallelHelper.IterateRowsWithTempBuffer<Vector4>(
this.Bounds(),
configuration,
this.Width,
(int y, IMemoryOwner<Vector4> tempRowBuffer) =>
{
Span<TPixel> sourceRow = this.GetPixelRowSpan(y);
Span<TPixel2> targetRow = target.GetPixelRowSpan(y);
Span<Vector4> tempRowSpan = tempRowBuffer.GetSpan();
PixelOperations<TPixel>.Instance.ToScaledVector4(sourceRow, tempRowSpan, sourceRow.Length);
PixelOperations<TPixel2>.Instance.PackFromScaledVector4(tempRowSpan, targetRow, targetRow.Length);
});
(rows, tempRowBuffer) =>
{
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> sourceRow = this.GetPixelRowSpan(y);
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);
}
});
return target;
}
@ -313,15 +318,16 @@ namespace SixLabors.ImageSharp
/// <param name="value">The value to initialize the bitmap with.</param>
internal void Clear(ParallelOptions parallelOptions, TPixel value)
{
Parallel.For(
0,
this.Height,
parallelOptions,
y =>
{
Span<TPixel> targetRow = this.GetPixelRowSpan(y);
targetRow.Fill(value);
});
Span<TPixel> span = this.GetPixelSpan();
if (value.Equals(default))
{
span.Clear();
}
else
{
span.Fill(value);
}
}
/// <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="rectangle">The rectangle subarea</param>
/// <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);
public static BufferArea<T> GetArea<T>(this Buffer2D<T> buffer, int x, int y, int width, int height)
where T : struct
{
var rectangle = new Rectangle(x, y, width, height);
return new BufferArea<T>(buffer, rectangle);
}
where T : struct => new BufferArea<T>(buffer, new Rectangle(x, y, width, height));
public static BufferArea<T> GetAreaBetweenRows<T>(this Buffer2D<T> buffer, int minY, int maxY)
where T : struct => new BufferArea<T>(buffer, new Rectangle(0, minY, buffer.Width, maxY - minY));
/// <summary>
/// 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>
public static BufferArea<T> GetArea<T>(this Buffer2D<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.
using System;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Binarization
@ -56,7 +56,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization
public TPixel LowerColor { get; set; }
/// <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;
TPixel upper = this.UpperColor;
@ -70,25 +73,29 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization
bool isAlphaOnly = typeof(TPixel) == typeof(Alpha8);
ParallelFor.WithConfiguration(
startY,
endY,
var workingRect = Rectangle.FromLTRB(startX, startY, endX, endY);
ParallelHelper.IterateRows(
workingRect,
configuration,
y =>
rows =>
{
Span<TPixel> row = source.GetPixelRowSpan(y);
Rgba32 rgba = default;
for (int x = startX; x < endX; x++)
for (int y = rows.Min; y < rows.Max; y++)
{
ref TPixel color = ref row[x];
color.ToRgba32(ref rgba);
Span<TPixel> row = source.GetPixelRowSpan(y);
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
float luminance = isAlphaOnly
? rgba.A
: (.2126F * rgba.R) + (.7152F * rgba.G) + (.0722F * rgba.B);
color = luminance >= threshold ? upper : lower;
// Convert to grayscale using ITU-R Recommendation BT.709 if required
float luminance = isAlphaOnly
? rgba.A
: (.2126F * rgba.R) + (.7152F * rgba.G) + (.0722F * rgba.B);
color = luminance >= threshold ? upper : lower;
}
}
});
}

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

@ -3,12 +3,12 @@
using System;
using System.Numerics;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Primitives;
using SixLabors.Memory;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Convolution
@ -42,7 +42,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
public DenseMatrix<float> KernelY { get; }
/// <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 kernelYWidth = this.KernelY.Columns;
@ -58,71 +61,77 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
int maxY = endY - 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);
ParallelFor.WithConfiguration(
startY,
endY,
configuration,
y =>
{
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
Span<TPixel> targetRow = targetPixels.GetRowSpan(y);
var workingRectangle = Rectangle.FromLTRB(startX, startY, endX, endY);
for (int x = startX; x < endX; x++)
ParallelHelper.IterateRows(
workingRectangle,
configuration,
rows =>
{
float rX = 0;
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++)
for (int y = rows.Min; y < rows.Max; y++)
{
int fyr = fy - radiusY;
int offsetY = y + fyr;
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
Span<TPixel> targetRow = targetPixels.GetRowSpan(y);
offsetY = offsetY.Clamp(0, maxY);
Span<TPixel> sourceOffsetRow = source.GetPixelRowSpan(offsetY);
for (int fx = 0; fx < kernelXWidth; fx++)
for (int x = startX; x < endX; x++)
{
int fxr = fx - radiusX;
int offsetX = x + fxr;
offsetX = offsetX.Clamp(0, maxX);
Vector4 currentColor = sourceOffsetRow[offsetX].ToVector4().Premultiply();
if (fy < kernelXHeight)
float rX = 0;
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++)
{
Vector4 kx = this.KernelX[fy, fx] * currentColor;
rX += kx.X;
gX += kx.Y;
bX += kx.Z;
int fyr = fy - radiusY;
int offsetY = y + fyr;
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)
{
Vector4 ky = this.KernelY[fy, fx] * currentColor;
rY += ky.X;
gY += ky.Y;
bY += ky.Z;
}
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());
}
}
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);
}

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

@ -6,6 +6,7 @@ using System.Numerics;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Primitives;
using SixLabors.ImageSharp.Processing.Processors;
@ -82,43 +83,47 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
int maxY = endY - 1;
int maxX = endX - 1;
ParallelFor.WithConfiguration(
startY,
endY,
configuration,
y =>
{
Span<TPixel> targetRow = targetPixels.GetRowSpan(y);
var workingRectangle = Rectangle.FromLTRB(startX, startY, endX, endY);
for (int x = startX; x < endX; x++)
ParallelHelper.IterateRows(
workingRectangle,
configuration,
rows =>
{
Vector4 destination = default;
// Apply each matrix multiplier to the color components for each pixel.
for (int fy = 0; fy < kernelHeight; fy++)
for (int y = rows.Min; y < rows.Max; y++)
{
int fyr = fy - radiusY;
int offsetY = y + fyr;
Span<TPixel> targetRow = targetPixels.GetRowSpan(y);
offsetY = offsetY.Clamp(0, maxY);
Span<TPixel> row = sourcePixels.GetRowSpan(offsetY);
for (int fx = 0; fx < kernelWidth; fx++)
for (int x = startX; x < endX; x++)
{
int fxr = fx - radiusX;
int offsetX = x + fxr;
Vector4 destination = default;
// 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();
destination += kernel[fy, fx] * currentColor;
for (int fx = 0; fx < kernelWidth; fx++)
{
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 SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Primitives;
using SixLabors.ImageSharp.Processing.Processors;
@ -52,50 +53,55 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
{
source.CopyTo(targetPixels);
ParallelFor.WithConfiguration(
startY,
endY,
configuration,
y =>
{
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
Span<TPixel> targetRow = targetPixels.GetRowSpan(y);
for (int x = startX; x < endX; x++)
{
float red = 0;
float green = 0;
float blue = 0;
// Apply each matrix multiplier to the color components for each pixel.
for (int fy = 0; fy < kernelLength; fy++)
{
int fyr = fy - radius;
int offsetY = y + fyr;
offsetY = offsetY.Clamp(0, maxY);
Span<TPixel> sourceOffsetRow = source.GetPixelRowSpan(offsetY);
for (int fx = 0; fx < kernelLength; fx++)
{
int fxr = fx - radius;
int offsetX = x + fxr;
offsetX = offsetX.Clamp(0, maxX);
Vector4 currentColor = sourceOffsetRow[offsetX].ToVector4().Premultiply();
currentColor *= this.KernelXY[fy, fx];
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());
}
});
var workingRect = Rectangle.FromLTRB(startX, startY, endX, endY);
ParallelHelper.IterateRows(
workingRect,
configuration,
rows =>
{
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
Span<TPixel> targetRow = targetPixels.GetRowSpan(y);
for (int x = startX; x < endX; x++)
{
float red = 0;
float green = 0;
float blue = 0;
// Apply each matrix multiplier to the color components for each pixel.
for (int fy = 0; fy < kernelLength; fy++)
{
int fyr = fy - radius;
int offsetY = y + fyr;
offsetY = offsetY.Clamp(0, maxY);
Span<TPixel> sourceOffsetRow = source.GetPixelRowSpan(offsetY);
for (int fx = 0; fx < kernelLength; fx++)
{
int fxr = fx - radius;
int offsetX = x + fxr;
offsetX = offsetX.Clamp(0, maxX);
Vector4 currentColor = sourceOffsetRow[offsetX].ToVector4().Premultiply();
currentColor *= this.KernelXY[fy, fx];
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());
}
}
});
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 SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Primitives;
using SixLabors.ImageSharp.Processing.Processors.Filters;
@ -124,6 +125,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
shiftY = 0;
}
var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY);
// Additional runs.
// ReSharper disable once ForCanBeConvertedToForeach
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> targetPixels = source.PixelBuffer;
ParallelFor.WithConfiguration(
minY,
maxY,
ParallelHelper.IterateRows(
workingRect,
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 targetPixelsBase = ref MemoryMarshal.GetReference(targetPixels.GetRowSpan(offsetY));
ref TPixel passPixelsBase =
ref MemoryMarshal.GetReference(passPixels.GetRowSpan(offsetY));
ref TPixel targetPixelsBase =
ref MemoryMarshal.GetReference(targetPixels.GetRowSpan(offsetY));
for (int x = minX; x < maxX; x++)
{
int offsetX = x - shiftX;
for (int x = minX; x < maxX; x++)
{
int offsetX = x - shiftX;
// Grab the max components of the two pixels
ref TPixel currentPassPixel = ref Unsafe.Add(ref passPixelsBase, offsetX);
ref TPixel currentTargetPixel = ref Unsafe.Add(ref targetPixelsBase, offsetX);
// Grab the max components of the two pixels
ref TPixel currentPassPixel = ref Unsafe.Add(ref passPixelsBase, offsetX);
ref TPixel currentTargetPixel =
ref Unsafe.Add(ref targetPixelsBase, offsetX);
var pixelValue = Vector4.Max(
currentPassPixel.ToVector4(),
currentTargetPixel.ToVector4());
var pixelValue = Vector4.Max(
currentPassPixel.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.Numerics;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Effects
@ -49,7 +49,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Effects
public int BrushSize { get; }
/// <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)
{
@ -70,69 +73,74 @@ namespace SixLabors.ImageSharp.Processing.Processors.Effects
{
source.CopyTo(targetPixels);
ParallelFor.WithConfiguration(
startY,
maxY,
var workingRect = Rectangle.FromLTRB(startX, startY, endX, endY);
ParallelHelper.IterateRows(
workingRect,
configuration,
y =>
{
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
Span<TPixel> targetRow = targetPixels.GetRowSpan(y);
for (int x = startX; x < endX; x++)
rows =>
{
int maxIntensity = 0;
int maxIndex = 0;
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
Span<TPixel> targetRow = targetPixels.GetRowSpan(y);
int[] intensityBin = new int[levels];
float[] redBin = new float[levels];
float[] blueBin = new float[levels];
float[] greenBin = new float[levels];
for (int x = startX; x < endX; x++)
{
int maxIntensity = 0;
int maxIndex = 0;
for (int fy = 0; fy <= radius; fy++)
{
int fyr = fy - radius;
int offsetY = y + fyr;
int[] intensityBin = new int[levels];
float[] redBin = new float[levels];
float[] blueBin = new float[levels];
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++)
{
int fxr = fx - radius;
int offsetX = x + fxr;
offsetX = offsetX.Clamp(0, maxX);
Span<TPixel> sourceOffsetRow = source.GetPixelRowSpan(offsetY);
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;
float sourceBlue = vector.Z;
float sourceGreen = vector.Y;
var vector = sourceOffsetRow[offsetX].ToVector4();
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]++;
blueBin[currentIntensity] += sourceBlue;
greenBin[currentIntensity] += sourceGreen;
redBin[currentIntensity] += sourceRed;
int currentIntensity = (int)MathF.Round(
(sourceBlue + sourceGreen + sourceRed) / 3F * (levels - 1));
if (intensityBin[currentIntensity] > maxIntensity)
{
maxIntensity = intensityBin[currentIntensity];
maxIndex = currentIntensity;
}
}
intensityBin[currentIntensity]++;
blueBin[currentIntensity] += sourceBlue;
greenBin[currentIntensity] += sourceGreen;
redBin[currentIntensity] += sourceRed;
float red = MathF.Abs(redBin[maxIndex] / maxIntensity);
float green = MathF.Abs(greenBin[maxIndex] / maxIntensity);
float blue = MathF.Abs(blueBin[maxIndex] / maxIntensity);
if (intensityBin[currentIntensity] > maxIntensity)
{
maxIntensity = intensityBin[currentIntensity];
maxIndex = currentIntensity;
}
}
ref TPixel pixel = ref targetRow[x];
pixel.PackFromVector4(new Vector4(red, green, blue, sourceRow[x].ToVector4().W));
float red = MathF.Abs(redBin[maxIndex] / maxIntensity);
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);
}

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

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

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

@ -6,6 +6,7 @@ using System.Buffers;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
using SixLabors.Primitives;
@ -67,6 +68,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays
int width = maxX - minX;
var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY);
using (IMemoryOwner<TPixel> colors = source.MemoryAllocator.Allocate<TPixel>(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<float> amountSpan = amount.GetSpan();
// TODO: Use Span.Fill?
for (int i = 0; i < width; i++)
{
colorSpan[i] = this.Color;
amountSpan[i] = this.GraphicsOptions.BlendPercentage;
}
colorSpan.Fill(this.Color);
amountSpan.Fill(this.GraphicsOptions.BlendPercentage);
PixelBlender<TPixel> blender = PixelOperations<TPixel>.Instance.GetPixelBlender(this.GraphicsOptions);
ParallelFor.WithConfiguration(
minY,
maxY,
ParallelHelper.IterateRows(
workingRect,
configuration,
y =>
{
Span<TPixel> destination = source.GetPixelRowSpan(y - startY).Slice(minX - startX, width);
rows =>
{
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
blender.Blend(source.MemoryAllocator, destination, colors.GetSpan(), destination, amount.GetSpan());
});
// 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());
}
});
}
}
}

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

@ -7,6 +7,7 @@ using System.Numerics;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Primitives;
using SixLabors.Memory;
@ -84,6 +85,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration)
{
// TODO: can we simplify the rectangle calculation?
int startY = sourceRectangle.Y;
int endY = sourceRectangle.Bottom;
int startX = sourceRectangle.X;
@ -113,36 +115,43 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays
}
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))
{
// Be careful! Do not capture rowColorsSpan in the lambda below!
Span<TPixel> rowColorsSpan = rowColors.GetSpan();
for (int i = 0; i < width; i++)
{
rowColorsSpan[i] = glowColor;
}
rowColors.GetSpan().Fill(glowColor);
ParallelFor.WithTemporaryBuffer<float>(
minY,
maxY,
ParallelHelper.IterateRowsWithTempBuffer<float>(
workingRect,
configuration,
width,
(y, amounts) =>
{
Span<float> amountsSpan = amounts.GetSpan();
int offsetY = y - startY;
int offsetX = minX - startX;
for (int i = 0; i < width; i++)
(rows, amounts) =>
{
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);
});
Span<float> amountsSpan = amounts.Span;
for (int y = rows.Min; y < rows.Max; y++)
{
int offsetY = y - startY;
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 SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Primitives;
using SixLabors.Memory;
@ -115,43 +116,43 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays
}
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))
{
// Be careful! Do not capture rowColorsSpan in the lambda below!
Span<TPixel> rowColorsSpan = rowColors.GetSpan();
for (int i = 0; i < width; i++)
{
rowColorsSpan[i] = vignetteColor;
}
rowColors.GetSpan().Fill(vignetteColor);
ParallelFor.WithTemporaryBuffer<float>(
minY,
maxY,
ParallelHelper.IterateRowsWithTempBuffer<float>(
workingRect,
configuration,
width,
(y, amounts) =>
(rows, amounts) =>
{
Span<float> amountsSpan = amounts.GetSpan();
int offsetY = y - startY;
int offsetX = minX - startX;
for (int i = 0; i < width; i++)
Span<float> amountsSpan = amounts.Span;
for (int y = rows.Min; y < rows.Max; y++)
{
float distance = Vector2.Distance(centre, new Vector2(i + offsetX, offsetY));
amountsSpan[i] =
(this.GraphicsOptions.BlendPercentage * (.9F * (distance / maxDistance))).Clamp(
0,
1);
int offsetY = y - startY;
for (int i = 0; i < width; i++)
{
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 SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
using SixLabors.Primitives;
@ -78,23 +79,25 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
if (this.Sampler is NearestNeighborResampler)
{
ParallelFor.WithConfiguration(
0,
height,
ParallelHelper.IterateRows(
targetBounds,
configuration,
y =>
{
Span<TPixel> destRow = destination.GetPixelRowSpan(y);
for (int x = 0; x < width; x++)
rows =>
{
var point = Point.Transform(new Point(x, y), matrix);
if (sourceBounds.Contains(point.X, point.Y))
for (int y = rows.Min; y < rows.Max; 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;
}
@ -116,86 +119,107 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
using (Buffer2D<float> yBuffer = memoryAllocator.Allocate2D<float>(yLength, height))
using (Buffer2D<float> xBuffer = memoryAllocator.Allocate2D<float>(xLength, height))
{
ParallelFor.WithConfiguration(
0,
height,
ParallelHelper.IterateRows(
targetBounds,
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++)
for (int y = rows.Min; y < rows.Max; y++)
{
// Use the single precision position to calculate correct bounding pixels
// 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;
}
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));
// 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
for (int x = 0; x < width; x++)
{
CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ref ySpanRef);
CalculateWeightsScaleUp(minX, maxX, point.X, sampler, ref xSpanRef);
}
// Use the single precision position to calculate correct bounding pixels
// 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
Vector4 sum = Vector4.Zero;
for (int yy = 0, j = minY; j <= maxY; j++, yy++)
{
float yWeight = Unsafe.Add(ref ySpanRef, yy);
// It appears these have to be calculated on-the-fly.
// Precalculating 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);
}
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);
var vector = source[i, j].ToVector4();
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;
// 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);
ref TPixel dest = ref Unsafe.Add(ref destRowRef, x);
// Reverse the premultiplication
dest.PackFromVector4(sum.UnPremultiply());
// Reverse the premultiplication
dest.PackFromVector4(sum.UnPremultiply());
}
}
});
}

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

@ -4,8 +4,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Primitives;
@ -53,21 +53,23 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
return;
}
int minY = Math.Max(this.CropRectangle.Y, sourceRectangle.Y);
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);
var rect = Rectangle.Intersect(this.CropRectangle, sourceRectangle);
ParallelFor.WithConfiguration(
minY,
maxY,
configuration,
y =>
{
Span<TPixel> sourceRow = source.GetPixelRowSpan(y).Slice(minX);
Span<TPixel> targetRow = destination.GetPixelRowSpan(y - minY);
sourceRow.Slice(0, maxX - minX).CopyTo(targetRow);
});
// Copying is cheap, we should process more pixels per task:
ParallelExecutionSettings parallelSettings = configuration.GetParallelSettings().MultiplyMinimumPixelsPerTask(4);
ParallelHelper.IterateRows(
rect,
parallelSettings,
rows =>
{
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.
using System;
using System.Buffers;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
using SixLabors.Primitives;
@ -55,27 +57,20 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
private void FlipX(ImageFrame<TPixel> source, Configuration configuration)
{
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(
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);
});
Span<TPixel> temp = tempBuffer.Memory.Span;
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>
private void FlipY(ImageFrame<TPixel> source, Configuration configuration)
{
int width = source.Width;
int height = source.Height;
int halfWidth = (int)Math.Ceiling(width * .5F);
using (Buffer2D<TPixel> targetPixels = configuration.MemoryAllocator.Allocate2D<TPixel>(source.Size()))
{
ParallelFor.WithConfiguration(
0,
height,
configuration,
y =>
ParallelHelper.IterateRows(
source.Bounds(),
configuration,
rows =>
{
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
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);
}
source.GetPixelRowSpan(y).Reverse();
}
});
}
}
}

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

@ -10,6 +10,7 @@ using System.Runtime.InteropServices;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
using SixLabors.Primitives;
@ -75,28 +76,30 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
if (this.Sampler is NearestNeighborResampler)
{
ParallelFor.WithConfiguration(
0,
height,
ParallelHelper.IterateRows(
targetBounds,
configuration,
y =>
{
Span<TPixel> destRow = destination.GetPixelRowSpan(y);
for (int x = 0; x < width; x++)
rows =>
{
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);
int px = (int)MathF.Round(v3.X / z);
int py = (int)MathF.Round(v3.Y / z);
for (int x = 0; x < width; x++)
{
var v3 = Vector3.Transform(new Vector3(x, y, 1), matrix);
if (sourceBounds.Contains(px, py))
{
destRow[x] = source[px, py];
float z = MathF.Max(v3.Z, Epsilon);
int px = (int)MathF.Round(v3.X / z);
int py = (int)MathF.Round(v3.Y / z);
if (sourceBounds.Contains(px, py))
{
destRow[x] = source[px, py];
}
}
}
}
});
});
return;
}
@ -121,92 +124,113 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
using (Buffer2D<float> yBuffer = memoryAllocator.Allocate2D<float>(yLength, height))
using (Buffer2D<float> xBuffer = memoryAllocator.Allocate2D<float>(xLength, height))
{
ParallelFor.WithConfiguration(
0,
height,
ParallelHelper.IterateRows(
targetBounds,
configuration,
y =>
{
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++)
rows =>
{
// Use the single precision position to calculate correct bounding pixels
// 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
for (int y = rows.Min; y < rows.Max; y++)
{
CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ref ySpanRef);
CalculateWeightsScaleUp(minX, maxX, point.X, sampler, ref xSpanRef);
}
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));
// 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++)
for (int x = 0; x < width; x++)
{
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;
// Use the single precision position to calculate correct bounding pixels
// 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);
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 SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
using SixLabors.Primitives;
@ -267,26 +268,31 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
if (this.Sampler is NearestNeighborResampler)
{
var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY);
// Scaling factors
float widthFactor = sourceRectangle.Width / (float)this.ResizeRectangle.Width;
float heightFactor = sourceRectangle.Height / (float)this.ResizeRectangle.Height;
ParallelFor.WithConfiguration(
minY,
maxY,
ParallelHelper.IterateRows(
workingRect,
configuration,
y =>
{
// 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++)
rows =>
{
// X coordinates of source points
targetRow[x] = sourceRow[(int)(((x - startX) * widthFactor) + sourceX)];
}
});
for (int y = rows.Min; y < rows.Max; y++)
{
// 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;
}
@ -300,72 +306,88 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
firstPassPixels.MemorySource.Clear();
ParallelFor.WithTemporaryBuffer(
0,
sourceRectangle.Bottom,
var processColsRect = new Rectangle(0, 0, source.Width, sourceRectangle.Bottom);
ParallelHelper.IterateRowsWithTempBuffer<Vector4>(
processColsRect,
configuration,
source.Width,
(int y, IMemoryOwner<Vector4> tempRowBuffer) =>
(rows, tempRowBuffer) =>
{
ref Vector4 firstPassRow = ref MemoryMarshal.GetReference(firstPassPixels.GetRowSpan(y));
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
Span<Vector4> tempRowSpan = tempRowBuffer.GetSpan();
for (int y = rows.Min; y < rows.Max; y++)
{
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)
{
for (int x = minX; x < maxX; x++)
if (this.Compand)
{
WeightsWindow window = this.horizontalWeights.Weights[x - startX];
Unsafe.Add(ref firstPassRow, x) = window.ComputeExpandedWeightedRowSum(tempRowSpan, sourceX);
for (int x = minX; x < maxX; x++)
{
WeightsWindow window = this.horizontalWeights.Weights[x - startX];
Unsafe.Add(ref firstPassRow, x) =
window.ComputeExpandedWeightedRowSum(tempRowSpan, sourceX);
}
}
}
else
{
for (int x = minX; x < maxX; x++)
else
{
WeightsWindow window = this.horizontalWeights.Weights[x - startX];
Unsafe.Add(ref firstPassRow, x) = window.ComputeWeightedRowSum(tempRowSpan, sourceX);
for (int x = minX; x < maxX; x++)
{
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.
ParallelFor.WithConfiguration(
minY,
maxY,
ParallelHelper.IterateRows(
processRowsRect,
configuration,
y =>
{
// 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
rows =>
{
for (int x = 0; x < width; x++)
for (int y = rows.Min; y < rows.Max; y++)
{
// Destination color components
Vector4 destinationVector = window.ComputeWeightedColumnSum(firstPassPixels, x, sourceY);
// Ensure offsets are normalized for cropping and padding.
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);
pixel.PackFromVector4(destinationVector);
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++)
{
// 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 SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.MetaData.Profiles.Exif;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
using SixLabors.Primitives;
@ -147,25 +148,27 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
int height = source.Height;
Rectangle destinationBounds = destination.Bounds();
ParallelFor.WithConfiguration(
0,
height,
ParallelHelper.IterateRows(
source.Bounds(),
configuration,
y =>
{
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
for (int x = 0; x < width; x++)
rows =>
{
int newX = height - y - 1;
newX = height - newX - 1;
int newY = width - x - 1;
if (destinationBounds.Contains(newX, newY))
for (int y = rows.Min; y < rows.Max; y++)
{
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>
@ -179,20 +182,22 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
int width = source.Width;
int height = source.Height;
ParallelFor.WithConfiguration(
0,
height,
ParallelHelper.IterateRows(
source.Bounds(),
configuration,
y =>
{
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
Span<TPixel> targetRow = destination.GetPixelRowSpan(height - y - 1);
for (int x = 0; x < width; x++)
rows =>
{
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>
@ -207,22 +212,25 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
int height = source.Height;
Rectangle destinationBounds = destination.Bounds();
ParallelFor.WithConfiguration(
0,
height,
ParallelHelper.IterateRows(
source.Bounds(),
configuration,
y =>
{
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
int newX = height - y - 1;
for (int x = 0; x < width; x++)
rows =>
{
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 SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors;
using SixLabors.ImageSharp.Processing.Processors.Overlays;
@ -112,26 +113,29 @@ namespace SixLabors.ImageSharp.Benchmarks
Buffer2D<TPixel> sourcePixels = source.PixelBuffer;
rowColors.GetSpan().Fill(glowColor);
ParallelFor.WithConfiguration(
minY,
maxY,
var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY);
ParallelHelper.IterateRows(
workingRect,
configuration,
y =>
rows =>
{
int offsetY = y - startY;
for (int x = minX; x < maxX; x++)
for (int y = rows.Min; y < rows.Max; y++)
{
int offsetX = x - startX;
float distance = Vector2.Distance(centre, new Vector2(offsetX, offsetY));
Vector4 sourceColor = sourcePixels[offsetX, offsetY].ToVector4();
TPixel packed = default(TPixel);
packed.PackFromVector4(
PremultipliedLerp(
sourceColor,
glowColor.ToVector4(),
1 - (.95F * (distance / maxDistance))));
sourcePixels[offsetX, offsetY] = packed;
int offsetY = y - startY;
for (int x = minX; x < maxX; x++)
{
int offsetX = x - startX;
float distance = Vector2.Distance(centre, new Vector2(offsetX, offsetY));
Vector4 sourceColor = sourcePixels[offsetX, offsetY].ToVector4();
TPixel packed = default(TPixel);
packed.PackFromVector4(
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]
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[,]
{
{ Rgba32.HotPink , 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.LimeGreen, Rgba32.LimeGreen}
});
{
{ Rgba32.HotPink, 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.LimeGreen, Rgba32.LimeGreen }
});
}
[Fact]
public void ImageShouldBeFloodFilledWithPercent10Transparent()
{
Test("Percent10_Transparent", Rgba32.Blue, Brushes.Percent10(Rgba32.HotPink),
new Rgba32[,] {
{ 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}
});
this.Test(
"Percent10_Transparent",
Rgba32.Blue,
Brushes.Percent10(Rgba32.HotPink),
new Rgba32[,]
{
{ 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]
public void ImageShouldBeFloodFilledWithPercent20()
{
Test("Percent20", Rgba32.Blue, Brushes.Percent20(Rgba32.HotPink, Rgba32.LimeGreen),
new Rgba32[,] {
{ 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}
});
this.Test(
"Percent20",
Rgba32.Blue,
Brushes.Percent20(Rgba32.HotPink, Rgba32.LimeGreen),
new Rgba32[,]
{
{ 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]
public void ImageShouldBeFloodFilledWithPercent20_transparent()
{
Test("Percent20_Transparent", Rgba32.Blue, Brushes.Percent20(Rgba32.HotPink),
new Rgba32[,] {
{ 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}
});
this.Test(
"Percent20_Transparent",
Rgba32.Blue,
Brushes.Percent20(Rgba32.HotPink),
new Rgba32[,]
{
{ 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]
public void ImageShouldBeFloodFilledWithHorizontal()
{
Test("Horizontal", Rgba32.Blue, Brushes.Horizontal(Rgba32.HotPink, Rgba32.LimeGreen),
new Rgba32[,] {
{ 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}
});
this.Test(
"Horizontal",
Rgba32.Blue,
Brushes.Horizontal(Rgba32.HotPink, Rgba32.LimeGreen),
new Rgba32[,]
{
{ 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]
public void ImageShouldBeFloodFilledWithHorizontal_transparent()
{
Test("Horizontal_Transparent", Rgba32.Blue, Brushes.Horizontal(Rgba32.HotPink),
new Rgba32[,] {
{ 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}
});
this.Test(
"Horizontal_Transparent",
Rgba32.Blue,
Brushes.Horizontal(Rgba32.HotPink),
new Rgba32[,]
{
{ 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]
public void ImageShouldBeFloodFilledWithMin()
{
Test("Min", Rgba32.Blue, Brushes.Min(Rgba32.HotPink, Rgba32.LimeGreen),
new Rgba32[,] {
{ 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}
});
this.Test(
"Min",
Rgba32.Blue,
Brushes.Min(Rgba32.HotPink, Rgba32.LimeGreen),
new Rgba32[,]
{
{ 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]
public void ImageShouldBeFloodFilledWithMin_transparent()
{
Test("Min_Transparent", Rgba32.Blue, Brushes.Min(Rgba32.HotPink),
new Rgba32[,] {
{ 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},
});
this.Test(
"Min_Transparent",
Rgba32.Blue,
Brushes.Min(Rgba32.HotPink),
new Rgba32[,]
{
{ 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]
public void ImageShouldBeFloodFilledWithVertical()
{
Test("Vertical", Rgba32.Blue, Brushes.Vertical(Rgba32.HotPink, 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}
});
this.Test(
"Vertical",
Rgba32.Blue,
Brushes.Vertical(Rgba32.HotPink, 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 }
});
}
[Fact]
public void ImageShouldBeFloodFilledWithVertical_transparent()
{
Test("Vertical_Transparent", Rgba32.Blue, Brushes.Vertical(Rgba32.HotPink),
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}
});
this.Test(
"Vertical_Transparent",
Rgba32.Blue,
Brushes.Vertical(Rgba32.HotPink),
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 }
});
}
[Fact]
public void ImageShouldBeFloodFilledWithForwardDiagonal()
{
Test("ForwardDiagonal", Rgba32.Blue, Brushes.ForwardDiagonal(Rgba32.HotPink, Rgba32.LimeGreen),
new Rgba32[,] {
{ 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}
});
this.Test(
"ForwardDiagonal",
Rgba32.Blue,
Brushes.ForwardDiagonal(Rgba32.HotPink, Rgba32.LimeGreen),
new Rgba32[,]
{
{ 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]
public void ImageShouldBeFloodFilledWithForwardDiagonal_transparent()
{
Test("ForwardDiagonal_Transparent", Rgba32.Blue, Brushes.ForwardDiagonal(Rgba32.HotPink),
new Rgba32[,] {
{ 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}
});
this.Test(
"ForwardDiagonal_Transparent",
Rgba32.Blue,
Brushes.ForwardDiagonal(Rgba32.HotPink),
new Rgba32[,]
{
{ 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]
public void ImageShouldBeFloodFilledWithBackwardDiagonal()
{
Test("BackwardDiagonal", Rgba32.Blue, Brushes.BackwardDiagonal(Rgba32.HotPink, Rgba32.LimeGreen),
new Rgba32[,] {
{ 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}
});
this.Test(
"BackwardDiagonal",
Rgba32.Blue,
Brushes.BackwardDiagonal(Rgba32.HotPink, Rgba32.LimeGreen),
new Rgba32[,]
{
{ 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]
public void ImageShouldBeFloodFilledWithBackwardDiagonal_transparent()
{
Test("BackwardDiagonal_Transparent", Rgba32.Blue, Brushes.BackwardDiagonal(Rgba32.HotPink),
new Rgba32[,] {
{ 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}
});
this.Test(
"BackwardDiagonal_Transparent",
Rgba32.Blue,
Brushes.BackwardDiagonal(Rgba32.HotPink),
new Rgba32[,]
{
{ 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.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Primitives;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using SixLabors.Primitives;
using SixLabors.Shapes;
using Xunit;
// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Tests.Drawing
{
using System;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using SixLabors.Primitives;
[GroupOutput("Drawing")]
public class FillSolidBrushTests
{
@ -55,7 +56,9 @@ namespace SixLabors.ImageSharp.Tests.Drawing
[Theory]
[WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, "Blue")]
[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>
{
using (Image<TPixel> image = provider.GetImage())
@ -63,7 +66,11 @@ namespace SixLabors.ImageSharp.Tests.Drawing
TPixel color = TestUtils.GetPixelOfNamedColor<TPixel>(newColorName);
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);
}
}
@ -84,7 +91,12 @@ namespace SixLabors.ImageSharp.Tests.Drawing
[Theory]
[WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 5, 7, 3, 8)]
[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>
{
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, "Green", 0.5f, PixelColorBlendingMode.Normal, 0.3f },
{ false, "HotPink", 0.8f, PixelColorBlendingMode.Normal, 0.8f },
{ false, "Blue", 0.5f, PixelColorBlendingMode.Multiply, 1.0f },
{ false, "Blue", 1.0f, PixelColorBlendingMode.Multiply, 0.5f },
{ false, "Green", 0.5f, PixelColorBlendingMode.Multiply, 0.3f },
{ false, "HotPink", 0.8f, PixelColorBlendingMode.Multiply, 0.8f },
{ false, "Blue", 0.5f, PixelColorBlendingMode.Add, 1.0f },
{ false, "Blue", 1.0f, PixelColorBlendingMode.Add, 0.5f },
{ false, "Green", 0.5f, PixelColorBlendingMode.Add, 0.3f },
{ false, "HotPink", 0.8f, PixelColorBlendingMode.Add, 0.8f },
{ true, "Blue", 0.5f, PixelColorBlendingMode.Normal, 1.0f },
{ true, "Blue", 1.0f, PixelColorBlendingMode.Normal, 0.5f },
{ true, "Green", 0.5f, PixelColorBlendingMode.Normal, 0.3f },
{ true, "HotPink", 0.8f, PixelColorBlendingMode.Normal, 0.8f },
{ true, "Blue", 0.5f, PixelColorBlendingMode.Multiply, 1.0f },
{ true, "Blue", 1.0f, PixelColorBlendingMode.Multiply, 0.5f },
{ true, "Green", 0.5f, PixelColorBlendingMode.Multiply, 0.3f },
{ true, "HotPink", 0.8f, PixelColorBlendingMode.Multiply, 0.8f },
{ true, "Blue", 0.5f, PixelColorBlendingMode.Add, 1.0f },
{ true, "Blue", 1.0f, PixelColorBlendingMode.Add, 0.5f },
{ true, "Green", 0.5f, PixelColorBlendingMode.Add, 0.3f },
@ -155,8 +162,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing
var options = new GraphicsOptions(false)
{
ColorBlendingMode = blenderMode,
BlendPercentage = blendPercentage
ColorBlendingMode = blenderMode, BlendPercentage = blendPercentage
};
if (triggerFillRegion)
@ -185,11 +191,13 @@ namespace SixLabors.ImageSharp.Tests.Drawing
appendPixelTypeToFileName: 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);
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
{
using SixLabors.ImageSharp.Advanced;
public class DetectEdgesTest : FileTestBase
{
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.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using SixLabors.Primitives;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
{
[GroupOutput("Transforms")]
public class CropTest : FileTestBase
{
[Theory]
[WithFileCollection(nameof(DefaultFiles), DefaultPixelType)]
public void ImageShouldCrop<TPixel>(TestImageProvider<TPixel> provider)
[WithTestPatternImages(70, 30, PixelTypes.Rgba32, 0, 0, 70, 30)]
[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>
{
using (Image<TPixel> image = provider.GetImage())
{
image.Mutate(x => x.Crop(image.Width / 2, image.Height / 2));
image.DebugSave(provider);
}
var rect = new Rectangle(x, y, w, h);
FormattableString info = $"X{x}Y{y}.W{w}H{h}";
provider.RunValidatingProcessorTest(
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 Xunit;
// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
{
using SixLabors.ImageSharp.Processing;
[GroupOutput("Transforms")]
public class FlipTests
{
public static readonly TheoryData<FlipMode> FlipValues
= new TheoryData<FlipMode>
{
{ FlipMode.None },
{ FlipMode.Vertical },
{ FlipMode.Horizontal },
};
public static readonly TheoryData<FlipMode> FlipValues =
new TheoryData<FlipMode>
{
FlipMode.None,
FlipMode.Vertical,
FlipMode.Horizontal,
};
[Theory]
[WithTestPatternImages(nameof(FlipValues), 20, 37, PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(FlipValues), 53, 37, PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(FlipValues), 17, 32, PixelTypes.Rgba32)]
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
{
public class RotateTests : FileTestBase
[GroupOutput("Transforms")]
public class RotateTests
{
public static readonly TheoryData<float> RotateAngles
= new TheoryData<float>
@ -25,29 +26,21 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
};
[Theory]
[WithTestPatternImages(nameof(RotateAngles), 100, 50, DefaultPixelType)]
[WithTestPatternImages(nameof(RotateAngles), 50, 100, DefaultPixelType)]
[WithTestPatternImages(nameof(RotateAngles), 100, 50, PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(RotateAngles), 50, 100, PixelTypes.Rgba32)]
public void Rotate_WithAngle<TPixel>(TestImageProvider<TPixel> provider, float value)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{
image.Mutate(x => x.Rotate(value));
image.DebugSave(provider, value);
}
provider.RunValidatingProcessorTest(ctx => ctx.Rotate(value), value, appendPixelTypeToFileName: false);
}
[Theory]
[WithTestPatternImages(nameof(RotateEnumValues), 100, 50, DefaultPixelType)]
[WithTestPatternImages(nameof(RotateEnumValues), 50, 100, DefaultPixelType)]
[WithTestPatternImages(nameof(RotateEnumValues), 100, 50, PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(RotateEnumValues), 50, 100, PixelTypes.Rgba32)]
public void Rotate_WithRotateTypeEnum<TPixel>(TestImageProvider<TPixel> provider, RotateMode value)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{
image.Mutate(x => x.Rotate(value));
image.DebugSave(provider, value);
}
provider.RunValidatingProcessorTest(ctx => ctx.Rotate(value), value, appendPixelTypeToFileName: false);
}
}
}

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 ITestOutputHelper Output { get; }
public static readonly TheoryData<string> ResamplerNames = new TheoryData<string>
{
nameof(KnownResamplers.Bicubic),

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

@ -441,14 +441,23 @@ namespace SixLabors.ImageSharp.Tests
{
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>

2
tests/Images/External

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