Abstract
本文介紹如何使用ANSI C讀寫24位元的BMP圖檔做簡單的影像處理,並解析BMP格式。
Introduction
之前曾在(原創) 如何使用ISO C++讀寫bmp圖檔? (C/C++) (Image Processing)介紹如何使用C++讀寫bmp檔,C++的優點是vector用法較高階,較人性化,程式可讀性較高,不過應該有不少人發現了一個問題:『用C++的vector處理影像的速度遠不如C的array!!』,而且上一篇文章專注在C++的vector部分,並沒有對bmp格式做深入的解析,將在本文一併探討。
C語言 / BmpReadWriteC3.c
2 (C) OOMusou 2007 http://oomusou.cnblogs.com
3
4 Filename : BmpReadWriteC3.c
5 Compiler : Visual C++ 8.0 / ANSI C
6 Description : Demo the how to read and write bmp by standard library
7 Release : 05/18/2008 1.0
8 */
9 #include < stdio.h >
10 #include < stdlib.h >
11
12 int upside_down( const char * fname_s, const char * fname_t) {
13 FILE * fp_s = NULL; // source file handler
14 FILE * fp_t = NULL; // target file handler
15 unsigned int x,y; // for loop counter
16 unsigned int width, height; // image width, image height
17 unsigned char * image_s = NULL; // source image array
18 unsigned char * image_t = NULL; // target image array
19 unsigned char R, G, B; // color of R, G, B
20 unsigned int y_avg; // average of y axle
21 unsigned int y_t; // target of y axle
22
23 unsigned char header[ 54 ] = {
24 0x42 , // identity : B
25 0x4d , // identity : M
26 0 , 0 , 0 , 0 , // file size
27 0 , 0 , // reserved1
28 0 , 0 , // reserved2
29 54 , 0 , 0 , 0 , // RGB data offset
30 40 , 0 , 0 , 0 , // struct BITMAPINFOHEADER size
31 0 , 0 , 0 , 0 , // bmp width
32 0 , 0 , 0 , 0 , // bmp height
33 1 , 0 , // planes
34 24 , 0 , // bit per pixel
35 0 , 0 , 0 , 0 , // compression
36 0 , 0 , 0 , 0 , // data size
37 0 , 0 , 0 , 0 , // h resolution
38 0 , 0 , 0 , 0 , // v resolution
39 0 , 0 , 0 , 0 , // used colors
40 0 , 0 , 0 , 0 // important colors
41 };
42
43 unsigned int file_size; // file size
44 unsigned int rgb_raw_data_offset; // RGB raw data offset
45
46 fp_s = fopen(fname_s, " rb " );
47 if (fp_s == NULL) {
48 printf( " fopen fp_s error\n " );
49 return - 1 ;
50 }
51
52 // move offset to 10 to find rgb raw data offset
53 fseek(fp_s, 10 , SEEK_SET);
54 fread( & rgb_raw_data_offset, sizeof (unsigned int ), 1 , fp_s);
55 // move offset to 18 to get width & height;
56 fseek(fp_s, 18 , SEEK_SET);
57 fread( & width, sizeof (unsigned int ), 1 , fp_s);
58 fread( & height, sizeof (unsigned int ), 1 , fp_s);
59 // move offset to rgb_raw_data_offset to get RGB raw data
60 fseek(fp_s, rgb_raw_data_offset, SEEK_SET);
61
62 image_s = (unsigned char * )malloc((size_t)width * height * 3 );
63 if (image_s == NULL) {
64 printf( " malloc images_s error\n " );
65 return - 1 ;
66 }
67
68 image_t = (unsigned char * )malloc((size_t)width * height * 3 );
69 if (image_t == NULL) {
70 printf( " malloc image_t error\n " );
71 return - 1 ;
72 }
73
74 fread(image_s, sizeof (unsigned char ), (size_t)( long )width * height * 3 , fp_s);
75
76 // vertical inverse algorithm
77 y_avg = 0 + (height - 1 );
78
79 for (y = 0 ; y != height; ++ y) {
80 for (x = 0 ; x != width; ++ x) {
81 R = * (image_s + 3 * (width * y + x) + 2 );
82 G = * (image_s + 3 * (width * y + x) + 1 );
83 B = * (image_s + 3 * (width * y + x) + 0 );
84
85 y_t = y_avg - y;
86
87 * (image_t + 3 * (width * y_t + x) + 2 ) = R;
88 * (image_t + 3 * (width * y_t + x) + 1 ) = G;
89 * (image_t + 3 * (width * y_t + x) + 0 ) = B;
90 }
91 }
92
93 // write to new bmp
94 fp_t = fopen(fname_t, " wb " );
95 if (fp_t == NULL) {
96 printf( " fopen fname_t error\n " );
97 return - 1 ;
98 }
99
100 // file size
101 file_size = width * height * 3 + rgb_raw_data_offset;
102 header[ 2 ] = (unsigned char )(file_size & 0x000000ff );
103 header[ 3 ] = (file_size >> 8 ) & 0x000000ff ;
104 header[ 4 ] = (file_size >> 16 ) & 0x000000ff ;
105 header[ 5 ] = (file_size >> 24 ) & 0x000000ff ;
106
107 // width
108 header[ 18 ] = width & 0x000000ff ;
109 header[ 19 ] = (width >> 8 ) & 0x000000ff ;
110 header[ 20 ] = (width >> 16 ) & 0x000000ff ;
111 header[ 21 ] = (width >> 24 ) & 0x000000ff ;
112
113 // height
114 header[ 22 ] = height & 0x000000ff ;
115 header[ 23 ] = (height >> 8 ) & 0x000000ff ;
116 header[ 24 ] = (height >> 16 ) & 0x000000ff ;
117 header[ 25 ] = (height >> 24 ) & 0x000000ff ;
118
119 // write header
120 fwrite(header, sizeof (unsigned char ), rgb_raw_data_offset, fp_t);
121 // write image
122 fwrite(image_t, sizeof (unsigned char ), (size_t)( long )width * height * 3 , fp_t);
123
124 fclose(fp_s);
125 fclose(fp_t);
126
127 return 0 ;
128 }
129
130 int main() {
131 upside_down( " clena.bmp " , " clena3.bmp " );
132 }
原圖

