Browse Source

refactor dithering and error diffusion to use Color

pull/908/head
Anton Firszov 7 years ago
parent
commit
16b24d4a08
  1. 67
      src/ImageSharp/Color/Color.cs
  2. 69
      src/ImageSharp/Processing/DiffuseExtensions.cs
  3. 55
      src/ImageSharp/Processing/DitherExtensions.cs
  4. 77
      src/ImageSharp/Processing/Processors/Dithering/ErrorDiffusionPaletteProcessor.cs
  5. 81
      src/ImageSharp/Processing/Processors/Dithering/ErrorDiffusionPaletteProcessor{TPixel}.cs
  6. 70
      src/ImageSharp/Processing/Processors/Dithering/OrderedDitherPaletteProcessor.cs
  7. 79
      src/ImageSharp/Processing/Processors/Dithering/OrderedDitherPaletteProcessor{TPixel}.cs
  8. 37
      src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor.cs
  9. 35
      src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs
  10. 42
      tests/ImageSharp.Tests/Processing/Dithering/DitherTest.cs

67
src/ImageSharp/Color/Color.cs

@ -5,6 +5,7 @@ using System;
using System.Buffers.Binary;
using System.Globalization;
using System.Numerics;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.PixelFormats;
@ -117,6 +118,44 @@ namespace SixLabors.ImageSharp
/// <inheritdoc />
public override string ToString() => this.ToHex();
public TPixel ToPixel<TPixel>()
where TPixel : struct, IPixel<TPixel>
{
TPixel pixel = default;
pixel.FromRgba64(this.data);
return pixel;
}
public bool Equals(Color other)
{
return this.data.PackedValue == other.data.PackedValue;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj))
{
return false;
}
return obj is Color other && this.Equals(other);
}
/// <inheritdoc />
public override int GetHashCode()
{
return this.data.PackedValue.GetHashCode();
}
internal static void ToPixel<TPixel>(
Configuration configuration,
ReadOnlySpan<Color> source,
Span<TPixel> destination)
where TPixel : struct, IPixel<TPixel>
{
ReadOnlySpan<Rgba64> rgba64Span = MemoryMarshal.Cast<Color, Rgba64>(source);
PixelOperations<TPixel>.Instance.FromRgba64(Configuration.Default, rgba64Span, destination);
}
/// <summary>
/// Converts the specified hex value to an rrggbbaa hex value.
/// </summary>
@ -153,33 +192,5 @@ namespace SixLabors.ImageSharp
return new string(new[] { r, r, g, g, b, b, a, a });
}
public TPixel ToPixel<TPixel>()
where TPixel : struct, IPixel<TPixel>
{
TPixel pixel = default;
pixel.FromRgba64(this.data);
return pixel;
}
public bool Equals(Color other)
{
return this.data.PackedValue == other.data.PackedValue;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj))
{
return false;
}
return obj is Color other && this.Equals(other);
}
/// <inheritdoc />
public override int GetHashCode()
{
return this.data.PackedValue.GetHashCode();
}
}
}

69
src/ImageSharp/Processing/DiffuseExtensions.cs

