diff --git a/src/ImageSharp/Primitives/Complex64.cs b/src/ImageSharp/Primitives/Complex64.cs new file mode 100644 index 0000000000..6219380f70 --- /dev/null +++ b/src/ImageSharp/Primitives/Complex64.cs @@ -0,0 +1,64 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Primitives +{ + /// + /// Represents a complex number, where the real and imaginary parts are stored as values. + /// + /// + /// This is a more efficient version of the type. + /// + internal readonly struct Complex64 + { + /// + /// The real part of the complex number + /// + public readonly float Real; + + /// + /// The imaginary part of the complex number + /// + public readonly float Imaginary; + + /// + /// Initializes a new instance of the struct. + /// + /// The real part in the complex number. + /// The imaginary part in the complex number. + public Complex64(float real, float imaginary) + { + this.Real = real; + this.Imaginary = imaginary; + } + + /// + /// Performs the multiplication operation between a intance and a scalar. + /// + /// The value to multiply. + /// The scalar to use to multiply the value. + /// The result + [MethodImpl(InliningOptions.ShortMethod)] + public static Complex64 operator *(Complex64 value, float scalar) => new Complex64(value.Real * scalar, value.Imaginary * scalar); + + /// + /// Performs the addition operation between two intances. + /// + /// The first value to sum. + /// The second value to sum. + /// The result + [MethodImpl(InliningOptions.ShortMethod)] + public static Complex64 operator +(Complex64 left, Complex64 right) => new Complex64(left.Real + right.Real, left.Imaginary + right.Imaginary); + + /// + /// Performs a weighted sum on the current instance according to the given parameters + /// + /// The 'a' parameter, for the real component + /// The 'b' parameter, for the imaginary component + /// The resulting value + [MethodImpl(InliningOptions.ShortMethod)] + public float WeightedSum(float a, float b) => (this.Real * a) + (this.Imaginary * b); + } +} diff --git a/src/ImageSharp/Primitives/Rational.cs b/src/ImageSharp/Primitives/Rational.cs index b598f0e02f..6b134bbfd7 100644 --- a/src/ImageSharp/Primitives/Rational.cs +++ b/src/ImageSharp/Primitives/Rational.cs @@ -102,7 +102,7 @@ public Rational(double value, bool bestPrecision) /// Determines whether the specified instances are not considered equal. /// /// The first to compare. - /// The second to compare. + /// The second to compare. /// The public static bool operator !=(Rational left, Rational right) { diff --git a/src/ImageSharp/Processing/BokehBlurExtensions.cs b/src/ImageSharp/Processing/BokehBlurExtensions.cs new file mode 100644 index 0000000000..9d7dd65f43 --- /dev/null +++ b/src/ImageSharp/Processing/BokehBlurExtensions.cs @@ -0,0 +1,52 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Convolution; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Processing +{ + /// + /// Adds bokeh blurring extensions to the type. + /// + public static class BokehBlurExtensions + { + /// + /// Applies a bokeh blur to the image. + /// + /// The pixel format. + /// The image this method extends. + /// The . + public static IImageProcessingContext BokehBlur(this IImageProcessingContext source) + where TPixel : struct, IPixel + => source.ApplyProcessor(new BokehBlurProcessor()); + + /// + /// Applies a bokeh blur to the image. + /// + /// The pixel format. + /// The image this method extends. + /// The 'radius' value representing the size of the area to sample. + /// The 'components' value representing the number of kernels to use to approximate the bokeh effect. + /// The . + public static IImageProcessingContext BokehBlur(this IImageProcessingContext source, int radius, int components) + where TPixel : struct, IPixel + => source.ApplyProcessor(new BokehBlurProcessor(radius, components)); + + /// + /// Applies a bokeh blur to the image. + /// + /// The pixel format. + /// The image this method extends. + /// The 'radius' value representing the size of the area to sample. + /// The 'components' value representing the number of kernels to use to approximate the bokeh effect. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext BokehBlur(this IImageProcessingContext source, int radius, int components, Rectangle rectangle) + where TPixel : struct, IPixel + => source.ApplyProcessor(new BokehBlurProcessor(radius, components), rectangle); + } +} diff --git a/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor.cs b/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor.cs new file mode 100644 index 0000000000..befc9eeec2 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor.cs @@ -0,0 +1,237 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Collections.Generic; +using System.Linq; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Primitives; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution +{ + /// + /// Applies bokeh blur processing to the image. + /// + /// The pixel format. + internal class BokehBlurProcessor : ImageProcessor + where TPixel : struct, IPixel + { + /// + /// The maximum size of the kernel in either direction. + /// + private readonly int kernelSize; + + /// + /// The number of components to use when applying the bokeh blur + /// + private readonly int componentsCount; + + /// + /// The kernel components to use for the current instance + /// + private readonly IReadOnlyList> kernelParameters; + + /// + /// The scaling factor for kernel values + /// + private readonly float kernelsScale; + + /// + /// The complex kernels to use to apply the blur for the current instance + /// + private readonly IReadOnlyList complexKernels; + + /// + /// The mapping of initialized complex kernels and parameters, to speed up the initialization of new instances + /// + private static readonly Dictionary<(int, int), (IReadOnlyList>, float, IReadOnlyList)> Cache = + new Dictionary<(int, int), (IReadOnlyList>, float, IReadOnlyList)>(); + + /// + /// Initializes a new instance of the class. + /// + /// + /// The 'radius' value representing the size of the area to sample. + /// + /// + /// The number of components to use to approximate the original 2D bokeh blur convolution kernel. + /// + public BokehBlurProcessor(int radius = 32, int components = 2) + { + this.Radius = radius; + this.kernelSize = (radius * 2) + 1; + this.componentsCount = components; + + // Reuse the initialized values from the cache, if possible + if (Cache.TryGetValue((radius, components), out (IReadOnlyList>, float, IReadOnlyList) info)) + { + this.kernelParameters = info.Item1; + this.kernelsScale = info.Item2; + this.complexKernels = info.Item3; + } + else + { + // Initialize the complex kernels and parameters with the current arguments + (this.kernelParameters, this.kernelsScale) = this.GetParameters(); + this.complexKernels = ( + from component in this.kernelParameters + select this.CreateComplex1DKernel(component['a'], component['b'])).ToArray(); + this.NormalizeKernels(); + + // Store them in the cache for future use + Cache.Add((radius, components), (this.kernelParameters, this.kernelsScale, this.complexKernels)); + } + } + + /// + /// Gets the Radius + /// + public int Radius { get; } + + /// + /// Gets the kernel scales to adjust the component values in each kernel + /// + private static IReadOnlyList KernelScales { get; } = new[] { 1.4f, 1.2f, 1.2f, 1.2f, 1.2f, 1.2f }; + + /// + /// Gets the available bokeh blur kernel parameters + /// + private static IReadOnlyList KernelComponents { get; } = new[] + { + // 1 component + new[,] { { 0.862325f, 1.624835f, 0.767583f, 1.862321f } }, + + // 2 components + new[,] + { + { 0.886528f, 5.268909f, 0.411259f, -0.548794f }, + { 1.960518f, 1.558213f, 0.513282f, 4.56111f } + }, + + // 3 components + new[,] + { + { 2.17649f, 5.043495f, 1.621035f, -2.105439f }, + { 1.019306f, 9.027613f, -0.28086f, -0.162882f }, + { 2.81511f, 1.597273f, -0.366471f, 10.300301f } + }, + + // 4 components + new[,] + { + { 4.338459f, 1.553635f, -5.767909f, 46.164397f }, + { 3.839993f, 4.693183f, 9.795391f, -15.227561f }, + { 2.791880f, 8.178137f, -3.048324f, 0.302959f }, + { 1.342190f, 12.328289f, 0.010001f, 0.244650f } + }, + + // 5 components + new[,] + { + { 4.892608f, 1.685979f, -22.356787f, 85.91246f }, + { 4.71187f, 4.998496f, 35.918936f, -28.875618f }, + { 4.052795f, 8.244168f, -13.212253f, -1.578428f }, + { 2.929212f, 11.900859f, 0.507991f, 1.816328f }, + { 1.512961f, 16.116382f, 0.138051f, -0.01f } + }, + + // 6 components + new[,] + { + { 5.143778f, 2.079813f, -82.326596f, 111.231024f }, + { 5.612426f, 6.153387f, 113.878661f, 58.004879f }, + { 5.982921f, 9.802895f, 39.479083f, -162.028887f }, + { 6.505167f, 11.059237f, -71.286026f, 95.027069f }, + { 3.869579f, 14.81052f, 1.405746f, -3.704914f }, + { 2.201904f, 19.032909f, -0.152784f, -0.107988f } + } + }; + + /// + /// Gets the kernel parameters and scaling factor for the current count value in the current instance + /// + private (IReadOnlyList> Parameters, float Scale) GetParameters() + { + // Prepare the kernel components + int index = Math.Max(0, Math.Min(this.componentsCount - 1, KernelComponents.Count)); + float[,] parameters = KernelComponents[index]; + var mapping = new IReadOnlyDictionary[parameters.GetLength(0)]; + for (int i = 0; i < parameters.GetLength(0); i++) + { + mapping[i] = new Dictionary + { + ['a'] = parameters[i, 0], + ['b'] = parameters[i, 1], + ['A'] = parameters[i, 2], + ['B'] = parameters[i, 3] + }; + } + + // Return the components and the adjustment scale + return (mapping, KernelScales[index]); + } + + /// + /// Creates a complex 1D kernel with the specified parameters + /// + /// The exponential parameter for each complex component + /// The angle component for each complex component + private Complex64[] CreateComplex1DKernel(float a, float b) + { + // Precompute the range values + float[] ax = Enumerable.Range(-this.Radius, this.Radius + 1).Select( + i => + { + float value = i * this.kernelsScale * (1f / this.Radius); + return value * value; + }).ToArray(); + + // Compute the complex kernels + var kernel = new Complex64[this.kernelSize]; + for (int i = 0; i < this.kernelSize; i++) + { + float + real = (float)(Math.Exp(-a * ax[i]) * Math.Cos(b * ax[i])), + imaginary = (float)(Math.Exp(-a * ax[i]) * Math.Sin(b * ax[i])); + kernel[i] = new Complex64(real, imaginary); + } + + return kernel; + } + + /// + /// Normalizes the kernels with respect to A * real + B * imaginary + /// + private void NormalizeKernels() + { + // Calculate the complex weighted sum + double total = 0; + foreach ((Complex64[] kernel, IReadOnlyDictionary param) in this.complexKernels.Zip(this.kernelParameters, (k, p) => (k, p))) + { + for (int i = 0; i < kernel.Length; i++) + { + for (int j = 0; j < kernel.Length; j++) + { + total += + (param['A'] * ((kernel[i].Real * kernel[j].Real) - (kernel[i].Imaginary * kernel[j].Imaginary))) + + (param['B'] * ((kernel[i].Real * kernel[j].Imaginary) + (kernel[i].Imaginary * kernel[j].Real))); + } + } + } + + // Normalize the kernels + float scalar = (float)(1f / Math.Sqrt(total)); + foreach (Complex64[] kernel in this.complexKernels) + { + for (int i = 0; i < kernel.Length; i++) + { + kernel[i] = kernel[i] * scalar; + } + } + } + + /// + protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) => throw new NotImplementedException(); + } +}