Browse Source

refactor HistogramEqualization

af/merge-core
Anton Firszov 7 years ago
parent
commit
394174bb53
  1. 44
      src/ImageSharp/Processing/HistogramEqualizationExtension.cs
  2. 526
      src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs
  3. 546
      src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor{TPixel}.cs
  4. 371
      src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs
  5. 390
      src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor{TPixel}.cs
  6. 99
      src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs
  7. 107
      src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs
  8. 2
      src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs
  9. 134
      src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs
  10. 139
      src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor{TPixel}.cs
  11. 4
      tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs
  12. 3
      tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs

44
src/ImageSharp/Processing/HistogramEqualizationExtension.cs

@ -1,7 +1,6 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Normalization;
namespace SixLabors.ImageSharp.Processing
@ -14,49 +13,20 @@ namespace SixLabors.ImageSharp.Processing
/// <summary>
/// Equalizes the histogram of an image to increases the contrast.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image this method extends.</param>
/// <returns>The <see cref="Image{TPixel}"/>.</returns>
public static IImageProcessingContext<TPixel> HistogramEqualization<TPixel>(this IImageProcessingContext<TPixel> source)
where TPixel : struct, IPixel<TPixel>
=> HistogramEqualization(source, HistogramEqualizationOptions.Default);
public static IImageProcessingContext HistogramEqualization(this IImageProcessingContext source) =>
HistogramEqualization(source, HistogramEqualizationOptions.Default);
/// <summary>
/// Equalizes the histogram of an image to increases the contrast.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image this method extends.</param>
/// <param name="options">The histogram equalization options to use.</param>
/// <returns>The <see cref="Image{TPixel}"/>.</returns>
public static IImageProcessingContext<TPixel> HistogramEqualization<TPixel>(this IImageProcessingContext<TPixel> source, HistogramEqualizationOptions options)
where TPixel : struct, IPixel<TPixel>
=> source.ApplyProcessor(GetProcessor<TPixel>(options));
private static HistogramEqualizationProcessor<TPixel> GetProcessor<TPixel>(HistogramEqualizationOptions options)
where TPixel : struct, IPixel<TPixel>
{
HistogramEqualizationProcessor<TPixel> processor;
switch (options.Method)
{
case HistogramEqualizationMethod.Global:
processor = new GlobalHistogramEqualizationProcessor<TPixel>(options.LuminanceLevels, options.ClipHistogram, options.ClipLimitPercentage);
break;
case HistogramEqualizationMethod.AdaptiveTileInterpolation:
processor = new AdaptiveHistEqualizationProcessor<TPixel>(options.LuminanceLevels, options.ClipHistogram, options.ClipLimitPercentage, options.Tiles);
break;
case HistogramEqualizationMethod.AdaptiveSlidingWindow:
processor = new AdaptiveHistEqualizationSWProcessor<TPixel>(options.LuminanceLevels, options.ClipHistogram, options.ClipLimitPercentage, options.Tiles);
break;
default:
processor = new GlobalHistogramEqualizationProcessor<TPixel>(options.LuminanceLevels, options.ClipHistogram, options.ClipLimitPercentage);
break;
}
return processor;
}
public static IImageProcessingContext HistogramEqualization(
this IImageProcessingContext source,
HistogramEqualizationOptions options) =>
source.ApplyProcessor(HistogramEqualizationProcessor.FromOptions(options));
}
}
}

526
src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs

