The fun and profit of manipulating the DIB color table can be done without having to modify it


If I were Michael Kaplan, I'd have a more clever title like I'm not touching you! or Look but don't touch or maybe Looking at a DIB through BITMAPINFO-colored glasses.

We saw some time ago that you can manipulate the DIB color table to perform wholesale color remapping. But in fact you can do this even without modifying the DIB color table, which is a handy trick if you want to do color remapping but you don't want to change the bitmap itself. For example, the bitmap is not one that is under your control (so you shouldn't be modifying it), or the bitmap might be in use on multiple threads (so modifying it will result in race conditions).

Let's demonstrate this technique by converting the "Gone Fishing" bitmap to grayscale, but doing so without actually modifying the bitmap. As always, we start with our scratch program and make the following changes:

HBITMAP g_hbm;


BOOL
OnCreate(HWND hwnd, LPCREATESTRUCT lpcs)
{
 // change path as appropriate
 g_hbm = (HBITMAP)LoadImage(g_hinst,
                      TEXT("C:\\Windows\\Gone Fishing.bmp"),
                      IMAGE_BITMAP, 0, 0,
                      LR_CREATEDIBSECTION | LR_LOADFROMFILE);
 return TRUE;
}

void
OnDestroy(HWND hwnd)
{
 if (g_hbm) DeleteObject(g_hbm);
 PostQuitMessage(0);
}

void
PaintContent(HWND hwnd, PAINTSTRUCT *pps)
{
 if (g_hbm) {
  BITMAP bm;
  if (GetObject(g_hbm, sizeof(bm), &bm) == sizeof(bm) &&
                bm.bmBits != NULL &&
                bm.bmPlanes * bm.bmBitsPixel <= 8) {
   struct BITMAPINFO256 {
    BITMAPINFOHEADER bmiHeader;
    RGBQUAD bmiColors[256];
   } bmiGray;
   ZeroMemory(&bmiGray, sizeof(bmiGray));
   HDC hdc = CreateCompatibleDC(NULL);
   if (hdc) {
    HBITMAP hbmPrev = SelectBitmap(hdc, g_hbm);
    UINT cColors = GetDIBColorTable(hdc, 0, 256, bmiGray.bmiColors);
    for (UINT iColor = 0; iColor < cColors; iColor++) {
     BYTE b = (BYTE)((30 * bmiGray.bmiColors[iColor].rgbRed +
                      59 * bmiGray.bmiColors[iColor].rgbGreen +
                      11 * bmiGray.bmiColors[iColor].rgbBlue) / 100);
     bmiGray.bmiColors[iColor].rgbRed   = b;
     bmiGray.bmiColors[iColor].rgbGreen = b;
     bmiGray.bmiColors[iColor].rgbBlue  = b;
    }
    bmiGray.bmiHeader.biSize        = sizeof(bmiGray.bmiHeader);
    bmiGray.bmiHeader.biWidth       = bm.bmWidth;
    bmiGray.bmiHeader.biHeight      = bm.bmHeight;
    bmiGray.bmiHeader.biPlanes      = bm.bmPlanes;
    bmiGray.bmiHeader.biBitCount    = bm.bmBitsPixel;
    bmiGray.bmiHeader.biCompression = BI_RGB;
    bmiGray.bmiHeader.biClrUsed     = cColors;
    SetDIBitsToDevice(pps->hdc, 0, 0,
                      bmiGray.bmiHeader.biWidth,
                      bmiGray.bmiHeader.biHeight, 0, 0,
                      0, bmiGray.bmiHeader.biHeight,
                      bm.bmBits,
                     (BITMAPINFO*)&bmiGray, DIB_RGB_COLORS);

    BitBlt(pps->hdc, bm.bmWidth, 0, bm.bmWidth, bm.bmHeight,
           hdc, 0, 0, SRCCOPY);
    SelectBitmap(hdc, hbmPrev);
    DeleteDC(hdc);
   }
  }
 }
}

Things start off innocently enough, loading the bitmap into a DIB section for use during painting.

We do our work at paint time. First, we confirm that we indeed have a DIB section and that it is 8bpp or lower, because bitmaps at higher than 8bpp do not use color tables.

We then select the bitmap into a DC so we can call GetDIBColorTable to get its current color table. (This is the only step that requires the bitmap to be selected into a device context.) We then edit the color table to convert each color to its grayscale equivalent.

Finally, we fill in the BITMAPINFO structure with the description of the bitmap bits, and then we call SetDIBitsToDevice to send the pixels to the destination DC.

