-
-
Notifications
You must be signed in to change notification settings - Fork 855
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #644 from brianpopow/feature/histogramEqualization
Feature: histogram equalization
- Loading branch information
Showing
3 changed files
with
230 additions
and
0 deletions.
There are no files selected for viewing
36 changes: 36 additions & 0 deletions
36
src/ImageSharp/Processing/HistogramEqualizationExtension.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
// Copyright (c) Six Labors and contributors. | ||
// Licensed under the Apache License, Version 2.0. | ||
|
||
using SixLabors.ImageSharp.PixelFormats; | ||
using SixLabors.ImageSharp.Processing.Processors.Normalization; | ||
|
||
namespace SixLabors.ImageSharp.Processing | ||
{ | ||
/// <summary> | ||
/// Adds extension that allow the adjustment of the contrast of an image via its histogram. | ||
/// </summary> | ||
public static class HistogramEqualizationExtension | ||
{ | ||
/// <summary> | ||
/// Equalizes the histogram of an image to increases the global contrast using 65536 luminance levels. | ||
/// </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); | ||
|
||
/// <summary> | ||
/// Equalizes the histogram of an image to increases the global 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> | ||
/// <returns>The <see cref="Image{TPixel}"/>.</returns> | ||
public static IImageProcessingContext<TPixel> HistogramEqualization<TPixel>(this IImageProcessingContext<TPixel> source, int luminanceLevels) | ||
where TPixel : struct, IPixel<TPixel> | ||
=> source.ApplyProcessor(new HistogramEqualizationProcessor<TPixel>(luminanceLevels)); | ||
} | ||
} |
126 changes: 126 additions & 0 deletions
126
src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
// Copyright (c) Six Labors and contributors. | ||
// 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 | ||
{ | ||
/// <summary> | ||
/// Applies a global histogram equalization to the image. | ||
/// </summary> | ||
/// <typeparam name="TPixel">The pixel format.</typeparam> | ||
internal class HistogramEqualizationProcessor<TPixel> : ImageProcessor<TPixel> | ||
where TPixel : struct, IPixel<TPixel> | ||
{ | ||
/// <summary> | ||
/// Initializes a new instance of the <see cref="HistogramEqualizationProcessor{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> | ||
public HistogramEqualizationProcessor(int luminanceLevels) | ||
{ | ||
Guard.MustBeGreaterThan(luminanceLevels, 0, nameof(luminanceLevels)); | ||
|
||
this.LuminanceLevels = luminanceLevels; | ||
} | ||
|
||
/// <summary> | ||
/// Gets the number of luminance levels. | ||
/// </summary> | ||
public int LuminanceLevels { 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(); | ||
|
||
// Build the histogram of the grayscale levels. | ||
using (IBuffer<int> histogramBuffer = memoryAllocator.AllocateClean<int>(this.LuminanceLevels)) | ||
using (IBuffer<int> cdfBuffer = memoryAllocator.AllocateClean<int>(this.LuminanceLevels)) | ||
{ | ||
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]++; | ||
} | ||
|
||
// 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); | ||
|
||
// 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)); | ||
} | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Calculates the cumulative distribution function. | ||
/// </summary> | ||
/// <param name="cdf">The array holding the cdf.</param> | ||
/// <param name="histogram">The histogram of the input image.</param> | ||
/// <returns>The first none zero value of the cdf.</returns> | ||
private int CalculateCdf(Span<int> cdf, Span<int> histogram) | ||
{ | ||
// Calculate the cumulative histogram | ||
int histSum = 0; | ||
for (int i = 0; i < histogram.Length; i++) | ||
{ | ||
histSum += histogram[i]; | ||
cdf[i] = histSum; | ||
} | ||
|
||
// Get the first none zero value of the cumulative histogram | ||
int cdfMin = 0; | ||
for (int i = 0; i < histogram.Length; i++) | ||
{ | ||
if (cdf[i] != 0) | ||
{ | ||
cdfMin = cdf[i]; | ||
break; | ||
} | ||
} | ||
|
||
// 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++) | ||
{ | ||
cdf[i] = Math.Max(0, cdf[i] - cdfMin); | ||
} | ||
|
||
return cdfMin; | ||
} | ||
|
||
/// <summary> | ||
/// Convert the pixel values to grayscale using ITU-R Recommendation BT.709. | ||
/// </summary> | ||
/// <param name="sourcePixel">The pixel to get the luminance from</param> | ||
/// <param name="luminanceLevels">The number of luminance levels (256 for 8 bit, 65536 for 16 bit grayscale images)</param> | ||
[MethodImpl(InliningOptions.ShortMethod)] | ||
private 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; | ||
} | ||
} | ||
} |
68 changes: 68 additions & 0 deletions
68
tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
// Copyright (c) Six Labors and contributors. | ||
// Licensed under the Apache License, Version 2.0. | ||
|
||
using SixLabors.ImageSharp.PixelFormats; | ||
using SixLabors.ImageSharp.Processing; | ||
using Xunit; | ||
|
||
namespace SixLabors.ImageSharp.Tests.Processing.Normalization | ||
{ | ||
public class HistogramEqualizationTests | ||
{ | ||
[Theory] | ||
[InlineData(256)] | ||
[InlineData(65536)] | ||
public void HistogramEqualizationTest(int luminanceLevels) | ||
{ | ||
// Arrange | ||
byte[] pixels = new byte[] | ||
{ | ||
52, 55, 61, 59, 70, 61, 76, 61, | ||
62, 59, 55, 104, 94, 85, 59, 71, | ||
63, 65, 66, 113, 144, 104, 63, 72, | ||
64, 70, 70, 126, 154, 109, 71, 69, | ||
67, 73, 68, 106, 122, 88, 68, 68, | ||
68, 79, 60, 79, 77, 66, 58, 75, | ||
69, 85, 64, 58, 55, 61, 65, 83, | ||
70, 87, 69, 68, 65, 73, 78, 90 | ||
}; | ||
|
||
var image = new Image<Rgba32>(8, 8); | ||
for (int y = 0; y < 8; y++) | ||
{ | ||
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[] | ||
{ | ||
0, 12, 53, 32, 146, 53, 174, 53, | ||
57, 32, 12, 227, 219, 202, 32, 154, | ||
65, 85, 93, 239, 251, 227, 65, 158, | ||
73, 146, 146, 247, 255, 235, 154, 130, | ||
97, 166, 117, 231, 243, 210, 117, 117, | ||
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(luminanceLevels)); | ||
|
||
// Assert | ||
for (int y = 0; y < 8; y++) | ||
{ | ||
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); | ||
} | ||
} | ||
} | ||
} | ||
} |