数字图像处理编程入门

glight2000

贡献于2012-01-16

字数:0 关键词: 图形/图像处理

本页只为图象处理爱好者交流之用, 未经本人同意, 请勿下载. 另,本书及所附 source 已于 1999 年由清华大学出版社出版,请支持正版书籍及软 件,谢谢. 前言 目录 第 1 章 Windows位图和调色板 第 2 章 图象的几何变换 第 3 章 图象的平滑(去噪 声),锐化 第 4 章 图象的半影调和 抖动技术 第 5 章 直方图修正和彩 色变换 第 6 章 腐蚀,膨胀,细化算 法 第 7 章 边沿检测与提取, 轮廓跟踪 第 8 章 图象的检测及模 板匹配 第 9 章 图象的压缩编 码,JPEG压缩编码标准 第 10 章 图象处理编程工 具及简单的多媒体编程 参考文献 后记 The University of Southern California does not screen or control the content on this website and thus does not guarantee the accuracy, integrity, or quality of such content. All content on this website is provided by and is the sole responsibility of the person from which such content originated, and such content does not necessarily reflect the opinions of the University administration or the Board of Trustees 中国图象图形网 www.image2003.com 前 言 当你看到那些用 PhotoShop 或 Corel PhotoPaint 等软件绘制出的精美图片,并被它们表现出的 神奇效果所折服时,是否曾想到做一个自己的图象处理软件?就象 PhotoShop 那样。“怎么 可能,吹牛吧!”你一定会这么说。呵呵,别着急,待我慢慢讲来。 我是一名清华大学计算机系的研究生,一直对图象处理、多媒体的东西非常着迷,选修过不 少这方面的课程,做过许多有意思的实验。我常常有这样一种冲动:把我做过的这些东西拿 出来与大家一起分享,把有关的原理、算法、程序介绍给大家。 有这种想法的另一个原因是:数字图象处理(Digital Image Processing)技术是一门非常实用的 技术。PhotoShop 的核心就是图象处理技术。而目前有关这方面的资料太少了,已有的书不 是内容太陈旧(还停留在 DOS 下的 CGA,EGA 甚至汇编编程阶段),就是理论性太强,不容 易懂,没有例子,看过以后也不知道该如何编程。我想:如果能够通过实际的例子来介绍这 些图象处理算法,大家就很容易理解了。 这本书的侧重点是介绍数字图象编程,因此在内容的选择上也有所考虑,介绍的都是图象处 理中的基本算法,大多可以用程序实现。而那些理论性很强,不易编程的算法,在这本书里 是找不到的。书中的每一章可以看做是一个专题,后面都附有实际运行通过的源程序例子, 全部程序都采用 Windows 编程(不过我并不打算介绍 Windows 的编程,这已经超出了本书的 范围)。 下面是本书的主要内容: (1) windows 位图 bitmap(即 bmp 文件)的结构和调色版的概念; (2) 图象的平移、旋转、镜象变换、转置变换、放缩; (3) 图象的平滑(去噪声)、锐化; (4) 图象的半影调、抖动技术; (5) 图象的直方图修正、彩色变换; (6) 图象的腐蚀和膨胀效果、细化算法、骨架的提取; (7) 图象的边沿检测与抽取、hough 变换、轮廓跟踪; 中国图象图形网 www.image2003.com (8) 图象的检测、模板匹配; (9) 图象的压缩编码、JPEG 压缩编码标准; (10) 图象处理编程工具 lead.ocx、DirectDraw 及简单的多媒体编程技术。 这里面有一些非常有趣的应用。例如第 4 章介绍了一个将一幅图象转换成 ASCII 码的算法。 对于喜欢上 BBS 的读者,这个算法是非常有用的。在第 10 章,介绍了一款非常实用的图象 处理编程工具 lead.ocx,利用它可以很快地开发出非常棒的图象处理软件。 学习了这些内容,你就掌握了图象处理中的一些最常用的算法。当然,这些内容还不足以编 出象 PhotoShop 那样“牛”的软件来(人家毕竟是 Adobe 公司的看家宝贝嘛!)。但是要知 道:万丈高楼平地起,很多非常复杂的功能可能是一些简单方法的叠加。相信你自己吧,你 一定能够成为图象处理大师的。 是不是还有些信心不足?好,让我变一个戏法给你瞧瞧。 图1 普通文本 图2 抽取骨架后的文本 上面有两幅图,图1是未经处理的普通文字,经过骨架抽取,变成了图2的样子。这可不是 用 PhotoShop 做的,而是我自己编的程序处理的。怎么样?还不错吧。 有人会问:“在编这样的程序之前有什么要求吗?”回答是:“有,只有两条:(1)对 c 语言 比较熟悉;(2)曾经编过 Windows 的程序。 中国图象图形网 www.image2003.com 有三点要说明: (1) 文中出现的所有例子都在我自己的机器上编译运行通过,我使用的编程语言为 Visual C++4.1,运行环境为中文 Windows95 或 Windows98。程序采用的是 Windows API 接口,全 部采用 C 语言编写,并未用到 C++的东西,所以也可以在其他的 C 编译器,如 Borland C, Watcom C 下编译通过(可能有些函数的名称有些差别,所以建议使用 Visual C++4.0,4.1, 4.2,5.0 版本都可以)。尽管在 Windows3.x 平台上也能编译运行这些程序,但强烈建议使用 Windows95 或 Windows98,因为 32 位的虚拟内存环境用起来爽极了。 (2) 既然是编图象的程序,当然要把机器的分辨率和颜色数调大一点了,这样显示出来的图 象才显得漂亮(我用的是 800*600,16bits 即 64K 色)。另外,装备一些好的图象软件是绝对必 要的。我经常使用以下几种软件: z Sea,在 DOS 下的看图工具,而且可以很方便地转换图象格式; z AcdSee,一个小巧玲珑的看图软件; z Ulead IPhotoPlus,最大的优点是可以进行调色板的处理; z Windows PaintBrush,不要以为画笔的功能很弱,其实很多情况下还是很有用的; z PhotoShop,就不用我多说了。 (3) 图象处理的算法之中不可避免地要遇到一些数学的公式,霍金说过: “每多一个公式就要吓跑一半读者”,我将尽可能用通俗的语言将这些原理,公 式讲解出来,力求做到公式尽可能的少;但遇到只有用公式才能讲明白的时候, 我也决不回避,希望大家能耐着性子看下去。 本书主要参考的是我上数字图象处理课时的教材,作者是朱志刚老师,在此表示感谢。还要 感谢我的好朋友袁昱,诸晓文和清华出版社的各位老师,没有他们的帮助,这本书的的出版 是不可能的。 好了,不多说了,现在就让我们进入五彩缤纷的图象世界吧! 作者 1999 年 6 月于清华大学 中国图象图形网 www.image2003.com 目 录 前 言... 目 录... 第 1 章 WINDOWS位图和调色板... 1.1 位图和调色板的概念... 1.2 BMP文件格式... 1.3 显示一个BMP文件的C程序... 第 2 章 图象的几何变换... 2.1 平移... 2.2 旋转... 2.3 镜象... 2.4 转置... 2.5 缩放... 第 3 章 图象的平滑(去噪声)、锐化... 3.1 平滑... 3.2 中值滤波... 3.3 锐化... 第 4 章 图象的半影调和抖动技术... 4.1 图案法... 中国图象图形网 www.image2003.com 4.2 抖动法... 4.3 将BMP文件转换为TXT文件... 第 5 章 直方图修正和彩色变换... 5.1 反色... 5.2 彩色图转灰度图... 5.3 真彩图转 256 色图... 5.4 对比度扩展... 5.5 削波... 5.6 阈值化... 5.7 灰度窗口变换... 5.8 灰度直方图统计... 5.9 灰度直方图均衡化... 第 6 章 腐蚀,膨胀,细化算法... 6.1 腐蚀... 6.2 膨胀... 6.3 开... 6.4 闭... 6.5 细化... 第 7 章 边沿检测与提取,轮廓跟踪... 中国图象图形网 www.image2003.com 7.1 边沿检测... 7.2 HOUGH变换... 7.3 轮廓提取... 7.4 种子填充... 7.5 轮廓跟踪... 第 8 章 图象的检测及模板匹配... 8.1 投影法... 8.2 差影法... 8.3 模板匹配... 第 9 章 图象的压缩编码,JPEG压缩编码标准... 9.1 哈夫曼编码... 9.2 行程编码... 9.3 LZW算法的大体思想... 9.4 JPEG压缩编码标准... 第 10 章 图象处理编程工具及简单的多媒体编程... 10.1 LEADTOOLS. 10.2 DIRECTDRAW... 10.3 简单的多媒体编程... 参考文献... 中国图象图形网 www.image2003.com 后 记... 中国图象图形网 www.image2003.com 第 1 章 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 级。这种分级概念称为量化。 中国图象图形网 www.image2003.com 这样,根据红、绿、蓝各种不同的组合我们就能表示出 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 中国图象图形网 www.image2003.com 位图,许多图象文件格式如 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; 中国图象图形网 www.image2003.com } 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; 中国图象图形网 www.image2003.com 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 的情况。 中国图象图形网 www.image2003.com 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; //该颜色的蓝色分量 中国图象图形网 www.image2003.com 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 的全 中国图象图形网 www.image2003.com 局变量 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){ 中国图象图形网 www.image2003.com 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); 中国图象图形网 www.image2003.com //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; 中国图象图形网 www.image2003.com 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) 中国图象图形网 www.image2003.com (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 不为零,说明用到了调色板 { 中国图象图形网 www.image2003.com //为逻辑调色板分配局部内存,大小为逻辑调色板结构长度加 //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++; //指针移到下一项 中国图象图形网 www.image2003.com } //产生逻辑调色板,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); 中国图象图形网 www.image2003.com //将原来的调色板(如果有的话)选入设备上下文句柄 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); 中国图象图形网 www.image2003.com 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); 中国图象图形网 www.image2003.com //将调色板选入内存设备上下文 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,表 中国图象图形网 www.image2003.com 示直接将源矩形拷贝到目标矩形。还可以是反色,擦除,做“与”运算等操作,具体细节见 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 中国图象图形网 www.image2003.com 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\S YSTEM; 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,欣赏一下你今天的劳动成果。 The University of Southern California does not screen or control the content on this website and thus does not gar antee the accuracy, integrity, or quality of such content. All content on this website is provided by and is the sole responsibility of the person from which such content originated, and such content does not necessarily reflect the o pinions of the University administration or the Board of Trustees 中国图象图形网 www.image2003.com 第 2 章 图象的几何变换 这一章我们将介绍图象的几何变换,包括图象的平移、旋转、镜象变换、转置、放缩等。如 果你熟悉矩阵运算,你将发现,实现这些变换是非常容易的。 2.1 平移 平移(translation)变换大概是几何变换中最简单的一种了。 如图 2.1 所示,初始坐标为(x0,y0)的点经过平移(tx,ty)(以向右,向下为正方向)后,坐标变为 (x1,y1)。这两点之间的关系是x1=x0+tx ,y1=y0+ty。 图 2.1 平移的示意图 以矩阵的形式表示为 (2.1) 我们更关心的是它的逆变换: (2.2) 中国图象图形网 www.image2003.com 这是因为:我们想知道的是平移后的图象中每个象素的颜色。例如我们想知道,新图中左上 角点的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 所示。 中国图象图形网 www.image2003.com 图 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 分量代表了色差信号(你不必了解 什么是色差,只要知道有这么一个概念就可以了)。使用这种表示方法有很多好处,最主要的 有两点: 中国图象图形网 www.image2003.com (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 等软件提供的颜色转换功能将彩色图 转换成灰度图。 中国图象图形网 www.image2003.com 好了,言归正传,下面给出Translation的源代码。算法的思想是先将所有区域填成白色,然 后找平移后显示区域的左上角点(x0,y0) 和右下角点(x1,y1) ,分几种情况进行处理。 先看 x 方向(width 指图象的宽度) (1) tx≤-width:很显然,图象完全移出了屏幕,不用做任何处理; (2) -width=0) && (x0=0) && (y0=0) && (x0=0) && (y0g2){ if(g2>g3) g=g2; else{ if(g1>g3) g=g3; else g=g1; } } 中国图象图形网 www.image2003.com 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); 中国图象图形网 www.image2003.com 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)式),又是个数学家的名字,可见学好数 学,走遍天下都不怕。 中国图象图形网 www.image2003.com (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}; 中国图象图形网 www.image2003.com 以后我们碰到其它的模板,仍然要用这个函数,所做的操作只是增加一个常量标识,及其对 应的模板数组。 要注意的是,运算后如果出现了大于 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){ //判断模板类型 中国图象图形网 www.image2003.com 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) { 中国图象图形网 www.image2003.com 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;y255.0) *lpTempPtr=(BYTE)255; else if(TempNum<0.0) *lpTempPtr=(unsigned char)fabs(TempNum); else *lpTempPtr=(BYTE)TempNum; } hDc=GetDC(hWnd); if(hBitmap!=NULL) 中国图象图形网 www.image2003.com 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; } The University of Southern California does not screen or control the content on this website and thus does not gua rantee the accuracy, integrity, or quality of such content. All content on this website is provided by and is the sole responsibility of the person from which such content originated, and such content does not necessarily reflect the opinions of the University administration or the Board of Trustees 中国图象图形网 www.image2003.com 第 4 章 图象的半影调和抖动技术 在介绍本章内容之前,先提出一个问题?普通的黑白针式打印机能打出灰度图来吗?如果说 能,从针式打印机的打印原理来分析,似乎是不可能的。因为针打是靠撞针击打色带在纸上 形成黑点的,不可能打出灰色的点来;如果说不能,可是我们的确见过用针式打印机打印出 来的灰色图象。到底是怎么回事呢? 你再仔细看看那些打印出来的所谓的灰色图象,最好用放大镜看。你会发现,原来这些灰色 图象都是由一些黑点组成的,黑点多一些,图象就暗一些;黑点少一些,图案就亮一些。下 面这几张图就很能说明这一点。 图 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。所以打出来的图象要比计 算机显示出来的清晰的多。扫描仪的分辨率要高一些,数码相机的分辨率更高。 中国图象图形网 www.image2003.com 言归正传,前面讲了,图案化使用图案来表示象素的灰度,那么我们来做一道计算题。假设 有一幅 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 标准图案举例 中国图象图形网 www.image2003.com 图 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 运算实质上是引入了随机成分,这就是我们下面要讲到的抖动技术。 中国图象图形网 www.image2003.com 图 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, 中国图象图形网 www.image2003.com 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); 中国图象图形网 www.image2003.com 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>2) > BayerPattern[y&7][x&7]) //右移两位后做比较 *(lpTempPtr++)=(unsigned char)255; //打白点 中国图象图形网 www.image2003.com 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); 中国图象图形网 www.image2003.com 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 打黑点 中国图象图形网 www.image2003.com 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 类型的缓冲区存储新值。另外, 当按从左到右,从上到下的顺序处理象素时,处理过的象素以后不会再用到了,所以用这个 中国图象图形网 www.image2003.com 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;//要开的缓冲区的大小 中国图象图形网 www.image2003.com 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 128 ){ //128 是中值 *lpIntPtr=255; //打白点 e=(float)(num-255.0); //计算误差 } else{ *lpIntPtr=0; //打黑点 e=(float)num; //计算误差 } if(x255) tempnum=255; else if (tempnum<0) tempnum=0; 中国图象图形网 www.image2003.com *(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); 中国图象图形网 www.image2003.com GlobalUnlock(hImgData); LocalUnlock(hTempImgData); LocalFree(hTempImgData); LocalUnlock(hIntBuf); LocalFree(hIntBuf); return TRUE; } 要注意的是,误差传播有时会引起流水效应,即误差不断向下,向右累加传播。解决的办法 是:奇数行从左到右传播,偶数行从右到左传播。 4.3 将bmp文件转换为txt文件 在讲图案化技术时,我突然想到了一个非常有趣的应用,那就是 bmp2txt。如果你喜欢上 BBS(电子公告牌系统),你可能想做一个花哨的签名档。瞧,这是我好朋友 Casper 的签名档 (见图 4.8),胖乎乎的,是不是特别可爱? 图 4.8 Casper 的签名档 中国图象图形网 www.image2003.com 你仔细观察一下,就会发现,这是一幅全部由字符组成的图,因为在 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','[',']', 中国图象图形网 www.image2003.com '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; 中国图象图形网 www.image2003.com 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; //共有多少行字符 中国图象图形网 www.image2003.com hf=_lcreat("c:\\bmp2txt.txt",0); for(i=0;ipalNumEntries =(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); 中国图象图形网 www.image2003.com hPalette=CreatePalette(pPal); //产生新的调色板 LocalUnlock(hPal); LocalFree(hPal); if(hPalette){ hPrevPalette=SelectPalette(hDc,hPalette,FALSE); RealizePalette(hDc); } } else{ //不带调色板,说明是真彩色图 for(y=0;ypalNumEntries =(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++) { //带调色板的彩色图 中国图象图形网 www.image2003.com 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); 中国图象图形网 www.image2003.com LocalUnlock(hPal); LocalFree(hPal); hDc=GetDC(hWnd); if(hPalette){ hPrevPalette=SelectPalette(hDc,hPalette,FALSE); RealizePalette(hDc); } if(NumColors==0) //真彩色图才需要处理位图数据 for(y=0;y>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]){ 中国图象图形网 www.image2003.com 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)); 中国图象图形网 www.image2003.com 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; 中国图象图形网 www.image2003.com 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); 中国图象图形网 www.image2003.com LocalUnlock(hPal); LocalFree(hPal); hDc=GetDC(hWnd); if(hPalette){ hPrevPalette=SelectPalette(hDc,hPalette,FALSE); RealizePalette(hDc); } for(y=0;y>4); for (i = 0; i < PalCounts;i++) if (ClrIndex == ColorIndex[i]){ 中国图象图形网 www.image2003.com //根据 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); } 中国图象图形网 www.image2003.com 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)的原理。 中国图象图形网 www.image2003.com 图 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 进行对比度扩展的结果。可以看出亮的区域(雕 塑)变得更亮,暗的区域(手)变得更暗。 中国图象图形网 www.image2003.com 图 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; 中国图象图形网 www.image2003.com 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; 中国图象图形网 www.image2003.com } 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; 中国图象图形网 www.image2003.com } 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(GraypalPalEntry[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); 中国图象图形网 www.image2003.com } 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); 中国图象图形网 www.image2003.com 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 进行削波的结果。把亮的区域(雕塑)提取了出来。 中国图象图形网 www.image2003.com 图 5.8 图 5.1 削波处理后的结果 削波的程序和对比度扩展的程序很类似,就不再给出了。 5.6 阈值化 阈值化(thresholding)可以看作是削波的一个特例,我们用图 5.9 说明阈值化的原理。 图 5.9 阈值化的原理 不难看出,只要令削波中的g1old=g2old就实现了阈值化。阈值就象个门槛,比它大就是白,比 它小就是黑。经过阈值化处理后的图象变成了黑白二值图,所以说阈值化是灰度图转二值图 的一种常用方法(我们以前介绍过图案化和抖动的方法)。进行阈值化只需给出阈值点g1old即 可。 图 5.10 为图 5.1 阈值取 128,阈值化处理后的结果,是一幅黑白图。 中国图象图形网 www.image2003.com 图 5.10 图 5.1 阈值化处理后的结果 阈值化的程序和对比度扩展的程序很类似,就不再给出了。 5.7 灰度窗口 变换 灰度窗口变换(slicing)是将某一区间的灰度级和其它部分(背景)分开。我们用图 5.11 和图 5.12 说明灰度窗口变换的原理。其中[g1old,g2old]称为灰度窗口。 图 5.11 清除背景的灰度窗口变换的原理 图 5.12 保留背景的灰度窗口变换的原理 灰度窗口变换有两种,一种是清除背景的,一种是保留背景的。前者把不在灰度窗口范围内 的象素都赋值为 0,在灰度窗口范围内的象素都赋值为 255,这也能实现灰度图的二值化; 后者是把不在灰度窗口范围内的象素保留原灰度值,在灰度窗口范围内的象素都赋值为 255。灰度窗口变换可以检测出在某一灰度窗口范围内的所有象素,是图象灰度分析中的一 个有力工具。 中国图象图形网 www.image2003.com 下面有三幅图,图 5.13 为原图;图 5.14 是经过清除背景的灰度窗口变换处理后的图(灰度窗 口取[200-255]),将夜景中大厦里的灯光提取了出来;图 5.15 是经过保留背景的灰度窗口变 换处理后的图(灰度窗口取[200-255]),将夜景中大厦里的灯光提取了出来,同时保留了大厦 的背景,可以看出它们的差别还是很明显的。 图 5.13 原图 图 5.14 图 5.13 经过 清除背景的灰度窗 口变换处理后的图 图 5.15 图 5.13 经过 保留背景的灰度窗 口变换处理后的图 灰度窗口变换的程序和对比度扩展的程序很类似,就不再给出了。 不久前在一本科学杂志上看到一篇文章,非常有趣,是介绍电影“阿甘正传”的特技制作 的。其中有一项就用到了类似灰度窗口变换的思想。相信看过这部电影的读者都会对那个断 腿的丹尼上校有深刻的印象。他的断腿是怎么拍出来的呢?其实方法很简单,先拍一幅没有 演员出现的背景画面,然后拍一幅有演员出现,其它不变的画面。要注意的是,此时演员的 腿用蓝布包裹。把前后两幅图输入计算机进行处理。第二幅图中凡是遇到蓝色的象素,就用 第一幅图中对应位置的背景象素代替。这样,一位断腿的上校就逼真的出现在屏幕上了。这 就是电影特技中经常用到的“蓝幕”技术。 说点题外话。其实现代电影,特别是好莱坞电影,越来越离不开计算机及图象处理技术。最 近引起轰动的大片“泰坦尼克号”中的很多特技镜头就是利用了庞大的 SGI 图形工作站机群 没日没夜的计算产生的。图象处理技术和我们所喜爱的电影艺术紧密的结合了起来,更增加 了我们学习它的兴趣。 中国图象图形网 www.image2003.com 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; 中国图象图形网 www.image2003.com 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)GlobalLo ck(hImgData); for(y=0;yMaxGrayNum) MaxGrayNum=temp; //找到更大的了 if( (temp0) ) MinGrayNum=temp; //找 到更小的了 } GlobalUnlock(hImgData); //产生新的窗口显示结果 hPopupWnd = CreateWindow ("PopupWindowClass", "Histogram Statistic Window", WS_OVERLAPPEDWINDOW,50,80,550,350, hWnd,NULL,ghInst,NULL); if (hPopupWnd){ 中国图象图形网 www.image2003.com 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); 中国图象图形网 www.image2003.com 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)); } 中国图象图形网 www.image2003.com 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),将原灰度直方图改造成你所希望的直方图。所 以,直方图修正的关键就是灰度映射函数。我们刚才介绍的阈值化、削波、灰度窗口变换等 等,都是灰度映射函数。 直方图均衡化是一种最常用的直方图修正。它是把给定图象的直方图分布改造成均匀直方图 分布。由信息学的理论来解释,具有最大熵(信息量)的图象为均衡化图象。直观地讲,直方 图均衡化导致图象的对比度增加。 由于直方图均衡化涉及到很多概率和数学的知识,具体的细节这里就不介绍了,只给出算 法。通过下面的例子,就很容易明白了。 中国图象图形网 www.image2003.com 有一幅图象,共有 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 要 均匀一些。 中国图象图形网 www.image2003.com 图 5.17 Pi 的分布 图 5.18 Qi 的分布 要注意的是,均衡化处理后的图象只能是近似均匀分布。均衡化图象的动态范围扩大了,但 其本质是扩大了量化间隔,而量化级别反而减少了,因此,原来灰度不同的象素经处理后可 能变的相同,形成了一片的相同灰度的区域,各区域之间有明显的边界,从而出现了伪轮 廓。 图 5.19 为图 5.13 经直方图均衡化处理后,量化为 128 级灰度的结果;图 5.20 为它的直方图 分布。为什么天亮了起来呢?分析一下就明白了:因为原图中低灰度的点太多了,所以 s 数 组前面的元素很大。经过 L[i]=s[i]×(128-1)的处理后,原图中低灰度的点的灰度值提高了不 少,所以那片暗区变亮了。同时可以看出,天空中出现了伪轮廓。 中国图象图形网 www.image2003.com 图 5.19 图 5.13 经直方图均衡化处理后的结果 图 5.20 图 5.19 的灰度直方图 图 5.21 为图 5.1 直方图均衡化后的结果(128 级灰度),暗的区域(手)变亮了,看起来更清楚一 些。 图 5.21 图 5.1 直方图均衡化后的结果 中国图象图形网 www.image2003.com 下面给出直方图均衡化的源程序: 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]; 中国图象图形网 www.image2003.com 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) 中国图象图形网 www.image2003.com { 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;ypalNumEntries =(WORD) 256; pPal->palVersion = 0x300; lpTempPtr=(char *)lpTempImgData+sizeof(BITMAPINFOHEADER); for (i = 0; i < EquaScale; i++) { 中国图象图形网 www.image2003.com 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); 中国图象图形网 www.image2003.com } for(y=0;y (*TempLine).topy){ //记录该直线最高点的 x,y 坐标 (*TempLine).topx=x; (*TempLine).topy=y; } if(y< (*TempLine).boty){ //记录该直线最低点的 x,y 坐标 (*TempLine).botx=x; (*TempLine).boty=y; } } } maxd=0; 中国图象图形网 www.image2003.com for (i=0;i<(long)Dist*Alpha;i++){ TempLine=(MYLINE*)(lpMyLine+i); k=*(lpDistAlpha+i); if(k > maxd){ //找到数组元素中最大的,及相应的直线端点 maxd=k; MaxdLine.topx=(*TempLine).topx; MaxdLine.topy=(*TempLine).topy; MaxdLine.botx=(*TempLine).botx; MaxdLine.boty=(*TempLine).boty; } } hDc = GetDC(hWnd); rhp = CreatePenIndirect(&rlp); SelectObject(hDc,rhp); MoveToEx(hDc,MaxdLine.botx,MaxdLine.boty,NULL); //在两端点之间画一条红线用来标识 LineTo(hDc,MaxdLine.topx,MaxdLine.topy); DeleteObject(rhp); ReleaseDC(hWnd,hDc); 中国图象图形网 www.image2003.com //释放内存及资源 GlobalUnlock(hImgData); GlobalUnlock(hDistAlpha); GlobalFree(hDistAlpha); GlobalUnlock(hMyLine); GlobalFree(hMyLine); return TRUE; } 如果 是给定的,用上述方法,我们可以找到该方向上最长的直线。 其实 Hough 变换能够查找任意的曲线,只要你给定它的方程。这里,我们就不详述了。 7.3 轮廓提取 轮廓提取的实例如图 7.9、图 7.10 所示。 图 7.9 原图 图 7.10 轮廓提取 轮廓提取的算法非常简单,就是掏空内部点:如果原图中有一点为黑,且它的 8 个相邻点都 是黑色时(此时该点是内部点),则将该点删除。要注意的是,我们处理的虽然是二值图,但 实际上是 256 级灰度图,不过只用到了 0 和 255 两种颜色。源程序如下: BOOL Outline(HWND hWnd) 中国图象图形网 www.image2003.com { DWORD OffBits,BufSize; LPBITMAPINFOHEADER lpImgData; LPSTR lpPtr; HLOCAL hTempImgData; LPBITMAPINFOHEADER lpTempImgData; LPSTR lpTempPtr; HDC hDc; HFILE hf; LONG x,y; int num; int nw,n,ne,w,e,sw,s,se; //我们处理的实际上是 256 级灰度图,不过只用到了 0 和 255 两种颜色。 if( NumColors!=256){ MessageBox(hWnd,"Must be a mono bitmap with grayscale palette!", "Error Message",MB_OK|MB_ICONEXCLAMATION); return FALSE; } OffBits=bf.bfOffBits-sizeof(BITMAPFILEHEADER); //BufSize 为缓冲区大小 中国图象图形网 www.image2003.com 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=1;y=SeedFillStack.ElementsNum) return FALSE; //栈已满,返回 FALSE //进栈,栈顶指针加 1 TempPtr=(POINT *)(SeedFillStack.lpMyStack+SeedFillStack.ptr++); (*TempPtr).x=p.x; (*TempPtr).y=p.y; return TRUE; } //pop 操作 POINT MyPop() { POINT InvalidP; InvalidP.x=-1; InvalidP.y=-1; if(SeedFillStack.ptr<=0) return InvalidP; //栈为空,返回无效点 SeedFillStack.ptr--; //栈顶指针减 1 //返回栈顶点 中国图象图形网 www.image2003.com return *(SeedFillStack.lpMyStack+SeedFillStack.ptr); } //判断堆栈是否为空 BOOL IsStackEmpty() { return (SeedFillStack.ptr==0)?TRUE:FALSE; } 如果读者对堆栈的概念还不清楚,请参阅有关数据结构方面的书籍,这里就不详述了。 要注意的是:(1)要填充的区域是封闭的;(2)我们处理的虽然是二值图,但实际上是 256 级灰 度图,不过只用到了 0 和 255 两种颜色;(3)在菜单中选择种子填充命令时,提示用户用鼠标 点取一个要填充区域中的点,处理是在 WM_LBUTTONDOWN 中。 MYSTACK SeedFillStack; BOOL SeedFill(HWND hWnd) { DWORD OffBits,BufSize; LPBITMAPINFOHEADER lpImgData; HLOCAL hTempImgData; LPBITMAPINFOHEADER lpTempImgData; LPSTR lpTempPtr,lpTempPtr1; HDC hDc; HFILE hf; 中国图象图形网 www.image2003.com POINT CurP,NeighborP; //我们处理的实际上是 256 级灰度图,不过只用到了 0 和 255 两种颜色。 if( NumColors!=256){ MessageBox(hWnd,"Must be a mono bitmap with grayscale palette!", "Error Message",MB_OK|MB_ICONEXCLAMATION); return FALSE; } 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=(LPBITMAPINFOHEADER)GlobalLock(hImgData); lpTempImgData=(LPBITMAPINFOHEADER)LocalLock(hTempImgData); //拷贝头信息和位图数据 中国图象图形网 www.image2003.com memcpy(lpTempImgData,lpImgData,BufSize); if(!InitStack(hWnd,(LONG)bi.biHeight*bi.biWidth)){ //初始化堆栈 //若失败,释放内存,返回 LocalUnlock(hTempImgData); LocalFree(hTempImgData); GlobalUnlock(hImgData); return FALSE; } lpTempPtr=(char*)lpTempImgData+ (BufSize-LineBytes-SeedPoint.y*LineBytes)+SeedPoint.x; if(*lpTempPtr==0){ //鼠标点到了黑点上,提示用户不能选择边界上的点,返回 FALSE MessageBox(hWnd,"The point you select is a contour point!", "Error Message",MB_OK|MB_ICONEXCLAMATION); LocalUnlock(hTempImgData); LocalFree(hTempImgData); GlobalUnlock(hImgData); DeInitStack(); return FALSE; } 中国图象图形网 www.image2003.com //push 该点(用户用鼠标选择的,处理是在 WM_LBUTTONDOWN 中 MyPush(SeedPoint); while(!IsStackEmpty()) //堆栈不空则一直处理 { CurP=MyPop(); //pop 栈顶的点 lpTempPtr=(char*)lpTempImgData+ (BufSize-LineBytes-CurP.y*LineBytes)+CurP.x; //将该点涂黑 *lpTempPtr=(unsigned char)0; //左邻点 if(CurP.x>0) //注意判断边界 { NeighborP.x=CurP.x-1; NeighborP.y=CurP.y; lpTempPtr1=lpTempPtr-1; if(*lpTempPtr1!=0) //如果为白,表示还没有填,进栈 MyPush(NeighborP); } //上邻点 if(CurP.y>0) //注意判断边界 中国图象图形网 www.image2003.com { NeighborP.x=CurP.x; NeighborP.y=CurP.y-1; lpTempPtr1=lpTempPtr+LineBytes; if(*lpTempPtr1!=0) //如果为白,表示还没有填,进栈 MyPush(NeighborP); } //右邻点 if(CurP.x0xc0,说明是行程(Run Length)信息,即 C 的低 6 位表示后面连续的字节个数(所以 最多 63 个连续颜色相同的象素,若还有颜色相同的象素,将在下一个行程处理),文件的下 一个字节就是实际的图象数据(即该颜色在调色板中的索引值)。若 C<0xc0,则表示 C 是实际 的图象数据。如此反复,直到这 bytes_per_line 个字节处理完,这一行的解码完成。PCX 就 是有若干个这样的解码行组成。 下面是实现 256 色 PCX 文件解码的源程序,其中第二个函数对一行进行解码,应该把阅读的 重点放在这个函数上。要注意的是,执行时文件 C:\\test.pcx 必须存在,而且是一个 256 色 PCX 文件。 unsigned int PcxBytesPerLine; BOOL LoadPcxFile (HWND hWnd,char *PcxFileName) { FILE *PCXfp; PCXHEAD header; LOGPALETTE *pPal; HPALETTE hPrevPalette; HDC hDc; HLOCAL hPal; DWORD ImgSize; DWORD OffBits,BufSize; LPBITMAPINFOHEADER lpImgData; DWORD i; LONG x,y; 中国图象图形网 www.image2003.com int PcxTag; unsigned char LineBuffer[6400]; LPSTR lpPtr; HFILE hfbmp; if((PCXfp=fopen(PcxFileName,"rb"))==NULL){ //文件没有找到 MessageBox(hWnd,"File c:\\test.pcx not found!","Error Message", MB_OK|MB_ICONEXCLAMATION); return FALSE; } //读出头信息 fread((char*)&header,1,sizeof(PCXHEAD),PCXfp); if(header.manufacturer!=0x0a){ //不是一个合法的 PCX 文件 MessageBox(hWnd,"Not a valid Pcx file!","Error Message", MB_OK|MB_ICONEXCLAMATION); fclose(PCXfp); return FALSE; } //将文件指针指向调色板开始处 fseek(PCXfp,-769L,SEEK_END); //获取颜色数信息 中国图象图形网 www.image2003.com PcxTag=fgetc(PCXfp)&0xff; if(PcxTag!=12){ //非 256 色,返回 MessageBox(hWnd,"Not a 256 colors Pcx file!","Error Message", MB_OK|MB_ICONEXCLAMATION); fclose(PCXfp); return FALSE; } //创建新的 BITMAPFILEHEADER 和 BITMAPINFOHEADER memset((char *)&bf,0,sizeof(BITMAPFILEHEADER)); memset((char *)&bi,0,sizeof(BITMAPINFOHEADER)); //填写 BITMAPINFOHEADER 头信息 bi.biSize=sizeof(BITMAPINFOHEADER); //得到图象的宽和高 bi.biWidth=header.xmax-header.xmin+1; bi.biHeight=header.ymax-header.ymin+1; bi.biPlanes=1; bi.biBitCount=8; bi.biCompression=BI_RGB; ImgWidth=bi.biWidth; ImgHeight=bi.biHeight; 中国图象图形网 www.image2003.com NumColors=256; LineBytes=(DWORD)WIDTHBYTES(bi.biWidth*bi.biBitCount); ImgSize=(DWORD)LineBytes*bi.biHeight; //填写 BITMAPFILEHEADER 头信息 bf.bfType=0x4d42; bf.bfSize=sizeof(BITMAPFILEHEADER)+sizeof(BITMAPINFOHEADER)+ NumColors*sizeof(RGBQUAD)+ImgSize; bf.bfOffBits=(DWORD)(NumColors*sizeof(RGBQUAD)+ sizeof(BITMAPFILEHEADER)+sizeof(BITMAPINFOHEADER)); //为新图分配缓冲区 if((hImgData=GlobalAlloc(GHND,(DWORD) (sizeof(BITMAPINFOHEADER)+ NumColors*sizeof(RGBQUAD)+ImgSize)))==NULL) { MessageBox(hWnd,"Error alloc memory!","ErrorMessage", MB_OK|MB_ICONEXCLAMATION); fclose(PCXfp); return FALSE; } lpImgData=(LPBITMAPINFOHEADER)GlobalLock(hImgData); 中国图象图形网 www.image2003.com //拷贝头信息 memcpy(lpImgData,(char *)&bi,sizeof(BITMAPINFOHEADER)); lpPtr=(char *)lpImgData+sizeof(BITMAPINFOHEADER); //为 256 色调色板分配内存 hPal=LocalAlloc(LHND,sizeof(LOGPALETTE)+ NumColors* sizeof(PALETTEENTRY)); pPal =(LOGPALETTE *)LocalLock(hPal); pPal->palNumEntries =256; pPal->palVersion = 0x300; for (i = 0; i < 256; i++) { //读取调色板中的 RGB 值 pPal->palPalEntry[i].peRed=(BYTE)fgetc(PCXfp); pPal->palPalEntry[i].peGreen=(BYTE)fgetc(PCXfp); pPal->palPalEntry[i].peBlue=(BYTE)fgetc(PCXfp); pPal->palPalEntry[i].peFlags=(BYTE)0; *(lpPtr++)=(unsigned char)pPal->palPalEntry[i].peBlue; *(lpPtr++)=(unsigned char)pPal->palPalEntry[i].peGreen; *(lpPtr++)=(unsigned char)pPal->palPalEntry[i].peRed; *(lpPtr++)=0; } 中国图象图形网 www.image2003.com //产生新的逻辑调色板 hPalette=CreatePalette(pPal); LocalUnlock(hPal); LocalFree(hPal); hDc=GetDC(hWnd); if(hPalette){ hPrevPalette=SelectPalette(hDc,hPalette,FALSE); RealizePalette(hDc); } //解码行所占的字节数 PcxBytesPerLine=(unsigned int)header.bytes_per_line; //将文件指针指向图象数据的开始处 fseek(PCXfp,(LONG)sizeof(PCXHEAD),SEEK_SET); //缓冲区大小 OffBits=bf.bfOffBits-sizeof(BITMAPFILEHEADER); //BufSize 为缓冲区大小 BufSize=OffBits+bi.biHeight*LineBytes; for(y=0;yAdd To Project->Components and Controls 菜单(其它的 VC 版本,如 4.0、4.1、4.2,为 中国图象图形网 www.image2003.com Insert->Component…菜单),出现对话框,选择“Registered ActiveX Controls”那 个目录(其它的 VC 版本为”OLE Controls”那一页);(2)选中"Lead Std Control"那一 项,如图 10.1 所示。然后按下 Insert 菜单即可; 图 10.1 Lead 控件 图 10.2 工具条 (3)如果没有该项,找到 lead32.ocx 文件所在的目录(一般为 95 的系统目录), 选择该文件,按下 Insert 按钮即可(其它 VC 版本的操作方法是:按下 Customize 菜单,出现一个新的对话框,按下 Import 菜单,找到 lead32.ocx 文件所在的目 录,选择该文件,按下 Import 按钮,再按下 OK 按钮,然后做(2)所做的工作即 可)。按下 Close 按钮,关闭 Component 对话框。插入该控件后,会出现一个对话 框,提示你新增加了两个类:Clead 和 Cpicture,选择 Ok。打开对话框资源,选 择 ID 为 IDD_TESTLEAD_DIALOG 的对话框,这时可以看到该控件已经添加到 工具条中,如图 10.2 所示。 步骤 3:将 Lead 控件从工具条拖到对话框中,设置好合适的大小,右击该控 件,选择 Property 菜单,设置其 ID 属性为 IDC_LEAD1,删除“TODO: Place form controls on this dialog”的静态文本。 中国图象图形网 www.image2003.com 步骤 4:按 Ctrl+w,出现 ClassWizard 对话框,选择 Member Variables 那一 页,为该 ID 添加一个变量 m_Lead1,Category 为 Control,Variable Type 为 Clead。 步骤 5:编辑 TestLeadDlg.cpp 文件中的 OnInitDialog()函数,在 // TODO: Add extra initialization here 后加入如下的代码: m_Lead1.Load(“c:\\test.jpg”,0,0); m_Lead1.SetAutoScroll(TRUE); m_Lead1.SetDstRect(0,0,m_Lead1.GetScaleWidth(), m_Lead1.GetScaleHeight()); m_Lead1.SetDstClipRect(0,0,m_Lead1.GetScaleWidth(), m_Lead1.GetScaleHeight()); m_Lead1.SetAutoRepaint(FALSE); m_Lead1.ForceRepaint(); 步骤 6:编译运行,结果如图 10.3 所示。 图 10.3 例 10.1 的运行结果 可以看到,使用了控件的编程是多么的简单。更复杂的应用,读者可以参考 帮助来完成,要注意的是,使用了 LeadTools 的应用程序一定要注意版权问题, 另外,程序中如果用到了 LeatTools 的 OCX,在制作安装程序(如用 InstallShield) 时一定要将 OLE 的信息进行注册,否则用户无法正常运行程序。 中国图象图形网 www.image2003.com 10.2 DirectDraw 本节内容主要参考自微软公司的技术文档。 相信游戏玩家对 DirectX 这个词并不陌生。最近有越来越多的游戏用到了 DirectX。那么 DirectX 究竟是什么呢? 在 Windows3.x 的时代,由于 Microsoft 提供的 Windows API 不能对硬件直接 操作,使得 Windows3.x 下的游戏不仅效果差,而且运行速度极慢。而 DOS4GW 有很多很好的性能,例如:可对硬件直接操作;访问超过 16 比特大小的内存 区;可对保护模式编程等等,使得大部分游戏是在 DOS4GW 下利用如 WATCOMC 一类的开发工具编制的,如老版本的《仙剑奇侠传》。为了吸引游 戏商到 Windows 平台上来,Microsoft 专门开发了 Windows 的游戏接口 WinG, 然而效果并不理想。在推出 Windows95 之后,Microsoft 又开发了该平台的 GAME SDK,这就是 DirectX。 DirectX 是非常成功的,很多优秀的游戏都又从 DOS 平台移植到 Windows95 平台,《仙剑奇侠传》出了 Win95 版本,Red Alert, Diablo,以及最近十分流行的 Age of Empire,都用到了 DirectX。 DirectX 技术的最大特点是能直接对硬件抽象层(HAL)操作,实现视频、声音 的输出、网络相互通信,特别能对游戏杆直接编程。与传统的 GDI 和 MCI 相 比,不仅大大加快了速度,而且大大地提高了游戏的质量,有人甚至已将 DirectX 称为 Windows95 的 GAME OS。目前的最新版 DirectX5.0 SDK 包括 DirectDraw、DirectSound、DirectPlay、Direct3D、DirectInput、DirectSetup 六个部 分。其中 DirectDraw 管理游戏的视频输出,DirectSound 管理游戏的声音输出, DirectPlay 管理游戏的网络通信,Direct3D 管理游戏的三维图形,DirectInput 管理 游戏的游戏杆控制,DirectSetup 管理游戏的安装。 因为本书的题目是图象处理编程,我们这里只介绍 DirectDraw 的大致原理。 有的读者可能会说:“跑题了,图象处理编程和编游戏有什么关系?”其实 DirectDraw 并不只是用在游戏中,由于它的显示处理速度快,在很多和视频有关 的软件中(比如说 Mpeg 的解码器)都可以用到。 DirectDraw 是为速度而设计的,它绕过与 Windows 的图形设备相连的多个层 次,直接与硬件的底层打交道。这很适合游戏编程,因为它着重于快速产生平稳 的图形。 中国图象图形网 www.image2003.com 但 DirectDraw 最重要的一点在于它对不同的显示适配器具有一个共同的接 口。你不必管你的程序它是否会工作。DirectDraw 利用包含在硬件抽象层(HAL) 中的信息来决定显示适配器的功能。(HAL 是由显示适配器厂商提供的)HAL 为不 同的硬件厂商和使用.DirectDraw 的开发者提供了共同的接口。 然而,DirectDraw 并不只限于利用显示适配器的硬件功能。如果你的程序指 定了某一种特定的显示适配器,例如 XXXX hardware blitter,但用户并没有该硬 件,程序就会使用 DirectDraw 的硬件仿真层(HEL)。在这种情况下,DirectDraw 利用内建的硬件仿真来仿真缺少的硬件。 下图一说明了 DirectDraw 和其他 Windows 显示构件的联系。 图 10.4 DirectDraw 和其他 Windows 显示构件 DirectDraw API 由 DirectDraw 对象组成,它表示具体的显示适配器。另外, DirectDraw API 还包括表示 surface 的 DirectDrawSurface 对象,表示 surface 调色 板的 DirectDrawPalette 对象和表示剪接列表的 DirectDrawClipper 对象。可以用 DirectDraw 对象来创建 DirectDrawSurface 和 DirectDrawPalette 对象。 本节只介绍到这里,目的主要是想说明由于游戏对显示速度的要求非常高, 所以 DirectDraw 是为速度而设计的。如果要编写一个对速度要求很高(如 Mpeg 的 解码器)的图象处理系统中,可以考虑使用 DirectDraw。 中国图象图形网 www.image2003.com 10.3 简单的多媒体编程 前面讲过的内容都是针对数字图象处理这一领域的,现在来谈一些题外的东 西,比如说多媒体和 MPEG。 多媒体这几个字,近年来是非常流行的。它包含的东西也是非常多的,除了 文本,静态图象,还包括音频,视频等媒体信息。有时候在程序中加入一小段多 媒体的东西,能给你的软件增色不少。 其实编制一个多媒体播放程序非常简单,关键代码不超过 10 行代码,你相 信吗?其实质是 MCI。MCI 是 Media Control Interface(媒体控制接口)的缩写,它 提供了一套与设备无关的命令消息和命令字来控制媒体的播放。MCI 可以播放的 文件类型有 AVI,WAV,MIDI,MPEG(如果系统中已经安装了 MPEG 的驱动程 序,如安装 XING,ACTIVEMOVIE 时带的),JPEG 等等。MCI 窗口底部有一个 播放条,上面有播放/暂停(Play/Pause)按键、有显示播放进度的标尺,还有一个菜 单,可调整窗口大小、声音大小,速度快慢等等,就和媒体播放器的界面一样。 库 VFW32.LIB 中提供了 MCI 的所有功能,编译后生成的是真正的可执行文件, 无需附加动态库和控件,如 VBX 和 OCX 等。 源代码如下,其中黑体部分为关键代码,可以采用如下的命令行编译: cl testmci.c user32.lib vfw32.lib //testmci.c #include #include void SetClientRect(HWND hwnd, HWND hwndMCI) { RECT rect; GetWindowRect(hwndMCI, &rect); AdjustWindowRectEx(&rect, GetWindowLong(hwnd, GWL_STYLE), FALSE, 中国图象图形网 www.image2003.com GetWindowLong(hwnd, GWL_EXSTYLE)); MoveWindow(hwnd, rect.left, rect.top, rect.right - rect.left,rect.bottom - rect.top, TRUE); } LRESULT CALLBACK WndProc(HWND hwnd, UINT uMsg,WPARAM wParam, LPARAM lParam) { switch(uMsg) { case MCIWNDM_NOTIFYPOS: case MCIWNDM_NOTIFYSIZE: SetClientRect(hwnd, (HWND)wParam); break; case WM_DESTROY: PostQuitMessage(0); break; default: return DefWindowProc(hwnd, uMsg, wParam, lParam); } return 0; 中国图象图形网 www.image2003.com } int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine,int nCmdShow) { MSG msg; HWND hwnd; WNDCLASS wndClass; if (hPrevInstance == NULL) { memset(&wndClass, 0, sizeof(wndClass)); wndClass.style = CS_HREDRAW | CS_VREDRAW; wndClass.lpfnWndProc = WndProc; wndClass.hInstance = hInstance; wndClass.hCursor = LoadCursor(NULL, IDC_ARROW); wndClass.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); wndClass.lpszClassName = "HELLO"; if (!RegisterClass(&wndClass)) return FALSE; } hwnd = CreateWindow("HELLO", "HELLO", WS_OVERLAPPED|WS_CAPTION|WS_SYSMENU, 中国图象图形网 www.image2003.com CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL); SetClientRect(hwnd, MCIWndCreate(hwnd, hInstance, WS_VISIBLE|WS_CHILD| MCIWNDF_SHOWALL| MCIWNDF_NOTIFYSIZE| MCIWNDF_NOTIFYPOS, “c:\\test.avi”)); ShowWindow(hwnd, nCmdShow); UpdateWindow(hwnd); while (GetMessage(&msg, NULL, 0, 0)) DispatchMessage(&msg); return msg.wParam; } 运行时文件 C:\test.avi 必须准备好,当然,你可以改成其它的文件名。其运 行结果如图 10.5 所示。 图 10.5 TestMCI 的运行结果 中国图象图形网 www.image2003.com 该程序非常简单,所有的代码加在一起不超过 60 行,其关键部分只有一个 MCIWndCreate 函数,细节就不介绍了,有兴趣的读者可以参看 VC 的帮助。 The University of Southern California does not screen or control the content on this website and thus does not guarantee the accuracy, integrity, or quality of such content. All content on this website is provided by and is t he sole responsibility of the person from which such content originated, and such content does not necessarily refl ect the opinions of the University administration or the Board of Trustees 参考文献 [1] 黎洪松,数字图象压缩编码技术及其 C 语言程序范例,北京:学苑出版社,1998 [2] 林福宗,图象文件格式大全,北京:清华大学出版社,1998 The University of Southern California does not screen or control the content on this website and thus does not guarantee the accuracy, integrity, or quality of such content. All content on this website is provided by and is the sole responsibility of the person from which such content originated, and such content does not necessarily reflect the opinions of the University administration or the Board of Trustees 后 记 本书的目的是想给对数字图象处理编程感兴趣的初学者介绍一些有关数字图象处理的基本原 理和算法,所以很多可以优化的工作并没有做。很多情况下,为了能够说的更通俗些,并没 有按照严格的定义去写,取而代之的是一种近似的,易懂的写法。实际上,有很多术语或概 念在各参考书中也没有一个统一的定义。写东西是一件很累的事,自己明白的害怕别人看不 懂,如果自己都不明白,就更要仔细查查资料,直到搞懂为止。所以很有些担惊受怕,怕写 中国图象图形网 www.image2003.com 错了什么。总之,本书也许会有不少错误和不足,欢迎读者批评指正。我的 Email 地址是 。本书所有的源程序都在附盘中,编译运行前请先阅读 readme.txt。大多 数情况下文件名为 c:\test.bmp,运行前请准备好,还要注意一下是不是所要求的颜色数。 在写作的过程中,遇到了一件事,清华计算机系的 ITA(信息技术协会)要举办几次学术沙龙 活动,全部由学生自己主讲,对象是低年级的学弟学妹。他们让我做一个数字图象处理的讲 座。我很高兴,因为写的这些东西可以派上用场了。 本书主要参考的是我上数字图象处理课时的教材,该教材的作者是朱志刚老师,在此表示感 谢。 The University of Southern California does not screen or control the content on this website and thus does not guarantee the accuracy, integrity, or quality of such content. All content on this website is provided by and is the sole responsibility of the person from which such content originated, and such content does not necessarily reflect the opinions of the University administration or the Board of Trustees 中国图象图形网 www.image2003.com

下载文档,方便阅读与编辑

文档的实际排版效果,会与网站的显示效果略有不同!!

需要 15 金币 [ 分享文档获得金币 ] 10 人已下载

下载文档

相关文档