Browse Source

Merge branch 'master' into js/quantization-tweaks

pull/1114/head
James Jackson-South 6 years ago
parent
commit
3a96474685
  1. 21
      src/ImageSharp/Advanced/IRowIntervalOperation.cs
  2. 25
      src/ImageSharp/Advanced/IRowIntervalOperation{TBuffer}.cs
  3. 8
      src/ImageSharp/Advanced/ParallelExecutionSettings.cs
  4. 110
      src/ImageSharp/Advanced/ParallelRowIterator.Wrappers.cs
  5. 162
      src/ImageSharp/Advanced/ParallelRowIterator.cs
  6. 178
      src/ImageSharp/Advanced/ParallelUtils/ParallelHelper.cs
  7. 14
      src/ImageSharp/Common/Extensions/EnumerableExtensions.cs
  8. 51
      src/ImageSharp/ImageFrame{TPixel}.cs
  9. 6
      src/ImageSharp/ImageSharp.csproj
  10. 4
      src/ImageSharp/PixelFormats/Utils/Vector4Converters.RgbaCompatible.cs
  11. 2
      src/ImageSharp/Primitives/Rectangle.cs
  12. 84
      src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor{TPixel}.cs
  13. 395
      src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs
  14. 7
      src/ImageSharp/Processing/Processors/Convolution/BoxBlurProcessor{TPixel}.cs
  15. 164
      src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor{TPixel}.cs
  16. 175
      src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs
  17. 149
      src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs
  18. 7
      src/ImageSharp/Processing/Processors/Convolution/EdgeDetector2DProcessor{TPixel}.cs
  19. 133
      src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor{TPixel}.cs
  20. 7
      src/ImageSharp/Processing/Processors/Convolution/GaussianBlurProcessor{TPixel}.cs
  21. 7
      src/ImageSharp/Processing/Processors/Convolution/GaussianSharpenProcessor{TPixel}.cs
  22. 68
      src/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs
  23. 21
      src/ImageSharp/Processing/Processors/Effects/IPixelRowDelegate.cs
  24. 205
      src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs
  25. 30
      src/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor.cs
  26. 70
      src/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessorBase{TPixel}.cs
  27. 104
      src/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor{TPixel,TDelegate}.cs
  28. 39
      src/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor{TPixel}.cs
  29. 117
      src/ImageSharp/Processing/Processors/Effects/PixelateProcessor{TPixel}.cs
  30. 30
      src/ImageSharp/Processing/Processors/Effects/PositionAwarePixelRowDelegateProcessor.cs
  31. 36
      src/ImageSharp/Processing/Processors/Effects/PositionAwarePixelRowDelegateProcessor{TPixel}.cs
  32. 65
      src/ImageSharp/Processing/Processors/Filters/FilterProcessor{TPixel}.cs
  33. 302
      src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs
  34. 425
      src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationSlidingWindowProcessor{TPixel}.cs
  35. 169
      src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs
  36. 114
      src/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor{TPixel}.cs
  37. 131
      src/ImageSharp/Processing/Processors/Overlays/GlowProcessor{TPixel}.cs
  38. 140
      src/ImageSharp/Processing/Processors/Overlays/VignetteProcessor{TPixel}.cs
  39. 56
      src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs
  40. 176
      src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor{TPixel}.cs
  41. 61
      src/ImageSharp/Processing/Processors/Transforms/CropProcessor{TPixel}.cs
  42. 55
      src/ImageSharp/Processing/Processors/Transforms/FlipProcessor{TPixel}.cs
  43. 176
      src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor{TPixel}.cs
  44. 4
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs
  45. 94
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs
  46. 2
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs
  47. 224
      src/ImageSharp/Processing/Processors/Transforms/RotateProcessor{TPixel}.cs
  48. 1
      tests/Directory.Build.targets
  49. 22
      tests/ImageSharp.Benchmarks/Config.cs
  50. 1
      tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj
  51. 1
      tests/ImageSharp.Benchmarks/Samplers/Crop.cs
  52. 2
      tests/ImageSharp.Tests/Helpers/ParallelExecutionSettingsTests.cs
  53. 293
      tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs
  54. 53
      tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs

21
src/ImageSharp/Advanced/IRowIntervalOperation.cs

@ -0,0 +1,21 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Advanced
{
/// <summary>
/// Defines the contract for an action that operates on a row interval.
/// </summary>
public interface IRowIntervalOperation
{
/// <summary>
/// Invokes the method passing the row interval.
/// </summary>
/// <param name="rows">The row interval.</param>
void Invoke(in RowInterval rows);
}
}

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

@ -0,0 +1,25 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Advanced
{
/// <summary>
/// Defines the contract for an action that operates on a row interval with a temporary buffer.
/// </summary>
/// <typeparam name="TBuffer">The type of buffer elements.</typeparam>
public interface IRowIntervalOperation<TBuffer>
where TBuffer : unmanaged
{
/// <summary>
/// Invokes the method passing the row interval and a buffer.
/// </summary>
/// <param name="rows">The row interval.</param>
/// <param name="span">The contiguous region of memory.</param>
void Invoke(in RowInterval rows, Span<TBuffer> span);
}
}

8
src/ImageSharp/Advanced/ParallelUtils/ParallelExecutionSettings.cs → src/ImageSharp/Advanced/ParallelExecutionSettings.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors and contributors. // Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System; using System;
@ -6,10 +6,10 @@ using System.Threading.Tasks;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Advanced.ParallelUtils namespace SixLabors.ImageSharp.Advanced
{ {
/// <summary> /// <summary>
/// Defines execution settings for methods in <see cref="ParallelHelper"/>. /// Defines execution settings for methods in <see cref="ParallelRowIterator"/>.
/// </summary> /// </summary>
public readonly struct ParallelExecutionSettings public readonly struct ParallelExecutionSettings
{ {
@ -89,7 +89,7 @@ namespace SixLabors.ImageSharp.Advanced.ParallelUtils
} }
/// <summary> /// <summary>
/// Get the default <see cref="SixLabors.ImageSharp.Advanced.ParallelUtils.ParallelExecutionSettings"/> for a <see cref="SixLabors.ImageSharp.Configuration"/> /// Get the default <see cref="SixLabors.ImageSharp.Advanced.ParallelExecutionSettings"/> for a <see cref="SixLabors.ImageSharp.Configuration"/>
/// </summary> /// </summary>
/// <param name="configuration">The <see cref="Configuration"/>.</param> /// <param name="configuration">The <see cref="Configuration"/>.</param>
/// <returns>The <see cref="ParallelExecutionSettings"/>.</returns> /// <returns>The <see cref="ParallelExecutionSettings"/>.</returns>

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

@ -0,0 +1,110 @@
// 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;
namespace SixLabors.ImageSharp.Advanced
{
/// <content>
/// Utility methods for batched processing of pixel row intervals.
/// Parallel execution is optimized for image processing based on values defined
/// <see cref="ParallelExecutionSettings"/> or <see cref="Configuration"/>.
/// Using this class is preferred over direct usage of <see cref="Parallel"/> utility methods.
/// </content>
public static partial class ParallelRowIterator
{
private readonly struct IterationParameters
{
public readonly int MinY;
public readonly int MaxY;
public readonly int StepY;
public readonly int Width;
public IterationParameters(int minY, int maxY, int stepY)
: this(minY, maxY, stepY, 0)
{
}
public IterationParameters(int minY, int maxY, int stepY, int width)
{
this.MinY = minY;
this.MaxY = maxY;
this.StepY = stepY;
this.Width = width;
}
}
private readonly struct RowIntervalOperationWrapper<T>
where T : struct, IRowIntervalOperation
{
private readonly IterationParameters info;
private readonly T operation;
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperationWrapper(in IterationParameters info, in T operation)
{
this.info = info;
this.operation = operation;
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int i)
{
int yMin = this.info.MinY + (i * this.info.StepY);
if (yMin >= this.info.MaxY)
{
return;
}
int yMax = Math.Min(yMin + this.info.StepY, this.info.MaxY);
var rows = new RowInterval(yMin, yMax);
// Skip the safety copy when invoking a potentially impure method on a readonly field
Unsafe.AsRef(in this.operation).Invoke(in rows);
}
}
private readonly struct RowIntervalOperationWrapper<T, TBuffer>
where T : struct, IRowIntervalOperation<TBuffer>
where TBuffer : unmanaged
{
private readonly IterationParameters info;
private readonly MemoryAllocator allocator;
private readonly T operation;
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperationWrapper(
in IterationParameters info,
MemoryAllocator allocator,
in T operation)
{
this.info = info;
this.allocator = allocator;
this.operation = operation;
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int i)
{
int yMin = this.info.MinY + (i * this.info.StepY);
if (yMin >= this.info.MaxY)
{
return;
}
int yMax = Math.Min(yMin + this.info.StepY, this.info.MaxY);
var rows = new RowInterval(yMin, yMax);
using IMemoryOwner<TBuffer> buffer = this.allocator.Allocate<TBuffer>(this.info.Width);
Unsafe.AsRef(in this.operation).Invoke(in rows, buffer.Memory.Span);
}
}
}
}

162
src/ImageSharp/Advanced/ParallelRowIterator.cs

@ -0,0 +1,162 @@
// 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;
namespace SixLabors.ImageSharp.Advanced
{
/// <summary>
/// Utility methods for batched processing of pixel row intervals.
/// Parallel execution is optimized for image processing based on values defined
/// <see cref="ParallelExecutionSettings"/> or <see cref="Configuration"/>.
/// Using this class is preferred over direct usage of <see cref="Parallel"/> utility methods.
/// </summary>
public static partial class ParallelRowIterator
{
/// <summary>
/// Iterate through the rows of a rectangle in optimized batches defined by <see cref="RowInterval"/>-s.
/// </summary>
/// <typeparam name="T">The type of row operation to perform.</typeparam>
/// <param name="configuration">The <see cref="Configuration"/> to get the parallel settings from.</param>
/// <param name="rectangle">The <see cref="Rectangle"/>.</param>
/// <param name="operation">The operation defining the iteration logic on a single <see cref="RowInterval"/>.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public static void IterateRows<T>(Configuration configuration, Rectangle rectangle, in T operation)
where T : struct, IRowIntervalOperation
{
var parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration);
IterateRows(rectangle, in parallelSettings, in operation);
}
/// <summary>
/// Iterate through the rows of a rectangle in optimized batches defined by <see cref="RowInterval"/>-s.
/// </summary>
/// <typeparam name="T">The type of row operation to perform.</typeparam>
/// <param name="rectangle">The <see cref="Rectangle"/>.</param>
/// <param name="parallelSettings">The <see cref="ParallelExecutionSettings"/>.</param>
/// <param name="operation">The operation defining the iteration logic on a single <see cref="RowInterval"/>.</param>
public static void IterateRows<T>(
Rectangle rectangle,
in ParallelExecutionSettings parallelSettings,
in T operation)
where T : struct, IRowIntervalOperation
{
ValidateRectangle(rectangle);
int top = rectangle.Top;
int bottom = rectangle.Bottom;
int width = rectangle.Width;
int height = rectangle.Height;
int maxSteps = DivideCeil(width * height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
// Avoid TPL overhead in this trivial case:
if (numOfSteps == 1)
{
var rows = new RowInterval(top, bottom);
Unsafe.AsRef(in operation).Invoke(in rows);
return;
}
int verticalStep = DivideCeil(rectangle.Height, numOfSteps);
var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = numOfSteps };
var info = new IterationParameters(top, bottom, verticalStep);
var wrappingOperation = new RowIntervalOperationWrapper<T>(in info, in operation);
Parallel.For(
0,
numOfSteps,
parallelOptions,
wrappingOperation.Invoke);
}
/// <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="operation"/> invocation.
/// </summary>
/// <typeparam name="T">The type of row operation to perform.</typeparam>
/// <typeparam name="TBuffer">The type of buffer elements.</typeparam>
/// <param name="configuration">The <see cref="Configuration"/> to get the parallel settings from.</param>
/// <param name="rectangle">The <see cref="Rectangle"/>.</param>
/// <param name="operation">The operation defining the iteration logic on a single <see cref="RowInterval"/>.</param>
public static void IterateRows<T, TBuffer>(Configuration configuration, Rectangle rectangle, in T operation)
where T : struct, IRowIntervalOperation<TBuffer>
where TBuffer : unmanaged
{
var parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration);
IterateRows<T, TBuffer>(rectangle, in parallelSettings, in operation);
}
/// <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="operation"/> invocation.
/// </summary>
/// <typeparam name="T">The type of row operation to perform.</typeparam>
/// <typeparam name="TBuffer">The type of buffer elements.</typeparam>
/// <param name="rectangle">The <see cref="Rectangle"/>.</param>
/// <param name="parallelSettings">The <see cref="ParallelExecutionSettings"/>.</param>
/// <param name="operation">The operation defining the iteration logic on a single <see cref="RowInterval"/>.</param>
public static void IterateRows<T, TBuffer>(
Rectangle rectangle,
in ParallelExecutionSettings parallelSettings,
in T operation)
where T : struct, IRowIntervalOperation<TBuffer>
where TBuffer : unmanaged
{
ValidateRectangle(rectangle);
int top = rectangle.Top;
int bottom = rectangle.Bottom;
int width = rectangle.Width;
int height = rectangle.Height;
int maxSteps = DivideCeil(width * height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
MemoryAllocator allocator = parallelSettings.MemoryAllocator;
// Avoid TPL overhead in this trivial case:
if (numOfSteps == 1)
{
var rows = new RowInterval(top, bottom);
using (IMemoryOwner<TBuffer> buffer = allocator.Allocate<TBuffer>(width))
{
Unsafe.AsRef(operation).Invoke(in rows, buffer.Memory.Span);
}
return;
}
int verticalStep = DivideCeil(height, numOfSteps);
var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = numOfSteps };
var info = new IterationParameters(top, bottom, verticalStep, width);
var wrappingOperation = new RowIntervalOperationWrapper<T, TBuffer>(in info, allocator, in operation);
Parallel.For(
0,
numOfSteps,
parallelOptions,
wrappingOperation.Invoke);
}
[MethodImpl(InliningOptions.ShortMethod)]
private static int DivideCeil(int dividend, int divisor) => 1 + ((dividend - 1) / divisor);
private static void ValidateRectangle(Rectangle rectangle)
{
Guard.MustBeGreaterThan(
rectangle.Width,
0,
$"{nameof(rectangle)}.{nameof(rectangle.Width)}");
Guard.MustBeGreaterThan(
rectangle.Height,
0,
$"{nameof(rectangle)}.{nameof(rectangle.Height)}");
}
}
}

178
src/ImageSharp/Advanced/ParallelUtils/ParallelHelper.cs

@ -1,178 +0,0 @@
// 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;
namespace SixLabors.ImageSharp.Advanced.ParallelUtils
{
/// <summary>
/// Utility methods for batched processing of pixel row intervals.
/// Parallel execution is optimized for image processing based on values defined
/// <see cref="ParallelExecutionSettings"/> or <see cref="Configuration"/>.
/// Using this class is preferred over direct usage of <see cref="Parallel"/> utility methods.
/// </summary>
public static class ParallelHelper
{
/// <summary>
/// Iterate through the rows of a rectangle in optimized batches defined by <see cref="RowInterval"/>-s.
/// </summary>
/// <param name="rectangle">The <see cref="Rectangle"/>.</param>
/// <param name="configuration">The <see cref="Configuration"/> to get the parallel settings from.</param>
/// <param name="body">The method body defining the iteration logic on a single <see cref="RowInterval"/>.</param>
public static void IterateRows(Rectangle rectangle, Configuration configuration, Action<RowInterval> body)
{
var parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration);
IterateRows(rectangle, parallelSettings, body);
}
/// <summary>
/// Iterate through the rows of a rectangle in optimized batches defined by <see cref="RowInterval"/>-s.
/// </summary>
/// <param name="rectangle">The <see cref="Rectangle"/>.</param>
/// <param name="parallelSettings">The <see cref="ParallelExecutionSettings"/>.</param>
/// <param name="body">The method body defining the iteration logic on a single <see cref="RowInterval"/>.</param>
public static void IterateRows(
Rectangle rectangle,
in ParallelExecutionSettings parallelSettings,
Action<RowInterval> body)
{
ValidateRectangle(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 };
int top = rectangle.Top;
int bottom = rectangle.Bottom;
Parallel.For(
0,
numOfSteps,
parallelOptions,
i =>
{
int yMin = top + (i * verticalStep);
if (yMin >= bottom)
{
return;
}
int yMax = Math.Min(yMin + verticalStep, 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>
internal static void IterateRowsWithTempBuffer<T>(
Rectangle rectangle,
in ParallelExecutionSettings parallelSettings,
Action<RowInterval, Memory<T>> body)
where T : unmanaged
{
ValidateRectangle(rectangle);
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 };
int top = rectangle.Top;
int bottom = rectangle.Bottom;
Parallel.For(
0,
numOfSteps,
parallelOptions,
i =>
{
int yMin = top + (i * verticalStep);
if (yMin >= bottom)
{
return;
}
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>
internal static void IterateRowsWithTempBuffer<T>(
Rectangle rectangle,
Configuration configuration,
Action<RowInterval, Memory<T>> body)
where T : unmanaged
{
IterateRowsWithTempBuffer(rectangle, ParallelExecutionSettings.FromConfiguration(configuration), body);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int DivideCeil(int dividend, int divisor) => 1 + ((dividend - 1) / divisor);
private static void ValidateRectangle(Rectangle rectangle)
{
Guard.MustBeGreaterThan(
rectangle.Width,
0,
$"{nameof(rectangle)}.{nameof(rectangle.Width)}");
Guard.MustBeGreaterThan(
rectangle.Height,
0,
$"{nameof(rectangle)}.{nameof(rectangle.Height)}");
}
}
}

14
src/ImageSharp/Common/Extensions/EnumerableExtensions.cs

@ -1,10 +1,10 @@
// Copyright (c) Six Labors and contributors. // Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace SixLabors.ImageSharp.Common namespace SixLabors.ImageSharp
{ {
/// <summary> /// <summary>
/// Encapsulates a series of time saving extension methods to the <see cref="T:System.Collections.IEnumerable"/> interface. /// Encapsulates a series of time saving extension methods to the <see cref="T:System.Collections.IEnumerable"/> interface.
@ -34,15 +34,11 @@ namespace SixLabors.ImageSharp.Common
/// <summary> /// <summary>
/// Generates a sequence of integral numbers within a specified range. /// Generates a sequence of integral numbers within a specified range.
/// </summary> /// </summary>
/// <param name="fromInclusive"> /// <param name="fromInclusive">The start index, inclusive.</param>
/// The start index, inclusive.
/// </param>
/// <param name="toDelegate"> /// <param name="toDelegate">
/// A method that has one parameter and returns a <see cref="bool"/> calculating the end index. /// A method that has one parameter and returns a <see cref="bool"/> calculating the end index.
/// </param> /// </param>
/// <param name="step"> /// <param name="step">The incremental step.</param>
/// The incremental step.
/// </param>
/// <returns> /// <returns>
/// The <see cref="IEnumerable{Int32}"/> that contains a range of sequential integral numbers. /// The <see cref="IEnumerable{Int32}"/> that contains a range of sequential integral numbers.
/// </returns> /// </returns>
@ -56,4 +52,4 @@ namespace SixLabors.ImageSharp.Common
} }
} }
} }
} }

51
src/ImageSharp/ImageFrame{TPixel}.cs

@ -4,9 +4,7 @@
using System; using System;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Advanced.ParallelUtils;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -261,19 +259,12 @@ namespace SixLabors.ImageSharp
} }
var target = new ImageFrame<TPixel2>(configuration, this.Width, this.Height, this.Metadata.DeepClone()); var target = new ImageFrame<TPixel2>(configuration, this.Width, this.Height, this.Metadata.DeepClone());
var operation = new RowIntervalOperation<TPixel2>(this, target, configuration);
ParallelHelper.IterateRows( ParallelRowIterator.IterateRows(
this.Bounds(),
configuration, configuration,
rows => this.Bounds(),
{ in operation);
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> sourceRow = this.GetPixelRowSpan(y);
Span<TPixel2> targetRow = target.GetPixelRowSpan(y);
PixelOperations<TPixel>.Instance.To(configuration, sourceRow, targetRow);
}
});
return target; return target;
} }
@ -295,5 +286,39 @@ namespace SixLabors.ImageSharp
span.Fill(value); span.Fill(value);
} }
} }
/// <summary>
/// A <see langword="struct"/> implementing the clone logic for <see cref="ImageFrame{TPixel}"/>.
/// </summary>
private readonly struct RowIntervalOperation<TPixel2> : IRowIntervalOperation
where TPixel2 : struct, IPixel<TPixel2>
{
private readonly ImageFrame<TPixel> source;
private readonly ImageFrame<TPixel2> target;
private readonly Configuration configuration;
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(
ImageFrame<TPixel> source,
ImageFrame<TPixel2> target,
Configuration configuration)
{
this.source = source;
this.target = target;
this.configuration = configuration;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
{
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> sourceRow = this.source.GetPixelRowSpan(y);
Span<TPixel2> targetRow = this.target.GetPixelRowSpan(y);
PixelOperations<TPixel>.Instance.To(this.configuration, sourceRow, targetRow);
}
}
}
} }
} }

6
src/ImageSharp/ImageSharp.csproj

@ -24,9 +24,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup Condition=" $(TargetFramework.StartsWith('netstandard')) OR '$(TargetFramework)' == 'net472'"> <ItemGroup Condition=" $(TargetFramework.StartsWith('netstandard')) OR '$(TargetFramework)' == 'net472'">
<PackageReference Include="System.Numerics.Vectors"/> <PackageReference Include="System.Numerics.Vectors" />
<PackageReference Include="System.Buffers"/> <PackageReference Include="System.Buffers" />
<PackageReference Include="System.Memory"/> <PackageReference Include="System.Memory" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard1.3'"> <ItemGroup Condition=" '$(TargetFramework)' == 'netstandard1.3'">

4
src/ImageSharp/PixelFormats/Utils/Vector4Converters.RgbaCompatible.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors and contributors. // Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System; using System;
@ -126,4 +126,4 @@ namespace SixLabors.ImageSharp.PixelFormats.Utils
} }
} }
} }
} }

2
src/ImageSharp/Primitives/Rectangle.cs

@ -460,4 +460,4 @@ namespace SixLabors.ImageSharp
this.Width.Equals(other.Width) && this.Width.Equals(other.Width) &&
this.Height.Equals(other.Height); this.Height.Equals(other.Height);
} }
} }

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

@ -2,9 +2,9 @@
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System; using System;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Advanced.ParallelUtils; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Binarization namespace SixLabors.ImageSharp.Processing.Processors.Binarization
@ -42,36 +42,66 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization
Configuration configuration = this.Configuration; Configuration configuration = this.Configuration;
var interest = Rectangle.Intersect(sourceRectangle, source.Bounds()); var interest = Rectangle.Intersect(sourceRectangle, source.Bounds());
int startY = interest.Y;
int endY = interest.Bottom;
int startX = interest.X;
int endX = interest.Right;
bool isAlphaOnly = typeof(TPixel) == typeof(A8); bool isAlphaOnly = typeof(TPixel) == typeof(A8);
var workingRect = Rectangle.FromLTRB(startX, startY, endX, endY); var operation = new RowIntervalOperation(interest, source, upper, lower, threshold, isAlphaOnly);
ParallelRowIterator.IterateRows(
ParallelHelper.IterateRows(
workingRect,
configuration, configuration,
rows => interest,
{ in operation);
Rgba32 rgba = default; }
for (int y = rows.Min; y < rows.Max; y++)
{ /// <summary>
Span<TPixel> row = source.GetPixelRowSpan(y); /// A <see langword="struct"/> implementing the clone logic for <see cref="BinaryThresholdProcessor{TPixel}"/>.
/// </summary>
private readonly struct RowIntervalOperation : IRowIntervalOperation
{
private readonly ImageFrame<TPixel> source;
private readonly TPixel upper;
private readonly TPixel lower;
private readonly byte threshold;
private readonly int minX;
private readonly int maxX;
private readonly bool isAlphaOnly;
for (int x = startX; x < endX; x++) [MethodImpl(InliningOptions.ShortMethod)]
{ public RowIntervalOperation(
ref TPixel color = ref row[x]; Rectangle bounds,
color.ToRgba32(ref rgba); ImageFrame<TPixel> source,
TPixel upper,
TPixel lower,
byte threshold,
bool isAlphaOnly)
{
this.source = source;
this.upper = upper;
this.lower = lower;
this.threshold = threshold;
this.minX = bounds.X;
this.maxX = bounds.Right;
this.isAlphaOnly = isAlphaOnly;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
{
Rgba32 rgba = default;
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> row = this.source.GetPixelRowSpan(y);
for (int x = this.minX; x < this.maxX; x++)
{
ref TPixel color = ref row[x];
color.ToRgba32(ref rgba);
// Convert to grayscale using ITU-R Recommendation BT.709 if required // Convert to grayscale using ITU-R Recommendation BT.709 if required
byte luminance = isAlphaOnly ? rgba.A : ImageMaths.Get8BitBT709Luminance(rgba.R, rgba.G, rgba.B); byte luminance = this.isAlphaOnly ? rgba.A : ImageMaths.Get8BitBT709Luminance(rgba.R, rgba.G, rgba.B);
color = luminance >= threshold ? upper : lower; color = luminance >= this.threshold ? this.upper : this.lower;
} }
} }
}); }
} }
} }
} }

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