執行結果

這個範例很簡單,想將lena作上下顛倒,整個upside_down()要做的事情有
Step 1:將bmp讀進arrray。
Step 2:處理上下顛倒演算法。
Step 3:將新的array寫入bmp。
Step 1:將bmp讀進array
與(原創) 如何使用ISO C++讀寫bmp圖檔? (C/C++) (Image Processing)讀取bmp的方式有兩點不同:
1.C++版本使用的是二維的vector,可讀性較高,但速度較慢。
2.C++版本須在程式內指定影像的width與height,若圖片改變,width和height就得重新設定。
在本範例,我們做了些改進:
1.C版本使用一維array增加速度。
2.C版本不須指定影像的width與height,若圖片改變,也不用設定width和height,我們直接從bmp的header獲知width與height。
BMP檔案格式結構解析
為什麼選擇用BMP格式呢?一般最常見的雖然是JPG與GIF,但這些都是壓縮格式,要讀取比較麻煩,必須額外靠OpenCV、.NET Framework或MFC之類的library,而影像處理重在演算法的測試,為了簡化起見,我們希望僅用C語言的標準函式庫就能處理,這樣在跨平台與嵌入式的應用上比較方便,所以我們選擇使用BMP格式。
BMP的檔案結構,如下圖所示,共分成3部分[1]

