Repository navigation
AdaptiveThreshold turns every pixel black when the image or rectangle is narrower than 16 pixels #3241
Description
Prerequisites
- I have written a descriptive issue title
- I have verified that I am running the latest version of ImageSharp
- I have verified if the problem exist in both
DEBUGandRELEASEmode - I have searched open and closed issues to ensure it has not already been reported
ImageSharp version
main @ a73c43d (also present in v4.1.3, same code)
Other ImageSharp packages and versions
None
Environment (Operating system, version and so on)
Windows 11 Pro 10.0.22631, x64
.NET Framework version
.NET 10 (net10.0)
Description
AdaptiveThreshold() gives the lower color to every pixel when the processed region is less than 16 pixels wide. This happens for a small image and also for a narrow rectangle passed to AdaptiveThreshold(upper, lower, rectangle). For example, a light 8x8 image with one dark pixel comes out all black. I expect only the dark pixel to become black, as it does for a 16x16 image.
AdaptiveThresholdProcessor<TPixel>.OnFrameApply sets the cluster size to (byte)Math.Clamp(interest.Width / 16F, 0, 255). This value is 0 for widths below 16. With a cluster size of 0, every averaging window over the integral image is empty (count == 0). The comparison luminance * count <= sum * limit then becomes 0 <= 0, which is always true, so every pixel takes the lower color.
A fix is proposed in #3229.
Steps to Reproduce
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
// 1. Whole image narrower than 16 px.
using Image<Rgba32> small = new(8, 8, new Rgba32(200, 200, 200));
small[4, 4] = new Rgba32(10, 10, 10);
small.Mutate(x => x.AdaptiveThreshold());
Console.WriteLine($"8x8: {CountBlack(small, new Rectangle(0, 0, 8, 8))} black of 64");
// 2. Narrow rectangle inside a larger image.
using Image<Rgba32> large = new(100, 100, new Rgba32(200, 200, 200));
Rectangle rect = new(10, 10, 8, 50);
large[14, 30] = new Rgba32(10, 10, 10);
large.Mutate(x => x.AdaptiveThreshold(Color.White, Color.Black, rect));
Console.WriteLine($"8x50 rectangle: {CountBlack(large, rect)} black of 400");
// For comparison, a 16x16 image with the same content.
using Image<Rgba32> wide = new(16, 16, new Rgba32(200, 200, 200));
wide[8, 8] = new Rgba32(10, 10, 10);
wide.Mutate(x => x.AdaptiveThreshold());
// The last row and column are excluded, as they always take the lower color.
Console.WriteLine($"16x16: {CountBlack(wide, new Rectangle(0, 0, 15, 15))} black of 225");
static int CountBlack(Image<Rgba32> image, Rectangle region)
{
int count = 0;
for (int y = region.Top; y < region.Bottom; y++)
{
for (int x = region.Left; x < region.Right; x++)
{
if (image[x, y].R == 0)
{
count++;
}
}
}
return count;
}Actual output:
8x8: 64 black of 64
8x50 rectangle: 400 black of 400
16x16: 1 black of 225
Expected: the 8x8 image and the 8x50 rectangle behave like the 16x16 image. Only the dark pixel becomes black and the light pixels become white. (With the smallest cluster size, the last row and column of a region always take the lower color. Regions 16 to 31 pixels wide already behave this way.)
Images
No images needed; the repro builds the input in memory.
Activity