@ -7,8 +7,7 @@ using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Advanced.ParallelUtils;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Convolution.Parameters; using SixLabors.ImageSharp.Processing.Processors.Convolution.Parameters;
@ -269,17 +268,26 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
protected override void OnFrameApply(ImageFrame<TPixel> source) protected override void OnFrameApply(ImageFrame<TPixel> source)
{ {
// Preliminary gamma highlight pass // Preliminary gamma highlight pass
this.ApplyGammaExposure(source.PixelBuffer, this.SourceRectangle, this.Configuration); var gammaOperation = new ApplyGammaExposureRowIntervalOperation(this.SourceRectangle, source.PixelBuffer, this.Configuration, this.gamma);
ParallelRowIterator.IterateRows<ApplyGammaExposureRowIntervalOperation, Vector4>(
this.Configuration,
this.SourceRectangle,
in gammaOperation);
// Create a 0-filled buffer to use to store the result of the component convolutions // Create a 0-filled buffer to use to store the result of the component convolutions
using (Buffer2D<Vector4> processingBuffer = this.Configuration.MemoryAllocator.Allocate2D<Vector4>(source.Size(), AllocationOptions.Clean)) using Buffer2D<Vector4> processingBuffer = this.Configuration.MemoryAllocator.Allocate2D<Vector4>(source.Size(), AllocationOptions.Clean);
{
// Perform the 1D convolutions on all the kernel components and accumulate the results
this.OnFrameApplyCore(source, this.SourceRectangle, this.Configuration, processingBuffer);
// Apply the inverse gamma exposure pass, and write the final pixel data // Perform the 1D convolutions on all the kernel components and accumulate the results
this.ApplyInverseGammaExposure(source.PixelBuffer, processingBuffer, this.SourceRectangle, this.Configuration); this.OnFrameApplyCore(source, this.SourceRectangle, this.Configuration, processingBuffer);
}
float inverseGamma = 1 / this.gamma;
// Apply the inverse gamma exposure pass, and write the final pixel data
var operation = new ApplyInverseGammaExposureRowIntervalOperation(this.SourceRectangle, source.PixelBuffer, processingBuffer, this.Configuration, inverseGamma);
ParallelRowIterator.IterateRows(
this.Configuration,
this.SourceRectangle,
in operation);
} }
/// <summary> /// <summary>
@ -295,216 +303,223 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
Configuration configuration, Configuration configuration,
Buffer2D<Vector4> processingBuffer) Buffer2D<Vector4> processingBuffer)
{ {
using (Buffer2D<ComplexVector4> firstPassBuffer = this.Configuration.MemoryAllocator.Allocate2D<ComplexVector4>(source.Size())) // Allocate the buffer with the intermediate convolution results
using Buffer2D<ComplexVector4> firstPassBuffer = this.Configuration.MemoryAllocator.Allocate2D<ComplexVector4>(source.Size());
// Perform two 1D convolutions for each component in the current instance
ref Complex64[] baseRef = ref MemoryMarshal.GetReference(this.kernels.AsSpan());
ref Vector4 paramsRef = ref MemoryMarshal.GetReference(this.kernelParameters.AsSpan());
for (int i = 0; i < this.kernels.Length; i++)
{ {
// Perform two 1D convolutions for each component in the current instance // Compute the resulting complex buffer for the current component
ref Complex64[] baseRef = ref MemoryMarshal.GetReference(this.kernels.AsSpan()); Complex64[] kernel = Unsafe.Add(ref baseRef, i);
ref Vector4 paramsRef = ref MemoryMarshal.GetReference(this.kernelParameters.AsSpan()); Vector4 parameters = Unsafe.Add(ref paramsRef, i);
for (int i = 0; i < this.kernels.Length; i++)
{ // Compute the vertical 1D convolution
// Compute the resulting complex buffer for the current component var verticalOperation = new ApplyVerticalConvolutionRowIntervalOperation(sourceRectangle, firstPassBuffer, source.PixelBuffer, kernel);
var interest = Rectangle.Intersect(sourceRectangle, source.Bounds()); ParallelRowIterator.IterateRows(
Complex64[] kernel = Unsafe.Add(ref baseRef, i); configuration,
Vector4 parameters = Unsafe.Add(ref paramsRef, i); sourceRectangle,
in verticalOperation);
// Compute the two 1D convolutions and accumulate the partial results on the target buffer
this.ApplyConvolution(firstPassBuffer, source.PixelBuffer, interest, kernel, configuration); // Compute the horizontal 1D convolutions and accumulate the partial results on the target buffer
this.ApplyConvolution(processingBuffer, firstPassBuffer, interest, kernel, configuration, parameters.Z, parameters.W); var horizontalOperation = new ApplyHorizontalConvolutionRowIntervalOperation(sourceRectangle, processingBuffer, firstPassBuffer, kernel, parameters.Z, parameters.W);
} ParallelRowIterator.IterateRows(
configuration,
sourceRectangle,
in horizontalOperation);
} }
} }
/// <summary> /// <summary>
/// Applies the process to the specified portion of the specified <see cref="ImageFrame{TPixel}"/> at the specified location /// A <see langword="struct"/> implementing the vertical convolution logic for <see cref="BokehBlurProcessor{T}"/>.
/// and with the specified size.
/// </summary> /// </summary>
/// <param name="targetValues">The target <see cref="ComplexVector4"/> values to use to store the results.</param> private readonly struct ApplyVerticalConvolutionRowIntervalOperation : IRowIntervalOperation
/// <param name="sourcePixels">The source pixels. Cannot be null.</param>
/// <param name="sourceRectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to draw.
/// </param>
/// <param name="kernel">The 1D kernel.</param>
/// <param name="configuration">The <see cref="Configuration"/></param>
private void ApplyConvolution(
Buffer2D<ComplexVector4> targetValues,
Buffer2D<TPixel> sourcePixels,
Rectangle sourceRectangle,
Complex64[] kernel,
Configuration configuration)
{ {
int startY = sourceRectangle.Y; private readonly Rectangle bounds;
int endY = sourceRectangle.Bottom; private readonly Buffer2D<ComplexVector4> targetValues;
int startX = sourceRectangle.X; private readonly Buffer2D<TPixel> sourcePixels;
int endX = sourceRectangle.Right; private readonly Complex64[] kernel;
int maxY = endY - 1; private readonly int maxY;
int maxX = endX - 1; private readonly int maxX;
var workingRectangle = Rectangle.FromLTRB(startX, startY, endX, endY); [MethodImpl(InliningOptions.ShortMethod)]
int width = workingRectangle.Width; public ApplyVerticalConvolutionRowIntervalOperation(
Rectangle bounds,
ParallelHelper.IterateRows( Buffer2D<ComplexVector4> targetValues,
workingRectangle, Buffer2D<TPixel> sourcePixels,
configuration, Complex64[] kernel)
rows => {
this.bounds = bounds;
this.maxY = this.bounds.Bottom - 1;
this.maxX = this.bounds.Right - 1;
this.targetValues = targetValues;
this.sourcePixels = sourcePixels;
this.kernel = kernel;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
{
for (int y = rows.Min; y < rows.Max; y++)
{ {
for (int y = rows.Min; y < rows.Max; y++) Span<ComplexVector4> targetRowSpan = this.targetValues.GetRowSpan(y).Slice(this.bounds.X);
{
Span<ComplexVector4> targetRowSpan = targetValues.GetRowSpan(y).Slice(startX);
for (int x = 0; x < width; x++) for (int x = 0; x < this.bounds.Width; x++)
{ {
Buffer2DUtils.Convolve4(kernel, sourcePixels, targetRowSpan, y, x, startY, maxY, startX, maxX); Buffer2DUtils.Convolve4(this.kernel, this.sourcePixels, targetRowSpan, y, x, this.bounds.Y, this.maxY, this.bounds.X, this.maxX);
}
} }
}); }
}
} }
/// <summary> /// <summary>
/// Applies the process to the specified portion of the specified <see cref="Buffer2D{T}"/> buffer at the specified location /// A <see langword="struct"/> implementing the horizontal convolution logic for <see cref="BokehBlurProcessor{T}"/>.
/// and with the specified size.
/// </summary> /// </summary>
/// <param name="targetValues">The target <see cref="Vector4"/> values to use to store the results.</param> private readonly struct ApplyHorizontalConvolutionRowIntervalOperation : IRowIntervalOperation
/// <param name="sourceValues">The source complex values. Cannot be null.</param>
/// <param name="sourceRectangle">The <see cref="Rectangle"/> structure that specifies the portion of the image object to draw.</param>
/// <param name="kernel">The 1D kernel.</param>
/// <param name="configuration">The <see cref="Configuration"/></param>
/// <param name="z">The weight factor for the real component of the complex pixel values.</param>
/// <param name="w">The weight factor for the imaginary component of the complex pixel values.</param>
private void ApplyConvolution(
Buffer2D<Vector4> targetValues,
Buffer2D<ComplexVector4> sourceValues,
Rectangle sourceRectangle,
Complex64[] kernel,
Configuration configuration,
float z,
float w)
{ {
int startY = sourceRectangle.Y; private readonly Rectangle bounds;
int endY = sourceRectangle.Bottom; private readonly Buffer2D<Vector4> targetValues;
int startX = sourceRectangle.X; private readonly Buffer2D<ComplexVector4> sourceValues;
int endX = sourceRectangle.Right; private readonly Complex64[] kernel;
int maxY = endY - 1; private readonly float z;
int maxX = endX - 1; private readonly float w;
private readonly int maxY;
var workingRectangle = Rectangle.FromLTRB(startX, startY, endX, endY); private readonly int maxX;
int width = workingRectangle.Width;
[MethodImpl(InliningOptions.ShortMethod)]
ParallelHelper.IterateRows( public ApplyHorizontalConvolutionRowIntervalOperation(
workingRectangle, Rectangle bounds,
configuration, Buffer2D<Vector4> targetValues,
rows => Buffer2D<ComplexVector4> sourceValues,
Complex64[] kernel,
float z,
float w)
{
this.bounds = bounds;
this.maxY = this.bounds.Bottom - 1;
this.maxX = this.bounds.Right - 1;
this.targetValues = targetValues;
this.sourceValues = sourceValues;
this.kernel = kernel;
this.z = z;
this.w = w;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
{
for (int y = rows.Min; y < rows.Max; y++)
{ {
for (int y = rows.Min; y < rows.Max; y++) Span<Vector4> targetRowSpan = this.targetValues.GetRowSpan(y).Slice(this.bounds.X);
{
Span<Vector4> targetRowSpan = targetValues.GetRowSpan(y).Slice(startX);
for (int x = 0; x < width; x++) for (int x = 0; x < this.bounds.Width; x++)
{ {
Buffer2DUtils.Convolve4AndAccumulatePartials(kernel, sourceValues, targetRowSpan, y, x, startY, maxY, startX, maxX, z, w); Buffer2DUtils.Convolve4AndAccumulatePartials(this.kernel, this.sourceValues, targetRowSpan, y, x, this.bounds.Y, this.maxY, this.bounds.X, this.maxX, this.z, this.w);
}
} }
}); }
}
} }
/// <summary> /// <summary>
/// Applies the gamma correction/highlight to the input pixel buffer. /// A <see langword="struct"/> implementing the gamma exposure logic for <see cref="BokehBlurProcessor{T}"/>.
/// </summary> /// </summary>
/// <param name="targetPixels">The target pixel buffer to adjust.</param> private readonly struct ApplyGammaExposureRowIntervalOperation : IRowIntervalOperation<Vector4>
/// <param name="sourceRectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to draw.
/// </param>
/// <param name="configuration">The <see cref="Configuration"/></param>
private void ApplyGammaExposure(
Buffer2D<TPixel> targetPixels,
Rectangle sourceRectangle,
Configuration configuration)
{ {
int startY = sourceRectangle.Y; private readonly Rectangle bounds;
int endY = sourceRectangle.Bottom; private readonly Buffer2D<TPixel> targetPixels;
int startX = sourceRectangle.X; private readonly Configuration configuration;
int endX = sourceRectangle.Right; private readonly float gamma;
var workingRectangle = Rectangle.FromLTRB(startX, startY, endX, endY); [MethodImpl(InliningOptions.ShortMethod)]
int width = workingRectangle.Width; public ApplyGammaExposureRowIntervalOperation(
float exp = this.gamma; Rectangle bounds,
Buffer2D<TPixel> targetPixels,
ParallelHelper.IterateRowsWithTempBuffer<Vector4>( Configuration configuration,
workingRectangle, float gamma)
configuration, {
(rows, vectorBuffer) => this.bounds = bounds;
this.targetPixels = targetPixels;
this.configuration = configuration;
this.gamma = gamma;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows, Span<Vector4> span)
{
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> targetRowSpan = this.targetPixels.GetRowSpan(y).Slice(this.bounds.X);
PixelOperations<TPixel>.Instance.ToVector4(this.configuration, targetRowSpan.Slice(0, span.Length), span, PixelConversionModifiers.Premultiply);
ref Vector4 baseRef = ref MemoryMarshal.GetReference(span);
for (int x = 0; x < this.bounds.Width; x++)
{ {
Span<Vector4> vectorSpan = vectorBuffer.Span; ref Vector4 v = ref Unsafe.Add(ref baseRef, x);
int length = vectorSpan.Length; v.X = MathF.Pow(v.X, this.gamma);
v.Y = MathF.Pow(v.Y, this.gamma);
for (int y = rows.Min; y < rows.Max; y++) v.Z = MathF.Pow(v.Z, this.gamma);
{ }
Span<TPixel> targetRowSpan = targetPixels.GetRowSpan(y).Slice(startX);
PixelOperations<TPixel>.Instance.ToVector4(configuration, targetRowSpan.Slice(0, length), vectorSpan, PixelConversionModifiers.Premultiply); PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, span, targetRowSpan);
ref Vector4 baseRef = ref MemoryMarshal.GetReference(vectorSpan); }
}
for (int x = 0; x < width; x++)
{
ref Vector4 v = ref Unsafe.Add(ref baseRef, x);
v.X = MathF.Pow(v.X, exp);
v.Y = MathF.Pow(v.Y, exp);
v.Z = MathF.Pow(v.Z, exp);
}
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, vectorSpan.Slice(0, length), targetRowSpan);
}
});
} }
/// <summary> /// <summary>
/// Applies the inverse gamma correction/highlight pass, and converts the input <see cref="Vector4"/> buffer into pixel values. /// A <see langword="struct"/> implementing the inverse gamma exposure logic for <see cref="BokehBlurProcessor{T}"/>.
/// </summary> /// </summary>
/// <param name="targetPixels">The target pixels to apply the process to.</param> private readonly struct ApplyInverseGammaExposureRowIntervalOperation : IRowIntervalOperation
/// <param name="sourceValues">The source <see cref="Vector4"/> values. Cannot be null.</param>
/// <param name="sourceRectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to draw.
/// </param>
/// <param name="configuration">The <see cref="Configuration"/></param>
private void ApplyInverseGammaExposure(
Buffer2D<TPixel> targetPixels,
Buffer2D<Vector4> sourceValues,
Rectangle sourceRectangle,
Configuration configuration)
{ {
int startY = sourceRectangle.Y; private readonly Rectangle bounds;
int endY = sourceRectangle.Bottom; private readonly Buffer2D<TPixel> targetPixels;
int startX = sourceRectangle.X; private readonly Buffer2D<Vector4> sourceValues;
int endX = sourceRectangle.Right; private readonly Configuration configuration;
private readonly float inverseGamma;
var workingRectangle = Rectangle.FromLTRB(startX, startY, endX, endY);
int width = workingRectangle.Width; [MethodImpl(InliningOptions.ShortMethod)]
float expGamma = 1 / this.gamma; public ApplyInverseGammaExposureRowIntervalOperation(
Rectangle bounds,
ParallelHelper.IterateRows( Buffer2D<TPixel> targetPixels,
workingRectangle, Buffer2D<Vector4> sourceValues,
configuration, Configuration configuration,
rows => float inverseGamma)
{
this.bounds = bounds;
this.targetPixels = targetPixels;
this.sourceValues = sourceValues;
this.configuration = configuration;
this.inverseGamma = inverseGamma;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
{
Vector4 low = Vector4.Zero;
var high = new Vector4(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity);
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> targetPixelSpan = this.targetPixels.GetRowSpan(y).Slice(this.bounds.X);
Span<Vector4> sourceRowSpan = this.sourceValues.GetRowSpan(y).Slice(this.bounds.X);
ref Vector4 sourceRef = ref MemoryMarshal.GetReference(sourceRowSpan);
for (int x = 0; x < this.bounds.Width; x++)
{ {
Vector4 low = Vector4.Zero; ref Vector4 v = ref Unsafe.Add(ref sourceRef, x);
var high = new Vector4(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity); var clamp = Vector4.Clamp(v, low, high);
v.X = MathF.Pow(clamp.X, this.inverseGamma);
for (int y = rows.Min; y < rows.Max; y++) v.Y = MathF.Pow(clamp.Y, this.inverseGamma);
{ v.Z = MathF.Pow(clamp.Z, this.inverseGamma);
Span<TPixel> targetPixelSpan = targetPixels.GetRowSpan(y).Slice(startX); }
Span<Vector4> sourceRowSpan = sourceValues.GetRowSpan(y).Slice(startX);
ref Vector4 sourceRef = ref MemoryMarshal.GetReference(sourceRowSpan); PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, sourceRowSpan.Slice(0, this.bounds.Width), targetPixelSpan, PixelConversionModifiers.Premultiply);
}
for (int x = 0; x < width; x++) }
{
ref Vector4 v = ref Unsafe.Add(ref sourceRef, x);
var clamp = Vector4.Clamp(v, low, high);
v.X = MathF.Pow(clamp.X, expGamma);
v.Y = MathF.Pow(clamp.Y, expGamma);
v.Z = MathF.Pow(clamp.Z, expGamma);
}
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, sourceRowSpan.Slice(0, width), targetPixelSpan, PixelConversionModifiers.Premultiply);
}
});
} }
} }
} }

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

@ -40,10 +40,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source) protected override void OnFrameApply(ImageFrame<TPixel> source)
{ {
using (var processor = new Convolution2PassProcessor<TPixel>(this.Configuration, this.KernelX, this.KernelY, false, this.Source, this.SourceRectangle)) using var processor = new Convolution2PassProcessor<TPixel>(this.Configuration, this.KernelX, this.KernelY, false, this.Source, this.SourceRectangle);
{
processor.Apply(source); processor.Apply(source);
}
} }
/// <summary> /// <summary>

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

@ -3,9 +3,9 @@
using System; using System;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Advanced.ParallelUtils;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -60,79 +60,105 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source) protected override void OnFrameApply(ImageFrame<TPixel> source)
{ {
DenseMatrix<float> matrixY = this.KernelY; using Buffer2D<TPixel> targetPixels = this.Configuration.MemoryAllocator.Allocate2D<TPixel>(source.Width, source.Height);
DenseMatrix<float> matrixX = this.KernelX;
bool preserveAlpha = this.PreserveAlpha; source.CopyTo(targetPixels);
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
int startY = interest.Y; var operation = new RowIntervalOperation(interest, targetPixels, source.PixelBuffer, this.KernelY, this.KernelX, this.Configuration, this.PreserveAlpha);
int endY = interest.Bottom;
int startX = interest.X; ParallelRowIterator.IterateRows<RowIntervalOperation, Vector4>(
int endX = interest.Right; this.Configuration,
int maxY = endY - 1; interest,
int maxX = endX - 1; in operation);
using (Buffer2D<TPixel> targetPixels = this.Configuration.MemoryAllocator.Allocate2D<TPixel>(source.Width, source.Height)) Buffer2D<TPixel>.SwapOrCopyContent(source.PixelBuffer, targetPixels);
}
/// <summary>
/// A <see langword="struct"/> implementing the convolution logic for <see cref="Convolution2DProcessor{T}"/>.
/// </summary>
private readonly struct RowIntervalOperation : IRowIntervalOperation<Vector4>
{
private readonly Rectangle bounds;
private readonly int maxY;
private readonly int maxX;
private readonly Buffer2D<TPixel> targetPixels;
private readonly Buffer2D<TPixel> sourcePixels;
private readonly DenseMatrix<float> kernelY;
private readonly DenseMatrix<float> kernelX;
private readonly Configuration configuration;
private readonly bool preserveAlpha;
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(
Rectangle bounds,
Buffer2D<TPixel> targetPixels,
Buffer2D<TPixel> sourcePixels,
DenseMatrix<float> kernelY,
DenseMatrix<float> kernelX,
Configuration configuration,
bool preserveAlpha)
{ {
source.CopyTo(targetPixels); this.bounds = bounds;
this.maxY = this.bounds.Bottom - 1;
this.maxX = this.bounds.Right - 1;
this.targetPixels = targetPixels;
this.sourcePixels = sourcePixels;
this.kernelY = kernelY;
this.kernelX = kernelX;
this.configuration = configuration;
this.preserveAlpha = preserveAlpha;
}
var workingRectangle = Rectangle.FromLTRB(startX, startY, endX, endY); /// <inheritdoc/>
int width = workingRectangle.Width; [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows, Span<Vector4> span)
{
ref Vector4 spanRef = ref MemoryMarshal.GetReference(span);
ParallelHelper.IterateRowsWithTempBuffer<Vector4>( for (int y = rows.Min; y < rows.Max; y++)
workingRectangle, {
this.Configuration, Span<TPixel> targetRowSpan = this.targetPixels.GetRowSpan(y).Slice(this.bounds.X);
(rows, vectorBuffer) => PixelOperations<TPixel>.Instance.ToVector4(this.configuration, targetRowSpan.Slice(0, span.Length), span);
if (this.preserveAlpha)
{
for (int x = 0; x < this.bounds.Width; x++)
{
DenseMatrixUtils.Convolve2D3(
in this.kernelY,
in this.kernelX,
this.sourcePixels,
ref spanRef,
y,
x,
this.bounds.Y,
this.maxY,
this.bounds.X,
this.maxX);
}
}
else
{
for (int x = 0; x < this.bounds.Width; x++)
{ {
Span<Vector4> vectorSpan = vectorBuffer.Span; DenseMatrixUtils.Convolve2D4(
int length = vectorSpan.Length; in this.kernelY,
ref Vector4 vectorSpanRef = ref MemoryMarshal.GetReference(vectorSpan); in this.kernelX,
this.sourcePixels,
for (int y = rows.Min; y < rows.Max; y++) ref spanRef,
{ y,
Span<TPixel> targetRowSpan = targetPixels.GetRowSpan(y).Slice(startX); x,
PixelOperations<TPixel>.Instance.ToVector4(this.Configuration, targetRowSpan.Slice(0, length), vectorSpan); this.bounds.Y,
this.maxY,
if (preserveAlpha) this.bounds.X,
{ this.maxX);
for (int x = 0; x < width; x++) }
{ }
DenseMatrixUtils.Convolve2D3(
in matrixY, PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, span, targetRowSpan);
in matrixX, }
source.PixelBuffer,
ref vectorSpanRef,
y,
x,
startY,
maxY,
startX,
maxX);
}
}
else
{
for (int x = 0; x < width; x++)
{
DenseMatrixUtils.Convolve2D4(
in matrixY,
in matrixX,
source.PixelBuffer,
ref vectorSpanRef,
y,
x,
startY,
maxY,
startX,
maxX);
}
}
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.Configuration, vectorSpan, targetRowSpan);
}
});
Buffer2D<TPixel>.SwapOrCopyContent(source.PixelBuffer, targetPixels);
} }
} }
} }

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

