mirror of https://github.com/SixLabors/ImageSharp
4 changed files with 350 additions and 147 deletions
@ -0,0 +1,229 @@ |
|||||
|
// Copyright (c) Six Labors and contributors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Numerics; |
||||
|
using SixLabors.ImageSharp.Advanced; |
||||
|
using SixLabors.ImageSharp.Memory; |
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
using SixLabors.Memory; |
||||
|
using SixLabors.Primitives; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Processing.Processors.Normalization |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Applies an adaptive histogram equalization to the image.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
|
internal class AdaptiveHistEqualizationSWProcessor<TPixel> : HistogramEqualizationProcessor<TPixel> |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="AdaptiveHistEqualizationSWProcessor{TPixel}"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="luminanceLevels">The number of different luminance levels. Typical values are 256 for 8-bit grayscale images
|
||||
|
/// or 65536 for 16-bit grayscale images.</param>
|
||||
|
/// <param name="clipHistogram">Indicating whether to clip the histogram bins at a specific value.</param>
|
||||
|
/// <param name="clipLimitPercentage">Histogram clip limit in percent of the total pixels in the grid. Histogram bins which exceed this limit, will be capped at this value.</param>
|
||||
|
/// <param name="gridSize">The grid size of the adaptive histogram equalization. Minimum value is 4.</param>
|
||||
|
public AdaptiveHistEqualizationSWProcessor(int luminanceLevels, bool clipHistogram, float clipLimitPercentage, int gridSize) |
||||
|
: base(luminanceLevels, clipHistogram, clipLimitPercentage) |
||||
|
{ |
||||
|
Guard.MustBeGreaterThanOrEqualTo(gridSize, 4, nameof(gridSize)); |
||||
|
|
||||
|
this.GridSize = gridSize; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the size of the grid for the adaptive histogram equalization.
|
||||
|
/// </summary>
|
||||
|
public int GridSize { get; } |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration) |
||||
|
{ |
||||
|
MemoryAllocator memoryAllocator = configuration.MemoryAllocator; |
||||
|
int numberOfPixels = source.Width * source.Height; |
||||
|
Span<TPixel> pixels = source.GetPixelSpan(); |
||||
|
|
||||
|
int pixelsInGrid = this.GridSize * this.GridSize; |
||||
|
int halfGridSize = this.GridSize / 2; |
||||
|
using (Buffer2D<TPixel> targetPixels = configuration.MemoryAllocator.Allocate2D<TPixel>(source.Width, source.Height)) |
||||
|
{ |
||||
|
ParallelFor.WithConfiguration( |
||||
|
0, |
||||
|
source.Width, |
||||
|
configuration, |
||||
|
x => |
||||
|
{ |
||||
|
using (System.Buffers.IMemoryOwner<int> histogramBuffer = memoryAllocator.Allocate<int>(this.LuminanceLevels, AllocationOptions.Clean)) |
||||
|
using (System.Buffers.IMemoryOwner<int> histogramBufferCopy = memoryAllocator.Allocate<int>(this.LuminanceLevels, AllocationOptions.Clean)) |
||||
|
using (System.Buffers.IMemoryOwner<int> cdfBuffer = memoryAllocator.Allocate<int>(this.LuminanceLevels, AllocationOptions.Clean)) |
||||
|
{ |
||||
|
Span<int> histogram = histogramBuffer.GetSpan(); |
||||
|
Span<int> histogramCopy = histogramBufferCopy.GetSpan(); |
||||
|
Span<int> cdf = cdfBuffer.GetSpan(); |
||||
|
int maxHistIdx = 0; |
||||
|
|
||||
|
// Build the histogram of grayscale values for the current grid.
|
||||
|
for (int dy = -halfGridSize; dy < halfGridSize; dy++) |
||||
|
{ |
||||
|
Span<TPixel> rowSpan = this.GetPixelRow(source, (int)x - halfGridSize, dy, this.GridSize); |
||||
|
int maxIdx = this.AddPixelsToHistogram(rowSpan, histogram, this.LuminanceLevels); |
||||
|
if (maxIdx > maxHistIdx) |
||||
|
{ |
||||
|
maxHistIdx = maxIdx; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
for (int y = 0; y < source.Height; 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.Slice(0, maxHistIdx).CopyTo(histogramCopy); |
||||
|
this.ClipHistogram(histogramCopy, this.ClipLimitPercentage, pixelsInGrid); |
||||
|
} |
||||
|
|
||||
|
// Calculate the cumulative distribution function, which will map each input pixel in the current grid to a new value.
|
||||
|
int cdfMin = this.ClipHistogramEnabled ? this.CalculateCdf(cdf, histogramCopy, maxHistIdx) : this.CalculateCdf(cdf, histogram, maxHistIdx); |
||||
|
float numberOfPixelsMinusCdfMin = pixelsInGrid - cdfMin; |
||||
|
|
||||
|
// Map the current pixel to the new equalized value
|
||||
|
int luminance = this.GetLuminance(source[x, y], this.LuminanceLevels); |
||||
|
float luminanceEqualized = cdf[luminance] / numberOfPixelsMinusCdfMin; |
||||
|
targetPixels[x, y].PackFromVector4(new Vector4(luminanceEqualized)); |
||||
|
|
||||
|
// Remove top most row from the histogram, mirroring rows which exceeds the borders.
|
||||
|
Span<TPixel> rowSpan = this.GetPixelRow(source, x - halfGridSize, y - halfGridSize, this.GridSize); |
||||
|
maxHistIdx = this.RemovePixelsFromHistogram(rowSpan, histogram, this.LuminanceLevels, maxHistIdx); |
||||
|
|
||||
|
// Add new bottom row to the histogram, mirroring rows which exceeds the borders.
|
||||
|
rowSpan = this.GetPixelRow(source, x - halfGridSize, y + halfGridSize, this.GridSize); |
||||
|
int maxIdx = this.AddPixelsToHistogram(rowSpan, histogram, this.LuminanceLevels); |
||||
|
if (maxIdx > maxHistIdx) |
||||
|
{ |
||||
|
maxHistIdx = maxIdx; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
Buffer2D<TPixel>.SwapOrCopyContent(source.PixelBuffer, targetPixels); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Get the a pixel row at a given position with a length of the grid size. Mirrors pixels which exceeds the edges.
|
||||
|
/// </summary>
|
||||
|
/// <param name="source">The source image.</param>
|
||||
|
/// <param name="x">The x position.</param>
|
||||
|
/// <param name="y">The y position.</param>
|
||||
|
/// <param name="gridSize">The grid size.</param>
|
||||
|
/// <returns>A pixel row of the length of the grid size.</returns>
|
||||
|
private Span<TPixel> GetPixelRow(ImageFrame<TPixel> source, int x, int y, int gridSize) |
||||
|
{ |
||||
|
if (y < 0) |
||||
|
{ |
||||
|
y = Math.Abs(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) |
||||
|
{ |
||||
|
var rowPixels = new TPixel[gridSize]; |
||||
|
int idx = 0; |
||||
|
for (int dx = x; dx < x + gridSize; dx++) |
||||
|
{ |
||||
|
rowPixels[idx] = source[Math.Abs(dx), y]; |
||||
|
idx++; |
||||
|
} |
||||
|
|
||||
|
return rowPixels; |
||||
|
} |
||||
|
else if (x + gridSize > source.Width) |
||||
|
{ |
||||
|
var rowPixels = new TPixel[gridSize]; |
||||
|
int idx = 0; |
||||
|
for (int dx = x; dx < x + gridSize; dx++) |
||||
|
{ |
||||
|
if (dx >= source.Width) |
||||
|
{ |
||||
|
int diff = dx - source.Width; |
||||
|
rowPixels[idx] = source[dx - diff - 1, y]; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
rowPixels[idx] = source[dx, y]; |
||||
|
} |
||||
|
|
||||
|
idx++; |
||||
|
} |
||||
|
|
||||
|
return rowPixels; |
||||
|
} |
||||
|
|
||||
|
return source.GetPixelRowSpan(y).Slice(start: x, length: gridSize); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Adds a row of grey values to the histogram.
|
||||
|
/// </summary>
|
||||
|
/// <param name="greyValues">The grey values to add</param>
|
||||
|
/// <param name="histogram">The histogram</param>
|
||||
|
/// <param name="luminanceLevels">The number of different luminance levels.</param>
|
||||
|
/// <returns>The maximum index where a value was changed.</returns>
|
||||
|
private int AddPixelsToHistogram(Span<TPixel> greyValues, Span<int> histogram, int luminanceLevels) |
||||
|
{ |
||||
|
int maxIdx = 0; |
||||
|
for (int idx = 0; idx < greyValues.Length; idx++) |
||||
|
{ |
||||
|
int luminance = this.GetLuminance(greyValues[idx], luminanceLevels); |
||||
|
histogram[luminance]++; |
||||
|
if (luminance > maxIdx) |
||||
|
{ |
||||
|
maxIdx = luminance; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return maxIdx; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Removes a row of grey values from the histogram.
|
||||
|
/// </summary>
|
||||
|
/// <param name="greyValues">The grey values to remove</param>
|
||||
|
/// <param name="histogram">The histogram</param>
|
||||
|
/// <param name="luminanceLevels">The number of different luminance levels.</param>
|
||||
|
/// <param name="maxHistIdx">The current maximum index of the histogram.</param>
|
||||
|
/// <returns>The (maybe changed) maximum index of the histogram.</returns>
|
||||
|
private int RemovePixelsFromHistogram(Span<TPixel> greyValues, Span<int> histogram, int luminanceLevels, int maxHistIdx) |
||||
|
{ |
||||
|
for (int idx = 0; idx < greyValues.Length; idx++) |
||||
|
{ |
||||
|
int luminance = this.GetLuminance(greyValues[idx], luminanceLevels); |
||||
|
histogram[luminance]--; |
||||
|
|
||||
|
// If the histogram at the maximum index has changed to 0, search for the next smaller value.
|
||||
|
if (luminance == maxHistIdx && histogram[luminance] == 0) |
||||
|
{ |
||||
|
for (int j = luminance; j >= 0; j--) |
||||
|
{ |
||||
|
maxHistIdx = j; |
||||
|
if (histogram[j] != 0) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return maxHistIdx; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue