Browse Source

Feature: adaptive histogram equalization (#673)

* first version of sliding window adaptive histogram equalization

* going now from top to bottom of the image, added more comments

* using memory allocator to create the histogram and the cdf

* mirroring rows which exceeds the borders

* mirroring also left and right borders

* gridsize and cliplimit are now parameters of the constructor

* using Parallel.For

* only applying clipping once, effect applying it multiple times is neglectable

* added abstract base class for histogram equalization, added option to enable / disable clipping

* small improvements

* clipLimit now in percent of the total number of pixels in the grid

* optimization: only calculating the cdf until the maximum histogram index

* fix: using configuration from the parameter instead of the default

* removed unnecessary loops in CalculateCdf, fixed typo in method name AddPixelsToHistogram

* added different approach for ahe: image is split up in tiles, cdf is computed for each tile. Grey value will be determined by interpolating between 4 tiles

* simplified interpolation between the tiles

* number of tiles is now fixed and depended on the width and height of the image

* moved calculating LUT's into separate method

* number of tiles is now part of the options and will be used with the sliding window approach also, so both methods are comparable

* removed no longer valid xml comment

* attempt fixing the borders

* refactoring to improve readability

* linear interpolation in the border tiles

* refactored processing the borders into separate methods

* fixing corner tiles

* fixed build errors

* fixing mistake during merge from upstream: setting test images to "update Resize reference output because of improved ResizeKernelMap calculations"

* using Parallel.ForEach for all inner tile calculations

* using Parallel.ForEach to calculate the lookup tables

* re-using pre allocated pixel row in GetPixelRow

* fixed issue with the border tiles, when tile width != tile height

* changed default value for ClipHistogram to false again

* alpha channel from the original image is now preserved

* added unit tests for adaptive histogram equalization

* Update External

* 2x faster adaptive tiled processor

* Remove double indexing and bounds checks

* Begin optimizing the global histogram

* Parallelize GlobalHistogramEqualizationProcessor

* Moving sliding window from left to right instead of from top to bottom

* The tile width and height is again depended on the image width: image.Width / Tiles

* Removed keeping track of the maximum histogram position

* Updated reference image for sliding window AHE for moving the sliding window from left to right

* Removed unnecessary call to Span.Clear(), all values are overwritten anyway

* Revert "Moving sliding window from left to right instead of from top to bottom"

This reverts commit 8f19e5edd2.

# Conflicts:
#	src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs

* Split GetPixelRow in two version: one which mirrors the edges (only needed in the borders of the images) and one which does not

* Refactoring and cleanup sliding window processor

* Added an upper limit of 100 tiles

* Performance tweaks

* Update External
af/merge-core
Brian Popow 7 years ago
committed by James Jackson-South
parent
commit
5283c6db6d
  1. 1
      src/ImageSharp/Common/Helpers/ImageMaths.cs
  2. 40
      src/ImageSharp/Processing/HistogramEqualizationExtension.cs
  3. 545
      src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs
  4. 389
      src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs
  5. 106
      src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs
  6. 26
      src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs
  7. 43
      src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs
  8. 138
      src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs
  9. 22
      tests/ImageSharp.Benchmarks/General/BasicMath/Round.cs
  10. 87
      tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs
  11. 1
      tests/ImageSharp.Tests/TestImages.cs
  12. 2
      tests/Images/External
  13. 3
      tests/Images/Input/Jpg/baseline/AsianCarvingLowContrast.jpg

1
src/ImageSharp/Common/Helpers/ImageMaths.cs

@ -5,7 +5,6 @@ using System;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp

40
src/ImageSharp/Processing/HistogramEqualizationExtension.cs

@ -12,25 +12,51 @@ namespace SixLabors.ImageSharp.Processing
public static class HistogramEqualizationExtension
{
/// <summary>
/// Equalizes the histogram of an image to increases the global contrast using 65536 luminance levels.
/// Equalizes the histogram of an image to increases the contrast.
/// </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, 65536);
=> HistogramEqualization(source, HistogramEqualizationOptions.Default);
/// <summary>
/// Equalizes the histogram of an image to increases the global contrast.
/// 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="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="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, int luminanceLevels)
public static IImageProcessingContext<TPixel> HistogramEqualization<TPixel>(this IImageProcessingContext<TPixel> source, HistogramEqualizationOptions options)
where TPixel : struct, IPixel<TPixel>
=> source.ApplyProcessor(new HistogramEqualizationProcessor<TPixel>(luminanceLevels));
=> 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;
}
}
}

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