@ -1,545 +1,37 @@
// 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
{
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal class AdaptiveHistEqualizationProcessor<TPixel> : HistogramEqualizationProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
internal class AdaptiveHistEqualizationProcessor : HistogramEqualizationProcessor
{
/// <summary>
/// Initializes a new instance of the <see cref="AdaptiveHistEqualizationProcessor{TPixel}"/> class.
/// Initializes a new instance of the <see cref="AdaptiveHistEqualizationProcessor"/> 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 tile. Histogram bins which exceed this limit, will be capped at this value.</param>
/// <param name="tiles">The number of tiles the image is split into (horizontal and vertically). Minimum value is 2. Maximum value is 100.</param>
public AdaptiveHistEqualizationProcessor(int luminanceLevels, bool clipHistogram, float clipLimitPercentage, int tiles)
/// <param name="numberOfTiles">The number of tiles the image is split into (horizontal and vertically). Minimum value is 2. Maximum value is 100.</param>
public AdaptiveHistEqualizationProcessor(int luminanceLevels, bool clipHistogram, float clipLimitPercentage, int numberOfTiles)
: base(luminanceLevels, clipHistogram, clipLimitPercentage)
{
Guard.MustBeGreaterThanOrEqualTo(tiles, 2, nameof(tiles));
Guard.MustBeLessThanOrEqualTo(tiles, 100, nameof(tiles));
this.Tiles = tiles;
this.NumberOfTiles = numberOfTiles;
}
/// <summary>
/// Gets the number of tiles the image is split into (horizontal and vertically) for the adaptive histogram equalization.
/// </summary>
private int Tiles { get; }
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> 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);
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="pixelsBase">The output pixels base reference.</param>
/// <param name="cdfData">The lookup table to remap the grey values.</param>
/// <param name="sourceWidth">The source image width.</param>
/// <param name="cdfX">The x-position in the CDF lookup map.</param>
/// <param name="cdfY">The y-position in the CDF lookup map.</param>
/// <param name="xStart">X start position.</param>
/// <param name="xEnd">X end position.</param>
/// <param name="yStart">Y start position.</param>
/// <param name="yEnd">Y end position.</param>
/// <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>
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));
}
}
}
/// <summary>
/// Processes a border column of the image which is half the size of the tile width.
/// </summary>
/// <param name="pixelBase">The output pixels reference.</param>
/// <param name="cdfData">The pre-computed lookup tables to remap the grey values for each tiles.</param>
/// <param name="cdfX">The X index of the lookup table to use.</param>
/// <param name="sourceWidth">The source image width.</param>
/// <param name="sourceHeight">The source image height.</param>
/// <param name="tileWidth">The width of a tile.</param>
/// <param name="tileHeight">The height of a tile.</param>
/// <param name="xStart">X start position in the image.</param>
/// <param name="xEnd">X end position of the image.</param>
/// <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>
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++;
}
}
public int NumberOfTiles { get; }
/// <summary>
/// Processes a border row of the image which is half of the size of the tile height.
/// </summary>
/// <param name="pixelBase">The output pixels base reference.</param>
/// <param name="cdfData">The pre-computed lookup tables to remap the grey values for each tiles.</param>
/// <param name="cdfY">The Y index of the lookup table to use.</param>
/// <param name="sourceWidth">The source image width.</param>
/// <param name="tileWidth">The width of a tile.</param>
/// <param name="yStart">Y start position in the image.</param>
/// <param name="yEnd">Y end position of the image.</param>
/// <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>
private static void ProcessBorderRow(
ref TPixel pixelBase,
CdfTileData cdfData,
int cdfY,
int sourceWidth,
int tileWidth,
int yStart,
int yEnd,
int luminanceLevels)
/// <inheritdoc />
public override IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>()
{
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++;
}
}
/// <summary>
/// Bilinear interpolation between four adjacent tiles.
/// </summary>
/// <param name="sourcePixel">The pixel to remap the grey value from.</param>
/// <param name="cdfData">The pre-computed lookup tables to remap the grey values for each tiles.</param>
/// <param name="tileCountX">The number of tiles in the x-direction.</param>
/// <param name="tileCountY">The number of tiles in the y-direction.</param>
/// <param name="tileX">X position inside the tile.</param>
/// <param name="tileY">Y position inside the tile.</param>
/// <param name="cdfX">X index of the top left lookup table to use.</param>
/// <param name="cdfY">Y index of the top left lookup table to use.</param>
/// <param name="tileWidth">Width of one tile in pixels.</param>
/// <param name="tileHeight">Height of one tile in pixels.</param>
/// <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>
/// <returns>A re-mapped grey value.</returns>
[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);
}
/// <summary>
/// Linear interpolation between two tiles.
/// </summary>
/// <param name="sourcePixel">The pixel to remap the grey value from.</param>
/// <param name="cdfData">The CDF lookup map.</param>
/// <param name="tileX1">X position inside the first tile.</param>
/// <param name="tileY1">Y position inside the first tile.</param>
/// <param name="tileX2">X position inside the second tile.</param>
/// <param name="tileY2">Y position inside the second tile.</param>
/// <param name="tilePos">Position inside the tile.</param>
/// <param name="tileWidth">Width of the tile.</param>
/// <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>
/// <returns>A re-mapped grey value.</returns>
[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);
}
/// <summary>
/// Bilinear interpolation between four tiles.
/// </summary>
/// <param name="tx">The interpolation value in x direction in the range of [0, 1].</param>
/// <param name="ty">The interpolation value in y direction in the range of [0, 1].</param>
/// <param name="lt">Luminance from top left tile.</param>
/// <param name="rt">Luminance from right top tile.</param>
/// <param name="lb">Luminance from left bottom tile.</param>
/// <param name="rb">Luminance from right bottom tile.</param>
/// <returns>Interpolated Luminance.</returns>
[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);
/// <summary>
/// Linear interpolation between two grey values.
/// </summary>
/// <param name="left">The left value.</param>
/// <param name="right">The right value.</param>
/// <param name="t">The interpolation value between the two values in the range of [0, 1].</param>
/// <returns>The interpolated value.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
private static float LinearInterpolation(float left, float right, float t)
=> left + ((right - left) * t);
/// <summary>
/// Contains the results of the cumulative distribution function for all tiles.
/// </summary>
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<int> cdfMinBuffer2D;
// Used for storing the LUT for each CDF entry.
private readonly Buffer2D<int> 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<int>(tileCountX, tileCountY);
this.cdfLutBuffer2D = this.memoryAllocator.Allocate2D<int>(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<TPixel> source, HistogramEqualizationProcessor<TPixel> 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<int> histogramBuffer = this.memoryAllocator.Allocate<int>(luminanceLevels))
{
Span<int> 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<int> GetCdfLutSpan(int tileX, int tileY) => this.cdfLutBuffer2D.GetRowSpan(tileY).Slice(tileX * this.luminanceLevels, this.luminanceLevels);
/// <summary>
/// Remaps the grey value with the cdf.
/// </summary>
/// <param name="tilesX">The tiles x-position.</param>
/// <param name="tilesY">The tiles y-position.</param>
/// <param name="luminance">The original luminance.</param>
/// <returns>The remapped luminance.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public float RemapGreyValue(int tilesX, int tilesY, int luminance)
{
int cdfMin = this.cdfMinBuffer2D[tilesX, tilesY];
Span<int> 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();
}
return new AdaptiveHistEqualizationProcessor<TPixel>(this.LuminanceLevels, this.ClipHistogram, this.ClipLimitPercentage, this.NumberOfTiles);
}
}
}

