深入理解BMP圖片數(shù)據(jù)結(jié)構(gòu)及其存儲原理
點(diǎn)擊下方卡片,關(guān)注“新機(jī)器視覺”公眾號
重磅干貨,第一時間送達(dá)
BMP(Bitmap)圖片是Windows操作系統(tǒng)中的標(biāo)準(zhǔn)圖像文件格式,它除了位深度可選外,不采用其它任何有損壓縮,這也意味著它保留了圖像中的最原始的數(shù)據(jù)。在很多圖像處理工程中,我們經(jīng)常使用到BMP位圖文件,今天我主要總結(jié)一下BMP文件的格式以及存儲原理。
我們先從物理層面說一下顯示器的顯像原理
顯示器顯像基本原理
我們拿液晶顯示器舉例,顯示器底部是一塊發(fā)光的白板燈,中間是液晶(一種高分子有機(jī)化合物,在電場的作用下可呈現(xiàn)不同的分子排列,引起光學(xué)形態(tài)的變化,從而形成不同亮度的灰階),然后是一些濾光片,顯示器的屏幕是由很多個“小塊”組成的,每塊后面都有紅綠藍(lán)三個濾光片,每個小塊就是1個像素點(diǎn)。濾光片能夠把顯示器背后發(fā)出的白光過濾,留下單色光通過,白光經(jīng)過三塊濾光片后被分解成了紅綠藍(lán)(人類視網(wǎng)膜上的視錐細(xì)胞對紅綠藍(lán)三色最為敏感)三束光,進(jìn)入人的眼睛。由于一個像素極其小,三個濾光片距離極其近,以至于透過它們的光進(jìn)入人眼后,人眼分不清這是3束光,即光在人眼中發(fā)生混色作用,于是一個像素便“有了”顏色。顯示器正是通過電壓來控制液晶分子的排列方式,來改變其光線的透過程度,形成不同程度的RGB亮度,進(jìn)而顯示出特定的顏色。這便是顯示器顯示圖像的最基本原理。

其實(shí),圖像的顯示跟顯示器顯示的原理極為相似,也是通過控制RGB分量(亮度)來顯示出每個像素顏色的。電壓信號是一種模擬信號,它是連的,比如從0到1,之間還有無數(shù)個數(shù)據(jù),但是顯示到圖像上卻是數(shù)字信號,它的信號是跳躍性的。一般情況下,每個亮度從最暗到最亮共分為256個級別,當(dāng)然,并不是所有的圖像的RGB亮度都分為256個級別,這與圖像的位深有關(guān),一般情況下圖像有1bit,4bit,8bit,24bit,32bit等等,位深越大,分量的區(qū)間被細(xì)分的就越多,從而能夠顯示顏色的種類就越多,這就好比你把尺子刻度分割的越細(xì),你的測量精度就越高。
一般情況下,我們使用的彩色圖像都是24位深圖像,它的RGB分量為256個級別(0~255),剛好為一個字節(jié),每個分量占用一個字節(jié),單個像素占用三個字節(jié),共計3×8=24位,24位及24位深度以上的圖像,我們叫它為真彩色。
為了更加直觀的說明,我們來做個實(shí)驗(yàn),打開你的Excel,在宏編輯器(具體使用方法請參考我上一篇文章?手把手教你用Excel編寫俄羅斯方塊)內(nèi)輸入下面一行代碼:
Sub test()4).Interior.Color = RGB(Cells(2, 1), Cells(2, 2), Cells(2, 3))End?Sub
我們添加一個按鈕,關(guān)聯(lián)一下這個宏,看一下實(shí)際運(yùn)行效果:

可見,每改變RGB的分量,都會呈現(xiàn)出不同的一種顏色,如果是24位真彩色,那么每個像素能夠表現(xiàn)出來的顏色種類為256*256*256=16777216色。日常我們使用過程中,除了三種常用的純紅、純綠、純藍(lán)之外,最常用的顏色RGB組合如下:
| 顏色 | R | G | B |
| 黃 | 255 | 255 | 0 |
| 紫 | 255 | 0 | 255 |
| 青 | 0 | 255 | 255 |
| 白 | 255 | 255 | 255 |
| 灰 | 128 | 128 | 128 |
我們先來看下面這張BMP圖片,這張是我在太湖東山島親手拍攝的一張日落照片:

