What is a good, optimized C/C++ algorithm for converting a 24-bit bitmap to 16-bit with dithering?

夙愿已清 提交于 2019-11-30 07:24:35

As you mentioned, the Floyd-Steinberg dithering method is popular because it's simple and fast. For the subtle differences between 24-bit and 16-bit color the results will be nearly optimal visually.

It was suggested that I use the sample picture Lena but I decided against it; despite its long history as a test image I consider it too sexist for modern sensibilities. Instead I present a picture of my own. First up is the original, followed by the conversion to dithered RGB565 (and converted back to 24-bit for display).

And the code, in C++:

inline BYTE Clamp(int n)
{
    n = n>255 ? 255 : n;
    return n<0 ? 0 : n;
}

struct RGBTriplet
{
    int r;
    int g;
    int b;
    RGBTriplet(int _r = 0, int _g = 0, int _b = 0) : r(_r), g(_g), b(_b) {};
};

void RGB565Dithered(const BYTE * pIn, int width, int height, int strideIn, BYTE * pOut, int strideOut)
{
    std::vector<RGBTriplet> oldErrors(width + 2);
    for (int y = 0;  y < height;  ++y)
    {
        std::vector<RGBTriplet> newErrors(width + 2);
        RGBTriplet errorAhead;
        for (int x = 0;  x < width;  ++x)
        {
            int b = (int)(unsigned int)pIn[3*x] + (errorAhead.b + oldErrors[x+1].b) / 16;
            int g = (int)(unsigned int)pIn[3*x + 1] + (errorAhead.g + oldErrors[x+1].g) / 16;
            int r = (int)(unsigned int)pIn[3*x + 2] + (errorAhead.r + oldErrors[x+1].r) / 16;
            int bAfter = Clamp(b) >> 3;
            int gAfter = Clamp(g) >> 2;
            int rAfter = Clamp(r) >> 3;
            int pixel16 = (rAfter << 11) | (gAfter << 5) | bAfter;
            pOut[2*x] = (BYTE) pixel16;
            pOut[2*x + 1] = (BYTE) (pixel16 >> 8);
            int error = r - ((rAfter * 255) / 31);
            errorAhead.r = error * 7;
            newErrors[x].r += error * 3;
            newErrors[x+1].r += error * 5;
            newErrors[x+2].r = error * 1;
            error = g - ((gAfter * 255) / 63);
            errorAhead.g = error * 7;
            newErrors[x].g += error * 3;
            newErrors[x+1].g += error * 5;
            newErrors[x+2].g = error * 1;
            error = b - ((bAfter * 255) / 31);
            errorAhead.b = error * 7;
            newErrors[x].b += error * 3;
            newErrors[x+1].b += error * 5;
            newErrors[x+2].b = error * 1;
        }
        pIn += strideIn;
        pOut += strideOut;
        oldErrors.swap(newErrors);
    }
}

I won't guarantee this code is perfect, I already had to fix one of those subtle errors that I alluded to in another comment. However it did generate the results above. It takes 24-bit pixels in BGR order as used by Windows, and produces R5G6B5 16-bit pixels in little endian order.

