Smart resize for monochrome bitmaps

If we make a monochrome bitmap smaller by a simple pixel resampling
the resulting image looks lousy (pic.2). The problem here is that we take
a stand-alone pixel and disregard its surrounding. We can improve the picture
a little bit by calculating the value of a pixel as the average of the
pixels we disregard and then rounding this value to the nearest color.
However the best result is obtained in a grayscale destination bitmap (pic.
3). This is implemented in the following function.

 







Pic.1 Original image Pic 2. Simple resize   Pic 3. Smart resize


#define WIDTHBYTES(bits) (((bits) + 31) / 32 * 4) // for padding

///////////////////////////////////////////////////////////////////
// Function name : ZoomOutBmp
// Description : creates a new bitmap which is a grayscale
// zoomed out version of the original
// Return type : HDIB – handle to a new bitmap
// Argument : double zoom – number of times to zoom out
// Argument : HDIB hSrcDIB – handle to a source bitmap
///////////////////////////////////////////////////////////////////
HDIB ZoomOutBmp(double zoom, HDIB hSrcDIB)
{
if (hSrcDIB == NULL) // nothing to do
return NULL;

if (zoom < 1) // no zoomin in this function return NULL; LPSTR pSrcDIB = (LPSTR) ::GlobalLock((HGLOBAL) hSrcDIB); BITMAPINFOHEADER& bmihSrc = *(BITMAPINFOHEADER*)pSrcDIB; ASSERT(bmihSrc.biBitCount == 1); // only monochrome bitmaps supported LPSTR pSrcBits = (LPSTR) (pSrcDIB + sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD)*2); BITMAPINFOHEADER bmihDst = bmihSrc; bmihDst.biWidth = (LONG)(bmihDst.biWidth / zoom + 0.5); bmihDst.biHeight = (LONG)(bmihDst.biHeight / zoom + 0.5); bmihDst.biBitCount = 8; // grayscale in any case bmihDst.biClrUsed = 0; // prepare destination bitmap DWORD dwDIBSize = sizeof(bmihDst) + sizeof(RGBQUAD)*256 + WIDTHBYTES(bmihDst.biWidth * bmihDst.biBitCount) * bmihDst.biHeight; bmihDst.biSizeImage = dwDIBSize; // allocate space for the new bitmap HDIB hDstDIB = (HDIB) ::GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, dwDIBSize); if (hDstDIB == 0) { ::GlobalUnlock((HGLOBAL) hSrcDIB); return NULL; } LPSTR pDstDIB = (LPSTR) ::GlobalLock((HGLOBAL) hDstDIB); // copy header memcpy(pDstDIB, &bmihDst, sizeof(bmihDst)); // prepare grayscale palette for (int i=0; i < (1 << bmihDst.biBitCount); i++) { RGBQUAD& palEntry = *(RGBQUAD*)(pDstDIB + sizeof(bmihDst) + i * sizeof(RGBQUAD)); palEntry.rgbRed = palEntry.rgbGreen = palEntry.rgbBlue = i; } LPSTR pDstBits = (LPSTR) (pDstDIB + sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD)*256); // now fill the bits LPSTR curSrcLineBits, curDstLineBits; int j, k; int scale = (int)(zoom + 0.5); // integer zoom out factor, i.e. 1:5 int hBase, vBase; unsigned char value; // for every _scale_ lines in a source bitmap we will get one line // in the destination bitmap. Similarly for _scale_ columns in the // source we'll obtain one destination column. for (int strip=0; strip < bmihDst.biHeight; strip++) { // for every dst line curDstLineBits = pDstBits + strip * WIDTHBYTES(bmihDst.biWidth * bmihDst.biBitCount); vBase = int(strip * zoom + 0.5); for (i=0; i < scale; i++) { // accumulate _scale_ rows curSrcLineBits = pSrcBits + (vBase + i) * WIDTHBYTES(bmihSrc.biWidth * bmihSrc.biBitCount); // prepare horizontally condensed lines for this strip for (j=0; j < bmihDst.biWidth; j++) { // for all bits in line hBase = int(j * zoom + 0.5); // mapped index on source for (k=0; k < scale; k++) { // accumulate _scale_ columns value = (curSrcLineBits[(hBase+k)/8] & (1 << (7 - (hBase+k)%8))) ? 0xff : 0; curDstLineBits[j] += value / scale / scale; // main accumulator } } } } // unlock memory ::GlobalUnlock((HGLOBAL) hSrcDIB); ::GlobalUnlock((HGLOBAL) hDstDIB); return hDstDIB; }

This function may be improved in several ways, for example, working with
a weighted fraction of a pixel or centering the accumulated source pixels.
However, the resulting visual impovement is not significant.

Download demo project – 38 KB

More by Author

Must Read