diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs index b05d10f455..3df1877339 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; using System.Collections.Generic; using System.Numerics; using SixLabors.ImageSharp.Advanced; @@ -8,9 +11,18 @@ using SixLabors.Primitives; namespace SixLabors.ImageSharp.Processing.Processors.Normalization { + /// + /// Applies an adaptive histogram equalization to the image. + /// + /// 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. public AdaptiveHistEqualizationProcessor(int luminanceLevels) : base(luminanceLevels) { @@ -24,59 +36,46 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization Span pixels = source.GetPixelSpan(); int gridSize = 64; + int contrastLimit = 5; int halfGridSize = gridSize / 2; int[] histogram = new int[this.LuminanceLevels]; int[] histogramCopy = new int[this.LuminanceLevels]; int[] cdf = new int[this.LuminanceLevels]; using (Buffer2D targetPixels = configuration.MemoryAllocator.Allocate2D(source.Width, source.Height)) { - for (int y = halfGridSize; y < source.Height - halfGridSize; y++) + for (int x = halfGridSize; x < source.Width - halfGridSize; x++) { + this.CleanArray(histogram); + this.CleanArray(cdf); + var pixelsGrid = new List(); - int x = halfGridSize; - for (int dy = y - halfGridSize; dy < (y + halfGridSize); dy++) + for (int dy = 0; dy < gridSize; dy++) { - Span rowSpan = source.GetPixelRowSpan(dy); - for (int dx = x - halfGridSize; dx < (x + halfGridSize); dx++) - { - pixelsGrid.Add(rowSpan[dx]); - } + Span rowSpan = source.GetPixelRowSpan(dy).Slice(start: x - halfGridSize, length: gridSize); + pixelsGrid.AddRange(rowSpan.ToArray()); } - this.CleanArray(histogram); - this.CleanArray(cdf); this.CalcHistogram(pixelsGrid, histogram, this.LuminanceLevels); - for (x = halfGridSize + 1; x < source.Width - halfGridSize; x++) + for (int y = halfGridSize + 1; y < source.Height - halfGridSize; y++) { - // remove left most column from the histogram - var leftMostColumnPixels = new List(); - int dx = x - halfGridSize - 1; - for (int dy = y - halfGridSize; dy < (y + halfGridSize); dy++) - { - leftMostColumnPixels.Add(pixels[(dy * source.Width) + dx]); - } - - this.RemovePixelsFromHistogram(leftMostColumnPixels, histogram, this.LuminanceLevels); - - // add right column from the histogram - dx = x + halfGridSize - 1; - var rightMostColumnPixels = new List(); - for (int dy = y - halfGridSize; dy < (y + halfGridSize); dy++) - { - rightMostColumnPixels.Add(pixels[(dy * source.Width) + dx]); - } - - this.AddPixelsTooHistogram(rightMostColumnPixels, histogram, this.LuminanceLevels); + // Remove top most row from the histogram + Span rowSpan = source.GetPixelRowSpan(y - halfGridSize - 1).Slice(start: x - halfGridSize, length: gridSize); + this.RemovePixelsFromHistogram(rowSpan, histogram, this.LuminanceLevels); + + // Add new bottom row to the histogram + rowSpan = source.GetPixelRowSpan(y + halfGridSize - 1).Slice(start: x - halfGridSize, length: gridSize); + this.AddPixelsTooHistogram(rowSpan, histogram, this.LuminanceLevels); + // Clipping the histogram, but doing it on a copy to keep the original un-clipped values histogram.AsSpan().CopyTo(histogramCopy); - this.ClipHistogram(histogramCopy, 5, gridSize); + this.ClipHistogram(histogramCopy, gridSize, contrastLimit); int cdfMin = this.CalculateCdf(cdf, histogramCopy); double numberOfPixelsMinusCdfMin = (double)(pixelsGrid.Count - cdfMin); + // Map the current pixel to the new equalized value int luminance = this.GetLuminance(pixels[(y * source.Width) + x], this.LuminanceLevels); double luminanceEqualized = cdf[luminance] / numberOfPixelsMinusCdfMin; - targetPixels[x, y].PackFromVector4(new Vector4((float)luminanceEqualized)); this.CleanArray(cdf); @@ -87,7 +86,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization } } - private void ClipHistogram(Span histogram, int contrastLimit, int gridSize) + /// + /// 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 histogram to apply the clipping + /// The grid size of the AHE + /// The contrast limit. Defaults to 5. + /// The number of pixels redistributed to all other bins + private int ClipHistogram(Span histogram, int gridSize, int contrastLimit = 5) { int clipLimit = (contrastLimit * (gridSize * gridSize)) / this.LuminanceLevels; @@ -106,20 +114,29 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization { histogram[i] += addToEachBin; } + + // The redistribution will push some bins over the clip limit again. + // Re-Applying the clipping until this effect no longer occurs. + while (addToEachBin > 1) + { + addToEachBin = this.ClipHistogram(histogram, gridSize, contrastLimit); + } + + return addToEachBin; } - private void AddPixelsTooHistogram(List greyValues, Span histogram, int luminanceLevels) + private void AddPixelsTooHistogram(Span greyValues, Span histogram, int luminanceLevels) { - for (int i = 0; i < greyValues.Count; i++) + for (int i = 0; i < greyValues.Length; i++) { int luminance = this.GetLuminance(greyValues[i], luminanceLevels); histogram[luminance]++; } } - private void RemovePixelsFromHistogram(List greyValues, Span histogram, int luminanceLevels) + private void RemovePixelsFromHistogram(Span greyValues, Span histogram, int luminanceLevels) { - for (int i = 0; i < greyValues.Count; i++) + for (int i = 0; i < greyValues.Length; i++) { int luminance = this.GetLuminance(greyValues[i], luminanceLevels); histogram[luminance]--;