深入理解BMP图片数据结构及其存储原理

新机器视觉

共 5910字,需浏览 12分钟

 ·

2022-03-02 15:28

点击下方卡片,关注“新机器视觉”公众号

重磅干货,第一时间送达

BMP(Bitmap)图片是Windows操作系统中的标准图像文件格式,它除了位深度可选外,不采用其它任何有损压缩,这也意味着它保留了图像中的最原始的数据。在很多图像处理工程中,我们经常使用到BMP位图文件,今天我主要总结一下BMP文件的格式以及存储原理。


我们先从物理层面说一下显示器的显像原理


显示器显像基本原理


我们拿液晶显示器举例,显示器底部是一块发光的白板灯,中间是液晶(一种高分子有机化合物,在电场的作用下可呈现不同的分子排列,引起光学形态的变化,从而形成不同亮度的灰阶),然后是一些滤光片,显示器的屏幕是由很多个“小块”组成的,每块后面都有红绿蓝三个滤光片,每个小块就是1个像素点。滤光片能够把显示器背后发出的白光过滤,留下单色光通过,白光经过三块滤光片后被分解成了红绿蓝(人类视网膜上的视锥细胞对红绿蓝三色最为敏感)三束光,进入人的眼睛。由于一个像素极其小,三个滤光片距离极其近,以至于透过它们的光进入人眼后,人眼分不清这是3束光,即光在人眼中发生混色作用,于是一个像素便“有了”颜色。显示器正是通过电压来控制液晶分子的排列方式,来改变其光线的透过程度,形成不同程度的RGB亮度,进而显示出特定的颜色。这便是显示器显示图像的基本原理。



其实,图像的显示跟显示器显示的原理极为相似,也是通过控制RGB分量(亮度)来显示出每个像素颜色的。电压信号是一种模拟信号,它是连的,比如从0到1,之间还有无数个数据,但是显示到图像上却是数字信号,它的信号是跳跃性的。一般情况下,每个亮度从最暗到最亮共分为256个级别,当然,并不是所有的图像的RGB亮度都分为256个级别,这与图像的位深有关,一般情况下图像有1bit,4bit,8bit,24bit,32bit等等,位深越大,分量的区间被细分的就越多,从而能够显示颜色的类就越多,这就好比你把尺子刻度分割的越细,你的测量精度就越高。


一般情况下,我们使用的彩色图像都是24位深图像,它的RGB分量为256个级别(0~255),刚好为一个字节,每个分量占用一个字节,单个像素占用三个字节,共计3×8=24位,24位及24位深度以上的图像,我们叫它为真彩色。


为了更加直观的说明,我们来做个实验,打开你的Excel,在宏编辑器(具体使用方法请参考我上一篇文章 手把手教你用Excel编写俄罗斯方块)内输入下面一行代码:


Sub test()    Cells(2, 4).Interior.Color = RGB(Cells(2, 1), Cells(2, 2), Cells(2, 3))End Sub


我们添加一个按钮,关联一下这个宏,看一下实际运行效果:



可见,每改变RGB的分量,都会呈现出不同的一种颜色,如果是24位真彩色,那么每个像素能够表现出来的颜色种类为256*256*256=16777216色。日常我们使用过程中,除了三种常用的纯红、纯绿、纯蓝之外,最常用的颜色RGB组合如下:


颜色
R
G
B

255
255
0

255
0
255

0
255
255

255
255255

128
128
128


我们先来看下面这张BMP图片,这张是我在太湖东山岛亲手拍摄的一张日落照片:


怎么样,很漂亮吧,哈哈。


我们用Windows自带的画图软件,打开这张图片,将其放到最大:



可以明显看到,图像被分成了一个个“小方格”,每个小方格就是一个像素,他就是通过调节不同的RGB分量来形成的。


我们再右键看一下它的属性



我们看到这张图片,位深为24位,按照每个像素占用三个字节,长宽有了,那么整张照片大小应该是2127*1200*3=7657200字节,但是实际大小却是7660854字节,而且占用空间与大小居然不一样,这是为什么呢?


这里就涉及到在Windows下存储BMP图片结构的特点了,除了存储像素元素外,BMP文件还有四个非常重要的文件结构,分别是文件头、信息头、颜色表。下面,我们一个个来详细说明。


BMP文件结构

BMP文件由4部分组成:

1.   位图文件头(bitmap-file header)

2.   位图信息头(bitmap-informationheader)

3.   颜色表(color table)

4.   颜色点阵数据(bits data)

24位真彩色位图没有颜色表,所以只有1、2、4这三部分。

1,位图文件头(BITMAPFILEHEADER)


我们先看一下,在wingdi.h文件中,它的数据结构:

typedef struct tagBITMAPFILEHEADER {      WORD    bfType;      DWORD   bfSize;      WORD    bfReserved1;      WORD    bfReserved2;      DWORD   bfOffBits;} BITMAPFILEHEADER, FAR *LPBITMAPFILEHEADER, *PBITMAPFILEHEADER;


位图文件头分为四个部分(我们将两个保留字节看成一部分),共14个字节:

名称

占用空间

内容

实际数据

bfType

2字节

标识,就是“BM”二字

0x42(B), 0x4d(M)

bfSize

4字节

整个BMP文件的大小

0x0074e536(7660854)【与右键查看图片属性里面的大小值一样】

bfReserved1/2

4字节

保留字,没用

0x00000000

bfOffBits

4字节

偏移数,即 位图文件头+位图信息头+调色板 的大小

0x00000036(54)


为了更加直观理解,我们使用Notepad++打开这张图片



打开后,我们看到,头两个字节存储的分别是B/M两个字母的十六进制ASCII码,后面四个字节实际存储的是整个图片的大小,由于Windows采用小端对齐模式,高内存地址存放高位,低内存地址存放低位,数据是倒着放的,所以实际数据应该是0x0074e536,十进制刚好与我们右键查看的大小7660854一样,随后的四个字节为保留字节,暂时没用;最后四个字节为偏移量数据,保存数据为位图文件头+位图信息头+调色板 = 54. 这便是位图文件头里面数据的含义,整个文件头占用14个字节。


2,位图信息头(BITMAPINFOHEADER)


我们来看一下信息头在wingdi.h文件中的数据结构:

typedef struct tagBITMAPINFOHEADER{      DWORD      biSize;      LONG       biWidth;      LONG       biHeight;      WORD       biPlanes;      WORD       biBitCount;      DWORD      biCompression;      DWORD      biSizeImage;      LONG       biXPelsPerMeter;      LONG       biYPelsPerMeter;      DWORD      biClrUsed;      DWORD      biClrImportant;} BITMAPINFOHEADER, FAR *LPBITMAPINFOHEADER, *PBITMAPINFOHEADER;


位图信息头占用字节比较多,共计40个字节:

名称

占用空间

内容

实际数据

biSize

4字节

位图信息头的大小,为40

0x28(40)

biWidth

4字节

位图的宽度,单位是像素

0x084f(2127)

biHeight

4字节

位图的高度,单位是像素

0x04b0(1200)

biPlanes

2字节

固定值1

0x0001

biBitCount

2字节

每个像素的位数

1-黑白图,4-16色,8-256色,24-真彩色

0x0018(24)

biCompression

4字节

压缩方式,BI_RGB(0)为不压缩

0

biSizeImage

4字节

位图全部像素占用的字节数,BI_RGB时可设为0

0x74e500(7660800)

biXPelsPerMeter

4字节

水平分辨率(像素/米)

0x1274(4724)

biYPelsPerMeter

4字节

垂直分辨率(像素/米)

0x1274(4724)

biClrUsed

4字节

位图使用的颜色数

如果为0,则颜色数为2的biBitCount次方

0

biClrImportant

4字节

