From af0b754194d471545ab670909e8189322a809254 Mon Sep 17 00:00:00 2001 From: popow Date: Thu, 19 Jul 2018 20:06:51 +0200 Subject: [PATCH 01/50] first version of sliding window adaptive histogram equalization --- .../AdaptiveHistEqualizationProcessor.cs | 146 ++++++++++++++++++ .../HistogramEqualizationProcessor.cs | 4 +- 2 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs new file mode 100644 index 0000000000..b05d10f455 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.Memory; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Processing.Processors.Normalization +{ + internal class AdaptiveHistEqualizationProcessor : HistogramEqualizationProcessor + where TPixel : struct, IPixel + { + public AdaptiveHistEqualizationProcessor(int luminanceLevels) + : base(luminanceLevels) + { + } + + /// + protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) + { + MemoryAllocator memoryAllocator = configuration.MemoryAllocator; + int numberOfPixels = source.Width * source.Height; + Span pixels = source.GetPixelSpan(); + + int gridSize = 64; + int halfGridSize = gridSize / 2; + int[] histogram = new int[this.LuminanceLevels]; + int[] histogramCopy = new int[this.LuminanceLevels]; + int[] cdf = new int[this.LuminanceLevels]; + using (Buffer2D targetPixels = configuration.MemoryAllocator.Allocate2D(source.Width, source.Height)) + { + for (int y = halfGridSize; y < source.Height - halfGridSize; y++) + { + var pixelsGrid = new List(); + int x = halfGridSize; + for (int dy = y - halfGridSize; dy < (y + halfGridSize); dy++) + { + Span rowSpan = source.GetPixelRowSpan(dy); + for (int dx = x - halfGridSize; dx < (x + halfGridSize); dx++) + { + pixelsGrid.Add(rowSpan[dx]); + } + } + + this.CleanArray(histogram); + this.CleanArray(cdf); + this.CalcHistogram(pixelsGrid, histogram, this.LuminanceLevels); + + for (x = halfGridSize + 1; x < source.Width - halfGridSize; x++) + { + // remove left most column from the histogram + var leftMostColumnPixels = new List(); + int dx = x - halfGridSize - 1; + for (int dy = y - halfGridSize; dy < (y + halfGridSize); dy++) + { + leftMostColumnPixels.Add(pixels[(dy * source.Width) + dx]); + } + + this.RemovePixelsFromHistogram(leftMostColumnPixels, histogram, this.LuminanceLevels); + + // add right column from the histogram + dx = x + halfGridSize - 1; + var rightMostColumnPixels = new List(); + for (int dy = y - halfGridSize; dy < (y + halfGridSize); dy++) + { + rightMostColumnPixels.Add(pixels[(dy * source.Width) + dx]); + } + + this.AddPixelsTooHistogram(rightMostColumnPixels, histogram, this.LuminanceLevels); + + histogram.AsSpan().CopyTo(histogramCopy); + this.ClipHistogram(histogramCopy, 5, gridSize); + int cdfMin = this.CalculateCdf(cdf, histogramCopy); + double numberOfPixelsMinusCdfMin = (double)(pixelsGrid.Count - cdfMin); + + int luminance = this.GetLuminance(pixels[(y * source.Width) + x], this.LuminanceLevels); + double luminanceEqualized = cdf[luminance] / numberOfPixelsMinusCdfMin; + + targetPixels[x, y].PackFromVector4(new Vector4((float)luminanceEqualized)); + + this.CleanArray(cdf); + } + } + + Buffer2D.SwapOrCopyContent(source.PixelBuffer, targetPixels); + } + } + + private void ClipHistogram(Span histogram, int contrastLimit, int gridSize) + { + int clipLimit = (contrastLimit * (gridSize * gridSize)) / this.LuminanceLevels; + + int sumOverClip = 0; + for (int i = 0; i < histogram.Length; i++) + { + if (histogram[i] > clipLimit) + { + sumOverClip += histogram[i] - clipLimit; + histogram[i] = clipLimit; + } + } + + int addToEachBin = (int)Math.Floor(sumOverClip / (double)this.LuminanceLevels); + for (int i = 0; i < histogram.Length; i++) + { + histogram[i] += addToEachBin; + } + } + + private void AddPixelsTooHistogram(List greyValues, Span histogram, int luminanceLevels) + { + for (int i = 0; i < greyValues.Count; i++) + { + int luminance = this.GetLuminance(greyValues[i], luminanceLevels); + histogram[luminance]++; + } + } + + private void RemovePixelsFromHistogram(List greyValues, Span histogram, int luminanceLevels) + { + for (int i = 0; i < greyValues.Count; i++) + { + int luminance = this.GetLuminance(greyValues[i], luminanceLevels); + histogram[luminance]--; + } + } + + private void CalcHistogram(List greyValues, Span histogram, int luminanceLevels) + { + for (int i = 0; i < greyValues.Count; i++) + { + int luminance = this.GetLuminance(greyValues[i], luminanceLevels); + histogram[luminance]++; + } + } + + private void CleanArray(Span array) + { + for (int i = 0; i < array.Length; i++) + { + array[i] = 0; + } + } + } +} diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs index ba56e392f4..ab4f8332b5 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs @@ -78,7 +78,7 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source /// The array holding the cdf. /// The histogram of the input image. /// The first none zero value of the cdf. - private int CalculateCdf(Span cdf, Span histogram) + protected int CalculateCdf(Span cdf, Span histogram) { // Calculate the cumulative histogram int histSum = 0; @@ -114,7 +114,7 @@ private int CalculateCdf(Span cdf, Span histogram) /// The pixel to get the luminance from /// The number of luminance levels (256 for 8 bit, 65536 for 16 bit grayscale images) [MethodImpl(InliningOptions.ShortMethod)] - private int GetLuminance(TPixel sourcePixel, int luminanceLevels) + protected int GetLuminance(TPixel sourcePixel, int luminanceLevels) { // Convert to grayscale using ITU-R Recommendation BT.709 var vector = sourcePixel.ToVector4(); From 274db88720524fab8046f32316c2af799968fd5f Mon Sep 17 00:00:00 2001 From: popow Date: Sun, 22 Jul 2018 20:34:44 +0200 Subject: [PATCH 02/50] going now from top to bottom of the image, added more comments --- .../AdaptiveHistEqualizationProcessor.cs | 93 +++++++++++-------- 1 file changed, 55 insertions(+), 38 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs index b05d10f455..3df1877339 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; using System.Collections.Generic; using System.Numerics; using SixLabors.ImageSharp.Advanced; @@ -8,9 +11,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization { + /// + /// Applies an adaptive histogram equalization to the image. + /// + /// The pixel format. internal class AdaptiveHistEqualizationProcessor : HistogramEqualizationProcessor where TPixel : struct, IPixel { + /// + /// Initializes a new instance of the class. + /// + /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images + /// or 65536 for 16-bit grayscale images. public AdaptiveHistEqualizationProcessor(int luminanceLevels) : base(luminanceLevels) { @@ -24,59 +36,46 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source Span pixels = source.GetPixelSpan(); int gridSize = 64; + int contrastLimit = 5; int halfGridSize = gridSize / 2; int[] histogram = new int[this.LuminanceLevels]; int[] histogramCopy = new int[this.LuminanceLevels]; int[] cdf = new int[this.LuminanceLevels]; using (Buffer2D targetPixels = configuration.MemoryAllocator.Allocate2D(source.Width, source.Height)) { - for (int y = halfGridSize; y < source.Height - halfGridSize; y++) + for (int x = halfGridSize; x < source.Width - halfGridSize; x++) { + this.CleanArray(histogram); + this.CleanArray(cdf); + var pixelsGrid = new List(); - int x = halfGridSize; - for (int dy = y - halfGridSize; dy < (y + halfGridSize); dy++) + for (int dy = 0; dy < gridSize; dy++) { - Span rowSpan = source.GetPixelRowSpan(dy); - for (int dx = x - halfGridSize; dx < (x + halfGridSize); dx++) - { - pixelsGrid.Add(rowSpan[dx]); - } + Span rowSpan = source.GetPixelRowSpan(dy).Slice(start: x - halfGridSize, length: gridSize); + pixelsGrid.AddRange(rowSpan.ToArray()); } - this.CleanArray(histogram); - this.CleanArray(cdf); this.CalcHistogram(pixelsGrid, histogram, this.LuminanceLevels); - for (x = halfGridSize + 1; x < source.Width - halfGridSize; x++) + for (int y = halfGridSize + 1; y < source.Height - halfGridSize; y++) { - // remove left most column from the histogram - var leftMostColumnPixels = new List(); - int dx = x - halfGridSize - 1; - for (int dy = y - halfGridSize; dy < (y + halfGridSize); dy++) - { - leftMostColumnPixels.Add(pixels[(dy * source.Width) + dx]); - } - - this.RemovePixelsFromHistogram(leftMostColumnPixels, histogram, this.LuminanceLevels); - - // add right column from the histogram - dx = x + halfGridSize - 1; - var rightMostColumnPixels = new List(); - for (int dy = y - halfGridSize; dy < (y + halfGridSize); dy++) - { - rightMostColumnPixels.Add(pixels[(dy * source.Width) + dx]); - } - - this.AddPixelsTooHistogram(rightMostColumnPixels, histogram, this.LuminanceLevels); + // Remove top most row from the histogram + Span rowSpan = source.GetPixelRowSpan(y - halfGridSize - 1).Slice(start: x - halfGridSize, length: gridSize); + this.RemovePixelsFromHistogram(rowSpan, histogram, this.LuminanceLevels); + + // Add new bottom row to the histogram + rowSpan = source.GetPixelRowSpan(y + halfGridSize - 1).Slice(start: x - halfGridSize, length: gridSize); + this.AddPixelsTooHistogram(rowSpan, histogram, this.LuminanceLevels); + // Clipping the histogram, but doing it on a copy to keep the original un-clipped values histogram.AsSpan().CopyTo(histogramCopy); - this.ClipHistogram(histogramCopy, 5, gridSize); + this.ClipHistogram(histogramCopy, gridSize, contrastLimit); int cdfMin = this.CalculateCdf(cdf, histogramCopy); double numberOfPixelsMinusCdfMin = (double)(pixelsGrid.Count - cdfMin); + // Map the current pixel to the new equalized value int luminance = this.GetLuminance(pixels[(y * source.Width) + x], this.LuminanceLevels); double luminanceEqualized = cdf[luminance] / numberOfPixelsMinusCdfMin; - targetPixels[x, y].PackFromVector4(new Vector4((float)luminanceEqualized)); this.CleanArray(cdf); @@ -87,7 +86,16 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source } } - private void ClipHistogram(Span histogram, int contrastLimit, int gridSize) + /// + /// AHE tends to over amplify the contrast in near-constant regions of the image, since the histogram in such regions is highly concentrated. + /// Clipping the histogram is meant to reduce this effect, by cutting of histogram bin's which exceed a certain amount and redistribute + /// the values over the clip limit to all other bins equally. + /// + /// The histogram to apply the clipping + /// The grid size of the AHE + /// The contrast limit. Defaults to 5. + /// The number of pixels redistributed to all other bins + private int ClipHistogram(Span histogram, int gridSize, int contrastLimit = 5) { int clipLimit = (contrastLimit * (gridSize * gridSize)) / this.LuminanceLevels; @@ -106,20 +114,29 @@ private void ClipHistogram(Span histogram, int contrastLimit, int gridSize) { histogram[i] += addToEachBin; } + + // The redistribution will push some bins over the clip limit again. + // Re-Applying the clipping until this effect no longer occurs. + while (addToEachBin > 1) + { + addToEachBin = this.ClipHistogram(histogram, gridSize, contrastLimit); + } + + return addToEachBin; } - private void AddPixelsTooHistogram(List greyValues, Span histogram, int luminanceLevels) + private void AddPixelsTooHistogram(Span greyValues, Span histogram, int luminanceLevels) { - for (int i = 0; i < greyValues.Count; i++) + for (int i = 0; i < greyValues.Length; i++) { int luminance = this.GetLuminance(greyValues[i], luminanceLevels); histogram[luminance]++; } } - private void RemovePixelsFromHistogram(List greyValues, Span histogram, int luminanceLevels) + private void RemovePixelsFromHistogram(Span greyValues, Span histogram, int luminanceLevels) { - for (int i = 0; i < greyValues.Count; i++) + for (int i = 0; i < greyValues.Length; i++) { int luminance = this.GetLuminance(greyValues[i], luminanceLevels); histogram[luminance]--; From c1c58fa336b1cc68e19276398239f5873b96681d Mon Sep 17 00:00:00 2001 From: popow Date: Tue, 24 Jul 2018 19:51:01 +0200 Subject: [PATCH 03/50] using memory allocator to create the histogram and the cdf --- .../AdaptiveHistEqualizationProcessor.cs | 59 +++++++++---------- 1 file changed, 27 insertions(+), 32 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs index 3df1877339..61ee6aea45 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Collections.Generic; using System.Numerics; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; @@ -36,27 +35,29 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source Span pixels = source.GetPixelSpan(); int gridSize = 64; + int pixelsInGrid = gridSize * gridSize; int contrastLimit = 5; int halfGridSize = gridSize / 2; - int[] histogram = new int[this.LuminanceLevels]; - int[] histogramCopy = new int[this.LuminanceLevels]; - int[] cdf = new int[this.LuminanceLevels]; + using (IBuffer histogramBuffer = memoryAllocator.AllocateClean(this.LuminanceLevels)) + using (IBuffer histogramBufferCopy = memoryAllocator.AllocateClean(this.LuminanceLevels)) + using (IBuffer cdfBuffer = memoryAllocator.AllocateClean(this.LuminanceLevels)) using (Buffer2D targetPixels = configuration.MemoryAllocator.Allocate2D(source.Width, source.Height)) { + Span histogram = histogramBuffer.GetSpan(); + Span histogramCopy = histogramBufferCopy.GetSpan(); + Span cdf = cdfBuffer.GetSpan(); + for (int x = halfGridSize; x < source.Width - halfGridSize; x++) { - this.CleanArray(histogram); - this.CleanArray(cdf); + histogram.Clear(); - var pixelsGrid = new List(); + // Build the histogram of grayscale values for the current grid. for (int dy = 0; dy < gridSize; dy++) { Span rowSpan = source.GetPixelRowSpan(dy).Slice(start: x - halfGridSize, length: gridSize); - pixelsGrid.AddRange(rowSpan.ToArray()); + this.AddPixelsTooHistogram(rowSpan, histogram, this.LuminanceLevels); } - this.CalcHistogram(pixelsGrid, histogram, this.LuminanceLevels); - for (int y = halfGridSize + 1; y < source.Height - halfGridSize; y++) { // Remove top most row from the histogram @@ -68,17 +69,16 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source this.AddPixelsTooHistogram(rowSpan, histogram, this.LuminanceLevels); // Clipping the histogram, but doing it on a copy to keep the original un-clipped values - histogram.AsSpan().CopyTo(histogramCopy); + histogram.CopyTo(histogramCopy); this.ClipHistogram(histogramCopy, gridSize, contrastLimit); + cdf.Clear(); int cdfMin = this.CalculateCdf(cdf, histogramCopy); - double numberOfPixelsMinusCdfMin = (double)(pixelsGrid.Count - cdfMin); + double numberOfPixelsMinusCdfMin = (double)(pixelsInGrid - cdfMin); // Map the current pixel to the new equalized value int luminance = this.GetLuminance(pixels[(y * source.Width) + x], this.LuminanceLevels); double luminanceEqualized = cdf[luminance] / numberOfPixelsMinusCdfMin; targetPixels[x, y].PackFromVector4(new Vector4((float)luminanceEqualized)); - - this.CleanArray(cdf); } } @@ -88,7 +88,7 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source /// /// AHE tends to over amplify the contrast in near-constant regions of the image, since the histogram in such regions is highly concentrated. - /// Clipping the histogram is meant to reduce this effect, by cutting of histogram bin's which exceed a certain amount and redistribute + /// Clipping the histogram is meant to reduce this effect, by cutting of histogram bin's which exceed a certain amount and redistribute /// the values over the clip limit to all other bins equally. /// /// The histogram to apply the clipping @@ -125,6 +125,12 @@ private int ClipHistogram(Span histogram, int gridSize, int contrastLimit = return addToEachBin; } + /// + /// Adds a row of grey values to the histogram. + /// + /// The grey values to add + /// The histogram + /// The number of different luminance levels. private void AddPixelsTooHistogram(Span greyValues, Span histogram, int luminanceLevels) { for (int i = 0; i < greyValues.Length; i++) @@ -134,6 +140,12 @@ private void AddPixelsTooHistogram(Span greyValues, Span histogram, } } + /// + /// Removes a row of grey values from the histogram. + /// + /// The grey values to remove + /// The histogram + /// The number of different luminance levels. private void RemovePixelsFromHistogram(Span greyValues, Span histogram, int luminanceLevels) { for (int i = 0; i < greyValues.Length; i++) @@ -142,22 +154,5 @@ private void RemovePixelsFromHistogram(Span greyValues, Span histog histogram[luminance]--; } } - - private void CalcHistogram(List greyValues, Span histogram, int luminanceLevels) - { - for (int i = 0; i < greyValues.Count; i++) - { - int luminance = this.GetLuminance(greyValues[i], luminanceLevels); - histogram[luminance]++; - } - } - - private void CleanArray(Span array) - { - for (int i = 0; i < array.Length; i++) - { - array[i] = 0; - } - } } } From 30f4d7369204ce68a56984ca7e43dd35117310aa Mon Sep 17 00:00:00 2001 From: popow Date: Wed, 25 Jul 2018 20:20:09 +0200 Subject: [PATCH 04/50] mirroring rows which exceeds the borders --- .../AdaptiveHistEqualizationProcessor.cs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs index 61ee6aea45..e3c62f813a 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs @@ -58,17 +58,24 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source this.AddPixelsTooHistogram(rowSpan, histogram, this.LuminanceLevels); } - for (int y = halfGridSize + 1; y < source.Height - halfGridSize; y++) + for (int y = 1; y < source.Height; y++) { - // Remove top most row from the histogram - Span rowSpan = source.GetPixelRowSpan(y - halfGridSize - 1).Slice(start: x - halfGridSize, length: gridSize); + // Remove top most row from the histogram, mirroring rows which exceeds the borders + Span rowSpan = source.GetPixelRowSpan(Math.Abs(y - halfGridSize - 1)).Slice(start: x - halfGridSize, length: gridSize); this.RemovePixelsFromHistogram(rowSpan, histogram, this.LuminanceLevels); - // Add new bottom row to the histogram - rowSpan = source.GetPixelRowSpan(y + halfGridSize - 1).Slice(start: x - halfGridSize, length: gridSize); + // Add new bottom row to the histogram, mirroring rows which exceeds the borders + int rowPos = y + halfGridSize - 1; + if (rowPos >= source.Height) + { + int diff = rowPos - source.Height; + rowPos = source.Height - diff - 1; + } + + rowSpan = source.GetPixelRowSpan(rowPos).Slice(start: x - halfGridSize, length: gridSize); this.AddPixelsTooHistogram(rowSpan, histogram, this.LuminanceLevels); - // Clipping the histogram, but doing it on a copy to keep the original un-clipped values + // 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, gridSize, contrastLimit); cdf.Clear(); From 3c2958a4abfc7c8c99e9e853494c16729f58ad54 Mon Sep 17 00:00:00 2001 From: popow Date: Thu, 26 Jul 2018 19:56:14 +0200 Subject: [PATCH 05/50] mirroring also left and right borders --- .../AdaptiveHistEqualizationProcessor.cs | 69 ++++++++++++++++--- 1 file changed, 58 insertions(+), 11 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs index e3c62f813a..b7d27f6356 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Collections.Generic; using System.Numerics; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; @@ -47,32 +48,25 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source Span histogramCopy = histogramBufferCopy.GetSpan(); Span cdf = cdfBuffer.GetSpan(); - for (int x = halfGridSize; x < source.Width - halfGridSize; x++) + for (int x = 0; x < source.Width; x++) { histogram.Clear(); // Build the histogram of grayscale values for the current grid. for (int dy = 0; dy < gridSize; dy++) { - Span rowSpan = source.GetPixelRowSpan(dy).Slice(start: x - halfGridSize, length: gridSize); + Span rowSpan = this.GetPixelRow(source, x - halfGridSize, dy, gridSize); this.AddPixelsTooHistogram(rowSpan, histogram, this.LuminanceLevels); } for (int y = 1; y < source.Height; y++) { // Remove top most row from the histogram, mirroring rows which exceeds the borders - Span rowSpan = source.GetPixelRowSpan(Math.Abs(y - halfGridSize - 1)).Slice(start: x - halfGridSize, length: gridSize); + Span rowSpan = this.GetPixelRow(source, x - halfGridSize, y - halfGridSize - 1, gridSize); this.RemovePixelsFromHistogram(rowSpan, histogram, this.LuminanceLevels); // Add new bottom row to the histogram, mirroring rows which exceeds the borders - int rowPos = y + halfGridSize - 1; - if (rowPos >= source.Height) - { - int diff = rowPos - source.Height; - rowPos = source.Height - diff - 1; - } - - rowSpan = source.GetPixelRowSpan(rowPos).Slice(start: x - halfGridSize, length: gridSize); + rowSpan = this.GetPixelRow(source, x - halfGridSize, y + halfGridSize - 1, gridSize); this.AddPixelsTooHistogram(rowSpan, histogram, this.LuminanceLevels); // Clipping the histogram, but doing it on a copy to keep the original un-clipped values for the next iteration @@ -93,6 +87,59 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source } } + /// + /// Get the a pixel row at a given position with a length of the grid size. Mirrors pixels which exceeds the edges. + /// + /// The source image. + /// The x position. + /// The y position. + /// The grid size. + /// A pixel row of the length of the grid size. + private Span GetPixelRow(ImageFrame source, int x, int y, int gridSize) + { + if (y < 0) + { + y = Math.Abs(y); + } + else if (y >= source.Height) + { + int diff = y - source.Height; + y = source.Height - diff - 1; + } + + // special cases for the left and the right border where GetPixelRowSpan can not be used + if (x < 0) + { + var rowPixels = new List(); + for (int dx = x; dx < x + gridSize; dx++) + { + rowPixels.Add(source[Math.Abs(dx), y]); + } + + return rowPixels.ToArray(); + } + else if (x + gridSize > source.Width) + { + var rowPixels = new List(); + for (int dx = x; dx < x + gridSize; dx++) + { + if (dx >= source.Width) + { + int diff = dx - source.Width; + rowPixels.Add(source[dx - diff - 1, y]); + } + else + { + rowPixels.Add(source[dx, y]); + } + } + + return rowPixels.ToArray(); + } + + return source.GetPixelRowSpan(y).Slice(start: x, length: gridSize); + } + /// /// AHE tends to over amplify the contrast in near-constant regions of the image, since the histogram in such regions is highly concentrated. /// Clipping the histogram is meant to reduce this effect, by cutting of histogram bin's which exceed a certain amount and redistribute From 024873c8f053f2d6a6460b1cd8aa65c9518c2b92 Mon Sep 17 00:00:00 2001 From: popow Date: Mon, 30 Jul 2018 20:55:40 +0200 Subject: [PATCH 06/50] gridsize and cliplimit are now parameters of the constructor --- .../AdaptiveHistEqualizationProcessor.cs | 59 +++++++++++-------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs index b7d27f6356..a79b5b8dac 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs @@ -21,13 +21,27 @@ internal class AdaptiveHistEqualizationProcessor : HistogramEqualization /// /// Initializes a new instance of the class. /// + /// The grid size of the adaptive histogram equalization. + /// The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images /// or 65536 for 16-bit grayscale images. - public AdaptiveHistEqualizationProcessor(int luminanceLevels) + public AdaptiveHistEqualizationProcessor(int gridSize, int clipLimit, int luminanceLevels) : base(luminanceLevels) { + this.GridSize = gridSize; + this.ClipLimit = clipLimit; } + /// + /// Gets the size of the grid for the adaptive histogram equalization. + /// + public int GridSize { get; } + + /// + /// Gets the histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. + /// + public int ClipLimit { get; } + /// protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) { @@ -35,10 +49,8 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source int numberOfPixels = source.Width * source.Height; Span pixels = source.GetPixelSpan(); - int gridSize = 64; - int pixelsInGrid = gridSize * gridSize; - int contrastLimit = 5; - int halfGridSize = gridSize / 2; + int pixelsInGrid = this.GridSize * this.GridSize; + int halfGridSize = this.GridSize / 2; using (IBuffer histogramBuffer = memoryAllocator.AllocateClean(this.LuminanceLevels)) using (IBuffer histogramBufferCopy = memoryAllocator.AllocateClean(this.LuminanceLevels)) using (IBuffer cdfBuffer = memoryAllocator.AllocateClean(this.LuminanceLevels)) @@ -53,25 +65,17 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source histogram.Clear(); // Build the histogram of grayscale values for the current grid. - for (int dy = 0; dy < gridSize; dy++) + for (int dy = -halfGridSize; dy < halfGridSize; dy++) { - Span rowSpan = this.GetPixelRow(source, x - halfGridSize, dy, gridSize); + Span rowSpan = this.GetPixelRow(source, x - halfGridSize, dy, this.GridSize); this.AddPixelsTooHistogram(rowSpan, histogram, this.LuminanceLevels); } - for (int y = 1; y < source.Height; y++) + for (int y = 0; y < source.Height; y++) { - // Remove top most row from the histogram, mirroring rows which exceeds the borders - Span rowSpan = this.GetPixelRow(source, x - halfGridSize, y - halfGridSize - 1, gridSize); - this.RemovePixelsFromHistogram(rowSpan, histogram, this.LuminanceLevels); - - // Add new bottom row to the histogram, mirroring rows which exceeds the borders - rowSpan = this.GetPixelRow(source, x - halfGridSize, y + halfGridSize - 1, gridSize); - this.AddPixelsTooHistogram(rowSpan, histogram, this.LuminanceLevels); - // 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, gridSize, contrastLimit); + this.ClipHistogram(histogramCopy, this.ClipLimit); cdf.Clear(); int cdfMin = this.CalculateCdf(cdf, histogramCopy); double numberOfPixelsMinusCdfMin = (double)(pixelsInGrid - cdfMin); @@ -80,6 +84,14 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source int luminance = this.GetLuminance(pixels[(y * source.Width) + x], this.LuminanceLevels); double luminanceEqualized = cdf[luminance] / numberOfPixelsMinusCdfMin; targetPixels[x, y].PackFromVector4(new Vector4((float)luminanceEqualized)); + + // Remove top most row from the histogram, mirroring rows which exceeds the borders + Span rowSpan = this.GetPixelRow(source, x - halfGridSize, y - halfGridSize, this.GridSize); + this.RemovePixelsFromHistogram(rowSpan, histogram, this.LuminanceLevels); + + // Add new bottom row to the histogram, mirroring rows which exceeds the borders + rowSpan = this.GetPixelRow(source, x - halfGridSize, y + halfGridSize, this.GridSize); + this.AddPixelsTooHistogram(rowSpan, histogram, this.LuminanceLevels); } } @@ -145,14 +157,11 @@ private Span GetPixelRow(ImageFrame source, int x, int y, int gr /// Clipping the histogram is meant to reduce this effect, by cutting of histogram bin's which exceed a certain amount and redistribute /// the values over the clip limit to all other bins equally. /// - /// The histogram to apply the clipping - /// The grid size of the AHE - /// The contrast limit. Defaults to 5. - /// The number of pixels redistributed to all other bins - private int ClipHistogram(Span histogram, int gridSize, int contrastLimit = 5) + /// The histogram to apply the clipping. + /// The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. + /// The number of pixels redistributed to all other bins. + private int ClipHistogram(Span histogram, int clipLimit) { - int clipLimit = (contrastLimit * (gridSize * gridSize)) / this.LuminanceLevels; - int sumOverClip = 0; for (int i = 0; i < histogram.Length; i++) { @@ -173,7 +182,7 @@ private int ClipHistogram(Span histogram, int gridSize, int contrastLimit = // Re-Applying the clipping until this effect no longer occurs. while (addToEachBin > 1) { - addToEachBin = this.ClipHistogram(histogram, gridSize, contrastLimit); + addToEachBin = this.ClipHistogram(histogram, clipLimit); } return addToEachBin; From f04ac406e3774160b1503bf200bc48fcae7266e8 Mon Sep 17 00:00:00 2001 From: popow Date: Tue, 31 Jul 2018 19:33:26 +0200 Subject: [PATCH 07/50] using Parallel.For --- .../AdaptiveHistEqualizationProcessor.cs | 90 ++++++++++--------- 1 file changed, 50 insertions(+), 40 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs index a79b5b8dac..9b66d434ee 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.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Memory; @@ -28,6 +29,9 @@ internal class AdaptiveHistEqualizationProcessor : HistogramEqualization public AdaptiveHistEqualizationProcessor(int gridSize, int clipLimit, int luminanceLevels) : base(luminanceLevels) { + Guard.MustBeGreaterThan(gridSize, 8, nameof(gridSize)); + Guard.MustBeGreaterThan(clipLimit, 1, nameof(clipLimit)); + this.GridSize = gridSize; this.ClipLimit = clipLimit; } @@ -51,49 +55,55 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source int pixelsInGrid = this.GridSize * this.GridSize; int halfGridSize = this.GridSize / 2; - using (IBuffer histogramBuffer = memoryAllocator.AllocateClean(this.LuminanceLevels)) - using (IBuffer histogramBufferCopy = memoryAllocator.AllocateClean(this.LuminanceLevels)) - using (IBuffer cdfBuffer = memoryAllocator.AllocateClean(this.LuminanceLevels)) using (Buffer2D targetPixels = configuration.MemoryAllocator.Allocate2D(source.Width, source.Height)) { - Span histogram = histogramBuffer.GetSpan(); - Span histogramCopy = histogramBufferCopy.GetSpan(); - Span cdf = cdfBuffer.GetSpan(); - - for (int x = 0; x < source.Width; x++) - { - histogram.Clear(); - - // Build the histogram of grayscale values for the current grid. - for (int dy = -halfGridSize; dy < halfGridSize; dy++) + Parallel.For( + 0, + source.Width, + configuration.ParallelOptions, + x => { - Span rowSpan = this.GetPixelRow(source, x - halfGridSize, dy, this.GridSize); - this.AddPixelsTooHistogram(rowSpan, histogram, this.LuminanceLevels); - } - - for (int y = 0; y < source.Height; y++) - { - // 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); - cdf.Clear(); - int cdfMin = this.CalculateCdf(cdf, histogramCopy); - double numberOfPixelsMinusCdfMin = (double)(pixelsInGrid - cdfMin); - - // Map the current pixel to the new equalized value - int luminance = this.GetLuminance(pixels[(y * source.Width) + x], this.LuminanceLevels); - double luminanceEqualized = cdf[luminance] / numberOfPixelsMinusCdfMin; - targetPixels[x, y].PackFromVector4(new Vector4((float)luminanceEqualized)); - - // Remove top most row from the histogram, mirroring rows which exceeds the borders - Span rowSpan = this.GetPixelRow(source, x - halfGridSize, y - halfGridSize, this.GridSize); - this.RemovePixelsFromHistogram(rowSpan, histogram, this.LuminanceLevels); - - // Add new bottom row to the histogram, mirroring rows which exceeds the borders - rowSpan = this.GetPixelRow(source, x - halfGridSize, y + halfGridSize, this.GridSize); - this.AddPixelsTooHistogram(rowSpan, histogram, this.LuminanceLevels); - } - } + using (IBuffer histogramBuffer = memoryAllocator.AllocateClean(this.LuminanceLevels)) + using (IBuffer histogramBufferCopy = memoryAllocator.AllocateClean(this.LuminanceLevels)) + using (IBuffer cdfBuffer = memoryAllocator.AllocateClean(this.LuminanceLevels)) + { + Span histogram = histogramBuffer.GetSpan(); + Span histogramCopy = histogramBufferCopy.GetSpan(); + Span cdf = cdfBuffer.GetSpan(); + + histogram.Clear(); + + // Build the histogram of grayscale values for the current grid. + for (int dy = -halfGridSize; dy < halfGridSize; dy++) + { + Span rowSpan = this.GetPixelRow(source, x - halfGridSize, dy, this.GridSize); + this.AddPixelsTooHistogram(rowSpan, histogram, this.LuminanceLevels); + } + + for (int y = 0; y < source.Height; y++) + { + // 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); + cdf.Clear(); + int cdfMin = this.CalculateCdf(cdf, histogramCopy); + double numberOfPixelsMinusCdfMin = (double)(pixelsInGrid - cdfMin); + + // Map the current pixel to the new equalized value + int luminance = this.GetLuminance(source[x, y], this.LuminanceLevels); + double luminanceEqualized = cdf[luminance] / numberOfPixelsMinusCdfMin; + targetPixels[x, y].PackFromVector4(new Vector4((float)luminanceEqualized)); + + // Remove top most row from the histogram, mirroring rows which exceeds the borders + Span rowSpan = this.GetPixelRow(source, x - halfGridSize, y - halfGridSize, this.GridSize); + this.RemovePixelsFromHistogram(rowSpan, histogram, this.LuminanceLevels); + + // Add new bottom row to the histogram, mirroring rows which exceeds the borders + rowSpan = this.GetPixelRow(source, x - halfGridSize, y + halfGridSize, this.GridSize); + this.AddPixelsTooHistogram(rowSpan, histogram, this.LuminanceLevels); + } + } + }); Buffer2D.SwapOrCopyContent(source.PixelBuffer, targetPixels); } From f84b03b5f13208296db10b841ec051b8d9f5281a Mon Sep 17 00:00:00 2001 From: popow Date: Tue, 31 Jul 2018 19:41:46 +0200 Subject: [PATCH 08/50] only applying clipping once, effect applying it multiple times is neglectable --- .../AdaptiveHistEqualizationProcessor.cs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs index 9b66d434ee..3d2ef02d52 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs @@ -169,8 +169,7 @@ private Span GetPixelRow(ImageFrame source, int x, int y, int gr /// /// The histogram to apply the clipping. /// The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. - /// The number of pixels redistributed to all other bins. - private int ClipHistogram(Span histogram, int clipLimit) + private void ClipHistogram(Span histogram, int clipLimit) { int sumOverClip = 0; for (int i = 0; i < histogram.Length; i++) @@ -187,15 +186,6 @@ private int ClipHistogram(Span histogram, int clipLimit) { histogram[i] += addToEachBin; } - - // The redistribution will push some bins over the clip limit again. - // Re-Applying the clipping until this effect no longer occurs. - while (addToEachBin > 1) - { - addToEachBin = this.ClipHistogram(histogram, clipLimit); - } - - return addToEachBin; } /// From d664870dc33e4acb19a326ce098d036edbb9d43b Mon Sep 17 00:00:00 2001 From: popow Date: Thu, 2 Aug 2018 20:06:08 +0200 Subject: [PATCH 09/50] added abstract base class for histogram equalization, added option to enable / disable clipping --- .../HistogramEqualizationExtension.cs | 36 ++++++-- .../AdaptiveHistEqualizationProcessor.cs | 63 ++++---------- .../GlobalHistogramEqualizationProcessor.cs | 74 ++++++++++++++++ .../HistogramEqualizationMethod.cs | 21 +++++ .../HistogramEqualizationOptions.cs | 38 ++++++++ .../HistogramEqualizationProcessor.cs | 87 +++++++++---------- .../HistogramEqualizationTests.cs | 6 +- 7 files changed, 228 insertions(+), 97 deletions(-) create mode 100644 src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs create mode 100644 src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs create mode 100644 src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs diff --git a/src/ImageSharp/Processing/HistogramEqualizationExtension.cs b/src/ImageSharp/Processing/HistogramEqualizationExtension.cs index 8dabfcc05c..3a279cc5fc 100644 --- a/src/ImageSharp/Processing/HistogramEqualizationExtension.cs +++ b/src/ImageSharp/Processing/HistogramEqualizationExtension.cs @@ -12,25 +12,47 @@ namespace SixLabors.ImageSharp.Processing public static class HistogramEqualizationExtension { /// - /// Equalizes the histogram of an image to increases the global contrast using 65536 luminance levels. + /// Equalizes the histogram of an image to increases the contrast. /// /// The pixel format. /// The image this method extends. /// The . public static IImageProcessingContext HistogramEqualization(this IImageProcessingContext source) where TPixel : struct, IPixel - => HistogramEqualization(source, 65536); + => HistogramEqualization(source, new HistogramEqualizationOptions()); /// - /// Equalizes the histogram of an image to increases the global contrast. + /// Equalizes the histogram of an image to increases the contrast. /// /// The pixel format. /// The image this method extends. - /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images - /// or 65536 for 16-bit grayscale images. + /// The histogram equalization options to use. /// The . - public static IImageProcessingContext HistogramEqualization(this IImageProcessingContext source, int luminanceLevels) + public static IImageProcessingContext HistogramEqualization(this IImageProcessingContext source, HistogramEqualizationOptions options) where TPixel : struct, IPixel - => source.ApplyProcessor(new HistogramEqualizationProcessor(luminanceLevels)); + => source.ApplyProcessor(GetProcessor(options)); + + private static HistogramEqualizationProcessor GetProcessor(HistogramEqualizationOptions options) + where TPixel : struct, IPixel + { + HistogramEqualizationProcessor processor; + + switch (options.Method) + { + case HistogramEqualizationMethod.Global: + processor = new GlobalHistogramEqualizationProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimit); + break; + + case HistogramEqualizationMethod.Adaptive: + processor = new AdaptiveHistEqualizationProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimit, options.GridSize); + break; + + default: + processor = new GlobalHistogramEqualizationProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimit); + break; + } + + return processor; + } } } diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs index 3d2ef02d52..218d2680f9 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs @@ -22,18 +22,17 @@ internal class AdaptiveHistEqualizationProcessor : HistogramEqualization /// /// Initializes a new instance of the class. /// - /// The grid size of the adaptive histogram equalization. - /// The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images /// or 65536 for 16-bit grayscale images. - public AdaptiveHistEqualizationProcessor(int gridSize, int clipLimit, int luminanceLevels) - : base(luminanceLevels) + /// Indicating whether to clip the histogram bins at a specific value. + /// The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. + /// The grid size of the adaptive histogram equalization. + public AdaptiveHistEqualizationProcessor(int luminanceLevels, bool clipHistogram, int clipLimit, int gridSize) + : base(luminanceLevels, clipHistogram, clipLimit) { Guard.MustBeGreaterThan(gridSize, 8, nameof(gridSize)); - Guard.MustBeGreaterThan(clipLimit, 1, nameof(clipLimit)); this.GridSize = gridSize; - this.ClipLimit = clipLimit; } /// @@ -41,11 +40,6 @@ public AdaptiveHistEqualizationProcessor(int gridSize, int clipLimit, int lumina /// public int GridSize { get; } - /// - /// Gets the histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. - /// - public int ClipLimit { get; } - /// protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) { @@ -82,23 +76,28 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source for (int y = 0; y < source.Height; y++) { - // 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); + 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 grid to a new value. cdf.Clear(); - int cdfMin = this.CalculateCdf(cdf, histogramCopy); - double numberOfPixelsMinusCdfMin = (double)(pixelsInGrid - cdfMin); + int cdfMin = this.ClipHistogramEnabled ? this.CalculateCdf(cdf, histogramCopy) : this.CalculateCdf(cdf, histogram); + float numberOfPixelsMinusCdfMin = pixelsInGrid - cdfMin; // Map the current pixel to the new equalized value int luminance = this.GetLuminance(source[x, y], this.LuminanceLevels); - double luminanceEqualized = cdf[luminance] / numberOfPixelsMinusCdfMin; + float luminanceEqualized = cdf[luminance] / numberOfPixelsMinusCdfMin; targetPixels[x, y].PackFromVector4(new Vector4((float)luminanceEqualized)); - // Remove top most row from the histogram, mirroring rows which exceeds the borders + // Remove top most row from the histogram, mirroring rows which exceeds the borders. Span rowSpan = this.GetPixelRow(source, x - halfGridSize, y - halfGridSize, this.GridSize); this.RemovePixelsFromHistogram(rowSpan, histogram, this.LuminanceLevels); - // Add new bottom row to the histogram, mirroring rows which exceeds the borders + // Add new bottom row to the histogram, mirroring rows which exceeds the borders. rowSpan = this.GetPixelRow(source, x - halfGridSize, y + halfGridSize, this.GridSize); this.AddPixelsTooHistogram(rowSpan, histogram, this.LuminanceLevels); } @@ -162,32 +161,6 @@ private Span GetPixelRow(ImageFrame source, int x, int y, int gr return source.GetPixelRowSpan(y).Slice(start: x, length: gridSize); } - /// - /// AHE tends to over amplify the contrast in near-constant regions of the image, since the histogram in such regions is highly concentrated. - /// Clipping the histogram is meant to reduce this effect, by cutting of histogram bin's which exceed a certain amount and redistribute - /// the values over the clip limit to all other bins equally. - /// - /// The histogram to apply the clipping. - /// The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. - private void ClipHistogram(Span histogram, int clipLimit) - { - int sumOverClip = 0; - for (int i = 0; i < histogram.Length; i++) - { - if (histogram[i] > clipLimit) - { - sumOverClip += histogram[i] - clipLimit; - histogram[i] = clipLimit; - } - } - - int addToEachBin = (int)Math.Floor(sumOverClip / (double)this.LuminanceLevels); - for (int i = 0; i < histogram.Length; i++) - { - histogram[i] += addToEachBin; - } - } - /// /// Adds a row of grey values to the histogram. /// diff --git a/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs new file mode 100644 index 0000000000..423840097d --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs @@ -0,0 +1,74 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.Memory; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Processing.Processors.Normalization +{ + /// + /// Applies a global histogram equalization to the image. + /// + /// The pixel format. + internal class GlobalHistogramEqualizationProcessor : HistogramEqualizationProcessor + where TPixel : struct, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images + /// or 65536 for 16-bit grayscale images. + /// Indicating whether to clip the histogram bins at a specific value. + /// The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. + public GlobalHistogramEqualizationProcessor(int luminanceLevels, bool clipHistogram, int clipLimit) + : base(luminanceLevels, clipHistogram, clipLimit) + { + } + + /// + protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) + { + MemoryAllocator memoryAllocator = configuration.MemoryAllocator; + int numberOfPixels = source.Width * source.Height; + Span pixels = source.GetPixelSpan(); + + // Build the histogram of the grayscale levels. + using (IBuffer histogramBuffer = memoryAllocator.AllocateClean(this.LuminanceLevels)) + using (IBuffer cdfBuffer = memoryAllocator.AllocateClean(this.LuminanceLevels)) + { + Span histogram = histogramBuffer.GetSpan(); + for (int i = 0; i < pixels.Length; i++) + { + TPixel sourcePixel = pixels[i]; + int luminance = this.GetLuminance(sourcePixel, this.LuminanceLevels); + histogram[luminance]++; + } + + if (this.ClipHistogramEnabled) + { + this.ClipHistogram(histogram, this.ClipLimit); + } + + // Calculate the cumulative distribution function, which will map each input pixel to a new value. + Span cdf = cdfBuffer.GetSpan(); + int cdfMin = this.CalculateCdf(cdf, histogram); + + // Apply the cdf to each pixel of the image + float numberOfPixelsMinusCdfMin = numberOfPixels - cdfMin; + for (int i = 0; i < pixels.Length; i++) + { + TPixel sourcePixel = pixels[i]; + + int luminance = this.GetLuminance(sourcePixel, this.LuminanceLevels); + float luminanceEqualized = cdf[luminance] / numberOfPixelsMinusCdfMin; + + pixels[i].PackFromVector4(new Vector4(luminanceEqualized)); + } + } + } + } +} diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs new file mode 100644 index 0000000000..cae745c9b3 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Processing.Processors.Normalization +{ + /// + /// Enumerates the different types of defined histogram equalization methods. + /// + public enum HistogramEqualizationMethod : int + { + /// + /// A global histogram equalization. + /// + Global, + + /// + /// Adaptive histogram equalization. + /// + Adaptive + } +} diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs new file mode 100644 index 0000000000..20aa113ac7 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Processing.Processors.Normalization +{ + /// + /// Data container providing the different options for the histogram equalization. + /// + public class HistogramEqualizationOptions + { + /// + /// Gets or sets the histogram equalization method to use. Defaults to global histogram equalization. + /// + public HistogramEqualizationMethod Method { get; set; } = HistogramEqualizationMethod.Global; + + /// + /// Gets or sets the number of different luminance levels. Typical values are 256 for 8-bit grayscale images + /// or 65536 for 16-bit grayscale images. Defaults to 256. + /// + public int LuminanceLevels { get; set; } = 256; + + /// + /// Gets or sets a value indicating whether to clip the histogram bins at a specific value. Defaults to true. + /// + public bool ClipHistogram { get; set; } = true; + + /// + /// Gets or sets the histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. + /// Defaults to 80. + /// + public int ClipLimit { get; set; } = 80; + + /// + /// Gets or sets the size of the grid for the adaptive histogram equalization. Defaults to 32. + /// + public int GridSize { get; set; } = 32; + } +} diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs index ab4f8332b5..298de13881 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs @@ -2,20 +2,15 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Numerics; -using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.Memory; -using SixLabors.Primitives; namespace SixLabors.ImageSharp.Processing.Processors.Normalization { /// - /// Applies a global histogram equalization to the image. + /// Defines a processor that normalizes the histogram of an image. /// /// The pixel format. - internal class HistogramEqualizationProcessor : ImageProcessor + internal abstract class HistogramEqualizationProcessor : ImageProcessor where TPixel : struct, IPixel { /// @@ -23,11 +18,16 @@ internal class HistogramEqualizationProcessor : ImageProcessor /// /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images /// or 65536 for 16-bit grayscale images. - public HistogramEqualizationProcessor(int luminanceLevels) + /// Indicates, if histogram bins should be clipped. + /// The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. + protected HistogramEqualizationProcessor(int luminanceLevels, bool clipHistogram, int clipLimit) { Guard.MustBeGreaterThan(luminanceLevels, 0, nameof(luminanceLevels)); + Guard.MustBeGreaterThan(clipLimit, 1, nameof(clipLimit)); this.LuminanceLevels = luminanceLevels; + this.ClipHistogramEnabled = clipHistogram; + this.ClipLimit = clipLimit; } /// @@ -35,42 +35,15 @@ public HistogramEqualizationProcessor(int luminanceLevels) /// public int LuminanceLevels { get; } - /// - protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) - { - MemoryAllocator memoryAllocator = configuration.MemoryAllocator; - int numberOfPixels = source.Width * source.Height; - Span pixels = source.GetPixelSpan(); - - // Build the histogram of the grayscale levels. - using (IBuffer histogramBuffer = memoryAllocator.AllocateClean(this.LuminanceLevels)) - using (IBuffer cdfBuffer = memoryAllocator.AllocateClean(this.LuminanceLevels)) - { - Span histogram = histogramBuffer.GetSpan(); - for (int i = 0; i < pixels.Length; i++) - { - TPixel sourcePixel = pixels[i]; - int luminance = this.GetLuminance(sourcePixel, this.LuminanceLevels); - histogram[luminance]++; - } - - // Calculate the cumulative distribution function, which will map each input pixel to a new value. - Span cdf = cdfBuffer.GetSpan(); - int cdfMin = this.CalculateCdf(cdf, histogram); - - // Apply the cdf to each pixel of the image - float numberOfPixelsMinusCdfMin = numberOfPixels - cdfMin; - for (int i = 0; i < pixels.Length; i++) - { - TPixel sourcePixel = pixels[i]; - - int luminance = this.GetLuminance(sourcePixel, this.LuminanceLevels); - float luminanceEqualized = cdf[luminance] / numberOfPixelsMinusCdfMin; + /// + /// Gets a value indicating whether to clip the histogram bins at a specific value. + /// + public bool ClipHistogramEnabled { get; } - pixels[i].PackFromVector4(new Vector4(luminanceEqualized)); - } - } - } + /// + /// Gets the histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. + /// + public int ClipLimit { get; } /// /// Calculates the cumulative distribution function. @@ -108,12 +81,38 @@ protected int CalculateCdf(Span cdf, Span histogram) return cdfMin; } + /// + /// AHE tends to over amplify the contrast in near-constant regions of the image, since the histogram in such regions is highly concentrated. + /// Clipping the histogram is meant to reduce this effect, by cutting of histogram bin's which exceed a certain amount and redistribute + /// the values over the clip limit to all other bins equally. + /// + /// The histogram to apply the clipping. + /// The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. + protected void ClipHistogram(Span histogram, int clipLimit) + { + int sumOverClip = 0; + for (int i = 0; i < histogram.Length; i++) + { + if (histogram[i] > clipLimit) + { + sumOverClip += histogram[i] - clipLimit; + histogram[i] = clipLimit; + } + } + + int addToEachBin = (int)Math.Floor(sumOverClip / (double)this.LuminanceLevels); + for (int i = 0; i < histogram.Length; i++) + { + histogram[i] += addToEachBin; + } + } + /// /// Convert the pixel values to grayscale using ITU-R Recommendation BT.709. /// /// The pixel to get the luminance from /// The number of luminance levels (256 for 8 bit, 65536 for 16 bit grayscale images) - [MethodImpl(InliningOptions.ShortMethod)] + [System.Runtime.CompilerServices.MethodImpl(InliningOptions.ShortMethod)] protected int GetLuminance(TPixel sourcePixel, int luminanceLevels) { // Convert to grayscale using ITU-R Recommendation BT.709 diff --git a/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs b/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs index 1595ed32cc..86d343e423 100644 --- a/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs +++ b/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs @@ -3,6 +3,7 @@ using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Normalization; using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Normalization @@ -50,7 +51,10 @@ public void HistogramEqualizationTest(int luminanceLevels) }; // Act - image.Mutate(x => x.HistogramEqualization(luminanceLevels)); + image.Mutate(x => x.HistogramEqualization(new HistogramEqualizationOptions() + { + LuminanceLevels = luminanceLevels + })); // Assert for (int y = 0; y < 8; y++) From adb06efe29ab3c4d9b94b496564584688ec40801 Mon Sep 17 00:00:00 2001 From: popow Date: Sat, 4 Aug 2018 18:53:08 +0200 Subject: [PATCH 10/50] small improvements --- .../Normalization/AdaptiveHistEqualizationProcessor.cs | 7 +++---- .../Normalization/GlobalHistogramEqualizationProcessor.cs | 2 +- .../Normalization/HistogramEqualizationOptions.cs | 4 ++-- .../Normalization/HistogramEqualizationProcessor.cs | 7 +++++-- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs index 218d2680f9..ad0543bcfd 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs @@ -26,11 +26,11 @@ internal class AdaptiveHistEqualizationProcessor : HistogramEqualization /// or 65536 for 16-bit grayscale images. /// Indicating whether to clip the histogram bins at a specific value. /// The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. - /// The grid size of the adaptive histogram equalization. + /// The grid size of the adaptive histogram equalization. Minimum value is 4. public AdaptiveHistEqualizationProcessor(int luminanceLevels, bool clipHistogram, int clipLimit, int gridSize) : base(luminanceLevels, clipHistogram, clipLimit) { - Guard.MustBeGreaterThan(gridSize, 8, nameof(gridSize)); + Guard.MustBeGreaterThanOrEqualTo(gridSize, 4, nameof(gridSize)); this.GridSize = gridSize; } @@ -84,14 +84,13 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source } // Calculate the cumulative distribution function, which will map each input pixel in the current grid to a new value. - cdf.Clear(); int cdfMin = this.ClipHistogramEnabled ? this.CalculateCdf(cdf, histogramCopy) : this.CalculateCdf(cdf, histogram); float numberOfPixelsMinusCdfMin = pixelsInGrid - cdfMin; // Map the current pixel to the new equalized value int luminance = this.GetLuminance(source[x, y], this.LuminanceLevels); float luminanceEqualized = cdf[luminance] / numberOfPixelsMinusCdfMin; - targetPixels[x, y].PackFromVector4(new Vector4((float)luminanceEqualized)); + targetPixels[x, y].PackFromVector4(new Vector4(luminanceEqualized)); // Remove top most row from the histogram, mirroring rows which exceeds the borders. Span rowSpan = this.GetPixelRow(source, x - halfGridSize, y - halfGridSize, this.GridSize); diff --git a/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs index 423840097d..6f3bfc13fe 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs @@ -36,10 +36,10 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source int numberOfPixels = source.Width * source.Height; Span pixels = source.GetPixelSpan(); - // Build the histogram of the grayscale levels. using (IBuffer histogramBuffer = memoryAllocator.AllocateClean(this.LuminanceLevels)) using (IBuffer cdfBuffer = memoryAllocator.AllocateClean(this.LuminanceLevels)) { + // Build the histogram of the grayscale levels. Span histogram = histogramBuffer.GetSpan(); for (int i = 0; i < pixels.Length; i++) { diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs index 20aa113ac7..fb021f3050 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs @@ -26,9 +26,9 @@ public class HistogramEqualizationOptions /// /// Gets or sets the histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. - /// Defaults to 80. + /// Defaults to 60. /// - public int ClipLimit { get; set; } = 80; + public int ClipLimit { get; set; } = 60; /// /// Gets or sets the size of the grid for the adaptive histogram equalization. Defaults to 32. diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs index 298de13881..243763d741 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs @@ -101,9 +101,12 @@ protected void ClipHistogram(Span histogram, int clipLimit) } int addToEachBin = (int)Math.Floor(sumOverClip / (double)this.LuminanceLevels); - for (int i = 0; i < histogram.Length; i++) + if (addToEachBin > 0) { - histogram[i] += addToEachBin; + for (int i = 0; i < histogram.Length; i++) + { + histogram[i] += addToEachBin; + } } } From e6011f05f2af655251b8eea430f9ccfed8c9ea07 Mon Sep 17 00:00:00 2001 From: popow Date: Mon, 6 Aug 2018 19:32:25 +0200 Subject: [PATCH 11/50] clipLimit now in percent of the total number of pixels in the grid --- .../HistogramEqualizationExtension.cs | 6 +++--- .../AdaptiveHistEqualizationProcessor.cs | 8 ++++---- .../GlobalHistogramEqualizationProcessor.cs | 8 ++++---- .../HistogramEqualizationOptions.cs | 6 +++--- .../HistogramEqualizationProcessor.cs | 18 ++++++++++-------- 5 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/ImageSharp/Processing/HistogramEqualizationExtension.cs b/src/ImageSharp/Processing/HistogramEqualizationExtension.cs index 3a279cc5fc..af83934741 100644 --- a/src/ImageSharp/Processing/HistogramEqualizationExtension.cs +++ b/src/ImageSharp/Processing/HistogramEqualizationExtension.cs @@ -40,15 +40,15 @@ private static HistogramEqualizationProcessor GetProcessor(Histo switch (options.Method) { case HistogramEqualizationMethod.Global: - processor = new GlobalHistogramEqualizationProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimit); + processor = new GlobalHistogramEqualizationProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimitPercentage); break; case HistogramEqualizationMethod.Adaptive: - processor = new AdaptiveHistEqualizationProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimit, options.GridSize); + processor = new AdaptiveHistEqualizationProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimitPercentage, options.GridSize); break; default: - processor = new GlobalHistogramEqualizationProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimit); + processor = new GlobalHistogramEqualizationProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimitPercentage); break; } diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs index ad0543bcfd..d13a0edf62 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs @@ -25,10 +25,10 @@ internal class AdaptiveHistEqualizationProcessor : HistogramEqualization /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images /// or 65536 for 16-bit grayscale images. /// Indicating whether to clip the histogram bins at a specific value. - /// The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. + /// Histogram clip limit in percent of the total pixels in the grid. Histogram bins which exceed this limit, will be capped at this value. /// The grid size of the adaptive histogram equalization. Minimum value is 4. - public AdaptiveHistEqualizationProcessor(int luminanceLevels, bool clipHistogram, int clipLimit, int gridSize) - : base(luminanceLevels, clipHistogram, clipLimit) + public AdaptiveHistEqualizationProcessor(int luminanceLevels, bool clipHistogram, float clipLimitPercentage, int gridSize) + : base(luminanceLevels, clipHistogram, clipLimitPercentage) { Guard.MustBeGreaterThanOrEqualTo(gridSize, 4, nameof(gridSize)); @@ -80,7 +80,7 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source { // 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); + this.ClipHistogram(histogramCopy, this.ClipLimitPercentage, pixelsInGrid); } // Calculate the cumulative distribution function, which will map each input pixel in the current grid to a new value. diff --git a/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs index 6f3bfc13fe..f7a4349106 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs @@ -23,9 +23,9 @@ internal class GlobalHistogramEqualizationProcessor : HistogramEqualizat /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images /// or 65536 for 16-bit grayscale images. /// Indicating whether to clip the histogram bins at a specific value. - /// The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. - public GlobalHistogramEqualizationProcessor(int luminanceLevels, bool clipHistogram, int clipLimit) - : base(luminanceLevels, clipHistogram, clipLimit) + /// Histogram clip limit in percent of the total pixels. Histogram bins which exceed this limit, will be capped at this value. + public GlobalHistogramEqualizationProcessor(int luminanceLevels, bool clipHistogram, float clipLimitPercentage) + : base(luminanceLevels, clipHistogram, clipLimitPercentage) { } @@ -50,7 +50,7 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source if (this.ClipHistogramEnabled) { - this.ClipHistogram(histogram, this.ClipLimit); + this.ClipHistogram(histogram, this.ClipLimitPercentage, numberOfPixels); } // Calculate the cumulative distribution function, which will map each input pixel to a new value. diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs index fb021f3050..9a6502854f 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs @@ -25,10 +25,10 @@ public class HistogramEqualizationOptions public bool ClipHistogram { get; set; } = true; /// - /// Gets or sets the histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. - /// Defaults to 60. + /// Gets or sets the histogram clip limit in percent of the total pixels in the grid. Histogram bins which exceed this limit, will be capped at this value. + /// Defaults to 0.35. /// - public int ClipLimit { get; set; } = 60; + public float ClipLimitPercentage { get; set; } = 0.035f; /// /// Gets or sets the size of the grid for the adaptive histogram equalization. Defaults to 32. diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs index 243763d741..bf46390328 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs @@ -19,15 +19,15 @@ internal abstract class HistogramEqualizationProcessor : ImageProcessor< /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images /// or 65536 for 16-bit grayscale images. /// Indicates, if histogram bins should be clipped. - /// The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. - protected HistogramEqualizationProcessor(int luminanceLevels, bool clipHistogram, int clipLimit) + /// Histogram clip limit in percent of the total pixels in the grid. Histogram bins which exceed this limit, will be capped at this value. + protected HistogramEqualizationProcessor(int luminanceLevels, bool clipHistogram, float clipLimitPercentage) { Guard.MustBeGreaterThan(luminanceLevels, 0, nameof(luminanceLevels)); - Guard.MustBeGreaterThan(clipLimit, 1, nameof(clipLimit)); + Guard.MustBeGreaterThan(clipLimitPercentage, 0.0f, nameof(clipLimitPercentage)); this.LuminanceLevels = luminanceLevels; this.ClipHistogramEnabled = clipHistogram; - this.ClipLimit = clipLimit; + this.ClipLimitPercentage = clipLimitPercentage; } /// @@ -41,9 +41,9 @@ protected HistogramEqualizationProcessor(int luminanceLevels, bool clipHistogram public bool ClipHistogramEnabled { get; } /// - /// Gets the histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. + /// Gets the histogram clip limit in percent of the total pixels in the grid. Histogram bins which exceed this limit, will be capped at this value. /// - public int ClipLimit { get; } + public float ClipLimitPercentage { get; } /// /// Calculates the cumulative distribution function. @@ -87,9 +87,11 @@ protected int CalculateCdf(Span cdf, Span histogram) /// the values over the clip limit to all other bins equally. /// /// The histogram to apply the clipping. - /// The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. - protected void ClipHistogram(Span histogram, int clipLimit) + /// Histogram clip limit in percent of the total pixels in the grid. Histogram bins which exceed this limit, will be capped at this value. + /// The numbers of pixels inside the grid. + protected void ClipHistogram(Span histogram, float clipLimitPercentage, int pixelCount) { + int clipLimit = Convert.ToInt32(pixelCount * clipLimitPercentage); int sumOverClip = 0; for (int i = 0; i < histogram.Length; i++) { From 94d85db455545508a167672b4f6a6283286e50d1 Mon Sep 17 00:00:00 2001 From: popow Date: Tue, 7 Aug 2018 19:41:13 +0200 Subject: [PATCH 12/50] optimization: only calculating the cdf until the maximum histogram index --- .../AdaptiveHistEqualizationProcessor.cs | 79 ++++++++++++++----- .../GlobalHistogramEqualizationProcessor.cs | 2 +- .../HistogramEqualizationOptions.cs | 4 +- .../HistogramEqualizationProcessor.cs | 9 ++- 4 files changed, 66 insertions(+), 28 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs index d13a0edf62..62f1d18b9c 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs @@ -64,14 +64,17 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source Span histogram = histogramBuffer.GetSpan(); Span histogramCopy = histogramBufferCopy.GetSpan(); Span cdf = cdfBuffer.GetSpan(); - - histogram.Clear(); + int maxHistIdx = 0; // Build the histogram of grayscale values for the current grid. for (int dy = -halfGridSize; dy < halfGridSize; dy++) { Span rowSpan = this.GetPixelRow(source, x - halfGridSize, dy, this.GridSize); - this.AddPixelsTooHistogram(rowSpan, histogram, this.LuminanceLevels); + int maxIdx = this.AddPixelsTooHistogram(rowSpan, histogram, this.LuminanceLevels); + if (maxIdx > maxHistIdx) + { + maxHistIdx = maxIdx; + } } for (int y = 0; y < source.Height; y++) @@ -79,12 +82,12 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source 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); + histogram.Slice(0, maxHistIdx).CopyTo(histogramCopy); this.ClipHistogram(histogramCopy, this.ClipLimitPercentage, pixelsInGrid); } // Calculate the cumulative distribution function, which will map each input pixel in the current grid to a new value. - int cdfMin = this.ClipHistogramEnabled ? this.CalculateCdf(cdf, histogramCopy) : this.CalculateCdf(cdf, histogram); + int cdfMin = this.ClipHistogramEnabled ? this.CalculateCdf(cdf, histogramCopy, maxHistIdx) : this.CalculateCdf(cdf, histogram, maxHistIdx); float numberOfPixelsMinusCdfMin = pixelsInGrid - cdfMin; // Map the current pixel to the new equalized value @@ -94,11 +97,15 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source // Remove top most row from the histogram, mirroring rows which exceeds the borders. Span rowSpan = this.GetPixelRow(source, x - halfGridSize, y - halfGridSize, this.GridSize); - this.RemovePixelsFromHistogram(rowSpan, histogram, this.LuminanceLevels); + maxHistIdx = this.RemovePixelsFromHistogram(rowSpan, histogram, this.LuminanceLevels, maxHistIdx); // Add new bottom row to the histogram, mirroring rows which exceeds the borders. rowSpan = this.GetPixelRow(source, x - halfGridSize, y + halfGridSize, this.GridSize); - this.AddPixelsTooHistogram(rowSpan, histogram, this.LuminanceLevels); + int maxIdx = this.AddPixelsTooHistogram(rowSpan, histogram, this.LuminanceLevels); + if (maxIdx > maxHistIdx) + { + maxHistIdx = maxIdx; + } } } }); @@ -127,34 +134,39 @@ private Span GetPixelRow(ImageFrame source, int x, int y, int gr y = source.Height - diff - 1; } - // special cases for the left and the right border where GetPixelRowSpan can not be used + // Special cases for the left and the right border where GetPixelRowSpan can not be used if (x < 0) { - var rowPixels = new List(); + var rowPixels = new TPixel[gridSize]; + int idx = 0; for (int dx = x; dx < x + gridSize; dx++) { - rowPixels.Add(source[Math.Abs(dx), y]); + rowPixels[idx] = source[Math.Abs(dx), y]; + idx++; } - return rowPixels.ToArray(); + return rowPixels; } else if (x + gridSize > source.Width) { - var rowPixels = new List(); + var rowPixels = new TPixel[gridSize]; + int idx = 0; for (int dx = x; dx < x + gridSize; dx++) { if (dx >= source.Width) { int diff = dx - source.Width; - rowPixels.Add(source[dx - diff - 1, y]); + rowPixels[idx] = source[dx - diff - 1, y]; } else { - rowPixels.Add(source[dx, y]); + rowPixels[idx] = source[dx, y]; } + + idx++; } - return rowPixels.ToArray(); + return rowPixels; } return source.GetPixelRowSpan(y).Slice(start: x, length: gridSize); @@ -166,13 +178,21 @@ private Span GetPixelRow(ImageFrame source, int x, int y, int gr /// The grey values to add /// The histogram /// The number of different luminance levels. - private void AddPixelsTooHistogram(Span greyValues, Span histogram, int luminanceLevels) + /// The maximum index where a value was changed. + private int AddPixelsTooHistogram(Span greyValues, Span histogram, int luminanceLevels) { - for (int i = 0; i < greyValues.Length; i++) + int maxIdx = 0; + for (int idx = 0; idx < greyValues.Length; idx++) { - int luminance = this.GetLuminance(greyValues[i], luminanceLevels); + int luminance = this.GetLuminance(greyValues[idx], luminanceLevels); histogram[luminance]++; + if (luminance > maxIdx) + { + maxIdx = luminance; + } } + + return maxIdx; } /// @@ -181,13 +201,30 @@ private void AddPixelsTooHistogram(Span greyValues, Span histogram, /// The grey values to remove /// The histogram /// The number of different luminance levels. - private void RemovePixelsFromHistogram(Span greyValues, Span histogram, int luminanceLevels) + /// The current maximum index of the histogram. + /// The (maybe changed) maximum index of the histogram. + private int RemovePixelsFromHistogram(Span greyValues, Span histogram, int luminanceLevels, int maxHistIdx) { - for (int i = 0; i < greyValues.Length; i++) + for (int idx = 0; idx < greyValues.Length; idx++) { - int luminance = this.GetLuminance(greyValues[i], luminanceLevels); + int luminance = this.GetLuminance(greyValues[idx], luminanceLevels); histogram[luminance]--; + + // If the histogram at the maximum index has changed to 0, search for the next smaller value. + if (luminance == maxHistIdx && histogram[luminance] == 0) + { + for (int j = luminance; j >= 0; j--) + { + maxHistIdx = j; + if (histogram[j] != 0) + { + break; + } + } + } } + + return maxHistIdx; } } } diff --git a/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs index f7a4349106..cdf1f43c08 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs @@ -55,7 +55,7 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source // Calculate the cumulative distribution function, which will map each input pixel to a new value. Span cdf = cdfBuffer.GetSpan(); - int cdfMin = this.CalculateCdf(cdf, histogram); + int cdfMin = this.CalculateCdf(cdf, histogram, histogram.Length - 1); // Apply the cdf to each pixel of the image float numberOfPixelsMinusCdfMin = numberOfPixels - cdfMin; diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs index 9a6502854f..6708a36998 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs @@ -20,9 +20,9 @@ public class HistogramEqualizationOptions public int LuminanceLevels { get; set; } = 256; /// - /// Gets or sets a value indicating whether to clip the histogram bins at a specific value. Defaults to true. + /// Gets or sets a value indicating whether to clip the histogram bins at a specific value. Defaults to false. /// - public bool ClipHistogram { get; set; } = true; + public bool ClipHistogram { get; set; } = false; /// /// Gets or sets the histogram clip limit in percent of the total pixels in the grid. Histogram bins which exceed this limit, will be capped at this value. diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs index bf46390328..4928a54674 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs @@ -50,12 +50,13 @@ protected HistogramEqualizationProcessor(int luminanceLevels, bool clipHistogram /// /// The array holding the cdf. /// 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) + protected int CalculateCdf(Span cdf, Span histogram, int maxIdx) { // Calculate the cumulative histogram int histSum = 0; - for (int i = 0; i < histogram.Length; i++) + for (int i = 0; i <= maxIdx; i++) { histSum += histogram[i]; cdf[i] = histSum; @@ -63,7 +64,7 @@ protected int CalculateCdf(Span cdf, Span histogram) // Get the first none zero value of the cumulative histogram int cdfMin = 0; - for (int i = 0; i < histogram.Length; i++) + for (int i = 0; i <= maxIdx; i++) { if (cdf[i] != 0) { @@ -73,7 +74,7 @@ protected int CalculateCdf(Span cdf, Span histogram) } // Creating the lookup table: subtracting cdf min, so we do not need to do that inside the for loop - for (int i = 0; i < histogram.Length; i++) + for (int i = 0; i <= maxIdx; i++) { cdf[i] = Math.Max(0, cdf[i] - cdfMin); } From 7fec97849df87c343a6e3c0601149f2608330e15 Mon Sep 17 00:00:00 2001 From: popow Date: Wed, 8 Aug 2018 19:08:57 +0200 Subject: [PATCH 13/50] fix: using configuration from the parameter instead of the default --- .../Normalization/AdaptiveHistEqualizationProcessor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs index c494980861..12c897183f 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs @@ -54,7 +54,7 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source ParallelFor.WithConfiguration( 0, source.Width, - Configuration.Default, + configuration, x => { using (System.Buffers.IMemoryOwner histogramBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean)) From 62b3ee3c49b38b868b5e2f615045df9030e0e33f Mon Sep 17 00:00:00 2001 From: popow Date: Sun, 12 Aug 2018 17:18:20 +0200 Subject: [PATCH 14/50] removed unnecessary loops in CalculateCdf, fixed typo in method name AddPixelsToHistogram --- .../AdaptiveHistEqualizationProcessor.cs | 7 ++--- .../HistogramEqualizationProcessor.cs | 30 +++++-------------- 2 files changed, 11 insertions(+), 26 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs index 12c897183f..5e3dea6c6f 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs @@ -3,7 +3,6 @@ using System; using System.Numerics; -using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -70,7 +69,7 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source for (int dy = -halfGridSize; dy < halfGridSize; dy++) { Span rowSpan = this.GetPixelRow(source, (int)x - halfGridSize, dy, this.GridSize); - int maxIdx = this.AddPixelsTooHistogram(rowSpan, histogram, this.LuminanceLevels); + int maxIdx = this.AddPixelsToHistogram(rowSpan, histogram, this.LuminanceLevels); if (maxIdx > maxHistIdx) { maxHistIdx = maxIdx; @@ -101,7 +100,7 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source // Add new bottom row to the histogram, mirroring rows which exceeds the borders. rowSpan = this.GetPixelRow(source, x - halfGridSize, y + halfGridSize, this.GridSize); - int maxIdx = this.AddPixelsTooHistogram(rowSpan, histogram, this.LuminanceLevels); + int maxIdx = this.AddPixelsToHistogram(rowSpan, histogram, this.LuminanceLevels); if (maxIdx > maxHistIdx) { maxHistIdx = maxIdx; @@ -179,7 +178,7 @@ private Span GetPixelRow(ImageFrame source, int x, int y, int gr /// The histogram /// The number of different luminance levels. /// The maximum index where a value was changed. - private int AddPixelsTooHistogram(Span greyValues, Span histogram, int luminanceLevels) + private int AddPixelsToHistogram(Span greyValues, Span histogram, int luminanceLevels) { int maxIdx = 0; for (int idx = 0; idx < greyValues.Length; idx++) diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs index a86c8b6183..e52aff1e76 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs @@ -2,11 +2,6 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Buffers; -using System.Numerics; -using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors.Normalization @@ -59,29 +54,20 @@ protected HistogramEqualizationProcessor(int luminanceLevels, bool clipHistogram /// The first none zero value of the cdf. protected int CalculateCdf(Span cdf, Span histogram, int maxIdx) { - // Calculate the cumulative histogram int histSum = 0; - for (int i = 0; i <= maxIdx; i++) - { - histSum += histogram[i]; - cdf[i] = histSum; - } - - // Get the first none zero value of the cumulative histogram int cdfMin = 0; + bool cdfMinFound = false; for (int i = 0; i <= maxIdx; i++) { - if (cdf[i] != 0) + histSum += histogram[i]; + if (!cdfMinFound && histSum != 0) { - cdfMin = cdf[i]; - break; + cdfMin = histSum; + cdfMinFound = true; } - } - // Creating the lookup table: subtracting cdf min, so we do not need to do that inside the for loop - for (int i = 0; i <= maxIdx; i++) - { - cdf[i] = Math.Max(0, cdf[i] - cdfMin); + // 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); } return cdfMin; @@ -108,7 +94,7 @@ protected void ClipHistogram(Span histogram, float clipLimitPercentage, int } } - int addToEachBin = (int)Math.Floor(sumOverClip / (double)this.LuminanceLevels); + int addToEachBin = sumOverClip > 0 ? (int)Math.Floor(sumOverClip / (double)this.LuminanceLevels) : 0; if (addToEachBin > 0) { for (int i = 0; i < histogram.Length; i++) From 23661e12d2367b080941febfb410d4b2ca93fa5a Mon Sep 17 00:00:00 2001 From: popow Date: Sun, 19 Aug 2018 14:00:48 +0200 Subject: [PATCH 15/50] added different approach for ahe: image is split up in tiles, cdf is computed for each tile. Grey value will be determined by interpolating between 4 tiles --- .../HistogramEqualizationExtension.cs | 4 + .../AdaptiveHistEqualizationProcessor.cs | 257 ++++++++---------- .../AdaptiveHistEqualizationSWProcessor.cs | 229 ++++++++++++++++ .../HistogramEqualizationMethod.cs | 7 +- 4 files changed, 350 insertions(+), 147 deletions(-) create mode 100644 src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs diff --git a/src/ImageSharp/Processing/HistogramEqualizationExtension.cs b/src/ImageSharp/Processing/HistogramEqualizationExtension.cs index af83934741..cbc96d5c3f 100644 --- a/src/ImageSharp/Processing/HistogramEqualizationExtension.cs +++ b/src/ImageSharp/Processing/HistogramEqualizationExtension.cs @@ -47,6 +47,10 @@ private static HistogramEqualizationProcessor GetProcessor(Histo processor = new AdaptiveHistEqualizationProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimitPercentage, options.GridSize); break; + case HistogramEqualizationMethod.AdaptiveSlidingWindow: + processor = new AdaptiveHistEqualizationSWProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimitPercentage, options.GridSize); + break; + default: processor = new GlobalHistogramEqualizationProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimitPercentage); break; diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs index 5e3dea6c6f..94000bd8da 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs @@ -48,182 +48,147 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source int pixelsInGrid = this.GridSize * this.GridSize; int halfGridSize = this.GridSize / 2; - using (Buffer2D targetPixels = configuration.MemoryAllocator.Allocate2D(source.Width, source.Height)) + int xtiles = Convert.ToInt32(Math.Ceiling(source.Width / (double)this.GridSize)); + int ytiles = Convert.ToInt32(Math.Ceiling(source.Height / (double)this.GridSize)); + + var cdfData = new CdfData[xtiles, ytiles]; + using (System.Buffers.IMemoryOwner histogramBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean)) + using (System.Buffers.IMemoryOwner cdfBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean)) { - ParallelFor.WithConfiguration( - 0, - source.Width, - configuration, - x => + Span histogram = histogramBuffer.GetSpan(); + Span cdf = cdfBuffer.GetSpan(); + + // The image is split up in square tiles of the size of the parameter GridSize. + // For each tile the cumulative distribution function will be calculated. + int cdfPosX = 0; + int cdfPosY = 0; + for (int y = 0; y < source.Height; y += this.GridSize) + { + cdfPosX = 0; + for (int x = 0; x < source.Width; x += this.GridSize) { - using (System.Buffers.IMemoryOwner histogramBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean)) - using (System.Buffers.IMemoryOwner histogramBufferCopy = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean)) - using (System.Buffers.IMemoryOwner cdfBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean)) + histogram.Clear(); + cdf.Clear(); + int ylimit = Math.Min(y + this.GridSize, source.Height); + int xlimit = Math.Min(x + this.GridSize, source.Width); + for (int dy = y; dy < ylimit; dy++) { - Span histogram = histogramBuffer.GetSpan(); - Span histogramCopy = histogramBufferCopy.GetSpan(); - Span cdf = cdfBuffer.GetSpan(); - int maxHistIdx = 0; - - // Build the histogram of grayscale values for the current grid. - for (int dy = -halfGridSize; dy < halfGridSize; dy++) + for (int dx = x; dx < xlimit; dx++) { - Span rowSpan = this.GetPixelRow(source, (int)x - halfGridSize, dy, this.GridSize); - int maxIdx = this.AddPixelsToHistogram(rowSpan, histogram, this.LuminanceLevels); - if (maxIdx > maxHistIdx) - { - maxHistIdx = maxIdx; - } + int luminace = this.GetLuminance(source[dx, dy], this.LuminanceLevels); + histogram[luminace]++; } + } - for (int y = 0; y < source.Height; 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.Slice(0, maxHistIdx).CopyTo(histogramCopy); - this.ClipHistogram(histogramCopy, this.ClipLimitPercentage, pixelsInGrid); - } - - // Calculate the cumulative distribution function, which will map each input pixel in the current grid to a new value. - int cdfMin = this.ClipHistogramEnabled ? this.CalculateCdf(cdf, histogramCopy, maxHistIdx) : this.CalculateCdf(cdf, histogram, maxHistIdx); - float numberOfPixelsMinusCdfMin = pixelsInGrid - cdfMin; - - // Map the current pixel to the new equalized value - int luminance = this.GetLuminance(source[x, y], this.LuminanceLevels); - float luminanceEqualized = cdf[luminance] / numberOfPixelsMinusCdfMin; - targetPixels[x, y].PackFromVector4(new Vector4(luminanceEqualized)); - - // Remove top most row from the histogram, mirroring rows which exceeds the borders. - Span rowSpan = this.GetPixelRow(source, x - halfGridSize, y - halfGridSize, this.GridSize); - maxHistIdx = this.RemovePixelsFromHistogram(rowSpan, histogram, this.LuminanceLevels, maxHistIdx); - - // Add new bottom row to the histogram, mirroring rows which exceeds the borders. - rowSpan = this.GetPixelRow(source, x - halfGridSize, y + halfGridSize, this.GridSize); - int maxIdx = this.AddPixelsToHistogram(rowSpan, histogram, this.LuminanceLevels); - if (maxIdx > maxHistIdx) - { - maxHistIdx = maxIdx; - } - } + if (this.ClipHistogramEnabled) + { + this.ClipHistogram(histogram, this.ClipLimitPercentage, pixelsInGrid); } - }); - Buffer2D.SwapOrCopyContent(source.PixelBuffer, targetPixels); - } - } + int cdfMin = this.CalculateCdf(cdf, histogram, histogram.Length - 1); + var currentCdf = new CdfData(cdf.ToArray(), cdfMin); + cdfData[cdfPosX, cdfPosY] = currentCdf; - /// - /// Get the a pixel row at a given position with a length of the grid size. Mirrors pixels which exceeds the edges. - /// - /// The source image. - /// The x position. - /// The y position. - /// The grid size. - /// A pixel row of the length of the grid size. - private Span GetPixelRow(ImageFrame source, int x, int y, int gridSize) - { - if (y < 0) - { - y = Math.Abs(y); - } - else if (y >= source.Height) - { - int diff = y - source.Height; - y = source.Height - diff - 1; - } + cdfPosX++; + } - // Special cases for the left and the right border where GetPixelRowSpan can not be used - if (x < 0) - { - var rowPixels = new TPixel[gridSize]; - int idx = 0; - for (int dx = x; dx < x + gridSize; dx++) - { - rowPixels[idx] = source[Math.Abs(dx), y]; - idx++; + cdfPosY++; } - return rowPixels; - } - else if (x + gridSize > source.Width) - { - var rowPixels = new TPixel[gridSize]; - int idx = 0; - for (int dx = x; dx < x + gridSize; dx++) + int tilePosX = 0; + int tilePosY = 0; + for (int y = halfGridSize; y < source.Height - halfGridSize; y += this.GridSize) { - if (dx >= source.Width) - { - int diff = dx - source.Width; - rowPixels[idx] = source[dx - diff - 1, y]; - } - else + tilePosX = 0; + for (int x = halfGridSize; x < source.Width - halfGridSize; x += this.GridSize) { - rowPixels[idx] = source[dx, y]; + int gridPosX = 0; + int gridPosY = 0; + int ylimit = Math.Min(y + this.GridSize, source.Height); + int xlimit = Math.Min(x + this.GridSize, source.Width); + for (int dy = y; dy < ylimit; dy++) + { + gridPosX = 0; + for (int dx = x; dx < xlimit; dx++) + { + TPixel sourcePixel = source[dx, dy]; + int luminace = this.GetLuminance(sourcePixel, this.LuminanceLevels); + + float cdfLeftTopLuminance = cdfData[tilePosX, tilePosY].RemapGreyValue(luminace, pixelsInGrid); + float cdfRightTopLuminance = cdfData[tilePosX + 1, tilePosY].RemapGreyValue(luminace, pixelsInGrid); + float cdfLeftBottomLuminance = cdfData[tilePosX, tilePosY + 1].RemapGreyValue(luminace, pixelsInGrid); + float cdfRightBottomLuminance = cdfData[tilePosX + 1, tilePosY + 1].RemapGreyValue(luminace, pixelsInGrid); + + float luminanceEqualized = this.BilinearInterpolation(gridPosX, gridPosY, this.GridSize, cdfLeftTopLuminance, cdfRightTopLuminance, cdfLeftBottomLuminance, cdfRightBottomLuminance); + pixels[(dy * source.Width) + dx].PackFromVector4(new Vector4(luminanceEqualized)); + gridPosX++; + } + + gridPosY++; + } + + tilePosX++; } - idx++; + tilePosY++; } - - return rowPixels; } - - return source.GetPixelRowSpan(y).Slice(start: x, length: gridSize); } /// - /// Adds a row of grey values to the histogram. + /// Bilinear interpolation between four tiles. /// - /// The grey values to add - /// The histogram - /// The number of different luminance levels. - /// The maximum index where a value was changed. - private int AddPixelsToHistogram(Span greyValues, Span histogram, int luminanceLevels) + /// X position. + /// Y position. + /// The size of the grid. + /// Luminance from tile top left. + /// Luminance from tile right top. + /// Luminance from tile left bottom. + /// Luminance from tile right bottom. + /// Interpolated Luminance. + private float BilinearInterpolation(int x, int y, int gridSize, float lt, float rt, float lb, float rb) { - int maxIdx = 0; - for (int idx = 0; idx < greyValues.Length; idx++) - { - int luminance = this.GetLuminance(greyValues[idx], luminanceLevels); - histogram[luminance]++; - if (luminance > maxIdx) - { - maxIdx = luminance; - } - } + float r1 = ((gridSize - x) / (float)gridSize * lb) + ((x / (float)gridSize) * rb); + float r2 = ((gridSize - x) / (float)gridSize * lt) + ((x / (float)gridSize) * rt); - return maxIdx; + float res = ((y / ((float)gridSize)) * r1) + (((y - gridSize) / (float)(-gridSize)) * r2); + + return res; } - /// - /// Removes a row of grey values from the histogram. - /// - /// The grey values to remove - /// The histogram - /// The number of different luminance levels. - /// The current maximum index of the histogram. - /// The (maybe changed) maximum index of the histogram. - private int RemovePixelsFromHistogram(Span greyValues, Span histogram, int luminanceLevels, int maxHistIdx) + private class CdfData { - for (int idx = 0; idx < greyValues.Length; idx++) + /// + /// 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) { - int luminance = this.GetLuminance(greyValues[idx], luminanceLevels); - histogram[luminance]--; - - // If the histogram at the maximum index has changed to 0, search for the next smaller value. - if (luminance == maxHistIdx && histogram[luminance] == 0) - { - for (int j = luminance; j >= 0; j--) - { - maxHistIdx = j; - if (histogram[j] != 0) - { - break; - } - } - } + this.Cdf = cdf; + this.CdfMin = cdfMin; } - return maxHistIdx; + /// + /// Gets the CDF. + /// + public int[] Cdf { get; } + + /// + /// Gets minimum value of the cdf. + /// + public int CdfMin { get; } + + /// + /// Remaps the grey value with the cdf. + /// + /// The original luminance. + /// The pixels in grid. + /// The remapped luminance. + public float RemapGreyValue(int luminance, int pixelsInGrid) + { + return (pixelsInGrid - this.CdfMin) == 0 ? this.Cdf[luminance] / (float)pixelsInGrid : this.Cdf[luminance] / (float)(pixelsInGrid - this.CdfMin); + } } } } diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs new file mode 100644 index 0000000000..63b8bc29cf --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs @@ -0,0 +1,229 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.Memory; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Processing.Processors.Normalization +{ + /// + /// Applies an adaptive histogram equalization to the image. + /// + /// The pixel format. + internal class AdaptiveHistEqualizationSWProcessor : HistogramEqualizationProcessor + where TPixel : struct, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images + /// or 65536 for 16-bit grayscale images. + /// Indicating whether to clip the histogram bins at a specific value. + /// Histogram clip limit in percent of the total pixels in the grid. Histogram bins which exceed this limit, will be capped at this value. + /// The grid size of the adaptive histogram equalization. Minimum value is 4. + public AdaptiveHistEqualizationSWProcessor(int luminanceLevels, bool clipHistogram, float clipLimitPercentage, int gridSize) + : base(luminanceLevels, clipHistogram, clipLimitPercentage) + { + Guard.MustBeGreaterThanOrEqualTo(gridSize, 4, nameof(gridSize)); + + this.GridSize = gridSize; + } + + /// + /// Gets the size of the grid for the adaptive histogram equalization. + /// + public int GridSize { get; } + + /// + protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) + { + MemoryAllocator memoryAllocator = configuration.MemoryAllocator; + int numberOfPixels = source.Width * source.Height; + Span pixels = source.GetPixelSpan(); + + int pixelsInGrid = this.GridSize * this.GridSize; + int halfGridSize = this.GridSize / 2; + using (Buffer2D targetPixels = configuration.MemoryAllocator.Allocate2D(source.Width, source.Height)) + { + ParallelFor.WithConfiguration( + 0, + source.Width, + configuration, + x => + { + using (System.Buffers.IMemoryOwner histogramBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean)) + using (System.Buffers.IMemoryOwner histogramBufferCopy = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean)) + using (System.Buffers.IMemoryOwner cdfBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean)) + { + Span histogram = histogramBuffer.GetSpan(); + Span histogramCopy = histogramBufferCopy.GetSpan(); + Span cdf = cdfBuffer.GetSpan(); + int maxHistIdx = 0; + + // Build the histogram of grayscale values for the current grid. + for (int dy = -halfGridSize; dy < halfGridSize; dy++) + { + Span rowSpan = this.GetPixelRow(source, (int)x - halfGridSize, dy, this.GridSize); + int maxIdx = this.AddPixelsToHistogram(rowSpan, histogram, this.LuminanceLevels); + if (maxIdx > maxHistIdx) + { + maxHistIdx = maxIdx; + } + } + + for (int y = 0; y < source.Height; 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.Slice(0, maxHistIdx).CopyTo(histogramCopy); + this.ClipHistogram(histogramCopy, this.ClipLimitPercentage, pixelsInGrid); + } + + // Calculate the cumulative distribution function, which will map each input pixel in the current grid to a new value. + int cdfMin = this.ClipHistogramEnabled ? this.CalculateCdf(cdf, histogramCopy, maxHistIdx) : this.CalculateCdf(cdf, histogram, maxHistIdx); + float numberOfPixelsMinusCdfMin = pixelsInGrid - cdfMin; + + // Map the current pixel to the new equalized value + int luminance = this.GetLuminance(source[x, y], this.LuminanceLevels); + float luminanceEqualized = cdf[luminance] / numberOfPixelsMinusCdfMin; + targetPixels[x, y].PackFromVector4(new Vector4(luminanceEqualized)); + + // Remove top most row from the histogram, mirroring rows which exceeds the borders. + Span rowSpan = this.GetPixelRow(source, x - halfGridSize, y - halfGridSize, this.GridSize); + maxHistIdx = this.RemovePixelsFromHistogram(rowSpan, histogram, this.LuminanceLevels, maxHistIdx); + + // Add new bottom row to the histogram, mirroring rows which exceeds the borders. + rowSpan = this.GetPixelRow(source, x - halfGridSize, y + halfGridSize, this.GridSize); + int maxIdx = this.AddPixelsToHistogram(rowSpan, histogram, this.LuminanceLevels); + if (maxIdx > maxHistIdx) + { + maxHistIdx = maxIdx; + } + } + } + }); + + Buffer2D.SwapOrCopyContent(source.PixelBuffer, targetPixels); + } + } + + /// + /// Get the a pixel row at a given position with a length of the grid size. Mirrors pixels which exceeds the edges. + /// + /// The source image. + /// The x position. + /// The y position. + /// The grid size. + /// A pixel row of the length of the grid size. + private Span GetPixelRow(ImageFrame source, int x, int y, int gridSize) + { + if (y < 0) + { + y = Math.Abs(y); + } + else if (y >= source.Height) + { + int diff = y - source.Height; + y = source.Height - diff - 1; + } + + // Special cases for the left and the right border where GetPixelRowSpan can not be used + if (x < 0) + { + var rowPixels = new TPixel[gridSize]; + int idx = 0; + for (int dx = x; dx < x + gridSize; dx++) + { + rowPixels[idx] = source[Math.Abs(dx), y]; + idx++; + } + + return rowPixels; + } + else if (x + gridSize > source.Width) + { + var rowPixels = new TPixel[gridSize]; + int idx = 0; + for (int dx = x; dx < x + gridSize; dx++) + { + if (dx >= source.Width) + { + int diff = dx - source.Width; + rowPixels[idx] = source[dx - diff - 1, y]; + } + else + { + rowPixels[idx] = source[dx, y]; + } + + idx++; + } + + return rowPixels; + } + + return source.GetPixelRowSpan(y).Slice(start: x, length: gridSize); + } + + /// + /// Adds a row of grey values to the histogram. + /// + /// The grey values to add + /// The histogram + /// The number of different luminance levels. + /// The maximum index where a value was changed. + private int AddPixelsToHistogram(Span greyValues, Span histogram, int luminanceLevels) + { + int maxIdx = 0; + for (int idx = 0; idx < greyValues.Length; idx++) + { + int luminance = this.GetLuminance(greyValues[idx], luminanceLevels); + histogram[luminance]++; + if (luminance > maxIdx) + { + maxIdx = luminance; + } + } + + return maxIdx; + } + + /// + /// Removes a row of grey values from the histogram. + /// + /// The grey values to remove + /// The histogram + /// The number of different luminance levels. + /// The current maximum index of the histogram. + /// The (maybe changed) maximum index of the histogram. + private int RemovePixelsFromHistogram(Span greyValues, Span histogram, int luminanceLevels, int maxHistIdx) + { + for (int idx = 0; idx < greyValues.Length; idx++) + { + int luminance = this.GetLuminance(greyValues[idx], luminanceLevels); + histogram[luminance]--; + + // If the histogram at the maximum index has changed to 0, search for the next smaller value. + if (luminance == maxHistIdx && histogram[luminance] == 0) + { + for (int j = luminance; j >= 0; j--) + { + maxHistIdx = j; + if (histogram[j] != 0) + { + break; + } + } + } + } + + return maxHistIdx; + } + } +} diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs index cae745c9b3..63546c744d 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs @@ -16,6 +16,11 @@ public enum HistogramEqualizationMethod : int /// /// Adaptive histogram equalization. /// - Adaptive + Adaptive, + + /// + /// Adaptive sliding window histogram equalization. + /// + AdaptiveSlidingWindow, } } From 176e7e9f83cc2a3dea0eac5fbcd61498f7c32dcf Mon Sep 17 00:00:00 2001 From: popow Date: Mon, 27 Aug 2018 19:38:14 +0200 Subject: [PATCH 16/50] simplified interpolation between the tiles --- .../AdaptiveHistEqualizationProcessor.cs | 53 ++++++++++++------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs index 94000bd8da..87fb63502d 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs @@ -58,7 +58,7 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source Span histogram = histogramBuffer.GetSpan(); Span cdf = cdfBuffer.GetSpan(); - // The image is split up in square tiles of the size of the parameter GridSize. + // The image is split up into square tiles of the size of the parameter GridSize. // For each tile the cumulative distribution function will be calculated. int cdfPosX = 0; int cdfPosY = 0; @@ -97,10 +97,10 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source int tilePosX = 0; int tilePosY = 0; - for (int y = halfGridSize; y < source.Height - halfGridSize; y += this.GridSize) + for (int y = halfGridSize; y < source.Height - this.GridSize; y += this.GridSize) { tilePosX = 0; - for (int x = halfGridSize; x < source.Width - halfGridSize; x += this.GridSize) + for (int x = halfGridSize; x < source.Width - this.GridSize; x += this.GridSize) { int gridPosX = 0; int gridPosY = 0; @@ -109,17 +109,24 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source for (int dy = y; dy < ylimit; dy++) { gridPosX = 0; + float ty = gridPosY / (float)(this.GridSize - 1); + int yTop = tilePosY; + int yBottom = yTop + 1; for (int dx = x; dx < xlimit; dx++) { TPixel sourcePixel = source[dx, dy]; int luminace = this.GetLuminance(sourcePixel, this.LuminanceLevels); - float cdfLeftTopLuminance = cdfData[tilePosX, tilePosY].RemapGreyValue(luminace, pixelsInGrid); - float cdfRightTopLuminance = cdfData[tilePosX + 1, tilePosY].RemapGreyValue(luminace, pixelsInGrid); - float cdfLeftBottomLuminance = cdfData[tilePosX, tilePosY + 1].RemapGreyValue(luminace, pixelsInGrid); - float cdfRightBottomLuminance = cdfData[tilePosX + 1, tilePosY + 1].RemapGreyValue(luminace, pixelsInGrid); + int xLeft = tilePosX; + int xRight = tilePosX + 1; + + float cdfLeftTopLuminance = cdfData[xLeft, yTop].RemapGreyValue(luminace, pixelsInGrid); + float cdfRightTopLuminance = cdfData[xRight, yTop].RemapGreyValue(luminace, pixelsInGrid); + float cdfLeftBottomLuminance = cdfData[xLeft, yBottom].RemapGreyValue(luminace, pixelsInGrid); + float cdfRightBottomLuminance = cdfData[xRight, yBottom].RemapGreyValue(luminace, pixelsInGrid); + + float luminanceEqualized = this.BilinearInterpolation(gridPosX, gridPosY, gridPosX / (float)(this.GridSize - 1), ty, cdfLeftTopLuminance, cdfRightTopLuminance, cdfLeftBottomLuminance, cdfRightBottomLuminance); - float luminanceEqualized = this.BilinearInterpolation(gridPosX, gridPosY, this.GridSize, cdfLeftTopLuminance, cdfRightTopLuminance, cdfLeftBottomLuminance, cdfRightBottomLuminance); pixels[(dy * source.Width) + dx].PackFromVector4(new Vector4(luminanceEqualized)); gridPosX++; } @@ -140,20 +147,28 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source /// /// X position. /// Y position. - /// The size of the grid. - /// Luminance from tile top left. - /// Luminance from tile right top. - /// Luminance from tile left bottom. - /// Luminance from tile right bottom. + /// The interpolation value in x direction in the range of [0, 1]. + /// The interpolation value in y direction in the range of [0, 1]. + /// Luminance from top left tile. + /// Luminance from right top tile. + /// Luminance from left bottom tile. + /// Luminance from right bottom tile. /// Interpolated Luminance. - private float BilinearInterpolation(int x, int y, int gridSize, float lt, float rt, float lb, float rb) + private float BilinearInterpolation(int x, int y, float tx, float ty, float lt, float rt, float lb, float rb) { - float r1 = ((gridSize - x) / (float)gridSize * lb) + ((x / (float)gridSize) * rb); - float r2 = ((gridSize - x) / (float)gridSize * lt) + ((x / (float)gridSize) * rt); - - float res = ((y / ((float)gridSize)) * r1) + (((y - gridSize) / (float)(-gridSize)) * r2); + return this.LinearInterpolation(this.LinearInterpolation(lt, rt, tx), this.LinearInterpolation(lb, rb, tx), ty); + } - return res; + /// + /// Linear interpolation between two grey values. + /// + /// The left value. + /// 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); } private class CdfData From 516d96f15762373630de4d9730d9497d7586b917 Mon Sep 17 00:00:00 2001 From: popow Date: Sun, 2 Sep 2018 17:13:42 +0200 Subject: [PATCH 17/50] number of tiles is now fixed and depended on the width and height of the image --- .../AdaptiveHistEqualizationProcessor.cs | 89 ++++++++++--------- 1 file changed, 46 insertions(+), 43 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs index 87fb63502d..4e1498ac76 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs @@ -46,12 +46,16 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source int numberOfPixels = source.Width * source.Height; Span pixels = source.GetPixelSpan(); - int pixelsInGrid = this.GridSize * this.GridSize; - int halfGridSize = this.GridSize / 2; - int xtiles = Convert.ToInt32(Math.Ceiling(source.Width / (double)this.GridSize)); - int ytiles = Convert.ToInt32(Math.Ceiling(source.Height / (double)this.GridSize)); + int numTilesX = 20; + int numTilesY = 20; - var cdfData = new CdfData[xtiles, ytiles]; + int tileWidth = Convert.ToInt32(Math.Ceiling(source.Width / (double)numTilesX)); + int tileHeight = Convert.ToInt32(Math.Ceiling(source.Height / (double)numTilesY)); + int pixelsInTile = tileWidth * tileHeight; + int halfTileWidth = tileWidth / 2; + int halfTileHeight = tileHeight / 2; + + var cdfData = new CdfData[numTilesX, numTilesY]; using (System.Buffers.IMemoryOwner histogramBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean)) using (System.Buffers.IMemoryOwner cdfBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean)) { @@ -60,17 +64,17 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source // The image is split up into square tiles of the size of the parameter GridSize. // For each tile the cumulative distribution function will be calculated. - int cdfPosX = 0; - int cdfPosY = 0; - for (int y = 0; y < source.Height; y += this.GridSize) + int tileX = 0; + int tileY = 0; + for (int y = 0; y < source.Height; y += tileHeight) { - cdfPosX = 0; - for (int x = 0; x < source.Width; x += this.GridSize) + tileX = 0; + for (int x = 0; x < source.Width; x += tileWidth) { histogram.Clear(); cdf.Clear(); - int ylimit = Math.Min(y + this.GridSize, source.Height); - int xlimit = Math.Min(x + this.GridSize, source.Width); + int ylimit = Math.Min(y + tileHeight, source.Height); + int xlimit = Math.Min(x + tileWidth, source.Width); for (int dy = y; dy < ylimit; dy++) { for (int dx = x; dx < xlimit; dx++) @@ -82,62 +86,61 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source if (this.ClipHistogramEnabled) { - this.ClipHistogram(histogram, this.ClipLimitPercentage, pixelsInGrid); + this.ClipHistogram(histogram, this.ClipLimitPercentage, pixelsInTile); } int cdfMin = this.CalculateCdf(cdf, histogram, histogram.Length - 1); var currentCdf = new CdfData(cdf.ToArray(), cdfMin); - cdfData[cdfPosX, cdfPosY] = currentCdf; + cdfData[tileX, tileY] = currentCdf; - cdfPosX++; + tileX++; } - cdfPosY++; + tileY++; } - int tilePosX = 0; - int tilePosY = 0; - for (int y = halfGridSize; y < source.Height - this.GridSize; y += this.GridSize) + tileX = 0; + tileY = 0; + for (int y = halfTileHeight; y < source.Height - tileHeight; y += tileHeight) { - tilePosX = 0; - for (int x = halfGridSize; x < source.Width - this.GridSize; x += this.GridSize) + tileX = 0; + for (int x = halfTileWidth; x < source.Width - tileWidth; x += tileWidth) { - int gridPosX = 0; - int gridPosY = 0; - int ylimit = Math.Min(y + this.GridSize, source.Height); - int xlimit = Math.Min(x + this.GridSize, source.Width); + int tilePosX = 0; + int tilePosY = 0; + int ylimit = Math.Min(y + tileHeight, source.Height); + int xlimit = Math.Min(x + tileWidth, source.Width); for (int dy = y; dy < ylimit; dy++) { - gridPosX = 0; - float ty = gridPosY / (float)(this.GridSize - 1); - int yTop = tilePosY; + tilePosX = 0; + float ty = tilePosY / (float)(tileHeight - 1); + int yTop = tileY; int yBottom = yTop + 1; for (int dx = x; dx < xlimit; dx++) { TPixel sourcePixel = source[dx, dy]; int luminace = this.GetLuminance(sourcePixel, this.LuminanceLevels); - int xLeft = tilePosX; - int xRight = tilePosX + 1; - - float cdfLeftTopLuminance = cdfData[xLeft, yTop].RemapGreyValue(luminace, pixelsInGrid); - float cdfRightTopLuminance = cdfData[xRight, yTop].RemapGreyValue(luminace, pixelsInGrid); - float cdfLeftBottomLuminance = cdfData[xLeft, yBottom].RemapGreyValue(luminace, pixelsInGrid); - float cdfRightBottomLuminance = cdfData[xRight, yBottom].RemapGreyValue(luminace, pixelsInGrid); + int xLeft = tileX; + int xRight = tileX + 1; - float luminanceEqualized = this.BilinearInterpolation(gridPosX, gridPosY, gridPosX / (float)(this.GridSize - 1), ty, cdfLeftTopLuminance, cdfRightTopLuminance, cdfLeftBottomLuminance, cdfRightBottomLuminance); + float cdfLeftTopLuminance = cdfData[xLeft, yTop].RemapGreyValue(luminace, pixelsInTile); + float cdfRightTopLuminance = cdfData[xRight, yTop].RemapGreyValue(luminace, pixelsInTile); + float cdfLeftBottomLuminance = cdfData[xLeft, yBottom].RemapGreyValue(luminace, pixelsInTile); + float cdfRightBottomLuminance = cdfData[xRight, yBottom].RemapGreyValue(luminace, pixelsInTile); + float luminanceEqualized = this.BilinearInterpolation(tilePosX, tilePosY, tilePosX / (float)(tileWidth - 1), ty, cdfLeftTopLuminance, cdfRightTopLuminance, cdfLeftBottomLuminance, cdfRightBottomLuminance); pixels[(dy * source.Width) + dx].PackFromVector4(new Vector4(luminanceEqualized)); - gridPosX++; + tilePosX++; } - gridPosY++; + tilePosY++; } - tilePosX++; + tileX++; } - tilePosY++; + tileY++; } } } @@ -198,11 +201,11 @@ public CdfData(int[] cdf, int cdfMin) /// Remaps the grey value with the cdf. /// /// The original luminance. - /// The pixels in grid. + /// The number of pixels in the tile. /// The remapped luminance. - public float RemapGreyValue(int luminance, int pixelsInGrid) + public float RemapGreyValue(int luminance, int pixelsInTile) { - return (pixelsInGrid - this.CdfMin) == 0 ? this.Cdf[luminance] / (float)pixelsInGrid : this.Cdf[luminance] / (float)(pixelsInGrid - this.CdfMin); + return (pixelsInTile - this.CdfMin) == 0 ? this.Cdf[luminance] / (float)pixelsInTile : this.Cdf[luminance] / (float)(pixelsInTile - this.CdfMin); } } } From 254cd336529f6947f6caac63ba266a02ab6392eb Mon Sep 17 00:00:00 2001 From: popow Date: Sun, 2 Sep 2018 18:07:58 +0200 Subject: [PATCH 18/50] moved calculating LUT's into separate method --- .../AdaptiveHistEqualizationProcessor.cs | 103 ++++++++++-------- .../AdaptiveHistEqualizationSWProcessor.cs | 2 +- 2 files changed, 61 insertions(+), 44 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs index 4e1498ac76..eb6da5d043 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs @@ -12,7 +12,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization { /// - /// Applies an adaptive histogram equalization to the image. + /// Applies an adaptive histogram equalization to the image. The image is split up in tiles. For each tile a cumulative distribution function (cdf) is calculated. + /// To calculate the final equalized pixel value, the cdf value of four adjacent tiles will be interpolated. /// /// The pixel format. internal class AdaptiveHistEqualizationProcessor : HistogramEqualizationProcessor @@ -55,52 +56,17 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source int halfTileWidth = tileWidth / 2; int halfTileHeight = tileHeight / 2; - var cdfData = new CdfData[numTilesX, numTilesY]; using (System.Buffers.IMemoryOwner histogramBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean)) using (System.Buffers.IMemoryOwner cdfBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean)) { Span histogram = histogramBuffer.GetSpan(); Span cdf = cdfBuffer.GetSpan(); - // The image is split up into square tiles of the size of the parameter GridSize. - // 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, histogram, cdf, numTilesX, numTilesY, tileWidth, tileHeight); + int tileX = 0; int tileY = 0; - for (int y = 0; y < source.Height; y += tileHeight) - { - tileX = 0; - for (int x = 0; x < source.Width; x += tileWidth) - { - 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++) - { - for (int dx = x; dx < xlimit; dx++) - { - int luminace = this.GetLuminance(source[dx, dy], this.LuminanceLevels); - histogram[luminace]++; - } - } - - if (this.ClipHistogramEnabled) - { - this.ClipHistogram(histogram, this.ClipLimitPercentage, pixelsInTile); - } - - int cdfMin = this.CalculateCdf(cdf, histogram, histogram.Length - 1); - var currentCdf = new CdfData(cdf.ToArray(), cdfMin); - cdfData[tileX, tileY] = currentCdf; - - tileX++; - } - - tileY++; - } - - tileX = 0; - tileY = 0; for (int y = halfTileHeight; y < source.Height - tileHeight; y += tileHeight) { tileX = 0; @@ -128,7 +94,7 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source float cdfRightTopLuminance = cdfData[xRight, yTop].RemapGreyValue(luminace, pixelsInTile); float cdfLeftBottomLuminance = cdfData[xLeft, yBottom].RemapGreyValue(luminace, pixelsInTile); float cdfRightBottomLuminance = cdfData[xRight, yBottom].RemapGreyValue(luminace, pixelsInTile); - float luminanceEqualized = this.BilinearInterpolation(tilePosX, tilePosY, tilePosX / (float)(tileWidth - 1), ty, cdfLeftTopLuminance, cdfRightTopLuminance, cdfLeftBottomLuminance, cdfRightBottomLuminance); + float luminanceEqualized = this.BilinearInterpolation(tilePosX / (float)(tileWidth - 1), ty, cdfLeftTopLuminance, cdfRightTopLuminance, cdfLeftBottomLuminance, cdfRightBottomLuminance); pixels[(dy * source.Width) + dx].PackFromVector4(new Vector4(luminanceEqualized)); tilePosX++; @@ -145,11 +111,62 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source } } + /// + /// Calculates the lookup tables for each tile of the image. + /// + /// The input image for which the tiles will be calculated. + /// Histogram buffer. + /// Buffer for calculating the cumulative distribution function. + /// 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, Span histogram, Span cdf, int numTilesX, int numTilesY, int tileWidth, int tileHeight) + { + var cdfData = new CdfData[numTilesX, numTilesY]; + int pixelsInTile = tileWidth * tileHeight; + int tileX = 0; + int tileY = 0; + for (int y = 0; y < source.Height; y += tileHeight) + { + tileX = 0; + for (int x = 0; x < source.Width; x += tileWidth) + { + 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++) + { + for (int dx = x; dx < xlimit; dx++) + { + int luminace = this.GetLuminance(source[dx, dy], this.LuminanceLevels); + histogram[luminace]++; + } + } + + if (this.ClipHistogramEnabled) + { + this.ClipHistogram(histogram, this.ClipLimitPercentage, pixelsInTile); + } + + int cdfMin = this.CalculateCdf(cdf, histogram, histogram.Length - 1); + var currentCdf = new CdfData(cdf.ToArray(), cdfMin); + cdfData[tileX, tileY] = currentCdf; + + tileX++; + } + + tileY++; + } + + return cdfData; + } + /// /// Bilinear interpolation between four tiles. /// - /// X position. - /// Y position. /// The interpolation value in x direction in the range of [0, 1]. /// The interpolation value in y direction in the range of [0, 1]. /// Luminance from top left tile. @@ -157,7 +174,7 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source /// Luminance from left bottom tile. /// Luminance from right bottom tile. /// Interpolated Luminance. - private float BilinearInterpolation(int x, int y, float tx, float ty, float lt, float rt, float lb, float rb) + 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); } diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs index 63b8bc29cf..03db1d00db 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs @@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization { /// - /// Applies an adaptive histogram equalization to the image. + /// Applies an adaptive histogram equalization to the image using an sliding window approach. /// /// The pixel format. internal class AdaptiveHistEqualizationSWProcessor : HistogramEqualizationProcessor From 198123152870b24c1a12959117146c7fc00f1f96 Mon Sep 17 00:00:00 2001 From: popow Date: Sat, 22 Sep 2018 18:44:32 +0200 Subject: [PATCH 19/50] number of tiles is now part of the options and will be used with the sliding window approach also, so both methods are comparable --- .../HistogramEqualizationExtension.cs | 4 +- .../AdaptiveHistEqualizationProcessor.cs | 21 ++++----- .../AdaptiveHistEqualizationSWProcessor.cs | 45 ++++++++++--------- .../HistogramEqualizationOptions.cs | 4 +- .../HistogramEqualizationProcessor.cs | 1 + 5 files changed, 37 insertions(+), 38 deletions(-) diff --git a/src/ImageSharp/Processing/HistogramEqualizationExtension.cs b/src/ImageSharp/Processing/HistogramEqualizationExtension.cs index cbc96d5c3f..460681d871 100644 --- a/src/ImageSharp/Processing/HistogramEqualizationExtension.cs +++ b/src/ImageSharp/Processing/HistogramEqualizationExtension.cs @@ -44,11 +44,11 @@ private static HistogramEqualizationProcessor GetProcessor(Histo break; case HistogramEqualizationMethod.Adaptive: - processor = new AdaptiveHistEqualizationProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimitPercentage, options.GridSize); + processor = new AdaptiveHistEqualizationProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimitPercentage, options.Tiles); break; case HistogramEqualizationMethod.AdaptiveSlidingWindow: - processor = new AdaptiveHistEqualizationSWProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimitPercentage, options.GridSize); + processor = new AdaptiveHistEqualizationSWProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimitPercentage, options.Tiles); break; default: diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs index eb6da5d043..012899ec37 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs @@ -26,19 +26,19 @@ internal class AdaptiveHistEqualizationProcessor : HistogramEqualization /// or 65536 for 16-bit grayscale images. /// Indicating whether to clip the histogram bins at a specific value. /// Histogram clip limit in percent of the total pixels in the grid. Histogram bins which exceed this limit, will be capped at this value. - /// The grid size of the adaptive histogram equalization. Minimum value is 4. - public AdaptiveHistEqualizationProcessor(int luminanceLevels, bool clipHistogram, float clipLimitPercentage, int gridSize) + /// The number of tiles the image is split into (horizontal and vertically). + public AdaptiveHistEqualizationProcessor(int luminanceLevels, bool clipHistogram, float clipLimitPercentage, int tiles) : base(luminanceLevels, clipHistogram, clipLimitPercentage) { - Guard.MustBeGreaterThanOrEqualTo(gridSize, 4, nameof(gridSize)); + Guard.MustBeGreaterThanOrEqualTo(tiles, 0, nameof(tiles)); - this.GridSize = gridSize; + this.Tiles = tiles; } /// - /// Gets the size of the grid for the adaptive histogram equalization. + /// Gets the number of tiles the image is split into (horizontal and vertically) for the adaptive histogram equalization. /// - public int GridSize { get; } + private int Tiles { get; } /// protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) @@ -47,11 +47,8 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source int numberOfPixels = source.Width * source.Height; Span pixels = source.GetPixelSpan(); - int numTilesX = 20; - int numTilesY = 20; - - int tileWidth = Convert.ToInt32(Math.Ceiling(source.Width / (double)numTilesX)); - int tileHeight = Convert.ToInt32(Math.Ceiling(source.Height / (double)numTilesY)); + int tileWidth = Convert.ToInt32(Math.Ceiling(source.Width / (double)this.Tiles)); + int tileHeight = Convert.ToInt32(Math.Ceiling(source.Height / (double)this.Tiles)); int pixelsInTile = tileWidth * tileHeight; int halfTileWidth = tileWidth / 2; int halfTileHeight = tileHeight / 2; @@ -63,7 +60,7 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source Span cdf = cdfBuffer.GetSpan(); // The image is split up into tiles. For each tile the cumulative distribution function will be calculated. - CdfData[,] cdfData = this.CalculateLookupTables(source, histogram, cdf, numTilesX, numTilesY, tileWidth, tileHeight); + CdfData[,] cdfData = this.CalculateLookupTables(source, histogram, cdf, this.Tiles, this.Tiles, tileWidth, tileHeight); int tileX = 0; int tileY = 0; diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs index 03db1d00db..b9fe2d0aec 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs @@ -25,19 +25,19 @@ internal class AdaptiveHistEqualizationSWProcessor : HistogramEqualizati /// or 65536 for 16-bit grayscale images. /// Indicating whether to clip the histogram bins at a specific value. /// Histogram clip limit in percent of the total pixels in the grid. Histogram bins which exceed this limit, will be capped at this value. - /// The grid size of the adaptive histogram equalization. Minimum value is 4. - public AdaptiveHistEqualizationSWProcessor(int luminanceLevels, bool clipHistogram, float clipLimitPercentage, int gridSize) + /// The number of tiles the image is split into (horizontal and vertically). + public AdaptiveHistEqualizationSWProcessor(int luminanceLevels, bool clipHistogram, float clipLimitPercentage, int tiles) : base(luminanceLevels, clipHistogram, clipLimitPercentage) { - Guard.MustBeGreaterThanOrEqualTo(gridSize, 4, nameof(gridSize)); + Guard.MustBeGreaterThanOrEqualTo(tiles, 0, nameof(tiles)); - this.GridSize = gridSize; + this.Tiles = tiles; } /// - /// Gets the size of the grid for the adaptive histogram equalization. + /// Gets the number of tiles the image is split into (horizontal and vertically) for the adaptive histogram equalization. /// - public int GridSize { get; } + private int Tiles { get; } /// protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) @@ -46,8 +46,9 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source int numberOfPixels = source.Width * source.Height; Span pixels = source.GetPixelSpan(); - int pixelsInGrid = this.GridSize * this.GridSize; - int halfGridSize = this.GridSize / 2; + int tileWidth = source.Width / this.Tiles; + int pixeInTile = tileWidth * tileWidth; + int halfTileWith = tileWidth / 2; using (Buffer2D targetPixels = configuration.MemoryAllocator.Allocate2D(source.Width, source.Height)) { ParallelFor.WithConfiguration( @@ -66,9 +67,9 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source int maxHistIdx = 0; // Build the histogram of grayscale values for the current grid. - for (int dy = -halfGridSize; dy < halfGridSize; dy++) + for (int dy = -halfTileWith; dy < halfTileWith; dy++) { - Span rowSpan = this.GetPixelRow(source, (int)x - halfGridSize, dy, this.GridSize); + Span rowSpan = this.GetPixelRow(source, (int)x - halfTileWith, dy, tileWidth); int maxIdx = this.AddPixelsToHistogram(rowSpan, histogram, this.LuminanceLevels); if (maxIdx > maxHistIdx) { @@ -82,12 +83,12 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source { // Clipping the histogram, but doing it on a copy to keep the original un-clipped values for the next iteration. histogram.Slice(0, maxHistIdx).CopyTo(histogramCopy); - this.ClipHistogram(histogramCopy, this.ClipLimitPercentage, pixelsInGrid); + this.ClipHistogram(histogramCopy, this.ClipLimitPercentage, pixeInTile); } // Calculate the cumulative distribution function, which will map each input pixel in the current grid to a new value. int cdfMin = this.ClipHistogramEnabled ? this.CalculateCdf(cdf, histogramCopy, maxHistIdx) : this.CalculateCdf(cdf, histogram, maxHistIdx); - float numberOfPixelsMinusCdfMin = pixelsInGrid - cdfMin; + float numberOfPixelsMinusCdfMin = pixeInTile - cdfMin; // Map the current pixel to the new equalized value int luminance = this.GetLuminance(source[x, y], this.LuminanceLevels); @@ -95,11 +96,11 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source targetPixels[x, y].PackFromVector4(new Vector4(luminanceEqualized)); // Remove top most row from the histogram, mirroring rows which exceeds the borders. - Span rowSpan = this.GetPixelRow(source, x - halfGridSize, y - halfGridSize, this.GridSize); + Span rowSpan = this.GetPixelRow(source, x - halfTileWith, y - halfTileWith, tileWidth); maxHistIdx = this.RemovePixelsFromHistogram(rowSpan, histogram, this.LuminanceLevels, maxHistIdx); // Add new bottom row to the histogram, mirroring rows which exceeds the borders. - rowSpan = this.GetPixelRow(source, x - halfGridSize, y + halfGridSize, this.GridSize); + rowSpan = this.GetPixelRow(source, x - halfTileWith, y + halfTileWith, tileWidth); int maxIdx = this.AddPixelsToHistogram(rowSpan, histogram, this.LuminanceLevels); if (maxIdx > maxHistIdx) { @@ -119,9 +120,9 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source /// The source image. /// The x position. /// The y position. - /// The grid size. + /// The width in pixels of a tile. /// A pixel row of the length of the grid size. - private Span GetPixelRow(ImageFrame source, int x, int y, int gridSize) + private Span GetPixelRow(ImageFrame source, int x, int y, int tileWidth) { if (y < 0) { @@ -136,9 +137,9 @@ private Span GetPixelRow(ImageFrame source, int x, int y, int gr // Special cases for the left and the right border where GetPixelRowSpan can not be used if (x < 0) { - var rowPixels = new TPixel[gridSize]; + var rowPixels = new TPixel[tileWidth]; int idx = 0; - for (int dx = x; dx < x + gridSize; dx++) + for (int dx = x; dx < x + tileWidth; dx++) { rowPixels[idx] = source[Math.Abs(dx), y]; idx++; @@ -146,11 +147,11 @@ private Span GetPixelRow(ImageFrame source, int x, int y, int gr return rowPixels; } - else if (x + gridSize > source.Width) + else if (x + tileWidth > source.Width) { - var rowPixels = new TPixel[gridSize]; + var rowPixels = new TPixel[tileWidth]; int idx = 0; - for (int dx = x; dx < x + gridSize; dx++) + for (int dx = x; dx < x + tileWidth; dx++) { if (dx >= source.Width) { @@ -168,7 +169,7 @@ private Span GetPixelRow(ImageFrame source, int x, int y, int gr return rowPixels; } - return source.GetPixelRowSpan(y).Slice(start: x, length: gridSize); + return source.GetPixelRowSpan(y).Slice(start: x, length: tileWidth); } /// diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs index 6708a36998..afc887db55 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs @@ -31,8 +31,8 @@ public class HistogramEqualizationOptions public float ClipLimitPercentage { get; set; } = 0.035f; /// - /// Gets or sets the size of the grid for the adaptive histogram equalization. Defaults to 32. + /// Gets or sets the number of tiles the image is split into (horizontal and vertically) for the adaptive histogram equalization. Defaults to 8. /// - public int GridSize { get; set; } = 32; + public int Tiles { get; set; } = 8; } } diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs index e52aff1e76..ec5e69e3ff 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs @@ -20,6 +20,7 @@ internal abstract class HistogramEqualizationProcessor : ImageProcessor< /// or 65536 for 16-bit grayscale images. /// Indicates, if histogram bins should be clipped. /// Histogram clip limit in percent of the total pixels in the grid. Histogram bins which exceed this limit, will be capped at this value. + /// The number of tiles the image is split into (horizontal and vertically) for the adaptive histogram equalization. Defaults to 8. protected HistogramEqualizationProcessor(int luminanceLevels, bool clipHistogram, float clipLimitPercentage) { Guard.MustBeGreaterThan(luminanceLevels, 0, nameof(luminanceLevels)); From 8792b1da4e901b90c77a34f092072288328facae Mon Sep 17 00:00:00 2001 From: popow Date: Sun, 23 Sep 2018 20:47:44 +0200 Subject: [PATCH 20/50] removed no longer valid xml comment --- .../Processors/Normalization/HistogramEqualizationProcessor.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs index ec5e69e3ff..e52aff1e76 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs @@ -20,7 +20,6 @@ internal abstract class HistogramEqualizationProcessor : ImageProcessor< /// or 65536 for 16-bit grayscale images. /// Indicates, if histogram bins should be clipped. /// Histogram clip limit in percent of the total pixels in the grid. Histogram bins which exceed this limit, will be capped at this value. - /// The number of tiles the image is split into (horizontal and vertically) for the adaptive histogram equalization. Defaults to 8. protected HistogramEqualizationProcessor(int luminanceLevels, bool clipHistogram, float clipLimitPercentage) { Guard.MustBeGreaterThan(luminanceLevels, 0, nameof(luminanceLevels)); From 135f8bebbc0cd830bd6e06dba3835bd7b85c727c Mon Sep 17 00:00:00 2001 From: popow Date: Sun, 2 Dec 2018 20:33:52 +0100 Subject: [PATCH 21/50] attempt fixing the borders --- .../AdaptiveHistEqualizationProcessor.cs | 124 +++++++++++++++++- 1 file changed, 118 insertions(+), 6 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs index 012899ec37..8a660306db 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs @@ -64,22 +64,22 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source int tileX = 0; int tileY = 0; - for (int y = halfTileHeight; y < source.Height - tileHeight; y += tileHeight) + for (int y = halfTileHeight; y < source.Height - halfTileHeight; y += tileHeight) { tileX = 0; - for (int x = halfTileWidth; x < source.Width - tileWidth; x += tileWidth) + for (int x = halfTileWidth; x < source.Width - halfTileWidth; x += tileWidth) { int tilePosX = 0; int tilePosY = 0; - int ylimit = Math.Min(y + tileHeight, source.Height); - int xlimit = Math.Min(x + tileWidth, source.Width); - for (int dy = y; dy < ylimit; dy++) + int yEnd = Math.Min(y + tileHeight, source.Height); + int xEnd = Math.Min(x + tileWidth, source.Width); + for (int dy = y; dy < yEnd; dy++) { tilePosX = 0; float ty = tilePosY / (float)(tileHeight - 1); int yTop = tileY; int yBottom = yTop + 1; - for (int dx = x; dx < xlimit; dx++) + for (int dx = x; dx < xEnd; dx++) { TPixel sourcePixel = source[dx, dy]; int luminace = this.GetLuminance(sourcePixel, this.LuminanceLevels); @@ -105,9 +105,121 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source tileY++; } + + // fix left column + tileX = 0; + tileY = 0; + for (int y = 0; y < source.Height; y += tileHeight) + { + int yLimit = Math.Min(y + tileHeight, source.Height - 1); + int tilePosY = 0; + for (int dy = y; dy < yLimit; dy++) + { + int tilePosX = 0; + for (int dx = 0; dx < halfTileWidth; dx++) + { + float luminanceEqualized = this.InterpolateBetweenTiles(source[dx, dy], cdfData, dx, dy, tilePosX, tilePosY, tileX, tileY, tileWidth, tileHeight, pixelsInTile); + pixels[(dy * source.Width) + dx].PackFromVector4(new Vector4(luminanceEqualized)); + tilePosX++; + } + + tilePosY++; + } + + tileY++; + } + + // fix right column + tileX = this.Tiles - 2; + tileY = 0; + for (int y = 0; y < source.Height; y += tileHeight) + { + int yLimit = Math.Min(y + tileHeight, source.Height - 1); + int tilePosY = 0; + for (int dy = y; dy < yLimit; dy++) + { + int tilePosX = halfTileWidth; + for (int dx = source.Width - halfTileWidth; dx < source.Width; dx++) + { + float luminanceEqualized = this.InterpolateBetweenTiles(source[dx, dy], cdfData, dx, dy, tilePosX, tilePosY, tileX, tileY, tileWidth, tileHeight, pixelsInTile); + pixels[(dy * source.Width) + dx].PackFromVector4(new Vector4(luminanceEqualized)); + tilePosX++; + } + + tilePosY++; + } + + tileY++; + } + + // fix top row + tileX = 0; + tileY = 0; + for (int x = 0; x < source.Width; x += tileWidth) + { + int tilePosY = 0; + for (int dy = 0; dy < halfTileHeight; dy++) + { + int tilePosX = 0; + int xLimit = Math.Min(x + tileWidth, source.Width - 1); + for (int dx = x; dx < xLimit; dx++) + { + float luminanceEqualized = this.InterpolateBetweenTiles(source[dx, dy], cdfData, dx, dy, tilePosX, tilePosY, tileX, tileY, tileWidth, tileHeight, pixelsInTile); + pixels[(dy * source.Width) + dx].PackFromVector4(new Vector4(luminanceEqualized)); + tilePosX++; + } + + tilePosY++; + } + + tileX++; + } + + // fix bottom row + tileX = 0; + tileY = 0; + for (int x = 0; x < source.Width; x += tileWidth) + { + int tilePosY = 0; + for (int dy = source.Height - halfTileHeight; dy < source.Height; dy++) + { + int tilePosX = 0; + int xLimit = Math.Min(x + tileWidth, source.Width - 1); + for (int dx = x; dx < xLimit; dx++) + { + float luminanceEqualized = this.InterpolateBetweenTiles(source[dx, dy], cdfData, dx, dy, tilePosX, tilePosY, tileX, tileY, tileWidth, tileHeight, pixelsInTile); + pixels[(dy * source.Width) + dx].PackFromVector4(new Vector4(luminanceEqualized)); + tilePosX++; + } + + tilePosY++; + } + + tileX++; + } } } + private float InterpolateBetweenTiles(TPixel sourcePixel, CdfData[,] cdfData, int dx, int dy, int tilePosX, int tilePosY, int tileX, int tileY, int tileWidth, int tileHeight, int pixelsInTile) + { + int luminace = this.GetLuminance(sourcePixel, this.LuminanceLevels); + float tx = tilePosX / (float)(tileWidth - 1); + float ty = tilePosY / (float)(tileHeight - 1); + + int yTop = tileY; + int yBottom = Math.Min(this.Tiles - 1, yTop + 1); + int xLeft = tileX; + int xRight = Math.Min(this.Tiles - 1, xLeft + 1); + + float cdfLeftTopLuminance = cdfData[xLeft, yTop].RemapGreyValue(luminace, pixelsInTile); + float cdfRightTopLuminance = cdfData[xRight, yTop].RemapGreyValue(luminace, pixelsInTile); + float cdfLeftBottomLuminance = cdfData[xLeft, yBottom].RemapGreyValue(luminace, pixelsInTile); + float cdfRightBottomLuminance = cdfData[xRight, yBottom].RemapGreyValue(luminace, pixelsInTile); + float luminanceEqualized = this.BilinearInterpolation(tx, ty, cdfLeftTopLuminance, cdfRightTopLuminance, cdfLeftBottomLuminance, cdfRightBottomLuminance); + + return luminanceEqualized; + } + /// /// Calculates the lookup tables for each tile of the image. /// From 895cecfe0d659bdd48eb90c98f8da361e04fe1e1 Mon Sep 17 00:00:00 2001 From: popow Date: Sun, 2 Dec 2018 20:53:16 +0100 Subject: [PATCH 22/50] refactoring to improve readability --- .../HistogramEqualizationExtension.cs | 2 +- .../AdaptiveHistEqualizationProcessor.cs | 122 +++++++++--------- .../HistogramEqualizationMethod.cs | 4 +- 3 files changed, 65 insertions(+), 63 deletions(-) diff --git a/src/ImageSharp/Processing/HistogramEqualizationExtension.cs b/src/ImageSharp/Processing/HistogramEqualizationExtension.cs index 460681d871..ceae4a1ed4 100644 --- a/src/ImageSharp/Processing/HistogramEqualizationExtension.cs +++ b/src/ImageSharp/Processing/HistogramEqualizationExtension.cs @@ -43,7 +43,7 @@ private static HistogramEqualizationProcessor GetProcessor(Histo processor = new GlobalHistogramEqualizationProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimitPercentage); break; - case HistogramEqualizationMethod.Adaptive: + case HistogramEqualizationMethod.AdaptiveTileInterpolation: processor = new AdaptiveHistEqualizationProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimitPercentage, options.Tiles); break; diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs index 8a660306db..5b659fa322 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs @@ -62,153 +62,155 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source // The image is split up into tiles. For each tile the cumulative distribution function will be calculated. CdfData[,] cdfData = this.CalculateLookupTables(source, histogram, cdf, this.Tiles, this.Tiles, tileWidth, tileHeight); + int cdfX = 0; + int cdfY = 0; int tileX = 0; int tileY = 0; for (int y = halfTileHeight; y < source.Height - halfTileHeight; y += tileHeight) { - tileX = 0; + cdfX = 0; for (int x = halfTileWidth; x < source.Width - halfTileWidth; x += tileWidth) { - int tilePosX = 0; - int tilePosY = 0; + 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++) { - tilePosX = 0; - float ty = tilePosY / (float)(tileHeight - 1); - int yTop = tileY; - int yBottom = yTop + 1; + tileX = 0; for (int dx = x; dx < xEnd; dx++) { - TPixel sourcePixel = source[dx, dy]; - int luminace = this.GetLuminance(sourcePixel, this.LuminanceLevels); - - int xLeft = tileX; - int xRight = tileX + 1; - - float cdfLeftTopLuminance = cdfData[xLeft, yTop].RemapGreyValue(luminace, pixelsInTile); - float cdfRightTopLuminance = cdfData[xRight, yTop].RemapGreyValue(luminace, pixelsInTile); - float cdfLeftBottomLuminance = cdfData[xLeft, yBottom].RemapGreyValue(luminace, pixelsInTile); - float cdfRightBottomLuminance = cdfData[xRight, yBottom].RemapGreyValue(luminace, pixelsInTile); - float luminanceEqualized = this.BilinearInterpolation(tilePosX / (float)(tileWidth - 1), ty, cdfLeftTopLuminance, cdfRightTopLuminance, cdfLeftBottomLuminance, cdfRightBottomLuminance); - + float luminanceEqualized = this.InterpolateBetweenTiles(source[dx, dy], cdfData, dx, dy, tileX, tileY, cdfX, cdfY, tileWidth, tileHeight, pixelsInTile); pixels[(dy * source.Width) + dx].PackFromVector4(new Vector4(luminanceEqualized)); - tilePosX++; + tileX++; } - tilePosY++; + tileY++; } - tileX++; + cdfX++; } - tileY++; + cdfY++; } // fix left column - tileX = 0; - tileY = 0; + cdfX = 0; + cdfY = 0; for (int y = 0; y < source.Height; y += tileHeight) { int yLimit = Math.Min(y + tileHeight, source.Height - 1); - int tilePosY = 0; + tileY = 0; for (int dy = y; dy < yLimit; dy++) { - int tilePosX = 0; + tileX = 0; for (int dx = 0; dx < halfTileWidth; dx++) { - float luminanceEqualized = this.InterpolateBetweenTiles(source[dx, dy], cdfData, dx, dy, tilePosX, tilePosY, tileX, tileY, tileWidth, tileHeight, pixelsInTile); + float luminanceEqualized = this.InterpolateBetweenTiles(source[dx, dy], cdfData, dx, dy, tileX, tileY, cdfX, cdfY, tileWidth, tileHeight, pixelsInTile); pixels[(dy * source.Width) + dx].PackFromVector4(new Vector4(luminanceEqualized)); - tilePosX++; + tileX++; } - tilePosY++; + tileY++; } - tileY++; + cdfY++; } // fix right column - tileX = this.Tiles - 2; - tileY = 0; + cdfX = this.Tiles - 2; + cdfY = 0; for (int y = 0; y < source.Height; y += tileHeight) { int yLimit = Math.Min(y + tileHeight, source.Height - 1); - int tilePosY = 0; + tileY = 0; for (int dy = y; dy < yLimit; dy++) { - int tilePosX = halfTileWidth; + tileX = halfTileWidth; for (int dx = source.Width - halfTileWidth; dx < source.Width; dx++) { - float luminanceEqualized = this.InterpolateBetweenTiles(source[dx, dy], cdfData, dx, dy, tilePosX, tilePosY, tileX, tileY, tileWidth, tileHeight, pixelsInTile); + float luminanceEqualized = this.InterpolateBetweenTiles(source[dx, dy], cdfData, dx, dy, tileX, tileY, cdfX, cdfY, tileWidth, tileHeight, pixelsInTile); pixels[(dy * source.Width) + dx].PackFromVector4(new Vector4(luminanceEqualized)); - tilePosX++; + tileX++; } - tilePosY++; + tileY++; } - tileY++; + cdfY++; } // fix top row - tileX = 0; - tileY = 0; + cdfX = 0; + cdfY = 0; for (int x = 0; x < source.Width; x += tileWidth) { - int tilePosY = 0; + tileY = 0; for (int dy = 0; dy < halfTileHeight; dy++) { - int tilePosX = 0; + tileX = 0; int xLimit = Math.Min(x + tileWidth, source.Width - 1); for (int dx = x; dx < xLimit; dx++) { - float luminanceEqualized = this.InterpolateBetweenTiles(source[dx, dy], cdfData, dx, dy, tilePosX, tilePosY, tileX, tileY, tileWidth, tileHeight, pixelsInTile); + float luminanceEqualized = this.InterpolateBetweenTiles(source[dx, dy], cdfData, dx, dy, tileX, tileY, cdfX, cdfY, tileWidth, tileHeight, pixelsInTile); pixels[(dy * source.Width) + dx].PackFromVector4(new Vector4(luminanceEqualized)); - tilePosX++; + tileX++; } - tilePosY++; + tileY++; } - tileX++; + cdfX++; } // fix bottom row - tileX = 0; - tileY = 0; + cdfX = 0; + cdfY = 0; for (int x = 0; x < source.Width; x += tileWidth) { - int tilePosY = 0; + tileY = 0; for (int dy = source.Height - halfTileHeight; dy < source.Height; dy++) { - int tilePosX = 0; + tileX = 0; int xLimit = Math.Min(x + tileWidth, source.Width - 1); for (int dx = x; dx < xLimit; dx++) { - float luminanceEqualized = this.InterpolateBetweenTiles(source[dx, dy], cdfData, dx, dy, tilePosX, tilePosY, tileX, tileY, tileWidth, tileHeight, pixelsInTile); + float luminanceEqualized = this.InterpolateBetweenTiles(source[dx, dy], cdfData, dx, dy, tileX, tileY, cdfX, cdfY, tileWidth, tileHeight, pixelsInTile); pixels[(dy * source.Width) + dx].PackFromVector4(new Vector4(luminanceEqualized)); - tilePosX++; + tileX++; } - tilePosY++; + tileY++; } - tileX++; + cdfX++; } } } - private float InterpolateBetweenTiles(TPixel sourcePixel, CdfData[,] cdfData, int dx, int dy, int tilePosX, int tilePosY, int tileX, int tileY, int tileWidth, int tileHeight, int pixelsInTile) + /// + /// Interpolates between four adjacent tiles. + /// + /// The pixel to remap the grey value from. + /// The pre-computed lookup tables to remap the grey values for each tiles. + /// X index in the image. + /// Y index in the image. + /// 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. + /// A re-mapped grey value. + private float InterpolateBetweenTiles(TPixel sourcePixel, CdfData[,] cdfData, int dx, int dy, int tileX, int tileY, int cdfX, int cdfY, int tileWidth, int tileHeight, int pixelsInTile) { int luminace = this.GetLuminance(sourcePixel, this.LuminanceLevels); - float tx = tilePosX / (float)(tileWidth - 1); - float ty = tilePosY / (float)(tileHeight - 1); + float tx = tileX / (float)(tileWidth - 1); + float ty = tileY / (float)(tileHeight - 1); - int yTop = tileY; + int yTop = cdfY; int yBottom = Math.Min(this.Tiles - 1, yTop + 1); - int xLeft = tileX; + int xLeft = cdfX; int xRight = Math.Min(this.Tiles - 1, xLeft + 1); float cdfLeftTopLuminance = cdfData[xLeft, yTop].RemapGreyValue(luminace, pixelsInTile); diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs index 63546c744d..5226e3f88e 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs @@ -14,9 +14,9 @@ public enum HistogramEqualizationMethod : int Global, /// - /// Adaptive histogram equalization. + /// Adaptive histogram equalization using a tile interpolation approach. /// - Adaptive, + AdaptiveTileInterpolation, /// /// Adaptive sliding window histogram equalization. From 3dfe5a5c1d0770eff6f74aa4917fc00dee300f82 Mon Sep 17 00:00:00 2001 From: popow Date: Tue, 4 Dec 2018 20:01:49 +0100 Subject: [PATCH 23/50] linear interpolation in the border tiles --- .../AdaptiveHistEqualizationProcessor.cs | 53 +++++++++++++------ 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs index 5b659fa322..b63026df83 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs @@ -79,7 +79,7 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source tileX = 0; for (int dx = x; dx < xEnd; dx++) { - float luminanceEqualized = this.InterpolateBetweenTiles(source[dx, dy], cdfData, dx, dy, tileX, tileY, cdfX, cdfY, tileWidth, tileHeight, pixelsInTile); + float luminanceEqualized = this.InterpolateBetweenFourTiles(source[dx, dy], cdfData, tileX, tileY, cdfX, cdfY, tileWidth, tileHeight, pixelsInTile); pixels[(dy * source.Width) + dx].PackFromVector4(new Vector4(luminanceEqualized)); tileX++; } @@ -96,7 +96,7 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source // fix left column cdfX = 0; cdfY = 0; - for (int y = 0; y < source.Height; y += tileHeight) + for (int y = halfTileWidth; y < source.Height - halfTileWidth; y += tileHeight) { int yLimit = Math.Min(y + tileHeight, source.Height - 1); tileY = 0; @@ -105,7 +105,7 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source tileX = 0; for (int dx = 0; dx < halfTileWidth; dx++) { - float luminanceEqualized = this.InterpolateBetweenTiles(source[dx, dy], cdfData, dx, dy, tileX, tileY, cdfX, cdfY, tileWidth, tileHeight, pixelsInTile); + float luminanceEqualized = this.InterpolateBetweenTwoTiles(source[dx, dy], cdfData[cdfX, cdfY], cdfData[cdfX, cdfY + 1], tileY, tileHeight, pixelsInTile); pixels[(dy * source.Width) + dx].PackFromVector4(new Vector4(luminanceEqualized)); tileX++; } @@ -117,9 +117,9 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source } // fix right column - cdfX = this.Tiles - 2; + cdfX = this.Tiles - 1; cdfY = 0; - for (int y = 0; y < source.Height; y += tileHeight) + for (int y = halfTileWidth; y < source.Height - halfTileWidth; y += tileHeight) { int yLimit = Math.Min(y + tileHeight, source.Height - 1); tileY = 0; @@ -128,7 +128,7 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source tileX = halfTileWidth; for (int dx = source.Width - halfTileWidth; dx < source.Width; dx++) { - float luminanceEqualized = this.InterpolateBetweenTiles(source[dx, dy], cdfData, dx, dy, tileX, tileY, cdfX, cdfY, tileWidth, tileHeight, pixelsInTile); + float luminanceEqualized = this.InterpolateBetweenTwoTiles(source[dx, dy], cdfData[cdfX, cdfY], cdfData[cdfX, cdfY + 1], tileY, tileHeight, pixelsInTile); pixels[(dy * source.Width) + dx].PackFromVector4(new Vector4(luminanceEqualized)); tileX++; } @@ -142,7 +142,7 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source // fix top row cdfX = 0; cdfY = 0; - for (int x = 0; x < source.Width; x += tileWidth) + for (int x = halfTileWidth; x < source.Width - halfTileWidth; x += tileWidth) { tileY = 0; for (int dy = 0; dy < halfTileHeight; dy++) @@ -151,7 +151,7 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source int xLimit = Math.Min(x + tileWidth, source.Width - 1); for (int dx = x; dx < xLimit; dx++) { - float luminanceEqualized = this.InterpolateBetweenTiles(source[dx, dy], cdfData, dx, dy, tileX, tileY, cdfX, cdfY, tileWidth, tileHeight, pixelsInTile); + float luminanceEqualized = this.InterpolateBetweenTwoTiles(source[dx, dy], cdfData[cdfX, cdfY], cdfData[cdfX + 1, cdfY], tileX, tileWidth, pixelsInTile); pixels[(dy * source.Width) + dx].PackFromVector4(new Vector4(luminanceEqualized)); tileX++; } @@ -164,8 +164,8 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source // fix bottom row cdfX = 0; - cdfY = 0; - for (int x = 0; x < source.Width; x += tileWidth) + cdfY = this.Tiles - 1; + for (int x = halfTileWidth; x < source.Width - halfTileWidth; x += tileWidth) { tileY = 0; for (int dy = source.Height - halfTileHeight; dy < source.Height; dy++) @@ -174,7 +174,7 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source int xLimit = Math.Min(x + tileWidth, source.Width - 1); for (int dx = x; dx < xLimit; dx++) { - float luminanceEqualized = this.InterpolateBetweenTiles(source[dx, dy], cdfData, dx, dy, tileX, tileY, cdfX, cdfY, tileWidth, tileHeight, pixelsInTile); + float luminanceEqualized = this.InterpolateBetweenTwoTiles(source[dx, dy], cdfData[cdfX, cdfY], cdfData[cdfX + 1, cdfY], tileX, tileWidth, pixelsInTile); pixels[(dy * source.Width) + dx].PackFromVector4(new Vector4(luminanceEqualized)); tileX++; } @@ -188,12 +188,10 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source } /// - /// Interpolates between four adjacent tiles. + /// Bilinear interpolation between four adjacent tiles. /// /// The pixel to remap the grey value from. /// The pre-computed lookup tables to remap the grey values for each tiles. - /// X index in the image. - /// Y index in the image. /// X position inside the tile. /// Y position inside the tile. /// X index of the top left lookup table to use. @@ -202,7 +200,7 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source /// Height of one tile in pixels. /// Amount of pixels in one tile. /// A re-mapped grey value. - private float InterpolateBetweenTiles(TPixel sourcePixel, CdfData[,] cdfData, int dx, int dy, int tileX, int tileY, int cdfX, int cdfY, int tileWidth, int tileHeight, int pixelsInTile) + private float InterpolateBetweenFourTiles(TPixel sourcePixel, CdfData[,] cdfData, int tileX, int tileY, int cdfX, int cdfY, int tileWidth, int tileHeight, int pixelsInTile) { int luminace = this.GetLuminance(sourcePixel, this.LuminanceLevels); float tx = tileX / (float)(tileWidth - 1); @@ -222,6 +220,28 @@ private float InterpolateBetweenTiles(TPixel sourcePixel, CdfData[,] cdfData, in return luminanceEqualized; } + /// + /// Linear interpolation between two tiles. + /// + /// The pixel to remap the grey value from. + /// First lookup table. + /// Second lookup table. + /// Position inside the tile. + /// Width of the tile. + /// Pixels in one tile. + /// A re-mapped grey value. + private float InterpolateBetweenTwoTiles(TPixel sourcePixel, CdfData cdfData1, CdfData cdfData2, int tilePos, int tileWidth, int pixelsInTile) + { + int luminace = this.GetLuminance(sourcePixel, this.LuminanceLevels); + float tx = tilePos / (float)(tileWidth - 1); + + float cdfLuminance1 = cdfData1.RemapGreyValue(luminace, pixelsInTile); + float cdfLuminance2 = cdfData2.RemapGreyValue(luminace, pixelsInTile); + float luminanceEqualized = this.LinearInterpolation(cdfLuminance1, cdfLuminance2, tx); + + return luminanceEqualized; + } + /// /// Calculates the lookup tables for each tile of the image. /// @@ -302,6 +322,9 @@ private float LinearInterpolation(float left, float right, float t) return left + ((right - left) * t); } + /// + /// Lookup table for remapping the grey values of one tile. + /// private class CdfData { /// From ad4c1da6a079cc5cb23595eb6001147e5e95a546 Mon Sep 17 00:00:00 2001 From: popow Date: Wed, 5 Dec 2018 20:17:44 +0100 Subject: [PATCH 24/50] refactored processing the borders into separate methods --- .../AdaptiveHistEqualizationProcessor.cs | 146 +++++++++--------- 1 file changed, 72 insertions(+), 74 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs index b63026df83..d055e8b862 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs @@ -94,96 +94,94 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source } // fix left column - cdfX = 0; - cdfY = 0; - for (int y = halfTileWidth; y < source.Height - halfTileWidth; y += tileHeight) - { - int yLimit = Math.Min(y + tileHeight, source.Height - 1); - tileY = 0; - for (int dy = y; dy < yLimit; dy++) - { - tileX = 0; - for (int dx = 0; dx < halfTileWidth; dx++) - { - float luminanceEqualized = this.InterpolateBetweenTwoTiles(source[dx, dy], cdfData[cdfX, cdfY], cdfData[cdfX, cdfY + 1], tileY, tileHeight, pixelsInTile); - pixels[(dy * source.Width) + dx].PackFromVector4(new Vector4(luminanceEqualized)); - tileX++; - } - - tileY++; - } - - cdfY++; - } + this.ProcessBorderColumn(source, pixels, cdfData, 0, tileWidth, tileHeight, xStart: 0, xEnd: halfTileWidth); // fix right column - cdfX = this.Tiles - 1; - cdfY = 0; - for (int y = halfTileWidth; y < source.Height - halfTileWidth; y += tileHeight) - { - int yLimit = Math.Min(y + tileHeight, source.Height - 1); - tileY = 0; - for (int dy = y; dy < yLimit; dy++) - { - tileX = halfTileWidth; - for (int dx = source.Width - halfTileWidth; dx < source.Width; dx++) - { - float luminanceEqualized = this.InterpolateBetweenTwoTiles(source[dx, dy], cdfData[cdfX, cdfY], cdfData[cdfX, cdfY + 1], tileY, tileHeight, pixelsInTile); - pixels[(dy * source.Width) + dx].PackFromVector4(new Vector4(luminanceEqualized)); - tileX++; - } + this.ProcessBorderColumn(source, pixels, cdfData, this.Tiles - 1, tileWidth, tileHeight, xStart: source.Width - halfTileWidth, xEnd: source.Width); - tileY++; - } + // fix top row + this.ProcessBorderRow(source, pixels, cdfData, 0, tileWidth, tileHeight, yStart: 0, yEnd: halfTileHeight); - cdfY++; - } + // fix bottom row + this.ProcessBorderRow(source, pixels, cdfData, this.Tiles - 1, tileWidth, tileHeight, yStart: source.Height - halfTileHeight, yEnd: source.Height); + } + } - // fix top row - cdfX = 0; - cdfY = 0; - for (int x = halfTileWidth; x < source.Width - halfTileWidth; x += tileWidth) + /// + /// Processes a border column of the image which is half the size of the tile width. + /// + /// The source image. + /// The output pixels. + /// The pre-computed lookup tables to remap the grey values for each tiles. + /// The X index of the lookup table to use. + /// The width of a tile. + /// 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) + { + int halfTileWidth = tileWidth / 2; + int halfTileHeight = tileHeight / 2; + int pixelsInTile = tileWidth * tileHeight; + + int cdfY = 0; + for (int y = halfTileWidth; y < source.Height - halfTileWidth; y += tileHeight) + { + int yLimit = Math.Min(y + tileHeight, source.Height - 1); + int tileY = 0; + for (int dy = y; dy < yLimit; dy++) { - tileY = 0; - for (int dy = 0; dy < halfTileHeight; dy++) + int tileX = halfTileWidth; + for (int dx = xStart; dx < xEnd; dx++) { - tileX = 0; - 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); - pixels[(dy * source.Width) + dx].PackFromVector4(new Vector4(luminanceEqualized)); - tileX++; - } - - tileY++; + float luminanceEqualized = this.InterpolateBetweenTwoTiles(source[dx, dy], cdfData[cdfX, cdfY], cdfData[cdfX, cdfY + 1], tileY, tileHeight, pixelsInTile); + pixels[(dy * source.Width) + dx].PackFromVector4(new Vector4(luminanceEqualized)); + tileX++; } - cdfX++; + tileY++; } - // fix bottom row - cdfX = 0; - cdfY = this.Tiles - 1; - for (int x = halfTileWidth; x < source.Width - halfTileWidth; x += tileWidth) + cdfY++; + } + } + + /// + /// Processes a border row of the image which is half of the size of the tile height. + /// + /// The source image. + /// The output pixels. + /// The pre-computed lookup tables to remap the grey values for each tiles. + /// The Y index of the lookup table to use. + /// The width of a tile. + /// 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) + { + 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) + { + int tileY = 0; + for (int dy = yStart; dy < yEnd; dy++) { - tileY = 0; - for (int dy = source.Height - halfTileHeight; dy < source.Height; dy++) + int tileX = 0; + int xLimit = Math.Min(x + tileWidth, source.Width - 1); + for (int dx = x; dx < xLimit; dx++) { - tileX = 0; - 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); - pixels[(dy * source.Width) + dx].PackFromVector4(new Vector4(luminanceEqualized)); - tileX++; - } - - tileY++; + float luminanceEqualized = this.InterpolateBetweenTwoTiles(source[dx, dy], cdfData[cdfX, cdfY], cdfData[cdfX + 1, cdfY], tileX, tileWidth, pixelsInTile); + pixels[(dy * source.Width) + dx].PackFromVector4(new Vector4(luminanceEqualized)); + tileX++; } - cdfX++; + tileY++; } + + cdfX++; } } From 80660c935b9d4d64393b3697f557d9f3387761af Mon Sep 17 00:00:00 2001 From: popow Date: Wed, 5 Dec 2018 20:42:26 +0100 Subject: [PATCH 25/50] fixing corner tiles --- .../AdaptiveHistEqualizationProcessor.cs | 51 ++++++++++++++++--- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs index d055e8b862..767feb0600 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs @@ -104,6 +104,41 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source // fix bottom row this.ProcessBorderRow(source, pixels, cdfData, this.Tiles - 1, tileWidth, tileHeight, yStart: source.Height - halfTileHeight, yEnd: source.Height); + + // left top corner + this.ProcessCornerTile(source, pixels, cdfData[0, 0], xStart: 0, xEnd: halfTileWidth, yStart: 0, yEnd: halfTileHeight, pixelsInTile: pixelsInTile); + + // left bottom corner + this.ProcessCornerTile(source, pixels, cdfData[0, this.Tiles - 1], xStart: 0, xEnd: halfTileWidth, yStart: source.Height - halfTileHeight, yEnd: source.Height, pixelsInTile: pixelsInTile); + + // right top corner + this.ProcessCornerTile(source, pixels, cdfData[this.Tiles - 1, 0], xStart: source.Width - halfTileWidth, xEnd: source.Width, yStart: 0, yEnd: halfTileHeight, pixelsInTile: pixelsInTile); + + // right bottom corner + this.ProcessCornerTile(source, pixels, cdfData[this.Tiles - 1, this.Tiles - 1], xStart: source.Width - halfTileWidth, xEnd: source.Width, yStart: source.Height - halfTileHeight, yEnd: source.Height, pixelsInTile: pixelsInTile); + } + } + + /// + /// Processes the part of a corner tile which was previously left out. It consists of 1 / 4 of a tile and does not need interpolation. + /// + /// The source image. + /// The output pixels. + /// The lookup table to remap the grey values. + /// 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) + { + 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); + pixels[(dy * source.Width) + dx].PackFromVector4(new Vector4(luminanceEqualized)); + } } } @@ -200,7 +235,7 @@ private void ProcessBorderRow(ImageFrame source, Span pixels, Cd /// 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) { - int luminace = this.GetLuminance(sourcePixel, this.LuminanceLevels); + int luminance = this.GetLuminance(sourcePixel, this.LuminanceLevels); float tx = tileX / (float)(tileWidth - 1); float ty = tileY / (float)(tileHeight - 1); @@ -209,10 +244,10 @@ private float InterpolateBetweenFourTiles(TPixel sourcePixel, CdfData[,] cdfData int xLeft = cdfX; int xRight = Math.Min(this.Tiles - 1, xLeft + 1); - float cdfLeftTopLuminance = cdfData[xLeft, yTop].RemapGreyValue(luminace, pixelsInTile); - float cdfRightTopLuminance = cdfData[xRight, yTop].RemapGreyValue(luminace, pixelsInTile); - float cdfLeftBottomLuminance = cdfData[xLeft, yBottom].RemapGreyValue(luminace, pixelsInTile); - float cdfRightBottomLuminance = cdfData[xRight, yBottom].RemapGreyValue(luminace, pixelsInTile); + 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; @@ -230,11 +265,11 @@ private float InterpolateBetweenFourTiles(TPixel sourcePixel, CdfData[,] cdfData /// A re-mapped grey value. private float InterpolateBetweenTwoTiles(TPixel sourcePixel, CdfData cdfData1, CdfData cdfData2, int tilePos, int tileWidth, int pixelsInTile) { - int luminace = this.GetLuminance(sourcePixel, this.LuminanceLevels); + int luminance = this.GetLuminance(sourcePixel, this.LuminanceLevels); float tx = tilePos / (float)(tileWidth - 1); - float cdfLuminance1 = cdfData1.RemapGreyValue(luminace, pixelsInTile); - float cdfLuminance2 = cdfData2.RemapGreyValue(luminace, pixelsInTile); + float cdfLuminance1 = cdfData1.RemapGreyValue(luminance, pixelsInTile); + float cdfLuminance2 = cdfData2.RemapGreyValue(luminance, pixelsInTile); float luminanceEqualized = this.LinearInterpolation(cdfLuminance1, cdfLuminance2, tx); return luminanceEqualized; From 0329fb13c982a8079b65956c5b50b6027535336b Mon Sep 17 00:00:00 2001 From: popow Date: Thu, 6 Dec 2018 20:01:25 +0100 Subject: [PATCH 26/50] fixed build errors --- .../AdaptiveHistEqualizationProcessor.cs | 8 ++++---- .../AdaptiveHistEqualizationSWProcessor.cs | 13 ++++++++----- .../GlobalHistogramEqualizationProcessor.cs | 2 +- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs index 767feb0600..ab73845f57 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs @@ -80,7 +80,7 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source for (int dx = x; dx < xEnd; dx++) { float luminanceEqualized = this.InterpolateBetweenFourTiles(source[dx, dy], cdfData, tileX, tileY, cdfX, cdfY, tileWidth, tileHeight, pixelsInTile); - pixels[(dy * source.Width) + dx].PackFromVector4(new Vector4(luminanceEqualized)); + pixels[(dy * source.Width) + dx].FromVector4(new Vector4(luminanceEqualized)); tileX++; } @@ -137,7 +137,7 @@ private void ProcessCornerTile(ImageFrame source, Span pixels, C for (int dx = xStart; dx < xEnd; dx++) { float luminanceEqualized = cdfData.RemapGreyValue(this.GetLuminance(source[dx, dy], this.LuminanceLevels), pixelsInTile); - pixels[(dy * source.Width) + dx].PackFromVector4(new Vector4(luminanceEqualized)); + pixels[(dy * source.Width) + dx].FromVector4(new Vector4(luminanceEqualized)); } } } @@ -170,7 +170,7 @@ private void ProcessBorderColumn(ImageFrame source, Span pixels, for (int dx = xStart; dx < xEnd; dx++) { float luminanceEqualized = this.InterpolateBetweenTwoTiles(source[dx, dy], cdfData[cdfX, cdfY], cdfData[cdfX, cdfY + 1], tileY, tileHeight, pixelsInTile); - pixels[(dy * source.Width) + dx].PackFromVector4(new Vector4(luminanceEqualized)); + pixels[(dy * source.Width) + dx].FromVector4(new Vector4(luminanceEqualized)); tileX++; } @@ -209,7 +209,7 @@ private void ProcessBorderRow(ImageFrame source, Span pixels, Cd for (int dx = x; dx < xLimit; dx++) { float luminanceEqualized = this.InterpolateBetweenTwoTiles(source[dx, dy], cdfData[cdfX, cdfY], cdfData[cdfX + 1, cdfY], tileX, tileWidth, pixelsInTile); - pixels[(dy * source.Width) + dx].PackFromVector4(new Vector4(luminanceEqualized)); + pixels[(dy * source.Width) + dx].FromVector4(new Vector4(luminanceEqualized)); tileX++; } diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs index b9fe2d0aec..dbdeb8c91b 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs @@ -3,8 +3,10 @@ using System; using System.Numerics; +using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.ParallelUtils; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Memory; using SixLabors.Primitives; @@ -46,15 +48,16 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source int numberOfPixels = source.Width * source.Height; Span pixels = source.GetPixelSpan(); + var parallelOptions = new ParallelOptions() { MaxDegreeOfParallelism = configuration.MaxDegreeOfParallelism }; int tileWidth = source.Width / this.Tiles; int pixeInTile = tileWidth * tileWidth; int halfTileWith = tileWidth / 2; using (Buffer2D targetPixels = configuration.MemoryAllocator.Allocate2D(source.Width, source.Height)) { - ParallelFor.WithConfiguration( + Parallel.For( 0, source.Width, - configuration, + parallelOptions, x => { using (System.Buffers.IMemoryOwner histogramBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean)) @@ -93,7 +96,7 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source // Map the current pixel to the new equalized value int luminance = this.GetLuminance(source[x, y], this.LuminanceLevels); float luminanceEqualized = cdf[luminance] / numberOfPixelsMinusCdfMin; - targetPixels[x, y].PackFromVector4(new Vector4(luminanceEqualized)); + targetPixels[x, y].FromVector4(new Vector4(luminanceEqualized)); // Remove top most row from the histogram, mirroring rows which exceeds the borders. Span rowSpan = this.GetPixelRow(source, x - halfTileWith, y - halfTileWith, tileWidth); @@ -115,13 +118,13 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source } /// - /// Get the a pixel row at a given position with a length of the grid size. Mirrors pixels which exceeds the edges. + /// Get the a pixel row at a given position with a length of the tile width. Mirrors pixels which exceeds the edges. /// /// The source image. /// The x position. /// The y position. /// The width in pixels of a tile. - /// A pixel row of the length of the grid size. + /// A pixel row of the length of the tile width. private Span GetPixelRow(ImageFrame source, int x, int y, int tileWidth) { if (y < 0) diff --git a/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs index 1d6042d8da..a4de02cc7e 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs @@ -67,7 +67,7 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source int luminance = this.GetLuminance(sourcePixel, this.LuminanceLevels); float luminanceEqualized = cdf[luminance] / numberOfPixelsMinusCdfMin; - pixels[i].PackFromVector4(new Vector4(luminanceEqualized)); + pixels[i].FromVector4(new Vector4(luminanceEqualized)); } } } From b6eda7f7f3cb0841554375224f444e21968bc091 Mon Sep 17 00:00:00 2001 From: popow Date: Thu, 6 Dec 2018 21:14:38 +0100 Subject: [PATCH 27/50] fixing mistake during merge from upstream: setting test images to "update Resize reference output because of improved ResizeKernelMap calculations" --- tests/Images/External | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Images/External b/tests/Images/External index 6abc3bc0ac..5b18d8c95a 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit 6abc3bc0ac253a24c9e88e68d7b7d853350a85da +Subproject commit 5b18d8c95acffb773012881870ba6f521ba13128 From e5121dcb63a4283b9a729cd1693d5acce68d717e Mon Sep 17 00:00:00 2001 From: popow Date: Sat, 8 Dec 2018 12:52:14 +0100 Subject: [PATCH 28/50] using Parallel.ForEach for all inner tile calculations --- .../AdaptiveHistEqualizationProcessor.cs | 31 +++++++++++++------ .../AdaptiveHistEqualizationSWProcessor.cs | 4 +-- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs index ab73845f57..167a10579c 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs @@ -2,7 +2,9 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Collections.Generic; using System.Numerics; +using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -26,11 +28,11 @@ internal class AdaptiveHistEqualizationProcessor : HistogramEqualization /// or 65536 for 16-bit grayscale images. /// Indicating whether to clip the histogram bins at a specific value. /// Histogram clip limit in percent of the total pixels in the grid. Histogram bins which exceed this limit, will be capped at this value. - /// The number of tiles the image is split into (horizontal and vertically). + /// The number of tiles the image is split into (horizontal and vertically). Minimum value is 2. public AdaptiveHistEqualizationProcessor(int luminanceLevels, bool clipHistogram, float clipLimitPercentage, int tiles) : base(luminanceLevels, clipHistogram, clipLimitPercentage) { - Guard.MustBeGreaterThanOrEqualTo(tiles, 0, nameof(tiles)); + Guard.MustBeGreaterThanOrEqualTo(tiles, 2, nameof(tiles)); this.Tiles = tiles; } @@ -45,7 +47,6 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source { MemoryAllocator memoryAllocator = configuration.MemoryAllocator; int numberOfPixels = source.Width * source.Height; - Span pixels = source.GetPixelSpan(); int tileWidth = Convert.ToInt32(Math.Ceiling(source.Width / (double)this.Tiles)); int tileHeight = Convert.ToInt32(Math.Ceiling(source.Height / (double)this.Tiles)); @@ -62,12 +63,21 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source // The image is split up into tiles. For each tile the cumulative distribution function will be calculated. CdfData[,] cdfData = this.CalculateLookupTables(source, histogram, cdf, this.Tiles, this.Tiles, tileWidth, tileHeight); - int cdfX = 0; + var tileYStartPositions = new List<(int y, int cdfY)>(); int cdfY = 0; - int tileX = 0; - int tileY = 0; for (int y = halfTileHeight; y < source.Height - halfTileHeight; y += tileHeight) { + tileYStartPositions.Add((y, cdfY)); + cdfY++; + } + + Parallel.ForEach(tileYStartPositions, new ParallelOptions() { MaxDegreeOfParallelism = configuration.MaxDegreeOfParallelism }, (tileYStartPosition) => + { + 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) { @@ -76,11 +86,12 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source int xEnd = Math.Min(x + tileWidth, source.Width); for (int dy = y; dy < yEnd; dy++) { + Span pixelRow = source.GetPixelRowSpan(dy); tileX = 0; for (int dx = x; dx < xEnd; dx++) { - float luminanceEqualized = this.InterpolateBetweenFourTiles(source[dx, dy], cdfData, tileX, tileY, cdfX, cdfY, tileWidth, tileHeight, pixelsInTile); - pixels[(dy * source.Width) + dx].FromVector4(new Vector4(luminanceEqualized)); + float luminanceEqualized = this.InterpolateBetweenFourTiles(source[dx, dy], cdfData, tileX, tileY, cdfX, tileYStartPosition.cdfY, tileWidth, tileHeight, pixelsInTile); + pixelRow[dx].FromVector4(new Vector4(luminanceEqualized)); tileX++; } @@ -89,9 +100,9 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source cdfX++; } + }); - cdfY++; - } + Span pixels = source.GetPixelSpan(); // fix left column this.ProcessBorderColumn(source, pixels, cdfData, 0, tileWidth, tileHeight, xStart: 0, xEnd: halfTileWidth); diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs index dbdeb8c91b..c96b590248 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs @@ -27,11 +27,11 @@ internal class AdaptiveHistEqualizationSWProcessor : HistogramEqualizati /// or 65536 for 16-bit grayscale images. /// Indicating whether to clip the histogram bins at a specific value. /// Histogram clip limit in percent of the total pixels in the grid. Histogram bins which exceed this limit, will be capped at this value. - /// The number of tiles the image is split into (horizontal and vertically). + /// The number of tiles the image is split into (horizontal and vertically). Minimum value is 2. public AdaptiveHistEqualizationSWProcessor(int luminanceLevels, bool clipHistogram, float clipLimitPercentage, int tiles) : base(luminanceLevels, clipHistogram, clipLimitPercentage) { - Guard.MustBeGreaterThanOrEqualTo(tiles, 0, nameof(tiles)); + Guard.MustBeGreaterThanOrEqualTo(tiles, 2, nameof(tiles)); this.Tiles = tiles; } From 09955daa338d81d10d176a685c375d34ccc468bb Mon Sep 17 00:00:00 2001 From: popow Date: Sat, 8 Dec 2018 19:26:41 +0100 Subject: [PATCH 29/50] using Parallel.ForEach to calculate the lookup tables --- .../AdaptiveHistEqualizationProcessor.cs | 227 +++++++++--------- .../AdaptiveHistEqualizationSWProcessor.cs | 8 +- 2 files changed, 120 insertions(+), 115 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs index 167a10579c..232bb1b0a7 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs @@ -45,89 +45,80 @@ public AdaptiveHistEqualizationProcessor(int luminanceLevels, bool clipHistogram /// protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) { - MemoryAllocator memoryAllocator = configuration.MemoryAllocator; 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 pixelsInTile = tileWidth * tileHeight; int halfTileWidth = tileWidth / 2; int halfTileHeight = tileHeight / 2; - using (System.Buffers.IMemoryOwner histogramBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean)) - using (System.Buffers.IMemoryOwner cdfBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean)) - { - Span histogram = histogramBuffer.GetSpan(); - Span cdf = cdfBuffer.GetSpan(); + // 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); - // The image is split up into tiles. For each tile the cumulative distribution function will be calculated. - CdfData[,] cdfData = this.CalculateLookupTables(source, histogram, cdf, 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) + { + tileYStartPositions.Add((y, cdfY)); + cdfY++; + } - 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++; - } + Parallel.ForEach(tileYStartPositions, new ParallelOptions() { MaxDegreeOfParallelism = configuration.MaxDegreeOfParallelism }, (tileYStartPosition) => + { + int cdfX = 0; + int tileX = 0; + int tileY = 0; + int y = tileYStartPosition.y; - Parallel.ForEach(tileYStartPositions, new ParallelOptions() { MaxDegreeOfParallelism = configuration.MaxDegreeOfParallelism }, (tileYStartPosition) => + cdfX = 0; + for (int x = halfTileWidth; x < source.Width - halfTileWidth; x += tileWidth) { - 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) + 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++) { - 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++) + Span pixelRow = source.GetPixelRowSpan(dy); + tileX = 0; + for (int dx = x; dx < xEnd; dx++) { - Span pixelRow = source.GetPixelRowSpan(dy); - tileX = 0; - for (int dx = x; dx < xEnd; dx++) - { - float luminanceEqualized = this.InterpolateBetweenFourTiles(source[dx, dy], cdfData, tileX, tileY, cdfX, tileYStartPosition.cdfY, tileWidth, tileHeight, pixelsInTile); - pixelRow[dx].FromVector4(new Vector4(luminanceEqualized)); - tileX++; - } - - tileY++; + float luminanceEqualized = this.InterpolateBetweenFourTiles(source[dx, dy], cdfData, tileX, tileY, cdfX, tileYStartPosition.cdfY, tileWidth, tileHeight, pixelsInTile); + pixelRow[dx].FromVector4(new Vector4(luminanceEqualized)); + tileX++; } - cdfX++; + tileY++; } - }); - Span pixels = source.GetPixelSpan(); + cdfX++; + } + }); + + Span pixels = source.GetPixelSpan(); - // fix left column - this.ProcessBorderColumn(source, pixels, cdfData, 0, tileWidth, tileHeight, xStart: 0, xEnd: halfTileWidth); + // fix left column + this.ProcessBorderColumn(source, pixels, cdfData, 0, tileWidth, tileHeight, xStart: 0, xEnd: halfTileWidth); - // fix right column - this.ProcessBorderColumn(source, pixels, cdfData, this.Tiles - 1, tileWidth, tileHeight, xStart: source.Width - halfTileWidth, xEnd: source.Width); + // fix right column + this.ProcessBorderColumn(source, pixels, cdfData, this.Tiles - 1, tileWidth, tileHeight, xStart: source.Width - halfTileWidth, xEnd: source.Width); - // fix top row - this.ProcessBorderRow(source, pixels, cdfData, 0, tileWidth, tileHeight, yStart: 0, yEnd: halfTileHeight); + // fix top row + this.ProcessBorderRow(source, pixels, cdfData, 0, tileWidth, tileHeight, yStart: 0, yEnd: halfTileHeight); - // fix bottom row - this.ProcessBorderRow(source, pixels, cdfData, this.Tiles - 1, tileWidth, tileHeight, yStart: source.Height - halfTileHeight, yEnd: source.Height); + // fix bottom row + this.ProcessBorderRow(source, pixels, cdfData, this.Tiles - 1, tileWidth, tileHeight, yStart: source.Height - halfTileHeight, yEnd: source.Height); - // left top corner - this.ProcessCornerTile(source, pixels, cdfData[0, 0], xStart: 0, xEnd: halfTileWidth, yStart: 0, yEnd: halfTileHeight, pixelsInTile: pixelsInTile); + // left top corner + this.ProcessCornerTile(source, pixels, cdfData[0, 0], xStart: 0, xEnd: halfTileWidth, yStart: 0, yEnd: halfTileHeight, pixelsInTile: pixelsInTile); - // left bottom corner - this.ProcessCornerTile(source, pixels, cdfData[0, this.Tiles - 1], xStart: 0, xEnd: halfTileWidth, yStart: source.Height - halfTileHeight, yEnd: source.Height, pixelsInTile: pixelsInTile); + // left bottom corner + this.ProcessCornerTile(source, pixels, cdfData[0, this.Tiles - 1], xStart: 0, xEnd: halfTileWidth, yStart: source.Height - halfTileHeight, yEnd: source.Height, pixelsInTile: pixelsInTile); - // right top corner - this.ProcessCornerTile(source, pixels, cdfData[this.Tiles - 1, 0], xStart: source.Width - halfTileWidth, xEnd: source.Width, yStart: 0, yEnd: halfTileHeight, pixelsInTile: pixelsInTile); + // right top corner + this.ProcessCornerTile(source, pixels, cdfData[this.Tiles - 1, 0], xStart: source.Width - halfTileWidth, xEnd: source.Width, yStart: 0, yEnd: halfTileHeight, pixelsInTile: pixelsInTile); - // right bottom corner - this.ProcessCornerTile(source, pixels, cdfData[this.Tiles - 1, this.Tiles - 1], xStart: source.Width - halfTileWidth, xEnd: source.Width, yStart: source.Height - halfTileHeight, yEnd: source.Height, pixelsInTile: pixelsInTile); - } + // right bottom corner + this.ProcessCornerTile(source, pixels, cdfData[this.Tiles - 1, this.Tiles - 1], xStart: source.Width - halfTileWidth, xEnd: source.Width, yStart: source.Height - halfTileHeight, yEnd: source.Height, pixelsInTile: pixelsInTile); } /// @@ -286,59 +277,6 @@ private float InterpolateBetweenTwoTiles(TPixel sourcePixel, CdfData cdfData1, C return luminanceEqualized; } - /// - /// Calculates the lookup tables for each tile of the image. - /// - /// The input image for which the tiles will be calculated. - /// Histogram buffer. - /// Buffer for calculating the cumulative distribution function. - /// 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, Span histogram, Span cdf, int numTilesX, int numTilesY, int tileWidth, int tileHeight) - { - var cdfData = new CdfData[numTilesX, numTilesY]; - int pixelsInTile = tileWidth * tileHeight; - int tileX = 0; - int tileY = 0; - for (int y = 0; y < source.Height; y += tileHeight) - { - tileX = 0; - for (int x = 0; x < source.Width; x += tileWidth) - { - 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++) - { - for (int dx = x; dx < xlimit; dx++) - { - int luminace = this.GetLuminance(source[dx, dy], this.LuminanceLevels); - histogram[luminace]++; - } - } - - if (this.ClipHistogramEnabled) - { - this.ClipHistogram(histogram, this.ClipLimitPercentage, pixelsInTile); - } - - int cdfMin = this.CalculateCdf(cdf, histogram, histogram.Length - 1); - var currentCdf = new CdfData(cdf.ToArray(), cdfMin); - cdfData[tileX, tileY] = currentCdf; - - tileX++; - } - - tileY++; - } - - return cdfData; - } - /// /// Bilinear interpolation between four tiles. /// @@ -366,6 +304,73 @@ private float LinearInterpolation(float left, float right, float t) return left + ((right - left) * t); } + /// + /// Calculates the lookup tables for each tile of the image. + /// + /// 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) + { + 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) + { + tileYStartPositions.Add((y, cdfY)); + cdfY++; + } + + Parallel.ForEach(tileYStartPositions, new ParallelOptions() { MaxDegreeOfParallelism = configuration.MaxDegreeOfParallelism }, (tileYStartPosition) => + { + using (System.Buffers.IMemoryOwner histogramBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean)) + using (System.Buffers.IMemoryOwner cdfBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean)) + { + int cdfX = 0; + int y = tileYStartPosition.y; + for (int x = 0; x < source.Width; x += 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++) + { + for (int dx = x; dx < xlimit; dx++) + { + int luminace = this.GetLuminance(source[dx, dy], this.LuminanceLevels); + histogram[luminace]++; + } + } + + if (this.ClipHistogramEnabled) + { + this.ClipHistogram(histogram, this.ClipLimitPercentage, pixelsInTile); + } + + int cdfMin = this.CalculateCdf(cdf, histogram, histogram.Length - 1); + var currentCdf = new CdfData(cdf.ToArray(), cdfMin); + cdfData[cdfX, tileYStartPosition.cdfY] = currentCdf; + + cdfX++; + } + + cdfY++; + } + }); + + return cdfData; + } + /// /// Lookup table for remapping the grey values of one tile. /// diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs index c96b590248..66775520f2 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs @@ -178,8 +178,8 @@ private Span GetPixelRow(ImageFrame source, int x, int y, int ti /// /// Adds a row of grey values to the histogram. /// - /// The grey values to add - /// The histogram + /// The grey values to add. + /// The histogram. /// The number of different luminance levels. /// The maximum index where a value was changed. private int AddPixelsToHistogram(Span greyValues, Span histogram, int luminanceLevels) @@ -201,8 +201,8 @@ private int AddPixelsToHistogram(Span greyValues, Span histogram, i /// /// Removes a row of grey values from the histogram. /// - /// The grey values to remove - /// The histogram + /// The grey values to remove. + /// The histogram. /// The number of different luminance levels. /// The current maximum index of the histogram. /// The (maybe changed) maximum index of the histogram. From 66b75503c5a80bbabc6c04d2c239084d76a569cb Mon Sep 17 00:00:00 2001 From: popow Date: Sat, 8 Dec 2018 19:47:04 +0100 Subject: [PATCH 30/50] re-using pre allocated pixel row in GetPixelRow --- .../AdaptiveHistEqualizationSWProcessor.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs index 66775520f2..bf6eeeb04e 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs @@ -63,16 +63,18 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source using (System.Buffers.IMemoryOwner histogramBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean)) using (System.Buffers.IMemoryOwner histogramBufferCopy = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean)) using (System.Buffers.IMemoryOwner cdfBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean)) + using (System.Buffers.IMemoryOwner pixelRowBuffer = memoryAllocator.Allocate(tileWidth, AllocationOptions.Clean)) { Span histogram = histogramBuffer.GetSpan(); Span histogramCopy = histogramBufferCopy.GetSpan(); Span cdf = cdfBuffer.GetSpan(); + Span pixelRow = pixelRowBuffer.GetSpan(); int maxHistIdx = 0; // Build the histogram of grayscale values for the current grid. for (int dy = -halfTileWith; dy < halfTileWith; dy++) { - Span rowSpan = this.GetPixelRow(source, (int)x - halfTileWith, dy, tileWidth); + Span rowSpan = this.GetPixelRow(source, pixelRow, (int)x - halfTileWith, dy, tileWidth); int maxIdx = this.AddPixelsToHistogram(rowSpan, histogram, this.LuminanceLevels); if (maxIdx > maxHistIdx) { @@ -99,11 +101,11 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source targetPixels[x, y].FromVector4(new Vector4(luminanceEqualized)); // Remove top most row from the histogram, mirroring rows which exceeds the borders. - Span rowSpan = this.GetPixelRow(source, x - halfTileWith, y - halfTileWith, tileWidth); + Span rowSpan = this.GetPixelRow(source, pixelRow, x - halfTileWith, y - halfTileWith, tileWidth); maxHistIdx = this.RemovePixelsFromHistogram(rowSpan, histogram, this.LuminanceLevels, maxHistIdx); // Add new bottom row to the histogram, mirroring rows which exceeds the borders. - rowSpan = this.GetPixelRow(source, x - halfTileWith, y + halfTileWith, tileWidth); + rowSpan = this.GetPixelRow(source, pixelRow, x - halfTileWith, y + halfTileWith, tileWidth); int maxIdx = this.AddPixelsToHistogram(rowSpan, histogram, this.LuminanceLevels); if (maxIdx > maxHistIdx) { @@ -121,12 +123,15 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source /// Get the a pixel row at a given position with a length of the tile width. Mirrors pixels which exceeds the edges. /// /// The source image. + /// Pre-allocated pixel row span of the size of a tile width. /// The x position. /// The y position. /// The width in pixels of a tile. /// A pixel row of the length of the tile width. - private Span GetPixelRow(ImageFrame source, int x, int y, int tileWidth) + private Span GetPixelRow(ImageFrame source, Span rowPixels, int x, int y, int tileWidth) { + rowPixels.Clear(); + if (y < 0) { y = Math.Abs(y); @@ -140,7 +145,6 @@ private Span GetPixelRow(ImageFrame source, int x, int y, int ti // Special cases for the left and the right border where GetPixelRowSpan can not be used if (x < 0) { - var rowPixels = new TPixel[tileWidth]; int idx = 0; for (int dx = x; dx < x + tileWidth; dx++) { @@ -152,7 +156,6 @@ private Span GetPixelRow(ImageFrame source, int x, int y, int ti } else if (x + tileWidth > source.Width) { - var rowPixels = new TPixel[tileWidth]; int idx = 0; for (int dx = x; dx < x + tileWidth; dx++) { From f72908f7a8f3c478dec1e5cfe12d16c90465fed0 Mon Sep 17 00:00:00 2001 From: popow Date: Sun, 9 Dec 2018 18:30:30 +0100 Subject: [PATCH 31/50] fixed issue with the border tiles, when tile width != tile height --- .../AdaptiveHistEqualizationProcessor.cs | 14 ++++++++------ .../AdaptiveHistEqualizationSWProcessor.cs | 4 ++-- .../Normalization/HistogramEqualizationMethod.cs | 2 +- .../Normalization/HistogramEqualizationOptions.cs | 10 +++++----- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs index 232bb1b0a7..6e55ec8472 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs @@ -100,25 +100,27 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source this.ProcessBorderColumn(source, pixels, cdfData, 0, tileWidth, tileHeight, xStart: 0, xEnd: halfTileWidth); // fix right column - this.ProcessBorderColumn(source, pixels, cdfData, this.Tiles - 1, tileWidth, tileHeight, xStart: source.Width - halfTileWidth, xEnd: source.Width); + int rightBorderStartX = ((this.Tiles - 1) * tileWidth) + halfTileWidth; + this.ProcessBorderColumn(source, pixels, cdfData, this.Tiles - 1, tileWidth, tileHeight, xStart: rightBorderStartX, xEnd: source.Width); // fix top row this.ProcessBorderRow(source, pixels, cdfData, 0, tileWidth, tileHeight, yStart: 0, yEnd: halfTileHeight); // fix bottom row - this.ProcessBorderRow(source, pixels, cdfData, this.Tiles - 1, tileWidth, tileHeight, yStart: source.Height - halfTileHeight, yEnd: source.Height); + int bottomBorderStartY = ((this.Tiles - 1) * tileHeight) + halfTileHeight; + this.ProcessBorderRow(source, pixels, cdfData, this.Tiles - 1, tileWidth, tileHeight, yStart: bottomBorderStartY, yEnd: source.Height); // left top corner this.ProcessCornerTile(source, pixels, cdfData[0, 0], xStart: 0, xEnd: halfTileWidth, yStart: 0, yEnd: halfTileHeight, pixelsInTile: pixelsInTile); // left bottom corner - this.ProcessCornerTile(source, pixels, cdfData[0, this.Tiles - 1], xStart: 0, xEnd: halfTileWidth, yStart: source.Height - halfTileHeight, yEnd: source.Height, pixelsInTile: pixelsInTile); + this.ProcessCornerTile(source, pixels, cdfData[0, this.Tiles - 1], xStart: 0, xEnd: halfTileWidth, yStart: bottomBorderStartY, yEnd: source.Height, pixelsInTile: pixelsInTile); // right top corner - this.ProcessCornerTile(source, pixels, cdfData[this.Tiles - 1, 0], xStart: source.Width - halfTileWidth, xEnd: source.Width, yStart: 0, yEnd: halfTileHeight, pixelsInTile: pixelsInTile); + this.ProcessCornerTile(source, pixels, cdfData[this.Tiles - 1, 0], xStart: rightBorderStartX, xEnd: source.Width, yStart: 0, yEnd: halfTileHeight, pixelsInTile: pixelsInTile); // right bottom corner - this.ProcessCornerTile(source, pixels, cdfData[this.Tiles - 1, this.Tiles - 1], xStart: source.Width - halfTileWidth, xEnd: source.Width, yStart: source.Height - halfTileHeight, yEnd: source.Height, pixelsInTile: pixelsInTile); + this.ProcessCornerTile(source, pixels, cdfData[this.Tiles - 1, this.Tiles - 1], xStart: rightBorderStartX, xEnd: source.Width, yStart: bottomBorderStartY, yEnd: source.Height, pixelsInTile: pixelsInTile); } /// @@ -162,7 +164,7 @@ private void ProcessBorderColumn(ImageFrame source, Span pixels, int pixelsInTile = tileWidth * tileHeight; int cdfY = 0; - for (int y = halfTileWidth; y < source.Height - halfTileWidth; y += tileHeight) + for (int y = halfTileHeight; y < source.Height - halfTileHeight; y += tileHeight) { int yLimit = Math.Min(y + tileHeight, source.Height - 1); int tileY = 0; diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs index bf6eeeb04e..a9ec21d2d1 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs @@ -130,8 +130,6 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source /// A pixel row of the length of the tile width. private Span GetPixelRow(ImageFrame source, Span rowPixels, int x, int y, int tileWidth) { - rowPixels.Clear(); - if (y < 0) { y = Math.Abs(y); @@ -145,6 +143,7 @@ private Span GetPixelRow(ImageFrame source, Span rowPixe // Special cases for the left and the right border where GetPixelRowSpan can not be used if (x < 0) { + rowPixels.Clear(); int idx = 0; for (int dx = x; dx < x + tileWidth; dx++) { @@ -156,6 +155,7 @@ private Span GetPixelRow(ImageFrame source, Span rowPixe } else if (x + tileWidth > source.Width) { + rowPixels.Clear(); int idx = 0; for (int dx = x; dx < x + tileWidth; dx++) { diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs index 5226e3f88e..641587c394 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs @@ -19,7 +19,7 @@ public enum HistogramEqualizationMethod : int AdaptiveTileInterpolation, /// - /// Adaptive sliding window histogram equalization. + /// Adaptive histogram equalization using sliding window. Slower then the tile interpolation mode, but can yield to better results. /// AdaptiveSlidingWindow, } diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs index afc887db55..d7a827c7d2 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs @@ -20,19 +20,19 @@ public class HistogramEqualizationOptions public int LuminanceLevels { get; set; } = 256; /// - /// Gets or sets a value indicating whether to clip the histogram bins at a specific value. Defaults to false. + /// Gets or sets a value indicating whether to clip the histogram bins at a specific value. Defaults to true. /// - public bool ClipHistogram { get; set; } = false; + public bool ClipHistogram { get; set; } = true; /// - /// Gets or sets the histogram clip limit in percent of the total pixels in the grid. Histogram bins which exceed this limit, will be capped at this value. + /// Gets or sets the histogram clip limit in percent of the total pixels in a tile. Histogram bins which exceed this limit, will be capped at this value. /// Defaults to 0.35. /// public float ClipLimitPercentage { get; set; } = 0.035f; /// - /// Gets or sets the number of tiles the image is split into (horizontal and vertically) for the adaptive histogram equalization. Defaults to 8. + /// Gets or sets the number of tiles the image is split into (horizontal and vertically) for the adaptive histogram equalization. Defaults to 10. /// - public int Tiles { get; set; } = 8; + public int Tiles { get; set; } = 10; } } From cadd2b3884665740174e8f80ee9e8592ee6d4f16 Mon Sep 17 00:00:00 2001 From: popow Date: Sun, 9 Dec 2018 19:51:37 +0100 Subject: [PATCH 32/50] changed default value for ClipHistogram to false again --- .../Processors/Normalization/HistogramEqualizationOptions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs index d7a827c7d2..190c664436 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs @@ -20,9 +20,9 @@ public class HistogramEqualizationOptions public int LuminanceLevels { get; set; } = 256; /// - /// Gets or sets a value indicating whether to clip the histogram bins at a specific value. Defaults to true. + /// Gets or sets a value indicating whether to clip the histogram bins at a specific value. Defaults to false. /// - public bool ClipHistogram { get; set; } = true; + public bool ClipHistogram { get; set; } = false; /// /// Gets or sets the histogram clip limit in percent of the total pixels in a tile. Histogram bins which exceed this limit, will be capped at this value. From fa1f0367204cc72790af373beaa318099534d987 Mon Sep 17 00:00:00 2001 From: popow Date: Mon, 10 Dec 2018 20:46:04 +0100 Subject: [PATCH 33/50] alpha channel from the original image is now preserved --- .../Normalization/AdaptiveHistEqualizationProcessor.cs | 10 +++++----- .../AdaptiveHistEqualizationSWProcessor.cs | 9 ++++----- .../GlobalHistogramEqualizationProcessor.cs | 2 +- .../Normalization/HistogramEqualizationOptions.cs | 2 +- .../Normalization/HistogramEqualizationProcessor.cs | 8 ++++---- 5 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs index 6e55ec8472..f9190a92b6 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs @@ -27,7 +27,7 @@ internal class AdaptiveHistEqualizationProcessor : HistogramEqualization /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images /// or 65536 for 16-bit grayscale images. /// Indicating whether to clip the histogram bins at a specific value. - /// Histogram clip limit in percent of the total pixels in the grid. Histogram bins which exceed this limit, will be capped at this value. + /// 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 number of tiles the image is split into (horizontal and vertically). Minimum value is 2. public AdaptiveHistEqualizationProcessor(int luminanceLevels, bool clipHistogram, float clipLimitPercentage, int tiles) : base(luminanceLevels, clipHistogram, clipLimitPercentage) @@ -83,7 +83,7 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source for (int dx = x; dx < xEnd; dx++) { float luminanceEqualized = this.InterpolateBetweenFourTiles(source[dx, dy], cdfData, tileX, tileY, cdfX, tileYStartPosition.cdfY, tileWidth, tileHeight, pixelsInTile); - pixelRow[dx].FromVector4(new Vector4(luminanceEqualized)); + pixelRow[dx].FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixelRow[dx].ToVector4().W)); tileX++; } @@ -141,7 +141,7 @@ private void ProcessCornerTile(ImageFrame source, Span pixels, C for (int dx = xStart; dx < xEnd; dx++) { float luminanceEqualized = cdfData.RemapGreyValue(this.GetLuminance(source[dx, dy], this.LuminanceLevels), pixelsInTile); - pixels[(dy * source.Width) + dx].FromVector4(new Vector4(luminanceEqualized)); + pixels[(dy * source.Width) + dx].FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, source[dx, dy].ToVector4().W)); } } } @@ -174,7 +174,7 @@ private void ProcessBorderColumn(ImageFrame source, Span pixels, for (int dx = xStart; dx < xEnd; dx++) { float luminanceEqualized = this.InterpolateBetweenTwoTiles(source[dx, dy], cdfData[cdfX, cdfY], cdfData[cdfX, cdfY + 1], tileY, tileHeight, pixelsInTile); - pixels[(dy * source.Width) + dx].FromVector4(new Vector4(luminanceEqualized)); + pixels[(dy * source.Width) + dx].FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, source[dx, dy].ToVector4().W)); tileX++; } @@ -213,7 +213,7 @@ private void ProcessBorderRow(ImageFrame source, Span pixels, Cd for (int dx = x; dx < xLimit; dx++) { float luminanceEqualized = this.InterpolateBetweenTwoTiles(source[dx, dy], cdfData[cdfX, cdfY], cdfData[cdfX + 1, cdfY], tileX, tileWidth, pixelsInTile); - pixels[(dy * source.Width) + dx].FromVector4(new Vector4(luminanceEqualized)); + pixels[(dy * source.Width) + dx].FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, source[dx, dy].ToVector4().W)); tileX++; } diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs index a9ec21d2d1..7d82b413fd 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.ParallelUtils; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Memory; using SixLabors.Primitives; @@ -26,7 +25,7 @@ internal class AdaptiveHistEqualizationSWProcessor : HistogramEqualizati /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images /// or 65536 for 16-bit grayscale images. /// Indicating whether to clip the histogram bins at a specific value. - /// Histogram clip limit in percent of the total pixels in the grid. Histogram bins which exceed this limit, will be capped at this value. + /// 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 number of tiles the image is split into (horizontal and vertically). Minimum value is 2. public AdaptiveHistEqualizationSWProcessor(int luminanceLevels, bool clipHistogram, float clipLimitPercentage, int tiles) : base(luminanceLevels, clipHistogram, clipLimitPercentage) @@ -71,7 +70,7 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source Span pixelRow = pixelRowBuffer.GetSpan(); int maxHistIdx = 0; - // Build the histogram of grayscale values for the current grid. + // Build the histogram of grayscale values for the current tile. for (int dy = -halfTileWith; dy < halfTileWith; dy++) { Span rowSpan = this.GetPixelRow(source, pixelRow, (int)x - halfTileWith, dy, tileWidth); @@ -91,14 +90,14 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source this.ClipHistogram(histogramCopy, this.ClipLimitPercentage, pixeInTile); } - // Calculate the cumulative distribution function, which will map each input pixel in the current grid to a new value. + // 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(cdf, histogramCopy, maxHistIdx) : this.CalculateCdf(cdf, histogram, maxHistIdx); float numberOfPixelsMinusCdfMin = pixeInTile - cdfMin; // Map the current pixel to the new equalized value int luminance = this.GetLuminance(source[x, y], this.LuminanceLevels); float luminanceEqualized = cdf[luminance] / numberOfPixelsMinusCdfMin; - targetPixels[x, y].FromVector4(new Vector4(luminanceEqualized)); + 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. Span rowSpan = this.GetPixelRow(source, pixelRow, x - halfTileWith, y - halfTileWith, tileWidth); diff --git a/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs index a4de02cc7e..5cad9e60a7 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs @@ -67,7 +67,7 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source int luminance = this.GetLuminance(sourcePixel, this.LuminanceLevels); float luminanceEqualized = cdf[luminance] / numberOfPixelsMinusCdfMin; - pixels[i].FromVector4(new Vector4(luminanceEqualized)); + pixels[i].FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, sourcePixel.ToVector4().W)); } } } diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs index 190c664436..6c0938c4ec 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs @@ -26,7 +26,7 @@ public class HistogramEqualizationOptions /// /// Gets or sets the histogram clip limit in percent of the total pixels in a tile. Histogram bins which exceed this limit, will be capped at this value. - /// Defaults to 0.35. + /// Defaults to 0.035f. /// public float ClipLimitPercentage { get; set; } = 0.035f; diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs index e52aff1e76..2763fcddd8 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs @@ -19,7 +19,7 @@ internal abstract class HistogramEqualizationProcessor : ImageProcessor< /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images /// or 65536 for 16-bit grayscale images. /// Indicates, if histogram bins should be clipped. - /// Histogram clip limit in percent of the total pixels in the grid. Histogram bins which exceed this limit, will be capped at this value. + /// Histogram clip limit in percent of the total pixels in the tile. Histogram bins which exceed this limit, will be capped at this value. protected HistogramEqualizationProcessor(int luminanceLevels, bool clipHistogram, float clipLimitPercentage) { Guard.MustBeGreaterThan(luminanceLevels, 0, nameof(luminanceLevels)); @@ -41,7 +41,7 @@ protected HistogramEqualizationProcessor(int luminanceLevels, bool clipHistogram public bool ClipHistogramEnabled { get; } /// - /// Gets the histogram clip limit in percent of the total pixels in the grid. Histogram bins which exceed this limit, will be capped at this value. + /// Gets the histogram clip limit in percent of the total pixels in the tile. Histogram bins which exceed this limit, will be capped at this value. /// public float ClipLimitPercentage { get; } @@ -79,8 +79,8 @@ protected int CalculateCdf(Span cdf, Span histogram, int maxIdx) /// the values over the clip limit to all other bins equally. /// /// The histogram to apply the clipping. - /// Histogram clip limit in percent of the total pixels in the grid. Histogram bins which exceed this limit, will be capped at this value. - /// The numbers of pixels inside the grid. + /// 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) { int clipLimit = Convert.ToInt32(pixelCount * clipLimitPercentage); From 05676ce5a68dae0d359c0f8ee8d5fc59e6cf0608 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 28 Dec 2018 17:25:20 +0100 Subject: [PATCH 34/50] added unit tests for adaptive histogram equalization --- .../HistogramEqualizationTests.cs | 43 ++++++++++++++++++ tests/ImageSharp.Tests/TestImages.cs | 1 + tests/Images/External | 2 +- .../Jpg/baseline/AsianCarvingLowContrast.jpg | Bin 0 -> 187216 bytes 4 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 tests/Images/Input/Jpg/baseline/AsianCarvingLowContrast.jpg diff --git a/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs b/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs index 86d343e423..984993748c 100644 --- a/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs +++ b/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs @@ -4,12 +4,15 @@ using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Normalization; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Normalization { public class HistogramEqualizationTests { + private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.0456F); + [Theory] [InlineData(256)] [InlineData(65536)] @@ -68,5 +71,45 @@ public void HistogramEqualizationTest(int luminanceLevels) } } } + + [Theory] + [WithFile(TestImages.Jpeg.Baseline.LowContrast, PixelTypes.Rgba32)] + public void Adaptive_SlidingWindow_15Tiles_WithClipping(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage()) + { + var options = new HistogramEqualizationOptions() + { + Method = HistogramEqualizationMethod.AdaptiveSlidingWindow, + LuminanceLevels = 256, + ClipHistogram = true, + Tiles = 15 + }; + image.Mutate(x => x.HistogramEqualization(options)); + image.DebugSave(provider); + image.CompareToReferenceOutput(ValidatorComparer, provider); + } + } + + [Theory] + [WithFile(TestImages.Jpeg.Baseline.LowContrast, PixelTypes.Rgba32)] + public void Adaptive_TileInterpolation_10Tiles_WithClipping(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage()) + { + var options = new HistogramEqualizationOptions() + { + Method = HistogramEqualizationMethod.AdaptiveTileInterpolation, + LuminanceLevels = 256, + ClipHistogram = true, + Tiles = 10 + }; + image.Mutate(x => x.HistogramEqualization(options)); + image.DebugSave(provider); + image.CompareToReferenceOutput(ValidatorComparer, provider); + } + } } } diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index e5b93ab77c..af2e2558d8 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -135,6 +135,7 @@ public static class Bad public const string Testorig420 = "Jpg/baseline/testorig.jpg"; public const string MultiScanBaselineCMYK = "Jpg/baseline/MultiScanBaselineCMYK.jpg"; public const string Ratio1x1 = "Jpg/baseline/ratio-1x1.jpg"; + public const string LowContrast = "Jpg/baseline/AsianCarvingLowContrast.jpg"; public static readonly string[] All = { diff --git a/tests/Images/External b/tests/Images/External index 69603ee5b6..9a852d6fa5 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit 69603ee5b6f7dd64114fc44d321e50d9b2d439be +Subproject commit 9a852d6fa566d5a88f989093f9430ee54f784e53 diff --git a/tests/Images/Input/Jpg/baseline/AsianCarvingLowContrast.jpg b/tests/Images/Input/Jpg/baseline/AsianCarvingLowContrast.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c1b13352a06d7933237fc1e6420f462a1df02fbe GIT binary patch literal 187216 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 b;13CXqfz~^8b@DN)fyU5R^Q4$(*gh4w{<6& literal 0 HcmV?d00001 From 2d31858ef4b939db7aaf600f54438b94ef132f5e Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 20 Jan 2019 18:22:13 +1100 Subject: [PATCH 35/50] Update External --- tests/Images/External | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Images/External b/tests/Images/External index 9a852d6fa5..74995302e3 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit 9a852d6fa566d5a88f989093f9430ee54f784e53 +Subproject commit 74995302e3c9a5913f1cf09d71b15f888d0ec022 From 4330c82f28fdd29e8292a8da4de02f769911a348 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 23 Jan 2019 12:03:20 +1100 Subject: [PATCH 36/50] 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.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 @@ public AdaptiveHistEqualizationProcessor(int luminanceLevels, bool clipHistogram 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 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source /// 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 @@ private void ProcessCornerTile(ImageFrame source, Span pixels, C /// 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 @@ private void ProcessBorderColumn(ImageFrame source, Span pixels, 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 @@ private void ProcessBorderColumn(ImageFrame source, Span pixels, /// 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 @@ private void ProcessBorderRow(ImageFrame source, Span pixels, Cd 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 @@ private void ProcessBorderRow(ImageFrame source, Span pixels, Cd /// /// 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 @@ private float InterpolateBetweenTwoTiles(TPixel sourcePixel, CdfData cdfData1, C /// 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 @@ private float BilinearInterpolation(float tx, float ty, float lt, float rt, floa /// 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 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source 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 @@ private int AddPixelsToHistogram(Span greyValues, Span histogram, i 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 @@ private int RemovePixelsFromHistogram(Span greyValues, Span histogr { 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 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source 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 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source { 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 @@ internal abstract class HistogramEqualizationProcessor : ImageProcessor< 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 @@ protected HistogramEqualizationProcessor(int luminanceLevels, bool clipHistogram /// 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 @@ protected int CalculateCdf(Span cdf, Span histogram, int maxIdx) } // 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 @@ protected int CalculateCdf(Span cdf, Span histogram, int maxIdx) /// 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 @@ protected void ClipHistogram(Span histogram, float clipLimitPercentage, int /// /// 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 @@ public void HistogramEqualizationTest(int luminanceLevels) 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 @@ public void HistogramEqualizationTest(int luminanceLevels) 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 @@ public void Adaptive_SlidingWindow_15Tiles_WithClipping(TestImageProvide 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 @@ public void Adaptive_TileInterpolation_10Tiles_WithClipping(TestImagePro 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); From a861b5419a60f8c7f30e6742727ddf75f58cf6f2 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 23 Jan 2019 18:07:28 +1100 Subject: [PATCH 37/50] Remove double indexing and bounds checks --- .../AdaptiveHistEqualizationProcessor.cs | 183 +++++++++++------- 1 file changed, 108 insertions(+), 75 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs index 20543369fa..f03c3ff9b2 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs @@ -2,11 +2,12 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Buffers; using System.Collections.Generic; using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Threading.Tasks; -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Memory; @@ -46,63 +47,71 @@ public AdaptiveHistEqualizationProcessor(int luminanceLevels, bool clipHistogram /// protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) { - int numberOfPixels = source.Width * source.Height; - int tileWidth = (int)MathF.Ceiling(source.Width / (float)this.Tiles); - int tileHeight = (int)MathF.Ceiling(source.Height / (float)this.Tiles); + int sourceWidth = source.Width; + int sourceHeight = source.Height; + int numberOfPixels = sourceWidth * sourceHeight; + int tileWidth = (int)MathF.Ceiling(sourceWidth / (float)this.Tiles); + int tileHeight = (int)MathF.Ceiling(sourceHeight / (float)this.Tiles); int pixelsInTile = tileWidth * tileHeight; int halfTileWidth = tileWidth / 2; int halfTileHeight = tileHeight / 2; int luminanceLevels = this.LuminanceLevels; // The image is split up into tiles. For each tile the cumulative distribution function will be calculated. - using (var cdfData = new CdfTileData(configuration, sourceRectangle.Height, this.Tiles, this.Tiles, tileWidth, tileHeight, luminanceLevels)) + using (var cdfData = new CdfTileData(configuration, sourceWidth, sourceHeight, this.Tiles, this.Tiles, tileWidth, tileHeight, luminanceLevels)) { cdfData.CalculateLookupTables(source, this); var tileYStartPositions = new List<(int y, int cdfY)>(); int cdfY = 0; - for (int y = halfTileHeight; y < source.Height - halfTileHeight; y += tileHeight) + for (int y = halfTileHeight; y < sourceHeight - halfTileHeight; y += tileHeight) { tileYStartPositions.Add((y, cdfY)); cdfY++; } - Parallel.ForEach( - tileYStartPositions, + Parallel.For( + 0, + tileYStartPositions.Count, new ParallelOptions() { MaxDegreeOfParallelism = configuration.MaxDegreeOfParallelism }, - tileYStartPosition => + index => { int cdfX = 0; int tileX = 0; int tileY = 0; - int y = tileYStartPosition.y; + int y = tileYStartPositions[index].y; + int cdfYY = tileYStartPositions[index].cdfY; + + // It's unfortunate that we have to do this per iteration. + ref TPixel sourceBase = ref source.GetPixelReference(0, 0); cdfX = 0; - for (int x = halfTileWidth; x < source.Width - halfTileWidth; x += tileWidth) + for (int x = halfTileWidth; x < sourceWidth - halfTileWidth; x += tileWidth) { tileY = 0; - int yEnd = Math.Min(y + tileHeight, source.Height); - int xEnd = Math.Min(x + tileWidth, source.Width); + int yEnd = Math.Min(y + tileHeight, sourceHeight); + int xEnd = Math.Min(x + tileWidth, sourceWidth); for (int dy = y; dy < yEnd; dy++) { - Span pixelRow = source.GetPixelRowSpan(dy); + int dyOffSet = dy * sourceWidth; tileX = 0; for (int dx = x; dx < xEnd; dx++) { + ref TPixel pixel = ref Unsafe.Add(ref sourceBase, dyOffSet + dx); float luminanceEqualized = InterpolateBetweenFourTiles( - source[dx, dy], + pixel, cdfData, this.Tiles, this.Tiles, tileX, tileY, cdfX, - tileYStartPosition.cdfY, + cdfYY, tileWidth, tileHeight, luminanceLevels); - pixelRow[dx].FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixelRow[dx].ToVector4().W)); + pixel.FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W)); tileX++; } @@ -113,42 +122,42 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source } }); - Span pixels = source.GetPixelSpan(); + ref TPixel pixelsBase = ref source.GetPixelReference(0, 0); // Fix left column - ProcessBorderColumn(source, pixels, cdfData, 0, tileWidth, tileHeight, xStart: 0, xEnd: halfTileWidth, luminanceLevels); + ProcessBorderColumn(ref pixelsBase, cdfData, 0, sourceWidth, sourceHeight, tileWidth, tileHeight, xStart: 0, xEnd: halfTileWidth, luminanceLevels); // Fix right column int rightBorderStartX = ((this.Tiles - 1) * tileWidth) + halfTileWidth; - ProcessBorderColumn(source, pixels, cdfData, this.Tiles - 1, tileWidth, tileHeight, xStart: rightBorderStartX, xEnd: source.Width, luminanceLevels); + ProcessBorderColumn(ref pixelsBase, cdfData, this.Tiles - 1, sourceWidth, sourceHeight, tileWidth, tileHeight, xStart: rightBorderStartX, xEnd: sourceWidth, luminanceLevels); // Fix top row - ProcessBorderRow(source, pixels, cdfData, 0, tileWidth, tileHeight, yStart: 0, yEnd: halfTileHeight, luminanceLevels); + ProcessBorderRow(ref pixelsBase, cdfData, 0, sourceWidth, tileWidth, tileHeight, yStart: 0, yEnd: halfTileHeight, luminanceLevels); // 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); + ProcessBorderRow(ref pixelsBase, cdfData, this.Tiles - 1, sourceWidth, tileWidth, tileHeight, yStart: bottomBorderStartY, yEnd: sourceHeight, luminanceLevels); // Left top corner - ProcessCornerTile(source, pixels, cdfData, 0, 0, xStart: 0, xEnd: halfTileWidth, yStart: 0, yEnd: halfTileHeight, luminanceLevels); + ProcessCornerTile(ref pixelsBase, cdfData, sourceWidth, 0, 0, xStart: 0, xEnd: halfTileWidth, yStart: 0, yEnd: halfTileHeight, luminanceLevels); // Left bottom corner - ProcessCornerTile(source, pixels, cdfData, 0, this.Tiles - 1, xStart: 0, xEnd: halfTileWidth, yStart: bottomBorderStartY, yEnd: source.Height, luminanceLevels); + ProcessCornerTile(ref pixelsBase, cdfData, sourceWidth, 0, this.Tiles - 1, xStart: 0, xEnd: halfTileWidth, yStart: bottomBorderStartY, yEnd: sourceHeight, luminanceLevels); // Right top corner - ProcessCornerTile(source, pixels, cdfData, this.Tiles - 1, 0, xStart: rightBorderStartX, xEnd: source.Width, yStart: 0, yEnd: halfTileHeight, luminanceLevels); + ProcessCornerTile(ref pixelsBase, cdfData, sourceWidth, this.Tiles - 1, 0, xStart: rightBorderStartX, xEnd: sourceWidth, yStart: 0, yEnd: halfTileHeight, luminanceLevels); // Right bottom corner - ProcessCornerTile(source, pixels, cdfData, this.Tiles - 1, this.Tiles - 1, xStart: rightBorderStartX, xEnd: source.Width, yStart: bottomBorderStartY, yEnd: source.Height, luminanceLevels); + ProcessCornerTile(ref pixelsBase, cdfData, sourceWidth, this.Tiles - 1, this.Tiles - 1, xStart: rightBorderStartX, xEnd: sourceWidth, yStart: bottomBorderStartY, yEnd: sourceHeight, luminanceLevels); } } /// /// Processes the part of a corner tile which was previously left out. It consists of 1 / 4 of a tile and does not need interpolation. /// - /// The source image. - /// The output pixels. + /// The output pixels base reference. /// The lookup table to remap the grey values. + /// The source image width. /// The x-position in the CDF lookup map. /// The y-position in the CDF lookup map. /// X start position. @@ -160,9 +169,9 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source /// or 65536 for 16-bit grayscale images. /// private static void ProcessCornerTile( - ImageFrame source, - Span pixels, + ref TPixel pixelsBase, CdfTileData cdfData, + int sourceWidth, int cdfX, int cdfY, int xStart, @@ -173,10 +182,12 @@ private static void ProcessCornerTile( { for (int dy = yStart; dy < yEnd; dy++) { + int dyOffSet = dy * sourceWidth; for (int dx = xStart; dx < xEnd; dx++) { - 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)); + ref TPixel pixel = ref Unsafe.Add(ref pixelsBase, dyOffSet + dx); + float luminanceEqualized = cdfData.RemapGreyValue(cdfX, cdfY, GetLuminance(pixel, luminanceLevels)); + pixel.FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W)); } } } @@ -184,10 +195,11 @@ private static void ProcessCornerTile( /// /// Processes a border column of the image which is half the size of the tile width. /// - /// The source image. - /// The output pixels. + /// The output pixels reference. /// The pre-computed lookup tables to remap the grey values for each tiles. /// The X index of the lookup table to use. + /// The source image width. + /// The source image height. /// The width of a tile. /// The height of a tile. /// X start position in the image. @@ -197,10 +209,11 @@ private static void ProcessCornerTile( /// or 65536 for 16-bit grayscale images. /// private static void ProcessBorderColumn( - ImageFrame source, - Span pixels, + ref TPixel pixelBase, CdfTileData cdfData, int cdfX, + int sourceWidth, + int sourceHeight, int tileWidth, int tileHeight, int xStart, @@ -211,17 +224,19 @@ private static void ProcessBorderColumn( int halfTileHeight = tileHeight / 2; int cdfY = 0; - for (int y = halfTileHeight; y < source.Height - halfTileHeight; y += tileHeight) + for (int y = halfTileHeight; y < sourceHeight - halfTileHeight; y += tileHeight) { - int yLimit = Math.Min(y + tileHeight, source.Height - 1); + int yLimit = Math.Min(y + tileHeight, sourceHeight - 1); int tileY = 0; for (int dy = y; dy < yLimit; dy++) { + int dyOffSet = dy * sourceWidth; int tileX = halfTileWidth; for (int dx = xStart; dx < xEnd; dx++) { - 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)); + ref TPixel pixel = ref Unsafe.Add(ref pixelBase, dyOffSet + dx); + float luminanceEqualized = InterpolateBetweenTwoTiles(pixel, cdfData, cdfX, cdfY, cdfX, cdfY + 1, tileY, tileHeight, luminanceLevels); + pixel.FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W)); tileX++; } @@ -235,10 +250,10 @@ private static void ProcessBorderColumn( /// /// Processes a border row of the image which is half of the size of the tile height. /// - /// The source image. - /// The output pixels. + /// The output pixels base reference. /// The pre-computed lookup tables to remap the grey values for each tiles. /// The Y index of the lookup table to use. + /// The source image width. /// The width of a tile. /// The height of a tile. /// Y start position in the image. @@ -248,10 +263,10 @@ private static void ProcessBorderColumn( /// or 65536 for 16-bit grayscale images. /// private static void ProcessBorderRow( - ImageFrame source, - Span pixels, + ref TPixel pixelBase, CdfTileData cdfData, int cdfY, + int sourceWidth, int tileWidth, int tileHeight, int yStart, @@ -262,17 +277,19 @@ private static void ProcessBorderRow( int halfTileHeight = tileHeight / 2; int cdfX = 0; - for (int x = halfTileWidth; x < source.Width - halfTileWidth; x += tileWidth) + for (int x = halfTileWidth; x < sourceWidth - halfTileWidth; x += tileWidth) { int tileY = 0; for (int dy = yStart; dy < yEnd; dy++) { + int dyOffSet = dy * sourceWidth; int tileX = 0; - int xLimit = Math.Min(x + tileWidth, source.Width - 1); + int xLimit = Math.Min(x + tileWidth, sourceWidth - 1); for (int dx = x; dx < xLimit; dx++) { - 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)); + ref TPixel pixel = ref Unsafe.Add(ref pixelBase, dyOffSet + dx); + float luminanceEqualized = InterpolateBetweenTwoTiles(pixel, cdfData, cdfX, cdfY, cdfX + 1, cdfY, tileX, tileWidth, luminanceLevels); + pixel.FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W)); tileX++; } @@ -396,10 +413,16 @@ private static float InterpolateBetweenTwoTiles( private sealed class CdfTileData : IDisposable { private readonly Configuration configuration; + private readonly MemoryAllocator memoryAllocator; + + // Used for storing the minimum value for each CDF entry. private readonly Buffer2D cdfMinBuffer2D; + + // Used for storing the LUT for each CDF entry. private readonly Buffer2D cdfLutBuffer2D; - private readonly Buffer2D histogramBuffer2D; private readonly int pixelsInTile; + private readonly int sourceWidth; + private readonly int sourceHeight; private readonly int tileWidth; private readonly int tileHeight; private readonly int luminanceLevels; @@ -407,6 +430,7 @@ private sealed class CdfTileData : IDisposable public CdfTileData( Configuration configuration, + int sourceWidth, int sourceHeight, int tileCountX, int tileCountY, @@ -415,10 +439,12 @@ public CdfTileData( int luminanceLevels) { this.configuration = configuration; - MemoryAllocator memoryAllocator = configuration.MemoryAllocator; + this.memoryAllocator = configuration.MemoryAllocator; this.luminanceLevels = luminanceLevels; - this.cdfMinBuffer2D = memoryAllocator.Allocate2D(tileCountX, tileCountY); - this.cdfLutBuffer2D = memoryAllocator.Allocate2D(tileCountX * luminanceLevels, tileCountY); + this.cdfMinBuffer2D = this.memoryAllocator.Allocate2D(tileCountX, tileCountY); + this.cdfLutBuffer2D = this.memoryAllocator.Allocate2D(tileCountX * luminanceLevels, tileCountY); + this.sourceWidth = sourceWidth; + this.sourceHeight = sourceHeight; this.tileWidth = tileWidth; this.tileHeight = tileHeight; this.pixelsInTile = tileWidth * tileHeight; @@ -431,51 +457,59 @@ public CdfTileData( this.tileYStartPositions.Add((y, cdfY)); cdfY++; } - - // Use 2D to avoid rent/return per iteration. - this.histogramBuffer2D = memoryAllocator.Allocate2D(luminanceLevels, this.tileYStartPositions.Count); } public void CalculateLookupTables(ImageFrame source, HistogramEqualizationProcessor processor) { + int sourceWidth = this.sourceWidth; + int sourceHeight = this.sourceHeight; + int tileWidth = this.tileWidth; + int tileHeight = this.tileHeight; + int luminanceLevels = this.luminanceLevels; + MemoryAllocator memoryAllocator = this.memoryAllocator; + Parallel.For( 0, this.tileYStartPositions.Count, new ParallelOptions() { MaxDegreeOfParallelism = this.configuration.MaxDegreeOfParallelism }, index => { - Span histogram = this.histogramBuffer2D.GetRowSpan(index); - int cdfX = 0; int cdfY = this.tileYStartPositions[index].cdfY; int y = this.tileYStartPositions[index].y; - int endY = Math.Min(y + this.tileHeight, source.Height); + int endY = Math.Min(y + tileHeight, sourceHeight); + ref TPixel sourceBase = ref source.GetPixelReference(0, 0); + ref int cdfMinBase = ref MemoryMarshal.GetReference(this.cdfMinBuffer2D.GetRowSpan(cdfY)); - for (int x = 0; x < source.Width; x += this.tileWidth) + using (IMemoryOwner histogramBuffer = this.memoryAllocator.Allocate(luminanceLevels)) { - histogram.Clear(); - Span cdf = this.GetCdfLutSpan(cdfX, index); + Span histogram = histogramBuffer.GetSpan(); - int xlimit = Math.Min(x + this.tileWidth, source.Width); - for (int dy = y; dy < endY; dy++) + for (int x = 0; x < sourceWidth; x += tileWidth) { - Span sourceRowSpan = source.GetPixelRowSpan(dy); + histogram.Clear(); + Span cdf = this.GetCdfLutSpan(cdfX, index); - for (int dx = x; dx < xlimit; dx++) + int xlimit = Math.Min(x + tileWidth, sourceWidth); + for (int dy = y; dy < endY; dy++) { - int luminace = GetLuminance(sourceRowSpan[dx], this.luminanceLevels); - histogram[luminace]++; + int dyOffset = dy * sourceWidth; + for (int dx = x; dx < xlimit; dx++) + { + int luminace = GetLuminance(Unsafe.Add(ref sourceBase, dyOffset + dx), luminanceLevels); + histogram[luminace]++; + } } - } - if (processor.ClipHistogramEnabled) - { - processor.ClipHistogram(histogram, processor.ClipLimitPercentage, this.pixelsInTile); - } + if (processor.ClipHistogramEnabled) + { + processor.ClipHistogram(histogram, processor.ClipLimitPercentage, this.pixelsInTile); + } - this.cdfMinBuffer2D[cdfX, cdfY] = processor.CalculateCdf(cdf, histogram, histogram.Length - 1); + Unsafe.Add(ref cdfMinBase, cdfX) = processor.CalculateCdf(cdf, histogram, histogram.Length - 1); - cdfX++; + cdfX++; + } } }); } @@ -503,7 +537,6 @@ public float RemapGreyValue(int tilesX, int tilesY, int luminance) public void Dispose() { this.cdfMinBuffer2D.Dispose(); - this.histogramBuffer2D.Dispose(); this.cdfLutBuffer2D.Dispose(); } } From 609cb673cce9639ac6dcae621488c7b0a5598f52 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 23 Jan 2019 23:23:35 +1100 Subject: [PATCH 38/50] Begin optimizing the global histogram --- .../AdaptiveHistEqualizationProcessor.cs | 5 ++-- .../AdaptiveHistEqualizationSWProcessor.cs | 27 ++++++++++++------- .../GlobalHistogramEqualizationProcessor.cs | 11 +++++--- .../HistogramEqualizationProcessor.cs | 8 +++--- .../HistogramEqualizationTests.cs | 8 +++--- 5 files changed, 36 insertions(+), 23 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs index f03c3ff9b2..5092b85ff4 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs @@ -484,11 +484,12 @@ public void CalculateLookupTables(ImageFrame source, HistogramEqualizati using (IMemoryOwner histogramBuffer = this.memoryAllocator.Allocate(luminanceLevels)) { Span histogram = histogramBuffer.GetSpan(); + ref int histogramBase = ref MemoryMarshal.GetReference(histogram); for (int x = 0; x < sourceWidth; x += tileWidth) { histogram.Clear(); - Span cdf = this.GetCdfLutSpan(cdfX, index); + ref int cdfBase = ref MemoryMarshal.GetReference(this.GetCdfLutSpan(cdfX, index)); int xlimit = Math.Min(x + tileWidth, sourceWidth); for (int dy = y; dy < endY; dy++) @@ -506,7 +507,7 @@ public void CalculateLookupTables(ImageFrame source, HistogramEqualizati processor.ClipHistogram(histogram, processor.ClipLimitPercentage, this.pixelsInTile); } - Unsafe.Add(ref cdfMinBase, cdfX) = processor.CalculateCdf(cdf, histogram, histogram.Length - 1); + Unsafe.Add(ref cdfMinBase, cdfX) = processor.CalculateCdf(ref cdfBase, ref histogramBase, histogram.Length - 1); cdfX++; } diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs index 2c24d79129..8c86e58c73 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs @@ -2,7 +2,10 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Buffers; using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; @@ -59,21 +62,24 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source parallelOptions, x => { - using (System.Buffers.IMemoryOwner histogramBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean)) - using (System.Buffers.IMemoryOwner histogramBufferCopy = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean)) - using (System.Buffers.IMemoryOwner cdfBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean)) - using (System.Buffers.IMemoryOwner pixelRowBuffer = memoryAllocator.Allocate(tileWidth, AllocationOptions.Clean)) + 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(tileWidth, AllocationOptions.Clean)) { Span histogram = histogramBuffer.GetSpan(); + ref int histogramBase = ref MemoryMarshal.GetReference(histogram); Span histogramCopy = histogramBufferCopy.GetSpan(); - Span cdf = cdfBuffer.GetSpan(); + ref int histogramCopyBase = ref MemoryMarshal.GetReference(histogramCopy); + ref int cdfBase = ref MemoryMarshal.GetReference(cdfBuffer.GetSpan()); + Span pixelRow = pixelRowBuffer.GetSpan(); int maxHistIdx = 0; // Build the histogram of grayscale values for the current tile. for (int dy = -halfTileWith; dy < halfTileWith; dy++) { - Span rowSpan = this.GetPixelRow(source, pixelRow, (int)x - halfTileWith, dy, tileWidth); + Span rowSpan = this.GetPixelRow(source, pixelRow, x - halfTileWith, dy, tileWidth); int maxIdx = this.AddPixelsToHistogram(rowSpan, histogram, this.LuminanceLevels); if (maxIdx > maxHistIdx) { @@ -91,12 +97,15 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source } // 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(cdf, histogramCopy, maxHistIdx) : this.CalculateCdf(cdf, histogram, maxHistIdx); + int cdfMin = this.ClipHistogramEnabled + ? this.CalculateCdf(ref cdfBase, ref histogramCopyBase, maxHistIdx) + : this.CalculateCdf(ref cdfBase, ref histogramBase, maxHistIdx); + float numberOfPixelsMinusCdfMin = pixeInTile - cdfMin; // Map the current pixel to the new equalized value int luminance = GetLuminance(source[x, y], this.LuminanceLevels); - float luminanceEqualized = cdf[luminance] / numberOfPixelsMinusCdfMin; + 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. @@ -218,7 +227,7 @@ private int RemovePixelsFromHistogram(Span greyValues, Span histogr // If the histogram at the maximum index has changed to 0, search for the next smaller value. if (luminance == maxHistIdx && histogram[luminance] == 0) { - for (int j = luminance; j >= 0; j--) + for (int j = luminance; j >= 0; j--) { maxHistIdx = j; if (histogram[j] != 0) diff --git a/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs index daa045afb3..cbd3c6174c 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs @@ -2,7 +2,9 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Buffers; using System.Numerics; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -37,11 +39,13 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source int numberOfPixels = source.Width * source.Height; Span pixels = source.GetPixelSpan(); - using (System.Buffers.IMemoryOwner histogramBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean)) - using (System.Buffers.IMemoryOwner cdfBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean)) + using (IMemoryOwner histogramBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean)) + using (IMemoryOwner cdfBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean)) { // Build the histogram of the grayscale levels. Span histogram = histogramBuffer.GetSpan(); + ref int histogramBase = ref MemoryMarshal.GetReference(histogram); + for (int i = 0; i < pixels.Length; i++) { TPixel sourcePixel = pixels[i]; @@ -56,7 +60,8 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source // Calculate the cumulative distribution function, which will map each input pixel to a new value. Span cdf = cdfBuffer.GetSpan(); - int cdfMin = this.CalculateCdf(cdf, histogram, histogram.Length - 1); + ref int cdfBase = ref MemoryMarshal.GetReference(cdf); + int cdfMin = this.CalculateCdf(ref cdfBase, ref histogramBase, histogram.Length - 1); // Apply the cdf to each pixel of the image float numberOfPixelsMinusCdfMin = numberOfPixels - cdfMin; diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs index 9290d941cc..23bdbf2a36 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs @@ -53,17 +53,15 @@ protected HistogramEqualizationProcessor(int luminanceLevels, bool clipHistogram /// /// Calculates the cumulative distribution function. /// - /// The array holding the cdf. - /// The histogram of the input image. + /// The reference to the array holding the cdf. + /// The reference to the histogram of the input image. /// Index of the maximum of the histogram. /// The first none zero value of the cdf. - public int CalculateCdf(Span cdf, Span histogram, int maxIdx) + public int CalculateCdf(ref int cdfBase, ref int histogramBase, 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++) { diff --git a/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs b/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs index 41c399bd5b..84d592bd96 100644 --- a/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs +++ b/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs @@ -37,7 +37,7 @@ public void HistogramEqualizationTest(int luminanceLevels) { for (int x = 0; x < 8; x++) { - byte luminance = pixels[y * 8 + x]; + byte luminance = pixels[(y * 8) + x]; image[x, y] = new Rgba32(luminance, luminance, luminance); } } @@ -66,9 +66,9 @@ public void HistogramEqualizationTest(int luminanceLevels) 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); + Assert.Equal(expected[(y * 8) + x], actual.R); + Assert.Equal(expected[(y * 8) + x], actual.G); + Assert.Equal(expected[(y * 8) + x], actual.B); } } } From f2930c75d3f662dd10ad12e00b4b1ae95d43a105 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 24 Jan 2019 10:27:24 +1100 Subject: [PATCH 39/50] Parallelize GlobalHistogramEqualizationProcessor --- .../GlobalHistogramEqualizationProcessor.cs | 68 +++++++++++++------ 1 file changed, 47 insertions(+), 21 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs index cbd3c6174c..aadde2424b 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs @@ -4,9 +4,11 @@ using System; using System.Buffers; using System.Numerics; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.ParallelUtils; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Memory; using SixLabors.Primitives; @@ -23,8 +25,10 @@ internal class GlobalHistogramEqualizationProcessor : HistogramEqualizat /// /// Initializes a new instance of the class. /// - /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images - /// or 65536 for 16-bit grayscale images. + /// + /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images + /// or 65536 for 16-bit grayscale images. + /// /// Indicating whether to clip the histogram bins at a specific value. /// Histogram clip limit in percent of the total pixels. Histogram bins which exceed this limit, will be capped at this value. public GlobalHistogramEqualizationProcessor(int luminanceLevels, bool clipHistogram, float clipLimitPercentage) @@ -38,42 +42,64 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source MemoryAllocator memoryAllocator = configuration.MemoryAllocator; int numberOfPixels = source.Width * source.Height; Span pixels = source.GetPixelSpan(); + var workingRect = new Rectangle(0, 0, source.Width, source.Height); using (IMemoryOwner histogramBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean)) using (IMemoryOwner cdfBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean)) { // Build the histogram of the grayscale levels. - Span histogram = histogramBuffer.GetSpan(); - ref int histogramBase = ref MemoryMarshal.GetReference(histogram); + ParallelHelper.IterateRows( + workingRect, + configuration, + rows => + { + ref int histogramBase = ref MemoryMarshal.GetReference(histogramBuffer.GetSpan()); + for (int y = rows.Min; y < rows.Max; y++) + { + ref TPixel pixelBase = ref MemoryMarshal.GetReference(source.GetPixelRowSpan(y)); - for (int i = 0; i < pixels.Length; i++) - { - TPixel sourcePixel = pixels[i]; - int luminance = GetLuminance(sourcePixel, this.LuminanceLevels); - histogram[luminance]++; - } + for (int x = 0; x < workingRect.Width; x++) + { + int luminance = GetLuminance(Unsafe.Add(ref pixelBase, x), this.LuminanceLevels); + Unsafe.Add(ref histogramBase, luminance)++; + } + } + }); + Span histogram = histogramBuffer.GetSpan(); if (this.ClipHistogramEnabled) { this.ClipHistogram(histogram, this.ClipLimitPercentage, numberOfPixels); } // Calculate the cumulative distribution function, which will map each input pixel to a new value. - Span cdf = cdfBuffer.GetSpan(); - ref int cdfBase = ref MemoryMarshal.GetReference(cdf); - int cdfMin = this.CalculateCdf(ref cdfBase, ref histogramBase, histogram.Length - 1); + int cdfMin = this.CalculateCdf( + ref MemoryMarshal.GetReference(cdfBuffer.GetSpan()), + ref MemoryMarshal.GetReference(histogram), + histogram.Length - 1); - // Apply the cdf to each pixel of the image float numberOfPixelsMinusCdfMin = numberOfPixels - cdfMin; - for (int i = 0; i < pixels.Length; i++) - { - TPixel sourcePixel = pixels[i]; - int luminance = GetLuminance(sourcePixel, this.LuminanceLevels); - float luminanceEqualized = cdf[luminance] / numberOfPixelsMinusCdfMin; + // Apply the cdf to each pixel of the image + ParallelHelper.IterateRows( + workingRect, + configuration, + rows => + { + ref int cdfBase = ref MemoryMarshal.GetReference(cdfBuffer.GetSpan()); + for (int y = rows.Min; y < rows.Max; y++) + { + ref TPixel pixelBase = ref MemoryMarshal.GetReference(source.GetPixelRowSpan(y)); - pixels[i].FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, sourcePixel.ToVector4().W)); - } + for (int x = 0; x < workingRect.Width; x++) + { + ref TPixel pixel = ref Unsafe.Add(ref pixelBase, x); + int luminance = GetLuminance(pixel, this.LuminanceLevels); + float luminanceEqualized = Unsafe.Add(ref cdfBase, luminance) / numberOfPixelsMinusCdfMin; + pixel.FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W)); + } + } + }); } } } From 8f19e5edd23f13fd1ddf93b4e795f82e7f1334bb Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 25 Jan 2019 18:29:49 +0100 Subject: [PATCH 40/50] Moving sliding window from left to right instead of from top to bottom --- .../AdaptiveHistEqualizationSWProcessor.cs | 95 ++++++++++--------- 1 file changed, 49 insertions(+), 46 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs index 8c86e58c73..0e2f29b80d 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs @@ -51,21 +51,22 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source Span pixels = source.GetPixelSpan(); var parallelOptions = new ParallelOptions() { MaxDegreeOfParallelism = configuration.MaxDegreeOfParallelism }; - int tileWidth = source.Width / this.Tiles; - int pixeInTile = tileWidth * tileWidth; - int halfTileWith = tileWidth / 2; + int tileHeight = source.Height / this.Tiles; + int pixeInTile = tileHeight * tileHeight; + int halfTileHeight = tileHeight / 2; + int halfTileWidth = halfTileHeight; using (Buffer2D targetPixels = configuration.MemoryAllocator.Allocate2D(source.Width, source.Height)) { Parallel.For( 0, - source.Width, + source.Height, parallelOptions, - x => + y => { 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(tileWidth, AllocationOptions.Clean)) + using (IMemoryOwner pixelColumnBuffer = memoryAllocator.Allocate(tileHeight, AllocationOptions.Clean)) { Span histogram = histogramBuffer.GetSpan(); ref int histogramBase = ref MemoryMarshal.GetReference(histogram); @@ -73,21 +74,21 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source ref int histogramCopyBase = ref MemoryMarshal.GetReference(histogramCopy); ref int cdfBase = ref MemoryMarshal.GetReference(cdfBuffer.GetSpan()); - Span pixelRow = pixelRowBuffer.GetSpan(); + Span pixelColumn = pixelColumnBuffer.GetSpan(); int maxHistIdx = 0; // Build the histogram of grayscale values for the current tile. - for (int dy = -halfTileWith; dy < halfTileWith; dy++) + for (int dx = -halfTileWidth; dx < halfTileWidth; dx++) { - Span rowSpan = this.GetPixelRow(source, pixelRow, x - halfTileWith, dy, tileWidth); - int maxIdx = this.AddPixelsToHistogram(rowSpan, histogram, this.LuminanceLevels); + Span columnSpan = this.GetPixelColumn(source, pixelColumn, dx, y - halfTileHeight, tileHeight); + int maxIdx = this.AddPixelsToHistogram(columnSpan, histogram, this.LuminanceLevels); if (maxIdx > maxHistIdx) { maxHistIdx = maxIdx; } } - for (int y = 0; y < source.Height; y++) + for (int x = 0; x < source.Width; x++) { if (this.ClipHistogramEnabled) { @@ -103,18 +104,18 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source float numberOfPixelsMinusCdfMin = pixeInTile - cdfMin; - // Map the current pixel to the new equalized value + // 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. - Span rowSpan = this.GetPixelRow(source, pixelRow, x - halfTileWith, y - halfTileWith, tileWidth); - maxHistIdx = this.RemovePixelsFromHistogram(rowSpan, histogram, this.LuminanceLevels, maxHistIdx); + // Remove left most column from the histogram, mirroring columns which exceeds the borders of the image. + Span columnSpan = this.GetPixelColumn(source, pixelColumn, x - halfTileWidth, y - halfTileHeight, tileHeight); + maxHistIdx = this.RemovePixelsFromHistogram(columnSpan, histogram, this.LuminanceLevels, maxHistIdx); - // Add new bottom row to the histogram, mirroring rows which exceeds the borders. - rowSpan = this.GetPixelRow(source, pixelRow, x - halfTileWith, y + halfTileWith, tileWidth); - int maxIdx = this.AddPixelsToHistogram(rowSpan, histogram, this.LuminanceLevels); + // Add new right column to the histogram, mirroring columns which exceeds the borders of the image. + columnSpan = this.GetPixelColumn(source, pixelColumn, x + halfTileWidth, y - halfTileHeight, tileHeight); + int maxIdx = this.AddPixelsToHistogram(columnSpan, histogram, this.LuminanceLevels); if (maxIdx > maxHistIdx) { maxHistIdx = maxIdx; @@ -128,62 +129,64 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source } /// - /// Get the a pixel row at a given position with a length of the tile width. Mirrors pixels which exceeds the edges. + /// Get the a pixel column at a given position with the size of the tile height. Mirrors pixels which exceeds the edges of the image. /// /// The source image. - /// Pre-allocated pixel row span of the size of a tile width. + /// Pre-allocated pixel row span of the size of a tile height. /// The x position. /// The y position. - /// The width in pixels of a tile. + /// The height in pixels of a tile. /// A pixel row of the length of the tile width. - private Span GetPixelRow(ImageFrame source, Span rowPixels, int x, int y, int tileWidth) + private Span GetPixelColumn(ImageFrame source, Span columnPixels, int x, int y, int tileHeight) { - if (y < 0) + if (x < 0) { - y = Math.Abs(y); + x = Math.Abs(x); } - else if (y >= source.Height) + else if (x >= source.Width) { - int diff = y - source.Height; - y = source.Height - diff - 1; + int diff = x - source.Width; + x = source.Width - diff - 1; } - // Special cases for the left and the right border where GetPixelRowSpan can not be used - if (x < 0) + int idx = 0; + columnPixels.Clear(); + if (y < 0) { - rowPixels.Clear(); - int idx = 0; - for (int dx = x; dx < x + tileWidth; dx++) + columnPixels.Clear(); + for (int dy = y; dy < y + tileHeight; dy++) { - rowPixels[idx] = source[Math.Abs(dx), y]; + columnPixels[idx] = source[x, Math.Abs(dy)]; idx++; } - - return rowPixels; } - else if (x + tileWidth > source.Width) + else if (y + tileHeight > source.Height) { - rowPixels.Clear(); - int idx = 0; - for (int dx = x; dx < x + tileWidth; dx++) + for (int dy = y; dy < y + tileHeight; dy++) { - if (dx >= source.Width) + if (dy >= source.Height) { - int diff = dx - source.Width; - rowPixels[idx] = source[dx - diff - 1, y]; + int diff = dy - source.Height; + columnPixels[idx] = source[x, dy - diff - 1]; } else { - rowPixels[idx] = source[dx, y]; + columnPixels[idx] = source[x, dy]; } idx++; } - - return rowPixels; + } + else + { + for (int dy = y; dy < y + tileHeight; dy++) + { + columnPixels[idx] = source[x, dy]; + idx++; + } } - return source.GetPixelRowSpan(y).Slice(start: x, length: tileWidth); + return columnPixels; } /// From 73af28629ac027967d17b759ccc157877cfcb9d4 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 27 Jan 2019 12:32:02 +0100 Subject: [PATCH 41/50] The tile width and height is again depended on the image width: image.Width / Tiles --- .../AdaptiveHistEqualizationSWProcessor.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs index 0e2f29b80d..bce497e3d6 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs @@ -51,8 +51,9 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source Span pixels = source.GetPixelSpan(); var parallelOptions = new ParallelOptions() { MaxDegreeOfParallelism = configuration.MaxDegreeOfParallelism }; - int tileHeight = source.Height / this.Tiles; - int pixeInTile = tileHeight * tileHeight; + int tileWidth = source.Width / this.Tiles; + int tileHeight = tileWidth; + int pixeInTile = tileWidth * tileHeight; int halfTileHeight = tileHeight / 2; int halfTileWidth = halfTileHeight; using (Buffer2D targetPixels = configuration.MemoryAllocator.Allocate2D(source.Width, source.Height)) @@ -132,7 +133,7 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source /// Get the a pixel column at a given position with the size of the tile height. Mirrors pixels which exceeds the edges of the image. /// /// The source image. - /// Pre-allocated pixel row span of the size of a tile height. + /// Pre-allocated pixel span of the size of a tile height. /// The x position. /// The y position. /// The height in pixels of a tile. @@ -190,7 +191,7 @@ private Span GetPixelColumn(ImageFrame source, Span colu } /// - /// Adds a row of grey values to the histogram. + /// Adds a column of grey values to the histogram. /// /// The grey values to add. /// The histogram. @@ -213,7 +214,7 @@ private int AddPixelsToHistogram(Span greyValues, Span histogram, i } /// - /// Removes a row of grey values from the histogram. + /// Removes a column of grey values from the histogram. /// /// The grey values to remove. /// The histogram. From 755116e700c7964757bd657d74108ff778bd4af3 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 30 Jan 2019 20:03:48 +0100 Subject: [PATCH 42/50] Removed keeping track of the maximum histogram position --- .../AdaptiveHistEqualizationSWProcessor.cs | 50 +++---------------- .../HistogramEqualizationProcessor.cs | 2 +- 2 files changed, 9 insertions(+), 43 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs index bce497e3d6..c9bae6eb41 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs @@ -76,17 +76,12 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source ref int cdfBase = ref MemoryMarshal.GetReference(cdfBuffer.GetSpan()); Span pixelColumn = pixelColumnBuffer.GetSpan(); - int maxHistIdx = 0; // Build the histogram of grayscale values for the current tile. for (int dx = -halfTileWidth; dx < halfTileWidth; dx++) { Span columnSpan = this.GetPixelColumn(source, pixelColumn, dx, y - halfTileHeight, tileHeight); - int maxIdx = this.AddPixelsToHistogram(columnSpan, histogram, this.LuminanceLevels); - if (maxIdx > maxHistIdx) - { - maxHistIdx = maxIdx; - } + this.AddPixelsToHistogram(columnSpan, histogram, this.LuminanceLevels); } for (int x = 0; x < source.Width; x++) @@ -94,14 +89,14 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source if (this.ClipHistogramEnabled) { // Clipping the histogram, but doing it on a copy to keep the original un-clipped values for the next iteration. - histogram.Slice(0, maxHistIdx).CopyTo(histogramCopy); + histogram.CopyTo(histogramCopy); this.ClipHistogram(histogramCopy, this.ClipLimitPercentage, pixeInTile); } // Calculate the cumulative distribution function, which will map each input pixel in the current tile to a new value. int cdfMin = this.ClipHistogramEnabled - ? this.CalculateCdf(ref cdfBase, ref histogramCopyBase, maxHistIdx) - : this.CalculateCdf(ref cdfBase, ref histogramBase, maxHistIdx); + ? this.CalculateCdf(ref cdfBase, ref histogramCopyBase, histogram.Length - 1) + : this.CalculateCdf(ref cdfBase, ref histogramBase, histogram.Length - 1); float numberOfPixelsMinusCdfMin = pixeInTile - cdfMin; @@ -112,15 +107,11 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source // Remove left most column from the histogram, mirroring columns which exceeds the borders of the image. Span columnSpan = this.GetPixelColumn(source, pixelColumn, x - halfTileWidth, y - halfTileHeight, tileHeight); - maxHistIdx = this.RemovePixelsFromHistogram(columnSpan, histogram, this.LuminanceLevels, maxHistIdx); + this.RemovePixelsFromHistogram(columnSpan, histogram, this.LuminanceLevels); // Add new right column to the histogram, mirroring columns which exceeds the borders of the image. columnSpan = this.GetPixelColumn(source, pixelColumn, x + halfTileWidth, y - halfTileHeight, tileHeight); - int maxIdx = this.AddPixelsToHistogram(columnSpan, histogram, this.LuminanceLevels); - if (maxIdx > maxHistIdx) - { - maxHistIdx = maxIdx; - } + this.AddPixelsToHistogram(columnSpan, histogram, this.LuminanceLevels); } } }); @@ -196,21 +187,13 @@ private Span GetPixelColumn(ImageFrame source, Span colu /// The grey values to add. /// The histogram. /// The number of different luminance levels. - /// The maximum index where a value was changed. - private int AddPixelsToHistogram(Span greyValues, Span histogram, int luminanceLevels) + private void AddPixelsToHistogram(Span greyValues, Span histogram, int luminanceLevels) { - int maxIdx = 0; for (int idx = 0; idx < greyValues.Length; idx++) { int luminance = GetLuminance(greyValues[idx], luminanceLevels); histogram[luminance]++; - if (luminance > maxIdx) - { - maxIdx = luminance; - } } - - return maxIdx; } /// @@ -219,30 +202,13 @@ private int AddPixelsToHistogram(Span greyValues, Span histogram, i /// The grey values to remove. /// The histogram. /// The number of different luminance levels. - /// The current maximum index of the histogram. - /// The (maybe changed) maximum index of the histogram. - private int RemovePixelsFromHistogram(Span greyValues, Span histogram, int luminanceLevels, int maxHistIdx) + private void RemovePixelsFromHistogram(Span greyValues, Span histogram, int luminanceLevels) { for (int idx = 0; idx < greyValues.Length; idx++) { 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. - if (luminance == maxHistIdx && histogram[luminance] == 0) - { - for (int j = luminance; j >= 0; j--) - { - maxHistIdx = j; - if (histogram[j] != 0) - { - break; - } - } - } } - - return maxHistIdx; } } } diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs index 23bdbf2a36..01e1cecae1 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs @@ -72,7 +72,7 @@ public int CalculateCdf(ref int cdfBase, ref int histogramBase, int maxIdx) cdfMinFound = true; } - // 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. Unsafe.Add(ref cdfBase, i) = Math.Max(0, histSum - cdfMin); } From a7338c46f101af81dee083427941c2bb89cc64f0 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 31 Jan 2019 19:15:04 +0100 Subject: [PATCH 43/50] Updated reference image for sliding window AHE for moving the sliding window from left to right --- tests/Images/External | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Images/External b/tests/Images/External index 74995302e3..f841afa270 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit 74995302e3c9a5913f1cf09d71b15f888d0ec022 +Subproject commit f841afa27004f9f8206a3a6629f04b7bf89d9650 From 69b3c70073010f535871aa7d69ffbec6b37e00a5 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 1 Feb 2019 20:25:16 +0100 Subject: [PATCH 44/50] Removed unnecessary call to Span.Clear(), all values are overwritten anyway --- .../Normalization/AdaptiveHistEqualizationSWProcessor.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs index c9bae6eb41..9e4cc9a581 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs @@ -142,10 +142,8 @@ private Span GetPixelColumn(ImageFrame source, Span colu } int idx = 0; - columnPixels.Clear(); if (y < 0) { - columnPixels.Clear(); for (int dy = y; dy < y + tileHeight; dy++) { columnPixels[idx] = source[x, Math.Abs(dy)]; From 95b842790fda608810578ddbefe351a04b541118 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 13 Mar 2019 20:51:59 +0100 Subject: [PATCH 45/50] Revert "Moving sliding window from left to right instead of from top to bottom" This reverts commit 8f19e5edd23f13fd1ddf93b4e795f82e7f1334bb. # Conflicts: # src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs --- .../AdaptiveHistEqualizationSWProcessor.cs | 87 +++++++++---------- 1 file changed, 43 insertions(+), 44 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs index 9e4cc9a581..286ff56cbf 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs @@ -60,31 +60,30 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source { Parallel.For( 0, - source.Height, + source.Width, parallelOptions, - y => + 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 pixelColumnBuffer = memoryAllocator.Allocate(tileHeight, AllocationOptions.Clean)) + using (IMemoryOwner pixelRowBuffer = memoryAllocator.Allocate(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 pixelColumn = pixelColumnBuffer.GetSpan(); + Span pixelRow = pixelRowBuffer.GetSpan(); // Build the histogram of grayscale values for the current tile. - for (int dx = -halfTileWidth; dx < halfTileWidth; dx++) + for (int dy = -halfTileHeight; dy < halfTileHeight; dy++) { - Span columnSpan = this.GetPixelColumn(source, pixelColumn, dx, y - halfTileHeight, tileHeight); - this.AddPixelsToHistogram(columnSpan, histogram, this.LuminanceLevels); + Span rowSpan = this.GetPixelRow(source, pixelRow, x - halfTileWidth, dy, tileWidth); + this.AddPixelsToHistogram(rowSpan, histogram, this.LuminanceLevels); } - for (int x = 0; x < source.Width; x++) + for (int y = 0; y < source.Height; y++) { if (this.ClipHistogramEnabled) { @@ -100,18 +99,18 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source float numberOfPixelsMinusCdfMin = pixeInTile - cdfMin; - // Map the current pixel to the new equalized value. + // 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 left most column from the histogram, mirroring columns which exceeds the borders of the image. - Span columnSpan = this.GetPixelColumn(source, pixelColumn, x - halfTileWidth, y - halfTileHeight, tileHeight); - this.RemovePixelsFromHistogram(columnSpan, histogram, this.LuminanceLevels); + // Remove top most row from the histogram, mirroring rows which exceeds the borders. + Span rowSpan = this.GetPixelRow(source, pixelRow, x - halfTileWidth, y - halfTileWidth, tileWidth); + this.RemovePixelsFromHistogram(rowSpan, histogram, this.LuminanceLevels); - // Add new right column to the histogram, mirroring columns which exceeds the borders of the image. - columnSpan = this.GetPixelColumn(source, pixelColumn, x + halfTileWidth, y - halfTileHeight, tileHeight); - this.AddPixelsToHistogram(columnSpan, histogram, this.LuminanceLevels); + // Add new bottom row to the histogram, mirroring rows which exceeds the borders. + rowSpan = this.GetPixelRow(source, pixelRow, x - halfTileWidth, y + halfTileWidth, tileWidth); + this.AddPixelsToHistogram(rowSpan, histogram, this.LuminanceLevels); } } }); @@ -121,62 +120,62 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source } /// - /// Get the a pixel column at a given position with the size of the tile height. Mirrors pixels which exceeds the edges of the image. + /// Get the a pixel row at a given position with a length of the tile width. Mirrors pixels which exceeds the edges. /// /// The source image. - /// Pre-allocated pixel span of the size of a tile height. + /// Pre-allocated pixel row span of the size of a the tile width. /// The x position. /// The y position. - /// The height in pixels of a tile. + /// The width in pixels of a tile. /// A pixel row of the length of the tile width. - private Span GetPixelColumn(ImageFrame source, Span columnPixels, int x, int y, int tileHeight) + private Span GetPixelRow(ImageFrame source, Span rowPixels, int x, int y, int tileWidth) { - if (x < 0) + if (y < 0) { - x = Math.Abs(x); + y = Math.Abs(y); } - else if (x >= source.Width) + else if (y >= source.Height) { - int diff = x - source.Width; - x = source.Width - diff - 1; + int diff = y - source.Height; + y = source.Height - diff - 1; } - int idx = 0; - if (y < 0) + // Special cases for the left and the right border where GetPixelRowSpan can not be used + if (x < 0) { - for (int dy = y; dy < y + tileHeight; dy++) + rowPixels.Clear(); + int idx = 0; + for (int dx = x; dx < x + tileWidth; dx++) { - columnPixels[idx] = source[x, Math.Abs(dy)]; + rowPixels[idx] = source[Math.Abs(dx), y]; idx++; } + + return rowPixels; } - else if (y + tileHeight > source.Height) + else if (x + tileWidth > source.Width) { - for (int dy = y; dy < y + tileHeight; dy++) + rowPixels.Clear(); + int idx = 0; + for (int dx = x; dx < x + tileWidth; dx++) { - if (dy >= source.Height) + if (dx >= source.Width) { - int diff = dy - source.Height; - columnPixels[idx] = source[x, dy - diff - 1]; + int diff = dx - source.Width; + rowPixels[idx] = source[dx - diff - 1, y]; } else { - columnPixels[idx] = source[x, dy]; + rowPixels[idx] = source[dx, y]; } idx++; } - } - else - { - for (int dy = y; dy < y + tileHeight; dy++) - { - columnPixels[idx] = source[x, dy]; - idx++; - } + + return rowPixels; } - return columnPixels; + return source.GetPixelRowSpan(y).Slice(start: x, length: tileWidth); } /// From 2d2d445972a1759df3d156376a38b24c5cd63ee5 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 17 Apr 2019 19:10:12 +0200 Subject: [PATCH 46/50] Split GetPixelRow in two version: one which mirrors the edges (only needed in the borders of the images) and one which does not --- .../AdaptiveHistEqualizationSWProcessor.cs | 140 +++++++++++------- 1 file changed, 89 insertions(+), 51 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs index 286ff56cbf..ca9a0cb711 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs @@ -58,65 +58,98 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source int halfTileWidth = halfTileHeight; using (Buffer2D targetPixels = configuration.MemoryAllocator.Allocate2D(source.Width, source.Height)) { + Parallel.For( + halfTileWidth, + source.Width - halfTileWidth, + parallelOptions, + this.ProcessSlidingWindow(source, memoryAllocator, targetPixels, tileWidth, tileHeight, pixeInTile, halfTileHeight, halfTileWidth, yStart: halfTileHeight, yEnd: source.Height - halfTileHeight)); + + // Left border Parallel.For( 0, + halfTileWidth, + parallelOptions, + this.ProcessSlidingWindow(source, memoryAllocator, targetPixels, tileWidth, tileHeight, pixeInTile, halfTileHeight, halfTileWidth, yStart: 0, yEnd: source.Height, useFastPath: false)); + + // Right border + Parallel.For( + source.Width - halfTileWidth, source.Width, parallelOptions, - x => + this.ProcessSlidingWindow(source, memoryAllocator, targetPixels, tileWidth, tileHeight, pixeInTile, halfTileHeight, halfTileWidth, yStart: 0, yEnd: source.Height, useFastPath: false)); + + // Top border + Parallel.For( + halfTileWidth, + source.Width - halfTileWidth, + parallelOptions, + this.ProcessSlidingWindow(source, memoryAllocator, targetPixels, tileWidth, tileHeight, pixeInTile, halfTileHeight, halfTileWidth, yStart: 0, yEnd: halfTileHeight, useFastPath: false)); + + // Bottom border + Parallel.For( + halfTileWidth, + source.Width - halfTileWidth, + parallelOptions, + this.ProcessSlidingWindow(source, memoryAllocator, targetPixels, tileWidth, tileHeight, pixeInTile, halfTileHeight, halfTileWidth, yStart: source.Height - halfTileHeight, yEnd: source.Height, useFastPath: false)); + + Buffer2D.SwapOrCopyContent(source.PixelBuffer, targetPixels); + } + } + + private Action ProcessSlidingWindow(ImageFrame source, MemoryAllocator memoryAllocator, Buffer2D targetPixels, int tileWidth, int tileHeight, int pixeInTile, int halfTileHeight, int halfTileWidth, int yStart, int yEnd, bool useFastPath = true) + { + 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(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(); + + // Build the histogram of grayscale values for the current tile. + for (int dy = yStart - halfTileHeight; dy < yStart + halfTileHeight; dy++) + { + Span rowSpan = useFastPath ? this.GetPixelRowFast(source, pixelRow, x - halfTileWidth, dy, tileWidth) : this.GetPixelRow(source, pixelRow, x - halfTileWidth, dy, tileWidth); + this.AddPixelsToHistogram(rowSpan, histogram, this.LuminanceLevels); + } + + for (int y = yStart; y < yEnd; y++) { - 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(tileWidth, AllocationOptions.Clean)) + if (this.ClipHistogramEnabled) { - 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(); - - // Build the histogram of grayscale values for the current tile. - for (int dy = -halfTileHeight; dy < halfTileHeight; dy++) - { - Span rowSpan = this.GetPixelRow(source, pixelRow, x - halfTileWidth, dy, tileWidth); - this.AddPixelsToHistogram(rowSpan, histogram, this.LuminanceLevels); - } - - for (int y = 0; y < source.Height; y++) - { - if (this.ClipHistogramEnabled) - { - // Clipping the histogram, but doing it on a copy to keep the original un-clipped values for the next iteration. - histogram.CopyTo(histogramCopy); - this.ClipHistogram(histogramCopy, this.ClipLimitPercentage, pixeInTile); - } - - // Calculate the cumulative distribution function, which will map each input pixel in the current tile to a new value. - int cdfMin = this.ClipHistogramEnabled - ? this.CalculateCdf(ref cdfBase, ref histogramCopyBase, histogram.Length - 1) - : this.CalculateCdf(ref cdfBase, ref histogramBase, histogram.Length - 1); - - float numberOfPixelsMinusCdfMin = pixeInTile - cdfMin; - - // Map the current pixel to the new equalized value - int luminance = GetLuminance(source[x, y], this.LuminanceLevels); - float luminanceEqualized = Unsafe.Add(ref cdfBase, luminance) / numberOfPixelsMinusCdfMin; - targetPixels[x, y].FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, source[x, y].ToVector4().W)); - - // Remove top most row from the histogram, mirroring rows which exceeds the borders. - Span rowSpan = this.GetPixelRow(source, pixelRow, x - halfTileWidth, y - halfTileWidth, tileWidth); - this.RemovePixelsFromHistogram(rowSpan, histogram, this.LuminanceLevels); - - // Add new bottom row to the histogram, mirroring rows which exceeds the borders. - rowSpan = this.GetPixelRow(source, pixelRow, x - halfTileWidth, y + halfTileWidth, tileWidth); - this.AddPixelsToHistogram(rowSpan, histogram, this.LuminanceLevels); - } + // Clipping the histogram, but doing it on a copy to keep the original un-clipped values for the next iteration. + histogram.CopyTo(histogramCopy); + this.ClipHistogram(histogramCopy, this.ClipLimitPercentage, pixeInTile); } - }); - Buffer2D.SwapOrCopyContent(source.PixelBuffer, targetPixels); - } + // 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 = pixeInTile - cdfMin; + + // Map the current pixel to the new equalized value + int luminance = GetLuminance(source[x, y], this.LuminanceLevels); + float luminanceEqualized = Unsafe.Add(ref cdfBase, luminance) / numberOfPixelsMinusCdfMin; + targetPixels[x, y].FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, source[x, y].ToVector4().W)); + + // Remove top most row from the histogram, mirroring rows which exceeds the borders. + Span rowSpan = useFastPath ? this.GetPixelRowFast(source, pixelRow, x - halfTileWidth, y - halfTileWidth, tileWidth) : this.GetPixelRow(source, pixelRow, x - halfTileWidth, y - halfTileWidth, tileWidth); + this.RemovePixelsFromHistogram(rowSpan, histogram, this.LuminanceLevels); + + // Add new bottom row to the histogram, mirroring rows which exceeds the borders. + rowSpan = useFastPath ? this.GetPixelRowFast(source, pixelRow, x - halfTileWidth, y + halfTileWidth, tileWidth) : this.GetPixelRow(source, pixelRow, x - halfTileWidth, y + halfTileWidth, tileWidth); + this.AddPixelsToHistogram(rowSpan, histogram, this.LuminanceLevels); + } + } + }; } /// @@ -178,6 +211,11 @@ private Span GetPixelRow(ImageFrame source, Span rowPixe return source.GetPixelRowSpan(y).Slice(start: x, length: tileWidth); } + private Span GetPixelRowFast(ImageFrame source, Span rowPixels, int x, int y, int tileWidth) + { + return source.GetPixelRowSpan(y).Slice(start: x, length: tileWidth); + } + /// /// Adds a column of grey values to the histogram. /// From 381ac4ae42b3e4d4e2ba01a22ca374f7b6442108 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 22 Apr 2019 21:21:25 +0200 Subject: [PATCH 47/50] Refactoring and cleanup sliding window processor --- .../AdaptiveHistEqualizationSWProcessor.cs | 92 +++++++++++++++---- 1 file changed, 72 insertions(+), 20 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs index ca9a0cb711..f4e926ac5c 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs @@ -56,54 +56,69 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source int pixeInTile = tileWidth * tileHeight; int halfTileHeight = tileHeight / 2; int halfTileWidth = halfTileHeight; + var slidingWindowInfos = new SlidingWindowInfos(tileWidth, tileHeight, halfTileWidth, halfTileHeight, pixeInTile); using (Buffer2D targetPixels = 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, tileWidth, tileHeight, pixeInTile, halfTileHeight, halfTileWidth, yStart: halfTileHeight, yEnd: source.Height - halfTileHeight)); + this.ProcessSlidingWindow(source, memoryAllocator, targetPixels, slidingWindowInfos, yStart: halfTileHeight, yEnd: source.Height - halfTileHeight)); - // Left border + // Process the left border of the image. Parallel.For( 0, halfTileWidth, parallelOptions, - this.ProcessSlidingWindow(source, memoryAllocator, targetPixels, tileWidth, tileHeight, pixeInTile, halfTileHeight, halfTileWidth, yStart: 0, yEnd: source.Height, useFastPath: false)); + this.ProcessSlidingWindow(source, memoryAllocator, targetPixels, slidingWindowInfos, yStart: 0, yEnd: source.Height, useFastPath: false)); - // Right border + // Process the right border of the image. Parallel.For( source.Width - halfTileWidth, source.Width, parallelOptions, - this.ProcessSlidingWindow(source, memoryAllocator, targetPixels, tileWidth, tileHeight, pixeInTile, halfTileHeight, halfTileWidth, yStart: 0, yEnd: source.Height, useFastPath: false)); + this.ProcessSlidingWindow(source, memoryAllocator, targetPixels, slidingWindowInfos, yStart: 0, yEnd: source.Height, useFastPath: false)); - // Top border + // Process the top border of the image. Parallel.For( halfTileWidth, source.Width - halfTileWidth, parallelOptions, - this.ProcessSlidingWindow(source, memoryAllocator, targetPixels, tileWidth, tileHeight, pixeInTile, halfTileHeight, halfTileWidth, yStart: 0, yEnd: halfTileHeight, useFastPath: false)); + this.ProcessSlidingWindow(source, memoryAllocator, targetPixels, slidingWindowInfos, yStart: 0, yEnd: halfTileHeight, useFastPath: false)); - // Bottom border + // Process the bottom border of the image. Parallel.For( halfTileWidth, source.Width - halfTileWidth, parallelOptions, - this.ProcessSlidingWindow(source, memoryAllocator, targetPixels, tileWidth, tileHeight, pixeInTile, halfTileHeight, halfTileWidth, yStart: source.Height - halfTileHeight, yEnd: source.Height, useFastPath: false)); + this.ProcessSlidingWindow(source, memoryAllocator, targetPixels, slidingWindowInfos, yStart: source.Height - halfTileHeight, yEnd: source.Height, useFastPath: false)); Buffer2D.SwapOrCopyContent(source.PixelBuffer, targetPixels); } } - private Action ProcessSlidingWindow(ImageFrame source, MemoryAllocator memoryAllocator, Buffer2D targetPixels, int tileWidth, int tileHeight, int pixeInTile, int halfTileHeight, int halfTileWidth, int yStart, int yEnd, bool useFastPath = true) + /// + /// 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. + /// Informations 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. + /// Action Delegate. + private Action ProcessSlidingWindow(ImageFrame source, MemoryAllocator memoryAllocator, Buffer2D targetPixels, SlidingWindowInfos swInfos, int yStart, int yEnd, bool useFastPath = true) { 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(tileWidth, AllocationOptions.Clean)) + using (IMemoryOwner pixelRowBuffer = memoryAllocator.Allocate(swInfos.TileWidth, AllocationOptions.Clean)) { Span histogram = histogramBuffer.GetSpan(); ref int histogramBase = ref MemoryMarshal.GetReference(histogram); @@ -112,10 +127,12 @@ private Action ProcessSlidingWindow(ImageFrame source, MemoryAlloca ref int cdfBase = ref MemoryMarshal.GetReference(cdfBuffer.GetSpan()); Span pixelRow = pixelRowBuffer.GetSpan(); - // Build the histogram of grayscale values for the current tile. - for (int dy = yStart - halfTileHeight; dy < yStart + halfTileHeight; dy++) + // Build the initial histogram of grayscale values. + for (int dy = yStart - swInfos.HalfTileHeight; dy < yStart + swInfos.HalfTileHeight; dy++) { - Span rowSpan = useFastPath ? this.GetPixelRowFast(source, pixelRow, x - halfTileWidth, dy, tileWidth) : this.GetPixelRow(source, pixelRow, x - halfTileWidth, dy, tileWidth); + Span rowSpan = useFastPath + ? this.GetPixelRowFast(source, pixelRow, x - swInfos.HalfTileWidth, dy, swInfos.TileWidth) + : this.GetPixelRow(source, pixelRow, x - swInfos.HalfTileWidth, dy, swInfos.TileWidth); this.AddPixelsToHistogram(rowSpan, histogram, this.LuminanceLevels); } @@ -125,7 +142,7 @@ private Action ProcessSlidingWindow(ImageFrame source, MemoryAlloca { // Clipping the histogram, but doing it on a copy to keep the original un-clipped values for the next iteration. histogram.CopyTo(histogramCopy); - this.ClipHistogram(histogramCopy, this.ClipLimitPercentage, pixeInTile); + this.ClipHistogram(histogramCopy, this.ClipLimitPercentage, swInfos.PixeInTile); } // Calculate the cumulative distribution function, which will map each input pixel in the current tile to a new value. @@ -133,19 +150,23 @@ private Action ProcessSlidingWindow(ImageFrame source, MemoryAlloca ? this.CalculateCdf(ref cdfBase, ref histogramCopyBase, histogram.Length - 1) : this.CalculateCdf(ref cdfBase, ref histogramBase, histogram.Length - 1); - float numberOfPixelsMinusCdfMin = pixeInTile - cdfMin; + float numberOfPixelsMinusCdfMin = swInfos.PixeInTile - cdfMin; - // Map the current pixel to the new equalized value + // 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. - Span rowSpan = useFastPath ? this.GetPixelRowFast(source, pixelRow, x - halfTileWidth, y - halfTileWidth, tileWidth) : this.GetPixelRow(source, pixelRow, x - halfTileWidth, y - halfTileWidth, tileWidth); + Span rowSpan = useFastPath + ? this.GetPixelRowFast(source, pixelRow, x - swInfos.HalfTileWidth, y - swInfos.HalfTileWidth, swInfos.TileWidth) + : this.GetPixelRow(source, pixelRow, x - swInfos.HalfTileWidth, y - swInfos.HalfTileWidth, swInfos.TileWidth); this.RemovePixelsFromHistogram(rowSpan, histogram, this.LuminanceLevels); // Add new bottom row to the histogram, mirroring rows which exceeds the borders. - rowSpan = useFastPath ? this.GetPixelRowFast(source, pixelRow, x - halfTileWidth, y + halfTileWidth, tileWidth) : this.GetPixelRow(source, pixelRow, x - halfTileWidth, y + halfTileWidth, tileWidth); + rowSpan = useFastPath + ? this.GetPixelRowFast(source, pixelRow, x - swInfos.HalfTileWidth, y + swInfos.HalfTileWidth, swInfos.TileWidth) + : this.GetPixelRow(source, pixelRow, x - swInfos.HalfTileWidth, y + swInfos.HalfTileWidth, swInfos.TileWidth); this.AddPixelsToHistogram(rowSpan, histogram, this.LuminanceLevels); } } @@ -173,7 +194,7 @@ private Span GetPixelRow(ImageFrame source, Span rowPixe y = source.Height - diff - 1; } - // Special cases for the left and the right border where GetPixelRowSpan can not be used + // Special cases for the left and the right border where GetPixelRowSpan can not be used. if (x < 0) { rowPixels.Clear(); @@ -211,6 +232,15 @@ private Span GetPixelRow(ImageFrame source, Span rowPixe return source.GetPixelRowSpan(y).Slice(start: x, length: tileWidth); } + /// + /// Get the a pixel row at a given position with a length of the tile width. + /// + /// The source image. + /// Pre-allocated pixel row span of the size of a the tile width. + /// The x position. + /// The y position. + /// The width in pixels of a tile. + /// A pixel row of the length of the tile width. private Span GetPixelRowFast(ImageFrame source, Span rowPixels, int x, int y, int tileWidth) { return source.GetPixelRowSpan(y).Slice(start: x, length: tileWidth); @@ -245,5 +275,27 @@ private void RemovePixelsFromHistogram(Span greyValues, Span histog histogram[luminance]--; } } + + private class SlidingWindowInfos + { + public SlidingWindowInfos(int tileWidth, int tileHeight, int halfTileWidth, int halfTileHeight, int pixeInTile) + { + this.TileWidth = tileWidth; + this.TileHeight = tileHeight; + this.HalfTileWidth = halfTileWidth; + this.HalfTileHeight = halfTileHeight; + this.PixeInTile = pixeInTile; + } + + public int TileWidth { get; private set; } + + public int TileHeight { get; private set; } + + public int PixeInTile { get; private set; } + + public int HalfTileWidth { get; private set; } + + public int HalfTileHeight { get; private set; } + } } } From c69bffe0d45ec3d73886151af20c68978c7b8b66 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 23 Apr 2019 19:13:35 +0200 Subject: [PATCH 48/50] Added an upper limit of 100 tiles --- .../Normalization/AdaptiveHistEqualizationProcessor.cs | 3 ++- .../Normalization/AdaptiveHistEqualizationSWProcessor.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs index 5092b85ff4..c562372867 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs @@ -30,11 +30,12 @@ internal class AdaptiveHistEqualizationProcessor : HistogramEqualization /// or 65536 for 16-bit grayscale images. /// Indicating whether to clip the histogram bins at a specific value. /// 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 number of tiles the image is split into (horizontal and vertically). Minimum value is 2. + /// The number of tiles the image is split into (horizontal and vertically). Minimum value is 2. Maximum value is 100. public AdaptiveHistEqualizationProcessor(int luminanceLevels, bool clipHistogram, float clipLimitPercentage, int tiles) : base(luminanceLevels, clipHistogram, clipLimitPercentage) { Guard.MustBeGreaterThanOrEqualTo(tiles, 2, nameof(tiles)); + Guard.MustBeLessThanOrEqualTo(tiles, 100, nameof(tiles)); this.Tiles = tiles; } diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs index f4e926ac5c..d567aca977 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs @@ -29,11 +29,12 @@ internal class AdaptiveHistEqualizationSWProcessor : HistogramEqualizati /// or 65536 for 16-bit grayscale images. /// Indicating whether to clip the histogram bins at a specific value. /// 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 number of tiles the image is split into (horizontal and vertically). Minimum value is 2. + /// The number of tiles the image is split into (horizontal and vertically). Minimum value is 2. Maximum value is 100. public AdaptiveHistEqualizationSWProcessor(int luminanceLevels, bool clipHistogram, float clipLimitPercentage, int tiles) : base(luminanceLevels, clipHistogram, clipLimitPercentage) { Guard.MustBeGreaterThanOrEqualTo(tiles, 2, nameof(tiles)); + Guard.MustBeLessThanOrEqualTo(tiles, 100, nameof(tiles)); this.Tiles = tiles; } From e46dc9e52f2d7ba6c336fb86d681df00ada9eef7 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 27 Apr 2019 23:44:28 +1000 Subject: [PATCH 49/50] Performance tweaks --- .../HistogramEqualizationExtension.cs | 2 +- .../AdaptiveHistEqualizationProcessor.cs | 13 +- .../AdaptiveHistEqualizationSWProcessor.cs | 181 +++++++++++++----- .../HistogramEqualizationOptions.cs | 5 + .../HistogramEqualizationProcessor.cs | 13 +- 5 files changed, 157 insertions(+), 57 deletions(-) diff --git a/src/ImageSharp/Processing/HistogramEqualizationExtension.cs b/src/ImageSharp/Processing/HistogramEqualizationExtension.cs index ceae4a1ed4..d967ef3622 100644 --- a/src/ImageSharp/Processing/HistogramEqualizationExtension.cs +++ b/src/ImageSharp/Processing/HistogramEqualizationExtension.cs @@ -19,7 +19,7 @@ public static class HistogramEqualizationExtension /// The . public static IImageProcessingContext HistogramEqualization(this IImageProcessingContext source) where TPixel : struct, IPixel - => HistogramEqualization(source, new HistogramEqualizationOptions()); + => HistogramEqualization(source, HistogramEqualizationOptions.Default); /// /// Equalizes the histogram of an image to increases the contrast. diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs index c562372867..cb52a88b7b 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs @@ -133,11 +133,11 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source ProcessBorderColumn(ref pixelsBase, cdfData, this.Tiles - 1, sourceWidth, sourceHeight, tileWidth, tileHeight, xStart: rightBorderStartX, xEnd: sourceWidth, luminanceLevels); // Fix top row - ProcessBorderRow(ref pixelsBase, cdfData, 0, sourceWidth, tileWidth, tileHeight, yStart: 0, yEnd: halfTileHeight, luminanceLevels); + ProcessBorderRow(ref pixelsBase, cdfData, 0, sourceWidth, tileWidth, yStart: 0, yEnd: halfTileHeight, luminanceLevels); // Fix bottom row int bottomBorderStartY = ((this.Tiles - 1) * tileHeight) + halfTileHeight; - ProcessBorderRow(ref pixelsBase, cdfData, this.Tiles - 1, sourceWidth, tileWidth, tileHeight, yStart: bottomBorderStartY, yEnd: sourceHeight, luminanceLevels); + ProcessBorderRow(ref pixelsBase, cdfData, this.Tiles - 1, sourceWidth, tileWidth, yStart: bottomBorderStartY, yEnd: sourceHeight, luminanceLevels); // Left top corner ProcessCornerTile(ref pixelsBase, cdfData, sourceWidth, 0, 0, xStart: 0, xEnd: halfTileWidth, yStart: 0, yEnd: halfTileHeight, luminanceLevels); @@ -256,7 +256,6 @@ private static void ProcessBorderColumn( /// The Y index of the lookup table to use. /// The source image width. /// The width of a tile. - /// The height of a tile. /// Y start position in the image. /// Y end position of the image. /// @@ -269,13 +268,11 @@ private static void ProcessBorderRow( int cdfY, int sourceWidth, int tileWidth, - int tileHeight, int yStart, int yEnd, int luminanceLevels) { int halfTileWidth = tileWidth / 2; - int halfTileHeight = tileHeight / 2; int cdfX = 0; for (int x = halfTileWidth; x < sourceWidth - halfTileWidth; x += tileWidth) @@ -396,7 +393,8 @@ private static float InterpolateBetweenTwoTiles( /// Luminance from right bottom tile. /// Interpolated Luminance. [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); + 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. @@ -406,7 +404,8 @@ private static float InterpolateBetweenTwoTiles( /// The interpolation value between the two values in the range of [0, 1]. /// The interpolated value. [MethodImpl(InliningOptions.ShortMethod)] - private static float LinearInterpolation(float left, float right, float t) => left + ((right - left) * t); + private static float LinearInterpolation(float left, float right, float t) + => left + ((right - left) * t); /// /// Contains the results of the cumulative distribution function for all tiles. diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs index d567aca977..aa9b530c6d 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationSWProcessor.cs @@ -58,6 +58,7 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source int halfTileHeight = tileHeight / 2; int halfTileWidth = halfTileHeight; var slidingWindowInfos = new SlidingWindowInfos(tileWidth, tileHeight, halfTileWidth, halfTileHeight, pixeInTile); + using (Buffer2D targetPixels = configuration.MemoryAllocator.Allocate2D(source.Width, source.Height)) { // Process the inner tiles, which do not require to check the borders. @@ -65,35 +66,75 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source halfTileWidth, source.Width - halfTileWidth, parallelOptions, - this.ProcessSlidingWindow(source, memoryAllocator, targetPixels, slidingWindowInfos, yStart: halfTileHeight, yEnd: source.Height - halfTileHeight)); + this.ProcessSlidingWindow( + source, + memoryAllocator, + targetPixels, + slidingWindowInfos, + yStart: halfTileHeight, + yEnd: source.Height - halfTileHeight, + useFastPath: true, + configuration)); // Process the left border of the image. Parallel.For( 0, halfTileWidth, parallelOptions, - this.ProcessSlidingWindow(source, memoryAllocator, targetPixels, slidingWindowInfos, yStart: 0, yEnd: source.Height, useFastPath: false)); + this.ProcessSlidingWindow( + source, + memoryAllocator, + targetPixels, + slidingWindowInfos, + yStart: 0, + yEnd: source.Height, + useFastPath: false, + configuration)); // Process the right border of the image. Parallel.For( source.Width - halfTileWidth, source.Width, parallelOptions, - this.ProcessSlidingWindow(source, memoryAllocator, targetPixels, slidingWindowInfos, yStart: 0, yEnd: source.Height, useFastPath: false)); + this.ProcessSlidingWindow( + source, + memoryAllocator, + targetPixels, + slidingWindowInfos, + yStart: 0, + yEnd: source.Height, + useFastPath: false, + configuration)); // Process the top border of the image. Parallel.For( halfTileWidth, source.Width - halfTileWidth, parallelOptions, - this.ProcessSlidingWindow(source, memoryAllocator, targetPixels, slidingWindowInfos, yStart: 0, yEnd: halfTileHeight, useFastPath: false)); + this.ProcessSlidingWindow( + source, + memoryAllocator, + targetPixels, + slidingWindowInfos, + yStart: 0, + yEnd: halfTileHeight, + useFastPath: false, + configuration)); // Process the bottom border of the image. Parallel.For( halfTileWidth, source.Width - halfTileWidth, parallelOptions, - this.ProcessSlidingWindow(source, memoryAllocator, targetPixels, slidingWindowInfos, yStart: source.Height - halfTileHeight, yEnd: source.Height, useFastPath: false)); + this.ProcessSlidingWindow( + source, + memoryAllocator, + targetPixels, + slidingWindowInfos, + yStart: source.Height - halfTileHeight, + yEnd: source.Height, + useFastPath: false, + configuration)); Buffer2D.SwapOrCopyContent(source.PixelBuffer, targetPixels); } @@ -111,30 +152,49 @@ protected override void OnFrameApply(ImageFrame source, Rectangle source /// 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 = true) + 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)) + 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(); + + 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++) { - Span rowSpan = useFastPath - ? this.GetPixelRowFast(source, pixelRow, x - swInfos.HalfTileWidth, dy, swInfos.TileWidth) - : this.GetPixelRow(source, pixelRow, x - swInfos.HalfTileWidth, dy, swInfos.TileWidth); - this.AddPixelsToHistogram(rowSpan, histogram, this.LuminanceLevels); + 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++) @@ -159,16 +219,28 @@ private Action ProcessSlidingWindow(ImageFrame source, MemoryAlloca 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. - Span rowSpan = useFastPath - ? this.GetPixelRowFast(source, pixelRow, x - swInfos.HalfTileWidth, y - swInfos.HalfTileWidth, swInfos.TileWidth) - : this.GetPixelRow(source, pixelRow, x - swInfos.HalfTileWidth, y - swInfos.HalfTileWidth, swInfos.TileWidth); - this.RemovePixelsFromHistogram(rowSpan, histogram, this.LuminanceLevels); + 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. - rowSpan = useFastPath - ? this.GetPixelRowFast(source, pixelRow, x - swInfos.HalfTileWidth, y + swInfos.HalfTileWidth, swInfos.TileWidth) - : this.GetPixelRow(source, pixelRow, x - swInfos.HalfTileWidth, y + swInfos.HalfTileWidth, swInfos.TileWidth); - this.AddPixelsToHistogram(rowSpan, histogram, this.LuminanceLevels); + 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); } } }; @@ -182,12 +254,18 @@ private Action ProcessSlidingWindow(ImageFrame source, MemoryAlloca /// The x position. /// The y position. /// The width in pixels of a tile. - /// A pixel row of the length of the tile width. - private Span GetPixelRow(ImageFrame source, Span rowPixels, int x, int y, int tileWidth) + /// The configuration. + private void CopyPixelRow( + ImageFrame source, + Span rowPixels, + int x, + int y, + int tileWidth, + Configuration configuration) { if (y < 0) { - y = Math.Abs(y); + y = ImageMaths.FastAbs(y); } else if (y >= source.Height) { @@ -202,11 +280,11 @@ private Span GetPixelRow(ImageFrame source, Span rowPixe int idx = 0; for (int dx = x; dx < x + tileWidth; dx++) { - rowPixels[idx] = source[Math.Abs(dx), y]; + rowPixels[idx] = source[ImageMaths.FastAbs(dx), y].ToVector4(); idx++; } - return rowPixels; + return; } else if (x + tileWidth > source.Width) { @@ -217,20 +295,20 @@ private Span GetPixelRow(ImageFrame source, Span rowPixe if (dx >= source.Width) { int diff = dx - source.Width; - rowPixels[idx] = source[dx - diff - 1, y]; + rowPixels[idx] = source[dx - diff - 1, y].ToVector4(); } else { - rowPixels[idx] = source[dx, y]; + rowPixels[idx] = source[dx, y].ToVector4(); } idx++; } - return rowPixels; + return; } - return source.GetPixelRowSpan(y).Slice(start: x, length: tileWidth); + this.CopyPixelRowFast(source, rowPixels, x, y, tileWidth, configuration); } /// @@ -241,39 +319,48 @@ private Span GetPixelRow(ImageFrame source, Span rowPixe /// The x position. /// The y position. /// The width in pixels of a tile. - /// A pixel row of the length of the tile width. - private Span GetPixelRowFast(ImageFrame source, Span rowPixels, int x, int y, int tileWidth) - { - return source.GetPixelRowSpan(y).Slice(start: x, length: tileWidth); - } + /// The configuration. + [MethodImpl(InliningOptions.ShortMethod)] + private void CopyPixelRowFast( + ImageFrame source, + Span rowPixels, + int x, + int y, + int tileWidth, + Configuration configuration) + => PixelOperations.Instance.ToVector4(configuration, source.GetPixelRowSpan(y).Slice(start: x, length: tileWidth), rowPixels); /// /// Adds a column of grey values to the histogram. /// - /// The grey values to add. - /// The histogram. + /// The reference to the span of grey values to add. + /// The reference to the histogram span. /// The number of different luminance levels. - private void AddPixelsToHistogram(Span greyValues, Span histogram, int luminanceLevels) + /// The grey values span length. + [MethodImpl(InliningOptions.ShortMethod)] + private void AddPixelsToHistogram(ref Vector4 greyValuesBase, ref int histogramBase, int luminanceLevels, int length) { - for (int idx = 0; idx < greyValues.Length; idx++) + for (int idx = 0; idx < length; idx++) { - int luminance = GetLuminance(greyValues[idx], luminanceLevels); - histogram[luminance]++; + int luminance = GetLuminance(ref Unsafe.Add(ref greyValuesBase, idx), luminanceLevels); + Unsafe.Add(ref histogramBase, luminance)++; } } /// /// Removes a column of grey values from the histogram. /// - /// The grey values to remove. - /// The histogram. + /// The reference to the span of grey values to remove. + /// The reference to the histogram span. /// The number of different luminance levels. - private void RemovePixelsFromHistogram(Span greyValues, Span histogram, int luminanceLevels) + /// The grey values span length. + [MethodImpl(InliningOptions.ShortMethod)] + private void RemovePixelsFromHistogram(ref Vector4 greyValuesBase, ref int histogramBase, int luminanceLevels, int length) { - for (int idx = 0; idx < greyValues.Length; idx++) + for (int idx = 0; idx < length; idx++) { - int luminance = GetLuminance(greyValues[idx], luminanceLevels); - histogram[luminance]--; + int luminance = GetLuminance(ref Unsafe.Add(ref greyValuesBase, idx), luminanceLevels); + Unsafe.Add(ref histogramBase, luminance)--; } } @@ -299,4 +386,4 @@ public SlidingWindowInfos(int tileWidth, int tileHeight, int halfTileWidth, int public int HalfTileHeight { get; private set; } } } -} +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs index 6c0938c4ec..0d1a378361 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs @@ -8,6 +8,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization /// public class HistogramEqualizationOptions { + /// + /// Gets the default instance. + /// + public static HistogramEqualizationOptions Default { get; } = new HistogramEqualizationOptions(); + /// /// Gets or sets the histogram equalization method to use. Defaults to global histogram equalization. /// diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs index 01e1cecae1..fd1b6b9784 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.PixelFormats; @@ -121,9 +122,17 @@ public void ClipHistogram(Span histogram, float clipLimitPercentage, int pi [MethodImpl(InliningOptions.ShortMethod)] public static int GetLuminance(TPixel sourcePixel, int luminanceLevels) { - // Convert to grayscale using ITU-R Recommendation BT.709 var vector = sourcePixel.ToVector4(); - return (int)MathF.Round(((.2126F * vector.X) + (.7152F * vector.Y) + (.0722F * vector.Y)) * (luminanceLevels - 1)); + return GetLuminance(ref vector, luminanceLevels); } + + /// + /// Convert the pixel values to grayscale using ITU-R Recommendation BT.709. + /// + /// The vector to get the luminance from + /// The number of luminance levels (256 for 8 bit, 65536 for 16 bit grayscale images) + [MethodImpl(InliningOptions.ShortMethod)] + public static int GetLuminance(ref Vector4 vector, int luminanceLevels) + => (int)MathF.Round(((.2126F * vector.X) + (.7152F * vector.Y) + (.0722F * vector.Y)) * (luminanceLevels - 1)); } } \ No newline at end of file From f194f4ffa1e7c45261ca8656059cc2a211478305 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 27 Apr 2019 23:55:17 +1000 Subject: [PATCH 50/50] Update External --- tests/Images/External | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Images/External b/tests/Images/External index 8693e2fd45..1ca5154996 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit 8693e2fd4577a9ac1a749da8db564095b5a05389 +Subproject commit 1ca515499663e8b0b7c924a49b8d212f7447bdb0