@ -3,9 +3,9 @@
using System; using System;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Advanced.ParallelUtils;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -59,95 +59,104 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source) protected override void OnFrameApply(ImageFrame<TPixel> source)
{ {
using (Buffer2D<TPixel> firstPassPixels = this.Configuration.MemoryAllocator.Allocate2D<TPixel>(source.Size())) using Buffer2D<TPixel> firstPassPixels = this.Configuration.MemoryAllocator.Allocate2D<TPixel>(source.Size());
{
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
this.ApplyConvolution(firstPassPixels, source.PixelBuffer, interest, this.KernelX, this.Configuration);
this.ApplyConvolution(source.PixelBuffer, firstPassPixels, interest, this.KernelY, this.Configuration); // Horizontal convolution
} var horizontalOperation = new RowIntervalOperation(interest, firstPassPixels, source.PixelBuffer, this.KernelX, this.Configuration, this.PreserveAlpha);
ParallelRowIterator.IterateRows<RowIntervalOperation, Vector4>(
this.Configuration,
interest,
in horizontalOperation);
// Vertical convolution
var verticalOperation = new RowIntervalOperation(interest, source.PixelBuffer, firstPassPixels, this.KernelY, this.Configuration, this.PreserveAlpha);
ParallelRowIterator.IterateRows<RowIntervalOperation, Vector4>(
this.Configuration,
interest,
in verticalOperation);
} }
/// <summary> /// <summary>
/// Applies the process to the specified portion of the specified <see cref="ImageFrame{TPixel}"/> at the specified location /// A <see langword="struct"/> implementing the convolution logic for <see cref="Convolution2PassProcessor{T}"/>.
/// and with the specified size.
/// </summary> /// </summary>
/// <param name="targetPixels">The target pixels to apply the process to.</param> private readonly struct RowIntervalOperation : IRowIntervalOperation<Vector4>
/// <param name="sourcePixels">The source pixels. Cannot be null.</param>
/// <param name="sourceRectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to draw.
/// </param>
/// <param name="kernel">The kernel operator.</param>
/// <param name="configuration">The <see cref="Configuration"/></param>
private void ApplyConvolution(
Buffer2D<TPixel> targetPixels,
Buffer2D<TPixel> sourcePixels,
Rectangle sourceRectangle,
in DenseMatrix<float> kernel,
Configuration configuration)
{ {
DenseMatrix<float> matrix = kernel; private readonly Rectangle bounds;
bool preserveAlpha = this.PreserveAlpha; private readonly Buffer2D<TPixel> targetPixels;
private readonly Buffer2D<TPixel> sourcePixels;
int startY = sourceRectangle.Y; private readonly DenseMatrix<float> kernel;
int endY = sourceRectangle.Bottom; private readonly Configuration configuration;
int startX = sourceRectangle.X; private readonly bool preserveAlpha;
int endX = sourceRectangle.Right;
int maxY = endY - 1; [MethodImpl(InliningOptions.ShortMethod)]
int maxX = endX - 1; public RowIntervalOperation(
Rectangle bounds,
var workingRectangle = Rectangle.FromLTRB(startX, startY, endX, endY); Buffer2D<TPixel> targetPixels,
int width = workingRectangle.Width; Buffer2D<TPixel> sourcePixels,
DenseMatrix<float> kernel,
ParallelHelper.IterateRowsWithTempBuffer<Vector4>( Configuration configuration,
workingRectangle, bool preserveAlpha)
configuration, {
(rows, vectorBuffer) => this.bounds = bounds;
{ this.targetPixels = targetPixels;
Span<Vector4> vectorSpan = vectorBuffer.Span; this.sourcePixels = sourcePixels;
int length = vectorSpan.Length; this.kernel = kernel;
ref Vector4 vectorSpanRef = ref MemoryMarshal.GetReference(vectorSpan); this.configuration = configuration;
this.preserveAlpha = preserveAlpha;
}
for (int y = rows.Min; y < rows.Max; y++) /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows, Span<Vector4> span)
{
ref Vector4 spanRef = ref MemoryMarshal.GetReference(span);
int maxY = this.bounds.Bottom - 1;
int maxX = this.bounds.Right - 1;
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> targetRowSpan = this.targetPixels.GetRowSpan(y).Slice(this.bounds.X);
PixelOperations<TPixel>.Instance.ToVector4(this.configuration, targetRowSpan.Slice(0, span.Length), span);
if (this.preserveAlpha)
{
for (int x = 0; x < this.bounds.Width; x++)
{ {
Span<TPixel> targetRowSpan = targetPixels.GetRowSpan(y).Slice(startX); DenseMatrixUtils.Convolve3(
PixelOperations<TPixel>.Instance.ToVector4(configuration, targetRowSpan.Slice(0, length), vectorSpan); in this.kernel,
this.sourcePixels,
if (preserveAlpha) ref spanRef,
{ y,
for (int x = 0; x < width; x++) x,
{ this.bounds.Y,
DenseMatrixUtils.Convolve3( maxY,
in matrix, this.bounds.X,
sourcePixels, maxX);
ref vectorSpanRef,
y,
x,
startY,
maxY,
startX,
maxX);
}
}
else
{
for (int x = 0; x < width; x++)
{
DenseMatrixUtils.Convolve4(
in matrix,
sourcePixels,
ref vectorSpanRef,
y,
x,
startY,
maxY,
startX,
maxX);
}
}
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, vectorSpan, targetRowSpan);
} }
}); }
else
{
for (int x = 0; x < this.bounds.Width; x++)
{
DenseMatrixUtils.Convolve4(
in this.kernel,
this.sourcePixels,
ref spanRef,
y,
x,
this.bounds.Y,
maxY,
this.bounds.X,
maxX);
}
}
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, span, targetRowSpan);
}
}
} }
} }
} }

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

@ -3,9 +3,9 @@
using System; using System;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Advanced.ParallelUtils;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -51,76 +51,99 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source) protected override void OnFrameApply(ImageFrame<TPixel> source)
{ {
DenseMatrix<float> matrix = this.KernelXY; using Buffer2D<TPixel> targetPixels = this.Configuration.MemoryAllocator.Allocate2D<TPixel>(source.Size());
bool preserveAlpha = this.PreserveAlpha;
source.CopyTo(targetPixels);
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
int startY = interest.Y; var operation = new RowIntervalOperation(interest, targetPixels, source.PixelBuffer, this.KernelXY, this.Configuration, this.PreserveAlpha);
int endY = interest.Bottom; ParallelRowIterator.IterateRows<RowIntervalOperation, Vector4>(
int startX = interest.X; this.Configuration,
int endX = interest.Right; interest,
int maxY = endY - 1; in operation);
int maxX = endX - 1;
using (Buffer2D<TPixel> targetPixels = this.Configuration.MemoryAllocator.Allocate2D<TPixel>(source.Size())) Buffer2D<TPixel>.SwapOrCopyContent(source.PixelBuffer, targetPixels);
{ }
source.CopyTo(targetPixels);
var workingRectangle = Rectangle.FromLTRB(startX, startY, endX, endY); /// <summary>
int width = workingRectangle.Width; /// A <see langword="struct"/> implementing the convolution logic for <see cref="ConvolutionProcessor{T}"/>.
/// </summary>
private readonly struct RowIntervalOperation : IRowIntervalOperation<Vector4>
{
private readonly Rectangle bounds;
private readonly int maxY;
private readonly int maxX;
private readonly Buffer2D<TPixel> targetPixels;
private readonly Buffer2D<TPixel> sourcePixels;
private readonly DenseMatrix<float> kernel;
private readonly Configuration configuration;
private readonly bool preserveAlpha;
ParallelHelper.IterateRowsWithTempBuffer<Vector4>( [MethodImpl(InliningOptions.ShortMethod)]
workingRectangle, public RowIntervalOperation(
this.Configuration, Rectangle bounds,
(rows, vectorBuffer) => Buffer2D<TPixel> targetPixels,
{ Buffer2D<TPixel> sourcePixels,
Span<Vector4> vectorSpan = vectorBuffer.Span; DenseMatrix<float> kernel,
int length = vectorSpan.Length; Configuration configuration,
ref Vector4 vectorSpanRef = ref MemoryMarshal.GetReference(vectorSpan); bool preserveAlpha)
{
this.bounds = bounds;
this.maxY = this.bounds.Bottom - 1;
this.maxX = this.bounds.Right - 1;
this.targetPixels = targetPixels;
this.sourcePixels = sourcePixels;
this.kernel = kernel;
this.configuration = configuration;
this.preserveAlpha = preserveAlpha;
}
for (int y = rows.Min; y < rows.Max; y++) /// <inheritdoc/>
{ [MethodImpl(InliningOptions.ShortMethod)]
Span<TPixel> targetRowSpan = targetPixels.GetRowSpan(y).Slice(startX); public void Invoke(in RowInterval rows, Span<Vector4> span)
PixelOperations<TPixel>.Instance.ToVector4(this.Configuration, targetRowSpan.Slice(0, length), vectorSpan); {
ref Vector4 spanRef = ref MemoryMarshal.GetReference(span);
if (preserveAlpha) for (int y = rows.Min; y < rows.Max; y++)
{ {
for (int x = 0; x < width; x++) Span<TPixel> targetRowSpan = this.targetPixels.GetRowSpan(y).Slice(this.bounds.X);
{ PixelOperations<TPixel>.Instance.ToVector4(this.configuration, targetRowSpan.Slice(0, span.Length), span);
DenseMatrixUtils.Convolve3(
in matrix,
source.PixelBuffer,
ref vectorSpanRef,
y,
x,
startY,
maxY,
startX,
maxX);
}
}
else
{
for (int x = 0; x < width; x++)
{
DenseMatrixUtils.Convolve4(
in matrix,
source.PixelBuffer,
ref vectorSpanRef,
y,
x,
startY,
maxY,
startX,
maxX);
}
}
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.Configuration, vectorSpan, targetRowSpan); if (this.preserveAlpha)
} {
}); for (int x = 0; x < this.bounds.Width; x++)
{
DenseMatrixUtils.Convolve3(
in this.kernel,
this.sourcePixels,
ref spanRef,
y,
x,
this.bounds.Y,
this.maxY,
this.bounds.X,
this.maxX);
}
}
else
{
for (int x = 0; x < this.bounds.Width; x++)
{
DenseMatrixUtils.Convolve4(
in this.kernel,
this.sourcePixels,
ref spanRef,
y,
x,
this.bounds.Y,
this.maxY,
this.bounds.X,
this.maxX);
}
}
Buffer2D<TPixel>.SwapOrCopyContent(source.PixelBuffer, targetPixels); PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, span, targetRowSpan);
}
} }
} }
} }

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

@ -63,10 +63,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// <inheritdoc /> /// <inheritdoc />
protected override void OnFrameApply(ImageFrame<TPixel> source) protected override void OnFrameApply(ImageFrame<TPixel> source)
{ {
using (var processor = new Convolution2DProcessor<TPixel>(this.Configuration, this.KernelX, this.KernelY, true, this.Source, this.SourceRectangle)) using var processor = new Convolution2DProcessor<TPixel>(this.Configuration, this.KernelX, this.KernelY, true, this.Source, this.SourceRectangle);
{
processor.Apply(source); processor.Apply(source);
}
} }
} }
} }

133
src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor{TPixel}.cs

@ -1,12 +1,10 @@
// Copyright (c) Six Labors and contributors. // Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Advanced.ParallelUtils;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Filters; using SixLabors.ImageSharp.Processing.Processors.Filters;
@ -55,88 +53,79 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
{ {
DenseMatrix<float>[] kernels = this.Kernels.Flatten(); DenseMatrix<float>[] kernels = this.Kernels.Flatten();
int startY = this.SourceRectangle.Y; var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
int endY = this.SourceRectangle.Bottom;
int startX = this.SourceRectangle.X;
int endX = this.SourceRectangle.Right;
// Align start/end positions. // We need a clean copy for each pass to start from
int minX = Math.Max(0, startX); using ImageFrame<TPixel> cleanCopy = source.Clone();
int maxX = Math.Min(source.Width, endX);
int minY = Math.Max(0, startY);
int maxY = Math.Min(source.Height, endY);
// we need a clean copy for each pass to start from using (var processor = new ConvolutionProcessor<TPixel>(this.Configuration, kernels[0], true, this.Source, interest))
using (ImageFrame<TPixel> cleanCopy = source.Clone())
{ {
using (var processor = new ConvolutionProcessor<TPixel>(this.Configuration, kernels[0], true, this.Source, this.SourceRectangle)) processor.Apply(source);
{ }
processor.Apply(source);
}
if (kernels.Length == 1) if (kernels.Length == 1)
{ {
return; return;
} }
int shiftY = startY; // Additional runs
int shiftX = startX; for (int i = 1; i < kernels.Length; i++)
{
using ImageFrame<TPixel> pass = cleanCopy.Clone();
// Reset offset if necessary. using (var processor = new ConvolutionProcessor<TPixel>(this.Configuration, kernels[i], true, this.Source, interest))
if (minX > 0)
{ {
shiftX = 0; processor.Apply(pass);
} }
if (minY > 0) var operation = new RowIntervalOperation(source.PixelBuffer, pass.PixelBuffer, interest);
{ ParallelRowIterator.IterateRows(
shiftY = 0; this.Configuration,
} interest,
in operation);
}
}
var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY); /// <summary>
/// A <see langword="struct"/> implementing the convolution logic for <see cref="EdgeDetectorCompassProcessor{T}"/>.
/// </summary>
private readonly struct RowIntervalOperation : IRowIntervalOperation
{
private readonly Buffer2D<TPixel> targetPixels;
private readonly Buffer2D<TPixel> passPixels;
private readonly int minX;
private readonly int maxX;
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(
Buffer2D<TPixel> targetPixels,
Buffer2D<TPixel> passPixels,
Rectangle bounds)
{
this.targetPixels = targetPixels;
this.passPixels = passPixels;
this.minX = bounds.X;
this.maxX = bounds.Right;
}
// Additional runs. /// <inheritdoc/>
// ReSharper disable once ForCanBeConvertedToForeach [MethodImpl(InliningOptions.ShortMethod)]
for (int i = 1; i < kernels.Length; i++) public void Invoke(in RowInterval rows)
{
for (int y = rows.Min; y < rows.Max; y++)
{ {
using (ImageFrame<TPixel> pass = cleanCopy.Clone()) ref TPixel passPixelsBase = ref MemoryMarshal.GetReference(this.passPixels.GetRowSpan(y));
ref TPixel targetPixelsBase = ref MemoryMarshal.GetReference(this.targetPixels.GetRowSpan(y));
for (int x = this.minX; x < this.maxX; x++)
{ {
using (var processor = new ConvolutionProcessor<TPixel>(this.Configuration, kernels[i], true, this.Source, this.SourceRectangle)) // Grab the max components of the two pixels
{ ref TPixel currentPassPixel = ref Unsafe.Add(ref passPixelsBase, x);
processor.Apply(pass); ref TPixel currentTargetPixel = ref Unsafe.Add(ref targetPixelsBase, x);
}
var pixelValue = Vector4.Max(currentPassPixel.ToVector4(), currentTargetPixel.ToVector4());
Buffer2D<TPixel> passPixels = pass.PixelBuffer;
Buffer2D<TPixel> targetPixels = source.PixelBuffer; currentTargetPixel.FromVector4(pixelValue);
ParallelHelper.IterateRows(
workingRect,
this.Configuration,
rows =>
{
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));
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);
var pixelValue = Vector4.Max(
currentPassPixel.ToVector4(),
currentTargetPixel.ToVector4());
currentTargetPixel.FromVector4(pixelValue);
}
}
});
} }
} }
} }

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

@ -44,10 +44,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source) protected override void OnFrameApply(ImageFrame<TPixel> source)
{ {
using (var processor = new Convolution2PassProcessor<TPixel>(this.Configuration, this.KernelX, this.KernelY, false, this.Source, this.SourceRectangle)) using var processor = new Convolution2PassProcessor<TPixel>(this.Configuration, this.KernelX, this.KernelY, false, this.Source, this.SourceRectangle);
{
processor.Apply(source); processor.Apply(source);
}
} }
} }
} }

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

@ -44,10 +44,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source) protected override void OnFrameApply(ImageFrame<TPixel> source)
{ {
using (var processor = new Convolution2PassProcessor<TPixel>(this.Configuration, this.KernelX, this.KernelY, false, this.Source, this.SourceRectangle)) using var processor = new Convolution2PassProcessor<TPixel>(this.Configuration, this.KernelX, this.KernelY, false, this.Source, this.SourceRectangle);
{
processor.Apply(source); processor.Apply(source);
}
} }
} }
} }

68
src/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs

@ -2,9 +2,9 @@
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System; using System;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Advanced.ParallelUtils; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Drawing namespace SixLabors.ImageSharp.Processing.Processors.Drawing
@ -99,18 +99,62 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing
"Cannot draw image because the source image does not overlap the target image."); "Cannot draw image because the source image does not overlap the target image.");
} }
ParallelHelper.IterateRows( var operation = new RowIntervalOperation(source, targetImage, blender, configuration, minX, width, locationY, targetX, this.Opacity);
workingRect, ParallelRowIterator.IterateRows(
configuration, configuration,
rows => workingRect,
in operation);
}
/// <summary>
/// A <see langword="struct"/> implementing the draw logic for <see cref="DrawImageProcessor{TPixelBg,TPixelFg}"/>.
/// </summary>
private readonly struct RowIntervalOperation : IRowIntervalOperation
{
private readonly ImageFrame<TPixelBg> sourceFrame;
private readonly Image<TPixelFg> targetImage;
private readonly PixelBlender<TPixelBg> blender;
private readonly Configuration configuration;
private readonly int minX;
private readonly int width;
private readonly int locationY;
private readonly int targetX;
private readonly float opacity;
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(
ImageFrame<TPixelBg> sourceFrame,
Image<TPixelFg> targetImage,
PixelBlender<TPixelBg> blender,
Configuration configuration,
int minX,
int width,
int locationY,
int targetX,
float opacity)
{
this.sourceFrame = sourceFrame;
this.targetImage = targetImage;
this.blender = blender;
this.configuration = configuration;
this.minX = minX;
this.width = width;
this.locationY = locationY;
this.targetX = targetX;
this.opacity = opacity;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
{
for (int y = rows.Min; y < rows.Max; y++)
{ {
for (int y = rows.Min; y < rows.Max; y++) Span<TPixelBg> background = this.sourceFrame.GetPixelRowSpan(y).Slice(this.minX, this.width);
{ Span<TPixelFg> foreground = this.targetImage.GetPixelRowSpan(y - this.locationY).Slice(this.targetX, this.width);
Span<TPixelBg> background = source.GetPixelRowSpan(y).Slice(minX, width); this.blender.Blend<TPixelFg>(this.configuration, background, background, foreground, this.opacity);
Span<TPixelFg> foreground = targetImage.GetPixelRowSpan(y - locationY).Slice(targetX, width); }
blender.Blend<TPixelFg>(configuration, background, background, foreground, this.Opacity); }
}
});
} }
} }
} }

21
src/ImageSharp/Processing/Processors/Effects/IPixelRowDelegate.cs

@ -0,0 +1,21 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Numerics;
namespace SixLabors.ImageSharp.Processing.Processors.Effects
{
/// <summary>
/// An <see langword="interface"/> used by the row delegates for a given <see cref="PixelRowDelegateProcessor{TPixel,TDelegate}"/> instance
/// </summary>
public interface IPixelRowDelegate
{
/// <summary>
/// Applies the current pixel row delegate to a target row of preprocessed pixels.
/// </summary>
/// <param name="span">The target row of <see cref="Vector4"/> pixels to process.</param>
/// <param name="offset">The initial horizontal and vertical offset for the input pixels to process.</param>
void Invoke(Span<Vector4> span, Point offset);
}
}

205
src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs

@ -5,9 +5,7 @@ using System;
using System.Buffers; using System.Buffers;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Advanced.ParallelUtils;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -45,122 +43,145 @@ namespace SixLabors.ImageSharp.Processing.Processors.Effects
throw new ArgumentOutOfRangeException(nameof(brushSize)); throw new ArgumentOutOfRangeException(nameof(brushSize));
} }
int startY = this.SourceRectangle.Y;
int endY = this.SourceRectangle.Bottom;
int startX = this.SourceRectangle.X;
int endX = this.SourceRectangle.Right;
int maxY = endY - 1;
int maxX = endX - 1;
int radius = brushSize >> 1;
int levels = this.definition.Levels;
int rowWidth = source.Width;
int rectangleWidth = this.SourceRectangle.Width;
Configuration configuration = this.Configuration;
using Buffer2D<TPixel> targetPixels = this.Configuration.MemoryAllocator.Allocate2D<TPixel>(source.Size()); using Buffer2D<TPixel> targetPixels = this.Configuration.MemoryAllocator.Allocate2D<TPixel>(source.Size());
source.CopyTo(targetPixels); source.CopyTo(targetPixels);
var workingRect = Rectangle.FromLTRB(startX, startY, endX, endY); var operation = new RowIntervalOperation(this.SourceRectangle, targetPixels, source, this.Configuration, brushSize >> 1, this.definition.Levels);
ParallelHelper.IterateRows( ParallelRowIterator.IterateRows(
workingRect,
this.Configuration, this.Configuration,
(rows) => this.SourceRectangle,
{ in operation);
/* Allocate the two temporary Vector4 buffers, one for the source row and one for the target row.
* The ParallelHelper.IterateRowsWithTempBuffers overload is not used in this case because
* the two allocated buffers have a length equal to the width of the source image,
* and not just equal to the width of the target rectangle to process.
* Furthermore, there are two buffers being allocated in this case, so using that overload would
* have still required the explicit allocation of the secondary buffer.
* Similarly, one temporary float buffer is also allocated from the pool, and that is used
* to create the target bins for all the color channels being processed.
* This buffer is only rented once outside of the main processing loop, and its contents
* are cleared for each loop iteration, to avoid the repeated allocation for each processed pixel. */
using (IMemoryOwner<Vector4> sourceRowBuffer = configuration.MemoryAllocator.Allocate<Vector4>(rowWidth))
using (IMemoryOwner<Vector4> targetRowBuffer = configuration.MemoryAllocator.Allocate<Vector4>(rowWidth))
using (IMemoryOwner<float> bins = configuration.MemoryAllocator.Allocate<float>(levels * 4))
{
Span<Vector4> sourceRowVector4Span = sourceRowBuffer.Memory.Span;
Span<Vector4> sourceRowAreaVector4Span = sourceRowVector4Span.Slice(startX, rectangleWidth);
Span<Vector4> targetRowVector4Span = targetRowBuffer.Memory.Span; Buffer2D<TPixel>.SwapOrCopyContent(source.PixelBuffer, targetPixels);
Span<Vector4> targetRowAreaVector4Span = targetRowVector4Span.Slice(startX, rectangleWidth); }
ref float binsRef = ref bins.GetReference();
ref int intensityBinRef = ref Unsafe.As<float, int>(ref binsRef);
ref float redBinRef = ref Unsafe.Add(ref binsRef, levels);
ref float blueBinRef = ref Unsafe.Add(ref redBinRef, levels);
ref float greenBinRef = ref Unsafe.Add(ref blueBinRef, levels);
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> sourceRowPixelSpan = source.GetPixelRowSpan(y);
Span<TPixel> sourceRowAreaPixelSpan = sourceRowPixelSpan.Slice(startX, rectangleWidth);
PixelOperations<TPixel>.Instance.ToVector4(configuration, sourceRowAreaPixelSpan, sourceRowAreaVector4Span); /// <summary>
/// A <see langword="struct"/> implementing the convolution logic for <see cref="OilPaintingProcessor{T}"/>.
/// </summary>
private readonly struct RowIntervalOperation : IRowIntervalOperation
{
private readonly Rectangle bounds;
private readonly Buffer2D<TPixel> targetPixels;
private readonly ImageFrame<TPixel> source;
private readonly Configuration configuration;
private readonly int radius;
private readonly int levels;
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(
Rectangle bounds,
Buffer2D<TPixel> targetPixels,
ImageFrame<TPixel> source,
Configuration configuration,
int radius,
int levels)
{
this.bounds = bounds;
this.targetPixels = targetPixels;
this.source = source;
this.configuration = configuration;
this.radius = radius;
this.levels = levels;
}
for (int x = startX; x < endX; x++) /// <inheritdoc/>
{ [MethodImpl(InliningOptions.ShortMethod)]
int maxIntensity = 0; public void Invoke(in RowInterval rows)
int maxIndex = 0; {
int maxY = this.bounds.Bottom - 1;
int maxX = this.bounds.Right - 1;
/* Allocate the two temporary Vector4 buffers, one for the source row and one for the target row.
* The ParallelHelper.IterateRowsWithTempBuffers overload is not used in this case because
* the two allocated buffers have a length equal to the width of the source image,
* and not just equal to the width of the target rectangle to process.
* Furthermore, there are two buffers being allocated in this case, so using that overload would
* have still required the explicit allocation of the secondary buffer.
* Similarly, one temporary float buffer is also allocated from the pool, and that is used
* to create the target bins for all the color channels being processed.
* This buffer is only rented once outside of the main processing loop, and its contents
* are cleared for each loop iteration, to avoid the repeated allocation for each processed pixel. */
using IMemoryOwner<Vector4> sourceRowBuffer = this.configuration.MemoryAllocator.Allocate<Vector4>(this.source.Width);
using IMemoryOwner<Vector4> targetRowBuffer = this.configuration.MemoryAllocator.Allocate<Vector4>(this.source.Width);
using IMemoryOwner<float> bins = this.configuration.MemoryAllocator.Allocate<float>(this.levels * 4);
Span<Vector4> sourceRowVector4Span = sourceRowBuffer.Memory.Span;
Span<Vector4> sourceRowAreaVector4Span = sourceRowVector4Span.Slice(this.bounds.X, this.bounds.Width);
Span<Vector4> targetRowVector4Span = targetRowBuffer.Memory.Span;
Span<Vector4> targetRowAreaVector4Span = targetRowVector4Span.Slice(this.bounds.X, this.bounds.Width);
ref float binsRef = ref bins.GetReference();
ref int intensityBinRef = ref Unsafe.As<float, int>(ref binsRef);
ref float redBinRef = ref Unsafe.Add(ref binsRef, this.levels);
ref float blueBinRef = ref Unsafe.Add(ref redBinRef, this.levels);
ref float greenBinRef = ref Unsafe.Add(ref blueBinRef, this.levels);
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> sourceRowPixelSpan = this.source.GetPixelRowSpan(y);
Span<TPixel> sourceRowAreaPixelSpan = sourceRowPixelSpan.Slice(this.bounds.X, this.bounds.Width);
// Clear the current shared buffer before processing each target pixel PixelOperations<TPixel>.Instance.ToVector4(this.configuration, sourceRowAreaPixelSpan, sourceRowAreaVector4Span);
bins.Memory.Span.Clear();
for (int fy = 0; fy <= radius; fy++) for (int x = this.bounds.X; x < this.bounds.Right; x++)
{ {
int fyr = fy - radius; int maxIntensity = 0;
int offsetY = y + fyr; int maxIndex = 0;
offsetY = offsetY.Clamp(0, maxY); // Clear the current shared buffer before processing each target pixel
bins.Memory.Span.Clear();
Span<TPixel> sourceOffsetRow = source.GetPixelRowSpan(offsetY); for (int fy = 0; fy <= this.radius; fy++)
{
int fyr = fy - this.radius;
int offsetY = y + fyr;
for (int fx = 0; fx <= radius; fx++) offsetY = offsetY.Clamp(0, maxY);
{
int fxr = fx - radius;
int offsetX = x + fxr;
offsetX = offsetX.Clamp(0, maxX);
var vector = sourceOffsetRow[offsetX].ToVector4(); Span<TPixel> sourceOffsetRow = this.source.GetPixelRowSpan(offsetY);
float sourceRed = vector.X; for (int fx = 0; fx <= this.radius; fx++)
float sourceBlue = vector.Z; {
float sourceGreen = vector.Y; int fxr = fx - this.radius;
int offsetX = x + fxr;
offsetX = offsetX.Clamp(0, maxX);
int currentIntensity = (int)MathF.Round((sourceBlue + sourceGreen + sourceRed) / 3F * (levels - 1)); var vector = sourceOffsetRow[offsetX].ToVector4();
Unsafe.Add(ref intensityBinRef, currentIntensity)++; float sourceRed = vector.X;
Unsafe.Add(ref redBinRef, currentIntensity) += sourceRed; float sourceBlue = vector.Z;
Unsafe.Add(ref blueBinRef, currentIntensity) += sourceBlue; float sourceGreen = vector.Y;
Unsafe.Add(ref greenBinRef, currentIntensity) += sourceGreen;
if (Unsafe.Add(ref intensityBinRef, currentIntensity) > maxIntensity) int currentIntensity = (int)MathF.Round((sourceBlue + sourceGreen + sourceRed) / 3F * (this.levels - 1));
{
maxIntensity = Unsafe.Add(ref intensityBinRef, currentIntensity);
maxIndex = currentIntensity;
}
}
float red = MathF.Abs(Unsafe.Add(ref redBinRef, maxIndex) / maxIntensity); Unsafe.Add(ref intensityBinRef, currentIntensity)++;
float blue = MathF.Abs(Unsafe.Add(ref blueBinRef, maxIndex) / maxIntensity); Unsafe.Add(ref redBinRef, currentIntensity) += sourceRed;
float green = MathF.Abs(Unsafe.Add(ref greenBinRef, maxIndex) / maxIntensity); Unsafe.Add(ref blueBinRef, currentIntensity) += sourceBlue;
float alpha = sourceRowVector4Span[x].W; Unsafe.Add(ref greenBinRef, currentIntensity) += sourceGreen;
targetRowVector4Span[x] = new Vector4(red, green, blue, alpha); if (Unsafe.Add(ref intensityBinRef, currentIntensity) > maxIntensity)
{
maxIntensity = Unsafe.Add(ref intensityBinRef, currentIntensity);
maxIndex = currentIntensity;
} }
} }
Span<TPixel> targetRowAreaPixelSpan = targetPixels.GetRowSpan(y).Slice(startX, rectangleWidth); float red = MathF.Abs(Unsafe.Add(ref redBinRef, maxIndex) / maxIntensity);
float blue = MathF.Abs(Unsafe.Add(ref blueBinRef, maxIndex) / maxIntensity);
float green = MathF.Abs(Unsafe.Add(ref greenBinRef, maxIndex) / maxIntensity);
float alpha = sourceRowVector4Span[x].W;
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, targetRowAreaVector4Span, targetRowAreaPixelSpan); targetRowVector4Span[x] = new Vector4(red, green, blue, alpha);
} }
} }
});
Buffer2D<TPixel>.SwapOrCopyContent(source.PixelBuffer, targetPixels); Span<TPixel> targetRowAreaPixelSpan = this.targetPixels.GetRowSpan(y).Slice(this.bounds.X, this.bounds.Width);
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, targetRowAreaVector4Span, targetRowAreaPixelSpan);
}
}
} }
} }
} }