@ -0,0 +1,545 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Normalization
{
/// <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();
}
}
}
}

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

@ -0,0 +1,389 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Normalization
{
/// <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; }
}
}
}

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

@ -0,0 +1,106 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Normalization
{
/// <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));
}
}
});
}
}
}
}

26
src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs

@ -0,0 +1,26 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Processing.Processors.Normalization
{
/// <summary>
/// Enumerates the different types of defined histogram equalization methods.
/// </summary>
public enum HistogramEqualizationMethod : int
{
/// <summary>
/// A global histogram equalization.
/// </summary>
Global,
/// <summary>
/// Adaptive histogram equalization using a tile interpolation approach.
/// </summary>
AdaptiveTileInterpolation,
/// <summary>
/// Adaptive histogram equalization using sliding window. Slower then the tile interpolation mode, but can yield to better results.
/// </summary>
AdaptiveSlidingWindow,
}
}

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

@ -0,0 +1,43 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Processing.Processors.Normalization
{
/// <summary>
/// Data container providing the different options for the histogram equalization.
/// </summary>
public class HistogramEqualizationOptions
{
/// <summary>
/// Gets the default <see cref="HistogramEqualizationOptions"/> instance.
/// </summary>
public static HistogramEqualizationOptions Default { get; } = new HistogramEqualizationOptions();
/// <summary>
/// Gets or sets the histogram equalization method to use. Defaults to global histogram equalization.
/// </summary>
public HistogramEqualizationMethod Method { get; set; } = HistogramEqualizationMethod.Global;
/// <summary>
/// Gets or sets the number of different luminance levels. Typical values are 256 for 8-bit grayscale images
/// or 65536 for 16-bit grayscale images. Defaults to 256.
/// </summary>
public int LuminanceLevels { get; set; } = 256;
/// <summary>
/// Gets or sets a value indicating whether to clip the histogram bins at a specific value. Defaults to false.
/// </summary>
public bool ClipHistogram { get; set; } = false;
/// <summary>
/// Gets or sets the histogram clip limit in percent of the total pixels in a tile. Histogram bins which exceed this limit, will be capped at this value.
/// Defaults to 0.035f.
/// </summary>
public float ClipLimitPercentage { get; set; } = 0.035f;
/// <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;
}
}

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