546
src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor{TPixel}.cs

@ -0,0 +1,546 @@
// 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
{
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal class AdaptiveHistEqualizationProcessor<TPixel> : HistogramEqualizationProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// Initializes a new instance of the <see cref="AdaptiveHistEqualizationProcessor{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 tile. Histogram bins which exceed this limit, will be capped at this value.</param>
/// <param name="tiles">The number of tiles the image is split into (horizontal and vertically). Minimum value is 2. Maximum value is 100.</param>
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;
}
/// <summary>
/// Gets the number of tiles the image is split into (horizontal and vertically) for the adaptive histogram equalization.
/// </summary>
private int Tiles { get; }
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> 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);
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="pixelsBase">The output pixels base reference.</param>
/// <param name="cdfData">The lookup table to remap the grey values.</param>
/// <param name="sourceWidth">The source image width.</param>
/// <param name="cdfX">The x-position in the CDF lookup map.</param>
/// <param name="cdfY">The y-position in the CDF lookup map.</param>
/// <param name="xStart">X start position.</param>
/// <param name="xEnd">X end position.</param>
/// <param name="yStart">Y start position.</param>
/// <param name="yEnd">Y end position.</param>
/// <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>
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));
}
}
}
/// <summary>
/// Processes a border column of the image which is half the size of the tile width.
/// </summary>
/// <param name="pixelBase">The output pixels reference.</param>
/// <param name="cdfData">The pre-computed lookup tables to remap the grey values for each tiles.</param>
/// <param name="cdfX">The X index of the lookup table to use.</param>
/// <param name="sourceWidth">The source image width.</param>
/// <param name="sourceHeight">The source image height.</param>
/// <param name="tileWidth">The width of a tile.</param>
/// <param name="tileHeight">The height of a tile.</param>
/// <param name="xStart">X start position in the image.</param>
/// <param name="xEnd">X end position of the image.</param>
/// <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>
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++;
}
}
/// <summary>
/// Processes a border row of the image which is half of the size of the tile height.
/// </summary>
/// <param name="pixelBase">The output pixels base reference.</param>
/// <param name="cdfData">The pre-computed lookup tables to remap the grey values for each tiles.</param>
/// <param name="cdfY">The Y index of the lookup table to use.</param>
/// <param name="sourceWidth">The source image width.</param>
/// <param name="tileWidth">The width of a tile.</param>
/// <param name="yStart">Y start position in the image.</param>
/// <param name="yEnd">Y end position of the image.</param>
/// <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>
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++;
}
}
/// <summary>
/// Bilinear interpolation between four adjacent tiles.
/// </summary>
/// <param name="sourcePixel">The pixel to remap the grey value from.</param>
/// <param name="cdfData">The pre-computed lookup tables to remap the grey values for each tiles.</param>
/// <param name="tileCountX">The number of tiles in the x-direction.</param>
/// <param name="tileCountY">The number of tiles in the y-direction.</param>
/// <param name="tileX">X position inside the tile.</param>
/// <param name="tileY">Y position inside the tile.</param>
/// <param name="cdfX">X index of the top left lookup table to use.</param>
/// <param name="cdfY">Y index of the top left lookup table to use.</param>
/// <param name="tileWidth">Width of one tile in pixels.</param>
/// <param name="tileHeight">Height of one tile in pixels.</param>
/// <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>
/// <returns>A re-mapped grey value.</returns>
[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);
}
/// <summary>
/// Linear interpolation between two tiles.
/// </summary>
/// <param name="sourcePixel">The pixel to remap the grey value from.</param>
/// <param name="cdfData">The CDF lookup map.</param>
/// <param name="tileX1">X position inside the first tile.</param>
/// <param name="tileY1">Y position inside the first tile.</param>
/// <param name="tileX2">X position inside the second tile.</param>
/// <param name="tileY2">Y position inside the second tile.</param>
/// <param name="tilePos">Position inside the tile.</param>
/// <param name="tileWidth">Width of the tile.</param>
/// <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>
/// <returns>A re-mapped grey value.</returns>
[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);
}
/// <summary>
/// Bilinear interpolation between four tiles.
/// </summary>
/// <param name="tx">The interpolation value in x direction in the range of [0, 1].</param>
/// <param name="ty">The interpolation value in y direction in the range of [0, 1].</param>
/// <param name="lt">Luminance from top left tile.</param>
/// <param name="rt">Luminance from right top tile.</param>
/// <param name="lb">Luminance from left bottom tile.</param>
/// <param name="rb">Luminance from right bottom tile.</param>
/// <returns>Interpolated Luminance.</returns>
[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);
/// <summary>
/// Linear interpolation between two grey values.
/// </summary>
/// <param name="left">The left value.</param>
/// <param name="right">The right value.</param>
/// <param name="t">The interpolation value between the two values in the range of [0, 1].</param>
/// <returns>The interpolated value.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
private static float LinearInterpolation(float left, float right, float t)
=> left + ((right - left) * t);
/// <summary>
/// Contains the results of the cumulative distribution function for all tiles.
/// </summary>
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<int> cdfMinBuffer2D;
// Used for storing the LUT for each CDF entry.
private readonly Buffer2D<int> 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<int>(tileCountX, tileCountY);
this.cdfLutBuffer2D = this.memoryAllocator.Allocate2D<int>(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<TPixel> source, HistogramEqualizationProcessor<TPixel> 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<int> histogramBuffer = this.memoryAllocator.Allocate<int>(luminanceLevels))
{
Span<int> 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<int> GetCdfLutSpan(int tileX, int tileY) => this.cdfLutBuffer2D.GetRowSpan(tileY).Slice(tileX * this.luminanceLevels, this.luminanceLevels);
/// <summary>
/// Remaps the grey value with the cdf.
/// </summary>
/// <param name="tilesX">The tiles x-position.</param>
/// <param name="tilesY">The tiles y-position.</param>
/// <param name="luminance">The original luminance.</param>
/// <returns>The remapped luminance.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public float RemapGreyValue(int tilesX, int tilesY, int luminance)
{
int cdfMin = this.cdfMinBuffer2D[tilesX, tilesY];
Span<int> 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();
}
}
}
}

