(原創) 如何使用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

  1 /* 
  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     0000,  // file size
 27     00,        // reserved1
 28     00,        // reserved2
 29     54000// RGB data offset
 30     40000// struct BITMAPINFOHEADER size
 31     0000,  // bmp width
 32     0000,  // bmp height
 33     10,        // planes
 34     240,       // bit per pixel
 35     0000,  // compression
 36     0000,  // data size
 37     0000,  // h resolution
 38     0000,  // v resolution 
 39     0000,  // used colors
 40     0000   // 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 }


原圖

clena.jpg


執行結果

clena3.jpg


這個範例很簡單,想將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]

bmp_struct.png


1.BITMAPFILEHEADER:BMP檔的檔頭,判斷是否為BMP格式,與檔案的大小(size)。
2.BITMAPINFO分成兩部分:
  a.BITMAPINFOHEADER:BMP檔案的資訊,如width、height、是否壓縮...等等。
  b.PALLETE:BMP調色盤。
3.RAW DATA:BMP每個pixel的RGB資訊。

若用C語言的struct,則可嚴謹的表示以上的架構。

struct BITMAPFILEHEADER {
  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 <stdio.h>
#include 
<stdlib.h>


我們只用了兩個C語言的標準函式庫,而沒用再用其他library,這對於嵌入式系統,如Nios II非常方便,不用再擔心其他library是否能在Nios II make成功。

131行

int main() {
  upside_down(
"clena.bmp""clena3.bmp");
}


整個函數只需傳入來源圖片檔名clena.bmp與目標圖片檔名clena3.bmp即可,不須再傳入寬度與高度。

23行

 unsigned char header[54= {
    
0x42,        // identity : B
    0x4d,        // identity : M
    0000,  // file size
    00,        // reserved1
    00,        // reserved2
    54000// RGB data offset
    40000// struct BITMAPINFOHEADER size
    0000,  // bmp width
    0000,  // bmp height
    10,        // planes
    240,       // bit per pixel
    0000,  // compression
    0000,  // data size
    0000,  // h resolution
    0000,  // v resolution 
    0000,  // used colors
    0000   // 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行所示

// move offset to 10 to find rgb raw data offset
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行所示

// move offset to 18    to get width & height;
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行

// move offset to rgb_raw_data_offset to get RGB raw data
fseek(fp_s, rgb_raw_data_offset, SEEK_SET);


將RGB資訊讀進一維陣列
要做影像處理的演算法,首要步驟就是將每個pixel的RGB資訊讀進陣列,在(原創) 如何使用ISO C++讀寫bmp圖檔? (C/C++) (Image Processing)使用了C++的vector,速度較慢,這次我們用C語言的一維陣列。

62行

image_s = (unsigned char *)malloc((size_t)width * height * 3);
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行

fread(image_s, sizeof(unsigned char), (size_t)(long)width * height * 3, fp_s);


正式從BMP檔案將每個pixel的RGB資訊讀進image_s這個一維陣列。

Step 2:處理上下顛倒演算法。
從一維陣列讀出每個pixel的RGB值
將RGB資訊讀進一維陣列還不夠,要做影像處理,還須將每個pixel的RGB讀出來,81行

= *(image_s + 3 * (width * y + x) + 2);
= *(image_s + 3 * (width * y + x) + 1);
= *(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  
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] swwuyamBMP檔案格式
[2] Charles Petzold 1998, Programming Windows, Microsoft Press
瘋小貓的華麗冒險點陣圖(Bitmap)檔案格式
BMP文件格式分析
賴岱佑、劉敏 2007,數位影像處理 技術手冊,文魁資訊
井上誠喜、八木申行、林 正樹、中須英輔、三古公二、奧井誠人 著 2006,吳上立,林宏燉 編譯,C語言數位影像處理,全華出版社

posted on 2008-05-06 00:08  真 OO无双  阅读(34645)  评论(18编辑  收藏  举报

导航