@ -2,34 +2,38 @@
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Normalization
{
/// <summary>
/// Applies a global histogram equalization to the image.
/// Defines a processor that normalizes the histogram of an image.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal class HistogramEqualizationProcessor<TPixel> : ImageProcessor<TPixel>
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>
public HistogramEqualizationProcessor(int luminanceLevels)
/// <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>
@ -37,77 +41,77 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
/// </summary>
public int LuminanceLevels { get; }
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration)
/// <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)
{
MemoryAllocator memoryAllocator = configuration.MemoryAllocator;
int numberOfPixels = source.Width * source.Height;
Span<TPixel> pixels = source.GetPixelSpan();
int histSum = 0;
int cdfMin = 0;
bool cdfMinFound = false;
// Build the histogram of the grayscale levels.
using (IMemoryOwner<int> histogramBuffer = memoryAllocator.Allocate<int>(this.LuminanceLevels, AllocationOptions.Clean))
using (IMemoryOwner<int> cdfBuffer = memoryAllocator.Allocate<int>(this.LuminanceLevels, AllocationOptions.Clean))
for (int i = 0; i <= maxIdx; i++)
{
Span<int> histogram = histogramBuffer.GetSpan();
for (int i = 0; i < pixels.Length; i++)
histSum += Unsafe.Add(ref histogramBase, i);
if (!cdfMinFound && histSum != 0)
{
TPixel sourcePixel = pixels[i];
int luminance = this.GetLuminance(sourcePixel, this.LuminanceLevels);
histogram[luminance]++;
cdfMin = histSum;
cdfMinFound = true;
}
// Calculate the cumulative distribution function, which will map each input pixel to a new value.
Span<int> cdf = cdfBuffer.GetSpan();
int cdfMin = this.CalculateCdf(cdf, histogram);
// Apply the cdf to each pixel of the image
float numberOfPixelsMinusCdfMin = numberOfPixels - cdfMin;
for (int i = 0; i < pixels.Length; i++)
{
TPixel sourcePixel = pixels[i];
int luminance = this.GetLuminance(sourcePixel, this.LuminanceLevels);
float luminanceEqualized = cdf[luminance] / numberOfPixelsMinusCdfMin;
pixels[i].FromVector4(new Vector4(luminanceEqualized));
}
// Creating the lookup table: subtracting cdf min, so we do not need to do that inside the for loop.
Unsafe.Add(ref cdfBase, i) = Math.Max(0, histSum - cdfMin);
}
return cdfMin;
}
/// <summary>
/// Calculates the cumulative distribution function.
/// AHE tends to over amplify the contrast in near-constant regions of the image, since the histogram in such regions is highly concentrated.
/// Clipping the histogram is meant to reduce this effect, by cutting of histogram bin's which exceed a certain amount and redistribute
/// the values over the clip limit to all other bins equally.
/// </summary>
/// <param name="cdf">The array holding the cdf.</param>
/// <param name="histogram">The histogram of the input image.</param>
/// <returns>The first none zero value of the cdf.</returns>
private int CalculateCdf(Span<int> cdf, Span<int> histogram)
/// <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)
{
// Calculate the cumulative histogram
int histSum = 0;
for (int i = 0; i < histogram.Length; i++)
{
histSum += histogram[i];
cdf[i] = histSum;
}
int clipLimit = (int)MathF.Round(pixelCount * clipLimitPercentage);
int sumOverClip = 0;
ref int histogramBase = ref MemoryMarshal.GetReference(histogram);
// Get the first none zero value of the cumulative histogram
int cdfMin = 0;
for (int i = 0; i < histogram.Length; i++)
{
if (cdf[i] != 0)
ref int histogramLevel = ref Unsafe.Add(ref histogramBase, i);
if (histogramLevel > clipLimit)
{
cdfMin = cdf[i];
break;
sumOverClip += histogramLevel - clipLimit;
histogramLevel = clipLimit;
}
}
// Creating the lookup table: subtracting cdf min, so we do not need to do that inside the for loop
for (int i = 0; i < histogram.Length; i++)
int addToEachBin = sumOverClip > 0 ? (int)MathF.Floor(sumOverClip / this.luminanceLevelsFloat) : 0;
if (addToEachBin > 0)
{
cdf[i] = Math.Max(0, cdf[i] - cdfMin);
for (int i = 0; i < histogram.Length; i++)
{
Unsafe.Add(ref histogramBase, i) += addToEachBin;
}
}
return cdfMin;
}
/// <summary>
@ -116,13 +120,19 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
/// <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)]
private int GetLuminance(TPixel sourcePixel, int luminanceLevels)
public static int GetLuminance(TPixel sourcePixel, int luminanceLevels)
{
// Convert to grayscale using ITU-R Recommendation BT.709
var vector = sourcePixel.ToVector4();
int luminance = Convert.ToInt32(((.2126F * vector.X) + (.7152F * vector.Y) + (.0722F * vector.Y)) * (luminanceLevels - 1));
return luminance;
return GetLuminance(ref vector, luminanceLevels);
}
/// <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));
}
}
}

22
tests/ImageSharp.Benchmarks/General/BasicMath/Round.cs

@ -0,0 +1,22 @@
using System;
using BenchmarkDotNet.Attributes;
namespace SixLabors.ImageSharp.Benchmarks.General.BasicMath
{
public class Round
{
private const float input = .51F;
[Benchmark]
public int ConvertTo() => Convert.ToInt32(input);
[Benchmark]
public int MathRound() => (int)Math.Round(input);
// Results 20th Jan 2019
// Method | Mean | Error | StdDev | Median |
//---------- |----------:|----------:|----------:|----------:|
// ConvertTo | 3.1967 ns | 0.1234 ns | 0.2129 ns | 3.2340 ns |
// MathRound | 0.0528 ns | 0.0374 ns | 0.1079 ns | 0.0000 ns |
}
}

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

