位圖(BMP)文件結構分析以及使用C++實現位圖的讀寫與顯示

1. 摘要

??BMP是英文Bitmap(位圖)的簡寫,它是Windows操作系統中的標準圖像文件格式,能夠被多種Windows應用程序所支持。BMP圖像文件是Windows采用的圖形文件格式,在Windows環境下運行的所有圖象處理軟件都支持BMP圖象文件格式。Windows系統內部各圖像繪制操作都是以BMP為基礎的。

??本實驗將會對BMP位圖的文件結構進行分析,并實現BMP位圖的讀取、顯示和保存。

??關鍵詞:BMP位圖文件結構分析、BMP位圖的讀取、BMP位圖的保存、BMP位圖的顯示

2. 實驗內容與相關平臺

2.1 實驗內容

  • 對BMP位圖的文件結構進行分析

  • 編寫程序,實現對24位彩色位圖進行讀取、顯示、保存操作

2.2 實驗的相關平臺與工具

??Notepad++(用于分析BMP圖像的文件結構)、Vs code、C++

3. BMP位圖文件結構分析

??在本節中,我們將要對BMP位圖的文件結構進行分析。在此之前,我們需要事先了解兩個關鍵點:

  • 在BMP文件中,數據存儲采用小端方式(little endian),即“低地址存放低位數據,高地址存放高位數據”。

  • 以下所有分析均以字節為單位進行。

3.1位圖的文件結構

??位圖的文件結構如表3-1所示,位圖的數據包括四項,分別是:位圖文件頭、位圖信息頭、調色板和位圖數據

??表3-1 位圖的文件格式:

位圖文件頭BITMAPFILEHEADER
位圖信息頭BITMAPINFOHEADER
調色板Palette
位圖數據ImageData

3.2 位圖的文件頭分析

??位圖文件頭主要用于識別位圖文件,以及記錄文件的大小、位圖數據位置等信息,共占14個字節。圖3-2是位圖文件頭結構的定義,位圖文件頭的字段含義如表3-3所示。

圖3-2

??表3-3 位圖頭文件的字段以及含義:

字段 字節數 含義
bfType 2 聲明位圖文件的類型,該值必須為0x4D42,即字符'BM'。表示這是Windows支持的位圖格式。 【注】該值也可以設置位’BA’,’CI’,’CP’等不同格式,但由于因為OS/2系統并沒有被普及開。因此在編程時,只需判斷第一個標識為否為“BM”即可。
bfSize 4 聲明BMP文件的大小,單位是字節
bfReserved1 2 保留字段,必須設置為0
bfReserved2 2 保留字段,必須設置為0
bfOffBits 4 聲明從文件頭開始到實際的圖象數據之間的字節的偏移量,可以用這個偏移值迅速的從文件中讀取到位數據。

??用Notepad++打開BMP圖像文件“lena-單色位.bmp”,如下圖3-4所示。可見紅框1中,第1-2字節數據為0x4d42,為BMP位圖的固定標識。在紅框2中,第3-6字節數據為0x00008d8e,表示36238字節,可見該值與在Window資源管理器中查看文件屬性中的圖片大小的是一致的。

??在紅框3中,此處的數據為0x0000003e,表示62字節,表示位圖數據位于從文件開始往后數的62字節處。

圖3-4

3.3 位圖的信息頭分析

??BITMAPINFO段由兩部分組成:BITMAPINFOHEADER結構體和RGBQUAD結構體,其中的BITMAPINFOHEADER結構體表示位圖信息頭。同樣地,Windows為位圖信息頭定義了如下結構體,如下圖3-5所示。位圖信息頭的字段含義如下表3-6所示。

圖3-5

??表3-6 位圖信息頭的字段以及含義:

字段 占字節數 含義
biSize 4 聲明BITMAPINFOHEADER占用的字節數
biWidth 4 聲明圖片的寬度,單位是像素
biHeight 4 聲明圖片的高度,單位是像素
biPlanes 2 聲明目標設備說明位面數,其值將總是被設為1
biBitCount 2 聲明單位像素的位數,表示Bmp圖像的顏色位數,如24位圖,32位圖
biCompression 4 聲明圖像壓縮屬性,由于bmp圖片是不壓縮,該值等于0
biSizeImage 4 聲明Bmp圖像數據區的大小
biXPelsPerMeter 4 聲明圖像的水平分辨率
biYPelsPerMeter 4 聲明圖像的垂直分辨率
biClrUsed 4 聲明使用了顏色索引表的數量
biClrImportant 4 聲明重要的顏色的數量,等于0時表示所有顏色都很重要