30
src/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor.cs

@ -1,6 +1,9 @@
// Copyright (c) Six Labors and contributors. // Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Effects namespace SixLabors.ImageSharp.Processing.Processors.Effects
@ -34,6 +37,31 @@ namespace SixLabors.ImageSharp.Processing.Processors.Effects
/// <inheritdoc /> /// <inheritdoc />
public IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>(Configuration configuration, Image<TPixel> source, Rectangle sourceRectangle) public IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>(Configuration configuration, Image<TPixel> source, Rectangle sourceRectangle)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
=> new PixelRowDelegateProcessor<TPixel>(configuration, this, source, sourceRectangle); {
return new PixelRowDelegateProcessor<TPixel, PixelRowDelegate>(
new PixelRowDelegate(this.PixelRowOperation),
configuration,
this.Modifiers,
source,
sourceRectangle);
}
/// <summary>
/// A <see langword="struct"/> implementing the row processing logic for <see cref="PixelRowDelegateProcessor"/>.
/// </summary>
public readonly struct PixelRowDelegate : IPixelRowDelegate
{
private readonly PixelRowOperation pixelRowOperation;
[MethodImpl(InliningOptions.ShortMethod)]
public PixelRowDelegate(PixelRowOperation pixelRowOperation)
{
this.pixelRowOperation = pixelRowOperation;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(Span<Vector4> span, Point offset) => this.pixelRowOperation(span);
}
} }
} }

70
src/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessorBase{TPixel}.cs

@ -1,70 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Numerics;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Advanced.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Effects
{
/// <summary>
/// The base class for all processors that accept a user defined row processing delegate.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal abstract class PixelRowDelegateProcessorBase<TPixel> : ImageProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// The <see cref="PixelConversionModifiers"/> to apply during the pixel conversions.
/// </summary>
private readonly PixelConversionModifiers modifiers;
/// <summary>
/// Initializes a new instance of the <see cref="PixelRowDelegateProcessorBase{TPixel}"/> class.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="modifiers">The <see cref="PixelConversionModifiers"/> to apply during the pixel conversions.</param>
/// <param name="source">The source <see cref="Image{TPixel}"/> for the current processor instance.</param>
/// <param name="sourceRectangle">The source area to process for the current processor instance.</param>
protected PixelRowDelegateProcessorBase(Configuration configuration, PixelConversionModifiers modifiers, Image<TPixel> source, Rectangle sourceRectangle)
: base(configuration, source, sourceRectangle)
=> this.modifiers = modifiers;
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source)
{
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
int startX = interest.X;
Configuration configuration = this.Configuration;
PixelConversionModifiers modifiers = this.modifiers;
ParallelHelper.IterateRowsWithTempBuffer<Vector4>(
interest,
this.Configuration,
(rows, vectorBuffer) =>
{
for (int y = rows.Min; y < rows.Max; y++)
{
Span<Vector4> vectorSpan = vectorBuffer.Span;
int length = vectorSpan.Length;
Span<TPixel> rowSpan = source.GetPixelRowSpan(y).Slice(startX, length);
PixelOperations<TPixel>.Instance.ToVector4(configuration, rowSpan, vectorSpan, modifiers);
// Run the user defined pixel shader to the current row of pixels
this.ApplyPixelRowDelegate(vectorSpan, new Point(startX, y));
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, vectorSpan, rowSpan, modifiers);
}
});
}
/// <summary>
/// Applies the current pixel row delegate to a target row of preprocessed pixels.
/// </summary>
/// <param name="span">The target row of <see cref="Vector4"/> pixels to process.</param>
/// <param name="offset">The initial horizontal and vertical offset for the input pixels to process.</param>
protected abstract void ApplyPixelRowDelegate(Span<Vector4> span, Point offset);
}
}

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

@ -0,0 +1,104 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Effects
{
/// <summary>
/// The base class for all processors that accept a user defined row processing delegate.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <typeparam name="TDelegate">The row processor type.</typeparam>
internal sealed class PixelRowDelegateProcessor<TPixel, TDelegate> : ImageProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
where TDelegate : struct, IPixelRowDelegate
{
private readonly TDelegate rowDelegate;
/// <summary>
/// The <see cref="PixelConversionModifiers"/> to apply during the pixel conversions.
/// </summary>
private readonly PixelConversionModifiers modifiers;
/// <summary>
/// Initializes a new instance of the <see cref="PixelRowDelegateProcessor{TPixel,TDelegate}"/> class.
/// </summary>
/// <param name="rowDelegate">The row processor to use to process each pixel row</param>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="modifiers">The <see cref="PixelConversionModifiers"/> to apply during the pixel conversions.</param>
/// <param name="source">The source <see cref="Image{TPixel}"/> for the current processor instance.</param>
/// <param name="sourceRectangle">The source area to process for the current processor instance.</param>
public PixelRowDelegateProcessor(
in TDelegate rowDelegate,
Configuration configuration,
PixelConversionModifiers modifiers,
Image<TPixel> source,
Rectangle sourceRectangle)
: base(configuration, source, sourceRectangle)
{
this.rowDelegate = rowDelegate;
this.modifiers = modifiers;
}
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source)
{
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
var operation = new RowIntervalOperation(interest.X, source, this.Configuration, this.modifiers, this.rowDelegate);
ParallelRowIterator.IterateRows<RowIntervalOperation, Vector4>(
this.Configuration,
interest,
in operation);
}
/// <summary>
/// A <see langword="struct"/> implementing the convolution logic for <see cref="PixelRowDelegateProcessor{TPixel,TDelegate}"/>.
/// </summary>
private readonly struct RowIntervalOperation : IRowIntervalOperation<Vector4>
{
private readonly int startX;
private readonly ImageFrame<TPixel> source;
private readonly Configuration configuration;
private readonly PixelConversionModifiers modifiers;
private readonly TDelegate rowProcessor;
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(
int startX,
ImageFrame<TPixel> source,
Configuration configuration,
PixelConversionModifiers modifiers,
in TDelegate rowProcessor)
{
this.startX = startX;
this.source = source;
this.configuration = configuration;
this.modifiers = modifiers;
this.rowProcessor = rowProcessor;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows, Span<Vector4> span)
{
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> rowSpan = this.source.GetPixelRowSpan(y).Slice(this.startX, span.Length);
PixelOperations<TPixel>.Instance.ToVector4(this.configuration, rowSpan, span, this.modifiers);
// Run the user defined pixel shader to the current row of pixels
Unsafe.AsRef(this.rowProcessor).Invoke(span, new Point(this.startX, y));
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, span, rowSpan, this.modifiers);
}
}
}
}
}

39
src/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor{TPixel}.cs

@ -1,39 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Effects
{
/// <summary>
/// Applies a user defined row processing delegate to the image.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal sealed class PixelRowDelegateProcessor<TPixel> : PixelRowDelegateProcessorBase<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// The user defined pixel row processing delegate.
/// </summary>
private readonly PixelRowOperation pixelRowOperation;
/// <summary>
/// Initializes a new instance of the <see cref="PixelRowDelegateProcessor{TPixel}"/> class.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="definition">The <see cref="PixelRowDelegateProcessor"/> defining the processor parameters.</param>
/// <param name="source">The source <see cref="Image{TPixel}"/> for the current processor instance.</param>
/// <param name="sourceRectangle">The source area to process for the current processor instance.</param>
public PixelRowDelegateProcessor(Configuration configuration, PixelRowDelegateProcessor definition, Image<TPixel> source, Rectangle sourceRectangle)
: base(configuration, definition.Modifiers, source, sourceRectangle)
{
this.pixelRowOperation = definition.PixelRowOperation;
}
/// <inheritdoc/>
protected override void ApplyPixelRowDelegate(Span<Vector4> span, Point offset) => this.pixelRowOperation(span);
}
}

117
src/ImageSharp/Processing/Processors/Effects/PixelateProcessor{TPixel}.cs

@ -3,10 +3,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Common;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Effects namespace SixLabors.ImageSharp.Processing.Processors.Effects
@ -29,86 +28,76 @@ namespace SixLabors.ImageSharp.Processing.Processors.Effects
/// <param name="sourceRectangle">The source area to process for the current processor instance.</param> /// <param name="sourceRectangle">The source area to process for the current processor instance.</param>
public PixelateProcessor(Configuration configuration, PixelateProcessor definition, Image<TPixel> source, Rectangle sourceRectangle) public PixelateProcessor(Configuration configuration, PixelateProcessor definition, Image<TPixel> source, Rectangle sourceRectangle)
: base(configuration, source, sourceRectangle) : base(configuration, source, sourceRectangle)
{ => this.definition = definition;
this.definition = definition;
}
private int Size => this.definition.Size; private int Size => this.definition.Size;
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source) protected override void OnFrameApply(ImageFrame<TPixel> source)
{ {
if (this.Size <= 0 || this.Size > source.Height || this.Size > source.Width) var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
{
throw new ArgumentOutOfRangeException(nameof(this.Size));
}
int startY = this.SourceRectangle.Y;
int endY = this.SourceRectangle.Bottom;
int startX = this.SourceRectangle.X;
int endX = this.SourceRectangle.Right;
int size = this.Size; int size = this.Size;
int offset = this.Size / 2;
// Align start/end positions. Guard.MustBeBetweenOrEqualTo(size, 0, interest.Width, nameof(size));
int minX = Math.Max(0, startX); Guard.MustBeBetweenOrEqualTo(size, 0, interest.Height, nameof(size));
int maxX = Math.Min(source.Width, endX);
int minY = Math.Max(0, startY);
int maxY = Math.Min(source.Height, endY);
// Reset offset if necessary. // Get the range on the y-plane to choose from.
if (minX > 0) // TODO: It would be nice to be able to pool this somehow but neither Memory<T> nor Span<T>
// implement IEnumerable<T>.
IEnumerable<int> range = EnumerableExtensions.SteppedRange(interest.Y, i => i < interest.Bottom, size);
Parallel.ForEach(
range,
this.Configuration.GetParallelOptions(),
new RowOperation(interest, size, source).Invoke);
}
private readonly struct RowOperation
{
private readonly int minX;
private readonly int maxX;
private readonly int maxXIndex;
private readonly int maxY;
private readonly int maxYIndex;
private readonly int size;
private readonly int radius;
private readonly ImageFrame<TPixel> source;
[MethodImpl(InliningOptions.ShortMethod)]
public RowOperation(
Rectangle bounds,
int size,
ImageFrame<TPixel> source)
{ {
startX = 0; this.minX = bounds.X;
this.maxX = bounds.Right;
this.maxXIndex = bounds.Right - 1;
this.maxY = bounds.Bottom;
this.maxYIndex = bounds.Bottom - 1;
this.size = size;
this.radius = size >> 1;
this.source = source;
} }
if (minY > 0) [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y)
{ {
startY = 0; Span<TPixel> rowSpan = this.source.GetPixelRowSpan(Math.Min(y + this.radius, this.maxYIndex));
}
// Get the range on the y-plane to choose from. for (int x = this.minX; x < this.maxX; x += this.size)
IEnumerable<int> range = EnumerableExtensions.SteppedRange(minY, i => i < maxY, size); {
// Get the pixel color in the centre of the soon to be pixelated area.
TPixel pixel = rowSpan[Math.Min(x + this.radius, this.maxXIndex)];
Parallel.ForEach( // For each pixel in the pixelate size, set it to the centre color.
range, for (int oY = y; oY < y + this.size && oY < this.maxY; oY++)
this.Configuration.GetParallelOptions(),
y =>
{ {
int offsetY = y - startY; for (int oX = x; oX < x + this.size && oX < this.maxX; oX++)
int offsetPy = offset;
// Make sure that the offset is within the boundary of the image.
while (offsetY + offsetPy >= maxY)
{ {
offsetPy--; this.source[oX, oY] = pixel;
} }
}
Span<TPixel> row = source.GetPixelRowSpan(offsetY + offsetPy); }
}
for (int x = minX; x < maxX; x += size)
{
int offsetX = x - startX;
int offsetPx = offset;
while (x + offsetPx >= maxX)
{
offsetPx--;
}
// Get the pixel color in the centre of the soon to be pixelated area.
TPixel pixel = row[offsetX + offsetPx];
// For each pixel in the pixelate size, set it to the centre color.
for (int l = offsetY; l < offsetY + size && l < maxY; l++)
{
for (int k = offsetX; k < offsetX + size && k < maxX; k++)
{
source[k, l] = pixel;
}
}
}
});
} }
} }
} }

30
src/ImageSharp/Processing/Processors/Effects/PositionAwarePixelRowDelegateProcessor.cs

@ -1,6 +1,9 @@
// Copyright (c) Six Labors and contributors. // Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Effects namespace SixLabors.ImageSharp.Processing.Processors.Effects
@ -34,6 +37,31 @@ namespace SixLabors.ImageSharp.Processing.Processors.Effects
/// <inheritdoc /> /// <inheritdoc />
public IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>(Configuration configuration, Image<TPixel> source, Rectangle sourceRectangle) public IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>(Configuration configuration, Image<TPixel> source, Rectangle sourceRectangle)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
=> new PositionAwarePixelRowDelegateProcessor<TPixel>(configuration, this, source, sourceRectangle); {
return new PixelRowDelegateProcessor<TPixel, PixelRowDelegate>(
new PixelRowDelegate(this.PixelRowOperation),
configuration,
this.Modifiers,
source,
sourceRectangle);
}
/// <summary>
/// A <see langword="struct"/> implementing the row processing logic for <see cref="PositionAwarePixelRowDelegateProcessor"/>.
/// </summary>
public readonly struct PixelRowDelegate : IPixelRowDelegate
{
private readonly PixelRowOperation<Point> pixelRowOperation;
[MethodImpl(InliningOptions.ShortMethod)]
public PixelRowDelegate(PixelRowOperation<Point> pixelRowOperation)
{
this.pixelRowOperation = pixelRowOperation;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(Span<Vector4> span, Point offset) => this.pixelRowOperation(span, offset);
}
} }
} }

36
src/ImageSharp/Processing/Processors/Effects/PositionAwarePixelRowDelegateProcessor{TPixel}.cs

@ -1,36 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Effects
{
/// <summary>
/// Applies a user defined, position aware, row processing delegate to the image.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal sealed class PositionAwarePixelRowDelegateProcessor<TPixel> : PixelRowDelegateProcessorBase<TPixel>
where TPixel : struct, IPixel<TPixel>
{
private readonly PixelRowOperation<Point> pixelRowOperation;
/// <summary>
/// Initializes a new instance of the <see cref="PositionAwarePixelRowDelegateProcessor{TPixel}"/> class.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="definition">The <see cref="PositionAwarePixelRowDelegateProcessor"/> defining the processor parameters.</param>
/// <param name="source">The source <see cref="Image{TPixel}"/> for the current processor instance.</param>
/// <param name="sourceRectangle">The source area to process for the current processor instance.</param>
public PositionAwarePixelRowDelegateProcessor(Configuration configuration, PositionAwarePixelRowDelegateProcessor definition, Image<TPixel> source, Rectangle sourceRectangle)
: base(configuration, definition.Modifiers, source, sourceRectangle)
{
this.pixelRowOperation = definition.PixelRowOperation;
}
/// <inheritdoc/>
protected override void ApplyPixelRowDelegate(Span<Vector4> span, Point offset) => this.pixelRowOperation(span, offset);
}
}

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

@ -3,8 +3,9 @@
using System; using System;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Advanced.ParallelUtils; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Filters namespace SixLabors.ImageSharp.Processing.Processors.Filters
@ -35,27 +36,51 @@ namespace SixLabors.ImageSharp.Processing.Processors.Filters
protected override void OnFrameApply(ImageFrame<TPixel> source) protected override void OnFrameApply(ImageFrame<TPixel> source)
{ {
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
int startX = interest.X; var operation = new RowIntervalOperation(interest.X, source, this.definition.Matrix, this.Configuration);
ColorMatrix matrix = this.definition.Matrix; ParallelRowIterator.IterateRows<RowIntervalOperation, Vector4>(
ParallelHelper.IterateRowsWithTempBuffer<Vector4>(
interest,
this.Configuration, this.Configuration,
(rows, vectorBuffer) => interest,
{ in operation);
for (int y = rows.Min; y < rows.Max; y++) }
{
Span<Vector4> vectorSpan = vectorBuffer.Span; /// <summary>
int length = vectorSpan.Length; /// A <see langword="struct"/> implementing the convolution logic for <see cref="FilterProcessor{TPixel}"/>.
Span<TPixel> rowSpan = source.GetPixelRowSpan(y).Slice(startX, length); /// </summary>
PixelOperations<TPixel>.Instance.ToVector4(this.Configuration, rowSpan, vectorSpan); private readonly struct RowIntervalOperation : IRowIntervalOperation<Vector4>
{
Vector4Utils.Transform(vectorSpan, ref matrix); private readonly int startX;
private readonly ImageFrame<TPixel> source;
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.Configuration, vectorSpan, rowSpan); private readonly ColorMatrix matrix;
} private readonly Configuration configuration;
});
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(
int startX,
ImageFrame<TPixel> source,
ColorMatrix matrix,
Configuration configuration)
{
this.startX = startX;
this.source = source;
this.matrix = matrix;
this.configuration = configuration;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows, Span<Vector4> span)
{
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> rowSpan = this.source.GetPixelRowSpan(y).Slice(this.startX, span.Length);
PixelOperations<TPixel>.Instance.ToVector4(this.configuration, rowSpan, span);
Vector4Utils.Transform(span, ref Unsafe.AsRef(this.matrix));
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, span, rowSpan);
}
}
} }
} }
} }

302
src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs

@ -7,8 +7,7 @@ using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -81,56 +80,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
yStart += tileHeight; yStart += tileHeight;
} }
Parallel.For( var operation = new RowIntervalOperation(cdfData, tileYStartPositions, tileWidth, tileHeight, tileCount, halfTileWidth, luminanceLevels, source);
0, ParallelRowIterator.IterateRows(
tileYStartPositions.Count, this.Configuration,
new ParallelOptions { MaxDegreeOfParallelism = this.Configuration.MaxDegreeOfParallelism }, new Rectangle(0, 0, sourceWidth, tileYStartPositions.Count),
index => in operation);
{
int y = tileYStartPositions[index].y;
int cdfYY = tileYStartPositions[index].cdfY;
// It's unfortunate that we have to do this per iteration.
ref TPixel sourceBase = ref source.GetPixelReference(0, 0);
int cdfX = 0;
int x = halfTileWidth;
for (int tile = 0; tile < tileCount - 1; tile++)
{
int tileY = 0;
int yEnd = Math.Min(y + tileHeight, sourceHeight);
int xEnd = Math.Min(x + tileWidth, sourceWidth);
for (int dy = y; dy < yEnd; dy++)
{
int dyOffSet = dy * sourceWidth;
int tileX = 0;
for (int dx = x; dx < xEnd; dx++)
{
ref TPixel pixel = ref Unsafe.Add(ref sourceBase, dyOffSet + dx);
float luminanceEqualized = InterpolateBetweenFourTiles(
pixel,
cdfData,
tileCount,
tileCount,
tileX,
tileY,
cdfX,
cdfYY,
tileWidth,
tileHeight,
luminanceLevels);
pixel.FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W));
tileX++;
}
tileY++;
}
cdfX++;
x += tileWidth;
}
});
ref TPixel pixelsBase = ref source.GetPixelReference(0, 0); ref TPixel pixelsBase = ref source.GetPixelReference(0, 0);
@ -416,6 +370,95 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
private static float LinearInterpolation(float left, float right, float t) private static float LinearInterpolation(float left, float right, float t)
=> left + ((right - left) * t); => left + ((right - left) * t);
private readonly struct RowIntervalOperation : IRowIntervalOperation
{
private readonly CdfTileData cdfData;
private readonly List<(int y, int cdfY)> tileYStartPositions;
private readonly int tileWidth;
private readonly int tileHeight;
private readonly int tileCount;
private readonly int halfTileWidth;
private readonly int luminanceLevels;
private readonly ImageFrame<TPixel> source;
private readonly int sourceWidth;
private readonly int sourceHeight;
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(
CdfTileData cdfData,
List<(int y, int cdfY)> tileYStartPositions,
int tileWidth,
int tileHeight,
int tileCount,
int halfTileWidth,
int luminanceLevels,
ImageFrame<TPixel> source)
{
this.cdfData = cdfData;
this.tileYStartPositions = tileYStartPositions;
this.tileWidth = tileWidth;
this.tileHeight = tileHeight;
this.tileCount = tileCount;
this.halfTileWidth = halfTileWidth;
this.luminanceLevels = luminanceLevels;
this.source = source;
this.sourceWidth = source.Width;
this.sourceHeight = source.Height;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
{
ref TPixel sourceBase = ref this.source.GetPixelReference(0, 0);
for (int index = rows.Min; index < rows.Max; index++)
{
(int y, int cdfY) tileYStartPosition = this.tileYStartPositions[index];
int y = tileYStartPosition.y;
int cdfYY = tileYStartPosition.cdfY;
int cdfX = 0;
int x = this.halfTileWidth;
for (int tile = 0; tile < this.tileCount - 1; tile++)
{
int tileY = 0;
int yEnd = Math.Min(y + this.tileHeight, this.sourceHeight);
int xEnd = Math.Min(x + this.tileWidth, this.sourceWidth);
for (int dy = y; dy < yEnd; dy++)
{
int dyOffSet = dy * this.sourceWidth;
int tileX = 0;
for (int dx = x; dx < xEnd; dx++)
{
ref TPixel pixel = ref Unsafe.Add(ref sourceBase, dyOffSet + dx);
float luminanceEqualized = InterpolateBetweenFourTiles(
pixel,
this.cdfData,
this.tileCount,
this.tileCount,
tileX,
tileY,
cdfX,
cdfYY,
this.tileWidth,
this.tileHeight,
this.luminanceLevels);
pixel.FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W));
tileX++;
}
tileY++;
}
cdfX++;
x += this.tileWidth;
}
}
}
}
/// <summary> /// <summary>
/// Contains the results of the cumulative distribution function for all tiles. /// Contains the results of the cumulative distribution function for all tiles.
/// </summary> /// </summary>
@ -431,7 +474,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
private readonly Buffer2D<int> cdfLutBuffer2D; private readonly Buffer2D<int> cdfLutBuffer2D;
private readonly int pixelsInTile; private readonly int pixelsInTile;
private readonly int sourceWidth; private readonly int sourceWidth;
private readonly int sourceHeight;
private readonly int tileWidth; private readonly int tileWidth;
private readonly int tileHeight; private readonly int tileHeight;
private readonly int luminanceLevels; private readonly int luminanceLevels;
@ -453,7 +495,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
this.cdfMinBuffer2D = this.memoryAllocator.Allocate2D<int>(tileCountX, tileCountY); this.cdfMinBuffer2D = this.memoryAllocator.Allocate2D<int>(tileCountX, tileCountY);
this.cdfLutBuffer2D = this.memoryAllocator.Allocate2D<int>(tileCountX * luminanceLevels, tileCountY); this.cdfLutBuffer2D = this.memoryAllocator.Allocate2D<int>(tileCountX * luminanceLevels, tileCountY);
this.sourceWidth = sourceWidth; this.sourceWidth = sourceWidth;
this.sourceHeight = sourceHeight;
this.tileWidth = tileWidth; this.tileWidth = tileWidth;
this.tileHeight = tileHeight; this.tileHeight = tileHeight;
this.pixelsInTile = tileWidth * tileHeight; this.pixelsInTile = tileWidth * tileHeight;
@ -470,57 +511,21 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
public void CalculateLookupTables(ImageFrame<TPixel> source, HistogramEqualizationProcessor<TPixel> processor) public void CalculateLookupTables(ImageFrame<TPixel> source, HistogramEqualizationProcessor<TPixel> processor)
{ {
int sourceWidth = this.sourceWidth; var operation = new RowIntervalOperation(
int sourceHeight = this.sourceHeight; processor,
int tileWidth = this.tileWidth; this.memoryAllocator,
int tileHeight = this.tileHeight; this.cdfMinBuffer2D,
int luminanceLevels = this.luminanceLevels; this.cdfLutBuffer2D,
this.tileYStartPositions,
Parallel.For( this.tileWidth,
0, this.tileHeight,
this.tileYStartPositions.Count, this.luminanceLevels,
new ParallelOptions { MaxDegreeOfParallelism = this.configuration.MaxDegreeOfParallelism }, source);
index =>
{ ParallelRowIterator.IterateRows(
int cdfX = 0; this.configuration,
int cdfY = this.tileYStartPositions[index].cdfY; new Rectangle(0, 0, this.sourceWidth, this.tileYStartPositions.Count),
int y = this.tileYStartPositions[index].y; in operation);
int endY = Math.Min(y + tileHeight, sourceHeight);
ref TPixel sourceBase = ref source.GetPixelReference(0, 0);
ref int cdfMinBase = ref MemoryMarshal.GetReference(this.cdfMinBuffer2D.GetRowSpan(cdfY));
using (IMemoryOwner<int> histogramBuffer = this.memoryAllocator.Allocate<int>(luminanceLevels))
{
Span<int> histogram = histogramBuffer.GetSpan();
ref int histogramBase = ref MemoryMarshal.GetReference(histogram);
for (int x = 0; x < sourceWidth; x += tileWidth)
{
histogram.Clear();
ref int cdfBase = ref MemoryMarshal.GetReference(this.GetCdfLutSpan(cdfX, index));
int xlimit = Math.Min(x + tileWidth, sourceWidth);
for (int dy = y; dy < endY; dy++)
{
int dyOffset = dy * sourceWidth;
for (int dx = x; dx < xlimit; dx++)
{
int luminance = GetLuminance(Unsafe.Add(ref sourceBase, dyOffset + dx), luminanceLevels);
histogram[luminance]++;
}
}
if (processor.ClipHistogramEnabled)
{
processor.ClipHistogram(histogram, processor.ClipLimit);
}
Unsafe.Add(ref cdfMinBase, cdfX) = processor.CalculateCdf(ref cdfBase, ref histogramBase, histogram.Length - 1);
cdfX++;
}
}
});
} }
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
@ -548,6 +553,93 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
this.cdfMinBuffer2D.Dispose(); this.cdfMinBuffer2D.Dispose();
this.cdfLutBuffer2D.Dispose(); this.cdfLutBuffer2D.Dispose();
} }
private readonly struct RowIntervalOperation : IRowIntervalOperation
{
private readonly HistogramEqualizationProcessor<TPixel> processor;
private readonly MemoryAllocator allocator;
private readonly Buffer2D<int> cdfMinBuffer2D;
private readonly Buffer2D<int> cdfLutBuffer2D;
private readonly List<(int y, int cdfY)> tileYStartPositions;
private readonly int tileWidth;
private readonly int tileHeight;
private readonly int luminanceLevels;
private readonly ImageFrame<TPixel> source;
private readonly int sourceWidth;
private readonly int sourceHeight;
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(
HistogramEqualizationProcessor<TPixel> processor,
MemoryAllocator allocator,
Buffer2D<int> cdfMinBuffer2D,
Buffer2D<int> cdfLutBuffer2D,
List<(int y, int cdfY)> tileYStartPositions,
int tileWidth,
int tileHeight,
int luminanceLevels,
ImageFrame<TPixel> source)
{
this.processor = processor;
this.allocator = allocator;
this.cdfMinBuffer2D = cdfMinBuffer2D;
this.cdfLutBuffer2D = cdfLutBuffer2D;
this.tileYStartPositions = tileYStartPositions;
this.tileWidth = tileWidth;
this.tileHeight = tileHeight;
this.luminanceLevels = luminanceLevels;
this.source = source;
this.sourceWidth = source.Width;
this.sourceHeight = source.Height;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
{
ref TPixel sourceBase = ref this.source.GetPixelReference(0, 0);
for (int index = rows.Min; index < rows.Max; index++)
{
int cdfX = 0;
int cdfY = this.tileYStartPositions[index].cdfY;
int y = this.tileYStartPositions[index].y;
int endY = Math.Min(y + this.tileHeight, this.sourceHeight);
ref int cdfMinBase = ref MemoryMarshal.GetReference(this.cdfMinBuffer2D.GetRowSpan(cdfY));
using IMemoryOwner<int> histogramBuffer = this.allocator.Allocate<int>(this.luminanceLevels);
Span<int> histogram = histogramBuffer.GetSpan();
ref int histogramBase = ref MemoryMarshal.GetReference(histogram);
for (int x = 0; x < this.sourceWidth; x += this.tileWidth)
{
histogram.Clear();
Span<int> cdfLutSpan = this.cdfLutBuffer2D.GetRowSpan(index).Slice(cdfX * this.luminanceLevels, this.luminanceLevels);
ref int cdfBase = ref MemoryMarshal.GetReference(cdfLutSpan);
int xlimit = Math.Min(x + this.tileWidth, this.sourceWidth);
for (int dy = y; dy < endY; dy++)
{
int dyOffset = dy * this.sourceWidth;
for (int dx = x; dx < xlimit; dx++)
{
int luminance = GetLuminance(Unsafe.Add(ref sourceBase, dyOffset + dx), this.luminanceLevels);
histogram[luminance]++;
}
}
if (this.processor.ClipHistogramEnabled)
{
this.processor.ClipHistogram(histogram, this.processor.ClipLimit);
}
Unsafe.Add(ref cdfMinBase, cdfX) = this.processor.CalculateCdf(ref cdfBase, ref histogramBase, histogram.Length - 1);
cdfX++;
}
}
}
}
} }
} }
} }

425
src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationSlidingWindowProcessor{TPixel}.cs

@ -66,191 +66,101 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
int halfTileWidth = halfTileHeight; int halfTileWidth = halfTileHeight;
var slidingWindowInfos = new SlidingWindowInfos(tileWidth, tileHeight, halfTileWidth, halfTileHeight, pixelInTile); var slidingWindowInfos = new SlidingWindowInfos(tileWidth, tileHeight, halfTileWidth, halfTileHeight, pixelInTile);
using (Buffer2D<TPixel> targetPixels = this.Configuration.MemoryAllocator.Allocate2D<TPixel>(source.Width, source.Height)) // TODO: If the process was able to be switched to operate in parallel rows instead of columns
{ // then we could take advantage of batching and allocate per-row buffers only once per batch.
// Process the inner tiles, which do not require to check the borders. using Buffer2D<TPixel> targetPixels = this.Configuration.MemoryAllocator.Allocate2D<TPixel>(source.Width, source.Height);
Parallel.For(
halfTileWidth, // Process the inner tiles, which do not require to check the borders.
source.Width - halfTileWidth, var innerOperation = new SlidingWindowOperation(
parallelOptions, this.Configuration,
this.ProcessSlidingWindow( this,
source, source,
memoryAllocator, memoryAllocator,
targetPixels, targetPixels,
slidingWindowInfos, slidingWindowInfos,
yStart: halfTileHeight, yStart: halfTileHeight,
yEnd: source.Height - halfTileHeight, yEnd: source.Height - halfTileHeight,
useFastPath: true, useFastPath: true);
this.Configuration));
Parallel.For(
// Process the left border of the image. halfTileWidth,
Parallel.For( source.Width - halfTileWidth,
0, parallelOptions,
halfTileWidth, innerOperation.Invoke);
parallelOptions,
this.ProcessSlidingWindow( // Process the left border of the image.
source, var leftBorderOperation = new SlidingWindowOperation(
memoryAllocator, this.Configuration,
targetPixels, this,
slidingWindowInfos, source,
yStart: 0, memoryAllocator,
yEnd: source.Height, targetPixels,
useFastPath: false, slidingWindowInfos,
this.Configuration)); yStart: 0,
yEnd: source.Height,
// Process the right border of the image. useFastPath: false);
Parallel.For(
source.Width - halfTileWidth, Parallel.For(
source.Width, 0,
parallelOptions, halfTileWidth,
this.ProcessSlidingWindow( parallelOptions,
source, leftBorderOperation.Invoke);
memoryAllocator,
targetPixels, // Process the right border of the image.
slidingWindowInfos, var rightBorderOperation = new SlidingWindowOperation(
yStart: 0, this.Configuration,
yEnd: source.Height, this,
useFastPath: false, source,
this.Configuration)); memoryAllocator,
targetPixels,
// Process the top border of the image. slidingWindowInfos,
Parallel.For( yStart: 0,
halfTileWidth, yEnd: source.Height,
source.Width - halfTileWidth, useFastPath: false);
parallelOptions,
this.ProcessSlidingWindow( Parallel.For(
source, source.Width - halfTileWidth,
memoryAllocator, source.Width,
targetPixels, parallelOptions,
slidingWindowInfos, rightBorderOperation.Invoke);
yStart: 0,
yEnd: halfTileHeight, // Process the top border of the image.
useFastPath: false, var topBorderOperation = new SlidingWindowOperation(
this.Configuration)); this.Configuration,
this,
// Process the bottom border of the image. source,
Parallel.For( memoryAllocator,
halfTileWidth, targetPixels,
source.Width - halfTileWidth, slidingWindowInfos,
parallelOptions, yStart: 0,
this.ProcessSlidingWindow( yEnd: halfTileHeight,
source, useFastPath: false);
memoryAllocator,
targetPixels, Parallel.For(
slidingWindowInfos, halfTileWidth,
yStart: source.Height - halfTileHeight, source.Width - halfTileWidth,
yEnd: source.Height, parallelOptions,
useFastPath: false, topBorderOperation.Invoke);
this.Configuration));
// Process the bottom border of the image.
Buffer2D<TPixel>.SwapOrCopyContent(source.PixelBuffer, targetPixels); var bottomBorderOperation = new SlidingWindowOperation(
} this.Configuration,
} this,
source,
/// <summary> memoryAllocator,
/// Applies the sliding window equalization to one column of the image. The window is moved from top to bottom. targetPixels,
/// Moving the window one pixel down requires to remove one row from the top of the window from the histogram and slidingWindowInfos,
/// adding a new row at the bottom. yStart: source.Height - halfTileHeight,
/// </summary> yEnd: source.Height,
/// <param name="source">The source image.</param> useFastPath: false);
/// <param name="memoryAllocator">The memory allocator.</param>
/// <param name="targetPixels">The target pixels.</param> Parallel.For(
/// <param name="swInfos"><see cref="SlidingWindowInfos"/> about the sliding window dimensions.</param> halfTileWidth,
/// <param name="yStart">The y start position.</param> source.Width - halfTileWidth,
/// <param name="yEnd">The y end position.</param> parallelOptions,
/// <param name="useFastPath">if set to true the borders of the image will not be checked.</param> bottomBorderOperation.Invoke);
/// <param name="configuration">The configuration.</param>
/// <returns>Action Delegate.</returns> Buffer2D<TPixel>.SwapOrCopyContent(source.PixelBuffer, targetPixels);
private Action<int> ProcessSlidingWindow(
ImageFrame<TPixel> source,
MemoryAllocator memoryAllocator,
Buffer2D<TPixel> targetPixels,
SlidingWindowInfos swInfos,
int yStart,
int yEnd,
bool useFastPath,
Configuration configuration)
{
return x =>
{
using (IMemoryOwner<int> histogramBuffer = memoryAllocator.Allocate<int>(this.LuminanceLevels, AllocationOptions.Clean))
using (IMemoryOwner<int> histogramBufferCopy = memoryAllocator.Allocate<int>(this.LuminanceLevels, AllocationOptions.Clean))
using (IMemoryOwner<int> cdfBuffer = memoryAllocator.Allocate<int>(this.LuminanceLevels, AllocationOptions.Clean))
using (IMemoryOwner<Vector4> pixelRowBuffer = memoryAllocator.Allocate<Vector4>(swInfos.TileWidth, AllocationOptions.Clean))
{
Span<int> histogram = histogramBuffer.GetSpan();
ref int histogramBase = ref MemoryMarshal.GetReference(histogram);
Span<int> histogramCopy = histogramBufferCopy.GetSpan();
ref int histogramCopyBase = ref MemoryMarshal.GetReference(histogramCopy);
ref int cdfBase = ref MemoryMarshal.GetReference(cdfBuffer.GetSpan());
Span<Vector4> pixelRow = pixelRowBuffer.GetSpan();
ref Vector4 pixelRowBase = ref MemoryMarshal.GetReference(pixelRow);
// Build the initial histogram of grayscale values.
for (int dy = yStart - swInfos.HalfTileHeight; dy < yStart + swInfos.HalfTileHeight; dy++)
{
if (useFastPath)
{
this.CopyPixelRowFast(source, pixelRow, x - swInfos.HalfTileWidth, dy, swInfos.TileWidth, configuration);
}
else
{
this.CopyPixelRow(source, pixelRow, x - swInfos.HalfTileWidth, dy, swInfos.TileWidth, configuration);
}
this.AddPixelsToHistogram(ref pixelRowBase, ref histogramBase, this.LuminanceLevels, pixelRow.Length);
}
for (int y = yStart; y < yEnd; y++)
{
if (this.ClipHistogramEnabled)
{
// Clipping the histogram, but doing it on a copy to keep the original un-clipped values for the next iteration.
histogram.CopyTo(histogramCopy);
this.ClipHistogram(histogramCopy, this.ClipLimit);
}
// Calculate the cumulative distribution function, which will map each input pixel in the current tile to a new value.
int cdfMin = this.ClipHistogramEnabled
? this.CalculateCdf(ref cdfBase, ref histogramCopyBase, histogram.Length - 1)
: this.CalculateCdf(ref cdfBase, ref histogramBase, histogram.Length - 1);
float numberOfPixelsMinusCdfMin = swInfos.PixelInTile - cdfMin;
// Map the current pixel to the new equalized value.
int luminance = GetLuminance(source[x, y], this.LuminanceLevels);
float luminanceEqualized = Unsafe.Add(ref cdfBase, luminance) / numberOfPixelsMinusCdfMin;
targetPixels[x, y].FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, source[x, y].ToVector4().W));
// Remove top most row from the histogram, mirroring rows which exceeds the borders.
if (useFastPath)
{
this.CopyPixelRowFast(source, pixelRow, x - swInfos.HalfTileWidth, y - swInfos.HalfTileWidth, swInfos.TileWidth, configuration);
}
else
{
this.CopyPixelRow(source, pixelRow, x - swInfos.HalfTileWidth, y - swInfos.HalfTileWidth, swInfos.TileWidth, configuration);
}
this.RemovePixelsFromHistogram(ref pixelRowBase, ref histogramBase, this.LuminanceLevels, pixelRow.Length);
// Add new bottom row to the histogram, mirroring rows which exceeds the borders.
if (useFastPath)
{
this.CopyPixelRowFast(source, pixelRow, x - swInfos.HalfTileWidth, y + swInfos.HalfTileWidth, swInfos.TileWidth, configuration);
}
else
{
this.CopyPixelRow(source, pixelRow, x - swInfos.HalfTileWidth, y + swInfos.HalfTileWidth, swInfos.TileWidth, configuration);
}
this.AddPixelsToHistogram(ref pixelRowBase, ref histogramBase, this.LuminanceLevels, pixelRow.Length);
}
}
};
} }
/// <summary> /// <summary>
@ -371,6 +281,141 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
} }
} }
/// <summary>
/// Applies the sliding window equalization to one column of the image. The window is moved from top to bottom.
/// Moving the window one pixel down requires to remove one row from the top of the window from the histogram and
/// adding a new row at the bottom.
/// </summary>
private readonly struct SlidingWindowOperation
{
private readonly Configuration configuration;
private readonly AdaptiveHistogramEqualizationSlidingWindowProcessor<TPixel> processor;
private readonly ImageFrame<TPixel> source;
private readonly MemoryAllocator memoryAllocator;
private readonly Buffer2D<TPixel> targetPixels;
private readonly SlidingWindowInfos swInfos;
private readonly int yStart;
private readonly int yEnd;
private readonly bool useFastPath;
/// <summary>
/// Initializes a new instance of the <see cref="SlidingWindowOperation"/> struct.
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="processor">The histogram processor.</param>
/// <param name="source">The source image.</param>
/// <param name="memoryAllocator">The memory allocator.</param>
/// <param name="targetPixels">The target pixels.</param>
/// <param name="swInfos"><see cref="SlidingWindowInfos"/> about the sliding window dimensions.</param>
/// <param name="yStart">The y start position.</param>
/// <param name="yEnd">The y end position.</param>
/// <param name="useFastPath">if set to true the borders of the image will not be checked.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public SlidingWindowOperation(
Configuration configuration,
AdaptiveHistogramEqualizationSlidingWindowProcessor<TPixel> processor,
ImageFrame<TPixel> source,
MemoryAllocator memoryAllocator,
Buffer2D<TPixel> targetPixels,
SlidingWindowInfos swInfos,
int yStart,
int yEnd,
bool useFastPath)
{
this.configuration = configuration;
this.processor = processor;
this.source = source;
this.memoryAllocator = memoryAllocator;
this.targetPixels = targetPixels;
this.swInfos = swInfos;
this.yStart = yStart;
this.yEnd = yEnd;
this.useFastPath = useFastPath;
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int x)
{
using (IMemoryOwner<int> histogramBuffer = this.memoryAllocator.Allocate<int>(this.processor.LuminanceLevels, AllocationOptions.Clean))
using (IMemoryOwner<int> histogramBufferCopy = this.memoryAllocator.Allocate<int>(this.processor.LuminanceLevels, AllocationOptions.Clean))
using (IMemoryOwner<int> cdfBuffer = this.memoryAllocator.Allocate<int>(this.processor.LuminanceLevels, AllocationOptions.Clean))
using (IMemoryOwner<Vector4> pixelRowBuffer = this.memoryAllocator.Allocate<Vector4>(this.swInfos.TileWidth, AllocationOptions.Clean))
{
Span<int> histogram = histogramBuffer.GetSpan();
ref int histogramBase = ref MemoryMarshal.GetReference(histogram);
Span<int> histogramCopy = histogramBufferCopy.GetSpan();
ref int histogramCopyBase = ref MemoryMarshal.GetReference(histogramCopy);
ref int cdfBase = ref MemoryMarshal.GetReference(cdfBuffer.GetSpan());
Span<Vector4> pixelRow = pixelRowBuffer.GetSpan();
ref Vector4 pixelRowBase = ref MemoryMarshal.GetReference(pixelRow);
// Build the initial histogram of grayscale values.
for (int dy = this.yStart - this.swInfos.HalfTileHeight; dy < this.yStart + this.swInfos.HalfTileHeight; dy++)
{
if (this.useFastPath)
{
this.processor.CopyPixelRowFast(this.source, pixelRow, x - this.swInfos.HalfTileWidth, dy, this.swInfos.TileWidth, this.configuration);
}
else
{
this.processor.CopyPixelRow(this.source, pixelRow, x - this.swInfos.HalfTileWidth, dy, this.swInfos.TileWidth, this.configuration);
}
this.processor.AddPixelsToHistogram(ref pixelRowBase, ref histogramBase, this.processor.LuminanceLevels, pixelRow.Length);
}
for (int y = this.yStart; y < this.yEnd; y++)
{
if (this.processor.ClipHistogramEnabled)
{
// Clipping the histogram, but doing it on a copy to keep the original un-clipped values for the next iteration.
histogram.CopyTo(histogramCopy);
this.processor.ClipHistogram(histogramCopy, this.processor.ClipLimit);
}
// Calculate the cumulative distribution function, which will map each input pixel in the current tile to a new value.
int cdfMin = this.processor.ClipHistogramEnabled
? this.processor.CalculateCdf(ref cdfBase, ref histogramCopyBase, histogram.Length - 1)
: this.processor.CalculateCdf(ref cdfBase, ref histogramBase, histogram.Length - 1);
float numberOfPixelsMinusCdfMin = this.swInfos.PixelInTile - cdfMin;
// Map the current pixel to the new equalized value.
int luminance = GetLuminance(this.source[x, y], this.processor.LuminanceLevels);
float luminanceEqualized = Unsafe.Add(ref cdfBase, luminance) / numberOfPixelsMinusCdfMin;
this.targetPixels[x, y].FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, this.source[x, y].ToVector4().W));
// Remove top most row from the histogram, mirroring rows which exceeds the borders.
if (this.useFastPath)
{
this.processor.CopyPixelRowFast(this.source, pixelRow, x - this.swInfos.HalfTileWidth, y - this.swInfos.HalfTileWidth, this.swInfos.TileWidth, this.configuration);
}
else
{
this.processor.CopyPixelRow(this.source, pixelRow, x - this.swInfos.HalfTileWidth, y - this.swInfos.HalfTileWidth, this.swInfos.TileWidth, this.configuration);
}
this.processor.RemovePixelsFromHistogram(ref pixelRowBase, ref histogramBase, this.processor.LuminanceLevels, pixelRow.Length);
// Add new bottom row to the histogram, mirroring rows which exceeds the borders.
if (this.useFastPath)
{
this.processor.CopyPixelRowFast(this.source, pixelRow, x - this.swInfos.HalfTileWidth, y + this.swInfos.HalfTileWidth, this.swInfos.TileWidth, this.configuration);
}
else
{
this.processor.CopyPixelRow(this.source, pixelRow, x - this.swInfos.HalfTileWidth, y + this.swInfos.HalfTileWidth, this.swInfos.TileWidth, this.configuration);
}
this.processor.AddPixelsToHistogram(ref pixelRowBase, ref histogramBase, this.processor.LuminanceLevels, pixelRow.Length);
}
}
}
}
private class SlidingWindowInfos private class SlidingWindowInfos
{ {
public SlidingWindowInfos(int tileWidth, int tileHeight, int halfTileWidth, int halfTileHeight, int pixelInTile) public SlidingWindowInfos(int tileWidth, int tileHeight, int halfTileWidth, int halfTileHeight, int pixelInTile)
@ -382,15 +427,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
this.PixelInTile = pixelInTile; this.PixelInTile = pixelInTile;
} }
public int TileWidth { get; private set; } public int TileWidth { get; }
public int TileHeight { get; private set; } public int TileHeight { get; }
public int PixelInTile { get; private set; } public int PixelInTile { get; }
public int HalfTileWidth { get; private set; } public int HalfTileWidth { get; }
public int HalfTileHeight { get; private set; } public int HalfTileHeight { get; }
} }
} }
} }

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

