diff --git a/src/ImageSharp/Processing/AdaptiveThresholdExtensions.cs b/src/ImageSharp/Processing/AdaptiveThresholdExtensions.cs
new file mode 100644
index 0000000000..3279d96e3a
--- /dev/null
+++ b/src/ImageSharp/Processing/AdaptiveThresholdExtensions.cs
@@ -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
+{
+ ///
+ /// Extensions to perform AdaptiveThreshold through Mutator.
+ ///
+ public static class AdaptiveThresholdExtensions
+ {
+ ///
+ /// Applies Bradley Adaptive Threshold to the image.
+ ///
+ /// The image this method extends.
+ /// The .
+ public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source)
+ => source.ApplyProcessor(new AdaptiveThresholdProcessor());
+
+ ///
+ /// Applies Bradley Adaptive Threshold to the image.
+ ///
+ /// The image this method extends.
+ /// Threshold limit (0.0-1.0) to consider for binarization.
+ /// The .
+ public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source, float thresholdLimit)
+ => source.ApplyProcessor(new AdaptiveThresholdProcessor(thresholdLimit));
+
+ ///
+ /// Applies Bradley Adaptive Threshold to the image.
+ ///
+ /// The image this method extends.
+ /// Upper (white) color for thresholding.
+ /// Lower (black) color for thresholding.
+ /// The .
+ public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source, Color upper, Color lower)
+ => source.ApplyProcessor(new AdaptiveThresholdProcessor(upper, lower));
+
+ ///
+ /// Applies Bradley Adaptive Threshold to the image.
+ ///
+ /// The image this method extends.
+ /// Upper (white) color for thresholding.
+ /// Lower (black) color for thresholding.
+ /// Threshold limit (0.0-1.0) to consider for binarization.
+ /// The .
+ public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source, Color upper, Color lower, float thresholdLimit)
+ => source.ApplyProcessor(new AdaptiveThresholdProcessor(upper, lower, thresholdLimit));
+
+ ///
+ /// Applies Bradley Adaptive Threshold to the image.
+ ///
+ /// The image this method extends.
+ /// Upper (white) color for thresholding.
+ /// Lower (black) color for thresholding.
+ /// Rectangle region to apply the processor on.
+ /// The .
+ public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source, Color upper, Color lower, Rectangle rectangle)
+ => source.ApplyProcessor(new AdaptiveThresholdProcessor(upper, lower), rectangle);
+
+ ///
+ /// Applies Bradley Adaptive Threshold to the image.
+ ///
+ /// The image this method extends.
+ /// Upper (white) color for thresholding.
+ /// Lower (black) color for thresholding.
+ /// Threshold limit (0.0-1.0) to consider for binarization.
+ /// Rectangle region to apply the processor on.
+ /// The .
+ public static IImageProcessingContext AdaptiveThreshold(this IImageProcessingContext source, Color upper, Color lower, float thresholdLimit, Rectangle rectangle)
+ => source.ApplyProcessor(new AdaptiveThresholdProcessor(upper, lower, thresholdLimit), rectangle);
+ }
+}
diff --git a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs
new file mode 100644
index 0000000000..3558a94899
--- /dev/null
+++ b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs
@@ -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
+{
+ ///
+ /// Performs Bradley Adaptive Threshold filter against an image.
+ ///
+ ///
+ /// 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
+ ///
+ public class AdaptiveThresholdProcessor : IImageProcessor
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public AdaptiveThresholdProcessor()
+ : this(Color.White, Color.Black, 0.85f)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Threshold limit.
+ public AdaptiveThresholdProcessor(float thresholdLimit)
+ : this(Color.White, Color.Black, thresholdLimit)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Color for upper threshold.
+ /// Color for lower threshold.
+ public AdaptiveThresholdProcessor(Color upper, Color lower)
+ : this(upper, lower, 0.85f)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Color for upper threshold.
+ /// Color for lower threshold.
+ /// Threshold limit.
+ public AdaptiveThresholdProcessor(Color upper, Color lower, float thresholdLimit)
+ {
+ this.Upper = upper;
+ this.Lower = lower;
+ this.ThresholdLimit = thresholdLimit;
+ }
+
+ ///
+ /// Gets or sets upper color limit for thresholding.
+ ///
+ public Color Upper { get; set; }
+
+ ///
+ /// Gets or sets lower color limit for threshold.
+ ///
+ public Color Lower { get; set; }
+
+ ///
+ /// Gets or sets the value for threshold limit.
+ ///
+ public float ThresholdLimit { get; set; }
+
+ ///
+ public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle)
+ where TPixel : unmanaged, IPixel
+ => new AdaptiveThresholdProcessor(configuration, this, source, sourceRectangle);
+ }
+}
diff --git a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs
new file mode 100644
index 0000000000..dd8833ad96
--- /dev/null
+++ b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs
@@ -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
+{
+ ///
+ /// Performs Bradley Adaptive Threshold filter against an image.
+ ///
+ internal class AdaptiveThresholdProcessor : ImageProcessor
+ where TPixel : unmanaged, IPixel
+ {
+ private readonly AdaptiveThresholdProcessor definition;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The configuration which allows altering default behaviour or extending the library.
+ /// The defining the processor parameters.
+ /// The source for the current processor instance.
+ /// The source area to process for the current processor instance.
+ public AdaptiveThresholdProcessor(Configuration configuration, AdaptiveThresholdProcessor definition, Image source, Rectangle sourceRectangle)
+ : base(configuration, source, sourceRectangle)
+ {
+ this.definition = definition;
+ }
+
+ ///
+ protected override void OnFrameApply(ImageFrame source)
+ {
+ var intersect = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
+
+ Configuration configuration = this.Configuration;
+ TPixel upper = this.definition.Upper.ToPixel();
+ TPixel lower = this.definition.Lower.ToPixel();
+ 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 intImage = this.Configuration.MemoryAllocator.Allocate2D(width, height))
+ {
+ Rgba32 rgb = default;
+ for (int x = startX; x < endX; x++)
+ {
+ ulong sum = 0;
+ for (int y = startY; y < endY; y++)
+ {
+ Span 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 source;
+ private readonly Buffer2D 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 source,
+ Buffer2D 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;
+ }
+
+ ///
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public void Invoke(int y)
+ {
+ Rgba32 rgb = default;
+ Span 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;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/tests/ImageSharp.Tests/Processing/Binarization/AdaptiveThresholdTests.cs b/tests/ImageSharp.Tests/Processing/Binarization/AdaptiveThresholdTests.cs
new file mode 100644
index 0000000000..f992ac35b3
--- /dev/null
+++ b/tests/ImageSharp.Tests/Processing/Binarization/AdaptiveThresholdTests.cs
@@ -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();
+ 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();
+ 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();
+ 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();
+ 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(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(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using (Image 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(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using (Image 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);
+ }
+ }
+ }
+}
diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs
index 272998a896..e475a7712f 100644
--- a/tests/ImageSharp.Tests/TestImages.cs
+++ b/tests/ImageSharp.Tests/TestImages.cs
@@ -83,6 +83,9 @@ public static class Png
public const string Ducky = "Png/ducky.png";
public const string Rainbow = "Png/rainbow.png";
+ public const string Bradley01 = "Png/Bradley01.png";
+ public const string Bradley02 = "Png/Bradley02.png";
+
// Issue 1014: https://github.com/SixLabors/ImageSharp/issues/1014
public const string Issue1014_1 = "Png/issues/Issue_1014_1.png";
public const string Issue1014_2 = "Png/issues/Issue_1014_2.png";
diff --git a/tests/Images/External b/tests/Images/External
index fe694a3938..6fdc6d19b1 160000
--- a/tests/Images/External
+++ b/tests/Images/External
@@ -1 +1 @@
-Subproject commit fe694a3938bea3565071a96cb1c90c4cbc586ff9
+Subproject commit 6fdc6d19b101dc1c00a297d3e92257df60c413d0
diff --git a/tests/Images/Input/Png/Bradley01.png b/tests/Images/Input/Png/Bradley01.png
new file mode 100644
index 0000000000..b8c3c0b6f6
Binary files /dev/null and b/tests/Images/Input/Png/Bradley01.png differ
diff --git a/tests/Images/Input/Png/Bradley02.png b/tests/Images/Input/Png/Bradley02.png
new file mode 100644
index 0000000000..8aea767ab8
Binary files /dev/null and b/tests/Images/Input/Png/Bradley02.png differ