@ -1,14 +1,15 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.PixelFormats;
using System;
using SixLabors.ImageSharp.Processing.Processors.Dithering;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Dithering
{
/// <summary>
/// Defines extension methods to apply diffusion to an <see cref="Image{TPixel}"/>
/// Defines extension methods to apply diffusion to an <see cref="Image"/>
/// using Mutate/Clone.
/// </summary>
public static class DiffuseExtensions
@ -16,68 +17,68 @@ namespace SixLabors.ImageSharp.Processing.Dithering
/// <summary>
/// Dithers the image reducing it to a web-safe palette using error diffusion.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image this method extends.</param>
/// <returns>The <see cref="IImageProcessingContext{TPixel}"/> to allow chaining of operations.</returns>
public static IImageProcessingContext<TPixel> Diffuse<TPixel>(this IImageProcessingContext<TPixel> source)
where TPixel : struct, IPixel<TPixel>
=> Diffuse(source, KnownDiffusers.FloydSteinberg, .5F);
/// <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>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <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{TPixel}"/> to allow chaining of operations.</returns>
public static IImageProcessingContext<TPixel> Diffuse<TPixel>(this IImageProcessingContext<TPixel> source, float threshold)
where TPixel : struct, IPixel<TPixel>
=> Diffuse(source, KnownDiffusers.FloydSteinberg, threshold);
/// <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>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <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{TPixel}"/> to allow chaining of operations.</returns>
public static IImageProcessingContext<TPixel> Diffuse<TPixel>(this IImageProcessingContext<TPixel> source, IErrorDiffuser diffuser, float threshold)
where TPixel : struct, IPixel<TPixel>
=> source.ApplyProcessor(new ErrorDiffusionPaletteProcessor<TPixel>(diffuser, threshold));
/// <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>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <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{TPixel}"/> to allow chaining of operations.</returns>
public static IImageProcessingContext<TPixel> Diffuse<TPixel>(this IImageProcessingContext<TPixel> source, IErrorDiffuser diffuser, float threshold, Rectangle rectangle)
where TPixel : struct, IPixel<TPixel>
=> source.ApplyProcessor(new ErrorDiffusionPaletteProcessor<TPixel>(diffuser, threshold), rectangle);
/// <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>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <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{TPixel}"/> to allow chaining of operations.</returns>
public static IImageProcessingContext<TPixel> Diffuse<TPixel>(this IImageProcessingContext<TPixel> source, IErrorDiffuser diffuser, float threshold, TPixel[] palette)
where TPixel : struct, IPixel<TPixel>
=> source.ApplyProcessor(new ErrorDiffusionPaletteProcessor<TPixel>(diffuser, threshold, palette));
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
public static IImageProcessingContext Diffuse(
this IImageProcessingContext source,
IErrorDiffuser diffuser,
float threshold,
ReadOnlySpan<Color> palette) =>
source.ApplyProcessor(new ErrorDiffusionPaletteProcessor(diffuser, threshold, palette));
/// <summary>
/// Dithers the image reducing it to the given palette using error diffusion.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <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>
@ -85,9 +86,13 @@ namespace SixLabors.ImageSharp.Processing.Dithering
/// <param name="rectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param>
/// <returns>The <see cref="IImageProcessingContext{TPixel}"/> to allow chaining of operations.</returns>
public static IImageProcessingContext<TPixel> Diffuse<TPixel>(this IImageProcessingContext<TPixel> source, IErrorDiffuser diffuser, float threshold, TPixel[] palette, Rectangle rectangle)
where TPixel : struct, IPixel<TPixel>
=> source.ApplyProcessor(new ErrorDiffusionPaletteProcessor<TPixel>(diffuser, threshold, palette), rectangle);
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
public static IImageProcessingContext Diffuse(
this IImageProcessingContext source,
IErrorDiffuser diffuser,
float threshold,
ReadOnlySpan<Color> palette,
Rectangle rectangle) =>
source.ApplyProcessor(new ErrorDiffusionPaletteProcessor(diffuser, threshold, palette), rectangle);
}
}

55
src/ImageSharp/Processing/DitherExtensions.cs

