mirror of https://github.com/SixLabors/ImageSharp
committed by
GitHub
8 changed files with 442 additions and 1 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 |
Loading…
Reference in new issue