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();
}
jb.
  • 1,848
  • 9
  • 27
  • 43
  • http://stackoverflow.com/questions/4669317/how-to-convert-a-bitmap-image-to-black-and-white-in-c – Dmitriy Jul 13 '12 at 14:36
  • possible duplicate of [convert image to Black-White or Sepia in c#](http://stackoverflow.com/questions/4624998/convert-image-to-black-white-or-sepia-in-c-sharp) – ken2k Jul 13 '12 at 14:37
  • 1
    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. – Ani Jul 13 '12 at 14:40
  • You might want to take a look at Floyd-Steinberg dithering (http://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. – Iridium Jul 13 '12 at 14:44
  • 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 http://support.microsoft.com/kb/322091 – Hans Passant Jul 13 '12 at 16:20
  • @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. – Ani Jul 16 '12 at 15:11

2 Answers2

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;
}
Iridium
  • 23,323
  • 6
  • 52
  • 74
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.

Community
  • 1
  • 1
Ani
  • 10,826
  • 3
  • 27
  • 46
  • 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. – TravisO Mar 28 '19 at 21:34