371
src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs

@ -1,389 +1,36 @@
// 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
{
/// <summary>
/// Applies an adaptive histogram equalization to the image using an sliding window approach.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal class AdaptiveHistEqualizationSWProcessor<TPixel> : HistogramEqualizationProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
internal class AdaptiveHistEqualizationSWProcessor : HistogramEqualizationProcessor
{
/// <summary>
/// Initializes a new instance of the <see cref="AdaptiveHistEqualizationSWProcessor{TPixel}"/> class.
/// Initializes a new instance of the <see cref="AdaptiveHistEqualizationSWProcessor"/> 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 tile. Histogram bins which exceed this limit, will be capped at this value.</param>
/// <param name="tiles">The number of tiles the image is split into (horizontal and vertically). Minimum value is 2. Maximum value is 100.</param>
public AdaptiveHistEqualizationSWProcessor(int luminanceLevels, bool clipHistogram, float clipLimitPercentage, int tiles)
/// <param name="numberOfTiles">The number of tiles the image is split into (horizontal and vertically). Minimum value is 2. Maximum value is 100.</param>
public AdaptiveHistEqualizationSWProcessor(int luminanceLevels, bool clipHistogram, float clipLimitPercentage, int numberOfTiles)
: base(luminanceLevels, clipHistogram, clipLimitPercentage)
{
Guard.MustBeGreaterThanOrEqualTo(tiles, 2, nameof(tiles));
Guard.MustBeLessThanOrEqualTo(tiles, 100, nameof(tiles));
this.Tiles = tiles;
this.NumberOfTiles = numberOfTiles;
}
/// <summary>
/// Gets the number of tiles the image is split into (horizontal and vertically) for the adaptive histogram equalization.
/// </summary>
private int Tiles { 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();
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<TPixel> targetPixels = configuration.MemoryAllocator.Allocate2D<TPixel>(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<TPixel>.SwapOrCopyContent(source.PixelBuffer, targetPixels);
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="source">The source image.</param>
/// <param name="memoryAllocator">The memory allocator.</param>
/// <param name="targetPixels">The target pixels.</param>
/// <param name="swInfos">Informations about the sliding window dimensions.</param>
/// <param name="yStart">The y start position.</param>
/// <param name="yEnd">The y end position.</param>
/// <param name="useFastPath">if set to true the borders of the image will not be checked.</param>
/// <param name="configuration">The configuration.</param>
/// <returns>Action Delegate.</returns>
private Action<int> ProcessSlidingWindow(
ImageFrame<TPixel> source,
MemoryAllocator memoryAllocator,
Buffer2D<TPixel> targetPixels,
SlidingWindowInfos swInfos,
int yStart,
int yEnd,
bool useFastPath,
Configuration configuration)
{
return x =>
{
using (IMemoryOwner<int> histogramBuffer = memoryAllocator.Allocate<int>(this.LuminanceLevels, AllocationOptions.Clean))
using (IMemoryOwner<int> histogramBufferCopy = memoryAllocator.Allocate<int>(this.LuminanceLevels, AllocationOptions.Clean))
using (IMemoryOwner<int> cdfBuffer = memoryAllocator.Allocate<int>(this.LuminanceLevels, AllocationOptions.Clean))
using (IMemoryOwner<Vector4> pixelRowBuffer = memoryAllocator.Allocate<Vector4>(swInfos.TileWidth, AllocationOptions.Clean))
{
Span<int> histogram = histogramBuffer.GetSpan();
ref int histogramBase = ref MemoryMarshal.GetReference(histogram);
Span<int> histogramCopy = histogramBufferCopy.GetSpan();
ref int histogramCopyBase = ref MemoryMarshal.GetReference(histogramCopy);
ref int cdfBase = ref MemoryMarshal.GetReference(cdfBuffer.GetSpan());
Span<Vector4> 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);
}
}
};
}
/// <summary>
/// Get the a pixel row at a given position with a length of the tile width. Mirrors pixels which exceeds the edges.
/// </summary>
/// <param name="source">The source image.</param>
/// <param name="rowPixels">Pre-allocated pixel row span of the size of a the tile width.</param>
/// <param name="x">The x position.</param>
/// <param name="y">The y position.</param>
/// <param name="tileWidth">The width in pixels of a tile.</param>
/// <param name="configuration">The configuration.</param>
private void CopyPixelRow(
ImageFrame<TPixel> source,
Span<Vector4> 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++;
}
public int NumberOfTiles { get; }
return;
}
this.CopyPixelRowFast(source, rowPixels, x, y, tileWidth, configuration);
}
/// <summary>
/// Get the a pixel row at a given position with a length of the tile width.
/// </summary>
/// <param name="source">The source image.</param>
/// <param name="rowPixels">Pre-allocated pixel row span of the size of a the tile width.</param>
/// <param name="x">The x position.</param>
/// <param name="y">The y position.</param>
/// <param name="tileWidth">The width in pixels of a tile.</param>
/// <param name="configuration">The configuration.</param>
[MethodImpl(InliningOptions.ShortMethod)]
private void CopyPixelRowFast(
ImageFrame<TPixel> source,
Span<Vector4> rowPixels,
int x,
int y,
int tileWidth,
Configuration configuration)
=> PixelOperations<TPixel>.Instance.ToVector4(configuration, source.GetPixelRowSpan(y).Slice(start: x, length: tileWidth), rowPixels);
/// <summary>
/// Adds a column of grey values to the histogram.
/// </summary>
/// <param name="greyValuesBase">The reference to the span of grey values to add.</param>
/// <param name="histogramBase">The reference to the histogram span.</param>
/// <param name="luminanceLevels">The number of different luminance levels.</param>
/// <param name="length">The grey values span length.</param>
[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)++;
}
}
/// <summary>
/// Removes a column of grey values from the histogram.
/// </summary>
/// <param name="greyValuesBase">The reference to the span of grey values to remove.</param>
/// <param name="histogramBase">The reference to the histogram span.</param>
/// <param name="luminanceLevels">The number of different luminance levels.</param>
/// <param name="length">The grey values span length.</param>
[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
/// <inheritdoc />
public override IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>()
{
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; }
return new AdaptiveHistEqualizationSWProcessor<TPixel>(this.LuminanceLevels, this.ClipHistogram, this.ClipLimitPercentage, this.NumberOfTiles);
}
}
}

