diff --git a/src/ImageSharp/Common/Helpers/ImageMaths.cs b/src/ImageSharp/Common/Helpers/ImageMaths.cs
index 0c5b05180..64bcb11c9 100644
--- a/src/ImageSharp/Common/Helpers/ImageMaths.cs
+++ b/src/ImageSharp/Common/Helpers/ImageMaths.cs
@@ -5,7 +5,6 @@ using System;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
-using SixLabors.ImageSharp.Processing.Processors.Transforms;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp
diff --git a/src/ImageSharp/Processing/HistogramEqualizationExtension.cs b/src/ImageSharp/Processing/HistogramEqualizationExtension.cs
index 8dabfcc05..d967ef362 100644
--- a/src/ImageSharp/Processing/HistogramEqualizationExtension.cs
+++ b/src/ImageSharp/Processing/HistogramEqualizationExtension.cs
@@ -12,25 +12,51 @@ namespace SixLabors.ImageSharp.Processing
public static class HistogramEqualizationExtension
{
///
- /// Equalizes the histogram of an image to increases the global contrast using 65536 luminance levels.
+ /// Equalizes the histogram of an image to increases the contrast.
///
/// The pixel format.
/// The image this method extends.
/// The .
public static IImageProcessingContext HistogramEqualization(this IImageProcessingContext source)
where TPixel : struct, IPixel
- => HistogramEqualization(source, 65536);
+ => HistogramEqualization(source, HistogramEqualizationOptions.Default);
///
- /// Equalizes the histogram of an image to increases the global contrast.
+ /// Equalizes the histogram of an image to increases the contrast.
///
/// The pixel format.
/// The image this method extends.
- /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images
- /// or 65536 for 16-bit grayscale images.
+ /// The histogram equalization options to use.
/// The .
- public static IImageProcessingContext HistogramEqualization(this IImageProcessingContext source, int luminanceLevels)
+ public static IImageProcessingContext HistogramEqualization(this IImageProcessingContext source, HistogramEqualizationOptions options)
where TPixel : struct, IPixel
- => source.ApplyProcessor(new HistogramEqualizationProcessor(luminanceLevels));
+ => source.ApplyProcessor(GetProcessor(options));
+
+ private static HistogramEqualizationProcessor GetProcessor(HistogramEqualizationOptions options)
+ where TPixel : struct, IPixel
+ {
+ HistogramEqualizationProcessor processor;
+
+ switch (options.Method)
+ {
+ case HistogramEqualizationMethod.Global:
+ processor = new GlobalHistogramEqualizationProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimitPercentage);
+ break;
+
+ case HistogramEqualizationMethod.AdaptiveTileInterpolation:
+ processor = new AdaptiveHistEqualizationProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimitPercentage, options.Tiles);
+ break;
+
+ case HistogramEqualizationMethod.AdaptiveSlidingWindow:
+ processor = new AdaptiveHistEqualizationSWProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimitPercentage, options.Tiles);
+ break;
+
+ default:
+ processor = new GlobalHistogramEqualizationProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimitPercentage);
+ break;
+ }
+
+ return processor;
+ }
}
}
diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs
new file mode 100644
index 000000000..cb52a88b7
--- /dev/null
+++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs
@@ -0,0 +1,545 @@
+// 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 System.Threading.Tasks;
+using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.Memory;
+using SixLabors.Primitives;
+
+namespace SixLabors.ImageSharp.Processing.Processors.Normalization
+{
+ ///
+ /// Applies an adaptive histogram equalization to the image. The image is split up in tiles. For each tile a cumulative distribution function (cdf) is calculated.
+ /// To calculate the final equalized pixel value, the cdf value of four adjacent tiles will be interpolated.
+ ///
+ /// The pixel format.
+ internal class AdaptiveHistEqualizationProcessor : HistogramEqualizationProcessor
+ where TPixel : struct, IPixel
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images
+ /// or 65536 for 16-bit grayscale images.
+ /// Indicating whether to clip the histogram bins at a specific value.
+ /// Histogram clip limit in percent of the total pixels in the tile. Histogram bins which exceed this limit, will be capped at this value.
+ /// The number of tiles the image is split into (horizontal and vertically). Minimum value is 2. Maximum value is 100.
+ public AdaptiveHistEqualizationProcessor(int luminanceLevels, bool clipHistogram, float clipLimitPercentage, int tiles)
+ : base(luminanceLevels, clipHistogram, clipLimitPercentage)
+ {
+ Guard.MustBeGreaterThanOrEqualTo(tiles, 2, nameof(tiles));
+ Guard.MustBeLessThanOrEqualTo(tiles, 100, nameof(tiles));
+
+ this.Tiles = tiles;
+ }
+
+ ///
+ /// Gets the number of tiles the image is split into (horizontal and vertically) for the adaptive histogram equalization.
+ ///
+ private int Tiles { get; }
+
+ ///
+ protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration)
+ {
+ int sourceWidth = source.Width;
+ int sourceHeight = source.Height;
+ int numberOfPixels = sourceWidth * sourceHeight;
+ int tileWidth = (int)MathF.Ceiling(sourceWidth / (float)this.Tiles);
+ int tileHeight = (int)MathF.Ceiling(sourceHeight / (float)this.Tiles);
+ int pixelsInTile = tileWidth * tileHeight;
+ int halfTileWidth = tileWidth / 2;
+ int halfTileHeight = tileHeight / 2;
+ int luminanceLevels = this.LuminanceLevels;
+
+ // The image is split up into tiles. For each tile the cumulative distribution function will be calculated.
+ using (var cdfData = new CdfTileData(configuration, sourceWidth, sourceHeight, this.Tiles, this.Tiles, tileWidth, tileHeight, luminanceLevels))
+ {
+ cdfData.CalculateLookupTables(source, this);
+
+ var tileYStartPositions = new List<(int y, int cdfY)>();
+ int cdfY = 0;
+ for (int y = halfTileHeight; y < sourceHeight - halfTileHeight; y += tileHeight)
+ {
+ tileYStartPositions.Add((y, cdfY));
+ cdfY++;
+ }
+
+ Parallel.For(
+ 0,
+ tileYStartPositions.Count,
+ new ParallelOptions() { MaxDegreeOfParallelism = configuration.MaxDegreeOfParallelism },
+ index =>
+ {
+ int cdfX = 0;
+ int tileX = 0;
+ int tileY = 0;
+ int y = tileYStartPositions[index].y;
+ int cdfYY = tileYStartPositions[index].cdfY;
+
+ // It's unfortunate that we have to do this per iteration.
+ ref TPixel sourceBase = ref source.GetPixelReference(0, 0);
+
+ cdfX = 0;
+ for (int x = halfTileWidth; x < sourceWidth - halfTileWidth; x += tileWidth)
+ {
+ tileY = 0;
+ int yEnd = Math.Min(y + tileHeight, sourceHeight);
+ int xEnd = Math.Min(x + tileWidth, sourceWidth);
+ for (int dy = y; dy < yEnd; dy++)
+ {
+ int dyOffSet = dy * sourceWidth;
+ tileX = 0;
+ for (int dx = x; dx < xEnd; dx++)
+ {
+ ref TPixel pixel = ref Unsafe.Add(ref sourceBase, dyOffSet + dx);
+ float luminanceEqualized = InterpolateBetweenFourTiles(
+ pixel,
+ cdfData,
+ this.Tiles,
+ this.Tiles,
+ tileX,
+ tileY,
+ cdfX,
+ cdfYY,
+ tileWidth,
+ tileHeight,
+ luminanceLevels);
+
+ pixel.FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W));
+ tileX++;
+ }
+
+ tileY++;
+ }
+
+ cdfX++;
+ }
+ });
+
+ ref TPixel pixelsBase = ref source.GetPixelReference(0, 0);
+
+ // Fix left column
+ ProcessBorderColumn(ref pixelsBase, cdfData, 0, sourceWidth, sourceHeight, tileWidth, tileHeight, xStart: 0, xEnd: halfTileWidth, luminanceLevels);
+
+ // Fix right column
+ int rightBorderStartX = ((this.Tiles - 1) * tileWidth) + halfTileWidth;
+ ProcessBorderColumn(ref pixelsBase, cdfData, this.Tiles - 1, sourceWidth, sourceHeight, tileWidth, tileHeight, xStart: rightBorderStartX, xEnd: sourceWidth, luminanceLevels);
+
+ // Fix top row
+ ProcessBorderRow(ref pixelsBase, cdfData, 0, sourceWidth, tileWidth, yStart: 0, yEnd: halfTileHeight, luminanceLevels);
+
+ // Fix bottom row
+ int bottomBorderStartY = ((this.Tiles - 1) * tileHeight) + halfTileHeight;
+ ProcessBorderRow(ref pixelsBase, cdfData, this.Tiles - 1, sourceWidth, tileWidth, yStart: bottomBorderStartY, yEnd: sourceHeight, luminanceLevels);
+
+ // Left top corner
+ ProcessCornerTile(ref pixelsBase, cdfData, sourceWidth, 0, 0, xStart: 0, xEnd: halfTileWidth, yStart: 0, yEnd: halfTileHeight, luminanceLevels);
+
+ // Left bottom corner
+ ProcessCornerTile(ref pixelsBase, cdfData, sourceWidth, 0, this.Tiles - 1, xStart: 0, xEnd: halfTileWidth, yStart: bottomBorderStartY, yEnd: sourceHeight, luminanceLevels);
+
+ // Right top corner
+ ProcessCornerTile(ref pixelsBase, cdfData, sourceWidth, this.Tiles - 1, 0, xStart: rightBorderStartX, xEnd: sourceWidth, yStart: 0, yEnd: halfTileHeight, luminanceLevels);
+
+ // Right bottom corner
+ ProcessCornerTile(ref pixelsBase, cdfData, sourceWidth, this.Tiles - 1, this.Tiles - 1, xStart: rightBorderStartX, xEnd: sourceWidth, yStart: bottomBorderStartY, yEnd: sourceHeight, luminanceLevels);
+ }
+ }
+
+ ///
+ /// Processes the part of a corner tile which was previously left out. It consists of 1 / 4 of a tile and does not need interpolation.
+ ///
+ /// The output pixels base reference.
+ /// The lookup table to remap the grey values.
+ /// The source image width.
+ /// The x-position in the CDF lookup map.
+ /// The y-position in the CDF lookup map.
+ /// X start position.
+ /// X end position.
+ /// Y start position.
+ /// Y end position.
+ ///
+ /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images
+ /// or 65536 for 16-bit grayscale images.
+ ///
+ private static void ProcessCornerTile(
+ ref TPixel pixelsBase,
+ CdfTileData cdfData,
+ int sourceWidth,
+ int cdfX,
+ int cdfY,
+ int xStart,
+ int xEnd,
+ int yStart,
+ int yEnd,
+ int luminanceLevels)
+ {
+ for (int dy = yStart; dy < yEnd; dy++)
+ {
+ int dyOffSet = dy * sourceWidth;
+ for (int dx = xStart; dx < xEnd; dx++)
+ {
+ ref TPixel pixel = ref Unsafe.Add(ref pixelsBase, dyOffSet + dx);
+ float luminanceEqualized = cdfData.RemapGreyValue(cdfX, cdfY, GetLuminance(pixel, luminanceLevels));
+ pixel.FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W));
+ }
+ }
+ }
+
+ ///
+ /// Processes a border column of the image which is half the size of the tile width.
+ ///
+ /// The output pixels reference.
+ /// The pre-computed lookup tables to remap the grey values for each tiles.
+ /// The X index of the lookup table to use.
+ /// The source image width.
+ /// The source image height.
+ /// The width of a tile.
+ /// The height of a tile.
+ /// X start position in the image.
+ /// X end position of the image.
+ ///
+ /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images
+ /// or 65536 for 16-bit grayscale images.
+ ///
+ private static void ProcessBorderColumn(
+ ref TPixel pixelBase,
+ CdfTileData cdfData,
+ int cdfX,
+ int sourceWidth,
+ int sourceHeight,
+ int tileWidth,
+ int tileHeight,
+ int xStart,
+ int xEnd,
+ int luminanceLevels)
+ {
+ int halfTileWidth = tileWidth / 2;
+ int halfTileHeight = tileHeight / 2;
+
+ int cdfY = 0;
+ for (int y = halfTileHeight; y < sourceHeight - halfTileHeight; y += tileHeight)
+ {
+ int yLimit = Math.Min(y + tileHeight, sourceHeight - 1);
+ int tileY = 0;
+ for (int dy = y; dy < yLimit; dy++)
+ {
+ int dyOffSet = dy * sourceWidth;
+ int tileX = halfTileWidth;
+ for (int dx = xStart; dx < xEnd; dx++)
+ {
+ ref TPixel pixel = ref Unsafe.Add(ref pixelBase, dyOffSet + dx);
+ float luminanceEqualized = InterpolateBetweenTwoTiles(pixel, cdfData, cdfX, cdfY, cdfX, cdfY + 1, tileY, tileHeight, luminanceLevels);
+ pixel.FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W));
+ tileX++;
+ }
+
+ tileY++;
+ }
+
+ cdfY++;
+ }
+ }
+
+ ///
+ /// Processes a border row of the image which is half of the size of the tile height.
+ ///
+ /// The output pixels base reference.
+ /// The pre-computed lookup tables to remap the grey values for each tiles.
+ /// The Y index of the lookup table to use.
+ /// The source image width.
+ /// The width of a tile.
+ /// Y start position in the image.
+ /// Y end position of the image.
+ ///
+ /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images
+ /// or 65536 for 16-bit grayscale images.
+ ///
+ private static void ProcessBorderRow(
+ ref TPixel pixelBase,
+ CdfTileData cdfData,
+ int cdfY,
+ int sourceWidth,
+ int tileWidth,
+ int yStart,
+ int yEnd,
+ int luminanceLevels)
+ {
+ int halfTileWidth = tileWidth / 2;
+
+ int cdfX = 0;
+ for (int x = halfTileWidth; x < sourceWidth - halfTileWidth; x += tileWidth)
+ {
+ int tileY = 0;
+ for (int dy = yStart; dy < yEnd; dy++)
+ {
+ int dyOffSet = dy * sourceWidth;
+ int tileX = 0;
+ int xLimit = Math.Min(x + tileWidth, sourceWidth - 1);
+ for (int dx = x; dx < xLimit; dx++)
+ {
+ ref TPixel pixel = ref Unsafe.Add(ref pixelBase, dyOffSet + dx);
+ float luminanceEqualized = InterpolateBetweenTwoTiles(pixel, cdfData, cdfX, cdfY, cdfX + 1, cdfY, tileX, tileWidth, luminanceLevels);
+ pixel.FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W));
+ tileX++;
+ }
+
+ tileY++;
+ }
+
+ cdfX++;
+ }
+ }
+
+ ///
+ /// Bilinear interpolation between four adjacent tiles.
+ ///
+ /// The pixel to remap the grey value from.
+ /// The pre-computed lookup tables to remap the grey values for each tiles.
+ /// The number of tiles in the x-direction.
+ /// The number of tiles in the y-direction.
+ /// X position inside the tile.
+ /// Y position inside the tile.
+ /// X index of the top left lookup table to use.
+ /// Y index of the top left lookup table to use.
+ /// Width of one tile in pixels.
+ /// Height of one tile in pixels.
+ ///
+ /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images
+ /// or 65536 for 16-bit grayscale images.
+ ///
+ /// A re-mapped grey value.
+ [MethodImpl(InliningOptions.ShortMethod)]
+ private static float InterpolateBetweenFourTiles(
+ TPixel sourcePixel,
+ CdfTileData cdfData,
+ int tileCountX,
+ int tileCountY,
+ int tileX,
+ int tileY,
+ int cdfX,
+ int cdfY,
+ int tileWidth,
+ int tileHeight,
+ int luminanceLevels)
+ {
+ int luminance = GetLuminance(sourcePixel, luminanceLevels);
+ float tx = tileX / (float)(tileWidth - 1);
+ float ty = tileY / (float)(tileHeight - 1);
+
+ int yTop = cdfY;
+ int yBottom = Math.Min(tileCountY - 1, yTop + 1);
+ int xLeft = cdfX;
+ int xRight = Math.Min(tileCountX - 1, xLeft + 1);
+
+ float cdfLeftTopLuminance = cdfData.RemapGreyValue(xLeft, yTop, luminance);
+ float cdfRightTopLuminance = cdfData.RemapGreyValue(xRight, yTop, luminance);
+ float cdfLeftBottomLuminance = cdfData.RemapGreyValue(xLeft, yBottom, luminance);
+ float cdfRightBottomLuminance = cdfData.RemapGreyValue(xRight, yBottom, luminance);
+ return BilinearInterpolation(tx, ty, cdfLeftTopLuminance, cdfRightTopLuminance, cdfLeftBottomLuminance, cdfRightBottomLuminance);
+ }
+
+ ///
+ /// Linear interpolation between two tiles.
+ ///
+ /// The pixel to remap the grey value from.
+ /// The CDF lookup map.
+ /// X position inside the first tile.
+ /// Y position inside the first tile.
+ /// X position inside the second tile.
+ /// Y position inside the second tile.
+ /// Position inside the tile.
+ /// Width of the tile.
+ ///
+ /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images
+ /// or 65536 for 16-bit grayscale images.
+ ///
+ /// A re-mapped grey value.
+ [MethodImpl(InliningOptions.ShortMethod)]
+ private static float InterpolateBetweenTwoTiles(
+ TPixel sourcePixel,
+ CdfTileData cdfData,
+ int tileX1,
+ int tileY1,
+ int tileX2,
+ int tileY2,
+ int tilePos,
+ int tileWidth,
+ int luminanceLevels)
+ {
+ int luminance = GetLuminance(sourcePixel, luminanceLevels);
+ float tx = tilePos / (float)(tileWidth - 1);
+
+ float cdfLuminance1 = cdfData.RemapGreyValue(tileX1, tileY1, luminance);
+ float cdfLuminance2 = cdfData.RemapGreyValue(tileX2, tileY2, luminance);
+ return LinearInterpolation(cdfLuminance1, cdfLuminance2, tx);
+ }
+
+ ///
+ /// Bilinear interpolation between four tiles.
+ ///
+ /// The interpolation value in x direction in the range of [0, 1].
+ /// The interpolation value in y direction in the range of [0, 1].
+ /// Luminance from top left tile.
+ /// Luminance from right top tile.
+ /// Luminance from left bottom tile.
+ /// Luminance from right bottom tile.
+ /// Interpolated Luminance.
+ [MethodImpl(InliningOptions.ShortMethod)]
+ private static float BilinearInterpolation(float tx, float ty, float lt, float rt, float lb, float rb)
+ => LinearInterpolation(LinearInterpolation(lt, rt, tx), LinearInterpolation(lb, rb, tx), ty);
+
+ ///
+ /// Linear interpolation between two grey values.
+ ///
+ /// The left value.
+ /// The right value.
+ /// The interpolation value between the two values in the range of [0, 1].
+ /// The interpolated value.
+ [MethodImpl(InliningOptions.ShortMethod)]
+ private static float LinearInterpolation(float left, float right, float t)
+ => left + ((right - left) * t);
+
+ ///
+ /// Contains the results of the cumulative distribution function for all tiles.
+ ///
+ private sealed class CdfTileData : IDisposable
+ {
+ private readonly Configuration configuration;
+ private readonly MemoryAllocator memoryAllocator;
+
+ // Used for storing the minimum value for each CDF entry.
+ private readonly Buffer2D cdfMinBuffer2D;
+
+ // Used for storing the LUT for each CDF entry.
+ private readonly Buffer2D cdfLutBuffer2D;
+ private readonly int pixelsInTile;
+ private readonly int sourceWidth;
+ private readonly int sourceHeight;
+ private readonly int tileWidth;
+ private readonly int tileHeight;
+ private readonly int luminanceLevels;
+ private readonly List<(int y, int cdfY)> tileYStartPositions;
+
+ public CdfTileData(
+ Configuration configuration,
+ int sourceWidth,
+ int sourceHeight,
+ int tileCountX,
+ int tileCountY,
+ int tileWidth,
+ int tileHeight,
+ int luminanceLevels)
+ {
+ this.configuration = configuration;
+ this.memoryAllocator = configuration.MemoryAllocator;
+ this.luminanceLevels = luminanceLevels;
+ this.cdfMinBuffer2D = this.memoryAllocator.Allocate2D(tileCountX, tileCountY);
+ this.cdfLutBuffer2D = this.memoryAllocator.Allocate2D(tileCountX * luminanceLevels, tileCountY);
+ this.sourceWidth = sourceWidth;
+ this.sourceHeight = sourceHeight;
+ this.tileWidth = tileWidth;
+ this.tileHeight = tileHeight;
+ this.pixelsInTile = tileWidth * tileHeight;
+
+ // Calculate the start positions and rent buffers.
+ this.tileYStartPositions = new List<(int y, int cdfY)>();
+ int cdfY = 0;
+ for (int y = 0; y < sourceHeight; y += tileHeight)
+ {
+ this.tileYStartPositions.Add((y, cdfY));
+ cdfY++;
+ }
+ }
+
+ public void CalculateLookupTables(ImageFrame source, HistogramEqualizationProcessor processor)
+ {
+ int sourceWidth = this.sourceWidth;
+ int sourceHeight = this.sourceHeight;
+ int tileWidth = this.tileWidth;
+ int tileHeight = this.tileHeight;
+ int luminanceLevels = this.luminanceLevels;
+ MemoryAllocator memoryAllocator = this.memoryAllocator;
+
+ Parallel.For(
+ 0,
+ this.tileYStartPositions.Count,
+ new ParallelOptions() { MaxDegreeOfParallelism = this.configuration.MaxDegreeOfParallelism },
+ index =>
+ {
+ int cdfX = 0;
+ int cdfY = this.tileYStartPositions[index].cdfY;
+ int y = this.tileYStartPositions[index].y;
+ int endY = Math.Min(y + tileHeight, sourceHeight);
+ ref TPixel sourceBase = ref source.GetPixelReference(0, 0);
+ ref int cdfMinBase = ref MemoryMarshal.GetReference(this.cdfMinBuffer2D.GetRowSpan(cdfY));
+
+ using (IMemoryOwner histogramBuffer = this.memoryAllocator.Allocate(luminanceLevels))
+ {
+ Span histogram = histogramBuffer.GetSpan();
+ ref int histogramBase = ref MemoryMarshal.GetReference(histogram);
+
+ for (int x = 0; x < sourceWidth; x += tileWidth)
+ {
+ histogram.Clear();
+ ref int cdfBase = ref MemoryMarshal.GetReference(this.GetCdfLutSpan(cdfX, index));
+
+ int xlimit = Math.Min(x + tileWidth, sourceWidth);
+ for (int dy = y; dy < endY; dy++)
+ {
+ int dyOffset = dy * sourceWidth;
+ for (int dx = x; dx < xlimit; dx++)
+ {
+ int luminace = GetLuminance(Unsafe.Add(ref sourceBase, dyOffset + dx), luminanceLevels);
+ histogram[luminace]++;
+ }
+ }
+
+ if (processor.ClipHistogramEnabled)
+ {
+ processor.ClipHistogram(histogram, processor.ClipLimitPercentage, this.pixelsInTile);
+ }
+
+ Unsafe.Add(ref cdfMinBase, cdfX) = processor.CalculateCdf(ref cdfBase, ref histogramBase, histogram.Length - 1);
+
+ cdfX++;
+ }
+ }
+ });
+ }
+
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public Span GetCdfLutSpan(int tileX, int tileY) => this.cdfLutBuffer2D.GetRowSpan(tileY).Slice(tileX * this.luminanceLevels, this.luminanceLevels);
+
+ ///
+ /// Remaps the grey value with the cdf.
+ ///
+ /// The tiles x-position.
+ /// The tiles y-position.
+ /// The original luminance.
+ /// The remapped luminance.
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public float RemapGreyValue(int tilesX, int tilesY, int luminance)
+ {
+ int cdfMin = this.cdfMinBuffer2D[tilesX, tilesY];
+ Span cdfSpan = this.GetCdfLutSpan(tilesX, tilesY);
+ return (this.pixelsInTile - cdfMin) == 0
+ ? cdfSpan[luminance] / this.pixelsInTile
+ : cdfSpan[luminance] / (float)(this.pixelsInTile - cdfMin);
+ }
+
+ public void Dispose()
+ {
+ this.cdfMinBuffer2D.Dispose();
+ this.cdfLutBuffer2D.Dispose();
+ }
+ }
+ }
+}
diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs
new file mode 100644
index 000000000..aa9b530c6
--- /dev/null
+++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs
@@ -0,0 +1,389 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Buffers;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using SixLabors.ImageSharp.Advanced;
+using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.Memory;
+using SixLabors.Primitives;
+
+namespace SixLabors.ImageSharp.Processing.Processors.Normalization
+{
+ ///
+ /// Applies an adaptive histogram equalization to the image using an sliding window approach.
+ ///
+ /// The pixel format.
+ internal class AdaptiveHistEqualizationSWProcessor : HistogramEqualizationProcessor
+ where TPixel : struct, IPixel
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images
+ /// or 65536 for 16-bit grayscale images.
+ /// Indicating whether to clip the histogram bins at a specific value.
+ /// Histogram clip limit in percent of the total pixels in the tile. Histogram bins which exceed this limit, will be capped at this value.
+ /// The number of tiles the image is split into (horizontal and vertically). Minimum value is 2. Maximum value is 100.
+ public AdaptiveHistEqualizationSWProcessor(int luminanceLevels, bool clipHistogram, float clipLimitPercentage, int tiles)
+ : base(luminanceLevels, clipHistogram, clipLimitPercentage)
+ {
+ Guard.MustBeGreaterThanOrEqualTo(tiles, 2, nameof(tiles));
+ Guard.MustBeLessThanOrEqualTo(tiles, 100, nameof(tiles));
+
+ this.Tiles = tiles;
+ }
+
+ ///
+ /// Gets the number of tiles the image is split into (horizontal and vertically) for the adaptive histogram equalization.
+ ///
+ private int Tiles { get; }
+
+ ///
+ protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration)
+ {
+ MemoryAllocator memoryAllocator = configuration.MemoryAllocator;
+ int numberOfPixels = source.Width * source.Height;
+ Span pixels = source.GetPixelSpan();
+
+ var parallelOptions = new ParallelOptions() { MaxDegreeOfParallelism = configuration.MaxDegreeOfParallelism };
+ int tileWidth = source.Width / this.Tiles;
+ int tileHeight = tileWidth;
+ int pixeInTile = tileWidth * tileHeight;
+ int halfTileHeight = tileHeight / 2;
+ int halfTileWidth = halfTileHeight;
+ var slidingWindowInfos = new SlidingWindowInfos(tileWidth, tileHeight, halfTileWidth, halfTileHeight, pixeInTile);
+
+ using (Buffer2D targetPixels = configuration.MemoryAllocator.Allocate2D(source.Width, source.Height))
+ {
+ // Process the inner tiles, which do not require to check the borders.
+ Parallel.For(
+ halfTileWidth,
+ source.Width - halfTileWidth,
+ parallelOptions,
+ this.ProcessSlidingWindow(
+ source,
+ memoryAllocator,
+ targetPixels,
+ slidingWindowInfos,
+ yStart: halfTileHeight,
+ yEnd: source.Height - halfTileHeight,
+ useFastPath: true,
+ configuration));
+
+ // Process the left border of the image.
+ Parallel.For(
+ 0,
+ halfTileWidth,
+ parallelOptions,
+ this.ProcessSlidingWindow(
+ source,
+ memoryAllocator,
+ targetPixels,
+ slidingWindowInfos,
+ yStart: 0,
+ yEnd: source.Height,
+ useFastPath: false,
+ configuration));
+
+ // Process the right border of the image.
+ Parallel.For(
+ source.Width - halfTileWidth,
+ source.Width,
+ parallelOptions,
+ this.ProcessSlidingWindow(
+ source,
+ memoryAllocator,
+ targetPixels,
+ slidingWindowInfos,
+ yStart: 0,
+ yEnd: source.Height,
+ useFastPath: false,
+ configuration));
+
+ // Process the top border of the image.
+ Parallel.For(
+ halfTileWidth,
+ source.Width - halfTileWidth,
+ parallelOptions,
+ this.ProcessSlidingWindow(
+ source,
+ memoryAllocator,
+ targetPixels,
+ slidingWindowInfos,
+ yStart: 0,
+ yEnd: halfTileHeight,
+ useFastPath: false,
+ configuration));
+
+ // Process the bottom border of the image.
+ Parallel.For(
+ halfTileWidth,
+ source.Width - halfTileWidth,
+ parallelOptions,
+ this.ProcessSlidingWindow(
+ source,
+ memoryAllocator,
+ targetPixels,
+ slidingWindowInfos,
+ yStart: source.Height - halfTileHeight,
+ yEnd: source.Height,
+ useFastPath: false,
+ configuration));
+
+ Buffer2D.SwapOrCopyContent(source.PixelBuffer, targetPixels);
+ }
+ }
+
+ ///
+ /// Applies the sliding window equalization to one column of the image. The window is moved from top to bottom.
+ /// Moving the window one pixel down requires to remove one row from the top of the window from the histogram and
+ /// adding a new row at the bottom.
+ ///
+ /// The source image.
+ /// The memory allocator.
+ /// The target pixels.
+ /// Informations about the sliding window dimensions.
+ /// The y start position.
+ /// The y end position.
+ /// if set to true the borders of the image will not be checked.
+ /// The configuration.
+ /// Action Delegate.
+ private Action ProcessSlidingWindow(
+ ImageFrame source,
+ MemoryAllocator memoryAllocator,
+ Buffer2D targetPixels,
+ SlidingWindowInfos swInfos,
+ int yStart,
+ int yEnd,
+ bool useFastPath,
+ Configuration configuration)
+ {
+ return x =>
+ {
+ using (IMemoryOwner histogramBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean))
+ using (IMemoryOwner histogramBufferCopy = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean))
+ using (IMemoryOwner cdfBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean))
+ using (IMemoryOwner pixelRowBuffer = memoryAllocator.Allocate(swInfos.TileWidth, AllocationOptions.Clean))
+ {
+ Span histogram = histogramBuffer.GetSpan();
+ ref int histogramBase = ref MemoryMarshal.GetReference(histogram);
+
+ Span histogramCopy = histogramBufferCopy.GetSpan();
+ ref int histogramCopyBase = ref MemoryMarshal.GetReference(histogramCopy);
+
+ ref int cdfBase = ref MemoryMarshal.GetReference(cdfBuffer.GetSpan());
+
+ Span pixelRow = pixelRowBuffer.GetSpan();
+ ref Vector4 pixelRowBase = ref MemoryMarshal.GetReference(pixelRow);
+
+ // Build the initial histogram of grayscale values.
+ for (int dy = yStart - swInfos.HalfTileHeight; dy < yStart + swInfos.HalfTileHeight; dy++)
+ {
+ if (useFastPath)
+ {
+ this.CopyPixelRowFast(source, pixelRow, x - swInfos.HalfTileWidth, dy, swInfos.TileWidth, configuration);
+ }
+ else
+ {
+ this.CopyPixelRow(source, pixelRow, x - swInfos.HalfTileWidth, dy, swInfos.TileWidth, configuration);
+ }
+
+ this.AddPixelsToHistogram(ref pixelRowBase, ref histogramBase, this.LuminanceLevels, pixelRow.Length);
+ }
+
+ for (int y = yStart; y < yEnd; y++)
+ {
+ if (this.ClipHistogramEnabled)
+ {
+ // Clipping the histogram, but doing it on a copy to keep the original un-clipped values for the next iteration.
+ histogram.CopyTo(histogramCopy);
+ this.ClipHistogram(histogramCopy, this.ClipLimitPercentage, swInfos.PixeInTile);
+ }
+
+ // Calculate the cumulative distribution function, which will map each input pixel in the current tile to a new value.
+ int cdfMin = this.ClipHistogramEnabled
+ ? this.CalculateCdf(ref cdfBase, ref histogramCopyBase, histogram.Length - 1)
+ : this.CalculateCdf(ref cdfBase, ref histogramBase, histogram.Length - 1);
+
+ float numberOfPixelsMinusCdfMin = swInfos.PixeInTile - cdfMin;
+
+ // Map the current pixel to the new equalized value.
+ int luminance = GetLuminance(source[x, y], this.LuminanceLevels);
+ float luminanceEqualized = Unsafe.Add(ref cdfBase, luminance) / numberOfPixelsMinusCdfMin;
+ targetPixels[x, y].FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, source[x, y].ToVector4().W));
+
+ // Remove top most row from the histogram, mirroring rows which exceeds the borders.
+ if (useFastPath)
+ {
+ this.CopyPixelRowFast(source, pixelRow, x - swInfos.HalfTileWidth, y - swInfos.HalfTileWidth, swInfos.TileWidth, configuration);
+ }
+ else
+ {
+ this.CopyPixelRow(source, pixelRow, x - swInfos.HalfTileWidth, y - swInfos.HalfTileWidth, swInfos.TileWidth, configuration);
+ }
+
+ this.RemovePixelsFromHistogram(ref pixelRowBase, ref histogramBase, this.LuminanceLevels, pixelRow.Length);
+
+ // Add new bottom row to the histogram, mirroring rows which exceeds the borders.
+ if (useFastPath)
+ {
+ this.CopyPixelRowFast(source, pixelRow, x - swInfos.HalfTileWidth, y + swInfos.HalfTileWidth, swInfos.TileWidth, configuration);
+ }
+ else
+ {
+ this.CopyPixelRow(source, pixelRow, x - swInfos.HalfTileWidth, y + swInfos.HalfTileWidth, swInfos.TileWidth, configuration);
+ }
+
+ this.AddPixelsToHistogram(ref pixelRowBase, ref histogramBase, this.LuminanceLevels, pixelRow.Length);
+ }
+ }
+ };
+ }
+
+ ///
+ /// Get the a pixel row at a given position with a length of the tile width. Mirrors pixels which exceeds the edges.
+ ///
+ /// The source image.
+ /// Pre-allocated pixel row span of the size of a the tile width.
+ /// The x position.
+ /// The y position.
+ /// The width in pixels of a tile.
+ /// The configuration.
+ private void CopyPixelRow(
+ ImageFrame source,
+ Span rowPixels,
+ int x,
+ int y,
+ int tileWidth,
+ Configuration configuration)
+ {
+ if (y < 0)
+ {
+ y = ImageMaths.FastAbs(y);
+ }
+ else if (y >= source.Height)
+ {
+ int diff = y - source.Height;
+ y = source.Height - diff - 1;
+ }
+
+ // Special cases for the left and the right border where GetPixelRowSpan can not be used.
+ if (x < 0)
+ {
+ rowPixels.Clear();
+ int idx = 0;
+ for (int dx = x; dx < x + tileWidth; dx++)
+ {
+ rowPixels[idx] = source[ImageMaths.FastAbs(dx), y].ToVector4();
+ idx++;
+ }
+
+ return;
+ }
+ else if (x + tileWidth > source.Width)
+ {
+ rowPixels.Clear();
+ int idx = 0;
+ for (int dx = x; dx < x + tileWidth; dx++)
+ {
+ if (dx >= source.Width)
+ {
+ int diff = dx - source.Width;
+ rowPixels[idx] = source[dx - diff - 1, y].ToVector4();
+ }
+ else
+ {
+ rowPixels[idx] = source[dx, y].ToVector4();
+ }
+
+ idx++;
+ }
+
+ return;
+ }
+
+ this.CopyPixelRowFast(source, rowPixels, x, y, tileWidth, configuration);
+ }
+
+ ///
+ /// Get the a pixel row at a given position with a length of the tile width.
+ ///
+ /// The source image.
+ /// Pre-allocated pixel row span of the size of a the tile width.
+ /// The x position.
+ /// The y position.
+ /// The width in pixels of a tile.
+ /// The configuration.
+ [MethodImpl(InliningOptions.ShortMethod)]
+ private void CopyPixelRowFast(
+ ImageFrame source,
+ Span rowPixels,
+ int x,
+ int y,
+ int tileWidth,
+ Configuration configuration)
+ => PixelOperations.Instance.ToVector4(configuration, source.GetPixelRowSpan(y).Slice(start: x, length: tileWidth), rowPixels);
+
+ ///
+ /// Adds a column of grey values to the histogram.
+ ///
+ /// The reference to the span of grey values to add.
+ /// The reference to the histogram span.
+ /// The number of different luminance levels.
+ /// The grey values span length.
+ [MethodImpl(InliningOptions.ShortMethod)]
+ private void AddPixelsToHistogram(ref Vector4 greyValuesBase, ref int histogramBase, int luminanceLevels, int length)
+ {
+ for (int idx = 0; idx < length; idx++)
+ {
+ int luminance = GetLuminance(ref Unsafe.Add(ref greyValuesBase, idx), luminanceLevels);
+ Unsafe.Add(ref histogramBase, luminance)++;
+ }
+ }
+
+ ///
+ /// Removes a column of grey values from the histogram.
+ ///
+ /// The reference to the span of grey values to remove.
+ /// The reference to the histogram span.
+ /// The number of different luminance levels.
+ /// The grey values span length.
+ [MethodImpl(InliningOptions.ShortMethod)]
+ private void RemovePixelsFromHistogram(ref Vector4 greyValuesBase, ref int histogramBase, int luminanceLevels, int length)
+ {
+ for (int idx = 0; idx < length; idx++)
+ {
+ int luminance = GetLuminance(ref Unsafe.Add(ref greyValuesBase, idx), luminanceLevels);
+ Unsafe.Add(ref histogramBase, luminance)--;
+ }
+ }
+
+ private class SlidingWindowInfos
+ {
+ public SlidingWindowInfos(int tileWidth, int tileHeight, int halfTileWidth, int halfTileHeight, int pixeInTile)
+ {
+ this.TileWidth = tileWidth;
+ this.TileHeight = tileHeight;
+ this.HalfTileWidth = halfTileWidth;
+ this.HalfTileHeight = halfTileHeight;
+ this.PixeInTile = pixeInTile;
+ }
+
+ public int TileWidth { get; private set; }
+
+ public int TileHeight { get; private set; }
+
+ public int PixeInTile { get; private set; }
+
+ public int HalfTileWidth { get; private set; }
+
+ public int HalfTileHeight { get; private set; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs
new file mode 100644
index 000000000..aadde2424
--- /dev/null
+++ b/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs
@@ -0,0 +1,106 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Buffers;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using SixLabors.ImageSharp.Advanced;
+using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.ParallelUtils;
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.Memory;
+using SixLabors.Primitives;
+
+namespace SixLabors.ImageSharp.Processing.Processors.Normalization
+{
+ ///
+ /// Applies a global histogram equalization to the image.
+ ///
+ /// The pixel format.
+ internal class GlobalHistogramEqualizationProcessor : HistogramEqualizationProcessor
+ where TPixel : struct, IPixel
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images
+ /// or 65536 for 16-bit grayscale images.
+ ///
+ /// Indicating whether to clip the histogram bins at a specific value.
+ /// Histogram clip limit in percent of the total pixels. Histogram bins which exceed this limit, will be capped at this value.
+ public GlobalHistogramEqualizationProcessor(int luminanceLevels, bool clipHistogram, float clipLimitPercentage)
+ : base(luminanceLevels, clipHistogram, clipLimitPercentage)
+ {
+ }
+
+ ///
+ protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration)
+ {
+ MemoryAllocator memoryAllocator = configuration.MemoryAllocator;
+ int numberOfPixels = source.Width * source.Height;
+ Span pixels = source.GetPixelSpan();
+ var workingRect = new Rectangle(0, 0, source.Width, source.Height);
+
+ using (IMemoryOwner histogramBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean))
+ using (IMemoryOwner cdfBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean))
+ {
+ // Build the histogram of the grayscale levels.
+ ParallelHelper.IterateRows(
+ workingRect,
+ configuration,
+ rows =>
+ {
+ ref int histogramBase = ref MemoryMarshal.GetReference(histogramBuffer.GetSpan());
+ for (int y = rows.Min; y < rows.Max; y++)
+ {
+ ref TPixel pixelBase = ref MemoryMarshal.GetReference(source.GetPixelRowSpan(y));
+
+ for (int x = 0; x < workingRect.Width; x++)
+ {
+ int luminance = GetLuminance(Unsafe.Add(ref pixelBase, x), this.LuminanceLevels);
+ Unsafe.Add(ref histogramBase, luminance)++;
+ }
+ }
+ });
+
+ Span histogram = histogramBuffer.GetSpan();
+ if (this.ClipHistogramEnabled)
+ {
+ this.ClipHistogram(histogram, this.ClipLimitPercentage, numberOfPixels);
+ }
+
+ // Calculate the cumulative distribution function, which will map each input pixel to a new value.
+ int cdfMin = this.CalculateCdf(
+ ref MemoryMarshal.GetReference(cdfBuffer.GetSpan()),
+ ref MemoryMarshal.GetReference(histogram),
+ histogram.Length - 1);
+
+ float numberOfPixelsMinusCdfMin = numberOfPixels - cdfMin;
+
+ // Apply the cdf to each pixel of the image
+ ParallelHelper.IterateRows(
+ workingRect,
+ configuration,
+ rows =>
+ {
+ ref int cdfBase = ref MemoryMarshal.GetReference(cdfBuffer.GetSpan());
+ for (int y = rows.Min; y < rows.Max; y++)
+ {
+ ref TPixel pixelBase = ref MemoryMarshal.GetReference(source.GetPixelRowSpan(y));
+
+ for (int x = 0; x < workingRect.Width; x++)
+ {
+ ref TPixel pixel = ref Unsafe.Add(ref pixelBase, x);
+ int luminance = GetLuminance(pixel, this.LuminanceLevels);
+ float luminanceEqualized = Unsafe.Add(ref cdfBase, luminance) / numberOfPixelsMinusCdfMin;
+ pixel.FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W));
+ }
+ }
+ });
+ }
+ }
+ }
+}
diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs
new file mode 100644
index 000000000..641587c39
--- /dev/null
+++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+namespace SixLabors.ImageSharp.Processing.Processors.Normalization
+{
+ ///
+ /// Enumerates the different types of defined histogram equalization methods.
+ ///
+ public enum HistogramEqualizationMethod : int
+ {
+ ///
+ /// A global histogram equalization.
+ ///
+ Global,
+
+ ///
+ /// Adaptive histogram equalization using a tile interpolation approach.
+ ///
+ AdaptiveTileInterpolation,
+
+ ///
+ /// Adaptive histogram equalization using sliding window. Slower then the tile interpolation mode, but can yield to better results.
+ ///
+ AdaptiveSlidingWindow,
+ }
+}
diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs
new file mode 100644
index 000000000..0d1a37836
--- /dev/null
+++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs
@@ -0,0 +1,43 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+namespace SixLabors.ImageSharp.Processing.Processors.Normalization
+{
+ ///
+ /// Data container providing the different options for the histogram equalization.
+ ///
+ public class HistogramEqualizationOptions
+ {
+ ///
+ /// Gets the default instance.
+ ///
+ public static HistogramEqualizationOptions Default { get; } = new HistogramEqualizationOptions();
+
+ ///
+ /// Gets or sets the histogram equalization method to use. Defaults to global histogram equalization.
+ ///
+ public HistogramEqualizationMethod Method { get; set; } = HistogramEqualizationMethod.Global;
+
+ ///
+ /// Gets or sets the number of different luminance levels. Typical values are 256 for 8-bit grayscale images
+ /// or 65536 for 16-bit grayscale images. Defaults to 256.
+ ///
+ public int LuminanceLevels { get; set; } = 256;
+
+ ///
+ /// Gets or sets a value indicating whether to clip the histogram bins at a specific value. Defaults to false.
+ ///
+ public bool ClipHistogram { get; set; } = false;
+
+ ///
+ /// Gets or sets the histogram clip limit in percent of the total pixels in a tile. Histogram bins which exceed this limit, will be capped at this value.
+ /// Defaults to 0.035f.
+ ///
+ public float ClipLimitPercentage { get; set; } = 0.035f;
+
+ ///
+ /// Gets or sets the number of tiles the image is split into (horizontal and vertically) for the adaptive histogram equalization. Defaults to 10.
+ ///
+ public int Tiles { get; set; } = 10;
+ }
+}
diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs
index 580adc7fe..fd1b6b978 100644
--- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs
+++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs
@@ -2,34 +2,38 @@
// Licensed under the Apache License, Version 2.0.
using System;
-using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices;
-using SixLabors.ImageSharp.Advanced;
-using SixLabors.ImageSharp.Memory;
+using System.Runtime.InteropServices;
using SixLabors.ImageSharp.PixelFormats;
-using SixLabors.Memory;
-using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Normalization
{
///
- /// Applies a global histogram equalization to the image.
+ /// Defines a processor that normalizes the histogram of an image.
///
/// The pixel format.
- internal class HistogramEqualizationProcessor : ImageProcessor
+ internal abstract class HistogramEqualizationProcessor : ImageProcessor
where TPixel : struct, IPixel
{
+ private readonly float luminanceLevelsFloat;
+
///
/// Initializes a new instance of the class.
///
/// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images
/// or 65536 for 16-bit grayscale images.
- public HistogramEqualizationProcessor(int luminanceLevels)
+ /// Indicates, if histogram bins should be clipped.
+ /// Histogram clip limit in percent of the total pixels in the tile. Histogram bins which exceed this limit, will be capped at this value.
+ protected HistogramEqualizationProcessor(int luminanceLevels, bool clipHistogram, float clipLimitPercentage)
{
Guard.MustBeGreaterThan(luminanceLevels, 0, nameof(luminanceLevels));
+ Guard.MustBeGreaterThan(clipLimitPercentage, 0F, nameof(clipLimitPercentage));
this.LuminanceLevels = luminanceLevels;
+ this.luminanceLevelsFloat = luminanceLevels;
+ this.ClipHistogramEnabled = clipHistogram;
+ this.ClipLimitPercentage = clipLimitPercentage;
}
///
@@ -37,77 +41,77 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
///
public int LuminanceLevels { get; }
- ///
- protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration)
+ ///
+ /// Gets a value indicating whether to clip the histogram bins at a specific value.
+ ///
+ public bool ClipHistogramEnabled { get; }
+
+ ///
+ /// Gets the histogram clip limit in percent of the total pixels in the tile. Histogram bins which exceed this limit, will be capped at this value.
+ ///
+ public float ClipLimitPercentage { get; }
+
+ ///
+ /// Calculates the cumulative distribution function.
+ ///
+ /// The reference to the array holding the cdf.
+ /// The reference to the histogram of the input image.
+ /// Index of the maximum of the histogram.
+ /// The first none zero value of the cdf.
+ public int CalculateCdf(ref int cdfBase, ref int histogramBase, int maxIdx)
{
- MemoryAllocator memoryAllocator = configuration.MemoryAllocator;
- int numberOfPixels = source.Width * source.Height;
- Span pixels = source.GetPixelSpan();
+ int histSum = 0;
+ int cdfMin = 0;
+ bool cdfMinFound = false;
- // Build the histogram of the grayscale levels.
- using (IMemoryOwner histogramBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean))
- using (IMemoryOwner cdfBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean))
+ for (int i = 0; i <= maxIdx; i++)
{
- Span histogram = histogramBuffer.GetSpan();
- for (int i = 0; i < pixels.Length; i++)
+ histSum += Unsafe.Add(ref histogramBase, i);
+ if (!cdfMinFound && histSum != 0)
{
- TPixel sourcePixel = pixels[i];
- int luminance = this.GetLuminance(sourcePixel, this.LuminanceLevels);
- histogram[luminance]++;
+ cdfMin = histSum;
+ cdfMinFound = true;
}
- // Calculate the cumulative distribution function, which will map each input pixel to a new value.
- Span cdf = cdfBuffer.GetSpan();
- int cdfMin = this.CalculateCdf(cdf, histogram);
-
- // Apply the cdf to each pixel of the image
- float numberOfPixelsMinusCdfMin = numberOfPixels - cdfMin;
- for (int i = 0; i < pixels.Length; i++)
- {
- TPixel sourcePixel = pixels[i];
-
- int luminance = this.GetLuminance(sourcePixel, this.LuminanceLevels);
- float luminanceEqualized = cdf[luminance] / numberOfPixelsMinusCdfMin;
-
- pixels[i].FromVector4(new Vector4(luminanceEqualized));
- }
+ // Creating the lookup table: subtracting cdf min, so we do not need to do that inside the for loop.
+ Unsafe.Add(ref cdfBase, i) = Math.Max(0, histSum - cdfMin);
}
+
+ return cdfMin;
}
///
- /// Calculates the cumulative distribution function.
+ /// AHE tends to over amplify the contrast in near-constant regions of the image, since the histogram in such regions is highly concentrated.
+ /// Clipping the histogram is meant to reduce this effect, by cutting of histogram bin's which exceed a certain amount and redistribute
+ /// the values over the clip limit to all other bins equally.
///
- /// The array holding the cdf.
- /// The histogram of the input image.
- /// The first none zero value of the cdf.
- private int CalculateCdf(Span cdf, Span histogram)
+ /// The histogram to apply the clipping.
+ /// Histogram clip limit in percent of the total pixels in the tile. Histogram bins which exceed this limit, will be capped at this value.
+ /// The numbers of pixels inside the tile.
+ public void ClipHistogram(Span histogram, float clipLimitPercentage, int pixelCount)
{
- // Calculate the cumulative histogram
- int histSum = 0;
- for (int i = 0; i < histogram.Length; i++)
- {
- histSum += histogram[i];
- cdf[i] = histSum;
- }
+ int clipLimit = (int)MathF.Round(pixelCount * clipLimitPercentage);
+ int sumOverClip = 0;
+ ref int histogramBase = ref MemoryMarshal.GetReference(histogram);
- // Get the first none zero value of the cumulative histogram
- int cdfMin = 0;
for (int i = 0; i < histogram.Length; i++)
{
- if (cdf[i] != 0)
+ ref int histogramLevel = ref Unsafe.Add(ref histogramBase, i);
+ if (histogramLevel > clipLimit)
{
- cdfMin = cdf[i];
- break;
+ sumOverClip += histogramLevel - clipLimit;
+ histogramLevel = clipLimit;
}
}
- // Creating the lookup table: subtracting cdf min, so we do not need to do that inside the for loop
- for (int i = 0; i < histogram.Length; i++)
+ int addToEachBin = sumOverClip > 0 ? (int)MathF.Floor(sumOverClip / this.luminanceLevelsFloat) : 0;
+ if (addToEachBin > 0)
{
- cdf[i] = Math.Max(0, cdf[i] - cdfMin);
+ for (int i = 0; i < histogram.Length; i++)
+ {
+ Unsafe.Add(ref histogramBase, i) += addToEachBin;
+ }
}
-
- return cdfMin;
}
///
@@ -116,13 +120,19 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
/// The pixel to get the luminance from
/// The number of luminance levels (256 for 8 bit, 65536 for 16 bit grayscale images)
[MethodImpl(InliningOptions.ShortMethod)]
- private int GetLuminance(TPixel sourcePixel, int luminanceLevels)
+ public static int GetLuminance(TPixel sourcePixel, int luminanceLevels)
{
- // Convert to grayscale using ITU-R Recommendation BT.709
var vector = sourcePixel.ToVector4();
- int luminance = Convert.ToInt32(((.2126F * vector.X) + (.7152F * vector.Y) + (.0722F * vector.Y)) * (luminanceLevels - 1));
-
- return luminance;
+ return GetLuminance(ref vector, luminanceLevels);
}
+
+ ///
+ /// Convert the pixel values to grayscale using ITU-R Recommendation BT.709.
+ ///
+ /// The vector to get the luminance from
+ /// The number of luminance levels (256 for 8 bit, 65536 for 16 bit grayscale images)
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public static int GetLuminance(ref Vector4 vector, int luminanceLevels)
+ => (int)MathF.Round(((.2126F * vector.X) + (.7152F * vector.Y) + (.0722F * vector.Y)) * (luminanceLevels - 1));
}
-}
+}
\ No newline at end of file
diff --git a/tests/ImageSharp.Benchmarks/General/BasicMath/Round.cs b/tests/ImageSharp.Benchmarks/General/BasicMath/Round.cs
new file mode 100644
index 000000000..2c18b2972
--- /dev/null
+++ b/tests/ImageSharp.Benchmarks/General/BasicMath/Round.cs
@@ -0,0 +1,22 @@
+using System;
+using BenchmarkDotNet.Attributes;
+
+namespace SixLabors.ImageSharp.Benchmarks.General.BasicMath
+{
+ public class Round
+ {
+ private const float input = .51F;
+
+ [Benchmark]
+ public int ConvertTo() => Convert.ToInt32(input);
+
+ [Benchmark]
+ public int MathRound() => (int)Math.Round(input);
+
+ // Results 20th Jan 2019
+ // Method | Mean | Error | StdDev | Median |
+ //---------- |----------:|----------:|----------:|----------:|
+ // ConvertTo | 3.1967 ns | 0.1234 ns | 0.2129 ns | 3.2340 ns |
+ // MathRound | 0.0528 ns | 0.0374 ns | 0.1079 ns | 0.0000 ns |
+ }
+}
diff --git a/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs b/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs
index 1595ed32c..84d592bd9 100644
--- a/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs
+++ b/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs
@@ -3,12 +3,16 @@
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
+using SixLabors.ImageSharp.Processing.Processors.Normalization;
+using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Processing.Normalization
{
public class HistogramEqualizationTests
{
+ private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.0456F);
+
[Theory]
[InlineData(256)]
[InlineData(65536)]
@@ -27,18 +31,19 @@ namespace SixLabors.ImageSharp.Tests.Processing.Normalization
70, 87, 69, 68, 65, 73, 78, 90
};
- var image = new Image(8, 8);
- for (int y = 0; y < 8; y++)
+ using (var image = new Image(8, 8))
{
- for (int x = 0; x < 8; x++)
+ for (int y = 0; y < 8; y++)
{
- byte luminance = pixels[y * 8 + x];
- image[x, y] = new Rgba32(luminance, luminance, luminance);
+ for (int x = 0; x < 8; x++)
+ {
+ byte luminance = pixels[(y * 8) + x];
+ image[x, y] = new Rgba32(luminance, luminance, luminance);
+ }
}
- }
- byte[] expected = new byte[]
- {
+ byte[] expected = new byte[]
+ {
0, 12, 53, 32, 146, 53, 174, 53,
57, 32, 12, 227, 219, 202, 32, 154,
65, 85, 93, 239, 251, 227, 65, 158,
@@ -47,22 +52,66 @@ namespace SixLabors.ImageSharp.Tests.Processing.Normalization
117, 190, 36, 190, 178, 93, 20, 170,
130, 202, 73, 20, 12, 53, 85, 194,
146, 206, 130, 117, 85, 166, 182, 215
- };
+ };
- // Act
- image.Mutate(x => x.HistogramEqualization(luminanceLevels));
+ // Act
+ image.Mutate(x => x.HistogramEqualization(new HistogramEqualizationOptions()
+ {
+ LuminanceLevels = luminanceLevels
+ }));
- // Assert
- for (int y = 0; y < 8; y++)
- {
- for (int x = 0; x < 8; x++)
+ // Assert
+ for (int y = 0; y < 8; y++)
{
- Rgba32 actual = image[x, y];
- Assert.Equal(expected[y * 8 + x], actual.R);
- Assert.Equal(expected[y * 8 + x], actual.G);
- Assert.Equal(expected[y * 8 + x], actual.B);
+ for (int x = 0; x < 8; x++)
+ {
+ Rgba32 actual = image[x, y];
+ Assert.Equal(expected[(y * 8) + x], actual.R);
+ Assert.Equal(expected[(y * 8) + x], actual.G);
+ Assert.Equal(expected[(y * 8) + x], actual.B);
+ }
}
}
}
+
+ [Theory]
+ [WithFile(TestImages.Jpeg.Baseline.LowContrast, PixelTypes.Rgba32)]
+ public void Adaptive_SlidingWindow_15Tiles_WithClipping(TestImageProvider provider)
+ where TPixel : struct, IPixel
+ {
+ using (Image image = provider.GetImage())
+ {
+ var options = new HistogramEqualizationOptions()
+ {
+ Method = HistogramEqualizationMethod.AdaptiveSlidingWindow,
+ LuminanceLevels = 256,
+ ClipHistogram = true,
+ Tiles = 15
+ };
+ image.Mutate(x => x.HistogramEqualization(options));
+ image.DebugSave(provider);
+ image.CompareToReferenceOutput(ValidatorComparer, provider);
+ }
+ }
+
+ [Theory]
+ [WithFile(TestImages.Jpeg.Baseline.LowContrast, PixelTypes.Rgba32)]
+ public void Adaptive_TileInterpolation_10Tiles_WithClipping(TestImageProvider provider)
+ where TPixel : struct, IPixel
+ {
+ using (Image image = provider.GetImage())
+ {
+ var options = new HistogramEqualizationOptions()
+ {
+ Method = HistogramEqualizationMethod.AdaptiveTileInterpolation,
+ LuminanceLevels = 256,
+ ClipHistogram = true,
+ Tiles = 10
+ };
+ image.Mutate(x => x.HistogramEqualization(options));
+ image.DebugSave(provider);
+ image.CompareToReferenceOutput(ValidatorComparer, provider);
+ }
+ }
}
}
diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs
index 8b2fd2e7f..23bebd621 100644
--- a/tests/ImageSharp.Tests/TestImages.cs
+++ b/tests/ImageSharp.Tests/TestImages.cs
@@ -141,6 +141,7 @@ namespace SixLabors.ImageSharp.Tests
public const string Testorig420 = "Jpg/baseline/testorig.jpg";
public const string MultiScanBaselineCMYK = "Jpg/baseline/MultiScanBaselineCMYK.jpg";
public const string Ratio1x1 = "Jpg/baseline/ratio-1x1.jpg";
+ public const string LowContrast = "Jpg/baseline/AsianCarvingLowContrast.jpg";
public const string Testorig12bit = "Jpg/baseline/testorig12.jpg";
public const string YcckSubsample1222 = "Jpg/baseline/ycck-subsample-1222.jpg";
diff --git a/tests/Images/External b/tests/Images/External
index 8693e2fd4..1ca515499 160000
--- a/tests/Images/External
+++ b/tests/Images/External
@@ -1 +1 @@
-Subproject commit 8693e2fd4577a9ac1a749da8db564095b5a05389
+Subproject commit 1ca515499663e8b0b7c924a49b8d212f7447bdb0
diff --git a/tests/Images/Input/Jpg/baseline/AsianCarvingLowContrast.jpg b/tests/Images/Input/Jpg/baseline/AsianCarvingLowContrast.jpg
new file mode 100644
index 000000000..d16a2864d
--- /dev/null
+++ b/tests/Images/Input/Jpg/baseline/AsianCarvingLowContrast.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:50f6359d228079ec5e6ead84046119eda84136026c1651c753e6d270405cd4b7
+size 187216