windows支持兩種位圖格式,DDB(device-dependent bitmap),DIB(device-independent bitmap)。設備相關位圖用於windows顯示系統中,其圖像格式與顯卡格式兼容,因此顯示速度很快。設備不相關位圖定義了位圖的文件格式,用於位圖傳輸,由於其數據格式可能與顯卡格式不一致,直接使用設備不相關位圖顯示圖像時需要進行轉換,因此顯示速度較慢。
歷史上顯卡支持16色或者256色,分別使用4位或者8位表示一個像素顏色。在16色系統中,僅支持黑白灰,紅綠藍,青品紅黃等基本顏色。在256色系統中,windows保留了20中基本顏色,剩余236中顏色通過顏色查找表定義。因此,基於以上色彩體系創建的位圖需要設備相關的顏色查找表來解釋真實顏色。
另外兩種不需要查找表的顏色包括16位色與24位色。使用16位(2字節)表示紅綠藍,每個顏色分量使用5位(或者綠色使用6位),被稱為 high color。使用24位(3字節)表示紅綠藍,每個顏色分量使用一個字節,被稱為 true color。在沒有查找表情況下,位圖紅綠藍分量排列順序可能不一致,因此DDB顯示速度要優於DIB。
現代顯示器一般都是32位真彩色,可以通過 ::GetDeviceCaps(hdc, BITSPIXEL) 獲得每個像素上的位數,在本機測試值為32,通過 ::GetDeviceCaps(hdc, PLANES) 獲得位面數,一般情況下該值為1。
windows提供了BITMAP結構體來描述DDB,定義如下:
/* Bitmap Header Definition */
typedef struct tagBITMAP { LONG bmType; LONG bmWidth; LONG bmHeight; LONG bmWidthBytes; WORD bmPlanes; WORD bmBitsPixel; LPVOID bmBits; } BITMAP, *PBITMAP, NEAR *NPBITMAP, FAR *LPBITMAP;
其中,bmWidth, bmHeight表示DDB的尺寸,bmPlanes通常為1,bmBitsPixel表示每個像素需要多少位來表示。
有兩個參數需要特別注意:
bmBits並不是指向DDB的一個指針,應用程序無法直接操作顯存,多數情況下該指針為空,但可以通過特定函數獲得DDB數據區的一個拷貝;
bmWidthBytes一般不需要賦值, windows會根據規則計算出正確的值,其規則是位圖每行字節為2的倍數,一般計算公式為 bmWidthBytes = 2 *((bmWidth * bmBitsPixel + 15) / 16)。
windows提供了兩個基本函數進行DDB繪制,BitBlt 直接將DDB一個區域拷貝到另一個區域中,StretchBlt 引入了縮放模式。
以下函數塊僅在左上角繪制一條直線段,然后利用 BitBlt 將其拷貝到其他區域,關鍵代碼如下:
::MoveToEx(hdc, 0, 0, NULL); ::LineTo(hdc, 10, 10); for (int y = 50; y < cyClient; y += cySource) for (int x = 50; x < cxClient; x += cxSource) { BitBlt(hdc, x, y, cxSource, cySource, hdc, 0, 0, SRCCOPY); }
得到如下顯示效果:
使用 StretchBlt 替代 BitBlt ,結果如下:
代碼如下:
::MoveToEx(hdc, 0, 0, NULL); ::LineTo(hdc, 10, 10); for (int y = 50; y < cyClient; y += 30) for (int x = 50; x < cxClient; x += 30) { ::StretchBlt(hdc, x, y, 20, 20, hdc, 0, 0, 10, 10, SRCCOPY); }
StretchBlt 將10*10區域拷貝到20*20區域,當目標區域與原始區域尺寸不一致時,必然存在插值操作,使用函數 SetStretchBltMode 設置插值模式,windows 定義了如下模式:
.BLACKONWHITE:位與操作,保留插值區域中最暗的值(縮小時應用該值,放大時直接映射到原始區域中)
.WHITEONBLACK:位或操作,保留插值區域中最亮的值(縮小時應用該值,放大時直接映射到原始區域中)
.COLORONCOLOR:抽取冗余行列,即得到縮小圖像(放大時同樣直接映射到原始區域中)
.HALFTONE:使用均值作為目標結果(縮小時先平均再采樣,放大時使用插值)
以上插值模式中,COLORONCOLOR速度最快,HALFTONE速度最慢,BLACKONWHITE與WHITEONBLACK應用在一些特殊場景中,一般使用COLORONCOLOR。
注意參數 SRCCOPY 定義了拷貝操作運算,該運算針對每個像素做同樣的操作,這是一個三元運算,其操作數包括源像素,目標像素以及模式像素(畫刷),該三元運算包括256中組合,下面僅給出部分重要組合:
.SRCCOPY: dest = src
.SRCPAINT: dest = source OR dest
.SRCAND: dest = source AND dest
.SRCINVERT: dest = source XOR dest
.SRCERASE: dest = source AND (NOT dest )
.NOTSRCCOPY: dest = (NOT source)
有了以上拷貝模式,很自然會想到是否可以使用某些運算組合實現橢圓圖像繪制,這里所謂橢圓圖像是指對矩形圖像繪制橢圓區域,典型應用如界面上圓角按鈕。
// load image hBitmapImag = (HBITMAP)LoadImage(hInstance, TEXT("img.bmp"), IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE | LR_CREATEDIBSECTION); // obtain image size GetObject(hBitmapImag, sizeof(BITMAP), &bitmap); cxBitmap = bitmap.bmWidth; cyBitmap = bitmap.bmHeight; // Select the original image into a memory DC hdcMemImag = CreateCompatibleDC(NULL); SelectObject(hdcMemImag, hBitmapImag); // Create the monochrome mask bitmap and memory DC hBitmapMask = CreateBitmap(cxBitmap, cyBitmap, 1, 1, NULL); hdcMemMask = CreateCompatibleDC(NULL); SelectObject(hdcMemMask, hBitmapMask); // Color the mask bitmap black with a white ellipse SelectObject(hdcMemMask, GetStockObject(BLACK_BRUSH)); Rectangle(hdcMemMask, 0, 0, cxBitmap, cyBitmap); SelectObject(hdcMemMask, GetStockObject(WHITE_BRUSH)); Ellipse(hdcMemMask, 0, 0, cxBitmap, cyBitmap); // Mask the original image BitBlt(hdcMemImag, 0, 0, cxBitmap, cyBitmap, hdcMemMask, 0, 0, SRCAND); // Center image x = (cxClient - cxBitmap) / 2; y = (cyClient - cyBitmap) / 2; // Do the bitblts SelectObject(hdc, GetStockObject(GRAY_BRUSH)); Rectangle(hdc, 0, 0, cxClient, cyClient); BitBlt(hdc, x, y, cxBitmap, cyBitmap, hdcMemMask, 0, 0, 0x220326); BitBlt(hdc, x, y, cxBitmap, cyBitmap, hdcMemImag, 0, 0, SRCPAINT); DeleteDC(hdcMemImag); DeleteDC(hdcMemMask);
以上代碼實現了繪制圖像橢圓區域,下面詳細解讀實現過程:
1)首先加載DDB圖像,並獲取圖像尺寸
2)創建內存兼容DC,並將DDB圖像加載到DC
3)創建與原始圖像尺寸一致的模板圖像,模板圖像為二值圖像,並加載到內存DC中
4)對模板圖像橢圓區域內設置為1,橢圓區域外設置為0
5)使用 SRCAND 操作將模板圖像繪制到原圖像中,此時原圖像保留圖像區域內像素值,橢圓區域外被設置為0
6)為了使圖像居中顯示,計算繪制起點x,y
7)將整個區域填充為中灰色
8)使用 0x220326 操作得到一個橢圓中心為黑色,橢圓外圍保持不變的DC,該操作沒有命名,具體執行邏輯為 Dest = Dest AND (Not Source)
9)使用 SRCPAINT 操作得到最終結果,該操作執行 Dest = Source OR Dest
windows 同樣提供了一個更加簡便的函數來實現以上功能,使用 PlgBlt 函數可以實現任意自定義區域繪制,同時該函數還可以做圖像變換功能。
BOOL PlgBlt( HDC hdcDest, CONST POINT * lpPoint, HDC hdcSrc, int xSrc, int ySrc, int width,
int height, HBITMAP hbmMask, int xMask, int yMask);
參數 hbmMask 為一個二值圖像,作為原始圖像的繪制模板
參數 hdcSrc 為原始圖像,同時定義了繪制區間
參數 hdcDest 為目標DC,圖像將安裝指定規則繪制到目標DC上
參數 lpPoint 包含了三個點,在目標區域上構成了一個平行四邊形,三個點分別對應左上,右上,左下,第四個點(右下)可以根據平行四邊形規則計算得出,
將原始圖像矩形映射到目標平行四邊形上即構成了一個變換,該變換包括平移,旋轉,縮放,放射變換。
下面給出 PlgBlt 函數的使用效果:
左邊圖像僅包括平移與縮放,右邊圖像為任意仿射變換,主要代碼如下:
POINT pt[3]; pt[0].x = 50; pt[0].y = 50; pt[1].x = 300; pt[1].y = 50; pt[2].x = 50; pt[2].y = 300; PlgBlt(hdc, pt, hdcMemImag, 0, 0, cxBitmap, cyBitmap, hBitmapMask, 0, 0); pt[0].x = 350; pt[0].y = 50; pt[1].x = 600; pt[1].y = 0; pt[2].x = 500; pt[2].y = 400; PlgBlt(hdc, pt, hdcMemImag, 0, 0, cxBitmap, cyBitmap, hBitmapMask, 0, 0);
除了對DDB圖像進行透明與仿射變換操作外,是否可以可以對DDB圖像做融合操作呢?msimg32.dll 提供了更懂的圖像操作函數。
TransparentBlt 函數可以對指定像素做透明處理,函數定義如下:
BOOL TransparentBlt(_In_ HDC hdcDest, _In_ int xoriginDest, _In_ int yoriginDest, _In_ int wDest, _In_ int hDest,
_In_ HDC hdcSrc, _In_ int xoriginSrc, _In_ int yoriginSrc, _In_ int wSrc, _In_ int hSrc, _In_ UINT crTransparent);
該函數對指定像素透明處理,但指定像素不能為RGB(0,0,0),不論DC縮放模式設置為什么值,該函數永遠執行 COLORONCOLOR 模式。
AlphaBlend 將圖像與背景融合,函數定義如下:
BOOL AlphaBlend (_In_ HDC hdcDest, _In_ int xoriginDest, _In_ int yoriginDest, _In_ int wDest, _In_ int hDest,
_In_ HDC hdcSrc, _In_ int xoriginSrc, _In_ int yoriginSrc, _In_ int wSrc, _In_ int hSrc, _In_ BLENDFUNCTION ftn);
該函數關鍵參數為 BLENDFUNCTION 結構體,當 AlphaFormat 取值為 AC_SRC_ALPHA 時,原圖像必須具有 alpha 通道。
當 SourceConstantAlpha 取值小於255時,整個圖像應用SourceConstantAlpha;當 SourceConstantAlpha 取值為255時,應用每個像素的alpha值。
圖像融合計算公式為 dest = source * alpha / 255 + dest * (1 - alpah / 255)。
一個注意點:source * alpha / 255 在調用顯示函數前可以提前計算出來,為了提升顯示速度,AlphaBlend 函數應用了公式 dest = source + dest * (1 - alpah / 255),
即 source 不再是原始圖像,而是 source = source * alpha / 255。
下面給出一個 AlphaBlend 函數應用示例,我創建了一個32位圖像,其上半部分圖像的alpha值被設置為128,下半部分圖像的alpha值設置為255,
同時上半部分圖像的每個像素進行了預計算(source = source * alpha / 255),以下為部分顯示代碼及效果:
hdcMemImag = CreateCompatibleDC(hdc); SelectObject(hdcMemImag, hBitmapImag); SelectObject(hdc, GetStockObject(GRAY_BRUSH)); Rectangle(hdc, 0, 0, cxClient, cyClient); BLENDFUNCTION bn; bn.AlphaFormat = AC_SRC_ALPHA; bn.BlendFlags = 0; bn.BlendOp = AC_SRC_OVER; bn.SourceConstantAlpha = 255; // 應用每個像素的alpha值 AlphaBlend(hdc, 0, 0, cxBitmap, cyBitmap, hdcMemImag, 0, 0, cxBitmap, cyBitmap, bn); DeleteDC(hdcMemImag);
以上是DDB的一些主要內容,windows同樣提供了DIB,其目的是為了圖像文件傳輸與存儲,因為DDB高度依賴設備。
windows DIB 結構集成自 OS/2,包括以下幾個部分:
1)文件頭,定義為 BITMAPFILEHEADER,包括文件識別及尺寸等基本信息;
2)信息頭,OS/2定義為BITMAPCOREHEADER, windows 對其進行擴展,定義為 BITMAPINFOHEADER,在windows中兩者均可使用;
3)查找表,高彩色或者真彩色不需要查找表;
4)圖像數據區;
這里不詳細解釋所有結構體成員,僅對一些自認為很重要的地方進行說明。
1)BITMAPCOREHEADER 與 BITMAPINFOHEADER 的成員變量 bcPlanes 與 biPlanes 永遠為1;
2)BITMAPCOREHEADER 的成員變量 bcBitCount可取值為 1,2,4,8,24,而windows擴展下 BITMAPINFOHEADER的成員變量biBitCount 可取值1,2,4,8,16,24,32;
3)BITMAPCOREINFO 使用查找表 RGBTRIPLE,而 BITMAPINFO 使用查找表 RGBQUAD;
4)由於 OS/2 風格定義也被 windows 支持,當拿到一個信息頭結構體后,可以通過 bcSize/biSize 字段來判斷到底是哪種風格位圖,因為兩個結構體尺寸分別為 12,40字節;
5)由於結構體在內存中的對齊方式由編譯器確定,不同編譯器可能得到不同大小的結構體,當將圖像結構寫入文件時,會導致無法正確讀取結構體(對齊方式未知),因此需要使用 packed 模式存儲結構體變量;
6)圖像數據區原點位於左下角(滿足笛卡爾坐標系),而顯示系統原點一般位於左上角,這會導致顯示圖像時上下翻轉;
7)位圖數據行一般是4字節整數倍,而DDB要求為兩字節整數倍,所以當DIB轉換為DDB時自然滿足對齊要求;
8)biCompression 可取值 BI_RGB,BI_RLE8, BI_RLE4,BI_BITFIELDS,具體解釋如下:
.1位位圖時,biCompression 取值只能為 BI_RGB;
.4位位圖時,biCompression 取值可以為 BI_RGB 或者 BI_RLE4, BI_RLE4為行程碼壓縮數據,該壓縮算法直觀簡單,因此編解碼速度會很快,但壓縮率有限;
.8位位圖時,biCompression 取值可以為 BI_RGB 或者 BI_RLE8, BI_RLE8為行程碼壓縮數據;
.24位位圖時,biCompression 取值只能為 BI_RGB;
.16位或者32位位圖時,biCompression 取值可以為 BI_RGB 或者 BI_BITFIELDS
.只要 biCompression 取值為 BI_RGB,不管16位,24位或者32位位圖,其排列順序均為blue->green->red->alpha(可能沒有),只是16位位圖時每個顏色占5位,24位與32位位圖時每個顏色占8位;
.當 biCompression 取值為 BI_BITFIELDS 時,在 BITMAPCOREHEADER 后緊跟3個DWORD數據作為顏色位標記符,程序根據這3個DWORD數據提取色彩;
與DDB類似,windows同樣提供了兩個函數用於顯示DIB, SetDIBitsToDevice直接將DIB顯示在DC上, StretchDIBits 提供了縮放顯示,定義如下:
int SetDIBitsToDevice(_In_ HDC hdc, _In_ int xDest, _In_ int yDest, _In_ DWORD w, _In_ DWORD h, _In_ int xSrc,
_In_ int ySrc, _In_ UINT StartScan, _In_ UINT cLines, _In_ CONST VOID * lpvBits, _In_ CONST BITMAPINFO * lpbmi, _In_ UINT ColorUse);
int StretchDIBits(_In_ HDC hdc, _In_ int xDest, _In_ int yDest, _In_ int DestWidth, _In_ int DestHeight, _In_ int xSrc, _In_ int ySrc, _In_ int SrcWidth, _In_ int SrcHeight,
_In_opt_ CONST VOID * lpBits, _In_ CONST BITMAPINFO * lpbmi, _In_ UINT iUsage, _In_ DWORD rop);
是否可以對使用 StretchDIBits 函數完成圖像仿射變換及橢圓顯示呢?可以采用如下方法實現:
1)直接對DIB圖像數據應用仿射變換;
2)使用類似位邏輯操作(如SRCPAINT等 )實現指定區域透明顯示功能;
要實現DIB圖像融合,目前沒有看到windows提供更多的方法,需要自己對數據直接處理。
在實際應用中,DDB與DIB一般會交互使用,以下流程給出了DIB與DDB之間的轉換方法:
1)從文件中加載一張DIB圖像;
2)使用 CreateCompatibleBitmap 創建與DIB尺寸一致且與設備兼容的位圖(DDB);
3)使用 CreateCompatibleDC 創建與設備兼容的內存DC,並將兼容位圖選人兼容內存DC;
4)使用 SetDIBitsToDevice 將DIB繪制到兼容內存DC上;
5)使用 BitBlt 將內存DC顯示到物理DC上,實現了DIB到DDB轉換並顯示;
6)使用 GetDIBits 並將第五個參數(數據指針)設置為 NULL,可獲得內存DC數據相關參數;
7)根據 6)獲得參數分配所需內存,再次調用 GetDIBits 可獲得數據,返回值表示成功拷貝圖像行數;
8)將獲得圖像保存到文件,對比與原始圖像一致,實現了DDB到DIB轉換並保存。
以下給出部分代碼:
// 打開圖像 ... // DIB轉換到DDB HBITMAP hBitmap = ::CreateCompatibleBitmap(hdc, w, h); HDC hMemDC = ::CreateCompatibleDC(hdc); ::SelectObject(hMemDC, hBitmap); ::SetDIBitsToDevice(hMemDC, 0, 0, w, h, 0, 0, 0, h, imgs[0].data, (LPBITMAPINFO)lpBmpInfoHead, DIB_RGB_COLORS); // 顯示DDB CRect rcClient; this->GetClientRect(&rcClient); BitBlt(hdc, rcClient.left, rcClient.top, rcClient.Width(), rcClient.Height(), hMemDC, 0, 0, SRCCOPY); // DIB信息頭,在第一次調用GetDIBits時被填充 BITMAPINFO bmi = { 0 }; int sz = sizeof(BITMAPINFO); bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); // 獲取DIB尺寸等參數 ::GetDIBits(hMemDC, hBitmap, 0, h, NULL, &bmi, DIB_RGB_COLORS); // 分配DIB數據所需內存 unsigned char *bitmapBits = new unsigned char[bmi.bmiHeader.biSizeImage]; memset(bitmapBits, 0, bmi.bmiHeader.biSizeImage); // 從DDB中獲取DIB數據 int lines = ::GetDIBits(hMemDC, hBitmap, 0, h, bitmapBits, &bmi, DIB_RGB_COLORS); // 拷貝數據並保存文件 ... // 刪除內存 delete []bitmapBits ;
參考資料:Programming Windows by Charles Petzold