Converting a bitmap to monochrome
Asked Answered
D

2

10

I am trying to save an image as monochrome (black&white, 1 bit-depth) but I'm coming up lost how to do it.

I am starting with a png and converting to a bitmap for printing (it's a thermal printer and only supports black anyway - plus its slow as hell for large images if I try to send them as color/grayscale).

My code so far is dead simple to convert it to a bitmap, but it is retaining the original colour depth.

Image image = Image.FromFile("C:\\test.png");

byte[] bitmapFileData = null;
int bitsPerPixel = 1;
int bitmapDataLength;

using (MemoryStream str = new MemoryStream())
{
    image.Save(str, ImageFormat.Bmp);
    bitmapFileData = str.ToArray();
}
Diffuser answered 13/7, 2012 at 14:31 Comment(6)
#4669817Undergo
possible duplicate of convert image to Black-White or Sepia in c#Multinuclear
Actually, it seems to me those questions are about converting to grayscale - whereas the OP wants to convert it to 1 BPP monochrome, which involves thresholding/dithering.Dialyse
You might want to take a look at Floyd-Steinberg dithering (en.wikipedia.org/wiki/Floyd%E2%80%93Steinberg_dithering), which will allow you to convert an image to a 1-bit/pixel image which looks (in my opinion) quite good. You'll probably want to convert the colour input pixels into greyscale first by using using a method such as the one in the answers linked by other commenters.Depersonalization
It isn't going to be any faster with a 1bpp image. You'll need to bypass the printer driver so the printer isn't operated in graphics mode and send printer commands directly. You'll need the printer's programming manual to know the correct command. And this support.microsoft.com/kb/322091Peake
@HansPassant While your point is valid in some cases, it is quite possible that the printer driver examines the bitmap's bit depth and issues the correct command by itself. This is what I would expect and can be confirmed by provided the printer driver 1-bit and 8-bit images and timing the print.Dialyse
D
13

Here's some code I put together that takes a full colour (24 bits/pixel) image, and converts it to a 1 bit/pixel output bitmap, applying a standard RGB to greyscale conversion, and then using Floyd-Steinberg to convert greyscale to the 1 bit/pixel output.

Note that this should by no means be considered an "ideal" implementation, but it does work. There are a number of improvements that could be applied if you wanted. For example, it copies the entire input image into the data array, whereas we really only need to keep two lines in memory (the "current" and "next" lines) for accumulating the error data. Despite this, performance seems acceptable.

public static Bitmap ConvertTo1Bit(Bitmap input)
{
    var masks = new byte[] { 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 };
    var output = new Bitmap(input.Width, input.Height, PixelFormat.Format1bppIndexed);
    var data = new sbyte[input.Width, input.Height];
    var inputData = input.LockBits(new Rectangle(0, 0, input.Width, input.Height), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
    try
    {
        var scanLine = inputData.Scan0;
        var line = new byte[inputData.Stride];
        for (var y = 0; y < inputData.Height; y++, scanLine += inputData.Stride)
        {
            Marshal.Copy(scanLine, line, 0, line.Length);
            for (var x = 0; x < input.Width; x++)
            {
                data[x, y] = (sbyte)(64 * (GetGreyLevel(line[x * 3 + 2], line[x * 3 + 1], line[x * 3 + 0]) - 0.5));
            }
        }
    }
    finally
    {
        input.UnlockBits(inputData);
    }
    var outputData = output.LockBits(new Rectangle(0, 0, output.Width, output.Height), ImageLockMode.WriteOnly, PixelFormat.Format1bppIndexed);
    try
    {
        var scanLine = outputData.Scan0;
        for (var y = 0; y < outputData.Height; y++, scanLine += outputData.Stride)
        {
            var line = new byte[outputData.Stride];
            for (var x = 0; x < input.Width; x++)
            {
                var j = data[x, y] > 0;
                if (j) line[x / 8] |= masks[x % 8];
                var error = (sbyte)(data[x, y] - (j ? 32 : -32));
                if (x < input.Width - 1) data[x + 1, y] += (sbyte)(7 * error / 16);
                if (y < input.Height - 1)
                {
                    if (x > 0) data[x - 1, y + 1] += (sbyte)(3 * error / 16);
                    data[x, y + 1] += (sbyte)(5 * error / 16);
                    if (x < input.Width - 1) data[x + 1, y + 1] += (sbyte)(1 * error / 16);
                }
            }
            Marshal.Copy(line, 0, scanLine, outputData.Stride);
        }
    }
    finally
    {
        output.UnlockBits(outputData);
    }
    return output;
}

public static double GetGreyLevel(byte r, byte g, byte b)
{
    return (r * 0.299 + g * 0.587 + b * 0.114) / 255;
}
Depersonalization answered 16/7, 2012 at 11:11 Comment(0)
D
4

What you want is a good dithering algorithm like Floyd-Steinberg or Bayer ordered. You can either implement the binarization yourself or use a library like AForge.NET to do it for you (download the image processing samples). You can find the binarization documentation here.

Dialyse answered 13/7, 2012 at 14:47 Comment(1)
This depends on your goal.... if you want an image that closely replicates the original color image but in greyscale, for the human eye, then yes you want dithering. If you are trying to store the least amount of data (greyscale is 1/4th the data of full RGB color with Alpha) for the purpose of comparative analysis then you probably don't want any dithering as it's changing the image.Tunisia

© 2022 - 2024 — McMap. All rights reserved.