代码改变世界

我的第一个MFC小项目(3)之 位图转换

2011-12-08 16:20  捣乱小子  阅读(3479)  评论(2编辑  收藏  举报

关于位图的格式,文件头,信息头,颜色表,像素位等等,我在以前在《我的第一个MFC小项目(2)》有过简单的介绍,当时还操VISIO自己画图呢,不过当时真的非常非常的模糊,甚至还没有灰度图和彩图的概念。没有捧着一本书认真研究,纯粹是完成项目过程当中遇到什么不懂的就直接google...欢迎拍砖,欢迎讨论。

 

8位位图除了可以索引彩色图像外,也可以是灰阶图像,相信更多的是用于灰度的图像,既然有8位的灰阶图像,也就是说从白到黑分成256种渐变,那16位灰阶图像也是存在的,只不过从白到黑分成2^16种渐变;但这是一种很大的浪费我觉得,因为灰阶图像应用不是非常广,在一些专业领域或许会用到。

 

而如今PC下的更多的是24位的,32位的,16位的也有。一开始的时候还不知道什么是RGB,其实简单来说就是Red,Green,Blue分别用一定的位数来存储他们的值。16位以上多见RGB格式的图,16位位图图片还可分为R5G6B5,R5G5B5X1(有1位不携带信息,其实就是最高位),R5G5B5A1,R4G4B4A4 等等。

其中RGB555,BITMAPINFOHEADER信息头字段biCompression成员的值是BI_RGB,它的存储格式是:

XRRRRRGG GGGBBBBB,注意它跟24位位图没有颜色表。

24位位图更简单,它的存储格式是:

RRRRRRRR GGGGGGGG BBBBBBBB

而32位的位图,新增了一个透明度,这在XP下很常见了:

AAAAAAAA RRRRRRRR GGGGGGGG BBBBBBBB

 

图形处理中,通常把RGB三种颜色信息称为红通道、绿通道和蓝通道,相应的把透明度称为Alpha通道。由RGB形成的图像均称做真彩色

                                                                                                                                                                                                                                        

OK,差不多就啰嗦到这里吧。真正在写程序中遇到的问题是位图的转换:将24位真彩位图转换为8位灰阶位图。而没有上面的东西,下面的东西看起来会很吃力(还包括《我的第一个MFC小项目(2)》)。

 

问题描述的清楚一点吧:原图片是24位位图,而如今项目当中的绝多数操作都是基于8位位图,不可能让客户多一手准备8位的位图,自然而然转换的任务就落在了程序的头上了。

 

我的解决方法是这样:就用一个方法来实现这个转换的操作,而函数的参数就一个路径(让客户只提供路径,这方便很多了),在转换之后再写为另一个8位位图文件。

 

或许一开始拿到这样的任务,还是摸不着头脑,不知道从何下手;我画了下面的一张图,

image

 

Y是什么,Y就是明度,在灰阶位图当中,只有明度(灰阶值),而没有色度和浓度,它和RGB是有区别的,是两种不同的颜色编码方法,RGB注重的是色彩,而YUV注重的亮度。幸运的是,两者之间可以进行转换,

 


\begin{array}{rll}
Y &= 0.299 * R + 0.587 * G + 0.114 * B \\
U &= 0.436 * (B - Y) / (1 - 0.114) \\
V &= 0.615 * (R - Y) / (1 - 0.299)
\end{array}

 

对,上面的第一个公式正是我们想要的,熟悉DIB格式的童鞋都应该有思路了吧,像素位里面就存着RGB,它与上面的公式能够帮助我们将24位位图转换为8位灰阶位图。

 

其中有个问题,就是涉及到位图的每行所占字节数的问题,在《我的第一个MFC小项目(2)》中也涉及了一点点,所以再次重申一次,Windows规定一个扫描行所占的字节数必须是4的倍数(即以long为单位),不足的以0填充,每行所占字节数的计算公式:((((width*32)+31)/32)*4)。

 

举一个简单的例子:假如一个8位灰阶位图,它的宽是31,则每一行需要31个字节存储,因为字节数必须是4的整倍数,所以应该是32字节,此时BITMAPINFOHEADER字段当中,biWidth=31,biBitCount=8,但要清楚,每行所占字节是32字节。因此在做每行转换为8位位图的时候,我们需要每个扫描行+32,而不是每个扫描行+31。

 

所以下面所展示的程序,看到宽度转换可以回到这里找答案。上代码吧,

 

void Convertto8Bit()
{
	HANDLE hFile;					//文件句柄
	DWORD dwWritten;			//记录以写入的字节数
	hFile   =   CreateFile(L"F:\\1.bmp",GENERIC_READ,
	FILE_SHARE_READ,
	NULL,
	OPEN_EXISTING,
	FILE_FLAG_SEQUENTIAL_SCAN,
	NULL);

	BITMAPFILEHEADER  bmfh;					//文件头
	ReadFile(hFile,&bmfh,sizeof(BITMAPFILEHEADER),&dwWritten,NULL);

	BITMAPINFOHEADER   bmif;					//信息头
	ReadFile(hFile,&bmif,40,&dwWritten,NULL);  

	DWORD   dwSizeImage;						//源文件像素位大小
	dwSizeImage   =									//计算 像素位 的大小
		bmfh.bfSize - 
		sizeof(BITMAPFILEHEADER) - 
		sizeof(BITMAPINFOHEADER);

	BYTE * pBits   =   new BYTE[dwSizeImage];   
	ReadFile(hFile,pBits,dwSizeImage,&dwWritten,NULL);

	::CloseHandle(hFile);

	long  lSrcWidth = bmif.biWidth;					//原图长与宽
	long lSrcHeight = bmif.biHeight;

	long lLineBytes;							//原图每行总字节数
	long lScanWidth;						//转换为8位图之后的宽度,必须是大于原图且为4的倍数

	lLineBytes = ((lSrcWidth*3)/4)*4;			//为了与8位位图数据对齐,原图每行总字节数也必须为4的倍数,
	if(lLineBytes<lSrcWidth*3)					//在这里转换需要比原图每行总字节数大
		lLineBytes += 4;

	lScanWidth = (lSrcWidth/4)*4;			//8位位图的宽度必须为4的倍数,在这里转换需要比原图宽度大
	if(lScanWidth<lSrcWidth)
		lSrcWidth += 4;

	DWORD dwSizeNewImage = lSrcWidth * lSrcHeight + 2;			//为什么要预留两位呢
	BYTE * bits = new BYTE[dwSizeNewImage];

	for(int i=0; i<lSrcHeight; i++)
	{
		for(int j=0; j<lSrcWidth; j++)
		{
			BYTE color[3];						//对应RGB的红绿蓝值
			DWORD dwColorTemp;			//Y值,RGB转换为Y之后的值Y=0.299*R+0.587*G+0.114*B
			for(int s=0;s<3;s++)			//一个RGB对应一个Y值
				color[s]=pBits[i*lLineBytes+j*3+s];

			dwColorTemp=unsigned int(color[2]*0.299+color[1]*0.587+color[0]*0.114);     

			if(dwColorTemp>255)
				dwColorTemp = 255;

			if(dwColorTemp<0)
				dwColorTemp = 0;
			bits[i*lScanWidth+j]=(unsigned char)dwColorTemp;
		}
	}
	bits[dwSizeNewImage-1] = 0;
	bits[dwSizeNewImage-2] = 0;

	RGBQUAD   *rgbQuad   =   new   RGBQUAD[256];   //颜色表
	for(int i=0;i<256;i++)   
	{   
		rgbQuad[i].rgbBlue   =   i;     
		rgbQuad[i].rgbGreen   =   i;     
		rgbQuad[i].rgbRed   =   i;     
		rgbQuad[i].rgbReserved   =   0;     
	}   

	//完善8位位图的文件头和信息头字段

	bmfh.bfOffBits = sizeof(BITMAPFILEHEADER) +
		sizeof(BITMAPINFOHEADER) + 
		256*sizeof(RGBQUAD);				//颜色表有256个

	bmfh.bfReserved1 = 0;
	bmfh.bfReserved2 = 0;
	
	bmfh.bfSize = bmfh.bfOffBits + dwSizeNewImage;			//size in byte of the file

	bmif.biBitCount = 8;															//信息头中bitcounts改为8
	bmif.biSizeImage =  dwSizeNewImage;

	//写入转换后得到的8位位图
	hFile=CreateFile(L"F://2.bmp",GENERIC_WRITE,
		FILE_SHARE_WRITE,NULL,
		CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL);
	::WriteFile(hFile,&bmfh,sizeof(BITMAPFILEHEADER),&dwWritten,NULL);
	::WriteFile(hFile,&bmif,sizeof(BITMAPINFOHEADER),&dwWritten,NULL);
	::WriteFile(hFile,rgbQuad,256*sizeof(RGBQUAD),&dwWritten,NULL);
	::WriteFile(hFile,bits,dwSizeNewImage,&dwWritten,NULL);
	::CloseHandle(hFile);
}

童鞋们,在这里缺了很多的检测,文件大小的检测,如果位图超过4M,就不被允许了,所以一开始应该结合getfilesize来检测,还有读入数据的时候是否已经读入正确的字节数,特别是文件头和信息头;BITMAPFILEHEADER中bftype字段应该为‘BM’;文件句柄的检测等等,因为程序长,我忽略了。一个优秀程序员都不应该忘记这些,很明显菜鸟一个啊....

传送门:

我的第一个MFC小项目(2)之 初涉位图
我的第一个MFC小项目(4)之 位图转换(续)

欢迎讨论,欢迎拍砖...

捣乱小子 2011-12-08