From f85d7359eb06c1b215a66652c9a4c0bf5cf4b085 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 9 Feb 2020 20:41:31 +1100 Subject: [PATCH] Update AdaptiveHistogramEqualizationSlidingWindowProcessor{TPixel}.cs --- ...alizationSlidingWindowProcessor{TPixel}.cs | 425 ++++++++++-------- 1 file changed, 235 insertions(+), 190 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationSlidingWindowProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationSlidingWindowProcessor{TPixel}.cs index 987e4e392f..4d0786947a 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationSlidingWindowProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationSlidingWindowProcessor{TPixel}.cs @@ -66,191 +66,101 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization int halfTileWidth = halfTileHeight; var slidingWindowInfos = new SlidingWindowInfos(tileWidth, tileHeight, halfTileWidth, halfTileHeight, pixelInTile); - using (Buffer2D targetPixels = this.Configuration.MemoryAllocator.Allocate2D(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, - this.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, - this.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, - this.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, - this.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, - this.Configuration)); - - Buffer2D.SwapOrCopyContent(source.PixelBuffer, targetPixels); - } - } - - /// - /// 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. - /// - /// The source image. - /// The memory allocator. - /// The target pixels. - /// about the sliding window dimensions. - /// The y start position. - /// The y end position. - /// if set to true the borders of the image will not be checked. - /// The configuration. - /// Action Delegate. - private Action ProcessSlidingWindow( - ImageFrame source, - MemoryAllocator memoryAllocator, - Buffer2D targetPixels, - SlidingWindowInfos swInfos, - int yStart, - int yEnd, - bool useFastPath, - Configuration configuration) - { - return x => - { - using (IMemoryOwner histogramBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean)) - using (IMemoryOwner histogramBufferCopy = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean)) - using (IMemoryOwner cdfBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean)) - using (IMemoryOwner pixelRowBuffer = memoryAllocator.Allocate(swInfos.TileWidth, AllocationOptions.Clean)) - { - Span histogram = histogramBuffer.GetSpan(); - ref int histogramBase = ref MemoryMarshal.GetReference(histogram); - - Span histogramCopy = histogramBufferCopy.GetSpan(); - ref int histogramCopyBase = ref MemoryMarshal.GetReference(histogramCopy); - - ref int cdfBase = ref MemoryMarshal.GetReference(cdfBuffer.GetSpan()); - - Span 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.ClipLimit); - } - - // 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.PixelInTile - 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); - } - } - }; + // TODO: If the process was able to be switched to operate in parallel rows instead of columns + // then we could take advantage of batching and allocate per-row buffers only once per batch. + using Buffer2D targetPixels = this.Configuration.MemoryAllocator.Allocate2D(source.Width, source.Height); + + // Process the inner tiles, which do not require to check the borders. + var innerOperation = new SlidingWindowOperation( + this.Configuration, + this, + source, + memoryAllocator, + targetPixels, + slidingWindowInfos, + yStart: halfTileHeight, + yEnd: source.Height - halfTileHeight, + useFastPath: true); + + Parallel.For( + halfTileWidth, + source.Width - halfTileWidth, + parallelOptions, + innerOperation.Invoke); + + // Process the left border of the image. + var leftBorderOperation = new SlidingWindowOperation( + this.Configuration, + this, + source, + memoryAllocator, + targetPixels, + slidingWindowInfos, + yStart: 0, + yEnd: source.Height, + useFastPath: false); + + Parallel.For( + 0, + halfTileWidth, + parallelOptions, + leftBorderOperation.Invoke); + + // Process the right border of the image. + var rightBorderOperation = new SlidingWindowOperation( + this.Configuration, + this, + source, + memoryAllocator, + targetPixels, + slidingWindowInfos, + yStart: 0, + yEnd: source.Height, + useFastPath: false); + + Parallel.For( + source.Width - halfTileWidth, + source.Width, + parallelOptions, + rightBorderOperation.Invoke); + + // Process the top border of the image. + var topBorderOperation = new SlidingWindowOperation( + this.Configuration, + this, + source, + memoryAllocator, + targetPixels, + slidingWindowInfos, + yStart: 0, + yEnd: halfTileHeight, + useFastPath: false); + + Parallel.For( + halfTileWidth, + source.Width - halfTileWidth, + parallelOptions, + topBorderOperation.Invoke); + + // Process the bottom border of the image. + var bottomBorderOperation = new SlidingWindowOperation( + this.Configuration, + this, + source, + memoryAllocator, + targetPixels, + slidingWindowInfos, + yStart: source.Height - halfTileHeight, + yEnd: source.Height, + useFastPath: false); + + Parallel.For( + halfTileWidth, + source.Width - halfTileWidth, + parallelOptions, + bottomBorderOperation.Invoke); + + Buffer2D.SwapOrCopyContent(source.PixelBuffer, targetPixels); } /// @@ -371,6 +281,141 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization } } + /// + /// 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. + /// + private readonly struct SlidingWindowOperation + { + private readonly Configuration configuration; + private readonly AdaptiveHistogramEqualizationSlidingWindowProcessor processor; + private readonly ImageFrame source; + private readonly MemoryAllocator memoryAllocator; + private readonly Buffer2D targetPixels; + private readonly SlidingWindowInfos swInfos; + private readonly int yStart; + private readonly int yEnd; + private readonly bool useFastPath; + + /// + /// Initializes a new instance of the struct. + /// + /// The configuration. + /// The histogram processor. + /// The source image. + /// The memory allocator. + /// The target pixels. + /// about the sliding window dimensions. + /// The y start position. + /// The y end position. + /// if set to true the borders of the image will not be checked. + [MethodImpl(InliningOptions.ShortMethod)] + public SlidingWindowOperation( + Configuration configuration, + AdaptiveHistogramEqualizationSlidingWindowProcessor processor, + ImageFrame source, + MemoryAllocator memoryAllocator, + Buffer2D targetPixels, + SlidingWindowInfos swInfos, + int yStart, + int yEnd, + bool useFastPath) + { + this.configuration = configuration; + this.processor = processor; + this.source = source; + this.memoryAllocator = memoryAllocator; + this.targetPixels = targetPixels; + this.swInfos = swInfos; + this.yStart = yStart; + this.yEnd = yEnd; + this.useFastPath = useFastPath; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(int x) + { + using (IMemoryOwner histogramBuffer = this.memoryAllocator.Allocate(this.processor.LuminanceLevels, AllocationOptions.Clean)) + using (IMemoryOwner histogramBufferCopy = this.memoryAllocator.Allocate(this.processor.LuminanceLevels, AllocationOptions.Clean)) + using (IMemoryOwner cdfBuffer = this.memoryAllocator.Allocate(this.processor.LuminanceLevels, AllocationOptions.Clean)) + using (IMemoryOwner pixelRowBuffer = this.memoryAllocator.Allocate(this.swInfos.TileWidth, AllocationOptions.Clean)) + { + Span histogram = histogramBuffer.GetSpan(); + ref int histogramBase = ref MemoryMarshal.GetReference(histogram); + + Span histogramCopy = histogramBufferCopy.GetSpan(); + ref int histogramCopyBase = ref MemoryMarshal.GetReference(histogramCopy); + + ref int cdfBase = ref MemoryMarshal.GetReference(cdfBuffer.GetSpan()); + + Span pixelRow = pixelRowBuffer.GetSpan(); + ref Vector4 pixelRowBase = ref MemoryMarshal.GetReference(pixelRow); + + // Build the initial histogram of grayscale values. + for (int dy = this.yStart - this.swInfos.HalfTileHeight; dy < this.yStart + this.swInfos.HalfTileHeight; dy++) + { + if (this.useFastPath) + { + this.processor.CopyPixelRowFast(this.source, pixelRow, x - this.swInfos.HalfTileWidth, dy, this.swInfos.TileWidth, this.configuration); + } + else + { + this.processor.CopyPixelRow(this.source, pixelRow, x - this.swInfos.HalfTileWidth, dy, this.swInfos.TileWidth, this.configuration); + } + + this.processor.AddPixelsToHistogram(ref pixelRowBase, ref histogramBase, this.processor.LuminanceLevels, pixelRow.Length); + } + + for (int y = this.yStart; y < this.yEnd; y++) + { + if (this.processor.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.processor.ClipHistogram(histogramCopy, this.processor.ClipLimit); + } + + // Calculate the cumulative distribution function, which will map each input pixel in the current tile to a new value. + int cdfMin = this.processor.ClipHistogramEnabled + ? this.processor.CalculateCdf(ref cdfBase, ref histogramCopyBase, histogram.Length - 1) + : this.processor.CalculateCdf(ref cdfBase, ref histogramBase, histogram.Length - 1); + + float numberOfPixelsMinusCdfMin = this.swInfos.PixelInTile - cdfMin; + + // Map the current pixel to the new equalized value. + int luminance = GetLuminance(this.source[x, y], this.processor.LuminanceLevels); + float luminanceEqualized = Unsafe.Add(ref cdfBase, luminance) / numberOfPixelsMinusCdfMin; + this.targetPixels[x, y].FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, this.source[x, y].ToVector4().W)); + + // Remove top most row from the histogram, mirroring rows which exceeds the borders. + if (this.useFastPath) + { + this.processor.CopyPixelRowFast(this.source, pixelRow, x - this.swInfos.HalfTileWidth, y - this.swInfos.HalfTileWidth, this.swInfos.TileWidth, this.configuration); + } + else + { + this.processor.CopyPixelRow(this.source, pixelRow, x - this.swInfos.HalfTileWidth, y - this.swInfos.HalfTileWidth, this.swInfos.TileWidth, this.configuration); + } + + this.processor.RemovePixelsFromHistogram(ref pixelRowBase, ref histogramBase, this.processor.LuminanceLevels, pixelRow.Length); + + // Add new bottom row to the histogram, mirroring rows which exceeds the borders. + if (this.useFastPath) + { + this.processor.CopyPixelRowFast(this.source, pixelRow, x - this.swInfos.HalfTileWidth, y + this.swInfos.HalfTileWidth, this.swInfos.TileWidth, this.configuration); + } + else + { + this.processor.CopyPixelRow(this.source, pixelRow, x - this.swInfos.HalfTileWidth, y + this.swInfos.HalfTileWidth, this.swInfos.TileWidth, this.configuration); + } + + this.processor.AddPixelsToHistogram(ref pixelRowBase, ref histogramBase, this.processor.LuminanceLevels, pixelRow.Length); + } + } + } + } + private class SlidingWindowInfos { public SlidingWindowInfos(int tileWidth, int tileHeight, int halfTileWidth, int halfTileHeight, int pixelInTile) @@ -382,15 +427,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization this.PixelInTile = pixelInTile; } - public int TileWidth { get; private set; } + public int TileWidth { get; } - public int TileHeight { get; private set; } + public int TileHeight { get; } - public int PixelInTile { get; private set; } + public int PixelInTile { get; } - public int HalfTileWidth { get; private set; } + public int HalfTileWidth { get; } - public int HalfTileHeight { get; private set; } + public int HalfTileHeight { get; } } } }