@ -3,12 +3,16 @@
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Normalization;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Processing.Normalization
{
public class HistogramEqualizationTests
{
private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.0456F);
[Theory]
[InlineData(256)]
[InlineData(65536)]
@ -27,18 +31,19 @@ namespace SixLabors.ImageSharp.Tests.Processing.Normalization
70, 87, 69, 68, 65, 73, 78, 90
};
var image = new Image<Rgba32>(8, 8);
for (int y = 0; y < 8; y++)
using (var image = new Image<Rgba32>(8, 8))
{
for (int x = 0; x < 8; x++)
for (int y = 0; y < 8; y++)
{
byte luminance = pixels[y * 8 + x];
image[x, y] = new Rgba32(luminance, luminance, luminance);
for (int x = 0; x < 8; x++)
{
byte luminance = pixels[(y * 8) + x];
image[x, y] = new Rgba32(luminance, luminance, luminance);
}
}
}
byte[] expected = new byte[]
{
byte[] expected = new byte[]
{
0, 12, 53, 32, 146, 53, 174, 53,
57, 32, 12, 227, 219, 202, 32, 154,
65, 85, 93, 239, 251, 227, 65, 158,
@ -47,22 +52,66 @@ namespace SixLabors.ImageSharp.Tests.Processing.Normalization
117, 190, 36, 190, 178, 93, 20, 170,
130, 202, 73, 20, 12, 53, 85, 194,
146, 206, 130, 117, 85, 166, 182, 215
};
};
// Act
image.Mutate(x => x.HistogramEqualization(luminanceLevels));
// Act
image.Mutate(x => x.HistogramEqualization(new HistogramEqualizationOptions()
{
LuminanceLevels = luminanceLevels
}));
// Assert
for (int y = 0; y < 8; y++)
{
for (int x = 0; x < 8; x++)
// Assert
for (int y = 0; y < 8; y++)
{
Rgba32 actual = image[x, y];
Assert.Equal(expected[y * 8 + x], actual.R);
Assert.Equal(expected[y * 8 + x], actual.G);
Assert.Equal(expected[y * 8 + x], actual.B);
for (int x = 0; x < 8; x++)
{
Rgba32 actual = image[x, y];
Assert.Equal(expected[(y * 8) + x], actual.R);
Assert.Equal(expected[(y * 8) + x], actual.G);
Assert.Equal(expected[(y * 8) + x], actual.B);
}
}
}
}
[Theory]
[WithFile(TestImages.Jpeg.Baseline.LowContrast, PixelTypes.Rgba32)]
public void Adaptive_SlidingWindow_15Tiles_WithClipping<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{
var options = new HistogramEqualizationOptions()
{
Method = HistogramEqualizationMethod.AdaptiveSlidingWindow,
LuminanceLevels = 256,
ClipHistogram = true,
Tiles = 15
};
image.Mutate(x => x.HistogramEqualization(options));
image.DebugSave(provider);
image.CompareToReferenceOutput(ValidatorComparer, provider);
}
}
[Theory]
[WithFile(TestImages.Jpeg.Baseline.LowContrast, PixelTypes.Rgba32)]
public void Adaptive_TileInterpolation_10Tiles_WithClipping<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{
var options = new HistogramEqualizationOptions()
{
Method = HistogramEqualizationMethod.AdaptiveTileInterpolation,
LuminanceLevels = 256,
ClipHistogram = true,
Tiles = 10
};
image.Mutate(x => x.HistogramEqualization(options));
image.DebugSave(provider);
image.CompareToReferenceOutput(ValidatorComparer, provider);
}
}
}
}

1
tests/ImageSharp.Tests/TestImages.cs

@ -141,6 +141,7 @@ namespace SixLabors.ImageSharp.Tests
public const string Testorig420 = "Jpg/baseline/testorig.jpg";
public const string MultiScanBaselineCMYK = "Jpg/baseline/MultiScanBaselineCMYK.jpg";
public const string Ratio1x1 = "Jpg/baseline/ratio-1x1.jpg";
public const string LowContrast = "Jpg/baseline/AsianCarvingLowContrast.jpg";
public const string Testorig12bit = "Jpg/baseline/testorig12.jpg";
public const string YcckSubsample1222 = "Jpg/baseline/ycck-subsample-1222.jpg";

2
tests/Images/External

@ -1 +1 @@
Subproject commit 8693e2fd4577a9ac1a749da8db564095b5a05389
Subproject commit 1ca515499663e8b0b7c924a49b8d212f7447bdb0

3
tests/Images/Input/Jpg/baseline/AsianCarvingLowContrast.jpg

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:50f6359d228079ec5e6ead84046119eda84136026c1651c753e6d270405cd4b7
size 187216
Loading…
Cancel
Save