Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions src/ImageSharp/Advanced/AotCompilerTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ private static void AotCompileImageProcessors<TPixel>()
AotCompileImageProcessor<TPixel, AdaptiveHistogramEqualizationProcessor>();
AotCompileImageProcessor<TPixel, AdaptiveHistogramEqualizationSlidingWindowProcessor>();
AotCompileImageProcessor<TPixel, GlobalHistogramEqualizationProcessor>();
AotCompileImageProcessor<TPixel, AutoLevelProcessor>();
AotCompileImageProcessor<TPixel, AchromatomalyProcessor>();
AotCompileImageProcessor<TPixel, AchromatopsiaProcessor>();
AotCompileImageProcessor<TPixel, BlackWhiteProcessor>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.

using SixLabors.ImageSharp.Processing.Processors.Normalization;
Expand Down Expand Up @@ -28,5 +28,14 @@ public static IImageProcessingContext HistogramEqualization(
this IImageProcessingContext source,
HistogramEqualizationOptions options) =>
source.ApplyProcessor(HistogramEqualizationProcessor.FromOptions(options));

/// <summary>
/// Normalize an image by stretching the dynamic range to full contrast
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
public static IImageProcessingContext AutoLevel(
this IImageProcessingContext source) =>
source.ApplyProcessor(new AutoLevelProcessor());
}
}
}
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);
}
}
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)))
Copy link
Member

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.BitsPerPixel and divide it by Unsafe.SizeOf<T>(). That will give you a per-pixel-component average (since all our pixel formats are blittable) which should be close enough.

Copy link
Author

@CoenraadS CoenraadS May 3, 2021

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 L16

Unsafe.SizeOf<L16>() would return 2
BitsPerPixel would return 16

The 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?

Copy link
Member

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 ChannelCount property on PixelTypeInfo.

@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?

Copy link
Member

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.

return typeof(TPixel) switch
{
Type t when t == typeof(A8) => PngColorType.GrayscaleWithAlpha,
Type t when t == typeof(Argb32) => PngColorType.RgbWithAlpha,
Type t when t == typeof(Bgr24) => PngColorType.Rgb,
Type t when t == typeof(Bgra32) => PngColorType.RgbWithAlpha,
Type t when t == typeof(L8) => PngColorType.Grayscale,
Type t when t == typeof(L16) => PngColorType.Grayscale,
Type t when t == typeof(La16) => PngColorType.GrayscaleWithAlpha,
Type t when t == typeof(La32) => PngColorType.GrayscaleWithAlpha,
Type t when t == typeof(Rgb24) => PngColorType.Rgb,
Type t when t == typeof(Rgba32) => PngColorType.RgbWithAlpha,
Type t when t == typeof(Rgb48) => PngColorType.Rgb,
Type t when t == typeof(Rgba64) => PngColorType.RgbWithAlpha,
Type t when t == typeof(RgbaVector) => PngColorType.RgbWithAlpha,
_ => PngColorType.RgbWithAlpha

{
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));
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,9 @@ public void AdaptiveHistogramEqualization()
LuminanceLevels = 256,
Method = HistogramEqualizationMethod.AdaptiveTileInterpolation
}));

[Benchmark(Description = "AutoLevel (Min/Max Contrast Stretch)")]
public void AutoLevelHistogram()
=> this.image.Mutate(img => img.AutoLevel());
}
}
111 changes: 111 additions & 0 deletions tests/ImageSharp.Tests/Processing/Normalization/AutoLevelTests.cs
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);
}
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.