@ -1,14 +1,15 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.PixelFormats;
using System;
using SixLabors.ImageSharp.Processing.Processors.Dithering;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing
{
/// <summary>
/// Defines dithering extensions to apply on an <see cref="Image{TPixel}"/>
/// Defines dithering extensions to apply on an <see cref="Image"/>
/// using Mutate/Clone.
/// </summary>
public static class DitherExtensions
@ -16,63 +17,63 @@ namespace SixLabors.ImageSharp.Processing
/// <summary>
/// Dithers the image reducing it to a web-safe palette using Bayer4x4 ordered dithering.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image this method extends.</param>
/// <returns>The <see cref="IImageProcessingContext{TPixel}"/> to allow chaining of operations.</returns>
public static IImageProcessingContext<TPixel> Dither<TPixel>(this IImageProcessingContext<TPixel> source)
where TPixel : struct, IPixel<TPixel>
=> Dither(source, KnownDitherers.BayerDither4x4);
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
public static IImageProcessingContext Dither(this IImageProcessingContext source) =>
Dither(source, KnownDitherers.BayerDither4x4);
/// <summary>
/// Dithers the image reducing it to a web-safe palette using ordered dithering.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image this method extends.</param>
/// <param name="dither">The ordered ditherer.</param>
/// <returns>The <see cref="IImageProcessingContext{TPixel}"/> to allow chaining of operations.</returns>
public static IImageProcessingContext<TPixel> Dither<TPixel>(this IImageProcessingContext<TPixel> source, IOrderedDither dither)
where TPixel : struct, IPixel<TPixel>
=> source.ApplyProcessor(new OrderedDitherPaletteProcessor<TPixel>(dither));
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
public static IImageProcessingContext Dither(this IImageProcessingContext source, IOrderedDither dither) =>
source.ApplyProcessor(new OrderedDitherPaletteProcessor(dither));
/// <summary>
/// Dithers the image reducing it to the given palette using ordered dithering.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image this method extends.</param>
/// <param name="dither">The ordered ditherer.</param>
/// <param name="palette">The palette to select substitute colors from.</param>
/// <returns>The <see cref="IImageProcessingContext{TPixel}"/> to allow chaining of operations.</returns>
public static IImageProcessingContext<TPixel> Dither<TPixel>(this IImageProcessingContext<TPixel> source, IOrderedDither dither, TPixel[] palette)
where TPixel : struct, IPixel<TPixel>
=> source.ApplyProcessor(new OrderedDitherPaletteProcessor<TPixel>(dither, palette));
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
public static IImageProcessingContext Dither(
this IImageProcessingContext source,
IOrderedDither dither,
ReadOnlySpan<Color> palette) =>
source.ApplyProcessor(new OrderedDitherPaletteProcessor(dither, palette));
/// <summary>
/// Dithers the image reducing it to a web-safe palette using ordered dithering.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image this method extends.</param>
/// <param name="dither">The ordered ditherer.</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{TPixel}"/> to allow chaining of operations.</returns>
public static IImageProcessingContext<TPixel> Dither<TPixel>(this IImageProcessingContext<TPixel> source, IOrderedDither dither, Rectangle rectangle)
where TPixel : struct, IPixel<TPixel>
=> source.ApplyProcessor(new OrderedDitherPaletteProcessor<TPixel>(dither), rectangle);
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
public static IImageProcessingContext Dither(
this IImageProcessingContext source,
IOrderedDither dither,
Rectangle rectangle) =>
source.ApplyProcessor(new OrderedDitherPaletteProcessor(dither), rectangle);
/// <summary>
/// Dithers the image reducing it to the given palette using ordered dithering.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image this method extends.</param>
/// <param name="dither">The ordered ditherer.</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{TPixel}"/> to allow chaining of operations.</returns>
public static IImageProcessingContext<TPixel> Dither<TPixel>(this IImageProcessingContext<TPixel> source, IOrderedDither dither, TPixel[] palette, Rectangle rectangle)
where TPixel : struct, IPixel<TPixel>
=> source.ApplyProcessor(new OrderedDitherPaletteProcessor<TPixel>(dither, palette), rectangle);
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
public static IImageProcessingContext Dither(
this IImageProcessingContext source,
IOrderedDither dither,
ReadOnlySpan<Color> palette,
Rectangle rectangle) =>
source.ApplyProcessor(new OrderedDitherPaletteProcessor(dither, palette), rectangle);
}
}

77
src/ImageSharp/Processing/Processors/Dithering/ErrorDiffusionPaletteProcessor.cs

