VC++圖象處理編程
基本概念
前言
數(shù)字圖像處理技術(shù)與理論是計算機(jī)應(yīng)用的一個重要領(lǐng)域,,許多工程應(yīng)用都涉及到圖像處理,,一直有一個強(qiáng)烈的愿望,想系統(tǒng)的寫一個關(guān)于數(shù)字圖像處理的講座,,由于工作學(xué)習(xí)很忙,,時至今日才得以實(shí)現(xiàn)。
“圖”是物體透射光或反射光的分布,,“像”是人的視覺系統(tǒng)對圖的接收在大腦中形成的印象或認(rèn)識,。圖像是兩者的結(jié)合。人類獲取外界信息是靠聽覺,、視覺,、觸覺、嗅覺,、味覺等,,但絕大部分(約80%左右)來自視覺所接收的圖像信息。圖像處理就是對圖像信息進(jìn)行加工處理,,以滿足人的視覺心理和實(shí)際應(yīng)用的需要,。簡單的說,依靠計算機(jī)對圖像進(jìn)行各種目的的處理我們就稱之為數(shù)字圖像處理。早期的數(shù)字圖像處理的目的是以人為對象,,為了滿足人的視覺效果而改善圖像的質(zhì)量,,處理過程中輸入的是質(zhì)量差的圖像,輸出的是質(zhì)量好的圖像,,常用的圖像處理方法有圖像增強(qiáng),、復(fù)原等。隨著計算機(jī)技術(shù)的發(fā)展,,有一類圖像處理是以機(jī)器為對象,,處理的目的是使機(jī)器能夠自動識別目標(biāo),這稱之為圖像的識別,,因?yàn)檫@其中要牽涉到一些復(fù)雜的模式識別的理論,,所以我們后續(xù)的講座只討論其中最基本的內(nèi)容。由于在許多實(shí)際應(yīng)用的編程中往往都要涉及到數(shù)字圖像處理,,涉及到其中的一些算法,,這也是許多編程愛好者感興趣的一個內(nèi)容,我們這個講座就是討論如何利用微軟的Visual C++開發(fā)工具來實(shí)現(xiàn)一些常用的數(shù)字圖像處理算法,,論述了圖像處理的理論,,同時給出了VC實(shí)現(xiàn)的源代碼。本講座主要的內(nèi)容分為基礎(chǔ)篇,、中級篇和高級篇,,具體包含的主要內(nèi)容有:
1. 圖像文件的格式;
2. 圖像編程的基礎(chǔ)-操作調(diào)色板,;
3. 圖像數(shù)據(jù)的讀取,、存儲和顯示、如何獲取圖像的尺寸等,;
4. 利用圖像來美化界面,;
5. 圖像的基本操作:圖像移動、圖像旋轉(zhuǎn),、圖像鏡像,、圖像的縮放、圖像的剪切板操作,;
6. 圖像顯示的各種特技效果,;
7. 圖像的基本處理:圖像的二值化、圖像的亮度和對比度的調(diào)整,、圖像的邊緣增強(qiáng),、如何得到圖像的直方圖、圖像直方圖的修正,、圖像的平滑,、圖像的銳化等,、圖像的偽彩色、彩色圖像轉(zhuǎn)換為黑白圖像,、物體邊緣的搜索等等,;
8. 二值圖像的處理:腐蝕、膨脹,、細(xì)化,、距離變換等;
9. 圖像分析:直線,、圓,、特定物體的識別;
10.JEPG,、GIF、PCX等格式文件相關(guān)操作,;
11.圖像文件格式的轉(zhuǎn)換,;
12.圖像的常用變換:付利葉變換、DCT變換,、沃爾什變換等,;
13.AVI視頻流的操作;
圖像處理技術(shù)博大精深,,不僅需要有很強(qiáng)的數(shù)學(xué)功底,,還需要熟練掌握一門計算機(jī)語言,在當(dāng)前流行的語言中,,我個人覺的Visual C++這個開發(fā)平臺是圖像開發(fā)人員的首選工具,。本講座只是起到拋磚引玉的作用,希望和廣大讀者共同交流,。
圖象的文件格式
一.圖象的文件格式
要利用計算機(jī)對數(shù)字化圖像進(jìn)行處理,,首先要對圖像的文件格式要有清楚的認(rèn)識,因?yàn)槲覀兦懊嬲f過,,自然界的圖像以模擬信號的形式存在,,在用計算機(jī)進(jìn)行處理以前,首先要數(shù)字化,,比如攝像頭(CCD)攝取的信號在送往計算機(jī)處理前,,一般情況下要經(jīng)過數(shù)模轉(zhuǎn)換,這個任務(wù)常常由圖像采集卡完成,,它的輸出一般為裸圖的形式,;如果用戶想要生成目標(biāo)圖像文件,必須根據(jù)文件的格式做相應(yīng)的處理,。隨著科技的發(fā)展,,數(shù)碼像機(jī),、數(shù)碼攝像機(jī)已經(jīng)進(jìn)入尋常百姓家,我們可以利用這些設(shè)備作為圖像處理系統(tǒng)的輸入設(shè)備來為后續(xù)的圖像處理提供信息源,。無論是什么設(shè)備,,它總是提供按一定的圖像文件格式來提供信息,比較常用的有BMP格式,、JPEG格式,、GIF格式等等,所以我們在進(jìn)行圖像處理以前,,首先要對圖像的格式要有清晰的認(rèn)識,,只有在此基礎(chǔ)上才可以進(jìn)行進(jìn)一步的開發(fā)處理。
在講述圖像文件格式前,,先對圖像作一個簡單的分類,。除了最簡單的圖像外,所有的圖像都有顏色,,而單色圖像則是帶有顏色的圖像中比較簡單的格式,,它一般由黑色區(qū)域和白色區(qū)域組成,可以用一個比特表示一個像素,,“1”表示黑色,,“0”表示白色,當(dāng)然也可以倒過來表示,,這種圖像稱之為二值圖像,。我們也可以用8個比特(一個字節(jié))表示一個像素,相當(dāng)于把黑和白等分為256個級別,,“0”表示為黑,,“255”表示為白,該字節(jié)的數(shù)值表示相應(yīng)像素值的灰度值或亮度值,,數(shù)值越接近“0”,,對應(yīng)像素點(diǎn)越黑,相反,,則對應(yīng)像素點(diǎn)越白,,此種圖像我們一般稱之為灰度圖像。單色圖像和灰度圖像又統(tǒng)稱為黑白圖像,,與之對應(yīng)存在著彩色圖像,,這種圖像要復(fù)雜一些,表示圖像時,,常用的圖像彩色模式有RGB模式,、CMYK模式和HIS模式,一般情況下我們只使用RGB模式,,R對應(yīng)紅色,,G對應(yīng)綠色,,B對應(yīng)藍(lán)色,它們統(tǒng)稱為三基色,,這三中色彩的不同搭配,,就可以搭配成各種現(xiàn)實(shí)中的色彩,此時彩色圖像的每一個像素都需要3個樣本組成的一組數(shù)據(jù)表示,,其中每個樣本用于表示該像素的一個基本顏色,。
對于現(xiàn)存的所有的圖像文件格式,我們在這里主要介紹BMP圖像文件格式,,并且文件里的圖像數(shù)據(jù)是未壓縮的,,因?yàn)閳D像的數(shù)字化處理主要是對圖像中的各個像素進(jìn)行相應(yīng)的處理,而未壓縮的BMP圖像中的像素數(shù)值正好與實(shí)際要處理的數(shù)字圖像相對應(yīng),,這種格式的文件最合適我們對之進(jìn)行數(shù)字化處理,。請讀者記住,壓縮過的圖像是無法直接進(jìn)行數(shù)字化處理的,,如JPEG,、GIF等格式的文件,此時首先要對圖像文件解壓縮,,這就要涉及到一些比較復(fù)雜的壓縮算法。后續(xù)章節(jié)中我們將針對特殊的文件格式如何轉(zhuǎn)換為BMP格式的文件問題作專門的論述,,經(jīng)過轉(zhuǎn)換,,我們就可以利用得到的未壓縮的BMP文件格式進(jìn)行后續(xù)處理。對于JPEG,、GIF等格式,,由于涉及到壓縮算法,這要求讀者掌握一定的信息論方面的知識,,如果展開的話,,可以寫一本書,限于篇幅原因,,我們只作一般性的講解,,有興趣的朋友可以參考相關(guān)書籍資料。
二.BMP文件結(jié)構(gòu)
1 BMP文件的組成
BMP文件由文件頭,、位圖信息頭,、顏色信息和圖形數(shù)據(jù)四部分組成。文件頭主要包含文件的大小,、文件類型,、圖像數(shù)據(jù)偏離文件頭的長度等信息;位圖信息頭包含圖象的尺寸信息,、圖像用幾個比特數(shù)值來表示一個像素,、圖像是否壓縮,、圖像所用的顏色數(shù)等信息。顏色信息包含圖像所用到的顏色表,,顯示圖像時需用到這個顏色表來生成調(diào)色板,,但如果圖像為真彩色,既圖像的每個像素用24個比特來表示,,文件中就沒有這一塊信息,,也就不需要操作調(diào)色板。文件中的數(shù)據(jù)塊表示圖像的相應(yīng)的像素值,,需要注意的是:圖像的像素值在文件中的存放順序?yàn)閺淖蟮接?,從下到上,也就是說,,在BMP文件中首先存放的是圖像的最后一行像素,,最后才存儲圖像的第一行像素,但對與同一行的像素,,則是按照先左邊后右邊的的順序存儲的,;另外一個需要讀者朋友關(guān)注的細(xì)節(jié)是:文件存儲圖像的每一行像素值時,如果存儲該行像素值所占的字節(jié)數(shù)為4的倍數(shù),,則正常存儲,,否則,需要在后端補(bǔ)0,,湊足4的倍數(shù),。
2. BMP文件頭
BMP文件頭數(shù)據(jù)結(jié)構(gòu)含有BMP文件的類型、文件大小和位圖起始位置等信息,。其結(jié)構(gòu)定義如下:
typedef struct tagBITMAPFILEHEADER
{
WORD bfType; // 位圖文件的類型,,必須為“BMP”
DWORD bfSize; // 位圖文件的大小,以字節(jié)為單位
WORD bfReserved1; // 位圖文件保留字,,必須為0
WORD bfReserved2; // 位圖文件保留字,,必須為0
DWORD bfOffBits; // 位圖數(shù)據(jù)的起始位置,以相對于位圖文件頭的偏移量表示,,以字節(jié)為單位
} BITMAPFILEHEADER,;該結(jié)構(gòu)占據(jù)14個字節(jié)。
3. 位圖信息頭
BMP位圖信息頭數(shù)據(jù)用于說明位圖的尺寸等信息,。其結(jié)構(gòu)如下:
typedef struct tagBITMAPINFOHEADER{
DWORD biSize; // 本結(jié)構(gòu)所占用字節(jié)數(shù)
LONG biWidth; // 位圖的寬度,,以像素為單位
LONG biHeight; // 位圖的高度,以像素為單位
WORD biPlanes; // 目標(biāo)設(shè)備的平面數(shù)不清,,必須為1
WORD biBitCount// 每個像素所需的位數(shù),,必須是1(雙色), 4(16色),8(256色)或24(真彩色)之一
DWORD biCompression; // 位圖壓縮類型,,必須是 0(不壓縮),1(BI_RLE8壓縮類型)或2(BI_RLE4壓縮類型)之一
DWORD biSizeImage; // 位圖的大小,,以字節(jié)為單位
LONG biXPelsPerMeter; // 位圖水平分辨率,,每米像素數(shù)
LONG biYPelsPerMeter; // 位圖垂直分辨率,每米像素數(shù)
DWORD biClrUsed;// 位圖實(shí)際使用的顏色表中的顏色數(shù)
DWORD biClrImportant;// 位圖顯示過程中重要的顏色數(shù)
} BITMAPINFOHEADER,;該結(jié)構(gòu)占據(jù)40個字節(jié),。
注意:對于BMP文件格式,在處理單色圖像和真彩色圖像的時候,,無論圖象數(shù)據(jù)多么龐大,,都不對圖象數(shù)據(jù)進(jìn)行任何壓縮處理,一般情況下,,如果位圖采用壓縮格式,,那么16色圖像采用RLE4壓縮算法,256色圖像采用RLE8壓縮算法,。
4. 顏色表
顏色表用于說明位圖中的顏色,,它有若干個表項(xiàng),每一個表項(xiàng)是一個RGBQUAD類型的結(jié)構(gòu),,定義一種顏色,。RGBQUAD結(jié)構(gòu)的定義如下:
typedef struct tagRGBQUAD {
BYTErgbBlue;// 藍(lán)色的亮度(值范圍為0-255)
BYTErgbGreen; // 綠色的亮度(值范圍為0-255)
BYTErgbRed; // 紅色的亮度(值范圍為0-255)
BYTErgbReserved;// 保留,必須為0
} RGBQUAD;
顏色表中RGBQUAD結(jié)構(gòu)數(shù)據(jù)的個數(shù)由BITMAPINFOHEADER 中的biBitCount項(xiàng)來確定,,當(dāng)biBitCount=1,4,8時,,分別有2,16,256個顏色表項(xiàng),當(dāng)biBitCount=24時,,圖像為真彩色,,圖像中每個像素的顏色用三個字節(jié)表示,分別對應(yīng)R,、G、B值,,圖像文件沒有顏色表項(xiàng),。位圖信息頭和顏色表組成位圖信息,BITMAPINFO結(jié)構(gòu)定義如下:
typedef struct tagBITMAPINFO {
BITMAPINFOHEADER bmiHeader; // 位圖信息頭
RGBQUAD bmiColors[1]; // 顏色表
} BITMAPINFO;
注意:RGBQUAD數(shù)據(jù)結(jié)構(gòu)中,,增加了一個保留字段rgbReserved,,它不代表任何顏色,必須取固定的值為“0”,,同時,,RGBQUAD結(jié)構(gòu)中定義的顏色值中,紅色,、綠色和藍(lán)色的排列順序與一般真彩色圖像文件的顏色數(shù)據(jù)排列順序恰好相反,,既:若某個位圖中的一個像素點(diǎn)的顏色的描述為“00,00,,ff,,00”,,則表示該點(diǎn)為紅色,而不是藍(lán)色,。
5. 位圖數(shù)據(jù)
位圖數(shù)據(jù)記錄了位圖的每一個像素值或該對應(yīng)像素的顏色表的索引值,,圖像記錄順序是在掃描行內(nèi)是從左到右,掃描行之間是從下到上。這種格式我們又稱為Bottom_Up位圖,,當(dāng)然與之相對的還有Up_Down形式的位圖,,它的記錄順序是從上到下的,對于這種形式的位圖,,也不存在壓縮形式,。位圖的一個像素值所占的字節(jié)數(shù):當(dāng)biBitCount=1時,8個像素占1個字節(jié),;當(dāng)biBitCount=4時,,2個像素占1個字節(jié);當(dāng)biBitCount=8時,,1個像素占1個字節(jié),;當(dāng)biBitCount=24時,1個像素占3個字節(jié),此時圖像為真彩色圖像,。當(dāng)圖像不是為真彩色時,,圖像文件中包含顏色表,位圖的數(shù)據(jù)表示對應(yīng)像素點(diǎn)在顏色表中相應(yīng)的索引值,,當(dāng)為真彩色時,,每一個像素用三個字節(jié)表示圖像相應(yīng)像素點(diǎn)彩色值,每個字節(jié)分別對應(yīng)R,、G,、B分量的值,這時候圖像文件中沒有顏色表,。上面我已經(jīng)講過了,,Windows規(guī)定圖像文件中一個掃描行所占的字節(jié)數(shù)必須是4的倍數(shù)(即以字為單位),不足的以0填充,圖像文件中
一個掃描行所占的字節(jié)數(shù)計算方法:
DataSizePerLine= (biWidth* biBitCount+31)/8,;// 一個掃描行所占的字節(jié)數(shù)
位圖數(shù)據(jù)的大小按下式計算(不壓縮情況下):
DataSize= DataSizePerLine* biHeight,。
上述是BMP文件格式的說明,搞清楚了以上的結(jié)構(gòu),,就可以正確的操作圖像文件,,對它進(jìn)行讀或?qū)懖僮髁恕?/p>
三.GIF圖象文件格式
GIF圖象格式的全稱為Graphics Interchange Format,從這個名字可以看出,,這種圖像格式主要是為了通過網(wǎng)絡(luò)傳輸圖像而設(shè)計的,。GIF文件不支持24位真彩色圖像,最多只能存儲256色的圖像或灰度圖像;GIF格式文件也無法存儲CMY和HIS模型的圖像數(shù)據(jù),;另外,,GIF圖像文件的各種數(shù)據(jù)區(qū)域一般沒有固定的數(shù)據(jù)長度和存儲順序,所以為了方便程序?qū)ふ覕?shù)據(jù)區(qū),,將數(shù)據(jù)區(qū)中的第一個字節(jié)作為標(biāo)志符,;最后需要讀者注意的是GIF文件存儲圖像數(shù)據(jù)是有二種排列順序:順序排列或交叉排列。交叉排列的方式適合網(wǎng)絡(luò)傳輸,,這樣一來允許用戶在不完全掌握圖像數(shù)據(jù)之前,,獲取當(dāng)前圖像的輪廓數(shù)據(jù)。
GIF文件格式分為87和89兩個版本,,對于87這個版本,,該文件主要是有五個部分組成,它,,們是按順序出現(xiàn)的:文件頭塊,、邏輯屏幕描述塊、可選擇的調(diào)色板塊,、圖像數(shù)據(jù)塊,、最后是標(biāo)志文件結(jié)束的尾塊,該塊總是取固定的值3BH,。其中第一和第二兩個塊用GIF圖像文件頭結(jié)構(gòu)描述:
GIFHEADER:{
DB Signature; //該字段占六個字節(jié),,為了用于指明圖像為GIF格式,前三個字符必須為“GIF”,,后三字符用于指定是哪個版本,,87或89。
DW ScreenWidth;//
DW ScreenDepth;//占兩個字節(jié),,以像素為單位表示圖像的寬,、高
DB GlobalFlagByte;//該字節(jié)的各個位用于調(diào)色版的描述
DB BackGroundColor;//代表圖象的背景顏色的索引
DB AspectRatio;圖像的長寬比
}
GIF格式中的調(diào)色板有通用調(diào)色板和局部調(diào)色板之分,因?yàn)镚IF格式允許一個文件中存儲多個圖像,,因此有這兩種調(diào)色板,,其中通用調(diào)色板適于文件中的所有圖像,而局部調(diào)色板只適用于某一個圖像,。格式中的數(shù)據(jù)區(qū)域一般分為四個部分,圖像數(shù)據(jù)識別區(qū)域,,局部調(diào)色板數(shù)據(jù),,采用壓縮算法得到的圖象數(shù)據(jù)區(qū)域和結(jié)束標(biāo)志區(qū)域。
在GIF89版本中,,它包含七個部分,,分別是文件頭、通用調(diào)色板數(shù)據(jù)、圖像數(shù)據(jù)區(qū)和四個補(bǔ)充數(shù)據(jù)區(qū),,它們主要是用于提示程序如何處理圖像的.
四.JEPG圖像文件
JEPG簡稱為聯(lián)合攝影專家小組,,作為一種技術(shù),主要用于數(shù)字化圖像的標(biāo)準(zhǔn)編碼,,JPEG主要采用有損的壓縮編碼方式,,它比GIF、BMP圖像文件要復(fù)雜的多,,這不是短短的幾頁篇幅可以將清楚的,,萬幸的是,我們可以通過一些別的方法將該格式轉(zhuǎn)化為BMP格式,。讀者需要知道的是在對JEPG文件格式編碼時,,通常需要分為以下四步:顏色轉(zhuǎn)化、DCT變換,、量化,、編碼。
以上介紹了一些常用的圖像文件,,對比較復(fù)雜的格式,,如GIF和JEPG,僅僅作了極其浮淺的介紹,,后文我們會和它們作進(jìn)一步的接觸,。實(shí)際應(yīng)用中,還有許多圖像格式,,文章中都沒有提到,,讀者如果需要做進(jìn)一步的研究,還需要參考一些關(guān)于圖像格式方面的資料,。
BMP圖像的基本操作
上一講我們主要介紹了圖像的格式,,其中重點(diǎn)說明了BMP文件的存儲格式,同時對JEPG和GIF等常用格式作了簡單的介紹,。本節(jié)主要講述如何操作BMP文件,,如對其讀、寫和顯示等,。
在實(shí)現(xiàn)數(shù)字圖象處理的過程中,,主要是通過對圖像中的每一個像素點(diǎn)運(yùn)用各種圖像處理算法來達(dá)到預(yù)期的效果,所以進(jìn)行圖像處理的第一步,,也是我們最關(guān)心的問題,,是如何得到圖像中每一個像素點(diǎn)的亮度值;為了觀察和驗(yàn)證處理的圖像效果,,另一個需要解決的問題是如何將處理前后的圖像正確的顯示出來,。我們這章內(nèi)容就是解決這些問題。
隨著科技的發(fā)展,圖像處理技術(shù)已經(jīng)滲透到人類生活的各個領(lǐng)域并得到越來越多的應(yīng)用,,但是突出的一個矛盾是圖像的格式也是越來越多,,目前圖像處理所涉及的主要的圖像格式就有很多種,如TIF,、JEMP,、BMP等等,一般情況下,,為了處理簡單方便,,進(jìn)行數(shù)字圖像處理所采用的都是BMP格式的圖像文件(有時也稱為DIB格式的圖像文件),并且這種格式的文件是沒有壓縮的,。我們通過操作這種格式的文件,,可以獲取正確顯示圖像所需的調(diào)色板信息,圖像的尺寸信息,,圖像中各個像素點(diǎn)的亮度信息等等,,有了這些數(shù)據(jù),開發(fā)人員就可以對圖像施加各種處理算法,,進(jìn)行相應(yīng)的處理,。如果特殊情況下需要處理其它某種格式的圖像,如GIF,、JEMP等格式的圖象文件,,可以首先將該格式轉(zhuǎn)換為BMP格式,然后再進(jìn)行相應(yīng)的處理,。這一點(diǎn)需要讀者清楚,。
BMP格式的圖像文件又可以分為許多種類,如真彩色位圖,、256色位圖,,采用RLE(游程編碼)壓縮格式的BMP位圖等等。由于在實(shí)際的工程應(yīng)用和圖像算法效果驗(yàn)證中經(jīng)常要處理的是256色并且是沒有壓縮的BMP灰度圖像,,例如通過黑白采集卡采集得到的圖像就是這種格式,,所以我們在整個講座中范例所處理的文件格式都是BMP灰度圖像。如果讀者對這種格式的位圖能夠作到熟練的操作,,那么對于其余形式的BMP位圖的操作也不會很困難,。
BMP灰度圖像作為Windows環(huán)境下主要的圖像格式之一,以其格式簡單,,適應(yīng)性強(qiáng)而倍受歡迎,。正如我們在上一講中介紹過的那樣,這種文件格式就是每一個像素用8bit表示,,顯示出來的圖像是黑白效果,最黑的像素的灰度(也叫作亮度)值為“0”,最白的像素的灰度值為“255”,,整個圖像各個像素的灰度值隨機(jī)的分布在“0”到“255”的區(qū)間中,,越黑的像素,其灰度值越接近于“0”,,越白(既越亮)的像素,,其灰度值越接近于“255”;與此對應(yīng)的是在該文件類型中的顏色表項(xiàng)的各個RGB分量值是相等的,,并且顏色表項(xiàng)的數(shù)目是256個,。
在進(jìn)行圖像處理時,操作圖像中的像素值就要得到圖像陣列,;經(jīng)過處理后的圖像的像素值需要存儲起來,;顯示圖像時要正確實(shí)現(xiàn)調(diào)色板、得到位圖的尺寸信息等,。結(jié)合這些問題,,下面我們針對性的給出了操作灰度BMP圖像時的部分函數(shù)實(shí)現(xiàn)代碼及注釋。
一.BMP位圖操作
回顧:BMP位圖包括位圖文件頭結(jié)構(gòu)BITMAPFILEHEADER,、位圖信息頭結(jié)構(gòu)BITMAPINFOHEADER,、位圖顏色表RGBQUAD和位圖像素數(shù)據(jù)四部分。處理位圖時要根據(jù)文件的這些結(jié)構(gòu)得到位圖文件大小,、位圖的寬,、高、實(shí)現(xiàn)調(diào)色板,、得到位圖像素值等等,。這里要注意的一點(diǎn)是在BMP位圖中,位圖的每行像素值要填充到一個四字節(jié)邊界,,即位圖每行所占的存儲長度為四字節(jié)的倍數(shù),,不足時將多余位用0填充。
有了上述知識,,可以開始編寫圖像處理的程序了,,關(guān)于在VC的開發(fā)平臺上如何開發(fā)程序的問題這里不再贅述,筆者假定讀者都具有一定的VC開發(fā)經(jīng)驗(yàn),。在開發(fā)該圖像處理程序的過程中,,筆者沒有采用面向?qū)ο蟮姆椒ǎm然面向?qū)ο蟮姆椒梢詫?shù)據(jù)封裝起來,,保護(hù)類中的數(shù)據(jù)不受外界的干擾,,提高數(shù)據(jù)的安全性,但是這種安全性是以降低程序的執(zhí)行效率為代價的,,為此,,我們充分利用了程序的文檔視圖結(jié)構(gòu),,在程序中直接使用了一些API函數(shù)來操作圖像。在微軟的MSDN中有一個名為Diblook的例子,,該例子演示了如何操作Dib位圖,,有興趣的讀者可以參考一下,相信一定會有所收獲,。
啟動Visual C++,,生成一個名為Dib的多文檔程序,將CDibView類的基類設(shè)為CscrollView類,,這樣作的目的是為了在顯示位圖時支持滾動條,,另外在處理圖像應(yīng)用程序的文檔類(CDibDoc.h)中聲明如下宏及公有變量:
#define WIDTHBYTES(bits) (((bits) + 31) / 32 * 4)//計算圖像每行象素所占的字節(jié)數(shù)目;
HANDLE m_hDIB;//存放位圖數(shù)據(jù)的句柄,;
CPalette* m_palDIB;//指向調(diào)色板Cpalette類的指針,;
CSize m_sizeDoc;//初始化視圖的尺寸,該尺寸為位圖的尺寸,;
最后將程序的字符串表中的字符串資源IDR_DibTYPE修改為:“\nDib\nDib\nDib Files(*.bmp;*.dib)\n.bmp\nDib.Document\nDib Document”,。這樣作的目的是為了在程序文件對話框中可以選擇BMP或DIB格式的位圖文件。
1,、 讀取灰度BMP位圖
可以根據(jù)BMP位圖文件的結(jié)構(gòu),,操作BMP位圖文件并讀入圖像數(shù)據(jù),為此我們充分利用了VC的文檔視圖結(jié)構(gòu),,重載了文擋類的OnOpenDocument()函數(shù),,這樣用戶就可以在自動生成程序的打開文件對話框中選擇所要打開的位圖文件,然后程序?qū)⒆詣诱{(diào)用該函數(shù)執(zhí)行讀取數(shù)據(jù)的操作,。該函數(shù)的實(shí)現(xiàn)代碼如下所示:
BOOL CDibDoc::OnOpenDocument(LPCTSTR lpszPathName)
{
LOGPALETTE *pPal;//定義邏輯調(diào)色板指針,;
pPal=new LOGPALETTE;//初始化該指針;
CFile file;
CFileException fe;
if (!file.Open(lpszPathName, CFile::modeRead | CFile::shareDenyWrite, &fe))
{//以“讀”的方式打開文件,;
AfxMessageBox("圖像文件打不開,!");
return FALSE;
}
DeleteContents();//刪除文擋;
BeginWaitCursor();
BITMAPFILEHEADER bmfHeader;//定義位圖文件頭結(jié)構(gòu),;
LPBITMAPINFO lpbmi;
DWORD dwBitsSize;
HANDLE hDIB;
LPSTR pDIB;//指向位圖數(shù)據(jù)的指針,;
BITMAPINFOHEADER *bmhdr;//指向位圖信息頭結(jié)構(gòu)的指針
dwBitsSize = file.GetLength();//得到文件長度
if (file.Read((LPSTR)&bmfHeader, sizeof(bmfHeader)) !=sizeof(bmfHeader))
return FALSE;//讀取位圖文件的文件頭結(jié)構(gòu)信息;
if (bmfHeader.bfType != 0x4d42) //檢查該文件是否為BMP格式的文件,;
return FALSE;
hDIB=(HANDLE) ::GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, dwBitsSize);
//為讀取圖像文件數(shù)據(jù)申請緩沖區(qū)
if (hDIB == 0)
{
return FALSE;
}
pDIB = (LPSTR) ::GlobalLock((HGLOBAL)hDIB);
//得到申請的緩沖區(qū)的指針,;
if (file.ReadHuge(pDIB, dwBitsSize - sizeof(BITMAPFILEHEADER)) !=
dwBitsSize - sizeof(BITMAPFILEHEADER) )
{
::GlobalUnlock((HGLOBAL)hDIB);
hDIB=NULL;
return FALSE;
}//此時pDIB數(shù)據(jù)塊中讀取的數(shù)據(jù)包括位圖頭信息、位圖顏色表,、圖像像素的灰度值,;
bmhdr=(BITMAPINFOHEADER*)pDIB;//為指向位圖信息頭結(jié)構(gòu)的指針賦值;
::GlobalUnlock((HGLOBAL)hDIB);
if ((*bmhdr).biBitCount!=8)//驗(yàn)證是否為8bit位圖
{
AfxMessageBox("該文件不是灰度位圖格式,!");
return FALSE;
}
m_hDIB=hDIB;//將內(nèi)部變量數(shù)據(jù)賦于全局變量,;
//下面是記錄位圖的尺寸,;
m_sizeDoc.x=bmhdr->biWidth;
m_sizeDoc.y=bmhdr->biHeight;
//下面是根據(jù)顏色表生成調(diào)色板;
m_palDIB=new Cpalette;
pPal->palVersion=0x300;//填充邏輯顏色表
pPal->palNumEntries=256;
lpbmi=(LPBITMAPINFO)bmhdr;
for(int i=0;i<256;i++)
{//每個顏色表項(xiàng)的R,、G,、B值相等,并且各個值從“0”到“255”序列展開,;
Pal->palPalentry[i].peRed=lpbmi->bmiColors[i].rgbRed;
pPal->palPalentry[i].peGreen=lpbmi->bmiColors[i].rgbGreen;
pPal->palPalentry[i].peBlue= lpbmi->bmiColors[i].rgbBlue;;
pPal->palPalentry[i].peFlags=0;
}
m_palDIB->CreatePalette(pPal);
//根據(jù)讀入的數(shù)據(jù)得到位圖的寬、高,、顏色表;
if(pPal)
delete pPal;
EndWaitCursor();
SetPathName(lpszPathName);//設(shè)置存儲路徑
SetModifiedFlag(FALSE); // 設(shè)置文件修改標(biāo)志為FALSE
return TRUE;
}
上面的方法是通過CFile類對象的操作來讀取位圖文件的,,它需要分析位圖中的文件頭信息,從而確定需要讀取的圖像長度,。這種方法相對來說有些繁瑣,,其實(shí)還可以以一種相對簡單的方法讀取位圖數(shù)據(jù),首先在程序的資源中定義DIB類型資源,,然后添加位圖到該類型中,,將圖像數(shù)據(jù)以資源的形式讀取出來,這時候就可以根據(jù)所獲取的數(shù)據(jù)中的位圖信息結(jié)構(gòu)來獲取,、顯示圖像數(shù)據(jù)了,。下面的函數(shù)實(shí)現(xiàn)了以資源形式裝載圖像文件數(shù)據(jù),該函數(shù)的實(shí)現(xiàn)代碼如下所示:
HANDLE LoadDIB(UINT uIDS, LPCSTR lpszDibType)
{
LPCSTR lpszDibRes =MAKEINTRESOURCE(uIDS);//根據(jù)資源標(biāo)志符確定資源的名字,;
HINSTANCE hInst=AfxGetInstanceHandle();//得到應(yīng)用程序的句柄,;
HRSRC hRes=::FindResource(hInst,lpszDibRes, lpszDibType);//獲取資源的句柄,這里lpszDibType為資源的名字“DIB”;
If(hRes==NULL)
return NULL
HGLOBAL hData=::LoadResource(hInst, hRes);//轉(zhuǎn)載資源數(shù)據(jù)并返回該句柄,;
return hData;
}
2,、 灰度位圖數(shù)據(jù)的存儲
為了將圖像處理后所得到的像素值保存起來,我們重載了文檔類的OnSaveDocument()函數(shù),,這樣用戶在點(diǎn)擊Save或SaveAs子菜單后程序自動調(diào)用該函數(shù),,實(shí)現(xiàn)圖像數(shù)據(jù)的存儲。該函數(shù)的具體實(shí)現(xiàn)如下:
BOOL CDibDoc::OnSaveDocument(LPCTSTR lpszPathName)
{
CFile file;
CFileException fe;
BITMAPFILEHEADER bmfHdr; // 位圖文件頭結(jié)構(gòu),;
LPBITMAPINFOHEADER lpBI;//指向位圖頭信息結(jié)構(gòu)的指針,;
DWORD dwDIBSize;;
if (!file.Open(lpszPathName, CFile::modeCreate |CFile::modeReadWrite | CFile::shareExclusive, &fe))
{
AfxMessageBox("文件打不開"),;
return FALSE;
}//以讀寫的方式打開文件,;
BOOL bSuccess = FALSE;
BeginWaitCursor();
lpBI = (LPBITMAPINFOHEADER) ::GlobalLock((HGLOBAL) m_hDIB);
if (lpBI == NULL)
return FALSE;
dwDIBSize = *(LPDWORD)lpBI + 256*sizeof(RGBQUAD);
//圖像的文件信息所占用的字節(jié)數(shù);
DWORD dwBmBitsSize;//BMP文件中位圖的像素所占的字節(jié)數(shù)
dwBmBitsSize=WIDTHBYTES((lpBI->biWidth)*((DWORD)lpBI->biBitCount))
*lpBI->biHeight;// 存儲時位圖所有像素所占的總字節(jié)數(shù)
dwDIBSize += dwBmBitsSize; //BMP文件除文件信息結(jié)構(gòu)外的所有數(shù)據(jù)占用的總字節(jié)數(shù),;
lpBI->biSizeImage = dwBmBitsSize; // 位圖所有像素所占的總字節(jié)數(shù)
//以下五句為文件頭結(jié)構(gòu)填充值
bmfHdr.bfType =0x4d42; // 文件為"BMP"類型
bmfHdr.bfSize = dwDIBSize + sizeof(BITMAPFILEHEADER);//文件總長度
bmfHdr.bfReserved1 = 0;
bmfHdr.bfReserved2 = 0;
bmfHdr.bfOffBits = (DWORD)sizeof(BITMAPFILEHEADER) + lpBI->biSize
+ 256*sizeof(RGBQUAD);
//位圖數(shù)據(jù)距離文件頭的偏移量,;
file.Write((LPSTR)&bmfHdr, sizeof(BITMAPFILEHEADER));//向文件中寫文件頭信息;
file.WriteHuge(lpBI, dwDIBSize);
//將位圖信息(信息頭結(jié)構(gòu),、顏色表,、像素數(shù)據(jù))寫入文件,;
::GlobalUnlock((HGLOBAL) m_hDIB);
EndWaitCursor();
SetModifiedFlag(FALSE),; // 將文檔設(shè)為“干凈”標(biāo)志,,表示此后文檔不需要存盤提示;
return TRUE,;
}
二.調(diào)色板的操作
通過上面的操作,,我們已經(jīng)可以獲取圖像中的數(shù)據(jù)了,現(xiàn)在的又一個問題是如何在窗口中顯示出圖像數(shù)據(jù),?;叶葓D像要正確顯示,必須實(shí)現(xiàn)邏輯調(diào)色板和系統(tǒng)調(diào)色板,。首先我們介紹一下邏輯調(diào)色板結(jié)構(gòu)LOGPALETTE,,該結(jié)構(gòu)定義如下:
typedef struct tagLOGPALETTE
{
WORD palVersion;//調(diào)色板的板本號,應(yīng)該指定該值為0x300;
WORD palNumEntries;//調(diào)色板中的表項(xiàng)數(shù),,對于灰度圖像該值為256,;
PALETEENTRY palPalEntry[1];//調(diào)色板中的顏色表項(xiàng),由于該表項(xiàng)的數(shù)目不一定,,所以這里數(shù)組長度定義為1,灰度圖像對應(yīng)的該數(shù)組的長度為256;
}LOGPALETTE;
顏色表項(xiàng)結(jié)構(gòu)PALETTEENTRY定義了調(diào)色板中的每一個顏色表項(xiàng)的顏色和使用方式,,定義如下:
typedef struct tagPALETTEENTRY
{
BYTE peRed; //R分量值;
BYTE peGreen; //G分量值,;
BYTE peBlue; //B分量值,;
BYTE peFlags; // 該顏色被使用的方式,一般情況下設(shè)為“0”,;
}PALETTEENTRY;
Windows系統(tǒng)使用調(diào)色板管理器來管理與調(diào)色板有關(guān)的操作,,通常活動窗口的調(diào)色板即是當(dāng)前系統(tǒng)調(diào)色板,,所有的非活動窗口都必須按照此系統(tǒng)調(diào)色板來顯示自己的顏色,,此時調(diào)色板管理器將自動的用系統(tǒng)調(diào)色板中的最近似顏色來映射相應(yīng)的顯示顏色。如果窗口或應(yīng)用程序按自己的調(diào)色板顯示顏色,,就必須將自己的調(diào)色板載入到系統(tǒng)調(diào)色板中,,這種操作叫作實(shí)現(xiàn)調(diào)色板,實(shí)現(xiàn)調(diào)色板包括兩個步驟,,既首先將調(diào)色板選擇到設(shè)備上下文中,,然后在設(shè)備上下文中實(shí)現(xiàn)它??梢酝ㄟ^CDC::SelectPalette(),、CDC::RealizePalette()或相應(yīng)的API函數(shù)來實(shí)現(xiàn)上述的兩個步驟。在實(shí)現(xiàn)調(diào)色板的過程中,,通過在主框架類中處理Windows定義的消息WM_QUERYNEWPALETTE ,、WM_PALETTECHANGED及視圖類中處理自定義消息WM_DOREALIZE(該消息在主框架窗口定義如下:#define WM_REALIZEPAL (WM_USER+101))來實(shí)現(xiàn)調(diào)色板的操作,。當(dāng)系統(tǒng)需要處理調(diào)色板的變化時,將向程序的主窗口發(fā)送WM_QUERYNEWPALETTE ,、WM_PALETTECHANGED,,例如當(dāng)某一窗口即將激活時,主框架窗口將收到WM_QUERYNEWPALETTE消息,,通知該窗口將要收到輸入焦點(diǎn),,給它一次機(jī)會實(shí)現(xiàn)其自身的邏輯調(diào)色板;當(dāng)系統(tǒng)調(diào)色板改變后,,主框架窗口將收到WM_PALETTECHANGED消息,,通知其它窗口系統(tǒng)調(diào)色板已經(jīng)改變,此時每一窗口都應(yīng)該實(shí)現(xiàn)其邏輯調(diào)色板,,重畫客戶區(qū)。
由于上述的調(diào)色板變更消息是發(fā)往主框架窗口的,,所以我們只能在主窗口中響應(yīng)這兩個消息,,然后由主框架窗口通知各個視窗,使得程序激活時能自動裝載自己的調(diào)色板,。我們定義的用戶消息WM_REALIZEPAL用于主框架窗口通知視窗它已經(jīng)收到調(diào)色板變更消息,,視窗應(yīng)該協(xié)調(diào)其調(diào)色板。下面我們給出了各個消息的響應(yīng)處理函數(shù)的具體實(shí)現(xiàn)代碼和注釋:
//////////////////////////////////////////////////////////////////////////
void CMainFrame::OnPaletteChanged(CWnd* pFocusWnd)
{//總實(shí)現(xiàn)活動視的調(diào)色板
CMDIFrameWnd::OnPaletteChanged(pFocusWnd);
CMDIChildWnd* pMDIChildWnd = MDIGetActive();//得到活動的子窗口指針,;
if (pMDIChildWnd == NULL)
return;
CView* pView = pMDIChildWnd->GetActiveView();//得到視圖的指針,;
ASSERT(pView != NULL);
SendMessageToDescendants(WM_DOREALIZE, (WPARAM)pView->m_hWnd);
//通知所有子窗口系統(tǒng)調(diào)色板已改變
}
////////////////////////////////////////////////
BOOL CMainFrame::OnQueryNewPalette()//提供實(shí)現(xiàn)系統(tǒng)調(diào)色板的機(jī)會
{
// 實(shí)現(xiàn)活動視的調(diào)色板
CMDIChildWnd* pMDIChildWnd = MDIGetActive();//得到活動的子窗口指針;
if (pMDIChildWnd == NULL)
return FALSE;//no active MDI child frame (no new palette)
CView* pView = pMDIChildWnd->GetActiveView();//得到活動子窗口的視圖指針,;
ASSERT(pView != NULL);
//通知活動視圖實(shí)現(xiàn)系統(tǒng)調(diào)色板
pView->SendMessage(WM_DOREALIZE, (WPARAM)pView->m_hWnd);
return TRUE;
}
/////////////////////////////////////////////////
BOOL CDibView::OnDoRealize(WPARAM wParam, LPARAM)//實(shí)現(xiàn)系統(tǒng)調(diào)色板
{
ASSERT(wParam != NULL);
CDibDoc* pDoc = GetDocument();
if (pDoc->m_hDIB == NULL)
return FALSE; // must be a new document
CPalette* pPal = pDoc->m_palDIB;
//調(diào)色板的顏色表數(shù)據(jù)在InitDIBData()函數(shù)中實(shí)現(xiàn)
if (pPal != NULL)
{
CMainFrame* pAppFrame = (CMainFrame*) AfxGetApp()->m_pMainWnd;//得到程序的主框架指針,;
ASSERT_KINDOF(CMainFrame, pAppFrame);
CClientDC appDC(pAppFrame);//獲取主框架的設(shè)備上下文;
CPalette* oldPalette = appDC.SelectPalette(pPal, ((HWND)wParam) != m_hWnd);
//只有活動視才可以設(shè)為"FALSE",,即根據(jù)活動視的調(diào)色板設(shè)為"前景"調(diào)色板,;
if (oldPalette != NULL)
{
UINT nColorsChanged = appDC.RealizePalette();//實(shí)現(xiàn)系統(tǒng)調(diào)色板
if (nColorsChanged > 0)
pDoc->UpdateAllViews(NULL);//更新視圖
appDC.SelectPalette(oldPalette, TRUE);
//將原系統(tǒng)調(diào)色板置為背景調(diào)色板
}
else
{
TRACE0(“\tSelectPalette failed in”);
}
return TRUE;
}
注:在調(diào)用API函數(shù)顯示位圖時,,不要忘記設(shè)置邏輯調(diào)色板,,即"背景"調(diào)色板,否則位圖將無法正確顯示,,讀者可以從后面的顯示部分的實(shí)現(xiàn)看出我們在顯示時實(shí)現(xiàn)了邏輯調(diào)色板,。上述的處理相對來說比較繁瑣復(fù)雜,可能對于初學(xué)者來說也比較難于理解,,所以如果我們的程序僅僅限于處理灰度圖象,,可以采用另外一種相對簡單的辦法,即在文檔類的初始化階段定義一個灰度調(diào)色板,,然后在設(shè)備上下文中實(shí)現(xiàn)它,,這樣作的好處是在度取灰度位圖時可以不再考慮文件中的顏色表信息,,提高了文件讀取速度,筆者在開發(fā)一個基于機(jī)器視覺的項(xiàng)目時采用的就是這種方法,,取的了比較滿意的效果,。首先定義一個指向邏輯顏色表結(jié)構(gòu)LOGPALETTE的指針pPal,填充該指針,,然后將該指針與調(diào)色板指針聯(lián)系起來,,該方法的具體實(shí)現(xiàn)如下:
/////////////////////////////////////////////////////////
CDibDoc::CDibDoc()
{
……………………….
LOGPALETTE *Pal;
Pal=new LOGPALETTE;
m_palDIB=new Cpalette;
pPal->palVersion=0x300;
pPal->palNumEntries=256;
for(int i=0;i<256;i++)
{//每個顏色表項(xiàng)的R、G,、B值相等,,并且各個值從“0”到“255”序列展開;
Pal->palPalentry[i].peRed=i;
pPal->palPalentry[i].peGreen=i;
pPal->palPalentry[i].peBlue=i;
pPal->palPalentry[i].peFlags=0;
}
m_palDIB->CreatePalette(pPal);
…………………..
}
三.圖象的顯示
顯示DIB位圖數(shù)據(jù)可以通過設(shè)備上下文CDC對象的成員函數(shù)CDC::Bitblt()或CDC::StretchBlt()來實(shí)現(xiàn),,也可以通過API函數(shù)SetDIBBitsToDevice()或StretchDIBBits()來實(shí)現(xiàn),,函數(shù)中具體所用到的各個參數(shù)的意義可以參考MSDN。其中StretchDIBBits()和CDC::StretchBlt()可以將圖像進(jìn)行放大和縮小顯示,。當(dāng)從文檔中裝入位圖文件時,,CDIBView類的OnInitialUpdate函數(shù)將被調(diào)用,因此可以在該函數(shù)中實(shí)現(xiàn)對視圖尺寸的設(shè)置,,用于正確的顯示位圖,,然后就可以在視圖類的OnDraw()函數(shù)中正確的顯示位圖了。這兩個函數(shù)的具體實(shí)現(xiàn)代碼分別如下所示:
/////////////////////////////////////////////////////////////
void CDIBView::OnInitialUpdate()
{
CscrollView::OnInitalUpdate();
CDIBDoc *pDoc=GetDocument();
If(pDoc->m_hDIB==NULL)//如果位圖數(shù)據(jù)為空,,設(shè)置m_sizeDoc的默認(rèn)尺寸,;
pDoc->m_sizeDoc.cx=pDoc->m_sizeDoc.cy=100;
SetScrollSizes(MM_TEXT,pDoc-> m_sizeDoc);
}
/////////////////////////////////////////////////////////////
void CDIBView::OnDraw(CDC *pDC)
{
BITMAPINFOHEADER *lpDIBHdr;//位圖信息頭結(jié)構(gòu)指針;
BYTE *lpDIBBits;//指向位圖像素灰度值的指針,;
BOOL bSuccess=FALSE;
CPalette*OldPal=NULL;//調(diào)色板指針,;
HDC hDC=pDC->GetSafeHdc();//獲取當(dāng)前設(shè)備上下文的句柄;
CDIBDoc *pDoc=GetDocument();//獲取活動文檔的指針,;
If(pDoc->m_hDIB ==NULL)
{//判斷圖像數(shù)據(jù)是否為空,;
AfxMessageBox("圖像數(shù)據(jù)不能為空,請首先讀取圖像數(shù)據(jù),!");
return;
}
lpDIBHdr=( BITMAPINFOHEADER *)GlobalLock(pDoc->m_hDIB);//得到圖像的位圖頭信息
lpDIBBits=lpDIBHdr+sizeof(BITMAPINFOHEADER)+256*sizeof(RGBQUAD);//獲取保存圖像像素值的緩沖區(qū)的指針,;
if(pDoc-> m_palDIB)
{//如果存在調(diào)色板信息,實(shí)現(xiàn)邏輯調(diào)色板,;
OldPal=pDC-> SelectPalette(pDoc-> m_palDIB,TRUE);
PDC->RealizePalette();
}
else
{
AfxMessageBox("圖像的調(diào)色板數(shù)據(jù)不能為空,,請首先讀取調(diào)色板信息!");
return ;
}
SetStretchBltMode(hDC,COLORONCOLOR);
//顯示圖像
BSuccess=StretchDIBBits(hDC,0,0,pDoc-> m_sizeDoc.cx, pDoc-> m_sizeDoc.cy,
0, pDoc-> m_sizeDoc.cy,0, pDoc-> m_sizeDoc.cy,
lpDIBBits,(LPBITMAPINFO)lpDIBHdr,
DIB_RGB_COLORS,
SRCCOPY);
GlobalUnlock(pDoc->m_hDIB);
If(OldPal)//恢復(fù)調(diào)色板,;
PDC->SelectPalette(OldPal,FALSE);
retrun;
}
BMP圖像顯示的特效操作
在上面的學(xué)習(xí)基礎(chǔ)上,,我們可以進(jìn)一步深化,學(xué)習(xí)并掌握圖像特效顯示技術(shù)。有了這種技術(shù),,可以用來在今后的項(xiàng)目開發(fā)中美化我們的軟件界面,,提高軟件的視覺效果。在如今的商業(yè)軟件中,,幾乎每一幅圖像的顯示都采用了圖像特效顯示,,例如讀者比較熟悉的Windows的屏幕保護(hù)程序就采用了各種各樣的圖像特效顯示,使人感到眼花繚亂和耳目一新,。專業(yè)圖像處理軟件更是提供了豐富的顯示方式供用戶使用,,可以方便的在程序中實(shí)現(xiàn)圖像的特效顯示,如PhotoShop ,、Authorware等,。本節(jié)主要介紹如何實(shí)現(xiàn)圖像的浮雕、雕刻,、百頁窗,、旋轉(zhuǎn)、掃描,、柵條,、馬賽克、和漸顯漸隱顯示等效果,。通過這期講座的學(xué)習(xí),讀者朋友們也可以自己動手制作擁有特效顯示效果的軟件了,。
圖像的顯示我們講過主要有BitBlt(),、SetDIBitsToDevice()和StretchDIBits()等函數(shù)。需要讀者注意的是,,在特效顯示時,,并不是每個顯示函數(shù)都適宜,BitBlt()函數(shù)主要是用來顯示設(shè)備無關(guān)位圖(DDB),,后兩個函數(shù)用來顯示設(shè)備無關(guān)位圖(DIB),。由于我們講座里處理的是設(shè)備無關(guān)位圖,所以我們主要關(guān)心的是后兩個函數(shù)的應(yīng)用,,其中SetDIBitsToDevice()使用起來較死板,,遠(yuǎn)不如StretchDIBits()用的靈活,并且對大多數(shù)的特效顯示無能為力,,所以為了實(shí)現(xiàn)圖像的特效顯示效果,,需要使用StretchDIBits()函數(shù)來顯示圖像,具體什么原因,,我想可能是微軟在實(shí)現(xiàn)這些函數(shù)時使用的方法不同吧,。這些函數(shù)如何使用,各個參數(shù)的含義,可以參考微軟的MSDN,。
實(shí)現(xiàn)圖像的特殊效果的顯示的基本思路是要么是操作圖像的像素,,要么是對圖像分塊按一定的方向或次序,分階段的顯示或擦除對應(yīng)的圖像塊,。對于第二種顯示的思路,,其中的要點(diǎn)是:1.劃分圖像塊;2.確定圖像塊的操作次序,;3.顯示或清除對應(yīng)的圖像塊,;4.在兩個連續(xù)顯示的圖像塊之間插入一個固定的延遲。其中圖像塊的劃分決定了圖像的顯示方式,,圖像塊的顯示順序決定了顯示的方向和細(xì)分的依據(jù),。不同的效果決定了不同的分塊方法和顯示次序,我們將在后面的各種特效顯示中介紹如何分塊和決定次序,。為了使圖像的顯示過程明顯的表現(xiàn)出來,,實(shí)現(xiàn)顯示的特效,就需要在圖像塊的依此顯示中插入固定的延遲,。也許讀者朋友會想到利用sleep()函數(shù)或用Settime()來實(shí)現(xiàn)延遲,,由于Windows是個基于消息的多任務(wù)操作系統(tǒng),這些方法所產(chǎn)生的延遲時間對于圖像的顯示來說是不精確的,,為了實(shí)現(xiàn)與機(jī)器無關(guān)的更精確的時間延遲,,可以采用timeGetTime()函數(shù)來產(chǎn)生微秒級的延遲。使用這個函數(shù)時為了編譯不產(chǎn)生錯誤,,要在連接設(shè)置中引入“Winmm.lib”庫,,并要包含頭文件“Mmsystem.h”。這里我們首先給出一個延遲函數(shù),,它用來實(shí)現(xiàn)固定時間的延遲:
void DelayTime(DWORD time)
{
DWORD BeginTime ,,EndTime;
BeginTime=timeGetTime();//得到當(dāng)前的系統(tǒng)時間、單位為微秒,;
do
{
EndTime=TimeGetTime();//再次得到當(dāng)前的系統(tǒng)時間,;
}
while((EndTime-BeginTime) }
一.操作位圖的像素實(shí)現(xiàn)顯示的特效
1."浮雕"圖象
我們首先介紹直接操作圖像中的像素的灰度值來實(shí)現(xiàn)圖像顯示的特效、這里我們主要介紹如何實(shí)現(xiàn)圖像的浮雕和雕刻效果,。經(jīng)??措娨暤呐笥褌儾恢⒁獾?jīng)]有,有些電視連續(xù)劇在每集片頭或片尾部分都有顯示一些特殊效果的圖像,,比如以前陣子中央一套放的《康熙王朝》(現(xiàn)在8套也在放),,這些特效稱為"圖像的浮雕效果"和"圖像的雕刻效果",經(jīng)過這些特效處理后的圖像增強(qiáng)了觀眾們的視覺效果,,它們看上去仿佛是使用3D技術(shù)作的,,這也許就是為什么這種技術(shù)那么流行的原因吧,。其實(shí),我們完全可以用一些簡單的數(shù)字圖像處理算法來實(shí)現(xiàn)這些看似復(fù)雜高深的顯示效果,。下面以一個標(biāo)準(zhǔn)的Lena灰度圖像為原圖,,給出了處理后的效果圖,同時給出了VC開發(fā)平臺上的部分實(shí)現(xiàn)源代碼,。
void CDibView::OnFDImage() //產(chǎn)生"浮雕"效果圖函數(shù)
{
HANDLE data1handle;//用來存放圖像數(shù)據(jù)的句柄
LPBITMAPINFOHEADER lpBi;//圖像的信息頭結(jié)構(gòu)
CDibDoc *pDoc=GetDocument();//得到文擋指針
HDIB hdib;//用來存放圖像數(shù)據(jù)的句柄,;
unsigned char *pData;//指向原始圖像數(shù)據(jù)的指針
unsigned char *data;//指向處理后圖像數(shù)據(jù)的指針
hdib=pDoc->m_hDIB;//拷貝存放已經(jīng)讀取的圖像文件數(shù)據(jù)句柄
lpBi=(LPBITMAPINFOHEADER)GlobalLock((HGLOBAL)hdib);//獲取圖像信息頭
pData=(unsigned char*)FindDIBBits((LPSTR)lpBi);
//FindDIBBits是我定義的一個函數(shù)、根據(jù)圖像的結(jié)構(gòu)得到位圖的灰度值數(shù)據(jù),、
pDoc->SetModifiedFlag(TRUE);
//設(shè)置文檔修改標(biāo)志為“真”,、為后續(xù)的修改存盤作準(zhǔn)備
data1handle=GlobalAlloc(GMEM_SHARE,WIDTHBYTES(lpBi->biWidth*8)*lpBi->biHeight); //聲明一個緩沖區(qū)用來暫存處理后的圖像數(shù)據(jù)
data=(unsigned char*)GlobalLock((HGLOBAL)data1handle);//得到該緩沖區(qū)的指針;
AfxGetApp()->BeginWaitCursor();
int i,j,buf;
for( i=lpBi->biHeight; i>=2; i--)//從圖像右下角開始對圖像的各個像素進(jìn)行“浮雕”處理,;
for( j=lpBi->biWidth; j>=2; j--)
{
//浮雕處理
buf=*(pData+(lpBi->biHeight-i)*WIDTHBYTES(lpBi->biWidth*8)+j)-*(pData+(lpBi->biHeight-i+1)*WIDTHBYTES(lpBi->biWidth*8)+j-1)+128;
if(buf>255) buf=255;
if(buf<0)buf=0; *(data+(lpBi->biHeight-i)*WIDTHBYTES(lpBi->biWidth*8)+j)=(BYTE)buf;
}
for( j=0; jbiHeight; j++)
for( i=0; ibiWidth; i++)
//重新寫回原始圖像的數(shù)據(jù)緩沖區(qū);
*(pData+i*WIDTHBYTES(lpBi->biWidth*8)+j)=*(data+i*WIDTHBYTES(lpBi->biWidth*8)+j); AfxGetApp()->EndWaitCursor();
pDoc->m_hDIB =hdib//將處理過的圖像數(shù)據(jù)寫回pDoc中的圖像緩沖區(qū),;
GlobalUnlock((HGLOBAL)hdib);//解鎖、釋放緩沖區(qū),;
GlobalUnlock((HGLOBAL)data1handle);
GlobalFree((HGLOBAL)hdib);
GlobalFree((HGLOBAL)data1handle);
Invalidate(TRUE);//顯示圖像
}
2."雕刻"圖像
上面講述了通過求一個像素和它左上方像素之間的差值并加上一個常數(shù)的方法生成"浮雕"效果的灰度圖像,,"雕刻"圖像與之相反,它是通過取一個像素和它右下方的像素之間的差值并加上一個常數(shù),,這里我也取128,,經(jīng)過這樣處理,就可以得到"雕刻"圖像,,這時候圖像的前景凹陷進(jìn)背景之中,。同樣需要讀者注意的是為了避免重復(fù)使用處理過的圖像像素,處理圖像時要從圖像的左上方的像素開始處理,。實(shí)現(xiàn)代碼如下:
void CDibView::OnDKImage()
{
// TODO: Add your command handler code here
HANDLE data1handle;//這里的內(nèi)部變量與前面的含義一致,、這里不再贅述;
LPBITMAPINFOHEADER lpBi;
CDibDoc *pDoc=GetDocument();
HDIB hdib;
unsigned char *pData;
unsigned char *data;
hdib=pDoc->m_hDIB;//拷貝圖像數(shù)據(jù)的句柄,;
lpBi=(LPBITMAPINFOHEADER)GlobalLock((HGLOBAL)hdib);
pData=(unsigned char*)FindDIBBits((LPSTR)lpBi);
pDoc->SetModifiedFlag(TRUE);
data1handle=GlobalAlloc(GMEM_SHARE,WIDTHBYTES(lpBi->biWidth*8)*lpBi->biHeight);//申請緩沖區(qū);
data=(unsigned char*)GlobalLock((HGLOBAL)data1handle);//得到新的緩沖去的指針,; AfxGetApp()->BeginWaitCursor();
int i,j,buf,;
for( i=0;i<=lpBi->biHeight-2; i++)//對圖像的各個像素循環(huán)進(jìn)行"雕刻"處理;
for( j=0;j<=lpBi->biWidth-2; j++)
{
buf=*(pData+(lpBi->biHeight-i)*WIDTHBYTES(lpBi->biWidth*8)+j)-*(pData+(lpBi->biHeight-i-1)*WIDTHBYTES(lpBi->biWidth*8)+j+1)+128;//“雕刻”處理,;
if(buf>255) buf=255;
if(buf<0)buf=0;
*(data+(lpBi->biHeight-i)*WIDTHBYTES(lpBi->biWidth*8)+j)=(BYTE)buf;
}
for( j=0; jbiHeight; j++)
for( i=0; ibiWidth; i++) //重新將處理后的圖像數(shù)據(jù)寫入原始的圖像緩沖區(qū)內(nèi),; *(pData+i*WIDTHBYTES(lpBi->biWidth*8)+j)=*(data+i*WIDTHBYTES(lpBi->biWidth*8)+j);
pDoc->m_hDIB =hdib//將處理過的圖像數(shù)據(jù)寫回pDoc中的圖像緩沖區(qū);
GlobalUnlock((HGLOBAL)hdib);//解鎖,、釋放緩沖區(qū),;
GlobalUnlock((HGLOBAL)data1handle);
GlobalFree((HGLOBAL)hdib);
GlobalFree((HGLOBAL)data1handle);
Invalidate(TRUE);//顯示圖像
}
3.圖像的旋轉(zhuǎn)
根據(jù)圖像像素的位置來調(diào)節(jié)該位置的灰度可以實(shí)現(xiàn)許多顯示的特效,例如圖像的鏡像,、翻轉(zhuǎn)等,。灰度圖像旋轉(zhuǎn)就是根據(jù)這一個思想實(shí)現(xiàn)的,它是指把定義的圖像繞某一點(diǎn)以逆時針或順時針方向旋轉(zhuǎn)一定的角度,,通常是指繞圖像的中心以逆時針方向旋轉(zhuǎn),。首先根據(jù)旋轉(zhuǎn)的角度、圖像對角線的長度計算旋轉(zhuǎn)后的圖像的最大寬度,、高度,,根據(jù)旋轉(zhuǎn)后圖象最大的寬度、高度生成新的緩沖區(qū),,假設(shè)圖像的左上角為(left, top),,右下角為(right, bottom),則圖像上任意點(diǎn)(x, y)繞其中心(xcenter, ycenter)逆時針旋轉(zhuǎn)angle角度后,,新的坐標(biāo)位置(x1, y1)的計算公式為:
xcenter = (width+1)/2+left;
ycenter = (height+1)/2+top;
x1 = (x-xcenter) cosθ - (y - ycenter) sinθ+xcenter;
y1 = (x-xcenter) sinθ+ (y- ycenter) cosθ+ ycenter;
與圖像的鏡像變換相類似,,下一步就是把原圖中的(x,y)處象素的灰度值讀入新緩沖區(qū)的(x1,,y1)點(diǎn)處,。注意在新緩沖區(qū)中與原圖沒有對應(yīng)的象素點(diǎn)的值用白色或指定的灰度代替。
二.圖像的分塊顯示和清除
1. 圖像的掃描顯示和清除
掃描顯示圖像是最基本的特效顯示方法,,它表現(xiàn)為圖像一行行(或一列列)地顯示出來或從屏幕上清除掉,,有種大戲院種的拉幕效果。根據(jù)掃描的方向的不同,,可以分為上,、下、左,、右,、水平平分和垂直平分等六種掃描。這里以向下移動為例,,分別介紹顯示和清除的實(shí)現(xiàn),。其余的掃描效果可以依次類推。向下掃描顯示的實(shí)現(xiàn)方法是:從圖像的底部開始將圖像一行一行的復(fù)制到目標(biāo)區(qū)域的頂部,。每復(fù)制一行后,,復(fù)制的行數(shù)便要增加一行,并加上一些延遲,;向下移動清除的實(shí)現(xiàn)方法是圖像向下移動顯示,,并在顯示區(qū)域的上部畫不斷增高的矩形。
1)掃描顯示的代碼:
CdibView::OnImageDownScan()
{
CDibDoc *pDoc=GetDocument();
HDIB hdib;
CClientDC pDC(this);
hdib=pDoc->m_hDIB;//獲取圖像數(shù)據(jù)句柄,;
BITMAPINFOHEADER *lpDIBHdr;//位圖信息頭結(jié)構(gòu)指針,;
BYTE *lpDIBBits;//指向位圖像素灰度值的指針;
HDC hDC=pDC.GetSafeHdc();//獲取當(dāng)前設(shè)備上下文的句柄,;
lpDIBHdr=( BITMAPINFOHEADER *)GlobalLock(hdib);//得到圖像的位圖頭信息,;
lpDIBBits=(BYTE*)lpDIBHdr+sizeof(BITMAPINFOHEADER)+256*sizeof(RGBQUAD);//獲取指向圖像像素值,;
SetStretchBltMode(hDC,COLORONCOLOR);
//顯示圖像;
for(int i=0;ibiHeight;i++)
{ //每次循環(huán)顯示圖象的“0”到“i”行數(shù)據(jù),;
SetDIBitsToDevice (hDC,0,0,lpDIBHdr->biWidth, lpDIBHdr->biHeight,
0, 0,0, i,
lpDIBBits,(LPBITMAPINFO)lpDIBHdr,
DIB_RGB_COLORS
);
DelayTime(50);//延遲,;
}
GlobalUnlock(hdib);
return;
}
2)清除代碼:
…………………………………//由于篇幅的限制,省略了與上面的相同代碼
Cbrush brush(crWhite);//定義一個“白色”的刷子,;
Cbrush *oldbrush=pDC->SelectObject(&brush);
for(int i=0;i < lpDIBHdr->biHeight ;i++)
{//每次循環(huán)將目標(biāo)區(qū)域中的“0”到“i”行刷成“白色”,;
pDC->Rectangle(0,0,,lpDIBHdr->biWidth,,lpDIBHdr->biHeight);
DelayTime(50);
}
…………………………………
2. 百頁窗效果
所謂百頁窗顯示效果,就如同關(guān)閉和開啟百頁窗一樣,,圖像被分為一條條或一列列地分別顯示或清除掉,,根據(jù)顯示時以行或列為單位可以將該效果分為垂直或水平兩種方式。以垂直百頁窗為例來說明如何實(shí)現(xiàn)這種特效顯示,。實(shí)現(xiàn)垂直百頁窗顯示時,,需要將圖像垂直等分為n部分由上向下掃描顯示,其中每一部分包括m個條,、這個n可以根據(jù)具體應(yīng)用時的需要來決定,、m既為圖像的高度除n。掃描顯示時,,依照差值進(jìn)行掃描顯示,,即第k次顯示k-1、k*m-1,、…k*n-1條掃描線,。同樣,垂直百頁窗清除的實(shí)現(xiàn)與垂直百頁窗的顯示相似,,不同的是將繪制位圖換成畫矩形而已,。在下面的例子中,我將圖像的分成8份,。
…………………………………
int m=8;
int n=lpDIBHdr->biHeight/m;//圖像的高度能夠整除8,;
for(int l=1;l<=m;l++)
for(int k=0;k{ //每次循環(huán)依次顯示圖像中的k-1、k*m-1,、…k*n-1行,;
StretchDIBits (hDC,0,4*k+l-1,lpDIBHdr->biWidth,1,
0, lpDIBHdr->biHeight-4*k-l+1,lpDIBHdr->biWidth,1,
lpDIBBits,(LPBITMAPINFO)lpDIBHdr,
DIB_RGB_COLORS,
SRCCOPY);//juanlianxiaoguo
DelayTime(50);
}
…………………………………
3.柵條顯示特效
柵條特效是移動特效的復(fù)雜組合,,可以分為垂直柵條和水平柵條兩類,。它的基本思想是將圖像分為垂直或水平的的小條,奇數(shù)條向上或向左顯示/清除,,偶數(shù)條向下或向右顯示/清除,。當(dāng)然也可以規(guī)定進(jìn)行相反的方向顯示/清除,。下面的代碼是實(shí)現(xiàn)垂直柵條的例子:
…………………………………
int m=8;
for(int i=0;i<=lpDIBHdr->biHeight;i++)
for(int j=0;j<=lpDIBHdr->biWidth;j+=m)
{//向下顯示偶數(shù)條;
StretchDIBits (hDC,j,0,m,i,j,lpDIBHdr->biHeight-i,
m,i,
lpDIBBits,(LPBITMAPINFO)lpDIBHdr,
DIB_RGB_COLORS,
SRCCOPY);//juanlianxiaoguo
j=j+m;
//向上顯示奇數(shù)條,;
StretchDIBits (hDC,j,lpDIBHdr->biHeight-i,m,i,j,0,
m,i,
lpDIBBits,(LPBITMAPINFO)lpDIBHdr,
DIB_RGB_COLORS,
SRCCOPY);//
DelayTime(20);
}…………………………………
4.馬賽克效果
馬賽克顯示是指圖像被分成許多的小塊,,它們以隨機(jī)的次序顯示出來,直到圖像顯示完畢,。實(shí)現(xiàn)馬賽克的效果主要解決的問題是如何定義顯示隨機(jī)序列的小方塊,,這個問題的解決可以在定義過小方塊的基礎(chǔ)上,用一個數(shù)組來記錄各個方塊的左上角的坐標(biāo)的位置,。顯示圖像過程中,,產(chǎn)生一個隨機(jī)數(shù)來挑選即將顯示的小方塊,顯示后將該方塊的位置坐標(biāo)從數(shù)組中剔除,。清除過程與之相仿,。剔除顯示過的方塊的位置坐標(biāo)的方法是將該數(shù)組中的最后的一個點(diǎn)的坐標(biāo)拷貝到當(dāng)前位置,然后刪除數(shù)組中的最后點(diǎn)的坐標(biāo),,經(jīng)過實(shí)現(xiàn)發(fā)現(xiàn)這樣處理有時顯示的圖像是不完整的,,分析其原因是生成隨機(jī)數(shù)的過程有舍入溢出誤差。讀者可以采用其它的辦法解決這個問題,,例如可以生成固定的隨機(jī)數(shù)組或采用一個動態(tài)的數(shù)組來跟蹤未顯示的圖像方塊的坐標(biāo)等方法,。
…………………………………
int m,n;
int RectSize=60;//方塊的寬、高尺寸為60個像素,;
if(lpDIBHdr->biWidth%RectSize!=0)//得到圖像水平方塊的個數(shù),;
m= lpDIBHdr->biWidth/RectSize+1;
else
m= lpDIBHdr->biWidth/RectSize;
if(lpDIBHdr->biHeight%RectSize!=0)//得到圖像垂直方塊的個數(shù);
n= lpDIBHdr->biHeight/RectSize+1;
else
n=lpDIBHdr->biHeight/RectSize;
POINT *point=new POINT[n*m];//申請一個數(shù)組用來記錄各個方塊的左上角的坐標(biāo),;
POINT point1;
for(int a=0;afor(int b=0;b{
point1.x=a*RectSize;
point1.y=b*RectSize;
*(point+a*b+b)=point1;
}
//開始隨機(jī)的顯示各個小方塊,;
double fMax=RAND_MAX;//定義Rand()函數(shù)的最大值;
for(int k=m*n-1;k>=0;k--)
{
int c=(int)((double)(m*n)*rand()/fMax);
int mx=point[c].x;
int my=point[c].y;
//顯示對應(yīng)的圖像的小塊,;
StretchDIBits (hDC,mx,my,RectSize,RectSize,
mx,lpDIBHdr->biHeight-my,RectSize,RectSize,
lpDIBBits,(LPBITMAPINFO)lpDIBHdr,
DIB_RGB_COLORS,
SRCCOPY);
point[c].x=point[k].x;
point[c].y=point[k].y;
DelayTime(50);
}
…………………………………
5.圖像的淡入淡出效果
圖像的淡入淡出的顯示效果被廣泛的應(yīng)用在多媒體娛樂軟件中,,是一種特別重要的特效顯示方法。淡入就是將顯示圖像的目標(biāo)區(qū)域由本色逐漸過度的圖像中的各個像素點(diǎn)的顏色,;淡出就是由顯示的圖像逐漸過度到目標(biāo)區(qū)域的本色,。實(shí)現(xiàn)圖像的淡入淡出有兩種辦法:一是均勻的改變圖像的調(diào)色板中的顏色索引值;另一種方法是改變圖像像素的灰度值,。第一種方法實(shí)現(xiàn)起來比較繁瑣,,第二種方法就比較簡單。下面是我們采用第二種方法實(shí)現(xiàn)圖像淡入效果的代碼:
…………………………………
//申請一個與圖像緩沖區(qū)相同大小的內(nèi)存,;
hdibcopy=(HDIB)GlobalAlloc(GMEM_SHARE,lpDIBHdr->biWidth*lpDIBHdr->biHeight);
lpbits=(BYTE*)GlobalLock(hdibcopy);
//將緩沖區(qū)的數(shù)據(jù)初始化,;
for(int k=0;kbiWidth*lpDIBHdr->biHeight;k++)
{
*(lpbits+k)=(BYTE)255;
}
//顯示最初的圖像為“白色”
StretchDIBits (hDC,0,0,lpDIBHdr->biWidth,lpDIBHdr->biHeight,0,0,
lpDIBHdr->biWidth,lpDIBHdr->biHeight,
lpbits,(LPBITMAPINFO)lpDIBHdr,
DIB_RGB_COLORS,
SRCCOPY);
//布爾變量end用來標(biāo)志何時淡入處理結(jié)束;
BOOL end=false;
while(!end)
{ int a=0;
for(int k=0;kbiWidth*lpDIBHdr->biHeight;k++)
{
//判斷是否待顯示的像素的灰度值已經(jīng)小于原始圖像對應(yīng)點(diǎn)的灰度值,,如是則計數(shù),;
if(*(lpbits+k)<*(lpDIBBits+k))
a++;
else//否則對應(yīng)點(diǎn)的灰度值繼續(xù)減少,;
*(lpbits+k)-=(BYTE)10;
}
//顯示處理后的圖像數(shù)據(jù)lpbits;
StretchDIBits (hDC,0,0,lpDIBHdr->biWidth,lpDIBHdr->biHeight,0,0,
lpDIBHdr->biWidth,lpDIBHdr->biHeight,
lpbits,(LPBITMAPINFO)lpDIBHdr,
DIB_RGB_COLORS,
SRCCOPY);
//如果所有的點(diǎn)的灰度值的都小于或等于原始圖像的像素點(diǎn)的灰度值,則認(rèn)為圖像的淡入處理結(jié)束,。
if(a==lpDIBHdr->biWidth*lpDIBHdr->biHeight)
end=true; DelayTime(50);
…………………………………