Android: Converting a Bitmap to a Monochrome Bitmap (1 Bit per Pixel)
Asked Answered
D

5

26

I want to print a Bitmap to a mobile Bluetooth Printer (Bixolon SPP-R200) - the SDK doesn't offer direkt methods to print an in-memory image. So I thought about converting a Bitmap like this:

Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);

To a Monochrome Bitmap. I am drawing black text on above given Bitmap using a Canvas, which works well. However, when I convert the above Bitmap to a ByteArray, the printer seems to be unable to handle those bytes. I suspect I need an Array with one Bit per Pixel (a Pixel would be either white = 1 or black = 0).

As there seems to be no convenient, out of the box way to do that, one idea I had was to use:

bitmap.getPixels(pixels, offset, stride, x, y, width, height)

to Obtain the pixels. I assume, I'd have to use it as follows:

int width = bitmap.getWidth();
int height = bitmap.getHeight();

int [] pixels = new int [width * height];
bitmap.getPixels(pixels, 0, width, 0, 0, width, height);

However - I am not sure about a few things:

  • In getPixels - does it make sense to simply pass the width as the "Stride" argument?
  • I guess I'd have to evaluate the color information of each pixel and either switch it to black or white (And I'd write this value in a new target byte array which I would ultimately pass to the printer)?
  • How to best evaluate each pixel color information in order to decide that it should be black or white? (The rendered Bitmap is black pain on a white background)

Does this approach make sense at all? Is there an easier way? It's not enough to just make the bitmap black & white, the main issue is to reduce the color information for each pixel into one bit.

UPDATE

As suggested by Reuben I'll first convert the Bitmap to a monochrome Bitmap. and then I'll iterate over each pixel:

    int width = bitmap.getWidth();
    int height = bitmap.getHeight();

    int[] pixels = new int[width * height];
    bitmap.getPixels(pixels, 0, width, 0, 0, width, height);

    // Iterate over height
    for (int y = 0; y < height; y++) {
        int offset = y * height;
        // Iterate over width
        for (int x = 0; x < width; x++) {
            int pixel = bitmap.getPixel(x, y);
        }
    }

Now Reuben suggested to "read the lowest byte of each 32-bit pixel" - that would relate to my question about how to evaluate the pixel color. My last question in this regard: Do I get the lowest byte by simply doing this:

// Using the pixel from bitmap.getPixel(x,y)
int lowestByte = pixel & 0xff;
Dickson answered 21/2, 2012 at 12:38 Comment(3)
I have this QA starred because I was working on a similar concept with regards to programmatically manipulating arrays to become monochrome bitmaps. I have a question & its eventual answer here: #17919478 If this helps people, please email me and I'll come up with a suitable answer to this question.Coprolite
Why are you using GetPixels to load all the pixels into a single array... Then nested looping through the bitmap AGAIN calling individual GetPixel calls (the least efficient way possible)? Just loop through your original array, then use SetPixels to push the entire array back into the bitmap.Indian
@ClintStLaurent good point - the code is really old xD - I don't even think we're using it in this form anymore, but as you pointed out correctly, it is very inefficient the way it's written there. Feel free to edit it according to your suggestion.Dickson
A
33

You can convert the image to monochrome 32bpp using a ColorMatrix.

Bitmap bmpMonochrome = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bmpMonochrome);
ColorMatrix ma = new ColorMatrix();
ma.setSaturation(0);
Paint paint = new Paint();
paint.setColorFilter(new ColorMatrixColorFilter(ma));
canvas.drawBitmap(bmpSrc, 0, 0, paint);

That simplifies the color->monochrome conversion. Now you can just do a getPixels() and read the lowest byte of each 32-bit pixel. If it's <128 it's a 0, otherwise it's a 1.

