BMP文件解析
一、简介
BMP(Bitmap-File)图形文件是Windows采用的图形文件格式,在Windows环境下运行的所有图象处理软件都支持BMP图像文件格式。Windows系统内部各图像绘制操作都是以BMP为基础的。Windows 3.0以前的BMP图文件格式与显示设备有关,因此把这种BMP图像文件格式称为设备相关位图DDB(device-dependent bitmap)文件格式。Windows 3.0以后的BMP图象文件与显示设备无关,因此把这种BMP图像文件格式称为设备无关位图DIB(device-independent bitmap)格式(注:Windows 3.0以后,在系统中仍然存在DDB位图,像BitBlt()这种函数就是基于DDB位图的,只不过如果你想将图像以BMP格式保存到磁盘文件中时,微软极力推荐你以DIB格式保存),目的是为了让Windows能够在任何类型的显示设备上显示所存储的图象。BMP位图文件默认的文件扩展名是BMP或者bmp(有时它也会以.DIB或.RLE作扩展名)。
Bmp文件是非常常用的位图文件。针对bmp文件的处理也有一堆现成的API进行调用,然而文件内部究竟怎样,如何自己来解析这样的文件呢?在解析格式之前,将WINDOWS编程中使用的类型名解释一下。
typedef unsigned short WORD;
typedef unsigned char BYTE;
typedef unsigned long DWORD;
(注:这里用到了无符号类型,在进行运算时注意数据类型的匹配与转换,否则将出现奇怪的结果。)
二、数据格式解析
第一块是bmp的文件头用于描述整个bmp文件的情况。结构如下:
typedef struct tagBITMAPFILEHEADER {
WORD bfType; //指定文件类型
DWORD bfSize; //指定文件大小
WORD bfReserved1; //为保留字,不用考虑
WORD bfReserved2; //为保留字,不用考虑
DWORD bfOffBits; //图像开始处的字节偏移
} BITMAPFILEHEADER, *PBITMAPFILEHEADER;
这些信息相当有用,如果你想直接来解析bmp文件。第一个bfType用于表示文件类型,如果它是bmp文件,那么它这个位置的值一定是
‘BM’ :Windows 3.1x, 95, NT,
‘BA’ :OS/2 Bitmap Array
‘CI’ :OS/2 Color Icon
‘CP’ :OS/2 Color Pointer
‘IC’ :OS/2 Icon
‘PT’ :OS/2 Pointer
(注:因为OS/2系统并没有被普及开,所以在编程时,你只需判断第一个标识“BM”就行。)第二个bfSize表示整个文件的字节数。第三第四个则保留,目前无意义。最后一个相当重要,表示,位图的数据信息离文件头的偏移量,以字节为单位,通常是前三个部分的长度之和。信息头的长度通常是14B(注:在Windows95、98、2000等操作系统中,位图信息头的长度并不一定是14B,因为微软已经制定出了新的BMP文件格式,其中的信息头结构变化比较大,长度加长。所以最好不要直接使用常数14B,而是应该从具体的文件中读取这个值。这样才能确保程序的兼容性。)如果信息头的长度是14B的话,这个偏移量就是54B+调色板的长度或者颜色掩码的长度。
第二块是位图信息头,即BITMAPINFOHEADER,用于描述整个位图文件的情况。
typedef struct tagBITMAPINFOHEADER{
DWORD biSize; //表示本结构的大小,40B
LONG biWidth; //位图的宽度
LONG biHeight; //位图的高度
WORD biPlanes; //位面数永远为1参见msdn解释
WORD biBitCount;//位图的位数 分为1 4 8 16 24 32
DWORD biCompression; //本以为压缩类型,但是却另外有作用,稍候解释
DWORD biSizeImage; //表示位图数据区域的大小以字节为单位
LONG biXPelsPerMeter; //表示显示设备的水平分辨率
LONG biYPelsPerMeter; //表示显示设备的垂直分辨率
DWORD biClrUsed; //实际使用的颜色数目,通常为0,表示全部使用
DWORD biClrImportant; //重要的颜色数量,通常为0,表示全部重要
} BITMAPINFOHEADER, *PBITMAPINFOHEADER;
biBitCount可以取下列值:
1 – 双色位图(缺省情况下是黑色和白色。你可以自己定义这两种颜色)
4 - 16 色位图
8 - 256 色位图
16 - 16bit 高彩色位图,(用16位表示三种颜色,有555,565两种方式)
24 - 24bit 真彩色位图
32 - 32bit 增强型真彩色位图
biCompression可以取下列值:
0 - 不压缩 (使用BI_RGB表示)
1 - RLE 8-使用8位RLE压缩方式(用BI_RLE8表示)
2 - RLE 4-使用4位RLE压缩方式(用BI_RLE4表示)
3 - Bitfields-位域存放方式(用BI_BITFIELDS表示)
第三块就是调色板或者掩码部分。如果是1、4、8则存放调色板;24与32位位图则存放RGB颜色的掩码,有3个,这些掩码用DWORD大小来存放。16位有存放调色版的也有存放掩码的。调色板的结构如下:
typedef struct tagRGBQUAD {
BYTE rgbBlue; //蓝色分量
BYTE rgbGreen; //绿色分量
BYTE rgbRed; //红色分量
BYTE rgbReserved; //保留值,通常为0
} RGBQUAD;
对于有调色板位图,调色板数量的计算公式是: (pow(2,bmpInfoH.biBitCount)) 。但是如果biClrUsed不是0,调色板数目就是biClrUsed。
typedef struct bi_bitField{
DWORD redCode;
DWORD greenCode
DWORD blueCode;
} bitField;
bitField即颜色掩码其实没有什么用处。
第四块就是位图的数据实体。对与有调色板的位图,表示颜色及亮度的位图数据实体存放的实际是本位置应有的颜色值在调色板数组中的位置,即索引值。位图像素区是按行存放的,每行从左到右与显示出来的图像对应;但是上下确是倒置的,即显示在上边的像素存放在后边。值得注意的一个问题是每行存放的字节数是4的整数倍。那么图像每行颜色与亮度信息存放的字节数storewidth是大于等于biWidth* biBitCount/8的最小4的整数倍。如果计算有误读入或者写入的图像将会是错位的。现在给出一个通用的公式: 4*((biWidth*biBitCoun+31)/32);
有了上面的知识,现在给出一c++中读入位图的函数:
为了方便给出符合c++中结构体特性的位图五种结构的定义;
typedef struct bi_bitField
{
DWORD redCode;
DWORD greenCode;
DWORD blueCode;
const struct bi_bitField & operator = (const struct bi_bitField & F)
{
if (&F != this)
{
redCode = F.redCode;
greenCode = F.greenCode;
blueCode = F.blueCode;
}
return *this;
}
friend std::istream & operator>>(std::istream & is,struct bi_bitField & F)
{
is >> F.redCode >> F.greenCode >> F.blueCode ;
return is;
}
friend std::ostream & operator<<(std::ostream & os,const struct bi_bitField & F)
{
os << F.redCode << F.greenCode << F.blueCode ;
return os;
}
}bitField;
//RGB颜色结构
typedef bitField ColorRGB;
//位图文件头
typedef struct tagBmpFile
{
WORD bfType;
DWORD bfSize;
WORD bfReserved1;
WORD bfReserved2;
DWORD bfOffBits;
const struct tagBmpFile & operator = (const struct tagBmpFile & F)
{
if (&F != this)
{
bfType = F.bfType;
bfSize = F.bfSize;
bfReserved1 = F.bfReserved1;
bfReserved2 = F.bfReserved2;
bfOffBits = F.bfOffBits;
}
return *this;
}
friend std::istream & operator>>(std::istream & is,struct tagBmpFile & F)
{
is >> F.bfType >> F.bfSize >> F.bfReserved1 >> F.bfReserved2 >> F.bfOffBits;
return is;
}
friend std::ostream & operator<<(std::ostream & os,const struct tagBmpFile & F)
{
os << F.bfType << F.bfSize << F.bfReserved1 << F.bfReserved2 << F.bfOffBits;
return os;
}
}bmpFile;
//位图信息头
typedef struct tagBmpInfo
{
DWORD biSize;
DWORD biWidth;
DWORD biHeight;
WORD biPlanes;
WORD biBitCount;
DWORD biCompression;
DWORD biSizeImage;
DWORD biXPelsPerMeter;
DWORD biYPelsPerMeter;
DWORD biClrUsed;
DWORD biClrImportant;
const struct tagBmpInfo & operator = (const struct tagBmpInfo & I)
{
if (&I != this)
{
biSize = I.biSize;
biWidth = I.biWidth;
biHeight = I.biHeight;
biPlanes = I.biPlanes;
biBitCount = I.biBitCount;
biCompression = I.biCompression;
biSizeImage = I.biSizeImage;
biXPelsPerMeter = I.biXPelsPerMeter;
biYPelsPerMeter = I.biYPelsPerMeter;
biClrUsed = I.biClrUsed;
biClrImportant = I.biClrImportant;
}
return *this;
}
friend std::istream & operator>>(std::istream & is,struct tagBmpInfo & F)
{
is >> F.biSize
>> F.biWidth
>> F.biHeight
>> F.biPlanes
>> F.biBitCount
>> F.biCompression
>> F.biSizeImage
>> F.biXPelsPerMeter
>> F.biYPelsPerMeter
>> F.biClrUsed
>> F.biClrImportant;
return is;
}
friend std::ostream & operator<<(std::ostream & os,const struct tagBmpInfo & F)
{
os << F.biSize
<< F.biWidth
<< F.biHeight
<< F.biPlanes
<< F.biBitCount
<< F.biCompression
<< F.biSizeImage
<< F.biXPelsPerMeter
<< F.biYPelsPerMeter
<< F.biClrUsed
<< F.biClrImportant;
return os;
}
}bmpInfo;
//调色板
typedef struct tagRGBQuad
{
BYTE rgbBlue;
BYTE rgbGreen;
BYTE rgbRed;
BYTE rgbReserved;
const struct tagRGBQuad & operator = (const struct tagRGBQuad & Q)
{
if (&Q != this)
{
rgbBlue = Q.rgbBlue;
rgbGreen = Q.rgbGreen;
rgbRed = Q.rgbRed;
rgbReserved = Q.rgbReserved;
}
return *this;
}
friend std::istream & operator>>(std::istream & is,struct tagRGBQuad & F)
{
is >> F.rgbBlue
>> F.rgbGreen
>> F.rgbRed
>> F.rgbReserved;
return is;
}
friend std::ostream & operator<<(std::ostream & os,const struct tagRGBQuad & F)
{
os << F.rgbBlue
<< F.rgbGreen
<< F.rgbRed
<< F.rgbReserved;
return os;
}
} RGBQuad;
bool openBmp(const std::string fname)
std::string fileName;
bmpFile bmpFileH;
bmpInfo bmpInfoH;
DWORD storeWidth;
bitField bmpBitField;
RGBQuad * bmpRGB;
BYTE * bmpData;
{
FILE * inbmpF;
WORD colorCount;
if ((inbmpF=fopen(fname.c_str(),"rb"))==NULL)
return false;
else
{
fileName.assign(fname);
fread(&bmpFileH.bfType,2,1,inbmpF);
fread(&bmpFileH.bfSize,4,1,inbmpF);
fread(&bmpFileH.bfReserved1,2,1,inbmpF);
fread(&bmpFileH.bfReserved2,2,1,inbmpF);
fread(&bmpFileH.bfOffBits,4,1,inbmpF);
//fread(&bmpFileH,14,1,inbmpF);
fread(&bmpInfoH,40,1,inbmpF);
if (0<bmpInfoH.biBitCount&&bmpInfoH.biBitCount<=8)
colorCount=(WORD)(pow(2,bmpInfoH.biBitCount));
else
colorCount=0;
storeWidth=4*((bmpInfoH.biWidth*bmpInfoH.biBitCount+31)/32);
bmpFileH.bfSize=54+4*colorCount+storeWidth*bmpInfoH.biHeight;
if ( 0 != colorCount )
{
bmpRGB = new RGBQuad[colorCount];
fread(bmpRGB,4,colorCount,inbmpF);
}
else
{
fread(&bmpBitField,12,1,inbmpF);
}
bmpInfoH.biSizeImage = storeWidth*bmpInfoH.biHeight;
bmpData=new BYTE[bmpInfoH.biSizeImage];
fseek(inbmpF,0-bmpInfoH.biSizeImage,SEEK_END);
fread(bmpData,bmpInfoH.biSizeImage,1,inbmpF);
fclose(inbmpF);
return true;
}
}
三、颜色及亮度信息解析
biBitCount=1
表示位图最多有两种颜色,缺省情况下是黑色和白色,你也可以自己定义这两种颜色。图像信息头装调色板中将有两个调色板项,称为索引0和索引1。图象数据阵列中的每一位表示一个象素。如果一个位是0,显示时就使用索引0的RGB值,如果位是1,则使用索引1的RGB值。
biBitCount=4
表示位图最多有2^4=16种颜色。每个象素用4位表示,并用这4位作为彩色表的索引来查找该象素的颜色。例如,如果位图中的第一个字节为0x1A,它表示两个象素,第一象素的颜色就在彩色表的第2项中,而第二个象素的颜色就在彩色表的第11项中。调色板中缺省情况下会有16个RGB项,对应于索引0到索引15。
biBitCount=8
表示位图最多有2^8种颜色。每个象素用8位表示,并用这8位作为彩色表的索引来查找该象素的颜色。例如,如果位图中的第一个字节为0x1A,这个象素的颜色就在彩色表的第26项中。此时,缺省情况下,调色板中会有256个RGB,对应于索引0到索引255。
biBitCount=16
表示位图最多有2^16种颜色。每个像素用16位(2个字节)表示。这种格式叫作高彩色,或叫增强型16位色,或64K色。它的情况比较复杂,当biCompression成员的值是BI_RGB时,它没有调色板。16位中,最低的5位表示蓝色分量,中间的5位表示绿色分量,高的5位表示红色分量,一共占用了15位,最高的一位保留,设为0。这种格式也被称作555 16位位图。
如果biCompression成员的值是BI_BITFIELDS,那么情况就复杂了,首先是原来调色板的位置被三个DWORD变量占据,称为红、绿、蓝掩码。分别用于描述红、绿、蓝分量在16位中所占的位置。在Windows 95(或98)中,系统可接受两种格式的位域:555和565,在555格式下,红、绿、蓝的掩码分别是:0x7C00、0x03E0、0x001F,而在565格式下,它们则分别为:0xF800、0x07E0、0x001F。你在读取一个像素之后,可以分别用掩码“与”上像素值,从而提取出想要的颜色分量(当然还要再经过适当的左右移操作)。在NT系统中,则没有格式限制,只不过要求掩码之间不能有重叠。(注:这种格式的图像使用起来是比较麻烦的,不过因为它的显示效果接近于真彩,而图像数据又比真彩图像小的多,所以,它更多的被用于游戏软件)。我们只需要读取其中的R或者G的掩码,来判断是那种格式。以红色掩码为例0111110000000000的时候就是555格式 1111100000000000就是565格式。
555 格式 xrrrrrgggggbbbbb
565 格式 rrrrrggggggbbbbb
解析555格式的代码:
BYTE b= bmpData[i*storeWidth+j]&0x1F;
BYTE g=((bmpData[i*storeWidth+j +1]<<6)>>3)+(buffer[i*storeWidth+j]>>5);
BYTE r=(bmpData[i*storeWidth+j +1]<<1)>>3;
有一点值得提醒的是由于有较多的位操作 ,所以在处理的时候在前一次操作的上面加上一对括号。
现在我们得到了55RGB各自的分量,但是还有一个新的问题,那就是由于两字节表示了3个颜色 555下每个颜色最多到0x1F。所以我们需要一个转换,很简单将得到的各颜色分量乘8就可以了。
以下是565格式时的数据分离:
BYTE b= bmpData[i*storeWidth+j]&0x1F;
BYTE g=((bmpData[i*storeWidth+j+1]<<5)>>2)+(buffer[i*storeWidth+j]>>5);
BYTE r=bmpData[i*storeWidth+j +1]>>3;
现在我们得到了565RGB各自的分量,但是仍然还有一个新的问题,565格式下最大的绿色分量也就0x3F。所以我们需要一个转换,很简单将得到的绿色分量乘以4,其余乘8就可以了。
biBitCount=24
表示位图最多有2^24种颜色。这种位图没有调色板。每3个字节代表一个象素,分别对应于颜色B、G、R。
biBitCount=32
表示位图最多有2^32种颜色。这种位图的结构与16位位图结构非常类似,当biCompression成员的值是BI_RGB时,它也没有调色板,32位中有24位用于存放RGB值,顺序是:最前一字节保留,红8位、绿8位、蓝8位。这种格式也被成为888 32位图。如果biCompression成员的值是BI_BITFIELDS时,原来调色板的位置将被三个DWORD变量占据,成为红、绿、蓝掩码,分别用于描述红、绿、蓝分量在32位中所占的位置。在Windows 95(or98)中,系统只接受888格式,也就是说三个掩码的值将只能是:0xFF0000、0xFF00、0xFF。而在NT系统中,你只要注意使掩码之间不产生重叠就行。
在次,给出灰度与RGB颜色的关系公式:gray = 0.3*r+0.6*g+0.1*b。有了这些的知识,现在给出用上述c++位图读入函数读入的位图的灰度数组转换函数;
//------------------------------------------------
//将一幅图片的像素区,转换为灰度值数组
//------------------------------------------------
template<typename T>
bool tmBmp::tranToArray(T * pA)
{
DWORD i,j;
WORD k,index=0,pixelPerB=0;
if (bmpInfoH.biBitCount==1)
{
pixelPerB=8;
for (i=0;i<bmpInfoH.biHeight;i++)
for (j=0;j<storeWidth;j++ )
{
k=0;
while (k<=7)
{
index=(grasp(i,j)>>(7-k))&1;
fetch(pA,bmpInfoH.biWidth,i,j*pixelPerB+k)=
(T)(0.3*bmpRGB[index].rgbRed
+ 0.6*bmpRGB[index].rgbGreen
+ 0.1*bmpRGB[index].rgbBlue);
k++;
}
}
}
else if (bmpInfoH.biBitCount==4)
{
pixelPerB=2;
for (i=0;i<bmpInfoH.biHeight;i++)
for (j=0;j<storeWidth;j++)
{
k=0;
while (k<=1&&(j*pixelPerB+k)<bmpInfoH.biWidth)
{
index=(grasp(i,j)>>(1-k)*4)&15;
fetch(pA,bmpInfoH.biWidth,i,j*pixelPerB+k) =
(T)(0.3*bmpRGB[index].rgbRed
+ 0.6*bmpRGB[index].rgbGreen
+ 0.1*bmpRGB[index].rgbBlue);
k++;
}
}
}
else if (bmpInfoH.biBitCount==8)
{
for (i=0;i<bmpInfoH.biHeight;i++)
for (j=0;j<storeWidth;j++)
{
index=grasp(i,j);
fetch(pA,bmpInfoH.biWidth,i,j)=
(T)(0.3*bmpRGB[index].rgbRed
+ 0.6*bmpRGB[index].rgbGreen
+ 0.1*bmpRGB[index].rgbBlue);
}
}
else if ( (bmpInfoH.biBitCount==16) && (BI_RGB == bmpInfoH.biCompression))
{
BYTE r,g,b;
for (i = 0; i < bmpInfoH.biHeight; i++)
for (j = 0, k = 0; j < bmpInfoH.biWidth; j++, k += 2)
{
b = (grasp(i,k)&0x1F)*8;
g = (grasp(i,k)>>5)*8 + ((grasp(i,k+1)<<5)>>2)*8;
r = (grasp(i,k+1)>>3)*8;
fetch(pA,bmpInfoH.biWidth,i,j) = (T)(0.3*r + 0.6*g + 0.1*b);
}
}
else if (bmpInfoH.biBitCount==24)
{
for (i = 0; i < bmpInfoH.biHeight; i++)
for (j = 0, k = 0; j < bmpInfoH.biWidth; j++ , k += 3)
{
fetch(pA,bmpInfoH.biWidth,i,j) =
(T)( 0.1*grasp(i,k) + 0.6*grasp(i,k+1) + 0.3*grasp(i,k+2) );
}
}
else if ( (bmpInfoH.biBitCount==32) && (BI_RGB == bmpInfoH.biCompression))
{
for (i = 0; i < bmpInfoH.biHeight; i++)
for (j = 0, k = 0; j < bmpInfoH.biWidth; j++, k += 4)
{
fetch(pA,bmpInfoH.biWidth,i,j)=
(T)( 0.1*grasp(i,k+1) + 0.6*grasp(i,k+2) + 0.3*grasp(i,k+3));
}
}
else return false;
return true;
}
四、压缩编码解析
BI_RLE8(8位位图的压缩)
在这种情况下BITMAPINFOHEADER结构中的biCompression设置为BI_RLE8,.使用256色位图行程编码格式将位图进行压缩。这种压缩方式包括绝对方式和编码方式。
编码方式:
在此方式下每两个字节组成一个信息单元。第一个字节给出其后面相连的象素的个数。第二个字节给出这些象素使用的颜色索引表中的索引。例如:信息单元03 04,03表示其后的象素个数是3个,04表示这些象素使用的是颜色索引表中的第五项的值。压缩数据展开后就是04 04 04 .同理04 05 可以展开为05 05 05 05.
信息单元的第一个字节也可以是00,这种情况下信息单元并不表示数据单元,而是表示一些特殊的含义。这些含义通常由信息单元的第二个字节的值来描述。这些值在0x00到0x02之间。具体含义如下:00 本行结束 ;01 位图结束 ;02 象素位置增量,表示紧跟在这个字节后面的信息单元里的两个字节中所包含的无符号值指定了下个象素相对于当前象素的水平和垂直偏移量(相对方式的意义体现在此)。例如:00 02 06 08表示的含义是下一个象素的位值是从当前位置向右移动5个象素,向下移动8个象素(不是字节)。
绝对方式:
绝对方式的标志是第一个字节是0,第二个字节是0x03到0xff之间的值。第二个字节的值表示跟随其后面的象素的字节数目。每个字节都包含一个象素的颜色索引。 每个行程编码都必须补齐到字的边界。
BI_RLE4(4位位图压缩)
这是BITMAPINFOHEADER的biCompression设置为BI_RLE4,使用16位行程编码格式进行位图压缩。压缩方式也包括编码方式和绝对方式。
编码方式:
4位压缩的编码方式跟8位的编码的压缩方式没有什么区别。每个信息单元也是由两个字节表示,第一个字节表示其后面所跟随的象素的个数。第二个字节表示象素在颜色索引表中的索引。这个字节又分为上下两个部分。第一个象素用上半部分指定的颜色表中的颜色画出。第二个象素用下半部分的颜色画出。第三个象素用下一个字节 的上半部分画出,依次类推。它只能压缩的颜色数不超过16的图像。因而这种压缩应用范围有限。
其余的跟BI_RLE8一样。