@ -6,9 +6,7 @@ using System.Buffers;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Advanced.ParallelUtils;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -49,64 +47,125 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
{ {
MemoryAllocator memoryAllocator = this.Configuration.MemoryAllocator; MemoryAllocator memoryAllocator = this.Configuration.MemoryAllocator;
int numberOfPixels = source.Width * source.Height; int numberOfPixels = source.Width * source.Height;
var workingRect = new Rectangle(0, 0, source.Width, source.Height); var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
using IMemoryOwner<int> histogramBuffer = memoryAllocator.Allocate<int>(this.LuminanceLevels, AllocationOptions.Clean);
// Build the histogram of the grayscale levels
var grayscaleOperation = new GrayscaleLevelsRowIntervalOperation(interest, histogramBuffer, source, this.LuminanceLevels);
ParallelRowIterator.IterateRows(
this.Configuration,
interest,
in grayscaleOperation);
Span<int> histogram = histogramBuffer.GetSpan();
if (this.ClipHistogramEnabled)
{
this.ClipHistogram(histogram, this.ClipLimit);
}
using IMemoryOwner<int> cdfBuffer = memoryAllocator.Allocate<int>(this.LuminanceLevels, AllocationOptions.Clean);
// Calculate the cumulative distribution function, which will map each input pixel to a new value.
int cdfMin = this.CalculateCdf(
ref MemoryMarshal.GetReference(cdfBuffer.GetSpan()),
ref MemoryMarshal.GetReference(histogram),
histogram.Length - 1);
float numberOfPixelsMinusCdfMin = numberOfPixels - cdfMin;
// Apply the cdf to each pixel of the image
var cdfOperation = new CdfApplicationRowIntervalOperation(interest, cdfBuffer, source, this.LuminanceLevels, numberOfPixelsMinusCdfMin);
ParallelRowIterator.IterateRows(
this.Configuration,
interest,
in cdfOperation);
}
/// <summary>
/// A <see langword="struct"/> implementing the grayscale levels logic for <see cref="GlobalHistogramEqualizationProcessor{TPixel}"/>.
/// </summary>
private readonly struct GrayscaleLevelsRowIntervalOperation : IRowIntervalOperation
{
private readonly Rectangle bounds;
private readonly IMemoryOwner<int> histogramBuffer;
private readonly ImageFrame<TPixel> source;
private readonly int luminanceLevels;
[MethodImpl(InliningOptions.ShortMethod)]
public GrayscaleLevelsRowIntervalOperation(
Rectangle bounds,
IMemoryOwner<int> histogramBuffer,
ImageFrame<TPixel> source,
int luminanceLevels)
{
this.bounds = bounds;
this.histogramBuffer = histogramBuffer;
this.source = source;
this.luminanceLevels = luminanceLevels;
}
using (IMemoryOwner<int> histogramBuffer = memoryAllocator.Allocate<int>(this.LuminanceLevels, AllocationOptions.Clean)) /// <inheritdoc/>
using (IMemoryOwner<int> cdfBuffer = memoryAllocator.Allocate<int>(this.LuminanceLevels, AllocationOptions.Clean)) [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
{ {
// Build the histogram of the grayscale levels. ref int histogramBase = ref MemoryMarshal.GetReference(this.histogramBuffer.GetSpan());
ParallelHelper.IterateRows( for (int y = rows.Min; y < rows.Max; y++)
workingRect,
this.Configuration,
rows =>
{
ref int histogramBase = ref MemoryMarshal.GetReference(histogramBuffer.GetSpan());
for (int y = rows.Min; y < rows.Max; y++)
{
ref TPixel pixelBase = ref MemoryMarshal.GetReference(source.GetPixelRowSpan(y));
for (int x = 0; x < workingRect.Width; x++)
{
int luminance = GetLuminance(Unsafe.Add(ref pixelBase, x), this.LuminanceLevels);
Unsafe.Add(ref histogramBase, luminance)++;
}
}
});
Span<int> histogram = histogramBuffer.GetSpan();
if (this.ClipHistogramEnabled)
{ {
this.ClipHistogram(histogram, this.ClipLimit); ref TPixel pixelBase = ref MemoryMarshal.GetReference(this.source.GetPixelRowSpan(y));
for (int x = 0; x < this.bounds.Width; x++)
{
int luminance = GetLuminance(Unsafe.Add(ref pixelBase, x), this.luminanceLevels);
Unsafe.Add(ref histogramBase, luminance)++;
}
} }
}
}
/// <summary>
/// A <see langword="struct"/> implementing the cdf application levels logic for <see cref="GlobalHistogramEqualizationProcessor{TPixel}"/>.
/// </summary>
private readonly struct CdfApplicationRowIntervalOperation : IRowIntervalOperation
{
private readonly Rectangle bounds;
private readonly IMemoryOwner<int> cdfBuffer;
private readonly ImageFrame<TPixel> source;
private readonly int luminanceLevels;
private readonly float numberOfPixelsMinusCdfMin;
[MethodImpl(InliningOptions.ShortMethod)]
public CdfApplicationRowIntervalOperation(
Rectangle bounds,
IMemoryOwner<int> cdfBuffer,
ImageFrame<TPixel> source,
int luminanceLevels,
float numberOfPixelsMinusCdfMin)
{
this.bounds = bounds;
this.cdfBuffer = cdfBuffer;
this.source = source;
this.luminanceLevels = luminanceLevels;
this.numberOfPixelsMinusCdfMin = numberOfPixelsMinusCdfMin;
}
// Calculate the cumulative distribution function, which will map each input pixel to a new value. /// <inheritdoc/>
int cdfMin = this.CalculateCdf( [MethodImpl(InliningOptions.ShortMethod)]
ref MemoryMarshal.GetReference(cdfBuffer.GetSpan()), public void Invoke(in RowInterval rows)
ref MemoryMarshal.GetReference(histogram), {
histogram.Length - 1); ref int cdfBase = ref MemoryMarshal.GetReference(this.cdfBuffer.GetSpan());
for (int y = rows.Min; y < rows.Max; y++)
float numberOfPixelsMinusCdfMin = numberOfPixels - cdfMin; {
ref TPixel pixelBase = ref MemoryMarshal.GetReference(this.source.GetPixelRowSpan(y));
// Apply the cdf to each pixel of the image
ParallelHelper.IterateRows( for (int x = 0; x < this.bounds.Width; x++)
workingRect, {
this.Configuration, ref TPixel pixel = ref Unsafe.Add(ref pixelBase, x);
rows => int luminance = GetLuminance(pixel, this.luminanceLevels);
{ float luminanceEqualized = Unsafe.Add(ref cdfBase, luminance) / this.numberOfPixelsMinusCdfMin;
ref int cdfBase = ref MemoryMarshal.GetReference(cdfBuffer.GetSpan()); pixel.FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W));
for (int y = rows.Min; y < rows.Max; y++) }
{ }
ref TPixel pixelBase = ref MemoryMarshal.GetReference(source.GetPixelRowSpan(y));
for (int x = 0; x < workingRect.Width; x++)
{
ref TPixel pixel = ref Unsafe.Add(ref pixelBase, x);
int luminance = GetLuminance(pixel, this.LuminanceLevels);
float luminanceEqualized = Unsafe.Add(ref cdfBase, luminance) / numberOfPixelsMinusCdfMin;
pixel.FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W));
}
}
});
} }
} }
} }

114
src/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor{TPixel}.cs

@ -3,9 +3,8 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Advanced.ParallelUtils;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -29,9 +28,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays
/// <param name="sourceRectangle">The source area to process for the current processor instance.</param> /// <param name="sourceRectangle">The source area to process for the current processor instance.</param>
public BackgroundColorProcessor(Configuration configuration, BackgroundColorProcessor definition, Image<TPixel> source, Rectangle sourceRectangle) public BackgroundColorProcessor(Configuration configuration, BackgroundColorProcessor definition, Image<TPixel> source, Rectangle sourceRectangle)
: base(configuration, source, sourceRectangle) : base(configuration, source, sourceRectangle)
{ => this.definition = definition;
this.definition = definition;
}
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source) protected override void OnFrameApply(ImageFrame<TPixel> source)
@ -39,65 +36,70 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays
TPixel color = this.definition.Color.ToPixel<TPixel>(); TPixel color = this.definition.Color.ToPixel<TPixel>();
GraphicsOptions graphicsOptions = this.definition.GraphicsOptions; GraphicsOptions graphicsOptions = this.definition.GraphicsOptions;
int startY = this.SourceRectangle.Y; var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
int endY = this.SourceRectangle.Bottom;
int startX = this.SourceRectangle.X;
int endX = this.SourceRectangle.Right;
// 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);
// Reset offset if necessary.
if (minX > 0)
{
startX = 0;
}
if (minY > 0)
{
startY = 0;
}
int width = maxX - minX;
var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY);
Configuration configuration = this.Configuration; Configuration configuration = this.Configuration;
MemoryAllocator memoryAllocator = configuration.MemoryAllocator; MemoryAllocator memoryAllocator = configuration.MemoryAllocator;
using (IMemoryOwner<TPixel> colors = memoryAllocator.Allocate<TPixel>(width)) using IMemoryOwner<TPixel> colors = memoryAllocator.Allocate<TPixel>(interest.Width);
using (IMemoryOwner<float> amount = memoryAllocator.Allocate<float>(width)) using IMemoryOwner<float> amount = memoryAllocator.Allocate<float>(interest.Width);
{
// Be careful! Do not capture colorSpan & amountSpan in the lambda below!
Span<TPixel> colorSpan = colors.GetSpan();
Span<float> amountSpan = amount.GetSpan();
colorSpan.Fill(color); colors.GetSpan().Fill(color);
amountSpan.Fill(graphicsOptions.BlendPercentage); amount.GetSpan().Fill(graphicsOptions.BlendPercentage);
PixelBlender<TPixel> blender = PixelOperations<TPixel>.Instance.GetPixelBlender(graphicsOptions); PixelBlender<TPixel> blender = PixelOperations<TPixel>.Instance.GetPixelBlender(graphicsOptions);
ParallelHelper.IterateRows( var operation = new RowIntervalOperation(configuration, interest, blender, amount, colors, source);
workingRect, ParallelRowIterator.IterateRows(
configuration, configuration,
rows => interest,
{ in operation);
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 private readonly struct RowIntervalOperation : IRowIntervalOperation
blender.Blend( {
configuration, private readonly Configuration configuration;
destination, private readonly Rectangle bounds;
colors.GetSpan(), private readonly PixelBlender<TPixel> blender;
destination, private readonly IMemoryOwner<float> amount;
amount.GetSpan()); private readonly IMemoryOwner<TPixel> colors;
} private readonly ImageFrame<TPixel> source;
});
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(
Configuration configuration,
Rectangle bounds,
PixelBlender<TPixel> blender,
IMemoryOwner<float> amount,
IMemoryOwner<TPixel> colors,
ImageFrame<TPixel> source)
{
this.configuration = configuration;
this.bounds = bounds;
this.blender = blender;
this.amount = amount;
this.colors = colors;
this.source = source;
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
{
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> destination =
this.source.GetPixelRowSpan(y)
.Slice(this.bounds.X, this.bounds.Width);
// Switch color & destination in the 2nd and 3rd places because we are
// applying the target color under the current one.
this.blender.Blend(
this.configuration,
destination,
this.colors.GetSpan(),
destination,
this.amount.GetSpan());
}
} }
} }
} }

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

@ -4,9 +4,8 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Advanced.ParallelUtils;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -39,78 +38,84 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source) protected override void OnFrameApply(ImageFrame<TPixel> source)
{ {
// TODO: can we simplify the rectangle calculation?
int startY = this.SourceRectangle.Y;
int endY = this.SourceRectangle.Bottom;
int startX = this.SourceRectangle.X;
int endX = this.SourceRectangle.Right;
TPixel glowColor = this.definition.GlowColor.ToPixel<TPixel>(); TPixel glowColor = this.definition.GlowColor.ToPixel<TPixel>();
Vector2 center = Rectangle.Center(this.SourceRectangle); float blendPercent = this.definition.GraphicsOptions.BlendPercentage;
float finalRadius = this.definition.Radius.Calculate(source.Size()); var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
Vector2 center = Rectangle.Center(interest);
float finalRadius = this.definition.Radius.Calculate(interest.Size);
float maxDistance = finalRadius > 0 float maxDistance = finalRadius > 0
? MathF.Min(finalRadius, this.SourceRectangle.Width * .5F) ? MathF.Min(finalRadius, interest.Width * .5F)
: this.SourceRectangle.Width * .5F; : interest.Width * .5F;
// Align start/end positions. Configuration configuration = this.Configuration;
int minX = Math.Max(0, startX); MemoryAllocator allocator = configuration.MemoryAllocator;
int maxX = Math.Min(source.Width, endX);
int minY = Math.Max(0, startY);
int maxY = Math.Min(source.Height, endY);
// Reset offset if necessary. using IMemoryOwner<TPixel> rowColors = allocator.Allocate<TPixel>(interest.Width);
if (minX > 0) rowColors.GetSpan().Fill(glowColor);
{
startX = 0;
}
if (minY > 0) var operation = new RowIntervalOperation(configuration, interest, rowColors, this.blender, center, maxDistance, blendPercent, source);
ParallelRowIterator.IterateRows<RowIntervalOperation, float>(
configuration,
interest,
in operation);
}
private readonly struct RowIntervalOperation : IRowIntervalOperation<float>
{
private readonly Configuration configuration;
private readonly Rectangle bounds;
private readonly PixelBlender<TPixel> blender;
private readonly Vector2 center;
private readonly float maxDistance;
private readonly float blendPercent;
private readonly IMemoryOwner<TPixel> colors;
private readonly ImageFrame<TPixel> source;
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(
Configuration configuration,
Rectangle bounds,
IMemoryOwner<TPixel> colors,
PixelBlender<TPixel> blender,
Vector2 center,
float maxDistance,
float blendPercent,
ImageFrame<TPixel> source)
{ {
startY = 0; this.configuration = configuration;
this.bounds = bounds;
this.colors = colors;
this.blender = blender;
this.center = center;
this.maxDistance = maxDistance;
this.blendPercent = blendPercent;
this.source = source;
} }
int width = maxX - minX; [MethodImpl(InliningOptions.ShortMethod)]
int offsetX = minX - startX; public void Invoke(in RowInterval rows, Span<float> span)
var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY);
float blendPercentage = this.definition.GraphicsOptions.BlendPercentage;
Configuration configuration = this.Configuration;
MemoryAllocator memoryAllocator = configuration.MemoryAllocator;
using (IMemoryOwner<TPixel> rowColors = memoryAllocator.Allocate<TPixel>(width))
{ {
rowColors.GetSpan().Fill(glowColor); Span<TPixel> colorSpan = this.colors.GetSpan();
ParallelHelper.IterateRowsWithTempBuffer<float>( for (int y = rows.Min; y < rows.Max; y++)
workingRect, {
configuration, for (int i = 0; i < this.bounds.Width; i++)
(rows, amounts) => {
{ float distance = Vector2.Distance(this.center, new Vector2(i + this.bounds.X, y));
Span<float> amountsSpan = amounts.Span; span[i] = (this.blendPercent * (1 - (.95F * (distance / this.maxDistance)))).Clamp(0, 1);
}
for (int y = rows.Min; y < rows.Max; y++)
{ Span<TPixel> destination = this.source.GetPixelRowSpan(y).Slice(this.bounds.X, this.bounds.Width);
int offsetY = y - startY;
this.blender.Blend(
for (int i = 0; i < width; i++) this.configuration,
{ destination,
float distance = Vector2.Distance(center, new Vector2(i + offsetX, offsetY)); destination,
amountsSpan[i] = colorSpan,
(blendPercentage * (1 - (.95F * (distance / maxDistance)))).Clamp(0, 1); span);
} }
Span<TPixel> destination = source.GetPixelRowSpan(offsetY).Slice(offsetX, width);
this.blender.Blend(
configuration,
destination,
destination,
rowColors.GetSpan(),
amountsSpan);
}
});
} }
} }
} }

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

@ -4,9 +4,8 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Advanced.ParallelUtils;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -20,7 +19,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
private readonly PixelBlender<TPixel> blender; private readonly PixelBlender<TPixel> blender;
private readonly VignetteProcessor definition; private readonly VignetteProcessor definition;
/// <summary> /// <summary>
@ -40,80 +38,92 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source) protected override void OnFrameApply(ImageFrame<TPixel> source)
{ {
int startY = this.SourceRectangle.Y;
int endY = this.SourceRectangle.Bottom;
int startX = this.SourceRectangle.X;
int endX = this.SourceRectangle.Right;
TPixel vignetteColor = this.definition.VignetteColor.ToPixel<TPixel>(); TPixel vignetteColor = this.definition.VignetteColor.ToPixel<TPixel>();
Vector2 centre = Rectangle.Center(this.SourceRectangle); float blendPercent = this.definition.GraphicsOptions.BlendPercentage;
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
Vector2 center = Rectangle.Center(interest);
float finalRadiusX = this.definition.RadiusX.Calculate(interest.Size);
float finalRadiusY = this.definition.RadiusY.Calculate(interest.Size);
Size sourceSize = source.Size();
float finalRadiusX = this.definition.RadiusX.Calculate(sourceSize);
float finalRadiusY = this.definition.RadiusY.Calculate(sourceSize);
float rX = finalRadiusX > 0 float rX = finalRadiusX > 0
? MathF.Min(finalRadiusX, this.SourceRectangle.Width * .5F) ? MathF.Min(finalRadiusX, interest.Width * .5F)
: this.SourceRectangle.Width * .5F; : interest.Width * .5F;
float rY = finalRadiusY > 0 float rY = finalRadiusY > 0
? MathF.Min(finalRadiusY, this.SourceRectangle.Height * .5F) ? MathF.Min(finalRadiusY, interest.Height * .5F)
: this.SourceRectangle.Height * .5F; : interest.Height * .5F;
float maxDistance = MathF.Sqrt((rX * rX) + (rY * rY)); float maxDistance = MathF.Sqrt((rX * rX) + (rY * rY));
// Align start/end positions. Configuration configuration = this.Configuration;
int minX = Math.Max(0, startX); MemoryAllocator allocator = configuration.MemoryAllocator;
int maxX = Math.Min(source.Width, endX);
int minY = Math.Max(0, startY);
int maxY = Math.Min(source.Height, endY);
// Reset offset if necessary. using IMemoryOwner<TPixel> rowColors = allocator.Allocate<TPixel>(interest.Width);
if (minX > 0) rowColors.GetSpan().Fill(vignetteColor);
{
startX = 0; var operation = new RowIntervalOperation(configuration, interest, rowColors, this.blender, center, maxDistance, blendPercent, source);
} ParallelRowIterator.IterateRows<RowIntervalOperation, float>(
configuration,
interest,
in operation);
}
if (minY > 0) private readonly struct RowIntervalOperation : IRowIntervalOperation<float>
{
private readonly Configuration configuration;
private readonly Rectangle bounds;
private readonly PixelBlender<TPixel> blender;
private readonly Vector2 center;
private readonly float maxDistance;
private readonly float blendPercent;
private readonly IMemoryOwner<TPixel> colors;
private readonly ImageFrame<TPixel> source;
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(
Configuration configuration,
Rectangle bounds,
IMemoryOwner<TPixel> colors,
PixelBlender<TPixel> blender,
Vector2 center,
float maxDistance,
float blendPercent,
ImageFrame<TPixel> source)
{ {
startY = 0; this.configuration = configuration;
this.bounds = bounds;
this.colors = colors;
this.blender = blender;
this.center = center;
this.maxDistance = maxDistance;
this.blendPercent = blendPercent;
this.source = source;
} }
int width = maxX - minX; [MethodImpl(InliningOptions.ShortMethod)]
int offsetX = minX - startX; public void Invoke(in RowInterval rows, Span<float> span)
var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY);
float blendPercentage = this.definition.GraphicsOptions.BlendPercentage;
Configuration configuration = this.Configuration;
MemoryAllocator memoryAllocator = configuration.MemoryAllocator;
using (IMemoryOwner<TPixel> rowColors = memoryAllocator.Allocate<TPixel>(width))
{ {
rowColors.GetSpan().Fill(vignetteColor); Span<TPixel> colorSpan = this.colors.GetSpan();
ParallelHelper.IterateRowsWithTempBuffer<float>( for (int y = rows.Min; y < rows.Max; y++)
workingRect, {
configuration, for (int i = 0; i < this.bounds.Width; i++)
(rows, amounts) => {
{ float distance = Vector2.Distance(this.center, new Vector2(i + this.bounds.X, y));
Span<float> amountsSpan = amounts.Span; span[i] = (this.blendPercent * (.9F * (distance / this.maxDistance))).Clamp(0, 1);
}
for (int y = rows.Min; y < rows.Max; y++)
{ Span<TPixel> destination = this.source.GetPixelRowSpan(y).Slice(this.bounds.X, this.bounds.Width);
int offsetY = y - startY;
this.blender.Blend(
for (int i = 0; i < width; i++) this.configuration,
{ destination,
float distance = Vector2.Distance(centre, new Vector2(i + offsetX, offsetY)); destination,
amountsSpan[i] = (blendPercentage * (.9F * (distance / maxDistance))).Clamp(0, 1); colorSpan,
} span);
}
Span<TPixel> destination = source.GetPixelRowSpan(offsetY).Slice(offsetX, width);
this.blender.Blend(
configuration,
destination,
destination,
rowColors.GetSpan(),
amountsSpan);
}
});
} }
} }
} }

56
src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs

@ -2,8 +2,9 @@
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System; using System;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Quantization namespace SixLabors.ImageSharp.Processing.Processors.Quantization
@ -35,27 +36,50 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
protected override void OnFrameApply(ImageFrame<TPixel> source) protected override void OnFrameApply(ImageFrame<TPixel> source)
{ {
Configuration configuration = this.Configuration; Configuration configuration = this.Configuration;
using (IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(configuration)) using IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(configuration);
using (IQuantizedFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(source)) using IQuantizedFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(source);
{
int paletteCount = quantized.Palette.Length - 1;
// Not parallel to remove "quantized" closure allocation. var operation = new RowIntervalOperation(this.SourceRectangle, source, quantized);
// We can operate directly on the source here as we've already read it to get the ParallelRowIterator.IterateRows(
// quantized result configuration,
for (int y = 0; y < source.Height; y++) this.SourceRectangle,
{ in operation);
Span<TPixel> row = source.GetPixelRowSpan(y); }
ReadOnlySpan<byte> quantizedPixelSpan = quantized.GetPixelSpan();
ReadOnlySpan<TPixel> paletteSpan = quantized.Palette.Span; private readonly struct RowIntervalOperation : IRowIntervalOperation
{
private readonly Rectangle bounds;
private readonly ImageFrame<TPixel> source;
private readonly IQuantizedFrame<TPixel> quantized;
private readonly int maxPaletteIndex;
int yy = y * source.Width; [MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(
Rectangle bounds,
ImageFrame<TPixel> source,
IQuantizedFrame<TPixel> quantized)
{
this.bounds = bounds;
this.source = source;
this.quantized = quantized;
this.maxPaletteIndex = quantized.Palette.Length - 1;
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
{
ReadOnlySpan<byte> quantizedPixelSpan = this.quantized.GetPixelSpan();
ReadOnlySpan<TPixel> paletteSpan = this.quantized.Palette.Span;
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> row = this.source.GetPixelRowSpan(y);
int yy = y * this.bounds.Width;
for (int x = 0; x < source.Width; x++) for (int x = this.bounds.X; x < this.bounds.Right; x++)
{ {
int i = x + yy; int i = x + yy;
row[x] = paletteSpan[Math.Min(paletteCount, quantizedPixelSpan[i])]; row[x] = paletteSpan[Math.Min(this.maxPaletteIndex, quantizedPixelSpan[i])];
} }
} }
} }

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

@ -3,8 +3,9 @@
using System; using System;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Advanced.ParallelUtils; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms namespace SixLabors.ImageSharp.Processing.Processors.Transforms
@ -16,8 +17,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
internal class AffineTransformProcessor<TPixel> : TransformProcessor<TPixel> internal class AffineTransformProcessor<TPixel> : TransformProcessor<TPixel>
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
private Size targetSize; private readonly Size targetSize;
private Matrix3x2 transformMatrix; private readonly Matrix3x2 transformMatrix;
private readonly IResampler resampler; private readonly IResampler resampler;
/// <summary> /// <summary>
@ -49,7 +50,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
} }
int width = this.targetSize.Width; int width = this.targetSize.Width;
Rectangle sourceBounds = this.SourceRectangle;
var targetBounds = new Rectangle(Point.Empty, this.targetSize); var targetBounds = new Rectangle(Point.Empty, this.targetSize);
Configuration configuration = this.Configuration; Configuration configuration = this.Configuration;
@ -58,70 +58,130 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
if (this.resampler is NearestNeighborResampler) if (this.resampler is NearestNeighborResampler)
{ {
ParallelHelper.IterateRows( var nnOperation = new NearestNeighborRowIntervalOperation(this.SourceRectangle, ref matrix, width, source, destination);
targetBounds, ParallelRowIterator.IterateRows(
configuration, configuration,
rows => targetBounds,
{ in nnOperation);
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> destRow = destination.GetPixelRowSpan(y);
for (int x = 0; x < width; x++)
{
var point = Point.Transform(new Point(x, y), matrix);
if (sourceBounds.Contains(point.X, point.Y))
{
destRow[x] = source[point.X, point.Y];
}
}
}
});
return; return;
} }
var kernel = new TransformKernelMap(configuration, source.Size(), destination.Size(), this.resampler); using var kernelMap = new TransformKernelMap(configuration, source.Size(), destination.Size(), this.resampler);
try var operation = new RowIntervalOperation(configuration, kernelMap, ref matrix, width, source, destination);
ParallelRowIterator.IterateRows<RowIntervalOperation, Vector4>(
configuration,
targetBounds,
in operation);
}
/// <summary>
/// A <see langword="struct"/> implementing the nearest neighbor resampler logic for <see cref="AffineTransformProcessor{T}"/>.
/// </summary>
private readonly struct NearestNeighborRowIntervalOperation : IRowIntervalOperation
{
private readonly Rectangle bounds;
private readonly Matrix3x2 matrix;
private readonly int maxX;
private readonly ImageFrame<TPixel> source;
private readonly ImageFrame<TPixel> destination;
[MethodImpl(InliningOptions.ShortMethod)]
public NearestNeighborRowIntervalOperation(
Rectangle bounds,
ref Matrix3x2 matrix,
int maxX,
ImageFrame<TPixel> source,
ImageFrame<TPixel> destination)
{ {
ParallelHelper.IterateRowsWithTempBuffer<Vector4>( this.bounds = bounds;
targetBounds, this.matrix = matrix;
configuration, this.maxX = maxX;
(rows, vectorBuffer) => this.source = source;
this.destination = destination;
}
/// <inheritdoc/>
/// <param name="rows"></param>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
{
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> destRow = this.destination.GetPixelRowSpan(y);
for (int x = 0; x < this.maxX; x++)
{
var point = Point.Transform(new Point(x, y), this.matrix);
if (this.bounds.Contains(point.X, point.Y))
{ {
Span<Vector4> vectorSpan = vectorBuffer.Span; destRow[x] = this.source[point.X, point.Y];
for (int y = rows.Min; y < rows.Max; y++) }
{ }
Span<TPixel> targetRowSpan = destination.GetPixelRowSpan(y); }
PixelOperations<TPixel>.Instance.ToVector4(configuration, targetRowSpan, vectorSpan);
ref float ySpanRef = ref kernel.GetYStartReference(y);
ref float xSpanRef = ref kernel.GetXStartReference(y);
for (int x = 0; x < width; x++)
{
// 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);
kernel.Convolve(
point,
x,
ref ySpanRef,
ref xSpanRef,
source.PixelBuffer,
vectorSpan);
}
PixelOperations<TPixel>.Instance.FromVector4Destructive(
configuration,
vectorSpan,
targetRowSpan);
}
});
} }
finally }
/// <summary>
/// A <see langword="struct"/> implementing the transformation logic for <see cref="AffineTransformProcessor{T}"/>.
/// </summary>
private readonly struct RowIntervalOperation : IRowIntervalOperation<Vector4>
{
private readonly Configuration configuration;
private readonly TransformKernelMap kernelMap;
private readonly Matrix3x2 matrix;
private readonly int maxX;
private readonly ImageFrame<TPixel> source;
private readonly ImageFrame<TPixel> destination;
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(
Configuration configuration,
TransformKernelMap kernelMap,
ref Matrix3x2 matrix,
int maxX,
ImageFrame<TPixel> source,
ImageFrame<TPixel> destination)
{
this.configuration = configuration;
this.kernelMap = kernelMap;
this.matrix = matrix;
this.maxX = maxX;
this.source = source;
this.destination = destination;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows, Span<Vector4> span)
{ {
kernel.Dispose(); for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> targetRowSpan = this.destination.GetPixelRowSpan(y);
PixelOperations<TPixel>.Instance.ToVector4(this.configuration, targetRowSpan, span);
ref float ySpanRef = ref this.kernelMap.GetYStartReference(y);
ref float xSpanRef = ref this.kernelMap.GetXStartReference(y);
for (int x = 0; x < this.maxX; x++)
{
// 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), this.matrix);
this.kernelMap.Convolve(
point,
x,
ref ySpanRef,
ref xSpanRef,
this.source.PixelBuffer,
span);
}
PixelOperations<TPixel>.Instance.FromVector4Destructive(
this.configuration,
span,
targetRowSpan);
}
} }
} }
} }

