mirror of https://github.com/SixLabors/ImageSharp
committed by
GitHub
18 changed files with 566 additions and 27 deletions
@ -0,0 +1,74 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using SixLabors.ImageSharp.Processing.Processors.Binarization; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing |
|||
{ |
|||
/// <summary>
|
|||
/// Extensions to perform AdaptiveThreshold through Mutator.
|
|||
/// </summary>
|
|||
public static class AdaptiveThresholdExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// Applies Bradley Adaptive Threshold to the image.
|
|||
/// </summary>
|
|||
/// <param name="source">The image this method extends.</param>
|
|||
/// <returns>The <see cref="Image{TPixel}"/>.</returns>
|
|||
public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source) |
|||
=> source.ApplyProcessor(new AdaptiveThresholdProcessor()); |
|||
|
|||
/// <summary>
|
|||
/// Applies Bradley Adaptive Threshold to the image.
|
|||
/// </summary>
|
|||
/// <param name="source">The image this method extends.</param>
|
|||
/// <param name="thresholdLimit">Threshold limit (0.0-1.0) to consider for binarization.</param>
|
|||
/// <returns>The <see cref="Image{TPixel}"/>.</returns>
|
|||
public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source, float thresholdLimit) |
|||
=> source.ApplyProcessor(new AdaptiveThresholdProcessor(thresholdLimit)); |
|||
|
|||
/// <summary>
|
|||
/// Applies Bradley Adaptive Threshold to the image.
|
|||
/// </summary>
|
|||
/// <param name="source">The image this method extends.</param>
|
|||
/// <param name="upper">Upper (white) color for thresholding.</param>
|
|||
/// <param name="lower">Lower (black) color for thresholding.</param>
|
|||
/// <returns>The <see cref="Image{TPixel}"/>.</returns>
|
|||
public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source, Color upper, Color lower) |
|||
=> source.ApplyProcessor(new AdaptiveThresholdProcessor(upper, lower)); |
|||
|
|||
/// <summary>
|
|||
/// Applies Bradley Adaptive Threshold to the image.
|
|||
/// </summary>
|
|||
/// <param name="source">The image this method extends.</param>
|
|||
/// <param name="upper">Upper (white) color for thresholding.</param>
|
|||
/// <param name="lower">Lower (black) color for thresholding.</param>
|
|||
/// <param name="thresholdLimit">Threshold limit (0.0-1.0) to consider for binarization.</param>
|
|||
/// <returns>The <see cref="Image{TPixel}"/>.</returns>
|
|||
public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source, Color upper, Color lower, float thresholdLimit) |
|||
=> source.ApplyProcessor(new AdaptiveThresholdProcessor(upper, lower, thresholdLimit)); |
|||
|
|||
/// <summary>
|
|||
/// Applies Bradley Adaptive Threshold to the image.
|
|||
/// </summary>
|
|||
/// <param name="source">The image this method extends.</param>
|
|||
/// <param name="upper">Upper (white) color for thresholding.</param>
|
|||
/// <param name="lower">Lower (black) color for thresholding.</param>
|
|||
/// <param name="rectangle">Rectangle region to apply the processor on.</param>
|
|||
/// <returns>The <see cref="Image{TPixel}"/>.</returns>
|
|||
public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source, Color upper, Color lower, Rectangle rectangle) |
|||
=> source.ApplyProcessor(new AdaptiveThresholdProcessor(upper, lower), rectangle); |
|||
|
|||
/// <summary>
|
|||
/// Applies Bradley Adaptive Threshold to the image.
|
|||
/// </summary>
|
|||
/// <param name="source">The image this method extends.</param>
|
|||
/// <param name="upper">Upper (white) color for thresholding.</param>
|
|||
/// <param name="lower">Lower (black) color for thresholding.</param>
|
|||
/// <param name="thresholdLimit">Threshold limit (0.0-1.0) to consider for binarization.</param>
|
|||
/// <param name="rectangle">Rectangle region to apply the processor on.</param>
|
|||
/// <returns>The <see cref="Image{TPixel}"/>.</returns>
|
|||
public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source, Color upper, Color lower, float thresholdLimit, Rectangle rectangle) |
|||
=> source.ApplyProcessor(new AdaptiveThresholdProcessor(upper, lower, thresholdLimit), rectangle); |
|||
} |
|||
} |
|||
@ -0,0 +1,77 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Binarization |
|||
{ |
|||
/// <summary>
|
|||
/// Performs Bradley Adaptive Threshold filter against an image.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Implements "Adaptive Thresholding Using the Integral Image",
|
|||
/// see paper: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.420.7883&rep=rep1&type=pdf
|
|||
/// </remarks>
|
|||
public class AdaptiveThresholdProcessor : IImageProcessor |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="AdaptiveThresholdProcessor"/> class.
|
|||
/// </summary>
|
|||
public AdaptiveThresholdProcessor() |
|||
: this(Color.White, Color.Black, 0.85f) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="AdaptiveThresholdProcessor"/> class.
|
|||
/// </summary>
|
|||
/// <param name="thresholdLimit">Threshold limit.</param>
|
|||
public AdaptiveThresholdProcessor(float thresholdLimit) |
|||
: this(Color.White, Color.Black, thresholdLimit) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="AdaptiveThresholdProcessor"/> class.
|
|||
/// </summary>
|
|||
/// <param name="upper">Color for upper threshold.</param>
|
|||
/// <param name="lower">Color for lower threshold.</param>
|
|||
public AdaptiveThresholdProcessor(Color upper, Color lower) |
|||
: this(upper, lower, 0.85f) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="AdaptiveThresholdProcessor"/> class.
|
|||
/// </summary>
|
|||
/// <param name="upper">Color for upper threshold.</param>
|
|||
/// <param name="lower">Color for lower threshold.</param>
|
|||
/// <param name="thresholdLimit">Threshold limit.</param>
|
|||
public AdaptiveThresholdProcessor(Color upper, Color lower, float thresholdLimit) |
|||
{ |
|||
this.Upper = upper; |
|||
this.Lower = lower; |
|||
this.ThresholdLimit = thresholdLimit; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets upper color limit for thresholding.
|
|||
/// </summary>
|
|||
public Color Upper { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets lower color limit for threshold.
|
|||
/// </summary>
|
|||
public Color Lower { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the value for threshold limit.
|
|||
/// </summary>
|
|||
public float ThresholdLimit { get; set; } |
|||
|
|||
/// <inheritdoc />
|
|||
public IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>(Configuration configuration, Image<TPixel> source, Rectangle sourceRectangle) |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
=> new AdaptiveThresholdProcessor<TPixel>(configuration, this, source, sourceRectangle); |
|||
} |
|||
} |
|||
@ -0,0 +1,160 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Runtime.InteropServices; |
|||
|
|||
using SixLabors.ImageSharp.Advanced; |
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Binarization |
|||
{ |
|||
/// <summary>
|
|||
/// Performs Bradley Adaptive Threshold filter against an image.
|
|||
/// </summary>
|
|||
internal class AdaptiveThresholdProcessor<TPixel> : ImageProcessor<TPixel> |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
private readonly AdaptiveThresholdProcessor definition; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="AdaptiveThresholdProcessor{TPixel}"/> class.
|
|||
/// </summary>
|
|||
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
|
|||
/// <param name="definition">The <see cref="AdaptiveThresholdProcessor"/> defining the processor parameters.</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 AdaptiveThresholdProcessor(Configuration configuration, AdaptiveThresholdProcessor definition, Image<TPixel> source, Rectangle sourceRectangle) |
|||
: base(configuration, source, sourceRectangle) |
|||
{ |
|||
this.definition = definition; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override void OnFrameApply(ImageFrame<TPixel> source) |
|||
{ |
|||
var intersect = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); |
|||
|
|||
Configuration configuration = this.Configuration; |
|||
TPixel upper = this.definition.Upper.ToPixel<TPixel>(); |
|||
TPixel lower = this.definition.Lower.ToPixel<TPixel>(); |
|||
float thresholdLimit = this.definition.ThresholdLimit; |
|||
|
|||
int startY = intersect.Y; |
|||
int endY = intersect.Bottom; |
|||
int startX = intersect.X; |
|||
int endX = intersect.Right; |
|||
|
|||
int width = intersect.Width; |
|||
int height = intersect.Height; |
|||
|
|||
// ClusterSize defines the size of cluster to used to check for average. Tweaked to support up to 4k wide pixels and not more. 4096 / 16 is 256 thus the '-1'
|
|||
byte clusterSize = (byte)Math.Truncate((width / 16f) - 1); |
|||
|
|||
// Using pooled 2d buffer for integer image table and temp memory to hold Rgb24 converted pixel data.
|
|||
using (Buffer2D<ulong> intImage = this.Configuration.MemoryAllocator.Allocate2D<ulong>(width, height)) |
|||
{ |
|||
Rgba32 rgb = default; |
|||
for (int x = startX; x < endX; x++) |
|||
{ |
|||
ulong sum = 0; |
|||
for (int y = startY; y < endY; y++) |
|||
{ |
|||
Span<TPixel> row = source.GetPixelRowSpan(y); |
|||
ref TPixel rowRef = ref MemoryMarshal.GetReference(row); |
|||
ref TPixel color = ref Unsafe.Add(ref rowRef, x); |
|||
color.ToRgba32(ref rgb); |
|||
|
|||
sum += (ulong)(rgb.R + rgb.G + rgb.G); |
|||
if (x - startX != 0) |
|||
{ |
|||
intImage[x - startX, y - startY] = intImage[x - startX - 1, y - startY] + sum; |
|||
} |
|||
else |
|||
{ |
|||
intImage[x - startX, y - startY] = sum; |
|||
} |
|||
} |
|||
} |
|||
|
|||
var operation = new RowOperation(intersect, source, intImage, upper, lower, thresholdLimit, clusterSize, startX, endX, startY); |
|||
ParallelRowIterator.IterateRows( |
|||
configuration, |
|||
intersect, |
|||
in operation); |
|||
} |
|||
} |
|||
|
|||
private readonly struct RowOperation : IRowOperation |
|||
{ |
|||
private readonly Rectangle bounds; |
|||
private readonly ImageFrame<TPixel> source; |
|||
private readonly Buffer2D<ulong> intImage; |
|||
private readonly TPixel upper; |
|||
private readonly TPixel lower; |
|||
private readonly float thresholdLimit; |
|||
private readonly int startX; |
|||
private readonly int endX; |
|||
private readonly int startY; |
|||
private readonly byte clusterSize; |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public RowOperation( |
|||
Rectangle bounds, |
|||
ImageFrame<TPixel> source, |
|||
Buffer2D<ulong> intImage, |
|||
TPixel upper, |
|||
TPixel lower, |
|||
float thresholdLimit, |
|||
byte clusterSize, |
|||
int startX, |
|||
int endX, |
|||
int startY) |
|||
{ |
|||
this.bounds = bounds; |
|||
this.source = source; |
|||
this.intImage = intImage; |
|||
this.upper = upper; |
|||
this.lower = lower; |
|||
this.thresholdLimit = thresholdLimit; |
|||
this.startX = startX; |
|||
this.endX = endX; |
|||
this.startY = startY; |
|||
this.clusterSize = clusterSize; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void Invoke(int y) |
|||
{ |
|||
Rgba32 rgb = default; |
|||
Span<TPixel> pixelRow = this.source.GetPixelRowSpan(y); |
|||
|
|||
for (int x = this.startX; x < this.endX; x++) |
|||
{ |
|||
TPixel pixel = pixelRow[x]; |
|||
pixel.ToRgba32(ref rgb); |
|||
|
|||
var x1 = Math.Max(x - this.startX - this.clusterSize + 1, 0); |
|||
var x2 = Math.Min(x - this.startX + this.clusterSize + 1, this.bounds.Width - 1); |
|||
var y1 = Math.Max(y - this.startY - this.clusterSize + 1, 0); |
|||
var y2 = Math.Min(y - this.startY + this.clusterSize + 1, this.bounds.Height - 1); |
|||
|
|||
var count = (uint)((x2 - x1) * (y2 - y1)); |
|||
var sum = (long)Math.Min(this.intImage[x2, y2] - this.intImage[x1, y2] - this.intImage[x2, y1] + this.intImage[x1, y1], long.MaxValue); |
|||
|
|||
if ((rgb.R + rgb.G + rgb.B) * count <= sum * this.thresholdLimit) |
|||
{ |
|||
this.source[x, y] = this.lower; |
|||
} |
|||
else |
|||
{ |
|||
this.source[x, y] = this.upper; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,127 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.ImageSharp.Processing; |
|||
using SixLabors.ImageSharp.Processing.Processors.Binarization; |
|||
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; |
|||
using Xunit; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Processing.Binarization |
|||
{ |
|||
public class AdaptiveThresholdTests : BaseImageOperationsExtensionTest |
|||
{ |
|||
[Fact] |
|||
public void AdaptiveThreshold_UsesDefaults_Works() |
|||
{ |
|||
// arrange
|
|||
var expectedThresholdLimit = .85f; |
|||
Color expectedUpper = Color.White; |
|||
Color expectedLower = Color.Black; |
|||
|
|||
// act
|
|||
this.operations.AdaptiveThreshold(); |
|||
|
|||
// assert
|
|||
AdaptiveThresholdProcessor p = this.Verify<AdaptiveThresholdProcessor>(); |
|||
Assert.Equal(expectedThresholdLimit, p.ThresholdLimit); |
|||
Assert.Equal(expectedUpper, p.Upper); |
|||
Assert.Equal(expectedLower, p.Lower); |
|||
} |
|||
|
|||
[Fact] |
|||
public void AdaptiveThreshold_SettingThresholdLimit_Works() |
|||
{ |
|||
// arrange
|
|||
var expectedThresholdLimit = .65f; |
|||
|
|||
// act
|
|||
this.operations.AdaptiveThreshold(expectedThresholdLimit); |
|||
|
|||
// assert
|
|||
AdaptiveThresholdProcessor p = this.Verify<AdaptiveThresholdProcessor>(); |
|||
Assert.Equal(expectedThresholdLimit, p.ThresholdLimit); |
|||
Assert.Equal(Color.White, p.Upper); |
|||
Assert.Equal(Color.Black, p.Lower); |
|||
} |
|||
|
|||
[Fact] |
|||
public void AdaptiveThreshold_SettingUpperLowerThresholds_Works() |
|||
{ |
|||
// arrange
|
|||
Color expectedUpper = Color.HotPink; |
|||
Color expectedLower = Color.Yellow; |
|||
|
|||
// act
|
|||
this.operations.AdaptiveThreshold(expectedUpper, expectedLower); |
|||
|
|||
// assert
|
|||
AdaptiveThresholdProcessor p = this.Verify<AdaptiveThresholdProcessor>(); |
|||
Assert.Equal(expectedUpper, p.Upper); |
|||
Assert.Equal(expectedLower, p.Lower); |
|||
} |
|||
|
|||
[Fact] |
|||
public void AdaptiveThreshold_SettingUpperLowerWithThresholdLimit_Works() |
|||
{ |
|||
// arrange
|
|||
var expectedThresholdLimit = .77f; |
|||
Color expectedUpper = Color.HotPink; |
|||
Color expectedLower = Color.Yellow; |
|||
|
|||
// act
|
|||
this.operations.AdaptiveThreshold(expectedUpper, expectedLower, expectedThresholdLimit); |
|||
|
|||
// assert
|
|||
AdaptiveThresholdProcessor p = this.Verify<AdaptiveThresholdProcessor>(); |
|||
Assert.Equal(expectedThresholdLimit, p.ThresholdLimit); |
|||
Assert.Equal(expectedUpper, p.Upper); |
|||
Assert.Equal(expectedLower, p.Lower); |
|||
} |
|||
|
|||
[Fact] |
|||
public void AdaptiveThreshold_SettingUpperLowerWithThresholdLimit_WithRectangle_Works() |
|||
{ |
|||
// arrange
|
|||
var expectedThresholdLimit = .77f; |
|||
Color expectedUpper = Color.HotPink; |
|||
Color expectedLower = Color.Yellow; |
|||
|
|||
// act
|
|||
this.operations.AdaptiveThreshold(expectedUpper, expectedLower, expectedThresholdLimit, this.rect); |
|||
|
|||
// assert
|
|||
AdaptiveThresholdProcessor p = this.Verify<AdaptiveThresholdProcessor>(this.rect); |
|||
Assert.Equal(expectedThresholdLimit, p.ThresholdLimit); |
|||
Assert.Equal(expectedUpper, p.Upper); |
|||
Assert.Equal(expectedLower, p.Lower); |
|||
} |
|||
|
|||
[Theory] |
|||
[WithFile(TestImages.Png.Bradley01, PixelTypes.Rgba32)] |
|||
[WithFile(TestImages.Png.Bradley02, PixelTypes.Rgba32)] |
|||
public void AdaptiveThreshold_Works<TPixel>(TestImageProvider<TPixel> provider) |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
using (Image<TPixel> image = provider.GetImage()) |
|||
{ |
|||
image.Mutate(img => img.AdaptiveThreshold()); |
|||
image.DebugSave(provider); |
|||
image.CompareToReferenceOutput(ImageComparer.Exact, provider); |
|||
} |
|||
} |
|||
|
|||
[Theory] |
|||
[WithFile(TestImages.Png.Bradley02, PixelTypes.Rgba32)] |
|||
public void AdaptiveThreshold_WithRectangle_Works<TPixel>(TestImageProvider<TPixel> provider) |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
using (Image<TPixel> image = provider.GetImage()) |
|||
{ |
|||
image.Mutate(img => img.AdaptiveThreshold(Color.White, Color.Black, new Rectangle(60, 90, 200, 30))); |
|||
image.DebugSave(provider); |
|||
image.CompareToReferenceOutput(ImageComparer.Exact, provider); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1 +1 @@ |
|||
Subproject commit fe694a3938bea3565071a96cb1c90c4cbc586ff9 |
|||
Subproject commit 6fdc6d19b101dc1c00a297d3e92257df60c413d0 |
|||
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 26 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in new issue