Skip to content

AdaptiveThreshold turns every pixel black when the image or rectangle is narrower than 16 pixels #3241

Open

Description

@jafin

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 DEBUG and RELEASE mode
  • 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions