mirror of https://github.com/SixLabors/ImageSharp
Browse Source
* Added base BokehBlurProcessor class, and kernel parameters * Added method to calculate the kernel parameters * Switched to float, added method to create the 1D kernels * Added complex kernels normalization * Added BokehBlurExtensions class * Added the Complex64 struct type * Switched to Complex64 in the BokehBlurProcessor * Added caching system for the bokeh processor parameters * Added WeightedSum method to the Complex64 type * Added IEquatable<T> interface to the Complex64 type * New complex types added * Added method to reshape a DenseMatrix<T> with no copies * Added bokeh convolution first pass (WIP) * Added second bokeh convolution pass (WIP) * Added image sum pass to the bokeh processor (WIP) * Minor bug fixes (WIP) * Switched to Vector4 processing in the bokeh computation * Minor tweaks * Added Unit test for the bokeh kernel components * Minor performance improvements * Minor code refactoring, added gamma parameter (WIP) * Removed unused temp buffers in the bokeh processing * Gamma highlight processing implemented * Speed optimizations, fixed partials computations in target rectangle * Increased epsilon value in the unit tests * Fixed for alpha transparency blur * Fixed a bug when only blurring a target rectangle * Added bokeh blur image tests (WIP) * Added IXunitSerializable interface to the test info class * culture independent parsing in BokehBlurTest.cs * Performance optimizations in the bokeh processor * Reduced number of memory allocations, fixed bug with multiple components * Initialization and other speed improvements * More initialization speed improvements * Replaced LINQ with manual loop * Added BokehBlur overload to just specify the target bounds * Speed optimizations to the bokeh 1D convolutions * More speed optimizations to the bokeh processor * Fixed code style and Complex64.ToString method * Fixed processing buffer initialization * Minor performance improvements * FIxed issue when applying bokeh blur to specific bounds * Minor speed optimizaations * Minor code refactoring * Fixed convolution upper bound in second 1D pass * improve BokehBlurTest coverage * use Gray8 instead of Alpha8 * Adjusted guard position in bokeh processor constructor * Added BokehBlurParameters struct * Added BokehBlurKernelData struct * Minor code refactoring * Fixed API change build errors * Bug fixes with the pixel premultiplication steps * Removed unwanted unpremultiplication pass * Removed unused using directives * Fixed missing using directives in conditional branches * Update from latest upstream master * Update Block8x8F.Generated.cs * Update GenericBlock8x8.Generated.cs * Manual checking for files with LF (see gitter) * Removed unused using directive * Added IEquatable<ComplexVector4> interface * Added IEquatable<BokehBlurParameters> interface * Moved bokeh blur parameters types * Added reference to original source code * Complex convolution methods moved to another class * Switched to MathF in the bokeh blur processor * Switched to Vector4.Clamp * Added Buffer2D<T>.Slice API * Added BokehBlurExecutionMode enum * Added new bokeh blur processor constructors * Added new bokeh blur extension overloads with execution mode * Code refactoring in preparation for the execution mode switch * Implemented execution mode switch in the bokeh processor * Moved BokehBlurExecutionMode struct * Removed unused using directives * Minor code refactoring * More minor code refactoring * Update External * Fix undisposed buffers * Bokeh blur processor cache switched to concurrent dictionary * Minor code refactoringpull/958/head
committed by
James Jackson-South
68 changed files with 1382 additions and 98 deletions
@ -0,0 +1,105 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Runtime.InteropServices; |
|||
|
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.ImageSharp.Primitives; |
|||
|
|||
namespace SixLabors.ImageSharp |
|||
{ |
|||
/// <summary>
|
|||
/// Extension methods for <see cref="Buffer2D{T}"/>.
|
|||
/// TODO: One day rewrite all this to use SIMD intrinsics. There's a lot of scope for improvement.
|
|||
/// </summary>
|
|||
internal static class Buffer2DUtils |
|||
{ |
|||
/// <summary>
|
|||
/// Computes the sum of vectors in <paramref name="targetRow"/> weighted by the kernel weight values.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
/// <param name="kernel">The 1D convolution kernel.</param>
|
|||
/// <param name="sourcePixels">The source frame.</param>
|
|||
/// <param name="targetRow">The target row.</param>
|
|||
/// <param name="row">The current row.</param>
|
|||
/// <param name="column">The current column.</param>
|
|||
/// <param name="minRow">The minimum working area row.</param>
|
|||
/// <param name="maxRow">The maximum working area row.</param>
|
|||
/// <param name="minColumn">The minimum working area column.</param>
|
|||
/// <param name="maxColumn">The maximum working area column.</param>
|
|||
public static void Convolve4<TPixel>( |
|||
Span<Complex64> kernel, |
|||
Buffer2D<TPixel> sourcePixels, |
|||
Span<ComplexVector4> targetRow, |
|||
int row, |
|||
int column, |
|||
int minRow, |
|||
int maxRow, |
|||
int minColumn, |
|||
int maxColumn) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
ComplexVector4 vector = default; |
|||
int kernelLength = kernel.Length; |
|||
int radiusY = kernelLength >> 1; |
|||
int sourceOffsetColumnBase = column + minColumn; |
|||
ref Complex64 baseRef = ref MemoryMarshal.GetReference(kernel); |
|||
|
|||
for (int i = 0; i < kernelLength; i++) |
|||
{ |
|||
int offsetY = (row + i - radiusY).Clamp(minRow, maxRow); |
|||
int offsetX = sourceOffsetColumnBase.Clamp(minColumn, maxColumn); |
|||
Span<TPixel> sourceRowSpan = sourcePixels.GetRowSpan(offsetY); |
|||
var currentColor = sourceRowSpan[offsetX].ToVector4(); |
|||
|
|||
vector.Sum(Unsafe.Add(ref baseRef, i) * currentColor); |
|||
} |
|||
|
|||
targetRow[column] = vector; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Computes the sum of vectors in <paramref name="targetRow"/> weighted by the kernel weight values.
|
|||
/// </summary>
|
|||
/// <param name="kernel">The 1D convolution kernel.</param>
|
|||
/// <param name="sourceValues">The source frame.</param>
|
|||
/// <param name="targetRow">The target row.</param>
|
|||
/// <param name="row">The current row.</param>
|
|||
/// <param name="column">The current column.</param>
|
|||
/// <param name="minRow">The minimum working area row.</param>
|
|||
/// <param name="maxRow">The maximum working area row.</param>
|
|||
/// <param name="minColumn">The minimum working area column.</param>
|
|||
/// <param name="maxColumn">The maximum working area column.</param>
|
|||
public static void Convolve4( |
|||
Span<Complex64> kernel, |
|||
Buffer2D<ComplexVector4> sourceValues, |
|||
Span<ComplexVector4> targetRow, |
|||
int row, |
|||
int column, |
|||
int minRow, |
|||
int maxRow, |
|||
int minColumn, |
|||
int maxColumn) |
|||
{ |
|||
ComplexVector4 vector = default; |
|||
int kernelLength = kernel.Length; |
|||
int radiusX = kernelLength >> 1; |
|||
int sourceOffsetColumnBase = column + minColumn; |
|||
|
|||
int offsetY = row.Clamp(minRow, maxRow); |
|||
ref ComplexVector4 sourceRef = ref MemoryMarshal.GetReference(sourceValues.GetRowSpan(offsetY)); |
|||
ref Complex64 baseRef = ref MemoryMarshal.GetReference(kernel); |
|||
|
|||
for (int x = 0; x < kernelLength; x++) |
|||
{ |
|||
int offsetX = (sourceOffsetColumnBase + x - radiusX).Clamp(minColumn, maxColumn); |
|||
vector.Sum(Unsafe.Add(ref baseRef, x) * Unsafe.Add(ref sourceRef, offsetX)); |
|||
} |
|||
|
|||
targetRow[column] = vector; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,95 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Numerics; |
|||
using System.Runtime.CompilerServices; |
|||
|
|||
namespace SixLabors.ImageSharp.Primitives |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a complex number, where the real and imaginary parts are stored as <see cref="float"/> values.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This is a more efficient version of the <see cref="Complex64"/> type.
|
|||
/// </remarks>
|
|||
internal readonly struct Complex64 : IEquatable<Complex64> |
|||
{ |
|||
/// <summary>
|
|||
/// The real part of the complex number
|
|||
/// </summary>
|
|||
public readonly float Real; |
|||
|
|||
/// <summary>
|
|||
/// The imaginary part of the complex number
|
|||
/// </summary>
|
|||
public readonly float Imaginary; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="Complex64"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="real">The real part in the complex number.</param>
|
|||
/// <param name="imaginary">The imaginary part in the complex number.</param>
|
|||
public Complex64(float real, float imaginary) |
|||
{ |
|||
this.Real = real; |
|||
this.Imaginary = imaginary; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Performs the multiplication operation between a <see cref="Complex64"/> intance and a <see cref="float"/> scalar.
|
|||
/// </summary>
|
|||
/// <param name="value">The <see cref="Complex64"/> value to multiply.</param>
|
|||
/// <param name="scalar">The <see cref="float"/> scalar to use to multiply the <see cref="Complex64"/> value.</param>
|
|||
/// <returns>The <see cref="Complex64"/> result</returns>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public static Complex64 operator *(Complex64 value, float scalar) => new Complex64(value.Real * scalar, value.Imaginary * scalar); |
|||
|
|||
/// <summary>
|
|||
/// Performs the multiplication operation between a <see cref="Complex64"/> intance and a <see cref="Vector4"/>.
|
|||
/// </summary>
|
|||
/// <param name="value">The <see cref="Complex64"/> value to multiply.</param>
|
|||
/// <param name="vector">The <see cref="Vector4"/> instance to use to multiply the <see cref="Complex64"/> value.</param>
|
|||
/// <returns>The <see cref="Complex64"/> result</returns>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public static ComplexVector4 operator *(Complex64 value, Vector4 vector) |
|||
{ |
|||
return new ComplexVector4 { Real = vector * value.Real, Imaginary = vector * value.Imaginary }; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Performs the multiplication operation between a <see cref="Complex64"/> intance and a <see cref="ComplexVector4"/>.
|
|||
/// </summary>
|
|||
/// <param name="value">The <see cref="Complex64"/> value to multiply.</param>
|
|||
/// <param name="vector">The <see cref="ComplexVector4"/> instance to use to multiply the <see cref="Complex64"/> value.</param>
|
|||
/// <returns>The <see cref="Complex64"/> result</returns>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public static ComplexVector4 operator *(Complex64 value, ComplexVector4 vector) |
|||
{ |
|||
Vector4 real = (value.Real * vector.Real) - (value.Imaginary * vector.Imaginary); |
|||
Vector4 imaginary = (value.Real * vector.Imaginary) + (value.Imaginary * vector.Real); |
|||
return new ComplexVector4 { Real = real, Imaginary = imaginary }; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public bool Equals(Complex64 other) |
|||
{ |
|||
return this.Real.Equals(other.Real) && this.Imaginary.Equals(other.Imaginary); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool Equals(object obj) => obj is Complex64 other && this.Equals(other); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override int GetHashCode() |
|||
{ |
|||
unchecked |
|||
{ |
|||
return (this.Real.GetHashCode() * 397) ^ this.Imaginary.GetHashCode(); |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override string ToString() => $"{this.Real}{(this.Imaginary >= 0 ? "+" : string.Empty)}{this.Imaginary}j"; |
|||
} |
|||
} |
|||
@ -0,0 +1,63 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Numerics; |
|||
using System.Runtime.CompilerServices; |
|||
|
|||
namespace SixLabors.ImageSharp.Primitives |
|||
{ |
|||
/// <summary>
|
|||
/// A vector with 4 values of type <see cref="Complex64"/>.
|
|||
/// </summary>
|
|||
internal struct ComplexVector4 : IEquatable<ComplexVector4> |
|||
{ |
|||
/// <summary>
|
|||
/// The real part of the complex vector
|
|||
/// </summary>
|
|||
public Vector4 Real; |
|||
|
|||
/// <summary>
|
|||
/// The imaginary part of the complex number
|
|||
/// </summary>
|
|||
public Vector4 Imaginary; |
|||
|
|||
/// <summary>
|
|||
/// Sums the values in the input <see cref="ComplexVector4"/> to the current instance
|
|||
/// </summary>
|
|||
/// <param name="value">The input <see cref="ComplexVector4"/> to sum</param>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void Sum(in ComplexVector4 value) |
|||
{ |
|||
this.Real += value.Real; |
|||
this.Imaginary += value.Imaginary; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Performs a weighted sum on the current instance according to the given parameters
|
|||
/// </summary>
|
|||
/// <param name="a">The 'a' parameter, for the real component</param>
|
|||
/// <param name="b">The 'b' parameter, for the imaginary component</param>
|
|||
/// <returns>The resulting <see cref="Vector4"/> value</returns>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public Vector4 WeightedSum(float a, float b) => (this.Real * a) + (this.Imaginary * b); |
|||
|
|||
/// <inheritdoc/>
|
|||
public bool Equals(ComplexVector4 other) |
|||
{ |
|||
return this.Real.Equals(other.Real) && this.Imaginary.Equals(other.Imaginary); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool Equals(object obj) => obj is ComplexVector4 other && this.Equals(other); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override int GetHashCode() |
|||
{ |
|||
unchecked |
|||
{ |
|||
return (this.Real.GetHashCode() * 397) ^ this.Imaginary.GetHashCode(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using SixLabors.ImageSharp.Processing.Processors.Convolution; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing |
|||
{ |
|||
/// <summary>
|
|||
/// An <see langword="enum"/> that indicates execution options for the <see cref="BokehBlurProcessor"/>.
|
|||
/// </summary>
|
|||
public enum BokehBlurExecutionMode |
|||
{ |
|||
/// <summary>
|
|||
/// Indicates that the maximum performance should be prioritized over memory usage.
|
|||
/// </summary>
|
|||
PreferMaximumPerformance, |
|||
|
|||
/// <summary>
|
|||
/// Indicates that the memory usage should be prioritized over raw performance.
|
|||
/// </summary>
|
|||
PreferLowMemoryUsage |
|||
} |
|||
} |
|||
@ -0,0 +1,106 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using SixLabors.ImageSharp.Processing.Processors.Convolution; |
|||
using SixLabors.Primitives; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing |
|||
{ |
|||
/// <summary>
|
|||
/// Adds bokeh blurring extensions to the <see cref="Image{TPixel}"/> type.
|
|||
/// </summary>
|
|||
public static class BokehBlurExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// Applies a bokeh blur to the image.
|
|||
/// </summary>
|
|||
/// <param name="source">The image this method extends.</param>
|
|||
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
|
|||
public static IImageProcessingContext BokehBlur(this IImageProcessingContext source) |
|||
=> source.ApplyProcessor(new BokehBlurProcessor()); |
|||
|
|||
/// <summary>
|
|||
/// Applies a bokeh blur to the image.
|
|||
/// </summary>
|
|||
/// <param name="source">The image this method extends.</param>
|
|||
/// <param name="executionMode">The execution mode to use when applying the processor.</param>
|
|||
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
|
|||
public static IImageProcessingContext BokehBlur(this IImageProcessingContext source, BokehBlurExecutionMode executionMode) |
|||
=> source.ApplyProcessor(new BokehBlurProcessor(executionMode)); |
|||
|
|||
/// <summary>
|
|||
/// Applies a bokeh blur to the image.
|
|||
/// </summary>
|
|||
/// <param name="source">The image this method extends.</param>
|
|||
/// <param name="radius">The 'radius' value representing the size of the area to sample.</param>
|
|||
/// <param name="components">The 'components' value representing the number of kernels to use to approximate the bokeh effect.</param>
|
|||
/// <param name="gamma">The gamma highlight factor to use to emphasize bright spots in the source image</param>
|
|||
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
|
|||
public static IImageProcessingContext BokehBlur(this IImageProcessingContext source, int radius, int components, float gamma) |
|||
=> source.ApplyProcessor(new BokehBlurProcessor(radius, components, gamma)); |
|||
|
|||
/// <summary>
|
|||
/// Applies a bokeh blur to the image.
|
|||
/// </summary>
|
|||
/// <param name="source">The image this method extends.</param>
|
|||
/// <param name="radius">The 'radius' value representing the size of the area to sample.</param>
|
|||
/// <param name="components">The 'components' value representing the number of kernels to use to approximate the bokeh effect.</param>
|
|||
/// <param name="gamma">The gamma highlight factor to use to emphasize bright spots in the source image</param>
|
|||
/// <param name="executionMode">The execution mode to use when applying the processor.</param>
|
|||
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
|
|||
public static IImageProcessingContext BokehBlur(this IImageProcessingContext source, int radius, int components, float gamma, BokehBlurExecutionMode executionMode) |
|||
=> source.ApplyProcessor(new BokehBlurProcessor(radius, components, gamma, executionMode)); |
|||
|
|||
/// <summary>
|
|||
/// Applies a bokeh blur to the image.
|
|||
/// </summary>
|
|||
/// <param name="source">The image this method extends.</param>
|
|||
/// <param name="rectangle">
|
|||
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
|
|||
/// </param>
|
|||
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
|
|||
public static IImageProcessingContext BokehBlur(this IImageProcessingContext source, Rectangle rectangle) |
|||
=> source.ApplyProcessor(new BokehBlurProcessor(), rectangle); |
|||
|
|||
/// <summary>
|
|||
/// Applies a bokeh blur to the image.
|
|||
/// </summary>
|
|||
/// <param name="source">The image this method extends.</param>
|
|||
/// <param name="rectangle">
|
|||
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
|
|||
/// </param>
|
|||
/// <param name="executionMode">The execution mode to use when applying the processor.</param>
|
|||
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
|
|||
public static IImageProcessingContext BokehBlur(this IImageProcessingContext source, Rectangle rectangle, BokehBlurExecutionMode executionMode) |
|||
=> source.ApplyProcessor(new BokehBlurProcessor(executionMode), rectangle); |
|||
|
|||
/// <summary>
|
|||
/// Applies a bokeh blur to the image.
|
|||
/// </summary>
|
|||
/// <param name="source">The image this method extends.</param>
|
|||
/// <param name="radius">The 'radius' value representing the size of the area to sample.</param>
|
|||
/// <param name="components">The 'components' value representing the number of kernels to use to approximate the bokeh effect.</param>
|
|||
/// <param name="gamma">The gamma highlight factor to use to emphasize bright spots in the source image</param>
|
|||
/// <param name="rectangle">
|
|||
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
|
|||
/// </param>
|
|||
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
|
|||
public static IImageProcessingContext BokehBlur(this IImageProcessingContext source, int radius, int components, float gamma, Rectangle rectangle) |
|||
=> source.ApplyProcessor(new BokehBlurProcessor(radius, components, gamma), rectangle); |
|||
|
|||
/// <summary>
|
|||
/// Applies a bokeh blur to the image.
|
|||
/// </summary>
|
|||
/// <param name="source">The image this method extends.</param>
|
|||
/// <param name="radius">The 'radius' value representing the size of the area to sample.</param>
|
|||
/// <param name="components">The 'components' value representing the number of kernels to use to approximate the bokeh effect.</param>
|
|||
/// <param name="gamma">The gamma highlight factor to use to emphasize bright spots in the source image</param>
|
|||
/// <param name="executionMode">The execution mode to use when applying the processor.</param>
|
|||
/// <param name="rectangle">
|
|||
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
|
|||
/// </param>
|
|||
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
|
|||
public static IImageProcessingContext BokehBlur(this IImageProcessingContext source, int radius, int components, float gamma, BokehBlurExecutionMode executionMode, Rectangle rectangle) |
|||
=> source.ApplyProcessor(new BokehBlurProcessor(radius, components, gamma, executionMode), rectangle); |
|||
} |
|||
} |
|||
@ -0,0 +1,121 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Convolution |
|||
{ |
|||
/// <summary>
|
|||
/// Applies bokeh blur processing to the image.
|
|||
/// </summary>
|
|||
public sealed class BokehBlurProcessor : IImageProcessor |
|||
{ |
|||
/// <summary>
|
|||
/// The default radius used by the parameterless constructor.
|
|||
/// </summary>
|
|||
public const int DefaultRadius = 32; |
|||
|
|||
/// <summary>
|
|||
/// The default component count used by the parameterless constructor.
|
|||
/// </summary>
|
|||
public const int DefaultComponents = 2; |
|||
|
|||
/// <summary>
|
|||
/// The default gamma used by the parameterless constructor.
|
|||
/// </summary>
|
|||
public const float DefaultGamma = 3F; |
|||
|
|||
/// <summary>
|
|||
/// The default execution mode used by the parameterless constructor.
|
|||
/// </summary>
|
|||
public const BokehBlurExecutionMode DefaultExecutionMode = BokehBlurExecutionMode.PreferLowMemoryUsage; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="BokehBlurProcessor"/> class.
|
|||
/// </summary>
|
|||
public BokehBlurProcessor() |
|||
: this(DefaultRadius, DefaultComponents, DefaultGamma, DefaultExecutionMode) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="BokehBlurProcessor"/> class.
|
|||
/// </summary>
|
|||
/// <param name="executionMode">
|
|||
/// The execution mode to use when applying the processor.
|
|||
/// </param>
|
|||
public BokehBlurProcessor(BokehBlurExecutionMode executionMode) |
|||
: this(DefaultRadius, DefaultComponents, DefaultGamma, executionMode) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="BokehBlurProcessor"/> class.
|
|||
/// </summary>
|
|||
/// <param name="radius">
|
|||
/// The 'radius' value representing the size of the area to sample.
|
|||
/// </param>
|
|||
/// <param name="components">
|
|||
/// The number of components to use to approximate the original 2D bokeh blur convolution kernel.
|
|||
/// </param>
|
|||
/// <param name="gamma">
|
|||
/// The gamma highlight factor to use to further process the image.
|
|||
/// </param>
|
|||
public BokehBlurProcessor(int radius, int components, float gamma) |
|||
: this(radius, components, gamma, DefaultExecutionMode) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="BokehBlurProcessor"/> class.
|
|||
/// </summary>
|
|||
/// <param name="radius">
|
|||
/// The 'radius' value representing the size of the area to sample.
|
|||
/// </param>
|
|||
/// <param name="components">
|
|||
/// The number of components to use to approximate the original 2D bokeh blur convolution kernel.
|
|||
/// </param>
|
|||
/// <param name="gamma">
|
|||
/// The gamma highlight factor to use to further process the image.
|
|||
/// </param>
|
|||
/// <param name="executionMode">
|
|||
/// The execution mode to use when applying the processor.
|
|||
/// </param>
|
|||
public BokehBlurProcessor(int radius, int components, float gamma, BokehBlurExecutionMode executionMode) |
|||
{ |
|||
Guard.MustBeGreaterThanOrEqualTo(gamma, 1, nameof(gamma)); |
|||
|
|||
this.Radius = radius; |
|||
this.Components = components; |
|||
this.Gamma = gamma; |
|||
this.ExecutionMode = executionMode; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the radius.
|
|||
/// </summary>
|
|||
public int Radius { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of components.
|
|||
/// </summary>
|
|||
public int Components { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the gamma highlight factor to use when applying the effect.
|
|||
/// </summary>
|
|||
public float Gamma { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the exection mode to use when applying the effect.
|
|||
/// </summary>
|
|||
public BokehBlurExecutionMode ExecutionMode { get; } |
|||
|
|||
/// <inheritdoc />
|
|||
public IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>() |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
return new BokehBlurProcessor<TPixel>(this); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,585 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Collections.Concurrent; |
|||
using System.Collections.Generic; |
|||
using System.Numerics; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Runtime.InteropServices; |
|||
|
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.ImageSharp.ParallelUtils; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.ImageSharp.Primitives; |
|||
using SixLabors.ImageSharp.Processing.Processors.Convolution.Parameters; |
|||
using SixLabors.Memory; |
|||
using SixLabors.Primitives; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Convolution |
|||
{ |
|||
/// <summary>
|
|||
/// Applies bokeh blur processing to the image.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
/// <remarks>This processor is based on the code from Mike Pound, see <a href="https://github.com/mikepound/convolve">github.com/mikepound/convolve</a>.</remarks>
|
|||
internal class BokehBlurProcessor<TPixel> : ImageProcessor<TPixel> |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
/// <summary>
|
|||
/// The kernel radius.
|
|||
/// </summary>
|
|||
private readonly int radius; |
|||
|
|||
/// <summary>
|
|||
/// The gamma highlight factor to use when applying the effect
|
|||
/// </summary>
|
|||
private readonly float gamma; |
|||
|
|||
/// <summary>
|
|||
/// The execution mode to use when applying the effect
|
|||
/// </summary>
|
|||
private readonly BokehBlurExecutionMode executionMode; |
|||
|
|||
/// <summary>
|
|||
/// The maximum size of the kernel in either direction
|
|||
/// </summary>
|
|||
private readonly int kernelSize; |
|||
|
|||
/// <summary>
|
|||
/// The number of components to use when applying the bokeh blur
|
|||
/// </summary>
|
|||
private readonly int componentsCount; |
|||
|
|||
/// <summary>
|
|||
/// The kernel parameters to use for the current instance (a: X, b: Y, A: Z, B: W)
|
|||
/// </summary>
|
|||
private readonly Vector4[] kernelParameters; |
|||
|
|||
/// <summary>
|
|||
/// The kernel components for the current instance
|
|||
/// </summary>
|
|||
private readonly Complex64[][] kernels; |
|||
|
|||
/// <summary>
|
|||
/// The scaling factor for kernel values
|
|||
/// </summary>
|
|||
private readonly float kernelsScale; |
|||
|
|||
/// <summary>
|
|||
/// The mapping of initialized complex kernels and parameters, to speed up the initialization of new <see cref="BokehBlurProcessor{TPixel}"/> instances
|
|||
/// </summary>
|
|||
private static readonly ConcurrentDictionary<BokehBlurParameters, BokehBlurKernelData> Cache = new ConcurrentDictionary<BokehBlurParameters, BokehBlurKernelData>(); |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="BokehBlurProcessor{TPixel}"/> class.
|
|||
/// </summary>
|
|||
/// <param name="definition">The <see cref="BoxBlurProcessor"/> defining the processor parameters.</param>
|
|||
public BokehBlurProcessor(BokehBlurProcessor definition) |
|||
{ |
|||
this.radius = definition.Radius; |
|||
this.kernelSize = (this.radius * 2) + 1; |
|||
this.componentsCount = definition.Components; |
|||
this.gamma = definition.Gamma; |
|||
this.executionMode = definition.ExecutionMode; |
|||
|
|||
// Reuse the initialized values from the cache, if possible
|
|||
var parameters = new BokehBlurParameters(this.radius, this.componentsCount); |
|||
if (Cache.TryGetValue(parameters, out BokehBlurKernelData info)) |
|||
{ |
|||
this.kernelParameters = info.Parameters; |
|||
this.kernelsScale = info.Scale; |
|||
this.kernels = info.Kernels; |
|||
} |
|||
else |
|||
{ |
|||
// Initialize the complex kernels and parameters with the current arguments
|
|||
(this.kernelParameters, this.kernelsScale) = this.GetParameters(); |
|||
this.kernels = this.CreateComplexKernels(); |
|||
this.NormalizeKernels(); |
|||
|
|||
// Store them in the cache for future use
|
|||
Cache.TryAdd(parameters, new BokehBlurKernelData(this.kernelParameters, this.kernelsScale, this.kernels)); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the complex kernels to use to apply the blur for the current instance
|
|||
/// </summary>
|
|||
public IReadOnlyList<Complex64[]> Kernels => this.kernels; |
|||
|
|||
/// <summary>
|
|||
/// Gets the kernel parameters used to compute the pixel values from each complex pixel
|
|||
/// </summary>
|
|||
public IReadOnlyList<Vector4> KernelParameters => this.kernelParameters; |
|||
|
|||
/// <summary>
|
|||
/// Gets the kernel scales to adjust the component values in each kernel
|
|||
/// </summary>
|
|||
private static IReadOnlyList<float> KernelScales { get; } = new[] { 1.4f, 1.2f, 1.2f, 1.2f, 1.2f, 1.2f }; |
|||
|
|||
/// <summary>
|
|||
/// Gets the available bokeh blur kernel parameters
|
|||
/// </summary>
|
|||
private static IReadOnlyList<Vector4[]> KernelComponents { get; } = new[] |
|||
{ |
|||
// 1 component
|
|||
new[] { new Vector4(0.862325f, 1.624835f, 0.767583f, 1.862321f) }, |
|||
|
|||
// 2 components
|
|||
new[] |
|||
{ |
|||
new Vector4(0.886528f, 5.268909f, 0.411259f, -0.548794f), |
|||
new Vector4(1.960518f, 1.558213f, 0.513282f, 4.56111f) |
|||
}, |
|||
|
|||
// 3 components
|
|||
new[] |
|||
{ |
|||
new Vector4(2.17649f, 5.043495f, 1.621035f, -2.105439f), |
|||
new Vector4(1.019306f, 9.027613f, -0.28086f, -0.162882f), |
|||
new Vector4(2.81511f, 1.597273f, -0.366471f, 10.300301f) |
|||
}, |
|||
|
|||
// 4 components
|
|||
new[] |
|||
{ |
|||
new Vector4(4.338459f, 1.553635f, -5.767909f, 46.164397f), |
|||
new Vector4(3.839993f, 4.693183f, 9.795391f, -15.227561f), |
|||
new Vector4(2.791880f, 8.178137f, -3.048324f, 0.302959f), |
|||
new Vector4(1.342190f, 12.328289f, 0.010001f, 0.244650f) |
|||
}, |
|||
|
|||
// 5 components
|
|||
new[] |
|||
{ |
|||
new Vector4(4.892608f, 1.685979f, -22.356787f, 85.91246f), |
|||
new Vector4(4.71187f, 4.998496f, 35.918936f, -28.875618f), |
|||
new Vector4(4.052795f, 8.244168f, -13.212253f, -1.578428f), |
|||
new Vector4(2.929212f, 11.900859f, 0.507991f, 1.816328f), |
|||
new Vector4(1.512961f, 16.116382f, 0.138051f, -0.01f) |
|||
}, |
|||
|
|||
// 6 components
|
|||
new[] |
|||
{ |
|||
new Vector4(5.143778f, 2.079813f, -82.326596f, 111.231024f), |
|||
new Vector4(5.612426f, 6.153387f, 113.878661f, 58.004879f), |
|||
new Vector4(5.982921f, 9.802895f, 39.479083f, -162.028887f), |
|||
new Vector4(6.505167f, 11.059237f, -71.286026f, 95.027069f), |
|||
new Vector4(3.869579f, 14.81052f, 1.405746f, -3.704914f), |
|||
new Vector4(2.201904f, 19.032909f, -0.152784f, -0.107988f) |
|||
} |
|||
}; |
|||
|
|||
/// <summary>
|
|||
/// Gets the kernel parameters and scaling factor for the current count value in the current instance
|
|||
/// </summary>
|
|||
private (Vector4[] Parameters, float Scale) GetParameters() |
|||
{ |
|||
// Prepare the kernel components
|
|||
int index = Math.Max(0, Math.Min(this.componentsCount - 1, KernelComponents.Count)); |
|||
return (KernelComponents[index], KernelScales[index]); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates the collection of complex 1D kernels with the specified parameters
|
|||
/// </summary>
|
|||
private Complex64[][] CreateComplexKernels() |
|||
{ |
|||
var kernels = new Complex64[this.kernelParameters.Length][]; |
|||
ref Vector4 baseRef = ref MemoryMarshal.GetReference(this.kernelParameters.AsSpan()); |
|||
for (int i = 0; i < this.kernelParameters.Length; i++) |
|||
{ |
|||
ref Vector4 paramsRef = ref Unsafe.Add(ref baseRef, i); |
|||
kernels[i] = this.CreateComplex1DKernel(paramsRef.X, paramsRef.Y); |
|||
} |
|||
|
|||
return kernels; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates a complex 1D kernel with the specified parameters
|
|||
/// </summary>
|
|||
/// <param name="a">The exponential parameter for each complex component</param>
|
|||
/// <param name="b">The angle component for each complex component</param>
|
|||
private Complex64[] CreateComplex1DKernel(float a, float b) |
|||
{ |
|||
var kernel = new Complex64[this.kernelSize]; |
|||
ref Complex64 baseRef = ref MemoryMarshal.GetReference(kernel.AsSpan()); |
|||
int r = this.radius, n = -r; |
|||
|
|||
for (int i = 0; i < this.kernelSize; i++, n++) |
|||
{ |
|||
// Incrementally compute the range values
|
|||
float value = n * this.kernelsScale * (1f / r); |
|||
value *= value; |
|||
|
|||
// Fill in the complex kernel values
|
|||
Unsafe.Add(ref baseRef, i) = new Complex64( |
|||
MathF.Exp(-a * value) * MathF.Cos(b * value), |
|||
MathF.Exp(-a * value) * MathF.Sin(b * value)); |
|||
} |
|||
|
|||
return kernel; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Normalizes the kernels with respect to A * real + B * imaginary
|
|||
/// </summary>
|
|||
private void NormalizeKernels() |
|||
{ |
|||
// Calculate the complex weighted sum
|
|||
float total = 0; |
|||
Span<Complex64[]> kernelsSpan = this.kernels.AsSpan(); |
|||
ref Complex64[] baseKernelsRef = ref MemoryMarshal.GetReference(kernelsSpan); |
|||
ref Vector4 baseParamsRef = ref MemoryMarshal.GetReference(this.kernelParameters.AsSpan()); |
|||
|
|||
for (int i = 0; i < this.kernelParameters.Length; i++) |
|||
{ |
|||
ref Complex64[] kernelRef = ref Unsafe.Add(ref baseKernelsRef, i); |
|||
int length = kernelRef.Length; |
|||
ref Complex64 valueRef = ref kernelRef[0]; |
|||
ref Vector4 paramsRef = ref Unsafe.Add(ref baseParamsRef, i); |
|||
|
|||
for (int j = 0; j < length; j++) |
|||
{ |
|||
for (int k = 0; k < length; k++) |
|||
{ |
|||
ref Complex64 jRef = ref Unsafe.Add(ref valueRef, j); |
|||
ref Complex64 kRef = ref Unsafe.Add(ref valueRef, k); |
|||
total += |
|||
(paramsRef.Z * ((jRef.Real * kRef.Real) - (jRef.Imaginary * kRef.Imaginary))) |
|||
+ (paramsRef.W * ((jRef.Real * kRef.Imaginary) + (jRef.Imaginary * kRef.Real))); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Normalize the kernels
|
|||
float scalar = 1f / MathF.Sqrt(total); |
|||
for (int i = 0; i < kernelsSpan.Length; i++) |
|||
{ |
|||
ref Complex64[] kernelsRef = ref Unsafe.Add(ref baseKernelsRef, i); |
|||
int length = kernelsRef.Length; |
|||
ref Complex64 valueRef = ref kernelsRef[0]; |
|||
|
|||
for (int j = 0; j < length; j++) |
|||
{ |
|||
Unsafe.Add(ref valueRef, j) *= scalar; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration) |
|||
{ |
|||
// Preliminary gamma highlight pass
|
|||
this.ApplyGammaExposure(source.PixelBuffer, sourceRectangle, configuration); |
|||
|
|||
// Create a 0-filled buffer to use to store the result of the component convolutions
|
|||
using (Buffer2D<Vector4> processing = configuration.MemoryAllocator.Allocate2D<Vector4>(source.Size(), AllocationOptions.Clean)) |
|||
{ |
|||
if (this.executionMode == BokehBlurExecutionMode.PreferLowMemoryUsage) |
|||
{ |
|||
// Memory usage priority: allocate a shared buffer and execute the second convolution in sequential mode
|
|||
using (Buffer2D<ComplexVector4> buffer = configuration.MemoryAllocator.Allocate2D<ComplexVector4>(source.Width, source.Height + this.radius)) |
|||
using (Buffer2D<ComplexVector4> firstPassBuffer = buffer.Slice(this.radius, source.Height)) |
|||
using (Buffer2D<ComplexVector4> secondPassBuffer = buffer.Slice(0, source.Height)) |
|||
{ |
|||
this.OnFrameApplyCore(source, sourceRectangle, configuration, processing, firstPassBuffer, secondPassBuffer); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
// Performance priority: allocate two independent buffers and execute both convolutions in parallel mode
|
|||
using (Buffer2D<ComplexVector4> firstPassValues = configuration.MemoryAllocator.Allocate2D<ComplexVector4>(source.Size())) |
|||
using (Buffer2D<ComplexVector4> secondPassBuffer = configuration.MemoryAllocator.Allocate2D<ComplexVector4>(source.Size())) |
|||
{ |
|||
this.OnFrameApplyCore(source, sourceRectangle, configuration, processing, firstPassValues, secondPassBuffer); |
|||
} |
|||
} |
|||
|
|||
// Apply the inverse gamma exposure pass, and write the final pixel data
|
|||
this.ApplyInverseGammaExposure(source.PixelBuffer, processing, sourceRectangle, configuration); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Computes and aggregates the convolution for each complex kernel component in the processor.
|
|||
/// </summary>
|
|||
/// <param name="source">The source image. 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 configuration.</param>
|
|||
/// <param name="processingBuffer">The buffer with the raw pixel data to use to aggregate the results of each convolution.</param>
|
|||
/// <param name="firstPassBuffer">The complex buffer to use for the first 1D convolution pass for each kernel.</param>
|
|||
/// <param name="secondPassBuffer">The complex buffer to use for the second 1D convolution pass for each kernel.</param>
|
|||
private void OnFrameApplyCore( |
|||
ImageFrame<TPixel> source, |
|||
Rectangle sourceRectangle, |
|||
Configuration configuration, |
|||
Buffer2D<Vector4> processingBuffer, |
|||
Buffer2D<ComplexVector4> firstPassBuffer, |
|||
Buffer2D<ComplexVector4> secondPassBuffer) |
|||
{ |
|||
// Perform two 1D convolutions for each component in the current instance
|
|||
ref Complex64[] baseRef = ref MemoryMarshal.GetReference(this.kernels.AsSpan()); |
|||
for (int i = 0; i < this.kernels.Length; i++) |
|||
{ |
|||
// Compute the resulting complex buffer for the current component
|
|||
var interest = Rectangle.Intersect(sourceRectangle, source.Bounds()); |
|||
Complex64[] kernel = Unsafe.Add(ref baseRef, i); |
|||
this.ApplyConvolution(firstPassBuffer, source.PixelBuffer, interest, kernel, configuration); |
|||
this.ApplyConvolution(secondPassBuffer, firstPassBuffer, interest, kernel, configuration); |
|||
|
|||
// Add the results of the convolution with the current kernel
|
|||
Vector4 parameters = this.kernelParameters[i]; |
|||
this.SumProcessingPartials(processingBuffer, secondPassBuffer, sourceRectangle, configuration, parameters.Z, parameters.W); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Applies the process to the specified portion of the specified <see cref="ImageFrame{TPixel}"/> at the specified location
|
|||
/// and with the specified size.
|
|||
/// </summary>
|
|||
/// <param name="targetValues">The target <see cref="ComplexVector4"/> values to use to store the results.</param>
|
|||
/// <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; |
|||
int endY = sourceRectangle.Bottom; |
|||
int startX = sourceRectangle.X; |
|||
int endX = sourceRectangle.Right; |
|||
int maxY = endY - 1; |
|||
int maxX = endX - 1; |
|||
|
|||
var workingRectangle = Rectangle.FromLTRB(startX, startY, endX, endY); |
|||
int width = workingRectangle.Width; |
|||
|
|||
ParallelHelper.IterateRows( |
|||
workingRectangle, |
|||
configuration, |
|||
rows => |
|||
{ |
|||
for (int y = rows.Min; y < rows.Max; y++) |
|||
{ |
|||
Span<ComplexVector4> targetRowSpan = targetValues.GetRowSpan(y).Slice(startX); |
|||
|
|||
for (int x = 0; x < width; x++) |
|||
{ |
|||
Buffer2DUtils.Convolve4(kernel, sourcePixels, targetRowSpan, y, x, startY, maxY, startX, maxX); |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Applies the process to the specified portion of the specified <see cref="Buffer2D{T}"/> buffer at the specified location
|
|||
/// and with the specified size.
|
|||
/// </summary>
|
|||
/// <param name="targetValues">The target <see cref="ComplexVector4"/> values to use to store the results.</param>
|
|||
/// <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>
|
|||
private void ApplyConvolution( |
|||
Buffer2D<ComplexVector4> targetValues, |
|||
Buffer2D<ComplexVector4> sourceValues, |
|||
Rectangle sourceRectangle, |
|||
Complex64[] kernel, |
|||
Configuration configuration) |
|||
{ |
|||
int startY = sourceRectangle.Y; |
|||
int endY = sourceRectangle.Bottom; |
|||
int startX = sourceRectangle.X; |
|||
int endX = sourceRectangle.Right; |
|||
int maxY = endY - 1; |
|||
int maxX = endX - 1; |
|||
|
|||
var workingRectangle = Rectangle.FromLTRB(startX, startY, endX, endY); |
|||
int width = workingRectangle.Width; |
|||
|
|||
if (this.executionMode == BokehBlurExecutionMode.PreferLowMemoryUsage) |
|||
{ |
|||
configuration = configuration.Clone(); |
|||
configuration.MaxDegreeOfParallelism = 1; |
|||
} |
|||
|
|||
ParallelHelper.IterateRows( |
|||
workingRectangle, |
|||
configuration, |
|||
rows => |
|||
{ |
|||
for (int y = rows.Min; y < rows.Max; y++) |
|||
{ |
|||
Span<ComplexVector4> targetRowSpan = targetValues.GetRowSpan(y).Slice(startX); |
|||
|
|||
for (int x = 0; x < width; x++) |
|||
{ |
|||
Buffer2DUtils.Convolve4(kernel, sourceValues, targetRowSpan, y, x, startY, maxY, startX, maxX); |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Applies the gamma correction/highlight to the input pixel buffer.
|
|||
/// </summary>
|
|||
/// <param name="targetPixels">The target pixel buffer to adjust.</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 ApplyGammaExposure( |
|||
Buffer2D<TPixel> targetPixels, |
|||
Rectangle sourceRectangle, |
|||
Configuration configuration) |
|||
{ |
|||
int startY = sourceRectangle.Y; |
|||
int endY = sourceRectangle.Bottom; |
|||
int startX = sourceRectangle.X; |
|||
int endX = sourceRectangle.Right; |
|||
|
|||
var workingRectangle = Rectangle.FromLTRB(startX, startY, endX, endY); |
|||
int width = workingRectangle.Width; |
|||
float exp = this.gamma; |
|||
|
|||
ParallelHelper.IterateRowsWithTempBuffer<Vector4>( |
|||
workingRectangle, |
|||
configuration, |
|||
(rows, vectorBuffer) => |
|||
{ |
|||
Span<Vector4> vectorSpan = vectorBuffer.Span; |
|||
int length = vectorSpan.Length; |
|||
|
|||
for (int y = rows.Min; y < rows.Max; y++) |
|||
{ |
|||
Span<TPixel> targetRowSpan = targetPixels.GetRowSpan(y).Slice(startX); |
|||
PixelOperations<TPixel>.Instance.ToVector4(configuration, targetRowSpan.Slice(0, length), vectorSpan, PixelConversionModifiers.Premultiply); |
|||
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>
|
|||
/// Applies the inverse gamma correction/highlight pass, and converts the input <see cref="Vector4"/> buffer into pixel values.
|
|||
/// </summary>
|
|||
/// <param name="targetPixels">The target pixels to apply the process to.</param>
|
|||
/// <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; |
|||
int endY = sourceRectangle.Bottom; |
|||
int startX = sourceRectangle.X; |
|||
int endX = sourceRectangle.Right; |
|||
|
|||
var workingRectangle = Rectangle.FromLTRB(startX, startY, endX, endY); |
|||
int width = workingRectangle.Width; |
|||
float expGamma = 1 / this.gamma; |
|||
|
|||
ParallelHelper.IterateRows( |
|||
workingRectangle, |
|||
configuration, |
|||
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 = targetPixels.GetRowSpan(y).Slice(startX); |
|||
Span<Vector4> sourceRowSpan = sourceValues.GetRowSpan(y).Slice(startX); |
|||
ref Vector4 sourceRef = ref MemoryMarshal.GetReference(sourceRowSpan); |
|||
|
|||
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); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Applies the process to the specified portion of the specified <see cref="ImageFrame{TPixel}"/> at the specified location
|
|||
/// and with the specified size.
|
|||
/// </summary>
|
|||
/// <param name="targetValues">The target <see cref="Buffer2D{T}"/> instance to use to store the results.</param>
|
|||
/// <param name="sourceValues">The source complex 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="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 SumProcessingPartials( |
|||
Buffer2D<Vector4> targetValues, |
|||
Buffer2D<ComplexVector4> sourceValues, |
|||
Rectangle sourceRectangle, |
|||
Configuration configuration, |
|||
float z, |
|||
float w) |
|||
{ |
|||
int startY = sourceRectangle.Y; |
|||
int endY = sourceRectangle.Bottom; |
|||
int startX = sourceRectangle.X; |
|||
int endX = sourceRectangle.Right; |
|||
|
|||
var workingRectangle = Rectangle.FromLTRB(startX, startY, endX, endY); |
|||
int width = workingRectangle.Width; |
|||
|
|||
ParallelHelper.IterateRows( |
|||
workingRectangle, |
|||
configuration, |
|||
rows => |
|||
{ |
|||
for (int y = rows.Min; y < rows.Max; y++) |
|||
{ |
|||
Span<Vector4> targetRowSpan = targetValues.GetRowSpan(y).Slice(startX); |
|||
Span<ComplexVector4> sourceRowSpan = sourceValues.GetRowSpan(y).Slice(startX); |
|||
ref Vector4 baseTargetRef = ref MemoryMarshal.GetReference(targetRowSpan); |
|||
ref ComplexVector4 baseSourceRef = ref MemoryMarshal.GetReference(sourceRowSpan); |
|||
|
|||
for (int x = 0; x < width; x++) |
|||
{ |
|||
Unsafe.Add(ref baseTargetRef, x) += Unsafe.Add(ref baseSourceRef, x).WeightedSum(z, w); |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System.Numerics; |
|||
|
|||
using SixLabors.ImageSharp.Primitives; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Convolution.Parameters |
|||
{ |
|||
/// <summary>
|
|||
/// A <see langword="struct"/> that contains data about a set of bokeh blur kernels
|
|||
/// </summary>
|
|||
internal readonly struct BokehBlurKernelData |
|||
{ |
|||
/// <summary>
|
|||
/// The kernel parameters to use for the current set of complex kernels
|
|||
/// </summary>
|
|||
public readonly Vector4[] Parameters; |
|||
|
|||
/// <summary>
|
|||
/// The scaling factor for the kernel values
|
|||
/// </summary>
|
|||
public readonly float Scale; |
|||
|
|||
/// <summary>
|
|||
/// The kernel components to apply the bokeh blur effect
|
|||
/// </summary>
|
|||
public readonly Complex64[][] Kernels; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="BokehBlurKernelData"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="parameters">The kernel parameters</param>
|
|||
/// <param name="scale">The kernel scale factor</param>
|
|||
/// <param name="kernels">The complex kernel components</param>
|
|||
public BokehBlurKernelData(Vector4[] parameters, float scale, Complex64[][] kernels) |
|||
{ |
|||
this.Parameters = parameters; |
|||
this.Scale = scale; |
|||
this.Kernels = kernels; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,52 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Convolution.Parameters |
|||
{ |
|||
/// <summary>
|
|||
/// A <see langword="struct"/> that contains parameters to apply a bokeh blur filter
|
|||
/// </summary>
|
|||
internal readonly struct BokehBlurParameters : IEquatable<BokehBlurParameters> |
|||
{ |
|||
/// <summary>
|
|||
/// The size of the convolution kernel to use when applying the bokeh blur
|
|||
/// </summary>
|
|||
public readonly int Radius; |
|||
|
|||
/// <summary>
|
|||
/// The number of complex components to use to approximate the bokeh kernel
|
|||
/// </summary>
|
|||
public readonly int Components; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="BokehBlurParameters"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="radius">The size of the kernel</param>
|
|||
/// <param name="components">The number of kernel components</param>
|
|||
public BokehBlurParameters(int radius, int components) |
|||
{ |
|||
this.Radius = radius; |
|||
this.Components = components; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public bool Equals(BokehBlurParameters other) |
|||
{ |
|||
return this.Radius.Equals(other.Radius) && this.Components.Equals(other.Components); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool Equals(object obj) => obj is BokehBlurParameters other && this.Equals(other); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override int GetHashCode() |
|||
{ |
|||
unchecked |
|||
{ |
|||
return (this.Radius.GetHashCode() * 397) ^ this.Components.GetHashCode(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,156 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Globalization; |
|||
using System.Linq; |
|||
using System.Text.RegularExpressions; |
|||
|
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.ImageSharp.Primitives; |
|||
using SixLabors.ImageSharp.Processing; |
|||
using SixLabors.ImageSharp.Processing.Processors.Convolution; |
|||
using SixLabors.Primitives; |
|||
|
|||
using Xunit; |
|||
using Xunit.Abstractions; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Convolution |
|||
{ |
|||
public class BokehBlurTest |
|||
{ |
|||
private static readonly string Components10x2 = @"
|
|||
[[ 0.00451261+0.0165137j 0.02161237-0.00299122j 0.00387479-0.02682816j |
|||
-0.02752798-0.01788438j -0.03553877+0.0154543j -0.01428268+0.04224722j |
|||
0.01747482+0.04687464j 0.04243676+0.03451751j 0.05564306+0.01742537j |
|||
0.06040984+0.00459225j 0.06136251+0.0j 0.06040984+0.00459225j |
|||
0.05564306+0.01742537j 0.04243676+0.03451751j 0.01747482+0.04687464j |
|||
-0.01428268+0.04224722j -0.03553877+0.0154543j -0.02752798-0.01788438j |
|||
0.00387479-0.02682816j 0.02161237-0.00299122j 0.00451261+0.0165137j ]] |
|||
[[-0.00227282+0.002851j -0.00152245+0.00604545j 0.00135338+0.00998296j |
|||
0.00698622+0.01370844j 0.0153483+0.01605112j 0.02565295+0.01611732j |
|||
0.03656958+0.01372368j 0.04662725+0.00954624j 0.05458942+0.00491277j |
|||
0.05963937+0.00133843j 0.06136251+0.0j 0.05963937+0.00133843j |
|||
0.05458942+0.00491277j 0.04662725+0.00954624j 0.03656958+0.01372368j |
|||
0.02565295+0.01611732j 0.0153483+0.01605112j 0.00698622+0.01370844j |
|||
0.00135338+0.00998296j -0.00152245+0.00604545j -0.00227282+0.002851j ]]";
|
|||
|
|||
[Fact] |
|||
public void VerifyComplexComponents() |
|||
{ |
|||
// Get the saved components
|
|||
var components = new List<Complex64[]>(); |
|||
foreach (Match match in Regex.Matches(Components10x2, @"\[\[(.*?)\]\]", RegexOptions.Singleline)) |
|||
{ |
|||
string[] values = match.Groups[1].Value.Trim().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); |
|||
Complex64[] component = values.Select( |
|||
value => |
|||
{ |
|||
Match pair = Regex.Match(value, @"([+-]?\d+\.\d+)([+-]?\d+\.\d+)j"); |
|||
return new Complex64( |
|||
float.Parse(pair.Groups[1].Value, CultureInfo.InvariantCulture), |
|||
float.Parse(pair.Groups[2].Value, CultureInfo.InvariantCulture)); |
|||
}).ToArray(); |
|||
components.Add(component); |
|||
} |
|||
|
|||
// Make sure the kernel components are the same
|
|||
var definition = new BokehBlurProcessor(10, BokehBlurProcessor.DefaultComponents, BokehBlurProcessor.DefaultGamma); |
|||
var processor = new BokehBlurProcessor<Rgb24>(definition); |
|||
Assert.Equal(components.Count, processor.Kernels.Count); |
|||
foreach ((Complex64[] a, Complex64[] b) in components.Zip(processor.Kernels, (a, b) => (a, b))) |
|||
{ |
|||
Span<Complex64> spanA = a.AsSpan(), spanB = b.AsSpan(); |
|||
Assert.Equal(spanA.Length, spanB.Length); |
|||
for (int i = 0; i < spanA.Length; i++) |
|||
{ |
|||
Assert.True(Math.Abs(Math.Abs(spanA[i].Real) - Math.Abs(spanB[i].Real)) < 0.0001f); |
|||
Assert.True(Math.Abs(Math.Abs(spanA[i].Imaginary) - Math.Abs(spanB[i].Imaginary)) < 0.0001f); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public sealed class BokehBlurInfo : IXunitSerializable |
|||
{ |
|||
public int Radius { get; set; } |
|||
|
|||
public int Components { get; set; } |
|||
|
|||
public float Gamma { get; set; } |
|||
|
|||
public void Deserialize(IXunitSerializationInfo info) |
|||
{ |
|||
this.Radius = info.GetValue<int>(nameof(this.Radius)); |
|||
this.Components = info.GetValue<int>(nameof(this.Components)); |
|||
this.Gamma = info.GetValue<float>(nameof(this.Gamma)); |
|||
} |
|||
|
|||
public void Serialize(IXunitSerializationInfo info) |
|||
{ |
|||
info.AddValue(nameof(this.Radius), this.Radius, typeof(int)); |
|||
info.AddValue(nameof(this.Components), this.Components, typeof(int)); |
|||
info.AddValue(nameof(this.Gamma), this.Gamma, typeof(float)); |
|||
} |
|||
|
|||
public override string ToString() => $"R{this.Radius}_C{this.Components}_G{this.Gamma}"; |
|||
} |
|||
|
|||
public static readonly TheoryData<BokehBlurInfo> BokehBlurValues = new TheoryData<BokehBlurInfo> |
|||
{ |
|||
new BokehBlurInfo { Radius = 8, Components = 1, Gamma = 1 }, |
|||
new BokehBlurInfo { Radius = 16, Components = 1, Gamma = 3 }, |
|||
new BokehBlurInfo { Radius = 16, Components = 2, Gamma = 3 } |
|||
}; |
|||
|
|||
public static readonly string[] TestFiles = |
|||
{ |
|||
TestImages.Png.CalliphoraPartial, |
|||
TestImages.Png.Bike, |
|||
TestImages.Png.BikeGrayscale, |
|||
TestImages.Png.Cross, |
|||
}; |
|||
|
|||
[Theory] |
|||
[WithFileCollection(nameof(TestFiles), nameof(BokehBlurValues), PixelTypes.Rgba32)] |
|||
[WithSolidFilledImages(nameof(BokehBlurValues), 50, 50, "Red", PixelTypes.Rgba32)] |
|||
[WithTestPatternImages(nameof(BokehBlurValues), 200, 100, PixelTypes.Rgba32)] |
|||
[WithTestPatternImages(nameof(BokehBlurValues), 23, 31, PixelTypes.Rgba32)] |
|||
[WithTestPatternImages(nameof(BokehBlurValues), 30, 20, PixelTypes.Rgba32)] |
|||
public void BokehBlurFilterProcessor<TPixel>(TestImageProvider<TPixel> provider, BokehBlurInfo value) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
provider.RunValidatingProcessorTest( |
|||
x => x.BokehBlur(value.Radius, value.Components, value.Gamma), |
|||
testOutputDetails: value.ToString(), |
|||
appendPixelTypeToFileName: false); |
|||
} |
|||
|
|||
[Theory] |
|||
[WithTestPatternImages(200, 200, PixelTypes.Bgr24 | PixelTypes.Bgra32 | PixelTypes.Gray8)] |
|||
public void BokehBlurFilterProcessor_WorksWithAllPixelTypes<TPixel>(TestImageProvider<TPixel> provider) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
provider.RunValidatingProcessorTest( |
|||
x => x.BokehBlur(8, 2, 3), |
|||
appendSourceFileOrDescription: false); |
|||
} |
|||
|
|||
|
|||
[Theory] |
|||
[WithFileCollection(nameof(TestFiles), nameof(BokehBlurValues), PixelTypes.Rgba32)] |
|||
public void BokehBlurFilterProcessor_Bounded<TPixel>(TestImageProvider<TPixel> provider, BokehBlurInfo value) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
provider.RunValidatingProcessorTest( |
|||
x => |
|||
{ |
|||
Size size = x.GetCurrentSize(); |
|||
var bounds = new Rectangle(10, 10, size.Width / 2, size.Height / 2); |
|||
x.BokehBlur(value.Radius, value.Components, value.Gamma, bounds); |
|||
}, |
|||
testOutputDetails: value.ToString(), |
|||
appendPixelTypeToFileName: false); |
|||
} |
|||
} |
|||
} |
|||
@ -1 +1 @@ |
|||
Subproject commit acc32594c125656840f8a17e69b0ebb49a370fa6 |
|||
Subproject commit 36f39bc624f8a49caf512077bf70cab30c2e5fb4 |
|||
Loading…
Reference in new issue