重要的颜色数,0代表所有颜色都重要

0



3,颜色表(调色板/color table)


彩色表/调色板(color table)是1色、16色和256色图像文件所特有的,相对应的调色板大小是2、16和256,调色板以4字节为单位,每4个字节存放一个颜色值,图像 的数据是指向调色板的索引。可以将调色板想象成一个数组,每个数组元素的大小为4字节,假设有一256色的BMP图像的调色板数据为:调色板[0]=黑、调色板[1]=白、调色板[2]=红、调色板[3]=蓝…调色板[255]=黄。


例如图像数据01 00 02 FF表示调用调色板[1]、调色板[0]、调色板[2]和调色板[255]中的数据来显示图像颜色。每个调色板的大小为4字节,按蓝、绿、红存储一个颜色值。


我们看一下在wingdi.h文件中,对调色板数据结构的定义:

typedef struct tagRGBQUAD {    BYTE    rgbBlue;     BYTE    rgbGreen;    BYTE    rgbRed;    BYTE    rgbReserved;} RGBQUAD;

调色板中的数据定义为:

字 段 名

大小(单位:字节)

描 述

rgbBlue

1

蓝色值

rgbGreen

1

绿色值

rgbRed

1

红色值

rgbReserved

1

保留,总为0


在24位以及24以上的真彩色图像中,没有调色板,信息头后面直接跟着的就是位图的像素数据。


4,位图数据(bitmap-data)


如果图像是单色、16色和256色,则紧跟着调色板的是位图数据,位图数据是指向调色板的索引序号。


如果位图是16位、24位和32位色,则图像文件中不保留调色板,即不存在调色板,图像的颜色直接在位图数据中给出。


16位图像使用2字节保存颜色值,常见有两种格式:5位红5位绿5位蓝和5位红6位绿5位蓝,即555格式和565格式。555格式只使用了15 位,最后一位保留,设为0.


24位图像使用3字节保存颜色值,每一个字节代表一种颜色,按红、绿、蓝排列。32位图像使用4字节保存颜色值,每一个字节代表一种颜色,除了原来的红、绿、蓝,还有Alpha通道,即透明色。


如果图像带有调色板,则位图数据可以根据需要选择压缩与不压缩,如果选择压缩,则根据BMP图像是16色或256色,采用RLE4或RLE8压缩算法压缩。


这里我们还以上面那张日落照片为例来进行说明:


由于这张照片是24位真彩色,所以它没有调色板,位图信息头(BITMAPINFOHEADER)后面紧接着就是图像的真实数据。不过这里有个细节,位图全部像素数据,在存储的过程中是按照自下而上,自左往右的顺序进行排列的,这点为什么是这样,我也不太清楚,我估计是历史遗留问题。还有一点,单个像素的通道存储顺序也是反着的,实际是按照BGR的顺序来存储的,这点很好理解,因为在存储过程中,RGB三个字节是同时写入的,低字节在前,所以实际在读取的时候顺序却是BGR. 这两点一定要注意,在实际使用过程中很容易出错。



有了上面这些知识,我们再回到本文开头提出的那个文件大小的问题,我们照片的大小为2127*1200*3=7657200字节,再加上两个文件头占用的54个字节,总共大小应该为7657200+54=7657254,但是实际大小却是7660854字节,与实际结果还有差距,这是怎么回事呢?


实际上,这是Windows操作系统中的内存对齐造成的,Windows要求位图的每一行像素所占字节数必须被4整除,因为这样对操作系统读取数据非常方便,若不能4整除,则在该位图每一行的十六进制码末尾"补"1至3个字节的"00"。


例如我们这张图片每行为2127个像素,每行占用2127*3=6381字节,6381除以4还余1,所以需要在每行再补齐3个字节,这样每行实际占用6381+3=6384个字节,总共像素占用6384*1200=7660800字节,另外再加上两个文件头信息占用的54个字节,整张图片的大小为7660854,刚好与我们右键信息的大小一致。


