數字圖像處理的代碼在網上已經非常普遍了,但是大部分文章中的代碼都是針對某一個功能的,而且實現功能的編程語言種類非常多,這在小賈學習的過程中造成了很多的麻煩。所以小賈打算寫幾篇的文章來總結小賈在入門時候遇到的“坑”。如果您只想參考圖像處理過程中的代碼,您可以直接跳到文章的代碼部分。小賈在代碼中也會有代碼的詳細注釋。當然,如果您剛剛入坑,小賈也會在文章中附加一些段落來幫你理解圖像處理。
@
1. 寫在前面
1.1. 先放一張圖
熟悉圖像處理的人一定認識這張圖像。因為他們在實驗或者項目任務中經常使用Lena圖像。Lena圖像已經成為被廣泛使用的測試圖像。今天,Lena圖像的使用被認為是數字圖像歷史上最重要的事件之一[1]。如果你剛開始接觸圖像處理,那么恭喜你以后要經常見到這個圖像了。
1.2. 再說一種圖像格式
說完了上面的圖片,那么接下來再了解一種圖像格式:BMP。
BMP是英文Bitmap(位圖)的縮寫,它是Windows操作系統中的標准圖像文件格式[2],其能夠被多種Windows應用程序所支持。這種格式的特點是包含的圖像信息比較豐富,幾乎不進行壓縮,但由此導致了它與生俱來的缺點——占用空間過大。
1.3. 接着介紹一個頭文件
隨着Windows的逐漸普及,支持BMP圖像格式的應用軟件越來越多。這主要是因為Windows把BMP作為圖像的標准格式,並且內含了一套支持BMP圖像處理的API函數。在C語言編程中,這套API函數的存在是以Windows.h頭文件存在於系統中。在用C語言[3]編程實現圖像處理的時候我們我們需要在代碼最前端引用Windows.h頭文件。
Windows.h頭文件中包含了windef.h、winnt.h、winbase.h、winuser.h、wingdi.h等頭文件,涉及到了Windows內核API,圖形界面接口,圖形設備函數等重要的功能。
2. BMP文件格式
BMP文件格式[4]可分為文件信息頭、位圖信息和位圖數據三部分。
2.1. 文件信息頭
BMP文件頭含有BMP文件的類型、大小和存放位置等信息。Windows.h中對其定義為:
typedef struct tagBITMAPFILEHEADER{
WORD bftype; // 位圖文件的類型,必須設置為BM
DWORD bfSize; // 位圖文件的大小,以字節為單位
WORD bfReserved1; // 位圖文件保留字,必須設置為0
WORD bfReserved2; // 位圖文件保留字,必須設置為0
DWORD bfoffBits; // 位圖數據相對於位圖文件頭的偏移量表示
} BITMAPFILEHEADER;
2.2. 位圖信息
位圖信息用BITMAPINFO結構定義,它是由位圖信息頭(bitmap-information header)和彩色表(color table)組成,位圖信息頭用BITMAPINFOHEADER結構定義,彩色表用RGBQUAD結構定義。BITMAPINFO結構具有如下形式:
typedef struct tagBITMAPINFO{
BITMAPINFOHEADER bmiHeader;
RGBQUAD bmiColor[];
} BITMAPINFO;
(1)bmiHeader是一個位圖信息頭(BITMAPINFOHEADER)類型的數據類型,用於說明位圖的尺寸。BITMAPINFOHEADER的定義如下:
typedef struct tagBITMAPINFOHEADER{
DWORD biSize; // bmiHeader結構的高度
DWORD biWidth; // 位圖的寬度,以像素為單位
DWORD biHeight; // 位圖的高度,以像素為單位
WORD biPlanes; // 目標設備的位平面數,必須為1
WORD biBitCount; // 每個像素的位數,必須是1(單色)、4(16色)、8(256色)或24(真彩色)
DWORD biCompression; // 位圖的壓縮類型,必須是0(不壓縮)、1(BI-RLE8壓縮類型)或2(BI-RLE4壓縮類型)
DWORD biSizeImage; // 位圖的大小,以字節為單位
DWORD biXPeIsPerMeter; // 位圖的目標設備水平分辨率,以每米像素數為單位
DWORD biYPeIsPerMeter; // 位圖的目標設備垂直分辨率,以每米像素數為單位
DWORD biClrUsed; // 位圖實際使用的顏色表中的顏色變址數
DWORD biClrImpotant; // 位圖顯示過程中被認為重要顏色的變址數
} BITMAPINFOHEADER;
(2)bmiColor[ ]是一個顏色表,用於說明位圖中的顏色。它有若干個表項,每一表項是一個RGBQUAD類型的結構,定義一種顏色。RGBQUAD的定義如下:
typedef tagRGBQUAD{
BYTE rgbBlue;
BYTE rgbGreen;
BYTE rgbRed;
BYTE rgbReserved;
} RGBQUAD;
在RGBQUAD定義的顏色中,藍色的亮度由rgbBlue確定,綠色的亮度由rgbGreen確定,紅色的亮度由rgbRed確定。rgbReserved必須為0。
例如:若某表項為00,00,FF,00,那么它定義的顏色為存紅色。
bmiColor[ ]表項的個數由biBiCount來定:
當 biBitCount = 1、4、8 時,bmiColor[ ]分別有 2,16,256 個表項。若某點的像素值為 n,則該像素的顏色為bmiColor[n]所定義的顏色。
當 biBitCount = 24 時,bmiColor[ ]的表項為空。位圖陣列的每 3 個字節代表一個像素,3 個字節直接定義了像素顏色中藍、綠、紅的相對亮度,因此省去了bmiColor[ ]顏色。
2.3. 位圖數據
位圖陣列記錄了位圖的每一個像素值。在生成位圖文件時,Windows從位圖的左下角開始(即從左到右從下到上)逐行掃描位圖,將位圖的像素值一一記錄下來。這些記錄像素值的字節組成了位圖陣列。位圖陣列有壓縮和非壓縮兩種存儲格式。
(1)非壓縮格式。在非壓縮格式中,位圖的每個像素值對應位圖陣列的若干位(bits),位圖陣列的大小由位圖的亮度、高度及位圖的顏色數決定。
a.位圖掃描行與位圖陣列的關系
設記錄一個掃描行的像素值需 n 個字節,則位圖陣列的 0 至 n-1 個字節記錄了位圖的第一個掃描行的像素值;位圖陣列的 n 至 2n-1 個字節記錄了位圖的第二個掃描行的像素值;依此類推,位圖排列的 (m-1) x n 至 m x n - 1 個字節記錄了位圖的第 m 個掃描行的像素值。位圖陣列的大小為 n * biHeight。
當 (biWidth * biBitCount) mod32 = 0 時,n = (biWidth * biBitCount) / 8
當 (biWidth * biBitCount) mod32 ≠ 0 時,n = (biWidth * biBitCount) / 8 + 4
上式中 +4 而不是 +1 的原因是為了使一個掃描行的像素值占用位圖陣列的字節數為 4 的倍數(Windows規定其必須在long邊界結束),不足的位用 0 補充。
b.位圖像素值與位圖陣列的關系(以第 m 掃描行為例)
設記錄第 m 個掃描行的像素值的 n 個字節分別為a0,a1,a2,···,則
當 biBitCount = 1 時:a0 的 D7 記錄了位圖的第 m 個掃描的第 1 個像素值,D6位記錄了位圖的第 m 個掃描行的第 2 個像素值,···,D0 記錄了位圖的第 m 個掃描行的第 8 個像素值,a1 的 D7 位記錄了位圖的第 m 個掃描行的第 9 個像素值,D6 位記錄了位圖的第 m 個掃描行的第 10 個像素值··· ···
當 biBitCount = 4 時:a0 的 D7 ~ D4 位記錄了位圖的第 m 個掃描行的第 1 個像素值,D3 ~ D0 位記錄了位圖的第 m 個掃描行的第 2 個像素值,a1 的 D7 ~ D4 位記錄了位圖的第 m 個掃描行的第 3 個像素值··· ···
當 biBitCount = 8 時:a0 記錄了位圖的第 m 個掃描行的第 1 個像素值,a1 記錄了位圖的第 m 個掃描行的第 2 個像素值··· ···
當 biBitCount = 24 時:a0、a1、a2 記錄了位圖的第 m 個掃描行的第 1 個像素值,a3、a4、a5 記錄了位圖的第 m 個掃描行的第 2 個像素值··· ···
位圖其他掃描行的像素值與位圖陣列的對應關系與此相似。
(2)壓縮格式。Windows支持 BI_RLE8 及 BI_RLE4 壓縮位圖存儲格式,壓縮減少了位圖陣列所占用的磁盤空間。
a. BI_RLE8 壓縮格式
當biCompression = 1 時,位圖文件采用此壓縮編碼格式。壓縮編碼以兩個字節為基本單位。其中第一個字節規定了用兩個字節所指定的顏色出現的連續像素的個數。
例如,壓縮編碼 05 04 表示從當前位置開始連續顯示 5 個像素,着 5 個像素的像素值均為 04。
在第一個字節為零時,第二個字節有特殊的含義:0——行末;1——圖末;2——轉義后面的兩個字節,這兩個字節分別表示一個像素從當前位置開始的水平位移和垂直位移;n (0x003 < n < 0xFF)——轉義后面的 n 個字節,其后的 n 像素分別用這 n 個字節所指定的顏色畫出。注意:實際編碼時必須保證后面的字節數是 4 的倍數,不足的位用 0 補充。
b. BI_RLE4 壓縮格式
當 biCompression = 2時,位圖文件采用此種壓縮編碼格式。它與 BI_RLE8 的編碼方式類似,唯一的不同是:BI_RLE4 的一個字節包含了兩個像素的顏色。當連續顯示時,第一個像素按字節高四位規定的顏色畫出··· ···直到所有像素都畫出為止。
歸納起來,BMP圖像文件有下列四個特點:
I. 改格式只能存放一幅圖像。
II. 只能存儲單色、16色、256色或彩色四種圖像數據之一。
III. 圖像數據有壓縮或不壓縮兩種處理方式,壓縮方式為:RLE_4 和 RLE_8。RLE_4只能處理 16 色圖像數據;而 RLE_8 則只能壓縮 256 色圖像數據。
IV. 調色板的數據存儲結構較為特殊。
看到這里BMP的文件格式就介紹完了,本次進行編碼實現的主要是不壓縮的數據格式,壓縮的數據格式等到以后有機會再詳細介紹。
2.4. 再說一種坐標系
說完了BMP文件格式,我們再說一說圖像坐標系[5],下面這張圖片是小賈畫的圖像坐標系和圖像的存儲順序。這張圖可能有助於你理解下面代碼中對於像素的提取。