@ -2,21 +2,19 @@
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Primitives;
using SixLabors.ImageSharp.Processing.Processors.Binarization;
namespace SixLabors.ImageSharp.Processing.Processors.Dithering
{
/// <summary>
/// An <see cref="IImageProcessor{TPixel}"/> that dithers an image using error diffusion.
/// 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>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal class ErrorDiffusionPaletteProcessor<TPixel> : PaletteDitherProcessorBase<TPixel>
where TPixel : struct, IPixel<TPixel>
public sealed class ErrorDiffusionPaletteProcessor : PaletteDitherProcessor
{
/// <summary>
/// Initializes a new instance of the <see cref="ErrorDiffusionPaletteProcessor{TPixel}"/> class.
/// Initializes a new instance of the <see cref="ErrorDiffusionPaletteProcessor"/> class.
/// </summary>
/// <param name="diffuser">The error diffuser</param>
public ErrorDiffusionPaletteProcessor(IErrorDiffuser diffuser)
@ -25,22 +23,22 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
}
/// <summary>
/// Initializes a new instance of the <see cref="ErrorDiffusionPaletteProcessor{TPixel}"/> class.
/// 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, NamedColors<TPixel>.WebSafePalette)
: this(diffuser, threshold, Color.WebSafePalette)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ErrorDiffusionPaletteProcessor{TPixel}"/> class.
/// 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, TPixel[] palette)
public ErrorDiffusionPaletteProcessor(IErrorDiffuser diffuser, float threshold, ReadOnlySpan<Color> palette)
: base(palette)
{
Guard.NotNull(diffuser, nameof(diffuser));
@ -60,59 +58,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// </summary>
public float Threshold { get; }
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration)
/// <inheritdoc />
public override IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>()
{
byte threshold = (byte)MathF.Round(this.Threshold * 255F);
bool isAlphaOnly = typeof(TPixel) == typeof(Alpha8);
var interest = Rectangle.Intersect(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 = 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))
{
pair = this.GetClosestPixelPair(ref sourcePixel);
// No error to spread, exact match.
if (sourcePixel.Equals(pair.First))
{
continue;
}
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 ? pair.Second : pair.First;
this.Diffuser.Dither(source, sourcePixel, transformedPixel, x, y, startX, startY, endX, endY);
}
}
return new ErrorDiffusionPaletteProcessor<TPixel>(this);
}
}
}

81
src/ImageSharp/Processing/Processors/Dithering/ErrorDiffusionPaletteProcessor{TPixel}.cs

@ -0,0 +1,81 @@
// 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.Primitives;
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 ErrorDiffusionPaletteProcessor<TPixel> : PaletteDitherProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
public ErrorDiffusionPaletteProcessor(ErrorDiffusionPaletteProcessor definition)
: base(definition)
{
}
private new ErrorDiffusionPaletteProcessor Definition => (ErrorDiffusionPaletteProcessor)base.Definition;
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration)
{
byte threshold = (byte)MathF.Round(this.Definition.Threshold * 255F);
bool isAlphaOnly = typeof(TPixel) == typeof(Alpha8);
var interest = Rectangle.Intersect(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 = 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))
{
pair = this.GetClosestPixelPair(ref sourcePixel);
// No error to spread, exact match.
if (sourcePixel.Equals(pair.First))
{
continue;
}
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 ? pair.Second : pair.First;
this.Definition.Diffuser.Dither(source, sourcePixel, transformedPixel, x, y, startX, startY, endX, endY);
}
}
}
}
}

70
src/ImageSharp/Processing/Processors/Dithering/OrderedDitherPaletteProcessor.cs

