I've been looking for an optimized (i.e., quick) algorithm that converts a 24-bit RGB bitmap to a 16-bit (RGB565) bitmap using dithering. I'm looking for something in C/C++ where I can actually control how the dithering is applied. GDI+ seems to provide some methods, but I can't tell if they dither or not. And, if they do dither, what mechanism are they using (Floyd-Steinberg?)
Does anyone have a good example of bitmap color-depth conversion with dithering?
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
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.
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!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With