(原創) 如何使用ANSI C讀寫24位元的BMP圖檔? (C/C++) (C) (Image Processing)
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語言數位影像處理,全華出版社