Browse Source

2x faster adaptive tiled processor

pull/673/head
James Jackson-South 7 years ago
parent
commit
4330c82f28
  1. 1
      src/ImageSharp/Common/Helpers/ImageMaths.cs
  2. 417
      src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs
  3. 6
      src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs
  4. 4
      src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs
  5. 43
      src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs
  6. 22
      tests/ImageSharp.Benchmarks/General/BasicMath/Round.cs
  7. 70
      tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs

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

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

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
@ -46,81 +47,100 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration) protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration)
{ {
int numberOfPixels = source.Width * source.Height; int numberOfPixels = source.Width * source.Height;
int tileWidth = Convert.ToInt32(Math.Ceiling(source.Width / (double)this.Tiles)); int tileWidth = (int)MathF.Ceiling(source.Width / (float)this.Tiles);
int tileHeight = Convert.ToInt32(Math.Ceiling(source.Height / (double)this.Tiles)); int tileHeight = (int)MathF.Ceiling(source.Height / (float)this.Tiles);
int pixelsInTile = tileWidth * tileHeight; int pixelsInTile = tileWidth * tileHeight;
int halfTileWidth = tileWidth / 2; int halfTileWidth = tileWidth / 2;
int halfTileHeight = tileHeight / 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. // The image is split up into tiles. For each tile the cumulative distribution function will be calculated.
CdfData[,] cdfData = this.CalculateLookupTables(source, configuration, this.Tiles, this.Tiles, tileWidth, tileHeight); using (var cdfData = new CdfTileData(configuration, sourceRectangle.Height, this.Tiles, this.Tiles, tileWidth, tileHeight, luminanceLevels))
var tileYStartPositions = new List<(int y, int cdfY)>();
int cdfY = 0;
for (int y = halfTileHeight; y < source.Height - halfTileHeight; y += tileHeight)
{ {
tileYStartPositions.Add((y, cdfY)); cdfData.CalculateLookupTables(source, this);
cdfY++;
}
Parallel.ForEach(tileYStartPositions, new ParallelOptions() { MaxDegreeOfParallelism = configuration.MaxDegreeOfParallelism }, (tileYStartPosition) => var tileYStartPositions = new List<(int y, int cdfY)>();
{ int cdfY = 0;
int cdfX = 0; for (int y = halfTileHeight; y < source.Height - halfTileHeight; y += tileHeight)
int tileX = 0; {
int tileY = 0; tileYStartPositions.Add((y, cdfY));
int y = tileYStartPosition.y; cdfY++;
}
cdfX = 0; Parallel.ForEach(
for (int x = halfTileWidth; x < source.Width - halfTileWidth; x += tileWidth) tileYStartPositions,
new ParallelOptions() { MaxDegreeOfParallelism = configuration.MaxDegreeOfParallelism },
tileYStartPosition =>
{ {
tileY = 0; int cdfX = 0;
int yEnd = Math.Min(y + tileHeight, source.Height); int tileX = 0;
int xEnd = Math.Min(x + tileWidth, source.Width); int tileY = 0;
for (int dy = y; dy < yEnd; dy++) int y = tileYStartPosition.y;
cdfX = 0;
for (int x = halfTileWidth; x < source.Width - halfTileWidth; x += tileWidth)
{ {
Span<TPixel> pixelRow = source.GetPixelRowSpan(dy); tileY = 0;
tileX = 0; int yEnd = Math.Min(y + tileHeight, source.Height);
for (int dx = x; dx < xEnd; dx++) int xEnd = Math.Min(x + tileWidth, source.Width);
for (int dy = y; dy < yEnd; dy++)
{ {
float luminanceEqualized = this.InterpolateBetweenFourTiles(source[dx, dy], cdfData, tileX, tileY, cdfX, tileYStartPosition.cdfY, tileWidth, tileHeight, pixelsInTile); Span<TPixel> pixelRow = source.GetPixelRowSpan(dy);
pixelRow[dx].FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixelRow[dx].ToVector4().W)); tileX = 0;
tileX++; for (int dx = x; dx < xEnd; dx++)
{
float luminanceEqualized = InterpolateBetweenFourTiles(
source[dx, dy],
cdfData,
this.Tiles,
this.Tiles,
tileX,
tileY,
cdfX,
tileYStartPosition.cdfY,
tileWidth,
tileHeight,
luminanceLevels);
pixelRow[dx].FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixelRow[dx].ToVector4().W));
tileX++;
}
tileY++;
} }
tileY++; cdfX++;
} }
});
cdfX++; Span<TPixel> pixels = source.GetPixelSpan();
}
});
Span<TPixel> pixels = source.GetPixelSpan();
// fix left column // Fix left column
this.ProcessBorderColumn(source, pixels, cdfData, 0, tileWidth, tileHeight, xStart: 0, xEnd: halfTileWidth); ProcessBorderColumn(source, pixels, cdfData, 0, tileWidth, tileHeight, xStart: 0, xEnd: halfTileWidth, luminanceLevels);
// fix right column // Fix right column
int rightBorderStartX = ((this.Tiles - 1) * tileWidth) + halfTileWidth; int rightBorderStartX = ((this.Tiles - 1) * tileWidth) + halfTileWidth;
this.ProcessBorderColumn(source, pixels, cdfData, this.Tiles - 1, tileWidth, tileHeight, xStart: rightBorderStartX, xEnd: source.Width); ProcessBorderColumn(source, pixels, cdfData, this.Tiles - 1, tileWidth, tileHeight, xStart: rightBorderStartX, xEnd: source.Width, luminanceLevels);
// fix top row // Fix top row
this.ProcessBorderRow(source, pixels, cdfData, 0, tileWidth, tileHeight, yStart: 0, yEnd: halfTileHeight); ProcessBorderRow(source, pixels, cdfData, 0, tileWidth, tileHeight, yStart: 0, yEnd: halfTileHeight, luminanceLevels);
// fix bottom row // Fix bottom row
int bottomBorderStartY = ((this.Tiles - 1) * tileHeight) + halfTileHeight; int bottomBorderStartY = ((this.Tiles - 1) * tileHeight) + halfTileHeight;
this.ProcessBorderRow(source, pixels, cdfData, this.Tiles - 1, tileWidth, tileHeight, yStart: bottomBorderStartY, yEnd: source.Height); ProcessBorderRow(source, pixels, cdfData, this.Tiles - 1, tileWidth, tileHeight, yStart: bottomBorderStartY, yEnd: source.Height, luminanceLevels);
// left top corner // Left top corner
this.ProcessCornerTile(source, pixels, cdfData[0, 0], xStart: 0, xEnd: halfTileWidth, yStart: 0, yEnd: halfTileHeight, pixelsInTile: pixelsInTile); ProcessCornerTile(source, pixels, cdfData, 0, 0, xStart: 0, xEnd: halfTileWidth, yStart: 0, yEnd: halfTileHeight, luminanceLevels);
// left bottom corner // Left bottom corner
this.ProcessCornerTile(source, pixels, cdfData[0, this.Tiles - 1], xStart: 0, xEnd: halfTileWidth, yStart: bottomBorderStartY, yEnd: source.Height, pixelsInTile: pixelsInTile); ProcessCornerTile(source, pixels, cdfData, 0, this.Tiles - 1, xStart: 0, xEnd: halfTileWidth, yStart: bottomBorderStartY, yEnd: source.Height, luminanceLevels);
// right top corner // Right top corner
this.ProcessCornerTile(source, pixels, cdfData[this.Tiles - 1, 0], xStart: rightBorderStartX, xEnd: source.Width, yStart: 0, yEnd: halfTileHeight, pixelsInTile: pixelsInTile); ProcessCornerTile(source, pixels, cdfData, this.Tiles - 1, 0, xStart: rightBorderStartX, xEnd: source.Width, yStart: 0, yEnd: halfTileHeight, luminanceLevels);
// right bottom corner // Right bottom corner
this.ProcessCornerTile(source, pixels, cdfData[this.Tiles - 1, this.Tiles - 1], xStart: rightBorderStartX, xEnd: source.Width, yStart: bottomBorderStartY, yEnd: source.Height, pixelsInTile: pixelsInTile); ProcessCornerTile(source, pixels, cdfData, this.Tiles - 1, this.Tiles - 1, xStart: rightBorderStartX, xEnd: source.Width, yStart: bottomBorderStartY, yEnd: source.Height, luminanceLevels);
}
} }
/// <summary> /// <summary>
@ -129,18 +149,33 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
/// <param name="source">The source image.</param> /// <param name="source">The source image.</param>
/// <param name="pixels">The output pixels.</param> /// <param name="pixels">The output pixels.</param>
/// <param name="cdfData">The lookup table to remap the grey values.</param> /// <param name="cdfData">The lookup table to remap the grey values.</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="xStart">X start position.</param>
/// <param name="xEnd">X end position.</param> /// <param name="xEnd">X end position.</param>
/// <param name="yStart">Y start position.</param> /// <param name="yStart">Y start position.</param>
/// <param name="yEnd">Y end position.</param> /// <param name="yEnd">Y end position.</param>
/// <param name="pixelsInTile">Pixels in a tile.</param> /// <param name="luminanceLevels">
private void ProcessCornerTile(ImageFrame<TPixel> source, Span<TPixel> pixels, CdfData cdfData, int xStart, int xEnd, int yStart, int yEnd, int pixelsInTile) /// 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(
ImageFrame<TPixel> source,
Span<TPixel> pixels,
CdfTileData cdfData,
int cdfX,
int cdfY,
int xStart,
int xEnd,
int yStart,
int yEnd,
int luminanceLevels)
{ {
for (int dy = yStart; dy < yEnd; dy++) for (int dy = yStart; dy < yEnd; dy++)
{ {
for (int dx = xStart; dx < xEnd; dx++) for (int dx = xStart; dx < xEnd; dx++)
{ {
float luminanceEqualized = cdfData.RemapGreyValue(this.GetLuminance(source[dx, dy], this.LuminanceLevels), pixelsInTile); float luminanceEqualized = cdfData.RemapGreyValue(cdfX, cdfY, GetLuminance(source[dx, dy], luminanceLevels));
pixels[(dy * source.Width) + dx].FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, source[dx, dy].ToVector4().W)); pixels[(dy * source.Width) + dx].FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, source[dx, dy].ToVector4().W));
} }
} }
@ -157,11 +192,23 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
/// <param name="tileHeight">The height 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="xStart">X start position in the image.</param>
/// <param name="xEnd">X end position of the image.</param> /// <param name="xEnd">X end position of the image.</param>
private void ProcessBorderColumn(ImageFrame<TPixel> source, Span<TPixel> pixels, CdfData[,] cdfData, int cdfX, int tileWidth, int tileHeight, int xStart, int xEnd) /// <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(
ImageFrame<TPixel> source,
Span<TPixel> pixels,
CdfTileData cdfData,
int cdfX,
int tileWidth,
int tileHeight,
int xStart,
int xEnd,
int luminanceLevels)
{ {
int halfTileWidth = tileWidth / 2; int halfTileWidth = tileWidth / 2;
int halfTileHeight = tileHeight / 2; int halfTileHeight = tileHeight / 2;
int pixelsInTile = tileWidth * tileHeight;
int cdfY = 0; int cdfY = 0;
for (int y = halfTileHeight; y < source.Height - halfTileHeight; y += tileHeight) for (int y = halfTileHeight; y < source.Height - halfTileHeight; y += tileHeight)
@ -173,7 +220,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
int tileX = halfTileWidth; int tileX = halfTileWidth;
for (int dx = xStart; dx < xEnd; dx++) for (int dx = xStart; dx < xEnd; dx++)
{ {
float luminanceEqualized = this.InterpolateBetweenTwoTiles(source[dx, dy], cdfData[cdfX, cdfY], cdfData[cdfX, cdfY + 1], tileY, tileHeight, pixelsInTile); float luminanceEqualized = InterpolateBetweenTwoTiles(source[dx, dy], cdfData, cdfX, cdfY, cdfX, cdfY + 1, tileY, tileHeight, luminanceLevels);
pixels[(dy * source.Width) + dx].FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, source[dx, dy].ToVector4().W)); pixels[(dy * source.Width) + dx].FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, source[dx, dy].ToVector4().W));
tileX++; tileX++;
} }
@ -196,11 +243,23 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
/// <param name="tileHeight">The height of a tile.</param> /// <param name="tileHeight">The height of a tile.</param>
/// <param name="yStart">Y start position in the image.</param> /// <param name="yStart">Y start position in the image.</param>
/// <param name="yEnd">Y end position of the image.</param> /// <param name="yEnd">Y end position of the image.</param>
private void ProcessBorderRow(ImageFrame<TPixel> source, Span<TPixel> pixels, CdfData[,] cdfData, int cdfY, int tileWidth, int tileHeight, int yStart, int yEnd) /// <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(
ImageFrame<TPixel> source,
Span<TPixel> pixels,
CdfTileData cdfData,
int cdfY,
int tileWidth,
int tileHeight,
int yStart,
int yEnd,
int luminanceLevels)
{ {
int halfTileWidth = tileWidth / 2; int halfTileWidth = tileWidth / 2;
int halfTileHeight = tileHeight / 2; int halfTileHeight = tileHeight / 2;
int pixelsInTile = tileWidth * tileHeight;
int cdfX = 0; int cdfX = 0;
for (int x = halfTileWidth; x < source.Width - halfTileWidth; x += tileWidth) for (int x = halfTileWidth; x < source.Width - halfTileWidth; x += tileWidth)
@ -212,7 +271,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
int xLimit = Math.Min(x + tileWidth, source.Width - 1); int xLimit = Math.Min(x + tileWidth, source.Width - 1);
for (int dx = x; dx < xLimit; dx++) for (int dx = x; dx < xLimit; dx++)
{ {
float luminanceEqualized = this.InterpolateBetweenTwoTiles(source[dx, dy], cdfData[cdfX, cdfY], cdfData[cdfX + 1, cdfY], tileX, tileWidth, pixelsInTile); float luminanceEqualized = InterpolateBetweenTwoTiles(source[dx, dy], cdfData, cdfX, cdfY, cdfX + 1, cdfY, tileX, tileWidth, luminanceLevels);
pixels[(dy * source.Width) + dx].FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, source[dx, dy].ToVector4().W)); pixels[(dy * source.Width) + dx].FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, source[dx, dy].ToVector4().W));
tileX++; tileX++;
} }
@ -229,54 +288,83 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
/// </summary> /// </summary>
/// <param name="sourcePixel">The pixel to remap the grey value from.</param> /// <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="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="tileX">X position inside the tile.</param>
/// <param name="tileY">Y 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="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="cdfY">Y index of the top left lookup table to use.</param>
/// <param name="tileWidth">Width of one tile in pixels.</param> /// <param name="tileWidth">Width of one tile in pixels.</param>
/// <param name="tileHeight">Height of one tile in pixels.</param> /// <param name="tileHeight">Height of one tile in pixels.</param>
/// <param name="pixelsInTile">Amount of pixels in one 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> /// <returns>A re-mapped grey value.</returns>
private float InterpolateBetweenFourTiles(TPixel sourcePixel, CdfData[,] cdfData, int tileX, int tileY, int cdfX, int cdfY, int tileWidth, int tileHeight, int pixelsInTile) [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 = this.GetLuminance(sourcePixel, this.LuminanceLevels); int luminance = GetLuminance(sourcePixel, luminanceLevels);
float tx = tileX / (float)(tileWidth - 1); float tx = tileX / (float)(tileWidth - 1);
float ty = tileY / (float)(tileHeight - 1); float ty = tileY / (float)(tileHeight - 1);
int yTop = cdfY; int yTop = cdfY;
int yBottom = Math.Min(this.Tiles - 1, yTop + 1); int yBottom = Math.Min(tileCountY - 1, yTop + 1);
int xLeft = cdfX; int xLeft = cdfX;
int xRight = Math.Min(this.Tiles - 1, xLeft + 1); int xRight = Math.Min(tileCountX - 1, xLeft + 1);
float cdfLeftTopLuminance = cdfData[xLeft, yTop].RemapGreyValue(luminance, pixelsInTile);
float cdfRightTopLuminance = cdfData[xRight, yTop].RemapGreyValue(luminance, pixelsInTile);
float cdfLeftBottomLuminance = cdfData[xLeft, yBottom].RemapGreyValue(luminance, pixelsInTile);
float cdfRightBottomLuminance = cdfData[xRight, yBottom].RemapGreyValue(luminance, pixelsInTile);
float luminanceEqualized = this.BilinearInterpolation(tx, ty, cdfLeftTopLuminance, cdfRightTopLuminance, cdfLeftBottomLuminance, cdfRightBottomLuminance);
return luminanceEqualized; 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> /// <summary>
/// Linear interpolation between two tiles. /// Linear interpolation between two tiles.
/// </summary> /// </summary>
/// <param name="sourcePixel">The pixel to remap the grey value from.</param> /// <param name="sourcePixel">The pixel to remap the grey value from.</param>
/// <param name="cdfData1">First lookup table.</param> /// <param name="cdfData">The CDF lookup map.</param>
/// <param name="cdfData2">Second lookup table.</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="tilePos">Position inside the tile.</param>
/// <param name="tileWidth">Width of the tile.</param> /// <param name="tileWidth">Width of the tile.</param>
/// <param name="pixelsInTile">Pixels in one 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> /// <returns>A re-mapped grey value.</returns>
private float InterpolateBetweenTwoTiles(TPixel sourcePixel, CdfData cdfData1, CdfData cdfData2, int tilePos, int tileWidth, int pixelsInTile) [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 = this.GetLuminance(sourcePixel, this.LuminanceLevels); int luminance = GetLuminance(sourcePixel, luminanceLevels);
float tx = tilePos / (float)(tileWidth - 1); float tx = tilePos / (float)(tileWidth - 1);
float cdfLuminance1 = cdfData1.RemapGreyValue(luminance, pixelsInTile); float cdfLuminance1 = cdfData.RemapGreyValue(tileX1, tileY1, luminance);
float cdfLuminance2 = cdfData2.RemapGreyValue(luminance, pixelsInTile); float cdfLuminance2 = cdfData.RemapGreyValue(tileX2, tileY2, luminance);
float luminanceEqualized = this.LinearInterpolation(cdfLuminance1, cdfLuminance2, tx); return LinearInterpolation(cdfLuminance1, cdfLuminance2, tx);
return luminanceEqualized;
} }
/// <summary> /// <summary>
@ -289,10 +377,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
/// <param name="lb">Luminance from left bottom tile.</param> /// <param name="lb">Luminance from left bottom tile.</param>
/// <param name="rb">Luminance from right bottom tile.</param> /// <param name="rb">Luminance from right bottom tile.</param>
/// <returns>Interpolated Luminance.</returns> /// <returns>Interpolated Luminance.</returns>
private float BilinearInterpolation(float tx, float ty, float lt, float rt, float lb, float rb) [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);
return this.LinearInterpolation(this.LinearInterpolation(lt, rt, tx), this.LinearInterpolation(lb, rb, tx), ty);
}
/// <summary> /// <summary>
/// Linear interpolation between two grey values. /// Linear interpolation between two grey values.
@ -301,113 +387,124 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
/// <param name="right">The right 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> /// <param name="t">The interpolation value between the two values in the range of [0, 1].</param>
/// <returns>The interpolated value.</returns> /// <returns>The interpolated value.</returns>
private float LinearInterpolation(float left, float right, float t) [MethodImpl(InliningOptions.ShortMethod)]
{ private static float LinearInterpolation(float left, float right, float t) => left + ((right - left) * t);
return left + ((right - left) * t);
}
/// <summary> /// <summary>
/// Calculates the lookup tables for each tile of the image. /// Contains the results of the cumulative distribution function for all tiles.
/// </summary> /// </summary>
/// <param name="source">The input image for which the tiles will be calculated.</param> private sealed class CdfTileData : IDisposable
/// <param name="configuration">The configuration.</param>
/// <param name="numTilesX">Number of tiles in the X Direction.</param>
/// <param name="numTilesY">Number of tiles in Y Direction.</param>
/// <param name="tileWidth">Width in pixels of one tile.</param>
/// <param name="tileHeight">Height in pixels of one tile.</param>
/// <returns>All lookup tables for each tile in the image.</returns>
private CdfData[,] CalculateLookupTables(ImageFrame<TPixel> source, Configuration configuration, int numTilesX, int numTilesY, int tileWidth, int tileHeight)
{ {
MemoryAllocator memoryAllocator = configuration.MemoryAllocator; private readonly Configuration configuration;
var cdfData = new CdfData[numTilesX, numTilesY]; private readonly Buffer2D<int> cdfMinBuffer2D;
int pixelsInTile = tileWidth * tileHeight; private readonly Buffer2D<int> cdfLutBuffer2D;
private readonly Buffer2D<int> histogramBuffer2D;
var tileYStartPositions = new List<(int y, int cdfY)>(); private readonly int pixelsInTile;
int cdfY = 0; private readonly int tileWidth;
for (int y = 0; y < source.Height; y += tileHeight) private readonly int tileHeight;
private readonly int luminanceLevels;
private readonly List<(int y, int cdfY)> tileYStartPositions;
public CdfTileData(
Configuration configuration,
int sourceHeight,
int tileCountX,
int tileCountY,
int tileWidth,
int tileHeight,
int luminanceLevels)
{ {
tileYStartPositions.Add((y, cdfY)); this.configuration = configuration;
cdfY++; MemoryAllocator memoryAllocator = configuration.MemoryAllocator;
this.luminanceLevels = luminanceLevels;
this.cdfMinBuffer2D = memoryAllocator.Allocate2D<int>(tileCountX, tileCountY);
this.cdfLutBuffer2D = memoryAllocator.Allocate2D<int>(tileCountX * luminanceLevels, tileCountY);
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++;
}
// Use 2D to avoid rent/return per iteration.
this.histogramBuffer2D = memoryAllocator.Allocate2D<int>(luminanceLevels, this.tileYStartPositions.Count);
} }
Parallel.ForEach(tileYStartPositions, new ParallelOptions() { MaxDegreeOfParallelism = configuration.MaxDegreeOfParallelism }, (tileYStartPosition) => public void CalculateLookupTables(ImageFrame<TPixel> source, HistogramEqualizationProcessor<TPixel> processor)
{ {
using (System.Buffers.IMemoryOwner<int> histogramBuffer = memoryAllocator.Allocate<int>(this.LuminanceLevels, AllocationOptions.Clean)) Parallel.For(
using (System.Buffers.IMemoryOwner<int> cdfBuffer = memoryAllocator.Allocate<int>(this.LuminanceLevels, AllocationOptions.Clean)) 0,
this.tileYStartPositions.Count,
new ParallelOptions() { MaxDegreeOfParallelism = this.configuration.MaxDegreeOfParallelism },
index =>
{ {
Span<int> histogram = this.histogramBuffer2D.GetRowSpan(index);
int cdfX = 0; int cdfX = 0;
int y = tileYStartPosition.y; int cdfY = this.tileYStartPositions[index].cdfY;
for (int x = 0; x < source.Width; x += tileWidth) int y = this.tileYStartPositions[index].y;
int endY = Math.Min(y + this.tileHeight, source.Height);
for (int x = 0; x < source.Width; x += this.tileWidth)
{ {
Span<int> histogram = histogramBuffer.GetSpan();
Span<int> cdf = cdfBuffer.GetSpan();
histogram.Clear(); histogram.Clear();
cdf.Clear(); Span<int> cdf = this.GetCdfLutSpan(cdfX, index);
int ylimit = Math.Min(y + tileHeight, source.Height);
int xlimit = Math.Min(x + tileWidth, source.Width); int xlimit = Math.Min(x + this.tileWidth, source.Width);
for (int dy = y; dy < ylimit; dy++) for (int dy = y; dy < endY; dy++)
{ {
Span<TPixel> sourceRowSpan = source.GetPixelRowSpan(dy);
for (int dx = x; dx < xlimit; dx++) for (int dx = x; dx < xlimit; dx++)
{ {
int luminace = this.GetLuminance(source[dx, dy], this.LuminanceLevels); int luminace = GetLuminance(sourceRowSpan[dx], this.luminanceLevels);
histogram[luminace]++; histogram[luminace]++;
} }
} }
if (this.ClipHistogramEnabled) if (processor.ClipHistogramEnabled)
{ {
this.ClipHistogram(histogram, this.ClipLimitPercentage, pixelsInTile); processor.ClipHistogram(histogram, processor.ClipLimitPercentage, this.pixelsInTile);
} }
int cdfMin = this.CalculateCdf(cdf, histogram, histogram.Length - 1); this.cdfMinBuffer2D[cdfX, cdfY] = processor.CalculateCdf(cdf, histogram, histogram.Length - 1);
var currentCdf = new CdfData(cdf.ToArray(), cdfMin);
cdfData[cdfX, tileYStartPosition.cdfY] = currentCdf;
cdfX++; cdfX++;
} }
});
cdfY++;
}
});
return cdfData;
}
/// <summary>
/// Lookup table for remapping the grey values of one tile.
/// </summary>
private class CdfData
{
/// <summary>
/// Initializes a new instance of the <see cref="CdfData"/> class.
/// </summary>
/// <param name="cdf">The cumulative distribution function, which remaps the grey values.</param>
/// <param name="cdfMin">The minimum value of the cdf.</param>
public CdfData(int[] cdf, int cdfMin)
{
this.Cdf = cdf;
this.CdfMin = cdfMin;
} }
/// <summary> [MethodImpl(InliningOptions.ShortMethod)]
/// Gets the CDF. public Span<int> GetCdfLutSpan(int tileX, int tileY) => this.cdfLutBuffer2D.GetRowSpan(tileY).Slice(tileX * this.luminanceLevels, this.luminanceLevels);
/// </summary>
public int[] Cdf { get; }
/// <summary>
/// Gets minimum value of the cdf.
/// </summary>
public int CdfMin { get; }
/// <summary> /// <summary>
/// Remaps the grey value with the cdf. /// Remaps the grey value with the cdf.
/// </summary> /// </summary>
/// <param name="tilesX">The tiles x-position.</param>
/// <param name="tilesY">The tiles y-position.</param>
/// <param name="luminance">The original luminance.</param> /// <param name="luminance">The original luminance.</param>
/// <param name="pixelsInTile">The number of pixels in the tile.</param>
/// <returns>The remapped luminance.</returns> /// <returns>The remapped luminance.</returns>
public float RemapGreyValue(int luminance, int pixelsInTile) [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()
{ {
return (pixelsInTile - this.CdfMin) == 0 ? this.Cdf[luminance] / (float)pixelsInTile : this.Cdf[luminance] / (float)(pixelsInTile - this.CdfMin); this.cdfMinBuffer2D.Dispose();
this.histogramBuffer2D.Dispose();
this.cdfLutBuffer2D.Dispose();
} }
} }
} }

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

