Binarization

edit

Threshold makes every frame black and white, in place: a pixel whose luminance is greater than its threshold becomes ThresholdOptions.UpperColor (white by default), and any other pixel LowerColor (black by default): a scanned document, a mask, or the input of a bilevel TIFF page or a portable bitmap.

Binarize a scanned page

public static void SaveScanAsBilevel(string inputPath, string tiffPath, string pbmPath)
{
    using var page = Image.Load(inputPath);

    // Sauvola's threshold follows uneven lighting: each pixel is compared with the mean and the contrast of the
    // 31 x 31 pixels around it, so the text becomes black and the paper white, even in a shadow. The pixels are now
    // exactly black or white, in the same pixel format.
    page.Threshold(new ThresholdOptions { Method = ThresholdMethod.Sauvola, WindowSize = 31 });

    // A bilevel page with Group 4 compression, the usual format of scanned documents
    page.Save(tiffPath, new TiffEncoder { PixelLayout = TiffPixelLayout.Bilevel, Compression = TiffCompression.CcittGroup4 });

    // A portable bitmap: one bit per pixel
    page.Save(pbmPath, new PnmEncoder { PixelLayout = PnmPixelLayout.Bitmap });
}

The pixel format is kept: the pixels are only black or white, which is what the bilevel layouts of the TIFF and PNM encoders require. These encoders never threshold an image themselves: they accept only pixels that are exactly black or white. See TIFF and Netpbm.

Choose the threshold

ThresholdMethodThresholdUse it for
Otsu (default)The level that best separates the two classes of the luminance histogram of the frameImages with an even lighting and two clear groups of tones
FixedLevel, a fraction of the range (0.5 by default: 128 and above become white on 8-bit samples)A threshold you know
MeanThe mean of the WindowSize square around each pixel, minus OffsetUneven lighting
SauvolaA threshold computed from the mean and the contrast of the window around each pixel (SauvolaK, SauvolaR)Documents: a blank background stays blank, even in a shadow

Pixel formats

The luminance is the Rec. 709 luma that Grayscale stores, at the storage precision. Alpha is ignored, and every pixel gets one of the two colors, alpha included: the pixel format must store them exactly, or Threshold throws an ArgumentException. Floating-point formats have no discrete levels and throw an UnsupportedImageFeatureException: convert them to Rgba64 first. The results are the same on every platform.

Dither to black and white

A threshold makes every gray area all black or all white. To render the gray levels as patterns of black and white pixels instead, quantize to a palette of the two colors with dithering (see Color reduction):

public static void DitherToBlackAndWhite(string inputPath, string outputPath)
{
    using var image = Image.Load(inputPath);

    // Only the colors of the palette are used. Error diffusion turns the gray levels into patterns of black and white
    // pixels, where a threshold would make them all black or all white
    var blackAndWhite = new ImagePalette([Colors.Black, Colors.White]);
    image.Quantize(new QuantizeOptions { Palette = blackAndWhite, Dithering = DitheringMode.FloydSteinberg });
    image.Save(outputPath);
}