mirror of https://github.com/SixLabors/ImageSharp
99 changed files with 3081 additions and 3526 deletions
@ -1,85 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using SixLabors.ImageSharp.Processing.Processors.Binarization; |
|||
using SixLabors.ImageSharp.Processing.Processors.Dithering; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing |
|||
{ |
|||
/// <summary>
|
|||
/// Defines extension methods to apply binary diffusion on an <see cref="Image"/>
|
|||
/// using Mutate/Clone.
|
|||
/// </summary>
|
|||
public static class BinaryDiffuseExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// Dithers the image reducing it to two colors using error diffusion.
|
|||
/// </summary>
|
|||
/// <param name="source">The image this method extends.</param>
|
|||
/// <param name="diffuser">The diffusion algorithm to apply.</param>
|
|||
/// <param name="threshold">The threshold to apply binarization of the image. Must be between 0 and 1.</param>
|
|||
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
|
|||
public static IImageProcessingContext BinaryDiffuse( |
|||
this IImageProcessingContext source, |
|||
IErrorDiffuser diffuser, |
|||
float threshold) => |
|||
source.ApplyProcessor(new BinaryErrorDiffusionProcessor(diffuser, threshold)); |
|||
|
|||
/// <summary>
|
|||
/// Dithers the image reducing it to two colors using error diffusion.
|
|||
/// </summary>
|
|||
/// <param name="source">The image this method extends.</param>
|
|||
/// <param name="diffuser">The diffusion algorithm to apply.</param>
|
|||
/// <param name="threshold">The threshold to apply binarization of the image. Must be between 0 and 1.</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 BinaryDiffuse( |
|||
this IImageProcessingContext source, |
|||
IErrorDiffuser diffuser, |
|||
float threshold, |
|||
Rectangle rectangle) => |
|||
source.ApplyProcessor(new BinaryErrorDiffusionProcessor(diffuser, threshold), rectangle); |
|||
|
|||
/// <summary>
|
|||
/// Dithers the image reducing it to two colors using error diffusion.
|
|||
/// </summary>
|
|||
/// <param name="source">The image this method extends.</param>
|
|||
/// <param name="diffuser">The diffusion algorithm to apply.</param>
|
|||
/// <param name="threshold">The threshold to apply binarization of the image. Must be between 0 and 1.</param>
|
|||
/// <param name="upperColor">The color to use for pixels that are above the threshold.</param>
|
|||
/// <param name="lowerColor">The color to use for pixels that are below the threshold</param>
|
|||
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
|
|||
public static IImageProcessingContext BinaryDiffuse( |
|||
this IImageProcessingContext source, |
|||
IErrorDiffuser diffuser, |
|||
float threshold, |
|||
Color upperColor, |
|||
Color lowerColor) => |
|||
source.ApplyProcessor(new BinaryErrorDiffusionProcessor(diffuser, threshold, upperColor, lowerColor)); |
|||
|
|||
/// <summary>
|
|||
/// Dithers the image reducing it to two colors using error diffusion.
|
|||
/// </summary>
|
|||
/// <param name="source">The image this method extends.</param>
|
|||
/// <param name="diffuser">The diffusion algorithm to apply.</param>
|
|||
/// <param name="threshold">The threshold to apply binarization of the image. Must be between 0 and 1.</param>
|
|||
/// <param name="upperColor">The color to use for pixels that are above the threshold.</param>
|
|||
/// <param name="lowerColor">The color to use for pixels that are below the threshold</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 BinaryDiffuse( |
|||
this IImageProcessingContext source, |
|||
IErrorDiffuser diffuser, |
|||
float threshold, |
|||
Color upperColor, |
|||
Color lowerColor, |
|||
Rectangle rectangle) => |
|||
source.ApplyProcessor( |
|||
new BinaryErrorDiffusionProcessor(diffuser, threshold, upperColor, lowerColor), |
|||
rectangle); |
|||
} |
|||
} |
|||
@ -1,97 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
|
|||
using SixLabors.ImageSharp.Processing.Processors.Dithering; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing |
|||
{ |
|||
/// <summary>
|
|||
/// Defines extension methods to apply diffusion to an <see cref="Image"/>
|
|||
/// using Mutate/Clone.
|
|||
/// </summary>
|
|||
public static class DiffuseExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// Dithers the image reducing it to a web-safe palette using error diffusion.
|
|||
/// </summary>
|
|||
/// <param name="source">The image this method extends.</param>
|
|||
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
|
|||
public static IImageProcessingContext Diffuse(this IImageProcessingContext source) => |
|||
Diffuse(source, KnownDiffusers.FloydSteinberg, .5F); |
|||
|
|||
/// <summary>
|
|||
/// Dithers the image reducing it to a web-safe palette using error diffusion.
|
|||
/// </summary>
|
|||
/// <param name="source">The image this method extends.</param>
|
|||
/// <param name="threshold">The threshold to apply binarization of the image. Must be between 0 and 1.</param>
|
|||
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
|
|||
public static IImageProcessingContext Diffuse(this IImageProcessingContext source, float threshold) => |
|||
Diffuse(source, KnownDiffusers.FloydSteinberg, threshold); |
|||
|
|||
/// <summary>
|
|||
/// Dithers the image reducing it to a web-safe palette using error diffusion.
|
|||
/// </summary>
|
|||
/// <param name="source">The image this method extends.</param>
|
|||
/// <param name="diffuser">The diffusion algorithm to apply.</param>
|
|||
/// <param name="threshold">The threshold to apply binarization of the image. Must be between 0 and 1.</param>
|
|||
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
|
|||
public static IImageProcessingContext Diffuse( |
|||
this IImageProcessingContext source, |
|||
IErrorDiffuser diffuser, |
|||
float threshold) => |
|||
source.ApplyProcessor(new ErrorDiffusionPaletteProcessor(diffuser, threshold)); |
|||
|
|||
/// <summary>
|
|||
/// Dithers the image reducing it to a web-safe palette using error diffusion.
|
|||
/// </summary>
|
|||
/// <param name="source">The image this method extends.</param>
|
|||
/// <param name="diffuser">The diffusion algorithm to apply.</param>
|
|||
/// <param name="threshold">The threshold to apply binarization of the image. Must be between 0 and 1.</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 Diffuse( |
|||
this IImageProcessingContext source, |
|||
IErrorDiffuser diffuser, |
|||
float threshold, |
|||
Rectangle rectangle) => |
|||
source.ApplyProcessor(new ErrorDiffusionPaletteProcessor(diffuser, threshold), rectangle); |
|||
|
|||
/// <summary>
|
|||
/// Dithers the image reducing it to the given palette using error diffusion.
|
|||
/// </summary>
|
|||
/// <param name="source">The image this method extends.</param>
|
|||
/// <param name="diffuser">The diffusion algorithm to apply.</param>
|
|||
/// <param name="threshold">The threshold to apply binarization of the image. Must be between 0 and 1.</param>
|
|||
/// <param name="palette">The palette to select substitute colors from.</param>
|
|||
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
|
|||
public static IImageProcessingContext Diffuse( |
|||
this IImageProcessingContext source, |
|||
IErrorDiffuser diffuser, |
|||
float threshold, |
|||
ReadOnlyMemory<Color> palette) => |
|||
source.ApplyProcessor(new ErrorDiffusionPaletteProcessor(diffuser, threshold, palette)); |
|||
|
|||
/// <summary>
|
|||
/// Dithers the image reducing it to the given palette using error diffusion.
|
|||
/// </summary>
|
|||
/// <param name="source">The image this method extends.</param>
|
|||
/// <param name="diffuser">The diffusion algorithm to apply.</param>
|
|||
/// <param name="threshold">The threshold to apply binarization of the image. Must be between 0 and 1.</param>
|
|||
/// <param name="palette">The palette to select substitute colors from.</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 Diffuse( |
|||
this IImageProcessingContext source, |
|||
IErrorDiffuser diffuser, |
|||
float threshold, |
|||
ReadOnlyMemory<Color> palette, |
|||
Rectangle rectangle) => |
|||
source.ApplyProcessor(new ErrorDiffusionPaletteProcessor(diffuser, threshold, palette), rectangle); |
|||
} |
|||
} |
|||
@ -1,58 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using SixLabors.ImageSharp.Processing.Processors.Dithering; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing |
|||
{ |
|||
/// <summary>
|
|||
/// Contains reusable static instances of known error diffusion algorithms
|
|||
/// </summary>
|
|||
public static class KnownDiffusers |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the error diffuser that implements the Atkinson algorithm.
|
|||
/// </summary>
|
|||
public static IErrorDiffuser Atkinson { get; } = new AtkinsonDiffuser(); |
|||
|
|||
/// <summary>
|
|||
/// Gets the error diffuser that implements the Burks algorithm.
|
|||
/// </summary>
|
|||
public static IErrorDiffuser Burks { get; } = new BurksDiffuser(); |
|||
|
|||
/// <summary>
|
|||
/// Gets the error diffuser that implements the Floyd-Steinberg algorithm.
|
|||
/// </summary>
|
|||
public static IErrorDiffuser FloydSteinberg { get; } = new FloydSteinbergDiffuser(); |
|||
|
|||
/// <summary>
|
|||
/// Gets the error diffuser that implements the Jarvis-Judice-Ninke algorithm.
|
|||
/// </summary>
|
|||
public static IErrorDiffuser JarvisJudiceNinke { get; } = new JarvisJudiceNinkeDiffuser(); |
|||
|
|||
/// <summary>
|
|||
/// Gets the error diffuser that implements the Sierra-2 algorithm.
|
|||
/// </summary>
|
|||
public static IErrorDiffuser Sierra2 { get; } = new Sierra2Diffuser(); |
|||
|
|||
/// <summary>
|
|||
/// Gets the error diffuser that implements the Sierra-3 algorithm.
|
|||
/// </summary>
|
|||
public static IErrorDiffuser Sierra3 { get; } = new Sierra3Diffuser(); |
|||
|
|||
/// <summary>
|
|||
/// Gets the error diffuser that implements the Sierra-Lite algorithm.
|
|||
/// </summary>
|
|||
public static IErrorDiffuser SierraLite { get; } = new SierraLiteDiffuser(); |
|||
|
|||
/// <summary>
|
|||
/// Gets the error diffuser that implements the Stevenson-Arce algorithm.
|
|||
/// </summary>
|
|||
public static IErrorDiffuser StevensonArce { get; } = new StevensonArceDiffuser(); |
|||
|
|||
/// <summary>
|
|||
/// Gets the error diffuser that implements the Stucki algorithm.
|
|||
/// </summary>
|
|||
public static IErrorDiffuser Stucki { get; } = new StuckiDiffuser(); |
|||
} |
|||
} |
|||
@ -1,33 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using SixLabors.ImageSharp.Processing.Processors.Dithering; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing |
|||
{ |
|||
/// <summary>
|
|||
/// Contains reusable static instances of known ordered dither matrices
|
|||
/// </summary>
|
|||
public static class KnownDitherers |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the order ditherer using the 2x2 Bayer dithering matrix
|
|||
/// </summary>
|
|||
public static IOrderedDither BayerDither2x2 { get; } = new BayerDither2x2(); |
|||
|
|||
/// <summary>
|
|||
/// Gets the order ditherer using the 3x3 dithering matrix
|
|||
/// </summary>
|
|||
public static IOrderedDither OrderedDither3x3 { get; } = new OrderedDither3x3(); |
|||
|
|||
/// <summary>
|
|||
/// Gets the order ditherer using the 4x4 Bayer dithering matrix
|
|||
/// </summary>
|
|||
public static IOrderedDither BayerDither4x4 { get; } = new BayerDither4x4(); |
|||
|
|||
/// <summary>
|
|||
/// Gets the order ditherer using the 8x8 Bayer dithering matrix
|
|||
/// </summary>
|
|||
public static IOrderedDither BayerDither8x8 { get; } = new BayerDither8x8(); |
|||
} |
|||
} |
|||
@ -0,0 +1,78 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using SixLabors.ImageSharp.Processing.Processors.Dithering; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing |
|||
{ |
|||
/// <summary>
|
|||
/// Contains reusable static instances of known dithering algorithms.
|
|||
/// </summary>
|
|||
public static class KnownDitherings |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the order ditherer using the 2x2 Bayer dithering matrix
|
|||
/// </summary>
|
|||
public static IDither Bayer2x2 { get; } = OrderedDither.Bayer2x2; |
|||
|
|||
/// <summary>
|
|||
/// Gets the order ditherer using the 3x3 dithering matrix
|
|||
/// </summary>
|
|||
public static IDither Ordered3x3 { get; } = OrderedDither.Ordered3x3; |
|||
|
|||
/// <summary>
|
|||
/// Gets the order ditherer using the 4x4 Bayer dithering matrix
|
|||
/// </summary>
|
|||
public static IDither Bayer4x4 { get; } = OrderedDither.Bayer4x4; |
|||
|
|||
/// <summary>
|
|||
/// Gets the order ditherer using the 8x8 Bayer dithering matrix
|
|||
/// </summary>
|
|||
public static IDither Bayer8x8 { get; } = OrderedDither.Bayer8x8; |
|||
|
|||
/// <summary>
|
|||
/// Gets the error Dither that implements the Atkinson algorithm.
|
|||
/// </summary>
|
|||
public static IDither Atkinson { get; } = ErrorDither.Atkinson; |
|||
|
|||
/// <summary>
|
|||
/// Gets the error Dither that implements the Burks algorithm.
|
|||
/// </summary>
|
|||
public static IDither Burks { get; } = ErrorDither.Burkes; |
|||
|
|||
/// <summary>
|
|||
/// Gets the error Dither that implements the Floyd-Steinberg algorithm.
|
|||
/// </summary>
|
|||
public static IDither FloydSteinberg { get; } = ErrorDither.FloydSteinberg; |
|||
|
|||
/// <summary>
|
|||
/// Gets the error Dither that implements the Jarvis-Judice-Ninke algorithm.
|
|||
/// </summary>
|
|||
public static IDither JarvisJudiceNinke { get; } = ErrorDither.JarvisJudiceNinke; |
|||
|
|||
/// <summary>
|
|||
/// Gets the error Dither that implements the Sierra-2 algorithm.
|
|||
/// </summary>
|
|||
public static IDither Sierra2 { get; } = ErrorDither.Sierra2; |
|||
|
|||
/// <summary>
|
|||
/// Gets the error Dither that implements the Sierra-3 algorithm.
|
|||
/// </summary>
|
|||
public static IDither Sierra3 { get; } = ErrorDither.Sierra3; |
|||
|
|||
/// <summary>
|
|||
/// Gets the error Dither that implements the Sierra-Lite algorithm.
|
|||
/// </summary>
|
|||
public static IDither SierraLite { get; } = ErrorDither.SierraLite; |
|||
|
|||
/// <summary>
|
|||
/// Gets the error Dither that implements the Stevenson-Arce algorithm.
|
|||
/// </summary>
|
|||
public static IDither StevensonArce { get; } = ErrorDither.StevensonArce; |
|||
|
|||
/// <summary>
|
|||
/// Gets the error Dither that implements the Stucki algorithm.
|
|||
/// </summary>
|
|||
public static IDither Stucki { get; } = ErrorDither.Stucki; |
|||
} |
|||
} |
|||
@ -1,76 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.ImageSharp.Processing.Processors.Dithering; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Binarization |
|||
{ |
|||
/// <summary>
|
|||
/// Performs binary threshold filtering against an image using error diffusion.
|
|||
/// </summary>
|
|||
public class BinaryErrorDiffusionProcessor : IImageProcessor |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="BinaryErrorDiffusionProcessor"/> class.
|
|||
/// </summary>
|
|||
/// <param name="diffuser">The error diffuser</param>
|
|||
public BinaryErrorDiffusionProcessor(IErrorDiffuser diffuser) |
|||
: this(diffuser, .5F) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="BinaryErrorDiffusionProcessor"/> class.
|
|||
/// </summary>
|
|||
/// <param name="diffuser">The error diffuser</param>
|
|||
/// <param name="threshold">The threshold to split the image. Must be between 0 and 1.</param>
|
|||
public BinaryErrorDiffusionProcessor(IErrorDiffuser diffuser, float threshold) |
|||
: this(diffuser, threshold, Color.White, Color.Black) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="BinaryErrorDiffusionProcessor"/> class.
|
|||
/// </summary>
|
|||
/// <param name="diffuser">The error diffuser</param>
|
|||
/// <param name="threshold">The threshold to split the image. Must be between 0 and 1.</param>
|
|||
/// <param name="upperColor">The color to use for pixels that are above the threshold.</param>
|
|||
/// <param name="lowerColor">The color to use for pixels that are below the threshold.</param>
|
|||
public BinaryErrorDiffusionProcessor(IErrorDiffuser diffuser, float threshold, Color upperColor, Color lowerColor) |
|||
{ |
|||
Guard.NotNull(diffuser, nameof(diffuser)); |
|||
Guard.MustBeBetweenOrEqualTo(threshold, 0, 1, nameof(threshold)); |
|||
|
|||
this.Diffuser = diffuser; |
|||
this.Threshold = threshold; |
|||
this.UpperColor = upperColor; |
|||
this.LowerColor = lowerColor; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the error diffuser.
|
|||
/// </summary>
|
|||
public IErrorDiffuser Diffuser { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the threshold value.
|
|||
/// </summary>
|
|||
public float Threshold { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the color to use for pixels that are above the threshold.
|
|||
/// </summary>
|
|||
public Color UpperColor { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the color to use for pixels that fall below the threshold.
|
|||
/// </summary>
|
|||
public Color LowerColor { get; } |
|||
|
|||
/// <inheritdoc />
|
|||
public IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>(Configuration configuration, Image<TPixel> source, Rectangle sourceRectangle) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
=> new BinaryErrorDiffusionProcessor<TPixel>(configuration, this, source, sourceRectangle); |
|||
} |
|||
} |
|||
@ -1,84 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
|
|||
using SixLabors.ImageSharp.Advanced; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.ImageSharp.Processing.Processors.Dithering; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Binarization |
|||
{ |
|||
/// <summary>
|
|||
/// Performs binary threshold filtering against an image using error diffusion.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
internal sealed class BinaryErrorDiffusionProcessor<TPixel> : ImageProcessor<TPixel> |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
private readonly BinaryErrorDiffusionProcessor definition; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="BinaryErrorDiffusionProcessor{TPixel}"/> class.
|
|||
/// </summary>
|
|||
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
|
|||
/// <param name="definition">The <see cref="BinaryErrorDiffusionProcessor"/> 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 BinaryErrorDiffusionProcessor(Configuration configuration, BinaryErrorDiffusionProcessor definition, Image<TPixel> source, Rectangle sourceRectangle) |
|||
: base(configuration, source, sourceRectangle) |
|||
{ |
|||
this.definition = definition; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override void OnFrameApply(ImageFrame<TPixel> source) |
|||
{ |
|||
TPixel upperColor = this.definition.UpperColor.ToPixel<TPixel>(); |
|||
TPixel lowerColor = this.definition.LowerColor.ToPixel<TPixel>(); |
|||
IErrorDiffuser diffuser = this.definition.Diffuser; |
|||
|
|||
byte threshold = (byte)MathF.Round(this.definition.Threshold * 255F); |
|||
bool isAlphaOnly = typeof(TPixel) == typeof(A8); |
|||
|
|||
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); |
|||
int startY = interest.Y; |
|||
int endY = interest.Bottom; |
|||
int startX = interest.X; |
|||
int endX = interest.Right; |
|||
|
|||
// Collect the values before looping so we can reduce our calculation count for identical sibling pixels
|
|||
TPixel sourcePixel = source[startX, startY]; |
|||
TPixel previousPixel = sourcePixel; |
|||
Rgba32 rgba = default; |
|||
sourcePixel.ToRgba32(ref rgba); |
|||
|
|||
// Convert to grayscale using ITU-R Recommendation BT.709 if required
|
|||
byte luminance = isAlphaOnly ? rgba.A : ImageMaths.Get8BitBT709Luminance(rgba.R, rgba.G, rgba.B); |
|||
|
|||
for (int y = startY; y < endY; y++) |
|||
{ |
|||
Span<TPixel> row = source.GetPixelRowSpan(y); |
|||
|
|||
for (int x = startX; x < endX; x++) |
|||
{ |
|||
sourcePixel = row[x]; |
|||
|
|||
// Check if this is the same as the last pixel. If so use that value
|
|||
// rather than calculating it again. This is an inexpensive optimization.
|
|||
if (!previousPixel.Equals(sourcePixel)) |
|||
{ |
|||
sourcePixel.ToRgba32(ref rgba); |
|||
luminance = isAlphaOnly ? rgba.A : ImageMaths.Get8BitBT709Luminance(rgba.R, rgba.G, rgba.B); |
|||
|
|||
// Setup the previous pointer
|
|||
previousPixel = sourcePixel; |
|||
} |
|||
|
|||
TPixel transformedPixel = luminance >= threshold ? upperColor : lowerColor; |
|||
diffuser.Dither(source, sourcePixel, transformedPixel, x, y, startX, endX, endY); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,58 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
|
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.ImageSharp.Processing.Processors.Dithering; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Binarization |
|||
{ |
|||
/// <summary>
|
|||
/// Defines a binary threshold filtering using ordered dithering.
|
|||
/// </summary>
|
|||
public class BinaryOrderedDitherProcessor : IImageProcessor |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="BinaryOrderedDitherProcessor"/> class.
|
|||
/// </summary>
|
|||
/// <param name="dither">The ordered ditherer.</param>
|
|||
public BinaryOrderedDitherProcessor(IOrderedDither dither) |
|||
: this(dither, Color.White, Color.Black) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="BinaryOrderedDitherProcessor"/> class.
|
|||
/// </summary>
|
|||
/// <param name="dither">The ordered ditherer.</param>
|
|||
/// <param name="upperColor">The color to use for pixels that are above the threshold.</param>
|
|||
/// <param name="lowerColor">The color to use for pixels that are below the threshold.</param>
|
|||
public BinaryOrderedDitherProcessor(IOrderedDither dither, Color upperColor, Color lowerColor) |
|||
{ |
|||
this.Dither = dither ?? throw new ArgumentNullException(nameof(dither)); |
|||
this.UpperColor = upperColor; |
|||
this.LowerColor = lowerColor; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the ditherer.
|
|||
/// </summary>
|
|||
public IOrderedDither Dither { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the color to use for pixels that are above the threshold.
|
|||
/// </summary>
|
|||
public Color UpperColor { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the color to use for pixels that fall below the threshold.
|
|||
/// </summary>
|
|||
public Color LowerColor { get; } |
|||
|
|||
/// <inheritdoc />
|
|||
public IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>(Configuration configuration, Image<TPixel> source, Rectangle sourceRectangle) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
=> new BinaryOrderedDitherProcessor<TPixel>(configuration, this, source, sourceRectangle); |
|||
} |
|||
} |
|||
@ -1,82 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
|
|||
using SixLabors.ImageSharp.Advanced; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.ImageSharp.Processing.Processors.Dithering; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Binarization |
|||
{ |
|||
/// <summary>
|
|||
/// Performs binary threshold filtering against an image using ordered dithering.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
internal class BinaryOrderedDitherProcessor<TPixel> : ImageProcessor<TPixel> |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
private readonly BinaryOrderedDitherProcessor definition; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="BinaryOrderedDitherProcessor{TPixel}"/> class.
|
|||
/// </summary>
|
|||
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
|
|||
/// <param name="definition">The <see cref="BinaryErrorDiffusionProcessor"/> 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 BinaryOrderedDitherProcessor(Configuration configuration, BinaryOrderedDitherProcessor definition, Image<TPixel> source, Rectangle sourceRectangle) |
|||
: base(configuration, source, sourceRectangle) |
|||
{ |
|||
this.definition = definition; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override void OnFrameApply(ImageFrame<TPixel> source) |
|||
{ |
|||
IOrderedDither dither = this.definition.Dither; |
|||
TPixel upperColor = this.definition.UpperColor.ToPixel<TPixel>(); |
|||
TPixel lowerColor = this.definition.LowerColor.ToPixel<TPixel>(); |
|||
|
|||
bool isAlphaOnly = typeof(TPixel) == typeof(A8); |
|||
|
|||
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); |
|||
int startY = interest.Y; |
|||
int endY = interest.Bottom; |
|||
int startX = interest.X; |
|||
int endX = interest.Right; |
|||
|
|||
// Collect the values before looping so we can reduce our calculation count for identical sibling pixels
|
|||
TPixel sourcePixel = source[startX, startY]; |
|||
TPixel previousPixel = sourcePixel; |
|||
Rgba32 rgba = default; |
|||
sourcePixel.ToRgba32(ref rgba); |
|||
|
|||
// Convert to grayscale using ITU-R Recommendation BT.709 if required
|
|||
byte luminance = isAlphaOnly ? rgba.A : ImageMaths.Get8BitBT709Luminance(rgba.R, rgba.G, rgba.B); |
|||
|
|||
for (int y = startY; y < endY; y++) |
|||
{ |
|||
Span<TPixel> row = source.GetPixelRowSpan(y); |
|||
|
|||
for (int x = startX; x < endX; x++) |
|||
{ |
|||
sourcePixel = row[x]; |
|||
|
|||
// Check if this is the same as the last pixel. If so use that value
|
|||
// rather than calculating it again. This is an inexpensive optimization.
|
|||
if (!previousPixel.Equals(sourcePixel)) |
|||
{ |
|||
sourcePixel.ToRgba32(ref rgba); |
|||
luminance = isAlphaOnly ? rgba.A : ImageMaths.Get8BitBT709Luminance(rgba.R, rgba.G, rgba.B); |
|||
|
|||
// Setup the previous pointer
|
|||
previousPixel = sourcePixel; |
|||
} |
|||
|
|||
dither.Dither(source, sourcePixel, upperColor, lowerColor, luminance, x, y); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,228 @@ |
|||
// 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; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Convolution.Parameters |
|||
{ |
|||
/// <summary>
|
|||
/// Provides parameters to be used in the <see cref="BokehBlurProcessor{TPixel}"/>.
|
|||
/// </summary>
|
|||
internal static class BokehBlurKernelDataProvider |
|||
{ |
|||
/// <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>
|
|||
/// 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 bokeh blur kernel data for the specified parameters.
|
|||
/// </summary>
|
|||
/// <param name="radius">The value representing the size of the area to sample.</param>
|
|||
/// <param name="kernelSize">The size of each kernel to compute.</param>
|
|||
/// <param name="componentsCount">The number of components to use to approximate the original 2D bokeh blur convolution kernel.</param>
|
|||
/// <returns>A <see cref="BokehBlurKernelData"/> instance with the kernel data for the current parameters.</returns>
|
|||
public static BokehBlurKernelData GetBokehBlurKernelData( |
|||
int radius, |
|||
int kernelSize, |
|||
int componentsCount) |
|||
{ |
|||
// Reuse the initialized values from the cache, if possible
|
|||
var parameters = new BokehBlurParameters(radius, componentsCount); |
|||
if (!Cache.TryGetValue(parameters, out BokehBlurKernelData info)) |
|||
{ |
|||
// Initialize the complex kernels and parameters with the current arguments
|
|||
(Vector4[] kernelParameters, float kernelsScale) = GetParameters(componentsCount); |
|||
Complex64[][] kernels = CreateComplexKernels(kernelParameters, radius, kernelSize, kernelsScale); |
|||
NormalizeKernels(kernels, kernelParameters); |
|||
|
|||
// Store them in the cache for future use
|
|||
info = new BokehBlurKernelData(kernelParameters, kernels); |
|||
Cache.TryAdd(parameters, info); |
|||
} |
|||
|
|||
return info; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the kernel parameters and scaling factor for the current count value in the current instance
|
|||
/// </summary>
|
|||
private static (Vector4[] Parameters, float Scale) GetParameters(int componentsCount) |
|||
{ |
|||
// Prepare the kernel components
|
|||
int index = Math.Max(0, Math.Min(componentsCount - 1, KernelComponents.Count)); |
|||
|
|||
return (KernelComponents[index], KernelScales[index]); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates the collection of complex 1D kernels with the specified parameters
|
|||
/// </summary>
|
|||
/// <param name="kernelParameters">The parameters to use to normalize the kernels</param>
|
|||
/// <param name="radius">The value representing the size of the area to sample.</param>
|
|||
/// <param name="kernelSize">The size of each kernel to compute.</param>
|
|||
/// <param name="kernelsScale">The scale factor for each kernel.</param>
|
|||
private static Complex64[][] CreateComplexKernels( |
|||
Vector4[] kernelParameters, |
|||
int radius, |
|||
int kernelSize, |
|||
float kernelsScale) |
|||
{ |
|||
var kernels = new Complex64[kernelParameters.Length][]; |
|||
ref Vector4 baseRef = ref MemoryMarshal.GetReference(kernelParameters.AsSpan()); |
|||
for (int i = 0; i < kernelParameters.Length; i++) |
|||
{ |
|||
ref Vector4 paramsRef = ref Unsafe.Add(ref baseRef, i); |
|||
kernels[i] = CreateComplex1DKernel(radius, kernelSize, kernelsScale, paramsRef.X, paramsRef.Y); |
|||
} |
|||
|
|||
return kernels; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates a complex 1D kernel with the specified parameters
|
|||
/// </summary>
|
|||
/// <param name="radius">The value representing the size of the area to sample.</param>
|
|||
/// <param name="kernelSize">The size of each kernel to compute.</param>
|
|||
/// <param name="kernelsScale">The scale factor for each kernel.</param>
|
|||
/// <param name="a">The exponential parameter for each complex component</param>
|
|||
/// <param name="b">The angle component for each complex component</param>
|
|||
private static Complex64[] CreateComplex1DKernel( |
|||
int radius, |
|||
int kernelSize, |
|||
float kernelsScale, |
|||
float a, |
|||
float b) |
|||
{ |
|||
var kernel = new Complex64[kernelSize]; |
|||
ref Complex64 baseRef = ref MemoryMarshal.GetReference(kernel.AsSpan()); |
|||
int r = radius, n = -r; |
|||
|
|||
for (int i = 0; i < kernelSize; i++, n++) |
|||
{ |
|||
// Incrementally compute the range values
|
|||
float value = n * 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>
|
|||
/// <param name="kernels">The current convolution kernels to normalize</param>
|
|||
/// <param name="kernelParameters">The parameters to use to normalize the kernels</param>
|
|||
private static void NormalizeKernels(Complex64[][] kernels, Vector4[] kernelParameters) |
|||
{ |
|||
// Calculate the complex weighted sum
|
|||
float total = 0; |
|||
Span<Complex64[]> kernelsSpan = kernels.AsSpan(); |
|||
ref Complex64[] baseKernelsRef = ref MemoryMarshal.GetReference(kernelsSpan); |
|||
ref Vector4 baseParamsRef = ref MemoryMarshal.GetReference(kernelParameters.AsSpan()); |
|||
|
|||
for (int i = 0; i < 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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,33 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Dithering |
|||
{ |
|||
/// <summary>
|
|||
/// Applies error diffusion based dithering using the Atkinson image dithering algorithm.
|
|||
/// <see href="http://www.efg2.com/Lab/Library/ImageProcessing/DHALF.TXT"/>
|
|||
/// </summary>
|
|||
public sealed class AtkinsonDiffuser : ErrorDiffuser |
|||
{ |
|||
private const float Divisor = 8F; |
|||
|
|||
/// <summary>
|
|||
/// The diffusion matrix
|
|||
/// </summary>
|
|||
private static readonly DenseMatrix<float> AtkinsonMatrix = |
|||
new float[,] |
|||
{ |
|||
{ 0, 0, 1 / Divisor, 1 / Divisor }, |
|||
{ 1 / Divisor, 1 / Divisor, 1 / Divisor, 0 }, |
|||
{ 0, 1 / Divisor, 0, 0 } |
|||
}; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="AtkinsonDiffuser"/> class.
|
|||
/// </summary>
|
|||
public AtkinsonDiffuser() |
|||
: base(AtkinsonMatrix) |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
@ -1,19 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Dithering |
|||
{ |
|||
/// <summary>
|
|||
/// Applies order dithering using the 2x2 Bayer dithering matrix.
|
|||
/// </summary>
|
|||
public sealed class BayerDither2x2 : OrderedDither |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="BayerDither2x2"/> class.
|
|||
/// </summary>
|
|||
public BayerDither2x2() |
|||
: base(2) |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
@ -1,19 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Dithering |
|||
{ |
|||
/// <summary>
|
|||
/// Applies order dithering using the 4x4 Bayer dithering matrix.
|
|||
/// </summary>
|
|||
public sealed class BayerDither4x4 : OrderedDither |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="BayerDither4x4"/> class.
|
|||
/// </summary>
|
|||
public BayerDither4x4() |
|||
: base(4) |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
@ -1,19 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Dithering |
|||
{ |
|||
/// <summary>
|
|||
/// Applies order dithering using the 8x8 Bayer dithering matrix.
|
|||
/// </summary>
|
|||
public sealed class BayerDither8x8 : OrderedDither |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="BayerDither8x8"/> class.
|
|||
/// </summary>
|
|||
public BayerDither8x8() |
|||
: base(8) |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
@ -1,32 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Dithering |
|||
{ |
|||
/// <summary>
|
|||
/// Applies error diffusion based dithering using the Burks image dithering algorithm.
|
|||
/// <see href="http://www.efg2.com/Lab/Library/ImageProcessing/DHALF.TXT"/>
|
|||
/// </summary>
|
|||
public sealed class BurksDiffuser : ErrorDiffuser |
|||
{ |
|||
private const float Divisor = 32F; |
|||
|
|||
/// <summary>
|
|||
/// The diffusion matrix
|
|||
/// </summary>
|
|||
private static readonly DenseMatrix<float> BurksMatrix = |
|||
new float[,] |
|||
{ |
|||
{ 0, 0, 0, 8 / Divisor, 4 / Divisor }, |
|||
{ 2 / Divisor, 4 / Divisor, 8 / Divisor, 4 / Divisor, 2 / Divisor } |
|||
}; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="BurksDiffuser"/> class.
|
|||
/// </summary>
|
|||
public BurksDiffuser() |
|||
: base(BurksMatrix) |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,188 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Dithering |
|||
{ |
|||
/// <summary>
|
|||
/// An error diffusion dithering implementation.
|
|||
/// </summary>
|
|||
public readonly partial struct ErrorDither |
|||
{ |
|||
/// <summary>
|
|||
/// Applies error diffusion based dithering using the Atkinson image dithering algorithm.
|
|||
/// </summary>
|
|||
public static ErrorDither Atkinson = CreateAtkinson(); |
|||
|
|||
/// <summary>
|
|||
/// Applies error diffusion based dithering using the Burks image dithering algorithm.
|
|||
/// </summary>
|
|||
public static ErrorDither Burkes = CreateBurks(); |
|||
|
|||
/// <summary>
|
|||
/// Applies error diffusion based dithering using the Floyd–Steinberg image dithering algorithm.
|
|||
/// </summary>
|
|||
public static ErrorDither FloydSteinberg = CreateFloydSteinberg(); |
|||
|
|||
/// <summary>
|
|||
/// Applies error diffusion based dithering using the Jarvis, Judice, Ninke image dithering algorithm.
|
|||
/// </summary>
|
|||
public static ErrorDither JarvisJudiceNinke = CreateJarvisJudiceNinke(); |
|||
|
|||
/// <summary>
|
|||
/// Applies error diffusion based dithering using the Sierra2 image dithering algorithm.
|
|||
/// </summary>
|
|||
public static ErrorDither Sierra2 = CreateSierra2(); |
|||
|
|||
/// <summary>
|
|||
/// Applies error diffusion based dithering using the Sierra3 image dithering algorithm.
|
|||
/// </summary>
|
|||
public static ErrorDither Sierra3 = CreateSierra3(); |
|||
|
|||
/// <summary>
|
|||
/// Applies error diffusion based dithering using the Sierra Lite image dithering algorithm.
|
|||
/// </summary>
|
|||
public static ErrorDither SierraLite = CreateSierraLite(); |
|||
|
|||
/// <summary>
|
|||
/// Applies error diffusion based dithering using the Stevenson-Arce image dithering algorithm.
|
|||
/// </summary>
|
|||
public static ErrorDither StevensonArce = CreateStevensonArce(); |
|||
|
|||
/// <summary>
|
|||
/// Applies error diffusion based dithering using the Stucki image dithering algorithm.
|
|||
/// </summary>
|
|||
public static ErrorDither Stucki = CreateStucki(); |
|||
|
|||
private static ErrorDither CreateAtkinson() |
|||
{ |
|||
const float Divisor = 8F; |
|||
const int Offset = 1; |
|||
|
|||
var matrix = new float[,] |
|||
{ |
|||
{ 0, 0, 1 / Divisor, 1 / Divisor }, |
|||
{ 1 / Divisor, 1 / Divisor, 1 / Divisor, 0 }, |
|||
{ 0, 1 / Divisor, 0, 0 } |
|||
}; |
|||
|
|||
return new ErrorDither(matrix, Offset); |
|||
} |
|||
|
|||
private static ErrorDither CreateBurks() |
|||
{ |
|||
const float Divisor = 32F; |
|||
const int Offset = 2; |
|||
|
|||
var matrix = new float[,] |
|||
{ |
|||
{ 0, 0, 0, 8 / Divisor, 4 / Divisor }, |
|||
{ 2 / Divisor, 4 / Divisor, 8 / Divisor, 4 / Divisor, 2 / Divisor } |
|||
}; |
|||
|
|||
return new ErrorDither(matrix, Offset); |
|||
} |
|||
|
|||
private static ErrorDither CreateFloydSteinberg() |
|||
{ |
|||
const float Divisor = 16F; |
|||
const int Offset = 1; |
|||
|
|||
var matrix = new float[,] |
|||
{ |
|||
{ 0, 0, 7 / Divisor }, |
|||
{ 3 / Divisor, 5 / Divisor, 1 / Divisor } |
|||
}; |
|||
|
|||
return new ErrorDither(matrix, Offset); |
|||
} |
|||
|
|||
private static ErrorDither CreateJarvisJudiceNinke() |
|||
{ |
|||
const float Divisor = 48F; |
|||
const int Offset = 2; |
|||
|
|||
var matrix = new float[,] |
|||
{ |
|||
{ 0, 0, 0, 7 / Divisor, 5 / Divisor }, |
|||
{ 3 / Divisor, 5 / Divisor, 7 / Divisor, 5 / Divisor, 3 / Divisor }, |
|||
{ 1 / Divisor, 3 / Divisor, 5 / Divisor, 3 / Divisor, 1 / Divisor } |
|||
}; |
|||
|
|||
return new ErrorDither(matrix, Offset); |
|||
} |
|||
|
|||
private static ErrorDither CreateSierra2() |
|||
{ |
|||
const float Divisor = 16F; |
|||
const int Offset = 2; |
|||
|
|||
var matrix = new float[,] |
|||
{ |
|||
{ 0, 0, 0, 4 / Divisor, 3 / Divisor }, |
|||
{ 1 / Divisor, 2 / Divisor, 3 / Divisor, 2 / Divisor, 1 / Divisor } |
|||
}; |
|||
|
|||
return new ErrorDither(matrix, Offset); |
|||
} |
|||
|
|||
private static ErrorDither CreateSierra3() |
|||
{ |
|||
const float Divisor = 32F; |
|||
const int Offset = 2; |
|||
|
|||
var matrix = new float[,] |
|||
{ |
|||
{ 0, 0, 0, 5 / Divisor, 3 / Divisor }, |
|||
{ 2 / Divisor, 4 / Divisor, 5 / Divisor, 4 / Divisor, 2 / Divisor }, |
|||
{ 0, 2 / Divisor, 3 / Divisor, 2 / Divisor, 0 } |
|||
}; |
|||
|
|||
return new ErrorDither(matrix, Offset); |
|||
} |
|||
|
|||
private static ErrorDither CreateSierraLite() |
|||
{ |
|||
const float Divisor = 4F; |
|||
const int Offset = 1; |
|||
|
|||
var matrix = new float[,] |
|||
{ |
|||
{ 0, 0, 2 / Divisor }, |
|||
{ 1 / Divisor, 1 / Divisor, 0 } |
|||
}; |
|||
|
|||
return new ErrorDither(matrix, Offset); |
|||
} |
|||
|
|||
private static ErrorDither CreateStevensonArce() |
|||
{ |
|||
const float Divisor = 200F; |
|||
const int Offset = 3; |
|||
|
|||
var matrix = new float[,] |
|||
{ |
|||
{ 0, 0, 0, 0, 0, 32 / Divisor, 0 }, |
|||
{ 12 / Divisor, 0, 26 / Divisor, 0, 30 / Divisor, 0, 16 / Divisor }, |
|||
{ 0, 12 / Divisor, 0, 26 / Divisor, 0, 12 / Divisor, 0 }, |
|||
{ 5 / Divisor, 0, 12 / Divisor, 0, 12 / Divisor, 0, 5 / Divisor } |
|||
}; |
|||
|
|||
return new ErrorDither(matrix, Offset); |
|||
} |
|||
|
|||
private static ErrorDither CreateStucki() |
|||
{ |
|||
const float Divisor = 42F; |
|||
const int Offset = 2; |
|||
|
|||
var matrix = new float[,] |
|||
{ |
|||
{ 0, 0, 0, 8 / Divisor, 4 / Divisor }, |
|||
{ 2 / Divisor, 4 / Divisor, 8 / Divisor, 4 / Divisor, 2 / Divisor }, |
|||
{ 1 / Divisor, 2 / Divisor, 4 / Divisor, 2 / Divisor, 1 / Divisor } |
|||
}; |
|||
|
|||
return new ErrorDither(matrix, Offset); |
|||
} |
|||
} |
|||
} |
|||
@ -1,93 +0,0 @@ |
|||
// 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.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Dithering |
|||
{ |
|||
/// <summary>
|
|||
/// The base class for performing error diffusion based dithering.
|
|||
/// </summary>
|
|||
public abstract class ErrorDiffuser : IErrorDiffuser |
|||
{ |
|||
private readonly int offset; |
|||
private readonly DenseMatrix<float> matrix; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ErrorDiffuser"/> class.
|
|||
/// </summary>
|
|||
/// <param name="matrix">The dithering matrix.</param>
|
|||
internal ErrorDiffuser(in DenseMatrix<float> matrix) |
|||
{ |
|||
// Calculate the offset position of the pixel relative to
|
|||
// the diffusion matrix.
|
|||
this.offset = 0; |
|||
|
|||
for (int col = 0; col < matrix.Columns; col++) |
|||
{ |
|||
if (matrix[0, col] != 0) |
|||
{ |
|||
this.offset = col - 1; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
this.matrix = matrix; |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void Dither<TPixel>(ImageFrame<TPixel> image, TPixel source, TPixel transformed, int x, int y, int minX, int maxX, int maxY) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
image[x, y] = transformed; |
|||
|
|||
// Equal? Break out as there's no error to pass.
|
|||
if (source.Equals(transformed)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
// Calculate the error
|
|||
Vector4 error = source.ToVector4() - transformed.ToVector4(); |
|||
this.DoDither(image, x, y, minX, maxX, maxY, error); |
|||
} |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
private void DoDither<TPixel>(ImageFrame<TPixel> image, int x, int y, int minX, int maxX, int maxY, Vector4 error) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
int offset = this.offset; |
|||
DenseMatrix<float> matrix = this.matrix; |
|||
|
|||
// Loop through and distribute the error amongst neighboring pixels.
|
|||
for (int row = 0, targetY = y; row < matrix.Rows && targetY < maxY; row++, targetY++) |
|||
{ |
|||
Span<TPixel> rowSpan = image.GetPixelRowSpan(targetY); |
|||
|
|||
for (int col = 0; col < matrix.Columns; col++) |
|||
{ |
|||
int targetX = x + (col - offset); |
|||
if (targetX >= minX && targetX < maxX) |
|||
{ |
|||
float coefficient = matrix[row, col]; |
|||
if (coefficient == 0) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
ref TPixel pixel = ref rowSpan[targetX]; |
|||
var result = pixel.ToVector4(); |
|||
|
|||
result += error * coefficient; |
|||
pixel.FromVector4(result); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,65 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Dithering |
|||
{ |
|||
/// <summary>
|
|||
/// Defines a dither operation using error diffusion.
|
|||
/// If no palette is given this will default to the web safe colors defined in the CSS Color Module Level 4.
|
|||
/// </summary>
|
|||
public sealed class ErrorDiffusionPaletteProcessor : PaletteDitherProcessor |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ErrorDiffusionPaletteProcessor"/> class.
|
|||
/// </summary>
|
|||
/// <param name="diffuser">The error diffuser</param>
|
|||
public ErrorDiffusionPaletteProcessor(IErrorDiffuser diffuser) |
|||
: this(diffuser, .5F) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ErrorDiffusionPaletteProcessor"/> class.
|
|||
/// </summary>
|
|||
/// <param name="diffuser">The error diffuser</param>
|
|||
/// <param name="threshold">The threshold to split the image. Must be between 0 and 1.</param>
|
|||
public ErrorDiffusionPaletteProcessor(IErrorDiffuser diffuser, float threshold) |
|||
: this(diffuser, threshold, Color.WebSafePalette) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ErrorDiffusionPaletteProcessor"/> class.
|
|||
/// </summary>
|
|||
/// <param name="diffuser">The error diffuser</param>
|
|||
/// <param name="threshold">The threshold to split the image. Must be between 0 and 1.</param>
|
|||
/// <param name="palette">The palette to select substitute colors from.</param>
|
|||
public ErrorDiffusionPaletteProcessor(IErrorDiffuser diffuser, float threshold, ReadOnlyMemory<Color> palette) |
|||
: base(palette) |
|||
{ |
|||
Guard.NotNull(diffuser, nameof(diffuser)); |
|||
Guard.MustBeBetweenOrEqualTo(threshold, 0, 1, nameof(threshold)); |
|||
|
|||
this.Diffuser = diffuser; |
|||
this.Threshold = threshold; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the error diffuser.
|
|||
/// </summary>
|
|||
public IErrorDiffuser Diffuser { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the threshold value.
|
|||
/// </summary>
|
|||
public float Threshold { get; } |
|||
|
|||
/// <inheritdoc />
|
|||
public override IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>(Configuration configuration, Image<TPixel> source, Rectangle sourceRectangle) |
|||
{ |
|||
return new ErrorDiffusionPaletteProcessor<TPixel>(configuration, this, source, sourceRectangle); |
|||
} |
|||
} |
|||
} |
|||
@ -1,85 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
|
|||
using SixLabors.ImageSharp.Advanced; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Dithering |
|||
{ |
|||
/// <summary>
|
|||
/// An <see cref="IImageProcessor{TPixel}"/> that dithers an image using error diffusion.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
internal sealed class ErrorDiffusionPaletteProcessor<TPixel> : PaletteDitherProcessor<TPixel> |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ErrorDiffusionPaletteProcessor{TPixel}"/> class.
|
|||
/// </summary>
|
|||
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
|
|||
/// <param name="definition">The <see cref="ErrorDiffusionPaletteProcessor"/> 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 ErrorDiffusionPaletteProcessor(Configuration configuration, ErrorDiffusionPaletteProcessor definition, Image<TPixel> source, Rectangle sourceRectangle) |
|||
: base(configuration, definition, source, sourceRectangle) |
|||
{ |
|||
} |
|||
|
|||
private new ErrorDiffusionPaletteProcessor Definition => (ErrorDiffusionPaletteProcessor)base.Definition; |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override void OnFrameApply(ImageFrame<TPixel> source) |
|||
{ |
|||
byte threshold = (byte)MathF.Round(this.Definition.Threshold * 255F); |
|||
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); |
|||
int startY = interest.Y; |
|||
int endY = interest.Bottom; |
|||
int startX = interest.X; |
|||
int endX = interest.Right; |
|||
|
|||
// Collect the values before looping so we can reduce our calculation count for identical sibling pixels
|
|||
TPixel sourcePixel = source[startX, startY]; |
|||
TPixel previousPixel = sourcePixel; |
|||
PixelPair<TPixel> pair = this.GetClosestPixelPair(ref sourcePixel); |
|||
Rgba32 rgba = default; |
|||
sourcePixel.ToRgba32(ref rgba); |
|||
|
|||
// Convert to grayscale using ITU-R Recommendation BT.709 if required
|
|||
byte luminance = ImageMaths.Get8BitBT709Luminance(rgba.R, rgba.G, rgba.B); |
|||
|
|||
for (int y = startY; y < endY; y++) |
|||
{ |
|||
Span<TPixel> row = source.GetPixelRowSpan(y); |
|||
|
|||
for (int x = startX; x < endX; x++) |
|||
{ |
|||
sourcePixel = row[x]; |
|||
|
|||
// Check if this is the same as the last pixel. If so use that value
|
|||
// rather than calculating it again. This is an inexpensive optimization.
|
|||
if (!previousPixel.Equals(sourcePixel)) |
|||
{ |
|||
pair = this.GetClosestPixelPair(ref sourcePixel); |
|||
|
|||
// No error to spread, exact match.
|
|||
if (sourcePixel.Equals(pair.First)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
sourcePixel.ToRgba32(ref rgba); |
|||
luminance = ImageMaths.Get8BitBT709Luminance(rgba.R, rgba.G, rgba.B); |
|||
|
|||
// Setup the previous pointer
|
|||
previousPixel = sourcePixel; |
|||
} |
|||
|
|||
TPixel transformedPixel = luminance >= threshold ? pair.Second : pair.First; |
|||
this.Definition.Diffuser.Dither(source, sourcePixel, transformedPixel, x, y, startX, endX, endY); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,220 @@ |
|||
// 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.PixelFormats; |
|||
using SixLabors.ImageSharp.Processing.Processors.Quantization; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Dithering |
|||
{ |
|||
/// <summary>
|
|||
/// An error diffusion dithering implementation.
|
|||
/// <see href="http://www.efg2.com/Lab/Library/ImageProcessing/DHALF.TXT"/>
|
|||
/// </summary>
|
|||
public readonly partial struct ErrorDither : IDither, IEquatable<ErrorDither>, IEquatable<IDither> |
|||
{ |
|||
private readonly int offset; |
|||
private readonly DenseMatrix<float> matrix; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ErrorDither"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="matrix">The diffusion matrix.</param>
|
|||
/// <param name="offset">The starting offset within the matrix.</param>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public ErrorDither(in DenseMatrix<float> matrix, int offset) |
|||
{ |
|||
this.matrix = matrix; |
|||
this.offset = offset; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Compares the two <see cref="ErrorDither"/> instances to determine whether they are equal.
|
|||
/// </summary>
|
|||
/// <param name="left">The first source instance.</param>
|
|||
/// <param name="right">The second source instance.</param>
|
|||
/// <returns>The <see cref="bool"/>.</returns>
|
|||
public static bool operator ==(IDither left, ErrorDither right) |
|||
=> right == left; |
|||
|
|||
/// <summary>
|
|||
/// Compares the two <see cref="ErrorDither"/> instances to determine whether they are unequal.
|
|||
/// </summary>
|
|||
/// <param name="left">The first source instance.</param>
|
|||
/// <param name="right">The second source instance.</param>
|
|||
/// <returns>The <see cref="bool"/>.</returns>
|
|||
public static bool operator !=(IDither left, ErrorDither right) |
|||
=> !(right == left); |
|||
|
|||
/// <summary>
|
|||
/// Compares the two <see cref="ErrorDither"/> instances to determine whether they are equal.
|
|||
/// </summary>
|
|||
/// <param name="left">The first source instance.</param>
|
|||
/// <param name="right">The second source instance.</param>
|
|||
/// <returns>The <see cref="bool"/>.</returns>
|
|||
public static bool operator ==(ErrorDither left, IDither right) |
|||
=> left.Equals(right); |
|||
|
|||
/// <summary>
|
|||
/// Compares the two <see cref="ErrorDither"/> instances to determine whether they are unequal.
|
|||
/// </summary>
|
|||
/// <param name="left">The first source instance.</param>
|
|||
/// <param name="right">The second source instance.</param>
|
|||
/// <returns>The <see cref="bool"/>.</returns>
|
|||
public static bool operator !=(ErrorDither left, IDither right) |
|||
=> !(left == right); |
|||
|
|||
/// <summary>
|
|||
/// Compares the two <see cref="ErrorDither"/> instances to determine whether they are equal.
|
|||
/// </summary>
|
|||
/// <param name="left">The first source instance.</param>
|
|||
/// <param name="right">The second source instance.</param>
|
|||
/// <returns>The <see cref="bool"/>.</returns>
|
|||
public static bool operator ==(ErrorDither left, ErrorDither right) |
|||
=> left.Equals(right); |
|||
|
|||
/// <summary>
|
|||
/// Compares the two <see cref="ErrorDither"/> instances to determine whether they are unequal.
|
|||
/// </summary>
|
|||
/// <param name="left">The first source instance.</param>
|
|||
/// <param name="right">The second source instance.</param>
|
|||
/// <returns>The <see cref="bool"/>.</returns>
|
|||
public static bool operator !=(ErrorDither left, ErrorDither right) |
|||
=> !(left == right); |
|||
|
|||
/// <inheritdoc/>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void ApplyQuantizationDither<TFrameQuantizer, TPixel>( |
|||
ref TFrameQuantizer quantizer, |
|||
ReadOnlyMemory<TPixel> palette, |
|||
ImageFrame<TPixel> source, |
|||
Memory<byte> output, |
|||
Rectangle bounds) |
|||
where TFrameQuantizer : struct, IFrameQuantizer<TPixel> |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
Span<byte> outputSpan = output.Span; |
|||
ReadOnlySpan<TPixel> paletteSpan = palette.Span; |
|||
int width = bounds.Width; |
|||
int offsetY = bounds.Top; |
|||
int offsetX = bounds.Left; |
|||
float scale = quantizer.Options.DitherScale; |
|||
|
|||
for (int y = bounds.Top; y < bounds.Bottom; y++) |
|||
{ |
|||
Span<TPixel> row = source.GetPixelRowSpan(y); |
|||
int rowStart = (y - offsetY) * width; |
|||
|
|||
for (int x = bounds.Left; x < bounds.Right; x++) |
|||
{ |
|||
TPixel sourcePixel = row[x]; |
|||
outputSpan[rowStart + x - offsetX] = quantizer.GetQuantizedColor(sourcePixel, paletteSpan, out TPixel transformed); |
|||
this.Dither(source, bounds, sourcePixel, transformed, x, y, scale); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void ApplyPaletteDither<TPixel>( |
|||
Configuration configuration, |
|||
ReadOnlyMemory<TPixel> palette, |
|||
ImageFrame<TPixel> source, |
|||
Rectangle bounds, |
|||
float scale) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
var pixelMap = new EuclideanPixelMap<TPixel>(palette); |
|||
|
|||
for (int y = bounds.Top; y < bounds.Bottom; y++) |
|||
{ |
|||
Span<TPixel> row = source.GetPixelRowSpan(y); |
|||
for (int x = bounds.Left; x < bounds.Right; x++) |
|||
{ |
|||
TPixel sourcePixel = row[x]; |
|||
pixelMap.GetClosestColor(sourcePixel, out TPixel transformed); |
|||
this.Dither(source, bounds, sourcePixel, transformed, x, y, scale); |
|||
row[x] = transformed; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Internal for AOT
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
internal TPixel Dither<TPixel>( |
|||
ImageFrame<TPixel> image, |
|||
Rectangle bounds, |
|||
TPixel source, |
|||
TPixel transformed, |
|||
int x, |
|||
int y, |
|||
float scale) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
// Equal? Break out as there's no error to pass.
|
|||
if (source.Equals(transformed)) |
|||
{ |
|||
return transformed; |
|||
} |
|||
|
|||
// Calculate the error
|
|||
Vector4 error = (source.ToVector4() - transformed.ToVector4()) * scale; |
|||
|
|||
int offset = this.offset; |
|||
DenseMatrix<float> matrix = this.matrix; |
|||
|
|||
// Loop through and distribute the error amongst neighboring pixels.
|
|||
for (int row = 0, targetY = y; row < matrix.Rows; row++, targetY++) |
|||
{ |
|||
if (targetY >= bounds.Bottom) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
Span<TPixel> rowSpan = image.GetPixelRowSpan(targetY); |
|||
|
|||
for (int col = 0; col < matrix.Columns; col++) |
|||
{ |
|||
int targetX = x + (col - offset); |
|||
if (targetX < bounds.Left || targetX >= bounds.Right) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
float coefficient = matrix[row, col]; |
|||
if (coefficient == 0) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
ref TPixel pixel = ref rowSpan[targetX]; |
|||
var result = pixel.ToVector4(); |
|||
|
|||
result += error * coefficient; |
|||
pixel.FromVector4(result); |
|||
} |
|||
} |
|||
|
|||
return transformed; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool Equals(object obj) |
|||
=> obj is ErrorDither dither && this.Equals(dither); |
|||
|
|||
/// <inheritdoc/>
|
|||
public bool Equals(ErrorDither other) |
|||
=> this.offset == other.offset && this.matrix.Equals(other.matrix); |
|||
|
|||
/// <inheritdoc/>
|
|||
public bool Equals(IDither other) |
|||
=> this.Equals((object)other); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override int GetHashCode() |
|||
=> HashCode.Combine(this.offset, this.matrix); |
|||
} |
|||
} |
|||
@ -1,32 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Dithering |
|||
{ |
|||
/// <summary>
|
|||
/// Applies error diffusion based dithering using the Floyd–Steinberg image dithering algorithm.
|
|||
/// <see href="http://www.efg2.com/Lab/Library/ImageProcessing/DHALF.TXT"/>
|
|||
/// </summary>
|
|||
public sealed class FloydSteinbergDiffuser : ErrorDiffuser |
|||
{ |
|||
private const float Divisor = 16F; |
|||
|
|||
/// <summary>
|
|||
/// The diffusion matrix
|
|||
/// </summary>
|
|||
private static readonly DenseMatrix<float> FloydSteinbergMatrix = |
|||
new float[,] |
|||
{ |
|||
{ 0, 0, 7 / Divisor }, |
|||
{ 3 / Divisor, 5 / Divisor, 1 / Divisor } |
|||
}; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="FloydSteinbergDiffuser"/> class.
|
|||
/// </summary>
|
|||
public FloydSteinbergDiffuser() |
|||
: base(FloydSteinbergMatrix) |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,53 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.ImageSharp.Processing.Processors.Quantization; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Dithering |
|||
{ |
|||
/// <summary>
|
|||
/// Defines the contract for types that apply dithering to images.
|
|||
/// </summary>
|
|||
public interface IDither |
|||
{ |
|||
/// <summary>
|
|||
/// Transforms the quantized image frame applying a dither matrix.
|
|||
/// This method should be treated as destructive, altering the input pixels.
|
|||
/// </summary>
|
|||
/// <typeparam name="TFrameQuantizer">The type of frame quantizer.</typeparam>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
/// <param name="quantizer">The frame quantizer.</param>
|
|||
/// <param name="palette">The quantized palette.</param>
|
|||
/// <param name="source">The source image.</param>
|
|||
/// <param name="output">The output target</param>
|
|||
/// <param name="bounds">The region of interest bounds.</param>
|
|||
void ApplyQuantizationDither<TFrameQuantizer, TPixel>( |
|||
ref TFrameQuantizer quantizer, |
|||
ReadOnlyMemory<TPixel> palette, |
|||
ImageFrame<TPixel> source, |
|||
Memory<byte> output, |
|||
Rectangle bounds) |
|||
where TFrameQuantizer : struct, IFrameQuantizer<TPixel> |
|||
where TPixel : struct, IPixel<TPixel>; |
|||
|
|||
/// <summary>
|
|||
/// Transforms the image frame applying a dither matrix.
|
|||
/// This method should be treated as destructive, altering the input pixels.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
/// <param name="configuration">The configuration.</param>
|
|||
/// <param name="palette">The quantized palette.</param>
|
|||
/// <param name="source">The source image.</param>
|
|||
/// <param name="bounds">The region of interest bounds.</param>
|
|||
/// <param name="scale">The dithering scale used to adjust the amount of dither. Range 0..1.</param>
|
|||
void ApplyPaletteDither<TPixel>( |
|||
Configuration configuration, |
|||
ReadOnlyMemory<TPixel> palette, |
|||
ImageFrame<TPixel> source, |
|||
Rectangle bounds, |
|||
float scale) |
|||
where TPixel : struct, IPixel<TPixel>; |
|||
} |
|||
} |
|||
@ -1,28 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Dithering |
|||
{ |
|||
/// <summary>
|
|||
/// Encapsulates properties and methods required to perform diffused error dithering on an image.
|
|||
/// </summary>
|
|||
public interface IErrorDiffuser |
|||
{ |
|||
/// <summary>
|
|||
/// Transforms the image applying the dither matrix. This method alters the input pixels array
|
|||
/// </summary>
|
|||
/// <param name="image">The image</param>
|
|||
/// <param name="source">The source pixel</param>
|
|||
/// <param name="transformed">The transformed pixel</param>
|
|||
/// <param name="x">The column index.</param>
|
|||
/// <param name="y">The row index.</param>
|
|||
/// <param name="minX">The minimum column value.</param>
|
|||
/// <param name="maxX">The maximum column value.</param>
|
|||
/// <param name="maxY">The maximum row value.</param>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
void Dither<TPixel>(ImageFrame<TPixel> image, TPixel source, TPixel transformed, int x, int y, int minX, int maxX, int maxY) |
|||
where TPixel : struct, IPixel<TPixel>; |
|||
} |
|||
} |
|||
@ -1,27 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Dithering |
|||
{ |
|||
/// <summary>
|
|||
/// Encapsulates properties and methods required to perform ordered dithering on an image.
|
|||
/// </summary>
|
|||
public interface IOrderedDither |
|||
{ |
|||
/// <summary>
|
|||
/// Transforms the image applying the dither matrix. This method alters the input pixels array
|
|||
/// </summary>
|
|||
/// <param name="image">The image</param>
|
|||
/// <param name="source">The source pixel</param>
|
|||
/// <param name="upper">The color to apply to the pixels above the threshold.</param>
|
|||
/// <param name="lower">The color to apply to the pixels below the threshold.</param>
|
|||
/// <param name="threshold">The threshold to split the image. Must be between 0 and 1.</param>
|
|||
/// <param name="x">The column index.</param>
|
|||
/// <param name="y">The row index.</param>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
void Dither<TPixel>(ImageFrame<TPixel> image, TPixel source, TPixel upper, TPixel lower, float threshold, int x, int y) |
|||
where TPixel : struct, IPixel<TPixel>; |
|||
} |
|||
} |
|||
@ -1,33 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Dithering |
|||
{ |
|||
/// <summary>
|
|||
/// Applies error diffusion based dithering using the JarvisJudiceNinke image dithering algorithm.
|
|||
/// <see href="http://www.efg2.com/Lab/Library/ImageProcessing/DHALF.TXT"/>
|
|||
/// </summary>
|
|||
public sealed class JarvisJudiceNinkeDiffuser : ErrorDiffuser |
|||
{ |
|||
private const float Divisor = 48F; |
|||
|
|||
/// <summary>
|
|||
/// The diffusion matrix
|
|||
/// </summary>
|
|||
private static readonly DenseMatrix<float> JarvisJudiceNinkeMatrix = |
|||
new float[,] |
|||
{ |
|||
{ 0, 0, 0, 7 / Divisor, 5 / Divisor }, |
|||
{ 3 / Divisor, 5 / Divisor, 7 / Divisor, 5 / Divisor, 3 / Divisor }, |
|||
{ 1 / Divisor, 3 / Divisor, 5 / Divisor, 3 / Divisor, 1 / Divisor } |
|||
}; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="JarvisJudiceNinkeDiffuser"/> class.
|
|||
/// </summary>
|
|||
public JarvisJudiceNinkeDiffuser() |
|||
: base(JarvisJudiceNinkeMatrix) |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Dithering |
|||
{ |
|||
/// <content>
|
|||
/// An ordered dithering matrix with equal sides of arbitrary length
|
|||
/// </content>
|
|||
public readonly partial struct OrderedDither : IDither |
|||
{ |
|||
/// <summary>
|
|||
/// Applies order dithering using the 2x2 Bayer dithering matrix.
|
|||
/// </summary>
|
|||
public static OrderedDither Bayer2x2 = new OrderedDither(2); |
|||
|
|||
/// <summary>
|
|||
/// Applies order dithering using the 4x4 Bayer dithering matrix.
|
|||
/// </summary>
|
|||
public static OrderedDither Bayer4x4 = new OrderedDither(4); |
|||
|
|||
/// <summary>
|
|||
/// Applies order dithering using the 8x8 Bayer dithering matrix.
|
|||
/// </summary>
|
|||
public static OrderedDither Bayer8x8 = new OrderedDither(8); |
|||
|
|||
/// <summary>
|
|||
/// Applies order dithering using the 3x3 ordered dithering matrix.
|
|||
/// </summary>
|
|||
public static OrderedDither Ordered3x3 = new OrderedDither(3); |
|||
} |
|||
} |
|||
@ -1,49 +1,303 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Runtime.CompilerServices; |
|||
using SixLabors.ImageSharp.Advanced; |
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.ImageSharp.Processing.Processors.Quantization; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Dithering |
|||
{ |
|||
/// <summary>
|
|||
/// An ordered dithering matrix with equal sides of arbitrary length
|
|||
/// </summary>
|
|||
public class OrderedDither : IOrderedDither |
|||
public readonly partial struct OrderedDither : IDither, IEquatable<OrderedDither>, IEquatable<IDither> |
|||
{ |
|||
private readonly DenseMatrix<uint> thresholdMatrix; |
|||
private readonly DenseMatrix<float> thresholdMatrix; |
|||
private readonly int modulusX; |
|||
private readonly int modulusY; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="OrderedDither"/> class.
|
|||
/// Initializes a new instance of the <see cref="OrderedDither"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="length">The length of the matrix sides</param>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public OrderedDither(uint length) |
|||
{ |
|||
DenseMatrix<uint> ditherMatrix = OrderedDitherFactory.CreateDitherMatrix(length); |
|||
|
|||
// Create a new matrix to run against, that pre-thresholds the values.
|
|||
// We don't want to adjust the original matrix generation code as that
|
|||
// creates known, easy to test values.
|
|||
// https://en.wikipedia.org/wiki/Ordered_dithering#Algorithm
|
|||
var thresholdMatrix = new DenseMatrix<float>((int)length); |
|||
float m2 = length * length; |
|||
for (int y = 0; y < length; y++) |
|||
{ |
|||
for (int x = 0; x < length; x++) |
|||
{ |
|||
thresholdMatrix[y, x] = ((ditherMatrix[y, x] + 1) / m2) - .5F; |
|||
} |
|||
} |
|||
|
|||
this.modulusX = ditherMatrix.Columns; |
|||
this.modulusY = ditherMatrix.Rows; |
|||
this.thresholdMatrix = thresholdMatrix; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Compares the two <see cref="OrderedDither"/> instances to determine whether they are equal.
|
|||
/// </summary>
|
|||
/// <param name="left">The first source instance.</param>
|
|||
/// <param name="right">The second source instance.</param>
|
|||
/// <returns>The <see cref="bool"/>.</returns>
|
|||
public static bool operator ==(IDither left, OrderedDither right) |
|||
=> right == left; |
|||
|
|||
/// <summary>
|
|||
/// Compares the two <see cref="OrderedDither"/> instances to determine whether they are unequal.
|
|||
/// </summary>
|
|||
/// <param name="left">The first source instance.</param>
|
|||
/// <param name="right">The second source instance.</param>
|
|||
/// <returns>The <see cref="bool"/>.</returns>
|
|||
public static bool operator !=(IDither left, OrderedDither right) |
|||
=> !(right == left); |
|||
|
|||
/// <summary>
|
|||
/// Compares the two <see cref="OrderedDither"/> instances to determine whether they are equal.
|
|||
/// </summary>
|
|||
/// <param name="left">The first source instance.</param>
|
|||
/// <param name="right">The second source instance.</param>
|
|||
/// <returns>The <see cref="bool"/>.</returns>
|
|||
public static bool operator ==(OrderedDither left, IDither right) |
|||
=> left.Equals(right); |
|||
|
|||
/// <summary>
|
|||
/// Compares the two <see cref="OrderedDither"/> instances to determine whether they are unequal.
|
|||
/// </summary>
|
|||
/// <param name="left">The first source instance.</param>
|
|||
/// <param name="right">The second source instance.</param>
|
|||
/// <returns>The <see cref="bool"/>.</returns>
|
|||
public static bool operator !=(OrderedDither left, IDither right) |
|||
=> !(left == right); |
|||
|
|||
/// <summary>
|
|||
/// Compares the two <see cref="OrderedDither"/> instances to determine whether they are equal.
|
|||
/// </summary>
|
|||
/// <param name="left">The first source instance.</param>
|
|||
/// <param name="right">The second source instance.</param>
|
|||
/// <returns>The <see cref="bool"/>.</returns>
|
|||
public static bool operator ==(OrderedDither left, OrderedDither right) |
|||
=> left.Equals(right); |
|||
|
|||
/// <summary>
|
|||
/// Compares the two <see cref="OrderedDither"/> instances to determine whether they are unequal.
|
|||
/// </summary>
|
|||
/// <param name="left">The first source instance.</param>
|
|||
/// <param name="right">The second source instance.</param>
|
|||
/// <returns>The <see cref="bool"/>.</returns>
|
|||
public static bool operator !=(OrderedDither left, OrderedDither right) |
|||
=> !(left == right); |
|||
|
|||
/// <inheritdoc/>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void ApplyQuantizationDither<TFrameQuantizer, TPixel>( |
|||
ref TFrameQuantizer quantizer, |
|||
ReadOnlyMemory<TPixel> palette, |
|||
ImageFrame<TPixel> source, |
|||
Memory<byte> output, |
|||
Rectangle bounds) |
|||
where TFrameQuantizer : struct, IFrameQuantizer<TPixel> |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
var ditherOperation = new QuantizeDitherRowIntervalOperation<TFrameQuantizer, TPixel>( |
|||
ref quantizer, |
|||
in Unsafe.AsRef(this), |
|||
source, |
|||
output, |
|||
bounds, |
|||
palette, |
|||
ImageMaths.GetBitsNeededForColorDepth(palette.Span.Length)); |
|||
|
|||
ParallelRowIterator.IterateRows( |
|||
quantizer.Configuration, |
|||
bounds, |
|||
in ditherOperation); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void ApplyPaletteDither<TPixel>( |
|||
Configuration configuration, |
|||
ReadOnlyMemory<TPixel> palette, |
|||
ImageFrame<TPixel> source, |
|||
Rectangle bounds, |
|||
float scale) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
var ditherOperation = new PaletteDitherRowIntervalOperation<TPixel>( |
|||
in Unsafe.AsRef(this), |
|||
source, |
|||
bounds, |
|||
palette, |
|||
scale, |
|||
ImageMaths.GetBitsNeededForColorDepth(palette.Span.Length)); |
|||
|
|||
// Adjust the matrix range for 0-255
|
|||
// TODO: It looks like it's actually possible to dither an image using it's own colors. We should investigate for V2
|
|||
// https://stackoverflow.com/questions/12422407/monochrome-dithering-in-javascript-bayer-atkinson-floyd-steinberg
|
|||
int multiplier = 256 / ditherMatrix.Count; |
|||
for (int y = 0; y < ditherMatrix.Rows; y++) |
|||
ParallelRowIterator.IterateRows( |
|||
configuration, |
|||
bounds, |
|||
in ditherOperation); |
|||
} |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
internal TPixel Dither<TPixel>( |
|||
TPixel source, |
|||
int x, |
|||
int y, |
|||
int bitDepth, |
|||
float scale) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
Rgba32 rgba = default; |
|||
source.ToRgba32(ref rgba); |
|||
Rgba32 attempt; |
|||
|
|||
// Spread assumes an even colorspace distribution and precision.
|
|||
// Calculated as 0-255/component count. 256 / bitDepth
|
|||
// https://bisqwit.iki.fi/story/howto/dither/jy/
|
|||
// https://en.wikipedia.org/wiki/Ordered_dithering#Algorithm
|
|||
int spread = 256 / bitDepth; |
|||
float factor = spread * this.thresholdMatrix[y % this.modulusY, x % this.modulusX] * scale; |
|||
|
|||
attempt.R = (byte)(rgba.R + factor).Clamp(byte.MinValue, byte.MaxValue); |
|||
attempt.G = (byte)(rgba.G + factor).Clamp(byte.MinValue, byte.MaxValue); |
|||
attempt.B = (byte)(rgba.B + factor).Clamp(byte.MinValue, byte.MaxValue); |
|||
attempt.A = (byte)(rgba.A + factor).Clamp(byte.MinValue, byte.MaxValue); |
|||
|
|||
TPixel result = default; |
|||
result.FromRgba32(attempt); |
|||
|
|||
return result; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool Equals(object obj) |
|||
=> obj is OrderedDither dither && this.Equals(dither); |
|||
|
|||
/// <inheritdoc/>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public bool Equals(OrderedDither other) |
|||
=> this.thresholdMatrix.Equals(other.thresholdMatrix) && this.modulusX == other.modulusX && this.modulusY == other.modulusY; |
|||
|
|||
/// <inheritdoc/>
|
|||
public bool Equals(IDither other) |
|||
=> this.Equals((object)other); |
|||
|
|||
/// <inheritdoc/>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public override int GetHashCode() |
|||
=> HashCode.Combine(this.thresholdMatrix, this.modulusX, this.modulusY); |
|||
|
|||
private readonly struct QuantizeDitherRowIntervalOperation<TFrameQuantizer, TPixel> : IRowIntervalOperation |
|||
where TFrameQuantizer : struct, IFrameQuantizer<TPixel> |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
private readonly TFrameQuantizer quantizer; |
|||
private readonly OrderedDither dither; |
|||
private readonly ImageFrame<TPixel> source; |
|||
private readonly Memory<byte> output; |
|||
private readonly Rectangle bounds; |
|||
private readonly ReadOnlyMemory<TPixel> palette; |
|||
private readonly int bitDepth; |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public QuantizeDitherRowIntervalOperation( |
|||
ref TFrameQuantizer quantizer, |
|||
in OrderedDither dither, |
|||
ImageFrame<TPixel> source, |
|||
Memory<byte> output, |
|||
Rectangle bounds, |
|||
ReadOnlyMemory<TPixel> palette, |
|||
int bitDepth) |
|||
{ |
|||
this.quantizer = quantizer; |
|||
this.dither = dither; |
|||
this.source = source; |
|||
this.output = output; |
|||
this.bounds = bounds; |
|||
this.palette = palette; |
|||
this.bitDepth = bitDepth; |
|||
} |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void Invoke(in RowInterval rows) |
|||
{ |
|||
for (int x = 0; x < ditherMatrix.Columns; x++) |
|||
ReadOnlySpan<TPixel> paletteSpan = this.palette.Span; |
|||
Span<byte> outputSpan = this.output.Span; |
|||
int width = this.bounds.Width; |
|||
int offsetY = this.bounds.Top; |
|||
int offsetX = this.bounds.Left; |
|||
float scale = this.quantizer.Options.DitherScale; |
|||
|
|||
for (int y = rows.Min; y < rows.Max; y++) |
|||
{ |
|||
ditherMatrix[y, x] = (uint)((ditherMatrix[y, x] + 1) * multiplier) - 1; |
|||
Span<TPixel> row = this.source.GetPixelRowSpan(y); |
|||
int rowStart = (y - offsetY) * width; |
|||
|
|||
// TODO: This can be a bulk operation.
|
|||
for (int x = this.bounds.Left; x < this.bounds.Right; x++) |
|||
{ |
|||
TPixel dithered = this.dither.Dither(row[x], x, y, this.bitDepth, scale); |
|||
outputSpan[rowStart + x - offsetX] = this.quantizer.GetQuantizedColor(dithered, paletteSpan, out TPixel _); |
|||
} |
|||
} |
|||
} |
|||
|
|||
this.thresholdMatrix = ditherMatrix; |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public void Dither<TPixel>(ImageFrame<TPixel> image, TPixel source, TPixel upper, TPixel lower, float threshold, int x, int y) |
|||
private readonly struct PaletteDitherRowIntervalOperation<TPixel> : IRowIntervalOperation |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
image[x, y] = this.thresholdMatrix[y % this.modulusY, x % this.modulusX] >= threshold ? lower : upper; |
|||
private readonly OrderedDither dither; |
|||
private readonly ImageFrame<TPixel> source; |
|||
private readonly Rectangle bounds; |
|||
private readonly EuclideanPixelMap<TPixel> pixelMap; |
|||
private readonly float scale; |
|||
private readonly int bitDepth; |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public PaletteDitherRowIntervalOperation( |
|||
in OrderedDither dither, |
|||
ImageFrame<TPixel> source, |
|||
Rectangle bounds, |
|||
ReadOnlyMemory<TPixel> palette, |
|||
float scale, |
|||
int bitDepth) |
|||
{ |
|||
this.dither = dither; |
|||
this.source = source; |
|||
this.bounds = bounds; |
|||
this.pixelMap = new EuclideanPixelMap<TPixel>(palette); |
|||
this.scale = scale; |
|||
this.bitDepth = bitDepth; |
|||
} |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void Invoke(in RowInterval rows) |
|||
{ |
|||
for (int y = rows.Min; y < rows.Max; y++) |
|||
{ |
|||
Span<TPixel> row = this.source.GetPixelRowSpan(y); |
|||
|
|||
for (int x = this.bounds.Left; x < this.bounds.Right; x++) |
|||
{ |
|||
TPixel dithered = this.dither.Dither(row[x], x, y, this.bitDepth, this.scale); |
|||
this.pixelMap.GetClosestColor(dithered, out TPixel transformed); |
|||
row[x] = transformed; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -1,19 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Dithering |
|||
{ |
|||
/// <summary>
|
|||
/// Applies order dithering using the 3x3 dithering matrix.
|
|||
/// </summary>
|
|||
public sealed class OrderedDither3x3 : OrderedDither |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="OrderedDither3x3"/> class.
|
|||
/// </summary>
|
|||
public OrderedDither3x3() |
|||
: base(3) |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
@ -1,40 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Dithering |
|||
{ |
|||
/// <summary>
|
|||
/// Defines a dithering operation that dithers an image using error diffusion.
|
|||
/// If no palette is given this will default to the web safe colors defined in the CSS Color Module Level 4.
|
|||
/// </summary>
|
|||
public sealed class OrderedDitherPaletteProcessor : PaletteDitherProcessor |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="OrderedDitherPaletteProcessor"/> class.
|
|||
/// </summary>
|
|||
/// <param name="dither">The ordered ditherer.</param>
|
|||
public OrderedDitherPaletteProcessor(IOrderedDither dither) |
|||
: this(dither, Color.WebSafePalette) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="OrderedDitherPaletteProcessor"/> class.
|
|||
/// </summary>
|
|||
/// <param name="dither">The ordered ditherer.</param>
|
|||
/// <param name="palette">The palette to select substitute colors from.</param>
|
|||
public OrderedDitherPaletteProcessor(IOrderedDither dither, ReadOnlyMemory<Color> palette) |
|||
: base(palette) => this.Dither = dither ?? throw new ArgumentNullException(nameof(dither)); |
|||
|
|||
/// <summary>
|
|||
/// Gets the ditherer.
|
|||
/// </summary>
|
|||
public IOrderedDither Dither { get; } |
|||
|
|||
/// <inheritdoc />
|
|||
public override IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>(Configuration configuration, Image<TPixel> source, Rectangle sourceRectangle) |
|||
=> new OrderedDitherPaletteProcessor<TPixel>(configuration, this, source, sourceRectangle); |
|||
} |
|||
} |
|||
@ -1,83 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
|
|||
using SixLabors.ImageSharp.Advanced; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Dithering |
|||
{ |
|||
/// <summary>
|
|||
/// An <see cref="IImageProcessor{TPixel}"/> that dithers an image using error diffusion.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
internal class OrderedDitherPaletteProcessor<TPixel> : PaletteDitherProcessor<TPixel> |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="OrderedDitherPaletteProcessor{TPixel}"/> class.
|
|||
/// </summary>
|
|||
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
|
|||
/// <param name="definition">The <see cref="OrderedDitherPaletteProcessor"/> 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 OrderedDitherPaletteProcessor(Configuration configuration, OrderedDitherPaletteProcessor definition, Image<TPixel> source, Rectangle sourceRectangle) |
|||
: base(configuration, definition, source, sourceRectangle) |
|||
{ |
|||
} |
|||
|
|||
private new OrderedDitherPaletteProcessor Definition => (OrderedDitherPaletteProcessor)base.Definition; |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override void OnFrameApply(ImageFrame<TPixel> source) |
|||
{ |
|||
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); |
|||
int startY = interest.Y; |
|||
int endY = interest.Bottom; |
|||
int startX = interest.X; |
|||
int endX = interest.Right; |
|||
|
|||
// Collect the values before looping so we can reduce our calculation count for identical sibling pixels
|
|||
TPixel sourcePixel = source[startX, startY]; |
|||
TPixel previousPixel = sourcePixel; |
|||
PixelPair<TPixel> pair = this.GetClosestPixelPair(ref sourcePixel); |
|||
Rgba32 rgba = default; |
|||
sourcePixel.ToRgba32(ref rgba); |
|||
|
|||
// Convert to grayscale using ITU-R Recommendation BT.709 if required
|
|||
byte luminance = ImageMaths.Get8BitBT709Luminance(rgba.R, rgba.G, rgba.B); |
|||
|
|||
for (int y = startY; y < endY; y++) |
|||
{ |
|||
Span<TPixel> row = source.GetPixelRowSpan(y); |
|||
|
|||
for (int x = startX; x < endX; x++) |
|||
{ |
|||
sourcePixel = row[x]; |
|||
|
|||
// Check if this is the same as the last pixel. If so use that value
|
|||
// rather than calculating it again. This is an inexpensive optimization.
|
|||
if (!previousPixel.Equals(sourcePixel)) |
|||
{ |
|||
pair = this.GetClosestPixelPair(ref sourcePixel); |
|||
|
|||
// No error to spread, exact match.
|
|||
if (sourcePixel.Equals(pair.First)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
sourcePixel.ToRgba32(ref rgba); |
|||
luminance = ImageMaths.Get8BitBT709Luminance(rgba.R, rgba.G, rgba.B); |
|||
|
|||
// Setup the previous pointer
|
|||
previousPixel = sourcePixel; |
|||
} |
|||
|
|||
this.Definition.Dither.Dither(source, sourcePixel, pair.Second, pair.First, luminance, x, y); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,48 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Dithering |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a composite pair of pixels. Used for caching color distance lookups.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
internal readonly struct PixelPair<TPixel> : IEquatable<PixelPair<TPixel>> |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="PixelPair{TPixel}"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="first">The first pixel color</param>
|
|||
/// <param name="second">The second pixel color</param>
|
|||
public PixelPair(TPixel first, TPixel second) |
|||
{ |
|||
this.First = first; |
|||
this.Second = second; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the first pixel color
|
|||
/// </summary>
|
|||
public TPixel First { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the second pixel color
|
|||
/// </summary>
|
|||
public TPixel Second { get; } |
|||
|
|||
/// <inheritdoc/>
|
|||
public bool Equals(PixelPair<TPixel> other) |
|||
=> this.First.Equals(other.First) && this.Second.Equals(other.Second); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool Equals(object obj) |
|||
=> obj is PixelPair<TPixel> other && this.First.Equals(other.First) && this.Second.Equals(other.Second); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override int GetHashCode() => HashCode.Combine(this.First, this.Second); |
|||
} |
|||
} |
|||
@ -1,32 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Dithering |
|||
{ |
|||
/// <summary>
|
|||
/// Applies error diffusion based dithering using the Sierra2 image dithering algorithm.
|
|||
/// <see href="http://www.efg2.com/Lab/Library/ImageProcessing/DHALF.TXT"/>
|
|||
/// </summary>
|
|||
public sealed class Sierra2Diffuser : ErrorDiffuser |
|||
{ |
|||
private const float Divisor = 16F; |
|||
|
|||
/// <summary>
|
|||
/// The diffusion matrix
|
|||
/// </summary>
|
|||
private static readonly DenseMatrix<float> Sierra2Matrix = |
|||
new float[,] |
|||
{ |
|||
{ 0, 0, 0, 4 / Divisor, 3 / Divisor }, |
|||
{ 1 / Divisor, 2 / Divisor, 3 / Divisor, 2 / Divisor, 1 / Divisor } |
|||
}; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="Sierra2Diffuser"/> class.
|
|||
/// </summary>
|
|||
public Sierra2Diffuser() |
|||
: base(Sierra2Matrix) |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
@ -1,33 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Dithering |
|||
{ |
|||
/// <summary>
|
|||
/// Applies error diffusion based dithering using the Sierra3 image dithering algorithm.
|
|||
/// <see href="http://www.efg2.com/Lab/Library/ImageProcessing/DHALF.TXT"/>
|
|||
/// </summary>
|
|||
public sealed class Sierra3Diffuser : ErrorDiffuser |
|||
{ |
|||
private const float Divisor = 32F; |
|||
|
|||
/// <summary>
|
|||
/// The diffusion matrix
|
|||
/// </summary>
|
|||
private static readonly DenseMatrix<float> Sierra3Matrix = |
|||
new float[,] |
|||
{ |
|||
{ 0, 0, 0, 5 / Divisor, 3 / Divisor }, |
|||
{ 2 / Divisor, 4 / Divisor, 5 / Divisor, 4 / Divisor, 2 / Divisor }, |
|||
{ 0, 2 / Divisor, 3 / Divisor, 2 / Divisor, 0 } |
|||
}; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="Sierra3Diffuser"/> class.
|
|||
/// </summary>
|
|||
public Sierra3Diffuser() |
|||
: base(Sierra3Matrix) |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
@ -1,32 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Dithering |
|||
{ |
|||
/// <summary>
|
|||
/// Applies error diffusion based dithering using the SierraLite image dithering algorithm.
|
|||
/// <see href="http://www.efg2.com/Lab/Library/ImageProcessing/DHALF.TXT"/>
|
|||
/// </summary>
|
|||
public sealed class SierraLiteDiffuser : ErrorDiffuser |
|||
{ |
|||
private const float Divisor = 4F; |
|||
|
|||
/// <summary>
|
|||
/// The diffusion matrix
|
|||
/// </summary>
|
|||
private static readonly DenseMatrix<float> SierraLiteMatrix = |
|||
new float[,] |
|||
{ |
|||
{ 0, 0, 2 / Divisor }, |
|||
{ 1 / Divisor, 1 / Divisor, 0 } |
|||
}; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="SierraLiteDiffuser"/> class.
|
|||
/// </summary>
|
|||
public SierraLiteDiffuser() |
|||
: base(SierraLiteMatrix) |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
@ -1,33 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Dithering |
|||
{ |
|||
/// <summary>
|
|||
/// Applies error diffusion based dithering using the Stevenson-Arce image dithering algorithm.
|
|||
/// </summary>
|
|||
public sealed class StevensonArceDiffuser : ErrorDiffuser |
|||
{ |
|||
private const float Divisor = 200F; |
|||
|
|||
/// <summary>
|
|||
/// The diffusion matrix
|
|||
/// </summary>
|
|||
private static readonly DenseMatrix<float> StevensonArceMatrix = |
|||
new float[,] |
|||
{ |
|||
{ 0, 0, 0, 0, 0, 32 / Divisor, 0 }, |
|||
{ 12 / Divisor, 0, 26 / Divisor, 0, 30 / Divisor, 0, 16 / Divisor }, |
|||
{ 0, 12 / Divisor, 0, 26 / Divisor, 0, 12 / Divisor, 0 }, |
|||
{ 5 / Divisor, 0, 12 / Divisor, 0, 12 / Divisor, 0, 5 / Divisor } |
|||
}; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="StevensonArceDiffuser"/> class.
|
|||
/// </summary>
|
|||
public StevensonArceDiffuser() |
|||
: base(StevensonArceMatrix) |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
@ -1,33 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Dithering |
|||
{ |
|||
/// <summary>
|
|||
/// Applies error diffusion based dithering using the Stucki image dithering algorithm.
|
|||
/// <see href="http://www.efg2.com/Lab/Library/ImageProcessing/DHALF.TXT"/>
|
|||
/// </summary>
|
|||
public sealed class StuckiDiffuser : ErrorDiffuser |
|||
{ |
|||
private const float Divisor = 42F; |
|||
|
|||
/// <summary>
|
|||
/// The diffusion matrix
|
|||
/// </summary>
|
|||
private static readonly DenseMatrix<float> StuckiMatrix = |
|||
new float[,] |
|||
{ |
|||
{ 0, 0, 0, 8 / Divisor, 4 / Divisor }, |
|||
{ 2 / Divisor, 4 / Divisor, 8 / Divisor, 4 / Divisor, 2 / Divisor }, |
|||
{ 1 / Divisor, 2 / Divisor, 4 / Divisor, 2 / Divisor, 1 / Divisor } |
|||
}; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="StuckiDiffuser"/> class.
|
|||
/// </summary>
|
|||
public StuckiDiffuser() |
|||
: base(StuckiMatrix) |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
Binary file not shown.
@ -0,0 +1,106 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Collections.Concurrent; |
|||
using System.Numerics; |
|||
using System.Runtime.CompilerServices; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Quantization |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the closest color to the supplied color based upon the Eucladean distance.
|
|||
/// TODO: Expose this somehow.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
internal readonly struct EuclideanPixelMap<TPixel> : IPixelMap<TPixel>, IEquatable<EuclideanPixelMap<TPixel>> |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
private readonly ConcurrentDictionary<int, Vector4> vectorCache; |
|||
private readonly ConcurrentDictionary<TPixel, int> distanceCache; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="EuclideanPixelMap{TPixel}"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="palette">The color palette to map from.</param>
|
|||
public EuclideanPixelMap(ReadOnlyMemory<TPixel> palette) |
|||
{ |
|||
Guard.MustBeGreaterThan(palette.Length, 0, nameof(palette)); |
|||
|
|||
this.Palette = palette; |
|||
ReadOnlySpan<TPixel> paletteSpan = this.Palette.Span; |
|||
this.vectorCache = new ConcurrentDictionary<int, Vector4>(); |
|||
this.distanceCache = new ConcurrentDictionary<TPixel, int>(); |
|||
|
|||
for (int i = 0; i < paletteSpan.Length; i++) |
|||
{ |
|||
this.vectorCache[i] = paletteSpan[i].ToScaledVector4(); |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public ReadOnlyMemory<TPixel> Palette { get; } |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool Equals(object obj) |
|||
=> obj is EuclideanPixelMap<TPixel> map && this.Equals(map); |
|||
|
|||
/// <inheritdoc/>
|
|||
public bool Equals(EuclideanPixelMap<TPixel> other) |
|||
=> this.Palette.Equals(other.Palette); |
|||
|
|||
/// <inheritdoc/>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public int GetClosestColor(TPixel color, out TPixel match) |
|||
{ |
|||
ReadOnlySpan<TPixel> paletteSpan = this.Palette.Span; |
|||
|
|||
// Check if the color is in the lookup table
|
|||
if (this.distanceCache.TryGetValue(color, out int index)) |
|||
{ |
|||
match = paletteSpan[index]; |
|||
return index; |
|||
} |
|||
|
|||
return this.GetClosestColorSlow(color, paletteSpan, out match); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override int GetHashCode() |
|||
=> this.vectorCache.GetHashCode(); |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
private int GetClosestColorSlow(TPixel color, ReadOnlySpan<TPixel> palette, out TPixel match) |
|||
{ |
|||
// Loop through the palette and find the nearest match.
|
|||
int index = 0; |
|||
float leastDistance = float.MaxValue; |
|||
Vector4 vector = color.ToScaledVector4(); |
|||
|
|||
for (int i = 0; i < palette.Length; i++) |
|||
{ |
|||
Vector4 candidate = this.vectorCache[i]; |
|||
float distance = Vector4.DistanceSquared(vector, candidate); |
|||
|
|||
// Less than... assign.
|
|||
if (distance < leastDistance) |
|||
{ |
|||
index = i; |
|||
leastDistance = distance; |
|||
|
|||
// And if it's an exact match, exit the loop
|
|||
if (distance == 0) |
|||
{ |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Now I have the index, pop it into the cache for next time
|
|||
this.distanceCache[color] = index; |
|||
match = palette[index]; |
|||
return index; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,136 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Runtime.CompilerServices; |
|||
using SixLabors.ImageSharp.Advanced; |
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.ImageSharp.Processing.Processors.Dithering; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Quantization |
|||
{ |
|||
/// <summary>
|
|||
/// Contains extension methods for frame quantizers.
|
|||
/// </summary>
|
|||
public static class FrameQuantizerExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// Quantizes an image frame and return the resulting output pixels.
|
|||
/// </summary>
|
|||
/// <typeparam name="TFrameQuantizer">The type of frame quantizer.</typeparam>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
/// <param name="quantizer">The frame </param>
|
|||
/// <param name="source">The source image frame to quantize.</param>
|
|||
/// <param name="bounds">The bounds within the frame to quantize.</param>
|
|||
/// <returns>
|
|||
/// A <see cref="QuantizedFrame{TPixel}"/> representing a quantized version of the source frame pixels.
|
|||
/// </returns>
|
|||
public static QuantizedFrame<TPixel> QuantizeFrame<TFrameQuantizer, TPixel>( |
|||
ref TFrameQuantizer quantizer, |
|||
ImageFrame<TPixel> source, |
|||
Rectangle bounds) |
|||
where TFrameQuantizer : struct, IFrameQuantizer<TPixel> |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
Guard.NotNull(source, nameof(source)); |
|||
var interest = Rectangle.Intersect(source.Bounds(), bounds); |
|||
|
|||
// Collect the palette. Required before the second pass runs.
|
|||
ReadOnlyMemory<TPixel> palette = quantizer.BuildPalette(source, interest); |
|||
MemoryAllocator memoryAllocator = quantizer.Configuration.MemoryAllocator; |
|||
|
|||
var quantizedFrame = new QuantizedFrame<TPixel>(memoryAllocator, interest.Width, interest.Height, palette); |
|||
Memory<byte> output = quantizedFrame.GetWritablePixelMemory(); |
|||
|
|||
if (quantizer.Options.Dither is null) |
|||
{ |
|||
SecondPass(ref quantizer, source, interest, output, palette); |
|||
} |
|||
else |
|||
{ |
|||
// We clone the image as we don't want to alter the original via error diffusion based dithering.
|
|||
using (ImageFrame<TPixel> clone = source.Clone()) |
|||
{ |
|||
SecondPass(ref quantizer, clone, interest, output, palette); |
|||
} |
|||
} |
|||
|
|||
return quantizedFrame; |
|||
} |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
private static void SecondPass<TFrameQuantizer, TPixel>( |
|||
ref TFrameQuantizer quantizer, |
|||
ImageFrame<TPixel> source, |
|||
Rectangle bounds, |
|||
Memory<byte> output, |
|||
ReadOnlyMemory<TPixel> palette) |
|||
where TFrameQuantizer : struct, IFrameQuantizer<TPixel> |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
IDither dither = quantizer.Options.Dither; |
|||
|
|||
if (dither is null) |
|||
{ |
|||
var operation = new RowIntervalOperation<TFrameQuantizer, TPixel>(quantizer, source, output, bounds, palette); |
|||
ParallelRowIterator.IterateRows( |
|||
quantizer.Configuration, |
|||
bounds, |
|||
in operation); |
|||
|
|||
return; |
|||
} |
|||
|
|||
dither.ApplyQuantizationDither(ref quantizer, palette, source, output, bounds); |
|||
} |
|||
|
|||
private readonly struct RowIntervalOperation<TFrameQuantizer, TPixel> : IRowIntervalOperation |
|||
where TFrameQuantizer : struct, IFrameQuantizer<TPixel> |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
private readonly TFrameQuantizer quantizer; |
|||
private readonly ImageFrame<TPixel> source; |
|||
private readonly Memory<byte> output; |
|||
private readonly Rectangle bounds; |
|||
private readonly ReadOnlyMemory<TPixel> palette; |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public RowIntervalOperation( |
|||
in TFrameQuantizer quantizer, |
|||
ImageFrame<TPixel> source, |
|||
Memory<byte> output, |
|||
Rectangle bounds, |
|||
ReadOnlyMemory<TPixel> palette) |
|||
{ |
|||
this.quantizer = quantizer; |
|||
this.source = source; |
|||
this.output = output; |
|||
this.bounds = bounds; |
|||
this.palette = palette; |
|||
} |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void Invoke(in RowInterval rows) |
|||
{ |
|||
ReadOnlySpan<TPixel> paletteSpan = this.palette.Span; |
|||
Span<byte> outputSpan = this.output.Span; |
|||
int width = this.bounds.Width; |
|||
int offsetY = this.bounds.Top; |
|||
int offsetX = this.bounds.Left; |
|||
|
|||
for (int y = rows.Min; y < rows.Max; y++) |
|||
{ |
|||
Span<TPixel> row = this.source.GetPixelRowSpan(y); |
|||
int rowStart = (y - offsetY) * width; |
|||
|
|||
// TODO: This can be a bulk operation.
|
|||
for (int x = this.bounds.Left; x < this.bounds.Right; x++) |
|||
{ |
|||
outputSpan[rowStart + x - offsetX] = this.quantizer.GetQuantizedColor(row[x], paletteSpan, out TPixel _); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,285 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Buffers; |
|||
using System.Collections.Generic; |
|||
using System.Numerics; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Runtime.InteropServices; |
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.ImageSharp.Processing.Processors.Dithering; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Quantization |
|||
{ |
|||
/// <summary>
|
|||
/// The base class for all <see cref="IFrameQuantizer{TPixel}"/> implementations
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
public abstract class FrameQuantizer<TPixel> : IFrameQuantizer<TPixel> |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
/// <summary>
|
|||
/// A lookup table for colors
|
|||
/// </summary>
|
|||
private readonly Dictionary<TPixel, byte> distanceCache = new Dictionary<TPixel, byte>(); |
|||
|
|||
/// <summary>
|
|||
/// Flag used to indicate whether a single pass or two passes are needed for quantization.
|
|||
/// </summary>
|
|||
private readonly bool singlePass; |
|||
|
|||
/// <summary>
|
|||
/// The vector representation of the image palette.
|
|||
/// </summary>
|
|||
private IMemoryOwner<Vector4> paletteVector; |
|||
|
|||
private bool isDisposed; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="FrameQuantizer{TPixel}"/> class.
|
|||
/// </summary>
|
|||
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
|
|||
/// <param name="quantizer">The quantizer</param>
|
|||
/// <param name="singlePass">
|
|||
/// If true, the quantization process only needs to loop through the source pixels once
|
|||
/// </param>
|
|||
/// <remarks>
|
|||
/// If you construct this class with a <value>true</value> for <paramref name="singlePass"/>, then the code will
|
|||
/// only call the <see cref="SecondPass(ImageFrame{TPixel}, Span{byte}, ReadOnlySpan{TPixel}, int, int)"/> method.
|
|||
/// If two passes are required, the code will also call <see cref="FirstPass(ImageFrame{TPixel}, int, int)"/>.
|
|||
/// </remarks>
|
|||
protected FrameQuantizer(Configuration configuration, IQuantizer quantizer, bool singlePass) |
|||
{ |
|||
Guard.NotNull(quantizer, nameof(quantizer)); |
|||
|
|||
this.Configuration = configuration; |
|||
this.Diffuser = quantizer.Diffuser; |
|||
this.Dither = this.Diffuser != null; |
|||
this.singlePass = singlePass; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="FrameQuantizer{TPixel}"/> class.
|
|||
/// </summary>
|
|||
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
|
|||
/// <param name="diffuser">The diffuser</param>
|
|||
/// <param name="singlePass">
|
|||
/// If true, the quantization process only needs to loop through the source pixels once
|
|||
/// </param>
|
|||
/// <remarks>
|
|||
/// If you construct this class with a <value>true</value> for <paramref name="singlePass"/>, then the code will
|
|||
/// only call the <see cref="SecondPass(ImageFrame{TPixel}, Span{byte}, ReadOnlySpan{TPixel}, int, int)"/> method.
|
|||
/// If two passes are required, the code will also call <see cref="FirstPass(ImageFrame{TPixel}, int, int)"/>.
|
|||
/// </remarks>
|
|||
protected FrameQuantizer(Configuration configuration, IErrorDiffuser diffuser, bool singlePass) |
|||
{ |
|||
this.Configuration = configuration; |
|||
this.Diffuser = diffuser; |
|||
this.Dither = this.Diffuser != null; |
|||
this.singlePass = singlePass; |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public IErrorDiffuser Diffuser { get; } |
|||
|
|||
/// <inheritdoc />
|
|||
public bool Dither { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the configuration which allows altering default behaviour or extending the library.
|
|||
/// </summary>
|
|||
protected Configuration Configuration { get; } |
|||
|
|||
/// <inheritdoc/>
|
|||
public void Dispose() |
|||
{ |
|||
this.Dispose(true); |
|||
GC.SuppressFinalize(this); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public IQuantizedFrame<TPixel> QuantizeFrame(ImageFrame<TPixel> image) |
|||
{ |
|||
Guard.NotNull(image, nameof(image)); |
|||
|
|||
// Get the size of the source image
|
|||
int height = image.Height; |
|||
int width = image.Width; |
|||
|
|||
// Call the FirstPass function if not a single pass algorithm.
|
|||
// For something like an Octree quantizer, this will run through
|
|||
// all image pixels, build a data structure, and create a palette.
|
|||
if (!this.singlePass) |
|||
{ |
|||
this.FirstPass(image, width, height); |
|||
} |
|||
|
|||
// Collect the palette. Required before the second pass runs.
|
|||
ReadOnlyMemory<TPixel> palette = this.GetPalette(); |
|||
MemoryAllocator memoryAllocator = this.Configuration.MemoryAllocator; |
|||
|
|||
this.paletteVector = memoryAllocator.Allocate<Vector4>(palette.Length); |
|||
PixelOperations<TPixel>.Instance.ToVector4( |
|||
this.Configuration, |
|||
palette.Span, |
|||
this.paletteVector.Memory.Span, |
|||
PixelConversionModifiers.Scale); |
|||
|
|||
var quantizedFrame = new QuantizedFrame<TPixel>(memoryAllocator, width, height, palette); |
|||
|
|||
Span<byte> pixelSpan = quantizedFrame.GetWritablePixelSpan(); |
|||
if (this.Dither) |
|||
{ |
|||
// We clone the image as we don't want to alter the original via dithering.
|
|||
using (ImageFrame<TPixel> clone = image.Clone()) |
|||
{ |
|||
this.SecondPass(clone, pixelSpan, palette.Span, width, height); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
this.SecondPass(image, pixelSpan, palette.Span, width, height); |
|||
} |
|||
|
|||
return quantizedFrame; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Disposes the object and frees resources for the Garbage Collector.
|
|||
/// </summary>
|
|||
/// <param name="disposing">Whether to dispose managed and unmanaged objects.</param>
|
|||
protected virtual void Dispose(bool disposing) |
|||
{ |
|||
if (this.isDisposed) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (disposing) |
|||
{ |
|||
this.paletteVector?.Dispose(); |
|||
} |
|||
|
|||
this.paletteVector = null; |
|||
|
|||
this.isDisposed = true; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Execute the first pass through the pixels in the image to create the palette.
|
|||
/// </summary>
|
|||
/// <param name="source">The source data.</param>
|
|||
/// <param name="width">The width in pixels of the image.</param>
|
|||
/// <param name="height">The height in pixels of the image.</param>
|
|||
protected virtual void FirstPass(ImageFrame<TPixel> source, int width, int height) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns the closest color from the palette to the given color by calculating the
|
|||
/// Euclidean distance in the Rgba colorspace.
|
|||
/// </summary>
|
|||
/// <param name="pixel">The color.</param>
|
|||
/// <returns>The <see cref="int"/></returns>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
protected byte GetClosestPixel(ref TPixel pixel) |
|||
{ |
|||
// Check if the color is in the lookup table
|
|||
if (this.distanceCache.TryGetValue(pixel, out byte value)) |
|||
{ |
|||
return value; |
|||
} |
|||
|
|||
return this.GetClosestPixelSlow(ref pixel); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Retrieve the palette for the quantized image.
|
|||
/// </summary>
|
|||
/// <returns>
|
|||
/// <see cref="ReadOnlyMemory{TPixel}"/>
|
|||
/// </returns>
|
|||
protected abstract ReadOnlyMemory<TPixel> GetPalette(); |
|||
|
|||
/// <summary>
|
|||
/// Returns the index of the first instance of the transparent color in the palette.
|
|||
/// </summary>
|
|||
/// <returns>The <see cref="int"/>.</returns>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
protected byte GetTransparentIndex() |
|||
{ |
|||
// Transparent pixels are much more likely to be found at the end of a palette.
|
|||
Span<Vector4> paletteVectorSpan = this.paletteVector.Memory.Span; |
|||
ref Vector4 paletteVectorSpanBase = ref MemoryMarshal.GetReference(paletteVectorSpan); |
|||
|
|||
int paletteVectorLengthMinus1 = paletteVectorSpan.Length - 1; |
|||
|
|||
int index = paletteVectorLengthMinus1; |
|||
for (int i = paletteVectorLengthMinus1; i >= 0; i--) |
|||
{ |
|||
ref Vector4 candidate = ref Unsafe.Add(ref paletteVectorSpanBase, i); |
|||
if (candidate.Equals(default)) |
|||
{ |
|||
index = i; |
|||
} |
|||
} |
|||
|
|||
return (byte)index; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Execute a second pass through the image to assign the pixels to a palette entry.
|
|||
/// </summary>
|
|||
/// <param name="source">The source image.</param>
|
|||
/// <param name="output">The output pixel array.</param>
|
|||
/// <param name="palette">The output color palette.</param>
|
|||
/// <param name="width">The width in pixels of the image.</param>
|
|||
/// <param name="height">The height in pixels of the image.</param>
|
|||
protected abstract void SecondPass( |
|||
ImageFrame<TPixel> source, |
|||
Span<byte> output, |
|||
ReadOnlySpan<TPixel> palette, |
|||
int width, |
|||
int height); |
|||
|
|||
[MethodImpl(MethodImplOptions.NoInlining)] |
|||
private byte GetClosestPixelSlow(ref TPixel pixel) |
|||
{ |
|||
// Loop through the palette and find the nearest match.
|
|||
int colorIndex = 0; |
|||
float leastDistance = float.MaxValue; |
|||
Vector4 vector = pixel.ToScaledVector4(); |
|||
float epsilon = Constants.EpsilonSquared; |
|||
Span<Vector4> paletteVectorSpan = this.paletteVector.Memory.Span; |
|||
ref Vector4 paletteVectorSpanBase = ref MemoryMarshal.GetReference(paletteVectorSpan); |
|||
|
|||
for (int index = 0; index < paletteVectorSpan.Length; index++) |
|||
{ |
|||
ref Vector4 candidate = ref Unsafe.Add(ref paletteVectorSpanBase, index); |
|||
float distance = Vector4.DistanceSquared(vector, candidate); |
|||
|
|||
// Greater... Move on.
|
|||
if (!(distance < leastDistance)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
colorIndex = index; |
|||
leastDistance = distance; |
|||
|
|||
// And if it's an exact match, exit the loop
|
|||
if (distance < epsilon) |
|||
{ |
|||
break; |
|||
} |
|||
} |
|||
|
|||
// Now I have the index, pop it into the cache for next time
|
|||
byte result = (byte)colorIndex; |
|||
this.distanceCache.Add(pixel, result); |
|||
return result; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Quantization |
|||
{ |
|||
/// <summary>
|
|||
/// Allows the mapping of input colors to colors within a given palette.
|
|||
/// TODO: Expose this somehow.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
internal interface IPixelMap<TPixel> |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the color palette containing colors to match.
|
|||
/// </summary>
|
|||
ReadOnlyMemory<TPixel> Palette { get; } |
|||
|
|||
/// <summary>
|
|||
/// Returns the closest color in the palette and the index of that pixel.
|
|||
/// </summary>
|
|||
/// <param name="color">The color to match.</param>
|
|||
/// <param name="match">The matched color.</param>
|
|||
/// <returns>The <see cref="int"/> index.</returns>
|
|||
int GetClosestColor(TPixel color, out TPixel match); |
|||
} |
|||
} |
|||
@ -1,38 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
|
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Quantization |
|||
{ |
|||
/// <summary>
|
|||
/// Defines an abstraction to represent a quantized image frame where the pixels indexed by a color palette.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
public interface IQuantizedFrame<TPixel> : IDisposable |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the width of this <see cref="QuantizedFrame{TPixel}"/>.
|
|||
/// </summary>
|
|||
int Width { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the height of this <see cref="QuantizedFrame{TPixel}"/>.
|
|||
/// </summary>
|
|||
int Height { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the color palette of this <see cref="QuantizedFrame{TPixel}"/>.
|
|||
/// </summary>
|
|||
ReadOnlyMemory<TPixel> Palette { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the pixels of this <see cref="QuantizedFrame{TPixel}"/>.
|
|||
/// </summary>
|
|||
/// <returns>The <see cref="Span{T}"/>The pixel span.</returns>
|
|||
ReadOnlySpan<byte> GetPixelSpan(); |
|||
} |
|||
} |
|||
@ -1,29 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Runtime.CompilerServices; |
|||
|
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Quantization |
|||
{ |
|||
/// <summary>
|
|||
/// Contains extension methods for <see cref="IQuantizedFrame{TPixel}"/>.
|
|||
/// </summary>
|
|||
public static class QuantizedFrameExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the representation of the pixels as a <see cref="Span{T}"/> of contiguous memory
|
|||
/// at row <paramref name="rowIndex"/> beginning from the the first pixel on that row.
|
|||
/// </summary>
|
|||
/// <param name="frame">The <see cref="IQuantizedFrame{TPixel}"/>.</param>
|
|||
/// <param name="rowIndex">The row.</param>
|
|||
/// <typeparam name="TPixel">The pixel type.</typeparam>
|
|||
/// <returns>The pixel row as a <see cref="ReadOnlySpan{T}"/>.</returns>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public static ReadOnlySpan<byte> GetRowSpan<TPixel>(this IQuantizedFrame<TPixel> frame, int rowIndex) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
=> frame.GetPixelSpan().Slice(rowIndex * frame.Width, frame.Width); |
|||
} |
|||
} |
|||
@ -0,0 +1,42 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using SixLabors.ImageSharp.Processing.Processors.Dithering; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Quantization |
|||
{ |
|||
/// <summary>
|
|||
/// Defines options for quantization.
|
|||
/// </summary>
|
|||
public class QuantizerOptions |
|||
{ |
|||
private float ditherScale = QuantizerConstants.MaxDitherScale; |
|||
private int maxColors = QuantizerConstants.MaxColors; |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the algorithm to apply to the output image.
|
|||
/// Defaults to <see cref="QuantizerConstants.DefaultDither"/>; set to <see langword="null"/> for no dithering.
|
|||
/// </summary>
|
|||
public IDither Dither { get; set; } = QuantizerConstants.DefaultDither; |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the dithering scale used to adjust the amount of dither. Range 0..1.
|
|||
/// Defaults to <see cref="QuantizerConstants.MaxDitherScale"/>.
|
|||
/// </summary>
|
|||
public float DitherScale |
|||
{ |
|||
get { return this.ditherScale; } |
|||
set { this.ditherScale = value.Clamp(QuantizerConstants.MinDitherScale, QuantizerConstants.MaxDitherScale); } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the maximum number of colors to hold in the color palette. Range 0..256.
|
|||
/// Defaults to <see cref="QuantizerConstants.MaxColors"/>.
|
|||
/// </summary>
|
|||
public int MaxColors |
|||
{ |
|||
get { return this.maxColors; } |
|||
set { this.maxColors = value.Clamp(QuantizerConstants.MinColors, QuantizerConstants.MaxColors); } |
|||
} |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
@ -1,106 +0,0 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using SixLabors.ImageSharp.Processing; |
|||
using SixLabors.ImageSharp.Processing.Processors.Binarization; |
|||
using SixLabors.ImageSharp.Processing.Processors.Dithering; |
|||
|
|||
using Xunit; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Processing.Binarization |
|||
{ |
|||
public class BinaryDitherTest : BaseImageOperationsExtensionTest |
|||
{ |
|||
private readonly IOrderedDither orderedDither; |
|||
private readonly IErrorDiffuser errorDiffuser; |
|||
|
|||
public BinaryDitherTest() |
|||
{ |
|||
this.orderedDither = KnownDitherers.BayerDither4x4; |
|||
this.errorDiffuser = KnownDiffusers.FloydSteinberg; |
|||
} |
|||
|
|||
[Fact] |
|||
public void BinaryDither_CorrectProcessor() |
|||
{ |
|||
this.operations.BinaryDither(this.orderedDither); |
|||
BinaryOrderedDitherProcessor p = this.Verify<BinaryOrderedDitherProcessor>(); |
|||
Assert.Equal(this.orderedDither, p.Dither); |
|||
Assert.Equal(Color.White, p.UpperColor); |
|||
Assert.Equal(Color.Black, p.LowerColor); |
|||
} |
|||
|
|||
[Fact] |
|||
public void BinaryDither_rect_CorrectProcessor() |
|||
{ |
|||
this.operations.BinaryDither(this.orderedDither, this.rect); |
|||
BinaryOrderedDitherProcessor p = this.Verify<BinaryOrderedDitherProcessor>(this.rect); |
|||
Assert.Equal(this.orderedDither, p.Dither); |
|||
Assert.Equal(Color.White, p.UpperColor); |
|||
Assert.Equal(Color.Black, p.LowerColor); |
|||
} |
|||
|
|||
[Fact] |
|||
public void BinaryDither_index_CorrectProcessor() |
|||
{ |
|||
this.operations.BinaryDither(this.orderedDither, Color.Yellow, Color.HotPink); |
|||
BinaryOrderedDitherProcessor p = this.Verify<BinaryOrderedDitherProcessor>(); |
|||
Assert.Equal(this.orderedDither, p.Dither); |
|||
Assert.Equal(Color.Yellow, p.UpperColor); |
|||
Assert.Equal(Color.HotPink, p.LowerColor); |
|||
} |
|||
|
|||
[Fact] |
|||
public void BinaryDither_index_rect_CorrectProcessor() |
|||
{ |
|||
this.operations.BinaryDither(this.orderedDither, Color.Yellow, Color.HotPink, this.rect); |
|||
BinaryOrderedDitherProcessor p = this.Verify<BinaryOrderedDitherProcessor>(this.rect); |
|||
Assert.Equal(this.orderedDither, p.Dither); |
|||
Assert.Equal(Color.HotPink, p.LowerColor); |
|||
} |
|||
|
|||
[Fact] |
|||
public void BinaryDither_ErrorDiffuser_CorrectProcessor() |
|||
{ |
|||
this.operations.BinaryDiffuse(this.errorDiffuser, .4F); |
|||
BinaryErrorDiffusionProcessor p = this.Verify<BinaryErrorDiffusionProcessor>(); |
|||
Assert.Equal(this.errorDiffuser, p.Diffuser); |
|||
Assert.Equal(.4F, p.Threshold); |
|||
Assert.Equal(Color.White, p.UpperColor); |
|||
Assert.Equal(Color.Black, p.LowerColor); |
|||
} |
|||
|
|||
[Fact] |
|||
public void BinaryDither_ErrorDiffuser_rect_CorrectProcessor() |
|||
{ |
|||
this.operations.BinaryDiffuse(this.errorDiffuser, .3F, this.rect); |
|||
BinaryErrorDiffusionProcessor p = this.Verify<BinaryErrorDiffusionProcessor>(this.rect); |
|||
Assert.Equal(this.errorDiffuser, p.Diffuser); |
|||
Assert.Equal(.3F, p.Threshold); |
|||
Assert.Equal(Color.White, p.UpperColor); |
|||
Assert.Equal(Color.Black, p.LowerColor); |
|||
} |
|||
|
|||
[Fact] |
|||
public void BinaryDither_ErrorDiffuser_CorrectProcessorWithColors() |
|||
{ |
|||
this.operations.BinaryDiffuse(this.errorDiffuser, .5F, Color.HotPink, Color.Yellow); |
|||
BinaryErrorDiffusionProcessor p = this.Verify<BinaryErrorDiffusionProcessor>(); |
|||
Assert.Equal(this.errorDiffuser, p.Diffuser); |
|||
Assert.Equal(.5F, p.Threshold); |
|||
Assert.Equal(Color.HotPink, p.UpperColor); |
|||
Assert.Equal(Color.Yellow, p.LowerColor); |
|||
} |
|||
|
|||
[Fact] |
|||
public void BinaryDither_ErrorDiffuser_rect_CorrectProcessorWithColors() |
|||
{ |
|||
this.operations.BinaryDiffuse(this.errorDiffuser, .5F, Color.HotPink, Color.Yellow, this.rect); |
|||
BinaryErrorDiffusionProcessor p = this.Verify<BinaryErrorDiffusionProcessor>(this.rect); |
|||
Assert.Equal(this.errorDiffuser, p.Diffuser); |
|||
Assert.Equal(.5F, p.Threshold); |
|||
Assert.Equal(Color.HotPink, p.UpperColor); |
|||
Assert.Equal(Color.Yellow, p.LowerColor); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,220 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
|
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.ImageSharp.Processing; |
|||
using SixLabors.ImageSharp.Processing.Processors.Quantization; |
|||
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; |
|||
using Xunit; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization |
|||
{ |
|||
public class QuantizerTests |
|||
{ |
|||
/// <summary>
|
|||
/// Something is causing tests to fail on NETFX in CI.
|
|||
/// Could be a JIT error as everything runs well and is identical to .NET Core output.
|
|||
/// Not worth investigating for now.
|
|||
/// <see href="https://github.com/SixLabors/ImageSharp/pull/1114/checks?check_run_id=448891164#step:11:631"/>
|
|||
/// </summary>
|
|||
private static readonly bool SkipAllQuantizerTests = TestEnvironment.RunsOnCI && TestEnvironment.IsFramework; |
|||
|
|||
public static readonly string[] CommonTestImages = |
|||
{ |
|||
TestImages.Png.CalliphoraPartial, |
|||
TestImages.Png.Bike |
|||
}; |
|||
|
|||
private static readonly QuantizerOptions NoDitherOptions = new QuantizerOptions { Dither = null }; |
|||
private static readonly QuantizerOptions DiffuserDitherOptions = new QuantizerOptions { Dither = KnownDitherings.FloydSteinberg }; |
|||
private static readonly QuantizerOptions OrderedDitherOptions = new QuantizerOptions { Dither = KnownDitherings.Bayer8x8 }; |
|||
|
|||
private static readonly QuantizerOptions Diffuser0_ScaleDitherOptions = new QuantizerOptions |
|||
{ |
|||
Dither = KnownDitherings.FloydSteinberg, |
|||
DitherScale = 0F |
|||
}; |
|||
|
|||
private static readonly QuantizerOptions Diffuser0_25_ScaleDitherOptions = new QuantizerOptions |
|||
{ |
|||
Dither = KnownDitherings.FloydSteinberg, |
|||
DitherScale = .25F |
|||
}; |
|||
|
|||
private static readonly QuantizerOptions Diffuser0_5_ScaleDitherOptions = new QuantizerOptions |
|||
{ |
|||
Dither = KnownDitherings.FloydSteinberg, |
|||
DitherScale = .5F |
|||
}; |
|||
|
|||
private static readonly QuantizerOptions Diffuser0_75_ScaleDitherOptions = new QuantizerOptions |
|||
{ |
|||
Dither = KnownDitherings.FloydSteinberg, |
|||
DitherScale = .75F |
|||
}; |
|||
|
|||
private static readonly QuantizerOptions Ordered0_ScaleDitherOptions = new QuantizerOptions |
|||
{ |
|||
Dither = KnownDitherings.Bayer8x8, |
|||
DitherScale = 0F |
|||
}; |
|||
|
|||
private static readonly QuantizerOptions Ordered0_25_ScaleDitherOptions = new QuantizerOptions |
|||
{ |
|||
Dither = KnownDitherings.Bayer8x8, |
|||
DitherScale = .25F |
|||
}; |
|||
|
|||
private static readonly QuantizerOptions Ordered0_5_ScaleDitherOptions = new QuantizerOptions |
|||
{ |
|||
Dither = KnownDitherings.Bayer8x8, |
|||
DitherScale = .5F |
|||
}; |
|||
|
|||
private static readonly QuantizerOptions Ordered0_75_ScaleDitherOptions = new QuantizerOptions |
|||
{ |
|||
Dither = KnownDitherings.Bayer8x8, |
|||
DitherScale = .75F |
|||
}; |
|||
|
|||
public static readonly TheoryData<IQuantizer> Quantizers |
|||
= new TheoryData<IQuantizer> |
|||
{ |
|||
// Known uses error diffusion by default.
|
|||
KnownQuantizers.Octree, |
|||
KnownQuantizers.WebSafe, |
|||
KnownQuantizers.Werner, |
|||
KnownQuantizers.Wu, |
|||
new OctreeQuantizer(NoDitherOptions), |
|||
new WebSafePaletteQuantizer(NoDitherOptions), |
|||
new WernerPaletteQuantizer(NoDitherOptions), |
|||
new WuQuantizer(NoDitherOptions), |
|||
new OctreeQuantizer(OrderedDitherOptions), |
|||
new WebSafePaletteQuantizer(OrderedDitherOptions), |
|||
new WernerPaletteQuantizer(OrderedDitherOptions), |
|||
new WuQuantizer(OrderedDitherOptions) |
|||
}; |
|||
|
|||
public static readonly TheoryData<IQuantizer> DitherScaleQuantizers |
|||
= new TheoryData<IQuantizer> |
|||
{ |
|||
new OctreeQuantizer(Diffuser0_ScaleDitherOptions), |
|||
new WebSafePaletteQuantizer(Diffuser0_ScaleDitherOptions), |
|||
new WernerPaletteQuantizer(Diffuser0_ScaleDitherOptions), |
|||
new WuQuantizer(Diffuser0_ScaleDitherOptions), |
|||
|
|||
new OctreeQuantizer(Diffuser0_25_ScaleDitherOptions), |
|||
new WebSafePaletteQuantizer(Diffuser0_25_ScaleDitherOptions), |
|||
new WernerPaletteQuantizer(Diffuser0_25_ScaleDitherOptions), |
|||
new WuQuantizer(Diffuser0_25_ScaleDitherOptions), |
|||
|
|||
new OctreeQuantizer(Diffuser0_5_ScaleDitherOptions), |
|||
new WebSafePaletteQuantizer(Diffuser0_5_ScaleDitherOptions), |
|||
new WernerPaletteQuantizer(Diffuser0_5_ScaleDitherOptions), |
|||
new WuQuantizer(Diffuser0_5_ScaleDitherOptions), |
|||
|
|||
new OctreeQuantizer(Diffuser0_75_ScaleDitherOptions), |
|||
new WebSafePaletteQuantizer(Diffuser0_75_ScaleDitherOptions), |
|||
new WernerPaletteQuantizer(Diffuser0_75_ScaleDitherOptions), |
|||
new WuQuantizer(Diffuser0_75_ScaleDitherOptions), |
|||
|
|||
new OctreeQuantizer(DiffuserDitherOptions), |
|||
new WebSafePaletteQuantizer(DiffuserDitherOptions), |
|||
new WernerPaletteQuantizer(DiffuserDitherOptions), |
|||
new WuQuantizer(DiffuserDitherOptions), |
|||
|
|||
new OctreeQuantizer(Ordered0_ScaleDitherOptions), |
|||
new WebSafePaletteQuantizer(Ordered0_ScaleDitherOptions), |
|||
new WernerPaletteQuantizer(Ordered0_ScaleDitherOptions), |
|||
new WuQuantizer(Ordered0_ScaleDitherOptions), |
|||
|
|||
new OctreeQuantizer(Ordered0_25_ScaleDitherOptions), |
|||
new WebSafePaletteQuantizer(Ordered0_25_ScaleDitherOptions), |
|||
new WernerPaletteQuantizer(Ordered0_25_ScaleDitherOptions), |
|||
new WuQuantizer(Ordered0_25_ScaleDitherOptions), |
|||
|
|||
new OctreeQuantizer(Ordered0_5_ScaleDitherOptions), |
|||
new WebSafePaletteQuantizer(Ordered0_5_ScaleDitherOptions), |
|||
new WernerPaletteQuantizer(Ordered0_5_ScaleDitherOptions), |
|||
new WuQuantizer(Ordered0_5_ScaleDitherOptions), |
|||
|
|||
new OctreeQuantizer(Ordered0_75_ScaleDitherOptions), |
|||
new WebSafePaletteQuantizer(Ordered0_75_ScaleDitherOptions), |
|||
new WernerPaletteQuantizer(Ordered0_75_ScaleDitherOptions), |
|||
new WuQuantizer(Ordered0_75_ScaleDitherOptions), |
|||
|
|||
new OctreeQuantizer(OrderedDitherOptions), |
|||
new WebSafePaletteQuantizer(OrderedDitherOptions), |
|||
new WernerPaletteQuantizer(OrderedDitherOptions), |
|||
new WuQuantizer(OrderedDitherOptions), |
|||
}; |
|||
|
|||
private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.05F); |
|||
|
|||
[Theory] |
|||
[WithFileCollection(nameof(CommonTestImages), nameof(Quantizers), PixelTypes.Rgba32)] |
|||
public void ApplyQuantizationInBox<TPixel>(TestImageProvider<TPixel> provider, IQuantizer quantizer) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
if (SkipAllQuantizerTests) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
string quantizerName = quantizer.GetType().Name; |
|||
string ditherName = quantizer.Options.Dither?.GetType()?.Name ?? "NoDither"; |
|||
string testOutputDetails = $"{quantizerName}_{ditherName}"; |
|||
|
|||
provider.RunRectangleConstrainedValidatingProcessorTest( |
|||
(x, rect) => x.Quantize(quantizer, rect), |
|||
comparer: ValidatorComparer, |
|||
testOutputDetails: testOutputDetails, |
|||
appendPixelTypeToFileName: false); |
|||
} |
|||
|
|||
[Theory] |
|||
[WithFileCollection(nameof(CommonTestImages), nameof(Quantizers), PixelTypes.Rgba32)] |
|||
public void ApplyQuantization<TPixel>(TestImageProvider<TPixel> provider, IQuantizer quantizer) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
if (SkipAllQuantizerTests) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
string quantizerName = quantizer.GetType().Name; |
|||
string ditherName = quantizer.Options.Dither?.GetType()?.Name ?? "NoDither"; |
|||
string testOutputDetails = $"{quantizerName}_{ditherName}"; |
|||
|
|||
provider.RunValidatingProcessorTest( |
|||
x => x.Quantize(quantizer), |
|||
comparer: ValidatorComparer, |
|||
testOutputDetails: testOutputDetails, |
|||
appendPixelTypeToFileName: false); |
|||
} |
|||
|
|||
[Theory] |
|||
[WithFile(TestImages.Png.David, nameof(DitherScaleQuantizers), PixelTypes.Rgba32)] |
|||
public void ApplyQuantizationWithDitheringScale<TPixel>(TestImageProvider<TPixel> provider, IQuantizer quantizer) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
if (SkipAllQuantizerTests) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
string quantizerName = quantizer.GetType().Name; |
|||
string ditherName = quantizer.Options.Dither.GetType().Name; |
|||
float ditherScale = quantizer.Options.DitherScale; |
|||
string testOutputDetails = FormattableString.Invariant($"{quantizerName}_{ditherName}_{ditherScale}"); |
|||
|
|||
provider.RunValidatingProcessorTest( |
|||
x => x.Quantize(quantizer), |
|||
comparer: ValidatorComparer, |
|||
testOutputDetails: testOutputDetails, |
|||
appendPixelTypeToFileName: false); |
|||
} |
|||
} |
|||
} |
|||
@ -1 +1 @@ |
|||
Subproject commit fbba5e2a78aa479c0752dc0fd91ec25b4948704a |
|||
Subproject commit 2d1505d7087d91cd83d0cda409aee213de7841ab |
|||
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 27 KiB |
Loading…
Reference in new issue