Just for good measure, we also BitBlt the original unmodified bitmap, to prove that the original bitmap is intact and unchanged.

This mini-program is really just a stepping stone to other things you can do with this technique of separating the metadata (the BITMAPINFO) from the pixels. We'll continue our investigations tomorrow.

(Before you all run out and use this technique everywhere you can imagine, wait for the remarks in Friday's installment.)

Comments (11)
  1. Tom says:

    Editoral Note: There seems to be a word or two missing from the following sentence fragment: “For example, the bitmap is not one that under your control”.  Perhaps it should read “For example, the bitmap is not one that is under your control.”

    This seems like the start of another series.  I’m excited!

    [Despite what others say, typo corrections are welcome. Thanks. -Raymond]
  2. Mark says:

    Tom: do you really think these posts are published here for proofreading?  If the code doesn’t compile, maybe that’s worth highlighting, but the three typos in the text are somewhat inconsequent.

  3. John says:

    Maybe he inserted the typos on purpose just to piss you off:

    http://blogs.msdn.com/oldnewthing/archive/2007/06/11/3215739.aspx#3238631

  4. Tom says:

    It would be difficult to get me angry over a post in this blog, for I am not one of those so frequently taunted by the Nit-picker’s Corner.  Raymond has provided excellent information on this blog for more than five years; I am simply seeking to marginally improve the quality of his posts for posterity.  Besides, Raymond should be used to this type of constructive criticism — he had an editor for the book he wrote, did he not?

    And I stand by my later comment: I’m excited by the prospect of another series!

  5. Worf says:

    Last time I had to do this programatically, I just did an RGB->YUV conversion and used the Y values. Happened again recently, and I said the simple solution was to use the RGB->YUV hardware in planar mode…

  6. hexatron says:

    I used to do this kind of thing a lot.

    I noticed using the NTSC color coefficients (.30 Red, .56 Green, .11 Blue) (nitpick–these are approximate values) results in the blue parts of an image becoming very dark (that darned .11 coefficient).

    I found I got better results with

    (2*Red+4*Green+Blue)/7

    or

    (Red + 2*Green + Blue)/4

    If you dig into the NTSC standard of 1953, there really isn’t much motivation for using .30,.59,.11

    It’s popular because it is a clear answer to ‘what coefficients should be used’, not because it’s valid in domains far removed from its original application.

  7. Jonathan says:

    I’ve always wondered why Red gets a higher coefficient than Blue – always seemed the oppsite to me. Maybe it’s just my red-green color blindness.

  8. Anonymous Coward says:

    Jonathan, that is probably the case. I’ve once run a lot of ‘perceived brightness’ tests where I juxtaposed solid colours and greys and I must say that on my LCD the coefficients are approximately correct. Of course not all people are identical, and so on and so forth, but I think the coefficients given are close enough and I can see the benefit of always getting the same result when you someone for a greyscale version of something. [Note: the coefficients as I read them are .299, .587 and .114 B, but after the second decimal place no one gives a damn.]

  9. alexx says:

    Error in the code:

    ‘PaintContent’ : ‘void’ function returning a value.

    [Fixed, thanks. -Raymond]
  10. JM says:

    @hexatron: this is quite a big can of worms. For starters, adding scaled components is by no means the only method of doing grayscale conversion, though it is the most convenient.

    The values Raymond uses are the "popular" values, indeed based on NTSC, and thus technically wrong as the sRGB color space isn’t the NTSC color space. But because most computer screens are woefully miscalibrated (based on what people think are "nice" colors rather than accurate ones) and the result doesn’t need to appear on paper, getting the values "right" is usually not that important. Using known coefficients is a better idea than just tweaking for whatever looks good on your screen, though, for the simple reason that people can at least see why you picked those values and what they’re supposed to accomplish, and because your screen is in all likelihood not representative of others.

    Color is a bit like floating-point in that most people that have to deal with it are not experts on the subject and would prefer to pretend that everything can be managed with magic formulas and assumptions. Thankfully, color is not as critical as floating-point for most people, so this usually holds.

    See also http://stackoverflow.com/questions/687261/converting-rgb-to-grayscale-intensity

    And of course, none of this touches on Raymond’s central point, but then again there are far worse things to go off-topic about. :-)

  11. Mark says:

    alexx: “return TRUE” quite obviously shouldn’t be there.

    * Waits for somebody to get stuck at “8bbp” *

    [Fixed, thanks. -Raymond]

Comments are closed.