原理篇:
一、编码的意义。
让我们从一个简单的问题开始,-2&-255(中间的操作符表示and的意思)的结果是多少,这个很简单的问题,但是能够写出解答过程的人并不 多。这个看起来和图片格式没有关系的问题恰恰是图片格式的核心内容以至于整个计算机系统的核心内容,多媒体技术虽然没有数据结构,操作系统等计算机基础课 所占的地位重,但是在于研究编码方面有着非常重要的地位。图像其实可以看做一种特殊编码过的文件。
二、从简单的24位bmp开始
bmp是最常见也是编码方式最简单的图片格式,这里不说明一幅图片是怎么显示在电脑上的,那不是多媒体技术研究的问题,我们来研究bmp的格式问题,为了使各位能够最快的了解bmp格式,我们从24位的一个16*16的小图像开始。
我们使用常用的绘图软件创建一个16*16的24位bmp图像,如下图所示:
可以看到图片很小,我们使用ultra-edit看看其内部是什么(ultra-edit是一个比记事本更加高级的编辑软件,可以在网上下载到),我们打开其内部看到的是如下的一个十六进制的数据文件:
看起来很高深而又很凌乱的样子,我们慢慢地说明这些看起来很凌乱的数据流都代表了什么意思,首先我们要说明的是,这里面一个数字代表的是一个字节,比如头两个数42 4d是两个十六进制的数,代表了两个字节。可以看到在UE中一行是十六个字节。
在具体说明每个字节的含义之前,首先需要说明的是字节的排布方式,在操作系统和计算机组成结构里面有大端法和小端法(如果有遗忘可以查一下书),简易的说 法是这样的,小端法的意思是“低地址村存放低位数据,高地址存放高位数据”,大端法就是反过来的,举个例子,如果地址从左到右依次增大,那么数据01 02的小端法存储方式是02 01,大端法的存储方式就是01 02。在所有的intel的机器上都是采用的小端法,而大端法主要存在于摩托罗拉造的处理器的机器上,所以如果你用的是一个果粉,用的是MAC的话,那么 你看到的数据排布方式是和我们说明中是相反的。
下面我们来依次说明每个字节的含义:
字节0-1:42 4d 转换成ASCII码就是BM (如果不知道什么是ASCII码可以查阅网上相关资料,在图像中,这种编码是很常见的,所以弄懂一个图像的格式可以学会很多编码的知识),这两个字节表示 的是一种标示,也就是当计算机把这个图片文件加载到内存中时,从第一个字节开始读取,读到头两个字节是BM, 那么计算机就知道了,这个文件是一个bmp图像文件。
字节2-5:注意这里是小端法,虽然我们看到的数据形式是36 03 00 00,但是实际的数值是0x00000336(0x表示十六进制),转换成十进制是822,这四个字节表示的是图像的大小,对比一下我们的图像,可以发现和图像的大小是一样的。
字节6-9:这四个0是保留的,因为最开始给计算机文件等等设定标准的是一帮数学家,所以,从这里的保留字符可以看到数学家独特的严谨,保留翻译成白话文就是暂时没有作用,以后留着扩展。
字节A-D:这四个字节十分重要,这个表示图像数据区的偏移,当你写程序需要找到图像数据区时就需要这个字段的值,在后面我们还可以对其进行验证,这里的值转换成十进制是54。
以上的14个字节被称作bmp文件头,顾名思义,就是介绍bmp文件的基本信息的。
接下的字节:
字节0E-11:这四个字节的十六进制数,转换成十进制就是40,这个的含义表示位图信息的版本,具体有哪些版本,因为和我们的实验无关,我们不进行说明,只要将这个值固定为40就可以了。
字节12-15:10 00 00 00 ,也就是0x10,转换成十进制是16,表示图像的宽度,当然单位是像素。
字节16-19:10 00 00 00 ,也就是0x10,转换成十进制是16,表示图像的高度,当然单位是像素。不过注意,高度有可能是负的,为什么,在下面位图数据部分还会说明。可以看到这两个信息和上图windows系统内显示的也是一致的。
字节1A-1B:永远为1,规定如此。
字节1C-1F:取出来是0x18,十进制24,表示每个像素占24个位,也就是3 Byte,对于24位的位图,明显这是正确的。
字节1E-21:这四个字节是0,表示使用BI_RGB,这个缩写的意思代表的是不压缩。关于压缩在最后的附件部分说明。
字节22-25:这四个字节表示图像大小,也就是图像数据的大小,去掉这些信息头,文件头和后面要说的调色板的大小。这里是0x300,我们这样验证这个 数据的正确性,在字节2-5中我们得到的图像大小为0x336,在A-D中偏移量为0x36,也就是前面的所有辅助信息的量为0x36,两个相减,刚好得 到0x300,也就是图像数据的大小。可以看到和取出来的大小是一致的。
字节26-29:水平分辨率,这个主要是打印用,可以不管,就用这个值就行了,基本上不会变。
字节2A-2D:垂直分辨率,这个主要是打印用,可以不管,就用这个值就行了,基本上不会变。
字节2E-32:这里表示实际使用的颜色索引,因为这是24位真彩色,所以没有调色板,自然也就没有索引,所以是0。
字节32-35:表示重要索引数,因为连索引都没有,更没有重要索引了,于是和上面一个部分的结果时一样的,也是0。
以上的39个字节被称作位图信息头,用来表示和说明位图的信息,注意,是位图的信息而不是文件的信息,打个比方,正常情况下,你拥有两个耳朵,一个鼻子, 两只眼睛,一对眉毛,一个嘴,这都是你作为一个人的特征,你拥有的是黑色头发,这是你作为你的特征。不同的位图的位图信息头一般是不同的,不同的位图,文 件信息头是很有可能相同的。
接下来的是位图数据,因为这是24位真彩色,没有调色板,所以接下来的一定是位图数据,也就是通常说的RGB值,看到这些位图数据的起始位置是36,和前面的偏移量是相同的,这里又有需要注意的地方了。
1. 由于采用的是小端法,所以这里取出来的值实际上是B G R,取第一个三元组(5E FF5E),也就是这个像素的B GR 分别是5E FF 5E, 不过这正巧B 和 R的值是相同的。
2. 我在1里面用的这个像素而不是第一个像素,是因为这并不是我们程序员意义上的第一个像素,程序员的坐标原点是在屏幕上物体的左上角,但是这个第一个像素表 示的是左下角的第一个像素,也就是(5E FF 5E)表示的是这个图像的最左下角的那个像素值。如果你是一个喜欢思考的人,你一定要问为什么是这个样子的呢,表面上看这个无伤大雅的原点位置其实混乱了 我们的思维方式。这个问题的答案可能会让你有点失望,因为最初玩计算机的是数学家,他们制定了很多计算机标准,比如bmp,在数学家的思维方式中中点才是 习惯的坐标原点,于是图像的右下角也就成了坐标原点。那么有没有可能将左上角看做坐标原点呢?这里要说的数学家们的思维绝对不是盖的,他们想出了一个巧妙 的办法,如果高度是负值,那么第一个像素三元组表示的就是第一个像素,为什么请自己想想(最可恨的提示:想想坐标原点其实还是在图像的左下角)。
如此上面就可以组成一个24位的位图,但是有没有结束呢?没有,这让我想到一句话,Everything will be OK inthe end. If it's not OK , it's not the end.这里没有OK,所以也还没有到end。
看似上面的说明已经能够完成一幅24位bmp的所有部分,但是下面要说的这个知识是涉及到的学科又是计算机组成结构和操作系统(所以学以致用非常重要),这个名词在你以后很多优化策略中都是有很大的作用的,请记住这个名词:数据对齐。
首先,要说明的是,对于windows默认的扫描的最小单位是4字节,也就是说一次性读4字节,那么如果数据对齐是满足这个条件的话,对数据的获取 速度等等都是大有好处的,这个参见操作系统里面磁盘,内存调度那一块的内容,然后,自然windows的文件如果能够尽量满足这个要求对文件的读取速度是 大大的提高,所以bmp也满足了这个特性。那么,具体是怎么做的呢?
在我们这个例子中不存在这个情况,因为,在我们的例子中,宽度是16,一行16个像素,一个像素由3个字节表示,那么一行就是48个字节,48对于4取模 是0,也就是说天然满足数据对齐的要求。如果我们是15*15,那么一行的字节数就变成了45,这个就不满足数据对齐的要求了,那么,我们就在字节的最后 补充0,离45最近而又比45大的4的倍数是48,所以我们在一行数据的末尾补充3个字节的0。
如果你觉得上面的还是很抽象,那么我举一个极端的例子,假设图像的大小是1*2(24位),图像数据区的组成为20,20,20,30,30,30,当 然,在实际的bmp中没有逗号,可以看到第一行是2020 20 ,不是4的倍数,那么我们补充一个字节的0,将第一行扩充为20 20 20 00, 同理,第三行是30 30 30 00,重新组成的数据区为20 20 20 00 30 30 30 00,计算机取一行数据的时候正好取得的是4的倍数,在这个例子中是2020 20 00,那么喜欢想问题的又会想,那计算机怎么识别多少个0是添加上去的呢?很简单,在前面的位图信息头中,我们有图像的宽度。
说了那么多,很自然的发现,如果加入了数据对齐,那么位图数据区的大小就未必是 图片宽 ×每像素字节数×图片高 能表示的了,因为加入了填充数据。我们可以根据下面的这个公式进行计算一行的字节数:
bpp表示每像素比特数,在24位bmp位图中就是24。
在程序中可以表示为:
int iLineByteCnt =(((m_iImageWidth * m_iBitsPerPixel) + 31) >> 5) << 2;
这些个公式都是的推导并不困难,如果有兴趣,你可以自己验证一下。
有一行的实际比特数,那么就能得到图像的真 数据区大小(也就是去掉填充比特的):
m_iImageDataSize = iLineByteCnt * m_iImageHeight;
只要有填充比特,那么扫描一行之后得到的一定不是下一行的数据,跳过填充数据的数量如下所示:
skip = 4 - ((m_iImageWidth * m_iBitsPerPixel)>>3) & 3;
如上所示,就是一个24位真彩色图像的构成方式,理论上看完这些,你就应该能自己组织数据构成一个bmp了。当然,也许只是理论上,实际上如果你还没明白的话,我会在操作篇进行演示。
三、如果32位,怎么办?
上面说了24位真彩色数据,是没有调色板的,那么如果是同样没有调色板的32位多了一个比特的什么呢?多的这一个被称作alpha,这个值表示的是透明 度,那么在数据区就是一个四元组表示的一个像素,排布方式是B G R A,当然相应位图信息头中有些数据的内容需要变化。如果想加深对bmp构成的了解,那么重新组织一下32位的数据信息头是个很好的做法。
四、调色板不只是目录
调色板可以理解为一种索引,但又不仅仅是索引的作用,如果采用调色板的图像那么就可以进行压缩,我们可以把调色板想象为一种数组,每个元素4字节大,下面,还是用一个具体的例子进行说明:
我们创建一个16*16的16色位图,数据少,好分析,用UE打开,显示的数据如下图所示:
首先,直观上,我们可以看到,数据量比24位位图要少很多,我们来和上面一样一个一个说明:
字节0-1:和24位的相同
字节2-5:除了数据不同,表示的意义完全相同,表示的是图像文件的大小
字节6-9:同样是保留
字节a-d: 一样的数据偏移,数据区开始的地方
字节0e-11:暂时认为一定为28
字节12-15:表示图像的宽度16
字节16-19:表示图像的高度16
字节1A-1B:永远为1,规定如此。
字节1C-1F:和上面一样的意义,只不过这里是一个像素由4位表示
字节1E-21:同样表示不压缩
字节22-25:图像数据大小
字节26-29:水平分辨率
字节2A-2D:垂直分辨率
字节2E-32:实际使用的颜色索引,注意,这里的0表示使用全部索引
字节32-35:重要索引数
我想关于含有调色板的信息头最首先想到的一个问题就是最后两个部分,字节2E-32和字节32-35,为什么需要这两个值,是因为在早期的计算机中,显卡 相对比较落后,不一定能保证显示所有颜色,所以在调色板中的颜色数据应尽可能将图像中主要的颜色按顺序排列在前面,字节32-35就表示了这个数值,然后 2E-32在如果这幅图像没有用到所有调色板项的时候会很有用处。但是在绝大数情况下都会是0的。
在有调色板的图像中,接下来的就是调色板项了,这是16位的位图,那么我们有15个调色板项,我们把它截取出来以便于特殊说明:
如果在寻找哪些是调色板项的时候你还是一个一个的数的话,那么我建议你返回前面再看一遍,绝对比你数要节省时间。这是这个图像的调色板,四个四个一组,我们发现正好16组。
我们取出前三个调色板项,(00 00 00 00),(00 00 80 00),(00 80 00 00),一样排布方式是B G R A 形式的,这三个值得索引依次为0 1 2,我们取得数据区“第一个”像素值,FF,记住我们用的是16位的图像,用4个比特位表示一个像素,那么这个FF实际上表示的是两个像素(实际上是索 引),这两个索引都是F,F 也就是最后一个调色板项,我们可以从图中得到是(FF FF FF 00),就是白色,依次类推,可以通过查询调色板的方式查找到实际的像素值。
只要是调色板的方式都是按照上面的形式组织的,只是调色板的大小和数据区表示像素的位数不一样而已。
五、辅助的补充
首先,我们要说明的是我们一直说的暂时认定是0x28的那个数值,也就是字节0e-11,这个字节是因为一些历史原因表示bmp版本的,由于现在很难用到其他几种系统的电脑,所以一般都是0x28,其意义如下:
28h - Windows 3.1x, 95, NT, …
0Ch - OS/21.x
F0h - OS/2 2.x
然后,我们要说下字节1E-21表示的压缩为问题,在bmp中这个数值可以有一些几种表示:
0 - 不压缩 (使用BI_RGB表示)
1 - RLE 8-使用8位RLE压缩方式(用BI_RLE8表示)
2 - RLE 4-使用4位RLE压缩方式(用BI_RLE4表示)
3 - Bitfields-位域存放方式(用BI_BITFIELDS表示)
不压缩就不用说明了吧,先来说明一下1和2,RLE表示的是行程编码,后缀 8 和4 表示的是4位的压缩还是8位的压缩,行程编码是最简单最常见的压缩方法。
我想先进行说明的是为什么要压缩,如果你是一个有一定经验的程序员,优化是一个初级的高等程序问题,比如说你在一个4bpp的图像中一行其实只有两种颜 色,前面100个像素全是白色,后面100个像素全是黑色,那么按照前面说的如果在不压缩的情况下,需要50个字节存储前面的白色,50个字节存储后面的 黑色。但是这是没有必要的,为了避免在出现这种情况,制定bmp的数学家们使用了无损压缩里面最基本的行程编码。基本思想是将一段数据编码成为两个部分 (也可以说是两个字节),第一个字节存储重复色彩的数量,第二个字节存储重复色彩的值,比如前面的100个像素是白色,100个像素是黑色转换为RLE- 4编码就是(100 白色像素的索引 100 黑色像素的索引 0),最后加一个0表示行程编码的结束,因为一个长度为0是没有意义的。这样100个字节就变成了5个字节。RLE-8和RLE-4类似,只是8bpp和 4bpp的差别。
对于BitField不能说是一种压缩方式,更是一种表示方法,在嵌入式系统中比较常见,如果有兴趣可以自己查询资料,在这里我们用不到,就不在叙述了。
也许你看完上面的介绍之后处于跃跃欲试的状态,当然也有可能处于一头雾水和我不感兴趣这两种状态,不论是那种状态,动手试一试能够获得成就感往往会使你确定自己到底对某件事物感不感兴趣。
为了演示最基本的和编码底层相关的原理,我们首选是CC++,MFC中有现成的操纵bitmap的结构和类,但是这里我们从0开始先自己构造出这个类。