From 4330c82f28fdd29e8292a8da4de02f769911a348 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 23 Jan 2019 12:03:20 +1100 Subject: [PATCH] 2x faster adaptive tiled processor --- src/ImageSharp/Common/Helpers/ImageMaths.cs | 1 - .../AdaptiveHistEqualizationProcessor.cs | 417 +++++++++++------- .../AdaptiveHistEqualizationSWProcessor.cs | 6 +- .../GlobalHistogramEqualizationProcessor.cs | 4 +- .../HistogramEqualizationProcessor.cs | 43 +- .../General/BasicMath/Round.cs | 22 + .../HistogramEqualizationTests.cs | 70 +-- 7 files changed, 346 insertions(+), 217 deletions(-) create mode 100644 tests/ImageSharp.Benchmarks/General/BasicMath/Round.cs diff --git a/src/ImageSharp/Common/Helpers/ImageMaths.cs b/src/ImageSharp/Common/Helpers/ImageMaths.cs index 0c5b051809..64bcb11c9f 100644 --- a/src/ImageSharp/Common/Helpers/ImageMaths.cs +++ b/src/ImageSharp/Common/Helpers/ImageMaths.cs @@ -5,7 +5,6 @@ using System; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing.Processors.Transforms; using SixLabors.Primitives; namespace SixLabors.ImageSharp diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs index f9190a92b6..20543369fa 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Numerics; +using System.Runtime.CompilerServices; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; @@ -46,81 +47,100 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) { int numberOfPixels = source.Width * source.Height; - int tileWidth = Convert.ToInt32(Math.Ceiling(source.Width / (double)this.Tiles)); - int tileHeight = Convert.ToInt32(Math.Ceiling(source.Height / (double)this.Tiles)); + int tileWidth = (int)MathF.Ceiling(source.Width / (float)this.Tiles); + int tileHeight = (int)MathF.Ceiling(source.Height / (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. - CdfData[,] cdfData = this.CalculateLookupTables(source, configuration, this.Tiles, this.Tiles, tileWidth, tileHeight); - - var tileYStartPositions = new List<(int y, int cdfY)>(); - int cdfY = 0; - for (int y = halfTileHeight; y < source.Height - halfTileHeight; y += tileHeight) + using (var cdfData = new CdfTileData(configuration, sourceRectangle.Height, this.Tiles, this.Tiles, tileWidth, tileHeight, luminanceLevels)) { - tileYStartPositions.Add((y, cdfY)); - cdfY++; - } + cdfData.CalculateLookupTables(source, this); - Parallel.ForEach(tileYStartPositions, new ParallelOptions() { MaxDegreeOfParallelism = configuration.MaxDegreeOfParallelism }, (tileYStartPosition) => - { - int cdfX = 0; - int tileX = 0; - int tileY = 0; - int y = tileYStartPosition.y; + 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)); + cdfY++; + } - cdfX = 0; - for (int x = halfTileWidth; x < source.Width - halfTileWidth; x += tileWidth) + Parallel.ForEach( + tileYStartPositions, + new ParallelOptions() { MaxDegreeOfParallelism = configuration.MaxDegreeOfParallelism }, + tileYStartPosition => { - tileY = 0; - int yEnd = Math.Min(y + tileHeight, source.Height); - int xEnd = Math.Min(x + tileWidth, source.Width); - for (int dy = y; dy < yEnd; dy++) + int cdfX = 0; + int tileX = 0; + int tileY = 0; + int y = tileYStartPosition.y; + + cdfX = 0; + for (int x = halfTileWidth; x < source.Width - halfTileWidth; x += tileWidth) { - Span pixelRow = source.GetPixelRowSpan(dy); - tileX = 0; - for (int dx = x; dx < xEnd; dx++) + tileY = 0; + int yEnd = Math.Min(y + tileHeight, source.Height); + 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); - pixelRow[dx].FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixelRow[dx].ToVector4().W)); - tileX++; + Span pixelRow = source.GetPixelRowSpan(dy); + tileX = 0; + 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 pixels = source.GetPixelSpan(); + Span pixels = source.GetPixelSpan(); - // fix left column - this.ProcessBorderColumn(source, pixels, cdfData, 0, tileWidth, tileHeight, xStart: 0, xEnd: halfTileWidth); + // Fix left column + ProcessBorderColumn(source, pixels, cdfData, 0, tileWidth, tileHeight, xStart: 0, xEnd: halfTileWidth, luminanceLevels); - // fix right column - int rightBorderStartX = ((this.Tiles - 1) * tileWidth) + halfTileWidth; - this.ProcessBorderColumn(source, pixels, cdfData, this.Tiles - 1, tileWidth, tileHeight, xStart: rightBorderStartX, xEnd: source.Width); + // Fix right column + int rightBorderStartX = ((this.Tiles - 1) * tileWidth) + halfTileWidth; + ProcessBorderColumn(source, pixels, cdfData, this.Tiles - 1, tileWidth, tileHeight, xStart: rightBorderStartX, xEnd: source.Width, luminanceLevels); - // fix top row - this.ProcessBorderRow(source, pixels, cdfData, 0, tileWidth, tileHeight, yStart: 0, yEnd: halfTileHeight); + // Fix top row + ProcessBorderRow(source, pixels, cdfData, 0, tileWidth, tileHeight, yStart: 0, yEnd: halfTileHeight, luminanceLevels); - // fix bottom row - int bottomBorderStartY = ((this.Tiles - 1) * tileHeight) + halfTileHeight; - this.ProcessBorderRow(source, pixels, cdfData, this.Tiles - 1, tileWidth, tileHeight, yStart: bottomBorderStartY, yEnd: source.Height); + // Fix bottom row + int bottomBorderStartY = ((this.Tiles - 1) * tileHeight) + halfTileHeight; + ProcessBorderRow(source, pixels, cdfData, this.Tiles - 1, tileWidth, tileHeight, yStart: bottomBorderStartY, yEnd: source.Height, luminanceLevels); - // left top corner - this.ProcessCornerTile(source, pixels, cdfData[0, 0], xStart: 0, xEnd: halfTileWidth, yStart: 0, yEnd: halfTileHeight, pixelsInTile: pixelsInTile); + // Left top corner + ProcessCornerTile(source, pixels, cdfData, 0, 0, xStart: 0, xEnd: halfTileWidth, yStart: 0, yEnd: halfTileHeight, luminanceLevels); - // left bottom corner - this.ProcessCornerTile(source, pixels, cdfData[0, this.Tiles - 1], xStart: 0, xEnd: halfTileWidth, yStart: bottomBorderStartY, yEnd: source.Height, pixelsInTile: pixelsInTile); + // Left bottom corner + ProcessCornerTile(source, pixels, cdfData, 0, this.Tiles - 1, xStart: 0, xEnd: halfTileWidth, yStart: bottomBorderStartY, yEnd: source.Height, luminanceLevels); - // right top corner - this.ProcessCornerTile(source, pixels, cdfData[this.Tiles - 1, 0], xStart: rightBorderStartX, xEnd: source.Width, yStart: 0, yEnd: halfTileHeight, pixelsInTile: pixelsInTile); + // Right top corner + ProcessCornerTile(source, pixels, cdfData, this.Tiles - 1, 0, xStart: rightBorderStartX, xEnd: source.Width, yStart: 0, yEnd: halfTileHeight, luminanceLevels); - // 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); + // Right bottom corner + ProcessCornerTile(source, pixels, cdfData, this.Tiles - 1, this.Tiles - 1, xStart: rightBorderStartX, xEnd: source.Width, yStart: bottomBorderStartY, yEnd: source.Height, luminanceLevels); + } } /// @@ -129,18 +149,33 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization /// The source image. /// The output pixels. /// The lookup table to remap the grey values. + /// The x-position in the CDF lookup map. + /// The y-position in the CDF lookup map. /// X start position. /// X end position. /// Y start position. /// Y end position. - /// Pixels in a tile. - private void ProcessCornerTile(ImageFrame source, Span 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. + /// + private static void ProcessCornerTile( + ImageFrame source, + Span 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 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)); } } @@ -157,11 +192,23 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization /// The height of a tile. /// X start position in the image. /// X end position of the image. - private void ProcessBorderColumn(ImageFrame source, Span pixels, CdfData[,] cdfData, int cdfX, int tileWidth, int tileHeight, int xStart, int xEnd) + /// + /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images + /// or 65536 for 16-bit grayscale images. + /// + private static void ProcessBorderColumn( + ImageFrame source, + Span pixels, + CdfTileData cdfData, + int cdfX, + int tileWidth, + int tileHeight, + int xStart, + int xEnd, + int luminanceLevels) { int halfTileWidth = tileWidth / 2; int halfTileHeight = tileHeight / 2; - int pixelsInTile = tileWidth * tileHeight; int cdfY = 0; for (int y = halfTileHeight; y < source.Height - halfTileHeight; y += tileHeight) @@ -173,7 +220,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization int tileX = halfTileWidth; 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)); tileX++; } @@ -196,11 +243,23 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization /// The height of a tile. /// Y start position in the image. /// Y end position of the image. - private void ProcessBorderRow(ImageFrame source, Span pixels, CdfData[,] cdfData, int cdfY, int tileWidth, int tileHeight, int yStart, int yEnd) + /// + /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images + /// or 65536 for 16-bit grayscale images. + /// + private static void ProcessBorderRow( + ImageFrame source, + Span pixels, + CdfTileData cdfData, + int cdfY, + int tileWidth, + int tileHeight, + int yStart, + int yEnd, + int luminanceLevels) { int halfTileWidth = tileWidth / 2; int halfTileHeight = tileHeight / 2; - int pixelsInTile = tileWidth * tileHeight; int cdfX = 0; 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); 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)); tileX++; } @@ -229,54 +288,83 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization /// /// The pixel to remap the grey value from. /// The pre-computed lookup tables to remap the grey values for each tiles. + /// The number of tiles in the x-direction. + /// The number of tiles in the y-direction. /// X position inside the tile. /// Y position inside the tile. /// X index of the top left lookup table to use. /// Y index of the top left lookup table to use. /// Width of one tile in pixels. /// Height of one tile in pixels. - /// Amount of pixels in one tile. + /// + /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images + /// or 65536 for 16-bit grayscale images. + /// /// A re-mapped grey value. - 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 ty = tileY / (float)(tileHeight - 1); int yTop = cdfY; - int yBottom = Math.Min(this.Tiles - 1, yTop + 1); + int yBottom = Math.Min(tileCountY - 1, yTop + 1); int xLeft = cdfX; - int xRight = Math.Min(this.Tiles - 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); + int xRight = Math.Min(tileCountX - 1, xLeft + 1); - 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); } /// /// Linear interpolation between two tiles. /// /// The pixel to remap the grey value from. - /// First lookup table. - /// Second lookup table. + /// The CDF lookup map. + /// X position inside the first tile. + /// Y position inside the first tile. + /// X position inside the second tile. + /// Y position inside the second tile. /// Position inside the tile. /// Width of the tile. - /// Pixels in one tile. + /// + /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images + /// or 65536 for 16-bit grayscale images. + /// /// A re-mapped grey value. - 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 cdfLuminance1 = cdfData1.RemapGreyValue(luminance, pixelsInTile); - float cdfLuminance2 = cdfData2.RemapGreyValue(luminance, pixelsInTile); - float luminanceEqualized = this.LinearInterpolation(cdfLuminance1, cdfLuminance2, tx); - - return luminanceEqualized; + float cdfLuminance1 = cdfData.RemapGreyValue(tileX1, tileY1, luminance); + float cdfLuminance2 = cdfData.RemapGreyValue(tileX2, tileY2, luminance); + return LinearInterpolation(cdfLuminance1, cdfLuminance2, tx); } /// @@ -289,10 +377,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization /// Luminance from left bottom tile. /// Luminance from right bottom tile. /// Interpolated Luminance. - private float BilinearInterpolation(float tx, float ty, float lt, float rt, float lb, float rb) - { - return this.LinearInterpolation(this.LinearInterpolation(lt, rt, tx), this.LinearInterpolation(lb, rb, tx), ty); - } + [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); /// /// Linear interpolation between two grey values. @@ -301,113 +387,124 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization /// The right value. /// The interpolation value between the two values in the range of [0, 1]. /// The interpolated value. - private float LinearInterpolation(float left, float right, float t) - { - return left + ((right - left) * t); - } + [MethodImpl(InliningOptions.ShortMethod)] + private static float LinearInterpolation(float left, float right, float t) => left + ((right - left) * t); /// - /// Calculates the lookup tables for each tile of the image. + /// Contains the results of the cumulative distribution function for all tiles. /// - /// The input image for which the tiles will be calculated. - /// The configuration. - /// Number of tiles in the X Direction. - /// Number of tiles in Y Direction. - /// Width in pixels of one tile. - /// Height in pixels of one tile. - /// All lookup tables for each tile in the image. - private CdfData[,] CalculateLookupTables(ImageFrame source, Configuration configuration, int numTilesX, int numTilesY, int tileWidth, int tileHeight) + private sealed class CdfTileData : IDisposable { - MemoryAllocator memoryAllocator = configuration.MemoryAllocator; - var cdfData = new CdfData[numTilesX, numTilesY]; - int pixelsInTile = tileWidth * tileHeight; - - var tileYStartPositions = new List<(int y, int cdfY)>(); - int cdfY = 0; - for (int y = 0; y < source.Height; y += tileHeight) + private readonly Configuration configuration; + private readonly Buffer2D cdfMinBuffer2D; + private readonly Buffer2D cdfLutBuffer2D; + private readonly Buffer2D histogramBuffer2D; + private readonly int pixelsInTile; + 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 sourceHeight, + int tileCountX, + int tileCountY, + int tileWidth, + int tileHeight, + int luminanceLevels) { - tileYStartPositions.Add((y, cdfY)); - cdfY++; + this.configuration = configuration; + MemoryAllocator memoryAllocator = configuration.MemoryAllocator; + this.luminanceLevels = luminanceLevels; + this.cdfMinBuffer2D = memoryAllocator.Allocate2D(tileCountX, tileCountY); + this.cdfLutBuffer2D = memoryAllocator.Allocate2D(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(luminanceLevels, this.tileYStartPositions.Count); } - Parallel.ForEach(tileYStartPositions, new ParallelOptions() { MaxDegreeOfParallelism = configuration.MaxDegreeOfParallelism }, (tileYStartPosition) => + public void CalculateLookupTables(ImageFrame source, HistogramEqualizationProcessor processor) { - using (System.Buffers.IMemoryOwner histogramBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean)) - using (System.Buffers.IMemoryOwner cdfBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean)) + Parallel.For( + 0, + this.tileYStartPositions.Count, + new ParallelOptions() { MaxDegreeOfParallelism = this.configuration.MaxDegreeOfParallelism }, + index => { + Span histogram = this.histogramBuffer2D.GetRowSpan(index); + int cdfX = 0; - int y = tileYStartPosition.y; - for (int x = 0; x < source.Width; x += tileWidth) + int cdfY = this.tileYStartPositions[index].cdfY; + 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 histogram = histogramBuffer.GetSpan(); - Span cdf = cdfBuffer.GetSpan(); histogram.Clear(); - cdf.Clear(); - int ylimit = Math.Min(y + tileHeight, source.Height); - int xlimit = Math.Min(x + tileWidth, source.Width); - for (int dy = y; dy < ylimit; dy++) + Span cdf = this.GetCdfLutSpan(cdfX, index); + + int xlimit = Math.Min(x + this.tileWidth, source.Width); + for (int dy = y; dy < endY; dy++) { + Span sourceRowSpan = source.GetPixelRowSpan(dy); + 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]++; } } - 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); - var currentCdf = new CdfData(cdf.ToArray(), cdfMin); - cdfData[cdfX, tileYStartPosition.cdfY] = currentCdf; + this.cdfMinBuffer2D[cdfX, cdfY] = processor.CalculateCdf(cdf, histogram, histogram.Length - 1); cdfX++; } - - cdfY++; - } - }); - - return cdfData; - } - - /// - /// Lookup table for remapping the grey values of one tile. - /// - private class CdfData - { - /// - /// Initializes a new instance of the class. - /// - /// The cumulative distribution function, which remaps the grey values. - /// The minimum value of the cdf. - public CdfData(int[] cdf, int cdfMin) - { - this.Cdf = cdf; - this.CdfMin = cdfMin; + }); } - /// - /// Gets the CDF. - /// - public int[] Cdf { get; } - - /// - /// Gets minimum value of the cdf. - /// - public int CdfMin { get; } + [MethodImpl(InliningOptions.ShortMethod)] + public Span GetCdfLutSpan(int tileX, int tileY) => this.cdfLutBuffer2D.GetRowSpan(tileY).Slice(tileX * this.luminanceLevels, this.luminanceLevels); /// /// Remaps the grey value with the cdf. /// + /// The tiles x-position. + /// The tiles y-position. /// The original luminance. - /// The number of pixels in the tile. /// The remapped luminance. - 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 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(); } } } diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs index 7d82b413fd..2c24d79129 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs @@ -95,7 +95,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization float numberOfPixelsMinusCdfMin = pixeInTile - cdfMin; // 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; 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; for (int idx = 0; idx < greyValues.Length; idx++) { - int luminance = this.GetLuminance(greyValues[idx], luminanceLevels); + int luminance = GetLuminance(greyValues[idx], luminanceLevels); histogram[luminance]++; if (luminance > maxIdx) { @@ -212,7 +212,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization { for (int idx = 0; idx < greyValues.Length; idx++) { - int luminance = this.GetLuminance(greyValues[idx], luminanceLevels); + int luminance = GetLuminance(greyValues[idx], luminanceLevels); histogram[luminance]--; // If the histogram at the maximum index has changed to 0, search for the next smaller value. diff --git a/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs index 5cad9e60a7..daa045afb3 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs +++ b/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++) { TPixel sourcePixel = pixels[i]; - int luminance = this.GetLuminance(sourcePixel, this.LuminanceLevels); + int luminance = GetLuminance(sourcePixel, this.LuminanceLevels); histogram[luminance]++; } @@ -64,7 +64,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization { TPixel sourcePixel = pixels[i]; - int luminance = this.GetLuminance(sourcePixel, this.LuminanceLevels); + int luminance = GetLuminance(sourcePixel, this.LuminanceLevels); float luminanceEqualized = cdf[luminance] / numberOfPixelsMinusCdfMin; pixels[i].FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, sourcePixel.ToVector4().W)); diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs index 2763fcddd8..9290d941cc 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs @@ -2,6 +2,8 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors.Normalization @@ -13,6 +15,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization internal abstract class HistogramEqualizationProcessor : ImageProcessor where TPixel : struct, IPixel { + private readonly float luminanceLevelsFloat; + /// /// Initializes a new instance of the class. /// @@ -23,9 +27,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization protected HistogramEqualizationProcessor(int luminanceLevels, bool clipHistogram, float clipLimitPercentage) { Guard.MustBeGreaterThan(luminanceLevels, 0, nameof(luminanceLevels)); - Guard.MustBeGreaterThan(clipLimitPercentage, 0.0f, nameof(clipLimitPercentage)); + Guard.MustBeGreaterThan(clipLimitPercentage, 0F, nameof(clipLimitPercentage)); this.LuminanceLevels = luminanceLevels; + this.luminanceLevelsFloat = luminanceLevels; this.ClipHistogramEnabled = clipHistogram; this.ClipLimitPercentage = clipLimitPercentage; } @@ -52,14 +57,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization /// The histogram of the input image. /// Index of the maximum of the histogram. /// The first none zero value of the cdf. - protected int CalculateCdf(Span cdf, Span histogram, int maxIdx) + public int CalculateCdf(Span cdf, Span histogram, int maxIdx) { int histSum = 0; int cdfMin = 0; bool cdfMinFound = false; + ref int cdfBase = ref MemoryMarshal.GetReference(cdf); + ref int histogramBase = ref MemoryMarshal.GetReference(histogram); + for (int i = 0; i <= maxIdx; i++) { - histSum += histogram[i]; + histSum += Unsafe.Add(ref histogramBase, i); if (!cdfMinFound && histSum != 0) { 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 - cdf[i] = Math.Max(0, histSum - cdfMin); + Unsafe.Add(ref cdfBase, i) = Math.Max(0, histSum - cdfMin); } return cdfMin; @@ -81,25 +89,28 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization /// The histogram to apply the clipping. /// Histogram clip limit in percent of the total pixels in the tile. Histogram bins which exceed this limit, will be capped at this value. /// The numbers of pixels inside the tile. - protected void ClipHistogram(Span histogram, float clipLimitPercentage, int pixelCount) + public void ClipHistogram(Span histogram, float clipLimitPercentage, int pixelCount) { - int clipLimit = Convert.ToInt32(pixelCount * clipLimitPercentage); + int clipLimit = (int)MathF.Round(pixelCount * clipLimitPercentage); int sumOverClip = 0; + ref int histogramBase = ref MemoryMarshal.GetReference(histogram); + for (int i = 0; i < histogram.Length; i++) { - if (histogram[i] > clipLimit) + ref int histogramLevel = ref Unsafe.Add(ref histogramBase, i); + if (histogramLevel > clipLimit) { - sumOverClip += histogram[i] - clipLimit; - histogram[i] = clipLimit; + sumOverClip += histogramLevel - 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) { 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 /// /// The pixel to get the luminance from /// The number of luminance levels (256 for 8 bit, 65536 for 16 bit grayscale images) - [System.Runtime.CompilerServices.MethodImpl(InliningOptions.ShortMethod)] - protected int GetLuminance(TPixel sourcePixel, int luminanceLevels) + [MethodImpl(InliningOptions.ShortMethod)] + public static int GetLuminance(TPixel sourcePixel, int luminanceLevels) { // Convert to grayscale using ITU-R Recommendation BT.709 var vector = sourcePixel.ToVector4(); - int luminance = Convert.ToInt32(((.2126F * vector.X) + (.7152F * vector.Y) + (.0722F * vector.Y)) * (luminanceLevels - 1)); - - return luminance; + return (int)MathF.Round(((.2126F * vector.X) + (.7152F * vector.Y) + (.0722F * vector.Y)) * (luminanceLevels - 1)); } } -} +} \ No newline at end of file diff --git a/tests/ImageSharp.Benchmarks/General/BasicMath/Round.cs b/tests/ImageSharp.Benchmarks/General/BasicMath/Round.cs new file mode 100644 index 0000000000..2c18b2972c --- /dev/null +++ b/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 | + } +} diff --git a/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs b/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs index 984993748c..41c399bd5b 100644 --- a/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs +++ b/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 }; - var image = new Image(8, 8); - for (int y = 0; y < 8; y++) + using (var image = new Image(8, 8)) { - for (int x = 0; x < 8; x++) + for (int y = 0; y < 8; y++) { - byte luminance = pixels[y * 8 + x]; - image[x, y] = new Rgba32(luminance, luminance, luminance); + for (int x = 0; x < 8; x++) + { + byte luminance = pixels[y * 8 + x]; + image[x, y] = new Rgba32(luminance, luminance, luminance); + } } - } - byte[] expected = new byte[] - { + byte[] expected = new byte[] + { 0, 12, 53, 32, 146, 53, 174, 53, 57, 32, 12, 227, 219, 202, 32, 154, 65, 85, 93, 239, 251, 227, 65, 158, @@ -51,23 +52,24 @@ namespace SixLabors.ImageSharp.Tests.Processing.Normalization 117, 190, 36, 190, 178, 93, 20, 170, 130, 202, 73, 20, 12, 53, 85, 194, 146, 206, 130, 117, 85, 166, 182, 215 - }; + }; - // Act - image.Mutate(x => x.HistogramEqualization(new HistogramEqualizationOptions() - { - LuminanceLevels = luminanceLevels - })); + // Act + image.Mutate(x => x.HistogramEqualization(new HistogramEqualizationOptions() + { + LuminanceLevels = luminanceLevels + })); - // Assert - for (int y = 0; y < 8; y++) - { - for (int x = 0; x < 8; x++) + // Assert + for (int y = 0; y < 8; y++) { - Rgba32 actual = image[x, y]; - Assert.Equal(expected[y * 8 + x], actual.R); - Assert.Equal(expected[y * 8 + x], actual.G); - Assert.Equal(expected[y * 8 + x], actual.B); + for (int x = 0; x < 8; x++) + { + Rgba32 actual = image[x, y]; + Assert.Equal(expected[y * 8 + x], actual.R); + Assert.Equal(expected[y * 8 + x], actual.G); + Assert.Equal(expected[y * 8 + x], actual.B); + } } } } @@ -80,12 +82,12 @@ namespace SixLabors.ImageSharp.Tests.Processing.Normalization using (Image image = provider.GetImage()) { var options = new HistogramEqualizationOptions() - { - Method = HistogramEqualizationMethod.AdaptiveSlidingWindow, - LuminanceLevels = 256, - ClipHistogram = true, - Tiles = 15 - }; + { + Method = HistogramEqualizationMethod.AdaptiveSlidingWindow, + LuminanceLevels = 256, + ClipHistogram = true, + Tiles = 15 + }; image.Mutate(x => x.HistogramEqualization(options)); image.DebugSave(provider); image.CompareToReferenceOutput(ValidatorComparer, provider); @@ -100,12 +102,12 @@ namespace SixLabors.ImageSharp.Tests.Processing.Normalization using (Image image = provider.GetImage()) { var options = new HistogramEqualizationOptions() - { - Method = HistogramEqualizationMethod.AdaptiveTileInterpolation, - LuminanceLevels = 256, - ClipHistogram = true, - Tiles = 10 - }; + { + Method = HistogramEqualizationMethod.AdaptiveTileInterpolation, + LuminanceLevels = 256, + ClipHistogram = true, + Tiles = 10 + }; image.Mutate(x => x.HistogramEqualization(options)); image.DebugSave(provider); image.CompareToReferenceOutput(ValidatorComparer, provider);