bitmap
bitmap copied to clipboard
How to create monochrome
Thanks for the fixes that resolved the previous issue I raised. It seems to be working OK. This is more of a question than an issue per-se.
Now that I have a bitmap image that is 384 pixels wide and however many pixels long I need to convert it to a format that I can send to a thermal printer that understands "black dot" and "no black dot".
Once the image is converted then I can send it one row at a time to the printer using its own slightly odd control codes.
Is there a more intelligent way to process the colour information so that some of the detail isn't lost?
-Andy.
It seems to be your lucky day, because I did write some code once to display a graphic on a monochrome e-Ink display, which I assume must be quite similar to a thermal printer.
I dug out the code and added it to the test program I created yesterday: issue11.zip
You can indeed convert the colour information to preserve some of the detail through a process called dithering. There are several algorithms to do this, and the attached code implements these:
- Threshold - The pixel is converted to a greyscale value between 0 and 255. If it is less than 128 then a black is output, otherwise a white pixel is output. This is not intelligent at all.
- Floyd–Steinberg dithering is what is known as an error-diffusion algorithm: It compares the input pixel to the available output colours (black or white in this case) and chooses the closest one. It then computes the error (the difference between the colour it chose and the colour it wanted) and spreads that error to the pixels next to it, so that when it computes the output for those pixels it takes the error it made into account.
- Ordered dithering is a bit more difficult to explain. Basically it adds values from a special grid of values to each pixel. The values in the grid are chosen through this Bayer's algorithm to essentially say "let's pretend this first pixel is slightly darker than it actually is; let's pretend the next pixel is slightly lighter than it actually is..." and so on, which has the effect of preserving more of the details from the original image.
Larger Bayer matrices preserve more details. I provide a 4×4 and 8×8 matrix.
This is the output of the Floyd-Steinberg algorithm on the test image:
I think it produces the best results, but those stipple lines cause some artefacts due to the way the error is diffused through the image.
This is the output of the 8×8 Bayer algorithm on the test image:
The lines in the Bayer version are lost. I think it would work better if they were thicker and/or darker.
In the attached code there is a function epd_load_img()
that implements all three algorithms (the global variable dither_mode
at the top of the file which determines which one is used).
It converts the image to black and white, and stores the results in an array of bytes (chars) called bitbuffer
. Each bit represents one pixel (0 for black and 1 for white) so each byte represents 8 pixels.
In each algorithm's implementation, there is a line that looks like bitbuffer[bi] |= mask;
surrounded by code that compares the greyscale value to the threshold. This uses the bitwise OR to set a bit in byte bi
of bitbuffer
which means that pixel should be white. This might be where you want to modify the code to write a pixel to your thermal printer - I'm not too familiar with their interfaces.
Alternatively, you might want to look at the epd_buf_as_bmp()
function at the bottom of the file that converts the bitbuffer()
back to a Bitmap
object to see how it interprets the pixel values, and write your thermal printer interface from there.
Thanks for all the information. I have implemented this in my application and just trying to debug the last stage of sending the bitbuffer to printer.
Where do some of the magic numbers in the code come from:
static int bayer8x8[64] = { /*(1/65)*/
1, 49, 13, 61, 4, 52, 16, 64,
33, 17, 45, 29, 36, 20, 48, 32,
9, 57, 5, 53, 12, 60, 8, 56,
41, 25, 37, 21, 44, 28, 40, 24,
3, 51, 15, 63, 2, 50, 14, 62,
35, 19, 47, 31, 34, 18, 46, 30,
11, 59, 7, 55, 10, 58, 6, 54,
43, 27, 39, 23, 42, 26, 38, 22,
};
// Convert image to grayscale
int c = (2126 * R + 7152 * G + 722 * B)/10000;
int threshold = 179 * 256;
-Andy.
Hi Andy,
You'll have to excuse some of the code. I wrote it a while back and cannot remember all the details, and a lot of it was experimental because I was also learning these concepts for the first time.
I copied the 4×4 and 8×8 Bayer matrices from somewhere off the internet, though I see I didn't write down the source.
It is normally generated by an algorithm that recursively builds a larger matrix from smaller matrices. The Wikipedia article describes how the algorithm works, but for my purposes it was simpler to just copy one that seemed to work off the internet. You'll see that my 8×8 looks a bit like the Wikipedia one if you rotate it and add 1 to each element.
A problem I had with the Bayer method is that the resulting image tends to look a bit brighter than the original. It happens because you're adding the values from the matrix to your greyscale colour value. If you then compare it to a threshold of 128 (which is half-way between 0 and 255) then more pixels end up white than would've if you didn't add anything from the matrix.
So this line
int threshold = 179 * 256; // 128, 162, 196
just uses a slightly higher value for the threshold. I just played around with values (the ones in the comments) until I found one that looked good (for my use-case of showing a picture on a e-Ink display; you might have better results with different values).
If I recall correctly, all the * 256
you see in the code is just to compensate for the fact that the code is using integers. Looking at it now, I am not really sure what the purpose is or why I chose 256. The only thing I can point to is that the lines
c = c * 256 + bayer4x4[(x & 0x03) + ((y & 0x03) << 2)] * 128;
int threshold = 179 * 256; // 128, 162, 196
if(c >= threshold) ...
is equivalent to
if(c * 256 + bayer4x4[(x & 0x03) + ((y & 0x03) << 2)] * 128 >= 179 * 256) ...
which if you divide by 256 on both sides, reduces to
if(c + bayer4x4[(x & 0x03) + ((y & 0x03) << 2)] / 2 >= 179) ...
and I probably didn't want the / 2
in there because I'm working with integers.
Actually, having typed out the above, I see that that / 2
might be a bug - if it was /4
then it would correspond to the 1/64
of the Wikipedia article (the Wikipedia article uses values between 0.0 and 1.0, whereas I use values between 0 and 255). That might also explain why my image seemed to come out too bright.
The formula for converting to greyscale comes from the Wikipedia article
int c = (2126 * R + 7152 * G + 722 * B)/10000;
Again, that particular conversion worked well for my use-case of converting a photo to greyscale (apparently the human eye is much more sensitive to green which is why it gives such a high weighting to the G component and a low weighting to the B component), but your mileage may vary.