wernsey / bitmap

A C module for manipulating bitmap/raster graphics
MIT No Attribution
70 stars 12 forks source link

How to create monochrome #11

Closed sdc-andy closed 9 months ago

sdc-andy commented 2 years ago

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.

wernsey commented 2 years ago

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:

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: errordiff

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: bayer8

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.

sdc-andy commented 2 years ago

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.

wernsey commented 2 years ago

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.