From ff2bf6fd3c401c69bacd2402332eeb03485fab85 Mon Sep 17 00:00:00 2001 From: Jason Finch Date: Sun, 18 Oct 2026 09:16:04 +1000 Subject: [PATCH] Keep the adaptive threshold window non-empty for regions narrower than 16 pixels The cluster size is the region width divided by 16, truncated, so it was 0 for regions narrower than 16 pixels. Every averaging window was then empty and each pixel took the lower color. Use a cluster size of at least 1, as regions 16 to 31 pixels wide already do. Wider regions are unchanged. --- .../AdaptiveThresholdProcessor{TPixel}.cs | 3 +- .../Binarization/AdaptiveThresholdTests.cs | 52 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs index e17de49d74..c3543053b2 100644 --- a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs @@ -40,7 +40,8 @@ protected override void OnFrameApply(ImageFrame source) // 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.Clamp(interest.Width / 16F, 0, 255); + // A cluster size of 0 would make every averaging window empty, so regions narrower than 16 pixels use 1. + byte clusterSize = (byte)Math.Clamp(interest.Width / 16F, 1, 255); using Buffer2D intImage = source.CalculateIntegralImage(interest); RowOperation operation = new(configuration, interest, source.PixelBuffer, intImage, upper, lower, thresholdLimit, clusterSize); diff --git a/tests/ImageSharp.Tests/Processing/Binarization/AdaptiveThresholdTests.cs b/tests/ImageSharp.Tests/Processing/Binarization/AdaptiveThresholdTests.cs index c797677860..e696144eba 100644 --- a/tests/ImageSharp.Tests/Processing/Binarization/AdaptiveThresholdTests.cs +++ b/tests/ImageSharp.Tests/Processing/Binarization/AdaptiveThresholdTests.cs @@ -113,6 +113,35 @@ public void AdaptiveThreshold_Works(TestImageProvider provider) } } + [Theory] + [InlineData(3, 3)] + [InlineData(8, 8)] + [InlineData(15, 15)] + [InlineData(15, 3)] + public void AdaptiveThreshold_NarrowImage_SeparatesDarkPixel(int width, int height) + { + using Image image = new(width, height, new Rgba32(200, 200, 200)); + Point dark = new(width / 2, height / 2); + image[dark.X, dark.Y] = new Rgba32(10, 10, 10); + + image.Mutate(img => img.AdaptiveThreshold()); + + AssertOnlyDarkPixelIsLower(image, new Rectangle(0, 0, width, height), dark); + } + + [Fact] + public void AdaptiveThreshold_NarrowRectangle_SeparatesDarkPixel() + { + using Image image = new(100, 100, new Rgba32(200, 200, 200)); + Rectangle rectangle = new(10, 10, 8, 50); + Point dark = new(14, 30); + image[dark.X, dark.Y] = new Rgba32(10, 10, 10); + + image.Mutate(img => img.AdaptiveThreshold(Color.White, Color.Black, rectangle)); + + AssertOnlyDarkPixelIsLower(image, rectangle, dark); + } + [Theory] [WithFile(TestImages.Png.Bradley02, PixelTypes.Rgba32)] public void AdaptiveThreshold_WithRectangle_Works(TestImageProvider provider) @@ -125,4 +154,27 @@ public void AdaptiveThreshold_WithRectangle_Works(TestImageProvider + /// Asserts that a thresholded region of uniform light pixels with a single dark pixel + /// maps the dark pixel to black and every other pixel to white. + /// + /// The thresholded image. + /// The region that was thresholded. + /// The location of the dark pixel. + private static void AssertOnlyDarkPixelIsLower(Image image, Rectangle region, Point dark) + { + Rgba32 black = Color.Black.ToPixel(); + Rgba32 white = Color.White.ToPixel(); + + // The last column and row are skipped. With the smallest cluster size their averaging windows hold + // no pixels, so they always take the lower color, matching regions 16 to 31 pixels wide. + for (int y = region.Top; y < region.Bottom - 1; y++) + { + for (int x = region.Left; x < region.Right - 1; x++) + { + Assert.Equal(x == dark.X && y == dark.Y ? black : white, image[x, y]); + } + } + } }