390
src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor{TPixel}.cs

@ -0,0 +1,390 @@
// 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
{
/// <summary>
/// Applies an adaptive histogram equalization to the image using an sliding window approach.
/// </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 tile. Histogram bins which exceed this limit, will be capped at this value.</param>
/// <param name="tiles">The number of tiles the image is split into (horizontal and vertically). Minimum value is 2. Maximum value is 100.</param>
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;
}
/// <summary>
/// Gets the number of tiles the image is split into (horizontal and vertically) for the adaptive histogram equalization.
/// </summary>
private int Tiles { 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();
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<TPixel> targetPixels = configuration.MemoryAllocator.Allocate2D<TPixel>(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<TPixel>.SwapOrCopyContent(source.PixelBuffer, targetPixels);
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="source">The source image.</param>
/// <param name="memoryAllocator">The memory allocator.</param>
/// <param name="targetPixels">The target pixels.</param>
/// <param name="swInfos">Informations about the sliding window dimensions.</param>
/// <param name="yStart">The y start position.</param>
/// <param name="yEnd">The y end position.</param>
/// <param name="useFastPath">if set to true the borders of the image will not be checked.</param>
/// <param name="configuration">The configuration.</param>
/// <returns>Action Delegate.</returns>
private Action<int> ProcessSlidingWindow(
ImageFrame<TPixel> source,
MemoryAllocator memoryAllocator,
Buffer2D<TPixel> targetPixels,
SlidingWindowInfos swInfos,
int yStart,
int yEnd,
bool useFastPath,
Configuration configuration)
{
return x =>
{
using (IMemoryOwner<int> histogramBuffer = memoryAllocator.Allocate<int>(this.LuminanceLevels, AllocationOptions.Clean))
using (IMemoryOwner<int> histogramBufferCopy = memoryAllocator.Allocate<int>(this.LuminanceLevels, AllocationOptions.Clean))
using (IMemoryOwner<int> cdfBuffer = memoryAllocator.Allocate<int>(this.LuminanceLevels, AllocationOptions.Clean))
using (IMemoryOwner<Vector4> pixelRowBuffer = memoryAllocator.Allocate<Vector4>(swInfos.TileWidth, AllocationOptions.Clean))
{
Span<int> histogram = histogramBuffer.GetSpan();
ref int histogramBase = ref MemoryMarshal.GetReference(histogram);
Span<int> histogramCopy = histogramBufferCopy.GetSpan();
ref int histogramCopyBase = ref MemoryMarshal.GetReference(histogramCopy);
ref int cdfBase = ref MemoryMarshal.GetReference(cdfBuffer.GetSpan());
Span<Vector4> 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);
}
}
};
}
/// <summary>
/// Get the a pixel row at a given position with a length of the tile width. Mirrors pixels which exceeds the edges.
/// </summary>
/// <param name="source">The source image.</param>
/// <param name="rowPixels">Pre-allocated pixel row span of the size of a the tile width.</param>
/// <param name="x">The x position.</param>
/// <param name="y">The y position.</param>
/// <param name="tileWidth">The width in pixels of a tile.</param>
/// <param name="configuration">The configuration.</param>
private void CopyPixelRow(
ImageFrame<TPixel> source,
Span<Vector4> 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);
}
/// <summary>
/// Get the a pixel row at a given position with a length of the tile width.
/// </summary>
/// <param name="source">The source image.</param>
/// <param name="rowPixels">Pre-allocated pixel row span of the size of a the tile width.</param>
/// <param name="x">The x position.</param>
/// <param name="y">The y position.</param>
/// <param name="tileWidth">The width in pixels of a tile.</param>
/// <param name="configuration">The configuration.</param>
[MethodImpl(InliningOptions.ShortMethod)]
private void CopyPixelRowFast(
ImageFrame<TPixel> source,
Span<Vector4> rowPixels,
int x,
int y,
int tileWidth,
Configuration configuration)
=> PixelOperations<TPixel>.Instance.ToVector4(configuration, source.GetPixelRowSpan(y).Slice(start: x, length: tileWidth), rowPixels);
/// <summary>
/// Adds a column of grey values to the histogram.
/// </summary>
/// <param name="greyValuesBase">The reference to the span of grey values to add.</param>
/// <param name="histogramBase">The reference to the histogram span.</param>
/// <param name="luminanceLevels">The number of different luminance levels.</param>
/// <param name="length">The grey values span length.</param>
[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)++;
}
}
/// <summary>
/// Removes a column of grey values from the histogram.
/// </summary>
/// <param name="greyValuesBase">The reference to the span of grey values to remove.</param>
/// <param name="histogramBase">The reference to the histogram span.</param>
/// <param name="luminanceLevels">The number of different luminance levels.</param>
/// <param name="length">The grey values span length.</param>
[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; }
}
}
}

