mirror of https://github.com/SixLabors/ImageSharp
Browse Source
* 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
committed by
James Jackson-South
13 changed files with 1311 additions and 92 deletions
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
} |
|||
@ -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)); |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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, |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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 |
|
|||
} |
|||
} |
|||
@ -1 +1 @@ |
|||
Subproject commit 8693e2fd4577a9ac1a749da8db564095b5a05389 |
|||
Subproject commit 1ca515499663e8b0b7c924a49b8d212f7447bdb0 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:50f6359d228079ec5e6ead84046119eda84136026c1651c753e6d270405cd4b7 |
|||
size 187216 |
|||
Loading…
Reference in new issue