??繼續用Notepad++分析BMP圖像的文件結構,如下圖3-7所示。可見紅框1所示的數據為biSize字段,它的值為0x00000028=40,表示位圖信息頭的大小為40字節。紅框2與3所示的數據表示圖像的寬度與高度,對應的值為0x0000 021b = 539像素,0x0000 0214 = 532像素。

??紅框4處表示圖像的位深度,因為這是一張黑白圖像,所以位深度為1。

圖3-7

??若打開的是24位深度的圖片,可見該字段的值為0x0018,代表顏色深度為24,如下圖3-8所示。

圖3-8

??繼續分析文件,如下圖3-9所示。紅框5處聲明了BMP圖像的數據區大小,即0x00008d50 = 36176字節。紅框6處定義了圖像的水平分辨率和垂直分辨率。

??紅框7處定義了使用彩色表的索引值的數量,當該值為0時,表示使用所有調色板項。

圖3-9

3.4 調色板分析

??調色板一般是針對16位以下的圖像設置的,對于16位及以上的BMP格式圖像,其位圖像素數據是直接對應像素的RGB顏色值進行描述,因此省去了調色板。對于16位以下的BMP格式圖像,其位圖像素數據中記錄的是調色板的索引值。調色板的作用是,當圖像的位深度值比較小時,通過調色板記錄所有的顏色值,而位圖數據則存儲調色板的索引項,因此達到節省存儲空間的效果。

??調色板的數據由RGBQUAD結構體項組成,該結構體由4個字節型數據組成,所以一個RGBQUAD結構體只占用4字節空間,從左到右每個字節依次表示(藍色,綠色,紅色,未使用)。調色板的結構體定義如下圖3-10所示:

圖3-10

??分析圖像的第55-62個字節,該處聲明的是圖像的彩色表項,由于現在使用的圖像是單色圖,只有黑白兩種顏色,所以調色板中也只有兩項,對應著黑色和白色。如下圖3-11所示。

圖3-11

??接下來,我們看一下位深度大于16位的BMP圖像的調色板。

??我們用Notepad++打開一張位深度為24的BMP圖像,如下圖3-12所示。紅框1處為位圖文件頭的bfOffBits字段,值為54字節,表示從文件頭起始到位圖數據之間的字節的偏移量54字節。

??紅框2處的字段為位圖信息頭的biSize字段,值為40字節。觀察兩組數據數據,位圖的文件頭固定為14字節,加上信息頭的40字節因此總字節數為54字節,正等于bfOffBits字段的偏移量。可以由此得知,24位位深度的BMP圖像沒有調色板數據。

圖3-12

1.3 位圖數據分析

??位圖數據記錄了位圖每一個像素的像素值,存儲的順序是在掃描行內是從左到右,掃描行之間是從下到上。根據不同的位圖,位圖數據所占據的字節數也是不同的。比如,對于24位的位圖,每三個字節表示一個像素。對于本案例中的單色圖,一個字節則可以對應八個像素點的像素值。

??根據圖像提供的位圖數據,可以得知每個像素點的值,以此繪制圖像。

??如下圖3-13所示,位圖數據共有36176字節,位圖文件頭與位圖信息頭共54字節,再加上彩色表的兩個索引項共8個字節,可以得知該圖像共36238字節。此數據與用Window資源管理器直接查看圖像的大小一致。

圖3-13

??在位圖數據存儲與讀取過程中,有一點需要特別注意:BMP存儲格式要求每行的字節數必須是4的倍數。若某行的字節數不是4的倍數,需要額外添加字符‘0’湊夠到4的倍數。在對位圖數據進行讀寫時,這一點需要特別留意,否則無法對位圖圖像進行正確的讀寫。

4. 使用C++實現對位圖文件的讀寫、顯示

??在本小節中,將會用C++語言實現對24位位圖文件的讀寫,顯示操作。