61
src/ImageSharp/Processing/Processors/Transforms/CropProcessor{TPixel}.cs

@ -2,8 +2,9 @@
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System; using System;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Advanced.ParallelUtils; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms namespace SixLabors.ImageSharp.Processing.Processors.Transforms
@ -15,7 +16,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
internal class CropProcessor<TPixel> : TransformProcessor<TPixel> internal class CropProcessor<TPixel> : TransformProcessor<TPixel>
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
private Rectangle cropRectangle; private readonly Rectangle cropRectangle;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="CropProcessor{TPixel}"/> class. /// Initializes a new instance of the <see cref="CropProcessor{TPixel}"/> class.
@ -47,21 +48,51 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
Rectangle bounds = this.cropRectangle; Rectangle bounds = this.cropRectangle;
// Copying is cheap, we should process more pixels per task: // Copying is cheap, we should process more pixels per task:
ParallelExecutionSettings parallelSettings = ParallelExecutionSettings.FromConfiguration(this.Configuration) ParallelExecutionSettings parallelSettings =
.MultiplyMinimumPixelsPerTask(4); ParallelExecutionSettings.FromConfiguration(this.Configuration).MultiplyMinimumPixelsPerTask(4);
ParallelHelper.IterateRows( var operation = new RowIntervalOperation(bounds, source, destination);
ParallelRowIterator.IterateRows(
bounds, bounds,
parallelSettings, in parallelSettings,
rows => in operation);
{ }
for (int y = rows.Min; y < rows.Max; y++)
{ /// <summary>
Span<TPixel> sourceRow = source.GetPixelRowSpan(y).Slice(bounds.Left); /// A <see langword="struct"/> implementing the processor logic for <see cref="CropProcessor{T}"/>.
Span<TPixel> targetRow = destination.GetPixelRowSpan(y - bounds.Top); /// </summary>
sourceRow.Slice(0, bounds.Width).CopyTo(targetRow); private readonly struct RowIntervalOperation : IRowIntervalOperation
} {
}); private readonly Rectangle bounds;
private readonly ImageFrame<TPixel> source;
private readonly ImageFrame<TPixel> destination;
/// <summary>
/// Initializes a new instance of the <see cref="RowIntervalOperation"/> struct.
/// </summary>
/// <param name="bounds">The target processing bounds for the current instance.</param>
/// <param name="source">The source <see cref="Image{TPixel}"/> for the current instance.</param>
/// <param name="destination">The destination <see cref="Image{TPixel}"/> for the current instance.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(Rectangle bounds, ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
{
this.bounds = bounds;
this.source = source;
this.destination = destination;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
{
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> sourceRow = this.source.GetPixelRowSpan(y).Slice(this.bounds.Left);
Span<TPixel> targetRow = this.destination.GetPixelRowSpan(y - this.bounds.Top);
sourceRow.Slice(0, this.bounds.Width).CopyTo(targetRow);
}
}
} }
} }
} }

55
src/ImageSharp/Processing/Processors/Transforms/FlipProcessor{TPixel}.cs

@ -3,9 +3,9 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Advanced.ParallelUtils; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms namespace SixLabors.ImageSharp.Processing.Processors.Transforms
@ -55,20 +55,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
private void FlipX(ImageFrame<TPixel> source, Configuration configuration) private void FlipX(ImageFrame<TPixel> source, Configuration configuration)
{ {
int height = source.Height; int height = source.Height;
using IMemoryOwner<TPixel> tempBuffer = configuration.MemoryAllocator.Allocate<TPixel>(source.Width);
Span<TPixel> temp = tempBuffer.Memory.Span;
using (IMemoryOwner<TPixel> tempBuffer = configuration.MemoryAllocator.Allocate<TPixel>(source.Width)) for (int yTop = 0; yTop < height / 2; yTop++)
{ {
Span<TPixel> temp = tempBuffer.Memory.Span; int yBottom = height - yTop - 1;
Span<TPixel> topRow = source.GetPixelRowSpan(yBottom);
for (int yTop = 0; yTop < height / 2; yTop++) Span<TPixel> bottomRow = source.GetPixelRowSpan(yTop);
{ topRow.CopyTo(temp);
int yBottom = height - yTop - 1; bottomRow.CopyTo(topRow);
Span<TPixel> topRow = source.GetPixelRowSpan(yBottom); temp.CopyTo(bottomRow);
Span<TPixel> bottomRow = source.GetPixelRowSpan(yTop);
topRow.CopyTo(temp);
bottomRow.CopyTo(topRow);
temp.CopyTo(bottomRow);
}
} }
} }
@ -79,16 +76,28 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <param name="configuration">The configuration.</param> /// <param name="configuration">The configuration.</param>
private void FlipY(ImageFrame<TPixel> source, Configuration configuration) private void FlipY(ImageFrame<TPixel> source, Configuration configuration)
{ {
ParallelHelper.IterateRows( var operation = new RowIntervalOperation(source);
source.Bounds(), ParallelRowIterator.IterateRows(
configuration, configuration,
rows => source.Bounds(),
{ in operation);
for (int y = rows.Min; y < rows.Max; y++) }
{
source.GetPixelRowSpan(y).Reverse(); private readonly struct RowIntervalOperation : IRowIntervalOperation
} {
}); private readonly ImageFrame<TPixel> source;
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(ImageFrame<TPixel> source) => this.source = source;
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
{
for (int y = rows.Min; y < rows.Max; y++)
{
this.source.GetPixelRowSpan(y).Reverse();
}
}
} }
} }
} }

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

@ -3,9 +3,9 @@
using System; using System;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Advanced.ParallelUtils; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms namespace SixLabors.ImageSharp.Processing.Processors.Transforms
@ -17,9 +17,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel> internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel>
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
private Size targetSize; private readonly Size targetSize;
private readonly IResampler resampler; private readonly IResampler resampler;
private Matrix4x4 transformMatrix; private readonly Matrix4x4 transformMatrix;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ProjectiveTransformProcessor{TPixel}"/> class. /// Initializes a new instance of the <see cref="ProjectiveTransformProcessor{TPixel}"/> class.
@ -50,7 +50,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
} }
int width = this.targetSize.Width; int width = this.targetSize.Width;
Rectangle sourceBounds = this.SourceRectangle;
var targetBounds = new Rectangle(Point.Empty, this.targetSize); var targetBounds = new Rectangle(Point.Empty, this.targetSize);
Configuration configuration = this.Configuration; Configuration configuration = this.Configuration;
@ -59,73 +58,126 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
if (this.resampler is NearestNeighborResampler) if (this.resampler is NearestNeighborResampler)
{ {
ParallelHelper.IterateRows( Rectangle sourceBounds = this.SourceRectangle;
targetBounds,
var nnOperation = new NearestNeighborRowIntervalOperation(sourceBounds, ref matrix, width, source, destination);
ParallelRowIterator.IterateRows(
configuration, configuration,
rows => targetBounds,
{ in nnOperation);
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> destRow = destination.GetPixelRowSpan(y);
for (int x = 0; x < width; x++)
{
Vector2 point = TransformUtils.ProjectiveTransform2D(x, y, matrix);
int px = (int)MathF.Round(point.X);
int py = (int)MathF.Round(point.Y);
if (sourceBounds.Contains(px, py))
{
destRow[x] = source[px, py];
}
}
}
});
return; return;
} }
var kernel = new TransformKernelMap(configuration, source.Size(), destination.Size(), this.resampler); using var kernelMap = new TransformKernelMap(configuration, source.Size(), destination.Size(), this.resampler);
try var operation = new RowIntervalOperation(configuration, kernelMap, ref matrix, width, source, destination);
ParallelRowIterator.IterateRows<RowIntervalOperation, Vector4>(
configuration,
targetBounds,
in operation);
}
private readonly struct NearestNeighborRowIntervalOperation : IRowIntervalOperation
{
private readonly Rectangle bounds;
private readonly Matrix4x4 matrix;
private readonly int maxX;
private readonly ImageFrame<TPixel> source;
private readonly ImageFrame<TPixel> destination;
[MethodImpl(InliningOptions.ShortMethod)]
public NearestNeighborRowIntervalOperation(
Rectangle bounds,
ref Matrix4x4 matrix,
int maxX,
ImageFrame<TPixel> source,
ImageFrame<TPixel> destination)
{ {
ParallelHelper.IterateRowsWithTempBuffer<Vector4>( this.bounds = bounds;
targetBounds, this.matrix = matrix;
configuration, this.maxX = maxX;
(rows, vectorBuffer) => this.source = source;
this.destination = destination;
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
{
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> destRow = this.destination.GetPixelRowSpan(y);
for (int x = 0; x < this.maxX; x++)
{
Vector2 point = TransformUtils.ProjectiveTransform2D(x, y, this.matrix);
int px = (int)MathF.Round(point.X);
int py = (int)MathF.Round(point.Y);
if (this.bounds.Contains(px, py))
{ {
Span<Vector4> vectorSpan = vectorBuffer.Span; destRow[x] = this.source[px, py];
for (int y = rows.Min; y < rows.Max; y++) }
{ }
Span<TPixel> targetRowSpan = destination.GetPixelRowSpan(y); }
PixelOperations<TPixel>.Instance.ToVector4(configuration, targetRowSpan, vectorSpan); }
ref float ySpanRef = ref kernel.GetYStartReference(y); }
ref float xSpanRef = ref kernel.GetXStartReference(y);
private readonly struct RowIntervalOperation : IRowIntervalOperation<Vector4>
for (int x = 0; x < width; x++) {
{ private readonly Configuration configuration;
// Use the single precision position to calculate correct bounding pixels private readonly TransformKernelMap kernelMap;
// otherwise we get rogue pixels outside of the bounds. private readonly Matrix4x4 matrix;
Vector2 point = TransformUtils.ProjectiveTransform2D(x, y, matrix); private readonly int maxX;
kernel.Convolve( private readonly ImageFrame<TPixel> source;
point, private readonly ImageFrame<TPixel> destination;
x,
ref ySpanRef, [MethodImpl(InliningOptions.ShortMethod)]
ref xSpanRef, public RowIntervalOperation(
source.PixelBuffer, Configuration configuration,
vectorSpan); TransformKernelMap kernelMap,
} ref Matrix4x4 matrix,
int maxX,
PixelOperations<TPixel>.Instance.FromVector4Destructive( ImageFrame<TPixel> source,
configuration, ImageFrame<TPixel> destination)
vectorSpan, {
targetRowSpan); this.configuration = configuration;
} this.kernelMap = kernelMap;
}); this.matrix = matrix;
this.maxX = maxX;
this.source = source;
this.destination = destination;
} }
finally
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows, Span<Vector4> span)
{ {
kernel.Dispose(); for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> targetRowSpan = this.destination.GetPixelRowSpan(y);
PixelOperations<TPixel>.Instance.ToVector4(this.configuration, targetRowSpan, span);
ref float ySpanRef = ref this.kernelMap.GetYStartReference(y);
ref float xSpanRef = ref this.kernelMap.GetXStartReference(y);
for (int x = 0; x < this.maxX; x++)
{
// Use the single precision position to calculate correct bounding pixels
// otherwise we get rogue pixels outside of the bounds.
Vector2 point = TransformUtils.ProjectiveTransform2D(x, y, this.matrix);
this.kernelMap.Convolve(
point,
x,
ref ySpanRef,
ref xSpanRef,
this.source.PixelBuffer,
span);
}
PixelOperations<TPixel>.Instance.FromVector4Destructive(
this.configuration,
span,
targetRowSpan);
}
} }
} }
} }

4
src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors and contributors. // Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System; using System;
@ -248,4 +248,4 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
return new ResizeKernel(left, rowPtr, length); return new ResizeKernel(left, rowPtr, length);
} }
} }
} }

94
src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs

@ -2,9 +2,8 @@
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System; using System;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Advanced.ParallelUtils;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -24,7 +23,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
private readonly int targetWidth; private readonly int targetWidth;
private readonly int targetHeight; private readonly int targetHeight;
private readonly IResampler resampler; private readonly IResampler resampler;
private Rectangle targetRectangle; private readonly Rectangle targetRectangle;
private readonly bool compand; private readonly bool compand;
// The following fields are not immutable but are optionally created on demand. // The following fields are not immutable but are optionally created on demand.
@ -77,7 +76,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
Configuration configuration = this.Configuration; Configuration configuration = this.Configuration;
// Handle resize dimensions identical to the original // Handle resize dimensions identical to the original
if (source.Width == destination.Width && source.Height == destination.Height && sourceRectangle == this.targetRectangle) if (source.Width == destination.Width
&& source.Height == destination.Height
&& sourceRectangle == this.targetRectangle)
{ {
// The cloned will be blank here copy all the pixel data over // The cloned will be blank here copy all the pixel data over
source.GetPixelSpan().CopyTo(destination.GetPixelSpan()); source.GetPixelSpan().CopyTo(destination.GetPixelSpan());
@ -86,14 +87,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
int width = this.targetWidth; int width = this.targetWidth;
int height = this.targetHeight; int height = this.targetHeight;
int sourceX = sourceRectangle.X; var interest = Rectangle.Intersect(this.targetRectangle, new Rectangle(0, 0, width, height));
int sourceY = sourceRectangle.Y;
int startY = this.targetRectangle.Y;
int startX = this.targetRectangle.X;
var targetWorkingRect = Rectangle.Intersect(
this.targetRectangle,
new Rectangle(0, 0, width, height));
if (this.resampler is NearestNeighborResampler) if (this.resampler is NearestNeighborResampler)
{ {
@ -101,24 +95,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
float widthFactor = sourceRectangle.Width / (float)this.targetRectangle.Width; float widthFactor = sourceRectangle.Width / (float)this.targetRectangle.Width;
float heightFactor = sourceRectangle.Height / (float)this.targetRectangle.Height; float heightFactor = sourceRectangle.Height / (float)this.targetRectangle.Height;
ParallelHelper.IterateRows( var operation = new RowIntervalOperation(sourceRectangle, this.targetRectangle, widthFactor, heightFactor, source, destination);
targetWorkingRect, ParallelRowIterator.IterateRows(
configuration, configuration,
rows => interest,
{ in operation);
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 = targetWorkingRect.Left; x < targetWorkingRect.Right; x++)
{
// X coordinates of source points
targetRow[x] = sourceRow[(int)(((x - startX) * widthFactor) + sourceX)];
}
}
});
return; return;
} }
@ -137,12 +118,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
this.horizontalKernelMap, this.horizontalKernelMap,
this.verticalKernelMap, this.verticalKernelMap,
width, width,
targetWorkingRect, interest,
this.targetRectangle.Location)) this.targetRectangle.Location))
{ {
worker.Initialize(); worker.Initialize();
var workingInterval = new RowInterval(targetWorkingRect.Top, targetWorkingRect.Bottom); var workingInterval = new RowInterval(interest.Top, interest.Bottom);
worker.FillDestinationPixels(workingInterval, destination.PixelBuffer); worker.FillDestinationPixels(workingInterval, destination.PixelBuffer);
} }
} }
@ -166,5 +147,56 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
this.isDisposed = true; this.isDisposed = true;
base.Dispose(disposing); base.Dispose(disposing);
} }
private readonly struct RowIntervalOperation : IRowIntervalOperation
{
private readonly Rectangle sourceBounds;
private readonly Rectangle destinationBounds;
private readonly float widthFactor;
private readonly float heightFactor;
private readonly ImageFrame<TPixel> source;
private readonly ImageFrame<TPixel> destination;
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(
Rectangle sourceBounds,
Rectangle destinationBounds,
float widthFactor,
float heightFactor,
ImageFrame<TPixel> source,
ImageFrame<TPixel> destination)
{
this.sourceBounds = sourceBounds;
this.destinationBounds = destinationBounds;
this.widthFactor = widthFactor;
this.heightFactor = heightFactor;
this.source = source;
this.destination = destination;
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
{
int sourceX = this.sourceBounds.X;
int sourceY = this.sourceBounds.Y;
int destX = this.destinationBounds.X;
int destY = this.destinationBounds.Y;
int destLeft = this.destinationBounds.Left;
int destRight = this.destinationBounds.Right;
for (int y = rows.Min; y < rows.Max; y++)
{
// Y coordinates of source points
Span<TPixel> sourceRow = this.source.GetPixelRowSpan((int)(((y - destY) * this.heightFactor) + sourceY));
Span<TPixel> targetRow = this.destination.GetPixelRowSpan(y);
for (int x = destLeft; x < destRight; x++)
{
// X coordinates of source points
targetRow[x] = sourceRow[(int)(((x - destX) * this.widthFactor) + sourceX)];
}
}
}
}
} }
} }

2
src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs

@ -19,7 +19,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// When sliding the window, the contents of the bottom window band are copied to the new top band. /// When sliding the window, the contents of the bottom window band are copied to the new top band.
/// For more details, and visual explanation, see "ResizeWorker.pptx". /// For more details, and visual explanation, see "ResizeWorker.pptx".
/// </summary> /// </summary>
internal class ResizeWorker<TPixel> : IDisposable internal sealed class ResizeWorker<TPixel> : IDisposable
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
private readonly Buffer2D<Vector4> transposedFirstPassBuffer; private readonly Buffer2D<Vector4> transposedFirstPassBuffer;

224
src/ImageSharp/Processing/Processors/Transforms/RotateProcessor{TPixel}.cs

@ -2,9 +2,9 @@
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System; using System;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Advanced.ParallelUtils; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -17,6 +17,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
internal class RotateProcessor<TPixel> : AffineTransformProcessor<TPixel> internal class RotateProcessor<TPixel> : AffineTransformProcessor<TPixel>
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
private readonly float degrees;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="RotateProcessor{TPixel}"/> class. /// Initializes a new instance of the <see cref="RotateProcessor{TPixel}"/> class.
/// </summary> /// </summary>
@ -26,11 +28,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <param name="sourceRectangle">The source area to process for the current processor instance.</param> /// <param name="sourceRectangle">The source area to process for the current processor instance.</param>
public RotateProcessor(Configuration configuration, RotateProcessor definition, Image<TPixel> source, Rectangle sourceRectangle) public RotateProcessor(Configuration configuration, RotateProcessor definition, Image<TPixel> source, Rectangle sourceRectangle)
: base(configuration, definition, source, sourceRectangle) : base(configuration, definition, source, sourceRectangle)
=> this.degrees = definition.Degrees;
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
{ {
this.Degrees = definition.Degrees; if (this.OptimizedApply(source, destination, this.Configuration))
} {
return;
}
private float Degrees { get; } base.OnFrameApply(source, destination);
}
/// <inheritdoc/> /// <inheritdoc/>
protected override void AfterImageApply(Image<TPixel> destination) protected override void AfterImageApply(Image<TPixel> destination)
@ -41,7 +50,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
return; return;
} }
if (MathF.Abs(WrapDegrees(this.Degrees)) < Constants.Epsilon) if (MathF.Abs(WrapDegrees(this.degrees)) < Constants.Epsilon)
{ {
// No need to do anything so return. // No need to do anything so return.
return; return;
@ -52,17 +61,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
base.AfterImageApply(destination); base.AfterImageApply(destination);
} }
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
{
if (this.OptimizedApply(source, destination, this.Configuration))
{
return;
}
base.OnFrameApply(source, destination);
}
/// <summary> /// <summary>
/// Wraps a given angle in degrees so that it falls withing the 0-360 degree range /// Wraps a given angle in degrees so that it falls withing the 0-360 degree range
/// </summary> /// </summary>
@ -95,7 +93,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
Configuration configuration) Configuration configuration)
{ {
// Wrap the degrees to keep within 0-360 so we can apply optimizations when possible. // Wrap the degrees to keep within 0-360 so we can apply optimizations when possible.
float degrees = WrapDegrees(this.Degrees); float degrees = WrapDegrees(this.degrees);
if (MathF.Abs(degrees) < Constants.Epsilon) if (MathF.Abs(degrees) < Constants.Epsilon)
{ {
@ -133,25 +131,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <param name="configuration">The configuration.</param> /// <param name="configuration">The configuration.</param>
private void Rotate180(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Configuration configuration) private void Rotate180(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Configuration configuration)
{ {
int width = source.Width; var operation = new Rotate180RowIntervalOperation(source.Width, source.Height, source, destination);
int height = source.Height; ParallelRowIterator.IterateRows(
ParallelHelper.IterateRows(
source.Bounds(),
configuration, configuration,
rows => source.Bounds(),
{ in operation);
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
Span<TPixel> targetRow = destination.GetPixelRowSpan(height - y - 1);
for (int x = 0; x < width; x++)
{
targetRow[width - x - 1] = sourceRow[x];
}
}
});
} }
/// <summary> /// <summary>
@ -162,31 +146,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <param name="configuration">The configuration.</param> /// <param name="configuration">The configuration.</param>
private void Rotate270(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Configuration configuration) private void Rotate270(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Configuration configuration)
{ {
int width = source.Width; var operation = new Rotate270RowIntervalOperation(destination.Bounds(), source.Width, source.Height, source, destination);
int height = source.Height; ParallelRowIterator.IterateRows(
Rectangle destinationBounds = destination.Bounds();
ParallelHelper.IterateRows(
source.Bounds(),
configuration, configuration,
rows => source.Bounds(),
{ in operation);
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
for (int x = 0; x < width; x++)
{
int newX = height - y - 1;
newX = height - newX - 1;
int newY = width - x - 1;
if (destinationBounds.Contains(newX, newY))
{
destination[newX, newY] = sourceRow[x];
}
}
}
});
} }
/// <summary> /// <summary>
@ -197,28 +161,132 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <param name="configuration">The configuration.</param> /// <param name="configuration">The configuration.</param>
private void Rotate90(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Configuration configuration) private void Rotate90(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Configuration configuration)
{ {
int width = source.Width; var operation = new Rotate90RowIntervalOperation(destination.Bounds(), source.Width, source.Height, source, destination);
int height = source.Height; ParallelRowIterator.IterateRows(
Rectangle destinationBounds = destination.Bounds();
ParallelHelper.IterateRows(
source.Bounds(),
configuration, configuration,
rows => source.Bounds(),
in operation);
}
private readonly struct Rotate180RowIntervalOperation : IRowIntervalOperation
{
private readonly int width;
private readonly int height;
private readonly ImageFrame<TPixel> source;
private readonly ImageFrame<TPixel> destination;
[MethodImpl(InliningOptions.ShortMethod)]
public Rotate180RowIntervalOperation(
int width,
int height,
ImageFrame<TPixel> source,
ImageFrame<TPixel> destination)
{
this.width = width;
this.height = height;
this.source = source;
this.destination = destination;
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
{
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> sourceRow = this.source.GetPixelRowSpan(y);
Span<TPixel> targetRow = this.destination.GetPixelRowSpan(this.height - y - 1);
for (int x = 0; x < this.width; x++)
{
targetRow[this.width - x - 1] = sourceRow[x];
}
}
}
}
private readonly struct Rotate270RowIntervalOperation : IRowIntervalOperation
{
private readonly Rectangle bounds;
private readonly int width;
private readonly int height;
private readonly ImageFrame<TPixel> source;
private readonly ImageFrame<TPixel> destination;
[MethodImpl(InliningOptions.ShortMethod)]
public Rotate270RowIntervalOperation(
Rectangle bounds,
int width,
int height,
ImageFrame<TPixel> source,
ImageFrame<TPixel> destination)
{
this.bounds = bounds;
this.width = width;
this.height = height;
this.source = source;
this.destination = destination;
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
{
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> sourceRow = this.source.GetPixelRowSpan(y);
for (int x = 0; x < this.width; x++)
{
int newX = this.height - y - 1;
newX = this.height - newX - 1;
int newY = this.width - x - 1;
if (this.bounds.Contains(newX, newY))
{
this.destination[newX, newY] = sourceRow[x];
}
}
}
}
}
private readonly struct Rotate90RowIntervalOperation : IRowIntervalOperation
{
private readonly Rectangle bounds;
private readonly int width;
private readonly int height;
private readonly ImageFrame<TPixel> source;
private readonly ImageFrame<TPixel> destination;
[MethodImpl(InliningOptions.ShortMethod)]
public Rotate90RowIntervalOperation(
Rectangle bounds,
int width,
int height,
ImageFrame<TPixel> source,
ImageFrame<TPixel> destination)
{
this.bounds = bounds;
this.width = width;
this.height = height;
this.source = source;
this.destination = destination;
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
{
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> sourceRow = this.source.GetPixelRowSpan(y);
int newX = this.height - y - 1;
for (int x = 0; x < this.width; x++)
{ {
for (int y = rows.Min; y < rows.Max; y++) if (this.bounds.Contains(newX, x))
{ {
Span<TPixel> sourceRow = source.GetPixelRowSpan(y); this.destination[newX, x] = sourceRow[x];
int newX = height - y - 1;
for (int x = 0; x < width; x++)
{
if (destinationBounds.Contains(newX, x))
{
destination[newX, x] = sourceRow[x];
}
}
} }
}); }
}
}
} }
} }
} }

