一、实验原理
1. BMP文件的组成
BMP文件由四部分组成:位图文件头数据、位图信息头数据、调色板和位图数据。下面结合具体图片说明。为了方便在查看数据项在图片里的具体内容,每行前面加了当前项在文件中的地址值。
(1)文件头 BITMAP_FILE_HEADER,包含如下内容
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
(2)信息头BITMAP_INFO_HEADER,包含如下内容
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
(3)调色板Palette。当图像位深度小于等于8时称为索引颜色,这时整个文件不保存每个像素的RGB值,而是把图像中的所有颜色编制成颜色表。在表示图片中每个像素的颜色信息时,使用颜色表的索引。由于位深度不超过8位,颜色表不超过256个数值。
调色板决定于位于0x2e~0x31的biClrUsed字段和位于0x1c~0x1d的biBitCount字段,每个元素的类型是一个 RGBQUAD 结构,顺序为B,G,R及保留字,每一个元素占用一个字节。
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
(4)位图数据ImageData。对于用到调色板的位图,图像数据就是该像素颜色在调色板中的索引值。对于真彩色图,图像数据就是实际的RGB值。
图像的每一扫描行由表示图像像素的连续的字节组成,每一行的字节数取决于图像的颜色数目和用像素表示的图像宽度。规定每一扫描行的字节数必须是 4 的整倍数,也就是DWORD 对齐的。扫描行是由底向上存储的,这就是说,阵列中的第一个字节表示位图左下角的像素,而最后一个字节表示位图右上角的像素。
表1是两幅具体图片及其文件内容,表格里只列出了文件头和信息头中不同部分的值。
| ||
---|---|---|
二进制方式打开 | ||
图像基本信息 | 分辨率:1280×720 位深度:8位索引颜色 文件大小:901KB=922,680B | 分辨率:1280×1280 位深度:24位真彩色 文件大小:4.68MB=4,915,256B |
0x0002~0x0005 | 38 14 0e 00 | 38 00 4b 00 |
0x000a~0x000d | 36 04 00 00 | 36 00 00 00 |
0x0016~0x0019 | d0 02 00 00 | 00 05 00 00 |
0x001c~0x001d | 08 00 | 18 00 |
0x0022~0x0025 | 02 10 0e 00 | 02 00 4b 00 |
- 0x00~0x01,标记了此文件为BMP文件,内容是B和M两个字母的ASCII码。
- 0x02~0x05,以字节为单位的文件大小。左图大小为922,680字节,写成十六进制就是E1438。在这里可以看出BMP文件的小端保存方式,先保存低位字节,再保存高位字节,保存顺序为38、14、0e。同理,右图为4,915,256(4B0038H)个字节,保存顺序为38、00、4b。
- 0x0a~0x0d,实际数据的字节偏移量。BMP文件共有的前两部分文件头占了54(36H)个字节,左图包含了8位也就是256种索引颜色的调色板,调色板中每个颜色的RGBQUAD结构占用4字节,总共1024个字节的调色板信息。因此左图的偏移量为54+1024=1078(436H)个字节。
- 0x16~0x19,左图高度为720(2D0H)个像素,右图高度为1280(500H)个像素。
- 0x1c~0x1d,左图位深度为8(8H),右图位深度为24(18H)。
- 0x22~0x25,BMP图像大小biSizeImage可由下式计算
biSizeImage=⌊cx×biBitCount+3132⌋×4×cy+2其中, cx,cy 表示水平和垂直方向的像素数。 cx×biBitCount 表示一行图像占了多少位。BMP规定这个图像大小必须是4字节的整数倍,也就是32位的整数倍,因此需要把 cx×biBitCount 加31再除以32后下取整,就保证了计算结果是离这个数最近的而且是比它大的32的倍数,也就保证了是4字节的整数倍。乘以4和行数,得到4字节整数倍的图像大小。
另外,BMP文件的末尾两个字节是保留位,无论图像是什么这两个字节都为0,因此最后计算结果还要加上2字节。图像大小biSizeImage+字节偏移量bfOffBits=文件大小bfSize。左图的图像大小经过计算为921,602(E1002H)字节,右图图像大小为4,915,202(4B0002H)字节。 - 0x26~0x29,水平分辨率。两幅图分辨率都是计算机上常见的72dpi,也就是72像素/英寸=28.346像素/厘米。以像素/米单位表示就是2834(B12H)。后面四个字节的垂直分辨率同理。
2.位深度的各种存储方式
BMP文件的位深度有以下几种:1,2,4,8,16,24,32。其中1,2,4,8位对应索引颜色,图像包含调色板数据。16,24,32位直接保存像素的颜色值,这三种位深度的像素保存方式有多种,见图1。
3.颜色转换公式
二、实验流程及代码分析
1. 主函数的流程
BMP转YUV可由以下几步构成
下面是关键代码分析,代码说明在每段代码的下方。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
BMP文件的结构组成包含在windows.h中,可以使用BITMAPFILEHEADER和BITMAPINFOHEADER两种数据类型方便地对文件头和信息头进行组织,这两种数据类型包含了第一部分中的全部属性。另外还有RGBQUAD类型,用来组织调色板。
首先读取文件头file_header,如果前两字节不是42H和4DH(B字母和M字母的ASCII码)说明不是BMP文件,退出。读完文件头之后文件指针就指向了信息头的起始,这时就可以进行读取信息头info_header的操作。读完两部分数据之后,使用信息头的biWidth和biHeight属性读取图像宽高信息,为下一步创建缓冲区做准备。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
BMP文件先通过BMP2RGB函数转成RGB数据,再通过RGB2YUV函数转成YUV数据。读取到的图像数据是倒序存放的,flip=0保证了RGB2YUV可以正确地对其转换,具体会在后面的分析中说明。下面开始分析BMP2RGB函数的具体细节。
2. BMP2YUV函数的流程
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
在BMP文件组成中说到,BMP文件规定一行的字节数必须是4字节的倍数,如果不是要补充相应的位数使之成为4的倍数。因此实际的字节数可能比像素数×位深度要多。使用上面说到的计算biSizeImage的公式可以得到实际每行有多少字节。由于整个文件最后两字节为保留字节,与获取像素值无关,因此这里w最后没有加2。
得到了实际存储的字节数后就可以开辟数据缓冲区了,使用文件头的字节偏移属性bfOffBits直接把文件指针定位到像素值数据的起始,开始读取数据,一次读取一个字节。
之后就是要根据量化位数的不同进行不同的操作,下面分别对其进行说明。
(1)8位及以下索引文件的读取
对于索引颜色,读取流程如下
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
索引颜色都有调色板,也就是索引值表。因此首先要把调色板保存下来。这里与课件中创建调色板的代码略有不同:
- 1
- 2
- 3
- 1
- 2
- 3
课件中调色板的大小设为了2的biBitCount次方,一个n比特量化的BMP图像调色板的实际颜色数取决于biClrUsed的值。图2展示了这样一个例子。这是一幅30×30像素的图像,图像在右下角,4bit量化,但是整幅图里只有黑白灰三种颜色。
从图2中也可以看出图像的存储方式是由底向上存储的。索引值表中第0个颜色是白色,第1个颜色是灰色,第2个颜色是黑色。0x42中前4位的值为2,说明存储的是黑色。图像左下角像素为黑色,左上角像素为白色,因此扫描行是由底向上存储的。
得到了调色板颜色数目colorCnt之后把文件指针移动到调色板的起始位置,也就是文件头加信息头个字节后,把调色板读取到缓冲区内。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
由于读取文件的缓冲区data是一个字节一个字节读取的,但是只有8位量化的索引值才足够一个字节,1,2,4位的索引值都不足一个字节,也就是一个字节里包含了多个像素的信息。因此在读取像素索引值时需要一个“蒙版”遮住不需要的索引值,读完当前像素值把该蒙版向后移动相应的位数,显示出下一个像素值的信息。“遮住”的具体操作就是与运算,把不需要的位与0,需要的位与1,根据这个规则,计算出蒙版值。
对于1位的图像,一个字节里的第一位为第一个像素的值,因此蒙版值为1000 0000,也就是0x80。2,4,8位的图像同理。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
得到了蒙版,就可以读取索引值了。使用条件运算符判断当前图像是不是8位的图像,如果是,则直接逐字节读取索引值index,while循环结束,继续下一字节的读取。
如果不是8位的图像,需要将当前字节data[i + w*j]与蒙版mask做与运算。与运算结束后,有效位还在高位,比如2位的索引值,这时是××000000,与需要的索引值××(000000××)相比需要向右移动6位,也就是8-biBitCount。读完之后一个字节还有后面其它像素的索引值,因此需要把mask向右移动相应的位数biBitCount,继续while循环,这时读到的索引值为00××0000,需要向右移动4位得到正确的索引值,也就是8-2*biBitCount。因此可以归纳出每次移位的位数是8-shiftCnt*biBitCount。
读取像素信息时行列分别进行循环,是为了方便判断是否达到行结尾处的补充位。pixCnt保存了当前一行里已经读了多少个像素的值,如果达到一行的像素数biWidth,则跳过后面的补充位,进到下一行。仍以图2中30×30像素4位的BMP图像为例,根据biSizeImage的计算公式算出一行实际字节数为16字节,有效位数为30*4=120bit,需要补充8位。实际图像数据从0x42开始,经过120bit(15字节)后到0x50,接下来的8位0x51就是补充的数据位,读取图像索引值时要跳过这样的位,把行循环i置为行结尾w-1,这样就可以进行新一行的循环。
另外在图2中可以看出,补充位不一定都是0,比如0xc1,0x101,0x111处都是补充位,但是这些字节里的某些位不是0。
(2)16位R5 G6 B5格式图像的读取
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
以这种方式保存的图像RGB也不是整字节的值,仍需要用蒙版进行与运算然后移位。16位2个字节中RGB的保存顺序如下
- 将第一个字节data[i + w*j]与蒙版0x1F(0001 1111)与运算得到蓝色值,这时得到的值都在字节的低5位,范围是0~31,所以还要左移3位扩大到0~248,由于后3位都是0,因此B的值以8为单位递增。
- 将第二个字节data[i + w*j + 1]与蒙版0x07(0000 0111)与运算得到绿色的高3位G1,左移5位搬移到整个字节的高位;第一个字节与蒙版0xE0(1110 0000)与运算得到绿色的低3位G2,右移3位接到G1的后面。绿色值为0~252,后2位都是0,以4为单位递增。
- 将第二个字节与蒙版0xF8(1111 1000)与运算得到红色值,此时已经在高5位,范围是0~248,不需要移位,也以8为单位递增。
(3)24位R8 G8 B8格式图像的读取
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
这种方式下RGB各占1字节,因此每次只要顺序读取即可。小端方式保存,保存顺序是B,G,R。读完之后前进3字节读取下一颜色值。
三、实验结果与总结
使用5张不同位深度的BMP图像进行测试,其中4位、8位、24位图各一张,16位图两张。分辨率均为800×540。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
输入图像为一个文件数组,数组大小由命令行参数控制,循环读入各个文件。实现此效果需要对YUV三个分量都进行处理,因此直接把YUV一起读入到缓冲区中,缓冲区大小为Y通道的1.5倍。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
实现方法很简单,但是效率比较低,对每张图的像素值要单独计算得出结果,一次写入一帧画面。由于读取时的顺序为YUV,输出时也是先计算完Y分量的值写入文件,再计算U分量的值写入文件,等等。实验结果如表2所示,其中截取了整个序列中的某些帧