📷 A modern, cross-platform, 2D Graphics library for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

292 lines
12 KiB

// 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 readonly partial struct OrderedDither : IDither, IEquatable<OrderedDither>, IEquatable<IDither>
{
private readonly DenseMatrix<float> thresholdMatrix;
private readonly int modulusX;
private readonly int modulusY;
/// <summary>
/// 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,
ImageFrame<TPixel> source,
QuantizedFrame<TPixel> destination,
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,
destination,
bounds);
ParallelRowIterator.IterateRows(
quantizer.Configuration,
bounds,
in ditherOperation);
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void ApplyPaletteDither<TPaletteDitherImageProcessor, TPixel>(
in TPaletteDitherImageProcessor processor,
ImageFrame<TPixel> source,
Rectangle bounds)
where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
var ditherOperation = new PaletteDitherRowIntervalOperation<TPaletteDitherImageProcessor, TPixel>(
in processor,
in Unsafe.AsRef(this),
source,
bounds);
ParallelRowIterator.IterateRows(
processor.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 QuantizedFrame<TPixel> destination;
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,
QuantizedFrame<TPixel> destination,
Rectangle bounds)
{
this.quantizer = quantizer;
this.dither = dither;
this.source = source;
this.destination = destination;
this.bounds = bounds;
this.palette = destination.Palette;
this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(destination.Palette.Span.Length);
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
{
ReadOnlySpan<TPixel> paletteSpan = this.palette.Span;
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++)
{
Span<TPixel> sourceRow = this.source.GetPixelRowSpan(y);
Span<byte> destinationRow = this.destination.GetPixelRowSpan(y - offsetY);
for (int x = this.bounds.Left; x < this.bounds.Right; x++)
{
TPixel dithered = this.dither.Dither(sourceRow[x], x, y, this.bitDepth, scale);
destinationRow[x - offsetX] = this.quantizer.GetQuantizedColor(dithered, paletteSpan, out TPixel _);
}
}
}
}
private readonly struct PaletteDitherRowIntervalOperation<TPaletteDitherImageProcessor, TPixel> : IRowIntervalOperation
where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
private readonly TPaletteDitherImageProcessor processor;
private readonly OrderedDither dither;
private readonly ImageFrame<TPixel> source;
private readonly Rectangle bounds;
private readonly float scale;
private readonly int bitDepth;
[MethodImpl(InliningOptions.ShortMethod)]
public PaletteDitherRowIntervalOperation(
in TPaletteDitherImageProcessor processor,
in OrderedDither dither,
ImageFrame<TPixel> source,
Rectangle bounds)
{
this.processor = processor;
this.dither = dither;
this.source = source;
this.bounds = bounds;
this.scale = processor.DitherScale;
this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(processor.Palette.Span.Length);
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
{
ReadOnlySpan<TPixel> paletteSpan = this.processor.Palette.Span;
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++)
{
ref TPixel sourcePixel = ref row[x];
TPixel dithered = this.dither.Dither(sourcePixel, x, y, this.bitDepth, this.scale);
sourcePixel = this.processor.GetPaletteColor(dithered, paletteSpan);
}
}
}
}
}
}