Smoothing of fonts with 8-bit color per point

8

I am using uGui to display fonts on the screen. But the final result is not as satisfactory as it should be. The hardware is quite limited: color of 8 bpp (256 colors in total), with 3 bits for red, 3 bits for green, and 2 bits for blue (3: 3: 2).

The original code to obtain the color of a point is:

color = (((fc & 0xFF) * b + (bc & 0xFF) * (256 - b)) >> 8) & 0xFF |//Blue component
        (((fc & 0xFF00) * b + (bc & 0xFF00) * (256 - b)) >> 8)  & 0xFF00|//Green component
        (((fc & 0xFF0000) * b + (bc & 0xFF0000) * (256 - b)) >> 8) & 0xFF0000; //Red component

Being:

  • fc : uint32_t , the color of the ink for that character.
  • bc : uint32_t , the color of the background for that character.
  • b : uint8_t , percentage of ink / background mix for a specific point of the character.

That last data is taken from the source file, previously rendered (using an external utility) and converted to array[] of unsigned char , in which each element represents the ink / background ratio of the point.

The results of the original code are quite satisfactory:

But several artifacts of color are appreciated (especially in A ). If we do zoom ...

In an attempt on my part to limit or eliminate this, and taking into account that only certain colors are used for the text / background, it occurred to me to artificially limit the number of possible colors to apply for the smoothing : for example, if the ink is black, use only the gray color for points that are not ink or background.

The code stayed like this:

// Calculamos el color para el antialiasing.
UG_COLOR acolor; // unsigned char
switch( fc ) {
case 0x92: // GRAY
  acolor = 0xDB;
  break;
case 0x1C: // GREEN
  acolor = 0x5E;
  break;
case 0xF0: // ORANGE
  acolor = 0x90;
  break;
case 0xDB: // SEMI_WHITE
  acolor = 0xFF;
  break;
default: // BLACK
  acolor = 0xB6; // GRAY;
  break;
}

color = (((fc & 0xFF) * b + (bc & 0xFF) * (256 - b)) >> 8) & 0xFF |//Blue component
        (((fc & 0xFF00) * b + (bc & 0xFF00) * (256 - b)) >> 8)  & 0xFF00|//Green component
        (((fc & 0xFF0000) * b + (bc & 0xFF0000) * (256 - b)) >> 8) & 0xFF0000; //Red component

// Si no vamos a pintar ni con la tinta ni con el fondo, lo hacemos
// con el color para el antialiasing.
if( ( color != fc ) && ( color != bc ) ) color = acolor;

The result was ... different :

It is appreciated that the points of inappropriate have almost disappeared, but the general quality ... leaves a lot to be desired.

What is the correct way to perform this type of smoothing? What bit combination / operation should I perform?

Note: C or C ++, bit operations are the same in both.

EDITO

In response to @abufalia.

To paint a pixel on the screen, a unsigned char is used:

*((unsigned char *)(posicion-en-memoria)) = color;

With color being a UG_COLOR, which in turn is a uint32_t .

In all paint text operations, the value of fc and bc is limited to 8 bits:

const UG_COLOR BLACK      = 0X00;
const UG_COLOR GRAY       = 0x92;
const UG_COLOR SEMI_WHITE = 0xDB;
const UG_COLOR WHITE      = 0XFF;
const UG_COLOR GREEN      = 0X1C;
const UG_COLOR ORANGE     = 0XF0;

To get the percentage of the mix, use:

b = font->p[index++];

being:

typedef struct {
   unsigned char* p;
   FONT_TYPE font_type;
   UG_S16 char_width;
   UG_S16 char_height;
   UG_U16 start_char;
   UG_U16 end_char;
   UG_U8  *widths;
} UG_FONT;
    
asked by Trauma 10.05.2018 в 12:09
source

3 answers

8

I do not know ... but that function has certain deficiencies or I have not understood something of the question:

using Color = unsigned int;

Color color(Color fc, Color bc, unsigned char b)
{
  return (((fc & 0xFF) * b + (bc & 0xFF) * (256 - b)) >> 8) & 0xFF |//Blue component
         (((fc & 0xFF00) * b + (bc & 0xFF00) * (256 - b)) >> 8)  & 0xFF00|//Green component
         (((fc & 0xFF0000) * b + (bc & 0xFF0000) * (256 - b)) >> 8) & 0xFF0000; //Red component
}

int main(int, char **)
{
  Color fc = 0xFF;
  Color bc = 0x00;
  unsigned char b = 255;

  std::cout << std::hex
            << color(fc,bc,255) << '\n'
            << color(fc,bc,125) << '\n'
            << color(fc,bc,0)   << '\n';
}

The output of the program is:

fe
7c
0