1.BITMAPFILEHEADER:BMP檔的檔頭,判斷是否為BMP格式,與檔案的大小(size)。
2.BITMAPINFO分成兩部分:
a.BITMAPINFOHEADER:BMP檔案的資訊,如width、height、是否壓縮...等等。
b.PALLETE:BMP調色盤。
3.RAW DATA:BMP每個pixel的RGB資訊。
若用C語言的struct,則可嚴謹的表示以上的架構。
unsigned short identity; // 2 byte : "BM"則為BMP
unsigned int file_size; // 4 byte : 檔案size
unsigned short reserved1; // 2 byte : 保留欄位,設為0
unsigned short reserved2; // 2 byte : 保留欄位,設為0
unsigned int data_offset; // 4 byte : RGB資料開始之前的資料偏移量
};
struct BITMAPINFOHEADER {
unsigned int header_size; // 4 byte : struct BITMAPINFOHEADER的size
int width; // 4 byte : 影像寬度(pixel)
int height; // 4 byte : 影像高度(pixel)
unsigned short planes; // 2 byte : 設為1
unsigned short bit_per_pixel; // 2 byte : 每個pixel所需的位元數(1/4/8/16/24/32)
unsigned int compression; // 4 byte : 壓縮方式, 0 : 未壓縮
unsigned int data_size; // 4 byte : 影像大小,設為0
int hresolution; // 4 byte : pixel/m
int vresolution; // 4 byte : pixel/m
unsigned int used_colors; // 4 byte : 使用調色盤顏色數,0表使用調色盤所有顏色
unsigned int important_colors; // 4 byte : 重要顏色數,當等於0或used_colors時,表全部都重要
};
struct PALLETTE {
char blue; // 1 byte : 調色盤藍色
char green; // 1 byte : 調色盤綠色
char red; // 1 byte : 調色盤紅色
char reserved; // 1 byte : 保留欄位,設為0
};
回到程式,9 ~ 10行
#include < stdlib.h >
我們只用了兩個C語言的標準函式庫,而沒用再用其他library,這對於嵌入式系統,如Nios II非常方便,不用再擔心其他library是否能在Nios II make成功。
131行
upside_down( " clena.bmp " , " clena3.bmp " );
}
整個函數只需傳入來源圖片檔名clena.bmp與目標圖片檔名clena3.bmp即可,不須再傳入寬度與高度。
23行
0x42 , // identity : B
0x4d , // identity : M
0 , 0 , 0 , 0 , // file size
0 , 0 , // reserved1
0 , 0 , // reserved2
54 , 0 , 0 , 0 , // RGB data offset
40 , 0 , 0 , 0 , // struct BITMAPINFOHEADER size
0 , 0 , 0 , 0 , // bmp width
0 , 0 , 0 , 0 , // bmp height
1 , 0 , // planes
24 , 0 , // bit per pixel
0 , 0 , 0 , 0 , // compression
0 , 0 , 0 , 0 , // data size
0 , 0 , 0 , 0 , // h resolution
0 , 0 , 0 , 0 , // v resolution
0 , 0 , 0 , 0 , // used colors
0 , 0 , 0 , 0 // important colors
};
這是為了要寫入BMP檔的檔頭做準備,為什麼是54呢?若要儲存一個非壓縮且沒應用調色盤的BMP,所需要的檔頭為struct BITMAPFILEHEADER (14 byte)、struct BITMAPINFOHEADER (40 byte),而不需struct PALLETTE,這樣共需54 byte,所以宣告了54 byte的陣列。至於每個byte所代表的意思,我已經在code中加了註解,而將來需要更改的,有file size、bmp width、bmp height,這三者在後面會處理。
BMP檔頭雖然有很多資訊,對於影響處理而言,所關心的只有2個:
1.從哪一個byte才能開始讀取每個pixel的RGB資訊?
2.影像的寬度與高度為多少?
RGB data的offset
由struct BITMAPFILEHEADER所知,data_offset儲存了哪一個byte才能開始讀取每個pixel的RGB資訊,或許你會問:『直接offset 54 byte不就好了?』對於沒有壓縮,沒有使用調色盤的BMP的確是如此,但若使用了調色盤,情況會很複雜,因為調色盤的長度沒有限制,所以offset不見得是54 byte,最保險的方式是讀取offset 10 byte的data_offset欄位,如52行所示
fseek(fp_s, 10 , SEEK_SET);
fread( & rgb_raw_data_offset, sizeof (unsigned int ), 1 , fp_s);
影像的寬度與高度
根據struct BITMAPINFOHEADER得知,offset 4 byte與offset 8 byte的width與height欄位可得知影像的寬度與高度,如55行所示
fseek(fp_s, 18 , SEEK_SET);
fread( & width, sizeof (unsigned int ), 1 , fp_s);
fread( & height, sizeof (unsigned int ), 1 , fp_s);
最後將offset移到rgb_raw_data_offset開始準備讀取RGB資訊,59行
fseek(fp_s, rgb_raw_data_offset, SEEK_SET);
將RGB資訊讀進一維陣列
要做影像處理的演算法,首要步驟就是將每個pixel的RGB資訊讀進陣列,在(原創) 如何使用ISO C++讀寫bmp圖檔? (C/C++) (Image Processing)使用了C++的vector,速度較慢,這次我們用C語言的一維陣列。
62行
if (image_s == NULL) {
printf( " malloc images_s error\n " );
return - 1 ;
}
image_t = (unsigned char * )malloc((size_t)width * height * 3 );
if (image_t == NULL) {
printf( " malloc image_t error\n " );
return - 1 ;
}
使用malloc()根據影像寬度與高度建立一個動態陣列,* 3是因為每個pixel有RGB,而R、G、B各占一個byte。
image_s表示source array,image_t表示target array。
74行
正式從BMP檔案將每個pixel的RGB資訊讀進image_s這個一維陣列。
Step 2:處理上下顛倒演算法。
從一維陣列讀出每個pixel的RGB值
將RGB資訊讀進一維陣列還不夠,要做影像處理,還須將每個pixel的RGB讀出來,81行
G = * (image_s + 3 * (width * y + x) + 1 );
B = * (image_s + 3 * (width * y + x) + 0 );
由於我們是用一維陣列去模擬二維陣列,所以程式碼讀起來比較難看些,* 3是因為陣列每個element要存RGB,故須3 byte,另外BMP結構存的順序是先B,然後 G,最後才是R,這和我們一般習慣的RGB不一樣。
Step 3:將新的array寫入bmp
寫入BMP
將image_t陣列寫入新的BMP檔案並不難,前面我們有提到在BMP檔頭還有三個資訊需要修正:
1.檔案大小
2.影像寬度
3.影像高度
100行,以檔案大小作為例子解釋,寬度和高度的原理都一樣
file_size = width * height * 3 + rgb_raw_data_offset;
header[ 2 ] = (unsigned char )(file_size & 0x000000ff );
header[ 3 ] = (file_size >> 8 ) & 0x000000ff ;
header[ 4 ] = (file_size >> 16 ) & 0x000000ff ;
header[ 5 ] = (file_size >> 24 ) & 0x000000ff ;
檔案大小的方法是寬度 * 高度 * 3,因為RGB占3 byte,最後在加上BMP檔頭大小。
由於header是一個unsigned char陣列,每個元素都是1 byte,也就是8 bit,但file_size是unsigned int,是32 bit,所以先對size_size做0x000000ff mask,將最低的8 bit取出,然後再>> 8,再做0x000000ff mask,對第2個8 bit取出,以此類推...。
Remark
若要詳細研究BMP格式,在Charles Petzold的Programming Windows[2] Ch.15有詳細完整的介紹。
Conclusion
要動影像處理演算法,第一步就是要將RGB資訊讀進陣列,才能做後續的處理,本文用C語言示範了讀取BMP檔的方式。除此之外,也深入探討BMP的格式,讓我們知道如一個檔案格式是如何被定義出來。事實上,我們也可模仿這種方式,定義出一種只有自己或公司能讀取與寫入的格式,只要格式結構不流出去,別人就很難得知該怎麼去讀寫這種檔案。
See Also
(原創) 如何使用ANSI C讀寫32位元的BMP圖檔? (C/C++) (C) (Image Processing)
(原創) 如何使用ANSI C讀寫24/32位元的BMP圖檔? (C/C++) (C) (Image Processing)
(原創) 如何使用ISO C++讀寫bmp圖檔? (C/C++) (Image Processing)
(原創) 如何將圖片上下翻轉? (.NET) (ASP.NET) (GDI+) (Image Processing)
(原創) 如何使用C++/CLI读/写jpg檔? (C++/CLI)
(原創) 如何用程序的方式载入jpg图形文件? (C#/ASP.NET)
Reference
[1] swwuyam的BMP檔案格式
[2] Charles Petzold 1998, Programming Windows, Microsoft Press
瘋小貓的華麗冒險的點陣圖(Bitmap)檔案格式
BMP文件格式分析
賴岱佑、劉敏 2007,數位影像處理 技術手冊,文魁資訊
井上誠喜、八木申行、林 正樹、中須英輔、三古公二、奧井誠人 著 2006,吳上立,林宏燉 編譯,C語言數位影像處理,全華出版社
转载请注明本文地址: (原創) 如何使用ANSI C讀寫24位元的BMP圖檔? (C/C++) (C) (Image Processing)