??本程序定義了一個新的結構體ImgInfo,里面包含了位圖文件頭BITMAPFILEHEADER、位圖信息頭BITMAPINFOHEADER,還有一個二維數組imgData,用于存放像素值信息。如下圖4-1所示,加入二維數組imgData字段的好處是可以使用二維數組更方便地對圖像的像素點進行操作。

??在程序的主函數中,調用了readBitmap、showBitmap、saveBitmap三個函數,實現對BMP圖像的讀取、顯示、保存操作,如圖4-2所示。

圖4-1
圖4-2

4.1 位圖文件的讀取

??位圖文件讀取主要由程序中的readBitmap函數實現,關鍵代碼如下圖4-3所示,通過使用fread函數實現對位圖文件頭與位圖信息頭的讀取。

圖4-3

??在下圖4-4中,通過fseek函數與位圖文件頭的bfOffBits字段,對圖像像素數據進行定位,以此來讀取像素數據信息,并存放到二維數組中。

??注意藍色框中的代碼,由于BMP位圖采用4字節對齊的存儲機制,可能會存在一些無意義的填充數據,因此我們在讀取數據時必須將他們排除。

圖4-4

4.2在控制臺上顯示位圖圖像

??在控制臺上顯示位圖圖像,主要由程序中的showBitmap函數實現。

??根據結構體ImgInfo中的imgData字段,我們可以很輕易地獲取圖像的像素值信息,并使用SetPixel函數將像素值顯示在控制臺特定的位置,這部分的關鍵代碼如下圖4-5所示。

??需要注意的是,BMP位圖的像素數據存儲方式是行內從左到右,行間從下到上(即第一個數據存放的是圖像左下角的像素信息,最后一個數據存放的是圖像右上角的像素信息),因此在編程時需要考慮清楚像素點與其圖像實際的坐標位置。

圖4-5

4.3 位圖文件的保存

??位圖文件的保存,主要在程序中的saveBitmap函數中實現,如下圖4-6所示。與位圖文件的讀取類似,按照BMP位圖的文件結構,先使用fwrite函數實現對位圖文件頭和位圖信息頭的寫入,再遍歷像素點信息將像素值寫入文件中。

??同樣地,位圖的像素信息存取采用4字節對齊的方式,在寫入每一行位圖數據后且字節長度不足4的倍數時,需要填充’0’字符。

圖4-6

4.4實驗效果

??如下圖4-7所示,在運行程序后,將圖像數據讀出,然后在控制臺上顯示圖像,最后將圖像保存到本地。

圖4-7

5.總結

  • 位圖的文件結構包括四項,分別是:位圖文件頭、位圖信息頭、調色板和位圖數據。

  • 位圖文件頭存放位圖文件的大小、位圖數據位置等信息。

  • 位圖信息圖存放位圖文件的寬高、圖像位深度、水平/垂直分辨率、位圖數據大小等等關鍵信息。

  • 調色板一般是針對16位以下的圖像設置的,對于16位及以上的BMP格式圖像,其位圖像素數據是直接對應像素的RGB顏色值進行描述。

  • 位圖數據記錄了位圖每一個像素的像素值,存儲的順序是在掃描行內是從左到右,掃描行之間是從下到上,并要求每行的字節數必須是4的倍數。

6. 參考文章

[1] 百度百科--Bitmap位圖.https://baike.baidu.com/item/Bitmap/6493270?fr=aladdin

[2] Bitmap 圖片格式并用 C++ 讀寫 Bitmap.

https://blog.csdn.net/weixin_34208185/article/details/86257499

[3] BMP格式詳解.https://blog.csdn.net/gwwgle/article/details/4775396

[4] Bitmap每行4字節對齊.https://blog.csdn.net/a_flying_bird/article/details/50585146

7.附--完整代碼

// ImgOpt.cpp : 此文件包含 "main" 函數。程序執行將在此處開始并結束。
//
#include <iostream>
#include <Windows.h>
#include <malloc.h>
#include <vector>

using namespace std;

string imgPath = "C:/Users/ZXX-PC/Desktop/lena-24位.bmp";
string saveImgPath = "C:/Users/ZXX-PC/Desktop/lena-24位-save.bmp";

