(原創) 如何使用ANSI C讀寫24位元的BMP圖檔? (C/C++) (C) (Image Processing)

本文介绍如何使用ANSIC读写24位的BMP图档进行简单的影像处理,并解析BMP格式。通过C语言实现读取和写入BMP文件的功能,包括上下颠倒图片的算法。详细解释了BMP档案格式结构,以及如何利用C语言标准函数库处理BMP文件,适用于嵌入式系统。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

原文地址为: (原創) 如何使用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       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  }


原圖

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
     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行所示

//  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語言數位影像處理,全華出版社


转载请注明本文地址: (原創) 如何使用ANSI C讀寫24位元的BMP圖檔? (C/C++) (C) (Image Processing)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值