@ -2,35 +2,30 @@
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Dithering
{
/// <summary>
/// An <see cref="IImageProcessor{TPixel}"/> that dithers an image using error diffusion.
/// 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>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal class OrderedDitherPaletteProcessor<TPixel> : PaletteDitherProcessorBase<TPixel>
where TPixel : struct, IPixel<TPixel>
public sealed class OrderedDitherPaletteProcessor : PaletteDitherProcessor
{
/// <summary>
/// Initializes a new instance of the <see cref="OrderedDitherPaletteProcessor{TPixel}"/> class.
/// Initializes a new instance of the <see cref="OrderedDitherPaletteProcessor"/> class.
/// </summary>
/// <param name="dither">The ordered ditherer.</param>
public OrderedDitherPaletteProcessor(IOrderedDither dither)
: this(dither, NamedColors<TPixel>.WebSafePalette)
: this(dither, Color.WebSafePalette)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="OrderedDitherPaletteProcessor{TPixel}"/> class.
/// 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, TPixel[] palette)
public OrderedDitherPaletteProcessor(IOrderedDither dither, ReadOnlySpan<Color> palette)
: base(palette) => this.Dither = dither ?? throw new ArgumentNullException(nameof(dither));
/// <summary>
@ -38,57 +33,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// </summary>
public IOrderedDither Dither { get; }
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration)
/// <inheritdoc />
public override IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>()
{
bool isAlphaOnly = typeof(TPixel) == typeof(Alpha8);
var interest = Rectangle.Intersect(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 = 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))
{
pair = this.GetClosestPixelPair(ref sourcePixel);
// No error to spread, exact match.
if (sourcePixel.Equals(pair.First))
{
continue;
}
sourcePixel.ToRgba32(ref rgba);
luminance = isAlphaOnly ? rgba.A : ImageMaths.Get8BitBT709Luminance(rgba.R, rgba.G, rgba.B);
// Setup the previous pointer
previousPixel = sourcePixel;
}
this.Dither.Dither(source, sourcePixel, pair.Second, pair.First, luminance, x, y);
}
}
return new OrderedDitherPaletteProcessor<TPixel>(this);
}
}
}

79
src/ImageSharp/Processing/Processors/Dithering/OrderedDitherPaletteProcessor{TPixel}.cs

@ -0,0 +1,79 @@
// 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.Primitives;
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>
{
public OrderedDitherPaletteProcessor(OrderedDitherPaletteProcessor definition)
: base(definition)
{
}
private new OrderedDitherPaletteProcessor Definition => (OrderedDitherPaletteProcessor)base.Definition;
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration)
{
bool isAlphaOnly = typeof(TPixel) == typeof(Alpha8);
var interest = Rectangle.Intersect(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 = 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))
{
pair = this.GetClosestPixelPair(ref sourcePixel);
// No error to spread, exact match.
if (sourcePixel.Equals(pair.First))
{
continue;
}
sourcePixel.ToRgba32(ref rgba);
luminance = isAlphaOnly ? rgba.A : 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);
}
}
}
}
}

37
src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor.cs

@ -0,0 +1,37 @@
// 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>
/// The base class for dither and diffusion processors that consume a palette.
/// </summary>
public abstract class PaletteDitherProcessor : IImageProcessor
{
private readonly Color[] palette;
/// <summary>
/// Initializes a new instance of the <see cref="PaletteDitherProcessor"/> class.
/// </summary>
/// <param name="palette">The palette to select substitute colors from.</param>
protected PaletteDitherProcessor(ReadOnlySpan<Color> palette)
{
// This shouldn't be a perf issue:
// these arrays are small, and created with low frequency.
this.palette = palette.ToArray();
}
/// <summary>
/// Gets the palette to select substitute colors from.
/// </summary>
public ReadOnlySpan<Color> Palette => this.palette;
/// <inheritdoc />
public abstract IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>()
where TPixel : struct, IPixel<TPixel>;
}
}

35
src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessorBase.cs → src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs

