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]--;