1
tests/Directory.Build.targets

@ -26,6 +26,7 @@
<ItemGroup> <ItemGroup>
<!--Test Dependencies--> <!--Test Dependencies-->
<PackageReference Update="BenchmarkDotNet" Version="0.12.0" /> <PackageReference Update="BenchmarkDotNet" Version="0.12.0" />
<PackageReference Update="BenchmarkDotNet.Diagnostics.Windows" Version="0.12.0" Condition="'$(OS)' == 'Windows_NT'" />
<PackageReference Update="Colourful" Version="2.0.3" /> <PackageReference Update="Colourful" Version="2.0.3" />
<PackageReference Update="coverlet.collector" Version="1.2.0" PrivateAssets="All"/> <PackageReference Update="coverlet.collector" Version="1.2.0" PrivateAssets="All"/>
<PackageReference Update="Magick.NET-Q16-AnyCPU" Version="7.14.4" /> <PackageReference Update="Magick.NET-Q16-AnyCPU" Version="7.14.4" />

22
tests/ImageSharp.Benchmarks/Config.cs

@ -1,6 +1,10 @@
// Copyright (c) Six Labors and contributors. // Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
#if Windows_NT
using System.Security.Principal;
using BenchmarkDotNet.Diagnostics.Windows;
#endif
using BenchmarkDotNet.Configs; using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Diagnosers; using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Environments; using BenchmarkDotNet.Environments;
@ -13,6 +17,14 @@ namespace SixLabors.ImageSharp.Benchmarks
public Config() public Config()
{ {
this.Add(MemoryDiagnoser.Default); this.Add(MemoryDiagnoser.Default);
#if Windows_NT
if (this.IsElevated)
{
this.Add(new NativeMemoryProfiler());
}
#endif
} }
public class ShortClr : Config public class ShortClr : Config
@ -25,5 +37,15 @@ namespace SixLabors.ImageSharp.Benchmarks
Job.Default.With(CoreRuntime.Core21).WithLaunchCount(1).WithWarmupCount(3).WithIterationCount(3)); Job.Default.With(CoreRuntime.Core21).WithLaunchCount(1).WithWarmupCount(3).WithIterationCount(3));
} }
} }
#if Windows_NT
private bool IsElevated
{
get
{
return new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator);
}
}
#endif
} }
} }

1
tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj

@ -19,6 +19,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Magick.NET-Q16-AnyCPU" /> <PackageReference Include="Magick.NET-Q16-AnyCPU" />
<PackageReference Include="BenchmarkDotNet" /> <PackageReference Include="BenchmarkDotNet" />
<PackageReference Include="BenchmarkDotNet.Diagnostics.Windows" Condition="'$(OS)' == 'Windows_NT'" />
<PackageReference Include="Colourful" /> <PackageReference Include="Colourful" />
<PackageReference Include="Pfim" /> <PackageReference Include="Pfim" />
<PackageReference Include="System.Drawing.Common" /> <PackageReference Include="System.Drawing.Common" />

1
tests/ImageSharp.Benchmarks/Samplers/Crop.cs

@ -13,6 +13,7 @@ using SDSize = System.Drawing.Size;
namespace SixLabors.ImageSharp.Benchmarks namespace SixLabors.ImageSharp.Benchmarks
{ {
[Config(typeof(Config.ShortClr))]
public class Crop : BenchmarkBase public class Crop : BenchmarkBase
{ {
[Benchmark(Baseline = true, Description = "System.Drawing Crop")] [Benchmark(Baseline = true, Description = "System.Drawing Crop")]

2
tests/ImageSharp.Tests/Helpers/ParallelExecutionSettingsTests.cs

@ -2,7 +2,7 @@
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System; using System;
using SixLabors.ImageSharp.Advanced.ParallelUtils; using SixLabors.ImageSharp.Advanced;
using Xunit; using Xunit;
namespace SixLabors.ImageSharp.Tests.Helpers namespace SixLabors.ImageSharp.Tests.Helpers

293
tests/ImageSharp.Tests/Helpers/ParallelHelperTests.cs → tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs

@ -2,25 +2,25 @@
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System; using System;
using System.Collections.Concurrent;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using System.Threading; using System.Threading;
using SixLabors.ImageSharp.Advanced.ParallelUtils; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
namespace SixLabors.ImageSharp.Tests.Helpers namespace SixLabors.ImageSharp.Tests.Helpers
{ {
public class ParallelHelperTests public class ParallelRowIteratorTests
{ {
public delegate void RowIntervalAction<T>(RowInterval rows, Span<T> span);
private readonly ITestOutputHelper output; private readonly ITestOutputHelper output;
public ParallelHelperTests(ITestOutputHelper output) public ParallelRowIteratorTests(ITestOutputHelper output)
{ {
this.output = output; this.output = output;
} }
@ -30,20 +30,20 @@ namespace SixLabors.ImageSharp.Tests.Helpers
/// </summary> /// </summary>
public static TheoryData<int, int, int, int, int, int> IterateRows_OverMinimumPixelsLimit_Data = public static TheoryData<int, int, int, int, int, int> IterateRows_OverMinimumPixelsLimit_Data =
new TheoryData<int, int, int, int, int, int> new TheoryData<int, int, int, int, int, int>
{ {
{ 1, 0, 100, -1, 100, 1 }, { 1, 0, 100, -1, 100, 1 },
{ 2, 0, 9, 5, 4, 2 }, { 2, 0, 9, 5, 4, 2 },
{ 4, 0, 19, 5, 4, 4 }, { 4, 0, 19, 5, 4, 4 },
{ 2, 10, 19, 5, 4, 2 }, { 2, 10, 19, 5, 4, 2 },
{ 4, 0, 200, 50, 50, 4 }, { 4, 0, 200, 50, 50, 4 },
{ 4, 123, 323, 50, 50, 4 }, { 4, 123, 323, 50, 50, 4 },
{ 4, 0, 1201, 301, 298, 4 }, { 4, 0, 1201, 301, 298, 4 },
{ 8, 10, 236, 29, 23, 8 }, { 8, 10, 236, 29, 23, 8 },
{ 16, 0, 209, 14, 13, 15 }, { 16, 0, 209, 14, 13, 15 },
{ 24, 0, 209, 9, 2, 24 }, { 24, 0, 209, 9, 2, 24 },
{ 32, 0, 209, 7, 6, 30 }, { 32, 0, 209, 7, 6, 30 },
{ 64, 0, 209, 4, 1, 53 }, { 64, 0, 209, 4, 1, 53 },
}; };
[Theory] [Theory]
[MemberData(nameof(IterateRows_OverMinimumPixelsLimit_Data))] [MemberData(nameof(IterateRows_OverMinimumPixelsLimit_Data))]
@ -64,20 +64,24 @@ namespace SixLabors.ImageSharp.Tests.Helpers
int actualNumberOfSteps = 0; int actualNumberOfSteps = 0;
ParallelHelper.IterateRows( void RowAction(RowInterval rows)
rectangle, {
parallelSettings, Assert.True(rows.Min >= minY);
rows => Assert.True(rows.Max <= maxY);
{
Assert.True(rows.Min >= minY); int step = rows.Max - rows.Min;
Assert.True(rows.Max <= maxY); int expected = rows.Max < maxY ? expectedStepLength : expectedLastStepLength;
Interlocked.Increment(ref actualNumberOfSteps);
Assert.Equal(expected, step);
}
int step = rows.Max - rows.Min; var operation = new TestRowIntervalOperation(RowAction);
int expected = rows.Max < maxY ? expectedStepLength : expectedLastStepLength;
Interlocked.Increment(ref actualNumberOfSteps); ParallelRowIterator.IterateRows(
Assert.Equal(expected, step); rectangle,
}); in parallelSettings,
in operation);
Assert.Equal(expectedNumberOfSteps, actualNumberOfSteps); Assert.Equal(expectedNumberOfSteps, actualNumberOfSteps);
} }
@ -102,16 +106,20 @@ namespace SixLabors.ImageSharp.Tests.Helpers
int[] expectedData = Enumerable.Repeat(0, minY).Concat(Enumerable.Range(minY, maxY - minY)).ToArray(); int[] expectedData = Enumerable.Repeat(0, minY).Concat(Enumerable.Range(minY, maxY - minY)).ToArray();
var actualData = new int[maxY]; var actualData = new int[maxY];
ParallelHelper.IterateRows( void RowAction(RowInterval rows)
{
for (int y = rows.Min; y < rows.Max; y++)
{
actualData[y] = y;
}
}
var operation = new TestRowIntervalOperation(RowAction);
ParallelRowIterator.IterateRows(
rectangle, rectangle,
parallelSettings, in parallelSettings,
rows => in operation);
{
for (int y = rows.Min; y < rows.Max; y++)
{
actualData[y] = y;
}
});
Assert.Equal(expectedData, actualData); Assert.Equal(expectedData, actualData);
} }
@ -133,30 +141,28 @@ namespace SixLabors.ImageSharp.Tests.Helpers
var rectangle = new Rectangle(0, minY, 10, maxY - minY); var rectangle = new Rectangle(0, minY, 10, maxY - minY);
var bufferHashes = new ConcurrentBag<int>();
int actualNumberOfSteps = 0; 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()); void RowAction(RowInterval rows, Span<Vector4> buffer)
{
Assert.True(rows.Min >= minY);
Assert.True(rows.Max <= maxY);
int step = rows.Max - rows.Min; int step = rows.Max - rows.Min;
int expected = rows.Max < maxY ? expectedStepLength : expectedLastStepLength; int expected = rows.Max < maxY ? expectedStepLength : expectedLastStepLength;
Interlocked.Increment(ref actualNumberOfSteps); Interlocked.Increment(ref actualNumberOfSteps);
Assert.Equal(expected, step); Assert.Equal(expected, step);
}); }
Assert.Equal(expectedNumberOfSteps, actualNumberOfSteps); var operation = new TestRowIntervalOperation<Vector4>(RowAction);
ParallelRowIterator.IterateRows<TestRowIntervalOperation<Vector4>, Vector4>(
rectangle,
in parallelSettings,
in operation);
int numberOfDifferentBuffers = bufferHashes.Distinct().Count(); Assert.Equal(expectedNumberOfSteps, actualNumberOfSteps);
Assert.Equal(actualNumberOfSteps, numberOfDifferentBuffers);
} }
[Theory] [Theory]
@ -179,31 +185,35 @@ namespace SixLabors.ImageSharp.Tests.Helpers
int[] expectedData = Enumerable.Repeat(0, minY).Concat(Enumerable.Range(minY, maxY - minY)).ToArray(); int[] expectedData = Enumerable.Repeat(0, minY).Concat(Enumerable.Range(minY, maxY - minY)).ToArray();
var actualData = new int[maxY]; var actualData = new int[maxY];
ParallelHelper.IterateRowsWithTempBuffer( void RowAction(RowInterval rows, Span<Vector4> buffer)
{
for (int y = rows.Min; y < rows.Max; y++)
{
actualData[y] = y;
}
}
var operation = new TestRowIntervalOperation<Vector4>(RowAction);
ParallelRowIterator.IterateRows<TestRowIntervalOperation<Vector4>, Vector4>(
rectangle, rectangle,
parallelSettings, in parallelSettings,
(RowInterval rows, Memory<Vector4> buffer) => in operation);
{
for (int y = rows.Min; y < rows.Max; y++)
{
actualData[y] = y;
}
});
Assert.Equal(expectedData, actualData); Assert.Equal(expectedData, actualData);
} }
public static TheoryData<int, int, int, int, int, int, int> IterateRows_WithEffectiveMinimumPixelsLimit_Data = public static TheoryData<int, int, int, int, int, int, int> IterateRows_WithEffectiveMinimumPixelsLimit_Data =
new TheoryData<int, int, int, int, int, int, int> new TheoryData<int, int, int, int, int, int, int>
{ {
{ 2, 200, 50, 2, 1, -1, 2 }, { 2, 200, 50, 2, 1, -1, 2 },
{ 2, 200, 200, 1, 1, -1, 1 }, { 2, 200, 200, 1, 1, -1, 1 },
{ 4, 200, 100, 4, 2, 2, 2 }, { 4, 200, 100, 4, 2, 2, 2 },
{ 4, 300, 100, 8, 3, 3, 2 }, { 4, 300, 100, 8, 3, 3, 2 },
{ 2, 5000, 1, 4500, 1, -1, 4500 }, { 2, 5000, 1, 4500, 1, -1, 4500 },
{ 2, 5000, 1, 5000, 1, -1, 5000 }, { 2, 5000, 1, 5000, 1, -1, 5000 },
{ 2, 5000, 1, 5001, 2, 2501, 2500 }, { 2, 5000, 1, 5001, 2, 2501, 2500 },
}; };
[Theory] [Theory]
[MemberData(nameof(IterateRows_WithEffectiveMinimumPixelsLimit_Data))] [MemberData(nameof(IterateRows_WithEffectiveMinimumPixelsLimit_Data))]
@ -225,20 +235,24 @@ namespace SixLabors.ImageSharp.Tests.Helpers
int actualNumberOfSteps = 0; int actualNumberOfSteps = 0;
ParallelHelper.IterateRows( void RowAction(RowInterval rows)
rectangle, {
parallelSettings, Assert.True(rows.Min >= 0);
rows => Assert.True(rows.Max <= height);
{
Assert.True(rows.Min >= 0);
Assert.True(rows.Max <= height);
int step = rows.Max - rows.Min; int step = rows.Max - rows.Min;
int expected = rows.Max < height ? expectedStepLength : expectedLastStepLength; int expected = rows.Max < height ? expectedStepLength : expectedLastStepLength;
Interlocked.Increment(ref actualNumberOfSteps); Interlocked.Increment(ref actualNumberOfSteps);
Assert.Equal(expected, step); Assert.Equal(expected, step);
}); }
var operation = new TestRowIntervalOperation(RowAction);
ParallelRowIterator.IterateRows(
rectangle,
in parallelSettings,
in operation);
Assert.Equal(expectedNumberOfSteps, actualNumberOfSteps); Assert.Equal(expectedNumberOfSteps, actualNumberOfSteps);
} }
@ -262,33 +276,38 @@ namespace SixLabors.ImageSharp.Tests.Helpers
var rectangle = new Rectangle(0, 0, width, height); var rectangle = new Rectangle(0, 0, width, height);
int actualNumberOfSteps = 0; 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; void RowAction(RowInterval rows, Span<Vector4> buffer)
int expected = rows.Max < height ? expectedStepLength : expectedLastStepLength; {
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);
}
Interlocked.Increment(ref actualNumberOfSteps); var operation = new TestRowIntervalOperation<Vector4>(RowAction);
Assert.Equal(expected, step);
}); ParallelRowIterator.IterateRows<TestRowIntervalOperation<Vector4>, Vector4>(
rectangle,
in parallelSettings,
in operation);
Assert.Equal(expectedNumberOfSteps, actualNumberOfSteps); Assert.Equal(expectedNumberOfSteps, actualNumberOfSteps);
} }
public static readonly TheoryData<int, int, int, int, int, int, int> IterateRectangularBuffer_Data = public static readonly TheoryData<int, int, int, int, int, int, int> IterateRectangularBuffer_Data =
new TheoryData<int, int, int, int, int, int, int> new TheoryData<int, int, int, int, int, int, int>
{ {
{ 8, 582, 453, 10, 10, 291, 226 }, // boundary data from DetectEdgesTest.DetectEdges_InBox { 8, 582, 453, 10, 10, 291, 226 }, // boundary data from DetectEdgesTest.DetectEdges_InBox
{ 2, 582, 453, 10, 10, 291, 226 }, { 2, 582, 453, 10, 10, 291, 226 },
{ 16, 582, 453, 10, 10, 291, 226 }, { 16, 582, 453, 10, 10, 291, 226 },
{ 16, 582, 453, 10, 10, 1, 226 }, { 16, 582, 453, 10, 10, 1, 226 },
{ 16, 1, 453, 0, 10, 1, 226 }, { 16, 1, 453, 0, 10, 1, 226 },
}; };
[Theory] [Theory]
[MemberData(nameof(IterateRectangularBuffer_Data))] [MemberData(nameof(IterateRectangularBuffer_Data))]
@ -325,17 +344,21 @@ namespace SixLabors.ImageSharp.Tests.Helpers
// Fill actual data using IterateRows: // Fill actual data using IterateRows:
var settings = new ParallelExecutionSettings(maxDegreeOfParallelism, memoryAllocator); var settings = new ParallelExecutionSettings(maxDegreeOfParallelism, memoryAllocator);
ParallelHelper.IterateRows( void RowAction(RowInterval rows)
{
this.output.WriteLine(rows.ToString());
for (int y = rows.Min; y < rows.Max; y++)
{
FillRow(y, actual);
}
}
var operation = new TestRowIntervalOperation(RowAction);
ParallelRowIterator.IterateRows(
rect, rect,
settings, settings,
rows => in operation);
{
this.output.WriteLine(rows.ToString());
for (int y = rows.Min; y < rows.Max; y++)
{
FillRow(y, actual);
}
});
// Assert: // Assert:
TestImageExtensions.CompareBuffers(expected.GetSpan(), actual.GetSpan()); TestImageExtensions.CompareBuffers(expected.GetSpan(), actual.GetSpan());
@ -353,8 +376,14 @@ namespace SixLabors.ImageSharp.Tests.Helpers
var rect = new Rectangle(0, 0, width, height); var rect = new Rectangle(0, 0, width, height);
void RowAction(RowInterval rows)
{
}
var operation = new TestRowIntervalOperation(RowAction);
ArgumentOutOfRangeException ex = Assert.Throws<ArgumentOutOfRangeException>( ArgumentOutOfRangeException ex = Assert.Throws<ArgumentOutOfRangeException>(
() => ParallelHelper.IterateRows(rect, parallelSettings, rows => { })); () => ParallelRowIterator.IterateRows(rect, in parallelSettings, in operation));
Assert.Contains(width <= 0 ? "Width" : "Height", ex.Message); Assert.Contains(width <= 0 ? "Width" : "Height", ex.Message);
} }
@ -370,10 +399,38 @@ namespace SixLabors.ImageSharp.Tests.Helpers
var rect = new Rectangle(0, 0, width, height); var rect = new Rectangle(0, 0, width, height);
void RowAction(RowInterval rows, Span<Rgba32> memory)
{
}
var operation = new TestRowIntervalOperation<Rgba32>(RowAction);
ArgumentOutOfRangeException ex = Assert.Throws<ArgumentOutOfRangeException>( ArgumentOutOfRangeException ex = Assert.Throws<ArgumentOutOfRangeException>(
() => ParallelHelper.IterateRowsWithTempBuffer<Rgba32>(rect, parallelSettings, (rows, memory) => { })); () => ParallelRowIterator.IterateRows<TestRowIntervalOperation<Rgba32>, Rgba32>(rect, in parallelSettings, in operation));
Assert.Contains(width <= 0 ? "Width" : "Height", ex.Message); Assert.Contains(width <= 0 ? "Width" : "Height", ex.Message);
} }
private readonly struct TestRowIntervalOperation : IRowIntervalOperation
{
private readonly Action<RowInterval> action;
public TestRowIntervalOperation(Action<RowInterval> action)
=> this.action = action;
public void Invoke(in RowInterval rows) => this.action(rows);
}
private readonly struct TestRowIntervalOperation<TBuffer> : IRowIntervalOperation<TBuffer>
where TBuffer : unmanaged
{
private readonly RowIntervalAction<TBuffer> action;
public TestRowIntervalOperation(RowIntervalAction<TBuffer> action)
=> this.action = action;
public void Invoke(in RowInterval rows, Span<TBuffer> span)
=> this.action(rows, span);
}
} }
} }

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

@ -7,7 +7,6 @@ using System.IO;
using System.Numerics; using System.Numerics;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Advanced.ParallelUtils;
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -703,25 +702,43 @@ namespace SixLabors.ImageSharp.Tests
{ {
Rectangle sourceRectangle = this.SourceRectangle; Rectangle sourceRectangle = this.SourceRectangle;
Configuration configuration = this.Configuration; Configuration configuration = this.Configuration;
ParallelHelper.IterateRowsWithTempBuffer<Vector4>(
sourceRectangle, var operation = new RowOperation(configuration, sourceRectangle, source);
ParallelRowIterator.IterateRows<RowOperation, Vector4>(
configuration, configuration,
(rows, temp) => sourceRectangle,
in operation);
}
private readonly struct RowOperation : IRowIntervalOperation<Vector4>
{
private readonly Configuration configuration;
private readonly Rectangle bounds;
private readonly ImageFrame<TPixel> source;
public RowOperation(Configuration configuration, Rectangle bounds, ImageFrame<TPixel> source)
{
this.configuration = configuration;
this.bounds = bounds;
this.source = source;
}
public void Invoke(in RowInterval rows, Span<Vector4> span)
{
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> rowSpan = this.source.GetPixelRowSpan(y).Slice(this.bounds.Left, this.bounds.Width);
PixelOperations<TPixel>.Instance.ToVector4(this.configuration, rowSpan, span, PixelConversionModifiers.Scale);
for (int i = 0; i < span.Length; i++)
{ {
Span<Vector4> tempSpan = temp.Span; ref Vector4 v = ref span[i];
for (int y = rows.Min; y < rows.Max; y++) v.W = 1F;
{ }
Span<TPixel> rowSpan = source.GetPixelRowSpan(y).Slice(sourceRectangle.Left, sourceRectangle.Width);
PixelOperations<TPixel>.Instance.ToVector4(configuration, rowSpan, tempSpan, PixelConversionModifiers.Scale); PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, span, rowSpan, PixelConversionModifiers.Scale);
for (int i = 0; i < tempSpan.Length; i++) }
{ }
ref Vector4 v = ref tempSpan[i];
v.W = 1F;
}
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, tempSpan, rowSpan, PixelConversionModifiers.Scale);
}
});
} }
} }
} }

Loading…
Cancel
Save