但是为什么图片的实际大小与图片的占用空间还不一样呢,这与Windows使用的NTFS和FAT文件管理系统有关,这里暂不做详细讲解,有空我会抽时间专门详细说明。


最后,我们根据上面内容做一个验证,查看一下东山岛那张照片左下角最后一行第一个像素值是否与我们上面Notepad++打开的内容一致,我们使用下面几行代码(c语言版)进行测试:

void saveBmpImage(){  char fileName[30] = "1.bmp";              //定义打开图像名字  char *buf;                                //定义文件读取缓冲区  char *p;  int r, g, b;  FILE *fp;                                 //定义文件指针  FILE *fpw;                                //定义保存文件指针  DWORD w, h;                                //定义读取图像的长和宽  DWORD bitSize;                            //定义图像的大小  BITMAPFILEHEADER bf;                      //图像文件头  BITMAPINFOHEADER bi;                      //图像文件头信息  if ((fp = fopen(fileName, "rb")) == NULL)  {    cout << "文件未找到!";    exit(0);  }  fread(&bf, sizeof(BITMAPFILEHEADER), 1, fp);//读取BMP文件头  fread(&bi, sizeof(BITMAPINFOHEADER), 1, fp);//读取BMP信息头  w = bi.biWidth;                            //获取图像的宽  h = bi.biHeight;                           //获取图像的高  bitSize = bi.biSizeImage;                  //获取图像的size  int c = (w * 3) % 4;  //分配缓冲区大小, 注意内存对齐  int buf_data_size = ((w * 3) + (4 - c)) * h;  buf = (char*)malloc(buf_data_size);    //定位到像素起始位置     fseek(fp, long(sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER)), 0);  fread(buf, buf_data_size, 1, fp);       //开始读取数据  p = buf;  //这里只输出最后一行  for (int j = h-1; j < h; j++)  {    for (int i = 0; i < w; i++)    {      b = *p++; g = *p++; r = *p++;      char result1[8];      char result2[8];      char result3[8];      sprintf(result1, "%02x", b);      sprintf(result2, "%02x", g);      sprintf(result3, "%02x", r);      cout << result1 << " " << result2 << " " << result3 << endl;    }  }  free(buf);}

在这段代码中,理论上给图像真实数据分配缓冲区大小直接使用计算出来的bi.biSizeImage即可,但是在实际项目中,需要我们自己去构建一个bmp图片,事先并不知道其真实数据区有多大,我们就需要根据实际情况来自己计算缓冲区大小了,所以在本例中我就自己动手计算了一下(上面代码第25行)。这里要特别注意每行像素占用的内存大小是否需要补零操作。


我们看一下运行结果:



可见,代码运行结果与我们实际用Notepad++打开的结果一致,说明我们的代码没问题,验证通过。另外,在本例中,由于我们两个头文件、图像真实数据缓冲区已经分配好,我们完全可以再生成一张图片,当然这张图片是上一张图片的复制版,上述代码在free缓冲区前加上下面几行:

  fpw = fopen("2.bmp", "wb");  fwrite(&bf, sizeof(BITMAPFILEHEADER), 1, fpw);  //写入文件头  fwrite(&bi, sizeof(BITMAPINFOHEADER), 1, fpw);  //写入信息头  p = buf;  fwrite(p, buf_data_size, 1, fpw);//bmp, data  fclose(fpw);  fclose(fp);

这样,我们就能生成一张新的BMP图片了,在实际项目中,生成BMP图片的顺序也一样,都是先定义文件头、信息头数据,最后再定义图像数据缓冲区,最后生成一张完整的图片,不过要特别注意是否需要补零操作。


结语:以上就是本文的全部内容了,希望通过本文能够加深你对BMP图片结构(尤其是两个头文件)及其存储原理的理解。

本文仅做学术分享,如有侵权,请联系删文。

—THE END—
浏览 544
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报