typedef struct tagRGBQUAD { BYTE rgbBlue; BYTE rgbGreen; BYTE rgbRed; BYTE rgbReserved;} RGBQUAD;
调色板中的数据定义为:
字 段 名 | 大小(单位:字节) | 描 述 |
rgbBlue | 1 | 蓝色值 |
rgbGreen | 1 | 绿色值 |
rgbRed | 1 | 红色值 |
rgbReserved | 1 | 保留,总为0 |
在24位以及24以上的真彩色图像中,没有调色板,信息头后面直接跟着的就是位图的像素数据。
如果图像是单色、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图片结构(尤其是两个头文件)及其存储原理的理解。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!