I do not know but unless I have understood something wrong, I am not convinced by these results:

  • The first thing that draws attention is that the function works with 1 byte for each component ... when 1 byte is what the color itself must occupy (the three components) ... in fact, this is reflected in the switch ( fc is defined in 1 byte)
  • the first result starts badly ... loses a bit of blue like that
  • the second goes completely ... has no blue component and should be gray. It draws attention to the fact that the G component is full ... ( 7c = 011 111 00 ). Here I would rather expect a value similar to 6e ( 011 011 10 ) or 6d .
  • the third value is the only one that matches ... it is the background color as is.

That function does not seem to be very fine since it invents colors ... which coincidentally is what happens to you.

I would start by replacing that algorithm with a bit more predictable:

typedef unsigned Color;

unsigned GetR(Color color)
{
  return color >> 5;
}

unsigned GetG(Color color)
{
  return (color >> 2) & 0x07;
}

unsigned GetB(Color color)
{
  return color & 0x03;
}

Color color1(Color fc, Color bc, unsigned char b)
{
  unsigned pfc = b;
  unsigned pbc = 255-b;

  Color color = (((GetR(fc) * pfc + GetR(bc) * pbc) / 255) << 5)
              | (((GetG(fc) * pfc + GetG(bc) * pbc) / 255) << 2)
              |  ((GetB(fc) * pfc + GetB(bc) * pbc) / 255);

  return color;
}

ADDED BY THE AUTHOR OF THE QUESTION

Since there is no way to test it without the proper hardware, I place here the results of this excellent answer:

Although they are similar to those originally generated, they are darker in general, which makes those apparently incorrect points barely noticeable; in fact, in the real display (remember that, to show it on a PC, you have to map from 8bpp to 24bpp, that makes the colors are not exact), the results are nothing short of perfect.

    
answered by 10.05.2018 / 13:21
source
5
  

What is the correct way to perform this type of smoothing?

I can not say 100% but I think the formula you use is correct, what is not correct is the treatment of color.

I understand that if you are mixing black font color with white background, the result should be gray; the gray color has the particularity that all its RGB components have the same value, for example 50% black and white , 75% white 25% black , 25% white 75% black ...

Your color structure is r3g3b2 but the treatment you do is r8g8b8, so if I pass these values:

  • fc : 0x00000000 (all bits to 0: black).
  • bc : 0xffffffff (all bits to 1: white).
  • b : 0x7f (half the maximum of uint8_t : 50%).

With your formula I get the value 128, which if we interpret it as r3g3b2 is 57.14% red 0% green and 0% blue , that is: red.

What you need is to treat the components as r3g3b2, for this I suggest you create some functions that allow you to obtain each component (C ++ code):

using color = std::uint8_t;

constexpr color R(color c) { return (c & 0b11100000) >> 5; } // 3 bits de mayor peso: R
constexpr color G(color c) { return (c & 0b00011100) >> 2; } // bits 2-4: G
constexpr color B(color c) { return (c & 0b00000011); }      // bits 0-1: B

I do not know exactly how the alpha blend works, but having these functions the operation could look like this:

color blend(color fc, color bc, color b)
{
    const float font = b / 255.f;         // % de color de fuente
    const float background = 1.f - font;  // % de color de fondo

    std::uint8_t red = (R(fc) * font) + (R(bc) * background);
    std::uint8_t green = (G(fc) * font) + (G(bc) * background);
    std::uint8_t blue = (B(fc) * font) + (B(bc) * background);

    /* R será los bits 5 a 7
       G será los bits 2 a 4
       B será los bits 0 a 1 */
    return (red << 5) | (green << 2) | blue;
}

The algorithm may not work with colors other than gray (mix red and blue should be fuzzy).

    
answered by 10.05.2018 в 14:21
4

I think the sizes of the data types in which you store colors are unclear.

On the one hand you say that fc and bc are of type uint32_t , which induces to think that they use 8 bits for each component, that is, it is RGB of 24 bits (and that therefore the target would be stored as 0xFFFFFF ).

In fact, the function that computes the color mix between fc and bc that you have provided is written assuming that both colors are RGB with 8 bits per component (see the masks you apply, such as 0xFF , 0xFF00 and 0xFF0000 ).

However, in the switch that you put later, it seems that in reality each color is 8 bits, because for example 0xF0 is labeled "orange".

If this were the case, then the answer from @eferion would be the one that correctly computes the color mix, with masks that use the cast (3: 3: 2) of bits per component.

It could also be the case that the ink and background colors really come to you in 24 bits, so your formula would be correct. In this case the color 0xFF does not represent white (as assumed by @eferion), but a very bright green ( 0x0000FF ), and the result of the formula for decreasing values of b would be fine, giving increasingly darker greens ( 0x0000FE , 0x00007C , 0x000000 ).

But since the formula produces a 24-bit RGB color and your display handles 8-bit colors, there would still be an extra step in which those 24 bits become 8 (3: 3: 2) and it could be that out that step who does it the wrong way. If this were the case, we would not be seeing that step. Maybe the hardware does it.

You should clarify in what format the colors of ink and paper come from (if in 8 or 24 bits), and in what format you should generate the resulting color (if in 8 or 24 bits).

    
answered by 10.05.2018 в 13:44