Windows位图和调色板
1.1 位图和调色板的概念
如今Windows(3.x以及95,98,NT)系列已经成为绝大多数用户使用的操作系统,它比DOS成功的一个重要因素是它可视化的漂亮界面。那么Windows是如何显示图象的呢?这就要谈到位图(bitmap)。
我们知道,普通的显示器屏幕是由许许多多点构成的,我们称之为象素。显示时采用扫描的方法:电子枪每次从左到右扫描一行,为每个象素着色,然后从上到下这样扫描若干行,就扫过了一屏。为了防止闪烁,每秒要重复上述过程几十次。例如我们常说的屏幕分辨率为640×480,刷新频率为70Hz,意思是说每行要扫描640个象素,一共有480行,每秒重复扫描屏幕70次。
我们称这种显示器为位映象设备。所谓位映象,就是指一个二维的象素矩阵,而位图就是采用位映象方法显示和存储的图象。举个例子,图1.1是一幅普通的黑白位图,图1.2是被放大后的图,图中每个方格代表了一个象素。我们可以看到:整个骷髅就是由这样一些黑点和白点组成的。
图1.1 骷髅 |
图1.2 放大后的骷髅位图 |
那么,彩色图是怎么回事呢?
我们先来说说三元色RGB概念。
我们知道,自然界中的所有颜色都可以由红、绿、蓝(R,G,B)组合而成。有的颜色含有红色成分多一些,如深红;有的含有红色成分少一些,如浅红。针对含有红色成分的多少,可以分成0到255共256个等级,0级表示不含红色成分;255级表示含有100%的红色成分。同样,绿色和蓝色也被分成256级。这种分级概念称为量化。
这样,根据红、绿、蓝各种不同的组合我们就能表示出256×256×256,约1600万种颜色。这么多颜色对于我们人眼来说已经足够丰富了。
表1.1 常见颜色的RGB组合值
颜色 | R | G | B |
红 | 255 | 0 | 0 |
蓝 | 0 | 255 | 0 |
绿 | 0 | 0 | 255 |
黄 | 255 | 255 | 0 |
紫 | 255 | 0 | 255 |
青 | 0 | 255 | 255 |
白 | 255 | 255 | 255 |
黑 | 0 | 0 | 0 |
灰 | 128 | 128 | 128 |
你大概已经明白了,当一幅图中每个象素赋予不同的RGB值时,能呈现出五彩缤纷的颜色了,这样就形成了彩色图。的确是这样的,但实际上的做法还有些差别。
让我们来看看下面的例子。
有一个长宽各为200个象素,颜色数为16色的彩色图,每一个象素都用R、G、B三个分量表示。因为每个分量有256个级别,要用8位(bit),即一个字节(byte)来表示,所以每个象素需要用3个字节。整个图象要用200×200×3,约120k字节,可不是一个小数目呀!如果我们用下面的方法,就能省的多。
因为是一个16色图,也就是说这幅图中最多只有16种颜色,我们可以用一个表:表中的每一行记录一种颜色的R、G、B值。这样当我们表示一个象素的颜色时,只需要指出该颜色是在第几行,即该颜色在表中的索引值。举个例子,如果表的第0行为255,0,0(红色),那么当某个象素为红色时,只需要标明0即可。
让我们再来计算一下:16种状态可以用4位(bit)表示,所以一个象素要用半个字节。整个图象要用200×200×0.5,约20k字节,再加上表占用的字节为3×16=48字节.整个占用的字节数约为前面的1/6,省很多吧?
这张R、G、B的表,就是我们常说的调色板(Palette),另一种叫法是颜色查找表LUT(Look Up Table),似乎更确切一些。Windows位图中便用到了调色板技术。其实不光是Windows位图,许多图象文件格式如pcx、tif、gif等都用到了。所以很好地掌握调色板的概念是十分有用的。
有一种图,它的颜色数高达256×256×256种,也就是说包含我们上述提到的R、G、B颜色表示方法中所有的颜色,这种图叫做真彩色图(true color)。真彩色图并不是说一幅图包含了所有的颜色,而是说它具有显示所有颜色的能力,即最多可以包含所有的颜色。表示真彩色图时,每个象素直接用R、G、B三个分量字节表示,而不采用调色板技术。原因很明显:如果用调色板,表示一个象素也要用24位,这是因为每种颜色的索引要用24位(因为总共有224种颜色,即调色板有224行),和直接用R,G,B三个分量表示用的字节数一样,不但没有任何便宜,还要加上一个256×256×256×3个字节的大调色板。所以真彩色图直接用R、G、B三个分量表示,它又叫做24位色图。
1.2 bmp文件格式
介绍完位图和调色板的概念,下面就让我们来看一看Windows的位图文件(.bmp文件)的格式是什么样子的。
bmp文件大体上分成四个部分,如图1.3所示。
位图文件头BITMAPFILEHEADER |
位图信息头BITMAPINFOHEADER |
调色板Palette |
实际的位图数据ImageDate |
图1.3 Windows位图文件结构示意图
第一部分为位图文件头BITMAPFILEHEADER,是一个结构,其定义如下:
typedef struct tagBITMAPFILEHEADER {
WORD bfType;
DWORD bfSize;
WORD bfReserved1;
WORD bfReserved2;
DWORD bfOffBits;
} BITMAPFILEHEADER;
这个结构的长度是固定的,为14个字节(WORD为无符号16位整数,DWORD为无符号32位整数),各个域的说明如下:
bfType
指定文件类型,必须是0x424D,即字符串“BM”,也就是说所有.bmp文件的头两个字节都是“BM”。
bfSize
指定文件大小,包括这14个字节。
bfReserved1,bfReserved2
为保留字,不用考虑
bfOffBits
为从文件头到实际的位图数据的偏移字节数,即图1.3中前三个部分的长度之和。
第二部分为位图信息头BITMAPINFOHEADER,也是一个结构,其定义如下:
typedef struct tagBITMAPINFOHEADER{
DWORD biSize;
LONG biWidth;
LONG biHeight;
WORD biPlanes;
WORD biBitCount
DWORD biCompression;
DWORD biSizeImage;
LONG biXPelsPerMeter;
LONG biYPelsPerMeter;
DWORD biClrUsed;
DWORD biClrImportant;
} BITMAPINFOHEADER;
这个结构的长度是固定的,为40个字节(LONG为32位整数),各个域的说明如下:
biSize
指定这个结构的长度,为40。
biWidth
指定图象的宽度,单位是象素。
biHeight
指定图象的高度,单位是象素。
biPlanes
必须是1,不用考虑。
biBitCount
指定表示颜色时要用到的位数,常用的值为1(黑白二色图), 4(16色图), 8(256色), 24(真彩色图)(新的.bmp格式支持32位色,这里就不做讨论了)。
biCompression
指定位图是否压缩,有效的值为BI_RGB,BI_RLE8,BI_RLE4,BI_BITFIELDS(都是一些Windows定义好的常量)。要说明的是,Windows位图可以采用RLE4,和RLE8的压缩格式,但用的不多。我们今后所讨论的只有第一种不压缩的情况,即biCompression为BI_RGB的情况。
biSizeImage
指定实际的位图数据占用的字节数,其实也可以从以下的公式中计算出来:
biSizeImage=biWidth’ × biHeight
要注意的是:上述公式中的biWidth’必须是4的整倍数(所以不是biWidth,而是biWidth’,表示大于或等于biWidth的,最接近4的整倍数。举个例子,如果biWidth=240,则biWidth’=240;如果biWidth=241,biWidth’=244)。
如果biCompression为BI_RGB,则该项可能为零
biXPelsPerMeter
指定目标设备的水平分辨率,单位是每米的象素个数,关于分辨率的概念,我们将在第4章详细介绍。
biYPelsPerMeter
指定目标设备的垂直分辨率,单位同上。
biClrUsed
指定本图象实际用到的颜色数,如果该值为零,则用到的颜色数为2biBitCount。
biClrImportant
指定本图象中重要的颜色数,如果该值为零,则认为所有的颜色都是重要的。
第三部分为调色板Palette,当然,这里是对那些需要调色板的位图文件而言的。有些位图,如真彩色图,前面已经讲过,是不需要调色板的,BITMAPINFOHEADER后直接是位图数据。
调色板实际上是一个数组,共有biClrUsed个元素(如果该值为零,则有2biBitCount个元素)。数组中每个元素的类型是一个RGBQUAD结构,占4个字节,其定义如下:
typedef struct tagRGBQUAD {
BYTE rgbBlue; //该颜色的蓝色分量
BYTE rgbGreen; //该颜色的绿色分量
BYTE rgbRed; //该颜色的红色分量
BYTE rgbReserved; //保留值
} RGBQUAD;
第四部分就是实际的图象数据了。对于用到调色板的位图,图象数据就是该象素颜在调色板中的索引值。对于真彩色图,图象数据就是实际的R、G、B值。下面针对2色、16色、256色位图和真彩色位图分别介绍。
对于2色位图,用1位就可以表示该象素的颜色(一般0表示黑,1表示白),所以一个字节可以表示8个象素。
对于16色位图,用4位可以表示一个象素的颜色,所以一个字节可以表示2个象素。
对于256色位图,一个字节刚好可以表示1个象素。
对于真彩色图,三个字节才能表示1个象素,哇,好费空间呀!没办法,谁叫你想让图的颜色显得更亮丽呢,有得必有失嘛。
要注意两点:
(1) 每一行的字节数必须是4的整倍数,如果不是,则需要补齐。这在前面介绍biSizeImage时已经提到了。
(2) 一般来说,.bMP文件的数据从下到上,从左到右的。也就是说,从文件中最先读到的是图象最下面一行的左边第一个象素,然后是左边第二个象素……接下来是倒数第二行左边第一个象素,左边第二个象素……依次类推 ,最后得到的是最上面一行的最右一个象素。
好了,终于介绍完bmp文件结构了,是不是觉得头有些大?别着急,对照着下面的程序,你就会很清楚了(我最爱看源程序了,呵呵)。
1.3 显示一个bmp文件的C程序
下面的函数LoadBmpFile,其功能是从一个.bmp文件中读取数据(包括BITMAPINFOHEADER,调色板和实际图象数据),将其存储在一个全局内存句柄hImgData中,这个hImgData将在以后的图象处理程序中用到。同时填写一个类型为HBITMAP的全局变量hBitmap和一个类型为HPALETTE的全局变量hPalette。这两个变量将在处理WM_PAINT消息时用到,用来显示位图。该函数的两个参数分别是用来显示位图的窗口句柄,和.bmp文件名(全路径)。当函数成功时,返回TRUE,否则返回FALSE。
BITMAPFILEHEADER bf;
BITMAPINFOHEADER bi;
BOOL LoadBmpFile (HWND hWnd,char *BmpFileName)
{
HFILE hf; //文件句柄
//指向BITMAPINFOHEADER结构的指针
LPBITMAPINFOHEADER lpImgData;
LOGPALETTE *pPal; //指向逻辑调色板结构的指针
LPRGBQUAD lpRGB; //指向RGBQUAD结构的指针
HPALETTE hPrevPalette; //用来保存设备中原来的调色板
HDC hDc; //设备句柄
HLOCAL hPal; //存储调色板的局部内存句柄
DWORD LineBytes; //每一行的字节数
DWORD ImgSize; //实际的图象数据占用的字节数
//实际用到的颜色数 ,即调色板数组中的颜色个数
DWORD NumColors;
DWORD i;
if((hf=_lopen(BmpFileName,OF_READ))==HFILE_ERROR){
MessageBox(hWnd,"File c://test.bmp not found!","Error Message",
MB_OK|MB_ICONEXCLAMATION);
return FALSE; //打开文件错误,返回
}
//将BITMAPFILEHEADER结构从文件中读出,填写到bf中
_lread(hf,(LPSTR)&bf,sizeof(BITMAPFILEHEADER));
//将BITMAPINFOHEADER结构从文件中读出,填写到bi中
_lread(hf,(LPSTR)&bi,sizeof(BITMAPINFOHEADER));
//我们定义了一个宏 #define WIDTHBYTES(i) ((i+31)/32*4)上面曾经
//提到过,每一行的字节数必须是4的整倍数,只要调用
//WIDTHBYTES(bi.biWidth*bi.biBitCount)就能完成这一换算。举一个例
//子,对于2色图,如果图象宽是31,则每一行需要31位存储,合3个
//字节加7位,因为字节数必须是4的整倍数,所以应该是4,而此时的
//biWidth=31,biBitCount=1,WIDTHBYTES(31*1)=4,和我们设想的一样。
//再举一个256色的例子,如果图象宽是31,则每一行需要31个字节存
//储,因为字节数必须是4的整倍数,所以应该是32,而此时的
//biWidth=31,biBitCount=8,WIDTHBYTES(31*8)=32,我们设想的一样。你可
//以多举几个例子来验证一下
//LineBytes为每一行的字节数
LineBytes=(DWORD)WIDTHBYTES(bi.biWidth*bi.biBitCount);
//ImgSize为实际的图象数据占用的字节数
ImgSize=(DWORD)LineBytes*bi.biHeight;
//NumColors为实际用到的颜色数 ,即调色板数组中的颜色个数
if(bi.biClrUsed!=0)
//如果bi.biClrUsed不为零,即为实际用到的颜色数
NumColors=(DWORD)bi.biClrUsed;
else //否则,用到的颜色数为2biBitCount。
switch(bi.biBitCount){
case 1:
NumColors=2;
break;
case 4:
NumColors=16;
break;
case 8:
NumColors=256;
break;
case 24:
NumColors=0; //对于真彩色图,没用到调色板
break;
default: //不处理其它的颜色数,认为出错。
MessageBox(hWnd,"Invalid color numbers!","Error Message",
MB_OK|MB_ICONEXCLAMATION);
_lclose(hf);
return FALSE; //关闭文件,返回FALSE
}
if(bf.bfOffBits!=(DWORD)(NumColors*sizeof(RGBQUAD)+
sizeof(BITMAPFILEHEADER)+
sizeof(BITMAPINFOHEADER)))
{
//计算出的偏移量与实际偏移量不符,一定是颜色数出错
MessageBox(hWnd,"Invalid color numbers!","Error Message",
MB_OK|MB_ICONEXCLAMATION);
_lclose(hf);
return FALSE; //关闭文件,返回FALSE
}
bf.bfSize=sizeof(BITMAPFILEHEADER)+sizeof(BITMAPINFOHEADER)+
NumColors*sizeof(RGBQUAD)+ImgSize;
//分配内存,大小为BITMAPINFOHEADER结构长度加调色板+实际位图
if((hImgData=GlobalAlloc(GHND,(DWORD)
(sizeof(BITMAPINFOHEADER)+
NumColors*sizeof(RGBQUAD)+
ImgSize)))==NULL)
{
//分配内存错误
MessageBox(hWnd,"Error alloc memory!","ErrorMessage",MB_OK|
MB_ICONEXCLAMATION);
_lclose(hf);
return FALSE; //关闭文件,返回FALSE
}
//指针lpImgData指向该内存区
lpImgData=(LPBITMAPINFOHEADER)GlobalLock(hImgData);
//文件指针重新定位到BITMAPINFOHEADER开始处
_llseek(hf,sizeof(BITMAPFILEHEADER),SEEK_SET);
//将文件内容读入lpImgData
_hread(hf,(char *)lpImgData,(long)sizeof(BITMAPINFOHEADER)
+(long)NumColors*sizeof(RGBQUAD)+ImgSize);
_lclose(hf); //关闭文件
if(NumColors!=0) //NumColors不为零,说明用到了调色板
{
//为逻辑调色板分配局部内存,大小为逻辑调色板结构长度加
//NumColors个PALETTENTRY
hPal=LocalAlloc(LHND,sizeof(LOGPALETTE)+
NumColors* sizeof(PALETTEENTRY));
//指针pPal指向该内存区
pPal =(LOGPALETTE *)LocalLock(hPal);
//填写逻辑调色板结构的头
pPal->palNumEntries = NumColors;
pPal->palVersion = 0x300;
//lpRGB指向的是调色板开始的位置
lpRGB = (LPRGBQUAD)((LPSTR)lpImgData +
(DWORD)sizeof(BITMAPINFOHEADER));
//填写每一项
for (i = 0; i < NumColors; i++)
{
pPal->palPalEntry[i].peRed=lpRGB->rgbRed;
pPal->palPalEntry[i].peGreen=lpRGB->rgbGreen;
pPal->palPalEntry[i].peBlue=lpRGB->rgbBlue;
pPal->palPalEntry[i].peFlags=(BYTE)0;
lpRGB++; //指针移到下一项
}
//产生逻辑调色板,hPalette是一个全局变量
hPalette=CreatePalette(pPal);
//释放局部内存
LocalUnlock(hPal);
LocalFree(hPal);
}
//获得设备上下文句柄
hDc=GetDC(hWnd);
if(hPalette) //如果刚才产生了逻辑调色板
{
//将新的逻辑调色板选入DC,将旧的逻辑调色板句柄保存在//hPrevPalette
hPrevPalette=SelectPalette(hDc,hPalette,FALSE);
RealizePalette(hDc);
}
//产生位图句柄
hBitmap=CreateDIBitmap(hDc,(LPBITMAPINFOHEADER)lpImgData,
(LONG)CBM_INIT,
(LPSTR)lpImgData+sizeof(BITMAPINFOHEADER)+NumColors*sizeof(RGBQUAD),
(LPBITMAPINFO)lpImgData, DIB_RGB_COLORS);
//将原来的调色板(如果有的话)选入设备上下文句柄
if(hPalette && hPrevPalette)
{
SelectPalette(hDc,hPrevPalette,FALSE);
RealizePalette(hDc);
}
ReleaseDC(hWnd,hDc); //释放设备上下文
GlobalUnlock(hImgData); //解锁内存区
return TRUE; //成功返回
}
对上面的程序要说明两点:
(1) 对于需要调色板的图,要想正确地显示,必须根据bmp文件,产生逻辑调色板。产生的方法是:①为逻辑调色板指针分配内存,大小为逻辑调色板结构(LOGPALETTE)长度加NumColors个PALETTENTRY大小(调色板的每一项都是一个PALETTEENTRY结构);②填写逻辑调色板结构的头pPal->palNumEntries = NumColors; pPal->palVersion = 0x300;③从文件中读取调色板的RGB值,填写到每一项中;④产生逻辑调色板:hPalette=CreatePalette(pPal)。
(2) 产生位图(BITMAP)句柄,该项工作由函数CreateDIBitmap来完成。
hBitmap=CreateDIBitmap(hDc,(LPBITMAPINFOHEADER)lpImgData,
(LONG)CBM_INIT,
(LPSTR)lpImgData+sizeof(BITMAPINFOHEADER)+NumColors*sizeof(RGBQUAD),
(LPBITMAPINFO)lpImgData, DIB_RGB_COLORS);
CreateDIBitmap的作用是产生一个和Windows设备无关的位图。该函数的第一项参数为设备上下文句柄。如果位图用到了调色板,要在调用CreateDIBitmap之前将逻辑调色板选入该设备上下文中,产生hBitmap后,再把原调色板选入该设备上下文中,并释放该上下文;第二项为指向BITMAPINFOHEADER的指针;第三项就用常量CBM_INI,不用考虑;第四项为指向调色板的指针;第五项为指向BITMAPINFO(包括BITMAPINFOHEADER,调色板,及实际的图象数据)的指针;第六项就用常量DIB_RGB_COLORS,不用考虑。
上面提到了设备上下文,相信编过Windows程序的读者对它并不陌生,这里再简单介绍一下。Windows操作系统统一管理着诸如显示,打印等操作,将它们看作是一个个的设备,每一个设备都有一个复杂的数据结构来维护。所谓设备上下文就是指这个数据结构。然而,我们不能直接和这些设备上下文打交道,只能通过引用标识它的句柄(实际上是一个整数),让Windows去做相应的处理。
产生的逻辑调色板句柄hPalette和位图句柄hBitmap要在处理WM_PAINT消息时使用,这样才能在屏幕上显示出来,处理过程如下面的程序。
Static HDC hDC,hMemDC;
PAINTSTRUCT ps;
case WM_PAINT:
{
hDC = BeginPaint(hwnd, &ps); //获得屏幕设备上下文
if (hBitmap) //hBitmap一开始是NULL,当不为NULL时表示有图
{
hMemDC = CreateCompatibleDC(hDC); //建立一个内存设备上下文
if (hPalette) //有调色板
{
//将调色板选入屏幕设备上下文
SelectPalette (hDC, hPalette, FALSE);
//将调色板选入内存设备上下文
SelectPalette (hMemDC, hpalette, FALSE);
RealizePalette (hDC);
}
//将位图选入内存设备上下文
SelectObject(hMemDC, hBitmap);
//显示位图
BitBlt(hDC, 0, 0, bi.biWidth, bi.biHeight, hMemDC, 0, 0, SRCCOPY);
//释放内存设备上下文
DeleteDC(hMemDC);
}
//释放屏幕设备上下文
EndPaint(hwnd, &ps);
break;
}
在上面的程序中,我们调用CreateCompatibleDC创建一个内存设备上下文。SelectObject函数将与设备无关的位图选入内存设备上下文中。然后我们调用BitBlt函数在内存设备上下文和屏幕设备上下文中进行位拷贝。由于所有操作都是在内存中进行,所以速度很快。
BitBlt函数的参数分别为:1.目标设备上下文,在上面的程序里,为屏幕设备上下文,如果改成打印设备上下文,就不是显示位图,而是打印;2.目标矩形左上角点x坐标;3. 目标矩形左上角点y坐标,在上面的程序中,2和3为(0,0),表示显示在窗口的左上角;4.目标矩形的宽度;5. 目标矩形的高度;6. 源设备上下文,在上面的程序里,为内存设备上下文;7. 源矩形左上角点x坐标;8. 源矩形左上角点y坐标;9.操作方式,在这里为SRCCOPY,表示直接将源矩形拷贝到目标矩形。还可以是反色,擦除,做“与”运算等操作,具体细节见VC++帮助。你可以试着改改第2、3、4、5、7、8、9项参数,就能体会到它们的含义了。
哇,终于讲完了。是不是觉得有点枯燥?这一章是有点儿枯燥,特别是当你对Windows的编程并不清楚时,就更觉得如此。不过,当一幅漂亮的bmp图显示在屏幕上时,你还是会兴奋地大叫“Yeah!”,至少当年我是这样。
在本书的附盘中包含所有的源程序,包括头文件和资源文件和例图。特别要注意的是,退出时,别忘了释放内存和资源,这是每个程序员应该养成的习惯。这些个程序并不是很完善,例如,如果一幅图很大,屏幕显示不下怎么办?你可以试着自己加上滚动条。另外,为了节省篇幅,.bmp文件名被固定为c:/test.bmp,可以自己加入打开文件对话框,任意选择你要显示的文件。图1.4为程序运行时的画面。
图1.4 运行时的画面
最后,再介绍一个命令行编译的窍门。为什么要用命令行编译呢?主要有两个好处:第一,不用进入IDE(集成开发环境),节省了时间,而且编译速度也比较快;第二,对于简单的程序,不用生成项目文件.mdp或.mak,直接就能生成.exe文件,这一点,在下面的例子中可以看到。
在安装完Visual C++时,在bin目录下会产生一个VCVARS32.BAT文件,它的作用是在命令行编译时设置正确的环境变量,如存放头文件的INCLUDE目录,存放库文件的LIB目录等。如果你没找到这个批处理文件,可以参考下面的例子,自己做一个批处理。
@echo off
set MSDevDir=d:/MSDEV
set VcOsDir=WIN95
set PATH="%MSDevDir%/BIN";"%MSDevDir%/BIN/%VcOsDir%";"%PATH%"
set INCLUDE=%MSDevDir%/INCLUDE;%MSDevDir%/MFC/INCLUDE;
%INCLUDE%
set LIB=%MSDevDir%/LIB;%MSDevDir%/MFC/LIB;%LIB%
set VcOsDir=
只要把上面的“d:/MSDEV”改成你自己的VC目录就可以了。在DOS PROMPT下执行该批处理文件,执行set命令,你就能看到新设置的环境变量了。如下所示:
PATH=D:/MSDEV/BIN;D:/MSDEV/BIN/WIN95;C:/WIN95;C:/WIN95/COMMAND;C:/WIN95/SYSTEM;
INCLUDE=d:/msdev/INCLUDE;d:/msdev/MFC/INCLUDE;
LIB=d:/msdev/LIB;d:/msdev/MFC/LIB;
现在我们就可以进行命令行编译了。首先编译资源文件,输入rc bmp.rc,将生成bmp.res文件,接着输入cl bmp.c bmp.res user32.lib gdi32.lib,就生成bmp.exe 了。可以看到,我们并没有用到项目文件,所以,对于这种简单的程序来说,使用命令行编译还是非常方便的。
有时命令行编译会出现“Out of enviroment space”的错误,那是因为command.com缺省的初始环境变量内存太小,首先执行command /e:2048 (或更大)命令即可解决改问题。
使用ide的方法是:new project,类型是win32 application->empty project,然后把.h,.rc,.c文件add to project编译即可。
好了,运行bmp.exe,欣赏一下你今天的劳动成果。
这一章我们将介绍图象的几何变换,包括图象的平移、旋转、镜象变换、转置、放缩等。如果你熟悉矩阵运算,你将发现,实现这些变换是非常容易的。
2.1 平移
平移(translation)变换大概是几何变换中最简单的一种了。
如图2.1所示,初始坐标为(x0,y0)的点经过平移(tx,ty)(以向右,向下为正方向)后,坐标变为(x1,y1)。这两点之间的关系是x1=x0+tx ,y1=y0+ty。
图2.1 平移的示意图
以矩阵的形式表示为
(2.1)
我们更关心的是它的逆变换:
(2.2)
这是因为:我们想知道的是平移后的图象中每个象素的颜色。例如我们想知道,新图中左上角点的RGB值是多少?很显然,该点是原图的某点经过平移后得到的,这两点的颜色肯定是一样的,所以只要知道了原图那点的RGB值即可。那么到底新图中的左上角点对应原图中的哪一点呢?将左上角点的坐标(0,0)入公式(2.2),得到x0=-tx ,y0=-ty;所以新图中的(0,0)点的颜色和原图中(-tx , -ty)的一样。
这样就存在一个问题:如果新图中有一点(x1,y1),按照公式(2.2)得到的(x0,y0)不在原图中该怎么办?通常的做法是,把该点的RGB值统一设成(0,0,0)或者(255,255,255)。
另一个问题是:平移后的图象是否要放大?一种做法是不放大,移出的部分被截断。例如,图2.2为原图,图2.3为移动后的图。这种处理,文件大小不会改变。
图2.2 移动前的图
图2.3 移动后的图
还有一种做法是:将图象放大,使得能够显示下所有部分,如图2.4所示。
图2.4 移动后图象被放大
这种处理,文件大小要改变。设原图的宽和高分别是w1,h1则新图的宽和高变为w1+|tx|和h1+|ty|,加绝对值符号是因为tx, ty有可能为负(即向左,向上移动)。
下面的函数Translation采用的是第一种做法,即移出的部分被截断。在给出源代码之前,先说明一个问题。
如果你用过Photoshop,Corel PhotoPaint等图象处理软件,可能听说过“灰度图”(grayscale)这个词。灰度图是指只含亮度信息,不含色彩信息的图象,就象我们平时看到的黑白照片:亮度由暗到明,变化是连续的。因此,要表示灰度图,就需要把亮度值进行量化。通常划分成0到255共256个级别,其中0最暗(全黑),255最亮(全白)。.bmp格式的文件中,并没有灰度图这个概念,但是,我们可以很容易在.bmp文件中表示灰度图。方法是用256色的调色板,只不过这个调色板有点特殊,每一项的RGB值都是相同的。也就是说RGB值从(0,0,0),(1,1,1)一直到(255,255,255)。(0,0,0)是全黑色,(255,255,255)是全白色,中间的是灰色。这样,灰度图就可以用256色图来表示了。为什么会这样呢?难道是一种巧合?其实并不是。
在表示颜色的方法中,除了RGB外,还有一种叫YUV的表示方法,应用也很多。电视信号中用的就是一种类似于YUV的颜色表示方法。
在这种表示方法中,Y分量的物理含义就是亮度,U和V分量代表了色差信号(你不必了解什么是色差,只要知道有这么一个概念就可以了)。使用这种表示方法有很多好处,最主要的有两点:
(1) 因为Y代表了亮度,所以Y分量包含了灰度图的所有信息,只用Y分量就能完全能够表示出一幅灰度图来。当同时考虑U,V分量时,就能够表示出彩色信息来。这样,用同一种表示方法可以很方便的在灰度和彩色图之间切换,而RGB表示方法就做不到这一点了。
(2) 人眼对于亮度信号非常敏感,而对色差信号的敏感程度相对较弱。也就是说,图象的主要信息包含在Y分量中。这就提示我们:如果在对YUV信号进行量化时,可以“偏心”一点,让Y的量化级别多一些(谁让它重要呢?)而让UV的量化级别少一些,就可以实现图象信息的压缩。这一点将在第9章介绍图象压缩时仔细研究,这里就不深入讨论了。而RGB的表示方法就做不到这一点,因为RGB三个分量同等重要,缺了谁也不行。YUV和RGB之间有着如下的对应关系
(2.3)
(2.4)
当RGB三个分量的大小一样时,假设都是a,代入公式(2.3),得到Y=a,U=0,V=0 。你现在该明白我前面所说不是巧合的原因了吧。
使用灰度图有一个好处,那就是方便。首先RGB的值都一样;其次,图象数据即调色板索引值,也就是实际的RGB值,也就是亮度值;另外,因为是256色调色板,所以图象数据中一个字节代表一个象素,很整齐。如果是2色图或16色图,还要拼凑字节,很麻烦。如果是彩色的256色图,由于图象处理后有可能会产生不属于这256种颜色的新颜色,就更麻烦了;这一点,今后你就会有深刻体会的。所以,做图象处理时,一般采用灰度图。为了将重点放在算法本身上,今后给出的程序如不做特殊说明,都是针对256级灰度图的。其它颜色的情况,你可以自己想一想,把算法补全。
如果想得到一幅灰度图,可以使用Sea或者PhotoShop等软件提供的颜色转换功能将彩色图转换成灰度图。
好了,言归正传,下面给出Translation的源代码。算法的思想是先将所有区域填成白色,然后找平移后显示区域的左上角点(x0,y0) 和右下角点(x1,y1) ,分几种情况进行处理。
先看x方向(width指图象的宽度)
(1) tx≤-width:很显然,图象完全移出了屏幕,不用做任何处理;
(2) -width<tx≤0:如图2.5所示。容易看出,图象区域的x范围从0到width-|tx|,对应原图的范围从|tx|到width;
图2.5 tx≤0,ty≤0的情况
(3) 0< tx <width:如图2.6所示。容易看出,图象区域的x范围从tx 到width,对应原图的范围从0到width - tx ;
图2.6 0< tx<width,0<ty<height的情况
(4) tx ≥width:很显然,图象完全移出了屏幕,不用做任何处理。
y方向是对应的(height表示图象的高度):
(1) ty≤-height,图象完全移出了屏幕,不用做任何处理;
(2) -height<ty≤0,图象区域的y范围从0到height-|ty|,对应原图的范围从|ty|到height;
(3) 0<ty<height ,图象区域的y范围从ty到height,对应原图的范围从0到height-ty;
(4) ty≥height,图象完全移出了屏幕,不用做任何处理。
这种做法利用了位图存储的连续性,即同一行的象素在内存中是相邻的。利用memcpy函数,从(x0,y0)点开始,一次可以拷贝一整行(宽度为x1-x0),然后将内存指针移到(x0,y0+1)处,拷贝下一行。这样拷贝(y1-y0)行就完成了全部操作,避免了一个一个象素的计算,提高了效率。Translation的源代码如下:
int xOffset=0,yOffset=0;
BOOL Translation(HWND hWnd)
{
DLGPROC dlgInputBox = NULL;
DWORD OffBits,BufSize;
LPBITMAPINFOHEADER lpImgData;
LPSTR lpPtr;
HLOCAL hTempImgData;
LPBITMAPINFOHEADER lpTempImgData;
LPSTR lpTempPtr;
int SrcX0,SrcY0,SrcX1,SrcY1;
int DstX0,DstY0,DstX1,DstY1;
int RectWidth,RectHeight;
BOOL xVisible,yVisible;
HDC hDc;
HFILE hf;
int i;
//出现对话框,输入x偏移量xOffset,和y偏移量yOffset
dlgInputBox = (DLGPROC) MakeProcInstance ( (FARPROC)InputBox,ghInst );
DialogBox (ghInst, "INPUTBOX", hWnd, dlgInputBox);
FreeProcInstance ( (FARPROC) dlgInputBox );
//OffBits为BITMAPINFOHEADER结构长度加调色板的大小
OffBits=bf.bfOffBits-sizeof(BITMAPFILEHEADER);
BufSize=OffBits+bi.biHeight*LineBytes;//要开的缓冲区的大小
//为新产生的位图分配缓冲区内存
if((hTempImgData=LocalAlloc(LHND,BufSize))==NULL)
{
MessageBox(hWnd,"Error alloc memory!","Error Message",MB_OK|
MB_ICONEXCLAMATION);
return FALSE; //失败,返回
}
//lpImgData为指向原来位图数据的指针
lpImgData=(LPBITMAPINFOHEADER)GlobalLock(hImgData);
//lpTempImgData为指向新产生位图数据的指针
lpTempImgData=(LPBITMAPINFOHEADER)LocalLock(hTempImgData);
lpPtr=(char *)lpImgData;
lpTempPtr=(char *)lpTempImgData;
//将新的缓冲区中的每个字节都填成255,这样以后未处理的象素就是白色
memset(lpTempPtr,(BYTE)255,BufSize);
//两幅图之间的头信息,包括调色板都是相同的,所以直接拷贝头和调色板
memcpy(lpTempPtr,lpPtr,OffBits);
//xVisible为FALSE时,表示x方向已经移出了可显示的范围
xVisible=TRUE;
if( xOffset<= -bi.biWidth )
xVisible=FALSE;
else if( xOffset<=0){
DstX0=0; //表示移动后,有图区域的左上角点的x坐标
DstX1=bi.biWidth+xOffset; //表示移动后,有图区域的右下角点的x坐标
}
else if ( xOffset<bi.biWidth){
DstX0=xOffset;
DstX1=bi.biWidth;
}
else
xVisible=FALSE;
SrcX0=DstX0-xOffset; //对应DstX0在原图中的x坐标
SrcX1=DstX1-xOffset; //对应DstX1在原图中的x坐标
RectWidth=DstX1-DstX0; //有图区域的宽度
//yVisible为FALSE时,表示y方向已经移出了可显示的范围
yVisible=TRUE;
if( yOffset<= -bi.biHeight )
yVisible=FALSE;
else if( yOffset<=0){
DstY0=0; //表示移动后,有图区域的左上角点的y坐标
DstY1=bi.biHeight+yOffset; //表示移动后,有图区域的右下角点的y坐标
}
else if ( yOffset<bi.biHeight){
DstY0=yOffset;
DstY1=bi.biHeight;
}
else
yVisible=FALSE;
SrcY0=DstY0-yOffset; //对应DstY0在原图中的y坐标
SrcY1=DstY1-yOffset; //对应DstY1在原图中的y坐标
RectHeight=DstY1-DstY0; //有图区域的高度
if( xVisible && yVisible){ //x,y方向都没有完全移出可显示的范围
for(i=0;i<RectHeight;i++){ //拷贝每一行
//lpPtr指向要拷贝的那一行的最左边的象素对应在原图中的位
//置。特别要注意的是,由于.bmp是上下颠倒的,偏移是
//(BufSize-LineBytes-(i+SrcY0)*LineBytes)+SrcX0,而不是
//(i+SrcY0)*LineBytes)+SrcX0,你试着举个例子就明白了。
lpPtr=(char*)lpImgData+(BufSize-LineBytes-
(i+SrcY0)*LineBytes)+SrcX0;
//lpTempPtr指向要拷贝的那一行的最左边的象素对应在新图中//的位置。同样要注意上面//的问题。
lpTempPtr=(char*)lpTempImgData+
(BufSize-LineBytes-(i+DstY0)*LineBytes)+DstX0;
//拷贝一行(宽度为RectWidth)
memcpy(lpTempPtr,lpPtr,RectWidth);
}
}
hDc=GetDC(hWnd);
if(hBitmap!=NULL)
DeleteObject(hBitmap); //释放原来的位图句柄
//产生新的位图
hBitmap=CreateDIBitmap(hDc,(LPBITMAPINFOHEADER)lpTempImgData,
(LONG)CBM_INIT,
(LPSTR)lpTempImgData+
sizeof(BITMAPINFOHEADER) +
NumColors*sizeof(RGBQUAD),
(LPBITMAPINFO)lpTempImgData,
DIB_RGB_COLORS);
//将平移后的图象存成文件
hf=_lcreat("c://translation.bmp",0);
_lwrite(hf,(LPSTR)&bf,sizeof(BITMAPFILEHEADER));
_lwrite(hf,(LPSTR)lpTempImgData,BufSize);
_lclose(hf);
//释放资源和内存
ReleaseDC(hWnd,hDc);
LocalUnlock(hTempImgData);
LocalFree(hTempImgData);
GlobalUnlock(hImgData);
return TRUE;
}
2.2 旋转
旋转(rotation)有一个绕着什么转的问题,通常的做法是以图象的中心为圆心旋转,举个例子,图2.7旋转30度(顺时针方向)后如图2.8所示:
图2.7 旋转前的图 |
图2.8 旋转后的图 |
可以看出,旋转后图象变大了。另一种做法是不让图象变大,转出的部分被裁剪掉。如图2.9所示。
我们采用第一种做法,首先给出变换矩阵。在我们熟悉的坐标系中,将一个点顺时针旋转a角后的坐标变换公式,如图2.10所示,r为该点到原点的距离,在旋转过程中,r保持不变;b为r与x轴之间的夹角。
图2.9 旋转后保持原图大小, 转出的部分被裁掉 |
图2.10 旋转示意图 |
旋转前:x0=rcosb;y0=rsinb
旋转a角度后:
x1=rcos(b-a)=rcosbcosa+rsinbsina=x0cosa+y0sina;
y1=rsin(b-a)=rsinbcosa-rcosbsina=-x0sina+y0cosa;
以矩阵的形式表示:
(2.5)
上面的公式中,坐标系xoy是以图象的中心为原点,向右为x轴正方向,向上为y轴正方向。它和以图象左上角点为原点o’,向右为x’轴正方向,向下为y’轴正方向的坐标系x’o’y’之间的转换关系如何呢?如图2.11所示。
图2.11 两种坐标系间的转换关系
设图象的宽为w,高为h,容易得到:
(2.6)
逆变换为:
(2.7)
有了上面的公式,我们可以把变换分成三步:
1.将坐标系o’变成o;
2.将该点顺时针旋转a角;
3.将坐标系o变回o’,这样,我们就得到了变换矩阵,是上面三个矩阵的级联。
(2.8)
要注意的是,因为新图变大,所以上面公式中出现了wold,hold,wnew,hnew,它们分别表示原图(old)和新图(new)的宽、高。我们从图2.8中容易看出:wnew=max(|x4-x1|,|x3-x2|) ;hnew=max(|y4-y1|,|y3-y2|)。
(2.8)的逆变换为
(2.9)
这样,对于新图中的每一点,我们就可以根据公式(2.9)求出对应原图中的点,得到它的灰度。如果超出原图范围,则填成白色。要注意的是,由于有浮点运算,计算出来点的坐标可能不是整数,采用取整处理,即找最接近的点,这样会带来一些误差(图象可能会出现锯齿)。更精确的方法是采用插值,将在图象缩放时介绍。
源程序如下:
#define PI 3.1415926535
#define RADIAN(angle) ((angle)*PI/180.0) //角度到弧度转化的宏
BOOL Rotation(HWND hWnd)
{
DLGPROC dlgInputBox = NULL;
DWORD OffBits,SrcBufSize,DstBufSize,DstLineBytes;
LPBITMAPINFOHEADER lpImgData;
LPSTR lpPtr;
HLOCAL hTempImgData;
LPBITMAPINFOHEADER lpTempImgData;
LPSTR lpTempPtr;
float SrcX1,SrcY1,SrcX2,SrcY2;
float SrcX3,SrcY3,SrcX4,SrcY4;
float DstX1,DstY1,DstX2,DstY2;
float DstX3,DstY3,DstX4,DstY4;
DWORD Wold,Hold,Wnew,Hnew;
HDC hDc;
HFILE hf;
DWORD x0,y0,x1,y1;
float cosa,sina; //cos(a),sin(a);
float num1,num2;
BITMAPFILEHEADER DstBf;
BITMAPINFOHEADER DstBi;
//出现对话框,输入旋转角度(顺时针方向)
dlgInputBox = (DLGPROC) MakeProcInstance ( (FARPROC)InputBox,ghInst );
DialogBox (ghInst, "INPUTBOX", hWnd, dlgInputBox);
FreeProcInstance ( (FARPROC) dlgInputBox );
//角度到弧度的转化
RotateAngle=(float)RADIAN(RotateAngle);
cosa=(float)cos((double)RotateAngle);
sina=(float)sin((double)RotateAngle);
//原图的宽度和高度
Wold=bi.biWidth;
Hold=bi.biHeight;
//原图的四个角的坐标
SrcX1=(float)(-0.5*Wold);
SrcY1=(float)(0.5*Hold);
SrcX2=(float)(0.5*Wold);
SrcY2=(float)(0.5*Hold);
SrcX3=(float)(-0.5*Wold);
SrcY3=(float)(-0.5*Hold);
SrcX4=(float)(0.5*Wold);
SrcY4=(float)(-0.5*Hold);
//新图四个角的坐标
DstX1=cosa*SrcX1+sina*SrcY1;
DstY1=-sina*SrcX1+cosa*SrcY1;
DstX2=cosa*SrcX2+sina*SrcY2;
DstY2=-sina*SrcX2+cosa*SrcY2;
DstX3=cosa*SrcX3+sina*SrcY3;
DstY3=-sina*SrcX3+cosa*SrcY3;
DstX4=cosa*SrcX4+sina*SrcY4;
DstY4=-sina*SrcX4+cosa*SrcY4;
//计算新图的宽度,高度
Wnew = (DWORD)(max(fabs(DstX4-DstX1), fabs(DstX3-DstX2))+0.5);
Hnew = (DWORD)(max(fabs(DstY4-DstY1), fabs(DstY3-DstY2))+0.5);
//计算矩阵(2.9)中的两个常数,这样不用以后每次都计算了
num1=(float)( -0.5*Wnew*cosa-0.5*Hnew*sina+0.5*Wold);
num2=(float)(0.5*Wnew*sina-0.5*Hnew*cosa+0.5*Hold);
//OffBits为BITMAPINFOHEADER结构长度加调色板的大小
OffBits=bf.bfOffBits-sizeof(BITMAPFILEHEADER);
SrcBufSize=OffBits+bi.biHeight*LineBytes;
//显示时,采用新图的宽度和高度,
ImgWidth=Wnew;
ImgHeight=Hnew;
//新图每行占用的字节
DstLineBytes=(DWORD)WIDTHBYTES(Wnew*bi.biBitCount);
DstBufSize=(DWORD)(sizeof(BITMAPINFOHEADER)+
NumColors*sizeof(RGBQUAD)+
(DWORD)DstLineBytes*Hnew); //要开的缓冲区的大小
//为新产生的位图分配缓冲区内存
if((hTempImgData=LocalAlloc(LHND,DstBufSize))==NULL)
{
MessageBox(hWnd,"Error alloc memory!","Error Message",
MB_OK|MB_ICONEXCLAMATION);
return FALSE; //失败,返回
}
lpImgData=(LPBITMAPINFOHEADER)GlobalLock(hImgData);
//lpTempImgData为指向新产生位图数据的指针
lpTempImgData=(LPBITMAPINFOHEADER)LocalLock(hTempImgData);
lpPtr=(char *)lpImgData;
lpTempPtr=(char *)lpTempImgData;
//将新的缓冲区中的每个字节都填成255,这样以后未处理的象素就是白色
memset(lpTempPtr,(BYTE)255,DstBufSize);
//拷贝头和调色板信息
memcpy(lpTempPtr,lpPtr,OffBits);
//得到新的BITMAPFILEDER和BITMAPINFOHERDER
memcpy((char *)&DstBf,(char *)&bf,sizeof(BITMAPFILEHEADER));
memcpy((char *)&DstBi,(char *)&bi,sizeof(BITMAPINFOHEADER));
//做一些必要的改变,这一点特别要注意
DstBf.bfSize=DstBufSize+sizeof(BITMAPFILEHEADER);
DstBi.biWidth=Wnew;
DstBi.biHeight=Hnew;
//用新的BITMAPINFOHERDER覆盖原来的那个
memcpy(lpTempPtr,(char *)&DstBi,sizeof(BITMAPINFOHEADER));
for(y1=0;y1<Hnew;y1++)
for(x1=0;x1<Wnew;x1++){
//x0,y0为对应的原图上的坐标
x0= (DWORD)(x1*cosa+y1*sina+num1);
y0= (DWORD)(-1.0f*x1*sina+y1*cosa+num2);
if( (x0>=0) && (x0<Wold) && (y0>=0) && (y0<Hold))
//在原图范围内
{
lpPtr=(char*)lpImgData+
(SrcBufSize-LineBytes-y0*LineBytes)+x0;
lpTempPtr=(char*)lpTempImgData+
(DstBufSize-DstLineBytes-y1*DstLineBytes)+x1;
*lpTempPtr=*lpPtr; //进行象素的复制
}
}
hDc=GetDC(hWnd);
if(hBitmap!=NULL)
DeleteObject(hBitmap); //释放原来的位图句柄
hBitmap=CreateDIBitmap(hDc,(LPBITMAPINFOHEADER)lpTempImgData,
(LONG)CBM_INIT,
(LPSTR)lpTempImgData+
sizeof(BITMAPINFOHEADER) +
NumColors*sizeof(RGBQUAD),
(LPBITMAPINFO)lpTempImgData,
DIB_RGB_COLORS);
//将旋转后的图象存成文件
hf=_lcreat("c://rotation.bmp",0);
_lwrite(hf,(LPSTR)&DstBf,sizeof(BITMAPFILEHEADER));
_lwrite(hf,(LPSTR)lpTempImgData,DstBufSize);
_lclose(hf);
//释放资源和内存
ReleaseDC(hWnd,hDc);
LocalUnlock(hTempImgData);
LocalFree(hTempImgData);
GlobalUnlock(hImgData);
return TRUE;
}
程序运行时的画面如图2.12所示
图2.12 旋转
2.3 镜象
镜象(mirror)分水平镜象和垂直镜象两种。图2.2的水平镜象和垂直镜象分别如图2.13和图2.14所示
图2.13 图2.2的水平镜象
图2.14 图2.2的垂直镜象
镜象的变换矩阵很简单。设原图宽为w,高为h,变换后,图的宽和高不变。
水平镜象的变化矩阵为:
(2.10)
垂直镜象的变化矩阵为:
(2.11)
镜象变换的源代码如下,因为和平移的那段程序很类似,程序中的注释就简单一些。
BOOL Mirror(HWND hWnd,BOOL XDirection)
//Xdirection为TRUE时表示水平镜象,为FALSE时表示垂直镜象变换
{
DWORD OffBits,BufSize;
LPBITMAPINFOHEADER lpImgData;
LPSTR lpPtr;
HLOCAL hTempImgData;
LPBITMAPINFOHEADER lpTempImgData;
LPSTR lpTempPtr;
HDC hDc;
HFILE hf;
LONG x0,y0,x1,y1;
OffBits=bf.bfOffBits-sizeof(BITMAPFILEHEADER);
BufSize=OffBits+bi.biHeight*LineBytes;
if((hTempImgData=LocalAlloc(LHND,BufSize))==NULL)
{
MessageBox(hWnd,"Error alloc memory!","Error Message",MB_OK|
MB_ICONEXCLAMATION);
return FALSE;
}
lpImgData=(LPBITMAPINFOHEADER)GlobalLock(hImgData); lpTempImgData=(LPBITMAPINFOHEADER)LocalLock(hTempImgData);
lpPtr=(char *)lpImgData;
lpTempPtr=(char *)lpTempImgData;
memset(lpTempPtr,(BYTE)255,BufSize);
memcpy(lpTempPtr,lpPtr,OffBits);
if( XDirection){ //水平镜象
for(y1=0;y1<bi.biHeight;y1++)
for(x1=0;x1<bi.biWidth;x1++){
x0=bi.biWidth-1-x1; //因为x坐标是从0到bi.biWidth-1
y0=y1;
lpPtr=(char *)lpImgData+(BufSize-LineBytes-y0*LineBytes)+x0;
lpTempPtr=(char *)lpTempImgData+
(BufSize-LineBytes-y1*LineBytes)+x1;
*lpTempPtr=*lpPtr;
}
}
else{ //垂直镜象
for(y1=0;y1<bi.biHeight;y1++)
for(x1=0;x1<bi.biWidth;x1++){
x0=x1;
y0=bi.biHeight-1-y1;
lpPtr=(char *)lpImgData+(BufSize-LineBytes-y0*LineBytes)+x0;
lpTempPtr=(char *)lpTempImgData+
(BufSize-LineBytes-y1*LineBytes)+x1;
*lpTempPtr=*lpPtr;
}
}
hDc=GetDC(hWnd);
if(hBitmap!=NULL)
DeleteObject(hBitmap);
hBitmap=CreateDIBitmap(hDc,(LPBITMAPINFOHEADER)lpTempImgData,
(LONG)CBM_INIT,
(LPSTR)lpTempImgData+
sizeof(BITMAPINFOHEADER)+
NumColors*sizeof(RGBQUAD),
(LPBITMAPINFO)lpTempImgData, DIB_RGB_COLORS);
if( XDirection)
hf=_lcreat("c://mirrorx.bmp",0);
else
hf=_lcreat("c://mirrory.bmp",0);
_lwrite(hf,(LPSTR)&bf,sizeof(BITMAPFILEHEADER));
_lwrite(hf,(LPSTR)lpTempImgData,BufSize);
_lclose(hf);
ReleaseDC(hWnd,hDc);
LocalUnlock(hTempImgData);
LocalFree(hTempImgData);
GlobalUnlock(hImgData);
return TRUE;
}
2.4 转置
转置(transpose)是指将x,y坐标对换,图2.2的转置如图2.15所示。
图2.15 图2.2的转置
要注意的是,转置和旋转900是有区别的,不信你可以试试:怎么旋转,图2.2也转不出图2.15来。另外,转置后图的宽高对换了。转置的变换矩阵很简单:
(2.12)
镜象变换的源代码如下,因为和旋转的那段程序很类似,程序中的注释就简单一些:
BOOL Transpose(HWND hWnd)
{
DWORD OffBits,SrcBufSize,DstBufSize,DstLineBytes;
LPBITMAPINFOHEADER lpImgData;
LPSTR lpPtr;
HLOCAL hTempImgData;
LPBITMAPINFOHEADER lpTempImgData;
LPSTR lpTempPtr;
DWORD Wnew,Hnew;
HDC hDc;
HFILE hf;
DWORD x0,y0,x1,y1;
BITMAPFILEHEADER DstBf;
BITMAPINFOHEADER DstBi;
//新图的宽度和高度
Wnew = (DWORD)bi.biHeight;
Hnew = (DWORD)bi.biWidth;
OffBits=bf.bfOffBits-sizeof(BITMAPFILEHEADER);
SrcBufSize=OffBits+bi.biHeight*LineBytes;
//显示时,采用新图的宽度和高度,
ImgWidth=Wnew;
ImgHeight=Hnew;
DstLineBytes=(DWORD)WIDTHBYTES(Wnew*bi.biBitCount);
DstBufSize=(DWORD)(sizeof(BITMAPINFOHEADER)+
NumColors*sizeof(RGBQUAD)+
(DWORD)DstLineBytes*Hnew);
if((hTempImgData=LocalAlloc(LHND,DstBufSize))==NULL)
{
MessageBox(hWnd,"Error alloc memory!","Error Message",MB_OK|
MB_ICONEXCLAMATION);
return FALSE;
}
lpImgData=(LPBITMAPINFOHEADER)GlobalLock(hImgData);
lpTempImgData=(LPBITMAPINFOHEADER)LocalLock(hTempImgData);
lpPtr=(char *)lpImgData;
lpTempPtr=(char *)lpTempImgData;
memset(lpTempPtr,(BYTE)255,DstBufSize);
memcpy(lpTempPtr,lpPtr,OffBits);
//头信息中做一些必要的改变,这一点非常重要
memcpy((char *)&DstBf,(char *)&bf,sizeof(BITMAPFILEHEADER));
memcpy((char *)&DstBi,(char *)&bi,sizeof(BITMAPINFOHEADER));
DstBf.bfSize=DstBufSize+sizeof(BITMAPFILEHEADER);
DstBi.biWidth=Wnew;
DstBi.biHeight=Hnew;
memcpy(lpTempPtr,(char *)&DstBi,sizeof(BITMAPINFOHEADER));
for(y1=0;y1<Hnew;y1++)
for(x1=0;x1<Wnew;x1++){
x0= y1;
y0= x1;
lpPtr=(char *)lpImgData+(SrcBufSize-LineBytes-y0*LineBytes)+x0;
lpTempPtr=(char *)lpTempImgData+
(DstBufSize-DstLineBytes-y1*DstLineBytes)+x1;
*lpTempPtr=*lpPtr;
}
hDc=GetDC(hWnd);
if(hBitmap!=NULL)
DeleteObject(hBitmap);
hBitmap=CreateDIBitmap(hDc,(LPBITMAPINFOHEADER)lpTempImgData,
(LONG)CBM_INIT,
(LPSTR)lpTempImgData+
sizeof(BITMAPINFOHEADER)+
NumColors*sizeof(RGBQUAD),
(LPBITMAPINFO)lpTempImgData, DIB_RGB_COLORS);
hf=_lcreat("c://transpose.bmp",0);
_lwrite(hf,(LPSTR)&DstBf,sizeof(BITMAPFILEHEADER));
_lwrite(hf,(LPSTR)lpTempImgData,DstBufSize);
_lclose(hf);
ReleaseDC(hWnd,hDc);
LocalUnlock(hTempImgData);
LocalFree(hTempImgData);
GlobalUnlock(hImgData);
return TRUE;
}
2.5 缩放
假设放大因子为ratio,(为了避免新图过大或过小,我们在程序中限制0.25≤ratio≤4),缩放(zoom)的变换矩阵很简单:
(2.13)
缩放变换的源代码如下,因为和转置的那段程序很类似,程序中的注释就简单一些。
float ZoomRatio=0.25f; //缩放比例,初始化为0.25
BOOL Zoom(HWND hWnd)
{
DLGPROC dlgInputBox = NULL;
DWORD OffBits,SrcBufSize,DstBufSize,DstLineBytes;
LPBITMAPINFOHEADER lpImgData;
LPSTR lpPtr;
HLOCAL hTempImgData;
LPBITMAPINFOHEADER lpTempImgData;
LPSTR lpTempPtr;
DWORD Wold,Hold,Wnew,Hnew;
HDC hDc;
HFILE hf;
DWORD x0,y0,x1,y1;
float num1;
BITMAPFILEHEADER DstBf;
BITMAPINFOHEADER DstBi;
//出现对话框,输入缩放比例
dlgInputBox = (DLGPROC) MakeProcInstance ( (FARPROC)InputBox, ghInst );
DialogBox (ghInst, "INPUTBOX", hWnd, dlgInputBox);
FreeProcInstance ( (FARPROC) dlgInputBox );
num1=(float)(1.0/ZoomRatio);
//原图宽度和高度
Wold=bi.biWidth;
Hold=bi.biHeight;
//新图宽度和高度
Wnew = (DWORD)(Wold*ZoomRatio+0.5);
Hnew = (DWORD)(Hold*ZoomRatio+0.5);
OffBits=bf.bfOffBits-sizeof(BITMAPFILEHEADER);
SrcBufSize=OffBits+bi.biHeight*LineBytes;
ImgWidth=Wnew;
ImgHeight=Hnew;
DstLineBytes=(DWORD)WIDTHBYTES(Wnew*bi.biBitCount);
DstBufSize=(DWORD)(sizeof(BITMAPINFOHEADER)+
NumColors*sizeof(RGBQUAD)+
(DWORD)DstLineBytes*Hnew);
if((hTempImgData=LocalAlloc(LHND,DstBufSize))==NULL)
{
MessageBox(hWnd,"Error alloc memory!","Error Message",MB_OK|
MB_ICONEXCLAMATION);
return FALSE;
}
lpImgData=(LPBITMAPINFOHEADER)GlobalLock(hImgData);
lpTempImgData=(LPBITMAPINFOHEADER)LocalLock(hTempImgData);
lpPtr=(char *)lpImgData;
lpTempPtr=(char *)lpTempImgData;
memset(lpTempPtr,(BYTE)255,DstBufSize);
memcpy(lpTempPtr,lpPtr,OffBits);
//头信息中做一些必要的改变,这一点非常重要
memcpy((char *)&DstBf,(char *)&bf,sizeof(BITMAPFILEHEADER));
memcpy((char *)&DstBi,(char *)&bi,sizeof(BITMAPINFOHEADER));
DstBf.bfSize=DstBufSize+sizeof(BITMAPFILEHEADER);
DstBi.biWidth=Wnew;
DstBi.biHeight=Hnew;
memcpy(lpTempPtr,(char *)&DstBi,sizeof(BITMAPINFOHEADER));
for(y1=0;y1<Hnew;y1++)
for(x1=0;x1<Wnew;x1++){
x0= (DWORD)(x1*num1);
y0= (DWORD)(y1*num1);
if( (x0>=0) && (x0<Wold) && (y0>=0) && (y0<Hold))
{
lpPtr=(char*)lpImgData+
(SrcBufSize-LineBytes-y0*LineBytes)+x0;
lpTempPtr=(char *)lpTempImgData+
(DstBufSize-DstLineBytes-y1*DstLineBytes)+x1;
*lpTempPtr=*lpPtr;
}
}
hDc=GetDC(hWnd);
if(hBitmap!=NULL)
DeleteObject(hBitmap);
hBitmap=CreateDIBitmap(hDc,(LPBITMAPINFOHEADER)lpTempImgData,
(LONG)CBM_INIT,
(LPSTR)lpTempImgData+
sizeof(BITMAPINFOHEADER)+
NumColors*sizeof(RGBQUAD),
(LPBITMAPINFO)lpTempImgData, DIB_RGB_COLORS);
hf=_lcreat("c://zoom.bmp",0);
_lwrite(hf,(LPSTR)&DstBf,sizeof(BITMAPFILEHEADER));
_lwrite(hf,(LPSTR)lpTempImgData,DstBufSize);
_lclose(hf);
ReleaseDC(hWnd,hDc);
LocalUnlock(hTempImgData);
LocalFree(hTempImgData);
GlobalUnlock(hImgData);
return TRUE;
}
由于放大图象时产生了新的象素,以及浮点数的操作,得到的坐标可能并不是整数,这一点我们在介绍旋转时就提到了。我们采用的做法是找与之最临近的点。实际上,更精确的做法是采用插值(interpolation),即利用邻域的象素来估计新的象素值。其实我们前面的做法也是一种插值,称为最邻近插值(Nearest Neighbour Interpolation)。下面先介绍线形插值(Linear Interpolation)。
线形插值使用原图中两个值来构造所求坐标处的值。举一个一维的例子。如图2.16所示,如果已经知道了两点x0,x2处的函数值f(x0),f(x2),现在要求x1处的函数值f(x1)。我们假设函数是线形的,利用几何知识可以知道
f(x1)=(f(x2)-f(x0))(x1-x0)/(x2-x0)+f(x0)
(2.13)
在图象处理中需要将线形插值扩展到二维的情况,即采用双线形插值(Bilinear Intrepolation),图2.17为双线形插值的示意图。
|
图.217 双线形插值的示意图 |
已知a、b、c、d四点的灰度,要求e点的灰度,可以先在水平方向上由a,b线形插值求出g、c、d线形插值求出f,然后在垂直方向上由g,f线形插值求出e。
线形插值基于这样的假设:原图的灰度在两个象素之间是线形变化的。一般情况下,这种插值的效果还不错。更精确的方法是采用曲线插值(Curvilinear Interpolation),即认为象素之间的灰度变化规律符合某种曲线,但这种处理的计算量是很大的。
关于插值,我们就介绍到这里,有兴趣的读者可以参考“数值分析”方面的书籍。
3.1 平滑
先举个例子说明一下什么是平滑(smoothing),如下面两幅图所示:可以看到,图3.2比图3.1柔和一些(也模糊一些)。是不是觉得很神奇?其实实现起来很简单。我们将原图中的每一点的灰度和它周围八个点的灰度相加,然后除以9,作为新图中对应点的灰度,就能实现上面的效果。
图3.1 原图 |
图3.2 经过平滑处理后的图 |
这么做并非瞎蒙,而是有其道理的。大概想一想,也很容易明白。举个例子,就象和面一样,先在中间加点水,然后不断把周围的面和进来,搅拌几次,面就均匀了。
用信号处理的理论来解释,这种做法实现的是一种简单的低通滤波器(low pass filter)。哇,好深奥呀!不要紧,这些理论的内容并不多,而且知道一些理论也是很有好处的。在灰度连续变化的图象中,如果出现了与相邻象素的灰度相差很大的点,比如说一片暗区中突然出现了一个亮点,人眼能很容易觉察到。就象看老电影时,由于胶片太旧,屏幕上经常会出现一些亮斑。这种情况被认为是一种噪声。灰度突变在频域中代表了一种高频分量,低通滤波器的作用就是滤掉高频分量,从而达到减少图象噪声的目的。
为了方便地叙述上面所说的“将原图中的每一点的灰度和它周围八个点的灰度相加,然后除以9,作为新图中对应点的灰度”这一操作,我们采用如下的表示方法:
(3.1)
这种表示方法有点象矩阵,我们称其为模板(template)。中间的黑点表示中心元素,即,用哪个元素做为处理后的元素。例如[2. 1]表示将自身的2倍加上右边的元素作为新值,而[2 1.]表示将自身加上左边元素的2倍作为新值。
通常,模板不允许移出边界,所以结果图象会比原图小,例如模板是 ,原图是
,经过模板操作后的图象为
;其中数字代表灰度,x表示边界上无法进行模板操作的点,通常的做法是复制原图的灰度,不进行任何处理。
模板操作实现了一种邻域运算(Neighborhood Operation),即某个象素点的结果灰度不仅和该象素灰度有关,而且和其邻域点的值有关。在以后介绍的细化算法中,我们还将接触到邻域运算。模板运算的数学涵义是一种卷积(或互相关)运算,你不需要知道卷积的确切含义,只要有这么一个概念就可以了。
模板运算在图象处理中经常要用到,可以看出,它是一项非常耗时的运算。以
(3.2)
为例,每个象素完成一次模板操作要用9个乘法、8个加法、1个除法。对于一幅n×n(宽度×高度)的图象,就是9n2个乘法,8n2个加法和n2个除法,算法复杂度为O(n2),这对于大图象来说,是非常可怕的。所以,一般常用的模板并不大,如3×3,4×4。有很多专用的图象处理系统,用硬件来完成模板运算,大大提高了速度。另外,可以设法将二维模板运算转换成一维模板运算,对速度的提高也是非常可观的。例如,(3.2)式可以分解成一个水平模板和一个垂直模板,即,
=
×
=
(3.3)
我们来验证一下。
设图象为 ,经过(3.2)式处理后变为
,经过(3.3)式处理后变为
,两者完全一样。如果计算时不考虑周围一圈的象素,前者做了4×(9个乘法,8个加法,1个除法),共36个乘法,32个加法,4个除法;后者做了4×(3个乘法,2个加法)+4×(3个乘法,2个加法)+4个除法,共24个乘法,16个加法,4个除法,运算简化了不少,如果是大图,效率的提高将是非常客观的。
平滑模板的思想是通过将一点和周围8个点作平均,从而去除突然变化的点,滤掉噪声,其代价是图象有一定程度的模糊。上面提到的模板(3.1),就是一种平滑模板,称之为Box模板。Box模板虽然考虑了邻域点的作用,但并没有考虑各点位置的影响,对于所有的9个点都一视同仁,所以平滑的效果并不理想。实际上我们可以想象,离某点越近的点对该点的影响应该越大,为此,我们引入了加权系数,将原来的模板改造成 ,可以看出,距离越近的点,加权系数越大。
新的模板也是一个常用的平滑模板,称为高斯(Gauss)模板。为什么叫这个名字,这是因为这个模板是通过采样2维高斯函数得到的。
设图象为 ,分别用两种平滑模板处理(周围一圈象素直接从原图拷贝)。采用Box模板的结果为
,采用高斯模板的结果为
。
可以看到,原图中出现噪声的区域是第2行第2列和第3行第2列,灰度从2一下子跳到了6,用Box模板处理后,灰度从3.11跳到4.33;用高斯模板处理后,灰度从3.跳到4.56,都缓和了跳变的幅度,从这一点上看,两者都达到了平滑的目的。但是,原图中的第3,第4行总的来说,灰度值是比较高的,经模板1处理后,第3行第2列元素的灰度变成了4.33,与第3,第4行的总体灰度相比偏小,另外,原图中第3行第2列元素的灰度为6,第3行第3列元素的灰度为4,变换后,后者4.56反而比前者4.33大了。而采用高斯模板没有出现这些问题,究其原因,就是因为它考虑了位置的影响。
举个实际的例子:下图中,从左到右分别是原图,用高斯模板处理的图,用Box模板处理的图,可以看出,采用高斯模板,在实现平滑效果的同时,要比Box模板清晰一些。
在学习锐化后,我们将给出一个通用的3×3模板操作的程序。
图3.3 高斯模板和Box模板的对比图
3.2 中值滤波
中值滤波也是一种典型的低通滤波器,它的目的是保护图象边缘的同时去除噪声。所谓中值滤波,是指把以某点(x,y)为中心的小窗口内的所有象素的灰度按从大到小的顺序排列,将中间值作为(x,y)处的灰度值(若窗口中有偶数个象素,则取两个中间值的平均)。中值滤波是如何去除噪声的呢?举个例子就很容易明白了。
原图 | 处理后的图 |
|
|
图中数字代表该处的灰度。可以看出原图中间的6和周围的灰度相差很大,是一个噪声点。经过3×1窗口(即水平3个象素取中间值)的中值滤波,得到右边那幅图,可以看出,噪声点被去除了。
下面将中值滤波和上面介绍的两种平滑模板作个比较,看看中值滤波有什么特点。我们以一维模板为例,只考虑水平方向,大小为3×1(宽×高)。Box模板为 ,高斯模板为
。
先考察第一幅图:
原图 | 经Box模板处理后 | 经Gauss模板处理后 | 经中值滤波处理后 |
|
|
|
|
从原图中不难看出左边区域灰度值低,右边区域灰度值高,中间有一条明显的边界,这一类图象称之为“step”(就象灰度上了个台阶)。应用平滑模板后,图象平滑了,但是也使边界模糊了。应用中值滤波,就能很好地保持原来的边界。所以说,中值滤波的特点是保护图象边缘的同时去除噪声。
再看第二幅图:
原图 | 经Box模板处理后 | 经Gauss模板处理后 | 经中值滤波处理后 |
|
|
|
|
不难看出,原图中有很多噪声点(灰度为正代表灰度值高的点,灰度为负代表灰度值低的点),而且是杂乱无章,随机分布的。这也是一类很典型的图,称之为高斯噪声。经过Box平滑,噪声的程度有所下降。Gauss模板对付高斯噪声非常有效。而中值滤波对于高斯噪声则无能为力。
最后看第三幅图:
原图 | 经Box模板处理后 | 经Gauss模板处理后 | 经中值滤波处理后 |
|
|
|
|
从原图中不难看出,中间的灰度要比两边高许多。这也是一类很典型的图,称之为脉冲 (impulse)。可见,中值滤波对脉冲噪声非常有效。
综合以上三类图,不难得出下面的结论:中值滤波容易去除孤立点,线的噪声同时保持图象的边缘;它能很好的去除二值噪声,但对高斯噪声无能为力。要注意的是,当窗口内噪声点的个数大于窗口宽度的一半时,中值滤波的效果不好。这是很显然的。
下面的程序实现了中值滤波,参数Hori是一个布尔变量,若为真,做水平中值滤波,否则,做垂直中值滤波。
BOOL MedianFilter(HWND hWnd,BOOL Hori)
{
DWORD OffBits,BufSize;
LPBITMAPINFOHEADER lpImgData;
LPSTR lpPtr;
HLOCAL hTempImgData;
LPBITMAPINFOHEADER lpTempImgData;
LPSTR lpTempPtr;
HDC hDc;
HFILE hf;
LONG x,y;
int g,g1,g2,g3;
//OffBits为BITMAPINFOHEADER结构长度加调色板的大小
OffBits=bf.bfOffBits-sizeof(BITMAPFILEHEADER);
BufSize=OffBits+bi.biHeight*LineBytes;//要开的缓冲区的大小
if((hTempImgData=LocalAlloc(LHND,BufSize))==NULL)
{
MessageBox(hWnd,"Error alloc memory!","Error Message",MB_OK|
MB_ICONEXCLAMATION);
return FALSE;
}
lpImgData=(LPBITMAPINFOHEADER)GlobalLock(hImgData);
lpTempImgData=(LPBITMAPINFOHEADER)LocalLock(hTempImgData);
//拷贝头信息及位图数据
memcpy(lpTempImgData,lpImgData,BufSize);
//注意边界点不处理,所以y从1到高度-2,x类似
for(y=1;y<bi.biHeight-1;y++)
for(x=1;x<bi.biWidth-1;x++){
lpPtr=(char *)lpImgData+(BufSize-LineBytes-y*LineBytes)+x;
lpTempPtr=(char*)lpTempImgData+
(BufSize-LineBytes-y*LineBytes)+x;
g2=(unsigned char)*(lpPtr);
if(Hori){ //水平方向
g1=(unsigned char)*(lpPtr-1); //左邻点
g3=(unsigned char)*(lpPtr+1); //右邻点
}
else{ //垂直方向
g1=(unsigned char)*(lpPtr+LineBytes); //上邻点
g3=(unsigned char)*(lpPtr-LineBytes); //下邻点
}
//三者取中
if(g1>g2){
if(g2>g3) g=g2;
else{
if(g1>g3) g=g3;
else g=g1;
}
}
else{ //g1<=g2
if(g1>g3) g=g1;
else{
if(g2>g3) g=g3;
else g=g2;
}
}
*lpTempPtr=(BYTE)g; //存入新的缓冲区内
}
hDc=GetDC(hWnd);
if(hBitmap!=NULL)
DeleteObject(hBitmap);
//产生新的位图
hBitmap=CreateDIBitmap(hDc,(LPBITMAPINFOHEADER)lpTempImgData,
(LONG)CBM_INIT,
(LPSTR)lpTempImgData+
sizeof(BITMAPINFOHEADER)+
NumColors*sizeof(RGBQUAD),
(LPBITMAPINFO)lpTempImgData,
DIB_RGB_COLORS);
if(Hori) //取不同的结果文件名
hf=_lcreat("c://hmedian.bmp",0);
else
hf=_lcreat("c://vmedian.bmp",0);
_lwrite(hf,(LPSTR)&bf,sizeof(BITMAPFILEHEADER));
_lwrite(hf,(LPSTR)lpTempImgData,BufSize);
_lclose(hf);
//释放内存及资源
ReleaseDC(hWnd,hDc);
LocalUnlock(hTempImgData);
LocalFree(hTempImgData);
GlobalUnlock(hImgData);
return TRUE;
}
3.3 锐化
锐化(sharpening)和平滑恰恰相反,它是通过增强高频分量来减少图象中的模糊,因此又称为高通滤波(high pass filter)。锐化处理在增强图象边缘的同时增加了图象的噪声。
常用的锐化模板是拉普拉斯(Laplacian)模板(见(3.4)式),又是个数学家的名字,可见学好数学,走遍天下都不怕。
(3.4)
容易看出拉普拉斯模板的作法:先将自身与周围的8个象素相减,表示自身与周围象素的差别;再将这个差别加上自身作为新象素的灰度。可见,如果一片暗区出现了一个亮点,那么锐化处理的结果是这个亮点变得更亮,增加了图象的噪声。
因为图象中的边缘就是那些灰度发生跳变的区域,所以锐化模板在边缘检测中很有用,这一点将在后面详细介绍。
图3.1经过拉普拉斯模板处理后,如图3.4所示
图3.4 锐化
下面给出的程序是一个通用的3×3模板的函数,其中第二参数为模板类型,为如下定义的常量:
#define TEMPLATE_SMOOTH_BOX 1 //Box平滑模板
#define TEMPLATE_SMOOTH_GAUSS 2 //高斯平滑模板
#define TEMPLATE_SHARPEN_LAPLACIAN 3 //拉普拉斯锐化模板
对应的模板数组如下
int Template_Smooth_Box[9]={1,1,1,1,1,1,1,1,1};
int Template_Smooth_Gauss[9]={1,2,1,2,4,2,1,2,1};
int Template_Sharpen_Laplacian[9]={-1,-1,-1,-1,9,-1,-1,-1,-1};
以后我们碰到其它的模板,仍然要用这个函数,所做的操作只是增加一个常量标识,及其对应的模板数组。
要注意的是,运算后如果出现了大于255或者小于0的点,称为溢出,溢出点的处理通常是截断,即大于255时,令其等于255;小于0时,取其绝对值。
这段程序和前几章介绍的代码许多地方是很相似的,所以注释简单一些。程序中并没有用到那种分解成两个一维模板的快速算法,你如果有兴趣,可以自己编着试试。
BOOL TemplateOperation(HWND hWnd, int TemplateType)
{
DWORD OffBits,BufSize;
LPBITMAPINFOHEADER lpImgData;
LPSTR lpPtr;
HLOCAL hTempImgData;
LPBITMAPINFOHEADER lpTempImgData;
LPSTR lpTempPtr;
HDC hDc;
HFILE hf;
LONG x,y;
float coef; //模板前面所乘的系数
int CoefArray[9]; //模板数组
float TempNum;
char filename[80];
switch(TemplateType){ //判断模板类型
case TEMPLATE_SMOOTH_BOX: //Box平滑模板
coef=(float)(1.0/9.0);
memcpy(CoefArray,Template_Smooth_Box,9*sizeof(int));
strcpy(filename,"c://smbox.bmp");
break;
case TEMPLATE_SMOOTH_GAUSS: //高斯平滑模板
coef=(float)(1.0/16.0);
memcpy(CoefArray,Template_Smooth_Gauss,9*sizeof(int));
strcpy(filename,"c://smgauss.bmp");
break;
case TEMPLATE_SHARPEN_LAPLACIAN: //拉普拉斯锐化模板
coef=(float)1.0;
memcpy(CoefArray,Template_Sharpen_Laplacian,9*sizeof(int));
strcpy(filename,"c://shlaplac.bmp");
break;
}
OffBits=bf.bfOffBits-sizeof(BITMAPFILEHEADER);
BufSize=OffBits+bi.biHeight*LineBytes;
if((hTempImgData=LocalAlloc(LHND,BufSize))==NULL)
{
MessageBox(hWnd,"Error alloc memory!","Error Message",MB_OK|
MB_ICONEXCLAMATION);
return FALSE;
}
lpImgData=(LPBITMAPINFOHEADER)GlobalLock(hImgData);
lpTempImgData=(LPBITMAPINFOHEADER)LocalLock(hTempImgData);
lpPtr=(char *)lpImgData;
lpTempPtr=(char *)lpTempImgData;
//先将原图直接拷贝过来,其实主要是拷贝周围一圈的象素
memcpy(lpTempPtr,lpPtr,BufSize);
for(y=1;y<bi.biHeight-1;y++) //注意y的范围是从1到bi.biHeight-2
for(x=1;x<bi.biWidth-1;x++){ //注意x的范围是从1到bi.biWidth-2
lpPtr=(char *)lpImgData+(BufSize-LineBytes-y*LineBytes)+x;
lpTempPtr=(char*)lpTempImgData+
(BufSize-LineBytes-y*LineBytes)+x;
TempNum=(float)((unsigned char)*(lpPtr+LineBytes-1))*
CoefArray[0];
TempNum+=(float)((unsigned char)*(lpPtr+LineBytes))*
CoefArray[1];
TempNum+=(float)((unsigned char)*(lpPtr+LineBytes+1))*
CoefArray[2];
TempNum+=(float)((unsigned char)*(lpPtr-1))*CoefArray[3];
TempNum+=(float)((unsigned char)*lpPtr)*CoefArray[4];
TempNum+=(float)((unsigned char)*(lpPtr+1))*CoefArray[5];
TempNum+=(float)((unsigned char)*(lpPtr-LineBytes-1))*
CoefArray[6];
TempNum+=(float)((unsigned char)*(lpPtr-LineBytes))*
CoefArray[7];
TempNum+=(float)((unsigned char)*(lpPtr-LineBytes+1))*
CoefArray[8];
//最后乘以系数
TempNum*=coef;
//注意对溢出点的处理
if(TempNum>255.0) *lpTempPtr=(BYTE)255;
else if(TempNum<0.0)
*lpTempPtr=(unsigned char)fabs(TempNum);
else *lpTempPtr=(BYTE)TempNum;
}
hDc=GetDC(hWnd);
if(hBitmap!=NULL)
DeleteObject(hBitmap);
hBitmap=CreateDIBitmap(hDc,(LPBITMAPINFOHEADER)lpTempImgData,
(LONG)CBM_INIT,
(LPSTR)lpTempImgData+
sizeof(BITMAPINFOHEADER)+
NumColors*sizeof(RGBQUAD),
(LPBITMAPINFO)lpTempImgData,
DIB_RGB_COLORS);
hf=_lcreat(filename,0);
_lwrite(hf,(LPSTR)&bf,sizeof(BITMAPFILEHEADER));
_lwrite(hf,(LPSTR)lpTempImgData,BufSize);
_lclose(hf);
ReleaseDC(hWnd,hDc);
LocalUnlock(hTempImgData);
LocalFree(hTempImgData);
GlobalUnlock(hImgData);
return TRUE;
}
在介绍本章内容之前,先提出一个问题?普通的黑白针式打印机能打出灰度图来吗?如果说能,从针式打印机的打印原理来分析,似乎是不可能的。因为针打是靠撞针击打色带在纸上形成黑点的,不可能打出灰色的点来;如果说不能,可是我们的确见过用针式打印机打印出来的灰色图象。到底是怎么回事呢?
你再仔细看看那些打印出来的所谓的灰色图象,最好用放大镜看。你会发现,原来这些灰色图象都是由一些黑点组成的,黑点多一些,图象就暗一些;黑点少一些,图案就亮一些。下面这几张图就很能说明这一点。
图4.1 用黑白两种颜色打印出灰度效果
图4.1中最左边的是原图,是一幅真正的灰度图,另外三张图都是黑白二值图。容易看出,最左的那幅和原图最接近。
由二值图象显示出灰度效果的方法,就是我们今天要讲的半影调(halftone)技术,它的一个主要用途就是在只有二值输出的打印机上打印图象。我们介绍两种方法:图案法和抖动法。
4.1 图案法
图案法(patterning)是指灰度可以用一定比例的黑白点组成的区域表示,从而达到整体图象的灰度感。黑白点的位置选择称为图案化。
在具体介绍图案法之前,先介绍一下分辨率的概念。计算机显示器,打印机,扫描仪等设备的一个重要指标就是分辨率,单位是dpi(dot per inch),即每英寸点数,点数越多,分辨率就越高,图象就越清晰。让我们来计算一下,计算机显示器的分辨率有多高。设显示器为15英寸(指对角线长度),最多显示1280×1024个点。因为宽高比为4:3,所以宽有12英寸,高有9英寸,则该显示器的水平分辨率为106dpi,垂直分辨率为113.8dpi。一般的激光打印机的分辨率有300dpi×300dpi,600dpi×600dpi,720dpi×720dpi。所以打出来的图象要比计算机显示出来的清晰的多。扫描仪的分辨率要高一些,数码相机的分辨率更高。
言归正传,前面讲了,图案化使用图案来表示象素的灰度,那么我们来做一道计算题。假设有一幅240×180×8bit的灰度图,当用分辨率为300dpi×300dpi的激光打印机将其打印到12.8×9.6英寸的纸上时,每个象素的图案有多大?
这道题很简单,这张纸最多可以打(300×12.8) ×(300×9.6)=3840×2880个点,所以每个象素可以用(3840/240)×(2880/180)=16×16个点大小的图案来表示,即一个象素256个点。如果这16×16的方块中一个黑点也没有,就可以表示灰度256;有一个黑点,就表示灰度255;依次类推,当都是黑点时,表示灰度0。这样,16×16的方块可以表示257级灰度,比要求的8bit共256级灰度还多了一个。所以上面的那幅图的灰度级别完全能够打印出来。
这里有一个图案构成的问题,即黑点打在哪里?比如说,只有一个黑点时,我们可以打在正中央,也可以打16×16的左上角。图案可以是规则的,也可以是不规则的。一般情况下,有规则的图案比随即图案能够避免点的丛集,但有时会导致图象中有明显的线条。
如图4.1中,2×2的图案可以表示5级灰度,当图象中有一片灰度为的1的区域时,如图4.2所示,有明显的水平和垂直线条。
图4.2 2×2的图案 |
图4.3 规则图案导致线条 |
如果想存储256级灰度的图案,就需要256×16×16的二值点阵,占用的空间还是相当可观的。有一个更好的办法是:只存储一个整数矩阵,称为标准图案,其中的每个值从0到255。图象的实际灰度和阵列中的每个值比较,当该值大于等于灰度时,对应点打一黑点。下面举一个25级灰度的例子加以说明。
图4.4 标准图案举例
图4.4中,左边为标准图案,右边为灰度为15的图案,共有10个黑点,15个白点。其实道理很简单,灰度为0时全是黑点,灰度每增加1,减少一个黑点。要注意的是,5×5的图案可以表示26种灰度,当灰度是25才是全白点,而不是灰度为24时。
下面介绍一种设计标准图案的算法,是由Limb在1969年提出的。
先以一个2×2的矩阵开始:设M1= ,通过递归关系有Mn+1=
,其中Mn和Un均为2n×2n的方阵,Un的所有元素都是1。根据这个算法,可以得到M2=
,为16级灰度的标准图案。
M3(8×8阵)比较特殊,称为Bayer抖动表。M4是一个16×16的矩阵。
根据上面的算法,如果利用M3一个象素要用8×8的图案表示,则一幅N×N的图将变成8N×8N大小。如果利用M4,就更不得了,变成16N×16N了。能不能在保持原图大小的情况下利用图案化技术呢?一种很自然的想法是:如果用M2阵,则将原图中每8×8个点中取一点,即重新采样,然后再应用图案化技术,就能够保持原图大小。实际上,这种方法并不可行。首先,你不知道这8×8个点中找哪一点比较合适,另外,8×8的间隔实在太大了,生成的图象和原图肯定相差很大,就象图4.1最右边的那幅图一样。
我们可以采用这样的做法:假设原图是256级灰度,利用Bayer抖动表,做如下处理
if (g[y][x]>>2) > bayer[y&7][x&7] then 打一白点 else 打一黑点
其中,x,y代表原图的象素坐标,g[y][x]代表该点灰度。首先将灰度右移两位,变成64级,然后将x,y做模8运算,找到Bayer表中的对应点,两者做比较,根据上面给出的判据做处理。
我们可以看到,模8运算使得原图分成了一个个8×8的小块,每个小块和8×8的Bayer表相对应。小块中的每个点都参与了比较,这样就避免了上面提到的选点和块划分过大的问题。模8运算实质上是引入了随机成分,这就是我们下面要讲到的抖动技术。
图4.5就是利用了这个算法,使用M3(Bayer抖动表)阵得到的;图6是使用M4阵得到的,可见两者的差别并不是很大,所以一般用Bayer表就可以了。
图4.5 利用M3抖动生成的图
图4.6 利用M4抖动生成的图
下面是算法的源程序,是针对Bayer表的。因为它是个常用的表,我们不再利用Limb公式,而是直接给出。针对M4阵的算法是类似的,不同的地方在于,要用Limb公式得到M4阵,灰度也不用右移2位。要注意的是,为了处理的方便,我们的结果图仍采用256级灰度图,不过只用到了0和255两种灰度。
BYTE BayerPattern[8][8]={ 0,32,8,40,2,34,10,42,
48,16,56,24,50,18,58,26,
12,44,4,36,14,46,6,38,
60,28,52,20,62,30,54,22,
3,35,11,43,1,33,9,41,
51,19,59,27,49,17,57,25,
15,47,7,39,13,45,5,37,
63,31,55,23,61,29,53,21};
BOOL LimbPatternM3(HWND hWnd)
{
DWORD OffBits,BufSize
LPBITMAPINFOHEADER lpImgData;
LPSTR lpPtr;
HLOCAL hTempImgData;
LPBITMAPINFOHEADER lpTempImgData;
LPSTR lpTempPtr;
HDC hDc;
HFILE hf;
LONG x,y;
unsigned char num;
OffBits=bf.bfOffBits-sizeof(BITMAPFILEHEADER);
BufSize=OffBits+bi.biHeight*LineBytes;//要开的缓冲区大小
if((hTempImgData=LocalAlloc(LHND,BufSize))==NULL)
{
MessageBox(hWnd,"Error alloc memory!","Error Message",MB_OK|
MB_ICONEXCLAMATION);
return FALSE;
}
lpImgData=(LPBITMAPINFOHEADER)GlobalLock(hImgData);
lpTempImgData=(LPBITMAPINFOHEADER)LocalLock(hTempImgData);
//拷贝头信息和位图数据
memcpy(lpTempImgData,lpImgData,BufSize);
for(y=0;y<bi.biHeight;y++){
//lpPtr为指向原图位图数据的指针
lpPtr=(char *)lpImgData+(BufSize-LineBytes-y*LineBytes);
//lpTempPtr为指向新图位图数据的指针
lpTempPtr=(char *)lpTempImgData+(BufSize-LineBytes-y*LineBytes);
for(x=0;x<bi.biWidth;x++){
num=(unsigned char)*lpPtr++;
if ( (num>>2) > BayerPattern[y&7][x&7]) //右移两位后做比较
*(lpTempPtr++)=(unsigned char)255; //打白点
else *(lpTempPtr++)=(unsigned char)0; //打黑点
}
}
if(hBitmap!=NULL)
DeleteObject(hBitmap);
hDc=GetDC(hWnd);
//形成新的位图
hBitmap=CreateDIBitmap(hDc,(LPBITMAPINFOHEADER)lpTempImgData,
(LONG)CBM_INIT,
(LPSTR)lpTempImgData+
sizeof(BITMAPINFOHEADER)+
NumColors*sizeof(RGBQUAD),
(LPBITMAPINFO)lpTempImgData,
DIB_RGB_COLORS);
hf=_lcreat("c://limbm3.bmp",0);
_lwrite(hf,(LPSTR)&bf,sizeof(BITMAPFILEHEADER));
_lwrite(hf,(LPSTR)lpTempImgData,BufSize);
_lclose(hf);
//释放内存和资源
ReleaseDC(hWnd,hDc);
LocalUnlock(hTempImgData);
LocalFree(hTempImgData);
GlobalUnlock(hImgData);
return TRUE;
}
4.2 抖动法
让我们考虑更坏的情况:即使使用了图案化技术,仍然得不到要求的灰度级别。举例说明:假设有一幅600×450×8bit的灰度图,当用分辨率为300dpi×300dpi的激光打印机将其打印到8×6英寸的纸上时,每个象素可以用(2400/600)×(1800/450)=4×4个点大小的图案来表示,最多能表示17级灰度,无法满足256级灰度的要求。可有两种解决方案:(1)减小图象尺寸,由600×450变为150×113;(2)降低图象灰度级,由256级变成16级。这两种方案都不理想。这时,我们就可以采用“抖动法”(dithering)的技术来解决这个问题。其实刚才给出的算法就是一种抖动算法,称为规则抖动(regular dithering)。规则抖动的优点是算法简单;缺点是图案化有时很明显,这是因为取模运算虽然引入了随机成分,但还是有规律的。另外,点之间进行比较时,只要比标准图案上点的值大就打白点,这种做法并不理想,因为,如果当标准图案点的灰度值本身就很小,而图象中点的灰度只比它大一点儿时,图象中的点更接近黑色,而不是白色。一种更好的方法是将这个误差传播到邻近的象素。
下面介绍的Floyd-Steinberg算法就采用了这种方案。
假设灰度级别的范围从b(black)到w(white),中间值t为(b+w)/2,对应256级灰度,b=0,w=255,t=127.5。设原图中象素的灰度为g,误差值为e,则新图中对应象素的值用如下的方法得到:
if g > t then
打白点
e=g-w
else
打黑点
e=g-b
3/8 × e 加到右边的象素
3/8 × e 加到下边的象素
1/4 × e 加到右下方的象素
算法的意思很明白:以256级灰度为例,假设一个点的灰度为130,在灰度图中应该是一个灰点。由于一般图象中灰度是连续变化的,相邻象素的灰度值很可能与本象素非常接近,所以该点及周围应该是一片灰色区域。在新图中,130大于128,所以打了白点,但130离真正的白点255还差的比较远,误差e=130-255=-125比较大。,将3/8×(-125)加到相邻象素后,使得相邻象素的值接近0而打黑点。下一次,e又变成正的,使得相邻象素的相邻象素打白点,这样一白一黑一白,表现出来刚好就是灰色。如果不传递误差,就是一片白色了。再举个例子,如果一个点的灰度为250,在灰度图中应该是一个白点,该点及周围应该是一片白色区域。在新图中,虽然e=-5也是负的,但其值很小,对相邻象素的影响不大,所以还是能够打出一片白色区域来。这样就验证了算法的正确性。其它的情况你可以自己推敲。图4.7是利用Floyd-Steinberg算法抖动生成的图。
图4.7 利用Floyd-Steinberg算法抖动生成的图
下面我们给出Floyd-Steinberg算法的源代码。有一点要说明,我们原来介绍的程序都是先开一个char类型的缓冲区,用来存储新图数据,但在这个算法中,因为e有可能是负数,为了防止得到的值超出char能表示的范围,我们使用了一个int类型的缓冲区存储新值。另外,当按从左到右,从上到下的顺序处理象素时,处理过的象素以后不会再用到了,所以用这个int类型的缓冲区存储新值是可行的。全部象素处理完后,再将这些值拷贝到char类型的缓冲区去。
BOOL Steinberg(HWND hWnd)
{
DWORD OffBits,BufSize,IntBufSize;
LPBITMAPINFOHEADER lpImgData;
HLOCAL hTempImgData;
LPBITMAPINFOHEADER lpTempImgData;
LPSTR lpPtr;
LPSTR lpTempPtr;
HDC hDc;
HFILE hf;
LONG x,y;
unsigned char num;
float e,f;
HLOCAL hIntBuf;
int *lpIntBuf,*lpIntPtr;
int tempnum;
//OffBits为BITMAPINFOHEADER结构长度加调色板的大小
OffBits=bf.bfOffBits-sizeof(BITMAPFILEHEADER);
BufSize=OffBits+bi.biHeight*LineBytes;//要开的缓冲区的大小
if((hTempImgData=LocalAlloc(LHND,BufSize))==NULL)
{
MessageBox(hWnd,"Error alloc memory!","Error Message",MB_OK|
MB_ICONEXCLAMATION);
return FALSE;
}
IntBufSize=(DWORD)bi.biHeight*LineBytes*sizeof(int); if((hIntBuf=LocalAlloc(LHND,IntBufSize))==NULL) //int 类型的缓冲区
{
MessageBox(hWnd,"Error alloc memory!","Error Message",MB_OK|
MB_ICONEXCLAMATION);
LocalFree(hTempImgData);
return FALSE;
}
lpImgData=(LPBITMAPINFOHEADER)GlobalLock(hImgData);
lpTempImgData=(LPBITMAPINFOHEADER)LocalLock(hTempImgData);
lpIntBuf=(int *)LocalLock(hIntBuf);
//拷贝头信息
memcpy(lpTempImgData,lpImgData,OffBits);
//将图象数据拷贝到int类型的缓冲区中
for(y=0;y<bi.biHeight;y++){
lpPtr=(char *)lpImgData+(BufSize-LineBytes-y*LineBytes);
lpIntPtr=(int *)lpIntBuf+(bi.biHeight-1-y)*LineBytes;
for(x=0;x<bi.biWidth;x++)
*(lpIntPtr++)=(unsigned char)*(lpPtr++);
}
for(y=0;y<bi.biHeight;y++){
for(x=0;x<bi.biWidth;x++){
lpIntPtr=(int *)lpIntBuf+(bi.biHeight-1-y)*LineBytes+x;
num=(unsigned char)*lpIntPtr;
if ( num > 128 ){ //128是中值
*lpIntPtr=255; //打白点
e=(float)(num-255.0); //计算误差
}
else{
*lpIntPtr=0; //打黑点
e=(float)num; //计算误差
}
if(x<bi.biWidth-1){ //注意判断边界
f=(float)*(lpIntPtr+1);
f+=(float)( (3.0/8.0) * e);
*(lpIntPtr+1)=(int)f; //向左传播
}
if(y<bi.biHeight-1){ //注意判断边界
f=(float)*(lpIntPtr-LineBytes);
f+=(float)( (3.0/8.0) * e);
*(lpIntPtr-LineBytes)=(int)f; //向下传播
f=(float)*(lpIntPtr-LineBytes+1);
f+=(float)( (1.0/4.0) * e);
*(lpIntPtr-LineBytes+1)=(int)f; //向右下传播
}
}
}
//从int类型的缓冲区拷贝到char类型的缓冲区
for(y=0;y<bi.biHeight;y++){
lpTempPtr=(char *)lpTempImgData+(BufSize-LineBytes-y*LineBytes);
lpIntPtr=(int *)lpIntBuf+(bi.biHeight-1-y)*LineBytes;
for(x=0;x<bi.biWidth;x++){
tempnum=*(lpIntPtr++);
if(tempnum>255) tempnum=255;
else if (tempnum<0) tempnum=0;
*(lpTempPtr++)=(unsigned char)tempnum;
}
}
if(hBitmap!=NULL)
DeleteObject(hBitmap);
hDc=GetDC(hWnd);
//产生新的位图
hBitmap=CreateDIBitmap(hDc,(LPBITMAPINFOHEADER)lpTempImgData,
(LONG)CBM_INIT,
(LPSTR)lpTempImgData+
sizeof(BITMAPINFOHEADER)+
NumColors*sizeof(RGBQUAD),
(LPBITMAPINFO)lpTempImgData,
DIB_RGB_COLORS);
hf=_lcreat("c://steinberg.bmp",0);
_lwrite(hf,(LPSTR)&bf,sizeof(BITMAPFILEHEADER));
_lwrite(hf,(LPSTR)lpTempImgData,BufSize);
_lclose(hf);
//释放内存和资源
ReleaseDC(hWnd,hDc);
GlobalUnlock(hImgData);
LocalUnlock(hTempImgData);
LocalFree(hTempImgData);
LocalUnlock(hIntBuf);
LocalFree(hIntBuf);
return TRUE;
}
要注意的是,误差传播有时会引起流水效应,即误差不断向下,向右累加传播。解决的办法是:奇数行从左到右传播,偶数行从右到左传播。
4.3 将bmp文件转换为txt文件
在讲图案化技术时,我突然想到了一个非常有趣的应用,那就是bmp2txt。如果你喜欢上BBS(电子公告牌系统),你可能想做一个花哨的签名档。瞧,这是我好朋友Casper的签名档(见图4.8),胖乎乎的,是不是特别可爱?
图4.8 Casper的签名档
你仔细观察一下,就会发现,这是一幅全部由字符组成的图,因为在BBS中只能出现文本的东西。那么,这幅图是怎么做出来的呢?难道是自己一个字符一个字符拼出来的。当然不是了,有一种叫bmp2txt的应用程序(2的发音和“to”一样,所以如此命名),能把位图文件转换成和图案很相似的字符文本。是不是觉得很神奇?其实原理很简单,用到了和图案化技术类似的思想:首先将位图分成同样大小的小块,求出每一块灰度的平均值,然后和每个字符的灰度做比较,找出最接近的那个字符,来代表这一小块图象。那么,怎么确定字符的灰度呢?做下面的实验就明白了。
打开记事本(notepad),输入字符“1”,选定该字符,使其反色。按Alt+PrintScreen键拷贝窗口屏幕。打开画笔(paintbrush),粘贴;然后把图放到最大(×8),打开“查看”→“缩放” →“显示网格”菜单,如图4.9所示:
图4.9 字符“1”的灰度
数数字符“1”用了几个点?是22个。我想你已经明白了,字符的灰度和它所占的黑色点数有关,点越少,灰度值越大,空格字符的灰度最大,为全白,因为它一个黑点也没有;而字符“W”的灰度值就比较低了。每个字符的面积是8×16(宽×高),所以一个字符的灰度值可以用如下的公式计算(1-所占的黑点数/(8×16))×255。下面是可显示的字符,及对应的灰度,共有95个。这可是我辛辛苦苦整理出来的呦!
static char ch[95]={
' ',
'`','1','2','3','4','5','6','7','8','9','0','-','=','//',
'q','w','e','r','t','y','u','i','o','p','[',']',
'a','s','d','f','g','h','j','k','l',';','/'',
'z','x','c','v','b','n','m',',','.','/',
'~','!','@','#','$','%','^','&','*','(',')','_','+','|',
'Q','W','E','R','T','Y','U','I','O','P','{','}',
'A','S','D','F','G','H','J','K','L',':','"',
'Z','X','C','V','B','N','M','<','>','?'
};
static int gr[95]= {
0,
7,22,28,31,31,27,32,22,38,32,40, 6,12,20,38,32,26,20,24,40,
29,24,28,38,32,32,26,22,34,24,44,33,32,32,24,16, 6,22,26,22,
26,34,29,35,10, 6,20,14,22,47,42,34,40,10,35,21,22,22,16,14,
26,40,39,29,38,22,28,36,22,36,30,22,22,36,26,36,25,34,38,24,
36,22,12,12,26,30,30,34,39,42,41,18,18,22
};
下面的这段程序实现了bmp2txt的功能,结果存到文件bmp2txt.txt中。
BOOL Bmp2Txt(HWND hWnd)
{
DWORD OffBits,BufSize;
LPBITMAPINFOHEADER lpImgData;
LPSTR lpPtr;
HFILE hf;
int i, j, k,h,tint,grayindex;
char tchar;
int TransHeight, TransWidth;
//先用起泡排序,将灰度值按从小到大排列,同时调整对应的字符位置
for(i=0;i<94;i++)
for(j=i+1;j<95;j++){
if(gr[i]>gr[j]){
tchar=ch[i],tint=gr[i];
ch[i]=ch[j],gr[i]=gr[j];
ch[j]=tchar,gr[j]=tint;
}
}
//OffBits为BITMAPINFOHEADER结构长度加调色板的大小
OffBits=bf.bfOffBits-sizeof(BITMAPFILEHEADER);
BufSize=OffBits+bi.biHeight*LineBytes;//要开的缓冲区的大小
lpImgData=(LPBITMAPINFOHEADER)GlobalLock(hImgData);
TransWidth = bi.biWidth/8; //每行字符的个数
TransHeight = bi.biHeight/16; //共有多少行字符
hf=_lcreat("c://bmp2txt.txt",0);
for(i=0;i<TransHeight;i++){
for(j=0;j<TransWidth;j++){
grayindex=0;
for(k=0;k<16;k++)
for(h=0;h<8;h++){ //求出8*16小块中各象素灰度之和
lpPtr=(char*)lpImgData+
BufSize-LineBytes-(i*16+k)*LineBytes+
j*8+h;
grayindex+=(unsigned char)*lpPtr;
}
grayindex/=16*8; //除以整个面积
grayindex=gr[94]*grayindex/255;
k=0;
while(gr[k+1]<grayindex)
k++; //寻找灰度最接近的字符
_lwrite(hf,(char *)&ch[k],sizeof(char)); //将该字符写入文件中
}
tchar=(char)13;
_lwrite(hf,(char *)&tchar,sizeof(char));
tchar=(char)10;
_lwrite(hf,(char *)&tchar,sizeof(char)); //每行加一个回车换行符
}
_lclose(hf);
GlobalUnlock(hImgData);
return TRUE;
}
上面的程序中,只考虑了8×16小块的平均灰度,而没有考虑小块内部象素的灰度分布。更精确的方法是将图象8×16小块和字符8×16小块每两个对应点之间相减,做平方误差计算,找出有最小平方误差的那个字符,来代表这一小块图象。显然,计算量要比刚才的大得多。这里我们就不给出程序了,有兴趣的读者可以自己实现。
其实利用图案化技术,还可以实现更有趣的应用,如图4.10,你仔细看看,贝多芬的头像是由许多个音乐符号组成的!
图4.10 贝多芬的头像
5.1 反色
反色(invert)就是形成底片效果。例如,图5.2为图5.1反色后的结果。
图5.1 原图 |
图5.2 图5.1反色后的结果 |
反色有时是很有用的,比如,图5.1中黑色区域占绝大多数,这样打印起来很费墨,我们可以先进行反色处理后再打印。
反色的实际含义是将R、G、B值反转。若颜色的量化级别是256,则新图的R、G、B值为255减去原图的R、G、B值。这里针对的是所有图,包括真彩图、带调色板的彩色图(又称为伪彩色图)、和灰度图。针对不同种类有不同的处理。
先看看真彩图。我们知道真彩图不带调色板,每个象素用3个字节,表示R、G、B三个分量。所以处理很简单,把反转后的R、G、B值写入新图即可。
再来看看带调色板的彩色图,我们知道位图中的数据只是对应调色板中的一个索引值,我们只需要将调色板中的颜色反转,形成新调色板,而位图数据不用动,就能够实现反转。
灰度图是一种特殊的伪彩色图,只不过调色板中的R、G、B值 都是一样的而已。所以反转的处理和上面讲的一样。
这里,我想澄清一个概念。过去我们讲二值图时,一直都说成黑白图。二值位图一定是黑白的吗?答案是不一定。我们安装Windows95时看到的那幅setup.bmp是由蓝色和黑色组成的,但它实际上是二值图。原来,它的调色板中的两种颜色是黑与蓝,而不是黑与白。所以说二值图也可以是彩色的,只不过一般情况下是黑白图而已。
下面的程序实现了反色,注意其中真彩图和调色板位图处理时的差别。
BOOL Invert(HWND hWnd)
{
DWORD OffBits,BufSize;
LPBITMAPINFOHEADER lpImgData;
LPSTR lpPtr;
HLOCAL hTempImgData;
LPBITMAPINFOHEADER lpTempImgData;
LPSTR lpTempPtr;
HDC hDc;
HFILE hf;
LONG x,y;
LOGPALETTE *pPal;
HPALETTE hPrevPalette=NULL;
HLOCAL hPal;
DWORD i;
unsigned char Red,Green,Blue;
OffBits=bf.bfOffBits-sizeof(BITMAPFILEHEADER);
BufSize=OffBits+bi.biHeight*LineBytes; //新开缓冲区的大小
if((hTempImgData=LocalAlloc(LHND,BufSize))==NULL)
{
MessageBox(hWnd,"Error alloc memory!","Error Message",MB_OK|
MB_ICONEXCLAMATION);
return FALSE;
}
lpImgData=(LPBITMAPINFOHEADER)GlobalLock(hImgData);
lpTempImgData=(LPBITMAPINFOHEADER)LocalLock(hTempImgData);
//拷贝头信息
memcpy(lpTempImgData,lpImgData,BufSize);
hDc=GetDC(hWnd);
if(NumColors!=0){ //NumColors不为0说明是带调色板的
lpPtr=(char *)lpImgData+sizeof(BITMAPINFOHEADER);
//指向原图数据
lpTempPtr=(char *)lpTempImgData+sizeof(BITMAPINFOHEADER);
//指向新图数据
//为新调色板分配内存
hPal=LocalAlloc(LHND,sizeof(LOGPALETTE)+
NumColors*sizeof(PALETTEENTRY));
pPal =(LOGPALETTE *)LocalLock(hPal);
pPal->palNumEntries =(WORD) NumColors;
pPal->palVersion = 0x300;
for (i = 0; i < NumColors; i++) {
Blue=(unsigned char )(*lpPtr++);
Green=(unsigned char )(*lpPtr++);
Red=(unsigned char )(*lpPtr++);
lpPtr++;
//反转调色板中的颜色,存入新的调色板
pPal->palPalEntry[i].peRed=(BYTE)(255-Red);
pPal->palPalEntry[i].peGreen=(BYTE)(255-Green);
pPal->palPalEntry[i].peBlue=(BYTE)(255-Blue);
pPal->palPalEntry[i].peFlags=0;
*(lpTempPtr++)=(unsigned char)(255-Blue);
*(lpTempPtr++)=(unsigned char)(255-Green);
*(lpTempPtr++)=(unsigned char)(255-Red);
*(lpTempPtr++)=0;
}
if(hPalette!=NULL)
DeleteObject(hPalette);
hPalette=CreatePalette(pPal); //产生新的调色板
LocalUnlock(hPal);
LocalFree(hPal);
if(hPalette){
hPrevPalette=SelectPalette(hDc,hPalette,FALSE);
RealizePalette(hDc);
}
}
else{ //不带调色板,说明是真彩色图
for(y=0;y<bi.biHeight;y++){
lpPtr=(char *)lpImgData+(BufSize-LineBytes-y*LineBytes);
lpTempPtr=(char *)lpTempImgData+(BufSize-LineBytes-y*LineBytes);
for(x=0;x<bi.biWidth;x++){
Blue=(unsigned char )(*lpPtr++);
Green=(unsigned char )(*lpPtr++);
Red=(unsigned char )(*lpPtr++);
//反转位图数据中的颜色,存入新的位图数据中
*(lpTempPtr++)=(unsigned char)(255-Blue);
*(lpTempPtr++)=(unsigned char)(255-Green);
*(lpTempPtr++)=(unsigned char)(255-Red);
}
}
}
if(hBitmap!=NULL)
DeleteObject(hBitmap);
hBitmap=CreateDIBitmap(hDc,(LPBITMAPINFOHEADER)lpTempImgData,
(LONG)CBM_INIT,
(LPSTR)lpTempImgData+
sizeof(BITMAPINFOHEADER)+
NumColors*sizeof(RGBQUAD),
(LPBITMAPINFO)lpTempImgData,
DIB_RGB_COLORS);
if(hPalette && hPrevPalette){
SelectPalette(hDc,hPrevPalette,FALSE);
RealizePalette(hDc);
}
hf=_lcreat("c://invert.bmp",0);
_lwrite(hf,(LPSTR)&bf,sizeof(BITMAPFILEHEADER));
_lwrite(hf,(LPSTR)lpTempImgData,BufSize);
_lclose(hf);
//释放内存和资源
ReleaseDC(hWnd,hDc);
LocalUnlock(hTempImgData);
LocalFree(hTempImgData);
GlobalUnlock(hImgData);
return TRUE;
}
5.2 彩色图转灰度图
第2章中提到了YUV的颜色表示方法,在这种表示方法中,Y分量的物理含义就是亮度,它含了灰度图(grayscale)的所有信息,只用Y分量就完全能够表示出一幅灰度图来。YUV和RGB之间有着如下的对应关系:
我们利用上式,根据R、G、B的值求出Y值后,将R、G、B值都赋值成Y,就能表示出灰度图来,这就是彩色图转灰度图的原理。
先看看真彩图。我们知道真彩图不带调色板,每个象素用3个字节,表示R、G、B三个分量。所以处理很简单,根据R、G、B的值求出Y值后,将R、G、B值都赋值成Y,写入新图即可。
再来看看带调色板的彩色图,我们知道位图中的数据只是对应调色板中的一个索引值,我们只需要将调色板中的彩色变成灰度,形成新调色板,而位图数据不用动,就可以了。
下面的程序实现了彩色图到灰度图的转换,注意其中真彩图和调色板位图处理时的差别。
BOOL ColortoGrayScale(HWND hWnd)
{
DWORD SrcOffBits,SrcBufSize,DstBufSize,DstLineBytes;
LPBITMAPINFOHEADER lpImgData;
LPSTR lpPtr;
HLOCAL hTempImgData;
LPBITMAPINFOHEADER lpTempImgData;
LPSTR lpTempPtr;
HDC hDc;
HFILE hf;
LONG x,y;
BITMAPFILEHEADER DstBf;
BITMAPINFOHEADER DstBi;
LOGPALETTE *pPal;
HPALETTE hPrevPalette;
HLOCAL hPal;
DWORD NewNumColors;
WORD NewBitCount;
float Y;
DWORD i;
unsigned char Red,Green,Blue,Gray;
NewNumColors=NumColors; //NewNumColors为新图的颜色数
NewBitCount=bi.biBitCount; //NewBitCount为新图的颜色位数
if(NumColors==0) //真彩图
{
NewNumColors=256;
NewBitCount=8;
}
//由于颜色位数有可能发生了改变,所以要重新计算每行占用的字节数以及
//新图的缓冲区大小
DstLineBytes=(DWORD)WIDTHBYTES(bi.biWidth*NewBitCount);
DstBufSize=(DWORD)(sizeof(BITMAPINFOHEADER)+NewNumColors*
sizeof(RGBQUAD)+(DWORD)DstLineBytes*bi.biHeight);
//DstBf和DstBi为新的BITMAPFILEHEADER和BITMAPINFOHEADER
//拷贝原来的头信息
memcpy((char *)&DstBf,(char *)&bf,sizeof(BITMAPFILEHEADER));
memcpy((char *)&DstBi,(char *)&bi,sizeof(BITMAPINFOHEADER));
//做必要的改变
DstBf.bfSize=DstBufSize+sizeof(BITMAPFILEHEADER);
DstBf.bfOffBits=(DWORD)(NewNumColors*sizeof(RGBQUAD)+
sizeof(BITMAPFILEHEADER)+
sizeof(BITMAPINFOHEADER));
DstBi.biClrUsed=0;
DstBi.biBitCount=NewBitCount;
//原图的缓冲区的大小
SrcOffBits=bf.bfOffBits- sizeof(BITMAPFILEHEADER);
SrcBufSize=SrcOffBits+bi.biHeight*LineBytes;
if((hTempImgData=LocalAlloc(LHND,DstBufSize))==NULL)
{
MessageBox(hWnd,"Error alloc memory!","Error Message",MB_OK|
MB_ICONEXCLAMATION);
return FALSE;
}
lpImgData=(LPBITMAPINFOHEADER)GlobalLock(hImgData);
lpTempImgData=(LPBITMAPINFOHEADER)LocalLock(hTempImgData);
//拷贝头信息和位图数据
memcpy(lpTempImgData,lpImgData,DstBufSize);
//用新的BITMAPINFOHEADER替换原来的头信息
memcpy(lpTempImgData,(char *)&DstBi,sizeof(BITMAPINFOHEADER));
//lpPtr指向原图的数据
lpPtr=(char *)lpImgData+sizeof(BITMAPINFOHEADER);
//lpTempPtr指向新图的数据
lpTempPtr=(char *)lpTempImgData+sizeof(BITMAPINFOHEADER);
//为新的调色板分配内存
hPal=LocalAlloc(LHND,sizeof(LOGPALETTE) + NewNumColors
* sizeof(PALETTEENTRY));
pPal =(LOGPALETTE *)LocalLock(hPal);
pPal->palNumEntries =(WORD) NewNumColors;
pPal->palVersion = 0x300;
if(NumColors==0) //真彩色
for (i = 0; i < 256; i++) { //灰度从(0,0,0)到(255,255,255)
pPal->palPalEntry[i].peRed=(BYTE)i;
pPal->palPalEntry[i].peGreen=(BYTE)i;
pPal->palPalEntry[i].peBlue=(BYTE)i;
pPal->palPalEntry[i].peFlags=(BYTE)0;
*(lpTempPtr++)=(unsigned char)i;
*(lpTempPtr++)=(unsigned char)i;
*(lpTempPtr++)=(unsigned char)i;
*(lpTempPtr++)=0;
}
else
for (i = 0; i < NewNumColors; i++) { //带调色板的彩色图
Blue=(unsigned char )(*lpPtr++);
Green=(unsigned char )(*lpPtr++);
Red=(unsigned char )(*lpPtr++);
Y=(float)(Red*0.299+Green*0.587+Blue*0.114);
Gray=(BYTE)Y;
lpPtr++;
//从原来的调色板中的颜色计算得到Y值,写入新的调色板
pPal->palPalEntry[i].peRed=Gray;
pPal->palPalEntry[i].peGreen=Gray;
pPal->palPalEntry[i].peBlue=Gray;
pPal->palPalEntry[i].peFlags=0;
*(lpTempPtr++)=(unsigned char)Gray;
*(lpTempPtr++)=(unsigned char)Gray;
*(lpTempPtr++)=(unsigned char)Gray;
*(lpTempPtr++)=0;
}
if(hPalette!=NULL)
DeleteObject(hPalette);
//生成新的逻辑调色板
hPalette=CreatePalette(pPal);
LocalUnlock(hPal);
LocalFree(hPal);
hDc=GetDC(hWnd);
if(hPalette){
hPrevPalette=SelectPalette(hDc,hPalette,FALSE);
RealizePalette(hDc);
}
if(NumColors==0) //真彩色图才需要处理位图数据
for(y=0;y<bi.biHeight;y++){
lpPtr=(char *)lpImgData+(SrcBufSize-LineBytes-y*LineBytes);
lpTempPtr=(char*)lpTempImgData+
(DstBufSize-DstLineBytes-y*DstLineBytes);
for(x=0;x<bi.biWidth;x++){
Blue=(unsigned char )(*lpPtr++);
Green=(unsigned char )(*lpPtr++);
Red=(unsigned char )(*lpPtr++);
Y=(float)(Red*0.299+Green*0.587+Blue*0.114);
//从位图数据计算得到Y值,写入新图中
Gray=(BYTE)Y;
*(lpTempPtr++)=(unsigned char)Gray;
}
}
if(hBitmap!=NULL)
DeleteObject(hBitmap);
//产生新的位图
hBitmap=CreateDIBitmap(hDc,(LPBITMAPINFOHEADER)lpTempImgData,
(LONG)CBM_INIT,
(LPSTR)lpTempImgData+
sizeof(BITMAPINFOHEADER)+
NewNumColors*sizeof(RGBQUAD),
(LPBITMAPINFO)lpTempImgData,
DIB_RGB_COLORS);
if(hPalette && hPrevPalette){
SelectPalette(hDc,hPrevPalette,FALSE);
RealizePalette(hDc);
}
hf=_lcreat("c://gray.bmp",0);
_lwrite(hf,(LPSTR)&DstBf,sizeof(BITMAPFILEHEADER));
_lwrite(hf,(LPSTR)lpTempImgData,DstBufSize);
_lclose(hf);
//释放内存和资源
ReleaseDC(hWnd,hDc);
LocalUnlock(hTempImgData);
LocalFree(hTempImgData);
GlobalUnlock(hImgData);
return TRUE;
}
5.3 真彩图转256色图
我们知道,真彩图中包含最多达224种颜色,怎样从中选出256种颜色,又要使颜色的失真比较小,这是一个比较复杂的问题。一种简单的做法是将R:G:B以3:3:2表示,即取R,G的高3位,B的高两位,组成一个字节,这样就可以表示256种颜色了,但不难想象,这种方法的失真肯定很严重。
我们下面介绍的算法能够比较好地实现真彩图到256色图的转换。它的思想是:准备一个长度为4096的数组,代表4096种颜色。对图中的每一个象素,取R、G、B的最高四位,拼成一个12位的整数,对应的数组元素加1。全部统计完后,就得到了这4096种颜色的使用频率。其中,可能有一些颜色一次也没用到,即对应的数组元素为零(假设不为零的数组元素共有PalCounts个)。将这些为零的数组元素清除出去,使得前PalCounts个元素都不为零。将这PalCounts个数按从大到小的顺序排列(这里我们使用起泡排序)。这样,前256种颜色就是用的最多的颜色,它们将作为调色板上的256种颜色。对于剩下的PalCounts-256种颜色并不是简单地丢弃,而是用前256种颜色中的一种来代替,代替的原则是找有最小平方误差的那个。再次对图中的每一个象素,取R、G、B的最高四位,拼成一个12位的整数,如果对应值在前256种颜色中,则直接将该索引值填入位图数据中,如果是在后PalCounts-256种颜色中,则用代替色的索引值填入位图数据中。
下面的两幅图中,图5.3是原真彩图,图.54是用上面的算法转换成的256色图,可以看出,效果还不错。
图5.3 原真彩图 |
图5.4 转换后的256色图 |
下面是上述算法的源程序。
BOOL Trueto256(HWND hWnd)
{
DWORD SrcBufSize,OffBits,DstBufSize,DstLineBytes;
LPBITMAPINFOHEADER lpImgData;
LPSTR lpPtr;
HLOCAL hTempImgData;
LPBITMAPINFOHEADER lpTempImgData;
LPSTR lpTempPtr;
HDC hDc;
HFILE hf;
LONG x,y;
BITMAPFILEHEADER DstBf;
BITMAPINFOHEADER DstBi;
LOGPALETTE *pPal;
HPALETTE hPrevPalette;
HLOCAL hPal;
WORD i,j;
int Red,Green,Blue,ClrIndex;
DWORD ColorHits[4096];
WORD ColorIndex[4096];
DWORD PalCounts,temp;
long ColorError1,ColorError2;
if(NumColors!=0){ //NumColors不为零,所以不是真彩图
MessageBox(hWnd,"Must be a true color bitmap!","Error Message",
MB_OK|MB_ICONEXCLAMATION);
return FALSE;
}
//由于颜色位数有可能发生了改变,所以要重新计算每行占用的字节数以及
//新图的缓冲区大小
DstLineBytes=(DWORD)WIDTHBYTES(bi.biWidth*8);
DstBufSize=(DWORD)(sizeof(BITMAPINFOHEADER)+
256*sizeof(RGBQUAD)+
(DWORD)DstLineBytes*bi.biHeight);
//DstBf和DstBi为新的BITMAPFILEHEADER和BITMAPINFOHEADER
//拷贝原来的头信息
memcpy((char *)&DstBf,(char *)&bf,sizeof(BITMAPFILEHEADER));
memcpy((char *)&DstBi,(char *)&bi,sizeof(BITMAPINFOHEADER));
//做必要的改变
DstBf.bfSize=DstBufSize+sizeof(BITMAPFILEHEADER);
DstBf.bfOffBits=(DWORD)(256*sizeof(RGBQUAD)+
sizeof(BITMAPFILEHEADER)
+sizeof(BITMAPINFOHEADER));
DstBi.biClrUsed=0;
DstBi.biBitCount=8;
//OffBits为到实际位图数据的偏移值
OffBits=bf.bfOffBits-sizeof(BITMAPFILEHEADER);
//SrcBufSize为原图缓冲区的大小
SrcBufSize=OffBits+bi.biHeight*LineBytes;
if((hTempImgData=LocalAlloc(LHND,DstBufSize))==NULL)
{
MessageBox(hWnd,"Error alloc memory!","Error Message",MB_OK|
MB_ICONEXCLAMATION);
return FALSE;
}
lpImgData=(LPBITMAPINFOHEADER)GlobalLock(hImgData);
lpTempImgData=(LPBITMAPINFOHEADER)LocalLock(hTempImgData);
//拷贝位图数据
memcpy(lpTempImgData,lpImgData,OffBits);
//用新的头信息取代旧的头信息
memcpy(lpTempImgData,(char *)&DstBi,sizeof(BITMAPINFOHEADER));
//ColorHits为记录颜色使用频率的数组,ColorIndex为记录颜色索引值的
//数组
//先全部清零
memset(ColorHits,0,4096*sizeof(DWORD));
memset(ColorIndex,0,4096*sizeof(WORD));
for(y=0;y<bi.biHeight;y++){
lpPtr=(unsigned char *)lpImgData+(SrcBufSize-LineBytes-y*LineBytes);
for(x=0;x<bi.biWidth;x++){
//R,G,B各取4位
Blue=(int)(*(lpPtr++) & 0xf0);
Green=(int)(*(lpPtr++) & 0xf0);
Red=(int)(*(lpPtr++) & 0xf0);
//拼成一个12位整数
ClrIndex=(Blue<<4) + Green +(Red >>4);
//相应的数组元素加1
ColorHits[ClrIndex]++;
}
}
PalCounts=0;
//将为零的元素清除出去
for (ClrIndex = 0; ClrIndex < 4096; ClrIndex++)
{
if(ColorHits[ClrIndex]!=0){
ColorHits[PalCounts]=ColorHits[ClrIndex];
//注意调整相应的索引值
ColorIndex[PalCounts]=ClrIndex;
PalCounts++; //颜色数加1
}
}
//用起泡排序将PalCounts种颜色按从大到小的顺序排列
for (i = 0; i < PalCounts-1; i++)
for (j = i + 1; j < PalCounts; j++){
if (ColorHits[j] > ColorHits[i]){
temp = ColorHits[i];
ColorHits[i] = ColorHits[j];
ColorHits[j] = temp;
//注意调整相应的索引值
temp = ColorIndex[i];
ColorIndex[i] = ColorIndex[j];
ColorIndex[j] = (WORD)temp;
}
}
//为新的调色板分配内存
hPal=LocalAlloc(LHND,sizeof(LOGPALETTE) +
256* sizeof(PALETTEENTRY));
pPal =(LOGPALETTE *)LocalLock(hPal);
pPal->palNumEntries =(WORD) 256;
pPal->palVersion = 0x300;
lpTempPtr=(char *)lpTempImgData+sizeof(BITMAPINFOHEADER);
for (i = 0; i < 256; i++) {
//由12位索引值得到R,G,B的最高4位值
pPal->palPalEntry[i].peRed=(BYTE)((ColorIndex[i] & 0x00f) << 4);
pPal->palPalEntry[i].peGreen=(BYTE)((ColorIndex[i] & 0x0f0));
pPal->palPalEntry[i].peBlue=(BYTE)((ColorIndex[i] & 0xf00) >> 4);
pPal->palPalEntry[i].peFlags=(BYTE)0;
*(lpTempPtr++)=(unsigned char)((ColorIndex[i] & 0xf00) >> 4);
*(lpTempPtr++)=(unsigned char)((ColorIndex[i] & 0x0f0));
*(lpTempPtr++)=(unsigned char)((ColorIndex[i] & 0x00f) << 4);
*(lpTempPtr++)=0;
//ColorHits作为颜色记数的作用已经完成了,下面的作用是记录12位索
//引值对应的调色板//中的索引值
ColorHits[i]=i;
}
//其余的颜色依据最小平方误差近似为前256中最接近的一种
if (PalCounts > 256){
for (i = 256; i < PalCounts; i++){
//ColorError1记录最小平方误差,一开始赋一个很大的值
ColorError1=1000000000;
//由12位索引值得到R,G,B的最高4位值
Blue = (long)((ColorIndex[i] & 0xf00) >> 4);
Green = (long)((ColorIndex[i] & 0x0f0));
Red = (long)((ColorIndex[i] & 0x00f) << 4);
ClrIndex = 0;
for (j = 0; j < 256; j++){
//ColorError2计算当前的平方误差
ColorError2=(long)(Blue-pPal->palPalEntry[j].peBlue)*
(Blue-pPal->palPalEntry[j].peBlue)+ (long)(Green-pPal->palPalEntry[j].peGreen)*
(Green-pPal->palPalEntry[j].peGreen)+
(long)(Red-pPal->palPalEntry[j].peRed)*
(Red-pPal->palPalEntry[j].peRed);
if (ColorError2 < ColorError1){ //找到更小的了
ColorError1 = ColorError2;
ClrIndex = j; //记录对应的调色板的索引值
}
}
//ColorHits记录12位索引值对应的调色板中的索引值
ColorHits[i] = ClrIndex;
}
}
if(hPalette!=NULL)
DeleteObject(hPalette);
//产生新的逻辑调色板
hPalette=CreatePalette(pPal);
LocalUnlock(hPal);
LocalFree(hPal);
hDc=GetDC(hWnd);
if(hPalette){
hPrevPalette=SelectPalette(hDc,hPalette,FALSE);
RealizePalette(hDc);
}
for(y=0;y<bi.biHeight;y++){
lpPtr=(char *)lpImgData+(SrcBufSize-LineBytes-y*LineBytes);
lpTempPtr=(char*)lpTempImgData+
(DstBufSize-DstLineBytes-y*DstLineBytes);
for(x=0;x<bi.biWidth;x++){
//R,G,B各取4位
Blue=(int)(*(lpPtr++) & 0xf0);
Green=(int)(*(lpPtr++) & 0xf0);
Red=(int)(*(lpPtr++) & 0xf0);
//拼成一个12位整数
ClrIndex=(Blue<<4) + Green +(Red >>4);
for (i = 0; i < PalCounts;i++)
if (ClrIndex == ColorIndex[i]){
//根据12索引值取得对应的调色板中的索引值
*(lpTempPtr++)=(unsigned char)ColorHits[i];
break;
}
}
}
if(hBitmap!=NULL)
DeleteObject(hBitmap);
//产生新的位图
hBitmap=CreateDIBitmap(hDc,(LPBITMAPINFOHEADER)lpTempImgData,
(LONG)CBM_INIT,
(LPSTR)lpTempImgData+
sizeof(BITMAPINFOHEADER)+
256*sizeof(RGBQUAD),
(LPBITMAPINFO)lpTempImgData,
DIB_RGB_COLORS);
if(hPalette && hPrevPalette){
SelectPalette(hDc,hPrevPalette,FALSE);
RealizePalette(hDc);
}
hf=_lcreat("c://256.bmp",0);
_lwrite(hf,(LPSTR)&DstBf,sizeof(BITMAPFILEHEADER));
_lwrite(hf,(LPSTR)lpTempImgData,DstBufSize);
_lclose(hf);
//释放内存和资源
ReleaseDC(hWnd,hDc);
LocalUnlock(hTempImgData);
LocalFree(hTempImgData);
GlobalUnlock(hImgData);
return TRUE;
}
以下我们将要介绍灰度变换,针对的都是256级灰度图。
5.4 对比度扩展
假设有一幅图,由于成象时光照不足,使得整幅图偏暗(例如,灰度范围从0到63);或者成象时光照过强,使得整幅图偏亮(例如,灰度范围从200到255),我们称这些情况为低对比度,即灰度都挤在一起,没有拉开。灰度扩展的意思就是把你所感性趣的灰度范围拉开,使得该范围内的象素,亮的越亮,暗的越暗,从而达到了增强对比度的目的。我们可以用图5.5来说明对比度扩展(contrast stretching)的原理。
图5.5 对比度扩展的原理
图5.5中的横坐标gold表示原图的灰度值,纵坐标gnew表示gold经过对比度扩展后得到了新的灰度值。a,b,c为三段直线的斜率,因为是对比度扩展,所以斜率b>1。g1old和g2old表示原图中要进行对比度扩展的范围,g1new和g2new表示对应的新值。用公式表示为
显然要得到对比度扩展后的灰度,我们需要知道a,b,c,g1old,g2old五个参数。由于有新图的灰度级别也是255这个约束,所以满足ag1old+b(gold-g1old)+c(255-g2old)=255这个方程。这样,我们只需给出四个参数,而另一个可以代入方程求得。我们假设a=c,这样,我们只要给出b,g1old和g2old,就可以求出
a=(255-b(g2old-g1old))/(255-(g2old-g1old))
要注意的是,给出的三个参数必须满:(1) b*(g2old-g1old)<=255;(2) (g2old-g1old)<=255。
下图为图5.1取g1old=100,g2old=150 ,b=3.0进行对比度扩展的结果。可以看出亮的区域(雕塑)变得更亮,暗的区域(手)变得更暗。
图5.6 图5.1对比度扩展后的结果
下面的这段程序实现了对比度扩展。首先出现对话框,输入b,g1old,g2old的三个参数(在程序中分别是StretchRatio,SecondPoint,FirstPoint),然后对调色板做响应的处理,而实际的位图数据不用改动。
BOOL ContrastStretch(HWND hWnd)
{
DLGPROC dlgInputBox = NULL;
DWORD OffBits,BufSize;
LPBITMAPINFOHEADER lpImgData;
LPSTR lpPtr;
HLOCAL hTempImgData;
LPBITMAPINFOHEADER lpTempImgData;
LPSTR lpTempPtr;
HDC hDc;
HFILE hf;
LOGPALETTE *pPal;
HPALETTE hPrevPalette=NULL;
HLOCAL hPal;
DWORD i;
unsigned char Gray;
float a,g1,g2,g;
if( NumColors!=256){ //必须是256级灰度图
MessageBox(hWnd,"Must be a 256 grayscale bitmap!","Error Message",
MB_OK|MB_ICONEXCLAMATION);
return FALSE;
}
//出现对话框,输入三个参数
dlgInputBox = (DLGPROC) MakeProcInstance ( (FARPROC)InputBox, ghInst );
DialogBox (ghInst, "INPUTBOX", hWnd, dlgInputBox);
FreeProcInstance ( (FARPROC) dlgInputBox );
if( StretchRatio*(SecondPoint-FirstPoint) > 255.0){ //参数不合法
MessageBox(hWnd,"StretchRatio*(SecondPoint-FirstPoint) can not be larger
than 255!",Error Message",
MB_OK|MB_ICONEXCLAMATION);
return FALSE;
}
if( (SecondPoint-FirstPoint) >=255){ //参数不合法
MessageBox(hWnd,"The area you selected can not be the whole scale!",
"Error Message",MB_OK|MB_ICONEXCLAMATION);
return FALSE;
}
//计算出第一和第三段的斜率a
a=(float)((255.0-StretchRatio*(SecondPoint-FirstPoint))/
(255.0-(SecondPoint-FirstPoint)));
//对比度扩展范围的边界点所对应的新的灰度
g1=a*FirstPoint;
g2=StretchRatio*(SecondPoint-FirstPoint)+g1;
//新开的缓冲区的大小
OffBits=bf.bfOffBits- sizeof(BITMAPFILEHEADER);
BufSize=OffBits+bi.biHeight*LineBytes;
if((hTempImgData=LocalAlloc(LHND,BufSize))==NULL)
{
MessageBox(hWnd,"Error alloc memory!","Error Message",MB_OK|
MB_ICONEXCLAMATION);
return FALSE;
}
lpImgData=(LPBITMAPINFOHEADER)GlobalLock(hImgData);
lpTempImgData=(LPBITMAPINFOHEADER)LocalLock(hTempImgData);
//拷贝头信息和实际位图数据
memcpy(lpTempImgData,lpImgData,BufSize);
hDc=GetDC(hWnd);
//lpPtr指向原图数据缓冲区,lpTempPtr指向新图数据缓冲区
lpPtr=(char *)lpImgData+sizeof(BITMAPINFOHEADER);
lpTempPtr=(char *)lpTempImgData+sizeof(BITMAPINFOHEADER);
//为新的逻辑调色板分配内存
hPal=LocalAlloc(LHND,sizeof(LOGPALETTE)+
NumColors*sizeof(PALETTEENTRY));
pPal =(LOGPALETTE *)LocalLock(hPal);
pPal->palNumEntries =(WORD) NumColors;
pPal->palVersion = 0x300;
for (i = 0; i < 256; i++) {
Gray=(unsigned char )*lpPtr;
lpPtr+=4;
//进行对比度扩展
if(Gray<FirstPoint) g=(float)(a*Gray);
else if (Gray<SecondPoint) g=g1+StretchRatio*(Gray-FirstPoint);
else g=g2+a*(Gray-SecondPoint);
pPal->palPalEntry[i].peRed=(BYTE)g;
pPal->palPalEntry[i].peGreen=(BYTE)g;
pPal->palPalEntry[i].peBlue=(BYTE)g;
pPal->palPalEntry[i].peFlags=0;
*(lpTempPtr++)=(unsigned char)g;
*(lpTempPtr++)=(unsigned char)g;
*(lpTempPtr++)=(unsigned char)g;
*(lpTempPtr++)=0;
}
if(hPalette!=NULL)
DeleteObject(hPalette);
//产生新的逻辑调色板
hPalette=CreatePalette(pPal);
LocalUnlock(hPal);
LocalFree(hPal);
if(hPalette){
hPrevPalette=SelectPalette(hDc,hPalette,FALSE);
RealizePalette(hDc);
}
if(hBitmap!=NULL)
DeleteObject(hBitmap);
//产生新的位图
hBitmap=CreateDIBitmap(hDc,(LPBITMAPINFOHEADER)lpTempImgData,
(LONG)CBM_INIT,
(LPSTR)lpTempImgData+
sizeof(BITMAPINFOHEADER) +
NumColors*sizeof(RGBQUAD),
(LPBITMAPINFO)lpTempImgData, DIB_RGB_COLORS);
if(hPalette && hPrevPalette){
SelectPalette(hDc,hPrevPalette,FALSE);
RealizePalette(hDc);
}
hf=_lcreat("c://stretch.bmp",0);
_lwrite(hf,(LPSTR)&bf,sizeof(BITMAPFILEHEADER));
_lwrite(hf,(LPSTR)lpTempImgData,BufSize);
_lclose(hf);
//释放内存和资源
ReleaseDC(hWnd,hDc);
LocalUnlock(hTempImgData);
LocalFree(hTempImgData);
GlobalUnlock(hImgData);
return TRUE;
}
5.5 削波
削波(cliping)可以看作是对比度扩展的一个特例,我们用图5.7说明削波的原理。
图5.7 削波的原理
不难看出,只要令对比度扩展中的a=c=0就实现了削波。我们只要给出范围的两个端点,斜率b就可以用方程b(g2old-g1old)=255求出。
图5.8为图5.1取g1old=150,g2old=200 进行削波的结果。把亮的区域(雕塑)提取了出来。
图5.8 图5.1削波处理后的结果
削波的程序和对比度扩展的程序很类似,就不再给出了。
5.6 阈值化
阈值化(thresholding)可以看作是削波的一个特例,我们用图5.9说明阈值化的原理。
图5.9 阈值化的原理
不难看出,只要令削波中的g1old=g2old就实现了阈值化。阈值就象个门槛,比它大就是白,比它小就是黑。经过阈值化处理后的图象变成了黑白二值图,所以说阈值化是灰度图转二值图的一种常用方法(我们以前介绍过图案化和抖动的方法)。进行阈值化只需给出阈值点g1old即可。
图5.10为图5.1阈值取128,阈值化处理后的结果,是一幅黑白图。
图5.10 图5.1阈值化处理后的结果
阈值化的程序和对比度扩展的程序很类似,就不再给出了。
5.7 灰度窗口变换
灰度窗口变换(slicing)是将某一区间的灰度级和其它部分(背景)分开。我们用图5.11和图5.12说明灰度窗口变换的原理。其中[g1old,g2old]称为灰度窗口。
图5.11 清除背景的灰度窗口变换的原理 |
图5.12 保留背景的灰度窗口变换的原理 |
灰度窗口变换有两种,一种是清除背景的,一种是保留背景的。前者把不在灰度窗口范围内的象素都赋值为0,在灰度窗口范围内的象素都赋值为255,这也能实现灰度图的二值化;后者是把不在灰度窗口范围内的象素保留原灰度值,在灰度窗口范围内的象素都赋值为255。灰度窗口变换可以检测出在某一灰度窗口范围内的所有象素,是图象灰度分析中的一个有力工具。
下面有三幅图,图5.13为原图;图5.14是经过清除背景的灰度窗口变换处理后的图(灰度窗口取[200-255]),将夜景中大厦里的灯光提取了出来;图5.15是经过保留背景的灰度窗口变换处理后的图(灰度窗口取[200-255]),将夜景中大厦里的灯光提取了出来,同时保留了大厦的背景,可以看出它们的差别还是很明显的。
图5.13 原图 |
图5.14 图5.13经过 清除背景的灰度窗 口变换处理后的图 |
图5.15 图5.13经过 保留背景的灰度窗 口变换处理后的图 |
灰度窗口变换的程序和对比度扩展的程序很类似,就不再给出了。
不久前在一本科学杂志上看到一篇文章,非常有趣,是介绍电影“阿甘正传”的特技制作的。其中有一项就用到了类似灰度窗口变换的思想。相信看过这部电影的读者都会对那个断腿的丹尼上校有深刻的印象。他的断腿是怎么拍出来的呢?其实方法很简单,先拍一幅没有演员出现的背景画面,然后拍一幅有演员出现,其它不变的画面。要注意的是,此时演员的腿用蓝布包裹。把前后两幅图输入计算机进行处理。第二幅图中凡是遇到蓝色的象素,就用第一幅图中对应位置的背景象素代替。这样,一位断腿的上校就逼真的出现在屏幕上了。这就是电影特技中经常用到的“蓝幕”技术。
说点题外话。其实现代电影,特别是好莱坞电影,越来越离不开计算机及图象处理技术。最近引起轰动的大片“泰坦尼克号”中的很多特技镜头就是利用了庞大的SGI图形工作站机群没日没夜的计算产生的。图象处理技术和我们所喜爱的电影艺术紧密的结合了起来,更增加了我们学习它的兴趣。
5.8 灰度直方图统计
有时我们需要知道一幅图中的灰度分布情况,这时就可以采用灰度直方图(histogram)来表示,图中的横坐标表示灰度值,纵坐标表示该灰度值出现的次数(频率)。图5.16为图5.13的灰度直方图,低灰度的象素占了绝大部分。
图5.16 图5.13的灰度直方图
下面的程序显示一幅图的灰度直方图。有两段程序,第一段统计出每个灰度的象素个数,存放在数组GrayTable[]中,然后产生一个新的窗口,把统计结果显示出来。第二段程序就是该窗口的消息处理函数。要注意的是,由于各灰度出现的频率可能相差很大,所以如何将结果显示在有限的窗口范围内,是一个必须考虑的问题。我们这里的做法是,在所有出现的灰度中,统计出一个最大值max和一个最小值min,假设能显示的窗口最大坐标为270,最小坐标为5,按成比例显示,这样,灰度出现的次数和显示坐标之间呈线形关系。设 a×grayhits+b=coordinate,其中grayhits为灰度出现的次数,coordinate为显示坐标,a和b为两个常数。我们将max和min代入,应该满足a×max+b=270,a×min+b=5;由此可以解得a=265/(max-min),b=270.0-a× max 。
还有一点,不要忘了在WinMain函数中注册那个新产生窗口的窗口类。
int GrayTable[256];
int MaxGrayNum;
int MinGrayNum;
BOOL Histogram(HWND hWnd)
{
DWORD OffBits,BufSize;
LPBITMAPINFOHEADER lpImgData;
LPSTR lpPtr;
int x,y;
int grayindex;
HWND hPopupWnd;
int temp;
//计数器清零
for(grayindex=0;grayindex<256;grayindex++)
GrayTable[grayindex]=0;
//OffBits为到实际位图数据的偏移值
OffBits=bf.bfOffBits-sizeof(BITMAPFILEHEADER);
//BufSize为缓冲区的大小
BufSize=OffBits+bi.biHeight*LineBytes; lpImgData=(LPBITMAPINFOHEADER)GlobalLock(hImgData);
for(y=0;y<bi.biHeight;y++){
lpPtr=(char *)lpImgData+(BufSize-LineBytes-y*LineBytes);
for(x=0;x<bi.biWidth;x++){
grayindex=(unsigned char)*(lpPtr++);
GrayTable[grayindex]++; //对应的颜色计数值加1
}
}
MaxGrayNum=0;
MinGrayNum=65535;
for(grayindex=0;grayindex<256;grayindex++){
temp=GrayTable[grayindex];
if(temp>MaxGrayNum)
MaxGrayNum=temp; //找到更大的了
if( (temp<MinGrayNum) && (temp>0) )
MinGrayNum=temp; //找 到更小的了
}
GlobalUnlock(hImgData);
//产生新的窗口显示结果
hPopupWnd = CreateWindow ("PopupWindowClass",
"Histogram Statistic Window",
WS_OVERLAPPEDWINDOW,50,80,550,350,
hWnd,NULL,ghInst,NULL);
if (hPopupWnd){
ShowWindow (hPopupWnd, SW_SHOW);
UpdateWindow (hPopupWnd);
}
return TRUE;
}
下面是新窗口的消息处理函数。
long FAR PASCAL PopupWndProc (HWND hWnd,UINT message,
WPARAM wParam,LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT ps;
DWORD i;
int xstart;
static LOGPEN blp={PS_SOLID,1,1,RGB(0,0,255)}; //蓝色画笔
HPEN bhp; //画笔句柄
float a,b,temp;
char str[10];
//计算上面所说的a,b的值
a=(float)(265.0 /( MaxGrayNum - MinGrayNum) );
b=(float) (270.0-a* MaxGrayNum);
switch (message){
case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
bhp = CreatePenIndirect(&blp);
SelectObject(hdc,bhp);
MoveToEx(hdc,2,270,NULL);
LineTo(hdc,518,270); //先画一条水平线
xstart=2;
for(i=0;i<256;i++){
MoveToEx(hdc,xstart,270,NULL);
if (GrayTable[i]!=0)
temp=(float)(a*GrayTable[i]+b);
else temp=0.0f; //如果灰度出现的次数是零,则不画线
LineTo(hdc,xstart,270-(int)temp); //画出该灰度的计数值
if (i%16 ==0){ //画出标尺,每16个一格
MoveToEx(hdc,xstart,270,NULL);
LineTo(hdc,xstart,280);
_itoa(i,str,10);
TextOut(hdc,xstart,285,str,strlen(str));
}
xstart+=2;
}
MoveToEx(hdc,xstart,270,NULL);
LineTo(hdc,xstart,280);
TextOut(hdc,xstart,285,"256",strlen("256"));
EndPaint(hWnd,&ps);
DeleteObject(bhp);
break;
default:
break;
}
return DefWindowProc (hWnd, message, wParam, lParam);
}
5.9 灰度直方图均衡化
在介绍灰度直方图均衡化(histogram equalization)之前,先讲讲直方图修正。所谓直方图修正,就是通过一个灰度映射函数Gnew=F(Gold),将原灰度直方图改造成你所希望的直方图。所以,直方图修正的关键就是灰度映射函数。我们刚才介绍的阈值化、削波、灰度窗口变换等等,都是灰度映射函数。
直方图均衡化是一种最常用的直方图修正。它是把给定图象的直方图分布改造成均匀直方图分布。由信息学的理论来解释,具有最大熵(信息量)的图象为均衡化图象。直观地讲,直方图均衡化导致图象的对比度增加。
由于直方图均衡化涉及到很多概率和数学的知识,具体的细节这里就不介绍了,只给出算法。通过下面的例子,就很容易明白了。
有一幅图象,共有16级灰度,其直方图分布为Pi, i=0,1,…,15,求经直方图均衡化后,量化级别为10级的灰度图象的直方图分布Qi,其中Pi和Qi为分布的概率,即灰度i出现的次数与总的点数之比。
Pi: 0.03,0,0.06,0.10,0.20,0.11,0,0,0,0.03,0,0.06,0.10,0.20,0.11,0
步骤1:用一个数组s记录Pi,即
s[0]=0.03,s[1]=0,s[2]=0.06,…,s[14]=0.11,s[15]=0
步骤2:i从1开始,令s[i]=s[i]+s[i-1],得到的结果是
s: 0.03,0.03,0.09,0.19,0.39,0.50,0.50,0.50,0.50,0.53,0.53,0.59,0.69,0.89,1.0,1.0
步骤3:用一个数组L记录新的调色板索引值,即令L[i]=s[i]×(10-1),得到的结果是L:0,0,1,2,4,5,5,5,5,5,5,5,6,8,9,9
这样就找到了原来的调色板索引值和新的调色板索引值之间的对应关系,即
0→0,1→0,2→1,3→2,4→4,5→5,6→5,7→5,8→5,9→5,10→5,11→5,12→6,
13→8,14→9,15→9。
步骤4:将老的索引值对应的概率合并,作为对应的新的索引值的概率。例如,原来的索引值0,1都对应了新的索引值0,则灰度索引值为0的概率为P0+P1=0.03;新的索引值3和7找不到老的索引值与之对应,所以令Q3和Q7为0。最后得到的结果是Qi:0.03,0.06,0.10,0,0.20,0.20,0.10,0,0.20,0.11
图5.17为Pi的分布,图5.18为Qi的分布,对照一下,不难发现图5.18的分布比图5.17要均匀一些。
图5.17 Pi的分布 |
图5.18 Qi的分布 |
要注意的是,均衡化处理后的图象只能是近似均匀分布。均衡化图象的动态范围扩大了,但其本质是扩大了量化间隔,而量化级别反而减少了,因此,原来灰度不同的象素经处理后可能变的相同,形成了一片的相同灰度的区域,各区域之间有明显的边界,从而出现了伪轮廓。
图5.19为图5.13经直方图均衡化处理后,量化为128级灰度的结果;图5.20为它的直方图分布。为什么天亮了起来呢?分析一下就明白了:因为原图中低灰度的点太多了,所以s数组前面的元素很大。经过L[i]=s[i]×(128-1)的处理后,原图中低灰度的点的灰度值提高了不少,所以那片暗区变亮了。同时可以看出,天空中出现了伪轮廓。
图5.19 图5.13经直方图均衡化处理后的结果
图5.20 图5.19的灰度直方图
图5.21为图5.1直方图均衡化后的结果(128级灰度),暗的区域(手)变亮了,看起来更清楚一些。
图5.21 图5.1直方图均衡化后的结果
下面给出直方图均衡化的源程序:
int EquaScale; //为新的灰度级别
BOOL HistogramEqua(HWND hWnd)
{
DLGPROC dlgInputBox = NULL;
DWORD BufSize,OffBits;
LPBITMAPINFOHEADER lpImgData;
LPSTR lpPtr;
HLOCAL hTempImgData;
LPBITMAPINFOHEADER lpTempImgData;
LPSTR lpTempPtr;
HDC hDc;
HFILE hf;
LONG x,y;
LOGPALETTE *pPal;
HPALETTE hPrevPalette;
HLOCAL hPal;
WORD i;
int Gray;
DWORD GrayHits[256];
int GrayIndex[256];
float s[256];
if( NumColors!=256){ //必须是256级灰度图
MessageBox(hWnd,"Must be a 256 grayscale bitmap!","Error Message",
MB_OK|MB_ICONEXCLAMATION);
return FALSE;
}
//出现对话框,输入新的灰度级别
dlgInputBox = (DLGPROC) MakeProcInstance ( (FARPROC)InputBox, ghInst );
DialogBox (ghInst, "INPUTBOX", hWnd, dlgInputBox);
FreeProcInstance ( (FARPROC) dlgInputBox );
if( EquaScale >=255){ //量化级别不能大于255
MessageBox(hWnd,"The new scale can not be larger than 255",
"Error Message",MB_OK|MB_ICONEXCLAMATION);
return FALSE;
}
//OffBits为到实际位图数据的偏移值
OffBits=bf.bfOffBits-sizeof(BITMAPFILEHEADER);
//BufSize为缓冲区的大小
BufSize=OffBits+bi.biHeight*LineBytes; if((hTempImgData=LocalAlloc(LHND,BufSize))==NULL)
{
MessageBox(hWnd,"Error alloc memory!","Error Message",MB_OK|
MB_ICONEXCLAMATION);
return FALSE;
}
//lpImgData指向原图,//lpTempImgData指向新开的缓冲区
lpImgData=(LPBITMAPINFOHEADER)GlobalLock(hImgData);
lpTempImgData=(LPBITMAPINFOHEADER)LocalLock(hTempImgData);
//拷贝头信息
memcpy(lpTempImgData,lpImgData,OffBits);
//ColorHits为记录颜色使用频率的数组,ColorIndex为记录颜色索引值的
//数组
//先清零
memset(GrayHits,0,256*sizeof(DWORD));
memset(GrayIndex,0,256*sizeof(WORD));
for(y=0;y<bi.biHeight;y++){
lpPtr=(unsigned char *)lpImgData+(BufSize-LineBytes-y*LineBytes);
for(x=0;x<bi.biWidth;x++){
Gray=(unsigned char )*(lpPtr++);
GrayHits[Gray]++; //统计该颜色用到的次数
}
}
for(i=0;i<256;i++)
//次数除以总点数得到频率
s[i]=(float)GrayHits[i]/((float)bi.biWidth*(float)bi.biHeight);
for(i=1;i<256;i++)
s[i]+=s[i-1]; //每一项都是前面所有项的累加
for(i=0;i<256;i++)
//根据新的量化级别,计算新的灰度索引值
GrayIndex[i]=(int)(s[i]*(EquaScale-1));
//为新的调色板分配内存
hPal=LocalAlloc(LHND,sizeof(LOGPALETTE)+
256*sizeof(PALETTEENTRY));
pPal =(LOGPALETTE *)LocalLock(hPal);
//先将调色板内存全部清零
memset(pPal,0,sizeof(LOGPALETTE) + 256* sizeof(PALETTEENTRY));
pPal->palNumEntries =(WORD) 256;
pPal->palVersion = 0x300;
lpTempPtr=(char *)lpTempImgData+sizeof(BITMAPINFOHEADER);
for (i = 0; i < EquaScale; i++) {
Gray=(int)(i*255.0/(EquaScale-1)); //根据新的量化级别,计算灰度值
pPal->palPalEntry[i].peRed=(BYTE)Gray;
pPal->palPalEntry[i].peGreen=(BYTE)Gray;
pPal->palPalEntry[i].peBlue=(BYTE)Gray;
pPal->palPalEntry[i].peFlags=(BYTE)0;
*(lpTempPtr++)=(unsigned char)Gray;
*(lpTempPtr++)=(unsigned char)Gray;
*(lpTempPtr++)=(unsigned char)Gray;
*(lpTempPtr++)=0;
}
if(hPalette!=NULL)
DeleteObject(hPalette);
//产生新的逻辑调色板
hPalette=CreatePalette(pPal);
LocalUnlock(hPal);
LocalFree(hPal);
hDc=GetDC(hWnd);
if(hPalette){
hPrevPalette=SelectPalette(hDc,hPalette,FALSE);
RealizePalette(hDc);
}
for(y=0;y<bi.biHeight;y++){
lpPtr=(char *)lpImgData+(BufSize-LineBytes-y*LineBytes);
lpTempPtr=(char *)lpTempImgData+(BufSize-LineBytes-y*LineBytes);
for(x=0;x<bi.biWidth;x++){
Gray=(unsigned char )*(lpPtr++); //原灰度索引值
Gray=GrayIndex[Gray]; //对应的新的灰度索引值
*(lpTempPtr++)=(unsigned char)Gray;
}
}
if(hBitmap!=NULL)
DeleteObject(hBitmap);
//产生新的位图
hBitmap=CreateDIBitmap(hDc,(LPBITMAPINFOHEADER)lpTempImgData,
(LONG)CBM_INIT,
(LPSTR)lpTempImgData+
sizeof(BITMAPINFOHEADER)+
256*sizeof(RGBQUAD),
(LPBITMAPINFO)lpTempImgData,
DIB_RGB_COLORS);
if(hPalette && hPrevPalette){
SelectPalette(hDc,hPrevPalette,FALSE);
RealizePalette(hDc);
}
hf=_lcreat("c://equa.bmp",0);
_lwrite(hf,(LPSTR)&bf,sizeof(BITMAPFILEHEADER));
_lwrite(hf,(LPSTR)lpTempImgData,BufSize);
_lclose(hf);
//释放内存和资源
ReleaseDC(hWnd,hDc);
LocalUnlock(hTempImgData);
LocalFree(hTempImgData);
GlobalUnlock(hImgData);
return TRUE;
}