@ -1,10 +1,11 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Primitives;
@ -14,7 +15,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// The base class for dither and diffusion processors that consume a palette.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal abstract class PaletteDitherProcessorBase<TPixel> : ImageProcessor<TPixel>
internal abstract class PaletteDitherProcessor<TPixel> : ImageProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
private readonly Dictionary<TPixel, PixelPair<TPixel>> cache = new Dictionary<TPixel, PixelPair<TPixel>>();
@ -24,19 +25,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// </summary>
private Vector4[] paletteVector;
private TPixel[] palette;
/// <summary>
/// Initializes a new instance of the <see cref="PaletteDitherProcessorBase{TPixel}"/> class.
/// Initializes a new instance of the <see cref="PaletteDitherProcessor{TPixel}"/> class.
/// </summary>
/// <param name="palette">The palette to select substitute colors from.</param>
protected PaletteDitherProcessorBase(TPixel[] palette)
protected PaletteDitherProcessor(PaletteDitherProcessor definition)
{
this.Palette = palette ?? throw new ArgumentNullException(nameof(palette));
this.Definition = definition;
}
/// <summary>
/// Gets the palette to select substitute colors from.
/// </summary>
public TPixel[] Palette { get; }
protected PaletteDitherProcessor Definition { get; }
/// <summary>
/// Returns the two closest colors from the palette calculated via Euclidean distance in the Rgba space.
@ -74,12 +73,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
{
leastDistance = distance;
secondClosest = closest;
closest = this.Palette[index];
closest = this.palette[index];
}
else if (distance < secondLeastDistance)
{
secondLeastDistance = distance;
secondClosest = this.Palette[index];
secondClosest = this.palette[index];
}
}
@ -94,11 +93,19 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
{
base.BeforeFrameApply(source, sourceRectangle, configuration);
// Lazy init palette:
if (this.palette is null)
{
ReadOnlySpan<Color> sourcePalette = this.Definition.Palette;
this.palette = new TPixel[sourcePalette.Length];
Color.ToPixel<TPixel>(configuration, sourcePalette, this.palette);
}
// Lazy init paletteVector:
if (this.paletteVector is null)
{
this.paletteVector = new Vector4[this.Palette.Length];
PixelOperations<TPixel>.Instance.ToVector4(configuration, (ReadOnlySpan<TPixel>)this.Palette, (Span<Vector4>)this.paletteVector, PixelConversionModifiers.Scale);
this.paletteVector = new Vector4[this.palette.Length];
PixelOperations<TPixel>.Instance.ToVector4(configuration, (ReadOnlySpan<TPixel>)this.palette, (Span<Vector4>)this.paletteVector, PixelConversionModifiers.Scale);
}
}
}

42
tests/ImageSharp.Tests/Processing/Dithering/DitherTest.cs

