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 System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
using SixLabors.Primitives; using SixLabors.Primitives;
namespace SixLabors.ImageSharp namespace SixLabors.ImageSharp

40
src/ImageSharp/Processing/HistogramEqualizationExtension.cs

@ -12,25 +12,51 @@ namespace SixLabors.ImageSharp.Processing
public static class HistogramEqualizationExtension public static class HistogramEqualizationExtension
{ {
/// <summary> /// <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> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image this method extends.</param> /// <param name="source">The image this method extends.</param>
/// <returns>The <see cref="Image{TPixel}"/>.</returns> /// <returns>The <see cref="Image{TPixel}"/>.</returns>
public static IImageProcessingContext<TPixel> HistogramEqualization<TPixel>(this IImageProcessingContext<TPixel> source) public static IImageProcessingContext<TPixel> HistogramEqualization<TPixel>(this IImageProcessingContext<TPixel> source)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
=> HistogramEqualization(source, 65536); => HistogramEqualization(source, HistogramEqualizationOptions.Default);
/// <summary> /// <summary>
/// Equalizes the histogram of an image to increases the global contrast. /// Equalizes the histogram of an image to increases the contrast.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image this method extends.</param> /// <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 /// <param name="options">The histogram equalization options to use.</param>
/// or 65536 for 16-bit grayscale images.</param>
/// <returns>The <see cref="Image{TPixel}"/>.</returns> /// <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> 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. // Licensed under the Apache License, Version 2.0.
using System; using System;
using System.Buffers;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced; using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Normalization namespace SixLabors.ImageSharp.Processing.Processors.Normalization
{ {
/// <summary> /// <summary>
/// Applies a global histogram equalization to the image. /// Defines a processor that normalizes the histogram of an image.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <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> where TPixel : struct, IPixel<TPixel>
{ {
private readonly float luminanceLevelsFloat;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="HistogramEqualizationProcessor{TPixel}"/> class. /// Initializes a new instance of the <see cref="HistogramEqualizationProcessor{TPixel}"/> class.
/// </summary> /// </summary>
/// <param name="luminanceLevels">The number of different luminance levels. Typical values are 256 for 8-bit grayscale images /// <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> /// 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(luminanceLevels, 0, nameof(luminanceLevels));
Guard.MustBeGreaterThan(clipLimitPercentage, 0F, nameof(clipLimitPercentage));
this.LuminanceLevels = luminanceLevels; this.LuminanceLevels = luminanceLevels;
this.luminanceLevelsFloat = luminanceLevels;
this.ClipHistogramEnabled = clipHistogram;
this.ClipLimitPercentage = clipLimitPercentage;
} }
/// <summary> /// <summary>
@ -37,77 +41,77 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
/// </summary> /// </summary>
public int LuminanceLevels { get; } public int LuminanceLevels { get; }
/// <inheritdoc/> /// <summary>
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration) /// 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 histSum = 0;
int numberOfPixels = source.Width * source.Height; int cdfMin = 0;
Span<TPixel> pixels = source.GetPixelSpan(); bool cdfMinFound = false;
// Build the histogram of the grayscale levels. for (int i = 0; i <= maxIdx; i++)
using (IMemoryOwner<int> histogramBuffer = memoryAllocator.Allocate<int>(this.LuminanceLevels, AllocationOptions.Clean))
using (IMemoryOwner<int> cdfBuffer = memoryAllocator.Allocate<int>(this.LuminanceLevels, AllocationOptions.Clean))
{ {
Span<int> histogram = histogramBuffer.GetSpan(); histSum += Unsafe.Add(ref histogramBase, i);
for (int i = 0; i < pixels.Length; i++) if (!cdfMinFound && histSum != 0)
{ {
TPixel sourcePixel = pixels[i]; cdfMin = histSum;
int luminance = this.GetLuminance(sourcePixel, this.LuminanceLevels); cdfMinFound = true;
histogram[luminance]++;
} }
// Calculate the cumulative distribution function, which will map each input pixel to a new value. // Creating the lookup table: subtracting cdf min, so we do not need to do that inside the for loop.
Span<int> cdf = cdfBuffer.GetSpan(); Unsafe.Add(ref cdfBase, i) = Math.Max(0, histSum - cdfMin);
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));
}
} }
return cdfMin;
} }
/// <summary> /// <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> /// </summary>
/// <param name="cdf">The array holding the cdf.</param> /// <param name="histogram">The histogram to apply the clipping.</param>
/// <param name="histogram">The histogram of the input image.</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>
/// <returns>The first none zero value of the cdf.</returns> /// <param name="pixelCount">The numbers of pixels inside the tile.</param>
private int CalculateCdf(Span<int> cdf, Span<int> histogram) public void ClipHistogram(Span<int> histogram, float clipLimitPercentage, int pixelCount)
{ {
// Calculate the cumulative histogram int clipLimit = (int)MathF.Round(pixelCount * clipLimitPercentage);
int histSum = 0; int sumOverClip = 0;
for (int i = 0; i < histogram.Length; i++) ref int histogramBase = ref MemoryMarshal.GetReference(histogram);
{
histSum += histogram[i];
cdf[i] = histSum;
}
// Get the first none zero value of the cumulative histogram
int cdfMin = 0;
for (int i = 0; i < histogram.Length; i++) 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]; sumOverClip += histogramLevel - clipLimit;
break; histogramLevel = clipLimit;
} }
} }
// Creating the lookup table: subtracting cdf min, so we do not need to do that inside the for loop int addToEachBin = sumOverClip > 0 ? (int)MathF.Floor(sumOverClip / this.luminanceLevelsFloat) : 0;
for (int i = 0; i < histogram.Length; i++) 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> /// <summary>
@ -116,13 +120,19 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
/// <param name="sourcePixel">The pixel to get the luminance from</param> /// <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> /// <param name="luminanceLevels">The number of luminance levels (256 for 8 bit, 65536 for 16 bit grayscale images)</param>
[MethodImpl(InliningOptions.ShortMethod)] [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(); var vector = sourcePixel.ToVector4();
int luminance = Convert.ToInt32(((.2126F * vector.X) + (.7152F * vector.Y) + (.0722F * vector.Y)) * (luminanceLevels - 1)); return GetLuminance(ref vector, luminanceLevels);
return luminance;
} }
/// <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.PixelFormats;
using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Normalization;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using Xunit; using Xunit;
namespace SixLabors.ImageSharp.Tests.Processing.Normalization namespace SixLabors.ImageSharp.Tests.Processing.Normalization
{ {
public class HistogramEqualizationTests public class HistogramEqualizationTests
{ {
private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.0456F);
[Theory] [Theory]
[InlineData(256)] [InlineData(256)]
[InlineData(65536)] [InlineData(65536)]
@ -27,18 +31,19 @@ namespace SixLabors.ImageSharp.Tests.Processing.Normalization
70, 87, 69, 68, 65, 73, 78, 90 70, 87, 69, 68, 65, 73, 78, 90
}; };
var image = new Image<Rgba32>(8, 8); using (var image = new Image<Rgba32>(8, 8))
for (int y = 0; y < 8; y++)
{ {
for (int x = 0; x < 8; x++) for (int y = 0; y < 8; y++)
{ {
byte luminance = pixels[y * 8 + x]; for (int x = 0; x < 8; x++)
image[x, y] = new Rgba32(luminance, luminance, luminance); {
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, 0, 12, 53, 32, 146, 53, 174, 53,
57, 32, 12, 227, 219, 202, 32, 154, 57, 32, 12, 227, 219, 202, 32, 154,
65, 85, 93, 239, 251, 227, 65, 158, 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, 117, 190, 36, 190, 178, 93, 20, 170,
130, 202, 73, 20, 12, 53, 85, 194, 130, 202, 73, 20, 12, 53, 85, 194,
146, 206, 130, 117, 85, 166, 182, 215 146, 206, 130, 117, 85, 166, 182, 215
}; };
// Act // Act
image.Mutate(x => x.HistogramEqualization(luminanceLevels)); image.Mutate(x => x.HistogramEqualization(new HistogramEqualizationOptions()
{
LuminanceLevels = luminanceLevels
}));
// Assert // Assert
for (int y = 0; y < 8; y++) for (int y = 0; y < 8; y++)
{
for (int x = 0; x < 8; x++)
{ {
Rgba32 actual = image[x, y]; for (int x = 0; x < 8; x++)
Assert.Equal(expected[y * 8 + x], actual.R); {
Assert.Equal(expected[y * 8 + x], actual.G); Rgba32 actual = image[x, y];
Assert.Equal(expected[y * 8 + x], actual.B); 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 Testorig420 = "Jpg/baseline/testorig.jpg";
public const string MultiScanBaselineCMYK = "Jpg/baseline/MultiScanBaselineCMYK.jpg"; public const string MultiScanBaselineCMYK = "Jpg/baseline/MultiScanBaselineCMYK.jpg";
public const string Ratio1x1 = "Jpg/baseline/ratio-1x1.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 Testorig12bit = "Jpg/baseline/testorig12.jpg";
public const string YcckSubsample1222 = "Jpg/baseline/ycck-subsample-1222.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