99
src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs

@ -1,106 +1,25 @@
// 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
{
/// <summary>
/// Applies a global histogram equalization to the image.
/// Defines a global histogram equalization applicable to an <see cref="Image"/>.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal class GlobalHistogramEqualizationProcessor<TPixel> : HistogramEqualizationProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
internal class GlobalHistogramEqualizationProcessor : HistogramEqualizationProcessor
{
/// <summary>
/// Initializes a new instance of the <see cref="GlobalHistogramEqualizationProcessor{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. Histogram bins which exceed this limit, will be capped at this value.</param>
public GlobalHistogramEqualizationProcessor(int luminanceLevels, bool clipHistogram, float clipLimitPercentage)
: base(luminanceLevels, clipHistogram, clipLimitPercentage)
{
}
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration)
/// <inheritdoc />
public override IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>()
{
MemoryAllocator memoryAllocator = configuration.MemoryAllocator;
int numberOfPixels = source.Width * source.Height;
Span<TPixel> pixels = source.GetPixelSpan();
var workingRect = new Rectangle(0, 0, source.Width, source.Height);
using (IMemoryOwner<int> histogramBuffer = memoryAllocator.Allocate<int>(this.LuminanceLevels, AllocationOptions.Clean))
using (IMemoryOwner<int> cdfBuffer = memoryAllocator.Allocate<int>(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<int> 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));
}
}
});
}
return new GlobalHistogramEqualizationProcessor<TPixel>(
this.LuminanceLevels,
this.ClipHistogram,
this.ClipLimitPercentage);
}
}
}
}

107
src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs

@ -0,0 +1,107 @@
// 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
{
/// <summary>
/// Applies a global histogram equalization to the image.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal class GlobalHistogramEqualizationProcessor<TPixel> : HistogramEqualizationProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// Initializes a new instance of the <see cref="GlobalHistogramEqualizationProcessor{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. Histogram bins which exceed this limit, will be capped at this value.</param>
public GlobalHistogramEqualizationProcessor(int luminanceLevels, bool clipHistogram, float clipLimitPercentage)
: base(luminanceLevels, clipHistogram, clipLimitPercentage)
{
}
/// <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();
var workingRect = new Rectangle(0, 0, source.Width, source.Height);
using (IMemoryOwner<int> histogramBuffer = memoryAllocator.Allocate<int>(this.LuminanceLevels, AllocationOptions.Clean))
using (IMemoryOwner<int> cdfBuffer = memoryAllocator.Allocate<int>(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<int> 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));
}
}
});
}
}
}
}

2
src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs

