From 23661e12d2367b080941febfb410d4b2ca93fa5a Mon Sep 17 00:00:00 2001 From: popow Date: Sun, 19 Aug 2018 14:00:48 +0200 Subject: [PATCH] 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 @@ namespace SixLabors.ImageSharp.Processing 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 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization 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 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization /// /// Adaptive histogram equalization. /// - Adaptive + Adaptive, + + /// + /// Adaptive sliding window histogram equalization. + /// + AdaptiveSlidingWindow, } }