3. 編碼實現
3.1. 創建一個C項目
首先我們打開任何一個能進行C語言編程的編譯,建立一個空項目的控制台程序,新建一個CPP文件,並將Lena.bmp拷到項目文件夾中(這里我以Visual Studio 2015 為例,點擊圖片可以看大圖)。





3.2. 編碼
在編譯器中寫上下面的代碼(下面的代碼是灰度圖的讀取,關於后面多波段的讀取,我們以后在再介紹)。
#include <stdio.h> // C語言代碼中必須要引用的頭文件
#include <windows.h> // 圖像讀取的頭文件
// 使代碼在新版本的VS中正常運行不報錯
#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable:4996)
// 定義一個結構體來讀取BMP的信息
typedef struct {
// 定義文件頭信息
BITMAPFILEHEADER header;
// 定義位圖信息頭
BITMAPINFOHEADER info;
// 定義彩色表
RGBQUAD rgb[256];
// 定義位圖數據指針
unsigned char *data;
} BMP;
// 讀圖像函數
void bmpRead(const char *bmpPath, BMP &bmp) {
// 以二進制的方式打開圖片
FILE *file = fopen(bmpPath, "rb");
// 讀取文件信息頭
fread(&bmp.header, sizeof(BITMAPFILEHEADER), 1, file);
// 讀取位圖信息頭
fread(&bmp.info, sizeof(BITMAPINFOHEADER), 1, file);
// 讀取彩色表
fread(&bmp.rgb, sizeof(RGBQUAD), 256, file);
// 定義位圖數據內存大小
bmp.data = new unsigned char[bmp.info.biWidth * bmp.info.biHeight];
// 讀取元素的位圖數據
fread(bmp.data, sizeof(unsigned char), bmp.info.biWidth*bmp.info.biHeight, file);
// 關閉圖片
fclose(file);
}
// 定一個函數將讀取到bmp.data的像素灰度值存儲到一個txt文檔中
void bmpCells(const char *pixelValuePath, BMP &bmp){
// 新建一個txt文件用於存儲像素值
FILE *file = fopen(pixelValuePath, "w");
// 循環讀取像素值
for (int i = 0; i < bmp.info.biHeight; i++)
{
for (int j = 0; j < bmp.info.biWidth; j++)
{
// 獲取循環到像元的像素值
unsigned char pixel = bmp.data[j + i*bmp.info.biWidth];
// 這里我們打印一下讀出的像元
printf("%d ", pixel);
// 保存到文件中
fprintf(file, "%d ", pixel);
}
// 打印一行后換行
printf("\n");
// 完成一行像素的保存后換行繼續進行保存
fprintf(file, "\n");
}
// 關閉文件
fclose(file);
}
int main() {
// 定義圖像結構體
BMP mbmp;
// 讀取圖像(如果不是按照本文的新建項目方法,建議使用絕對路徑,否則可能會因為文件路徑錯誤而報錯。)
bmpRead("../lena.bmp", mbmp);
// 將像素值存儲到一個txt文本中
bmpCells("../pixelValue.txt", mbmp);
// 刪除指針,釋放內存
delete[] mbmp.data;
// 使程序暫停,便於查看
scanf("……");
return 0;
}
4. 驗證一下結果
4.1. 結果圖
接下來讓我們看一下運行的結果。


4.2. 驗證正確性
如果你了解遙感圖像處理,那么你一定會知道 ERDAS IMAGE 這個軟件,現在我們就用這個軟件來驗證我們編碼的結果是否正確。下圖中是我們在ERDAS IMAGE中打開的圖像像素值。

通過上面的初步對比,我們我們可以看出我們讀取的像素值是正確的。
這節的介紹我們到這里就結束了,有什么錯誤歡迎大家指出。下一節我繼續介紹《數字圖像處理(二)——BMP圖像的統計》,下節將介紹如何獲取圖像像素的最小值、最大值、均值、標准差、計算圖像的熵、以及統計圖像灰度直方圖。
Lena圖像:了解更多關於Lena圖像的信息,請轉到Lena圖像的百度百科介紹。 ↩︎
圖像格式:BMP僅是眾多圖像格式中的一種,了解更多的格式請轉到圖像格式的百度百科介紹。 ↩︎
C語言編程入門教程:網上關於C語言的編程的內容很多。例如,菜鳥教程、MOOC慕課平台。小賈這里給列出兩種,僅供參考。 ↩︎
BMP文件格式:關於BMP文件格式的介紹來源於《數字圖像處理(第三版)》(賈永紅版)。 ↩︎