@ -1,6 +1,8 @@
// 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.Dithering;
@ -12,13 +14,21 @@ namespace SixLabors.ImageSharp.Tests.Processing.Binarization
{
public class DitherTest : BaseImageOperationsExtensionTest
{
private class Assert : Xunit.Assert
{
public static void Equal(ReadOnlySpan<Color> a, ReadOnlySpan<Color> b)
{
Xunit.Assert.True(a.SequenceEqual(b));
}
}
private readonly IOrderedDither orderedDither;
private readonly IErrorDiffuser errorDiffuser;
private readonly Rgba32[] TestPalette =
private readonly Color[] TestPalette =
{
Rgba32.Red,
Rgba32.Green,
Rgba32.Blue
Color.Red,
Color.Green,
Color.Blue
};
public DitherTest()
@ -31,24 +41,24 @@ namespace SixLabors.ImageSharp.Tests.Processing.Binarization
public void Dither_CorrectProcessor()
{
this.operations.Dither(this.orderedDither);
OrderedDitherPaletteProcessor<Rgba32> p = this.Verify<OrderedDitherPaletteProcessor<Rgba32>>();
OrderedDitherPaletteProcessor p = this.Verify<OrderedDitherPaletteProcessor>();
Assert.Equal(this.orderedDither, p.Dither);
Assert.Equal(NamedColors<Rgba32>.WebSafePalette, p.Palette);
Assert.Equal(Color.WebSafePalette.ToArray(), p.Palette);
}
[Fact]
public void Dither_rect_CorrectProcessor()
{
this.operations.Dither(this.orderedDither, this.rect);
OrderedDitherPaletteProcessor<Rgba32> p = this.Verify<OrderedDitherPaletteProcessor<Rgba32>>(this.rect);
OrderedDitherPaletteProcessor p = this.Verify<OrderedDitherPaletteProcessor>(this.rect);
Assert.Equal(this.orderedDither, p.Dither);
Assert.Equal(NamedColors<Rgba32>.WebSafePalette, p.Palette);
Assert.Equal(Color.WebSafePalette, p.Palette);
}
[Fact]
public void Dither_index_CorrectProcessor()
{
this.operations.Dither(this.orderedDither, this.TestPalette);
OrderedDitherPaletteProcessor<Rgba32> p = this.Verify<OrderedDitherPaletteProcessor<Rgba32>>();
OrderedDitherPaletteProcessor p = this.Verify<OrderedDitherPaletteProcessor>();
Assert.Equal(this.orderedDither, p.Dither);
Assert.Equal(this.TestPalette, p.Palette);
}
@ -57,7 +67,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Binarization
public void Dither_index_rect_CorrectProcessor()
{
this.operations.Dither(this.orderedDither, this.TestPalette, this.rect);
OrderedDitherPaletteProcessor<Rgba32> p = this.Verify<OrderedDitherPaletteProcessor<Rgba32>>(this.rect);
OrderedDitherPaletteProcessor p = this.Verify<OrderedDitherPaletteProcessor>(this.rect);
Assert.Equal(this.orderedDither, p.Dither);
Assert.Equal(this.TestPalette, p.Palette);
}
@ -67,27 +77,27 @@ namespace SixLabors.ImageSharp.Tests.Processing.Binarization
public void Dither_ErrorDiffuser_CorrectProcessor()
{
this.operations.Diffuse(this.errorDiffuser, .4F);
ErrorDiffusionPaletteProcessor<Rgba32> p = this.Verify<ErrorDiffusionPaletteProcessor<Rgba32>>();
ErrorDiffusionPaletteProcessor p = this.Verify<ErrorDiffusionPaletteProcessor>();
Assert.Equal(this.errorDiffuser, p.Diffuser);
Assert.Equal(.4F, p.Threshold);
Assert.Equal(NamedColors<Rgba32>.WebSafePalette, p.Palette);
Assert.Equal(Color.WebSafePalette, p.Palette);
}
[Fact]
public void Dither_ErrorDiffuser_rect_CorrectProcessor()
{
this.operations.Diffuse(this.errorDiffuser, .3F, this.rect);
ErrorDiffusionPaletteProcessor<Rgba32> p = this.Verify<ErrorDiffusionPaletteProcessor<Rgba32>>(this.rect);
ErrorDiffusionPaletteProcessor p = this.Verify<ErrorDiffusionPaletteProcessor>(this.rect);
Assert.Equal(this.errorDiffuser, p.Diffuser);
Assert.Equal(.3F, p.Threshold);
Assert.Equal(NamedColors<Rgba32>.WebSafePalette, p.Palette);
Assert.Equal(Color.WebSafePalette, p.Palette);
}
[Fact]
public void Dither_ErrorDiffuser_CorrectProcessorWithColors()
{
this.operations.Diffuse(this.errorDiffuser, .5F, this.TestPalette);
ErrorDiffusionPaletteProcessor<Rgba32> p = this.Verify<ErrorDiffusionPaletteProcessor<Rgba32>>();
ErrorDiffusionPaletteProcessor p = this.Verify<ErrorDiffusionPaletteProcessor>();
Assert.Equal(this.errorDiffuser, p.Diffuser);
Assert.Equal(.5F, p.Threshold);
Assert.Equal(this.TestPalette, p.Palette);
@ -97,7 +107,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Binarization
public void Dither_ErrorDiffuser_rect_CorrectProcessorWithColors()
{
this.operations.Diffuse(this.errorDiffuser, .5F, this.TestPalette, this.rect);
ErrorDiffusionPaletteProcessor<Rgba32> p = this.Verify<ErrorDiffusionPaletteProcessor<Rgba32>>(this.rect);
ErrorDiffusionPaletteProcessor p = this.Verify<ErrorDiffusionPaletteProcessor>(this.rect);
Assert.Equal(this.errorDiffuser, p.Diffuser);
Assert.Equal(.5F, p.Threshold);
Assert.Equal(this.TestPalette, p.Palette);

Loading…
Cancel
Save