@ -38,6 +38,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
/// <summary>
/// Gets or sets the number of tiles the image is split into (horizontal and vertically) for the adaptive histogram equalization. Defaults to 10.
/// </summary>
public int Tiles { get; set; } = 10;
public int NumberOfTiles { get; set; } = 10;
}
}

134
src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs

@ -1,10 +1,6 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Normalization
@ -12,14 +8,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
/// <summary>
/// Defines a processor that normalizes the histogram of an image.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal abstract class HistogramEqualizationProcessor<TPixel> : ImageProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
internal abstract class HistogramEqualizationProcessor : IImageProcessor
{
private readonly float luminanceLevelsFloat;
/// <summary>
/// Initializes a new instance of the <see cref="HistogramEqualizationProcessor{TPixel}"/> class.
/// Initializes a new instance of the <see cref="HistogramEqualizationProcessor"/> 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>
@ -27,12 +19,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
/// <param name="clipLimitPercentage">Histogram clip limit in percent of the total pixels in the tile. Histogram bins which exceed this limit, will be capped at this value.</param>
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.ClipHistogram = clipHistogram;
this.ClipLimitPercentage = clipLimitPercentage;
}
@ -44,95 +32,61 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
/// <summary>
/// Gets a value indicating whether to clip the histogram bins at a specific value.
/// </summary>
public bool ClipHistogramEnabled { get; }
public bool ClipHistogram { get; }
/// <summary>
/// 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.
/// </summary>
public float ClipLimitPercentage { get; }
/// <summary>
/// Calculates the cumulative distribution function.
/// </summary>
/// <param name="cdfBase">The reference to the array holding the cdf.</param>
/// <param name="histogramBase">The reference to the histogram of the input image.</param>
/// <param name="maxIdx">Index of the maximum of the histogram.</param>
/// <returns>The first none zero value of the cdf.</returns>
public int CalculateCdf(ref int cdfBase, ref int histogramBase, int maxIdx)
{
int histSum = 0;
int cdfMin = 0;
bool cdfMinFound = false;
for (int i = 0; i <= maxIdx; i++)
{
histSum += Unsafe.Add(ref histogramBase, i);
if (!cdfMinFound && histSum != 0)
{
cdfMin = histSum;
cdfMinFound = true;
}
// 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;
}
/// <inheritdoc />
public abstract IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>()
where TPixel : struct, IPixel<TPixel>;
/// <summary>
/// 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.
/// Creates the <see cref="HistogramEqualizationProcessor"/> that implements the algorithm
/// defined by the given <see cref="HistogramEqualizationOptions"/>.
/// </summary>
/// <param name="histogram">The histogram to apply the clipping.</param>
/// <param name="clipLimitPercentage">Histogram clip limit in percent of the total pixels in the tile. Histogram bins which exceed this limit, will be capped at this value.</param>
/// <param name="pixelCount">The numbers of pixels inside the tile.</param>
public void ClipHistogram(Span<int> histogram, float clipLimitPercentage, int pixelCount)
/// <param name="options">The <see cref="HistogramEqualizationOptions"/>.</param>
/// <returns>The <see cref="HistogramEqualizationProcessor"/>.</returns>
public static HistogramEqualizationProcessor FromOptions(HistogramEqualizationOptions options)
{
int clipLimit = (int)MathF.Round(pixelCount * clipLimitPercentage);
int sumOverClip = 0;
ref int histogramBase = ref MemoryMarshal.GetReference(histogram);
HistogramEqualizationProcessor processor;
for (int i = 0; i < histogram.Length; i++)
switch (options.Method)
{
ref int histogramLevel = ref Unsafe.Add(ref histogramBase, i);
if (histogramLevel > clipLimit)
{
sumOverClip += histogramLevel - clipLimit;
histogramLevel = clipLimit;
}
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.NumberOfTiles);
break;
case HistogramEqualizationMethod.AdaptiveSlidingWindow:
processor = new AdaptiveHistEqualizationSWProcessor(
options.LuminanceLevels,
options.ClipHistogram,
options.ClipLimitPercentage,
options.NumberOfTiles);
break;
default:
processor = new GlobalHistogramEqualizationProcessor(
options.LuminanceLevels,
options.ClipHistogram,
options.ClipLimitPercentage);
break;
}
int addToEachBin = sumOverClip > 0 ? (int)MathF.Floor(sumOverClip / this.luminanceLevelsFloat) : 0;
if (addToEachBin > 0)
{
for (int i = 0; i < histogram.Length; i++)
{
Unsafe.Add(ref histogramBase, i) += addToEachBin;
}
}
return processor;
}
/// <summary>
/// Convert the pixel values to grayscale using ITU-R Recommendation BT.709.
/// </summary>
/// <param name="sourcePixel">The pixel to get the luminance from</param>
/// <param name="luminanceLevels">The number of luminance levels (256 for 8 bit, 65536 for 16 bit grayscale images)</param>
[MethodImpl(InliningOptions.ShortMethod)]
public static int GetLuminance(TPixel sourcePixel, int luminanceLevels)
{
var vector = sourcePixel.ToVector4();
return GetLuminance(ref vector, luminanceLevels);
}
/// <summary>
/// Convert the pixel values to grayscale using ITU-R Recommendation BT.709.
/// </summary>
/// <param name="vector">The vector to get the luminance from</param>
/// <param name="luminanceLevels">The number of luminance levels (256 for 8 bit, 65536 for 16 bit grayscale images)</param>
[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));
}
}

