數字影象處理程式設計入門
Windows點陣圖和調色盤
如今Windows(3.x以及95,98,NT)系列已經成為絕大多數使用者使用的作業系統,它比DOS成功的一個重要因素是它視覺化的漂亮介面。那麼Windows是如何顯示圖象的呢?這就要談到點陣圖(bitmap)。
我們知道,普通的顯示器螢幕是由許許多多點構成的,我們稱之為象素。顯示時採用掃描的方法:電子槍每次從左到右掃描一行,為每個象素著色,然後從上到下這樣掃描若干行,就掃過了一屏。為了防止閃爍,每秒要重複上述過程幾十次。例如我們常說的螢幕解析度為
我們稱這種顯示器為位映象裝置。所謂位映象,就是指一個二維的象素矩陣,而點陣圖就是採用位映象方法顯示和儲存的圖象。舉個例子,圖1.1是一幅普通的黑白點陣圖,圖1.2是被放大後的圖,圖中每個方格代表了一個象素。我們可以看到:整個骷髏就是由這樣一些黑點和白點組成的。
圖1.1 骷髏 |
圖 |
那麼,彩色圖是怎麼回事呢?
我們先來說說三元色RGB概念。
我們知道,自然界中的所有顏色都可以由紅、綠、藍(R,G,B)組合而成。有的顏色含有紅色成分多一些,如深紅;有的含有紅色成分少一些,如淺紅。針對含有紅色成分的多少,可以分成0到255共256個等級,0級表示不含紅色成分;255級表示含有100%的紅色成分。同樣,綠色和藍色也被分成256級。這種分級概念稱為量化。
這樣,根據紅、綠、藍各種不同的組合我們就能表示出256×256×256,約1600萬種顏色。這麼多顏色對於我們人眼來說已經足夠豐富了。
表1.1 常見顏色的RGB組合值
顏色 |
R |
G |
B |
紅 |
255 |
0 |
0 |
藍 |
0 |
255 |
0 |
綠 |
0 |
0 |
255 |
黃 |
255 |
255 |
0 |
紫 |
255 |
0 |
255 |
青 |
0 |
255 |
255 |
白 |
255 |
255 |
255 |
黑 |
0 |
0 |
0 |
灰 |
128 |
128 |
128 |
你大概已經明白了,當一幅圖中每個象素賦予不同的RGB值時,能呈現出五彩繽紛的顏色了,這樣就形成了彩色圖。的確是這樣的,但實際上的做法還有些差別。
讓我們來看看下面的例子。
有一個長寬各為200個象素,顏色數為16色的彩色圖,每一個象素都用R、G、B三個分量表示。因為每個分量有256個級別,要用8位(bit),即一個位元組(byte)來表示,所以每個象素需要用3個位元組。整個圖象要用200×200×3,約120k位元組,可不是一個小數目呀!如果我們用下面的方法,就能省的多。
因為是一個16色圖,也就是說這幅圖中最多隻有16種顏色,我們可以用一個表:表中的每一行記錄一種顏色的R、G、B值。這樣當我們表示一個象素的顏色時,只需要指出該顏色是在第幾行,即該顏色在表中的索引值。舉個例子,如果表的第0行為255,0,0(紅色),那麼當某個象素為紅色時,只需要標明0即可。
讓我們再來計算一下:16種狀態可以用4位(bit)表示,所以一個象素要用半個位元組。整個圖象要用200×200×0.5,約20k位元組,再加上表佔用的位元組為3×16=48位元組.整個佔用的位元組數約為前面的1/6,省很多吧?
這張R、G、B的表,就是我們常說的調色盤(Palette),另一種叫法是顏色查詢表LUT(Look Up Table),似乎更確切一些。Windows點陣圖中便用到了調色盤技術。其實不光是Windows點陣圖,許多圖象檔案格式如pcx、tif、gif等都用到了。所以很好地掌握調色盤的概念是十分有用的。
有一種圖,它的顏色數高達256×256×256種,也就是說包含我們上述提到的R、G、B顏色表示方法中所有的顏色,這種圖叫做真彩色圖(true color)。真彩色圖並不是說一幅圖包含了所有的顏色,而是說它具有顯示所有顏色的能力,即最多可以包含所有的顏色。表示真彩色圖時,每個象素直接用R、G、B三個分量位元組表示,而不採用調色盤技術。原因很明顯:如果用調色盤,表示一個象素也要用24位,這是因為每種顏色的索引要用24位(因為總共有224種顏色,即調色盤有224行),和直接用R,G,B三個分量表示用的位元組數一樣,不但沒有任何便宜,還要加上一個256×256×256×3個位元組的大調色盤。所以真彩色圖直接用R、G、B三個分量表示,它又叫做24位色圖。
1.2 bmp檔案格式
介紹完點陣圖和調色盤的概念,下面就讓我們來看一看Windows的點陣圖檔案(.bmp檔案)的格式是什麼樣子的。
bmp檔案大體上分成四個部分,如圖1.3所示。
點陣圖檔案頭BITMAPFILEHEADER |
點陣圖資訊頭BITMAPINFOHEADER |
調色盤Palette |
實際的點陣圖資料ImageDate |
圖1.3 Windows點陣圖檔案結構示意圖
第一部分為點陣圖檔案頭BITMAPFILEHEADER,是一個結構,其定義如下:
typedef struct tagBITMAPFILEHEADER {
WORD bfType;
DWORD bfSize;
WORD bfReserved1;
WORD bfReserved2;
DWORD bfOffBits;
} BITMAPFILEHEADER;
這個結構的長度是固定的,為14個位元組(WORD為無符號16位整數,DWORD為無符號32位整數),各個域的說明如下:
bfType
指定檔案型別,必須是0x424D,即字串“BM”,也就是說所有.bmp檔案的頭兩個位元組都是“BM”。
bfSize
指定檔案大小,包括這14個位元組。
bfReserved1,bfReserved2
為保留字,不用考慮
bfOffBits
為從檔案頭到實際的點陣圖資料的偏移位元組數,即圖1.3中前三個部分的長度之和。
第二部分為點陣圖資訊頭BITMAPINFOHEADER,也是一個結構,其定義如下:
typedef struct tagBITMAPINFOHEADER{
DWORD biSize;
LONG biWidth;
LONG biHeight;
WORD biPlanes;
WORD biBitCount
DWORD biCompression;
DWORD biSizeImage;
LONG biXPelsPerMeter;
LONG biYPelsPerMeter;
DWORD biClrUsed;
DWORD biClrImportant;
} BITMAPINFOHEADER;
這個結構的長度是固定的,為40個位元組(LONG為32位整數),各個域的說明如下:
biSize
指定這個結構的長度,為40。
biWidth
指定圖象的寬度,單位是象素。
biHeight
指定圖象的高度,單位是象素。
biPlanes
必須是1,不用考慮。
biBitCount
指定表示顏色時要用到的位數,常用的值為1(黑白二色圖), 4(16色圖), 8(256色), 24(真彩色圖)(新的.bmp格式支援32位色,這裡就不做討論了)。
biCompression
指定點陣圖是否壓縮,有效的值為BI_RGB,BI_RLE8,BI_RLE4,BI_BITFIELDS(都是一些Windows定義好的常量)。要說明的是,Windows點陣圖可以採用RLE4,和RLE8的壓縮格式,但用的不多。我們今後所討論的只有第一種不壓縮的情況,即biCompression為BI_RGB的情況。
biSizeImage
指定實際的點陣圖資料佔用的位元組數,其實也可以從以下的公式中計算出來:
biSizeImage=biWidth’ × biHeight
要注意的是:上述公式中的biWidth’必須是4的整倍數(所以不是biWidth,而是biWidth’,表示大於或等於biWidth的,最接近4的整倍數。舉個例子,如果biWidth=240,則biWidth’=240;如果biWidth=241,biWidth’=244)。
如果biCompression為BI_RGB,則該項可能為零
biXPelsPerMeter
指定目標裝置的水平解析度,單位是每米的象素個數,關於解析度的概念,我們將在第4章詳細介紹。
biYPelsPerMeter
指定目標裝置的垂直解析度,單位同上。
biClrUsed
指定本圖象實際用到的顏色數,如果該值為零,則用到的顏色數為2biBitCount。
biClrImportant
指定本圖象中重要的顏色數,如果該值為零,則認為所有的顏色都是重要的。
第三部分為調色盤Palette,當然,這裡是對那些需要調色盤的點陣圖檔案而言的。有些點陣圖,如真彩色圖,前面已經講過,是不需要調色盤的,BITMAPINFOHEADER後直接是點陣圖資料。
調色盤實際上是一個數組,共有biClrUsed個元素(如果該值為零,則有2biBitCount個元素)。陣列中每個元素的型別是一個RGBQUAD結構,佔4個位元組,其定義如下:
typedef struct tagRGBQUAD {
BYTE rgbBlue; //該顏色的藍色分量
BYTE rgbGreen; //該顏色的綠色分量
BYTE rgbRed; //該顏色的紅色分量
BYTE rgbReserved; //保留值
} RGBQUAD;
第四部分就是實際的圖象資料了。對於用到調色盤的點陣圖,圖象資料就是該象素顏在調色盤中的索引值。對於真彩色圖,圖象資料就是實際的R、G、B值。下面針對2色、16色、256色點陣圖和真彩色點陣圖分別介紹。
對於2色點陣圖,用1位就可以表示該象素的顏色(一般0表示黑,1表示白),所以一個位元組可以表示8個象素。
對於16色點陣圖,用4位可以表示一個象素的顏色,所以一個位元組可以表示2個象素。
對於256色點陣圖,一個位元組剛好可以表示1個象素。
對於真彩色圖,三個位元組才能表示1個象素,哇,好費空間呀!沒辦法,誰叫你想讓圖的顏色顯得更亮麗呢,有得必有失嘛。
要注意兩點:
(1)每一行的位元組數必須是4的整倍數,如果不是,則需要補齊。這在前面介紹biSizeImage時已經提到了。
(2)一般來說,.bMP檔案的資料從下到上,從左到右的。也就是說,從檔案中最先讀到的是圖象最下面一行的左邊第一個象素,然後是左邊第二個象素……接下來是倒數第二行左邊第一個象素,左邊第二個象素……依次類推 ,最後得到的是最上面一行的最右一個象素。
好了,終於介紹完bmp檔案結構了,是不是覺得頭有些大?彆著急,對照著下面的程式,你就會很清楚了(我最愛看源程式了,呵呵)。
下面的函式LoadBmpFile,其功能是從一個.bmp檔案中讀取資料(包括BITMAPINFOHEADER,調色盤和實際圖象資料),將其儲存在一個全域性記憶體控制代碼hImgData中,這個hImgData將在以後的圖象處理程式中用到。同時填寫一個型別為HBITMAP的全域性變數hBitmap和一個型別為HPALETTE的全域性變數hPalette。這兩個變數將在處理WM_PAINT訊息時用到,用來顯示點陣圖。該函式的兩個引數分別是用來顯示點陣圖的視窗控制代碼,和.bmp檔名(全路徑)。當函式成功時,返回TRUE,否則返回FALSE。
BITMAPFILEHEADER bf;
BITMAPINFOHEADER bi;
BOOL LoadBmpFile (HWND hWnd,char *BmpFileName)
{
HFILE hf; //檔案控制代碼
//指向BITMAPINFOHEADER結構的指標
LPBITMAPINFOHEADER lpImgData;
LOGPALETTE *pPal; //指向邏輯調色盤結構的指標
LPRGBQUAD lpRGB; //指向RGBQUAD結構的指標
HPALETTE hPrevPalette; //用來儲存裝置中原來的調色盤
HDC hDc; //裝置控制代碼
HLOCAL hPal; //儲存調色盤的區域性記憶體控制代碼
DWORD LineBytes; //每一行的位元組數
DWORD ImgSize; //實際的圖象資料佔用的位元組數
//實際用到的顏色數 ,即調色盤陣列中的顏色個數
DWORD NumColors;
DWORD i;
if((hf=_lopen(BmpFileName,OF_READ))==HFILE_ERROR){
MessageBox(hWnd,"File c://test.bmp not found!","Error Message",
MB_OK|MB_ICONEXCLAMATION);
return FALSE; //開啟檔案錯誤,返回
}
//將BITMAPFILEHEADER結構從檔案中讀出,填寫到bf中
_lread(hf,(LPSTR)&bf,sizeof(BITMAPFILEHEADER));
//將BITMAPINFOHEADER結構從檔案中讀出,填寫到bi中
_lread(hf,(LPSTR)&bi,sizeof(BITMAPINFOHEADER));
//我們定義了一個巨集 #define WIDTHBYTES(i) ((i+31)/32*4)上面曾經
//提到過,每一行的位元組數必須是4的整倍數,只要呼叫
//WIDTHBYTES(bi.biWidth*bi.biBitCount)就能完成這一換算。舉一個例
//子,對於2色圖,如果圖象寬是31,則每一行需要31位儲存,合3個
//位元組加7位,因為位元組數必須是4的整倍數,所以應該是4,而此時的
//biWidth=31,biBitCount=1,WIDTHBYTES(31*1)=4,和我們設想的一樣。
//再舉一個256色的例子,如果圖象寬是31,則每一行需要31個位元組存
//儲,因為位元組數必須是4的整倍數,所以應該是32,而此時的
//biWidth=31,biBitCount=8,WIDTHBYTES(31*8)=32,我們設想的一樣。你可
//以多舉幾個例子來驗證一下
//LineBytes為每一行的位元組數
LineBytes=(DWORD)WIDTHBYTES(bi.biWidth*bi.biBitCount);
//ImgSize為實際的圖象資料佔用的位元組數
ImgSize=(DWORD)LineBytes*bi.biHeight;
//NumColors為實際用到的顏色數 ,即調色盤陣列中的顏色個數
if(bi.biClrUsed!=0)
//如果bi.biClrUsed不為零,即為實際用到的顏色數
NumColors=(DWORD)bi.biClrUsed;
else //否則,用到的顏色數為2biBitCount。
switch(bi.biBitCount){
case 1:
NumColors=2;
break;
case 4:
NumColors=16;
break;
case 8:
NumColors=256;
break;
case 24:
NumColors=0; //對於真彩色圖,沒用到調色盤
break;
default: //不處理其它的顏色數,認為出錯。
MessageBox(hWnd,"Invalid color numbers!","Error Message",
MB_OK|MB_ICONEXCLAMATION);
_lclose(hf);
return FALSE; //關閉檔案,返回FALSE
}
if(bf.bfOffBits!=(DWORD)(NumColors*sizeof(RGBQUAD)+
sizeof(BITMAPFILEHEADER)+
sizeof(BITMAPINFOHEADER)))
{
//計算出的偏移量與實際偏移量不符,一定是顏色數出錯
MessageBox(hWnd,"Invalid color numbers!","Error Message",
MB_OK|MB_ICONEXCLAMATION);
_lclose(hf);
return FALSE; //關閉檔案,返回FALSE
}
bf.bfSize=sizeof(BITMAPFILEHEADER)+sizeof(BITMAPINFOHEADER)+
NumColors*sizeof(RGBQUAD)+ImgSize;
//分配記憶體,大小為BITMAPINFOHEADER結構長度加調色盤+實際點陣圖
if((hImgData=GlobalAlloc(GHND,(DWORD)
(sizeof(BITMAPINFOHEADER)+
NumColors*sizeof(RGBQUAD)+
ImgSize)))==NULL)
{
//分配記憶體錯誤
MessageBox(hWnd,"Error alloc memory!","ErrorMessage",MB_OK|
MB_ICONEXCLAMATION);
_lclose(hf);
return FALSE; //關閉檔案,返回FALSE
}
//指標lpImgData指向該記憶體區
lpImgData=(LPBITMAPINFOHEADER)GlobalLock(hImgData);
//檔案指標重新定位到BITMAPINFOHEADER開始處
_llseek(hf,sizeof(BITMAPFILEHEADER),SEEK_SET);
//將檔案內容讀入lpImgData
_hread(hf,(char *)lpImgData,(long)sizeof(BITMAPINFOHEADER)
+(long)NumColors*sizeof(RGBQUAD)+ImgSize);
_lclose(hf); //關閉檔案
if(NumColors!=0) //NumColors不為零,說明用到了調色盤
{
//為邏輯調色盤分配區域性記憶體,大小為邏輯調色盤結構長度加
//NumColors個PALETTENTRY
hPal=LocalAlloc(LHND,sizeof(LOGPALETTE)+
NumColors* sizeof(PALETTEENTRY));
//指標pPal指向該記憶體區
pPal =(LOGPALETTE *)LocalLock(hPal);
//填寫邏輯調色盤結構的頭
pPal->palNumEntries = NumColors;
pPal->palVersion = 0x300;
//lpRGB指向的是調色盤開始的位置
lpRGB = (LPRGBQUAD)((LPSTR)lpImgData +
(DWORD)sizeof(BITMAPINFOHEADER));
//填寫每一項
for (i = 0; i < NumColors; i++)
{
pPal->palPalEntry[i].peRed=lpRGB->rgbRed;
pPal->palPalEntry[i].peGreen=lpRGB->rgbGreen;
pPal->palPalEntry[i].peBlue=lpRGB->rgbBlue;
pPal->palPalEntry[i].peFlags=(BYTE)0;
lpRGB++; //指標移到下一項
}
//產生邏輯調色盤,hPalette是一個全域性變數
hPalette=CreatePalette(pPal);
//釋放區域性記憶體
LocalUnlock(hPal);
LocalFree(hPal);
}
//獲得裝置上下文控制代碼
hDc=GetDC(hWnd);
if(hPalette) //如果剛才產生了邏輯調色盤
{
//將新的邏輯調色盤選入DC,將舊的邏輯調色盤控制代碼儲存在//hPrevPalette
hPrevPalette=SelectPalette(hDc,hPalette,FALSE);
RealizePalette(hDc);
}
//產生點陣圖控制代碼
hBitmap=CreateDIBitmap(hDc,(LPBITMAPINFOHEADER)lpImgData,
(LONG)CBM_INIT,
(LPSTR)lpImgData+sizeof(BITMAPINFOHEADER)+NumColors*sizeof(RGBQUAD),
(LPBITMAPINFO)lpImgData, DIB_RGB_COLORS);
//將原來的調色盤(如果有的話)選入裝置上下文控制代碼
if(hPalette && hPrevPalette)
{
SelectPalette(hDc,hPrevPalette,FALSE);
RealizePalette(hDc);
}
ReleaseDC(hWnd,hDc); //釋放裝置上下文
GlobalUnlock(hImgData); //解鎖記憶體區
return TRUE; //成功返回
}
對上面的程式要說明兩點:
(1)對於需要調色盤的圖,要想正確地顯示,必須根據bmp檔案,產生邏輯調色盤。產生的方法是:①為邏輯調色盤指標分配記憶體,大小為邏輯調色盤結構(LOGPALETTE)長度加NumColors個PALETTENTRY大小(調色盤的每一項都是一個PALETTEENTRY結構);②填寫邏輯調色盤結構的頭pPal->palNumEntries = NumColors; pPal->palVersion = 0x300;③從檔案中讀取調色盤的RGB值,填寫到每一項中;④產生邏輯調色盤:hPalette=CreatePalette(pPal)。
(2)產生點陣圖(BITMAP)控制代碼,該項工作由函式CreateDIBitmap來完成。
hBitmap=CreateDIBitmap(hDc,(LPBITMAPINFOHEADER)lpImgData,
(LONG)CBM_INIT,
(LPSTR)lpImgData+sizeof(BITMAPINFOHEADER)+NumColors*sizeof(RGBQUAD),
(LPBITMAPINFO)lpImgData, DIB_RGB_COLORS);
CreateDIBitmap的作用是產生一個和Windows裝置無關的點陣圖。該函式的第一項引數為裝置上下文控制代碼。如果點陣圖用到了調色盤,要在呼叫CreateDIBitmap之前將邏輯調色盤選入該裝置上下文中,產生hBitmap後,再把原調色盤選入該裝置上下文中,並釋放該上下文;第二項為指向BITMAPINFOHEADER的指標;第三項就用常量CBM_INI,不用考慮;第四項為指向調色盤的指標;第五項為指向BITMAPINFO(包括BITMAPINFOHEADER,調色盤,及實際的圖象資料)的指標;第六項就用常量DIB_RGB_COLORS,不用考慮。
上面提到了裝置上下文,相信編過Windows程式的讀者對它並不陌生,這裡再簡單介紹一下。Windows作業系統統一管理著諸如顯示,列印等操作,將它們看作是一個個的裝置,每一個裝置都有一個複雜的資料結構來維護。所謂裝置上下文就是指這個資料結構。然而,我們不能直接和這些裝置上下文打交道,只能通過引用標識它的控制代碼(實際上是一個整數),讓Windows去做相應的處理。
產生的邏輯調色盤控制代碼hPalette和點陣圖控制代碼hBitmap要在處理WM_PAINT訊息時使用,這樣才能在螢幕上顯示出來,處理過程如下面的程式。
Static HDC hDC