Albur answered 21/2, 2012 at 12:52 Comment(11)
Thank you very much for your answer! Just to clarify the pixel evaluation I updated my question (below UPDATE) essentially: do I get the lowest byte of a 32-bit int by doing this: int lowestByte = pixel & 0xff; ?Dickson
Yes. Actually bit 7 of the lowest byte is exactly the bit you want for your 1bpp pixel. The expression '(pixel & 0x80) >> 7' will give you a '0' or '1' for that.Albur
On an additional note: Can it be, that the byte array I produce is not readable by android? Using BitmapFactory.decodeByteArray(imageInBytes, 0, imageInBytes.length) returns no usable Bitmap (to display in android), and the printer doesn't like it neither...Dickson
Well firstly, Android doesn't support 1bpp bitmaps, and secondly BitmapFactory.decode* exists to decode PNG & JPEG formats. You will need to refer to the printer's documentation and work out exactly what it does support.Albur
Thanks for the clarification - jepp the printers SDk is a bit messy :( it can print images from the web & file system but it can't print in-memory images, that's why I thought about sending the bits directly... I'll keep trying :)Dickson
If it can "print images from the web" then it may be that the driver has built-in support for image formats such as PNG and/or JPEG. Try using Bitmap.compress() with a ByteArrayOutputStream, get the compressed bytes with toByteArray() and see if it prints.Albur
Thanks for the hint! I was considering sending the images via a socket, however the SDK states, that it does not support 32 bit image depths so I'd have to scale down the image to an Image of 1Bit depth. We're doing it already in c# code where we send it bit-wise via a raw socket stream, but I am really trying to avoid that and I'd rather use the SDK ...Dickson
I decompiled the SDK (using JD Gui) and they do indeed have a method for converting Bitmaps to proper byte arrays :) and I am surprised how well it reads - I remember a time, when decompiled code looked like a big mess ... I'll give it a shot with this one.Dickson
I believe I'm trying to do the same thing here. Is there a proper way of converting the resulting int[] into a byte[] for a 1bpp bitmap? I can only think shift 1s and 0s into bytes as I iterate through the int[]. Haven't figured much else.Snowfield
@dispake: Did you try the approach u mentioned? If yes did it work out? If no let me know and I'll try to think abt something...Dickson
so after converting it to black and white will image size reduced ? (size in kb)Spice
P
13

Well I think its quite late now to reply to this thread but I was also working on this stuff sometimes back and decided to build my own library that will convert any jpg or png image to 1bpp .bmp. Most printers that require 1bpp images will support this image (tested on one of those :)). Here you can find library as well as a test project that uses it to make a monochrome single channel image. Feel free to change it..:)

https://github.com/acdevs/1bpp-monochrome-android

Enjoy..!! :)

Phanotron answered 20/3, 2013 at 9:19 Comment(1)
Thanks for the effort! I am sure it'll be helpful to others and I'll go take a look at the project you posted as well :) .Dickson
T
11

You should convert each pixel into HSV space and use the value to determine if the Pixel on the target image should be black or white:

  Bitmap bwBitmap = Bitmap.createBitmap( bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.RGB_565 );
  float[] hsv = new float[ 3 ];
  for( int col = 0; col < bitmap.getWidth(); col++ ) {
    for( int row = 0; row < bitmap.getHeight(); row++ ) {
      Color.colorToHSV( bitmap.getPixel( col, row ), hsv );
      if( hsv[ 2 ] > 0.5f ) {
        bwBitmap.setPixel( col, row, 0xffffffff );
      } else {
        bwBitmap.setPixel( col, row, 0xff000000 );
      }
    }
  }
  return bwBitmap;
Templar answered 28/7, 2016 at 11:37 Comment(1)
Thanks, this was a cool implementation.Valkyrie
M
5

Converting to monochrome with exact the same size as the original bitmap is not enough to print.

Printers can only print each "pixel" (dot) as monochrome because each spot of ink has only 1 color, so they must use much more dots than enough and adjust their size, density... to emulate the grayscale-like feel. This technique is called halftoning. You can see that printers often have resolution at least 600dpi, normally 1200-4800dpi, while display screen often tops at 200-300ppi.

Halftone

So your monochrome bitmap should be at least 3 times the original resolution in each side.

Model answered 2/8, 2013 at 7:4 Comment(3)
You mean dithering by any chance? I had dithering already integrated into my code :)Dickson
No. Dithering is a comletely different business. In 8-bit or more grayscale bitmap there's 255 (or more, depending on how many bits you use) levels of grey, so the original resolution is fine. But if you use monochrome bitmap then almost all details will be lost significantly because you're reducing from more than 16.7 million colors to only 2. Halftoning increases resolution and uses extra pixels to give the effect of grayscale as a way to pass this limitation. You can reference the link I gave above.Model
Ah, thanks for clearing that up :) ! In my particular use-case I already draw on a canvas in Black & white. However it can also happen that we print photographs, so for these cases half-toning might really come in handy - thanks again :) !Dickson
E
0

I think that keep the color will produce better 1 bit pixel image; I have commented the high contract filter and gray matrix, if want to try.

I don't have benchmark the code for speed, the objective is sending data to POS printer, 96 pixel (screen) can be converted to 203 pixel (printer)

if you prefix the image with BMP header (62 bytes), maybe can be converted to BMP format.