139
src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor{TPixel}.cs

@ -0,0 +1,139 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Normalization
{
/// <summary>
/// Defines a processor that normalizes the histogram of an image.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal abstract class HistogramEqualizationProcessor<TPixel> : ImageProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
private readonly float luminanceLevelsFloat;
/// <summary>
/// Initializes a new instance of the <see cref="HistogramEqualizationProcessor{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">Indicates, if histogram bins should be clipped.</param>
/// <param name="clipLimitPercentage">Histogram clip limit in percent of the total pixels in the tile. Histogram bins which exceed this limit, will be capped at this value.</param>
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;
}
/// <summary>
/// Gets the number of luminance levels.
/// </summary>
public int LuminanceLevels { get; }
/// <summary>
/// Gets a value indicating whether to clip the histogram bins at a specific value.
/// </summary>
public bool ClipHistogramEnabled { get; }
/// <summary>
/// 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.
/// </summary>
public float ClipLimitPercentage { get; }
/// <summary>
/// Calculates the cumulative distribution function.
/// </summary>
/// <param name="cdfBase">The reference to the array holding the cdf.</param>
/// <param name="histogramBase">The reference to the histogram of the input image.</param>
/// <param name="maxIdx">Index of the maximum of the histogram.</param>
/// <returns>The first none zero value of the cdf.</returns>
public int CalculateCdf(ref int cdfBase, ref int histogramBase, int maxIdx)
{
int histSum = 0;
int cdfMin = 0;
bool cdfMinFound = false;
for (int i = 0; i <= maxIdx; i++)
{
histSum += Unsafe.Add(ref histogramBase, i);
if (!cdfMinFound && histSum != 0)
{
cdfMin = histSum;
cdfMinFound = true;
}
// 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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="histogram">The histogram to apply the clipping.</param>
/// <param name="clipLimitPercentage">Histogram clip limit in percent of the total pixels in the tile. Histogram bins which exceed this limit, will be capped at this value.</param>
/// <param name="pixelCount">The numbers of pixels inside the tile.</param>
public void ClipHistogram(Span<int> histogram, float clipLimitPercentage, int pixelCount)
{
int clipLimit = (int)MathF.Round(pixelCount * clipLimitPercentage);
int sumOverClip = 0;
ref int histogramBase = ref MemoryMarshal.GetReference(histogram);
for (int i = 0; i < histogram.Length; i++)
{
ref int histogramLevel = ref Unsafe.Add(ref histogramBase, i);
if (histogramLevel > clipLimit)
{
sumOverClip += histogramLevel - clipLimit;
histogramLevel = clipLimit;
}
}
int addToEachBin = sumOverClip > 0 ? (int)MathF.Floor(sumOverClip / this.luminanceLevelsFloat) : 0;
if (addToEachBin > 0)
{
for (int i = 0; i < histogram.Length; i++)
{
Unsafe.Add(ref histogramBase, i) += addToEachBin;
}
}
}
/// <summary>
/// Convert the pixel values to grayscale using ITU-R Recommendation BT.709.
/// </summary>
/// <param name="sourcePixel">The pixel to get the luminance from</param>
/// <param name="luminanceLevels">The number of luminance levels (256 for 8 bit, 65536 for 16 bit grayscale images)</param>
[MethodImpl(InliningOptions.ShortMethod)]
public static int GetLuminance(TPixel sourcePixel, int luminanceLevels)
{
var vector = sourcePixel.ToVector4();
return GetLuminance(ref vector, luminanceLevels);
}
/// <summary>
/// Convert the pixel values to grayscale using ITU-R Recommendation BT.709.
/// </summary>
/// <param name="vector">The vector to get the luminance from</param>
/// <param name="luminanceLevels">The number of luminance levels (256 for 8 bit, 65536 for 16 bit grayscale images)</param>
[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));
}
}

4
tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs

@ -86,7 +86,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Normalization
Method = HistogramEqualizationMethod.AdaptiveSlidingWindow,
LuminanceLevels = 256,
ClipHistogram = true,
Tiles = 15
NumberOfTiles = 15
};
image.Mutate(x => x.HistogramEqualization(options));
image.DebugSave(provider);
@ -106,7 +106,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Normalization
Method = HistogramEqualizationMethod.AdaptiveTileInterpolation,
LuminanceLevels = 256,
ClipHistogram = true,
Tiles = 10
NumberOfTiles = 10
};
image.Mutate(x => x.HistogramEqualization(options));
image.DebugSave(provider);

3
tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs

@ -50,8 +50,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization
/// The output is visually correct old 32bit runtime,
/// but it is very different because of floating point inaccuracies.
/// </summary>
private static readonly bool SkipAllDitherTests =
!TestEnvironment.Is64BitProcess && string.IsNullOrEmpty(TestEnvironment.NetCoreVersion);
private static readonly bool SkipAllDitherTests = false;
[Theory]
[WithFile(TestImages.Png.CalliphoraPartial, PixelTypes.Rgba32)]

Loading…
Cancel
Save