//自定義了一個ImgInfo的結構體,包含BMP文件頭、BMP信息頭和像素點的RGB值。
//目前只支持24位圖像的讀取和顯示

typedef struct{
    BITMAPFILEHEADER bf;
    BITMAPINFOHEADER bi;
    vector<vector<char>> imgData;
}ImgInfo;

//根據圖片路徑讀取Bmp圖像,生成ImgInfo對象
ImgInfo readBitmap(string imgPath) {
    ImgInfo imgInfo;
    char* buf;                                              //定義文件讀取緩沖區
    char* p;

    FILE* fp;
    fopen_s(&fp, imgPath.c_str(), "rb");
    if (fp == NULL) {
        cout << "打開文件失敗!" << endl;
        exit(0);
    }

    fread(&imgInfo.bf, sizeof(BITMAPFILEHEADER), 1, fp);
    fread(&imgInfo.bi, sizeof(BITMAPINFOHEADER), 1, fp);

    if (imgInfo.bi.biBitCount != 24){
        cout << "不支持該格式的BMP位圖!" << endl;
        exit(0);
    }

    fseek(fp, imgInfo.bf.bfOffBits, 0);

    buf = (char*)malloc(imgInfo.bi.biWidth * imgInfo.bi.biHeight * 3);
    fread(buf, 1, imgInfo.bi.biWidth * imgInfo.bi.biHeight * 3, fp);

    p = buf;

    vector<vector<char>> imgData;
    for (int y = 0; y < imgInfo.bi.biHeight; y++){
        for (int x = 0; x < imgInfo.bi.biWidth; x++) {
            vector<char> vRGB;

            vRGB.push_back(*(p++));     //blue
            vRGB.push_back(*(p++));     //green
            vRGB.push_back(*(p++));     //red

            if (x == imgInfo.bi.biWidth - 1)
            {
                for (int k = 0; k < imgInfo.bi.biWidth % 4; k++) p++;
            }
            imgData.push_back(vRGB);
        }
    }
    fclose(fp);
    imgInfo.imgData = imgData;
    return imgInfo;
}

void showBitmap(ImgInfo imgInfo) {
    HWND hWindow;                                                //窗口句柄
    HDC hDc;                                                     //繪圖設備環境句柄
    int yOffset = 150;                      
    hWindow = GetForegroundWindow();
    hDc = GetDC(hWindow);

    int posX, posY;
    for (int i = 0; i < imgInfo.imgData.size(); i++){
        char blue = imgInfo.imgData.at(i).at(0);
        char green = imgInfo.imgData.at(i).at(1);
        char red = imgInfo.imgData.at(i).at(2);

        posX = i % imgInfo.bi.biWidth;
        posY = imgInfo.bi.biHeight - i / imgInfo.bi.biWidth + yOffset;
        SetPixel(hDc, posX, posY, RGB(red, green, blue));
    }
}

void saveBitmap(ImgInfo imgInfo) {
    FILE* fpw;
    fopen_s(&fpw, saveImgPath.c_str(), "wb");
    fwrite(&imgInfo.bf, sizeof(BITMAPFILEHEADER), 1, fpw);  //寫入文件頭
    fwrite(&imgInfo.bi, sizeof(BITMAPINFOHEADER), 1, fpw);  //寫入文件頭信息

    int size = imgInfo.bi.biWidth * imgInfo.bi.biHeight;
    for (int i = 0; i < size; i++) {
        fwrite(&imgInfo.imgData.at(i).at(0), 1, 1, fpw);
        fwrite(&imgInfo.imgData.at(i).at(1), 1, 1, fpw);
        fwrite(&imgInfo.imgData.at(i).at(2), 1, 1, fpw);

        if (i % imgInfo.bi.biWidth == imgInfo.bi.biWidth - 1) {
            char ch = '0';
            for (int j = 0; j < imgInfo.bi.biWidth % 4; j++) {
                fwrite(&ch, 1, 1, fpw);
            }
        }
    }
    fclose(fpw);
    cout << "已保存圖像至: " + saveImgPath << endl;
}

int main() {
    ImgInfo imgInfo = readBitmap(imgPath);
    showBitmap(imgInfo);
    saveBitmap(imgInfo);
}

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374