-
-
Notifications
You must be signed in to change notification settings - Fork 886
Add AutoLevel Processor #1619
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
Closed
Closed
Add AutoLevel Processor #1619
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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
This file contains hidden or 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
19 changes: 19 additions & 0 deletions
19
src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor.cs
This file contains hidden or 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,19 @@ | ||
| // Copyright (c) Six Labors. | ||
| // Licensed under the Apache License, Version 2.0. | ||
|
|
||
| using SixLabors.ImageSharp.PixelFormats; | ||
|
|
||
| namespace SixLabors.ImageSharp.Processing.Processors.Normalization | ||
| { | ||
| /// <summary> | ||
| /// Normalize an image by stretching the dynamic range to full contrast | ||
| /// Applicable to an <see cref="Image"/>. | ||
| /// </summary> | ||
| public class AutoLevelProcessor : IImageProcessor | ||
| { | ||
| /// <inheritdoc /> | ||
| public IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>(Configuration configuration, Image<TPixel> source, Rectangle sourceRectangle) | ||
| where TPixel : unmanaged, IPixel<TPixel> | ||
| => new AutoLevelProcessor<TPixel>(configuration, source, sourceRectangle); | ||
| } | ||
| } |
177 changes: 177 additions & 0 deletions
177
src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs
This file contains hidden or 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,177 @@ | ||
| // Copyright (c) Six Labors. | ||
| // Licensed under the Apache License, Version 2.0. | ||
|
|
||
| using System; | ||
| using System.Linq; | ||
| using System.Numerics; | ||
| using System.Runtime.CompilerServices; | ||
| using SixLabors.ImageSharp.Advanced; | ||
| using SixLabors.ImageSharp.PixelFormats; | ||
|
|
||
| namespace SixLabors.ImageSharp.Processing.Processors.Normalization | ||
| { | ||
| /// <summary> | ||
| /// Normalize an image by stretching the dynamic range to full contrast | ||
| /// Applicable to an <see cref="Image"/>. | ||
| /// </summary> | ||
| /// <typeparam name="TPixel">The pixel format.</typeparam> | ||
| internal class AutoLevelProcessor<TPixel> : ImageProcessor<TPixel> | ||
| where TPixel : unmanaged, IPixel<TPixel> | ||
| { | ||
| private readonly int luminanceLevels; | ||
|
|
||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="AutoLevelProcessor{TPixel}"/> class. | ||
| /// </summary> | ||
| /// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param> | ||
| /// <param name="source">The source <see cref="Image{TPixel}"/> for the current processor instance.</param> | ||
| /// <param name="sourceRectangle">The source area to process for the current processor instance.</param> | ||
| public AutoLevelProcessor( | ||
| Configuration configuration, | ||
| Image<TPixel> source, | ||
| Rectangle sourceRectangle) | ||
| : base(configuration, source, sourceRectangle) | ||
| { | ||
| // TODO I don't know how to get channel bit depth for non-grayscale types | ||
| if (!(typeof(TPixel) == typeof(L16) || typeof(TPixel) == typeof(L8))) | ||
| { | ||
| throw new ArgumentException("AutoLevelHistogramProcessor only works for L8 or L16 pixel types"); | ||
| } | ||
|
|
||
| this.luminanceLevels = ColorNumerics.GetColorCountForBitDepth(source.PixelType.BitsPerPixel); | ||
| } | ||
|
|
||
| protected override void OnFrameApply(ImageFrame<TPixel> source) | ||
| { | ||
| var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); | ||
|
|
||
| int[] rowMinimums = new int[source.Height]; | ||
| int[] rowMaximums = new int[source.Height]; | ||
|
|
||
| var grayscaleOperation = new GrayscaleLevelsMinMaxRowOperation(interest, source, this.luminanceLevels, rowMinimums, rowMaximums); | ||
| ParallelRowIterator.IterateRows( | ||
| this.Configuration, | ||
| interest, | ||
| in grayscaleOperation); | ||
|
|
||
| int minLuminance = rowMinimums.Min(); | ||
| int maxLuminance = rowMaximums.Max(); | ||
|
|
||
| if (minLuminance == 0 && maxLuminance == this.luminanceLevels - 1) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| var contrastStretchOperation = new GrayscaleLevelsContrastStretchOperation(interest, source, this.luminanceLevels, minLuminance, maxLuminance); | ||
| ParallelRowIterator.IterateRows( | ||
| this.Configuration, | ||
| interest, | ||
| in contrastStretchOperation); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// A <see langword="struct"/> to calculate the min and max luminance of a row for <see cref="AutoLevelProcessor{TPixel}"/>. | ||
| /// </summary> | ||
| private readonly struct GrayscaleLevelsMinMaxRowOperation : IRowOperation | ||
| { | ||
| private readonly Rectangle bounds; | ||
| private readonly ImageFrame<TPixel> source; | ||
| private readonly int luminanceLevels; | ||
| private readonly int[] rowMinimums; | ||
| private readonly int[] rowMaximums; | ||
|
|
||
| [MethodImpl(InliningOptions.ShortMethod)] | ||
| public GrayscaleLevelsMinMaxRowOperation( | ||
| Rectangle bounds, | ||
| ImageFrame<TPixel> source, | ||
| int luminanceLevels, | ||
| int[] rowMinimums, | ||
| int[] rowMaximums) | ||
| { | ||
| this.bounds = bounds; | ||
| this.source = source; | ||
| this.luminanceLevels = luminanceLevels; | ||
| this.rowMinimums = rowMinimums; | ||
| this.rowMaximums = rowMaximums; | ||
| } | ||
|
|
||
| /// <inheritdoc/> | ||
| #if NETSTANDARD2_0 | ||
| // https://github.com/SixLabors/ImageSharp/issues/1204 | ||
| [MethodImpl(MethodImplOptions.NoOptimization)] | ||
| #else | ||
| [MethodImpl(InliningOptions.ShortMethod)] | ||
| #endif | ||
| public void Invoke(int y) | ||
| { | ||
| Span<TPixel> pixelRow = this.source.GetPixelRowSpan(y); | ||
| int levels = this.luminanceLevels; | ||
|
|
||
| int minLuminance = int.MaxValue; | ||
| int maxLuminance = int.MinValue; | ||
|
|
||
| for (int x = this.bounds.X; x < this.bounds.Width; x++) | ||
| { | ||
| // TODO: We should bulk convert here. | ||
| var vector = pixelRow[x].ToVector4(); | ||
| int luminance = ColorNumerics.GetBT709Luminance(ref vector, levels); | ||
| minLuminance = Math.Min(luminance, minLuminance); | ||
| maxLuminance = Math.Max(luminance, maxLuminance); | ||
| } | ||
|
|
||
| this.rowMinimums[y] = minLuminance; | ||
| this.rowMaximums[y] = maxLuminance; | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// A <see langword="struct"/> to contrast stretch a row for <see cref="AutoLevelProcessor{TPixel}"/>. | ||
| /// </summary> | ||
| private readonly struct GrayscaleLevelsContrastStretchOperation : IRowOperation | ||
| { | ||
| private readonly Rectangle bounds; | ||
| private readonly ImageFrame<TPixel> source; | ||
| private readonly int luminanceLevels; | ||
| private readonly int minLuminance; | ||
| private readonly int maxLuminance; | ||
|
|
||
| [MethodImpl(InliningOptions.ShortMethod)] | ||
| public GrayscaleLevelsContrastStretchOperation( | ||
| Rectangle bounds, | ||
| ImageFrame<TPixel> source, | ||
| int luminanceLevels, | ||
| int minLuminance, | ||
| int maxLuminance) | ||
| { | ||
| this.bounds = bounds; | ||
| this.source = source; | ||
| this.luminanceLevels = luminanceLevels; | ||
| this.minLuminance = minLuminance; | ||
| this.maxLuminance = maxLuminance; | ||
| } | ||
|
|
||
| /// <inheritdoc/> | ||
| #if NETSTANDARD2_0 | ||
| // https://github.com/SixLabors/ImageSharp/issues/1204 | ||
| [MethodImpl(MethodImplOptions.NoOptimization)] | ||
| #else | ||
| [MethodImpl(InliningOptions.ShortMethod)] | ||
| #endif | ||
| public void Invoke(int y) | ||
| { | ||
| Span<TPixel> pixelRow = this.source.GetPixelRowSpan(y); | ||
| float dynamicRange = this.maxLuminance - this.minLuminance; | ||
|
|
||
| for (int x = this.bounds.X; x < this.bounds.Width; x++) | ||
| { | ||
| // TODO: We should bulk convert here. | ||
| ref TPixel pixel = ref pixelRow[x]; | ||
| var vector = pixel.ToVector4(); | ||
| int luminance = ColorNumerics.GetBT709Luminance(ref vector, this.luminanceLevels); | ||
| float luminanceConstrastStretched = (luminance - this.minLuminance) / dynamicRange; | ||
| pixel.FromVector4(new Vector4(luminanceConstrastStretched, luminanceConstrastStretched, luminanceConstrastStretched, vector.W)); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or 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
111 changes: 111 additions & 0 deletions
111
tests/ImageSharp.Tests/Processing/Normalization/AutoLevelTests.cs
This file contains hidden or 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,111 @@ | ||
| // Copyright (c) Six Labors. | ||
| // Licensed under the Apache License, Version 2.0. | ||
|
|
||
| using System.Linq; | ||
| using SixLabors.ImageSharp.PixelFormats; | ||
| using SixLabors.ImageSharp.Processing; | ||
| using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; | ||
| using Xunit; | ||
|
|
||
| namespace SixLabors.ImageSharp.Tests.Processing.Normalization | ||
| { | ||
| // ReSharper disable InconsistentNaming | ||
| public class AutoLevelTests | ||
| { | ||
| private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.0456F); | ||
|
|
||
| [Fact] | ||
| public void AutoLevel_WhenL16_StretchesBetweenMinAndMax() | ||
| { | ||
| // Arrange | ||
| ushort[] pixels = new ushort[] | ||
| { | ||
| 100, 120, 140, 160, 180, 200, | ||
| }; | ||
|
|
||
| ushort step = (ushort)(ushort.MaxValue / (pixels.Length - 1)); | ||
|
|
||
| using var image = new Image<L16>(pixels.Length, 1); | ||
| for (int x = 0; x < pixels.Length; x++) | ||
| { | ||
| image[x, 0] = new L16(pixels[x]); | ||
| } | ||
|
|
||
| ushort[] expected = | ||
| Enumerable.Range(0, pixels.Length) | ||
| .Select(e => (ushort)(e * step)) | ||
| .ToArray(); | ||
|
|
||
| // Act | ||
| image.Mutate(x => x.AutoLevel()); | ||
|
|
||
| ushort[] actual = image.GetPixelRowSpan(0).ToArray().Select(e => e.PackedValue).ToArray(); | ||
|
|
||
| // Assert | ||
| Assert.Equal(expected, actual); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void AutoLevel_WhenL8_StretchesBetweenMinAndMax() | ||
| { | ||
| // Arrange | ||
| byte[] pixels = new byte[] | ||
| { | ||
| 100, 120, 140, 160, 180, 200, | ||
| }; | ||
|
|
||
| byte step = (byte)(byte.MaxValue / (pixels.Length - 1)); | ||
|
|
||
| using var image = new Image<L8>(pixels.Length, 1); | ||
| for (int x = 0; x < pixels.Length; x++) | ||
| { | ||
| image[x, 0] = new L8(pixels[x]); | ||
| } | ||
|
|
||
| byte[] expected = | ||
| Enumerable.Range(0, pixels.Length) | ||
| .Select(e => (byte)(e * step)) | ||
| .ToArray(); | ||
|
|
||
| // Act | ||
| image.Mutate(x => x.AutoLevel()); | ||
|
|
||
| byte[] actual = image.GetPixelRowSpan(0).ToArray().Select(e => e.PackedValue).ToArray(); | ||
|
|
||
| // Assert | ||
| Assert.Equal(expected, actual); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void AutoLevel_WhenTwoRows_StretchesBetweenMinAndMax() | ||
| { | ||
| // Arrange | ||
| byte[] pixels = new byte[] | ||
| { | ||
| 100, 200 | ||
| }; | ||
|
|
||
| using var image = new Image<L8>(1, 2); | ||
| image[0, 0] = new L8(pixels[0]); | ||
| image[0, 1] = new L8(pixels[1]); | ||
|
|
||
| // Act | ||
| image.Mutate(x => x.AutoLevel()); | ||
|
|
||
| // Assert | ||
| Assert.Equal(0, image[0, 0].PackedValue); | ||
| Assert.Equal(byte.MaxValue, image[0, 1].PackedValue); | ||
| } | ||
|
|
||
| [Theory] | ||
| [WithFile(TestImages.Jpeg.Baseline.HistogramEqImage, PixelTypes.L8)] | ||
| public void AutoLevel_CompareToReferenceOutput<TPixel>(TestImageProvider<TPixel> provider) | ||
| where TPixel : unmanaged, IPixel<TPixel> | ||
| { | ||
| using Image<TPixel> image = provider.GetImage(); | ||
| image.Mutate(x => x.AutoLevel()); | ||
| image.DebugSave(provider); | ||
| image.CompareToReferenceOutput(ValidatorComparer, provider); | ||
| } | ||
| } | ||
| } |
3 changes: 3 additions & 0 deletions
3
...Tests/AutoLevel_CompareToReferenceOutput_L8_640px-Unequalized_Hawkes_Bay_NZ.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be ok to take the
PixelType.BitsPerPixeland divide it byUnsafe.SizeOf<T>(). That will give you a per-pixel-component average (since all our pixel formats are blittable) which should be close enough.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I would try that with
L16Unsafe.SizeOf<L16>()would return2BitsPerPixelwould return16The result is half of what it needs to be? (L16 is only 1 component?)
Or did you mean it's an operation only for non-luminescence types?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah right yep, my bad We need a
ChannelCountproperty onPixelTypeInfo.@antonfirsov This would be a breaking change (which we can probably allow for 1.1 release) but do you have any ideas how we could introduce it in a non breaking way?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A hack you could do just now is something similar to what we do in the png encoder, I.E use a switch for known types and a default for unknown.
ImageSharp/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs
Lines 159 to 174 in a8cb711