I suggested to used ordered dithering (http://en.wikipedia.org/wiki/Ordered_dithering), since Floyd-Steinberg need more processing and calculating and only works on still image / doesn't works well for animated or constant changed on display.

I Created my own optimized ordered dithering from 24/32bit RGB color to 16bit RGB565 color, that seperate tresshold into subpixel (Used in my AROMA project). It was way faster then Floyd-Steinberg, because there is no expensive calculations (specially no multiplies and divs calculations), and able to used on animations because it used fixed tresshold.

It's quality also so much better than ordered dithering algorithm that defined on wiki.

Here an example of dithering result:

And here the source. Enjoy!

/* Dither Tresshold for Red Channel */
static const BYTE dither_tresshold_r[64] = {
  1, 7, 3, 5, 0, 8, 2, 6,
  7, 1, 5, 3, 8, 0, 6, 2,
  3, 5, 0, 8, 2, 6, 1, 7,
  5, 3, 8, 0, 6, 2, 7, 1,

  0, 8, 2, 6, 1, 7, 3, 5,
  8, 0, 6, 2, 7, 1, 5, 3,
  2, 6, 1, 7, 3, 5, 0, 8,
  6, 2, 7, 1, 5, 3, 8, 0
};

/* Dither Tresshold for Green Channel */
static const BYTE dither_tresshold_g[64] = {
  1, 3, 2, 2, 3, 1, 2, 2,
  2, 2, 0, 4, 2, 2, 4, 0,
  3, 1, 2, 2, 1, 3, 2, 2,
  2, 2, 4, 0, 2, 2, 0, 4,

  1, 3, 2, 2, 3, 1, 2, 2,
  2, 2, 0, 4, 2, 2, 4, 0,
  3, 1, 2, 2, 1, 3, 2, 2,
  2, 2, 4, 0, 2, 2, 0, 4
};

/* Dither Tresshold for Blue Channel */
static const BYTE dither_tresshold_b[64] = {
  5, 3, 8, 0, 6, 2, 7, 1,
  3, 5, 0, 8, 2, 6, 1, 7,
  8, 0, 6, 2, 7, 1, 5, 3,
  0, 8, 2, 6, 1, 7, 3, 5,

  6, 2, 7, 1, 5, 3, 8, 0,
  2, 6, 1, 7, 3, 5, 0, 8,
  7, 1, 5, 3, 8, 0, 6, 2,
  1, 7, 3, 5, 0, 8, 2, 6
};

/* Get 16bit closest color */
BYTE closest_rb(BYTE c) { 
  return (c >> 3 << 3); /* red & blue */
}
BYTE closest_g(BYTE c) {
  return (c >> 2 << 2); /* green */
}

/* RGB565 */
WORD RGB16BIT(BYTE r, BYTE g, BYTE b) {
  return ((WORD)((r>>3)<<11)|((g>>2)<<5)|(b>>3));
}

/* Dithering by individual subpixel */
WORD dither_xy(
  int x, 
  int y, 
  BYTE r, 
  BYTE g, 
  BYTE b
){
  /* Get Tresshold Index */
  BYTE tresshold_id = ((y & 7) << 3) + (x & 7);

  r = closest_rb(
          MIN(r + dither_tresshold_r[tresshold_id], 0xff)
       );
  g = closest_g(
          MIN(g + dither_tresshold_g[tresshold_id], 0xff)
       );
  b = closest_rb(
          MIN(b + dither_tresshold_b[tresshold_id], 0xff)
       );
  return RGB16BIT(r, g, b);
}

/* Dithering Pixel from 32/24bit RGB 
 *
 * GetR, GetG, GetB -> Function to get individual color in pixel
 *
 */
WORD dither_color_xy(int x, int y, DWORD col) {
  return dither_xy(x, y, GetR(col), GetG(col), GetB(col));
}

/* EXAMPLES */
void ExampleDither1(WORD * dest, DWORD * src, int width, int height){
  int x, y;
  for (y=0; y<height; y++){
    for (x=0; x<width; x++){
      int pos = y * width + x;
      dest[pos] = dither_color_xy(x,y,src[pos]);
    }
  }
}
void ExampleDither2(WORD * dest, BYTE * src, int width, int height){
  int x, y;
  for (y=0; y<height; y++){
    for (x=0; x<width; x++){
      int pos = y * width + x;
      dest[pos] = dither_xy(x,y,src[pos*3],src[pos*3+1],src[pos*3+2]);
    }
  }
}

Another Result (Top 24bit - Bottom Ordered RGB565-16bit):

View full resolution image

Floyd–Steinberg dithering

for each y from top to bottom
   for each x from left to right
      oldpixel := pixel[x][y]
      newpixel := find_closest_palette_color(oldpixel)
      pixel[x][y] := newpixel
      quant_error := oldpixel - newpixel
      pixel[x+1][y] := pixel[x+1][y] + 7/16 * quant_error
      pixel[x-1][y+1] := pixel[x-1][y+1] + 3/16 * quant_error
      pixel[x][y+1] := pixel[x][y+1] + 5/16 * quant_error
      pixel[x+1][y+1] := pixel[x+1][y+1] + 1/16 * quant_error

I bet a buck you can implement this easily!

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!