@ -95,7 +95,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
float numberOfPixelsMinusCdfMin = pixeInTile - cdfMin; float numberOfPixelsMinusCdfMin = pixeInTile - cdfMin;
// Map the current pixel to the new equalized value // Map the current pixel to the new equalized value
int luminance = this.GetLuminance(source[x, y], this.LuminanceLevels); int luminance = GetLuminance(source[x, y], this.LuminanceLevels);
float luminanceEqualized = cdf[luminance] / numberOfPixelsMinusCdfMin; float luminanceEqualized = cdf[luminance] / numberOfPixelsMinusCdfMin;
targetPixels[x, y].FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, source[x, y].ToVector4().W)); targetPixels[x, y].FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, source[x, y].ToVector4().W));
@ -189,7 +189,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
int maxIdx = 0; int maxIdx = 0;
for (int idx = 0; idx < greyValues.Length; idx++) for (int idx = 0; idx < greyValues.Length; idx++)
{ {
int luminance = this.GetLuminance(greyValues[idx], luminanceLevels); int luminance = GetLuminance(greyValues[idx], luminanceLevels);
histogram[luminance]++; histogram[luminance]++;
if (luminance > maxIdx) if (luminance > maxIdx)
{ {
@ -212,7 +212,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
{ {
for (int idx = 0; idx < greyValues.Length; idx++) for (int idx = 0; idx < greyValues.Length; idx++)
{ {
int luminance = this.GetLuminance(greyValues[idx], luminanceLevels); int luminance = GetLuminance(greyValues[idx], luminanceLevels);
histogram[luminance]--; histogram[luminance]--;
// If the histogram at the maximum index has changed to 0, search for the next smaller value. // If the histogram at the maximum index has changed to 0, search for the next smaller value.

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

@ -45,7 +45,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
for (int i = 0; i < pixels.Length; i++) for (int i = 0; i < pixels.Length; i++)
{ {
TPixel sourcePixel = pixels[i]; TPixel sourcePixel = pixels[i];
int luminance = this.GetLuminance(sourcePixel, this.LuminanceLevels); int luminance = GetLuminance(sourcePixel, this.LuminanceLevels);
histogram[luminance]++; histogram[luminance]++;
} }
@ -64,7 +64,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
{ {
TPixel sourcePixel = pixels[i]; TPixel sourcePixel = pixels[i];
int luminance = this.GetLuminance(sourcePixel, this.LuminanceLevels); int luminance = GetLuminance(sourcePixel, this.LuminanceLevels);
float luminanceEqualized = cdf[luminance] / numberOfPixelsMinusCdfMin; float luminanceEqualized = cdf[luminance] / numberOfPixelsMinusCdfMin;
pixels[i].FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, sourcePixel.ToVector4().W)); pixels[i].FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, sourcePixel.ToVector4().W));

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

@ -2,6 +2,8 @@
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System; using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Normalization namespace SixLabors.ImageSharp.Processing.Processors.Normalization
@ -13,6 +15,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
internal abstract 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>
@ -23,9 +27,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
protected HistogramEqualizationProcessor(int luminanceLevels, bool clipHistogram, float clipLimitPercentage) protected HistogramEqualizationProcessor(int luminanceLevels, bool clipHistogram, float clipLimitPercentage)
{ {
Guard.MustBeGreaterThan(luminanceLevels, 0, nameof(luminanceLevels)); Guard.MustBeGreaterThan(luminanceLevels, 0, nameof(luminanceLevels));
Guard.MustBeGreaterThan(clipLimitPercentage, 0.0f, nameof(clipLimitPercentage)); Guard.MustBeGreaterThan(clipLimitPercentage, 0F, nameof(clipLimitPercentage));
this.LuminanceLevels = luminanceLevels; this.LuminanceLevels = luminanceLevels;
this.luminanceLevelsFloat = luminanceLevels;
this.ClipHistogramEnabled = clipHistogram; this.ClipHistogramEnabled = clipHistogram;
this.ClipLimitPercentage = clipLimitPercentage; this.ClipLimitPercentage = clipLimitPercentage;
} }
@ -52,14 +57,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
/// <param name="histogram">The histogram of the input image.</param> /// <param name="histogram">The histogram of the input image.</param>
/// <param name="maxIdx">Index of the maximum of the histogram.</param> /// <param name="maxIdx">Index of the maximum of the histogram.</param>
/// <returns>The first none zero value of the cdf.</returns> /// <returns>The first none zero value of the cdf.</returns>
protected int CalculateCdf(Span<int> cdf, Span<int> histogram, int maxIdx) public int CalculateCdf(Span<int> cdf, Span<int> histogram, int maxIdx)
{ {
int histSum = 0; int histSum = 0;
int cdfMin = 0; int cdfMin = 0;
bool cdfMinFound = false; bool cdfMinFound = false;
ref int cdfBase = ref MemoryMarshal.GetReference(cdf);
ref int histogramBase = ref MemoryMarshal.GetReference(histogram);
for (int i = 0; i <= maxIdx; i++) for (int i = 0; i <= maxIdx; i++)
{ {
histSum += histogram[i]; histSum += Unsafe.Add(ref histogramBase, i);
if (!cdfMinFound && histSum != 0) if (!cdfMinFound && histSum != 0)
{ {
cdfMin = histSum; cdfMin = histSum;
@ -67,7 +75,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
} }
// Creating the lookup table: subtracting cdf min, so we do not need to do that inside the for loop // Creating the lookup table: subtracting cdf min, so we do not need to do that inside the for loop
cdf[i] = Math.Max(0, histSum - cdfMin); Unsafe.Add(ref cdfBase, i) = Math.Max(0, histSum - cdfMin);
} }
return cdfMin; return cdfMin;
@ -81,25 +89,28 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
/// <param name="histogram">The histogram to apply the clipping.</param> /// <param name="histogram">The histogram to apply the clipping.</param>
/// <param name="clipLimitPercentage">Histogram clip limit in percent of the total pixels in the tile. Histogram bins which exceed this limit, will be capped at this value.</param> /// <param name="clipLimitPercentage">Histogram clip limit in percent of the total pixels in the tile. Histogram bins which exceed this limit, will be capped at this value.</param>
/// <param name="pixelCount">The numbers of pixels inside the tile.</param> /// <param name="pixelCount">The numbers of pixels inside the tile.</param>
protected void ClipHistogram(Span<int> histogram, float clipLimitPercentage, int pixelCount) public void ClipHistogram(Span<int> histogram, float clipLimitPercentage, int pixelCount)
{ {
int clipLimit = Convert.ToInt32(pixelCount * clipLimitPercentage); int clipLimit = (int)MathF.Round(pixelCount * clipLimitPercentage);
int sumOverClip = 0; int sumOverClip = 0;
ref int histogramBase = ref MemoryMarshal.GetReference(histogram);
for (int i = 0; i < histogram.Length; i++) for (int i = 0; i < histogram.Length; i++)
{ {
if (histogram[i] > clipLimit) ref int histogramLevel = ref Unsafe.Add(ref histogramBase, i);
if (histogramLevel > clipLimit)
{ {
sumOverClip += histogram[i] - clipLimit; sumOverClip += histogramLevel - clipLimit;
histogram[i] = clipLimit; histogramLevel = clipLimit;
} }
} }
int addToEachBin = sumOverClip > 0 ? (int)Math.Floor(sumOverClip / (double)this.LuminanceLevels) : 0; int addToEachBin = sumOverClip > 0 ? (int)MathF.Floor(sumOverClip / this.luminanceLevelsFloat) : 0;
if (addToEachBin > 0) if (addToEachBin > 0)
{ {
for (int i = 0; i < histogram.Length; i++) for (int i = 0; i < histogram.Length; i++)
{ {
histogram[i] += addToEachBin; Unsafe.Add(ref histogramBase, i) += addToEachBin;
} }
} }
} }
@ -109,14 +120,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
/// </summary> /// </summary>
/// <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>
[System.Runtime.CompilerServices.MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
protected int GetLuminance(TPixel sourcePixel, int luminanceLevels) public static int GetLuminance(TPixel sourcePixel, int luminanceLevels)
{ {
// Convert to grayscale using ITU-R Recommendation BT.709 // 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 (int)MathF.Round(((.2126F * vector.X) + (.7152F * vector.Y) + (.0722F * vector.Y)) * (luminanceLevels - 1));
return luminance;
} }
} }
} }

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 |
}
}

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

@ -31,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,
@ -51,23 +52,24 @@ 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(new HistogramEqualizationOptions() image.Mutate(x => x.HistogramEqualization(new HistogramEqualizationOptions()
{ {
LuminanceLevels = luminanceLevels 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);
}
} }
} }
} }
@ -80,12 +82,12 @@ namespace SixLabors.ImageSharp.Tests.Processing.Normalization
using (Image<TPixel> image = provider.GetImage()) using (Image<TPixel> image = provider.GetImage())
{ {
var options = new HistogramEqualizationOptions() var options = new HistogramEqualizationOptions()
{ {
Method = HistogramEqualizationMethod.AdaptiveSlidingWindow, Method = HistogramEqualizationMethod.AdaptiveSlidingWindow,
LuminanceLevels = 256, LuminanceLevels = 256,
ClipHistogram = true, ClipHistogram = true,
Tiles = 15 Tiles = 15
}; };
image.Mutate(x => x.HistogramEqualization(options)); image.Mutate(x => x.HistogramEqualization(options));
image.DebugSave(provider); image.DebugSave(provider);
image.CompareToReferenceOutput(ValidatorComparer, provider); image.CompareToReferenceOutput(ValidatorComparer, provider);
@ -100,12 +102,12 @@ namespace SixLabors.ImageSharp.Tests.Processing.Normalization
using (Image<TPixel> image = provider.GetImage()) using (Image<TPixel> image = provider.GetImage())
{ {
var options = new HistogramEqualizationOptions() var options = new HistogramEqualizationOptions()
{ {
Method = HistogramEqualizationMethod.AdaptiveTileInterpolation, Method = HistogramEqualizationMethod.AdaptiveTileInterpolation,
LuminanceLevels = 256, LuminanceLevels = 256,
ClipHistogram = true, ClipHistogram = true,
Tiles = 10 Tiles = 10
}; };
image.Mutate(x => x.HistogramEqualization(options)); image.Mutate(x => x.HistogramEqualization(options));
image.DebugSave(provider); image.DebugSave(provider);
image.CompareToReferenceOutput(ValidatorComparer, provider); image.CompareToReferenceOutput(ValidatorComparer, provider);

Loading…
Cancel
Save