I'm using C# with SkiaSharp, but can be useful for java code.

    static int WidthBytes(int bits)
    {
        return ((((bits) + 31) / 32) * 4);
    }
    
    internal static byte[] GetByteArray(SKBitmap bmp, bool reverseBlack, out SKSize size, float dpi = 203f)
    {
        var bytesPerPixel = bmp.BytesPerPixel;
        var ratio = dpi / 96f;
        //var ratio = 1.0f;
        var nHeight = (int)Math.Round(bmp.Height * ratio);
        var prnWidth = (int)Math.Round(bmp.Width * ratio);
        var remain = prnWidth % 8;
        if (remain > 0) prnWidth += (8 - remain);
        var bitWidth = WidthBytes(prnWidth);
        var nWidth = bitWidth - (prnWidth % bitWidth);
        nWidth += prnWidth;
        //var grayScale = new float[]
        //        {
        //            0.21f, 0.72f, 0.07f, 0, 0,
        //            0.21f, 0.72f, 0.07f, 0, 0,
        //            0.21f, 0.72f, 0.07f, 0, 0,
        //            0,     0,     0,     1, 0
        //        };
        //var colorFilter = SKColorFilter.CreateColorMatrix(grayScale);
        //SKHighContrastConfig config = new SKHighContrastConfig(true, SKHighContrastConfigInvertStyle.NoInvert, 0.2f);
        //var colorFilter = SKColorFilter.CreateHighContrast(config);
        var bmp1 = new SKBitmap(nWidth, nHeight, SKColorType.Bgra8888, SKAlphaType.Opaque);
        var canvas = new SKCanvas(bmp1);
        using (SKPaint paint = new SKPaint())
        {
            var backColor = reverseBlack ? SKColors.Black : SKColors.White;
            paint.Style = SKPaintStyle.Fill;
            //paint.ColorFilter = colorFilter;
            canvas.Clear(backColor);
            //paint.Color = reverseBlack ? SKColors.White: SKColors.Black;
            var dest = new SKRect(0, 0, prnWidth, nHeight);
            canvas.DrawBitmap(bmp, dest, paint);
        }
        bmp = bmp1;
        ///var ratio = 96.0f / 25.4f;
        //size = new SKSize((int)Math.Round(bmp.Width * ratio), (int)Math.Round(bmp.Height * ratio));
        size = new SKSize(bmp.Width, bmp.Height);
        //
        var stride = bmp.RowBytes;
        var width = bmp.Width;
        var pixelLen = stride / width;
        var height = bmp.Height;
        int bytes = Math.Abs(stride) * height;
        //var raw = new byte[bytes];
        IntPtr ptr = bmp.GetPixels();
        //Marshal.Copy(ptr, raw, 0, bytes);
        //var data = new List<byte>();
        // Start Send Lines
        byte[,] dots = new byte[width, height];
        var threshold = 127;
        byte black = 0x1;
        byte white = 0x0;
        if (reverseBlack)
        {
            black = 0x0;
            white = 0x1;
        }
        unsafe
        {
            var pTarget = (uint*)ptr.ToPointer();
            for (int y = 0; y < bmp.Height; y++)
            {
                var rowOffset = y * bmp.Width;
                for (int x = 0; x < bmp.Width; x++)
                {
                    uint c1 = *pTarget;
                    byte[] bites = BitConverter.GetBytes(c1);
                    byte gray;
                    //gray = (byte)(0.299 * bites[0] + 0.587 * bites[1] + 0.114 * bites[2]);
                    gray = (byte)Math.Round((bites[0] + bites[1] + bites[2]) / 3.0);
                    var c2 = white;
                    if (gray < threshold) c2 = black;
                    dots[x, y] = c2;
                    pTarget += 1;
                }
            }
        }
    
        // Save Image
        //SKImage img1 = SKImage.FromBitmap(bmp1);
        //SKData encode1 = img1.Encode(SKEncodedImageFormat.Jpeg, 100);
        //MemoryStream? ms = new MemoryStream();
        //encode1.SaveTo(ms);
        //if (ms != null) CommunityToolkit.Maui.Storage.FileSaver.SaveAsync("BlackWhite.jpg", ms);
    
        //raw = null;
        // Transform bytes in bits
        var buffer = new byte[(width * height) / 8];
        var pos = 0;
        for (int iCol = height - 1; iCol >= 0; iCol -= 1)
        {
            // start: bytes and bits
            for (int iRow = 0; iRow < width; iRow += 8)
            {
                int prnPixel = dots[iRow, iCol] << 7;
                prnPixel += dots[iRow + 1, iCol] << 6;
                prnPixel += dots[iRow + 2, iCol] << 5;
                prnPixel += dots[iRow + 3, iCol] << 4;
                prnPixel += dots[iRow + 4, iCol] << 3;
                prnPixel += dots[iRow + 5, iCol] << 2;
                prnPixel += dots[iRow + 6, iCol] << 1;
                prnPixel += dots[iRow + 7, iCol] << 0;
                buffer[pos] = (byte)prnPixel;
                pos++;
            }
        }
        //var ms = new MemoryStream(buffer);
        //if (ms != null) CommunityToolkit.Maui.Storage.FileSaver.SaveAsync("binarydata.dat", ms);
        //var filename = System.IO.Path.Combine(FileSystem.CacheDirectory, "binarydata.dat");
        //File.WriteAllBytes(filename, buffer);
        return buffer;
    }

[Sample image]

Eating answered 18/6 at 6:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.