Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: adaptive histogram equalization #673

Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
af0b754
first version of sliding window adaptive histogram equalization
brianpopow Jul 19, 2018
274db88
going now from top to bottom of the image, added more comments
brianpopow Jul 22, 2018
c1c58fa
using memory allocator to create the histogram and the cdf
brianpopow Jul 24, 2018
30f4d73
mirroring rows which exceeds the borders
brianpopow Jul 25, 2018
3c2958a
mirroring also left and right borders
brianpopow Jul 26, 2018
024873c
gridsize and cliplimit are now parameters of the constructor
brianpopow Jul 30, 2018
f04ac40
using Parallel.For
brianpopow Jul 31, 2018
f84b03b
only applying clipping once, effect applying it multiple times is neg…
brianpopow Jul 31, 2018
d664870
added abstract base class for histogram equalization, added option to…
brianpopow Aug 2, 2018
adb06ef
small improvements
brianpopow Aug 4, 2018
e6011f0
clipLimit now in percent of the total number of pixels in the grid
brianpopow Aug 6, 2018
94d85db
optimization: only calculating the cdf until the maximum histogram index
brianpopow Aug 7, 2018
2835cf4
Merge remote-tracking branch 'upstream/master' into feature/adaptiveH…
brianpopow Aug 7, 2018
7fec978
fix: using configuration from the parameter instead of the default
brianpopow Aug 8, 2018
6908921
Merge branch 'master' into feature/adaptiveHistogramEqualization
JimBobSquarePants Aug 12, 2018
62b3ee3
removed unnecessary loops in CalculateCdf, fixed typo in method name …
brianpopow Aug 12, 2018
23661e1
added different approach for ahe: image is split up in tiles, cdf is …
brianpopow Aug 19, 2018
176e7e9
simplified interpolation between the tiles
brianpopow Aug 27, 2018
d24cfb2
Merge branch 'master' into feature/adaptiveHistogramEqualization
brianpopow Aug 27, 2018
516d96f
number of tiles is now fixed and depended on the width and height of …
brianpopow Sep 2, 2018
254cd33
moved calculating LUT's into separate method
brianpopow Sep 2, 2018
1981231
number of tiles is now part of the options and will be used with the …
brianpopow Sep 22, 2018
d9dcfc7
Merge branch 'master' into feature/adaptiveHistogramEqualization
brianpopow Sep 23, 2018
8792b1d
removed no longer valid xml comment
brianpopow Sep 23, 2018
135f8be
attempt fixing the borders
brianpopow Dec 2, 2018
895cecf
refactoring to improve readability
brianpopow Dec 2, 2018
3dfe5a5
linear interpolation in the border tiles
brianpopow Dec 4, 2018
ad4c1da
refactored processing the borders into separate methods
brianpopow Dec 5, 2018
80660c9
fixing corner tiles
brianpopow Dec 5, 2018
d8fd6f0
Merge remote-tracking branch 'upstream/master' into feature/adaptiveH…
brianpopow Dec 5, 2018
0329fb1
fixed build errors
brianpopow Dec 6, 2018
b6eda7f
fixing mistake during merge from upstream: setting test images to "up…
brianpopow Dec 6, 2018
e5121dc
using Parallel.ForEach for all inner tile calculations
brianpopow Dec 8, 2018
09955da
using Parallel.ForEach to calculate the lookup tables
brianpopow Dec 8, 2018
66b7550
re-using pre allocated pixel row in GetPixelRow
brianpopow Dec 8, 2018
f72908f
fixed issue with the border tiles, when tile width != tile height
brianpopow Dec 9, 2018
cadd2b3
changed default value for ClipHistogram to false again
brianpopow Dec 9, 2018
fa1f036
alpha channel from the original image is now preserved
brianpopow Dec 10, 2018
c687f3e
Merge branch 'master' into feature/adaptiveHistogramEqualization
JimBobSquarePants Dec 10, 2018
0d2c57a
Merge branch 'master' into feature/adaptiveHistogramEqualization
JimBobSquarePants Dec 12, 2018
1f46ec6
Merge branch 'master' into feature/adaptiveHistogramEqualization
brianpopow Dec 13, 2018
d397620
Merge branch 'master' into feature/adaptiveHistogramEqualization
brianpopow Dec 27, 2018
05676ce
added unit tests for adaptive histogram equalization
brianpopow Dec 28, 2018
04ece7c
Merge remote-tracking branch 'upstream/master' into feature/adaptiveH…
JimBobSquarePants Jan 20, 2019
2d31858
Update External
JimBobSquarePants Jan 20, 2019
4330c82
2x faster adaptive tiled processor
JimBobSquarePants Jan 23, 2019
a861b54
Remove double indexing and bounds checks
JimBobSquarePants Jan 23, 2019
609cb67
Begin optimizing the global histogram
JimBobSquarePants Jan 23, 2019
bf6a750
Merge remote-tracking branch 'upstream/master' into feature/adaptiveH…
JimBobSquarePants Jan 23, 2019
f2930c7
Parallelize GlobalHistogramEqualizationProcessor
JimBobSquarePants Jan 23, 2019
8f19e5e
Moving sliding window from left to right instead of from top to bottom
brianpopow Jan 25, 2019
be51b29
Merge branch 'master' into feature/adaptiveHistogramEqualization
brianpopow Jan 25, 2019
73af286
The tile width and height is again depended on the image width: image…
brianpopow Jan 27, 2019
94b5634
Merge branch 'master' into feature/adaptiveHistogramEqualization
JimBobSquarePants Jan 30, 2019
755116e
Removed keeping track of the maximum histogram position
brianpopow Jan 30, 2019
a7338c4
Updated reference image for sliding window AHE for moving the sliding…
brianpopow Jan 31, 2019
69b3c70
Removed unnecessary call to Span.Clear(), all values are overwritten …
brianpopow Feb 1, 2019
726a988
Merge branch 'master' into feature/adaptiveHistogramEqualization
JimBobSquarePants Feb 14, 2019
10a84cd
Merge remote-tracking branch 'upstream/master' into feature/adaptiveH…
JimBobSquarePants Feb 17, 2019
95b8427
Revert "Moving sliding window from left to right instead of from top …
brianpopow Mar 13, 2019
2d2d445
Split GetPixelRow in two version: one which mirrors the edges (only n…
brianpopow Apr 17, 2019
381ac4a
Refactoring and cleanup sliding window processor
brianpopow Apr 22, 2019
c69bffe
Added an upper limit of 100 tiles
brianpopow Apr 23, 2019
d251b05
Merge branch 'master' into feature/adaptiveHistogramEqualization
brianpopow Apr 23, 2019
e46dc9e
Performance tweaks
JimBobSquarePants Apr 27, 2019
42d380c
Merge remote-tracking branch 'upstream/master' into feature/adaptiveH…
JimBobSquarePants Apr 27, 2019
f194f4f
Update External
JimBobSquarePants Apr 27, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 29 additions & 7 deletions src/ImageSharp/Processing/HistogramEqualizationExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,47 @@ namespace SixLabors.ImageSharp.Processing
public static class HistogramEqualizationExtension
{
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image this method extends.</param>
/// <returns>The <see cref="Image{TPixel}"/>.</returns>
public static IImageProcessingContext<TPixel> HistogramEqualization<TPixel>(this IImageProcessingContext<TPixel> source)
where TPixel : struct, IPixel<TPixel>
=> HistogramEqualization(source, 65536);
=> HistogramEqualization(source, new HistogramEqualizationOptions());

/// <summary>
/// Equalizes the histogram of an image to increases the global contrast.
/// Equalizes the histogram of an image to increases the contrast.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image this method extends.</param>
/// <param name="luminanceLevels">The number of different luminance levels. Typical values are 256 for 8-bit grayscale images
/// or 65536 for 16-bit grayscale images.</param>
/// <param name="options">The histogram equalization options to use.</param>
/// <returns>The <see cref="Image{TPixel}"/>.</returns>
public static IImageProcessingContext<TPixel> HistogramEqualization<TPixel>(this IImageProcessingContext<TPixel> source, int luminanceLevels)
public static IImageProcessingContext<TPixel> HistogramEqualization<TPixel>(this IImageProcessingContext<TPixel> source, HistogramEqualizationOptions options)
where TPixel : struct, IPixel<TPixel>
=> source.ApplyProcessor(new HistogramEqualizationProcessor<TPixel>(luminanceLevels));
=> source.ApplyProcessor(GetProcessor<TPixel>(options));

private static HistogramEqualizationProcessor<TPixel> GetProcessor<TPixel>(HistogramEqualizationOptions options)
where TPixel : struct, IPixel<TPixel>
{
HistogramEqualizationProcessor<TPixel> processor;

switch (options.Method)
{
case HistogramEqualizationMethod.Global:
processor = new GlobalHistogramEqualizationProcessor<TPixel>(options.LuminanceLevels, options.ClipHistogram, options.ClipLimitPercentage);
break;

case HistogramEqualizationMethod.Adaptive:
processor = new AdaptiveHistEqualizationProcessor<TPixel>(options.LuminanceLevels, options.ClipHistogram, options.ClipLimitPercentage, options.GridSize);
break;

default:
processor = new GlobalHistogramEqualizationProcessor<TPixel>(options.LuminanceLevels, options.ClipHistogram, options.ClipLimitPercentage);
break;
}

return processor;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.

using System;
using System.Numerics;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
using SixLabors.Primitives;

namespace SixLabors.ImageSharp.Processing.Processors.Normalization
{
/// <summary>
/// Applies an adaptive histogram equalization to the image.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal class AdaptiveHistEqualizationProcessor<TPixel> : HistogramEqualizationProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// Initializes a new instance of the <see cref="AdaptiveHistEqualizationProcessor{TPixel}"/> class.
/// </summary>
/// <param name="luminanceLevels">The number of different luminance levels. Typical values are 256 for 8-bit grayscale images
/// or 65536 for 16-bit grayscale images.</param>
/// <param name="clipHistogram">Indicating whether to clip the histogram bins at a specific value.</param>
/// <param name="clipLimitPercentage">Histogram clip limit in percent of the total pixels in the grid. Histogram bins which exceed this limit, will be capped at this value.</param>
/// <param name="gridSize">The grid size of the adaptive histogram equalization. Minimum value is 4.</param>
public AdaptiveHistEqualizationProcessor(int luminanceLevels, bool clipHistogram, float clipLimitPercentage, int gridSize)
: base(luminanceLevels, clipHistogram, clipLimitPercentage)
{
Guard.MustBeGreaterThanOrEqualTo(gridSize, 4, nameof(gridSize));

this.GridSize = gridSize;
}

/// <summary>
/// Gets the size of the grid for the adaptive histogram equalization.
/// </summary>
public int GridSize { get; }

/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration)
{
MemoryAllocator memoryAllocator = configuration.MemoryAllocator;
int numberOfPixels = source.Width * source.Height;
Span<TPixel> pixels = source.GetPixelSpan();

int pixelsInGrid = this.GridSize * this.GridSize;
int halfGridSize = this.GridSize / 2;
using (Buffer2D<TPixel> targetPixels = configuration.MemoryAllocator.Allocate2D<TPixel>(source.Width, source.Height))
{
ParallelFor.WithConfiguration(
0,
source.Width,
Configuration.Default,
brianpopow marked this conversation as resolved.
Show resolved Hide resolved
x =>
{
using (System.Buffers.IMemoryOwner<int> histogramBuffer = memoryAllocator.Allocate<int>(this.LuminanceLevels, AllocationOptions.Clean))
using (System.Buffers.IMemoryOwner<int> histogramBufferCopy = memoryAllocator.Allocate<int>(this.LuminanceLevels, AllocationOptions.Clean))
using (System.Buffers.IMemoryOwner<int> cdfBuffer = memoryAllocator.Allocate<int>(this.LuminanceLevels, AllocationOptions.Clean))
{
Span<int> histogram = histogramBuffer.GetSpan();
Span<int> histogramCopy = histogramBufferCopy.GetSpan();
Span<int> cdf = cdfBuffer.GetSpan();
int maxHistIdx = 0;

// Build the histogram of grayscale values for the current grid.
for (int dy = -halfGridSize; dy < halfGridSize; dy++)
{
Span<TPixel> rowSpan = this.GetPixelRow(source, (int)x - halfGridSize, dy, this.GridSize);
int maxIdx = this.AddPixelsTooHistogram(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<TPixel> 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.AddPixelsTooHistogram(rowSpan, histogram, this.LuminanceLevels);
if (maxIdx > maxHistIdx)
{
maxHistIdx = maxIdx;
}
}
}
});

Buffer2D<TPixel>.SwapOrCopyContent(source.PixelBuffer, targetPixels);
}
}

/// <summary>
/// Get the a pixel row at a given position with a length of the grid size. Mirrors pixels which exceeds the edges.
/// </summary>
/// <param name="source">The source image.</param>
/// <param name="x">The x position.</param>
/// <param name="y">The y position.</param>
/// <param name="gridSize">The grid size.</param>
/// <returns>A pixel row of the length of the grid size.</returns>
private Span<TPixel> GetPixelRow(ImageFrame<TPixel> 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];
brianpopow marked this conversation as resolved.
Show resolved Hide resolved
int idx = 0;
for (int dx = x; dx < x + gridSize; dx++)
{
rowPixels[idx] = source[Math.Abs(dx), y];
JimBobSquarePants marked this conversation as resolved.
Show resolved Hide resolved
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);
}

/// <summary>
/// Adds a row of grey values to the histogram.
/// </summary>
/// <param name="greyValues">The grey values to add</param>
/// <param name="histogram">The histogram</param>
/// <param name="luminanceLevels">The number of different luminance levels.</param>
/// <returns>The maximum index where a value was changed.</returns>
private int AddPixelsTooHistogram(Span<TPixel> greyValues, Span<int> histogram, int luminanceLevels)
brianpopow marked this conversation as resolved.
Show resolved Hide resolved
{
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;
}

/// <summary>
/// Removes a row of grey values from the histogram.
/// </summary>
/// <param name="greyValues">The grey values to remove</param>
/// <param name="histogram">The histogram</param>
/// <param name="luminanceLevels">The number of different luminance levels.</param>
/// <param name="maxHistIdx">The current maximum index of the histogram.</param>
/// <returns>The (maybe changed) maximum index of the histogram.</returns>
private int RemovePixelsFromHistogram(Span<TPixel> greyValues, Span<int> 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;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// 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
{
/// <summary>
/// Applies a global histogram equalization to the image.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal class GlobalHistogramEqualizationProcessor<TPixel> : HistogramEqualizationProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// Initializes a new instance of the <see cref="GlobalHistogramEqualizationProcessor{TPixel}"/> class.
/// </summary>
/// <param name="luminanceLevels">The number of different luminance levels. Typical values are 256 for 8-bit grayscale images
/// or 65536 for 16-bit grayscale images.</param>
/// <param name="clipHistogram">Indicating whether to clip the histogram bins at a specific value.</param>
/// <param name="clipLimitPercentage">Histogram clip limit in percent of the total pixels. Histogram bins which exceed this limit, will be capped at this value.</param>
public GlobalHistogramEqualizationProcessor(int luminanceLevels, bool clipHistogram, float clipLimitPercentage)
: base(luminanceLevels, clipHistogram, clipLimitPercentage)
{
}

/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration)
{
MemoryAllocator memoryAllocator = configuration.MemoryAllocator;
int numberOfPixels = source.Width * source.Height;
Span<TPixel> pixels = source.GetPixelSpan();

using (System.Buffers.IMemoryOwner<int> histogramBuffer = memoryAllocator.Allocate<int>(this.LuminanceLevels, AllocationOptions.Clean))
using (System.Buffers.IMemoryOwner<int> cdfBuffer = memoryAllocator.Allocate<int>(this.LuminanceLevels, AllocationOptions.Clean))
{
// Build the histogram of the grayscale levels.
Span<int> 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.ClipLimitPercentage, numberOfPixels);
}

// Calculate the cumulative distribution function, which will map each input pixel to a new value.
Span<int> cdf = cdfBuffer.GetSpan();
int cdfMin = this.CalculateCdf(cdf, 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 = this.GetLuminance(sourcePixel, this.LuminanceLevels);
float luminanceEqualized = cdf[luminance] / numberOfPixelsMinusCdfMin;

pixels[i].PackFromVector4(new Vector4(luminanceEqualized));
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.

namespace SixLabors.ImageSharp.Processing.Processors.Normalization
{
/// <summary>
/// Enumerates the different types of defined histogram equalization methods.
/// </summary>
public enum HistogramEqualizationMethod : int
{
/// <summary>
/// A global histogram equalization.
/// </summary>
Global,

/// <summary>
/// Adaptive histogram equalization.
/// </summary>
Adaptive
}
}
Loading