怎么樣,很漂亮吧,哈哈。
我們用Windows自帶的畫圖軟件,打開這張圖片,將其放到最大:

可以明顯看到,圖像被分成了一個個“小方格”,每個小方格就是一個像素,他就是通過調(diào)節(jié)不同的RGB分量來形成的。
我們再右鍵看一下它的屬性


我們看到這張圖片,位深為24位,按照每個像素占用三個字節(jié),長寬有了,那么整張照片大小應(yīng)該是2127*1200*3=7657200字節(jié),但是實(shí)際大小卻是7660854字節(jié),而且占用空間與大小居然不一樣,這是為什么呢?
這里就涉及到在Windows下存儲BMP圖片結(jié)構(gòu)的特點(diǎn)了,除了存儲像素元素外,BMP文件還有四個非常重要的文件結(jié)構(gòu),分別是文件頭、信息頭、顏色表。下面,我們一個個來詳細(xì)說明。
BMP文件結(jié)構(gòu)
1,位圖文件頭(BITMAPFILEHEADER)
我們先看一下,在wingdi.h文件中,它的數(shù)據(jù)結(jié)構(gòu):
typedef struct tagBITMAPFILEHEADER {WORD bfType;DWORD bfSize;WORD bfReserved1;WORD bfReserved2;DWORD bfOffBits;BITMAPFILEHEADER, FAR *LPBITMAPFILEHEADER, *PBITMAPFILEHEADER;
位圖文件頭分為四個部分(我們將兩個保留字節(jié)看成一部分),共14個字節(jié):
為了更加直觀理解,我們使用Notepad++打開這張圖片

打開后,我們看到,頭兩個字節(jié)存儲的分別是B/M兩個字母的十六進(jìn)制ASCII碼,后面四個字節(jié)實(shí)際存儲的是整個圖片的大小,由于Windows采用小端對齊模式,高內(nèi)存地址存放高位,低內(nèi)存地址存放低位,數(shù)據(jù)是倒著放的,所以實(shí)際數(shù)據(jù)應(yīng)該是0x0074e536,十進(jìn)制剛好與我們右鍵查看的大小7660854一樣,隨后的四個字節(jié)為保留字節(jié),暫時沒用;最后四個字節(jié)為偏移量數(shù)據(jù),保存數(shù)據(jù)為位圖文件頭+位圖信息頭+調(diào)色板?= 54. 這便是位圖文件頭里面數(shù)據(jù)的含義,整個文件頭占用14個字節(jié)。
2,位圖信息頭(BITMAPINFOHEADER)
我們來看一下信息頭在wingdi.h文件中的數(shù)據(jù)結(jié)構(gòu):
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, FAR *LPBITMAPINFOHEADER, *PBITMAPINFOHEADER;
位圖信息頭占用字節(jié)比較多,共計40個字節(jié):

3,顏色表(調(diào)色板/color table)
彩色表/調(diào)色板(color table)是1色、16色和256色圖像文件所特有的,相對應(yīng)的調(diào)色板大小是2、16和256,調(diào)色板以4字節(jié)為單位,每4個字節(jié)存放一個顏色值,圖像 的數(shù)據(jù)是指向調(diào)色板的索引。可以將調(diào)色板想象成一個數(shù)組,每個數(shù)組元素的大小為4字節(jié),假設(shè)有一256色的BMP圖像的調(diào)色板數(shù)據(jù)為:調(diào)色板[0]=黑、調(diào)色板[1]=白、調(diào)色板[2]=紅、調(diào)色板[3]=藍(lán)…調(diào)色板[255]=黃。
例如圖像數(shù)據(jù)01 00 02 FF表示調(diào)用調(diào)色板[1]、調(diào)色板[0]、調(diào)色板[2]和調(diào)色板[255]中的數(shù)據(jù)來顯示圖像顏色。每個調(diào)色板的大小為4字節(jié),按藍(lán)、綠、紅存儲一個顏色值。
我們看一下在wingdi.h文件中,對調(diào)色板數(shù)據(jù)結(jié)構(gòu)的定義:
typedef struct tagRGBQUAD {BYTE rgbBlue;BYTE rgbGreen;BYTE rgbRed;BYTE rgbReserved;RGBQUAD;
調(diào)色板中的數(shù)據(jù)定義為:
在24位以及24以上的真彩色圖像中,沒有調(diào)色板,信息頭后面直接跟著的就是位圖的像素數(shù)據(jù)。
4,位圖數(shù)據(jù)(bitmap-data)
如果圖像是單色、16色和256色,則緊跟著調(diào)色板的是位圖數(shù)據(jù),位圖數(shù)據(jù)是指向調(diào)色板的索引序號。
如果位圖是16位、24位和32位色,則圖像文件中不保留調(diào)色板,即不存在調(diào)色板,圖像的顏色直接在位圖數(shù)據(jù)中給出。
16位圖像使用2字節(jié)保存顏色值,常見有兩種格式:5位紅5位綠5位藍(lán)和5位紅6位綠5位藍(lán),即555格式和565格式。555格式只使用了15 位,最后一位保留,設(shè)為0.
24位圖像使用3字節(jié)保存顏色值,每一個字節(jié)代表一種顏色,按紅、綠、藍(lán)排列。32位圖像使用4字節(jié)保存顏色值,每一個字節(jié)代表一種顏色,除了原來的紅、綠、藍(lán),還有Alpha通道,即透明色。
如果圖像帶有調(diào)色板,則位圖數(shù)據(jù)可以根據(jù)需要選擇壓縮與不壓縮,如果選擇壓縮,則根據(jù)BMP圖像是16色或256色,采用RLE4或RLE8壓縮算法壓縮。
這里我們還以上面那張日落照片為例來進(jìn)行說明:
由于這張照片是24位真彩色,所以它沒有調(diào)色板,位圖信息頭(BITMAPINFOHEADER)后面緊接著就是圖像的真實(shí)數(shù)據(jù)。不過這里有個細(xì)節(jié),位圖全部像素數(shù)據(jù),在存儲的過程中是按照自下而上,自左往右的順序進(jìn)行排列的,這點(diǎn)為什么是這樣,我也不太清楚,我估計是歷史遺留問題。還有一點(diǎn),單個像素的通道存儲順序也是反著的,實(shí)際是按照BGR的順序來存儲的,這點(diǎn)很好理解,因?yàn)樵诖鎯^程中,RGB三個字節(jié)是同時寫入的,低字節(jié)在前,所以實(shí)際在讀取的時候順序卻是BGR. 這兩點(diǎn)一定要注意,在實(shí)際使用過程中很容易出錯。

有了上面這些知識,我們再回到本文開頭提出的那個文件大小的問題,我們照片的大小為2127*1200*3=7657200字節(jié),再加上兩個文件頭占用的54個字節(jié),總共大小應(yīng)該為7657200+54=7657254,但是實(shí)際大小卻是7660854字節(jié),與實(shí)際結(jié)果還有差距,這是怎么回事呢?
實(shí)際上,這是Windows操作系統(tǒng)中的內(nèi)存對齊造成的,Windows要求位圖的每一行像素所占字節(jié)數(shù)必須被4整除,因?yàn)檫@樣對操作系統(tǒng)讀取數(shù)據(jù)非常方便,若不能被4整除,則在該位圖每一行的十六進(jìn)制碼末尾"補(bǔ)"1至3個字節(jié)的"00"。
例如我們這張圖片每行為2127個像素,每行占用2127*3=6381字節(jié),6381除以4還余1,所以需要在每行再補(bǔ)齊3個字節(jié),這樣每行實(shí)際占用6381+3=6384個字節(jié),總共像素占用6384*1200=7660800字節(jié),另外再加上兩個文件頭信息占用的54個字節(jié),整張圖片的大小為7660854,剛好與我們右鍵信息的大小一致。
但是為什么圖片的實(shí)際大小與圖片的占用空間還不一樣呢,這與Windows使用的NTFS和FAT文件管理系統(tǒng)有關(guān),這里暫不做詳細(xì)講解,有空我會抽時間專門詳細(xì)說明。
最后,我們根據(jù)上面內(nèi)容做一個驗(yàn)證,查看一下東山島那張照片左下角最后一行第一個像素值是否與我們上面Notepad++打開的內(nèi)容一致,我們使用下面幾行代碼(c語言版)進(jìn)行測試:
void saveBmpImage(){char fileName[30] = "1.bmp"; //定義打開圖像名字char *buf; //定義文件讀取緩沖區(qū)char *p;int r, g, b;FILE *fp; //定義文件指針FILE *fpw; //定義保存文件指針DWORD w, h; //定義讀取圖像的長和寬DWORD bitSize; //定義圖像的大小BITMAPFILEHEADER bf; //圖像文件頭BITMAPINFOHEADER bi; //圖像文件頭信息if ((fp = fopen(fileName, "rb")) == NULL){cout << "文件未找到!";exit(0);}fread(&bf, sizeof(BITMAPFILEHEADER), 1, fp);//讀取BMP文件頭fread(&bi, sizeof(BITMAPINFOHEADER), 1, fp);//讀取BMP信息頭w = bi.biWidth; //獲取圖像的寬h = bi.biHeight; //獲取圖像的高??bitSize?=?bi.biSizeImage;??????????????????//獲取圖像的sizeint c = (w * 3) % 4;//分配緩沖區(qū)大小, 注意內(nèi)存對齊int buf_data_size = ((w * 3) + (4 - c)) * h;buf = (char*)malloc(buf_data_size);//定位到像素起始位置fseek(fp, long(sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER)), 0);??fread(buf,?buf_data_size,?1,?fp);???????//開始讀取數(shù)據(jù)??p?=?buf;??//這里只輸出最后一行??for?(int?j?=?h-1;?j?{for (int i = 0; i < w; i++){b = *p++; g = *p++; r = *p++;char result1[8];char result2[8];char result3[8];sprintf(result1, "%02x", b);sprintf(result2, "%02x", g);??????sprintf(result3,?"%02x",?r);cout << result1 << " " << result2 << " " << result3 << endl;}??}free(buf);}
在這段代碼中,理論上給圖像真實(shí)數(shù)據(jù)分配緩沖區(qū)大小直接使用計算出來的bi.biSizeImage即可,但是在實(shí)際項目中,需要我們自己去構(gòu)建一個bmp圖片,事先并不知道其真實(shí)數(shù)據(jù)區(qū)有多大,我們就需要根據(jù)實(shí)際情況來自己計算緩沖區(qū)大小了,所以在本例中我就自己動手計算了一下(上面代碼第25行)。這里要特別注意每行像素占用的內(nèi)存大小是否需要補(bǔ)零操作。
我們看一下運(yùn)行結(jié)果:

可見,代碼運(yùn)行結(jié)果與我們實(shí)際用Notepad++打開的結(jié)果一致,說明我們的代碼沒問題,驗(yàn)證通過。另外,在本例中,由于我們兩個頭文件、圖像真實(shí)數(shù)據(jù)緩沖區(qū)已經(jīng)分配好,我們完全可以再生成一張圖片,當(dāng)然這張圖片是上一張圖片的復(fù)制版,上述代碼在free緩沖區(qū)前加上下面幾行:
fpw = fopen("2.bmp", "wb");fwrite(&bf, sizeof(BITMAPFILEHEADER), 1, fpw); //寫入文件頭fwrite(&bi, sizeof(BITMAPINFOHEADER), 1, fpw); //寫入信息頭p = buf;fwrite(p, buf_data_size, 1, fpw);//bmp, datafclose(fpw);fclose(fp);
這樣,我們就能生成一張新的BMP圖片了,在實(shí)際項目中,生成BMP圖片的順序也一樣,都是先定義文件頭、信息頭數(shù)據(jù),最后再定義圖像數(shù)據(jù)緩沖區(qū),最后生成一張完整的圖片,不過要特別注意是否需要補(bǔ)零操作。
結(jié)語:以上就是本文的全部內(nèi)容了,希望通過本文能夠加深你對BMP圖片結(jié)構(gòu)(尤其是兩個頭文件)及其存儲原理的理解。
本文僅做學(xué)術(shù)分享,如有侵權(quán),請聯(lián)系刪文。
