Visual C++数字图像处理典型算法及实现


数字图像分析与处理技术 Visual C++数字图像获取、处理及实践应用 杨枝灵 王开 等编著 人民邮电出版社 Visual C++数字图像获取、处理及实践应用 · 2· 图书在版编目()数据 Visual C++数字图像获取、处理及实践应用 / 杨枝灵,王开等编著. —北京:人民邮电出版社, 2003.1 ISBN 7–115–10957–5 Ⅰ.V… Ⅱ.①杨…②王… Ⅲ.C 语言—程序设计Ⅳ. TP312 中国版本图书馆 CIP 数据核字(2002)第 101890 号 内容提要 本书全面系统地讨论了数字图像处理的理论、设计及应用。全书由自成体系而又互有联系的 12 章 组成,分别讨论了位图及图像类的概念、图像获取、图像增强、图像复原、正交变换、压缩编码、图像 配准、运动检测、特征提取、图像分割及识别的相关知识,基本涵概了从图像获取到图像处理的各个领 域,并结合 Microsoft 公司面向对象的可视化集成编程系统 Visual C++,给出了相应的算法和完整的源代 码。 本书在介绍了数字图像处理基础知识的同时,加入了该领域一些较新的研究成果,内容丰富新颖、 实用性强,适合希望运用 Visual C++进行数字图像处理的工作者阅读参考。 数字图像分析与处理技术 Visual C++数字图像获取、处理及实践应用   编    著  杨枝灵  王开等   责任编辑  张立科    人民邮电出版社出版发行 北京市崇文区夕照寺街 14 号     邮编 100061 电子函件 315@ptpress.com.cn 网址 http://www. ptpress.com.cn 读者热线       北京汉魂图文设计有限公司制作     北京隆昌伟业印刷有限公司印制     新华书店总店北京发行所经销  开本:787×1092 1/16 印张:39 字数:1216 千字 2002 年 12 月第 1 版 印数:1 – 6 000 册 2002 年 12 月北京第 1 次印刷 ISBN 7-115-10957-5/TP· 3276 定价:68.00 元(附光盘) 本书如有印装质量问题,请与本社联系 电话:(010)67129223 前 言 数字图像处理技术从广义上可看作是各种图像加工技术的总称。它包括利用计算机和其他电子设备 完成的一系列工作,如图像采集、获取、编码、存储和传输,图像的合成和产生,图像的显示、绘制和 输出,图像变换、增强、恢复和重建,特征的提取和测量,目标的检测、表达和描述,序列图像的校正, 图像数据库的建立、索引、查询和抽取,图像的分类、表示和识别,3D 景物的重建复原,图像模型的建 立,图像知识的利用和匹配,图像和场景的解释和理解,以及基于它们的推理、判断、决策和行为规划, 等等。另外图像处理技术还包括为完成上述功能而进行的硬件和系统的设计及制作等方面的技术。 数字图像处理是从 20 世纪 60 年代以来随着计算机技术和 VLSI 的发展而产生、发展和不断成熟起 来的一个新兴技术领域,它在理论上和实际应用上都取得了巨大的成就,并引起各方面人士的广泛重视。 首先,视觉是人类最重要的感知手段,图像又是视觉的基础。因此数字图像成为心理学、生物医学、计 算机科学等诸多方面的学者研究视觉感知的有效工具。其次,数字图像处理在军事、遥感、工业图像处 理等大型应用中也有不断增长的需求。 Visual C++是 Microsoft 公司推出的开发 Win 32 环境程序,面向对象的可视化集成编程系统。它不 但具有程序框架自动生成、灵活方便的类管理、代码编写和界面设计集成交互操作、可开发多种程序等 优点,而且通过简单的设置就可使其生成的程序框架支持数据库接口、OLE 2、WinSock 网络、3D 控制 界面。由于 Visual C++本身就是一个图形的开发界面,它提供了丰富的关于位图操作的函数,对开发图 像处理系统提供了极大的方便。因此,它现在已经成为开发 Win32 程序,包括图像处理程序的主要开发 工具。 全书基本涵概了从图像获取到处理的各个领域,由自成体系而又互有联系的 12 章组成。在介绍数字 图像处理基础知识的同时,加入了该领域一些较新的研究成果和应用,例如小波变换、Canny 分割算子、 JPEG 2000 图像编码等。书中对所介绍的图像处理算法都附有完整的 Visual C++源代码,并给出了相应 的编程实例和处理效果,读者在学习过程中可以结合程序来加深理解,也可以将代码直接用于图像处理。 本书由杨枝灵、王开等编著。此外参与编写工作的还有肖洪伟、李廷文、张增强、王洪涛、吴继刚、 周学明、李闽溟、黄沙、宣小平、但正刚、张文毅、张小磊、胡昱、范国平、陈晓鹏、王凯封、潘邦、 王锐、闫卫东、赵明华、许福、施新刚、 郑刚、李现勇、谭思量、邹超群、郭瑞军、宋文超、彭坷珂 张东胜、孙莉莉、刘灵波、刘兵、李英浩、张宏林等。在此,谨向他们的辛勤劳动表示衷心的感谢。 由于水平有限,加之时间仓促,书中难免有不妥之处。恳请读者批评指正,本书责任编辑的电子信 箱为 zhanglike@ptpress.com.cn,欢迎联系讨论。 配套光盘使用说明 1.运行环境 硬件环境:奔腾主频 200MHz,内存 32MB 以上。 软件环境:Windows 95/98/2000/NT/XP,Microsoft Vsiaul C++ 6.0。 2.说明 为了方便读者学习,我们将书中所有的源代码以及用到的图像都收录在本书的配套光盘中,相信一 定会对大家的学习和应用有所帮助。 编者 目 录 第 1 章 位图基础知识...........................................................................................................1 1.1 引言 .........................................................................................................................1 1.2 数字图像的基本概念 ..............................................................................................2 1.3 颜色和调色板..........................................................................................................4 1.3.1 颜色.................................................................................................................4 1.3.2 调色板的基本概念 .........................................................................................7 1.3.3 调色板的操作.................................................................................................8 1.4 与设备相关位图....................................................................................................13 1.5 与设备无关的位图 ................................................................................................18 1.5.1 DIB 位图的结构 ...........................................................................................18 1.5.2 Win32 SDK DIB 位图操作函数...................................................................21 1.5.3 自定义 DIB 位图函数 ..................................................................................25 第 2 章 构造 CDib 类 ..........................................................................................................31 2.1 CDib 类的总体设计 ..............................................................................................31 2.1.1 CDib 类的基本功能 .....................................................................................31 2.1.2 CDib 类基本操作函数..................................................................................32 2.2 基于 CDib 类的其他操作函数..............................................................................68 第 3 章 图像感知与获取.....................................................................................................73 3.1 视觉基础................................................................................................................73 3.1.1 视觉系统.......................................................................................................73 3.1.2 视觉模型.......................................................................................................75 3.2 图像获取................................................................................................................77 3.3 图像采样................................................................................................................78 3.3.1 确定性图像场抽样 .......................................................................................78 3.3.2 随机图像取样...............................................................................................80 3.4 量化 .......................................................................................................................82 3.5 图像显示................................................................................................................86 3.5.1 图案法显示...................................................................................................86 3.5.2 图案法显示图像的 Visual C++实现 ............................................................88 3.5.3 随机抖动法显示图像 ...................................................................................91 3.5.4 随机抖动法显示图像的 Visual C++实现 ....................................................92 第 4 章 图像增强.................................................................................................................97 4.1 对比度增强............................................................................................................97 4.1.1 灰度变换法...................................................................................................97 4.1.2 直方图修整法.............................................................................................100 Visual C++数字图像获取、处理及实践应用 · 2· 4.1.3 Visual C++编程实现 ..................................................................................102 4.2 图像平滑..............................................................................................................130 4.2.1 模板操作.....................................................................................................130 4.2.2 图像平滑技术.............................................................................................134 4.2.3 Visual C++编程实现 ..................................................................................137 4.3 图像锐化..............................................................................................................173 4.3.1 微分方法.....................................................................................................174 4.3.2 高通滤波方法.............................................................................................175 4.3.3 Visual C++编程实现 ..................................................................................175 4.4 伪彩色和假彩色增强 ..........................................................................................192 4.4.1 伪彩色和假彩色增强技术 .........................................................................192 4.4.2 Visual C++编程实现 ..................................................................................192 第 5 章 图像复原...............................................................................................................201 5.1 图像退化的数学模型 ..........................................................................................201 5.1.1 退化系统的基本定义 .................................................................................202 5.1.2 连续函数的退化模型 .................................................................................202 5.1.3 离散函数的退化模型 .................................................................................203 5.2 运动模糊图像复原 ..............................................................................................206 5.2.1 由匀速直线运动引起的图像模糊..............................................................206 5.2.2 运动模糊图像复原的 Visual C++实现 ......................................................208 5.3 非约束复原..........................................................................................................218 5.3.1 非约束复原的基本方法 .............................................................................218 5.3.2 逆滤波复原.................................................................................................219 5.3.3 逆滤波复原的 Visual C++实现..................................................................220 5.3.4 维纳滤波方法.............................................................................................231 5.3.5 维纳滤波的 Visual C++实现......................................................................233 5.4 约束复原..............................................................................................................244 第 6 章 图像处理中的正交变换 ....................................................................................245 6.1 傅立叶变换..........................................................................................................245 6.1.1 傅立叶级数.................................................................................................245 6.1.2 连续变量的傅立叶变换及其性质..............................................................246 6.1.3 离散傅立叶变换及其性质 .........................................................................249 6.1.4 快速傅立叶变换.........................................................................................250 6.1.5 二维图像的快速傅立叶变换 .....................................................................253 6.1.6 二维图像快速傅立叶变换的 Visual C++程序实现...................................253 6.2 离散余弦变换(DCT) ......................................................................................272 6.2.1 离散余弦变换定义 .....................................................................................272 6.2.2 离散余弦变换的计算 .................................................................................274 目录 · 3· 6.2.3 离散余弦变换的 Visual C++实现..............................................................275 6.3 沃尔什变换..........................................................................................................282 6.3.1 沃尔什函数.................................................................................................283 6.3.2 离散沃尔什变换.........................................................................................288 6.3.3 快速沃尔什变换.........................................................................................289 6.3.4 沃尔什-哈达玛变换的 Visual C++实现 ..................................................292 6.4 基于特征向量的变换 ..........................................................................................299 6.4.1 特征分析.....................................................................................................299 6.4.2 主向量分析(PCA) .................................................................................299 6.4.3 霍特林(Hotelling)变换 ..........................................................................300 6.4.4 SVD 变换....................................................................................................302 6.4.5 霍特林变换的 Visual C++实现..................................................................303 6.5 小波变换..............................................................................................................316 6.5.1 连续小波变换.............................................................................................317 6.5.2 离散小波变换.............................................................................................319 6.5.3 二进小波变换.............................................................................................320 6.5.4 小波变换的多分辨率分析 .........................................................................320 6.5.5 Mallat 算法 .................................................................................................321 6.5.6 小波变换的 Visual C++实现......................................................................323 第 7 章 图像压缩编码.......................................................................................................338 7.1 图像压缩编码理论基础 ......................................................................................338 7.2 图像编码分类......................................................................................................340 7.3 霍夫曼(Huffman)编码....................................................................................341 7.3.1 霍夫曼编码理论及算法 .............................................................................341 7.3.2 霍夫曼编码的 Visual C++实现..................................................................343 7.4 香农-费诺(Shannon-Fano)编码 ...................................................................353 7.4.1 香农-费诺编码的理论及算法 .................................................................353 7.4.2 香农-费诺码的 Visual C++实现..............................................................354 7.5 算术编码..............................................................................................................366 7.5.1 算术编码的理论及算法 .............................................................................367 7.5.2 算术编码的 Visual C++实现......................................................................369 7.6 游程编码(Run Length Coding).......................................................................377 7.6.1 基本原理.....................................................................................................377 7.6.2 PCX 文件格式及其编码方法.....................................................................378 7.6.3 编程实现 PCX 文件格式的读写................................................................380 7.7 位平面编码..........................................................................................................380 7.7.1 位编码理论.................................................................................................380 7.7.2 位平面编码的 Visual C++实现..................................................................382 7.8 预测编码..............................................................................................................385 Visual C++数字图像获取、处理及实践应用 · 4· 7.8.1 DPCM 的基本原理.....................................................................................385 7.8.2 预测编码的类型.........................................................................................386 7.8.3 预测编码的 Visual C++实现......................................................................387 7.9 JPEG 2000 编码...................................................................................................401 7.9.1 JPEG 2000 概述..........................................................................................402 7.9.2 JPEG 2000 图像编解码系统 ......................................................................403 7.9.3 JPEG 2000 图像压缩码流格式 ..................................................................407 第 8 章 图像配准...............................................................................................................409 8.1 图像配准理论基础 ..............................................................................................409 8.1.1 图像变换.....................................................................................................409 8.1.2 相似性测度.................................................................................................410 8.1.3 插值.............................................................................................................412 8.1.4 最小二乘法.................................................................................................412 8.2 图像配准中常用的技术 ......................................................................................413 8.2.1 点映射.........................................................................................................413 8.2.2 基于弹性模型的匹配 .................................................................................414 8.2.3 特征空间的选择.........................................................................................414 8.2.4 相似性测度的选择 .....................................................................................414 8.2.5 搜索空间和策略的选择 .............................................................................415 8.3 Visual C++编程实现图像配准............................................................................415 第 9 章 目标检测与运动检测 ...........................................................................................457 9.1 静止背景下的运动目标检测 ..............................................................................457 9.1.1 背景恢复.....................................................................................................458 9.1.2 基于静止背景的运动目标提取 .................................................................458 9.2 运动背景下的运动目标检测 ..............................................................................459 9.2.1 光流方法.....................................................................................................459 9.2.2 全局运动预测.............................................................................................460 9.2.3 基于块的运动检测方法 .............................................................................461 9.3 Visual C++编程实现 ...........................................................................................463 第 10 章 图像形状特征分析.............................................................................................475 10.1 图像的矩............................................................................................................475 10.1.1 矩的定义...................................................................................................475 10.1.2 Visual C++编程求图像的矩.....................................................................476 10.2 图像的空穴检出 ................................................................................................481 10.2.1 定义及算法...............................................................................................481 10.2.2 空穴检出的 Visual C++实现....................................................................481 10.3 图像的骨架检出 ................................................................................................489 10.3.1 图像的市街区距离 ...................................................................................489 目录 · 5· 10.3.2 图像骨架提取的 Visual C++实现............................................................489 10.4 轮廓提取............................................................................................................499 10.4.1 基本概念...................................................................................................499 10.4.2 Visual C++实现 ........................................................................................499 第 11 章 图像分割.............................................................................................................504 11.1 图像分割研究....................................................................................................504 11.1.1 图像分割定义 ...........................................................................................504 11.1.2 图像分割的方法.......................................................................................505 11.2 并行边界分割....................................................................................................506 11.2.1 边界检测的数学基础 ...............................................................................507 11.2.2 数字图像的边界检测 ...............................................................................508 11.2.3 并行边界分割的 Visual C++实现............................................................509 11.3 串行边界分割....................................................................................................528 11.3.1 边界跟踪...................................................................................................529 11.3.2 边界跟踪的 Visual C++实现....................................................................530 11.4 并行区域分割....................................................................................................535 11.4.1 阈值分割...................................................................................................535 11.4.2 自适应阈值选取.......................................................................................537 11.4.3 阈值分割的 Visual C++实现....................................................................538 11.5 串行区域分割....................................................................................................546 11.5.1 区域生长...................................................................................................546 11.5.2 分裂合并...................................................................................................548 11.5.3 区域生长的 Visual C++实现....................................................................548 11.6 Canny 算子 ........................................................................................................553 11.6.1 Canny 算子介绍........................................................................................553 11.6.2 Canny 算子的 Visual C++实现 ................................................................553 第 12 章 图像的模式识别.................................................................................................573 12.1 图像匹配............................................................................................................573 12.1.1 模板匹配...................................................................................................574 12.1.2 其他快速匹配方法 ...................................................................................577 12.2 统计模式识别....................................................................................................582 12.2.1 决策理论方法 ...........................................................................................582 12.2.2 统计分类法...............................................................................................587 12.2.3 特征的提取和选择 ...................................................................................591 12.3 线性分类器........................................................................................................592 12.3.1 Fisher 判别准则 ........................................................................................592 12.4 Visual C++编程实现 .........................................................................................595 第第 11 章章 位位图图基基础础知知识识 人类通过眼、耳、鼻、舌、身接受信息,感知世界,并进而认识世界和改造世界。据统计,约有 70% 的信息是通过视觉系统获取的。粗略地说,图像是二维或三维景物呈现在人心目中的影像。如果接受并 加工这种视觉信息的是电子计算机,则我们称之为计算机图像处理和识别。 近年来,由于计算机技术的迅猛发展,计算机的速度越来越快,图像处理系统的价格日益下降,从 而使图像处理得以广泛应用于众多的科学与工程领域,如遥感、工业检测、医学、气象、侦察、通信、 智能机器人等。这些技术正在明显地改变着人们的生产手段和生活方式。传统的生产、管理、教育等, 正向信息化、多样化转变。 正因为图像有着如此多的应用,如此与我们的工作和生活方式息息相关,所以有必要对图像和图像 技术进行深入细致的研究。在 Windows 环境中最重要的图像就是位图。在计算机处理中,位图的概念可 以和图像等同。后面的叙述中,经常用到图像和位图,如不加特殊说明,可以认为它们是相同的。本章 先讨论数字图像的基本概念以及 VC 中有关位图的知识。 本章的内容安排是:在引言中,介绍图像处理技术的基本类别,然后介绍数字图像的基本概念,色 度系统和调色板的概念将在 1.3 节中叙述,1.4 节和 1.5 节介绍 VC 中的 DDB(设备相关位图)和 DIB(设 备无关位图)。 1.1 引言 人们用各种技术方式和手段对图像进行加工以获得重要信息。图像技术从广义上可看作是各种图像 加工技术的总称,它包括利用计算机和其他电子设备进行和完成的一系列工作,例如图像采集、获取、 编码、存储和传输,图像的合成和产生,图像的显示、绘制和输出,图像变换、增强、恢复和重建,特 征的提取和测量,目标的检测、表达和描述,序列图像的校正,图像数据库的建立、索引、查询和抽取, 图像的分类、表示和识别,3D 景物的重建复原,图像模型的建立,图像知识的利用和匹配,图像和场景 的解释和理解,以及基于它们的推理、判断、决策和行为规划,等等。另外图像技术还包括为完成上述 功能而进行的硬件和系统设计及制作等方面的技术。本书所涉及到的图像处理技术主要包括以下的内容。 图像增强:图像增强是用以改善供人观看的图像的主观质量,而不一定追究图像降质的原因。直方 图修正,强化图像轮廓等都是常用手段。由于接受者是人,所以质量好坏就受观看者的心理、爱好、文 化素质等因素的影响,评判只能是相对的。 图像复原(也叫图像恢复):图像恢复则需要找出图像降质的起因,并尽可能消除它,使图像恢复本 来面目。常用的恢复有纠正几何失真、从已知图像信号和噪声的统计特性入手,用 Wiener 滤波等方法来 改善信噪比。 图像变换:图像处理的方法可以分为两大类:空域法和频域法。其中频域法也称为变换域法。在频 域法中最为关键的预处理就是图像变换。这种变换一般是线性变换,其基本线性运算是严格可逆的,并 且满足一定的正交条件,因此,也将其称为酉变换。常用的图像变换有傅立叶变换、DCT 变换,小波变 换等。 图像编码:二维形式呈现的数字图像,其信息量很大,给传输、处理、存储、显示等都带来了不少 问题。另一方面,图像中又有很大冗余信息,根据香农(Shannon)的率失真理论。不论在传输或存储时, 都可对数字图像进行一定方式编码,删除其中冗余信息,实现不失真压缩,或在容许失真限度内的进行 有失真压缩,以换取更大的压缩率。对于供人观看的图像,如电视信号,这时人是通信系统中的一环,Visual C++数字图像获取、处理及实践应用 · 2· 人的视觉特性,如掩盖效应,对灰度分辨率和空间分辨率的有限性等,也可以用来为压缩服务。 图像配准:图像配准可以近似地看成匹配的过程。简单地说就是根据图像的某些区域或者特征,在 另一幅图像中找到对应的区域或者特征。图像配准在图像识别、图像拼接、三维图像的重建等方面有着 重要的应用。 图像分析和特征提取:图像分析(或称为图像理解)可看作是一个描述过程,主要研究用自动或半 自动装置和系统,从图像中提取有用的测度、数据或信息,生成非图像的描述或者表示,这是当前图像 处理与识别领域中一个最为活跃的分支。图像分析并不仅仅是给景物中的诸区域在一定数目的已知类别 中进行分类,还要对千变万化和难以预测的复杂景物加以描述。图像分析常常依靠某种知识来说明景物 中物体与物体、物体与背景之间的关系。利用人工智能技术在分析系统中进行各层次控制和有效地访问 知识库正在被越来越普遍地采用。图像分析的内容分为特征提取、符号描述、目标检测、景物匹配和识 别等几个部分。图像特征是指图像场中可用作标志的属性,其中有些是视觉直接感受到的自然特征,如 区域的亮度、彩色、纹理或轮廓等等,有些是需要通过变换或测量才可得到的人为特征,如各种变换频 谱、直方图、矩等。图像特征提取就是从图像中提取出某些可能涉及到高层语义信息的图像特征,以进 行后续的分析。 目标和运动检测:目标检测是运动分析领域的一项重要研究内容。在监控系统、控制系统、仿真系 统以及识别系统中,目标检测都扮演着重要的角色。目标检测按照有无人的参与可以分成两类:第一是 自动目标检测,这种检测方式在实现的过程中不需要加入额外的信息;第二是交互目标检测,这种检测 方式在实现过程中需要加入一些交互信息,从而实现更为精确的目标提取。 图像分割:人能方便地从一幅图像中找出感兴趣的物体或区域,而要让计算机做到这一点却需要给 它以客观测度,使之按照灰度、颜色或几何性质等把一些物体或区域加以分离,这称之为图像分割。 形状描述:对于已经分离出来的区域或物体边界,用适当的数学语言(如图论、句法、形态学等) 来表示其统计或者区域之间的关系,得出一种简练的表达方式,这叫做描述。如果我们只对物体的形状 感兴趣,并对形状进行描述,则为形状描述。 图像识别:视觉识别是人和动物的眼-脑系统固有的功能,它是在长期的进化中形成的。人的知识、 生活经验使其在头脑中建立起为数巨大的物体模型和常识供识别使用。但指望计算机能代替人类完成各 种复杂工作和识别景物,就必须赋予它有近于人的视觉功能和识别能力。由于对人的视觉机理至今研究 得仍然不够,完善的视觉模型尚未建立,因而,目前用图像处理和识别技术能识别的景物是相对简单的。 模板匹配、统计识别等是一些常用的识别方法。 数字图像处理的应用越来越广,数字图像处理包含的内容也越来越多。当然,数字图像处理包含的 内容远远不止上述这些,但由于篇幅有限,就不在本书中进行讨论了。读者可以从其他的数字图像处理 书中获取相关的知识。 Visual C++是 Microsoft 公司推出的开发 Win 32 环境程序,是面向对象的可视化集成编程系统。它 不但具有程序框架自动生成、灵活方便的类管理、代码编写和界面设计集成交互操作、可开发多种程序 (应用程序、动态链接库、ActiveX 控件等)等优点,而且通过简单的设置就可使其生成的程序框架支持 数据库接口、OLE 2、 WinSock 网络、3D 控制界面。由于 Visual C++本身就是一个图形的开发界面,它 提供了丰富的关于位图操作的函数,为开发图像处理系统提供了极大的方便。因此,它现在已经成为开 发 Win32 程序,包括图像处理程序的主要开发工具。本书不对 Visual C++进行全面的介绍,只对其中与 数字图像处理相关的内容进行说明。为此,假设本书的读者都具有一定的 Visual C++基础。在本书后续 的章节中,结合 Visual C++的开发环境,对图像处理技术进行详细叙述,并给出编程实例。 1.2 数字图像的基本概念 人眼看到的任何自然界的图像都是连续的模拟图像,其形状和形态表现由图像各位置的颜色决定。第 1 章 位图基础知识 · 3· 当我们从某一点观察景物时,物体所发出的光线进入人眼,在人眼的视网膜上成象,该像反映了客观景 物的亮度和颜色随空间位置的变化,因此它是空间坐标的函数。所谓图像,就是视觉景物的某种形成的 表示和记录。我们都熟悉彩色或黑白照片。黑白(或称单色)照片记录了景物中每点的亮度,而彩色照 片不仅记录亮度还记录颜色。在传统的摄影技术中,记录的机制是化学反应,它取决于落在胶片上的光 线和胶片本身的化学特性。随着科学技术的发展,人类不仅能够获得并记录那些人眼可见的图像信息, 而且可以利用非可见光和其他手段成像,并转换成人眼可见的图像,如 X 射线成像,红外成像,超声成 像等,这就使人的视觉能力大大地增强和延伸。 一幅平面图像所包含的信息首先表现为光的强度(Intensity),它是随空间坐标(x, y),光线的波长 u 和时间 t 而变化的,因此图像函数可以表示为 ),,,( tuyxfI = 若只考虑光的能量而不考虑它的波长,在视觉效果上只有黑白深浅之分,而无彩色变化,这时称为 黑白图像(灰度图像),则图像模型可表示为 0 (,,) (,,,)()dI fxyt fxyutVuu ∞= = ∫ 其中 V(u)为相对视敏函数。 当考虑不同波长光的彩色效应,则为彩色图像。根据三基色的原理,任何一种彩色可分解为红、绿、 蓝三种基色,所以彩色图像可表示为 )},,(),,,(),,,({ tyxBtyxGtyxRI = 其中: 0 (,,) (,,,)()dRxyt fxyutRuu ∞= ∫ 0 (,,) (,,,)()dGxyt fxyutGuu ∞= ∫ 0 (,,) (,,,)()dBxyt fxyutBuu ∞= ∫ 其中 R(u)、G(u),B(u)分别为 R、G、B 三基色的视敏函数。 当图像内容随时间变化时,称为时变图像或运动图像。当图像内容不随时间变化时,称为静止图像。对 灰度图像而言,其函数为 ),( yxfI = 作为数字图像处理的基础,本书中的很多算法就主要针对灰度静止图像 ),( yxf 进行讨论的。 由于人眼和其他成像系统视野有限,因此图像在空间上是有界的,其取值区域通常定义为矩形,即 Mx ≤≤0 My ≤≤0 图像函数在某一点的值常称为强度或灰度,它与图像在该点的亮度相对应,并用正实数表示,而且 这个数值的大小是有限的,即 Byxf ≤≤ ),(0 其中 B 表示最大亮度。 由于计算机仅能处理离散的数据,所以如要用计算机来处理图像,则连续的图像函数必须转换为离 散的数据集,这一过程叫做数字图像采集。数字图像采集由图像采集系统完成,经过成像、采样和量化Visual C++数字图像获取、处理及实践应用 · 4· 得到数字图像。其中,采样是对空间坐标的量化过程,量化则是对图像函数值的离散化过程。采样和量 化统称为数字化。关于图像采集系统的详细内容,将在第 3 章中进行详细叙述。数字化后的图像一般都 用二维矩阵进行表示。图 1-1 中一幅普通的黑白位图,图 1-2 是被放大后的图,图中每个方格代表了一 个像素,我们可以看到:整个圆就是由这样一些黑点和白点组成的。 图 1-1 圆 图 1-2 放大后的圆位图 1.3 颜色和调色板 1.3.1 颜色 根据人眼结构,所有颜色都可看作是 3 个基本颜色——红( R,red),绿(G,green)和蓝(B,blue) —— 的不同组合。 区分颜色常用 3 种基本特性量:亮度、色调和饱和度。色调和饱和度合起来称为色度。颜色可用亮 度和色度共同表示。当把红、绿、蓝三色光混合时,通过改变三者各自的强度比例可得到白色以及其他 各种色调和饱和度的彩色。 C ≡ rR + gG + bB 其中 C 代表某一特定色,≡ 表示匹配,R,G,B 表示三原色,r,g,b 代表比例系数,且有 r + g + b = 1 1931 年 CIE 制定了色度图(见图 1-3),用组成某种颜色的三原色的比例来规定这种颜色。 1.0 0.6 0.4 0.2 0.4 0.6 0.8 1.0 0.0 0.8 0.2 520 540 510 550 560 570 580 590 600 530 700~770 620 500 490 480 470 460 绿 黄 红 蓝 紫 400 图 1-3 色度图示意 第 1 章 位图基础知识 · 5· 1 种颜色可用 3 个基本量来描述,所以建立颜色模型就是建立 1 个 3-D 坐标系统,其中每个空间点 都代表某 1 种颜色。 目前常用的颜色模型可分两类。 1. RGB 模型 这个模型基于笛卡尔坐标系统,3 个轴分别为 R、G、B,如图 1-4 所示。 R G B (0,1,0) (1,0,0) b m c 黑 yr 白 g (0,0,1) 图 1-4 RGB 模型 2. HSI 模型 这个模型基于两个重要的事实:其一,I 分量与图像的彩色信息无关;其二,H 和 S 分量与人感受 颜色的方式是紧密相连的。 HSI 模型中的各分量可定义在如图 1-5(a)所示的双棱锥中,其中每个横截面如图 1-5(b)所示。对 其中的任一个色点 P,其 H 的值对应指向该点的矢量与 R 轴的夹角。这个点的 S 与指向该点的矢量长成 正比,越长越饱和。 H 黑 B R G I 白 B H GR P Y M C (b)(a) P 图 1-5 HSL 模型 3. 颜色模型转换 对任何 3 个在[0, 1]范围内的 R、G、B 值,其对应 HSI 模型中的 I、S、H 分量可分别由下面给出的 公式计算: Visual C++数字图像获取、处理及实践应用 · 6· ()BGRI ++= 3 1 [ ]),,min()( 31 BGRBGRS ++−= ()()[] ()()()[]       −−+− −+−= 21 2 2 arccos BGBRGR BRGRH 若设 S,I 的值在[0, 1]之间,R,G,B 的值也在[0, 1]之间,则从 HSI 到 RGB 的转换公式为(分成 3 段以利用对称性): (1) 当 H 在[0°, 120°]之间: )1( SIB −=       − += )60cos( cos1 H HSIR  )(3 RBIG +−= (2) 当 H 在[120°, 240°]之间: )1( SIR −=         − −+= )180cos( )120cos(1 H HSIG   )(3 GRIB +−= (3) 当 H 在[240°, 360°]之间: )1( SIG −=       − −+= )300cos( )240cos(1 H HSIB   )(3 BGIR +−= 图 1-6 给出一组用灰度图形式表示彩色图像的例子,其中图 1-6(a)、图 1-6(b)、图 1-6(c)分别为 1 幅 彩色图像的 R,G,B 分量(每个分量用 8 bit 表示),图 1-6(d)、图 1-6(e)、图 1-6(f )分别为这幅彩色图像 的 H、S、I 分量(每个分量也各用 8 bit 表示)。 (a) (b) (c) 第 1 章 位图基础知识 · 7· (d) (e) (f) 图 1-6 彩色图像的 RGB 和 HSL 各分量图示 1.3.2 调色板的基本概念 现实世界的颜色种类是无限的,但计算机显示系统所能表现的颜色数量有限,因此,为了使计算机 能最好地重现实际图景,就必须用一定的技术来管理和取舍颜色。 Windows 提供了一个独立于硬件的颜色接口。程序提供了一个“绝对的”颜色码,Windows 把这个 代码映射成计算机显示器上适当的颜色或颜色组合。对于一些常见的显示卡,大多数 Windows 程序都会 优化应用程序的颜色显示。在计算机中,常用的显示卡有: 标准视频图形阵列显示卡(VGA):标准视频图形阵列(VGA)显示卡使用 18 位的颜色寄存器,因 此有一个 262 144 种颜色的调色板。然而,由于显存的限制,标准 VGA 卡采用 4 色代码,这意味着,它 一次只能显示 16 种颜色。因为 Windows 对标题、边框、滚动条等需要固定的颜色,程序就可以只使用 16 种“标准”的纯色。但并不能方便地使用显示卡显示的其他颜色。 每一种 Windows 颜色由 8 位的红、绿、蓝值的组合来表示。16 种标准 VGA“纯”(非抖动)颜色如 表 1-1 所示。 256 色显示卡:大多数显示卡可以在所有空间分辨率下支持 8 位颜色码,这意味着,它们可以同时 显示 256 色。256 色方式现在被认为是颜色编程中的最低要求。 表  标准  的  种“纯”颜色 红 绿 蓝 颜色 0 0 0 黑色 0 0 255 蓝色 0 255 0 绿色 0 255 255 青色 255 0 0 红色 255 0 255 洋红色 255 255 0 黄色 255 255 255 白色 0 0 128 暗蓝色 0 128 0 暗绿色 0 128 128 暗青色 128 0 0 暗洋红色 128 128 0 暗黄色 128 128 128 暗灰色 192 192 192 亮灰色 如果 Windows 配置的是 256 色显示卡,则用户的程序限制在 20 种标准纯色内,除非激活 Windows 的调色板系统,该系统由 MFC 库的 CPalette 类和 Windows 应用程序编程接口支持。此时,可以从 1670 多万种颜色中选定自己的 256 种颜色。 安装了 SVGA(Super VGA)256 色显示驱动程序之后,可以获得的颜色比表 1-1 所列出的 16 种 VGAVisual C++数字图像获取、处理及实践应用 · 8· 颜色多 4 个,总共 20 个。表 1-2 列出了增加的 4 种颜色。 表     中增加的 种颜色 红 绿 蓝 颜色 192 220 192 浅绿色 166 202 240 天蓝色 255 251 240 乳白色 160 160 1641 中灰色 16 位颜色显示卡:现代大多数的显示卡支持 1024×768 像素的分辨率,并且,如果有 1MB 显存, 便可以支持 8 位颜色。如果显示卡有 2MB 显存,它就可以支持 16 位颜色,其中红、绿、蓝分别用 5 位 表示。这意味着,可以同时显示 32 768 种颜色。这似乎很多,但对于红、绿、蓝每一种纯色,都只有 32 种渐变。 24 位颜色显示卡:高级的显示卡支持 24 位颜色。24 位容量可以显示一 1670 多万种纯色,这就是我 们常说的 24 位真彩色,红、绿、蓝三原色分别用 8 位来表示。如果正在使用 24 位显示卡,就可以直接 使用所有的颜色。如果 1024×768 像素分辨率下需要 24 位颜色,则需要 2.5MB 的显存。 在 Windows 中,采用了这种方法来表现颜色,其 SDK 提供了一个名为 RGB 的宏将不同的 RGB 颜 色值转化为 24 位的颜色值,其原型为: COLORREF RGB(BYTE bred, BYTE bGreen, BYTE bBlue) COLORREF 是表示颜色值的数据类型,是一个 32 位的无符号长整型。bRed,bGreen,bBlue 分别 表示红、绿、蓝三原色的浓度,其类型为 BYTE,长度为 8。其 16 进制数据表示形式为 0x00bbggrr,其 中字节 rr、gg、bb 分别表示红、绿、蓝三原色的浓度,最高位字节为 0,用于保留与将来的系统兼容。 在真彩色系统中,每一个像素都用 24 位来表示,像素值与真彩色颜色值可以一一对应,所以像素值 就是所表现的颜色值。但对于仅能同时显示 16 色或 256 色的系统,每一个像素仅能分别采用 4 位或者 8 位来表示,像素值和真彩色颜色值不能一一对应,用像素值代表颜色值的方法将不能得到最佳的效果, 而必须采用调色板技术。所谓调色板就是在 16 色或者 256 色显示系统中,将图像中出现最频繁的 16 或 256 种颜色所组成的颜色表。对这些颜色按 4 位或者 8 位,即 0 到 15 或者 255 进行编号,每一编号代表 其中的一种颜色。这种颜色编号叫做颜色的索引号,4 位或者 8 位的索引号与 24 位的颜色值的对应表叫 做颜色查找表。使用调色板的图像叫做调色板图像,它们的像素值并不是颜色值,而是颜色在调色板查 找表中的索引号。 为了保证 Windows 的基本显示界面的一致性,Windows 保留了一个上述 20 种颜色的内部系统调色 板,用来绘制窗口的图标、边界等通用界面。该调色板在所有的显示设置中都保持不变。在 16 色的显示 系统中,系统调色板通过 16 种的抖动来产生其余 4 种颜色。在 256 色的显示系统中,Windows 也保持 20 种颜色的次序,其余 236 种颜色由当前的调色板分配。 在这里,需要注意的是,真彩色不需要调色板,其中的像素值就是 24 位的颜色值。16 色系统通常 采用 Windows 的内部系统调色板,一般并不直接操作调色板。所以,通常,仅在 256 色显示系统中操作 调色板。 1.3.3 调色板的操作 在调色板显示系统中,每一幅图像都有自己的调色板,而每一个活动窗口或应用程序都根据自己的 需要操作当前的调色板,但是对调色板的操作将影响到整个显示系统。这将导致如下的结果,即如果两 个窗口或者应用程序使用了不同的调色板,每一时刻将只有一个窗口或应用程序的显示是正确的,另一 个则将被强制使用了不同的调色板而显示出错误的颜色。通常是当前活动窗口或应用程序对当前调色板 具有较高的控制优先级。 在窗口中显示的每一个图像的调色板都保存在内存中,称为逻辑调色板。显示系统当前使用的调色第 1 章 位图基础知识 · 9· 板称为硬件调色板或者系统调色板,在任一时刻,仅有一个系统调色板,由它决定了当前屏幕上实际的 颜色显示。 调色板在 Windows 中是一个重要的概念,Windows 调色板编程非常复杂。但是,只有在用户需要以 每像素 8 位以下模式运行程序时,才需要处理调色板。 1. Win32 SDK 中的调色板操作函数 在 Visual C++中,MFC 定义了 CPalette 类来对调色板进行操作。CPalette 类封装的功能函数如表 1-3 所示。 表    类封装的函数 函数名 功能描述 CPalette CPalette 类的构造函数 CreatePalette 创建 Windows 颜色调色板,并将之与 CPalette 对象连接 CreateHalftonnePalette 为设备上下文创建中间色调色板,并将之与 CPalette 对象连接 FromHadle 给定 Windows 的一个调色板对象的句柄时,返回 CPalette 对象的指针 AnimatePalette 替换 CPalette 对象所表示的逻辑调色板的颜色表项,调用该函数后,新的颜色表项 将立即映射到系统调色板中,并改变图像显示的颜色 GetNearestPaletteIndex 获取逻辑调色板中与指定颜色最接近的颜色表项的索引号 ResizePalette 改变调色板的颜色表项数目 GetEntryCount 获取逻辑调色板的调色板表项数目 GetPaletteEntries 获取逻辑调色板中指定范围的调色板表项 SetPaletteEntries 将指定的 RGB 颜色和对应的标记设置为逻辑调色板中指定范围的调色板表项 Operator HPALETTE 返回与 CPalette 相连的 HPALETTE 最常用的调色板函数是创建调色板函数,其原型为: BOOL CreatePalette(LPLOGPALETTE lpLogPalette); 参数 lpLogPalette 是指向逻辑调色板颜色表项结构 LOGPALETTE 的指针,该结构定义如下: typedef struct tagLOGPALETTE { WORD palVersion; //调色板的版本号,应指定为 0x300 WORD palNumEntries; //调色板中表项数 PALETTEENTRY palPalEntry[1]; //调色板表项数组 }LOGPALETTE; 调色板表项结构PALETTEENTRY定义了调色板中每一颜色表项的颜色和使用方式,定义如下: typedef struct tagPALETTEENTRY { BYTE peRed; //该颜色中红色分量值 BYTE peGreen; //该颜色中绿色分量值 BYTE peBlue; //该颜色中蓝色分量值 BYTE peFlags; //该颜色被使用方式 }PALETTEENTRY; 其中 peFlags 表示颜色被使用的方式,可以为如下值之一: NULL 普通方式 PC_EXPLICT 逻辑调色板表项的低位字表示硬件调色板索引 PC_NOCOLLAPSE 规定颜色系统中调色板的未使用的表项,这些颜色不利用系统调 色板中的匹配颜色 PC_RESERVED 指定用于调色板动画的逻辑调色板表项 如果窗口或应用程序想按自己的调色板显示颜色,就必须将自己的调色板载入系统调色板中,这叫 做实现调色板。实现调色板包括两个步骤,即将调色板选择到设备上下文(Device Context)中,并在设Visual C++数字图像获取、处理及实践应用 · 10· 备上下文中实现它。函数 CDC::SelectPalette 和 CDC::RealizePalette 用于该操作。函数原型为 CPalette* SelectPalette ( CPalette* pPalette, BOOL bForceBackground ); UNIT RealizePalette ( ); Windows 系统用调色板管理器来管理与调色板有关的操作。活动窗口的调色板一般即是当前的系统 调色板,所有的非活动窗口都必须按照此调色板来显示自己的颜色;但调色板管理器将自动使用系统调 色板中最近似的匹配颜色来映射相应的显示颜色。 Windows 定义 3 个调色板消息用于通知窗口系统调色板的变化状态,窗口接受到这些消息后,可以 进行适当的调色板操作来维护自身的显示。这 3 个消息为: WM_QUERYNEWPALETTE 通知窗口它将接收到输入焦点,给它一次机会实现其自身的逻 辑调色板 WM_PALETTECHANGED 通知窗口系统调色板已经被其他窗口改变 WM_PALETTECHANGING 通知窗口系统调色板将被其他窗口改变 为保证图像显示颜色的正确,每一窗口接收到 WM_PALETTECHANGED 消息时都应实现其逻辑调 色板,并重画客户区。 Windows 中的位图操作与调色板密切相关。Windows 使用两种不同的位图,即设备相关位图 DDB (Device Dependent Bitmap)和设备无关位图 DIB(Device Independent Bitmap)。DIB 位图文件中包含该 位图的逻辑调色板的颜色表,其像素值是该调色板中的颜色索引值。DDB 位图中不包含调色板信息,其 像素值是该系统调色板中的颜色索引值。在结构上,DIB 与 DDB 的主要区别在于 DIB 包含一个名为 RGBQUAD 的结构,它描述了 DIB 位图的颜色表。 Win32 SDK 使用调色板句柄 HPALETTE 来表示调色板,HPALETTE 与 CPalette 对象的相互转换可 由函数 CPalette::FromHandle 和 CPalette::GetSafeHandle 实现。 2. 自定义调色板处理函数 虽然 Win32 SDK 和 MFC CPalette 类提供了不少调色板函数,但还是不能满足我们的要求。在这里, 定义两个在实际编程中需要用到的函数,以方便对调色板的操作。这两个调色板操作函数如表 1-4 所示。 表     调色板操作函数集 函数名 功能描述 CopyPalette 拷贝调色板 GetSystemPalette 获取当前的系统调色板 下面对这两个调色板操作函数进行说明,并给出具体的代码。 函数 CopyPalette 实现调色板的拷贝。其中需要说明的是,拷贝调色板时先利用 GetPaletteEntries 判 断源调色板是否为空,但并不将源调色板的表项拷贝。得到源调色板表项数后,分配内存,再次调用 GetPaletteEntries 将源调色板的表项拷贝到目标调色板中。其代码如下: /************************************************************************* * * \函数名称: * CopyPalette * * \输入参数: * HPALETTE hPalSrc - 需要拷贝的源调色板句柄 * * \返回值: * HPALETTE - 如果操作成功,则返回拷贝的调色板句柄 * 第 1 章 位图基础知识 · 11· * \说明: * 该函数将创建一个新的调色板,并从指定的调色板拷贝调色板内容 * ************************************************************************* */ HPALETTE CopyPalette(HPALETTE hPalSrc) { // 调色板指针,临时变量 PLOGPALETTE plogPal; // 声明一个调色板句柄和一个临时句柄 HPALETTE hPalette; HANDLE hTemp; // 调色板表项数 int iNumEntries=0; // 获取调色板中的表项数 iNumEntries = GetPaletteEntries(hPalSrc, 0, iNumEntries, NULL); if (iNumEntries == 0) return (HPALETTE) NULL; // 分配调色板内存,得到句柄 hTemp = GlobalAlloc(GHND, sizeof(DWORD) + sizeof(PALETTEENTRY)*iNumEntries); if (! hTemp) return (HPALETTE) NULL; // 得到调色板的指针 plogPal = (PLOGPALETTE)GlobalLock(hTemp); if (! plogPal) return (HPALETTE) NULL; // 设置调色板的信息 plogPal->palVersion = 0x300; plogPal->palNumEntries = (WORD) iNumEntries; // 获取逻辑调色板中指定范围的调色板表项 GetPaletteEntries(hPalSrc, 0, iNumEntries, plogPal->palPalEntry); // 创建一个 Windows 调色板 hPalette = CreatePalette(plogPal); // 释放以分配的内存 GlobalUnlock( hTemp ); Visual C++数字图像获取、处理及实践应用 · 12· GlobalFree ( hTemp ); // 返回调色板句柄 return hPalette; } GetSystemPalette 是一个很有用的函数,它将当前正在使用的系统调色板进行拷贝,并返回调色板 句柄。其实现代码如下: /************************************************************************* * \函数名称: * GetSystemPalette() * * \输入参数: * 无 * * \返回值: * HPALETTE - 系统调色板句柄 * * \说明: * 该函数获得当前正在使用的系统调色板的句柄 * ************************************************************************* */ HPALETTE GetSystemPalette() { // 设备上下文 HDC hDC; // 声明调色板句柄、指针等临时变量 static HPALETTE hPal = NULL; HANDLE hLogPal; LPLOGPALETTE lpLogPal; // 当前系统调色板的颜色数 int nColors; // 获得当前系统设备上下文 hDC = GetDC(NULL); if (!hDC) return NULL; // 获得当前系统调色板的颜色数目 nColors = ( 1 << (GetDeviceCaps(hDC, BITSPIXEL) * GetDeviceCaps(hDC, PLANES))); // 给调色板分配内存 第 1 章 位图基础知识 · 13· hLogPal = GlobalAlloc(GHND, sizeof(LOGPALETTE) + nColors * sizeof(PALETTEENTRY)); if (!hLogPal) return NULL; // 得到调色板内存指针 lpLogPal = (LPLOGPALETTE)GlobalLock(hLogPal); // 设置调色板信息 lpLogPal->palVersion = 0x300; lpLogPal->palNumEntries = nColors; // 将系统的调色板拷贝到当前的逻辑调色板中 GetSystemPaletteEntries(hDC, 0, nColors, (LPPALETTEENTRY)(lpLogPal->palPalEntry)); // 创建 Windows 调色板 hPal = CreatePalette(lpLogPal); // 释放已分配内存并删除临时对象 GlobalUnlock(hLogPal); GlobalFree(hLogPal); ReleaseDC(NULL, hDC); // 返回 return hPal; } 1.4 与设备相关位图 在 Windows 中有两种类型的位图:DDB 位图(设备相关位图,也有的书上称为 GDI 位图)和 DIB 位图(设备无关位图)。DDB 位图对象是由 MFC 库 6.0 版本 CBitmap 类定义的。DDB 位图对象有一个 与之关联的 Windows 数据结构,它在 Windows GDI 模块内进行维护。程序可以获得位图数据的副本, 但是其中位的排列则取决于显示硬件。在同一台机器中,GDI 位图可以在各个程序之间任意进行传输。 但是,由于它们是设备相关的,所以,通过磁盘或网络来传输位图,其意义就不很明显。 DDB 中不包括颜色信息,因此显示时是以系统的调色板为基础进行各位的颜色映射的,Windows 只能保证系统调色板的前二十种颜色稳定不变,所以 DDB 只能保证正确显示少于二十色的位图。 Windows SDK 提供了标准的 DDB 位图操作函数,MFC 6.0 版本更是定义了 Cbitmap 类对 DDB 结构 BITMAP 和 DDB 位图操作进行了封装。 结构 BITMAP 定义了 DDB 位图的类型、宽度、高度、颜色格式和像素位置,该结构在 Windows 中定义如下: typedef struct tagBITMAP { int bmType; // 位图类型,必须设置为 0 Visual C++数字图像获取、处理及实践应用 · 14· int bmWidth; // 位图高度 int bmHeight; // 位图宽度 int bmWidthBytes; // 位图中每一扫描行中的字节数 BYTE bmPlanes; // 颜色层数 BYTE bmBitsPixel; // 每一像素所占的位数 void FAR* bmBits; // 存放像素值内存块的地址 } BITMAP; CBitmap 类封装的函数如表 1-5 所示。 表    调色板操作函数集 函数名 功能描述 CBitmap 构造函数 初始化函数 LoadBitmap 从应用程序的资源中载入位图资料,并将其与 CBitmap 对象连接 LoadOEMBitmap 加载一个预定义的 Windows 位图来初始化位图对象 LoadMappedBitmap 加载一个位图并把它的颜色映射为系统颜色 CreateBitmap 用指定宽度、高度和位模式的的内存位图来创建位图,并将其与 CBitmap 对象连接 CreateBitmapIndirect 用 BITMAP 结构中给出的宽度、高度和模式(可以不指定)的位图初始化位图对象 CreateCompatibleBitmap 创建与指定设备兼容的位图,并将其与 CBitmap 对象连接 CreateDiscardableBitmap 用一个可丢弃的、与指定设备兼容的位图初始化对象 属性函数 GetBitmap 从位图中获取信息,并填充 BITMAP 结构 Operator HBITMAP 返回 CBitmap 对象上的 Windows 句柄 操作函数 FromHandle 给出 Windows HBITMAP 结构的指针时,返回指向 CBitmap 对象的指针 SetBitmapBits 用指定的图像位来设置位图的位值 GetBitmapBits 拷贝指定位图的位值到指定的缓冲 SetBitmapDimension 设置位图的宽度和高度(以 0.1mm 为单位) GetBitmapDimension 返回位图的宽度和高度。要求已经调用 SetBitmapDimension 设置位图的宽度和高度 下面给出了几个常用的 Cbitmap 类成员函数的说明: BOOL LoadBitmap( LPCTSTR lpszRecourceName ); BOOL LoadBitmap( UINT nIDResource ); 其中 lpszResourceName 指向一个包含了位图资源名字的字符串(该字符串以 NULL 结尾)。 nIDResource 指定位图资源中资源的 ID 号。函数从应用的可执行文件中加载由 lpszResourceName 指定名 字或者由 nIDResource 指定的 ID 号标志的位图资源。加载的位图被附在 CBitmap 对象上。如果由 lpszResourceName 指定名字的对象不存在,或者没有足够的内存加载位图,函数将返回 0。可以调用函 数 CGdiObject::DeleteObject 删除由 LoadBitmap 加载的位图,否则 CBitmap 的析构函数将删除该位图对 象。函数调用成功时返回非零值,否则为 0。 需要注意的是,在删除位图对象之前,要保证它没有被选到设备上下文中。在 Windows 3.1 以及以 后的版本中,增加了如下的位图: OBM_UPARROWI, ORM_DNARROWI, OBM_RGARROWI, OBM_LFARROWI,在 Windows 3.0 或者更早版本的设备驱动程序中不支持这些位图。位图的完整列表 和图形请参阅“Win32 程序员参考”。 BOOL CreateCompatibleBitmap ( CDC* pDC, // 指定设备上下文 int nWidth, // 指定位图的宽度(以像素数为单位) int nHeight // 指定位图的高度(以像素数为单位) ); 第 1 章 位图基础知识 · 15· 函数初始化一个与 pDC 指定的设备上下文兼容的位图。位图与指定的设备上下文具有相同的颜色 位面数或相同的每个像素的位数。任何与 pDC 指定的设备兼容的内存设备都可以选择它作为当前位图。 如果 pDC 指向的是内存设备上下文,则返回的位图与设备上下文中当前选中的位图具有相同的格式。 “内存设备上下文”是一块表示显示区域的内存,它可以把图像存储在内存中,以备拷贝到兼容设备的真 实显示区域中。建立一个内存设备上下文时,GDI 自动地为它选择一个黑白原始位图。既然彩色内存设 备上下文的当前位图既可以是彩色的也可以是黑白的,CreateCompatibleBitmap 返回的位图就不一定是相 同的格式设置。但是,非内存设备上下文的兼容位图的格式总是和设备的格式一致。终止用 CreateCompatibleBitmap 建立的 Cbitmap 对象,要先从设备上下文中移出位图,然后删除该对象。函数调 用成功时返回非零值,否则为 0。 BOOL CreateBitmap ( int nWidth, // 指定位图的宽度(以像素数为单位) int nHeight, // 指定位图的高度(以像素数为单位) UINT nPlanes, // 指定位图中的彩色位面数 UINT nBitcount, // 指定位图中每个像素颜色的位数 const void* lpBits // 指向一个短整型数组,数组中记录了位图的初始位值,如 // 果为 NULL,则新的位图没有被初始化 ); 函数用于指定的宽度、高度和位模式初始化依赖于设备的内存位图。对彩色位图来说,参数 nPlanes 和 nBitcount 有一个要被设置为 1。如果二者都被设置为 1,则建立一个黑白位图。调用成功时返回非零 值,否则为 0。 int GetBimap( BITMAP* pBitMap ); 其中 pBitMap 指向 BITMAP 结构的一个指针,不能为 NULL。函数用于查看 Cbitmap 对象的信息。 返回的信息存放在 pBitMap 指向的 BITMAP 结构中。函数调用成功时返回非零值,否则为 0。 DWORD SetBitmapBits ( DWORD dwCount, // 指定由 lpBits 指向的字节数 const void* lpBits // 指向一个 BYTE 类型的数组,数组中记录了要拷贝到 // CBitmap 对象的位值 ); 函数用 lpBits 指定的位值设置位图的位值,函数调用成功时返回设置位图位值的字节数,否则为 0。 Win32 SDK 使用位图句柄 HBITMAP 来表示 DDB 位图,HBITMAP 与 CBitmap 对象的相互转换可 由函数 CBitmap::FromHandle 和 CBitmap::GetSafeHandle 实现。 在 MFC 中,CDC 类提供了位图操作函数用于 DDB 位图操作,如表 1-6 所示。 表     类位图操作函数集 函数名 功能描述 PatBlt 创建位特征 BitBlt 从指定设备上下文拷贝位图 StretchBlt 把位图由源矩形和设备移动到目标矩形,必要时拉伸或压缩位图以适合目标矩形的维数 GetPixel 获取指定点像素的 RGB 颜色值 SetPixel 设置指定点像素为最接近指定色的近似值 SetPixelV 设置指定点坐标为最接近指定色的近似值。 FloodFill 用当前画刷填充区域 ExtFloodFill 用当前画刷填充区域。比 FloodFill 成员函数提供更多灵活性 MaskBlt 使用给定屏蔽和光栅操作对源和目标位图合并颜色数据 PlgBlt 从源设备上下文的指定矩形到给定设备上下文中指定平行多边形,执行颜色数据位的位块传递 Visual C++数字图像获取、处理及实践应用 · 16· 下面对几个常用函数进行说明。 BOOL BitBlt ( int x, // 指定目标矩形左上角的逻辑 X 坐标 int y, // 指定目标矩形左上角的逻辑 Y 坐标 int nWidth, // 指定目标矩形和源位图的宽度(逻辑单位) int nHeight, // 指定目标矩形和源位图的高度(逻辑单位) CDC* pSrcDC, // 指向 CDC 对象的指针,标识待拷贝位图的设备上下文。如果 dwRop // 指定不包括源的光栅操作,则它必须为 NULL int xSrc, // 指定源位图左上角的逻辑 X 坐标 intySrc, // 指定源位图左上角的逻辑 Y 坐标 DWORD dwRop // 指定要执行的光栅操作 ); 其中光栅操作代码定义 GDC 如何合并输出操作中的颜色,包括当前画刷、可能的源位图和目标位 图。对于常用的光栅操作代码请参看表 1-7。 表    常用光栅操作代码 光栅操作代码 功能描述 BLACKNESS 所有输出变黑 DSTINVERT 反转目标位图 MERGECOPY 使用布尔 AND 操作符合并特征与源位图 MERGEPAINT 使用布尔 OR 操作符合并特征与源位图 NOTSRCCOPY 拷贝反转源位图到目标 NOTSRCERASE 反转使用布尔 OR 操作符合并源和目标位图的结果 PATCOPY 拷贝特征到目标位图 PATINVERT 使用布尔 XOR 操作符合并目标位图和特征 PATPAINT 使用布尔 OR 操作符合并反转源位图和特征。用布尔 OR 操作符合并这项操作结果与目标位图 SRCAND 使用布尔 AND 操作符合并目标像素和源位图 SRCCOPY 拷贝源位图到目标位图 SRCERASE 反转目标位图并用布尔 AND 操作符合并这个结果和源位图 SRCINVERT 使用布尔 XOR 操作符合并目标像素和源位图 SRCPAINT 使用布尔 OR 操作符合并目标像素和源位图 WHITENESS 所有输出变白 函数从源设备上下文拷贝位图到这个当前设备上下文。源和目标可以在同一个设备上下文中,也可 以在不同的设备上下文中。函数调用成功,则返回非零值,否则为 0。 BOOL PatBlt ( int x , // 即将接收模式的矩形的左上角的 X 逻辑坐标 int y , // 即将接收模式的矩形的左上角的 Y 逻辑坐标 int nWidth , // 即将接收模式的矩形的宽度(逻辑单位) int nHeight, // 即将接收模式的矩形的高度(逻辑单位) DWORD dwRop // 光栅操作代码 ); 函数在设备上创建模式。将设备已有的模式与选择的画刷组合。dwRop 指定的光栅操作代码说明了 模式是怎样组合的。不是所有的设备上下文都支持 PatBlt 函数。可调用带 RASTERCAPS 索引的 Get-DeviceCaps 成员函数决定设备上下文是否支持 PatBlt,并检验 RC_BITBLT 标记的返回值。调用如果 成功,则返回非零值,否则为 0。 第 1 章 位图基础知识 · 17· BOOL StretchBlt ( int x, // 目标矩形左上角的 X 逻辑坐标 int y , // 目标矩形左上角的 Y 逻辑坐标 int nWidth, // 目标矩形的宽度(逻辑单位) int nHeight, // 目标矩形的高度(逻辑单位) CDC*pSrcDC, // 指定源设备上下文 intxSrc, // 源矩形左上角的 X 逻辑坐标 int ySrc, // 源矩形左上角的 Y 逻辑坐标 int nSrcWidth, // 源矩形的宽度(逻辑单位) int nSrcHeight. // 源矩形的高度(逻辑单位) DWORD dwRop // 指定光栅操作 ); 函数将源矩形中的位图拷贝到目标矩形中,如果有必要,可以扩展或压缩该位图使其与目标矩形尺 寸吻合。函数使用目标设备上下文(由 SetStretchBltMode 设置)的扩展模式来决定如何扩展或压缩位图。 StretchBlt 函数将 pSrcDC 源设备中的位图移动到目标矩形,该矩形用成员函数正在调用的设备上下文来 表示。StretchBlt 函数在内存中对源位图进行扩展或压缩,然后将结果拷贝到目标矩形中。如果模板要与 结果组合,则在扩展后的位图拷贝到目标矩形后才组合。如果用到画刷,应使用目标设备上下文中选定 的画刷。目标坐标根据目标设备上下文来转换,源坐标根据源设备上下文来转换。如果位图已经绘制, 则返回非零值,否则为 0。 显示 DDB 位图的基本过程如下: (1) 生成 CBitmap 类的对象,使用 CBitmap::LoadBitmap 函数将位图加载入内存。 (2) 生成 CDC 对象,用 CDC::CreateCompatibleDC 函数创建与显示设备上下文兼容的内存设备 上下文 CDC 对象。CreateCompatibleDC 的函数原型为: virtual BOOL CreateCompatibleDC ( CDC* pDC ); (3) 用 CDC::SelectObject 函数将位图对象选入创建的内存设备上下文中,并保存内存设备上下 文中原有的位图指针。SelectObject 函数的原型为: CBitmap* SelectObject ( CBitmap* pBitmap ); 用 CDC::BitBlt 函数显示位图。 (4) 如需要对位图放大或者压缩,可以采用 CDC::StretchBlt 函数。 (5) 用 CDC::SelectObject 函数恢复内存设备上下文中的原有位图。 下面的函数 DisplayDDB 进行 DDB 位的显示。 /************************************************************************* * \函数名称: * DisplayDDB * * \输入参数: * CDC* pDC - 内存设备上下文 * LPCSTR lpszBitmap - 位图资料名 * * \返回值: * TRUE - 如果显示成功 * FALSE - 如果显示失败 * Visual C++数字图像获取、处理及实践应用 · 18· * \说明: * 无 * ************************************************************************* */ BOOL DisplayDDB( CDC* pDC, LPCSTR lpszBitmap) { CBitmap Bitmap, *pOldBitmap; // 创建 CBitmap 对象和旧的 CBitmap 对象指针 BITMAP bm; // BITMAP 结构,用于存放位图数据 CDC MemDC; // 内存设备上下文 // 载入位图资源 if ( ! Bitmap.LoadBitmap(lpszBitmap)) return FALSE; // 从位图中获取信息,并填充 BITMAP 结构到 bm 中 Bitmap.GetObject(sizeof(BITMAP), &bm); // 生成与 pDC 可兼容的设备上下文 if( ! MemDC.CreateCompatibleDC(pDC)) return FALSE; // 将位图对象选入创建的内存设备上下文,并保存内存设备上下文中原有的位图指针 pOldBitmap = (CBitmap *)MemDC.SelectObject( &Bitmap); // 显示位图 pDC->BitBlt(0,0,bm.bmWidth, bm.bmHeight, &MemDC, 0, 0, SRCCOPY); // 恢复内存设备上下文中的原有位图 MemDC.SelectObject( pOldBitmap ); // 操作成功,返回 TRUE return TRUE; } 1.5 与设备无关的位图 Windows 3.1 以上版本提供了对设备无关位图 DIB 的支持。DIB 位图可以在不同的机器或系统中显 示位图所固有的图像。与 DDB 相比而言,DIB 是一种外部的位图格式,经常存储为以 BMP 为后缀的位 图文件(有时也以 DIB 为后缀)。DIB 位图还支持图像数据的压缩。 1.5.1 DIB 位图的结构 DIB 是标准的 Windows 位图格式,BMP 文件包含了一个 DIB。一个 BMP 文件包括位图文件头结构第 1 章 位图基础知识 · 19· BITMAPFILEHEADER、位图信息头结构 BITMAPINFOHEADER、调色板 PALETTE 和位图像素数据 4 个部分,如图 1-7 所示。 图 1-7 BMP 文件结构示意图 上述 3 个结构在 windows.h 中进行了定义。下面对它们进行详细说明。 第一部分为位图文件头结构 BITMAPFILEHEADER,其结构为: typedef struct tagBITMAPFILEHEADER { WORD bfType; DWORD bfSize; WORD bfReserved1; WORD bfReserved2; DWORD bfOffBits; } BITMAPFILEHEADER, FAR *LPBITMAPFILEHEADER, *PBITMAPFILEHEADER 这个结构的长度是固定的,为 14 个字节(WORD 为无符号 16 位整数,DWORD 为无符号 32 位整 数),各个域的说明如下:  bfType 指定文件类型,必须是 0x424D,即字符串“BM”,也就是说所有.bmp 文件的头两个字节都是“BM”。  bfSize 指定文件大小,包括这 14 个字节。  bfReserved1,bfReserved2 为保留字,不用考虑。  bfOffBits 为从文件头到实际的位图数据的偏移字节数,即图 1-7 中前三个部分的长度之和。 第二部分为位图信息头 BITMAPINFOHEADER,也是一个结构,其定义如下: typedef struct tagBITMAPINFOHEADER { DWORD biSize; LONG biWidth; LONG biHeight; WORD biPlanes; WORD biBitCount DWORD biCompression; DWORD biSizeImage; Visual C++数字图像获取、处理及实践应用 · 20· LONG biXPelsPerMeter; LONG biYPelsPerMeter; DWORD biClrUsed; DWORD biClrImportant; } BITMAPINFOHEADER; 这个结构的长度是固定的,为 40 个字节(WORD 为无符号 16 位整数,DWORD 无符号 32 位整数, 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 的压缩格式,但用的不多。  biSizeImage 指定实际的位图数据占用的字节数,其实也可以从以下的公式中计算出来: biSizeImage=biWidth’ biHeight× 要注意的是:上述公式中的 biWidth’必须是 4 的整倍数(所以不是 biWidth,而是 biWidth’,表示大 于或等于 biWidth 的、离 4 最近的整倍数。举个例子,如果 biWidth=240,则 biWidth’=240;如果 biWidth=241, 则 biWidth’=244)。 如果 biCompression 为 BI_RGB,则该项可能为零。  biXPelsPerMeter 指定目标设备的水平分辨率,单位是每米的像素个数,关于分辨率的概念,将在打印部分详细介绍。  biYPelsPerMeter 指定目标设备的垂直分辨率,单位同上。  biClrUsed 指定本图像实际用到的颜色数,如果该值为零,则用到的颜色数为 2 的 biBitCount 次方。  biClrImportant 指定本图像中重要的颜色数,如果该值为零,则认为所有的颜色都是重要的。 第三部分为调色板(Palette),当然,这里是对那些需要调色板的位图文件而言的。有些位图,如真 彩色图,前面已经讲过,是不需要调色板的,BITMAPINFOHEADER 后直接是位图数据。 调色板实际上是一个数组,共有 biClrUsed 个元素(如果该值为零,则有 2 的 biBitCount 次方个元 素)。数组中每个元素的类型是一个 RGBQUAD 结构,占 4 个字节,其定义如下: typedef struct tagRGBQUAD { BYTE rgbBlue; //该颜色的蓝色分量 第 1 章 位图基础知识 · 21· 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 文件的数据从下到上,从左到右。也就是说,从文件中最先读到的 是图像最下面一行的左边第一个像素,然后是左边第二个像素⋯接下来是倒数第二行左 边第一个像素,左边第二个像素⋯依次类推 ,最后得到的是最上面一行的最右一个像 素。 1.5.2 Win32 SDK DIB 位图操作函数 Win32 SDK 支持一些重要的 DIB 操作函数,但是这些函数还没有封装到 MFC 中。这些函数如表 1-8 所示。 表       中的  操作函数 函数 功能描述 GetDIBits 从 DDB 中获取位图的图像位,用于将 DDB 转换为 DIB 格式 SetDIBits 设置 DIB 位图的图像位,用于将 DIB 转换为 DDB 形式 CreateDIBitmap 用指定的 DIB 来创建 DDB,并用 DIB 信息初始化位图的图像位 SetDIBitsToDevice 直接将 DIB 位图的图像位输出到设备,用于显示 DIB StretchDIBits 将 DIB 位图映射输出到设备的一个矩形区域,位图可能被缩放 CreateDIBPatternBrush 用 DIB 位图来创建模式画刷 CreateDIBSection 创建一个可直接写入的 DIB GetDIBColortable 获取 DIB 颜色表 SetDIBColorTable 设置 DIB 的颜色表 下面对这些 DIB 位图操作函数进行说明。 int SetDIBitsToDevice ( HDC hdc, // 显示设备上下文句柄 int xDest, // 目标区域左上角 x 坐标 int yDest, // 目标区域左上角 y 坐标 DWORD dwWidth, // 源矩形宽度 DWORD dwHeight, // 源矩形高度 int xSrc, // 源矩形左上角 x 坐标 int ySrc, // 源矩形左上角 y 坐标 Visual C++数字图像获取、处理及实践应用 · 22· UINT uStartScan, // 第一扫描线位置 UINT cScanLines, // 扫描线数 CONST VOID *lpvBits, // DIB 数据指针 CONST BITMAPINFO *lpbmi, // BITMAPINFO 结构指针 UINT fuColorUse // 使用颜色的方式 ); 其中 fuColorUse 指定 BITMAPINFO 结构中的 bmiColors 包含的是 RGB 值还是调色板中的索引值。 它的取值为: DIB_PAL_COLORS:调色板中包含的是当前逻辑调色板的索引值。 DIB_RGB_COLORS:调色板中包含的是真正的 RGB 数值。 该函数可以直接在显示器或者打印机上显示 DIB。在显示时不进行缩放处理,即位图的每一个像素 对应于一个显示器的像素点或一个打印机的打印点。正是这个特点限制了该函数的使用,使它不能像函 数 BitBlt( )一样,因为后者使用的是逻辑坐标。函数如果调用成功,返回绘制的行数,否则返回 0。 int StretchDIBits( HDC hdc, // 显示设备上下文句柄 int xDest, // 目标区域左上角 x 坐标 int yDest, // 目标区域左上角 y 坐标 int nDestWidth, // 目标矩形宽度 int nDestHeight, // 目标矩形高度 int xSrc, // 源矩形左上角 x 坐标 int ySrc, // 源矩形左上角 y 坐标 int nSrcWidth, // 源矩形宽度 int nSrcHeight, // 源矩形高度 CONST VOID *lpBits, // DIB 像素数据指针 CONST BITMAPINFO *lpBitsInfo, // BITMAPINFO 结构指针 UINT iUsage, // 使用颜色方式 DWORD dwRop // 光栅操作代码 ); 其中,颜色使用方式和 SetDIBitsToDevice 中的说明相同。该函数类似于 StretchBlt,利用它可以缩 放显示 DIB 于显示器或打印机行。函数如果调用成功,返回绘制的行数,否则返回 0。 int GetDIBits( HDC hdc, // 设备上下文句柄 HBITMAP hbmp, // 位图句柄 UINT uStartScan, // 设置到目标位图的第一扫描线 UINT cScanLines, // 要拷贝的扫描线数 LPVOID lpvBits, // 位图位数组地址 LPBITMAPINFO lpbi, // 位图 BITMAPINFO 结构 UINT uUsage // RGB 或调色板索引 ); 该函数利用申请到的内存,由 DDB 位图来构造 DIB。通过该函数,可以对 DIB 的格式进行控制, 可以指定每个像素颜色的位数,而且可以指定是否可以压缩。如果采用了压缩格式,则必须调用两次, 一次用于计算所需要的内存,另外一次用于产生 DIB 数据。GetDIBits 用法比较复杂,在调用之前首先 要设置好 BITMAPINFOHEADER 结构(包含在 BITMAPINFO 结构中)的几个数据域,同时,应分配足 够的调色板空间。如果设置 lpBits 为 NULL,则调用 GetDiBits 时不会返回任何位,只是填充 biSizeImage第 1 章 位图基础知识 · 23· 域(计算位图图像位的存储量)和调色板。函数如果调用成功并且 lpvBits 非空,返回绘制的行数;如果 失败,则返回 0; int SetDIBits( HDC hdc, // 设备上下文句柄 HBITMAP hbmp, // 位图句柄 UINT uStartScan, // 设置到目标位图的第一扫描线 UINT cScanLines, // 要拷贝的扫描线数 LPVOID lpvBits, // 位图位数组地址 LPBITMAPINFO lpbi, // 位图 BITMAPINFO 结构 UINT fuColorUse // RGB 或调色板索引 ); 该函数将 DIB 转换为 DDB。其用法相对简单,其余可参考 GetDIBits 中的用法。 HBITMAP CreateDIBitmap( HDC hdc, // 设备上下文句柄 CONST BITMAPINFOHEADER *lpbmih, // 指向 BITMAPINFOHEADER 结构 DWORD fdwInit, // 指定是否按照特定的格式初始化位图 CONST VOID *lpbInit, // 位图位数组地址 CONST BITMAPINFO *lpbmi, // 指向初始化位图的 BITMAPINFO 结构指针 UINT fuUsage // RGB 或调色板索引方式 ); 该函数利用 DIB 来创建 DDB 位图。它等价于如下的语句组合: hBitmap = CreateCompatibleBitmap ( hDC, (WORD)lpInfo->biWidth, (WORD)lpInfo->biHeight ); SetDIBits ( hDC, hBitmap, 0, (WORD)lpInfo0->biHeight, lpBits, lpInfo, wUsage ); HBITMAP CreateDIBSection( HDC hdc, // 设备上下文句柄 CONST BITMAPINFO *pbmi, // BITMAPINFO 结构指针 UINT iUsage, // 使用的颜色类型 VOID **ppvBits, // 存放位图图像位值的地址 HANDLE hSection, // 可选用的文件映射对象的句柄 DWORD dwOffset // 文件映射对象中位图位值的偏移量 ); 其中,如果 hSection 为 NULL,系统将为 DIB 分配内存。该函数能创建一种特殊的 DIB,称之为 DIB 项(DIB Section),然后返回一个代表 DIB 的位图句柄,但其中包含 DDB 位图句柄。当用 GetObject 检索其信息时,如 cbBuffer 设置为 sizeof(DIBSECTION),则检索的是 DIBSECTION 结构信息(DIB 结 构信息);如 cbBuffer 设置为 sizeof(BITMAP),则检索的是 BITMAP 结构信息(DDB 结构信息)。它提 供了 DIB 和 DDB 的最好特性。这样我们可以直接访问 DIB 的内存,可以利用位图句柄和内存设备环境, 我们甚至还可以在 DIB 中调用 GDI 函数来绘图。 DIBSECTION 结构包含了 CreateDIBSection 函数所创建的 DIB 位图的信息,其定义如下: typedef struct tagDIBSECTION { BITMAP dsBm; // 对应的 DDB 结构 BITMAPINFOHEADER dsBmih; // 位图信息头结构 DWORD dsBitfields[3]; // 颜色屏蔽值 HANDLE dshSection; // 文件映射对象的句柄 Visual C++数字图像获取、处理及实践应用 · 24· DWORD dsOffset; // 文件映射对象中位值偏移量 } DIBSECTION, *PDIBSECTION; HANDLE LoadImage( HINSTANCE hinst, // 需要加载图像的实例 LPCTSTR lpszName, // 需要加载的图像的文件或者资源名称 UNIT uType, // 需要加载的图像类型 int cxDesired, // 要加载的光标和图标的像素宽度 int cyDesired, // 要加载的光标和图标的像素高度 UNIT fuLoad // 操作代码 ); 其中 ,uType 取 值为 IMAGE_BITMAP 表 示 要加 载 的为 位图 ,IMAGE_CURSOR 表 示光 标 , IMAGE_INCON 表示图标。fuLoad 为操作代码的组合,其常用取值如表 1-9 所示。 表       中加载图像操作代码 操作代码 功能描述 LR_DEFAULTCOLOR 默认值,不对图像颜色进行处理 LR_CREATEDIBSECTION 指定创建一个 DIB 项 LR_DEFAULTSIZE 使用图像默认大小 LR_LOADFROMFILE 指明从由参数 lpszName 指定的文件中加载图像,如果不指明,默认是从由参数 lpszName 指定的资源中加载图像 LR_LOADMAP3DCOLORS 自动把颜色 RGB(128, 128, 128)替换为 COLOR_3DSHADOW,RGB(192, 192, 192)替换为 COLOR_3DFACE,RGB(223, 223, 223)替换为 COLOR_3DLIGHT LR_MONOCHROME 将图像转换为黑白图像 LR_SHARED 共享图像句柄 LR_VGACOLOR 使用 VGA 颜色 该函数为 Windows SDK 提供的函数,直接从磁盘文件中读入一个位图,并返回一个 DIB 句柄。如 果调用成功,返回读取位图的句柄,否则返回 NULL。 BOOL DrawDibDraw( HDRAWDIB hdd, // DrawDib DC 句柄。可通过::DrawDibOpen()返回 HDC hdc, // 设备上下文句柄 int xDst, // 目标区域的左上角 x 坐标 int yDst, // 目标区域的左上角 y 坐标 int dxDst, // 指定 DIB 的宽度 int dyDst, // 指定 DIB 的高度 LPBITMAPINFOHEADER lpbi, // BITMAPINFOHEADER 结构的指针 LPVOID lpBits, // DIB 数据指针 int xSrc, // 源位图要绘制区域的左上角 x 坐标(像素) int ySrc, // 源位图要绘制区域的左上角 y 坐标(像素) int dxSrc, // 要复制原图像矩形区域的宽度(像素) int dySrc, // 要复制原图像矩形区域的高度(像素) UINT wFlags // 绘制方式 ); Windows 提供了窗口视频(VFW,Video for Windows)组件,VFW 中的 DrawDibDraw 函数是一个 可以替代 StretchDIBits 的函数。其主要优点是可以使用抖动颜色,并且提供显示 DIB 的速度,缺点是必 须将 VFW 代码连接到进程中。 第 1 章 位图基础知识 · 25· UINT GetDIBColorTable( HDC hdc, // 已选入 DIB 位图区位图的设备上下文句柄 UINT uStartIndex, // 要检索的第一个调色板的索引 UINT cEntries, // 要检索的第一个调色板的数目 RGBQUAD *pColors // 存放检索的调色板的地址 ); 该函数检索 DIB 位图的调色板的指定部分,DIB 位图区位图(DIB section bitmap)应先选入设备上 下文中。 UINT SetDIBColorTable( HDC hdc, // 已选入 DIB 位图区位图的设备上下文句柄 UINT uStartIndex, // 要设置的第一个调色板的索引 UINT cEntries, // 要设置的第一个调色板的数目 CONST RGBQUAD *pColors // 调色板的地址 ); 该函数设置 DIB 位图的调色板的指定部分,DIB 位图区位图应先选入设备上下文中。 1.5.3 自定义 DIB 位图函数 前面介绍了一些 DIB 位图的基本知识,对 DIB 位图的结构和 Win32 SDK 中有关 DIB 图像的操作函 数进行详细说明。但显然,这些函数还不能满足我们的要求。下一章,将给出一个 CDib 类,在这个类 中,封装了一些 DIB 图像操作的数据和操作。但在这里,我们也给出了几个常用的 DIB 操作函数,以满 足读者不同的需要。 表 1-10 给出了将要介绍的函数。 表    自定义  位图函数集 函数 功能描述 ReadDIB 将指定文件的 DIB 载入 MakeDIBPalette 如果需要调色板的话,读取颜色表,并创建一个 Windows 调色板 PaintDIB 将 DIB 图像进行显示 ReadDIB 函数可以将指定文件 pFile 中的 DIB 读入到内存中,其中信息头和颜色表(如果颜色表存 在的话)存放在*pLpBMIH 中,图像数据存放在*pLpImage 中。在这里,对数据的存放利用了指针的指 针,是因为内存的分配需要在函数中进行(内存的大小需要通过读取信息头中的信息来确定)。下面给出 了该函数的实现代码。 /************************************************************************* * * \函数名称: * ReadDIB * * \输入参数: * CFile* pFile - 需要打开的 DIB 文件 * LPBITMAPINFOHEADER* pLpBMIH - DIB 信息头指针的指针 * LPBYTE* pLpImage - DIB 位图数据块指针的指针 * * \返回值: * BOOL - 如果操作成功,则返回 TRUE Visual C++数字图像获取、处理及实践应用 · 26· * * \说明: * 该函数将指定文件中的 DIB 文件载入,其中信息头和调色板放在*pLpBMIH 中 * 图像数据存放到*pLpImage 中 * ************************************************************************* */ BOOL ReadDIB(CFile* pFile, LPBITMAPINFOHEADER* pLpBMIH, LPBYTE* pLpImage) { // 临时存放信息的变量 int nCount, nSize; BITMAPFILEHEADER bmfh; // 信息头指针 //LPBITMAPINFOHEADER lpBMIH; // DIB 图像数据指针 //LPBYTE lpImage; // 进行读操作 try { // 读取文件头 nCount = pFile->Read((LPVOID) &bmfh, sizeof(BITMAPFILEHEADER)); if(nCount != sizeof(BITMAPFILEHEADER)) { throw new CException; } // 如果文件类型不是"BM",则返回并进行相应错误处理 if(bmfh.bfType != 0x4d42) { throw new CException; } // 计算信息头加上调色板的大小,并分配相应的内存 nSize = bmfh.bfOffBits - sizeof(BITMAPFILEHEADER); *pLpBMIH = (LPBITMAPINFOHEADER) new char[nSize]; // 读取信息头和调色板 nCount = pFile->Read(*pLpBMIH, nSize); // 计算图像数据大小并设置调色板指针 if((*pLpBMIH)->biSize != sizeof(BITMAPINFOHEADER)) { TRACE("Not a valid Windows bitmap -- probably an OS/2 bitmap\n"); throw new CException; 第 1 章 位图基础知识 · 27· } // 如果图像数据内存大小为 0,则重新计算 if((*pLpBMIH)->biSizeImage == 0) { DWORD dwBytes = ((DWORD) (*pLpBMIH)->biWidth * (*pLpBMIH)->biBitCount) / 32; if(((DWORD) (*pLpBMIH)->biWidth * (*pLpBMIH)->biBitCount) % 32) { dwBytes++; } dwBytes *= 4; (*pLpBMIH)->biSizeImage = dwBytes * (*pLpBMIH)->biHeight; } // 分配图像数据内存,并从文件中读取图像数据 *pLpImage = (LPBYTE) new char[(*pLpBMIH)->biSizeImage]; nCount = pFile->Read((*pLpImage), (*pLpBMIH)->biSizeImage); } // 错误处理 catch(CException* pe) { AfxMessageBox("Read error"); pe->Delete(); return FALSE; } // 将指针赋值 // 返回 return TRUE; } MakeDIBPalette 函数将读取颜色表(如果存在的话),并创建一个 Windows 调色板。这个调色板将 在 DIB 图像显示的时候使用。在显示前,将调色板选入即可。下面给出了该函数的实现代码。 /************************************************************************* * * \函数名称: * MakeDIBPalette() * * \输入参数: * LPVOID lpvColorTable - 颜色表指针 * LPBITMAPINFOHEADER lpBMIH - DIB 信息头指针 * Visual C++数字图像获取、处理及实践应用 · 28· * \返回值: * HPALETTE - 如果成功,则调色板句柄 * * \说明: * 该函数将读取颜色表,并创建一个 Windows 调色板,并返回此调色板的句柄 * ************************************************************************ */ HPALETTE MakeDIBPalette(LPVOID lpvColorTable, LPBITMAPINFOHEADER lpBMIH) { // 调色板句柄 HPALETTE hPalette = NULL; // 颜色表颜色数 int nColorTableEntries; // 设置 DIB 中的调色板指针 // lpvColorTable = (LPBYTE) lpBMIH + sizeof(BITMAPINFOHEADER); // 计算调色板的表项数 if(lpBMIH->biClrUsed == 0) { switch(lpBMIH->biBitCount) { case 1: nColorTableEntries = 2; break; case 4: nColorTableEntries = 16; break; case 8: nColorTableEntries = 256; break; case 16: case 24: case 32: nColorTableEntries = 0; break; default: break; } } // 否则调色板的表项数就是用到的颜色数目 else { nColorTableEntries = lpBMIH->biClrUsed; 第 1 章 位图基础知识 · 29· } ASSERT((nColorTableEntries >= 0) && (nColorTableEntries <= 256)); // 如果不存在调色板,则返回 FALSE if(nColorTableEntries == 0) return FALSE; // 给逻辑调色板分配内存 LPLOGPALETTE pLogPal = (LPLOGPALETTE) new char[2 * sizeof(WORD) + nColorTableEntries * sizeof(PALETTEENTRY)]; // 设置逻辑调色板的信息 pLogPal->palVersion = 0x300; pLogPal->palNumEntries = nColorTableEntries; // 拷贝 DIB 中的颜色表到逻辑调色板 LPRGBQUAD pDibQuad = (LPRGBQUAD) lpvColorTable; for(int i = 0; i < nColorTableEntries; i++) { pLogPal->palPalEntry[i].peRed = pDibQuad->rgbRed; pLogPal->palPalEntry[i].peGreen = pDibQuad->rgbGreen; pLogPal->palPalEntry[i].peBlue = pDibQuad->rgbBlue; pLogPal->palPalEntry[i].peFlags = 0; pDibQuad++; } // 创建逻辑调色板 hPalette = ::CreatePalette(pLogPal); // 删除临时变量 delete pLogPal; // 返回调色板句柄 return hPalette; } 函数 PaintDIB 对 DIB 图像进行显示。其中,信息头和颜色表放在 lpBMIH 中,DIB 图像数据放在 lpImage 中。如果调色板存在的话,还需要给出调色板。 /************************************************************************* * * \函数名称: * PaintDIB() * * \输入参数: * CDC* pDC - 指向将要接收 DIB 图像的设备上下文指针 * LPBITMAPINFOHEADER lpBMIH - DIB 信息头指针 Visual C++数字图像获取、处理及实践应用 · 30· * LPBYTE lpImage - DIB 位图数据块地址 * CPoint origin - 显示 DIB 的逻辑坐标 * CSize size - 显示矩形的宽度和高度 * HPALETTE hPalette - 指向 DIB 的调色板句柄 * * \返回值: * BOOL - 如果成功,则返回 TRUE * * \说明: * 该函数将 DIB 图像进行显示 * ************************************************************************ */ BOOL PaintDIB(CDC* pDC, LPBITMAPINFOHEADER lpBMIH, LPBYTE lpImage, CPoint origin, CSize size, HPALETTE hPalette) { if(lpBMIH == NULL) return FALSE; // 如果调色板不为空,则将调色板选入设备上下文 if(hPalette != NULL) { ::SelectPalette(pDC->GetSafeHdc(), hPalette, TRUE); } // 设置显示模式 pDC->SetStretchBltMode(COLORONCOLOR); // 在设备的 origin 位置上画出大小为 size 的图像 ::StretchDIBits(pDC->GetSafeHdc(), origin.x, origin.y,size.cx,size.cy, 0, 0, lpBMIH->biWidth, lpBMIH->biHeight, lpImage, (LPBITMAPINFO) lpBMIH, DIB_RGB_COLORS, SRCCOPY); // 返回 return TRUE; } 要说明的是,这些函数都是经过调试过的,读者可以放心使用。由于在下一章将对 DIB 操作进行封 装,构造 CDib 类。在那里,将定义更丰富的操作函数集,限于篇幅,这里就不给出 DIB 显示的工程实 现了。 第第 22 章章 构构造造 CCDDiibb 类类 第一章中,介绍了数字图像的基本概念,并对颜色和调色板进行了介绍。在这些基础上,对 DDB 和 DIB 进行了比较详细的介绍。其中,对 Win32 SDK 提供的一些 DIB 处理函数也进行了详细说明。这 时候,读者应该对 DIB 建立了基本的概念。MFC 没有对 DIB 进行封装,这些函数比较凌乱,用起来不 方便,而且,这也不符合面向对象的编程习惯。因此,在这一章,笔者将试图建立一个处理 DIB 的类- CDib。在这个类中,对 DIB 的数据和基本的 DIB 操作函数进行封装,并对 DIB 的操作进行了深入讨论。 通过本章的阅读,读者将会对 DIB 豁然开朗。 2.1 CDib 类的总体设计 类描述了一组有相同特性(数据元素)和相同行为(函数)的对象。类实际上就是数据类型,例如, 浮点数也有一组特性和行为。它们之间区别在于程序员定义类是为了与具体问题相适应,而不是被迫使 用已存在的数据类型。这些已存在的数据类型的设计动机仅仅是为了描述机器的存储单元。程序员可以 通过增添他所需要的新数据类型来扩展这个程序设计语言。简单的说,类就是特性加上行为。 要想构造一个十全十美的类是困难的。在构造 CDib 类的过程中,笔者也遇到了不少的问题,特别 是要考虑究竟哪些函数需要封装,数据是否应该隐藏在类中等。将数据隐藏在类中,可以使类的数据成 员不会被意外地修改,但这需要付出降低程序效率的代价。在这里,笔者把一些认为是必要的操作进行 了封装,读者可以在这个基础上根据自己的需要进行修改和扩充。 首先申明一点,这些类都是上一章的基础上进行的,读者如果跳过了第一章的内容,则遇到操作 DIB Win32 SDK 函数时,可以查阅 Visual C++的参考手册或者到上一章中查找。 2.1.1 CDib 类的基本功能 通过上一章对 DIB 的介绍和分析,CDib 类应该包括以下基本功能: • 能够用多种方式来构造 CDib 类,利用多种方式得到 DIB。 • 能够从资源中载入 DIB 图像。 • 能够进行 DIB 格式文件的读、写操作,包括进行串行化。 • 能够提供图像数据进行操作。 • 能够提供必要的 DIB 信息。 • 能够进行 DIB 与 DDB 之间的相互转换。 • 能够读写压缩格式的 DIB,并进行相互转换。 • 能够对 DIB 的调色板进行操作,包括创建,选入设备上下文等。 • 能够使用内存映射文件。 • 能够将 DIB 进行显示,包括缩放显示。 • 能够直接在 DIB 上使用 GDI 操作。 对于上面的一些功能,考虑到要进行类的嵌套,或者觉得不封装到类中反而会更方便使用,因此笔 者没有将它们封装进去,例如 DDB 到 DIB 的转换。 Visual C++数字图像获取、处理及实践应用 · 32· Á 2.1.2 CDib 类基本操作函数 下面,将逐步构建 CDib 类。首先,定义 CDib 类的基本操作函数。CDib 类中的基本操作函数集如 表 2-1 所示。 表 类中基本操作函数集 函数类型 函数名 功能描述 构造函数 CDib 构造函数 析构函数 ~CDib 析构函数 Read 从文件中读取 DIB ReadSection 从文件中读取 DIB,并创建 DIBSection Write 将 DIB 写入文件中 输入 输出 函数 Serialize 串行化 GetDibSaveDim 获得 DIB 存放的高度和宽度 GetDimensions 获得 DIB 的宽度和高度 GetPixel 获得指定位置的像素 GetPixelOffset 获得指定位置的像素在 DIB 中存放的位置 GetSizeHeader 获得信息头文件中的字节数及其颜色表 GetSizeImage 获得 DIB 图像中字节数 PaletteSize 获得 DIB 中颜色表表项数 属性 函数 IsEmpty 判断 DIB 是否为空 显示函数 Draw 在指定设备和位置上显示 DIB 压缩格式转换 Compress 将 DIB 重新生成为压缩或不压缩的 DIB AttachMapFile 打开内存映射文件 AttachMemory 将内存中的 DIB 与已有的 CDib 对象关联 CopyToMapFile 创建一个新的内存映射文件,并将 DIB 数据复制到文件的内存 内存映射文件 DetachMapFile 断开文件映射 MakePalette 创建调色板(如果有的话) UsePalette 将调色板选入设备环境 调色板操作函数 SetSystemPalette 创建一个与系统兼容的调色板 CreateBitmap 从 DIB 中创建 DDB 位图 ConvertFromDDB 从 DDB 中创建 DIB CreateSection 创建一个 DIBSection 其他 Empty 释放已经分配的内存 另外,对一些有用的数据,也在 CDib 类中进行了封装。这包括信息头指针,颜色表的指针,图像 数据指针,图像的颜色数目等。 上面的这些函数只是 CDib 类应该具有的基本功能。将它们封装后,读者可以根据自己的需要进行 修改和扩充。其头文件和实现文件分别为 CDib.h 和 CDib.cpp。 文件 CDib.h 包含了和 CDib 类的类声明有关的所有信息。CDib 类声明及其实现细节如以下代码所示。 // cdib.h 对 CDib 类中数据和函数进行进行声明 #ifndef _INSIDE_VISUAL_CPP_CDIB #define _INSIDE_VISUAL_CPP_CDIB class CDib : public CObject { public: enum Alloc {noAlloc, crtAlloc, heapAlloc}; // 枚举类型,表示内存分配的状况 DECLARE_SERIAL(CDib) public: LPVOID m_lpvColorTable; // 调色板指针 第 2 章 构造 CDib 类 · 33· HBITMAP m_hBitmap; // BITMAP 结构指针 LPBYTE m_lpImage; // DIB 位图数据块地址 LPBITMAPINFOHEADER m_lpBMIH; // DIB 信息头指针 HGLOBAL m_hGlobal; // 全局的句柄,用于内存映射文件中 Alloc m_nBmihAlloc; // 表示信息头内存分配的状况 Alloc m_nImageAlloc; // 表示位图数据分配的状况 DWORD m_dwSizeImage; // DIB 位图中的字节数(信息头和调色板数据 // 除外) int m_nColorTableEntries; // 调色板表项数 HANDLE m_hFile; // 文件句柄 HANDLE m_hMap; // 内存映射文件句柄 LPVOID m_lpvFile; // 文件句柄 HPALETTE m_hPalette; // 调色板句柄 public: RGBQUAD GetPixel(int x, int y); // 获取像素真实的颜色值 LONG GetPixelOffset(int x, int y); // 获取像素在图像数据块中的位置 CSize GetDibSaveDim(); // 获取 DIB 位图数据块的存储尺寸 BOOL IsEmpty(); // 判断 DIB 是否为空 WORD PaletteSize(); // 计算调色板的表项数 CDib(); // 构造函数 CDib(CSize size, int nBitCount); // 根据指定的位图尺寸和像素位数来构造 // CDib 实例 ~CDib(); // 析构函数 int GetSizeImage() {return m_dwSizeImage;} // 获取 DIB 位图中数据的字节数 int GetSizeHeader() // 获取信息头和调色板的尺寸 {return sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * m_nColorTableEntries;} CSize GetDimensions(); // 获取以像素表示的 DIB 的宽度和高度 // 以读模式打开内存映射文件,并将其与 CDib 对象进行关联 BOOL AttachMapFile(const char* strPathname, BOOL bShare = FALSE); // 创建一个新的内存映射文件,并进行数据的复制 BOOL CopyToMapFile(const char* strPathname); // 用内存中的 DIB 与已有的 CDib 对象关联 BOOL AttachMemory(LPVOID lpvMem, BOOL bMustDelete = FALSE, HGLOBAL hGlobal = NULL); // 将 CDib 对象按照指定的尺寸输出到显示器(或者打印机) BOOL Draw(CDC* pDC, CPoint origin, CSize size); // until we implemnt CreateDibSection // 创建一个 DIB 短,图像内存将不被初始化 Visual C++数字图像获取、处理及实践应用 · 34· HBITMAP CreateSection(CDC* pDC = NULL); // 将 CDib 对象的逻辑调色板选入设备上下文,然后实现该调色板 UINT UsePalette(CDC* pDC, BOOL bBackground = FALSE); // 如果调色板存在的话,读取调色板,并创建一个 Windows 调色板 BOOL MakePalette(); // 设置系统调色板 BOOL SetSystemPalette(CDC* pDC); // 将 DIB 重新生成为压缩的或者不压缩的 DIB。 BOOL Compress(CDC* pDC, BOOL bCompress = TRUE); // 从已有的 DIB 中创建 GDI 位图 HBITMAP CreateBitmap(CDC* pDC); // 从文件中读取数据,并填充文件头、信息头、调色板和位图数据 BOOL Read(CFile* pFile); // 从 BMP 文件中读取信息头,调用 CreateDIBSection 来分配位图数据内存,然后将位图从 // 该文件读入刚才分配的内存 BOOL ReadSection(CFile* pFile, CDC* pDC = NULL); // 将 DIB 从 CDib 对象写入文件 BOOL Write(CFile* pFile); // 串行化过程 void Serialize(CArchive& ar); // 清空 DIB,释放已经分配的内存,并且关闭映射文件 void Empty(); // 计算调色板的尺寸 void ComputePaletteSize(int nBitCount); private: // 断开映射文件的关联 void DetachMapFile(); // 计算调色板和位图尺寸 void ComputeMetrics(); }; #endif // _INSIDE_VISUAL_CPP_CDIB 第 2 章 构造 CDib 类 · 35· 1. 数据成员 在 CDib 类中,对一些有用的数据进行了封装,并放在 public 中。 LPVOID m_lpvColorTable; // 颜色表指针 HBITMAP m_hBitmap; // BITMAP 结构指针 LPBYTE m_lpImage; // DIB 位图数据块地址 LPBITMAPINFOHEADER m_lpBMIH; // DIB 信息头指针 HGLOBAL m_hGlobal; // 全局的句柄,用于内存映射文件中 Alloc m_nBmihAlloc; // 表示信息头内存分配的状况 Alloc m_nImageAlloc; // 表示位图数据分配的状况 DWORD m_dwSizeImage; // DIB 图像的字节数(信息头和调色板数据除 // 外) int m_nColorTableEntries; // 调色板表项数 HANDLE m_hFile; // 文件句柄 HANDLE m_hMap; // 内存映射文件句柄 LPVOID m_lpvFile; // 文件句柄 HPALETTE m_hPalette; // 调色板句柄 其中,m_hGlobal 是 DIB 结构的句柄,在内存映射文件中创建。DIB 的信息头放在 m_lpBMIH 中, 同时,颜色表放在 m_lpBMIH 后面,并给出了数据成员 m_lpvColorTable 来表示。 2. 构造和析构函数 创建一个 CDib 对象,需要两步操作。先调用构造函数构造对象,然后再调用输入函数将数据载入。 在 CDib 类中,设计了两个构造函数。其实现代码为: /************************************************************************* * * \函数名称: * CDib() * * \输入参数: * 无 * * \返回值: * 无 * * \说明: * 构造函数 * ************************************************************************ */ CDib::CDib() { m_hFile = NULL; m_hBitmap = NULL; m_hPalette = NULL; Visual C++数字图像获取、处理及实践应用 · 36· m_nBmihAlloc = m_nImageAlloc = noAlloc; Empty(); } 另一个构造函数为: /************************************************************************* * * \函数名称: * CDib() * * \输入参数: * CSize size - 位图尺寸 * int nBitCount - 像素位数 * * \返回值: * 无 * * \说明: * 构造函数 * 根据给定的位图尺寸和像素位数构造 CDib 对象,并对信息头和调色板分配内存 * 但并没有给位图数据分配内存 * ************************************************************************ */ CDib::CDib(CSize size, int nBitCount) { m_hFile = NULL; m_hBitmap = NULL; m_hPalette = NULL; m_nBmihAlloc = m_nImageAlloc = noAlloc; Empty(); // 根据像素位数计算调色板尺寸 ComputePaletteSize(nBitCount); // 分配 DIB 信息头和调色板的内存 m_lpBMIH = (LPBITMAPINFOHEADER) new char[sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * m_nColorTableEntries]; // 设置信息头内存分配状态 m_nBmihAlloc = crtAlloc; // 设置信息头中的信息 m_lpBMIH->biSize = sizeof(BITMAPINFOHEADER); m_lpBMIH->biWidth = size.cx; 第 2 章 构造 CDib 类 · 37· m_lpBMIH->biHeight = size.cy; m_lpBMIH->biPlanes = 1; m_lpBMIH->biBitCount = nBitCount; m_lpBMIH->biCompression = BI_RGB; m_lpBMIH->biSizeImage = 0; m_lpBMIH->biXPelsPerMeter = 0; m_lpBMIH->biYPelsPerMeter = 0; m_lpBMIH->biClrUsed = m_nColorTableEntries; m_lpBMIH->biClrImportant = m_nColorTableEntries; // 计算图像数据内存的大小,并设置此 DIB 的调色板的指针 ComputeMetrics(); // 将此 DIB 的调色板初始化为 0 memset(m_lpvColorTable, 0, sizeof(RGBQUAD) * m_nColorTableEntries); // 暂时不分配图像数据内存 m_lpImage = NULL; } 在析构函数中,需要将 CDib 已分配的内存全部释放。如果进行了内存映射,则需要断开关联。其 实现代码为: /************************************************************************* * \函数名称: * ~CDib() * * \输入参数: * 无 * * \返回值: * 无 * * \说明: * 析构函数,并释放所有分配的 DIB 内存 * ************************************************************************ */ CDib::~CDib() { Empty(); } 其中 Empty 函数就是释放内存和断开与内存映射文件关联的函数。这个函数将在后面介绍。 3. 输入输出函数 在 CDib 类中,定义了 4 个输入输出的函数,以进行文件的读写操作。最基本的就是 Read 和 WriteVisual C++数字图像获取、处理及实践应用 · 38· 函数。如果还觉得麻烦,可以通过重载 Serialize 实现串行化。 函数 Read 从文件 CFile 中将数据读入,并设置相应的数据成员。其中调用了 ComputeMetrics、 ComputePaletteSize 和 MakePalette 函数,这 3 个函数都是 CDib 类的成员函数,后面将具体介绍。在上 一章中给出的 Read 函数,也和这个函数相类似,不过由于对数据进行了封装,可以不再用指针的指针 来进行操作了。其实现代码如下: /************************************************************************* * * \函数名称: * Read() * * \输入参数: * CFile* pFile - 指向 CFile 对象的指针 * * \返回值: * BOOL - 如果成功,则返回 TRUE * * \说明: * 该函数从一个文件读入 CDib 对象,该文件必须成功打开。如果该文件是 BMP 文件 * 读取工作从文件头开始。如果该文件是一个文档,读取工作则从当前文件指针处开始 * ************************************************************************ */ BOOL CDib::Read(CFile* pFile) { // 释放已经分配的内存 Empty(); // 临时存放信息的变量 int nCount, nSize; BITMAPFILEHEADER bmfh; // 进行读操作 try { // 读取文件头 nCount = pFile->Read((LPVOID) &bmfh, sizeof(BITMAPFILEHEADER)); if(nCount != sizeof(BITMAPFILEHEADER)) { throw new CException; } // 如果文件类型部位"BM",则返回并进行相应错误处理 if(bmfh.bfType != 0x4d42) { throw new CException; } 第 2 章 构造 CDib 类 · 39· // 计算信息头加上调色板的大小,并分配相应的内存 nSize = bmfh.bfOffBits - sizeof(BITMAPFILEHEADER); m_lpBMIH = (LPBITMAPINFOHEADER) new char[nSize]; m_nBmihAlloc = m_nImageAlloc = crtAlloc; // 读取信息头和调色板 nCount = pFile->Read(m_lpBMIH, nSize); // 计算图像数据大小并设置调色板指针 ComputeMetrics(); // 计算调色板的表项数 ComputePaletteSize(m_lpBMIH->biBitCount); // 如果 DIB 中存在调色板,则创建一个 Windows 调色板 MakePalette(); // 分配图像数据内存,并从文件中读取图像数据 m_lpImage = (LPBYTE) new char[m_dwSizeImage]; nCount = pFile->Read(m_lpImage, m_dwSizeImage); } // 错误处理 catch(CException* pe) { AfxMessageBox("Read error"); pe->Delete(); return FALSE; } // 返回 return TRUE; } ReadSection 函数从文件 Cfile 中读入数据,并创建一个 DIBSection。对于 DIBSection,读者可以从 第一章或者 MSDN 中查阅有关的信息。对于这个特殊的 DIBSection,可以先调用这个函数,然后利用 m_hBitmap 成员数据来进行 GDI 操作。 /************************************************************************* * * \函数名称: * ReadSection() * * \输入参数: * CFile* pFile - 指向 CFile 对象的指针;对应的磁盘 Visual C++数字图像获取、处理及实践应用 · 40· * - 文件中包含 DIB * CDC* pDC - 设备上下文指针 * * \返回值: * BOOL - 如果成功,则返回 TRUE * * \说明: * 该函数从 BMP 文件中读取信息头,调用 CreateDIBSection 来分配图像内存,然后将 * 图像从该文件读入刚才分配的内存。如果你想从磁盘读取一个 DIB,然后通过调用 * GDI 函数编辑它的话,可以使用该函数。你可以用 Write 或 CopyToMapFile 将 DIB 写 * 回到磁盘 * ************************************************************************ */ BOOL CDib::ReadSection(CFile* pFile, CDC* pDC /* = NULL */) { // 释放已经分配的内存 Empty(); // 临时变量 int nCount, nSize; BITMAPFILEHEADER bmfh; // 从文件中读取数据 try { // 读取文件头 nCount = pFile->Read((LPVOID) &bmfh, sizeof(BITMAPFILEHEADER)); if(nCount != sizeof(BITMAPFILEHEADER)) { throw new CException; } // 如果文件类型不是"BM",则返回并进行相应错误处理 if(bmfh.bfType != 0x4d42) { throw new CException; } // 计算信息头加上调色板的大小,并分配相应的内存 nSize = bmfh.bfOffBits - sizeof(BITMAPFILEHEADER); m_lpBMIH = (LPBITMAPINFOHEADER) new char[nSize]; m_nBmihAlloc = crtAlloc; m_nImageAlloc = noAlloc; // 读取信息头和调色板 第 2 章 构造 CDib 类 · 41· nCount = pFile->Read(m_lpBMIH, nSize); // 如果图像为压缩格式,则不进行后续处理 if(m_lpBMIH->biCompression != BI_RGB) { throw new CException; } // 计算图像数据大小并设置调色板指针 ComputeMetrics(); // 计算调色板的表项数 ComputePaletteSize(m_lpBMIH->biBitCount); // 如果 DIB 中存在调色板,则创建一个 Windows 调色板 MakePalette(); // 将 CDib 对象的逻辑调色板选入设备上下文 UsePalette(pDC); // 创建一个 DIB 段,并分配图像内存 m_hBitmap = ::CreateDIBSection(pDC->GetSafeHdc(), (LPBITMAPINFO) m_lpBMIH, DIB_RGB_COLORS, (LPVOID*) &m_lpImage, NULL, 0); ASSERT(m_lpImage != NULL); // 从文件中读取图像数据 nCount = pFile->Read(m_lpImage, m_dwSizeImage); } // 错误处理 catch(CException* pe) { AfxMessageBox("ReadSection error"); pe->Delete(); return FALSE; } return TRUE; } 函数 Write 是基本的输出函数。它将 DIB 数据写入指定的文件中。 /************************************************************************* * * \函数名称: * Write() * * \输入参数: * CFile* pFile - 指向 CFile 对象的指针 Visual C++数字图像获取、处理及实践应用 · 42· * * \返回值: * BOOL - 如果成功,则返回 TRUE * * \说明: * 该函数把 DIB 从 CDib 对象写进文件中。该文件必须成功打开或者创建 * ************************************************************************ */ BOOL CDib::Write(CFile* pFile) { BITMAPFILEHEADER bmfh; // 设置文件头中文件类型为"BM" bmfh.bfType = 0x4d42; // 计算信息头和调色板的大小尺寸 int nSizeHdr = sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * m_nColorTableEntries; // 设置文件头信息 bmfh.bfSize = sizeof(BITMAPFILEHEADER) + nSizeHdr + m_dwSizeImage; bmfh.bfReserved1 = bmfh.bfReserved2 = 0; bmfh.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * m_nColorTableEntries; // 进行写操作 try { pFile->Write((LPVOID) &bmfh, sizeof(BITMAPFILEHEADER)); pFile->Write((LPVOID) m_lpBMIH, nSizeHdr); pFile->Write((LPVOID) m_lpImage, m_dwSizeImage); } // 错误处理 catch(CException* pe) { pe->Delete(); AfxMessageBox("write error"); return FALSE; } // 返回 return TRUE; } 通过重载 Serialize 函数,CDib 类实现了串行化。在 Serialize 函数中,通过调用前面给出的 Read 和 Write 函数来实现对文件的读写。需要注意的是,要重载 Serialize 函数,需要做到以下几点: 第 2 章 构造 CDib 类 · 43· • 以 Cobject 类为父类派生类 • 覆盖 Cobject 类中的 Serialize 函数 • 在类说明中使用 DECLARE_SERIAL 宏 • 定义一个不带参数的构造函数 • 在类的实现文件(.cpp 文件)中使用 IMPLEMENT_SERIAL 宏。在 CDib.cpp 文件中,就使用了 MPLEMENT_SERIAL(CDib, Cobject, 0)。 /************************************************************************* * * \函数名称: * Serialize() * * \输入参数: * CArchive& ar - 指向应用程序归档对象 * * \返回值: * 无 * * \说明: * 该函数进行串行化过程,将 CDib 数据进行读入或者写出 * ************************************************************************ */ void CDib::Serialize(CArchive& ar) { DWORD dwPos; // 获得此归档文件的 CFile 对象指针 dwPos = ar.GetFile()->GetPosition(); TRACE("CDib::Serialize -- pos = %d\n", dwPos); // 从归档文件缓冲区中冲掉未写入数据 ar.Flush(); // 重新获得此归档文件的 CFile 对象指针 dwPos = ar.GetFile()->GetPosition(); TRACE("CDib::Serialize -- pos = %d\n", dwPos); // 确定归档文件是否被存储,是则进行存储 if(ar.IsStoring()) { Write(ar.GetFile()); } // 否则进行加载 else { Visual C++数字图像获取、处理及实践应用 · 44· Read(ar.GetFile()); } } 4. 属性函数 在 CDib 类中,对一些属性信息进行封装,DIB 的信息不能被外界改变,外界只能通过这组函数来 获取这些信息。CDib 类中提供了 8 个属性函数,这些函数可参看表 2-1。下面对这些函数进行详细说明。 函数 GetDibSaveDim 用来获取 DIB 图像数据实际存储的高度和宽度。由于 DIB 图像数据的宽度必 须是 DWORD(4 字节)对齐的,所以,还必须另外计算,而不能直接套用 DIB 图像的宽度和高度来对 数据进行存储和定位。其实现代码如下: /************************************************************************* * * \函数名称: * GetDibSaveDim() * * \输入参数: * 无 * * \返回值: * CSize - DIB 实际存储的高度和宽度 * * \说明: * 该函数函数用来得到 dib 的实际存储宽度(DWORD 对齐) * ************************************************************************ */ CSize CDib::GetDibSaveDim() { CSize sizeSaveDim; sizeSaveDim.cx = ( m_lpBMIH->biWidth * m_lpBMIH->biBitCount + 31)/32 * 4; sizeSaveDim.cy = m_lpBMIH->biHeight; return sizeSaveDim; } 函数 GetDimensions 是用来获得 DIB 图像的高度和宽度的。和 GetDibSaveDim 中返回的数据进行比 较,会发现其中的不同之处。其实现代码如下: /************************************************************************* * * \函数名称: * GetDimensions() * * \输入参数: * 无 第 2 章 构造 CDib 类 · 45· * * \返回值: * CSize - DIB 的宽度和高度 * * \说明: * 返回以像素表示的 DIB 的宽度和高度 * ************************************************************************ */ CSize CDib::GetDimensions() { if(m_lpBMIH == NULL) return CSize(0, 0); return CSize((int) m_lpBMIH->biWidth, (int) m_lpBMIH->biHeight); } 函数 GetPixel 将返回 DIB 中指定位置的颜色值,这个颜色值将用 RGBQUAD 的格式给出。 /************************************************************************* * * \函数名称: * GetPixel() * * \输入参数: * int x - 像素在 X 轴的坐标 * int y - 像素在 Y 轴的坐标 * * \返回值: * RGBQUAD - 返回 DIB 在该点真实的颜色 * * \说明: * 该函数得到 DIB 图像在该点真实的颜色 * ************************************************************************ */ RGBQUAD CDib::GetPixel(int x, int y) { RGBQUAD cColor; //CPalette* pPaletteTemp; switch (m_lpBMIH->biBitCount) { case 1 : if (1<<(7-x%8) & *(LPBYTE)(m_lpImage+GetPixelOffset(x, y))) { cColor.rgbBlue = 255; cColor.rgbGreen = 255; cColor.rgbRed = 255; Visual C++数字图像获取、处理及实践应用 · 46· cColor.rgbReserved =0; } else { cColor.rgbBlue = 0; cColor.rgbGreen = 0; cColor.rgbRed = 0; cColor.rgbReserved =0; } break; case 4 : { int nIndex = (*(LPBYTE)(m_lpImage+GetPixelOffset(x, y)) & (x%2 ? 0x0f : 0xf0)) >> (x%2 ? 0 : 4); LPRGBQUAD pDibQuad = (LPRGBQUAD) (m_lpvColorTable) + nIndex; cColor.rgbBlue = pDibQuad->rgbBlue; cColor.rgbGreen = pDibQuad->rgbGreen; cColor.rgbRed = pDibQuad->rgbRed; cColor.rgbReserved =0; } break; case 8 : { int nIndex = *(BYTE*)(m_lpImage+GetPixelOffset(x, y)); LPRGBQUAD pDibQuad = (LPRGBQUAD) (m_lpvColorTable) + nIndex; cColor.rgbBlue = pDibQuad->rgbBlue; cColor.rgbGreen = pDibQuad->rgbGreen; cColor.rgbRed = pDibQuad->rgbRed; cColor.rgbReserved =0; } break; default: int nIndex = *(BYTE*)(m_lpImage+GetPixelOffset(x, y)); cColor.rgbRed = m_lpImage[nIndex]; cColor.rgbGreen = m_lpImage[nIndex + 1]; cColor.rgbBlue = m_lpImage[nIndex + 2]; cColor.rgbReserved =0; break; } return cColor; } 函数 GetPixelOffset 获取指定位置(逻辑坐标)的像素在实际存储时其在内存中的相对偏移量。由于 DIB 的数据存放是按照从下至上,从左至右的顺序,所以还需要进行转换。 第 2 章 构造 CDib 类 · 47· /************************************************************************* * * \函数名称: * GetPixelOffset() * * \输入参数: * int x - 像素在 X 轴的坐标 * int y - 像素在 Y 轴的坐标 * * \返回值: * int - 返回像素在图像数据块中的真实地址 * * \说明: * 该函数得到坐标为(x,y)的像素点的真实地址。由于 DIB 结构中对图像数据排列的 * 方式为从下到上,从左到右的,所以需要进行转换 * ************************************************************************ */ LONG CDib::GetPixelOffset(int x, int y) { CSize sizeSaveDim; sizeSaveDim = GetDibSaveDim(); LONG lOffset = (LONG) (sizeSaveDim.cy - y - 1) * sizeSaveDim.cx + x / (8 / m_lpBMIH->biBitCount); return lOffset; } 函数 GetSizeHeader 将获得信息头和颜色表(如果存在的话)的字节数。 int GetSizeHeader() // 获取信息头和调色板的尺寸 {return sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * m_nColorTableEntries;} 函数 GetSizeImage 获得 DIB 图像实际存储的字节数。 int GetSizeImage() {return m_dwSizeImage;} // 获取 DIB 位图中数据的字节数 函数 PaletteSize 将返回 DIB 中颜色表的表项数,也就是调色板的表项数。 /************************************************************************* * * \函数名称: * PaletteSize() * * \输入参数: * 无 * * \返回值: * DWORD - 返回调色板的尺寸 * Visual C++数字图像获取、处理及实践应用 · 48· * \说明: * 该函数计算机调色板所需的尺寸 * ************************************************************************ */ WORD CDib::PaletteSize() { // 临时变量 WORD NumColors; LPBITMAPINFOHEADER lpbi=m_lpBMIH; // 如果 biClrUsed 为零,且图像像素位数小于 8,则计算调色板用到的表项数 NumColors = ((lpbi)->biClrUsed == 0 && (lpbi)->biBitCount <= 8 \ ? (int)(1 << (int)(lpbi)->biBitCount) \ : (int)(lpbi)->biClrUsed); // 根据颜色表示的字节数计算调色板的尺寸 if (lpbi->biSize == sizeof(BITMAPCOREHEADER)) return NumColors * sizeof(RGBTRIPLE); else return NumColors * sizeof(RGBQUAD); } 函数 IsEmpty 用于判断 DIB 是否为空。 /************************************************************************* * * \函数名称: * IsEmpty() * * \输入参数: * 无 * * \返回值: * BOOL - 如果信息头和图像数据为空,则返回 TRUE * * \说明: * 判断信息头和图像数据是否为空 * ************************************************************************ */ BOOL CDib::IsEmpty() { if( m_lpBMIH == NULL&&m_lpImage == NULL) 第 2 章 构造 CDib 类 · 49· return TRUE; else return FALSE; } 5. 显示函数 CDib 类中只提供了一个显示函数 Draw。但这个函数是一个功能强大的显示函数,它调用了 StretchDIBits 进行显示,可以进行缩放。在进行显示之前,需要调用 UsePalette 函数(将在后面介绍) 将调色板选入。其实现代码如下: /************************************************************************* * * \函数名称: * Draw() * * \输入参数: * CDC* pDC - 指向将要接收 DIB 图像的设备上下文指针 * CPoint origin - 显示 DIB 的逻辑坐标 * CSize size - 显示矩形的宽度和高度 * * \返回值: * BOOL - 如果成功,则为 TRUE, * * \说明: * 通过调用 Win32 SDK 的 StretchDIBits 函数将 CDib 对象输出到显示器(或者打印机)。 * 为了适合指定的矩形,位图可以进行必要的拉伸 * ************************************************************************ */ BOOL CDib::Draw(CDC* pDC, CPoint origin, CSize size) { // 如果信息头为空,表示尚未有数据,返回 FALSE if(m_lpBMIH == NULL) return FALSE; // 如果调色板不为空,则将调色板选入设备上下文 if(m_hPalette != NULL) { ::SelectPalette(pDC->GetSafeHdc(), m_hPalette, TRUE); } // 设置显示模式 pDC->SetStretchBltMode(COLORONCOLOR); // 在设备的 origin 位置上画出大小为 size 的图像 ::StretchDIBits(pDC->GetSafeHdc(), origin.x, origin.y,size.cx,size.cy, 0, 0, m_lpBMIH->biWidth, m_lpBMIH->biHeight, Visual C++数字图像获取、处理及实践应用 · 50· m_lpImage, (LPBITMAPINFO) m_lpBMIH, DIB_RGB_COLORS, SRCCOPY); // 返回 return TRUE; } 6. 压缩格式转换 CDib 类中还提供了一个可进行压缩格式转换的函数 Compress。该函数可以将 DIB 重新生成为压缩 或者不压缩格式的 DIB(由参数 bCompress 控制)。在其实现过程中,它将已有的 DIB 转换为 DDB 位图, 然后生成一个新的压缩或不压缩的 DIB。另外,在从 DDB 转换到 DIB 中,需要调用 GetDIBits 两次,一 次用于计算长度,另一次才是真正载入数据。其实现代码如下: /************************************************************************* * * \函数名称: * Compress() * * \输入参数: * CDC* pDC - 设备上下文指针 * BOOL bCompress - TRUE 对应于压缩的 DIB,FALSE 对应于不压缩的 DIB * * \返回值: * BOOL - 如果成功,则返回 TRUE * * \说明: * 该函数将 DIB 重新生成为压缩或者不压缩的 DIB。在内部,它转换已有的 DIB 为 DDB 位 * 图然后生成一个新的压缩或者不压缩的 DIB。压缩仅为 4bpp 和 8bpp 的 DIB 所支持。不能 * 压缩 DIB 段 * ************************************************************************ */ BOOL CDib::Compress(CDC* pDC, BOOL bCompress /* = TRUE */) { // 判断是否为 4bpp 或者 8bpp 位图,否则,不进行压缩,返回 FALSE if((m_lpBMIH->biBitCount != 4) && (m_lpBMIH->biBitCount != 8)) return FALSE; // 如果为 DIB 段,也不能支持压缩,返回 FALSE if(m_hBitmap) return FALSE; TRACE("Compress: original palette size = %d\n", m_nColorTableEntries); // 获得设备上下文句柄 HDC hdc = pDC->GetSafeHdc(); // 将此 DIB 的调色板选入设备上下文,并保存以前的调色板句柄 第 2 章 构造 CDib 类 · 51· HPALETTE hOldPalette = ::SelectPalette(hdc, m_hPalette, FALSE); HBITMAP hBitmap; // 创建一个 DDB 位图,如果不成功,则返回 FALSE if((hBitmap = CreateBitmap(pDC)) == NULL) return FALSE; // 计算信息头加上调色板的大小尺寸,并给它们分配内存 int nSize = sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * m_nColorTableEntries; LPBITMAPINFOHEADER lpBMIH = (LPBITMAPINFOHEADER) new char[nSize]; // 将信息头和调色板拷贝到内存中 memcpy(lpBMIH, m_lpBMIH, nSize); // new header // 如果需要进行压缩,设置相应的信息,并创建压缩格式的 DIB if(bCompress) { switch (lpBMIH->biBitCount) { case 4: lpBMIH->biCompression = BI_RLE4; break; case 8: lpBMIH->biCompression = BI_RLE8; break; default: ASSERT(FALSE); } // 设置位图数据指针为 NULL,调用 GetDIBits 来得到压缩格式的 DIB 的尺寸 // 如果不能创建 DIB,则进行相应的错误处理。 if(!::GetDIBits(pDC->GetSafeHdc(), hBitmap, 0, (UINT) lpBMIH->biHeight, NULL, (LPBITMAPINFO) lpBMIH, DIB_RGB_COLORS)) { AfxMessageBox("Unable to compress this DIB"); // 删除临时变量,并释放已分配内存 ::DeleteObject(hBitmap); delete [] lpBMIH; // 重新将以前的调色板选入,并返回 FALSE ::SelectPalette(hdc, hOldPalette, FALSE); return FALSE; } // 如果位图数据为空,则进行相应的错误处理 if (lpBMIH->biSizeImage == 0) { Visual C++数字图像获取、处理及实践应用 · 52· AfxMessageBox("Driver can’t do compression"); // 删除临时变量,并释放已分配内存 ::DeleteObject(hBitmap); delete [] lpBMIH; // 重新将以前的调色板选入,并返回 FALSE ::SelectPalette(hdc, hOldPalette, FALSE); return FALSE; } // 将位图数据尺寸赋值给类的成员变量 else { m_dwSizeImage = lpBMIH->biSizeImage; } } // 如果是解压缩,进行相应的处理 else { // 设置压缩格式为不压缩 lpBMIH->biCompression = BI_RGB; // 根据位图的宽度和高度计算位图数据内存的大小 DWORD dwBytes = ((DWORD) lpBMIH->biWidth * lpBMIH->biBitCount) / 32; if(((DWORD) lpBMIH->biWidth * lpBMIH->biBitCount) % 32) { dwBytes++; } dwBytes *= 4; // 将得到位图数据的大小尺寸保存在类的成员变量中 m_dwSizeImage = dwBytes * lpBMIH->biHeight; // 将位图数据内存的大小赋值给临时的信息头中的相应的变量 lpBMIH->biSizeImage = m_dwSizeImage; } // 再次调用 GetDIBits 来生成 DIB 数据 // 分配临时存放位图数据 LPBYTE lpImage = (LPBYTE) new char[m_dwSizeImage]; // 再次调用 GetDIBits 来生成 DIB 数据,注意此时位图数据指针不为空 VERIFY(::GetDIBits(pDC->GetSafeHdc(), hBitmap, 0, (UINT) lpBMIH->biHeight, 第 2 章 构造 CDib 类 · 53· lpImage, (LPBITMAPINFO) lpBMIH, DIB_RGB_COLORS)); TRACE("dib successfully created - height = %d\n", lpBMIH->biHeight); // 压缩转换完毕,进行相应的其他处理 // 删除临时的 DDB 位图 ::DeleteObject(hBitmap); // 释放原来的 DIB 分配的内存 Empty(); // 重新设置图像信息头和图像数据内存分配状态 m_nBmihAlloc = m_nImageAlloc = crtAlloc; // 重新定位信息头和图像数据指针 m_lpBMIH = lpBMIH; m_lpImage = lpImage; // 计算图像数据尺寸,并设置 DIB 中调色板的指针 ComputeMetrics(); // 计算 DIB 中调色板的尺寸 ComputePaletteSize(m_lpBMIH->biBitCount); // 如果 DIB 中调色板存在的话,读取并创建一个 Windows 调色板 MakePalette(); // 恢复以前的调色板 ::SelectPalette(hdc, hOldPalette, FALSE); TRACE("Compress: new palette size = %d\n", m_nColorTableEntries); // 返回 return TRUE; } 7. 内存映射文件操作函数 CDib 类 中 提 供 了 4 个对内存映射文件的操作函数,其中断开内存映射文件的关联函数 DetachMapFile 函数封装在 CDib 类的 private 中,它在释放内存的函数 Empty 中调用。因此不管有无内 存映射,只需要调用 Empty 函数就可以了,Empty 函数会进行判断是否进行了内存映射。 AttachMapFile 以读模式打开内存映射文件,并将其与 CDib 对象进行关联。由于在文件使用之前还 没有读入内存,因此还需要调用 AttachMemory 将之与已有的 CDib 对象关联。注意,此函数只是建立了 关联,而并没有真正将数据读入内存。在使用文件对 CDib 操作的时候,它才将数据读入内存。其实现 代码如下: /************************************************************************* Visual C++数字图像获取、处理及实践应用 · 54· * * \函数名称: * AttachMapFile() * * \输入参数: * const char* strPathname - 映射文件的路径名 * BOOL bShare - 如果文件以共享形式打开,设置为 TRUE * - 默认值为 FALSE * * \返回值: * BOOL - 如果成功,则为 TRUE * * \说明: * 以读模式打开内存映射文件,并将其与 CDib 对象进行关联。因为在文件使用之前并没有读 * 入内存,所以它立即返回。不过,当访问这个 DIB 的时候,可能会有一些延迟,因为文件 * 是分页的。 * ************************************************************************ */ BOOL CDib::AttachMapFile(const char* strPathname, BOOL bShare) // for reading { // 获取文件句柄,并设置打开模式为共享 HANDLE hFile = ::CreateFile(strPathname, GENERIC_WRITE | GENERIC_READ, bShare ? FILE_SHARE_READ : 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); ASSERT(hFile != INVALID_HANDLE_VALUE); // 获取文件的尺寸 DWORD dwFileSize = ::GetFileSize(hFile, NULL); // 创建文件映射对象,并设置文件映射的模式为读写 HANDLE hMap = ::CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, 0, NULL); DWORD dwErr = ::GetLastError(); if(hMap == NULL) { AfxMessageBox("Empty bitmap file"); return FALSE; } // 映射整个文件,注意 FILE_MAP_WRITE 为读写模式 LPVOID lpvFile = ::MapViewOfFile(hMap, FILE_MAP_WRITE, 0, 0, 0); // map whole file ASSERT(lpvFile != NULL); if(((LPBITMAPFILEHEADER) lpvFile)->bfType != 0x4d42) { AfxMessageBox("Invalid bitmap file"); DetachMapFile(); 第 2 章 构造 CDib 类 · 55· return FALSE; } // 将内存中的 DIB 与已有的 CDib 对象关联 AttachMemory((LPBYTE) lpvFile + sizeof(BITMAPFILEHEADER)); // 将这些有用的句柄设置为类数据成员 m_lpvFile = lpvFile; m_hFile = hFile; m_hMap = hMap; // 返回 return TRUE; } 函数 AttachMemory 用内存中的 DIB 与已有的 CDib 对象关联。其实现代码如下: /************************************************************************* * * \函数名称: * AttachMemory() * * \输入参数: * LPVOID lpvMem - 要关联的内存地址 * BOOL bMustDelete - 如果 CDib 类负责删除这个内存,标记为 TRUE * - 默认值为 FALSE * HGLOBAL hGlobal - 如果内存是通过 Win32 GlobalAlloc 得到的, * - 则 CDib 对象必须保存该句柄,这样,以后 * - 可以释放句柄。这里假设 bMustDelete 设置为 TRUE * * \返回值: * BOOL - 如果成功,则为 TRUE * * \说明: * 用内存中的 DIB 与已有的 CDib 对象关联。此内存可能是程序的资源,或者可能是剪贴 * 板或者 OLE 数据对象内存。内存可能已经由 CRT 堆栈用 new 运算符分配了,或者可能已 * 经由 Windows 堆栈用 GlobalAlloc 分配了 * ************************************************************************ */ BOOL CDib::AttachMemory(LPVOID lpvMem, BOOL bMustDelete, HGLOBAL hGlobal) { // 首先释放已经分配的内存 Empty(); m_hGlobal = hGlobal; Visual C++数字图像获取、处理及实践应用 · 56· // bMustDelete 为 TRUE 表示此 CDib 类分配的内存,负责删除 // 否则的设置信息头分配状态为 noAlloc if(bMustDelete == FALSE) { m_nBmihAlloc = noAlloc; } else { m_nBmihAlloc = ((hGlobal == NULL) ? crtAlloc : heapAlloc); } try { // 设置信息头指针 m_lpBMIH = (LPBITMAPINFOHEADER) lpvMem; // 重新计算得到图像数据块的大小,并设置调色板的指针 ComputeMetrics(); // 计算调色板的尺寸 ComputePaletteSize(m_lpBMIH->biBitCount); // 设置图像数据指针 m_lpImage = (LPBYTE) m_lpvColorTable + sizeof(RGBQUAD) * m_nColorTableEntries; // 如果调色板存在的话,读取它,并创建一个 Windows 调色板, // 并将调色板的句柄存放在数据成员中 MakePalette(); } // 错误处理 catch(CException* pe) { AfxMessageBox("AttachMemory error"); pe->Delete(); return FALSE; } // 返回 return TRUE; } 函数 CopyToMapFile 将创建一个新的内存映射文件,并将现有的 DIB 数据复制到文件的内存,并 释放以前分配的所有内存,关闭现有的所有内存映射文件。其实现代码如下: /************************************************************************* * * \函数名称: * CopyToMapFile() * * \输入参数: 第 2 章 构造 CDib 类 · 57· * const char* strPathname - 映射文件的路径名 * * \返回值: * BOOL - 如果成功,则为 TRUE * * \说明: * 该函数可以创建一个新的内存映射文件,并将现有的 CDib 数据复制到该文件的内存 * 释放以前的所有内存。并关闭现有的所有内存映射文件。实际上,直到新文件 * 关闭的时候,才将这个数据写到磁盘,但是,当 CDib 对象被重复使用或被破坏 * 时,也会发生写磁盘操作 * ************************************************************************ */ BOOL CDib::CopyToMapFile(const char* strPathname) { BITMAPFILEHEADER bmfh; // 设置文件头信息 bmfh.bfType = 0x4d42; bmfh.bfSize = m_dwSizeImage + sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * m_nColorTableEntries + sizeof(BITMAPFILEHEADER); bmfh.bfReserved1 = bmfh.bfReserved2 = 0; bmfh.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * m_nColorTableEntries; // 创建接收数据的文件 HANDLE hFile = ::CreateFile(strPathname, GENERIC_WRITE | GENERIC_READ, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); ASSERT(hFile != INVALID_HANDLE_VALUE); // 计算文件的大小尺寸 int nSize = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * m_nColorTableEntries + m_dwSizeImage; // 创建内存映射文件对象 HANDLE hMap = ::CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, nSize, NULL); DWORD dwErr = ::GetLastError(); ASSERT(hMap != NULL); // 映射整个文件 LPVOID lpvFile = ::MapViewOfFile(hMap, FILE_MAP_WRITE, 0, 0, 0); ASSERT(lpvFile != NULL); // 临时文件指针 Visual C++数字图像获取、处理及实践应用 · 58· LPBYTE lpbCurrent = (LPBYTE) lpvFile; // 拷贝文件头信息到内存映射文件中 memcpy(lpbCurrent, &bmfh, sizeof(BITMAPFILEHEADER)); // 计算信息头在文件中的地址,并拷贝信息头信息 lpbCurrent += sizeof(BITMAPFILEHEADER); LPBITMAPINFOHEADER lpBMIH = (LPBITMAPINFOHEADER) lpbCurrent; memcpy(lpbCurrent, m_lpBMIH, sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * m_nColorTableEntries); // 计算调色板在文件中的地址,并拷贝调色板 lpbCurrent += sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * m_nColorTableEntries; memcpy(lpbCurrent, m_lpImage, m_dwSizeImage); // 暂时存放图像数据尺寸变量 DWORD dwSizeImage = m_dwSizeImage; // 释放一起分配的所有内存 Empty(); // 设置图像数据尺寸并设置内存分配状态 m_dwSizeImage = dwSizeImage; m_nBmihAlloc = m_nImageAlloc = noAlloc; // 信息头指针重新指向文件中的位置 m_lpBMIH = lpBMIH; // 图像数据指针重新指向文件中的数据地址 m_lpImage = lpbCurrent; // 设置文件句柄 m_hFile = hFile; // 设置映射对象句柄 m_hMap = hMap; // 设置映射文件指针 m_lpvFile = lpvFile; // 重新计算得到调色板尺寸 ComputePaletteSize(m_lpBMIH->biBitCount); // 重新计算图像数据块大小,并设置调色板指针 第 2 章 构造 CDib 类 · 59· ComputeMetrics(); // 如果调色板存在的话,读取并创建一个 Windows 调色板 MakePalette(); // 返回 return TRUE; } 函数 DetachMapFile 关闭内存映射文件。该函数只是内部的 private 成员。其实现代码如下: /************************************************************************* * * \函数名称: * DetachMapFile() * * \输入参数: * 无 * * \返回值: * 无 * * \说明: * 函数可以释放现有的已分配的内存,并关闭以前关联的任何内存映射文件 * ************************************************************************ */ void CDib::DetachMapFile() { // 如果没有进行内存映射,则不进行处理 if(m_hFile == NULL) return; // 关闭内存映射 ::UnmapViewOfFile(m_lpvFile); // 关闭内存映射对象和文件 ::CloseHandle(m_hMap); ::CloseHandle(m_hFile); m_hFile = NULL; } 8. 调色板操作函数 CDib 类中涉及了 3 个必要的调色板操作函数。其中 MakePalette 是创建调色板,SetSystemPalette 创建与系统兼容的调色板,UsePalette 则将调色板选入设备上下文中。 如果调色板存在的话,MakePalette 将创建一个新的 Windows 调色板,并将调色板的句柄存入数据 成员中。其实现代码如下: Visual C++数字图像获取、处理及实践应用 · 60· /************************************************************************* * \函数名称: * MakePalette() * * \输入参数: * 无 * * \返回值: * BOOL - 如果成功,则为 TRUE * * \说明: * 如果颜色表存在的话,该函数将读取它,并创建一个 Windows 调色板。 * HPALETTE 存储在一个数据成员中。 * ************************************************************************ */ BOOL CDib::MakePalette() { // 如果不存在调色板,则返回 FALSE if(m_nColorTableEntries == 0) return FALSE; if(m_hPalette != NULL) ::DeleteObject(m_hPalette); TRACE("CDib::MakePalette -- m_nColorTableEntries = %d\n", m_nColorTableEntries); // 给逻辑调色板分配内存 LPLOGPALETTE pLogPal = (LPLOGPALETTE) new char[2 * sizeof(WORD) + m_nColorTableEntries * sizeof(PALETTEENTRY)]; // 设置逻辑调色板的信息 pLogPal->palVersion = 0x300; pLogPal->palNumEntries = m_nColorTableEntries; // 拷贝 DIB 中的颜色表到逻辑调色板 LPRGBQUAD pDibQuad = (LPRGBQUAD) m_lpvColorTable; for(int i = 0; i < m_nColorTableEntries; i++) { pLogPal->palPalEntry[i].peRed = pDibQuad->rgbRed; pLogPal->palPalEntry[i].peGreen = pDibQuad->rgbGreen; pLogPal->palPalEntry[i].peBlue = pDibQuad->rgbBlue; pLogPal->palPalEntry[i].peFlags = 0; pDibQuad++; } // 创建逻辑调色板 m_hPalette = ::CreatePalette(pLogPal); 第 2 章 构造 CDib 类 · 61· // 删除临时变量并返回 TRUE delete pLogPal; return TRUE; } SetSystemPalette 主要是创建一个与系统兼容的调色板,以免出现在较低级的显示器上显示时,不 具备任何调色板,只有 20 种标准的 Windows 颜色出现在 DIB 上的情况。其实现代码如下; /************************************************************************* * \函数名称: * SetSystemPalette() * * \输入参数: * CDC* pDC - 设备上下文指针 * * \返回值: * BOOL - 如果成功,则为 TRUE, * * \说明: * 如果 16bpp、24bpp 或 32bppDIB 不具备调色板,则该函数可以为 CDib 对象创建一个逻辑 * 调色板,它与由 CreatehalftonePalette 函数返回的调色板相匹配。如果程序在 256 色调色板 * 显示器上运行,而又没有调用 SetSystemPalette,那么,你将不具有任何调色板,只有 20 * 种标准的 Windows 颜色出现在 DIB 中 * ************************************************************************ */ BOOL CDib::SetSystemPalette(CDC* pDC) { // 如果 DIB 不具备调色板,则需要利用系统的调色板 if(m_nColorTableEntries != 0) return FALSE; // 为设备上下文创建中间调色板,并将其与 CPalette 对象连接 m_hPalette = ::CreateHalftonePalette(pDC->GetSafeHdc()); // 返回 return TRUE; } 函数 UsePalette 将 CDib 对象的逻辑调色板选入设备上下文,并实现该调色板。在调用 UsePalette 之前,必须是使用过 MakePalette(这个函数在 Read 等输入函数中进行了调用)或 SetSystemPalette。在 进行显示之前(调用 Draw 之前)需要调用 UsePalette 将调色板选入设备上下文并实现之。其实现代码 如下: /************************************************************************* * \函数名称: * UsePalette() * Visual C++数字图像获取、处理及实践应用 · 62· * \输入参数: * CDC* pDC - 要实现调色板的设备上下文指针 * BOOL bBackground - 如果标记为 FALSE(默认值),并且应用 * - 程序正在前台运行,则 Windows 将把该调 * - 色板作为前台调色板来实现(向系统调色 * - 板中复制尽可能多的颜色)。如果标记为 * - TURE,则 Windows 将把该调色板作为后台 * - 调色板来实现(尽可能相系统调色板映射 * - 逻辑调色板) * * \返回值: * UINT - 如果成功,则返回映射到系统调色板的逻 * - 辑调色板的表项数,否则返回 GDI_ERROR * * \说明: * 该函数将 CDib 对象的逻辑调色板选入设备上下文,然后实现该调色板。Draw 成员函 * 数在绘制 DIB 之前调用 UsePalette。 * ************************************************************************ */ UINT CDib::UsePalette(CDC* pDC, BOOL bBackground /* = FALSE */) { // 判断是否存在调色板 if(m_hPalette == NULL) return 0; // 得到设备上下文句柄 HDC hdc = pDC->GetSafeHdc(); // 选择调色板到设备上下文 ::SelectPalette(hdc, m_hPalette, bBackground); // 实现该调色板 return ::RealizePalette(hdc); } 9. 其他操作函数 在 CDib 类中,还定义了 CreateBitmap 函数创建 DDB 图像,实现 DIB 到 DDB 的转换。并定义了函 数 ConvertFromDDB 创建 DIB 图像,实现了 DDB 到 DIB 的转换。CreateSection 将创建一个 DIBSection, 这样可以直接进行 GDI 操作。另外,还定义了一个重要的释放内存和关闭内存映射文件的函数 Empty(这 个函数多次被其他函数调用)。 函数 CreateBitmap 将从已有的 DIB 中创建一个 DDB 图像。其实现代码为: /************************************************************************* * * \函数名称: 第 2 章 构造 CDib 类 · 63· * CreateBitmap() * * \输入参数: * CDC* pDC - 设备上下文指针 * * \返回值: * HBITMAP - 到 GDI 位图的句柄;如果不成功,则为 NULL * - 该句柄不是作为公共数据成员存储的 * * \说明: * 从已有的 DIB 中创建 DDB 位图。不要将这个函数与 CreateSection * 弄混了,后者的作用是生成 DIB 并保存句柄 * ************************************************************************ */ HBITMAP CDib::CreateBitmap(CDC* pDC) { // 如果不存在图像数据,则返回 NULL if (m_dwSizeImage == 0) return NULL; // 用指定的 DIB 来创建 DDB,并用 DIB 信息初始化位图的图像位 HBITMAP hBitmap = ::CreateDIBitmap(pDC->GetSafeHdc(), m_lpBMIH, CBM_INIT, m_lpImage, (LPBITMAPINFO) m_lpBMIH, DIB_RGB_COLORS); ASSERT(hBitmap != NULL); // 返回 DDB 位图句柄 return hBitmap; } 函数 ConvertFromDDB 将根据 DDB 创建 DIB,并和 CDib 对象相连,实现了 DDB 到 DIB 的转换。 其实现代码如下: /************************************************************************* * \函数名称: * ConvertDDBToDIB() * * \输入参数: * HBITMAP hBitmap - 指向源数据的 BITMAP 句柄 * CDib* pDibDst - 指向转换目标的 CDib 对象指针 * * \返回值: * BOOL - 如果操作成功,则返回 TRUE * * \说明: * 该函数将源 BITMAP 类 pDibSrc 中的数据拷贝到 pDibDst 中,并对相应的数据成员赋值 * Visual C++数字图像获取、处理及实践应用 · 64· ************************************************************************* */ BOOL CDib::ConvertFromDDB(HBITMAP hBitmap, HPALETTE hPal) { // 声明一个 BITMAP 结构 BITMAP bm; // 设备上下文 HDC hDC; // 像素位数 WORD biBitCount; // 调色板表项数 int nColorTableEntries; // 如果 hBitmap 句柄无效,则返回 if(!hBitmap){ return FALSE; } // 释放已分配的内存 Empty(); // 填充图像数据到 bm 中,其中最后一个参数表示接收这个指定的对象的指针 if(!GetObject(hBitmap,sizeof(BITMAP),(LPBYTE)&bm)){ return FALSE; } // 计算像素位数 biBitCount=bm.bmPlanes*bm.bmBitsPixel; if(biBitCount<=1) biBitCount=1; else if(biBitCount<=4) biBitCount=4; else if(biBitCount<=8) biBitCount=8; else biBitCount=24; // 计算调色板的尺寸 // 如果 biClrUsed 为零,则用到的颜色数为 2 的 biBitCount 次方 switch(biBitCount) { case 1: 第 2 章 构造 CDib 类 · 65· nColorTableEntries = 2; break; case 4: nColorTableEntries = 16; break; case 8: nColorTableEntries = 256; break; case 16: case 24: case 32: nColorTableEntries = 0; break; default: ASSERT(FALSE); } ASSERT((nColorTableEntries >= 0) && (nColorTableEntries <= 256)); m_nColorTableEntries = nColorTableEntries; // 分配 DIB 信息头和调色板的内存 m_lpBMIH = (LPBITMAPINFOHEADER) new char [sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * nColorTableEntries]; m_nBmihAlloc = m_nImageAlloc = crtAlloc; m_lpBMIH->biSize = sizeof(BITMAPINFOHEADER); m_lpBMIH->biWidth = bm.bmWidth; m_lpBMIH->biHeight = bm.bmHeight; m_lpBMIH->biPlanes = 1; m_lpBMIH->biBitCount = biBitCount; m_lpBMIH->biCompression = BI_RGB; m_lpBMIH->biSizeImage = 0; m_lpBMIH->biXPelsPerMeter = 0; m_lpBMIH->biYPelsPerMeter = 0; m_lpBMIH->biClrUsed = nColorTableEntries; m_lpBMIH->biClrImportant = nColorTableEntries; // 获得设备上下文句柄 hDC=GetDC(NULL); // 如果没有指定调色板,则从系统中获得当前的系统调色板 if(hPal==NULL){ hPal = GetSystemPalette(); } hPal = SelectPalette(hDC, hPal, FALSE); RealizePalette(hDC); Visual C++数字图像获取、处理及实践应用 · 66· // 调用 GetDIBits 填充信息头,并获得图像数据的尺寸。注意这里图像数据指针为 NULL GetDIBits( hDC, hBitmap, 0, (UINT)m_lpBMIH->biHeight, NULL, (LPBITMAPINFO) m_lpBMIH, DIB_RGB_COLORS); // 如果没有正确的获得图像数据尺寸,则重新计算 if( m_lpBMIH->biSizeImage == 0 ){ m_lpBMIH->biSizeImage=(((bm.bmWidth*biBitCount) + 31) / 32 * 4)*bm.bmHeight; } // 分配存放图像数据的内存 m_lpImage = (LPBYTE) new char[m_lpBMIH->biSizeImage]; // 调用 GetDIBits 加载图像数据,注意这里给出了图像数据指针 // 如果加载图像数据不成功,则释放已经分配的内存,并返回 FALSE if( GetDIBits( hDC, hBitmap, 0, (UINT)m_lpBMIH->biHeight, (LPBYTE)m_lpImage, (LPBITMAPINFO)m_lpBMIH, DIB_RGB_COLORS) == 0 ){ //clean up and return NULL Empty(); SelectPalette( hDC, hPal, TRUE ); RealizePalette( hDC ); ReleaseDC( NULL, hDC ); return FALSE; } // 删除临时变量 SelectPalette(hDC, hPal, TRUE); RealizePalette(hDC); ReleaseDC(NULL, hDC); return TRUE; } 函数 CreateSection 将创建一个 DIBSection,并得到一个 DDB 位图句柄。在这个基础上,加上内存 设备上下文,就可以调用 GDI 函数在 DIB 中画图。其实现代码为: /************************************************************************* * * \函数名称: * CreateSection() * * \输入参数: * CDC* pDC - 设备上下文指针 * 第 2 章 构造 CDib 类 · 67· * \返回值: * HBITMAP - 到 GDI 位图的句柄。如果不成功,则为 NULL。 * - 该句柄也是作为公共数据成员存储的 * * \说明: * 通过调用 Win32 SDK 的 CreateDIBSection 函数创建一个 DIB 段。图像内存将不被初始化 * ************************************************************************ */ HBITMAP CDib::CreateSection(CDC* pDC /* = NULL */) { // 如果信息头为空,不作任何处理 if(m_lpBMIH == NULL) return NULL; // 如果图像数据不存在,不作任何处理 if(m_lpImage != NULL) return NULL; // 创建一个 DIB 段 m_hBitmap = ::CreateDIBSection(pDC->GetSafeHdc(), (LPBITMAPINFO) m_lpBMIH, DIB_RGB_COLORS, (LPVOID*) &m_lpImage, NULL, 0); ASSERT(m_lpImage != NULL); // 返回 HBIMAP 句柄 return m_hBitmap; } 函数 Empty 将释放所有已经分配的内存,并关闭内存映射文件。其实现代码为: /************************************************************************* * \函数名称: * Empty() * * \输入参数: * 无 * * \返回值: * 无 * * \说明: * 该函数清空 DIB,释放已分配的内存,并且必要的时候关闭映射文件 * ************************************************************************ */ void CDib::Empty() { // 关闭内存映射文件的连接 Visual C++数字图像获取、处理及实践应用 · 68· DetachMapFile(); // 根据内存分配的状态,调用相应的函数释放信息头 if(m_nBmihAlloc == crtAlloc) { delete [] m_lpBMIH; } else if(m_nBmihAlloc == heapAlloc) { ::GlobalUnlock(m_hGlobal); ::GlobalFree(m_hGlobal); } // 释放图像数据内存 if(m_nImageAlloc == crtAlloc) delete [] m_lpImage; // 释放调色板句柄 if(m_hPalette != NULL) ::DeleteObject(m_hPalette); // 如果创建了 BITMAP,则进行释放 if(m_hBitmap != NULL) ::DeleteObject(m_hBitmap); // 重新设置内存分配状态 m_nBmihAlloc = m_nImageAlloc = noAlloc; // 释放内存后,还需要将指针设置为 NULL 并将相应的数据设置为 0 m_hGlobal = NULL; m_lpBMIH = NULL; m_lpImage = NULL; m_lpvColorTable = NULL; m_nColorTableEntries = 0; m_dwSizeImage = 0; m_lpvFile = NULL; m_hMap = NULL; m_hFile = NULL; m_hBitmap = NULL; m_hPalette = NULL; } 2.2 基于 CDib 类的其他操作函数 在定义了 CDib 类后,就可以方便地对 DIB 进行操作了。下面定义了几个在编程中常用的 DIB 的操 作函数。这些函数由于种种原因没有封装到到 CDib 类中,如拷贝 DIB,拷贝屏幕中指定区域到 DIB 中。 函数 CopyDIB 实现了 CDib 对象数据的拷贝,并对相关的数据成员进行复制。在参数传入时,需要 先声明一个 CDib 对象。其实现代码如下: 第 2 章 构造 CDib 类 · 69· /************************************************************************* * \函数名称: * CopyDIB() * * \输入参数: * CDib* pDibSrc - 指向源数据的 CDib 对象指针 * CDib* pDibDst - 指向拷贝目标的 CDib 对象指针 * * \返回值: * BOOL - 如果操作成功,则返回 TRUE * * \说明: * 该函数将源 CDib 类 pDibSrc 中的数据拷贝到 pDibDst 中,并对相应的数据成员赋值 * ************************************************************************* */ BOOL CopyDIB(CDib* pDibSrc, CDib* pDibDst) { // 将目的 CDib 对象清空 pDibDst->Empty(); // 计算信息头加上调色板的大小,并分配相应的内存 int nSizeHdr = sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * pDibSrc->m_nColorTableEntries; pDibDst->m_lpBMIH = (LPBITMAPINFOHEADER) new char[nSizeHdr]; pDibDst->m_nBmihAlloc = pDibDst->m_nImageAlloc = pDibDst->crtAlloc; try{ // 拷贝信息头和调色板 memcpy(pDibDst->m_lpBMIH,pDibSrc->m_lpBMIH,nSizeHdr); // 如果结构的长度不对,则进行错误处理 if(pDibDst->m_lpBMIH->biSize != sizeof(BITMAPINFOHEADER)) { TRACE("Not a valid Windows bitmap -- probably an OS/2 bitmap\n"); throw new CException; } // 保存图像数据内存大小到 CDib 对象的数据成员中 pDibDst->m_dwSizeImage = pDibDst->m_lpBMIH->biSizeImage; // 如果图像数据内存大小为 0,则重新计算 if(pDibDst->m_dwSizeImage == 0) { DWORD dwBytes = ((DWORD) pDibDst->m_lpBMIH->biWidth Visual C++数字图像获取、处理及实践应用 · 70· * pDibDst->m_lpBMIH->biBitCount) / 32; if(((DWORD) pDibDst->m_lpBMIH->biWidth * pDibDst->m_lpBMIH->biBitCount) % 32) { dwBytes++; } dwBytes *= 4; pDibDst->m_dwSizeImage = dwBytes * pDibDst->m_lpBMIH->biHeight; } // 设置 DIB 中的调色板指针 pDibDst->m_lpvColorTable = (LPBYTE) pDibDst->m_lpBMIH + sizeof(BITMAPINFOHEADER); // 计算调色板的表项数 pDibDst->ComputePaletteSize(pDibDst->m_lpBMIH->biBitCount); // 如果 DIB 中存在调色板,则创建一个 Windows 调色板 pDibDst->MakePalette(); // 分配图像数据内存,并拷贝图像数据 pDibDst->m_lpImage = (LPBYTE) new char[pDibDst->m_dwSizeImage]; memcpy(pDibDst->m_lpImage, pDibSrc->m_lpImage,pDibDst->m_dwSizeImage); } catch(CException* pe) { AfxMessageBox("Copy DIB error"); pDibDst->Empty(); pe->Delete(); return FALSE; } return TRUE; } 函数 CopyScreenToDIB 可以获取屏幕中指定区域的图像并存入 CDib 对象中。在调用函数之前,需 要先声明一个 CDib 对象,以便存放图像数据。其实现代码如下: /************************************************************************* * \函数名称: * CopyScreenToDIB * * \输入参数: * LPRECT lpRect - 需要拷贝的屏幕区域 * CDib* pDibDest - 指向目标 CDib 对象的指针 * 第 2 章 构造 CDib 类 · 71· * \返回值: * BOOL - 如果操作成功,则返回 TRUE * \说明: * 该函数将指定矩形位置内的屏幕内容拷贝到 DIB 中源 CDib 类 pDibSrc 中 * 的数据拷贝到 pDibDst 中 * ************************************************************************* */ BOOL CopyScreenToDIB(LPRECT lpRect, CDib* pDibDest) { // 屏幕设备上下文和内存设备上下文句柄 HDC hScrDC, hMemDC; // 声明 BITMAP 临时句柄和以前的 BITMAP 句柄 HBITMAP hBitmap, hOldBitmap; // 调色板句柄 HPALETTE hPalette; // 获取矩形区域的坐标 int nX, nY, nX2, nY2; // DIB 图像的高度和宽度 int nWidth, nHeight; // 屏幕分辨率 int xScrn, yScrn; // 如果给定的矩形区域为空,则不进行进一步的处理 if (IsRectEmpty(lpRect)) return FALSE; // 得到一个屏幕设备上下文 hScrDC = CreateDC("DISPLAY", NULL, NULL, NULL); // 创建与屏幕设备兼容的内存设备上下文 hMemDC = CreateCompatibleDC(hScrDC); // 得到矩形的区域坐标 nX = lpRect->left; nY = lpRect->top; nX2 = lpRect->right; nY2 = lpRect->bottom; // 得到屏幕的分辨率,以便后面的判断处理 Visual C++数字图像获取、处理及实践应用 · 72· xScrn = GetDeviceCaps(hScrDC, HORZRES); yScrn = GetDeviceCaps(hScrDC, VERTRES); // 判断矩形区域是否超出屏幕 if (nX < 0) nX = 0; if (nY < 0) nY = 0; if (nX2 > xScrn) nX2 = xScrn; if (nY2 > yScrn) nY2 = yScrn; // 计算 DIB 图像的高度和宽度 nWidth = nX2 - nX; nHeight = nY2 - nY; // 创建一个与屏幕设备上下文兼容的 DDB 位图 hBitmap = CreateCompatibleBitmap(hScrDC, nWidth, nHeight); // 将 DDB 位图选入内存设备上下文 hOldBitmap = (HBITMAP)SelectObject(hMemDC, hBitmap); // 将屏幕中指定区域的图像传输到内存设备上下文中 BitBlt(hMemDC, 0, 0, nWidth, nHeight, hScrDC, nX, nY, SRCCOPY); // 然后将以前的图像选入,并得到屏幕区域的 DDB 图像句柄 hBitmap = (HBITMAP)SelectObject(hMemDC, hOldBitmap); // 将临时的设备上下文删除 DeleteDC(hScrDC); DeleteDC(hMemDC); // 得到当前系统调色板 hPalette = GetSystemPalette(); // 将 DDB 图像转换为 DIB 图像 pDibDest->ConvertFromDDB(hBitmap,hPalette); // 删除临时对象 DeleteObject(hPalette); DeleteObject(hBitmap); return TRUE; } 第第 33 章章 图图像像感感知知与与获获取取 从广义上说,图像是自然界景物的客观反映。以照片形式保存的图像是连续的。由于计算机只能处 理数字图像,而自然界提供的图像却是其他形式的,所以数字图像处理的一个先决条件就是将图像转化 为数字形式。图像数字化就是将连续图像离散化,其工作包括两个方面:取样和量化。 所谓取样,就是把一幅连续图像在空间上分割成 M×N 个网格,每个网格用一亮度值来表示。由于 结果是一个样点值阵列,故又叫点阵取样。 取样使连续图像在空间上离散化,但取样点上图像的亮度值还是某个幅度区间内的连续分布。根据 取样定义,每个网格上只能用一个确定的亮度值表示。把取样点上对应的亮度连续变化区间转换为单个 特定数码的过程,称之为量化,即样点亮度的离散化。 在取样和量化过程中,取样密度取多大合适?以多少个等级表示样本的亮度值为最好?这些都将影响 到离散图像能否保持连续图像信息的问题。原则上,M×N 的取值主要取决于其是否满足取样定理,因为 在满足取样定理的情况下,重建图像就不会产生失真。灰度级的确定将根据图像的内容和要求来考虑。 在数字图像处理的发展初期,图像数字化的设备非常昂贵和复杂,只有很少数的研究中心能够负担 得起。但随着技术的进步,现在这些设备已比较便宜而且被广泛应用了。在本章中,笔者在探讨视觉和 视觉特性的基础上,对图像的采集、量化和表示进行介绍。 3.1 视觉基础 3.1.1 视觉系统 光线照在物体上其透射或反射光的分布就是“图”,形成的印象或认识就是“像”。前者是客观存在, 后者是人的感觉,图像就是二者的结合。如图 3-1 所示是人眼构造的截面示意图。 图 3-1 人眼截面示意 视觉系统从外界获取图像,就是在眼睛视网膜上获得周围世界的光学成像,然后由视觉接收器(杆Visual C++数字图像获取、处理及实践应用 · 74· 状体和锥状体在视网膜上作为视觉接收器)将光图像信息转化为视网膜的神经活动电信息,最后通过视 神经纤维,把这些图像信息传送入大脑,由大脑获得图像感知。 视网膜上有杆状体和锥状体两类视觉接收器: 视杆体(Rods):细长而薄,数量约 1 亿个左右,它们提供暗视(Scotopic Vision),即在最低几个 数量级亮度时的视觉响应,其光灵敏度高。 视锥体(Cons):结构上短而粗,数量少,约 6 百万左右,光灵敏度较低,它们提供明视(Photopic Vision),其响应光亮度范围比视杆体要高 5~6 个数量级。中间亮度范围是两种视觉细胞同时起作用。 视锥体集中分布在视网膜中心。 光图像激活视杆体或视锥体时,发生光电化学反应,同时产生视神经脉冲。视觉系统的可视波长范 围为 350nm~780nm。 视网膜中有 3 种视锥体,具有不同的光谱特性,峰值吸收分别在光谱的红、绿、蓝区域,从实测光 谱吸收曲线可以看到视锥体主要对蓝光响应,灵敏度相对较低一些。而且,吸收曲线有相当多的部分是 相互重叠的。这是三基色原理的生理基础。 大脑对视神经纤维传送来的图像信息进行分析和理解,通过图像获得对周围世界感知的信息和知 识。因此,所谓视知觉,也就是视觉思维。它不是对刺激物的简单复制,而是一种积极的理性活动。 人眼对不同波长的光有不同的敏感度,波长不同而幅射功率相同的光不仅给人以不同的色彩感觉, 而且亮度感觉也不同。人眼对具有光幅射 ( ,,)I x y λ 分布物体的亮度感觉为: 0 ( , ) ( , , ) ( )df x y I x y Vλ λ λ∞= ∫ (0-1) 式中的 ( )V λ 为视觉系统的相对视敏函数。对于人眼,是钟形曲线,如图 3-2 所示。 图 3-2 人眼相对视敏感函数 视杆体和视锥体的相对视敏曲线有所不同。视锥体在 λ =555nm 时对绿光亮度最敏感,对视杆体暗 视情况,则在 λ =505nm 时最敏感。 物体和它周围亮度的交互作用,产生一种称为马赫带的效应,这个效应说明视觉的明亮程度并不是 亮度的单调函数。例如,灰阶条带图像呈现的明亮视觉感觉沿着条带是不均匀的,在条带过渡部分具有 负轮廓的边缘。图 3-3 简单说明了马赫带效应。 第 3 章 图像感知与获取 · 75· 图 3-3 马赫带效应 3.1.2 视觉模型 将视觉系统的功能抽象化为简单的模型,以此模型为基础可以对视觉系统的功能进行研究或加以模 仿。模型有电子线路模型以及化学模型等,对我们来说,一般是对电子线路模型感兴趣,这样可以把视 觉系统的一些优越性引入图像通信和信息处理系统中加以研究和应用。 (一)黑白视觉模型 把眼睛产生视觉过程简化成如图 3-4 所示的线性光学系统。 输入 光学系统 输出 (,)G u v (,)H u v (,)G u v 图 3-4 线性光学系统 设 (,)F u v 为输入图像 (,)f x y 的傅立叶变换, (,)G u v 为输出图像 (,)g x y 的傅立叶变换。根据 线性系统理论可以得到: (,)(,)(,)G u v F u v H u v= (0-2) 式中的 (,)H u v 称作该光学系统的传递函数 MTF。 为了把光学系统的概念用到人的视觉系统方面,人们已经作了很多研究。从实验结果可以看出, MTF 测试结果与输入对比度的大小有关,而且如果输入正弦光栅相对于眼球光轴进行旋转时,则测得的 MTF 形状也有某些改变。于是可以推出:人的视觉系统 MTF 是非线性和各向异性的(即旋转可变的)。 另外,根据一些人的实际测试结果认为眼睛对光强的非线性响应为对数型,并且发生在视觉系统的开始 附近(也就是视觉信号在锥状及杆状细胞空间上发生相互作用之前)。 由以上的分析可以得出如图 3-5 所示的人眼黑白视觉简单对数模型。 Visual C++数字图像获取、处理及实践应用 · 76· 神经信号 光接收器 对数 线性系统 图 3-5 人眼黑白视觉简单对数模型 (二)彩色模型 根据三色假说为基础来研究彩色视觉模型,图 3-6 是 Frei 建议的彩色模型。在该模型中, 1e 、 2e 、 3e 代表视网膜上 3 个具有 1()s λ 、 2 ()s λ 、 3 ()s λ 谱灵敏度的感受器,其输出分别为: 1 1 2 2 3 3 ( ) ( )d ( ) ( )d ( ) ( )d e c S e c S e c S λ λ λ λ λ λ λ λ λ = = = ()c λ 为入射光源的谱线分布函数, 1e 、 2e 、 3e 经对数传递后并组合为 1 2 3d d d、、 输出: 1 1 2 2 2 1 1 3 1 3 1 1 lg( ) lg( ) lg( ) lg lg( ) lg( ) lg d e ed e e e ed e e e = = − = = − = 最后,信号 1 2 3d d d、、 分别经传递函数的线性系统输出 1 2 3g g g、、。 图 3-6 彩色视觉模型 信号 d2 和 d3 与彩色光的色度有关,而 d1 则正比于它的亮度。这个模型相当准确地预测许多彩色 现象,也能满足色度学的基本定律。 Á Á第 3 章 图像感知与获取 · 77· Á 3.2 图像获取 图像获取也就是图像的数字化过程,即将图像采集到计算机中的过程,主要涉及成象及模数转换 (A/D Converter)技术。随着计算机与微电子特别是固体成象设备(电耦合设备 CCD)的快速发展,使 得图像获取设备的成本显著降低,因而越来越普及。 以 CCD 技术为核心,目前图像获取设备有黑白摄像机、彩色摄像机、扫描仪、数字相机等,性能 与价格主要取决于 CCD 的规格,如尺寸等。除了这些常见的类型外,目前有许多厂商提供各种其他的 专用设备,如显微摄像设备、红外摄像机、高速摄像机、胶片扫描器等等。此外,遥感卫星、激光雷达 等设备提供其他类型的数字图像。 目前,图像的数字化设备可分为两类,一类是使用图像采集卡或通过图像卡将模拟制式的视频信号 (RS170/CCIR 黑白电视信号,PAL/NTSC 彩色电视信号,S-Video 视频信号等)采集到计算机,另一类是 摄像机本身带有数字化部件可以直接将数字图像通过计算机端口(如并口、USB 接口)或标准设备(如 磁盘驱动器)传送进计算机。 图像卡仍是目前专业中常用的图像数字化设备,目前低端的图像采集卡一般不具有图像帧存体而是 直接将图像采集到计算机的内存中以供处理,如加拿大 Matrox 公司的 Metero 系列,高端的图像卡是集 采集和处理于一身的昂贵的非标准配件 Matrox 公司的 Genesis 图像卡,具有帧存体和数字信号处理器 DSP 及邻域处理加速器 NOA,用于开发高速或实时处理应用。此外,还有一类普及型的多媒体视频采集 卡,如宝狮 Boser602,主要用于视频会议、视频邮件等应用。最后,还应提到的是一类多媒体应用中使 用的压缩卡,如 AV8 Mpeg 压缩卡,可以将视频压缩成 Mpeg-I 格式,主要用于 VCD 制作和视觉保安系 统中,当然也具有图像采集功能。 下面以 CCD 成像设备为例,介绍图像的采集。 CCD 的全称为 Charge Coupled Devices。它的主要部分为位于单个集成电路芯片上的一组线性或矩 形光传感器阵列,并包含能读出由入射图像产生的充电电荷的必要电路。 CCD 被制造在一片光敏结晶硅片上。一个矩形的光电探测器(势阱)阵列被制造在硅片中。在局 部区域中产生的光电子被受容在最近的势阱中,并作为一个电荷包沿一串势阱移动直到到达外部引出端。 可以用 3 种不同的结构读出 CCD 图像检取器件的累积电荷:经典结构(又叫全帧结构),行间传送 结构和帧传送结构。其具体结构如图 3-7 所示。 像素移动 像素移动 像素移动 全帧 CCD 行间传送 CCD 帧传送 CCD 全帧阵列 行 移动 行 移动 行 移动 串行寄存器 串行存储器 串行存储器 图像阵列 掩膜存储阵列 图 3-7 CCD 器件结构 Visual C++数字图像获取、处理及实践应用 · 78· 全帧 CCD:在曝光之后,全帧 CCD 必须在读出过程中关闭快门以使其保持黑暗。然后它把传感器 的底行电荷移出,每次移动一个像素。当底行被移空后,所有行的电荷被向下移动—行,然后底行被移 出。这个过程被重复直到最后顶行被移到底行,并被移出。然后该器件准备累积另—幅图像。 行间传送 CCD:在行间传送 CCD 中,传感器的每个偶数列被一个不透明的掩膜覆盖着,这些被掩 盖的势阱构成的列仅在读出过程中使用。曝光后,每—个曝光的势阱中的电荷包被移动到相邻的掩膜阱 中。因为所有的电荷组一起被移动,这种传送仅需很少时间。当暴露的势阱在累积下一幅图像时,掩膜 阱中的电荷被移下和移出,就像经典 CCD 那样。在这种类型的传感器中,芯片上每列的像素数是每列 实际阱数的—半。只有不超过百分之五十的芯片面积是光敏的,因为被掩盖的列占据了表面的一半。 帧传送 CCD:帧传送 CCD 芯片有一个双倍高度的传感器阵列。上面的—半以标准方式获取图像。 下面的一半是存储陈列,它被不透明的掩膜盖着,以防止接触入射光线。在累积期结束时,传感器阵列 中累积的整个电荷图像被一行行地快速移入存储阵列。当传感器阵列在累积下一幅图像时,存储阵列中 的图像按标准方式逐个像素地被移出。与行间传送一样,这种技术同时累积电核和传出图像,使以视颇 速度的图像获取成为可能。 CCD 可以按不同的方式配置构成一系列可以应用于电视和图像数字化目的的小型而稳定的固态摄 像机。这类摄像机每有几何畸变,而且对光的响应是高度线性的。CCD 可以作为多种图像传感应用的可 选设备。 3.3 图像采样 数字图像处理的对象是由连续信号取样和量化后的数字图像信号,原始的数字图像经过图像处理 后,获得的数字图像阵列将被重构为可供观测的连续信号。 连续图像信号取样是将其在空间的离散化,量化是将离散化后图像信号在幅度上离散化。因此,取 样和量化后的数字信号应尽可能地代表原始的连续图像信号,且能够使取样后的离散图像信号无失真地 恢复原始信号。 在设计和分析图像抽样系统和重建系统时,一般认为输入图像是确定性的场。然而在某些情况下, 将图像处理系统的输入,特别是噪声输入,看成是二维随机过程的样本会更有效。下面将用这两种观点 分析图像的抽样和重建方法。 3.3.1 确定性图像场抽样 令 ( ,)f x y 为一个有限带宽的二为连续函数, ( ,)f x y 的傅立叶变换对为: ( ,)(,)f x y F u v 图 3-8 频域能量示意 (,)F x y 具有以下特点:当 u,v 方向上的带宽分别为 uL 和 vL 时, (,)f x y 的能量集中在半径为 uL第 3 章 图像感知与获取 · 79· 或 vL 所包含的区域内(图 3-8)。 在完善的图像拍样系统中,理想图像的空间样本实际上是用空间抽样函数 (,)s x y 与理想图像相乘 的结果。二维取样函数 (,)s x y 可以表示为: (,)(,) i j s x y x i x y j yδ +∞ +∞ =−∞ =−∞ = − ∆ − ∆∑ ∑ (0-3) 式中, x∆ 、 y∆ 为 X、Y 方向的取样间隔。3-3 式是脉冲函数 (,)x yδ 在 X、Y 方向以 x∆ 、 y∆ 为间隔 的展开。图 3-9 是脉冲函数阵列的示意图。 图 3-9 脉冲函数阵列 抽样的过程可以看作连续图形和抽样函数的相乘,用下式进行表示: (,)(,)(,) (,)(,) (,)(,) I i j i j f x y f x y s x y f x y x i x y i y fixjy xixy jy δ δ +∞ +∞ =−∞ =−∞ +∞ +∞ =−∞ =−∞ = = − ∆ − ∆ = ∆ ∆ − ∆ − ∆ ∑ ∑ ∑ ∑ (0-4) 取样函数 (,)s x y 的傅立叶变换为: i j 2 i j 1 1 1S(u,v) (ui ,yj )x y x y 4 (u i u, y j v)x y +∞ +∞ =−∞ =−∞ +∞ +∞ =−∞ =−∞ = δ − −∆ ∆ ∆ ∆ π= δ − ∆ − ∆∆ ∆ ∑ ∑ ∑ ∑ (0-5) 根据傅立叶变换的性质,时域上的相乘相当于频域上进行卷积,所以式 3-4 的频域变换为: I 2 1F(u,v) F(u,v) S(u,v)4 = ∗π (0-6) 将 3-5 代入到 3-6 式中,展开卷积式并进一步整理,有: Visual C++数字图像获取、处理及实践应用 · 80· ( , ) 1 1(,)(,) 1 (,) I i j i j F u vF u v u i y jx y x y F u i u y j vx y δ +∞ +∞ =−∞ =−∞ +∞ +∞ =−∞ =−∞ = − −∆ ∆ ∆ ∆ = − ∆ − ∆∆ ∆ ∑ ∑ ∑ ∑ (0-7) 由上式不难看出,取样图像 (,)If x y 的频谱是连续图像谱在 (,)u v 方向上以一定间隔的分布。当 x∆ 、 y∆ 选择适当。使得 u∆ 、 v∆ 大于或等于原图像覆盖频率间隔 uL 、 vL 两倍时。取样就不会出现 重叠现象(见图 3-10),从而可以获得期望的取样点阵。 图 3-10 取样频谱图 从上面的分析可以得到,要保证正确取样,必须满足以下条件: 2 2 2 2 u u u v u L u L x L y L ∆ ≥ ∆ ≥ ∆ ≤ ∆ ≤ (0-8) 3.3.2 随机图像取样 图像场的另一种模型是随机场。随机图像场取样后其功率谱如何分布呢?下面通过分析作一些简单 介绍。 令 (,)f x y 是连续平稳的二维随机图像场,其自相关函数为: 1 2 1 2 1 2 1 2(,)(,)(,)fRxxy y Efxxfxx∗− − = (0-9) f ∗ 为 f 的共轭。用脉冲函数 (,)x yδ 对 (,)f x y 进行抽样,所得的图像场为 (,)sf x y : (,)(,)(,)sf x y f x y s x y= 由式 3-3 可得: 第 3 章 图像感知与获取 · 81· (,)(,)(,)s i j fxy fxy xixyiyδ +∞ +∞ =−∞ =−∞ = − ∆ − ∆∑ ∑ (0-10) (,)sf x y 的自相关函数 1 2 1 2(,)fsR x x y y− − 的表达式如下: { }1 2 1 2 1 1 2 2(,)(,)(,)fs s sRxxyy Efxyfxy∗− − = (0-11) 结合 3-3 式,有: { }1 2 1 2 1 1 1 1 2 2 1 2 1 2 1 2 1 2 (,)(,)(,)(,) (,)(,) fs s s f Rxxy y Efxysxyfxy R x x y y s x x y y ∗− − = = − − − − ⋅ ⋅ (0-12) 若用 1 2 1 2,x yx x y yτ τ= − = − 进行变量替换,式 3-12 可以表示成: (,)(,)(,)fs x y f x y x yR R sτ τ τ τ τ τ= ⋅ (0-13) 上式右边第一项为随机图像场的自相关函数,第二项是间隔为 x τ , y τ 的取样函数。可见,取样图像场 也是平稳的随机过程。对式 3-13 式两边作傅立叶变换,由傅立叶变换的频域卷积性质可得: 24 (,)fs f i j P P u i u y j vx y π +∞ +∞ =−∞ =−∞ = − ∆ − ∆∆ ∆ ∑ ∑ (0-14) fsP 、 fP 分别为取样图像和原图像的功率谱密度。同固定场类似,随机图像场的功率谱密度也是 以 u∆ 、 v∆ 为间隔在空间上的重复所组成。只要满足式 3-8 所示的条件,取样矩阵中的各阵元就不会 产生交叠。 实际上有附加噪声的确定性像场可以被模拟为随机场。这样上述结果便可以直接应用到这类图像过 行抽样的实际问题中。图 3-11 给出有噪抽样图像的频谱,它指出了一个潜在的重要问题:噪声频谱可以 比理想图像频谱宽,只要噪声过程是欠抽样的,抽样后噪声频谱的尾部就会交叠在图像重建滤波器的通 带内,从而引入附加的噪声误差。自然,解决这个问题的办法是在抽样之前对有噪图像进行滤波,以减 小噪声带宽。 信号频谱 噪声频谱 抽样信号频谱 抽样噪声频谱 图 3-11 抽样有噪声图像的频谱 Á ÁVisual C++数字图像获取、处理及实践应用 · 82· Á 3.4 量化 任何模拟量要由计算机进行处理,就必须表示为与其幅度成比例的整数。模拟样本向离散值样本的 转换过程称为量化。取样可以要求无失真的重构,而量化必然带来不可恢复的量化失真。好的量化器设 计只能要求尽可能小的量化失真。 量化可以分成标量量化和向量量化两种方法。下面我们将分别介绍量化过程及其解析方法。 图 3-12 所示是标量信号量化的一个典型例子。在量化过程中,模拟信号样本的幅度与一组判决电平 相比较,若样本幅度落在两个判决电平之间,则该样本便被量化到两个判决电平内相对应的重构值。 重构电平 判决电平 图 3-12 标量量化 为了定量研究标量信号的量化技术,令 f 和 ˆf 分别代表实际标量信号样本的幅度和该幅度的量化 值。设 f 是随机过程的样本,其相应的概率密度为 P(f)。 f 的取值范围为[,]l ht t 。 设将 f 量化成 L 层,每一层相应的判决电平和重构电平分别为 kt 和 kr 。即如果 1k kt f t +≤ < , f 对应的量化值为 kr 。 选择判决电平和重构电平时应使 f 和 ˆf 之间计量的某种有效量化误差测度为最小。通常选用的量 化误差测度是均方误差测度。在均方误差判决准则下,量化的均方误差为: ()() () Á Á Á 2 2 1 2 1 ( )d ( )d Á   t ft L t ftk E f f f f p f f f f p f f ε + + + =  = − = −   = − ∫ ∑∫    (0-15) 均方误差最小化的条件是: ()()()()1 0k k k k k f k k t r pf t t r p tt ε − ∂ = − − − =∂ 第 3 章 图像感知与获取 · 83· ()Á22 ( )d 0Á Á t k ft k f r p f fr ε +∂ = − =∂ ∫ (0-16) 利用 1k kt t +< ,简化上述方程可得: [] Á Á 1 ( )d | ( )d Á Á Á Á t ft k k kt ft f p f f r E u t u t p f f + + + ⋅ = = ≤ < ∫ ∫ (0-17) 上述为非线性方程,可以利用牛顿迭代法解上式,得到 k 不同时的 kt 和 kr 。当量化级数很大时,可 以将概率密度近似为分段不变的函数,即 1()()f j f j j j jp f p t t f t += ≤ <, ,如图 3-13 所示。 () Áp f f 图 3-13 概率密度的近似表示 利用这一近似,进行所需的最小化计算,获得判决电平的解为: ÁÁ Á  1 1 11 1 [ ( )] d [ ( )] d Áz t f k L f A p f f t t p f f + − + + − = +∫ ∫ (0-18) 式中 1 1( ( / ) 1,2 ,l kA t t z k L A k L+= − = = ), , 。 A 称为动态范围。一旦转换电平 kt 确定,重建电平 kr 很容易地由 1 1 ()2k k kr t t += + 确定。量化器 的均方误差为: { }Á  1 2 1 1 [ ( )] d12 L fp f fL ε + −= ∫ (0-19) 两种常用的图像数据量化的密度函数是高斯(Gaussian)和拉普拉斯(Laplacian)概率分布,它们 的概率密度函数为: Visual C++数字图像获取、处理及实践应用 · 84· () 2 22 1 ( )Gaussian: ( ) exp 22 Laplacian: ( ) exp | |2 f f f up f p f f u δπδ α α  − −=    = − − (0-20) 其中, u 和 δ 分别表示随机变量 f 的均值和方差,在 Laplacian 分布中方差为 2 2σ α= 。 上述的最优均方误差量化器也称作 Lloyd-Max 量化器。 对于均匀概率分布情况,均方量化变成线性,转换电平和重构电平将是等间隔的,即为线性量化器。 f 的概率分布为: 1 1 1 1 1 () 0 L Lf t f tt tp f + +  ≤ ≤ −=   其余取值 (0-21) 代入式 3-17 和 3-18 中可以解出: 1 1k k k kt t t t const q+ −− = − = = (0-22) 式中 1 1Lt tq L + −= ,重建电平为: 1 2 k k k t tr + −= (0-23) 量化间隔和重构间隔是相同的,量化误差 e f f= −  在 ( )/ 2 / 2,q q− 之间均匀分布,因此均方误差为: 2/ 2 2 / 2 1 12 q q qf dfq ε − = =∫ (0-24) 图 3-14 给出了不同量化灰度级下 lena 图的效果。可以看出,如果量化的级别越少,lena 图的质量 越差,也就是与原图的误差越大。 连续幅度样本序列的量化,通常是按顺序进行的。序列中每一幅度连续变化的样本可视为标量,并 按标量量化的方法分别量化。但若对序列中的许多样本进行联合量化的重建,便有可能减少量化误差。 以 N×1 元信号向量 f 为例,设它是向量随机过程的样本,并已知该向量随机过程的 n 阶概率密度 为: { }1 2(),, Np f p f f f=  f 的向量量化就是将 N 维空间细分为 L 个判决区 iD i L≤ ≤,1 ,每一判决区含有 L 个重建值 第 3 章 图像感知与获取 · 85· lena 原图(256 级灰度) 128 级灰度 64 级灰度 32 级灰度 16 级灰度 8 级灰度 4 级灰度 2 级灰度 图 3-14 不同量化灰度级下的 lena 图效果 之一。如果信号向量 f 在判决区域 iD 内,则将 f 量化为重建向量 iD 。图 3-15 所示分别说明了二维和 三维的矢量量化。 Áf Áf Ár Áf Ár Áf Áf 二维矢量量化 三维矢量量化 图 3-15 矢量量化 对于向量量化的均方误差可以写作: Visual C++数字图像获取、处理及实践应用 · 86· 1 ( )( ) ( )d Á L T i i i D f r f r p f fε =  = − − ∑ ∫ (0-25) 同标量量化一样,对式 3-25 求导,令 0 ir ε∂ =∂ ,可以求得: ( )d ( )d Á Á D i D f p f f r p f f ⋅ = ∫ ∫ (0-26) 3.5 图像显示 这一节我们来考虑怎么用普通的黑白针式打印机来打印,也就是显示出灰度图。从针式打印机的打 印原理来分析,这似乎是不可能实现的。因为针打是靠撞针击打色带在纸上形成黑点的,不可能打出灰 色的点来。只要你仔细看看那些打印出来的所谓灰色图像,就会发现,这些灰色图像都是由一些黑点组 成的,黑点多的地方,图像就暗;黑点少的地方,图案就亮一些。 图 3-16 用黑白两种颜色打印出的灰度图 图 3-16 所示最左边的是原图,是一幅真正的灰度图。另外两张是用黑白两种颜色打印出来的灰度 图,中间图案的分辨率较高,右边的较低。可以看出中间那一幅相对与原图较接近。 由二值图像显示出灰度效果的方法,也就是所谓半影调技术,它的一个主要用途就是在只有二值输 出的打印机上打印图像。下面将介绍两种方法:图案法和抖动法。 3.5.1 图案法显示 图案法是指灰度可以用一定比例的黑白点组成的区域表示,从而达到整体图像的灰度感。黑白点的 位置选择称为图案化。 计算机显示器、打印机、扫描仪等视频输出设备的一个重要指标就是分辨率,单位是 dpi(dot per inch),即每英寸点数。每英寸上的点数越多,分辨率就越高,图像就越清晰。假设打印的时候,有一幅 160×120×8bit 的灰度图,需要用分辨率为 200dpi×200dpi 的激光打印机将其打印到 12.8×9.6 英寸的纸上。 由 于 这 张 纸 最 多 可 以 打 (200×12.8)×(200×9.6)=2560×1920 个点,所以每个像素可以用 (2560×1960)/ (160×160)=256 个点大小的图案来表示,即一个像素可以有 16×16=256 个点。如果这 16×16 的方块中 一个黑点也没有,就可以表示灰度 256;有一个黑点,就表示灰度 255;依次类推,当都是黑点时,表示 灰度 0。这样,16×16 的方块可以表示 257 级灰度,比要求的 8bit 共 256 级灰度还多了一个。所以上面 那幅图的灰度级别完全能够打印出来。 第 3 章 图像感知与获取 · 87· 图 3-17 所示即是一个用 2×2 图案来表示 5 级灰度的例子。 4 3 2 1 0 图 3-17 2×2 图案表示 5 级灰度 表示的时候有图案构成的问题,即黑点打在哪里?例如,只有一个黑点时,我们可以打在正中央, 也可以打 16×16 的左上角。所以最后用来表示灰度的图案可以是规则的,也可以是不规则的。一般来说, 有规则的图案比随机图案能够避免点的丛集,但有时会导致图像中有明显的线条。 假设使用 16×16 个二值点来打印一个灰度像素,如果想存储 256 级灰度的图案的标准模板,就需要 256×16×16 的二值点阵,占用的空间还是相当可观的。一个更好的办法是:只存储一个 16×16 整数矩阵, 称为标准图案,矩阵元素的取值从 0 到 255。像素的实际灰度和阵列中的每个值比较,当该值大于等于 灰度时,标准图案中标号大于或等于灰度值的对应点打一黑点。 下面介绍一种由 Limb 在 1969 年提出的设计标准图案的算法。先以一个 2×2 的矩阵开始。 设 M1= 0 2 3 1      ,通过递归关系有 Mn+1= 4* 4* 2* 4* 3* 4* Mn Mn Un Mn Un Mn Un +   + +  ,其中 Mn 和 Un 均为 2n×2n 的方阵,Un 的所有元素都是 1。根据这个算法,可以得到: 2 0 8 2 10 12 4 14 6 3 11 1 9 15 7 13 5 M      =       M2 为 16 级灰度的标准图案,如果一个 16 级灰度图像的对应像素值为 11 的话,M2 所有大于 11 的 对应点都要打印为黑色。M3(8×8 阵)比较特殊,称为 Bayer 抖动表。M4 是一个 16×16 的矩阵。 根据上面的算法,如果利用 M3,则一个像素要用 8×8 的图案表示,一幅 N×N 的图将变成 8N×8N 大小。如果利用 M4,变成 16N×16N 了,也就是在相同分辨率的情况下,打印出来的图像是真实图像的 256 倍大小。如果要在保持原图大小的情况下利用图案化技术,则只需要在像素所对应的图案中取一点, 即重新采样,然后再应用图案化技术,就能够保持原图大小。对于 M3 阵来说,即在原图中每 8×8 个点 中取一点。但是实际上,这种方法并不可行。因为实际不知道这 8×8 个点中找哪一点比较合适,而且 8×8 的间隔实在太大了,生成的图像和原图肯定相差很大。 采用如下的算法可以部分避免上面的问题。 假设原图是 256 级灰度,利用 Bayer 抖动表,做如下处理: if (p[y][x]>>2) > bayer[y&7][x&7] then 显示白点 else 显示黑点 x,y 代表原图的像素坐标,p[y][x]代表该点灰度。在显示的时候首先将灰度右移两位,变成 64 级, 然后将 x,y 做模 8 运算,找到 Bayer 表中的对应点,两者做比较,判断当前的像素输出为黑色还是白色。Visual C++数字图像获取、处理及实践应用 · 88· 可以看到,模 8 运算使得原图分成了一个个 8×8 的小块,每个小块和 8×8 的 Bayer 表相对应。小块 中的每个点都参与了比较,这样就避免了上面提到的选点和块划分过大的问题。模 8 运算实质上是引入 了随机成分。 3.5.2 图案法显示图像的 Visual C++实现 下面来编程实现上面的算法。程序中定义了一个 8×8 的矩阵,用来存储 Bayer 表。 首先在主菜单中加入“图像显示”的菜单,如图 3-18 所示。 图 3-18 第三章菜单 函数 LimbPatternBayer 利用 Bayer 表来对图像图像进行二值显示。 /************************************************************************* * * 函数名称: * LimbPatternBayer() * * 参数: * CDib *pDib - 指向 CDib 类的指针 * * 返回值: * BOOL - 成功返回 TRUE,否则返回 FALSE * * 说明: * 该函数利用 BAYER 表抖动显示图像 * ************************************************************************/ BOOL LimbPatternBayer(CDib *pDib) { // Bayer 表的定义 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}; // 指向源图像的指针 BYTE * lpSrc; 第 3 章 图像感知与获取 · 89· //图像的宽度和高度 LONG lWidth; LONG lHeight; //得到实际的 Dib 图像存储大小 CSize SizeRealDim; SizeRealDim = pDib->GetDibSaveDim(); // 图像每行的字节数 LONG lLineBytes; //得到图像的宽度和高度 CSize SizeDim; SizeDim = pDib->GetDimensions(); lWidth = SizeDim.cx; lHeight = SizeDim.cy;; // 计算图像每行的字节数 lLineBytes = SizeRealDim.cx; //图像数据的指针 LPBYTE lpDIBBits = pDib->m_lpImage; // 循环变量 int i, j; // 像素的值 int nPixelValue; // 将图像二值化,利用 BAYER 表抖动显示图像 for (j = 0; j < lHeight ; j++) { for(i = 0; i < lLineBytes ; i++) { // 指向源图像倒数第 j 行,第 i 个像素的指针 lpSrc = (unsigned char *)lpDIBBits + lLineBytes * j + i; nPixelValue = (*lpSrc); nPixelValue =nPixelValue; // 右移两位后做比较 if ( (nPixelValue>>2) > BayerPattern[j&7][i&7]) //打白点 Visual C++数字图像获取、处理及实践应用 · 90· *(lpSrc)=(unsigned char)255; else //打黑点 *(lpSrc)=(unsigned char)0; } } return true; } 在 ImageProcessingView.cpp 中对 Bayer 抖动子菜单添加命令处理函数,代码如下。 void CImageProcessingView::OnViewBayer() { // Bayer 抖动法显示图像 // 更改光标形状 BeginWaitCursor(); // 获取文档 CImageProcessingDoc* pDoc = GetDocument(); // 获得图像 CDib 类的指针 CDib * pDib = pDoc->m_pDibInit; // 获得图像的头文件信息 LPBITMAPINFOHEADER lpBMIH=pDib->m_lpBMIH; // 判断是否是 8-bpp 位图 if (lpBMIH->biBitCount != 8) { // 提示用户 MessageBox("目前只支持 256 色位图!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } ::LimbPatternBayer(pDib); // 设置脏标记 pDoc->SetModifiedFlag(TRUE); // 更新视图 第 3 章 图像感知与获取 · 91· pDoc->UpdateAllViews(NULL); // 恢复光标 EndWaitCursor(); } 利用函数 LimbPatternBayer 可以实现对 256 级灰度图像进行 Bayer 法二值显示,结果如图 3-19 所示。 Lena 原图 Bayer 法二值显示 图 3-19 Bayer 法二值显示 3.5.3 随机抖动法显示图像 如果使用了图案化技术,仍然得不到要求的灰度级别时,就可以采取随机抖动法来显示图案。 例如:有一幅 400×300×8bit 的灰度图,当用分辨率为 200dpi×200dpi 的激光打印机将其打印到 8×6 英寸的纸上时,每个像素只能用(1600/400)×(1200/300)=4×4 个点大小的图案来表示,这样最多能表示 17 级灰度,无法满足 256 级灰度的要求。可以通过减小图像尺寸,将其由 400×300 变为 100×75,或者降低 图像灰度级,由 256 级变成 16 级来解决这个问题。但这两种方案都不理想。这时,可以采用抖动法来解 决这个问题。 上一节给出的 Bayer 算法是一种规则抖动算法。规则抖动的优点是算法简单,缺点是有时图案化很 明显。这是因为取模运算虽然引入了随机成分,但它还是有规律的。另外,点之间进行比较时,只要比 标准图案上点的值大就打白点,这种做法并不理想,因为,如果当标准图案点的灰度值本身就很小,而 图像中点的灰度只比它大一点儿时,图像中的点更接近黑色,而不是白色。一种更好的方法是将这个误 差传播到邻近的像素。 下面将介绍的 Floyd-Steinberg 算法就采用了随机抖动的思想。 假设灰度级别的范围从 b 到 w,中间值 m=(b+w)/2。对于 256 级灰度来说,b=0,w=255,m=128。 设原图中像素的灰度为 p,误差值为 error,则用于显示的图中对应像素值用如下的方法得到: if p > m then 显示白点 error = p - w else 显示黑点 Visual C++数字图像获取、处理及实践应用 · 92· error = p - b a × error 与右边的像素相加 a × error 与下边的像素相加 b × error 与右下方的像素相加 算法最大的特点是对可能出现的误差进行了传递,使得最后的显示结果比较接近真实值。例如,要 打印输出一幅 256 级灰度的图像,如果某个像素值灰度为 130,在灰度图中对应一个灰点。一般图像中 灰度是连续变化的,相邻像素灰度值可能与当前像素值非常接近,所以该点及周围出现灰色像素的可能 较多。在打印生成的图中,130 大于 128,所以显示为白点,但 130 其实在原图中是一个灰色点,离白点 255 还差得比较远,误差 error=130-255=-125,比较大。将 a×(-125)加到相邻像素后,使得相邻像素 的值接近 0 而显示黑点。下一次,error 又变成正的,使得相邻像素的相邻像素显示白点,这样一白一黑 一白,表现出来刚好就是灰色。如果不传递误差,就是一片白色了。再如一个点的灰度为 250,在灰度 图中应该是一个白点,该点及周围应该是一片白色区域。在打印图中,虽然 error=-5 也是负的,但由 于值比较小,对相邻像素的影响不大,所以还是能够显示出一片白色区域来。 算法中 a 和 b 的取值控制误差的传递大小,对图像最后的打印显示质量影响较大。a 和 b 的取值大, 相邻像素的相关性大,最后的打印效果颗粒现象较为严重;a 和 b 取值较小,图像的细节部分不能体现。 在最后的实现中 a 取 3/16,b 取 1/8。 3.5.4 随机抖动法显示图像的 Visual C++实现 函数 DitherFloydSteinberg 利用 Floyd-Steinberg 算法对图像进行二值显示。 /************************************************************************* * * 函数名称: * DitherFloydSteinberg() * * 参数: * CDib *pDib - 指向 CDib 类的指针 * * 返回值: * BOOL - 成功返回 TRUE,否则返回 FALSE * * 说明: * 该函数用来用 Floyd-Steinberg 算法抖动生成图像 * ************************************************************************/ BOOL DitherFloydSteinberg(CDib *pDib) { // 指向源图像的指针 BYTE * lpSrc; //图像的宽度和高度 LONG lWidth; 第 3 章 图像感知与获取 · 93· LONG lHeight; // 图像每行的字节数 LONG lLineBytes; //得到图像的宽度和高度 CSize SizeDim; SizeDim = pDib->GetDimensions(); lWidth = SizeDim.cx; lHeight = SizeDim.cy; //得到实际的 Dib 图像存储大小 CSize SizeRealDim; SizeRealDim = pDib->GetDibSaveDim(); // 计算图像每行的字节数 lLineBytes = SizeRealDim.cx; //图像数据的指针 LPBYTE lpDIBBits = pDib->m_lpImage; // 循环变量 int i, j; // 误差传播系数 double temp, error; // 像素值 int nPixelValue; // 将图像二值化,并用 Floyd-Steinberg 算法抖动生成图像 for (j = 0; j < lHeight; j++) { for(i = 0; i < lLineBytes; i++) { // 指向源图像倒数第 j 行,第 i 个像素的指针 lpSrc = (unsigned char *)lpDIBBits + lLineBytes * j + i; nPixelValue = *lpSrc; //128 是中值 if ( nPixelValue > 128 ) { //打白点 Visual C++数字图像获取、处理及实践应用 · 94· *lpSrc=255; //计算误差 error = (double)(nPixelValue-255.0); } else { //打黑点 *lpSrc=0; //计算误差 error = (double)nPixelValue; } // 如果不是边界 if(i < lLineBytes-1) { //向右传播 temp = (float)*(lpSrc+1); temp = temp + error * (1.5/8.0); if(temp > 255.0) temp = 255.0; *(lpSrc+1)=(int)temp; } // 如果不是边界 if(j < lHeight - 1) { // 向下传播 temp = (float)*(lpSrc + lLineBytes); temp = temp + error * (1.5/8.0); *(lpSrc+lLineBytes) = (int)temp; if(i < lLineBytes-1) { // 向右下传播 temp = (float)*(lpSrc + lLineBytes + 1); 第 3 章 图像感知与获取 · 95· temp = temp + error * (2.0/16.0); *(lpSrc + lLineBytes + 1) = (int)temp; } } } } return true; } 在 ImageProcessingView.cpp 中对随机抖动法显示图像子菜单添加命令处理函数,代码如下。 void CImageProcessingView::OnVIEWFloydSteinberg() { // Floyd-Steinberg 抖动法显示图像 // 更改光标形状 BeginWaitCursor(); // 获取文档 CImageProcessingDoc* pDoc = GetDocument(); // 获得图像 CDib 类的指针 CDib * pDib = pDoc->m_pDibInit; // 获得图像的头文件信息 LPBITMAPINFOHEADER lpBMIH=pDib->m_lpBMIH; // 判断是否是 8-bpp 位图 if (lpBMIH->biBitCount != 8) { // 提示用户 MessageBox("目前只支持 256 色位图!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } ::DitherFloydSteinberg(pDib); // 设置脏标记 Visual C++数字图像获取、处理及实践应用 · 96· pDoc->SetModifiedFlag(TRUE); // 更新视图 pDoc->UpdateAllViews(NULL); // 恢复光标 EndWaitCursor(); } 图 3-20 所示便是利用上述函数实现的 Floyd-Steinberg 算法对 256 级灰度 lena 图进行二值显示的结 果。可以看出相对于 Bayer 算法,显示图像的图案化没有那么明显,细节表现较好,最后的效果更接近 真实的图像。 Lena 原图 Floyd-Steinberg 法二值显示 图 3-20 Floyd-Steinberg 法二值显示 第第 44 章章 图图像像增增强强 图像增强的目的是采用一系列技术改善图像的视觉效果,或者将图像转换成一种更适合于人或机器 进行分析处理的形式。它并不一定追究图像降质的原因。直方图修正、强化图像轮廓等都是常用手段。 由于接受者是人,因此质量好坏就受观看者的心理、爱好、文化素质等因素的影响,评判只能是相对的。 图像增强的方法基本上可以分为空间域处理和频域处理两大类。空间域处理是在原图像上直接进行 数据运算。它又分为两类,一类是在与像素点邻域有关的空间域进行,称为局部运算,例如空间域卷积 运算。另一类是对图像做逐点运算,称为点运算。处理可以是线性或非线性的,例如用指数、对数、比 值、黑白到彩色的变化等。频域处理是在图像的 Fourier 等变换域上进行修改,增强我们感兴趣的频率分 量,然后修改后的 Fourier 等变换值再作 Fourier 反变换,便得到增强的图像。 目前对图像增强还缺乏统一的理论,衡量图像的质量还难以建立客观标准,一些增强方法往往有针 对性,处理结果也以主观判断为准。对某类图像效果较好的增强方法未必适合于另一类图像,使用时务 必注意选择。本章将重点介绍一些在实践中行之有效的方法。 本章的内容安排是:首先介绍对比度增强(点处理),然后在第二节中介绍图像平滑,图像锐化和 伪彩色增强将在第三节和第四节中进行叙述,在第五节中,讨论在频域进行图像增强的方法。 4.1 对比度增强 有些图像的对比度很差,例如 X 光照片或卫星多光谱图像。因此,需要对图像中每一像素的灰度进 行灰度标度变换,扩大图像灰度的范围,达到增强的目的。这是一种简单的方法。设原始图像在(x, y)处 的灰度为 f,而改变后的图像为 g,则对图像的增强可表述为将在(x, y)处的灰度 f 映射为 g,此映射可以 表示为: ( , ) [ ( , )]g x y T f x y= (4-1) 对比度增强属于点运算,因为其增强的方式为逐点地进行运算,其结果只和此像素点的特征有关。 根据映射方式的不同,对比度增强可分为灰度变换法和直方图修整法。其中前者又可以分为线性, 分段线性和非线性的灰度变换以及其他的灰度变换。直方图修整法通常分为直方图均衡化和直方图化两 类。下面对这些方法进行简单的介绍,并给出其中的一些实现代码。 4.1.1 灰度变换法 灰度变换是零记忆点运算,该运算把一给定的灰度级映射到另一灰度级上。常用的灰度变换有: 1. 线性灰度变换 当图像由于成象时曝光不足或过度,由于成象设备的非线性或图像记录设备动态范围太窄等因素, 都会产生对比度不足的弊病,使图像中细节分辨不清。这时如将图像灰度线性扩展,常能显著改善图像 的主观质量。假设原图像 f(x, y)的灰度范围为[a, b],希望变换后图像 g(x, y)的灰度范围扩展到[c, d],则 可采用如下的线性变换来实现: Visual C++数字图像获取、处理及实践应用 · 98· 0 ( , ) (,)(,)(,) (,) c f x y a d cgxy fxycafxybb a d b f x y Mf ≤ <  −= ⋅ + ≤ < − ≤ ≤ (4-2) 其中 Mf 表示 f(x, y)的最大值。 2. 分段线性变换灰度变换 为了突出感兴趣的目标或者灰度区间,相对抑制那些不感兴趣的灰度区域,可采用分段线性法。常 用的是三段线性变换,如图 4-1 所示。数学表达式为: ( / ) ( , ) 0 ( , ) (,)[(,)](,) [(,)](,) c a f x y f x y a d cgxy fxyac afxybb a Mg d fxy b db fxy MfMf b   ≤ < −= ⋅ − + ≤ < − − − + ≤ ≤ − (4-3) 图 4-1 分段线性变换 图中对灰度区间[a, b]进行了线性扩展,而灰度区间[0, a]和[b, Mf]则受到了压缩。通过细心调整折线 拐点的位置并控制分段直线的斜率,可对任一灰度区间进行扩展或者压缩。 3. 非线性灰度变换 当用某些非线性函数,例如对数函数、指数函数等,作为映射函数时,可实现图像灰度的非线性变 换。常用的非线性变换有对数变换和指数变换。 对数变换:对数变换一般式为 ln[ ( , ) 1(,) ln f x yg x y a b c += + ⋅ (4-4) 这里 a、b、c 是为了调整曲线的位置和形状而引入的参数。当希望对图像的低灰度区进行较大的扩 展而对高灰度区进行压缩时,可采用此变换,它能使图像灰度的分布均匀,与人的视觉特性相匹配。 指数变换:指数变换的一般式为 [(,)]( , ) 1c f x y ag x y b −= − (4-5)第 4 章 图像增强 · 99· 这里参数 a、b、c 用来调整曲线的位置和形状。这种变换能对图像的高灰度区给予较大的扩展。 4. 其他灰度变换 (1) 灰度倒置变换 变换如图 4-2 所示,当被显示图像在低灰度区间呈现高度非线性时,此变换能使原图像低灰度区间 的细节转换到高灰度区,并且是近似线性较好的部分,使细节清晰可见。 图 4-2 灰度倒置变换 (2) 锯齿形变换 变换如图 4-3 所示,常在小动态范围的显示器中用来显示动态范围较大的图像,或者用来在某些灰 度范围附近产生鲜明的伪轮廓。 图 4-3 锯齿形变换 (3) 灰度分层切片 变换如图 4-4 所示,它把输入图像中某一小灰度范围值抽取出来,转换成最大灰度或用假彩色显示。 在交互处理中,做人工图像分析时这是很有效的方法。 图 4-4 灰度分层切片 Visual C++数字图像获取、处理及实践应用 · 100· Á 4.1.2 直方图修整法 直方图表示数字图像中每一灰度与其出现频度间的统计关系。直方图能给出该图像的概貌性描述, 例如图像的灰度范围、每个灰度级的频度和灰度的分布、整幅图像的平均明暗和对比度等,由此可得出 进一步处理的重要依据。自然图像由于其灰度通常分布在较窄的区间,引起图像细节不清楚。采用直方 图修整后可使图像的灰度间距拉开或者使灰度分别均匀,从而增大了反差,使图像细节清晰,达到图像 增强的目的。直方图修整法通常有直方图均衡化和直方图规定化两类。 设 f(x, y)的灰度范围为 fmin 和 fmax(如果每像素是用 8 位表示,那么,一般来说,fmin=0,fmax=255)。 将直方图正规化为 ]1,0[∈r ,即 min max min f f fr f f L −= =− (4-6) 其中 L 表示图像灰度的范围。则灰度直方图表示为 ( ) (k 0, ,L)k r k nP r n = =  (4-7) 其中 n 为一幅图像总像素数目,nk 表示灰度为 rk 的像素数目。如图 4-5 就是一个直方图 。 0 0.1 0.2 0.3 0 1 2 3 4 5 6 7 图 4-5 直方图示例 直方图修整就是通过灰度映射函数 ][rTs = ,将原直方图 )(rPr 改变成希望的直方图 )(sPs 。 ][rTs = 满足以下 4 个条件: • (1)在 ]1,0[∈r 内, ][rTs = 是单调增加的 • (2)s 和 r 一一对应。 • (3)对于 ]1,0[∈r ,有 ]1,0[][ ∈= rTs 。 • (4)反变换 ][1 sTr −= 满足条件(1)-(3) 由概率论知道 d()() ds r rP s P r s = (4-8) 第 4 章 图像增强 · 101· 1. 直方图均衡化 直方图均衡化就是把给定图像的直方图分布改变成均匀分布直方图分布。直观的看,直方图均衡化 将导致信号值所占区域的对比度增加。要进行直方图均衡化,意味着 )(sPs =1, ]1,0[∈s ,由公式(4-8) 可得 ( )d ( )d ( )d( ) 1 r r r s P r r P r rds P r rP s = = = (4-9) 从而 0 ( ) ( )dr rs T r P w w= = ∫ (4-10) 即 T( r )为为分布累积函数。 在数字图像中,直方图均衡化的离散化公式为: k k 0 0 ( ) ( ) 0 r 1,0 1, 0,1, , 1 k k j k k r j j j ns T r P r s k Ln= = = = = ≤ ≤ ≤ ≤ = −∑ ∑  (4-11) 由公式(4-11)计算出的 S( k )需要根据良好间隔归入各自的量化等级,即 [ ( 1)]k kl s L= − (4-12) 其中 ][• 表示四舍五入取整。 需要注意的是,由于灰度离散化,均衡化图像的直方图只是近似均匀的直方图分布。均衡化后的图 像动态范围扩大了,但其本质是扩大了量化层间隔,而非量化层的数目,相反,均衡化后级数分布减少, 因而可能会出现伪轮廓。 2. 直方图规定化 均衡化的方法是直方图修整法的一个特例,即修整后灰度概率密度伪均匀分布即 )(sPs =1。在交互 式图像增强中,希望能够达到预先给定的分布密度 )(zPz ,以便突出感兴趣的灰度范围。这种方法称为 直方图规定化。 直方图规定化的方法,可以借助于直方图均衡化中的算法实现,其实现的步骤为 • (1)将给定的图像进行直方图均衡化,即 0 ( ) ( )dr rs T r P w w= = ∫ • (2)规定希望的密度函数 )(zPz • (3)将希望的直方图 )(zPz 进行均衡化处理 0 ( ) ( )dz zu G z P w w= = ∫ 其反变换为 1−= Gz Visual C++数字图像获取、处理及实践应用 · 102· • (4)由于 )(sPs 和 )(zPz 都是均匀概率密度函数,故将 s 代替 u,再带入公式(4-9)和(4-10), 得 )]([)( 11 rTGsGz −− == 这样对原灰度 r,可通过公式得到规定化得灰度值 z。 在离散的情况下,规定化的步骤为: • (1)对已知 )( kr rP 做均衡化处理 1,,1,0,10,1r0 )()( kk 00 −=≤≤≤≤=== ∑∑ == Lksn nrPrTs k j j k j jrkk  • (2)对给定直方图 )( kz zP 做均衡化处理 1,,1,0,10,10 )()( kk 00 −=≤≤≤≤=== ∑∑ == Lkuzn nzPzGu k j j k j jzkk  • 建立 rk 和 zk 之间的关系 )](([)](()( 111 kroxkroxkk rTPGsPGuGz −−− === 其中 Prox(sk)表示与 sk 最接近 uk。 4.1.3 Visual C++编程实现 有了上面的理论基础,就可以通过 Visual C++来实现图像对比度的增强了。在这里,由于篇幅有限, 只给出其中的一部分代码实现。首先,给出了分段线性变换的程序代码。直方图在图像处理中占有重要 的地位,因此,在这里还给出关于直方图的代码实现,这包括直方图的显示与直方图均衡。其他的方法, 读者可以参照文献和给出的代码自己实现。 1. 灰度变换法 分段线性变换是灰度变换法的一种,在这一节的开始已经给予了介绍,下面的函数 GraySegLinTrans() 实现了分段线性变换,其代码如下所示。 /************************************************************************* * * \函数名称: * GraySegLinTrans() * * \输入参数: * CDib* pDib - 指向 CDib 类的指针,含有原始图像信息 * int nX1 - 分段线性灰度变换第一个拐点的 X 坐标 * int nY1 - 分段线性灰度变换第一个拐点的 Y 坐标 * int nX2 - 分段线性灰度变换第二个拐点的 X 坐标 * int nY2 - 分段线性灰度变换第二个拐点的 Y 坐标 * 第 4 章 图像增强 · 103· * \返回值: * BOOL - 成功返回 TRUE,否则返回 FALSE。 * * \说明: * 该函数用来对图像进行分段线性灰度变换,输入参数中包含了两个拐点的坐标 * ************************************************************************* */ BOOL GraySegLinTrans(CDib* pDib, int nX1, int nY1, int nX2, int nY2) { // 指向源图像的指针 unsigned char* lpSrc; // 循环变量 int i,j; // 灰度映射表 BYTE byMap[256]; // 图像每行的字节数 //LONG lLineBytes; // 图像的高度和宽度 CSize sizeImage; sizeImage = pDib->GetDimensions(); // 获得图像数据存储的高度和宽度 CSize SizeSaveImage; SizeSaveImage = pDib->GetDibSaveDim(); // 计算图像每行的字节数 //lLineBytes = WIDTHBYTES(sizeImage.cx * 8); // 计算灰度映射表 for (i = 0; i <= nX1; i++) { // 判断 nX1 是否大于 0(防止分母为 0) if (nX1 > 0) { // 线性变换 byMap[i] = (BYTE) nY1 * i / nX1; } Visual C++数字图像获取、处理及实践应用 · 104· else { // 直接赋值为 0 byMap[i] = 0; } } for (; i <= nX2; i++) { // 判断 nX1 是否等于 nX2(防止分母为 0) if (nX2 != nX1) { // 线性变换 byMap[i] = nY1 + (BYTE) ((nY2 - nY1) * (i - nX1) / (nX2 - nX1)); } else { // 直接赋值为 nY1 byMap[i] = nY1; } } for (; i < 256; i++) { // 判断 nX2 是否等于 255(防止分母为 0) if (nX2 != 255) { // 线性变换 byMap[i] = nY2 + (BYTE) ((255 - nY2) * (i - nX2) / (255 - nX2)); } else { // 直接赋值为 255 byMap[i] = 255; } } // 对图像的像素值进行变换 // 每行 for(i = 0; i < sizeImage.cy; i++) { // 每列 for(j = 0; j < sizeImage.cx; j++) { // 指向 DIB 第 i 行,第 j 个像素的指针 lpSrc = (unsigned char*)pDib->m_lpImage + pDib->GetPixelOffset(i,j); 第 4 章 图像增强 · 105· // 计算新的灰度值 *lpSrc = byMap[*lpSrc]; } } // 返回 return TRUE; } 为了设置分段线性变换中拐点等参数,笔者设计了一个对话框,将它加入到工程中,取名为 IDD_DLG_ENHANCE_LINTRANS。然后给此对话框创建新的类 CdlgEhnLinTrans。在这个类中,可以通 过鼠标的拖动来设置拐点的位置。其实现代码如下所示。 (1) 对话框头文件(DlgEnhLinTrans.h) ///////////////////////////////////////////////////////////////////////////// // CDlgEhnLinTrans dialog class CDlgEhnLinTrans : public CDialog { // Construction public: // 当前鼠标拖动状态,0 表示未拖动,1 表示正在拖动第一点,2 表示正在拖动第二点。 int m_nIsDraging; // 相应鼠标事件的矩形区域 CRect m_rectMouse; // 标识是否已经绘制橡皮筋线 BOOL m_bDrawed; // 保存鼠标左键单击时的位置 CPoint m_pointMsClick; // 保存鼠标拖动时的位置 CPoint m_pointMsMove; CDlgEhnLinTrans(CWnd* pParent = NULL); // standard constructor // Dialog Data //{{AFX_DATA(CDlgEhnLinTrans) enum { IDD = IDD_DLG_ENHANCE_LINTRANS }; int m_nX1; int m_nX2; int m_nY1; Visual C++数字图像获取、处理及实践应用 · 106· int m_nY2; //}}AFX_DATA // Implementation protected: // Generated message map functions //{{AFX_MSG(CDlgEhnLinTrans) afx_msg void OnLButtonDown(UINT nFlags, CPoint point); afx_msg void OnMouseMove(UINT nFlags, CPoint point); afx_msg void OnLButtonUp(UINT nFlags, CPoint point); afx_msg void OnPaint(); afx_msg void OnKillfocusEditLintransX1(); afx_msg void OnKillfocusEditLintransX2(); afx_msg void OnKillfocusEditLintransY1(); afx_msg void OnKillfocusEditLintransY2(); virtual BOOL OnInitDialog(); virtual void OnOK(); //}}AFX_MSG DECLARE_MESSAGE_MAP() }; (2) 对话框文件(DlgEnhLinTrans.cpp) CDlgEhnLinTrans::CDlgEhnLinTrans(CWnd* pParent /*=NULL*/) : CDialog(CDlgEhnLinTrans::IDD, pParent) { //{{AFX_DATA_INIT(CDlgEhnLinTrans) m_nX1 = 0; m_nX2 = 0; m_nY1 = 0; m_nY2 = 0; //}}AFX_DATA_INIT } void CDlgEhnLinTrans::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); //{{AFX_DATA_MAP(CDlgEhnLinTrans) DDX_Text(pDX, IDC_EDIT_LINTRANS_X1, m_nX1); DDV_MinMaxInt(pDX, m_nX1, 0, 255); DDX_Text(pDX, IDC_EDIT_LINTRANS_X2, m_nX2); DDV_MinMaxInt(pDX, m_nX2, 0, 255); DDX_Text(pDX, IDC_EDIT_LINTRANS_Y1, m_nY1); 第 4 章 图像增强 · 107· DDV_MinMaxInt(pDX, m_nY1, 0, 255); DDX_Text(pDX, IDC_EDIT_LINTRANS_Y2, m_nY2); DDV_MinMaxInt(pDX, m_nY2, 0, 255); //}}AFX_DATA_MAP } BEGIN_MESSAGE_MAP(CDlgEhnLinTrans, CDialog) //{{AFX_MSG_MAP(CDlgEhnLinTrans) ON_WM_LBUTTONDOWN() ON_WM_MOUSEMOVE() ON_WM_LBUTTONUP() ON_WM_PAINT() ON_EN_KILLFOCUS(IDC_EDIT_LINTRANS_X1, OnKillfocusEditLintransX1) ON_EN_KILLFOCUS(IDC_EDIT_LINTRANS_X2, OnKillfocusEditLintransX2) ON_EN_KILLFOCUS(IDC_EDIT_LINTRANS_Y1, OnKillfocusEditLintransY1) ON_EN_KILLFOCUS(IDC_EDIT_LINTRANS_Y2, OnKillfocusEditLintransY2) //}}AFX_MSG_MAP END_MESSAGE_MAP() ///////////////////////////////////////////////////////////////////////////// // CDlgEhnLinTrans message handlers BOOL CDlgEhnLinTrans::OnInitDialog() { // 调用默认 OnInitDialog 函数 CDialog::OnInitDialog(); // 获取绘制直方图的标签 CWnd* pWnd = GetDlgItem(IDC_LINTRANS_SHOWCOR); // 计算接受鼠标事件的有效区域 pWnd->GetClientRect(m_rectMouse); pWnd->ClientToScreen(&m_rectMouse); CRect rect; GetClientRect(rect); ClientToScreen(&rect); m_rectMouse.top -= rect.top; m_rectMouse.left -= rect.left; // 设置接受鼠标事件的有效区域 m_rectMouse.top += 25; m_rectMouse.left += 10; Visual C++数字图像获取、处理及实践应用 · 108· m_rectMouse.bottom = m_rectMouse.top + 255; m_rectMouse.right = m_rectMouse.left + 256; // 初始化拖动状态 m_nIsDraging = 0; return TRUE; // return TRUE unless you set the focus to a control // EXCEPTION: OCX Property Pages should return FALSE } void CDlgEhnLinTrans::OnLButtonDown(UINT nFlags, CPoint point) { // 当用户单击鼠标左键开始拖动 if(m_rectMouse.PtInRect(point)) { CRect rectTemp; // 计算点 1 临近区域 rectTemp.left = m_rectMouse.left + m_nX1 - 2; rectTemp.right = m_rectMouse.left + m_nX1 + 2; rectTemp.top = 255 + m_rectMouse.top - m_nY1 - 2; rectTemp.bottom = 255 + m_rectMouse.top - m_nY1 + 2; // 判断用户是不是想拖动点 1 if (rectTemp.PtInRect(point)) { // 设置拖动状态 1,拖动点 1 m_nIsDraging = 1; // 更改光标 ::SetCursor(::LoadCursor(NULL, IDC_SIZEALL)); } else { // 计算点 2 临近区域 rectTemp.left = m_rectMouse.left + m_nX2 - 2; rectTemp.right = m_rectMouse.left + m_nX2 + 2; rectTemp.top = 255 + m_rectMouse.top - m_nY2 - 2; rectTemp.bottom = 255 + m_rectMouse.top - m_nY2 + 2; // 判断用户是不是想拖动点 2 if (rectTemp.PtInRect(point)) { 第 4 章 图像增强 · 109· // 设置拖动状态为 2,拖动点 2 m_nIsDraging = 2; // 更改光标 ::SetCursor(::LoadCursor(NULL, IDC_SIZEALL)); } } } // 默认单击鼠标左键处理事件 CDialog::OnLButtonDown(nFlags, point); } void CDlgEhnLinTrans::OnLButtonUp(UINT nFlags, CPoint point) { // 当用户释放鼠标左键停止拖动 if (m_nIsDraging != 0) { // 重置拖动状态 m_nIsDraging = 0; } // 默认释放鼠标左键处理事件 CDialog::OnLButtonUp(nFlags, point); } void CDlgEhnLinTrans::OnMouseMove(UINT nFlags, CPoint point) { // 判断当前光标是否在绘制区域 if(m_rectMouse.PtInRect(point)) { // 判断是否正在拖动 if (m_nIsDraging != 0) { // 判断正在拖动点 1 还是点 2 if (m_nIsDraging == 1) { // 判断是否下限<上限 if (point.x - m_rectMouse.left < m_nX2) { // 更改下限 m_nX1 = point.x - m_rectMouse.left; } Visual C++数字图像获取、处理及实践应用 · 110· else { // 下限拖过上限,设置为上限-1 m_nX1 = m_nX2 - 1; // 重设鼠标位置 point.x = m_rectMouse.left + m_nX2 - 1; } // 更改 Y 坐标 m_nY1 = 255 + m_rectMouse.top - point.y; } else { // 正在拖动点 2 // 判断是否上限>下限 if (point.x - m_rectMouse.left > m_nX1) { // 更改下限 m_nX2 = point.x - m_rectMouse.left; } else { // 下限拖过上限,设置为下限+1 m_nX2 = m_nX1 + 1; // 重设鼠标位置 point.x = m_rectMouse.left + m_nX1 + 1; } // 更改 Y 坐标 m_nY2 = 255 + m_rectMouse.top - point.y; } // 更改光标 ::SetCursor(::LoadCursor(NULL, IDC_SIZEALL)); // 更新 UpdateData(FALSE); // 重绘 InvalidateRect(m_rectMouse, TRUE); } 第 4 章 图像增强 · 111· else { CRect rectTemp1; CRect rectTemp2; // 计算点 1 临近区域 rectTemp1.left = m_rectMouse.left + m_nX1 - 2; rectTemp1.right = m_rectMouse.left + m_nX1 + 2; rectTemp1.top = 255 + m_rectMouse.top - m_nY1 - 2; rectTemp1.bottom = 255 + m_rectMouse.top - m_nY1 + 2; // 计算点 2 临近区域 rectTemp2.left = m_rectMouse.left + m_nX2 - 2; rectTemp2.right = m_rectMouse.left + m_nX2 + 2; rectTemp2.top = 255 + m_rectMouse.top - m_nY2 - 2; rectTemp2.bottom = 255 + m_rectMouse.top - m_nY2 + 2; // 判断用户在点 1 或点 2 旁边 if ((rectTemp1.PtInRect(point)) || (rectTemp2.PtInRect(point))) { // 更改光标 ::SetCursor(::LoadCursor(NULL, IDC_SIZEALL)); } } } // 默认鼠标移动处理事件 CDialog::OnMouseMove(nFlags, point); } void CDlgEhnLinTrans::OnPaint() { // 设备上下文 CPaintDC dc(this); // device context for painting // 字符串 CString str; // 获取绘制坐标的文本框 CWnd* pWnd = GetDlgItem(IDC_LINTRANS_SHOWCOR); // 指针 CDC* pDC = pWnd->GetDC(); pWnd->Invalidate(); Visual C++数字图像获取、处理及实践应用 · 112· pWnd->UpdateWindow(); pDC->Rectangle(0,0,330,300); // 创建画笔对象 CPen* pPenRed = new CPen; // 红色画笔 pPenRed->CreatePen(PS_SOLID, 2, RGB(255,0,0)); // 创建画笔对象 CPen* pPenBlue = new CPen; // 蓝色画笔 pPenBlue->CreatePen(PS_SOLID, 1, RGB(0,0, 255)); // 选中当前红色画笔,并保存以前的画笔 CGdiObject* pOldPen = pDC->SelectObject(pPenRed); // 绘制坐标轴 pDC->MoveTo(10,10); // 垂直轴 pDC->LineTo(10,280); // 水平轴 pDC->LineTo(320,280); // 写坐标 str.Format("0"); pDC->TextOut(10, 281, str); str.Format("255"); pDC->TextOut(265, 281, str); pDC->TextOut(11, 25, str); // 绘制 X 轴箭头 pDC->LineTo(315,275); pDC->MoveTo(320,280); pDC->LineTo(315,285); // 绘制 X 轴箭头 pDC->MoveTo(10,10); pDC->LineTo(5,15); 第 4 章 图像增强 · 113· pDC->MoveTo(10,10); pDC->LineTo(15,15); // 更改成蓝色画笔 pDC->SelectObject(pPenBlue); // 绘制坐标值 str.Format("(%d, %d)", m_nX1, m_nY1); pDC->TextOut(m_nX1 + 10, 281 - m_nY1, str); str.Format("(%d, %d)", m_nX2, m_nY2); pDC->TextOut(m_nX2 + 10, 281 - m_nY2, str); // 绘制用户指定的变换直线 pDC->MoveTo(10, 280); pDC->LineTo(m_nX1 + 10, 280 - m_nY1); pDC->LineTo(m_nX2 + 10, 280 - m_nY2); pDC->LineTo(265, 25); // 绘制点边缘的小矩形 CBrush brush; brush.CreateSolidBrush(RGB(0,255,0)); // 选中刷子 CGdiObject* pOldBrush = pDC->SelectObject(&brush); // 绘制小矩形 pDC->Rectangle(m_nX1 + 10 - 2, 280 - m_nY1 - 2, m_nX1 + 12, 280 - m_nY1 + 2); pDC->Rectangle(m_nX2 + 10 - 2, 280 - m_nY2 - 2, m_nX2 + 12, 280 - m_nY2 + 2); // 恢复以前的画笔 pDC->SelectObject(pOldPen); // 绘制边缘 pDC->MoveTo(10,25); pDC->LineTo(265,25); pDC->LineTo(265,280); // 删除新的画笔 delete pPenRed; delete pPenBlue; // Do not call CDialog::OnPaint() for painting messages } Visual C++数字图像获取、处理及实践应用 · 114· void CDlgEhnLinTrans::OnKillfocusEditLintransX1() { // 更新 UpdateData(TRUE); // 判断是否下限超过上限 if (m_nX1 > m_nX2) { // 互换 int nTemp = m_nX1; m_nX1 = m_nX2; m_nX2 = nTemp; nTemp = m_nY1; m_nY1 = m_nY2; m_nY2 = nTemp; // 更新 UpdateData(FALSE); } // 重绘 InvalidateRect(m_rectMouse, TRUE); } void CDlgEhnLinTrans::OnKillfocusEditLintransX2() { // 更新 UpdateData(TRUE); // 判断是否下限超过上限 if (m_nX1 > m_nX2) { // 互换 int nTemp = m_nX1; m_nX1 = m_nX2; m_nX2 = nTemp; nTemp = m_nY1; m_nY1 = m_nY2; m_nY2 = nTemp; // 更新 UpdateData(FALSE); } 第 4 章 图像增强 · 115· // 重绘 InvalidateRect(m_rectMouse, TRUE); } void CDlgEhnLinTrans::OnKillfocusEditLintransY1() { // 更新 UpdateData(TRUE); // 重绘 InvalidateRect(m_rectMouse, TRUE); } void CDlgEhnLinTrans::OnKillfocusEditLintransY2() { // 更新 UpdateData(TRUE); // 重绘 InvalidateRect(m_rectMouse, TRUE); } void CDlgEhnLinTrans::OnOK() { // 判断是否下限超过上限 if (m_nX1 > m_nX2) { // 互换 int nTemp = m_nX1; m_nX1 = m_nX2; m_nX2 = nTemp; nTemp = m_nY1; m_nY1 = m_nY2; m_nY2 = nTemp; // 更新 UpdateData(FALSE); } // 默认处理事件 CDialog::OnOK(); } 通过对话框完成了分段线性变换中参数的设置后,即可向菜单“图像增强”中添加菜单项“灰度变 换”,如图 4-6 所示。 Visual C++数字图像获取、处理及实践应用 · 116· 图 4-6 灰度变换菜单项 在类 CimageProcessingView 中添加点击事件处理程序,其代码如下所示。 void CImageProcessingView::OnEnhanceLintrans() { // 获取文档 CImageProcessingDoc* pDoc = GetDocument(); // 创建对话框 CDlgEhnLinTrans dlgPara; // 点 1 坐标 int nX1; int nY1; // 点 2 坐标 int nX2; int nY2; // 判断是否是 8-bpp 位图(这里为了方便,只处理 8-bpp 位图的灰度拉伸,其他的可以类推) if(pDoc->m_pDibInit->m_nColorTableEntries != 256) { // 提示用户 MessageBox("目前只支持 256 色位图的伪彩色变换!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } // 初始化变量值 dlgPara.m_nX1 = 50; dlgPara.m_nY1 = 30; dlgPara.m_nX2 = 200; dlgPara.m_nY2 = 220; 第 4 章 图像增强 · 117· // 显示对话框,提示用户设定拉伸位置 if (dlgPara.DoModal() != IDOK) { // 返回 return; } // 获取用户的设定 nX1 = dlgPara.m_nX1; nY1 = dlgPara.m_nY1; nX2 = dlgPara.m_nX2; nY2 = dlgPara.m_nY2; // 删除对话框 delete dlgPara; // 更改光标形状 BeginWaitCursor(); // 调用 GrayStretch()函数进行灰度拉伸 GraySegLinTrans(pDoc->m_pDibInit, nX1, nY1, nX2, nY2); // 设置脏标记 pDoc->SetModifiedFlag(TRUE); // 更新视图 pDoc->UpdateAllViews(NULL); // 恢复光标 EndWaitCursor(); } 运行上述代码,选择如图 4-7 所示的参数,其运行结果如图 4-8 所示,其中图 4-8(a)为原图像, 图 4-8(b)为灰度变换后的图像。 图 4-7 分段线性变换参数设置 Visual C++数字图像获取、处理及实践应用 · 118· (a) (b) 图 4-8 灰度变换运行结果 2. 直方图显示 直方图是图像处理中很重要的一个概念,它给出了一个描述图像灰度的简单实用的统计。在前面已 经对直方图以及利用直方图进行图像增强进行了介绍,下面给出图像直方图的显示和直方图均衡的编码 实现。首先介绍直方图的显示。 在程序中添加一个对话框 IDD_DLG_SHOW_HISTOGRAM,此对话框的设置请参看附件中的完整 工程,并给此对话框创建新的类 CDlgHistShow。为了在对话框中绘制出图像的灰度直方图,首先应该得 到图像的指针,为此需要给类 CdlgHistShow 添加一个类成员变量 CDib* m_pDib。得到图像指针后,还 需要添加一个保存直方图数据的成员变量 int m_nHist[256]。 下面给出了该对话框的完整代码。 (1) 对话框头文件(DlgHistShow.h) #if !defined(AFX_DLGHISTSHOW1_H__0D22FC15_DE60_4FD8_A522_D9433D189AEF__INCLUD ED_) #define AFX_DLGHISTSHOW1_H__0D22FC15_DE60_4FD8_A522_D9433D189AEF__INCLUDED_ #include "cdib.h" #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 // DlgHistShow1.h : header file // ///////////////////////////////////////////////////////////////////////////// // CDlgHistShow dialog class CDlgHistShow : public CDialog { // Construction public: 第 4 章 图像增强 · 119· // CDib 对象指针 CDib* m_pDib; // 直方图数组 int m_nHist[256]; CDlgHistShow(CWnd* pParent = NULL); // standard constructor // Dialog Data //{{AFX_DATA(CDlgHistShow) enum { IDD = IDD_DLG_SHOW_HISTOGRAM }; // NOTE: the ClassWizard will add data members here //}}AFX_DATA // Overrides // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(CDlgHistShow) protected: virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support //}}AFX_VIRTUAL // Implementation protected: // Generated message map functions //{{AFX_MSG(CDlgHistShow) virtual BOOL OnInitDialog(); afx_msg void OnPaint(); //}}AFX_MSG DECLARE_MESSAGE_MAP() }; //{{AFX_INSERT_LOCATION}} // Microsoft Visual C++ will insert additional declarations immediately before the previous line. #endif // !defined(AFX_DLGHISTSHOW1_H__0D22FC15_DE60_4FD8_A522_D9433D189AEF__INCLUDED_) (2) 话框程序(DlgHistShow.cpp) // DlgHistShow1.cpp : implementation file // Visual C++数字图像获取、处理及实践应用 · 120· #include "stdafx.h" #include "ImageProcessing.h" #include "DlgHistShow1.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif ///////////////////////////////////////////////////////////////////////////// // CDlgHistShow dialog CDlgHistShow::CDlgHistShow(CWnd* pParent /*=NULL*/) : CDialog(CDlgHistShow::IDD, pParent) { //{{AFX_DATA_INIT(CDlgHistShow) // NOTE: the ClassWizard will add member initialization here //}}AFX_DATA_INIT } void CDlgHistShow::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); //{{AFX_DATA_MAP(CDlgHistShow) // NOTE: the ClassWizard will add DDX and DDV calls here //}}AFX_DATA_MAP } BEGIN_MESSAGE_MAP(CDlgHistShow, CDialog) //{{AFX_MSG_MAP(CDlgHistShow) ON_WM_PAINT() //}}AFX_MSG_MAP END_MESSAGE_MAP() ///////////////////////////////////////////////////////////////////////////// // CDlgHistShow message handlers BOOL CDlgHistShow::OnInitDialog() { CDialog::OnInitDialog(); 第 4 章 图像增强 · 121· // 设置直线原图像像素的指针 unsigned char * lpSrc; // 循环变量 int i,j; // 获取绘制直方图的标签 CWnd* pWnd = GetDlgItem(IDC_DLG_HIST_SHOW); // 计算得到直方图 // 图像的高度和宽度 CSize sizeImage; sizeImage = m_pDib->GetDimensions(); // 获得图像数据存储的高度和宽度 CSize sizeSaveImage; sizeSaveImage = m_pDib->GetDibSaveDim(); // 重置计数为 0 for (i = 0; i < 256; i ++) { // 清零 m_nHist[i] = 0; } // 计算各个灰度值的计数,即得到直方图 for (i = 0; i < sizeImage.cy; i ++) { for (j = 0; j < sizeImage.cx; j ++) { lpSrc = (unsigned char *)m_pDib->m_lpImage + sizeSaveImage.cx * i + j; // 计数加 1 m_nHist[*(lpSrc)]++; } } return TRUE; // return TRUE unless you set the focus to a control // EXCEPTION: OCX Property Pages should return FALSE } void CDlgHistShow::OnPaint() { Visual C++数字图像获取、处理及实践应用 · 122· CPaintDC dc(this); // device context for painting // 循环变量 int i; // 获取绘制直方图文本框的标签 CWnd* pWnd = GetDlgItem(IDC_DLG_HIST_SHOW); // 获取设备上下文 CDC* pDC = pWnd->GetDC(); pWnd->Invalidate(); pWnd->UpdateWindow(); pDC->Rectangle(0, 0, 330, 300); // 创建画笔对象 CPen* pPenRed = new CPen; // 创建红色画笔(用于绘制坐标轴) pPenRed->CreatePen(PS_SOLID, 1, RGB(255,0,0)); // 选入红色画笔,并保存以前的画笔 CPen* pOldPen = pDC->SelectObject(pPenRed); // 绘制坐标轴 pDC->MoveTo(10,10); // 绘制垂直轴 pDC->LineTo(10, 280); // 绘制水平轴 pDC->LineTo(320, 280); // 绘制 X 轴刻度值 CString strTemp; strTemp.Format("0"); pDC->TextOut(10, 283, strTemp); strTemp.Format("50"); pDC->TextOut(60, 283, strTemp); strTemp.Format("100"); pDC->TextOut(110, 283, strTemp); strTemp.Format("150"); pDC->TextOut(160, 283, strTemp); strTemp.Format("200"); pDC->TextOut(210, 283, strTemp); 第 4 章 图像增强 · 123· strTemp.Format("255"); pDC->TextOut(265, 283, strTemp); // 绘制 X 轴刻度 for (i = 0; i < 256; i += 5) { if ((i & 1) == 0) { // 10 的倍数 pDC->MoveTo(i + 10, 280); pDC->LineTo(i + 10, 284); } else { // 5 的奇数倍数 pDC->MoveTo(i + 10, 280); pDC->LineTo(i + 10, 282); } } // 绘制 X 轴箭头 pDC->MoveTo(315,275); pDC->LineTo(320,280); pDC->LineTo(315,285); // 绘制 Y 轴箭头 pDC->MoveTo(10,10); pDC->LineTo(5,15); pDC->MoveTo(10,10); pDC->LineTo(15,15); // 直方图中最大计数值 LONG lMaxCount = 0; // 计算最大计数值 for (i = 0; i <= 255; i ++) { // 判断是否大于当前最大值 if (m_nHist[i] > lMaxCount) { // 更新最大值 lMaxCount = m_nHist[i]; } } Visual C++数字图像获取、处理及实践应用 · 124· // 输出最大计数值 pDC->MoveTo(10, 25); pDC->LineTo(14, 25); strTemp.Format("%d", lMaxCount); pDC->TextOut(11, 26, strTemp); // 声名画笔对象 CPen* pPenBlue = new CPen; // 创建蓝色画笔(用于绘制直方图) pPenBlue->CreatePen(PS_SOLID, 1, RGB(0,0,255)); // 选入蓝色画笔 pDC->SelectObject(pPenBlue); // 判断是否存在计数值 if(lMaxCount > 0){ // 绘制直方图 for (i = 0; i <= 255; i ++) { pDC->MoveTo(i + 10, 280); pDC->LineTo(i + 10, 281 - (int) (m_nHist[i] * 256 / lMaxCount)); } } // 恢复以前的画笔 pDC->SelectObject(pOldPen); // 删除新的画笔 delete pPenRed; delete pPenBlue; } 接下来,在“查看”菜单中添加一个名为“显示直方图”的菜单项,如图 4-9 所示。 图 4-9 添加“显示直方图”菜单项 在类 CimageProcessingView 中添加该菜单点击事件的处理程序。需要注意的是,灰度直方图并不对 彩色进行处理。如果要处理彩色的话,一般都是分开各个通道进行的。因此,在程序中添加了判断是否 为 256 色的代码段,在以后的程序中,也有类似的情况,后面就不一一解释了。 /************************************************************************* 第 4 章 图像增强 · 125· * * \函数名称: * OnViewHistogram() * * \输入参数: * 无 * * \返回值: * 无 * * \说明: * 查看直方图,弹出直方图显示界面 * ************************************************************************* */ void CImageProcessingView::OnViewHistogram() { // 获取文档 CImageProcessingDoc* pDoc = GetDocument(); // DIB 的颜色数目 int nColorTableEntries; nColorTableEntries = pDoc->m_pDibInit->m_nColorTableEntries; // 判断是否是 8bpp 位图(这里只处理 8bpp 位图) if ( nColorTableEntries != 256) { // 提示用户,不再进行处理 MessageBox(" 目 前 只 支 持 查 看 256 色 位 图 灰 度 直 方 图 ! ", " 系 统 提 示 " , MB_ICONINFORMATION | MB_OK); // 返回 return; } // 更改光标形状 BeginWaitCursor(); // 创建对话框 CDlgHistShow dlgHistShow; // 初始化变量值 dlgHistShow.m_pDib = pDoc->m_pDibInit; Visual C++数字图像获取、处理及实践应用 · 126· // 显示对话框 if (dlgHistShow.DoModal() != IDOK) { return; // 返回 } // 恢复光标 EndWaitCursor(); return; // 返回 } 运行上述代码,可以看到 Lena 图像的灰度直方图,如图 4-10 所示。其中(a)为原图像,(b)为直方图 显示结果。 (a) (b) 图 4-10 运行显示直方图结果 3. 直方图均衡 直方图均衡的理论已经在前面进行了叙述,下面给出的函数 HistogramEqualize()利用直方图均衡实 现图像的增强,其代码如下。 /************************************************************************* * * \函数名称: * HistogramEqualize() * * \输入参数: * CDib * pDib - 指向 CDib 类的指针,含有原始图像信息 * * \返回值: * BOOL - 成功则返回 TRUE,否则返回 FALSE * * \说明: * 该函数对指定的图像进行直方图均衡化处理 * ************************************************************************* 第 4 章 图像增强 · 127· */ BOOL HistogramEqualize(CDib* pDib) { // 指向源图像的指针 unsigned char* lpSrc; // 临时变量 int nTemp; // 循环变量 int i,j; // 累积直方图,即灰度映射表 BYTE byMap[256]; // 直方图 int nCount[256]; // 图像的高度和宽度 CSize sizeImage; sizeImage = pDib->GetDimensions(); // 获得图像数据存储的高度和宽度 CSize SizeSaveImage; SizeSaveImage = pDib->GetDibSaveDim(); // 重置计数为 0 for (i = 0; i < 256; i ++) { // 清零 nCount[i] = 0; } // 计算各个灰度值的计数,即得到直方图 for (i = 0; i < sizeImage.cy; i ++) { for (j = 0; j < sizeImage.cx; j ++) { lpSrc = (unsigned char *)pDib->m_lpImage + SizeSaveImage.cx * i + j; // 计数加 1 nCount[*(lpSrc)]++; } } Visual C++数字图像获取、处理及实践应用 · 128· // 计算累积直方图 for (i = 0; i < 256; i++) { // 初始为 0 nTemp = 0; for (j = 0; j <= i ; j++) { nTemp += nCount[j]; } // 计算对应的新灰度值 byMap[i] = (BYTE) (nTemp * 255 / sizeImage.cy / sizeImage.cx); } // 每行 for(i = 0; i < sizeImage.cy; i++) { // 每列 for(j = 0; j < sizeImage.cx; j++) { // 指向 DIB 第 i 行,第 j 个像素的指针 lpSrc = (unsigned char*)pDib->m_lpImage + pDib->GetPixelOffset(i,j); // 计算新的灰度值 *lpSrc = byMap[*lpSrc]; } } // 返回 return TRUE; } 在菜单“图像增强”中添加菜单项“直方图均衡”,如图 4-11 所示。 图 4-11 直方图均衡菜单 在类 CimageProcessingView 中添加“直方图均衡”的点击事件处理程序,其代码如下。 void CImageProcessingView::OnEnhanceHistequ() 第 4 章 图像增强 · 129· { // 直方图均衡 // 获取文档 CImageProcessingDoc* pDoc = GetDocument(); // 判断是否是 8-bpp 位图(这里只处理 8-bpp 位图的直方图均衡,其他的可以类推) if(pDoc->m_pDibInit->m_nColorTableEntries != 256) { // 提示用户 MessageBox("目前只支持 256 色位图的伪彩色变换!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } // 更改光标形状 BeginWaitCursor(); // 调用 HistogramEqualize()函数进行直方图均衡 HistogramEqualize(pDoc->m_pDibInit); // 设置脏标记 pDoc->SetModifiedFlag(TRUE); // 更新视图 pDoc->UpdateAllViews(NULL); // 恢复光标 EndWaitCursor(); } 运行上述代码,运行结果如图 4-12 所示。其中(a)为原图像,(b)为直方图均衡后的图像。 (a) (b) 图 4-12 直方图均衡运行结果 Visual C++数字图像获取、处理及实践应用 · 130· Á 4.2 图像平滑 图像在生成和传输过程中常受到各种噪声的干扰和影响,使图像质量下降。为了抑制噪声改善图像质量, 必须对图像进行平滑处理,这可在空域或频域中进行。在平滑噪声时应尽量不损害图像中边沿和各种细节。 对于滤除图像中的噪声,人们已经提出了很多的方法。通常,将数字图像的平滑技术划分为两类。 一类是全局处理,即对噪声图像的整体或大的块进行校正以得到平滑的图像。例如在变换域中使用 Wiener 滤波、最小二乘滤波等。使用这些技术需要知道信号和噪声的统计模型。但对于大多数图像而言, 人们不知道或不可能用简单的随机过程精确地描述统计模型,而且,这些技术计算量也相当大。另一类 平滑技术是对噪声图像使用局部算子。当对某一像素进行平滑处理时,仅对它的局部小邻域的一些像素 加以运算,其优点是计算效率高,而且可以多个像素并行处理。因此可实现实时或者准实时处理。 在介绍图像平滑之前,首先介绍模板操作的概念。 4.2.1 模板操作 模板操作是数字图像处理中常用的运算方法,在以后介绍的很多图像处理技术中都要用到。 对于最简单的局部平均平滑算法,即非加权邻域平均。它均等地对待邻域中地每个像素。设图像中 某像素的灰度值为 f(x, y),它的邻域 S 为 N×N,点集的总数为 M,则平滑后这点的灰度值为 ^ 1 , 1(,)(,) i j S f x y f i jM ∈ = ∑ (4-13) 对于这样操作,可以用模板操作来表示。为了叙述方便,设 N=3,则可以用如下的表示方法来表示 此操作 1 1 1 1 * 1 1. 19 1 1 1          (4-14) 这种表示方法有点象矩阵,我们称其为模板(template)。中间的黑点表示中心元素,即,用哪个元素 做为处理后的元素。例如[]2. 1 表示将自身的 2 倍加上右边的元素作为新值,而[]2 1. 表示将自身加上 左边元素的 2 倍作为新值。 通常,模板不允许移出边界,所以结果图像会比原图小,例如模板是 1. 0 0 1 ,原图是  1 1 1 1 1 2 2 2 2 2 3 3 3 3 3 4 4 4 4 4 ,经过模板操作后的图像为  3 3 3 3 x 5 5 5 5 x 7 7 7 7 x x x x x x ,数字代表灰度,x 表示边界上无法进行模 板操作的点,通常的做法是复制原图的灰度,不进行任何处理。 模板操作实现的实际上就是邻域运算(Neighborhood Operation),即,某个像素点的结果不仅和本像 素灰度有关,而且和其邻域点的值有关。在以后介绍的细化算法中,我们还将接触到邻域运算。模板运 算的数学涵义是卷积(或互相关)运算,你不需要知道卷积的确切概念,有这么一个概念就可以了。 模板运算在图像处理中经常用到,可以看出,它是一项非常耗时的运算。以 1/16 *      1 2 1 2 4. 2 1 2 1 为例, 每个像素完成一次模板操作要用 9 次乘法,8 次加法,1 次除法。对于一幅 N*N(宽度*高度)的图像来说,第 4 章 图像增强 · 131· 就是 9N2 次乘法,8N2 次加法和 N2 次除法,算法复杂度为 O(N2 ),这对于大图像来说,这种运算是 非常可怕的。所以,一般常用的模板并不大,如 3*3,4*4。有很多专用的图像处理系统,用硬件来完成 模板运算,大大提高了处理速度。另外,可以设法将 2 维模板运算转换成 1 维模板运算,这对速度的提 高也是非常可观的。例如,上面例子中的模板可以分解成一个水平模板和一个竖直模板,即, 1/16 *      1 2 1 2 4. 2 1 2 1 =1/4 * []1 2. 1 * 1/4 *      1 2. 1 =1/16 * []1 2. 1 *      1 2. 1 (4-15) 我们来验证一下 设图像为  2 2 2 2 3 2 3 3 4 6 4 5 5 6 6 6 ,经过处理后变为  x x x x x 3 3.06 x x 4.56 4.56 x x x x x ,经过处理后变为  x x x x x 3 3.06 x x 4.56 4.56 x x x x x , 两者完全一样。如果计算时不考虑周围一圈的像素,前者做了 4*(9 次乘法,8 次加法,1 次除法),共 36 次乘法,32 次加法,4 次除法;后者做了 4*(3 次乘法,2 次加法)+4*(3 次乘法,2 次加法)+4 次 除法,共 24 次乘法,16 次加法,4 次除法,运算简化了不少,如果是大图,效率的提高将是非常显著的。 平滑模板的思想是通过一点和周围 8 个点的平均来去除突然变化的点,从而滤掉一定的噪声,其代 价是图像有一定程度的模糊。上面提到的模板,就是一种平滑模板,称之为 Box 模板。Box 模板虽然考 虑了邻域点的作用,但并没有考虑各点位置的影响,对于所有的 9 个点都一视同仁,所以平滑的效果并 不理想。实际上我们可以想象,离某点越近的点对该点的影响应该越大,为此,我们引入了加权系数, 将原来的模板改造成 1/16 *      1 2 1 2 4. 2 1 2 1 (4-16) 可以看出,距离越近的点,加权系数越大。 新的模板其实也是一个常用的平滑模板,称为高斯(Gauss)模板。为什么叫这个名字,这是因为这个 模板是通过采样 2 维高斯函数得到的。 设图像为  2 2 2 2 3 2 3 3 4 6 4 5 5 6 6 6 ,分别用两种平滑模板处理(周围元素直接从原图拷贝),结果如下: 采用模板 1:  2 2 2 2 3 3.11 3.22 3 4 4.33 4.56 5 5 6 6 6 采用高斯模板:  2 2 2 2 3 3 3.06 3 4 4.56 4.56 5 5 6 6 6 可以看到,原图中出现噪声的区域是第 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 大了。而采用高斯模板没有出现这些问题,究其原因,就是因为它考虑了位Visual C++数字图像获取、处理及实践应用 · 132· 置的影响。 下面给出了一个通用的图像模板操作函数。该函数的实现代码如下: /************************************************************************* * * \函数名称: * GeneralTemplate() * * \输入参数: * CDib * pDib - 指向 CDib 类的指针,含有原始图像信息 * int nTempWidth - 模板的宽度 * int nTempHeight - 模板的高度 * int nTempCenX - 模板中心的 X 坐标(相对于模板) * int nTempCenY - 模板中心的 Y 坐标(相对于模板) * double* pdbTemp - 模板数组的指针 * double* dbCoef - 模板的系数 * * \返回值: * BOOL - 成功则返回 TRUE,否则返回 FALSE * * \说明: * 该函数用指定的模板对 pDib 指向的图像进行模板操作。模板的定义了宽度,高度, * 中心坐标和系数,模板的数据存放在 pdbTemp 中。对图像进行模板操作后,仍 * 然存放在 pDib 指向的 CDib 对象中。需要注意的是,该函数只能处理 8 位的图像, * 否则,指向的数据将出错。 * ************************************************************************* */ BOOL GeneralTemplate(CDib* pDib, int nTempWidth, int nTempHeight, int nTempCenX, int nTempCenY, double* pdbTemp, double dbCoef) { // 临时存放图像数据的指针 LPBYTE lpImage; // 循环变量 int i,j,k,l; // 指向源图像的指针 unsigned char* lpSrc; // 指向要复制区域的指针 unsigned char* lpDst; // 计算结果 第 4 章 图像增强 · 133· double dbResult; // 图像的高度和宽度 CSize sizeImage; sizeImage = pDib->GetDimensions(); // 获得图像数据存储的尺寸 int nSizeImage; nSizeImage = pDib->GetSizeImage(); // 给临时存放数据分配内存 lpImage = (LPBYTE) new char[nSizeImage]; // 判断是否内存分配失败 if (lpImage == NULL) { // 分配内存失败 return FALSE; } // 将原始图像的数据拷贝到临时存放内存中 memcpy(lpImage, pDib->m_lpImage, nSizeImage); // 进行模板操作 // 行(除去边缘几行) for(i = nTempCenY ; i < sizeImage.cy - nTempHeight + nTempCenY + 1; i++) { // 列(除去边缘几列) for(j = nTempCenX; j < sizeImage.cx - nTempWidth + nTempCenX + 1; j++) { // 指向新 DIB 第 i 行,第 j 个像素的指针 lpDst = (unsigned char*)lpImage + pDib->GetPixelOffset(i,j); dbResult = 0; // 计算 for (k = 0; k < nTempHeight; k++) { for (l = 0; l < nTempWidth; l++) { // 指向 DIB 第 i-nTempCenY +k 行,第 j - nTempCenX + l 个像素的指针 lpSrc = (unsigned char*)pDib->m_lpImage + pDib->GetPixelOffset (i-nTempCenY+k, j-nTempCenX+l); Visual C++数字图像获取、处理及实践应用 · 134· // 保存像素值 dbResult += (* lpSrc) * pdbTemp[k * nTempWidth + l]; } } // 乘上系数 dbResult *= dbCoef; // 取绝对值 dbResult = (double ) fabs(dbResult); // 判断是否超过 255 if(dbResult > 255) { // 直接赋值为 255 * lpDst = 255; } else { // 赋值 * lpDst = (unsigned char) (dbResult + 0.5); } } } // 复制变换后的图像 memcpy(pDib->m_lpImage, lpImage, nSizeImage); // 释放内存 delete[]lpImage; // 返回 return TRUE; } 介绍了模板操作之后,下面再来讨论图像平滑的一些技术。 4.2.2 图像平滑技术 1. 局部平均法 局部平均法是一种直接在空域上进行平滑处理的技术。认为图像是由许多灰度恒定的小块组成,相 邻像素间存在很高的空间相关性,而噪声则是统计独立的。因此,可用像素邻域内的像素的平均灰度值第 4 章 图像增强 · 135· 代替该像素原来的灰度值,实现图像的平滑。 最简单的局部平均法就是非加权邻域平均,它均等地对待邻域中的每个像素。这个在讲述模板地时 候已经对此进行了介绍。该算法简单,计算速度快,但主要缺点是在降低噪声的同时使图像模糊,特别 在边沿和细节处。邻域越大,模糊程度越厉害。 如果对上述算法进行改进,可以用超限像素平滑法,即先将 ),(1 ^ yxf 和 ),( yxf 的绝对差值与选 定的阈值相比较,根据比较结果决定点(x, y)的最后灰度值。这种算法对抑制椒盐噪声比较有效,对保 护仅有微小灰度差的细节和纹理也有效。 另外,还有其他的一些改进算法,如灰度最相近的 K 个邻点平均法,梯度倒数加权平滑,最大均匀 性平滑,自适应滤波,局部统计滤波等等。读者可以参考相关的数字图像处理文献。 这些方法,我们都能容易地在上面叙述的模板操作函数的基础上进行实现。 在这里还需要重点介绍一下局部平均法中一种重要的方法-中值滤波法。 2. 中值滤波法 中值滤波也是一种局部平均平滑技术。它对脉冲干扰和椒盐的抑制效果好,在抑制随机噪声的同时 能使边沿减少模糊。 中值滤波法是是一种非线性的图像平滑方法,它对一个滑动窗口内的诸像素灰度排序,用其中值代 替窗口中心像素(x,y)原来的灰度(若窗口中有偶数个像素,则取两个中间值的平均)中值滤波是如何去除 噪声的呢?下面给出个例子进行说明。 原图 处理后的图 0000000 00000 0000000 00000 0011100 01110 0016100 01110 0011100 01110 0000000 00000 上图中左边是原图,数字代表该处的灰度。可以看出中间的 6 和周围的灰度相差很大,是一个噪声 点。经过 3*1 窗口(即水平 3 个像素取中间值)的中值滤波,得到右边那幅图,可以看出,噪声点被去 除了。 拿中值滤波和上面介绍的两种平滑模板做个比较,看看中值滤波有什么特点,我们以一维模板为例, 只考虑水平方向,大小为 3*1(宽*高)。Box 模板为 1/3 * (1 1. 1);Gauss 模板为 1/4 *(1 2. 1)。先 考察第一幅图: 原图 0 0 0 1 1 1 0 0 1/3 2/3 1 1 0 0 1/4 3/4 1 1 0 0 0 1 1 1 经 Box 模板处理后的图 经 Gauss 模板处理后的图 经中值滤波处理后的图 01/32/31 01/43/41 0011 1/9 1/32/3 8/9 1/121/3 2/311/12 01/32/31 1/12 1/3 2/3 11/12 1/16 5/16 11/16 15/16 0 1/4 3/4 1 0 1/32/3 1 0 1/4 3/4 1 00 11 从原图中不难看出左边区域灰度值低,右边区域灰度值高,中间有一条明显的边界,这是一类图像, 我们称之为 Step(就象灰度上了个台阶)。应用平滑模板,图像平滑了,但是也使边界模糊了。应用中Visual C++数字图像获取、处理及实践应用 · 136· 值滤波,就能很好地保持原来的边界。所以说,中值滤波的特点是保护图像边缘的同时去除噪声。 再看第二类图: 原图 -1 1 -1 1 -1 1/3 –1/3 1/3 –1/3 1/3 0 0 0 0 0 1 –1 1 –1 1 经Box模板处理后的图 经Gauss模板处理后的图 经中值滤波处理后的图 -1/3 1/3 –1/3 0 0 0 -1 1 –1 1/9 –1/91/9 000 1/3 –1/3 1/3 0 0 0 0 0 0 0 0 0 1/3 –1/3 1/3 0 0 0 1 –1 1 从原图中不难看出,有很多噪声点(灰度为正代表灰度值高的点,为负代表灰度值低的点),而且 是杂乱无章,随机分布的。这也是一类很典型的图,称之为高斯噪声。经过 Box 平滑,噪声的程度有所 下降。Gauss 模板对付高斯噪声非常有效。而中值滤波对于高斯噪声则无能为力。 再看第三类图: 原图 1 2 10 2 1 1 3 11 3 1 1 2 10 2 1 经Box模板处理后的图 经Gauss模板处理后的图 经中值滤波处理后的图 13/314/313/3 15/4615/4 222 517/35 9/279/2 333 13/314/313/3 15/4615/4 222 从原图中不难看出,中间的灰度要比两边高许多。这也是一类很典型的图,称之为 Impulse(脉冲)。 可见,中值滤波对 Impulse 噪声非常有效。 综合以上三类图,不难得出下面的结论:中值滤波容易去除孤立点,线的噪声同时保持图像的边缘, 它能很好地去除二值噪声,但对高斯噪声无能为力。 要注意的是,当窗口内噪声点的个数大于窗口宽度的一半时,中值滤波的效果不好。这是很显然的。 因此,正确选择窗口尺寸的大小是用好中值滤波的重要环节。一般很难事先确定最佳的窗口尺寸,需要 通过从小窗口到大窗口的试验,再从中选取最好的结果。 3. 频域中的平滑技术 图像的平滑除了可以在空域中进行外,也可以在频域中进行。频域中的平滑是一维信号低通滤波器 概念在二维图像中的直接推广。图像经过二维傅立叶变换后,噪声频谱一般位于空间频率较高的区域, 而图像本身的频率分量则处于空间频率较低的区域内,因此可以通过低通滤波器的方法,使高频分量受 到抑制,从而实现图像的平滑。滤波器的数学表达式为: ( ,)(,)(,)G u v H u v F u v= (4-17) 其中 F(u, v)为原图像的傅立叶变换,G(u, v)是平滑后图像的傅立叶变换,H(u, v)是滤波器的转移函 数。常用的低通滤波器有: (1) 理想低通滤波器 理想低通滤波器的转移函数为: 第 4 章 图像增强 · 137· 0 0 1 ( , )(,) 0 ( , ) D u v DH u v D u v D ≤=  > (4-18) 其中 0D 为截止频率, 2/122 )(),( vuvuD += 是点(u, v)到频率平面原点的距离。理想低通滤波器 虽然有陡峭的截至特性,但效果并不好。图像由于高频分量的滤除而变得模糊,同时还会产生振铃效应。 (2) Butterworth 滤波器 Butterworth 滤波器的转移函数为 2 0 1(,)(,)1 [ ] n H u v D u v D = + (4-19) 其中 0D 为截至频率,当 ),( vuD = 0D 和 n=1 时, ),( vuH 降为最大值的 1/2。n 为阶数,取正整 数。阶数 n 控制曲线的形状。由于转移特性曲线较为平滑,没有振铃效应,故图像的模糊将减少。 (3) 数形滤波器 指数形滤波器的转移函数为 Á (,)[] ( , ) e ÁD u v DH u v − = (4-20) 其中 0D 为截至频率,n 为阶数,当 ),( vuD = 0D 和 n=1 时, ),( vuH 降为最大值的 1/e。图像较 Butterworth 滤波器模糊一些,但没有振铃效应。 4.2.3 Visual C++编程实现 通过上面对通用模板操作和平滑技术的介绍,我们就可以在其基础上用 Visual C++进行实现。下 面的程序实现了局部平均平滑、中值滤波和频域滤波技术。在程序中,读者可以自定义模板,实现自己 的平滑算法。 1. 局部平均平滑编程实现 利用前面介绍的通用模板就可以轻松地实现局部平均平滑的操作。下面,我们先建立一个设置平滑 模板的对话框 IDD_DLG_ENHANCE_SMOOTH。在对话框中,设置了平均模板、高斯模板和自定义模板。 通过自定义模板,读者可以定义自己的局部平均模板算子。对此对话框创建一个新的类 CdlgSmooth。此 对话框的功能是设置局部平均平滑算子。其具体的代码实现如下所示。 (1)对话框头文件(DlgSmooth.h) #if !defined(AFX_DLGSMOOTH_H__C3C270A3_299B_4E53_ADC8_FEE54D770B97__INCLUDED_ ) #define AFX_DLGSMOOTH_H__C3C270A3_299B_4E53_ADC8_FEE54D770B97__INCLUDED_ #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 // DlgSmooth.h : header file Visual C++数字图像获取、处理及实践应用 · 138· // ///////////////////////////////////////////////////////////////////////////// // CDlgSmooth dialog class CDlgSmooth : public CDialog { // Construction public: // 更新显示 void UpdateEdit(); // 模板数组 double *m_pdbTemp; // 声名模板类型 int m_nTemType; CDlgSmooth(CWnd* pParent = NULL); // standard constructor // Dialog Data //{{AFX_DATA(CDlgSmooth) enum { IDD = IDD_DLG_ENHANCE_SMOOTH }; // 模板中心 X 坐标 int m_nSmthTemCenX; // 模板中心 Y 坐标 int m_nSmthTemCenY; // 模板系数 double m_dbSmthTemCoef; // 模板高度 int m_nSmthTemHeight; // 模板宽度 int m_nSmthTemWidth; //}}AFX_DATA // Overrides // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(CDlgSmooth) 第 4 章 图像增强 · 139· protected: virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support //}}AFX_VIRTUAL // Implementation protected: // Generated message map functions //{{AFX_MSG(CDlgSmooth) afx_msg void OnRadioAvertem(); afx_msg void OnRadioGuasstem(); afx_msg void OnRadioSelftem(); afx_msg void OnKillfocusEditSelftemHeight(); afx_msg void OnKillfocusEditSelftemWidth(); virtual void OnOK(); afx_msg void OnChangeEditSelftemWidth(); //}}AFX_MSG DECLARE_MESSAGE_MAP() }; //{{AFX_INSERT_LOCATION}} // Microsoft Visual C++ will insert additional declarations immediately before the previous line. #endif // !defined(AFX_DLGSMOOTH_H__C3C270A3_299B_4E53_ADC8_FEE54D770B97__INCLUDED_) (2)对话框文件(DlgSmooth.cpp) // DlgSmooth.cpp : implementation file // #include "stdafx.h" #include "ImageProcessing.h" #include "DlgSmooth.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif ///////////////////////////////////////////////////////////////////////////// // CDlgSmooth dialog CDlgSmooth::CDlgSmooth(CWnd* pParent /*=NULL*/) Visual C++数字图像获取、处理及实践应用 · 140· : CDialog(CDlgSmooth::IDD, pParent) { //{{AFX_DATA_INIT(CDlgSmooth) m_nTemType = 0; m_nSmthTemCenX = 0; m_nSmthTemCenY = 0; m_dbSmthTemCoef = 0.0; m_nSmthTemHeight = 0; m_nSmthTemWidth = 0; //}}AFX_DATA_INIT } void CDlgSmooth::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); //{{AFX_DATA_MAP(CDlgSmooth) DDX_Text(pDX, IDC_EDIT_SELFTEM_CEN_X, m_nSmthTemCenX); DDX_Text(pDX, IDC_EDIT_SELFTEM_CEN_Y, m_nSmthTemCenY); DDX_Text(pDX, IDC_EDIT_SELFTEM_COEF, m_dbSmthTemCoef); DDX_Text(pDX, IDC_EDIT_SELFTEM_HEIGHT, m_nSmthTemHeight); DDV_MinMaxInt(pDX, m_nSmthTemHeight, 2, 5); DDX_Text(pDX, IDC_EDIT_SELFTEM_WIDTH, m_nSmthTemWidth); DDV_MinMaxInt(pDX, m_nSmthTemWidth, 2, 5); DDX_Text(pDX, IDC_EDIT_SELFTEM_EL0 , m_pdbTemp[0 ]); DDX_Text(pDX, IDC_EDIT_SELFTEM_EL1 , m_pdbTemp[1 ]); DDX_Text(pDX, IDC_EDIT_SELFTEM_EL2 , m_pdbTemp[2 ]); DDX_Text(pDX, IDC_EDIT_SELFTEM_EL3 , m_pdbTemp[3 ]); DDX_Text(pDX, IDC_EDIT_SELFTEM_EL4 , m_pdbTemp[4 ]); DDX_Text(pDX, IDC_EDIT_SELFTEM_EL5 , m_pdbTemp[5 ]); DDX_Text(pDX, IDC_EDIT_SELFTEM_EL6 , m_pdbTemp[6 ]); DDX_Text(pDX, IDC_EDIT_SELFTEM_EL7 , m_pdbTemp[7 ]); DDX_Text(pDX, IDC_EDIT_SELFTEM_EL8 , m_pdbTemp[8 ]); DDX_Text(pDX, IDC_EDIT_SELFTEM_EL9 , m_pdbTemp[9 ]); DDX_Text(pDX, IDC_EDIT_SELFTEM_EL10 , m_pdbTemp[10]); DDX_Text(pDX, IDC_EDIT_SELFTEM_EL11 , m_pdbTemp[11]); DDX_Text(pDX, IDC_EDIT_SELFTEM_EL12 , m_pdbTemp[12]); DDX_Text(pDX, IDC_EDIT_SELFTEM_EL13 , m_pdbTemp[13]); DDX_Text(pDX, IDC_EDIT_SELFTEM_EL14 , m_pdbTemp[14]); DDX_Text(pDX, IDC_EDIT_SELFTEM_EL15 , m_pdbTemp[15]); DDX_Text(pDX, IDC_EDIT_SELFTEM_EL16 , m_pdbTemp[16]); DDX_Text(pDX, IDC_EDIT_SELFTEM_EL17 , m_pdbTemp[17]); DDX_Text(pDX, IDC_EDIT_SELFTEM_EL18 , m_pdbTemp[18]); DDX_Text(pDX, IDC_EDIT_SELFTEM_EL19 , m_pdbTemp[19]); DDX_Text(pDX, IDC_EDIT_SELFTEM_EL20 , m_pdbTemp[20]); 第 4 章 图像增强 · 141· DDX_Text(pDX, IDC_EDIT_SELFTEM_EL22 , m_pdbTemp[22]); DDX_Text(pDX, IDC_EDIT_SELFTEM_EL23 , m_pdbTemp[23]); DDX_Text(pDX, IDC_EDIT_SELFTEM_EL24 , m_pdbTemp[24]); //}}AFX_DATA_MAP } BEGIN_MESSAGE_MAP(CDlgSmooth, CDialog) //{{AFX_MSG_MAP(CDlgSmooth) ON_BN_CLICKED(IDC_RADIO_AVERTEM, OnRadioAvertem) ON_BN_CLICKED(IDC_RADIO_GUASSTEM, OnRadioGuasstem) ON_BN_CLICKED(IDC_RADIO_SELFTEM, OnRadioSelftem) ON_EN_KILLFOCUS(IDC_EDIT_SELFTEM_HEIGHT, OnKillfocusEditSelftemHeight) ON_EN_KILLFOCUS(IDC_EDIT_SELFTEM_WIDTH, OnKillfocusEditSelftemWidth) ON_EN_CHANGE(IDC_EDIT_SELFTEM_WIDTH, OnChangeEditSelftemWidth) //}}AFX_MSG_MAP END_MESSAGE_MAP() ///////////////////////////////////////////////////////////////////////////// // CDlgSmooth message handlers void CDlgSmooth::OnRadioAvertem() { // 设置 3×3 平均模板参数 m_nTemType = 0; m_nSmthTemHeight = 3; m_nSmthTemWidth = 3; m_nSmthTemCenX = 1; m_nSmthTemCenY = 1; m_dbSmthTemCoef = (double)1.0/9.0; // 设置 3×3 平均模板元素 m_pdbTemp[0] = 1.0; m_pdbTemp[1] = 1.0; m_pdbTemp[2] = 1.0; m_pdbTemp[3] = 0.0; m_pdbTemp[4] = 0.0; m_pdbTemp[5] = 1.0; m_pdbTemp[6] = 1.0; m_pdbTemp[7] = 1.0; m_pdbTemp[8] = 0.0; m_pdbTemp[9] = 0.0; m_pdbTemp[10] = 1.0; m_pdbTemp[11] = 1.0; m_pdbTemp[12] = 1.0; m_pdbTemp[13] = 0.0; m_pdbTemp[14] = 0.0; m_pdbTemp[15] = 0.0; Visual C++数字图像获取、处理及实践应用 · 142· m_pdbTemp[16] = 0.0; m_pdbTemp[17] = 0.0; m_pdbTemp[18] = 0.0; m_pdbTemp[19] = 0.0; m_pdbTemp[20] = 0.0; m_pdbTemp[21] = 0.0; m_pdbTemp[22] = 0.0; m_pdbTemp[23] = 0.0; m_pdbTemp[24] = 0.0; // 更新文本框状态 //UpdateEdit(); // 更新 UpdateData(FALSE); } void CDlgSmooth::OnRadioGuasstem() { // 3×3 高斯模板 m_nTemType = 1; m_nSmthTemHeight = 3; m_nSmthTemWidth = 3; m_nSmthTemCenX = 1; m_nSmthTemCenY = 1; m_dbSmthTemCoef = (double)(1.0/16.0); // 设置模板元素 m_pdbTemp[0] = 1.0; m_pdbTemp[1] = 2.0; m_pdbTemp[2] = 1.0; m_pdbTemp[3] = 0.0; m_pdbTemp[4] = 0.0; m_pdbTemp[5] = 2.0; m_pdbTemp[6] = 4.0; m_pdbTemp[7] = 2.0; m_pdbTemp[8] = 0.0; m_pdbTemp[9] = 0.0; m_pdbTemp[10] = 1.0; m_pdbTemp[11] = 2.0; m_pdbTemp[12] = 1.0; m_pdbTemp[13] = 0.0; m_pdbTemp[14] = 0.0; m_pdbTemp[15] = 0.0; 第 4 章 图像增强 · 143· m_pdbTemp[16] = 0.0; m_pdbTemp[17] = 0.0; m_pdbTemp[18] = 0.0; m_pdbTemp[19] = 0.0; m_pdbTemp[20] = 0.0; m_pdbTemp[21] = 0.0; m_pdbTemp[22] = 0.0; m_pdbTemp[23] = 0.0; m_pdbTemp[24] = 0.0; // 更新文本框状态 UpdateEdit(); // 更新 UpdateData(FALSE); } void CDlgSmooth::OnRadioSelftem() { // TODO: Add your control notification handler code here // 自定义模板 m_nTemType = 2; // 更新文本框状态 UpdateEdit(); } void CDlgSmooth::OnKillfocusEditSelftemHeight() { // TODO: Add your control notification handler code here // 更新 UpdateData(TRUE); // 更新文本框状态 UpdateEdit(); } void CDlgSmooth::OnKillfocusEditSelftemWidth() { // TODO: Add your control notification handler code here // 更新 UpdateData(TRUE); Visual C++数字图像获取、处理及实践应用 · 144· // 更新文本框状态 UpdateEdit(); } void CDlgSmooth::OnOK() { // 获取用户设置(更新) UpdateData(TRUE); // 判断设置是否有效 if ((m_nSmthTemCenX < 0) || (m_nSmthTemCenX > m_nSmthTemWidth - 1) || (m_nSmthTemCenY < 0) || (m_nSmthTemCenY > m_nSmthTemHeight - 1)) { // 提示用户参数设置错误 MessageBox("中心元素参数设置错误!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } // 更新模板元素数组(将有效元素放置在数组的前面) for (int i = 0; i < m_nSmthTemHeight; i++) { for (int j = 0; j < m_nSmthTemWidth; j++) { m_pdbTemp[i * m_nSmthTemWidth + j] = m_pdbTemp[i * 5 + j]; } } // 更新 UpdateData(FALSE); // 退出 CDialog::OnOK(); } void CDlgSmooth::OnChangeEditSelftemWidth() { // TODO: If this is a RICHEDIT control, the control will not // send this notification unless you override the CDialog::OnInitDialog() // function and call CRichEditCtrl().SetEventMask() // with the ENM_CHANGE flag ORed into the mask. 第 4 章 图像增强 · 145· // TODO: Add your control notification handler code here } void CDlgSmooth::UpdateEdit() { BOOL bEnable; // 循环变量 int i; int j; // 判断是不是自定义模板 if (m_nTemType == 2) { bEnable = TRUE; } else { bEnable = FALSE; } // 设置文本框可用状态 (CEdit *) GetDlgItem(IDC_EDIT_SELFTEM_HEIGHT)->EnableWindow(bEnable); (CEdit *) GetDlgItem(IDC_EDIT_SELFTEM_WIDTH)->EnableWindow(bEnable); (CEdit *) GetDlgItem(IDC_EDIT_SELFTEM_COEF)->EnableWindow(bEnable); (CEdit *) GetDlgItem(IDC_EDIT_SELFTEM_CEN_X)->EnableWindow(bEnable); (CEdit *) GetDlgItem(IDC_EDIT_SELFTEM_CEN_Y)->EnableWindow(bEnable); // IDC_EDIT_SELFTEM_EL0 等 ID 其实是一个整数,它的数值定义在 Resource.h 中定义。 // 设置模板元素文本框 bEnable 状态 for (i = IDC_EDIT_SELFTEM_EL0; i <= IDC_EDIT_SELFTEM_EL24; i++) { // 设置文本框不可编辑 (CEdit *) GetDlgItem(i)->EnableWindow(bEnable); } // 显示应该可见的模板元素文本框 for (i = 0; i < m_nSmthTemHeight; i++) { for (j = 0; j < m_nSmthTemWidth; j++) { // 设置文本框可见 Visual C++数字图像获取、处理及实践应用 · 146· (CEdit *) GetDlgItem(IDC_EDIT_SELFTEM_EL0 + i*5 + j)-> ShowWindow(SW_SHOW); } } // 隐藏应该不可见的模板元素文本框(前 m_nSmthTemHeight 行的后几列) for (i = 0; i < m_nSmthTemHeight; i++) { for (j = m_nSmthTemWidth; j < 5; j++) { // 设置不可见 (CEdit *) GetDlgItem(IDC_EDIT_SELFTEM_EL0 + i*5 + j)-> ShowWindow(SW_HIDE); } } // 隐藏应该不可见的模板元素文本框(后几行) for (i = m_nSmthTemHeight; i < 5; i++) { for (j = 0; j < 5; j++) { // 设置不可见 (CEdit *) GetDlgItem(IDC_EDIT_SELFTEM_EL0 + i*5 + j)-> ShowWindow(SW_HIDE); } } } 设置完对话框,下面在菜单“图像增强”下添加“局部平均平滑”菜单项,如图 4-13 所示,并在 CimageProcessingView 类中添加点击事件处理程序。 图 4-13 局部平均平滑菜单 点击事件处理程序代码如下所示: /************************************************************************* * * \函数名称: * OnEnhanceSmooth() 第 4 章 图像增强 · 147· * * \输入参数: * 无 * * \返回值: * 无 * * \说明: * 对图像进行平滑处理,并弹出平滑模板设置对话框 * ************************************************************************* */ void CImageProcessingView::OnEnhanceSmooth() { // TODO: Add your command handler code here // 图像平滑 // 获取文档 CImageProcessingDoc* pDoc = GetDocument(); // 模板高度 int nTempHeight; // 模板宽度 int nTempWidth; // 模板系数 double dbTempCoef; // 模板中心元素 X 坐标 int nTempCenX; // 模板中心元素 Y 坐标 int nTempCenY; // 模板元素数组 double pdbTemp[25]; // 判断是否是 8-bpp 位图(这里为了方便,只处理 8-bpp 位图的平滑,其他的可以类推) if(pDoc->m_pDibInit->m_nColorTableEntries != 256) //if (::DIBNumColors(lpDIB) != 256) { // 提示用户 MessageBox("目前只支持 256 色位图的平滑!", "系统提示" , Visual C++数字图像获取、处理及实践应用 · 148· MB_ICONINFORMATION | MB_OK); // 返回 return; } // 创建对话框 CDlgSmooth dlgSmth; // 给模板数组赋初值(为平均模板) pdbTemp[0] = 1.0; pdbTemp[1] = 1.0; pdbTemp[2] = 1.0; pdbTemp[3] = 0.0; pdbTemp[4] = 0.0; pdbTemp[5] = 1.0; pdbTemp[6] = 1.0; pdbTemp[7] = 1.0; pdbTemp[8] = 0.0; pdbTemp[9] = 0.0; pdbTemp[10] = 1.0; pdbTemp[11] = 1.0; pdbTemp[12] = 1.0; pdbTemp[13] = 0.0; pdbTemp[14] = 0.0; pdbTemp[15] = 0.0; pdbTemp[16] = 0.0; pdbTemp[17] = 0.0; pdbTemp[18] = 0.0; pdbTemp[19] = 0.0; pdbTemp[20] = 0.0; pdbTemp[21] = 0.0; pdbTemp[22] = 0.0; pdbTemp[23] = 0.0; pdbTemp[24] = 0.0; // 初始化对话框变量值 dlgSmth.m_nTemType = 0; dlgSmth.m_nSmthTemHeight = 3; dlgSmth.m_nSmthTemWidth = 3; dlgSmth.m_nSmthTemCenX = 1; dlgSmth.m_nSmthTemCenY = 1; dlgSmth.m_dbSmthTemCoef = (double) (1.0 / 9.0); dlgSmth.m_pdbTemp = pdbTemp; 第 4 章 图像增强 · 149· // 显示对话框,提示用户设定平移量 if (dlgSmth.DoModal() != IDOK) { // 返回 return; } // 获取用户设定的平移量 nTempHeight = dlgSmth.m_nSmthTemHeight; nTempWidth = dlgSmth.m_nSmthTemWidth; nTempCenX = dlgSmth.m_nSmthTemCenX; nTempCenY = dlgSmth.m_nSmthTemCenY; dbTempCoef = dlgSmth.m_dbSmthTemCoef; // 删除对话框 delete dlgSmth; // 更改光标形状 BeginWaitCursor(); // 调用 Template()函数平滑 DIB if (GeneralTemplate(pDoc->m_pDibInit, nTempWidth, nTempHeight, nTempCenX, nTempCenY, pdbTemp, dbTempCoef)) { // 设置脏标记 pDoc->SetModifiedFlag(TRUE); // 更新视图 pDoc->UpdateAllViews(NULL); } else { // 提示用户 MessageBox("分配内存失败!", "系统提示" , MB_ICONINFORMATION | MB_OK); } // 恢复光标 EndWaitCursor(); } 运行上述代码,利用如图 4-14 所示的参数设置,结果显示如图 4-15 所示。其中(a)为原图像,(b) 为经过局部平均平滑的运行结果。 Visual C++数字图像获取、处理及实践应用 · 150· 图 4-14 局部平均平滑参数设置 (a) (b) 图 4-15 局部平均平滑运行结果 2. 中值滤波编程实现 对于中值滤波,我们构建函数 MedianFilter()来实现,其实现代码如下所示。 /************************************************************************* * * 函数名称: * MedianFilter() * * \输入参数: * CDib * pDib - 指向 CDib 类的指针,含有原始图像信息 * int nTempWidth - 模板的宽度 * int nTempHeight - 模板的高度 * int nTempCenX - 模板中心的 X 坐标(相对于模板) * int nTempCenY - 模板中心的 Y 坐标(相对于模板) * * \返回值: * BOOL - 成功则返回 TRUE,否则返回 FALSE 第 4 章 图像增强 · 151· * * 说明: * 该函数对指定的 DIB 图像进行中值滤波。 * ************************************************************************/ BOOL MedianFilter(CDib* pDib, int nTempWidth, int nTempHeight, int nTempCenX, int nTempCenY) { // 临时存放图像数据的指针 LPBYTE lpImage; // 循环变量 int i,j,k,l; // 指向源图像的指针 unsigned char* lpSrc; // 指向要复制区域的指针 unsigned char* lpDst; // 图像的高度和宽度 CSize sizeImage; sizeImage = pDib->GetDimensions(); // 获得图像数据存储的尺寸 int nSizeImage; nSizeImage = pDib->GetSizeImage(); // 指向滤波器数组的指针 unsigned char* pUnchFltValue; // 给临时存放数据分配内存 lpImage = (LPBYTE) new char[nSizeImage]; // 判断是否内存分配失败 if (lpImage == NULL) { // 返回 return FALSE; } // 将原始图像的数据拷贝到临时存放内存中 memcpy(lpImage, pDib->m_lpImage, nSizeImage); Visual C++数字图像获取、处理及实践应用 · 152· // 暂时分配内存,以保存滤波器数组 pUnchFltValue = new unsigned char[nTempHeight * nTempWidth]; // 判断是否内存分配失败 if (pUnchFltValue == NULL) { // 释放已分配内存 delete[]lpImage; // 返回 return FALSE; } // 开始中值滤波 // 行(除去边缘几行) for(i = nTempCenY; i < sizeImage.cy - nTempHeight + nTempCenY + 1; i++) { // 列(除去边缘几列) for(j = nTempCenX; j < sizeImage.cx - nTempWidth + nTempCenX + 1; j++) { // 指向新 DIB 第 i 行,第 j 个像素的指针 lpDst = (unsigned char*)lpImage + pDib->GetPixelOffset(i,j); //lpDst = (unsigned char*)lpImage + sizeImage.cx * (sizeImage.cy - 1 - i) + j; // 读取滤波器数组 for (k = 0; k < nTempHeight; k++) { for (l = 0; l < nTempWidth; l++) { // 指向 DIB 第 i - nTempCenY + k 行,第 j - nTempCenX + l 个像素的指针 lpSrc = (unsigned char*)pDib->m_lpImage + pDib-> GetPixelOffset(i-nTempCenY+k, j-nTempCenX+l); //lpSrc = (unsigned char*)pDib->m_lpImage + sizeImage.cx * (sizeImage.cy - 1 - i + nTempCenY - k) + j - nTempCenX + l; // 保存像素值 pUnchFltValue[k * nTempWidth + l] = *lpSrc; } } // 获取中值 //* lpDst = GetMedianValue(pUnchFltValue, nTempHeight * nTempWidth); } 第 4 章 图像增强 · 153· } // 复制变换后的图像 memcpy(pDib->m_lpImage, lpImage, nSizeImage); // 释放内存 delete[]lpImage; delete[]pUnchFltValue; // 返回 return TRUE; } 其中调用了函数 GetMedianValue(),这是一个排序的函数,以便能获得排序后的中值。 /************************************************************************* * * 函数名称: * GetMedianValue() * * 参数: * unsigned char * pUnchFltValue - 指向要获取中值的数组指针 * int iFilterLen - 数组长度 * * 返回值: * unsigned char - 返回指定数组的中值。 * * 说明: * 该函数用冒泡法对一维数组进行排序,并返回数组元素的中值。 * ************************************************************************/ unsigned char GetMedianValue(unsigned char * pUnchFltValue, int iFilterLen) { // 循环变量 int i; int j; // 中间变量 unsigned char bTemp; // 用冒泡法对数组进行排序 for (j = 0; j < iFilterLen - 1; j ++) { for (i = 0; i < iFilterLen - j - 1; i ++) Visual C++数字图像获取、处理及实践应用 · 154· { if (pUnchFltValue[i] > pUnchFltValue[i + 1]) { // 互换 bTemp = pUnchFltValue[i]; pUnchFltValue[i] = pUnchFltValue[i + 1]; pUnchFltValue[i + 1] = bTemp; } } } // 计算中值 if ((iFilterLen & 1) > 0) { // 数组有奇数个元素,返回中间一个元素 bTemp = pUnchFltValue[(iFilterLen + 1) / 2]; } else { // 数组有偶数个元素,返回中间两个元素平均值 bTemp = (pUnchFltValue[iFilterLen / 2] + pUnchFltValue[iFilterLen / 2 + 1]) / 2; } // 返回中值 return bTemp; } 仿照局部平均平滑,添加一个设置中值滤波模板设置的对话框 IDD_DLG_ENHANCE_MEDIAN。 并为此对话框创建新的类 CDlgMedian。在对话框中,还可以进行自定义模板。其具体实现代码如下所示。 (1)对话框头文件(DlgMedian.h) #if !defined(AFX_DLGMEDIAN_H__63E4C51C_3AFA_4A57_85C2_CB444BEEAE62__INCLUDED_ ) #define AFX_DLGMEDIAN_H__63E4C51C_3AFA_4A57_85C2_CB444BEEAE62__INCLUDED_ #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 // DlgMedian.h : header file // ///////////////////////////////////////////////////////////////////////////// // CDlgMedian dialog class CDlgMedian : public CDialog { 第 4 章 图像增强 · 155· // Construction public: CDlgMedian(CWnd* pParent = NULL); // standard constructor // Dialog Data //{{AFX_DATA(CDlgMedian) enum { IDD = IDD_DLG_ENHANCE_MEDIAN }; // 滤波器中心位置 X 坐标 int m_nFilterCenX; // 滤波器中心位置 Y 坐标 int m_nFilterCenY; // 滤波器高度 int m_nFilterHeight; // 滤波器宽度 int m_nFilterWidth; // 滤波器类型 int m_nFilterType; //}}AFX_DATA // Overrides // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(CDlgMedian) protected: virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support //}}AFX_VIRTUAL // Implementation protected: // Generated message map functions //{{AFX_MSG(CDlgMedian) afx_msg void OnRadioMedian2dimen(); afx_msg void OnRadioMedianHorizon(); afx_msg void OnRadioMedianVertical(); afx_msg void OnRadioMedianSelfdef(); virtual void OnOK(); //}}AFX_MSG Visual C++数字图像获取、处理及实践应用 · 156· DECLARE_MESSAGE_MAP() }; //{{AFX_INSERT_LOCATION}} // Microsoft Visual C++ will insert additional declarations immediately before the previous line. #endif // !defined(AFX_DLGMEDIAN_H__63E4C51C_3AFA_4A57_85C2_CB444BEEAE62__INCLUDED_) (2)对话框文件(DlgMedian.cpp) // DlgMedian.cpp : implementation file // #include "stdafx.h" #include "ImageProcessing.h" #include "DlgMedian.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif ///////////////////////////////////////////////////////////////////////////// // CDlgMedian dialog CDlgMedian::CDlgMedian(CWnd* pParent /*=NULL*/) : CDialog(CDlgMedian::IDD, pParent) { //{{AFX_DATA_INIT(CDlgMedian) // 初始化数据 m_nFilterCenX = 0; m_nFilterCenY = 0; m_nFilterHeight = 0; m_nFilterWidth = 0; m_nFilterType = -1; //}}AFX_DATA_INIT } void CDlgMedian::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); 第 4 章 图像增强 · 157· //{{AFX_DATA_MAP(CDlgMedian) DDX_Text(pDX, IDC_EDIT_MEDIAN_FILCENX, m_nFilterCenX); DDX_Text(pDX, IDC_EDIT_MEDIAN_FILCENY, m_nFilterCenY); DDX_Text(pDX, IDC_EDIT_MEDIAN_FILH, m_nFilterHeight); DDV_MinMaxInt(pDX, m_nFilterHeight, 1, 8); DDX_Text(pDX, IDC_EDIT_MEDIAN_FILW, m_nFilterWidth); DDV_MinMaxInt(pDX, m_nFilterWidth, 1, 8); DDX_Radio(pDX, IDC_RADIO_MEDIAN_VERTICAL, m_nFilterType); //}}AFX_DATA_MAP } BEGIN_MESSAGE_MAP(CDlgMedian, CDialog) //{{AFX_MSG_MAP(CDlgMedian) ON_BN_CLICKED(IDC_RADIO_MEDIAN_2DIMEN, OnRadioMedian2dimen) ON_BN_CLICKED(IDC_RADIO_MEDIAN_HORIZON, OnRadioMedianHorizon) ON_BN_CLICKED(IDC_RADIO_MEDIAN_VERTICAL, OnRadioMedianVertical) ON_BN_CLICKED(IDC_RADIO_MEDIAN_SELFDEF, OnRadioMedianSelfdef) //}}AFX_MSG_MAP END_MESSAGE_MAP() ///////////////////////////////////////////////////////////////////////////// // CDlgMedian message handlers void CDlgMedian::OnRadioMedian2dimen() { // 3×3 模板 m_nFilterType = 2; m_nFilterHeight = 3; m_nFilterWidth = 3; m_nFilterCenX = 1; m_nFilterCenY = 1; // 设置文本框不可用 (CEdit *) GetDlgItem(IDC_EDIT_MEDIAN_FILH)->EnableWindow(FALSE); (CEdit *) GetDlgItem(IDC_EDIT_MEDIAN_FILW)->EnableWindow(FALSE); (CEdit *) GetDlgItem(IDC_EDIT_MEDIAN_FILCENX)->EnableWindow(FALSE); (CEdit *) GetDlgItem(IDC_EDIT_MEDIAN_FILCENY)->EnableWindow(FALSE); // 更新 UpdateData(FALSE); } void CDlgMedian::OnRadioMedianHorizon() Visual C++数字图像获取、处理及实践应用 · 158· { // 1×3 模板 m_nFilterType = 1; m_nFilterHeight = 1; m_nFilterWidth = 3; m_nFilterCenX = 1; m_nFilterCenY = 0; // 设置文本框不可用 (CEdit *) GetDlgItem(IDC_EDIT_MEDIAN_FILH)->EnableWindow(FALSE); (CEdit *) GetDlgItem(IDC_EDIT_MEDIAN_FILW)->EnableWindow(FALSE); (CEdit *) GetDlgItem(IDC_EDIT_MEDIAN_FILCENX)->EnableWindow(FALSE); (CEdit *) GetDlgItem(IDC_EDIT_MEDIAN_FILCENY)->EnableWindow(FALSE); // 更新 UpdateData(FALSE); } void CDlgMedian::OnRadioMedianVertical() { // 3×1 模板 m_nFilterType = 0; m_nFilterHeight = 3; m_nFilterWidth = 1; m_nFilterCenX = 0; m_nFilterCenY = 1; // 设置文本框不可用 (CEdit *) GetDlgItem(IDC_EDIT_MEDIAN_FILH)->EnableWindow(FALSE); (CEdit *) GetDlgItem(IDC_EDIT_MEDIAN_FILW)->EnableWindow(FALSE); (CEdit *) GetDlgItem(IDC_EDIT_MEDIAN_FILCENX)->EnableWindow(FALSE); (CEdit *) GetDlgItem(IDC_EDIT_MEDIAN_FILCENY)->EnableWindow(FALSE); // 更新 UpdateData(FALSE); } void CDlgMedian::OnRadioMedianSelfdef() { // 自定义模板 (CEdit *) GetDlgItem(IDC_EDIT_MEDIAN_FILH)->EnableWindow(TRUE); (CEdit *) GetDlgItem(IDC_EDIT_MEDIAN_FILW)->EnableWindow(TRUE); (CEdit *) GetDlgItem(IDC_EDIT_MEDIAN_FILCENX)->EnableWindow(TRUE); (CEdit *) GetDlgItem(IDC_EDIT_MEDIAN_FILCENY)->EnableWindow(TRUE); 第 4 章 图像增强 · 159· } void CDlgMedian::OnOK() { // 获取用户设置(更新) UpdateData(TRUE); // 判断设置是否有效 if ((m_nFilterCenX < 0) || (m_nFilterCenX > m_nFilterWidth - 1) || (m_nFilterCenY < 0) || (m_nFilterCenY > m_nFilterHeight - 1)) { // 提示用户参数设置错误 MessageBox("参数设置错误!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } CDialog::OnOK(); } 设置完对话框后,在菜单“图像增强”下添加菜单项“中值滤波”,如图 4-16 所示。 图 4-16 中值滤波菜单 在 CimageProcessingView 类中添加点击事件处理程序,其代码如下所示。 /************************************************************************* * * \函数名称: * OnEnhanceMedian() * * \输入参数: * 无 * * \返回值: * 无 * * \说明: Visual C++数字图像获取、处理及实践应用 · 160· * 对图像进行中值滤波,并弹出平滑模板设置对话框 * ************************************************************************* */ void CImageProcessingView::OnEnhanceMedian() { // 中值滤波 // 获取文档 CImageProcessingDoc* pDoc = GetDocument(); // 滤波器的高度 int nFilterHeight; // 滤波器的宽度 int nFilterWidth; // 中心元素的 X 坐标 int nFilterCenX; // 中心元素的 Y 坐标 int nFilterCenY; // 判断是否是 8-bpp 位图(这里为了方便,只处理 8-bpp 位图的平滑,其他的可以类推) if(pDoc->m_pDibInit->m_nColorTableEntries != 256) { // 提示用户 MessageBox("目前只支持 256 色位图的平滑!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } // 创建对话框 CDlgMedian dlgMedian; // 初始化变量值 dlgMedian.m_nFilterType = 0; dlgMedian.m_nFilterHeight = 3; dlgMedian.m_nFilterWidth = 1; dlgMedian.m_nFilterCenX = 0; dlgMedian.m_nFilterCenY = 1; 第 4 章 图像增强 · 161· // 显示对话框,提示用户设定平移量 if (dlgMedian.DoModal() != IDOK) { // 返回 return; } // 获取用户的设定 nFilterHeight = dlgMedian.m_nFilterHeight; nFilterWidth = dlgMedian.m_nFilterWidth; nFilterCenX = dlgMedian.m_nFilterCenX; nFilterCenY = dlgMedian.m_nFilterCenY; // 删除对话框 delete dlgMedian; // 更改光标形状 BeginWaitCursor(); // 调用 MedianFilter()函数中值滤波 if (MedianFilter(pDoc->m_pDibInit, nFilterWidth, nFilterHeight, nFilterCenX, nFilterCenY )) { // 设置脏标记 pDoc->SetModifiedFlag(TRUE); // 更新视图 pDoc->UpdateAllViews(NULL); } else { // 提示用户 MessageBox("分配内存失败!", "系统提示" , MB_ICONINFORMATION | MB_OK); } // 恢复光标 EndWaitCursor(); } 运行上述代码,利用如图 4-17 的参数设置,其结果如图 4-18 所示。其中(a)为原图像,(b)为中值滤 波后的结果。 Visual C++数字图像获取、处理及实践应用 · 162· 图 4-17 图像中值滤波参数设置 (a) (b) 图 4-18 中值滤波运行结果 3. 频域中的平滑编程实现 前面介绍了频域中的图像平滑技术,下面我们利用 Visual C++来实现它。在这里,将给出利用理想 低通滤波和 Butterworth 滤波进行图像平滑的代码实现。 首先介绍理想低通滤波,下面给出的函数 LowPassFilterEnhance()实现了理想低通滤波,其具体代码 如下所示。 /************************************************************************* * * \函数名称: * LowPassFilterEnhance() * * \输入参数: * LPBYTE lpImage - 指向需要增强得图像数据 * int nWidth - 数据宽度 第 4 章 图像增强 · 163· * int nHeight - 数据高度 * int nRadius - 低通滤波的滤波半径 * * \返回值: * 无 * * \说明: * lpImage 是指向需要增强的数据指针。注意,这个指针指向的数据区不能是 CDib 指向的数 * 据区,因为 CDib 指向的数据区的每一行是 DWORD 对齐的。 * 经过低通滤波的数据存储在 lpImage 当中 * ************************************************************************* */ void LowPassFilterEnhance(LPBYTE lpImage, int nWidth, int nHeight, int nRadius) { // 循环控制变量 int y ; int x ; double dTmpOne ; double dTmpTwo ; // 傅立叶变换的宽度和高度(2 的整数次幂) int nTransWidth ; int nTransHeight; // 图像像素值 unsigned char unchValue; // 指向时域数据的指针 complex * pCTData ; // 指向频域数据的指针 complex * pCFData ; // 计算进行傅立叶变换的点数 (2 的整数次幂) dTmpOne = log(nWidth)/log(2); dTmpTwo = ceil(dTmpOne) ; dTmpTwo = pow(2,dTmpTwo) ; nTransWidth = (int) dTmpTwo ; // 计算进行傅立叶变换的点数 (2 的整数次幂) dTmpOne = log(nHeight)/log(2); dTmpTwo = ceil(dTmpOne) ; dTmpTwo = pow(2,dTmpTwo) ; Visual C++数字图像获取、处理及实践应用 · 164· nTransHeight = (int) dTmpTwo ; // 傅立叶变换的实部和虚部 double dReal; double dImag; // 低通滤波的半径不能超过频域的最大半径 if(nRadius>nTransWidth-1 || nRadius>nTransHeight-1) { return ; } // 分配内存 pCTData=new complex[nTransWidth * nTransHeight]; pCFData=new complex[nTransWidth * nTransHeight]; // 初始化 // 图像数据的宽和高不一定是 2 的整数次幂,所以 pCTData // 有一部分数据需要补 0 for(y=0; y(0,0); } } // 把图像数据传给 pCTData for(y=0; y(unchValue,0); } } // 傅立叶正变换 DIBFFT_2D(pCTData, nWidth, nHeight, pCFData) ; // 下面开始实施低通滤波,把所有大于 nRadius 的高频分量设置为 0 // 注意这里高频分量采用的范数不是欧式距离,而是无穷大范数 // || (u,v)-(0,0) || = max(|u|,|v|) for(y=nRadius; y(0,0); } } // 经过低通滤波的图像进行反变换 IFFT_2D(pCFData, pCTData, nWidth, nHeight); // 反变换的数据传给 lpImage for(y=0; yGetDocument(); CDib * pDib = pDoc->m_pDibInit; LPBITMAPINFOHEADER lpBMIH=pDib->m_lpBMIH; // 判断是否是 8-bpp 位图 if (lpBMIH->biBitCount != 8) { // 提示用户 MessageBox("目前只支持 256 色位图的图像分割!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } // 循环控制变量 int y; int x; CSize sizeImage = pDib->GetDimensions(); 第 4 章 图像增强 · 167· int nWidth = sizeImage.cx ; int nHeight= sizeImage.cy ; int nSaveWidth = pDib->GetDibSaveDim().cx; // 开辟内存,存储图像数据,该数据的存储不是 DWORD 对齐的 unsigned char * pUnchImage = new unsigned char[nWidth*nHeight]; for(y=0; ym_lpImage[y*nSaveWidth+x]; } } // 调用低通滤波函数进行图像增强 LowPassFilterEnhance(pUnchImage, nWidth, nHeight, nWidth/16) ; // 增强以后的图像拷贝到 pDib 中,进行显示 for(y=0; ym_lpImage[y*nSaveWidth+x] = pUnchImage[y*nWidth+x]; } } // 释放内存 delete []pUnchImage; pUnchImage = NULL ; // 恢复光标形状 EndWaitCursor(); // 设置脏标记 pDoc->SetModifiedFlag(TRUE); // 更新视图 pDoc->UpdateAllViews(NULL); } 运行上述代码,结果如图 4-20 所示。其中(a)为原图像,(b)为理想低通后的结果。 Visual C++数字图像获取、处理及实践应用 · 168· (a) (b) 图 4-20 理想低通平滑运行结果 下面介绍利用 Butterworth 低通滤波,Butterworth 低通滤波由于保留了部分高频,因此图像会看起 来比理想低通的效果好。下面给出的函数 ButterWorthLowPass()实现了 Butterworth 低通滤波,其具体实 现代码如下所示。 /************************************************************************* * * \函数名称: * ButterWorthLowPass() * * \输入参数: * LPBYTE lpImage - 指向需要增强得图像数据 * int nWidth - 数据宽度 * int nHeight - 数据高度 * int nRadius - ButterWorth 低通滤波的“半功率”点 * * \返回值: * 无 * * \说明: * lpImage 是指向需要增强的数据指针。注意,这个指针指向的数据区不能是 CDib 指向的数据 区 * 因为 CDib 指向的数据区的每一行是 DWORD 对齐的。 * 经过 ButterWorth 低通滤波的数据存储在 lpImage 当中。 * ************************************************************************* */ void ButterWorthLowPass(LPBYTE lpImage, int nWidth, int nHeight, int nRadius) { // 循环控制变量 int y ; int x ; 第 4 章 图像增强 · 169· double dTmpOne ; double dTmpTwo ; // ButterWorth 滤波系数 double H ; // 傅立叶变换的宽度和高度(2 的整数次幂) int nTransWidth ; int nTransHeight; double dReal ; double dImag ; // 图像像素值 unsigned char unchValue; // 指向时域数据的指针 complex * pCTData ; // 指向频域数据的指针 complex * pCFData ; // 计算进行傅立叶变换的点数 (2 的整数次幂) dTmpOne = log(nWidth)/log(2); dTmpTwo = ceil(dTmpOne) ; dTmpTwo = pow(2,dTmpTwo) ; nTransWidth = (int) dTmpTwo ; // 计算进行傅立叶变换的点数 (2 的整数次幂) dTmpOne = log(nHeight)/log(2); dTmpTwo = ceil(dTmpOne) ; dTmpTwo = pow(2,dTmpTwo) ; nTransHeight = (int) dTmpTwo ; // 分配内存 pCTData=new complex[nTransWidth * nTransHeight]; pCFData=new complex[nTransWidth * nTransHeight]; // 初始化 // 图像数据的宽和高不一定是 2 的整数次幂,所以 pCTData // 有一部分数据需要补 0 for(y=0; y(0,0); } } // 把图像数据传给 pCTData for(y=0; y(unchValue,0); } } // 傅立叶正变换 DIBFFT_2D(pCTData, nWidth, nHeight, pCFData) ; // 下面开始实施 ButterWorth 低通滤波 for(y=0; y(pCFData[y*nTransWidth + x].real()*H, pCFData[y*nTransWidth + x].imag()*H); } } // 经过 ButterWorth 低通滤波的图像进行反变换 IFFT_2D(pCFData, pCTData, nWidth, nHeight); // 反变换的数据传给 lpImage for(y=0; yGetDocument(); CDib * pDib = pDoc->m_pDibInit; LPBITMAPINFOHEADER lpBMIH=pDib->m_lpBMIH; // 判断是否是 8-bpp 位图 if (lpBMIH->biBitCount != 8) { // 提示用户 MessageBox("目前只支持 256 色位图的图像分割!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } // 循环控制变量 int y; int x; CSize sizeImage = pDib->GetDimensions(); int nWidth = sizeImage.cx ; int nHeight= sizeImage.cy ; int nSaveWidth = pDib->GetDibSaveDim().cx; // 开辟内存,存储图像数据,该数据的存储不是 DWORD 对齐的 unsigned char * pUnchImage = new unsigned char[nWidth*nHeight]; for(y=0; ym_lpImage[y*nSaveWidth+x]; } } // 调用 ButterWorth 低通滤波函数进行图像增强 ButterWorthLowPass(pUnchImage, nWidth, nHeight, nWidth/2) ; // 增强以后的图像拷贝到 pDib 中,进行显示 第 4 章 图像增强 · 173· for(y=0; ym_lpImage[y*nSaveWidth+x] = pUnchImage[y*nWidth+x]; } } // 释放内存 delete []pUnchImage; pUnchImage = NULL ; // 恢复光标形状 EndWaitCursor(); // 设置脏标记 pDoc->SetModifiedFlag(TRUE); // 更新视图 pDoc->UpdateAllViews(NULL); } 运行上述代码,结果如图 4-22 所示。其中(a)为原图像,(b)为经过 Butterworth 低通滤波后的运行结 果。 (a) (b) 图 4-22 Butterworth 低通滤波运行结果 4.3 图像锐化 图像在传输和变换过程中会受到各种干扰而退化,典型的就是图像模糊。而在图像的观看和识别中, 常常需要突出目标的轮廓或边缘信息。这样,看起来更惬意,对目标的识别也更容易一些。图像的锐化 可在空域或频域中进行。 Visual C++数字图像获取、处理及实践应用 · 174· 4.3.1 微分方法 图像的模糊相当于图像被平均或被积分,为实现图像的锐化,必须用它的反运算“微分”,加强高 频分量的作用,从而使图像轮廓清晰。由于模糊图像的特征(如边沿的走向等)各不相同,要进行锐化, 应该采用各向同性、具有旋转不变的线性微分算子。 图像处理中最常用的微分方法是求梯度。对于图像 f(x, y),它所在的梯度是一个矢量,定义为 (,)rad f xG f x y f y ∂   ∂ = ∂   ∂  (4-21) 点(x, y)梯度的幅度为梯度的模,即 2 2(,)| (,)| () ()rad f fGM x y G f x y x y ∂ ∂= = +∂ ∂ (4-22) 对数字图像用微分运算不方便,一般用差分来近似。常用的梯度差分有: 2 2 1/ 2(,) {[(,) ( 1,)] [(,) (, 1)]}GMxy fxy fx y fxy fxy= − + + − + (4-23) 为了运算简便,可以简化为 (,)|(,) ( 1,)||(,) (, 1)|GM x y f x y f x y f x y f x y≈ − + + − + (4-24) 或者利用 Roberts 梯度算子 2 2 1/ 2(,) {[(,) ( 1, 1)] [( 1,) (, 1)]}GMxy fxy fx y fx y fxy= − + + + + − + (4-25) Roberts 算子也可以简化为 (,)|(,) ( 1, 1)||( 1,) (, 1)|GM x y f x y f x y f x y f x y= − + + + + − + (4-26) 常用的梯度算子还有 Laplacian 算子。Lapalacian 算子是仿效属性上的 2 2 2 2 ji ∂ ∂+∂ ∂ ,它是用二阶差 分实现的。 2 (,) ( 1,) ( 1,) (, 1) (, 1)4(,)fij fi j fi j fij fij fij∇ = + + − + + + − − (4-27) 用模板算子来表示为: 0 1 0 1 -4 1 0 1 0 也可以推广 Laplacian 算子,考虑进对角线方向,这样它就是一个 8 邻域的算子,其模板为 1 1 1 1 -8 1 1 1 1 Laplacian 算子有两个缺点,一个是边沿的方向被丢失,另一个是 Laplacian 算子为二阶差分,双倍 加强了图中的噪声影响。优点是各向同性,即旋转不变。 第 4 章 图像增强 · 175· 梯度算子一旦算出后,就可以根据不同的需要生成不同的梯度增强图像。 最简单的就是用该点的梯度幅度代替此点的灰度。此方法的缺点是增强的图像仅仅显示灰度变换比 较陡峭的边沿轮廓,而灰度变化比较平缓或者比较均匀的地方则呈现黑色。 人们又提出了一些改进的方法,例如 (,)(,)(,)(,) GM x y GM x y Tg x y f x y ≥=   其他 (4-28) 其中 T 是一个非负的阈值。适当的选取 T,即可使明显的边沿轮廓得到突出,并且不会破坏原来灰 度变换比较平缓的背景。 还有一些其他的方法,例如将梯度幅度大于阈值的设置为某个指定的灰度或者梯度幅度,梯度幅度小于阈 值的像素则设置为某个指定灰度或者梯度幅度等,这里就不一一介绍了,读者可以参考数字图像处理文献。 4.3.2 高通滤波方法 图像中的边沿或线条与图像频谱中的高频分量相对应,因此,可以采用高通滤波的方法,使低频分 量得到抑制,从而达到增强高频分量,使图像的边沿或线条变得清晰,实现图像的锐化。 常用的高频滤波器有: 1. 理想高通滤波器 理想高通滤波器的转移函数为: 0 0 0 ( , )(,) 1 ( , ) D u v DH u v D u v D ≤=  > (4-29) 其中 0D 为截至频率, 2/122 )(),( vuvuD += 是点(u, v)到频率平面原点的距离。 2. Butterworth 滤波器 Butterworth 滤波器的转移函数为 20 1(,) 1 [ ](,) n H u v D D u v = + (4-30) 其中 0D 为截至频率,阶数 n 控制曲线的形状。 3. 指数形滤波器 指数形滤波器的转移函数为 Á[](,)(,) ÁD D u vH u v e − = (4-31) 其中 0D 为截至频率,n 为阶数。 4.3.3 Visual C++编程实现 通过上面对空域和频域中的图像锐化技术的介绍,我们就可以在其基础上进行 Visual C++的实现Visual C++数字图像获取、处理及实践应用 · 176· 了。下面的程序利用微分算子、理想高通滤波和 Butterworth 高通滤波进行图像锐化。 1. 微分算子图像锐化编程实现 利用前面介绍的微分算子就可以轻松地实现空域中图像锐化。在这里,我们利用 Lapacian 算子来进 行示例。 下面的函数 LinearSharp()利用 Lapacian 算子实现了图像的锐化,其具体代码实现如下所示。 /************************************************************************* * * \函数名称: * LinearSharpen() * * \输入参数: * LPBYTE lpImage - 指向图像数据得指针 * int nWidth - 图像数据宽度 * int nHeight - 图像数据高度 * * \返回值: * 无 * * \说明: * 线性锐化图像增强 * 本函数采用拉普拉斯算子对图像进行线性锐化 * 在原来图像上加上拉普拉斯算子锐化的信息 * ************************************************************************* */ void LinearSharpen (LPBYTE lpImage, int nWidth, int nHeight) { // 遍历图像的纵坐标 int y; // 遍历图像的横坐标 int x; double * pdGrad ; pdGrad = new double[nWidth*nHeight]; // 初始化为 0 memset(pdGrad, 0, nWidth*nHeight*sizeof(double)) ; // 设置模板系数 static int nWeight[3][3] ; nWeight[0][0] = -1 ; nWeight[0][1] = -1 ; 第 4 章 图像增强 · 177· nWeight[0][2] = -1 ; nWeight[1][0] = -1 ; nWeight[1][1] = 8 ; nWeight[1][2] = -1 ; nWeight[2][0] = -1 ; nWeight[2][1] = -1 ; nWeight[2][2] = -1 ; //这个变量用来表示 Laplacian 算子像素值 int nTmp[3][3]; // 临时变量 double dGrad; // 模板循环控制变量 int yy ; int xx ; for(y=1; yGetDocument(); CDib * pDib = pDoc->m_pDibInit; LPBITMAPINFOHEADER lpBMIH=pDib->m_lpBMIH; // 判断是否是 8-bpp 位图 if (lpBMIH->biBitCount != 8) { // 提示用户 MessageBox("目前只支持 256 色位图的图像分割!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } // 循环控制变量 int y; int x; CSize sizeImage = pDib->GetDimensions(); int nWidth = sizeImage.cx ; int nHeight= sizeImage.cy ; int nSaveWidth = pDib->GetDibSaveDim().cx; // 开辟内存,存储图像数据,该数据的存储不是 DWORD 对齐的 unsigned char * pUnchImage = new unsigned char[nWidth*nHeight]; for(y=0; ym_lpImage[y*nSaveWidth+x]; } } // 调用 LinearSharpen 函数进行图像锐化增强 LinearSharpen(pUnchImage, nWidth, nHeight) ; Visual C++数字图像获取、处理及实践应用 · 180· // 增强以后的图像拷贝到 pDib 中,进行显示 for(y=0; ym_lpImage[y*nSaveWidth+x] = pUnchImage[y*nWidth+x]; } } // 释放内存 delete []pUnchImage; pUnchImage = NULL ; // 恢复光标形状 EndWaitCursor(); // 设置脏标记 pDoc->SetModifiedFlag(TRUE); // 更新视图 pDoc->UpdateAllViews(NULL); } 运行上述代码,运行结果如图 4-24 所示。其中,(a)为原图像,(b)为经过图像锐化后的结果。 (a) (b) 图 4-24 微分算子图像锐化运行结果 2. 理想高通滤波图像锐化编程实现 利用前面介绍的理想高通滤波实现图像锐化。 下面的函数 HighPassFilterEnhance()利用理想高通滤波实现了图像锐化,其具体代码实现如下所示。 /************************************************************************* 第 4 章 图像增强 · 181· * * \函数名称: * HighPassFilterEnhance() * * \输入参数: * LPBYTE lpImage - 指向需要增强得图像数据 * int nWidth - 数据宽度 * int nHeight - 数据高度 * int nRadius - 高通滤波的滤波半径 * * \返回值: * 无 * * \说明: * lpImage 是指向需要增强的数据指针。注意,这个指针指向的数据区不能是 CDib 指向的数 * 据区,因为 CDib 指向的数据区的每一行是 DWORD 对齐的。 * 经过高通滤波的数据存储在 lpImage 当中 * ************************************************************************* */ void HighPassFilterEnhance(LPBYTE lpImage, int nWidth, int nHeight, int nRadius) { // 循环控制变量 int y ; int x ; double dTmpOne ; double dTmpTwo ; // 傅立叶变换的宽度和高度(2 的整数次幂) int nTransWidth ; int nTransHeight; // 图像像素值 unsigned char unchValue; // 指向时域数据的指针 complex * pCTData ; // 指向频域数据的指针 complex * pCFData ; double dReal; double dImag; Visual C++数字图像获取、处理及实践应用 · 182· // 计算进行傅立叶变换的点数 (2 的整数次幂) dTmpOne = log(nWidth)/log(2); dTmpTwo = ceil(dTmpOne) ; dTmpTwo = pow(2,dTmpTwo) ; nTransWidth = (int) dTmpTwo ; // 计算进行傅立叶变换的点数 (2 的整数次幂) dTmpOne = log(nHeight)/log(2); dTmpTwo = ceil(dTmpOne) ; dTmpTwo = pow(2,dTmpTwo) ; nTransHeight = (int) dTmpTwo ; // 滤波的半径不能超过频域的最大半径 if(nRadius>nTransWidth-1 || nRadius>nTransHeight-1) { return ; } // 分配内存 pCTData=new complex[nTransWidth * nTransHeight]; pCFData=new complex[nTransWidth * nTransHeight]; // 初始化 // 图像数据的宽和高不一定是 2 的整数次幂,所以 pCTData 有一部分数据需要补 0 for(y=0; y(0,0); } } // 把图像数据传给 pCTData for(y=0; y(unchValue,0); } } // 傅立叶正变换 DIBFFT_2D(pCTData, nWidth, nHeight, pCFData) ; 第 4 章 图像增强 · 183· // 下面开始滤波,把所有小于 nRadius 的低频分量设置为 0 // 采用的范数不是欧式距离,而是无穷大范数 // || (u,v)-(0,0) || = max(|u|,|v|) for(y=0; y(0,0); } } // 经过滤波的图像进行反变换 IFFT_2D(pCFData, pCTData, nWidth, nHeight); // 反变换的数据传给 lpImage for(y=0; yGetDocument(); CDib * pDib = pDoc->m_pDibInit; LPBITMAPINFOHEADER lpBMIH=pDib->m_lpBMIH; // 判断是否是 8-bpp 位图 第 4 章 图像增强 · 185· if (lpBMIH->biBitCount != 8) { // 提示用户 MessageBox("目前只支持 256 色位图的图像分割!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } // 循环控制变量 int y; int x; CSize sizeImage = pDib->GetDimensions(); int nWidth = sizeImage.cx ; int nHeight= sizeImage.cy ; int nSaveWidth = pDib->GetDibSaveDim().cx; // 开辟内存,存储图像数据,该数据的存储不是 DWORD 对齐的 unsigned char * pUnchImage = new unsigned char[nWidth*nHeight]; for(y=0; ym_lpImage[y*nSaveWidth+x]; } } // 调用高通滤波函数进行图像增强 HighPassFilterEnhance(pUnchImage, nWidth, nHeight, 50) ; // 增强以后的图像拷贝到 pDib 中,进行显示 for(y=0; ym_lpImage[y*nSaveWidth+x] = pUnchImage[y*nWidth+x]; } } // 释放内存 Visual C++数字图像获取、处理及实践应用 · 186· delete []pUnchImage; pUnchImage = NULL ; // 恢复光标形状 EndWaitCursor(); // 设置脏标记 pDoc->SetModifiedFlag(TRUE); // 更新视图 pDoc->UpdateAllViews(NULL); } 运行上述代码,运行结果如图 4-26 所示。其中(a)为原图像,(b)为理想高通滤波运行结果。 (a) (b) 图 4-26 理想高通图像锐化运行结果 3. Butterworth 高通滤波图像锐化编程实现 利用前面介绍的 Butterworth 高通滤波实现图像锐化。 下面的函数 ButterWorthHighPass()利用 Butterworth 高通滤波实现了图像锐化,其具体代码实现如下 所示。 /************************************************************************* * * \函数名称: * ButterWorthHighPass() * * \输入参数: * LPBYTE lpImage - 指向需要增强得图像数据 * int nWidth - 数据宽度 * int nHeight - 数据高度 * int nRadius - ButterWorth 高通滤波的“半功率”点 * 第 4 章 图像增强 · 187· * \返回值: * 无 * * \说明: * lpImage 是指向需要增强的数据指针。注意,这个指针指向的数据区不能是 * CDib 指向的数据区, 因为 CDib 指向的数据区的每一行是 DWORD 对齐的。 * 经过 ButterWorth 高通滤波的数据存储在 lpImage 当中。 * ************************************************************************* */ void ButterWorthHighPass(LPBYTE lpImage, int nWidth, int nHeight, int nRadius) { // 循环控制变量 int y ; int x ; double dTmpOne ; double dTmpTwo ; // ButterWorth 滤波系数 double H ; // 傅立叶变换的宽度和高度(2 的整数次幂) int nTransWidth ; int nTransHeight; double dReal ; double dImag ; // 图像像素值 unsigned char unchValue; // 指向时域数据的指针 complex * pCTData ; // 指向频域数据的指针 complex * pCFData ; // 计算进行傅立叶变换的点数 (2 的整数次幂) dTmpOne = log(nWidth)/log(2); dTmpTwo = ceil(dTmpOne) ; dTmpTwo = pow(2,dTmpTwo) ; nTransWidth = (int) dTmpTwo ; // 计算进行傅立叶变换的点数 (2 的整数次幂) dTmpOne = log(nHeight)/log(2); Visual C++数字图像获取、处理及实践应用 · 188· dTmpTwo = ceil(dTmpOne) ; dTmpTwo = pow(2,dTmpTwo) ; nTransHeight = (int) dTmpTwo ; // 分配内存 pCTData=new complex[nTransWidth * nTransHeight]; pCFData=new complex[nTransWidth * nTransHeight]; // 初始化 // 图像数据的宽和高不一定是 2 的整数次幂,所以 pCTData // 有一部分数据需要补 0 for(y=0; y(0,0); } } // 把图像数据传给 pCTData for(y=0; y(unchValue,0); } } // 傅立叶正变换 DIBFFT_2D(pCTData, nWidth, nHeight, pCFData) ; // 下面开始实施 ButterWorth 高通滤波 for(y=0; y(H*(pCFData[y*nTransWidth + x].real()), H*(pCFData[y*nTransWidth + x].imag()) ); } 第 4 章 图像增强 · 189· } // 经过 ButterWorth 高通滤波的图像进行反变换 IFFT_2D(pCFData, pCTData, nWidth, nHeight); // 反变换的数据传给 lpImage for(y=0; yGetDocument(); CDib * pDib = pDoc->m_pDibInit; LPBITMAPINFOHEADER lpBMIH=pDib->m_lpBMIH; // 判断是否是 8-bpp 位图 if (lpBMIH->biBitCount != 8) { // 提示用户 MessageBox("目前只支持 256 色位图的图像分割!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } // 循环控制变量 int y; int x; CSize sizeImage = pDib->GetDimensions(); int nWidth = sizeImage.cx ; int nHeight= sizeImage.cy ; int nSaveWidth = pDib->GetDibSaveDim().cx; // 开辟内存,存储图像数据,该数据的存储不是 DWORD 对齐的 unsigned char * pUnchImage = new unsigned char[nWidth*nHeight]; for(y=0; ym_lpImage[y*nSaveWidth+x]; } } // 调用 ButterWorth 高通滤波函数进行图像增强 ButterWorthHighPass(pUnchImage, nWidth, nHeight, nWidth/2) ; // 增强以后的图像拷贝到 pDib 中,进行显示 for(y=0; ym_lpImage[y*nSaveWidth+x] = pUnchImage[y*nWidth+x]; } } // 释放内存 delete []pUnchImage; pUnchImage = NULL ; // 恢复光标形状 EndWaitCursor(); // 设置脏标记 pDoc->SetModifiedFlag(TRUE); // 更新视图 pDoc->UpdateAllViews(NULL); } 运行上述代码,运行结果如图 4-28 所示。其中(a)为原图像,(b)为 Butterworth 高通滤波结果。 (a) (b) 图 4-28 Butterworth 高通滤波图像锐化运行结果 Visual C++数字图像获取、处理及实践应用 · 192· 4.4 伪彩色和假彩色增强 4.4.1 伪彩色和假彩色增强技术 人的视觉对彩色相当敏感。人眼一般能区分的灰度等级只有二十多个,但是能区分有不同联度、色 度和饱和度的几千种彩色。根据人的这个特点,可将彩色用于增强中,以提高图像的可鉴别性。 伪彩色增强是把黑白图像的个个不同灰度按照线性或者非线性的映射函数变换成不同的彩色,使图 像细节更容易辨认,目标更容易识别,也可将原来不是图像的数据先表示成黑白图像,再转换成彩色图 像达到增强目的。 可以有多种方式实现从灰度到彩色的变换,最简单的就是把黑白图像的灰度级别从 0 到 255 分成 255 个区间,给每个区间指定一种彩色。此方法比较简单直观,缺点是变换出的彩色有限。 从灰度到彩色的一种更具代表性的变换方式如图 4-29 所示。它是根据色度学的原理,将原图像 f(x ,y) 的灰度经过红绿蓝三种不同变换,变成三基色分量 R(x, y)、G(x, y),B(x, y),生成相应的彩色。彩色的 含量由变换函数的形状而定。 B(x, y) G(x, y) R (x, y) Tr( ) Tg( ) f(x, y) Tb( ) 图 4-29 伪彩色增强原理图 还有一种伪彩色增强并不是直接对灰度进行彩色变换,而是先将图像转换到频域,然后在频域用不 同特性的滤波器分离成三个独立的分量,再进行 Fourier 反变换得到三幅代表不同频率分量的单色图像, 再对这三幅图像进行进一步处理,并合成在一起,从而实现了彩色增强。 与伪彩色处理相似的另一种颜色处理方法是假彩色处理,即将一幅彩色图像中的每一像素的 RGB 值都映射到显示 RGB 空间的一个新点的过程。利用假彩色处理,可以将不可见光谱图像转化为可见光 谱图像,使人眼可以直接获得更多的图像信息。 伪彩色和假彩色处理都不改变像素的几何位置,而仅仅改变其颜色,提高人眼对图像的分辨能力, 这是一种很实用的图像增强技术,被广泛应用于遥感、医学图像处理中。 4.4.2 Visual C++编程实现 用 Visual C++编程实现伪彩色变换非常方便,实际上就是用指定的伪彩色调色板来替换当前图像的 调色板。其操作可分两步完成: (1) 按照伪彩色编码方法更改当前 DIB 的颜色表 (2) 生成 DIB 的调色板,并实现该调色板 下面给出了替换 DIB 颜色表的函数 ReplaceDIBColorTable(),其实现代码如下: /************************************************************************* 第 4 章 图像增强 · 193· * * 函数名称: * ReplaceDIBColorTable() * * 参数: * CDib * pDib - 指向 CDib 类的指针,含有原始图像信息 * LPBYTE pColorsTable - 伪彩色编码表 * * 返回值: * BOOL - 成功返回 TRUE,否则返回 FALSE。 * * 说明: * 该函数用指定的伪彩色编码表来替换指定的 DIB 图像的颜色表,参数 pColorsTable * 指向要替换的伪彩色编码表。 * ************************************************************************/ BOOL ReplaceDIBColorTable(CDib* pDib, LPBYTE pColorsTable) { // 循环变量 int i; // 颜色表的表项数 int nColorEntries; // 临时变量 LPRGBQUAD pDibQuad; // 指向 DIB 的颜色表 pDibQuad = (LPRGBQUAD) pDib->m_lpvColorTable; // 获取 DIB 中颜色表中的表项数 nColorEntries = pDib->m_nColorTableEntries; // 判断颜色数目是否是 256 色 if (nColorEntries == 256) { // 读取伪彩色编码,更新 DIB 颜色表 for (i = 0; i < nColorEntries; i++) { // 更新 DIB 调色板红色分量 pDibQuad->rgbRed = pColorsTable[i * 4]; // 更新 DIB 调色板绿色分量 Visual C++数字图像获取、处理及实践应用 · 194· pDibQuad->rgbGreen = pColorsTable[i * 4 + 1]; // 更新 DIB 调色板蓝色分量 pDibQuad->rgbBlue = pColorsTable[i * 4 + 2]; // 更新 DIB 调色板保留位 pDibQuad->rgbReserved = 0; pDibQuad++; } } // 如果不是 256 色的 DIB,则不进行处理 else return FALSE; // 返回 return TRUE; } 在进行伪彩色图像增强中,我们也需要添加一个对话框 IDD_DLG_ENHANCE_COLOR 来选择要进 行何种伪彩色增强。并创建了新的类 CDlgEhnColor 实现伪彩色的选择。其实现代码如下所示。 (1) 对话框头文件(DlgEhnColor.h) #if !defined(AFX_DLGENHCOLOR_H__02EEF6BD_CD04_41A1_87E3_461CC7008153__INCLUDE D_) #define AFX_DLGENHCOLOR_H__02EEF6BD_CD04_41A1_87E3_461CC7008153__INCLUDED_ #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 // DlgEnhColor.h : header file // ///////////////////////////////////////////////////////////////////////////// // CDlgEnhColor dialog class CDlgEnhColor : public CDialog { // Construction public: // 颜色名称字符串长度 int m_nNameLen; // 颜色名称字符串数组指针 LPSTR m_lpColorName; 第 4 章 图像增强 · 195· // 颜色数目 int m_nColorCount; // 当前选择的伪彩色编码表 int m_nColor; CDlgEnhColor(CWnd* pParent = NULL); // standard constructor // Dialog Data //{{AFX_DATA(CDlgEnhColor) enum { IDD = IDD_DLG_ENHANCE_COLOR }; CListBox m_lstColor; //}}AFX_DATA // Overrides // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(CDlgEnhColor) protected: virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support //}}AFX_VIRTUAL // Implementation protected: // Generated message map functions //{{AFX_MSG(CDlgEnhColor) virtual BOOL OnInitDialog(); afx_msg void OnDblclkListEnhColor(); virtual void OnOK(); //}}AFX_MSG DECLARE_MESSAGE_MAP() }; //{{AFX_INSERT_LOCATION}} // Microsoft Visual C++ will insert additional declarations immediately before the previous line. #endif // !defined(AFX_DLGENHCOLOR_H__02EEF6BD_CD04_41A1_87E3_461CC7008153__INCLUDED_) (2) 对话框文件(DlgEhnColor.cpp) // DlgEnhColor.cpp : implementation file // #include "stdafx.h" Visual C++数字图像获取、处理及实践应用 · 196· #include "ImageProcessing.h" #include "DlgEnhColor.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif ///////////////////////////////////////////////////////////////////////////// // CDlgEnhColor dialog CDlgEnhColor::CDlgEnhColor(CWnd* pParent /*=NULL*/) : CDialog(CDlgEnhColor::IDD, pParent) { //{{AFX_DATA_INIT(CDlgEnhColor) // NOTE: the ClassWizard will add member initialization here //}}AFX_DATA_INIT } void CDlgEnhColor::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); //{{AFX_DATA_MAP(CDlgEnhColor) DDX_Control(pDX, IDC_LIST_ENH_COLOR, m_lstColor); //}}AFX_DATA_MAP } BEGIN_MESSAGE_MAP(CDlgEnhColor, CDialog) //{{AFX_MSG_MAP(CDlgEnhColor) ON_LBN_DBLCLK(IDC_LIST_ENH_COLOR, OnDblclkListEnhColor) //}}AFX_MSG_MAP END_MESSAGE_MAP() ///////////////////////////////////////////////////////////////////////////// // CDlgEnhColor message handlers BOOL CDlgEnhColor::OnInitDialog() { // 循环变量 int i; // 调用默认 OnInitDialog 函数 CDialog::OnInitDialog(); 第 4 章 图像增强 · 197· // 添加伪彩色编码 for (i = 0; i < m_nColorCount; i++) { m_lstColor.AddString(m_lpColorName + i * m_nNameLen); } // 选中初始编码表 m_lstColor.SetCurSel(m_nColor); return TRUE; // return TRUE unless you set the focus to a control // EXCEPTION: OCX Property Pages should return FALSE } void CDlgEnhColor::OnDblclkListEnhColor() { // 双击事件,直接调用 OnOK()成员函数 OnOK(); } void CDlgEnhColor::OnOK() { // 用户单击确定按钮 m_nColor = m_lstColor.GetCurSel(); // 调用默认的 OnOK()函数 CDialog::OnOK(); } 利用对话框完成参数的设置后,在菜单“图像增强”中添加菜单项“伪彩色增强”,如图 4-30 所示。 图 4-30 伪彩色增强菜单项 在类 CimageProcessingView 中添加该菜单的点击事件处理程序。 void CImageProcessingView::OnEnhancePseudcolor() { // 伪彩色编码 Visual C++数字图像获取、处理及实践应用 · 198· // 获取文档 CImageProcessingDoc* pDoc = GetDocument(); // 保存用户选择的伪彩色编码表索引 int nColor; // 判断是否是 8-bpp 位图(这里为了方便,只处理 8-bpp 位图的平滑,其他的可以类推) if(pDoc->m_pDibInit->m_nColorTableEntries != 256) { // 提示用户 MessageBox("目前只支持 256 色位图的伪彩色变换!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } // 参数对话框 CDlgEnhColor dlgPara; // 初始化变量值 if (pDoc->m_nColorIndex >= 0) { // 初始选中当前的伪彩色 dlgPara.m_nColor = pDoc->m_nColorIndex; } else { // 初始选中灰度伪彩色编码表 dlgPara.m_nColor = 0; } // 指向名称数组的指针 dlgPara.m_lpColorName = (LPSTR) ColorScaleName; // 伪彩色编码数目 dlgPara.m_nColorCount = COLOR_SCALE_COUNT; // 名称字符串长度 dlgPara.m_nNameLen = sizeof(ColorScaleName) / COLOR_SCALE_COUNT; // 显示对话框,提示用户设定平移量 if (dlgPara.DoModal() != IDOK) 第 4 章 图像增强 · 199· { // 返回 return; } // 获取用户的设定 nColor = dlgPara.m_nColor; // 删除对话框 delete dlgPara; // 更改光标形状 BeginWaitCursor(); // 判断伪彩色编码是否改动 if (pDoc->m_nColorIndex != nColor) { // 调用 ReplaceColorPal()函数变换调色板 ReplaceDIBColorTable(pDoc->m_pDibInit, (LPBYTE)ColorsTable[nColor]); // 更新调色板 pDoc->m_pDibInit->MakePalette(); // 更新类成员变量 pDoc->m_nColorIndex = nColor; // 设置脏标记 pDoc->SetModifiedFlag(TRUE); // 更新视图 pDoc->UpdateAllViews(NULL); } // 恢复光标 EndWaitCursor(); } 运行上述代码,采用如图 4-31 所示的参数设置,其运行结果如图 4-32 所示。其中,(a)为原图像, (b)为伪彩色运行结果。 Visual C++数字图像获取、处理及实践应用 · 200· 图 4-31 伪彩色增强参数设置 (a) (b) 图 4-32 伪彩色增强运行结果 第第 55 章章 图图像像复复原原 成像过程中的“退化”指由于成像系统各种因素的影响,使得图像质量降低。在成像系统中,引起图像 退化的原因很多。例如,成像系统的散焦,成像设备与物体的相对运动,成像器材的固有缺陷以及外部干扰 等。 与图像增强相似,图像复原的目的也是改善图像的质量。图像复原可以看作图像退化的逆过程,是将图 像退化的过程加以估计,建立退化的数学模型后,补偿退化过程造成的失真,以便获得未经干扰退化的原始 图像或原始图像的最优估值,从而改善图像质量。在图像退化确知的情况下,图像退化的逆过程是有可能进 行。但实际情况往往是退化过程并不知晓,这种复原称为盲目复原。由于图像模糊的同时,噪声和干扰也会 同时存在,这也为复原过程也带来了困难和不确定性。 因此,本章讨论的基础是退化的数学模型。由于图像复原是寻求在一定优化准则下的原始图像的最优估 计。因此,不同的优化准则会获得不同的图像复原。 5.1 图像退化的数学模型 典型的图像系统由图像产生系统、检测器和记录器构成。 由于不同图像产生系统的不同频率响应、图像检测和记录系统的非线性变换,以及不同的附加噪声, 造成不同的图像退化模型。 图像的退化和复原模型如图 5-1 所示。图像的退化由系统特性和噪声两部分引起,复原通过设计复原 滤波器(逆滤波)来实现。 图 5-1 图像的退化复原模型 图 5-1 中的 f(x,y)代表一幅静止、二维的图像,在外来的噪声 n(x,y)的作用下,通过系统 h(x,y)以后,使 其退化成图像 w(x,y)。 对于退化图像的复原,一般可采用两种方法。一种方法适用于对于图像缺乏先验知识的情况,此时可 对退化过程建立模型,进行描述,并进而寻找—种去除或削弱其影响的过程。由于这种方法试图估计图像被 一些特性相对已知的退化过程影响以前的情况,因此它是一种估计方法。另一方面,如果对于原始图像有足 够的先验知识,则对原始图像建立一个数学模型并根据它对退化图像进行拟合会更有效。例如,假设已知图 像仅含有确定大小的圆形物体(如星辰、颗粒、细胞等),这样由于仅是原始图像很少的几个参数(数目、位置、 幅度等)未知,因此这是一个检测问题。 在进行图像复原时,还有许多其他选择。首先,问题既可以用连续数学,也可以用离散数学进行处理。 其次,处理既可在空域,也可在频域进行。此外,当复原必须用数字方法进行时,处理既可通过空域的卷积, 也可通过频域的相乘来实现。 Visual C++数字图像获取、处理及实践应用 · 202· Á 5.1.1 退化系统的基本定义 系统按不同的性质可以分为线性系统和非线性系统;时变系统和非时变系统;集中参数系统和分布参数 系统;连续系统和离散系统等。在本章中,我们考虑的主要是线性时不变系统。 设有一成像系统,当输入为 f(x,y)时,输出为 g(x,y),即: [ ]( ,)(,)g x y H f x y= (5-1) 如果系统是线性的,设有输入信号 1 2( ,)(,)f x y f x y、 ,对应的输出信号为 1 2( ,)(,)g x y x y、g , 通过系统后有下式成立: [ ] [ ][] [][] 1 1 2 2 1 1 2 2 1 1 2 2 1 1 2 2 (,)(,) ( ,)(,) ( ,)(,) ( ,)(,) H k f x y k f x y H k f x y H k f x y k H f x y k H f x y k g x y k g x y ⋅ + ⋅ = ⋅ + ⋅ = ⋅ + ⋅ = ⋅ + ⋅ (5-2) 式 5-2 说明,如果 H 为线性系统,那么两个信号输入之和的响应等于两个信号响应输出之和。 如果一个系统的参数不随着时间变化而变化,则称之为时不变系统。如果一个系统既满足线性又满足 时不变的条件,那么这个系统就是一个线性时不变系统。相应地,如果图像中的任何一点通过一个系统的响 应只取决于该点的输入值,而与图像所在的位置无关,则称该系统为空间不变系统。 对于图 5-1 所示的系统来说,退化过程可以表示成下式: [ ]( ,)(,)(,)g x y H f x y n x y= + (5-3) 5.1.2 连续函数的退化模型 线性系统的理论中,定义了冲激信号 ( )tδ 。它的表达式为: ( )d 1 ( ) 0 0 t t t t δ δ +∞ −∞  =  = ≠ ∫ (5-4) 冲击信号的一个重要性质是取样特性。即: ( ) ( )d ( ) (0)d (0) ( )d (0)t f t t t f t f t t fδ δ δ+∞ +∞ +∞ −∞ −∞ −∞ = = =∫ ∫ ∫ (5-5) 冲激信号的卷积取样公式为: ( ) ( )d ( )f x t t t f xδ+∞ −∞ − =∫ (5-6) 将一维的冲激函数推广到二维空间,可以得到二维的冲激函数 ( ,)x yδ 。利用冲激函数,可以把 f(x,y) 写成如下形式: ( , ) ( , ) ( , )d df x y f x yα β δ α β α β+∞ +∞ −∞ −∞ = ⋅ − −∫ ∫ (5-7) 在空域中,如果让 f(x,y)通过一个线性不变系统 H,则有: 第 5 章 图像复原 · 203· [ ](,)(,) [ ( , ) ( , )d d ] [ ( , ) ( , )d d ( , ) [ ( , )]d d g x y H f x y H f x y H f x y f H x y α β δ α β α β α β δ α β α β α β δ α β α β +∞ +∞ −∞ −∞ +∞ +∞ −∞ −∞ +∞ +∞ −∞ −∞ = = ⋅ − − = ⋅ − − = ⋅ − − ∫ ∫ ∫ ∫ ∫ ∫ (5-8) 式 5-8 中的 [ ( , )]H x yδ α β− − 可以看作系统冲激函数 (,)x yδ α β− − 的响应。如果令: [ ( , )] ( , )H x y h x yδ α β α β− − = − − (5-9) 式 5-8 变成: (,) (,)( , )ddg x y f h x yα β α β α β+∞ +∞ −∞ −∞ == ⋅ − −∫ ∫ (5-10) 可以看出,如果系统 H 对冲激函数的响应已知,则任意系统的输出都可以通过式 5-10 求出。 在有噪声的情况下,退化模型可以表示为: (,) (,)( , )dd (,)g x y f h x y n x yα β α β α β+∞ +∞ −∞ −∞ == ⋅ − − +∫ ∫ (5-11) 5.1.3 离散函数的退化模型 上一小节介绍了连续函数的退化模型,如果把连续函数的输入和系统响应函数 (,)h x yα β− − 取样 后就可以引出离散的退化模型。 设离散序列 f(n)定义在 n=0,1,…,N − 1 各点上,在其他位置 f(n)=0。h(m)定义在 m=0,1,…,M − 1 各点上, 在其他位置 h(m)=0,且 N>M。则连续函数中的连续卷积转化为离散卷积: 1 0 ()()()()() N k gm fkhmk fmhm − = = − = ∗∑ (5-12) 由此得到的离散序列 g(m)的长度为 M+N+1,且可以表示为矩阵形式: (0) (0) 0 ... 0 (0) (1) (1) (0) 0 ... 0 (1) ... ... ... ... ... ... ... ( 2) ( 1) ( 1) g h f g h h f g M N h m f N                        =                  + − − −      (5-13) 如果 f(n)和 h(m)不具备周期性,可以将其延拓为周期函数。为克服混叠,拓展的长度应该为 P(P=M+ N+1)。从而可用周期卷积表示,即: e ( ) 0 1() 0 1 1 f n n Nf n N n p ≤ ≤ −=  − ≤ ≤ − (5-14)Visual C++数字图像获取、处理及实践应用 · 204· ( ) 0 1( ) 0 1 1e h m m Mh m M m p ≤ ≤ −=  − ≤ ≤ − (5-15) 有 fe(m)=fe(P+m)以及 he(m)=he(P+m),其离散周期卷积为: 1 e e e e e 0 ( )()()()() P k g m f m h m f k h m k − = = ⊗ = −∑ (5-16) 经过延拓,一个非周期的卷积问题就可以变成周期卷积问题了。因此可以用快速卷积法进行计算。如果 表示为矩阵,则有: e e e e e e e e e e e e e e e e e e (0) ( 1) ( 2) ... ( 1) (0)(0) (1) (0) ( 1) ... ( 2) (1)(1) ... ... ... ... ... ...... ( 1) ( 2) (0) ( 1)( 1) (0) ( 1) ( 2) ... h h h h P fg h h h h P fg h P h P h f Pg P h h P h P h − − − +          − − +          =                − − −−      − − = e e e e e e e e e (1) (0) (1) (0) ( 1) ... (2) (1) ... ... ... ... ... ... ( 1) ( 2) (0) ( 1)e f h h h P h f h P h P h f P        −                   − − −    (5-17) 下面将一维的情况推广到二维。设二维离散信号 f(m,n),其中 0 1n N≤ ≤ − , 0 1m M≤ ≤ − ,并 设退化系统冲击响应函数 h(m,n)属于 0 1m J≤ ≤ − , 0 1m K≤ ≤ − 。 同样对 f(m,n),h(m,n)拓展为周期 P×Q 的周期序列: e ( , ) 0 1,0 1( ,) 0 f m n n N m Mf m n ≤ ≤ − ≤ ≤ −=   其他 e ( , ) 0 1,0 1( ,) 0 h m n m J m Kh m n ≤ ≤ − ≤ ≤ −=   其他 其中 P=M+J-1,Q=N+K-1,则有 11 e e e e e 0 0 ( ,)(,)(,)(,)(,) QP k l g mn fmn hmn fklhmknl −− = = = ⊗ = − −∑∑ (5-18) 考虑加性噪声的影响,上式变为: 11 e e e e 0 0 ( ,)(,)(,)(,) QP k l g mn fklhmknl nmn −− = = = − − +∑∑ (5-19) 为便于讨论和求解,用堆叠方式将二维信号表为一维向量,即: 第 5 章 图像复原 · 205· e e e e e e e e e e e e e e e e e e (0,0) (0,0) (0,1) (0,1) (0, 1) (0, 1) (1,0) (1,0) (1,1) (1,1) , (1, 1) (1, 1) ( 1,0) ( 1,0) ( 1,1) ( 1,1) ( 1, 1) ( 1, f g f g f Q g Q f g f g f g f Q g Q f P g P f P g P f P Q g P Q          − −         = =  − −       − −  − −      − − −        1)                                       −  (5-20) 如果使得 g H f= ⋅ ,则 H 为 PQ×PQ 阶的分块矩阵: 0 1 2 1 1 0 1 2 1 2 0 ... ... ... ... ... ... ... PP P PP h h h h h h h h H h h h − − − − −        =        其子块 h 为: e e e e e e e e e e e ( ,0) ( , 1) ( , 2) ... ( ,1) ( ,1) ( ,0) ( , 1) ... ( ,2) ... ... ... ... ... ( , 1) ( , 2) ( ,0) j h j h j Q h j Q h j h j h j h j Q h j h h j Q h j Q h j − −   −   =      − −  因此,H 为分块循环矩阵,分块 h 中元素的第二个下标也是按循环方式变化的 于是,二维离散的退化模型化为 g H f n= ⋅ + (5-21) 解决退化的问题化为求解线性方程组的问题。可以用线性代数和数值分析的方法进行处理,即: 1 []f H g n−= ⋅ − (5-22) Visual C++数字图像获取、处理及实践应用 · 206· 5.2 运动模糊图像复原 在照相的曝光期间,如果物体和相机有相对运动,则反映在底片上的图像有明显的移动,形成了模糊 的运动图像。 5.2.1 由匀速直线运动引起的图像模糊 我们先分析图像只在 X 方向运动造成模糊的情况,在 Y 方向或者 X、Y 方向均速运动的情况可以类推。 设运动方向为 X 轴方向,则图像函数可以写成 0( ( ), )f x x t y− 。这里 0 ( )x t 是沿 X 的正方向运动的 距离。设曝光时间为 0 t T≤ ≤ ,在此期间移动的距离是 a,所以 0 ( )/x t at T= 。所形成的模糊图像为: 0 ( , ) ( , )dT atg x y f x y tT α= −∫ (5-23) 式 5-29 中α 是与照片感光灵敏度有关的系数,现已知 (,)g x y 和α ,求 (,)f x y 。 图 5-2 是运动造成图像模糊的一个示意图。 f(x,y) f(x-a,y) 图 5-2 运动图像模糊 由于模糊只在 X 方向,而和 Y 方向无关。所以可以一行一行地复原。在某一行 0y y= 时,把 0(,)g x y 和 0(,)f x y 简写成 ()g x 和 ()f x ,则式 5-23 可以简写为: 0 ( ) ( )dT atg x f x tT α= −∫ (5-24) 令 atx T τ = − ,代入上式中有: 1( ) ( )dx x a g fτ τ τβ − = ∫ (5-25) 对式 5-25 进行微分有: 第 5 章 图像复原 · 207· ’()()()f x g x f x aβ= + − (5-26) 式中的 a T β α= 。可以看出,通过递推的方法可以求得原来图像的像素值 ()f x 。 模糊图像 ()g x 的导数 ’()g x 是可以求得的,所以已知 ()f x a− 就可以通过式 5-26 求得 ()f x 。而 实际上,只要求出总长度为 a 的区间上的图像值,就可以推得整个图像了。 设模糊图像的宽度为 L, L ka= 。并设 x L> 和 0x < 范围的像素值为零。则变量 x 可以表示为: x z ma= + (5-27) z 的取值范围在[0, ]a 之间,m 是 x a 的整数部分。将式 5-27 代入式 5-26 有: ’()()()fzma gzma fzmaaβ+ = + + + − (5-28) 设 ()zφ 为曝光期间在 0 x a≤ < 范围内移动的景物部分,即: ()()z f z aφ = − (5-29) 通过 ()zφ ,利用递推法可以求解式 5-28。 当 m=0 时,有: ’’()()()()()f z g z f z a g z zβ φ= + − = + (5-30) 当 m=1 时,有: ’’’()()()()()()fza gzafz gza gz zβ β β φ+ = + + = + + + (5-31) 以次类推,最后可以得到如下结果: ’ 0 ( ) ( ) ( ) 0 m k fzma gzka z zaβ ϕ = + = + + ≤ <∑ (5-32) 用 x z ma= + 来代入式 5-32 中有: ’ 0 ( ) ( ) ( ) 0 m k fx gxka xma xLβ φ = = − + − ≤ <∑ (5-33) 上式中 ’()g x 可以由模糊图像求得,但 ()zφ 是未知函数,对于任何 ()zφ ,都有一个满足式 5-33 的解, 所以只有确定了某一长度为 a 区间的图像以后才能确定 ()zφ ,下面介绍一种粗糙的近似方法。 由式 5-32 有 ’ 0 ( ) ( ) ( ) m k z g z ka f z maφ β = = − + + +∑ (5-34)Visual C++数字图像获取、处理及实践应用 · 208· 式中 0,1, , 1m k= − ,共有 k 项,将上式以 k 项相加得: -1 -1 ’ =0 0 =0 -1 -1 ’ =0 0 =0 ( ) ( ) ( ) 1 1( ) ( ) ( ) k m k m k m k m k m k m k z g z ka f z ma z g z ka f z mak k φ β φ β = = = − + + + = − + + + ∑ ∑ ∑ ∑ ∑ ∑ (5-35) 上式中第二项当 k 值较大时,趋向于 f 的平均值,因此可将它近似地看作一个常数,所以设: -1 =0 1 ( ) k m f z ma Ak + =∑ (5-36) 则式 5-35 可以写成: -1 ’ =0 0 ()() k m m k z A g z kak βφ = = − +∑ ∑ (5-37) 这里的 A 是一个未知数,需要用试验的方法进行确定,将上式代入式 5-32 可以得到: -1 ’’ 0 =0 0 ()()() m k m k m k fzma gzkaA gzkak ββ = = + = + + − +∑ ∑ ∑ (5-38) 最后可以得到: -1 ’’ 0 =0 0 ()()() m k m k m k fxA gxka gxkak ββ = = = + − − −∑ ∑ ∑ (5-39) 5.2.2 运动模糊图像复原的 Visual C++实现 下面编程实现完成运动模糊图像及其复原过程的函数。最后将在菜单中添加对二维图像实现 X 方向运 动模糊以及对模糊图像实现复原的功能。 首先,我们添加一个“图像复原”的菜单,如图 5-3 所示。里面的子菜单是我们在图像复原这一章将实现 的几种退化及其复原过程。 实现对图像在 X 方向运动模糊的函数是 DIBMotionDegeneration。图像进行运动模糊时,设总的运动时 间为 10s,运动距离为 10 个像素点,照片感光灵敏度 a 为 0.1。 图 5-3 图像复原的菜单 /************************************************************************* * * 函数名称: * DIBMotionDegeneration() 第 5 章 图像复原 · 209· * * 参数: * CDib *pDib - 指向 CDib 类的指针 * * 返回值: * BOOL - 成功返回 TRUE,否则返回 FALSE * * 说明: * 该函数用来对 DIB 图像模拟由匀速直线运动造成的模糊 * ************************************************************************/ BOOL WINAPI DIBMotionDegeneration(CDib *pDib) { // 指向源图像的指针 BYTE * lpSrc; //图像的宽度和高度 LONG lWidth; LONG lHeight; // 图像每行的字节数 LONG lLineBytes; //得到图像的宽度和高度 CSize SizeDim; SizeDim = pDib->GetDimensions(); lWidth = SizeDim.cx; lHeight = SizeDim.cy; //得到实际的 Dib 图像存储大小 CSize SizeRealDim; SizeRealDim = pDib->GetDibSaveDim(); // 计算图像每行的字节数 lLineBytes = SizeRealDim.cx; //图像数据的指针 LPBYTE lpDIBBits = pDib->m_lpImage; //循环变量 long iColumn; long jRow; //临时变量 Visual C++数字图像获取、处理及实践应用 · 210· int temp,n,m; // 临时变量 double p,q; int nTotTime, nTotLen, nTime; //总的运动时间 10s nTotTime = 10; // 总的运动距离 10 个像素点 nTotLen = 10; // 摄像机的曝光系数 double B; B = 0.1; //用来存储源图像和变换核的时域数据 int *nImageDegener; // 为时域和频域的数组分配空间 nImageDegener = new int [lHeight*lLineBytes]; // 将数据存入时域数组 for (jRow = 0; jRow < lHeight; jRow++) { for(iColumn = 0; iColumn < lLineBytes; iColumn++) { temp=0; // 指向源图像倒数第 jRow 行,第 iColumn 个像素的指针 lpSrc = (unsigned char *)lpDIBBits + lLineBytes * jRow + iColumn; // 像素点的像素值积累 for ( nTime = 0; nTime < nTotTime; nTime++ ) { p = (float)iColumn - (float)(nTotLen)*nTime/nTotTime; if (p > 0) { q = p - floor((double)p); if(q >= 0.5) 第 5 章 图像复原 · 211· m = (int)ceil((double)p); else m = (int)floor((double)p); // 累加 lpSrc = (unsigned char *)lpDIBBits + lLineBytes * jRow + m; temp = temp + *lpSrc; } } // 乘以摄像机的曝光系数 temp = B * temp; temp=(int)ceil((double)temp); // 使得 temp 的取值符合取值范围 if(temp<0) temp=0; if(temp>255) temp=255; nImageDegener[lLineBytes*jRow + iColumn] = temp; } } //转换为图像 for (jRow = 0;jRow < lHeight ;jRow++) { for(iColumn = 0;iColumn < lLineBytes ;iColumn++) { // 指向源图像倒数第 jRow 行,第 iColumn 个像素的指针 lpSrc = (unsigned char *)lpDIBBits + lLineBytes * jRow + iColumn; *lpSrc = nImageDegener[lLineBytes*jRow + iColumn]; } } //释放存储空间 delete nImageDegener; // 返回 return true; } Visual C++数字图像获取、处理及实践应用 · 212· 实现对在 X 方向运动模糊图像进行复原的函数 DIBMotionDegeneration,代码如下。未知数 A 的值设定 为 80。 /************************************************************************* * * 函数名称: * DIBMotionRestore() * * 参数: * CDib *pDib - 指向 CDib 类的指针 * * 返回值: * BOOL - 成功返回 TRUE,否则返回 FALSE * * 说明: * 该函数用来对拟由匀速直线运动造成的模糊图像进行复原 * *********************************************************************** */ BOOL WINAPI DIBMotionRestore(CDib *pDib) { // 指向源图像的指针 BYTE * lpSrc; //图像的宽度和高度 LONG lWidth; LONG lHeight; // 图像每行的字节数 LONG lLineBytes; //得到图像的宽度和高度 CSize SizeDim; SizeDim = pDib->GetDimensions(); lWidth = SizeDim.cx; lHeight = SizeDim.cy; //得到实际的 Dib 图像存储大小 CSize SizeRealDim; SizeRealDim = pDib->GetDibSaveDim(); // 计算图像每行的字节数 lLineBytes = SizeRealDim.cx; //图像数据的指针 LPBYTE lpDIBBits = pDib->m_lpImage; 第 5 章 图像复原 · 213· //循环变量 long iColumn; long jRow; int i,num,n,m; //临时变量 int temp1,temp2, totalq,q1,q2,z; double p,q; // 常量 A 赋值 int A = 80; //常量 B 赋值 int B = 10; //总的移动距离 int nTotLen=10; // 图像宽度包含多少个 ntotlen int K=lLineBytes/nTotLen; int error[10]; //用来存储源图像时域数据 int *nImageDegener; // 为时域数组分配空间 nImageDegener = new int [lHeight*lLineBytes]; // 将像素存入数组中 for (jRow = 0; jRow < lHeight; jRow++) { for(iColumn = 0; iColumn < lLineBytes; iColumn++) { lpSrc = (unsigned char *)lpDIBBits + lLineBytes * jRow + iColumn; nImageDegener[lLineBytes*jRow + iColumn] = (*lpSrc); } } Visual C++数字图像获取、处理及实践应用 · 214· for (jRow = 0; jRow < lHeight; jRow++) { // 计算 error[i] for(i = 0; i < 10; i++) { error[i] = 0; for(n = 0; n < K; n++) for(m = 0; m <= n; m++) { // 像素是否为一行的开始处 if(i == 0 && m == 0) { temp1=temp2=0; } // 进行差分运算 else { lpSrc = (unsigned char *)lpDIBBits + lLineBytes * jRow + m*10+i; temp1=*lpSrc; lpSrc = (unsigned char *)lpDIBBits + lLineBytes * jRow + m*10+i-1; temp2 = *lpSrc; } error[i] = error[i] + temp1 - temp2; } error[i] = B * error[i] / K; } for(iColumn = 0; iColumn < lLineBytes; iColumn++) { // 计算 m,以及 z m = iColumn / nTotLen; z = iColumn - m*nTotLen; // 初始化 totalq = 0; q = 0; 第 5 章 图像复原 · 215· for(n = 0; n <= m; n++) { q1 = iColumn - nTotLen*n; if(q1 == 0) q = 0; // 进行差分运算 else { q2 = q1 - 1; lpSrc = (unsigned char *)lpDIBBits + lLineBytes * jRow + q1; temp1 = *lpSrc; lpSrc = (unsigned char *)lpDIBBits + lLineBytes * jRow + q2; temp2 = *lpSrc; q = (temp1 - temp2) * B; } totalq = totalq + q; } p = error[z]; // 得到 f(x,y)的值 temp1 = totalq + A - p; // 使得像素的取值符合取值范围 if(temp1 < 0) temp1 = 0; if(temp1 > 255) temp1 = 255; nImageDegener[lLineBytes*jRow + iColumn] = temp1; } } //转换为图像 for (jRow = 0;jRow < lHeight ;jRow++) { for(iColumn = 0;iColumn < lLineBytes ;iColumn++) Visual C++数字图像获取、处理及实践应用 · 216· { // 指向源图像倒数第 jRow 行,第 iColumn 个像素的指针 lpSrc = (unsigned char *)lpDIBBits + lLineBytes * jRow + iColumn; // 存储像素值 *lpSrc = nImageDegener[lLineBytes*jRow + iColumn]; } } //释放存储空间 delete nImageDegener; // 返回 return true; } 接下来在 ImageProcessingView.cpp 中对运动图像模糊子菜单添加命令处理函数,代码如下。 void CImageProcessingView::OnDegenerationMotion() { // 运动图像模糊 // 更改光标形状 BeginWaitCursor(); // 获取文档 CImageProcessingDoc* pDoc = GetDocument(); // 获得图像 CDib 类的指针 CDib * pDib = pDoc->m_pDibInit; // 获得图像的头文件信息 LPBITMAPINFOHEADER lpBMIH=pDib->m_lpBMIH; // 判断是否是 8-bpp 位图(这里为了方便,只处理 8-bpp 位图的运动模糊) if (lpBMIH->biBitCount != 8) { // 提示用户 MessageBox("目前只支持 256 色位图的运动模糊!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } ::DIBMotionDegeneration(pDib); 第 5 章 图像复原 · 217· // 设置脏标记 pDoc->SetModifiedFlag(TRUE); // 更新视图 pDoc->UpdateAllViews(NULL); // 恢复光标 EndWaitCursor(); } 为运动模糊图像复原子菜单添加命令处理函数,代码如下。 void CImageProcessingView::OnRestoreMotion() { // 运动图像模糊复原 // 更改光标形状 BeginWaitCursor(); // 获取文档 CImageProcessingDoc* pDoc = GetDocument(); // 获得图像 CDib 类的指针 CDib * pDib = pDoc->m_pDibInit; // 获得图像的头文件信息 LPBITMAPINFOHEADER lpBMIH=pDib->m_lpBMIH; // 判断是否是 8-bpp 位图(这里为了方便,只处理 8-bpp 位图的运动模糊复原) if (lpBMIH->biBitCount != 8) { // 提示用户 MessageBox("目前只支持 256 色位图的运动模糊复原!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } ::DIBMotionRestore(pDib); // 设置脏标记 pDoc->SetModifiedFlag(TRUE); Visual C++数字图像获取、处理及实践应用 · 218· // 更新视图 pDoc->UpdateAllViews(NULL); // 恢复光标 EndWaitCursor(); } 用在菜单中实现的功能对图像进行运动模糊及其复原的操作,图 5-4 所示是进行模糊前后的效果。 lena 原图 经过运动模糊后的图象 图 5-4 lena 原图以及经过运动模糊后的图像 图 5-5 所示是利用算法对模糊后的图像进行复原后的效果。 图 5-5 运动模糊图像复原效果 5.3 非约束复原 当图像退化系统为线性不变系统,且噪声为加性噪声时,可将复原问题在线性系统理论框架内处理。 对退化模型 g H f n= ⋅ + ,根据给定的退化图像 g 和退化模型 H 及噪声的先验知识,需寻找一个对原始 图像的最优估计,使某种实现确定的准则达到最小,其中常见的准则为最小二乘准则。 5.3.1 非约束复原的基本方法 由式 5-21 可知,退化模型的噪声项为: 第 5 章 图像复原 · 219· n H f g= ⋅ − (5-40) 若对 n 一无所知或 n 为白噪声,那么准则是寻找 ˆf 使 ˆH f⋅ 与 g 的偏差符合最小二乘方准则,即使得 n H f g= ⋅ − 范数的平方尽可能小。若假设噪声项的范数也尽可能小,则有: 2 2 ˆmin ming Hf n− ⇒ 这里,范数的定义为: 2 ()()Tn nTn g Hf g Hf g Hf= = − = − −   (5-41) 因此可将图像复原问题看作是对 ˆf 求下式的最小值: 2 ()J f g Hf= −  (5-42) 这里除了选择 ˆf 使得 ()J f 最小外,不受任何其他条件的约束,因此称为非约束复原。求式 5-42 的最 小值方法如下: 2 ()()()T TTTTTT Jf gHf gHf gHf gg fHg gHf fHHf = − = − − = − − +        又有: ()[ ] 2ˆ ˆ2 ˆ2 ( ) 0 TTTT TTT T J f H g g H H Hf f H g H g H Hf H g Hf ∂ = − − + ∂ = − − + = − − =   (5-43) 于是可以求得: 1ˆ ()TTf H H H g−= (5-44) 若设 P=Q,即 H 为方阵,并设 1H − 存在,则有: 1 1 1ˆ ()TTf H H H g H g− − −= = (5-45) 5.3.2 逆滤波复原 逆滤波也叫反向滤波。在没有噪声的情况下,退化模型由式 5-10 表示。在没有噪声的情况下,对式 5-10 做傅立叶变换后,根据傅立叶变换的卷积定理可知有下式成立: (,)(,)(,)G u v H u v F u v= ⋅ (5-46) 式 中 的 (,)(,)(,)G u v H u v F u v、、 分 别 是 退 化 图 像 (,)g x y 、 点 扩 散 函 数 (,)h x y 和 原 始 图 像Visual C++数字图像获取、处理及实践应用 · 220· ( ,)f x y 的傅立叶变换。根据式 5-46,可以求得: (,)(,)(,) G u vF u v H u v = (5-47) 如果已经知道退化图像和传递函数的傅立叶变换,则可以求得原始图像的傅立叶变换。经傅立叶反变 换就可以求得原始图像 (,)f x y 。过程可以由下式表示。 1 1 (,)( , ) [ ( , )] [ ](,) G u vf x y F u v H u v − −= =FF (5-48) 在有噪声的情况下,逆滤波的原理可以写成如下形式: (,)(,)(,)(,)Guv Huv Fuv Nuv= ⋅ + (5-49) (,)(,)(,)(,)(,) G u v N u vF u v H u v H u v = − (5-50) 以上说明的是逆滤波图像复原,它的优点是实现比较简单。逆滤波图像复原中也存在一些问题:当 (,)H u v 出现奇异点或者 (,)H u v 非常小的时候,即使没有噪声,也无法精确恢复 (,)f x y 。另外高频较 小时,或当存在噪声时, (,)H u v 可能比噪声 (,)N u v 的值小很多,这样也可能使得 (,)f x y 无法正确恢 复。 5.3.3 逆滤波复原的 Visual C++实现 下面编程实现图像无噪退化及逆滤波复原过程的函数。 模拟对原图像的退化过程时,利用一个点扩展函数来对图像进行卷积。为了简单起见,不加入噪声项: g h f= ∗ 其中: 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 125 1 1 1 1 1 1 1 1 1 1 h        =        卷积的过程实际上就是用 5×5 的模版对图像进行平滑。在编程实现时,先对图像和模版进行傅立叶变 换。根据傅立叶变换的性质,在时域对图像进行卷积,相当于在频域进行相乘。所以只要将图像和模版的傅 立叶变换进行相乘,再进行反变换就可以得到退化的图像。以上过程可以用下式表示: 1 1 (,)(,)(,) (,) [(,)] [(,) (,)] G u v H u v F u v gxy guv HuvFuv− − = ⋅ = = ⋅FF 第 5 章 图像复原 · 221· 进行逆滤波复原时,根据先验知识,得到系统函数 (,)H u v ,利用式 5-48 就可以实现复原过程。 DIBNoRestriction 是实现对图像进行卷积模糊退化的函数。 /************************************************************************* * * 函数名称: * DIBNoRestriction() * * 参数: * CDib *pDib - 指向 CDib 类的指针 * * 返回值: * BOOL - 成功返回 TRUE,否则返回 FALSE * * 说明: * 该函数用来对 DIB 图像进行模糊操作。 * ************************************************************************/ BOOL WINAPI DIBNoRestriction(CDib *pDib) { // 指向源图像的指针 BYTE * lpSrc; //图像的宽度和高度 LONG lWidth; LONG lHeight; // 图像每行的字节数 LONG lLineBytes; //得到图像的宽度和高度 CSize SizeDim; SizeDim = pDib->GetDimensions(); lWidth = SizeDim.cx; lHeight = SizeDim.cy; //得到实际的 Dib 图像存储大小 CSize SizeRealDim; SizeRealDim = pDib->GetDibSaveDim(); // 计算图像每行的字节数 lLineBytes = SizeRealDim.cx; //图像数据的指针 Visual C++数字图像获取、处理及实践应用 · 222· LPBYTE lpDIBBits = pDib->m_lpImage; //循环变量 long i; long j; //临时变量 double temp; // 实际进行付立叶变换的宽度和高度 LONG lW = 1; LONG lH = 1; int wp = 0; int hp = 0; // 保证离散傅立叶变换的宽度和高度为 2 的整数次方 while(lW * 2 <= lLineBytes) { lW = lW * 2; wp++; } while(lH * 2 <= lHeight) { lH = lH * 2; hp++; } //用来存储源图像和变换核的时域数据 complex *pCTSrc,*pCTH; //用来存储源图像和变换核的频域数据 complex *pCFSrc,*pCFH; //图像归一化因子 double MaxNum; //输入图像的长和宽必须为 2 的整数倍 if(lW != (int) lLineBytes) { return false; } 第 5 章 图像复原 · 223· if(lH != (int) lHeight) { return false; } // 为时域和频域的数组分配空间 pCTSrc = new complex [lHeight*lLineBytes]; pCTH = new complex [lHeight*lLineBytes]; pCFSrc = new complex [lHeight*lLineBytes]; pCFH = new complex [lHeight*lLineBytes]; // 将数据存入时域数组 for (j = 0; j < lHeight; j++) { for(i = 0; i < lLineBytes; i++) { // 指向源图像倒数第 j 行,第 i 个像素的指针 lpSrc = (unsigned char *)lpDIBBits + lLineBytes * j + i; pCTSrc[ lLineBytes*j + i ] = complex((double)*lpSrc , 0); pCFSrc[ lLineBytes*j + i ] = complex(0.0 , 0.0); if(i < 5 && j < 5) { pCTH[ lLineBytes*j + i ] = complex(0.04 , 0.0); } else { pCTH[ lLineBytes*j + i ] = complex(0.0 , 0.0); } pCFH[ lLineBytes*j + i ] = complex(0.0 , 0.0); } } //对源图像进行 FFT ::DIBFFT_2D(pCTSrc, lLineBytes, lHeight, pCFSrc); //对变换核图像进行 FFT ::DIBFFT_2D(pCTH, lLineBytes, lHeight, pCFH); //频域相乘 for (i = 0;i GetDimensions(); lWidth = SizeDim.cx; lHeight = SizeDim.cy; //得到实际的 Dib 图像存储大小 CSize SizeRealDim; SizeRealDim = pDib->GetDibSaveDim(); // 计算图像每行的字节数 lLineBytes = SizeRealDim.cx; //图像数据的指针 LPBYTE lpDIBBits = pDib->m_lpImage; Visual C++数字图像获取、处理及实践应用 · 226· //循环变量 long i; long j; //临时变量 double temp, tempre, tempim, a, b, c, d; // 实际进行付立叶变换的宽度和高度 LONG lW = 1; LONG lH = 1; int wp = 0; int hp = 0; // 保证离散傅立叶变换的宽度和高度为 2 的整数次方 while(lW * 2 <= lLineBytes) { lW = lW * 2; wp++; } while(lH * 2 <= lHeight) { lH = lH * 2; hp++; } //用来存储源图像和变换核的时域数据 complex *pCTSrc,*pCTH; //用来存储源图像和变换核的频域数据 complex *pCFSrc,*pCFH; //图像归一化因子 double MaxNum; //输入退化图像的长和宽必须为 2 的整数倍 if(lW != (int) lLineBytes) { return false; } if(lH != (int) lHeight) { 第 5 章 图像复原 · 227· return false; } // 为时域和频域的数组分配空间 pCTSrc = new complex [lHeight*lLineBytes]; pCTH = new complex [lHeight*lLineBytes]; pCFSrc = new complex [lHeight*lLineBytes]; pCFH = new complex [lHeight*lLineBytes]; // 将退化图像数据存入时域数组 for (j = 0; j < lHeight; j++) { for(i = 0; i < lLineBytes; i++) { // 指向退化图像倒数第 j 行,第 i 个像素的指针 lpSrc = (unsigned char *)lpDIBBits + lLineBytes * j + i; pCTSrc[ lLineBytes*j + i ] = complex((double)*lpSrc , 0); pCFSrc[ lLineBytes*j + i ] = complex(0.0 , 0.0); if(i < 5 && j < 5) { pCTH[ lLineBytes*j + i ] = complex(0.04 , 0.0); } else { pCTH[ lLineBytes*j + i ] = complex(0.0 , 0.0); } pCFH[ lLineBytes*j + i ] = complex(0.0 , 0.0); } } //对退化图像进行 FFT ::DIBFFT_2D(pCTSrc, lLineBytes, lHeight, pCFSrc); //对变换核图像进行 FFT ::DIBFFT_2D(pCTH, lLineBytes, lHeight, pCFH); //频域相除 for (i = 0;i 1e-3) { tempre = ( a*c + b*d ) / ( c*c + d*d ); tempim = ( b*c - a*d ) / ( c*c + d*d ); } pCFSrc[i]= complex(tempre , tempim); } //对复原图像进行反 FFT IFFT_2D(pCFSrc, pCTSrc, lLineBytes, lHeight); //确定归一化因子 MaxNum=300; //转换为复原图像 for (j = 0;j < lHeight ;j++) { for(i = 0;i < lLineBytes ;i++) { // 指向复原图像倒数第 j 行,第 i 个像素的指针 lpSrc = (unsigned char *)lpDIBBits + lLineBytes * j + i; *lpSrc = (unsigned char) (pCTSrc[(lLineBytes)*j + i].real()*255.0/MaxNum); } } //释放存储空间 delete pCTSrc; delete pCTH; delete pCFSrc; delete pCFH; // 返回 return true; } 接下来在 ImageProcessingView.cpp 中对逆滤波的图像模糊子菜单添加命令处理函数,代码如下。 void CImageProcessingView::OnDegenerationInverse() { 第 5 章 图像复原 · 229· // 图像的模糊 // 更改光标形状 BeginWaitCursor(); // 获取文档 CImageProcessingDoc* pDoc = GetDocument(); // 获得图像 CDib 类的指针 CDib * pDib = pDoc->m_pDibInit; // 获得图像的头文件信息 LPBITMAPINFOHEADER lpBMIH=pDib->m_lpBMIH; // 判断是否是 8-bpp 位图(这里为了方便,只处理 8-bpp 位图的图像模糊) if (lpBMIH->biBitCount != 8) { // 提示用户 MessageBox("目前只支持 256 色位图的图像模糊!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } ::DIBNoRestriction(pDib); // 设置脏标记 pDoc->SetModifiedFlag(TRUE); // 更新视图 pDoc->UpdateAllViews(NULL); // 恢复光标 EndWaitCursor(); } 在 ImageProcessingView.cpp 中对图像逆滤波复原子菜单添加命令处理函数,代码如下。 void CImageProcessingView::OnRestoreInverse() { // 图像的逆滤波 // 更改光标形状 BeginWaitCursor(); // 获取文档 CImageProcessingDoc* pDoc = GetDocument(); Visual C++数字图像获取、处理及实践应用 · 230· // 获得图像 CDib 类的指针 CDib * pDib = pDoc->m_pDibInit; // 获得图像的头文件信息 LPBITMAPINFOHEADER lpBMIH=pDib->m_lpBMIH; // 判断是否是 8-bpp 位图(这里为了方便,只处理 8-bpp 位图的逆滤波) if (lpBMIH->biBitCount != 8) { // 提示用户 MessageBox("目前只支持 256 色位图的逆滤波!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } ::DIBInverseFilter(pDib); // 设置脏标记 pDoc->SetModifiedFlag(TRUE); // 更新视图 pDoc->UpdateAllViews(NULL); // 恢复光标 EndWaitCursor(); } 用在菜单中实现的功能对图像进行没有噪声的卷积模糊及逆滤波复原的操作。图 5-6 所示是进行模糊 前后的效果。 lena 原图 卷积模糊后图像 图 5-6 lena 原图及卷积模糊后图像 图 5-7 所示是经逆滤波复原后的图像。 第 5 章 图像复原 · 231· 图 5-7 逆滤波复原后的图像 5.3.4 维纳滤波方法 维纳滤波方法也就是最小二乘滤波。它是使得原始图像 (,)f x y 以及其恢复图像 ˆ(,)f x y 之间的均方 误差最小的复原方法。 设原始图像、退化图像和图像噪声为 (,)f x y 、 (,)g x y 、 (,)n x y 。他们之间的关系如下: (,) ( , )(,)dd (,)g x y h x y f n x yα β α β α β= − − +∫∫ (5-51) 式中的 (,)f x y 、 (,)g x y 、 (,)n x y 都是随机场,并假定噪声的统计特性已知。因此,在给定了 (,)g x y ,仍然不能精确求解 (,)f x y ,只能找出 (,)f x y 的一个估计值 ˆ(,)f x y ,使得均方误差式: { }2 2ˆe E [ ( , ) ( , )]f x y f x y= − (5-52) 最小。其中的 ˆ(,)f x y 是给定 (,)g x y 对 (,)f x y 的最小二乘方估计。 假定 ˆ(,)f x y 是 (,)g x y 灰度级的线性函数,则有: (,) (,,,)(,)ddf x y m x y gα β α β α β= ∫∫ (5-53) 如果随机场均匀,权重 (,,,)m x y α β 只与 (,)x yα β− − 有关,上式 5-53 可以写成: (,) ( , )(,)ddf x y m x y gα β α β α β= − −∫∫ (5-54) 均方误差可以写成: { }2 2e E[(,) ( , )(,)dd]f x y m x y gα β α β α β= − − −∫∫ (5-55) 所以最小二乘方估计的目的就是寻找点扩散函数 (,)m x y ,使得均方误差最小。可以证明对于 xy 平面 上的所有位置向量 (,)x y 和 (,)α β 都满足下式: Visual C++数字图像获取、处理及实践应用 · 232· { }’ ’E [(,) ( , )(,)dd] (,) 0f x y m x y g gα β α β α β α β− − − × =∫∫ (5-56) 将上式进行改写有: { }’ ’’’( , )E{(,)(, )}dd E (,)(, )m x y g g f x y gα β α β α β α β α β− − =∫∫ (5-57) 设随机场均匀,并且令: { } { } ’ ’’’’’ ’ ’’’’’ E (,)(,)(,,,)(,) E (,)(,)(,,,)(,) gg gg fg fg g g R R f x y g R x y R x y α βαβ αβαβ ααββ α β α β α β = = − − = = − − 代入式 5-57,有: ’ ’’’( , ) ( , )d d ( , )gg fgm x y R R x yα β α α β β α β α β− − − − = − −∫∫ (5-58) 对上式的变量进行替换,并对两边进行傅立叶变换,可得: ( ,)(,)(,)gg fgM u v S u v S u v= (5-59) 其中 ( ,)ggS u v 是退化图像 ( ,)g x y 的谱密度, ( ,)fgS u v 是退化图像与原始图像的互谱密度。如果图 像 ( ,)f x y 与噪声 ( ,)n x y 不相关并且有零均值,则有下式成立: { } { } { }E (,)(,) E (,)E (,) 0f xynxy fxy nxy= = (5-60) 这种情况下的滤波器的形式可以通过如下的推导得出。考虑到随机场的均匀性有: {} {} ’’’’ ’’ ’’ ’’ (,,,)(,) (,)(,) ( , )E (,)(,)dd ( , ) ( , )d d fg fg ff R x y R x y E f x y g h f x y f h R x y α β α β α β α α β β α β α β α α β β α β α β = − − = = − − = − − − − ∫∫ ∫∫ (5-61) 式 5-61 经过一系列的变量替换后可得: ( , ) ( , ) ( , )d dfg ffR x y h x y Rα β α β α β= − −∫∫ (5-62) 对等式两边进行傅立叶变换有: *( ,)(,)(,)fg ggS u v H u v S u v= (5-63) 又因为: 2(,)(,)(,)(,)gg ff nnS uv SuvHuv Suv= + (5-64) 联立式 5-64、5-63、5-59 可得: 2 2 (,)1(,)(,)(,) [ (,)/ (,)]nn ff H u vM u v H u v H u v S u v S u v = + (5-65)第 5 章 图像复原 · 233· 可见当 ( , ) 0nnS u v = 时,就是理想的滤波器。通常认为噪声是白噪声,即 (,)nnS u v = 常数。如果有 关的随机过程的统计特性都不知道,可以用下式近似的表示滤波器: 2 2 (,)1(,)(,)(,) H u vM u v H u v H u v τ = + (5-66) 5.3.5 维纳滤波的 Visual C++实现 有了上面的理论知识,下面编程实现对图像卷积模糊加噪,并利用维纳滤波对图像进行复原的过程。 模拟对原图像的退化过程时,同上小节一样,还是利用一个点扩展函数来对图像进行卷积。不同的是, 在卷积的同时,还加入伪随机噪声一项: g h f n= ∗ + 其中 h 同样是: 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 125 1 1 1 1 1 1 1 1 1 1 h        =        在编程实现时,对图像和模版进行傅立叶变换后,将图像和模版的傅立叶变换进行相乘,再进行反变换 可以得到 h f∗ ,最后在时域上加入伪随机噪声,完成图像的退化过程。 进行维纳滤波束复原时,根据先验知识,得到系统函数 (,)H u v ,利用式 5-66 就可以实现复原过程。 式中的常数τ 取值为 0.5。 函数 DIBNoiseDegeneration 用来实现对图像进行实现卷积模糊以及加入噪声的过程。 /************************************************************************* * * 函数名称: * DIBNoiseDegeneration() * * 参数: * CDib *pDib - 指向 CDib 类的指针 * * 返回值: * BOOL - 模糊加噪操作成功返回 TRUE,否则返回 FALSE。 * * 说明: * 该函数用来对 DIB 图像进行模糊加噪操作。 * ************************************************************************/ Visual C++数字图像获取、处理及实践应用 · 234· BOOL WINAPI DIBNoiseDegeneration (CDib *pDib) { // 指向源图像的指针 BYTE * lpSrc; //图像的宽度和高度 LONG lWidth; LONG lHeight; // 图像每行的字节数 LONG lLineBytes; //得到图像的宽度和高度 CSize SizeDim; SizeDim = pDib->GetDimensions(); lWidth = SizeDim.cx; lHeight = SizeDim.cy; //得到实际的 Dib 图像存储大小 CSize SizeRealDim; SizeRealDim = pDib->GetDibSaveDim(); // 计算图像每行的字节数 lLineBytes = SizeRealDim.cx; //图像数据的指针 LPBYTE lpDIBBits = pDib->m_lpImage; //循环变量 long i; long j; //转换为图像,加噪 unsigned char NoisePoint; //临时变量 double temp; //图像归一化因子 double MaxNum; // 实际进行傅立叶变换的宽度和高度 LONG lW = 1; 第 5 章 图像复原 · 235· LONG lH = 1; int wp = 0; int hp = 0; // 保证离散傅立叶变换的宽度和高度为 2 的整数次方 while(lW * 2 <= lLineBytes) { lW = lW * 2; wp++; } while(lH * 2 <= lHeight) { lH = lH * 2; hp++; } //用来存储源图像和变换核的时域数据 complex *pCTSrc,*pCTH; //用来存储源图像和变换核的频域数据 complex *pCFSrc,*pCFH; // 为时域和频域的数组分配空间 pCTSrc = new complex [lHeight*lLineBytes]; pCTH = new complex [lHeight*lLineBytes]; pCFSrc = new complex [lHeight*lLineBytes]; pCFH = new complex [lHeight*lLineBytes]; for (j = 0;j < lHeight ;j++) { for(i = 0;i < lLineBytes ;i++) { // 指向源图像倒数第 j 行,第 i 个像素的指针 lpSrc = (unsigned char *)lpDIBBits + lLineBytes * j + i; // 将像素值存储到时域数组中 pCTSrc[ lLineBytes*j + i ] = complex((double)*lpSrc , 0); // 频域赋零值 pCFSrc[ lLineBytes*j + i ] = complex(0.0 , 0.0); Visual C++数字图像获取、处理及实践应用 · 236· // 用来对图像做退化的系统 if(i < 5 && j <5 ) { pCTH[ lLineBytes*j + i ] = complex(0.04 , 0.0); } else { pCTH[ lLineBytes*j + i ] = complex(0.0 , 0.0); } // 频域赋零值 pCFH[ lLineBytes*j + i ] = complex(0.0 , 0.0); } } //对源图像进行 FFT ::DIBFFT_2D(pCTSrc, lLineBytes, lHeight, pCFSrc); //对变换核图像进行 FFT ::DIBFFT_2D(pCTH, lLineBytes, lHeight, pCFH); //频域相乘 for (i = 0;i 255) *lpSrc = 255 ; } } //释放存储空间 delete pCTSrc; delete pCTH; delete pCFSrc; delete pCFH; // 返回 return true; } 函数 DIBWinnerFilter 用来对退化后的图像进行维纳滤波复原,代码如下。 /************************************************************************* * * 函数名称: * DIBWinnerFilter() * * 参数: * CDib *pDib - 指向 CDib 类的指针 * * 返回值: Visual C++数字图像获取、处理及实践应用 · 238· * BOOL - 维纳滤波复原操作成功返回 TRUE,否则返回 FALSE。 * * 说明: * 该函数用来对 DIB 图像进行维纳滤波复原操作。 * ************************************************************************/ BOOL WINAPI DIBWinnerFilter (CDib *pDib) { // 指向源图像的指针 BYTE * lpSrc; //图像的宽度和高度 LONG lWidth; LONG lHeight; // 图像每行的字节数 LONG lLineBytes; //得到图像的宽度和高度 CSize SizeDim; SizeDim = pDib->GetDimensions(); lWidth = SizeDim.cx; lHeight = SizeDim.cy; //得到实际的 Dib 图像存储大小 CSize SizeRealDim; SizeRealDim = pDib->GetDibSaveDim(); // 计算图像每行的字节数 lLineBytes = SizeRealDim.cx; //图像数据的指针 LPBYTE lpDIBBits = pDib->m_lpImage; //循环变量 long i; long j; //临时变量 double temp, tempre, tempim, a, b, c, d, norm2; // 实际进行傅立叶变换的宽度和高度 LONG lW = 1; LONG lH = 1; 第 5 章 图像复原 · 239· int wp = 0; int hp = 0; // 保证离散傅立叶变换的宽度和高度为 2 的整数次方 while(lW * 2 <= lLineBytes) { lW = lW * 2; wp++; } while(lH * 2 <= lHeight) { lH = lH * 2; hp++; } //用来存储源图像和变换核的时域数据 complex *pCTSrc,*pCTH; //用来存储源图像和变换核的频域数据 complex *pCFSrc,*pCFH; //图像归一化因子 double MaxNum; //输入退化图像的长和宽必须为 2 的整数倍 if(lW != (int) lLineBytes) { return false; } if(lH != (int) lHeight) { return false; } // 为时域和频域的数组分配空间 pCTSrc = new complex [lHeight*lLineBytes]; pCTH = new complex [lHeight*lLineBytes]; pCFSrc = new complex [lHeight*lLineBytes]; pCFH = new complex [lHeight*lLineBytes]; // 滤波器加权系数 double *pCFFilter = new double [lHeight*lLineBytes]; for (j = 0;j < lHeight ;j++) { for(i = 0;i < lLineBytes ;i++) { Visual C++数字图像获取、处理及实践应用 · 240· // 指向退化图象倒数第 j 行,第 i 个像素的指针 lpSrc = (unsigned char *)lpDIBBits + lLineBytes * j + i; // 将像素值存储到时域数组中 pCTSrc[ lLineBytes*j + i ] = complex((double)*lpSrc , 0); // 频域赋零值 pCFSrc[ lLineBytes*j + i ] = complex(0.0 , 0.0); // 退化系统时域及维纳滤波加权系数赋值 if(i < 5 && j <5) { pCTH[ lLineBytes*j + i ] = complex(0.04 , 0.0); pCFFilter[ lLineBytes*j + i ] = 0.5; } else { pCTH[ lLineBytes*j + i ] = complex(0.0 , 0.0); pCFFilter[ lLineBytes*j + i ] = 0.05; } // 频域赋零值 pCFH[ lLineBytes*j + i ] = complex(0.0 , 0.0); } } //对退化图像进行 FFT ::DIBFFT_2D(pCTSrc, lLineBytes, lHeight, pCFSrc); //对变换核图像进行 FFT ::DIBFFT_2D(pCTH, lLineBytes, lHeight, pCFH); // 计算 M for (i = 0; i < lHeight * lLineBytes; i++) { // 赋值 a = pCFSrc[i].real(); b = pCFSrc[i].imag(); c = pCFH[i].real(); d = pCFH[i].imag(); // |H(u,v)|*|H(u,v)| norm2 = c * c + d * d; // |H(u,v)|*|H(u,v)|/(|H(u,v)|*|H(u,v)|+a) temp = (norm2 ) / (norm2 + pCFFilter[i]); 第 5 章 图像复原 · 241· { tempre = ( a*c + b*d ) / ( c*c + d*d ); tempim = ( b*c - a*d ) / ( c*c + d*d ); // 求得 f(u,v) pCFSrc[i]= complex(temp*tempre , temp*tempim); } } //对复原图像进行反 FFT IFFT_2D(pCFSrc, pCTSrc, lLineBytes, lHeight); //转换为复原图像 for (j = 0;j < lHeight ;j++) { for(i = 0;i < lLineBytes ;i++) { // 指向复原图像倒数第 j 行,第 i 个像素的指针 lpSrc = (unsigned char *)lpDIBBits + lLineBytes * j + i; a = pCTSrc[(lLineBytes)*j + i].real(); b = pCTSrc[(lLineBytes)*j + i].imag(); norm2 = a*a + b*b; norm2 = sqrt(norm2) + 40; if(norm2 > 255) norm2 = 255.0; if(norm2 < 0) norm2 = 0; *lpSrc = (unsigned char) (norm2); } } //释放存储空间 delete pCTSrc; delete pCTH; delete pCFSrc; delete pCFH; delete pCFFilter; // 返回 return true; Visual C++数字图像获取、处理及实践应用 · 242· } 在 ImageProcessingView.cpp 中对图像加噪卷积退化子菜单添加命令处理函数,代码如下。 void CImageProcessingView::OnDEGENERATIONWinner() { // 图像的加噪模糊 // 更改光标形状 BeginWaitCursor(); // 获取文档 CImageProcessingDoc* pDoc = GetDocument(); // 获得图像 CDib 类的指针 CDib * pDib = pDoc->m_pDibInit; // 获得图像的头文件信息 LPBITMAPINFOHEADER lpBMIH=pDib->m_lpBMIH; // 判断是否是 8-bpp 位图(这里为了方便,只处理 8-bpp 位图的加噪模糊) if (lpBMIH->biBitCount != 8) { // 提示用户 MessageBox("目前只支持 256 色位图的加噪模糊!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } ::DIBNoiseDegeneration(pDib); // 设置脏标记 pDoc->SetModifiedFlag(TRUE); // 更新视图 pDoc->UpdateAllViews(NULL); // 恢复光标 EndWaitCursor(); } 在 ImageProcessingView.cpp 中对图像维纳滤波复原子菜单添加命令处理函数,代码如下。 void CImageProcessingView::OnRestoreWinner() { // 图像的维纳滤波 // 更改光标形状 BeginWaitCursor(); // 获取文档 第 5 章 图像复原 · 243· CImageProcessingDoc* pDoc = GetDocument(); // 获得图像 CDib 类的指针 CDib * pDib = pDoc->m_pDibInit; // 获得图像的头文件信息 LPBITMAPINFOHEADER lpBMIH=pDib->m_lpBMIH; // 判断是否是 8-bpp 位图(这里为了方便,只处理 8-bpp 位图的维纳滤波) if (lpBMIH->biBitCount != 8) { // 提示用户 MessageBox("目前只支持 256 色位图的维纳滤波!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } ::DIBWinnerFilter(pDib); // 设置脏标记 pDoc->SetModifiedFlag(TRUE); // 更新视图 pDoc->UpdateAllViews(NULL); // 恢复光标 EndWaitCursor(); } 用在菜单中实现的功能对图像进行加入噪声的卷积模糊及维纳滤波复原的操作。图 5-8 所示是进行模 糊前后的效果。 lena 原图 加噪卷积模糊 图 5-8 lena 原图及加噪模糊后效果 图 5-9 所示是经维纳滤波复原后的图像。 Visual C++数字图像获取、处理及实践应用 · 244· 图 5-9 维纳滤波复原后图像 5.4 约束复原 在最小二乘方复原处理中,常常附加某种约束条件。例如,如果令 Q 为 f 的线性算子,那么最小二乘 方复原的问题可以看成使形式为 2ˆQf 的函数,服从约束条件 2 2ˆg Hf n− = 的最小化问题。这种有附 加条件的极值问题可以用拉格朗日乘数法来处理。 寻找一个 ˆf ,使下述准则函数为最小: 2 2 2()()J f Qf g Hf nλ= + − −   (5-67) 式中 λ 为一个常数,是拉格朗日系数。加上约束条件以后,就可以按一般求最小值的方法进行求解。 式 5-67 对 ˆf 微分,并使得结果为零,则有: TT() 2 2 ( ) 0J f Q Qf H g Hf f λ∂ = − − = ∂     (5-68) 从上式中可以解得 ˆf : TTT 0Q Qf H Hf H gλ λ+ − =  T T 1 T1()f Q Q H H H gλ −= + ⋅ (5-69) 第第 66 章章 图图像像处处理理中中的的正正交交变变换换 数字图像处理的方法主要分为两大类:一类是空域的处理法(或称空域法),一类是频域法(或称变 换域法)。把图像变换到频率域可以从另外一个角度来分析图像的特性,以便更准确地处理它。在频域处 理法中最为关键的预处理便是变换处理。这种变换一般是线性变换,其基本线性运算式是严格可逆的, 并且每一个变换都存在自己的正交函数集,满足一定的正交条件,因此,也可以将其称作酉变换。正如 表示空间的一个矢量可以用不同的坐标系一样,变换的途径虽然不同,但是它们都是空域图像的变换域 表示式。 目前,在图像处理技术中,正交变换正被广泛地运用于图像的特征提取、图像增强、图像复原、图 像识别以及图像的变换编码等领域中。 本章从理论最完善、应用程序最多的傅立叶变换入手,对几种主要的正交变换进行较详细的讨论。 6.1 傅立叶变换 1807 年傅立叶(Fourier)提出了“任何周期信号都可用正弦函数级数表示”的说法,1829 年狄里 赫利第一个给出收敛条件,傅立叶变换于 1822 年首次发表在《热的分析理论》一书中。在这以前,数学、 物理学等科学领域对信号或者函数的分析主要是从时域入手的;在傅立叶变换提出之后,信号处理和分 析有了新的方法,那就是频谱分析。频域的分析在后来的理论和技术发展中都起着很大的作用,今天我 们仍然在使用频域的各种分析方法,应用的领域也更加广泛,如工业控制、无线电技术、工业检测等。 6.1.1 傅立叶级数 傅立叶变换始于周期信号的频谱分析,周期信号可展开为正交函数线性组合的无穷级数。注意:不 是所有的周期函数都可以进行傅立叶级数展开,傅立叶级数展开需要满足狄利赫利条件: (1)在一个周期内只有有限个间断点。 (2)在一个周期内有有限个极值点。 (3)在一个周期内函数绝对可积,即: Á Á ( ) d + ⋅ < ∞∫t T t f t t 如果 )(tf 满足上述 3 个条件,那么 )(tf 可以进行三角函数展开: 0 1 1 1 ( ) ( cos sin )n n n f t a a n t b n tω ω ∞ = = + +∑ (6-1) 其中: 0a 为直流分量: Á Á 0 1 1 ( ) d += ⋅∫ t T t a f t tT na 为余弦分量: Á Á 1 1 2 ( ) cos dω+= × ⋅∫ t T n t a f t n t tT Visual C++数字图像获取、处理及实践应用 · 246· nb 为正弦分量: Á Á 1 1 2 ( ).sin dω+= ⋅∫ t T n t b f t n t tT 上述变换的形式是以三角函数作为扩展基函数,根据欧拉公式,傅立叶级数也可以写成指数形式:  1( ) ( )e ωω ∞ =−∞ = ∑ jn t n f t F n (6-2) 其中: 0)0( aF = )(2 1)( 1 nn jbanF −=ω )(2 1)( 1 nn jbanF +=− ω 如果令 nFnF =)( 1ω ,那么傅立叶变换可以简写成: Á Á 1 1 ( )e dt T jn t n t F f t tT ω+ −= ∫ (6-3) 6.1.2 连续变量的傅立叶变换及其性质 当信号为非周期信号,或者信号的周期很大时,式 6-3 中的 1ω 会随着周期的变大而逐渐变小,相 邻频率傅立叶级数系数的频率差越来越小,当信号的周期为无穷大时,频率差就会趋近于 0。这时,时 域信号不再是周期信号,它的频谱分析也不能通过傅立叶级数来完成。此时可以通过傅立叶变换进行频 谱分析。连续傅立叶变换的定义如下,其中式 6-4、6-5 中的ω 是连续的频域变量。 正变换: ( ) ( )e dωω ω∞ − −∞ = ∫ j tF f t (6-4) 反变换: 1( ) ( )e d2 ωω ω∞ −∞ = ∫ j tf t F (6-5) 值得注意的是式 6-5 中的系数 1 2 可以放到式 6-4 中,也就是傅立叶变换可以写成如下形式: 1( ) ( )e d2 ωω ω∞ − −∞ = ∫ j tF f t ( ) ( )e dj tf t F ωω ω∞ −∞ = ∫ 第 6 章 图像处理中的正交变换 · 247· 傅立叶变换具有如下性质。 1. 对偶性 如果 [ ])()( tfFTF =ω 那么: [ ]( ) 2  ω=FT F t f (6-6) 这个性质在傅立叶变换计算中得到了广泛应用,假设知道 )(tf 的傅立叶变换是 )(ωF ,那么利用 式 6-6,就可以很容易的计算 )(tF 的傅立叶变换。 2. 线性(叠加性) 如果 [ ] )()( ωii FtfFT = 那么: 1 1 ()() n n i i i i i i FT a f t a F ω = =   =   ∑ ∑ (6-7) 该性质说明时域信号的傅立叶变换等于各信号傅立叶变换之和,这一性质在线性系统分析中有重要 的作用。 3. 奇偶虚实性 如果 )()]([ ωFtfFT = 那么: )()]([ ω−=− FtfFT : 时域反摺,频域反摺 )()]([** ω−= FtfFT : 时域共轭,频域共轭并且反摺 )()]([** ωFtfFT =− : 时域共轭反摺,频域共轭 4. 尺度变换特性 如果 )()]([ ωFtfFT = 那么: 1[ ( )] ( )= wFT f at Fa a (6-8) 这个性质告诉我们,只要知道“基”函数 )(tf 的傅立叶变换,那么和它“类似”的一系列函数的 傅立叶变换就可以通过对 )(tf 的傅立叶变换伸缩得到。 Visual C++数字图像获取、处理及实践应用 · 248· Á 5. 时移特性和频移特性 如果 [ ] )()( ωFtfFT = 那么: [ ] e Á-jw t 0FT f(t - t ) = F(w) (6-9) Á 0[ ( )e ] ( )ω ω ω= −j tFT f t F (6-10) 联合 6-8 和 6-9 两式,就可以得到带有尺度变换的时移特性 Á- 0 1[(-)] e () , 0 ω ω= > tj aFT f at t F aa a (6-11) 6. 微分和积分特性 如果 [ ( )] ( )=FT f t F w 那么: d d      f(t)FT = jwF(w)t d ( ) ()()d ω ω  =   n n n f tFT j Ft (6-12) 如果同时满足 ()0, ωω ω= < ∞F ,或者 (0) 0=F 那么: ()( )d ωτ τ ω−∞   =  ∫ t FFT f j (6-13) 已知 )(tf 的傅立叶变换,利用式 6-12 和 6-13 就可以方便的计算 )(tf 任意阶微分的傅立叶变换。 7. 卷积定理 如果 )()]([ 11 ωFtfFT = )()]([ 22 ωFtfFT = 那么: 1 2 1 2[()* ()] () ()FT f t f t F Fω ω= (6-14) 1 2 1 2 1[()()] ()*()2 ω ω=FT f t f t F F (6-15)第 6 章 图像处理中的正交变换 · 249· 式 6-14 和式 6-15 是线性系统傅立叶分析的理论基础,是频谱分析、系统分析的基础。 6.1.3 离散傅立叶变换及其性质 信号处理技术总是得益于信号处理理论与实现信号处理系统的硬件之间的紧密联系。20 世纪 50 年 代之前,信号处理技术主要是模拟技术,处理的信号也多是连续时间的模拟信号。大规模集成电路的发 展,使计算机和微处理器得到了广泛的应用,随着同时期其他理论和技术的成果一起使人类的生活进入 了数字化时代。数字化导致了数字信号处理领域的出现,数字信号处理的基本点是基于数字序列的处理, 而且也无需专门的硬件去实现,这就为数字信号的广泛应用奠定了基础。在数字信号处理领域,信号是 用序列来表示的,并且用数字运算来实现运算和处理。 信号处理问题不仅限于一维信号,随着计算机处理能力的增强,科学家和工程师们已经把数字信号 处理问题扩展到二维、三维,甚至高维。虽然理论上一维与多维信号处理存在着一些差别,但是经过适 当的变化,科学家已经成功的把数字信号处理理论扩展到二维图像。在目前的多媒体技术中,许多处理 问题需要用到二维信号处理技术,如视频编码、医学图像处理、遥感图像的分析处理。 利用计算机实现信号处理时,不是所有的算法都能够实时处理,尤其早期的计算机处理速度还相对 较慢。某些算法就是为了提高速度而产生的,如 1965 产生的一种计算傅立叶变换的快速算法。也有某些 算法用模拟设备是无法实现的,而信号处理算法的发展反过来促进了数字化信号处理系统的分析。快速 傅立叶变换算法(FFT)的产生使得数字信号处理走上了一个新的台阶,一般而言,早期的信号处理算 法所需要的处理时间根本不能满足实时处理的要求,如谱分析,采用原始的傅立叶变换,不可能有效的 利用它。而快速傅立叶变换算法将傅立叶变换的计算时间减少了几个数量级,这样,一些信号处理算法 的实现在其处理时间上就大大缩短。更为重要的是,FFT 算法可以用专门的硬件来实现,再一次加快了 处理速度。下面将逐一介绍离散傅立叶变换及其性质。 对于一个长度为 N 的有限长数字序列,定义序列的离散傅立叶正变换(分析式): Á1 1 0 0 ( ) ( )e ( ) , 0,1,2... 1 − − − = = = = = −∑ ∑Á NN j kn nk n n X k x n x n W n N (6-16) 反变换(综合式): Á1 1 0 0 1 1( ) ( )e ( ) , 0,1,2... 1 − − − − = = = = = −∑ ∑Á NN j nk nk k k x n X k X k W k NNN (6-17) 离散傅立叶变换的性质有些和连续傅立叶变换很相近,具体如下。 1. 线性 如果有两个有限长序列 )(1 nx 、 )(2 nx 线性组合,如 )()()( 213 nbxnaxnx += ,则 )(3 nx 的 DFT 为: 3 1 2()()()X k aX k bX k= + (6-18) 2. 时移特性 设 Nnx ))(( 为 )(nx 的周期延拓,并且 )()]([ kXnxDFT = , )())(()( nGmnxny NN−= , 其中 )(nGN 为窗函数: 1, 0 1() 0,Á n NG n ≤ ≤ −= 其他 ,那么: Visual C++数字图像获取、处理及实践应用 · 250· Á [ ( )] ( ), e−= = ÁjmkDFT y n W X k W (6-19) 3. 频移特性 如果 )()]([ kXnxDFT = )k())(()( NN GlkXkY −= 那么: [ ( )] ( ) −= lnIDFT Y k x n W (6-20) 4. 对偶性 和连续时间傅立叶变换的性质相似,数字信号傅立叶变换也具有对偶性: [ ( )] [(( )) ]NDFT X n Nx k= − (6-21) 5. 频域圆周卷积定理 如果 )()()( nhnxny = 那么: 1 0 1 0 ( ) [ ( )] 1 ( ) (( )) ( ) 1 ( ) (( )) ( ) N NN l N NN l Y k DFT y n X l H k l G kN H l X k l G kN − = − = = = − = − ∑ ∑ (6-22) 6. 时域圆周卷积定理 如果 )()()( kHkXkY = 那么: 1 0 ( ) ( ) (( )) ( ) N NN m y n x m h n m G n − = = −∑ (6-23) 6.1.4 快速傅立叶变换 离散傅立叶变换(DFT)在离散时间系统的分析、设计中起着重要的作用。前面讲述的傅立叶变换 和离散傅立叶变换的基本性质,使得我们在频域中分析和设计系统非常方便。但是对于一个算法同样重 要的是计算的时间和空间复杂度。快速傅立叶变换 FFT 算法的计算效率要比基本的离散傅立叶变换高出 几个数量级,由于 FFT 的高效率,在许多情况下实现卷积最有效的方法是先计算参与卷积的序列的离散 傅立叶变换,然后将它们的变换相乘,最后计算这些变换乘积的逆变换。下面我们就来介绍 FFT 的原理 和算法实现。 第 6 章 图像处理中的正交变换 · 251· 如果直接利用式 6-16 和 6-17 进行计算,那么可以计算每一个 k 值,直接计算 X(k)就需要 4N 次实数 乘法和(4N-2)次实数加法。因为必须计算对于 N 个不同 k 值的 X(k),所以序列 x(n)的离散傅立叶变换的 直接计算法需要 4 ÁN 次实数乘法和 N (4N-2)次实数加法。由于计算的总次数以及所需的时间大致上与 ÁN 成正比,显然当 N 值很大时用直接法计算 DFT 所需要的运算次数就非常大。正是这个原因,我们才 关注能减少乘法和加法次数的计算方法。 改善 DFT 计算效率的大多数方法均利用了离散傅立叶变换的对称性和周期性,我们以 N=4 为例来 阐述快速傅立叶变换的原理。 考虑W 因子的周期性和半周期性( Á e−= ÁjW ),从离散傅立叶变换的性质,我们不难得到: (1) 1,1,1 00 ==== mN NN N NNWWWW; (2) *][ rNr N r N mNr N WWWW −+ == ; (3) Á ÁÁ ÁÁÂe 1, 1− +−= = = − = −ÁÁ ÁÁÁÂÃÁ ÃÃW e W ; (4) r N r N WW Á −=+ Á。 那么,离散傅立叶变换的各个系数为: 0=k 时, ÁÁÁÁ ÂÂÂÂ(0) (0) (1) (2) (3)= + + +X x W x W x W x W 。 1=k 时, 3 4 2 4 1 4 0 4 )3()2()1()0()1( WxWxWxWxX +++= 。 2=k 时, 6 4 4 4 2 4 0 4 )3()2()1()0()2( WxWxWxWxX +++= 。 3=k 时, 9 4 6 4 3 4 0 4 )3()2()1()0()3( WxWxWxWxX +++= 。 写成矩阵的形式为:                           =             )3( )2( )1( )0( )3( )2( )1( )0( 9 4 6 4 3 4 0 4 6 4 4 4 2 4 0 4 3 4 2 4 1 4 0 4 0 4 0 4 0 4 0 4 x x x x WWWW WWWW WWWW WWWW X X X X 因为 10 =NW 、 1=mN NW ,等式变成:                         =             )3( )2( )1( )0( 1 11 1 1111 )3( )2( )1( )0( 9 4 6 4 3 4 6 4 2 4 3 4 2 4 1 4 x x x x WWW WW WWW X X X X 因为 1)( Á−=+ ÁmN NW ,等式变成: Visual C++数字图像获取、处理及实践应用 · 252·                         − −− −=             )3( )2( )1( )0( 11 1111 11 1111 )3( )2( )1( )0( 9 4 3 4 3 4 1 4 x x x x WW WW X X X X 因为 r N r N WW Á−=+ )( Á,等式变成: 1 1 4 4 1 1 4 4 1 1 1 1(0) (0) 1 1(1) (1) 1 1 1 1(2) (2) 1 1(3) (3) X x WWX x X x WWX x          − −    =     − −     − −      (6-24) 由上面的方程可以看出,4 点傅立叶变换系数只与 x(1)、x(3)有关,这就说明在运算的过程中,没有 必要按照式 6-16 直接计算,而是可以找到某种办法,节省一些计算。 傅立叶快速变换算法有很多种方法,目前广为使用的快速算法是蝶形算法,两点傅立叶变换的蝶形 如图 6-1 所示。图 6-2 所示是按时间抽取将 8 点 DFT 计算完全分解的流图。利用流图就可以对傅立叶变 换进行快速计算。 x(0) x(4) 0 1=NW / 2 2 1= =N NWW ÁÂÃÄ ÁÂÅÄ ÁÂÆÄ ÁÂÇÄ ÁÂÈÄ ÁÂÉÄ ÁÂ Ä ÁÂ Ä Á ÁW Á ÁW Á ÁW Á ÁW  ÁW  ÁW ÁW  ÁW  ÁW  ÁW  ÁW  ÁW Á ÁW Á ÁW à ÁW à ÁW Ä ÁW Ä ÁW ÂÃÄ ÂÈÄ ÂÆÄ Â Ä ÂÅÄ ÂÉÄ ÂÇÄ Â Ä Â ÁW à ÁW Å ÁW Æ ÁW Á ÁW Ç ÁW Ä ÁW È ÁW 图 6-1 2 点 FFT 的蝶形图 图 6-2 按时间抽取将 8 点 DFT 计算完全分解的流图 这种蝶形算法的计算首先需要把输入序列倒序重排,倒序重排后就可以按照蝶形图进行计算。 第 6 章 图像处理中的正交变换 · 253· 6.1.5 二维图像的快速傅立叶变换 图像处理的一大类运算是线性的,其输出图像是输入图像的线性组合。这类线性运算包括叠加、卷积、 变换和线性滤波等。傅立叶变换在二维线性系统分析中有着广泛的应用。离散图像傅立叶变换定义如下。 正变换: ()() 2 1 1 0 0 1, , e e − − − − = = = ∑∑NN j mu j nvNN m n F u v f m nN (6-25) 反变换: ()() 2 1 1 0 0 1, , e e − − = = = ∑∑NN j mu j nvNN u v f m n F u vN (6-26) 二维傅立叶变换的运算复杂度是 O(NÁ)。二维傅立叶变换是行列可分的,可以利用行列顺序运算的 2N 次一维 DFT 实现,其运算复杂度是 ( )ON Á ,二维傅立叶变换还可以利用行列顺序运算的 2N 次一维 FFT 实现,其运算复杂度是 ( )ONNÁlog 。 二维傅立叶变换利用行列可分,可以先列变换然后行变换,也可以先行变换然后列变换,其理论基 础如下。 令: () 21 0 ( , ) , e − − = = ∑N j muN m x u n f m n (6-27) 则有: () 21 0 1, ( , )e − − = = ∑N j muN m F u v x v mN (6-28) 从式 6-27 和 6-28 可得知,计算二维傅立叶变换,可以先对列进行一维傅立叶变换,然后对行进行 一维傅立叶变化,进行一维傅立叶变换可以采用快速算法 FFT 来提高速度。 6.1.6 二维图像快速傅立叶变换的 Visual C++程序实现 有了以上的理论基础,我们就可以建立傅立叶变换函数库了。函数库包含直接计算傅立叶变换和反 变换的函数以及一维傅立叶快速变换和反变换、二维傅立叶快速变换和反变换的函数。最后将在菜单中 加入对二维图像实现傅立叶变换和快速傅立叶变换的功能。 首先,我们添加一个“正交变换”的菜单,如图 6-3 所示。子菜单中是可实现的 6 种正交变换。 图 6-3 图像正交变换的菜单 Visual C++数字图像获取、处理及实践应用 · 254· 函数 DFT_2D 可根据傅立叶变换的定义,直接计算二维傅立叶变换,代码如下。 /************************************************************************* * * \函数名称: * DFT_2D() * * \输入参数: * CDib * pDib - 指向 CDib 类的指针,含有图像数据 * double * pTrRstRpart - 指向傅立叶系数实部的指针 * double * pTrRstIpart - 指向傅立叶系数虚部的指针 * * \返回值: * 无 * * \说明: * 二维傅立叶变换 * ************************************************************************* */ VOID WINAPI DIBDFT_2D(CDib * pDib,double * pTrRstRpart, double * pTrRstIpart) { // 定义常量 double PI = 3.14159; // 遍历图像的纵坐标 int y; // 遍历图像的横坐标 int x; // 频域的横坐标 int m; // 频域的纵坐标 int n; // 图像的长宽大小 CSize sizeImage = pDib->GetDimensions() ; int nWidth = sizeImage.cx ; int nHeight = sizeImage.cy ; // 图像在计算机在存储中的实际大小 CSize sizeImageSave = pDib->GetDibSaveDim(); 第 6 章 图像处理中的正交变换 · 255· int nSaveWidth = sizeImageSave.cx; // 图像数据的指针 LPBYTE pImageData = pDib->m_lpImage; // 初始化结果数据 for(n=0; nGetDimensions() ; int nWidth = sizeImage.cx ; int nHeight = sizeImage.cy ; // 图像在计算机在存储中的实际大小 CSize sizeImageSave = pDib->GetDibSaveDim(); int nSaveWidth = sizeImageSave.cx; // 图像数据的指针 第 6 章 图像处理中的正交变换 · 257· LPBYTE pImageData = pDib->m_lpImage; // 正弦和余弦值 double fCosTable; double fSinTable; fCosTable=0 ; fSinTable=0 ; // 临时变量 double fTmpPxValue; double fRpartValue; double fIpartValue; fTmpPxValue=0; fRpartValue=0; fIpartValue=0; for(y=0; y * pCTData - 指向时域数据的指针,输入的需要变换的数据 * complex * pCFData - 指向频域数据的指针,输出的经过变换的数据 * nLevel - 傅立叶变换蝶形算法的级数,2 的幂数 * * \返回值: * 无 * * \说明: * 一维快速傅立叶变换 * ************************************************************************* */ VOID WINAPI FFT_1D(complex * pCTData, complex * pCFData, int nLevel) { // 循环控制变量 int i; int j; int k; // 定义常量 double PI = 3.1415926; // 傅立叶变换点数 int nCount =0 ; // 计算傅立叶变换点数 nCount =(int)pow(2,nLevel) ; // 某一级的长度 int nBtFlyLen; nBtFlyLen = 0 ; // 变换系数的角度 =2 * PI * i / nCount double dAngle; complex *pCW ; 第 6 章 图像处理中的正交变换 · 259· // 分配内存,存储傅立叶变化需要的系数表 pCW = new complex[nCount / 2]; // 计算傅立叶变换的系数 for(i = 0; i < nCount / 2; i++) { dAngle = -2 * PI * i / nCount; pCW[i] = complex ( cos(dAngle), sin(dAngle) ); } // 变换需要的工作空间 complex *pCWork1,*pCWork2 ; // 分配工作空间 pCWork1 = new complex[nCount]; pCWork2 = new complex[nCount]; // 临时变量 complex *pCTmp ; // 初始化,写入数据 memcpy(pCWork1, pCTData, sizeof(complex) * nCount); // 临时变量 int nInter; nInter = 0; // 蝶形算法进行快速傅立叶变换 for(k = 0; k < nLevel; k++) { for(j = 0; j < (int)pow(2,k); j++) { //计算长度 nBtFlyLen = (int)pow( 2,(nLevel-k) ); //倒序重排,加权计算 for(i = 0; i < nBtFlyLen/2; i++) { nInter = j * nBtFlyLen; pCWork2[i + nInter] = pCWork1[i + nInter] + pCWork1[i + nInter + nBtFlyLen / 2]; pCWork2[i + nInter + nBtFlyLen / 2] = Visual C++数字图像获取、处理及实践应用 · 260· (pCWork1[i + nInter] - pCWork1[i + nInter + nBtFlyLen / 2]) * pCW[(int)(i * pow(2,k))]; } } // 交换 pCWork1 和 pCWork2 的数据 pCTmp = pCWork1 ; pCWork1 = pCWork2 ; pCWork2 = pCTmp ; } // 重新排序 for(j = 0; j < nCount; j++) { nInter = 0; for(i = 0; i < nLevel; i++) { if ( j&(1< * pCTData - 指向时域数据的指针,输入的需要反变换的数据 * complex * pCFData - 指向频域数据的指针,输出的经过反变换的数据 * nLevel - 傅立叶变换蝶形算法的级数,2 的幂数 第 6 章 图像处理中的正交变换 · 261· * * \返回值: * 无 * * \说明: * 一维快速傅立叶反变换 * ************************************************************************ */ VOID WINAPI IFFT_1D(complex * pCFData, complex * pCTData, int nLevel) { // 循环控制变量 int i; // 傅立叶反变换点数 int nCount; // 计算傅立叶变换点数 nCount = (int)pow(2,nLevel) ; // 变换需要的工作空间 complex *pCWork; // 分配工作空间 pCWork = new complex[nCount]; // 将需要反变换的数据写入工作空间 pCWork memcpy(pCWork, pCFData, sizeof(complex) * nCount); // 为了利用傅立叶正变换,可以把傅立叶频域的数据取共轭 // 然后直接利用正变换,输出结果就是傅立叶反变换结果的共轭 for(i = 0; i < nCount; i++) { pCWork[i] = complex (pCWork[i].real(), -pCWork[i].imag()); } // 调用快速傅立叶变换实现反变换,结果存储在 pCTData 中 FFT_1D(pCWork, pCTData, nLevel); // 求时域点的共轭,求得最终结果 // 根据傅立叶变换原理,利用这样的方法求得的结果和实际的时域数据 // 相差一个系数 nCount for(i = 0; i < nCount; i++) Visual C++数字图像获取、处理及实践应用 · 262· { pCTData[i] = complex (pCTData[i].real() / nCount, -pCTData[i].imag() / nCount); } // 释放内存 delete pCWork; pCWork = NULL; } 函数 FFT_2D 可实现二维快速傅立叶变换,代码如下。 /************************************************************************* * * \函数名称: * FFT_2D() * * \输入参数: * complex * pCTData - 图像数据 * int nWidth - 数据宽度 * int nHeight - 数据高度 * complex * pCFData - 傅立叶变换后的结果 * * \返回值: * 无 * * \说明: * 二维傅立叶变换 * ************************************************************************ */ VOID WINAPI DIBFFT_2D(complex * pCTData, int nWidth, int nHeight, complex * pCFData) { // 循环控制变量 int x; int y; // 临时变量 double dTmpOne; double dTmpTwo; // 进行傅立叶变换的宽度和高度,(2 的整数次幂) // 图像的宽度和高度不一定为 2 的整数次幂 int nTransWidth; 第 6 章 图像处理中的正交变换 · 263· int nTransHeight; // 计算进行傅立叶变换的宽度 (2 的整数次幂) dTmpOne = log(nWidth)/log(2); dTmpTwo = ceil(dTmpOne); dTmpTwo = pow(2,dTmpTwo); nTransWidth = (int) dTmpTwo; // 计算进行傅立叶变换的高度 (2 的整数次幂) dTmpOne = log(nHeight)/log(2); dTmpTwo = ceil(dTmpOne) ; dTmpTwo = pow(2,dTmpTwo) ; nTransHeight = (int) dTmpTwo ; // x,y(行列)方向上的迭代次数 int nXLev; int nYLev; // 计算 x,y(行列)方向上的迭代次数 nXLev = (int) ( log(nTransWidth)/log(2) + 0.5 ); nYLev = (int) ( log(nTransHeight)/log(2) + 0.5 ); for(y = 0; y < nTransHeight; y++) { // x 方向进行快速傅立叶变换 FFT_1D(&pCTData[nTransWidth * y], &pCFData[nTransWidth * y], nXLev); } // pCFData 中目前存储了 pCTData 经过行变换的结果 // 为了直接利用 FFT_1D,需要把 pCFData 的二维数据转置,再一次利用 FFT_1D 进行 // 傅立叶行变换(实际上相当于对列进行傅立叶变换) for(y = 0; y < nTransHeight; y++) { for(x = 0; x < nTransWidth; x++) { pCTData[nTransHeight * x + y] = pCFData[nTransWidth * y + x]; } } for(x = 0; x < nTransWidth; x++) { // 对 x 方向进行快速傅立叶变换,实际上相当于对原来的图像数据进行列方向的 // 傅立叶变换 FFT_1D(&pCTData[x * nTransHeight], &pCFData[x * nTransHeight], nYLev); Visual C++数字图像获取、处理及实践应用 · 264· } // pCFData 中目前存储了 pCTData 经过二维傅立叶变换的结果,但是为了方便列方向 // 的傅立叶变换,对其进行了转置,现在把结果转置回来 for(y = 0; y < nTransHeight; y++) { for(x = 0; x < nTransWidth; x++) { pCTData[nTransWidth * y + x] = pCFData[nTransHeight * x + y]; } } memcpy(pCTData, pCFData, sizeof(complex) * nTransHeight * nTransWidth ); } 函数 IFFT_2D 可实现二维快速傅立叶反变换,代码如下。 /************************************************************************* * * \函数名称: * IFFT_2D() * * \输入参数: * complex * pCFData - 频域数据 * complex * pCFData - 时域数据 * int nWidth - 图像数据宽度 * int nHeight - 图像数据高度 * * \返回值: * 无 * * \说明: * 二维傅立叶反变换 * ************************************************************************ */ VOID WINAPI IFFT_2D(complex * pCFData, complex * pCTData, int nWidth, int nHeight) { // 循环控制变量 int x; int y; // 临时变量 double dTmpOne; double dTmpTwo; 第 6 章 图像处理中的正交变换 · 265· // 进行傅立叶变换的宽度和高度,(2 的整数次幂) // 图像的宽度和高度不一定为 2 的整数次幂 int nTransWidth; int nTransHeight; // 计算进行傅立叶变换的宽度 (2 的整数次幂) dTmpOne = log(nWidth)/log(2) ; dTmpTwo = ceil(dTmpOne) ; dTmpTwo = pow(2,dTmpTwo) ; nTransWidth = (int) dTmpTwo ; // 计算进行傅立叶变换的高度 (2 的整数次幂) dTmpOne = log(nHeight)/log(2) ; dTmpTwo = ceil(dTmpOne) ; dTmpTwo = pow(2,dTmpTwo) ; nTransHeight = (int) dTmpTwo ; // 分配工作需要的内存空间 complex *pCWork= new complex[nTransWidth * nTransHeight]; //临时变量 complex *pCTmp ; // 为了利用傅立叶正变换,可以把傅立叶频域的数据取共轭 // 然后直接利用正变换,输出结果就是傅立叶反变换结果的共轭 for(y = 0; y < nTransHeight; y++) { for(x = 0; x < nTransWidth; x++) { pCTmp = &pCFData[nTransWidth * y + x] ; pCWork[nTransWidth * y + x] = complex( pCTmp->real() , -pCTmp->imag() ); } } // 调用傅立叶正变换 ::DIBFFT_2D(pCWork, nWidth, nHeight, pCTData) ; // 求时域点的共轭,求得最终结果 // 根据傅立叶变换原理,利用这样的方法求得的结果和实际的时域数据 // 相差一个系数 for(y = 0; y < nTransHeight; y++) { Visual C++数字图像获取、处理及实践应用 · 266· for(x = 0; x < nTransWidth; x++) { pCTmp = &pCTData[nTransWidth * y + x] ; pCTData[nTransWidth * y + x] = complex( pCTmp->real()/(nTransWidth*nTransHeight), -pCTmp->imag()/(nTransWidth*nTransHeight) ); } } delete pCWork ; pCWork = NULL ; } 在 ImageProcessingView.cpp 中对离散傅立叶变换子菜单添加命令处理函数,代码如下。需要注意的 是,由于没有利用快速变换,因此实现图像傅立叶变换的时间非常长。 void CImageProcessingView::OnFreqDft() { //图像离散傅立叶变换 //提示用户,直接进行离散傅立叶变换的时间很长 MessageBox("没有使用 FFT,时间可能很长!", "作者提示" , MB_ICONINFORMATION | MB_OK); //更改光标形状 BeginWaitCursor(); // 循环控制变量 int y; int x; CImageProcessingDoc * pDoc = (CImageProcessingDoc *)this->GetDocument(); CDib * pDib = pDoc->m_pDibInit; LPBITMAPINFOHEADER lpBMIH=pDib->m_lpBMIH; // 判断是否是 8-bpp 位图(这里为了方便,只处理 8-bpp 位图的离散傅立叶变换) if (lpBMIH->biBitCount != 8) { // 提示用户 MessageBox("目前只支持 256 色位图的离散傅立叶变换!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } 第 6 章 图像处理中的正交变换 · 267· //图像的长宽大小 CSize sizeImage = pDib->GetDimensions(); int nWidth = sizeImage.cx ; int nHeight = sizeImage.cy ; // 计算图像数据存储每行需要的字节数 // BMP 文件的每行数据存储是 DWORD 对齐的 int nSaveWidth; nSaveWidth = ( (nWidth << 3) + 31)/32 * 4 ; // 指向图像数据的指针 LPBYTE lpImage ; lpImage = pDib->m_lpImage ; double * pTrRstRpart = new double [nWidth*nHeight]; double * pTrRstIpart = new double [nWidth*nHeight]; ::DIBDFT_2D(pDib, pTrRstRpart,pTrRstIpart); // 临时变量 double dTmp; for(y=0; ySetModifiedFlag(TRUE); // 更新视图 pDoc->UpdateAllViews(NULL); // 恢复光标形状 EndWaitCursor(); // 刷新屏幕 Invalidate(); } 在 ImageProcessingView.cpp 中对快速离散傅立叶变换子菜单添加命令处理函数,代码如下。 void CImageProcessingView::OnFreqFft() { //图像 FFT 变换 // 更改光标形状 BeginWaitCursor(); // 循环控制变量 int y; int x; // 获得 Doc 类的指针 CImageProcessingDoc * pDoc = (CImageProcessingDoc *)this->GetDocument(); CDib * pDib = pDoc->m_pDibInit; LPBITMAPINFOHEADER lpBMIH=pDib->m_lpBMIH; 第 6 章 图像处理中的正交变换 · 269· // 判断是否是 8-bpp 位图(这里为了方便,只处理 8-bpp 位图的离散傅立叶变换) if (lpBMIH->biBitCount != 8) { // 提示用户 MessageBox("目前只支持 256 色位图的离散傅立叶变换!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } // 图像的宽长 CSize sizeImage ; int nWidth ; int nHeight; // 获得图像的宽长 sizeImage = pDib->GetDimensions() ; nWidth = sizeImage.cx; nHeight= sizeImage.cy; // 临时变量 double dTmpOne; double dTmpTwo; // 傅立叶变换竖直方向点数 int nTransHeight ; // 傅立叶变换水平方向点数 int nTransWidth ; // 计算进行傅立叶变换的点数 (2 的整数次幂) dTmpOne = log(nWidth)/log(2); dTmpTwo = ceil(dTmpOne) ; dTmpTwo = pow(2,dTmpTwo) ; nTransWidth = (int) dTmpTwo ; // 计算进行傅立叶变换的点数 (2 的整数次幂) dTmpOne = log(nHeight)/log(2); dTmpTwo = ceil(dTmpOne); dTmpTwo = pow(2,dTmpTwo); nTransHeight = (int) dTmpTwo; Visual C++数字图像获取、处理及实践应用 · 270· // 计算图像数据存储每行需要的字节数 // BMP 文件的每行数据存储是 DWORD 对齐的 int nSaveWidth; nSaveWidth = ( (nWidth << 3) + 31)/32 * 4 ; // 指向图像数据的指针 LPBYTE lpImage ; lpImage = pDib->m_lpImage ; // 图像像素值 unsigned char unchValue; // 指向时域数据的指针 complex * pCTData ; // 指向频域数据的指针 complex * pCFData ; // 分配内存 pCTData=new complex[nTransWidth * nTransHeight]; pCFData=new complex[nTransWidth * nTransHeight]; // 初始化 // 图像数据的宽和高不一定是 2 的整数次幂,所以 pCTData // 有一部分数据需要补 0 for(y=0; y(0,0); } } // 把图像数据传给 pCTData for(y=0; y(unchValue,0); } } // 傅立叶正变换 DIBFFT_2D(pCTData, nWidth, nHeight, pCFData) ; 第 6 章 图像处理中的正交变换 · 271· // 临时变量 double dTmp; for(y=0; ySetModifiedFlag(TRUE); // 更新视图 pDoc->UpdateAllViews(NULL); // 恢复光标形状 EndWaitCursor(); } 利用在菜单中的功能对图像进行傅立叶变换,图 6-4 所示为进行变换前后的效果。 图 6-4 图像的傅立叶变换 可以看出,经过傅立叶变换,频域的能量集中在两条对角线附近。 6.2 离散余弦变换(DCT) 图像处理中常用的正交变换除了傅立叶变换以外,还有其他一些有用的正交变换,离散余弦就是其 中的一种。离散余弦变换表示为 DCT。 6.2.1 离散余弦变换定义 一维序列离散余弦变换的定义可以由下式表示: 1 0 1(0) ( ) N x F f x N − = = ∑ (6-29) 1 0 2 2( 1)( ) ( )cos 2 N x x uF u f xNN − = += ∑ (6-30) 式中 F(u)是第 u 个余弦变换系数,u 是广义频率变量,u=1,2,…,N-1,f(x)是时域 N 点序列, x=0,1,2,…,N-1。 一维离散余弦反变换(IDCT)由下式表示: 1 1 1 2 (2 1)( ) (0) ( )cos 2 N u x uf x F F uNNN − = += + ∑ (6-31)第 6 章 图像处理中的正交变换 · 273· 把上述的一维 DCT 推广到二维离散变换,表达式如下: 1 1 0 0 (2 1)  ( , ) ( ) ( ) ( , )cos cos2 2 NN x y x u y vF u v a u a v f x y NN − − = = + += ⋅∑ ∑ (6-32) 逆变换可以表示为: 1 1 0 0 (2 1)  ( , ) ( ) ( ) ( , )cos cos2 2 NN u v x u y vf x y a u a v F u v NN − − = = + += ⋅∑∑ (6-33) 其中空域和变换域元素矩阵维数为 N,f(x,y)是二维空间域的元素,x、y=0,1,2,…, -1N , F(u,v)为经过二维离散余弦变换后的变换域元素。式中系数为: 1 2(0) , ( ) ,1 1a a x x NNN = = ≤ ≤ − (6-34) 上述是离散余弦变换的解析表达式,更为简单的是采用矩阵形式表示。取 N=4,对于一维 DCT 变 换,根据解析式的定义,可以展开得到下式: (0) 0.500 (0) 0.500 (1) 0.500 (2) 0.500 (3) (1) 0.653 (0) 0.271 (1) 0.271 (2) 0.653 (3) (2) 0.500 (0) 0.500 (1) 0.500 (2) 0.500 (3) (3) 0.271 (0) 0.653 (1) 0.653 (2) 0.271 (3) F f f f f F f f f f F f f f f F f f f f = + + +  = + − − = − − +  = − + − (6-35) 写成矩阵形式: (0) 0.500 0.500 0.500 0.500 (0) (1) 0.653 0.271 0.271 0.653 (1) (2) 0.500 0.500 0.500 0.500 (2) (3) 0.271 0.653 0.653 0.271 (3) F f F f F f F f            − −     =     − −      − −      ⋅ (6-36) 如果定义 [ ]A 为变换矩阵, [ ]()F u 为变换系数矩阵, [ ]()f x 为时域数据矩阵,则一维离散余弦 可以用下面的矩阵形式定义: [ ] [ ] [ ]()()F u A f x= ⋅ (6-37) 同理,可以根据式 6-35 得到一维离散余弦反变换的矩阵形式: (0) 0.500 (0) 0.653 (1) 0.500 (2) 0.500 (3) (1) 0.500 (0) 0.271 (1) 0.500 (2) 0.653 (3) (2) 0.500 (0) 0.271 (1) 0.500 (2) 0.653 (3) (3) 0.500 (0) 0.653 (1) 0.500 (2) 0.271 (3) f F F F F f F F F F f F F F F f F F F F = + + +  = + − − = − − +  = − + − (6-38) 也就是: [ ] [ ] [ ]()()Tf x A F u= ⋅ (6-39) 二维离散余弦变换也可以写成矩阵形式: Visual C++数字图像获取、处理及实践应用 · 274· [ ] [ ] [ ][ ]( ,)(,) TF u v A f x y A= (6-40) 反变换的矩阵形式为: [ ] [ ] [ ][ ]( ,)(,)Tf x y A F u v A= (6-41) 其中 [ ]( ,)f x y 是空域数据元素, [ ]( ,)F u v 是变换系数矩阵, [ ]A 是变换矩阵。 6.2.2 离散余弦变换的计算 与傅立叶变换一样,离散余弦变换自然可以从定义出发进行计算。但是在实际中为了减少计算量, 使得计算更加方便,需要寻找一种快速算法。 以一维离散余弦变换为例,对快速算法进行推导。 对于 F(u),可以写成以下的形式: 1 0 (2 1)1 2 0 (2 1)1 2 0 2 (2 1)( ) ( )cos 2 2 ( ) Re e 2 Re ( )e N x x uN j N x x uN j N x x uF u f xNN f xN f xN − = +− − = +− − = +=  =      =     ∑ ∑ ∑ (6-42) 其中 Re 代表取实部。对于时域数据进行如下的延拓: e ( ) 0,1,2, , 1() 0 , 1, ,2 1 f x x Nf x x N N N = −=  = + −   (6-43) 则 e ()f x 的离散余弦变换可以写成: 2 1 e 0 2 1 e 0 (2 1) p2 1 2 e 0 22 1 2 2 e 0 1(0) ( ) 2 (2 1)( ) ( )cos 2 2 Re ( )e 2 Re e ( )e N x N x x uN j N x u xuNj jNN x F f x N x uF u f xNN f xN f xN π − = − = +− − = −− − = = +=  =      =     ∑ ∑ ∑ ∑ (6-44) 由式 6-44 可得: 22 1 2 e 0 ( )e xuN j N x f x − − = ∑ 第 6 章 图像处理中的正交变换 · 275· 是 2N 点的离散傅立叶变换。所以在做离散余弦变换的时候,可以把序列的长度延长为 2N,然后做离 散傅立叶变换,产生的结果取实部就可以得到结果。 同理,在做反变换时,首先在变换空间把 [ ]()F u 做如下的延拓: e ( ) 0,1,2, , 1() 0 , 1, ,2 1 F u u NF u u N N N = −=  = + −   (6-45) 这样,反变换可以表示为: 2 1 e e 1 (2 1)2 1 2 e e 1 22 1 2 2 e e 1 22 1 2 2 e e 1 e 1 2 (2 1)( ) (0) ( )cos 2 1 2(0) ( ) Re e 1 2(0) ( ) Re e e 1 2(0) Re ( ) e e 1 2 (0) N u x uN j N u xu uN j jNN u xu uN j jNN u x uf x F F uNNN F F uNN F F uNN F F uNN FNN − = +− = − = − = += +  = +      = +      = +      = − +    ∑ ∑ ⋅∑ ⋅ ⋅∑ 2 1 2 2 e 0 2 Re ( ) e e u xuN j jNN u F uN − =          ⋅∑ (6-46) 由式 6-46 可知,离散余弦反变换快速算法可以由 Á e ( ) e Á ÂjF u ⋅  的 2N 点反傅立叶变换快速算法实 现。 6.2.3 离散余弦变换的 Visual C++实现 在这一节中我们建立实现 DCT 变换的函数库,包括实现一维离散余弦变换和反变换的快速算法及 实现二维变换的函数,最后将在菜单中添加对二维图像实现离散余弦变换的功能。 函数 DCT 可实现一维离散余弦变换快速算法,代码如下。 /************************************************************************* * * 函数名称: * DCT() * * 参数: * double * dpf - 指向时域值的指针 * double * dpF - 指向频域值的指针 * r - 2 的幂数 * * 返回值: * 无 * Visual C++数字图像获取、处理及实践应用 · 276· * 说明: * 实现一维快速离散余弦变换 * *********************************************************************** */ VOID WINAPI DCT(double *dpf, double *dpF, int r) { double PI = 3.1415926; // 离散余弦变换点数 LONG lNum; // 循环变量 int i; // 中间变量 double dTemp; complex *comX; // 离散余弦变换点数 lNum = 1<[lNum*2]; // 赋初值 0 memset(comX, 0, sizeof(complex) * lNum * 2); // 将时域点转化为复数形式 for(i=0;i (dpf[i], 0); } // 调用快速傅立叶变换 FFT_1D(comX,comX,r+1); // 调整系数 dTemp = 1/sqrt(lNum); // 求 dpF[0] dpF[0] = comX[0].real() * dTemp; 第 6 章 图像处理中的正交变换 · 277· dTemp *= sqrt(2); // 求 dpF[u] for(i = 1; i < lNum; i++) { dpF[i]=(comX[i].real() * cos(i*PI/(lNum*2)) + comX[i].imag() * sin(i*PI/(lNum*2))) * dTemp; } // 释放内存 delete comX; } 函数 IDCT 可实现一维离散余弦反变换快速算法,代码如下。 /************************************************************************* * * 函数名称: * IDCT() * * 参数: * double * dpF - 指向频域值的指针 * double * dpf - 指向时域值的指针 * r - 2 的幂数 * * 返回值: * 无 * * 说明: * 实现一维快速离散余弦反变换 * ************************************************************************/ VOID WINAPI IDCT(double *dpF, double *dpf, int r) { double PI = 3.1415926; // 离散余弦反变换点数 LONG lNum; // 计算离散余弦变换点数 lNum = 1< *comX; // 给复数变量分配内存 comX = new complex[lNum*2]; // 赋初值 0 memset(comX, 0, sizeof(complex) * lNum * 2); // 将频域变换后点写入数组 comX for(i=0;i (cos(i*PI/(lNum*2)) * dpF[i], sin(i*PI/(lNum*2) * dpF[i])); } // 调用快速傅立叶反变换 IFFT_1D(comX,comX,r+1); // 调整系数 dTemp = sqrt(2.0/lNum); d0 = (sqrt(1.0/lNum) - dTemp) * dpF[0]; // 计算 dpf(x) for(i = 0; i < lNum; i++) { dpf[i] = d0 + 2 * lNum * comX[i].real()* dTemp ; } // 释放内存 delete comX; } 对图像进行二维离散余弦变换的函数 DIBDct,它调用一维离散余弦变换来实现二维快速离散余弦 变换,代码如下。 /************************************************************************* * * 函数名称: * DIBDct() * * 参数: * CDib *pDib - 指向 CDib 类的指针 * * 返回值: * BOOL - 成功返回 TRUE,否则返回 FALSE 第 6 章 图像处理中的正交变换 · 279· * * 说明: * 该函数对图像进行二维离散余弦变换 * *********************************************************************** */ BOOL WINAPI DIBDct(CDib *pDib) { // 指向源图像的指针 unsigned char* lpSrc; //图像的宽度和高度 LONG lWidth; LONG lHeight; // 循环变量 LONG i; LONG j; // 离散余弦变换的宽度和高度,必须为 2 的整数次方 LONG lW = 1; LONG lH = 1; int wp = 0; int hp = 0; // 中间变量 double dTemp; //得到图像的宽度和高度 CSize SizeDim; SizeDim = pDib->GetDimensions(); lWidth = SizeDim.cx; lHeight = SizeDim.cy; // 图像每行的字节数 LONG lLineBytes; //得到实际的 Dib 图像存储大小 CSize SizeRealDim; SizeRealDim = pDib->GetDibSaveDim(); // 计算图像每行的字节数 Visual C++数字图像获取、处理及实践应用 · 280· lLineBytes = SizeRealDim.cx; //图像数据的指针 LPBYTE lpDIBBits = pDib->m_lpImage; // 保证离散余弦变换的宽度和高度为 2 的整数次方 while(lW * 2 <= lWidth) { lW = lW * 2; wp++; } while(lH * 2 <= lHeight) { lH = lH * 2; hp++; } // 分配内存 double *dpf = new double[lW * lH]; double *dpF = new double[lW * lH]; // 时域赋值 for(i = 0; i < lH; i++) { for(j = 0; j < lW; j++) { // 指针指向位图 i 行 j 列的像素 lpSrc = lpDIBBits + lLineBytes * (lHeight - 1 - i) + j; // 将像素值赋给时域数组 dpf[j + i * lW] = *(lpSrc); } } for(i = 0; i < lH; i++) // y 方向进行离散余弦变换 DCT(&dpf[lW * i], &dpF[lW * i], wp); // 保存计算结果 for(i = 0; i < lH; i++) for(j = 0; j < lW; j++) dpf[j * lH + i] = dpF[j + lW * i]; for(j = 0; j < lW; j++) 第 6 章 图像处理中的正交变换 · 281· // x 方向进行离散余弦变换 DCT(&dpf[j * lH], &dpF[j * lH], hp); // 频谱的计算 for(i = 0; i < lH; i++) { for(j = 0; j < lW; j++) { dTemp = fabs(dpF[j*lH+i]); // 是否超过 255 if (dTemp > 255) // 如果超过,设置为 255 dTemp = 255; // 指针指向位图 i 行 j 列的像素 lpSrc = lpDIBBits + lLineBytes * (lHeight - 1 - i) + j; // 更新源图像 * (lpSrc) = (BYTE)(dTemp); } } // 释放内存 delete dpf; delete dpF; // 返回 return TRUE; } 最后在 ImageProcessingView.cpp 中对离散余弦变换子菜单添加命令处理函数,代码如下。 void CImageProcessingView::OnFreqDct() { // 图像的离散余弦变换 // 更改光标形状 BeginWaitCursor(); // 获取文档 CImageProcessingDoc* pDoc = GetDocument(); // 获得图像 CDib 类的指针 CDib * pDib = pDoc->m_pDibInit; Visual C++数字图像获取、处理及实践应用 · 282· LPBITMAPINFOHEADER lpBMIH=pDib->m_lpBMIH; // 判断是否是 8-bpp 位图(这里为了方便,只处理 8-bpp 位图的离散余弦变换) if (lpBMIH->biBitCount != 8) { // 提示用户 MessageBox("目前只支持 256 色位图的离散余弦变换!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } ::DIBDct(pDib); // 设置脏标记 pDoc->SetModifiedFlag(TRUE); // 更新视图 pDoc->UpdateAllViews(NULL); // 恢复光标 EndWaitCursor(); } 用菜单中的功能对图像进行离散余弦变换,图 6-5 所示是进行变换前后的效果。 图 6-5 lena 图像的离散余弦变换 从变换前后的对比可知,经过离散余弦变换,变换域的能量集中在低频分量附近。图像压缩中的 DCT 编码正是利用 DCT 变换的这一特性,在对二维图像进行 DCT 变换后,只对变换域低频分量进行编 码,抛弃部分高频分量,减少携带的信息量,从而实现对图像的有损压缩编码。 6.3 沃尔什变换 傅立叶变换和离散余弦变换快速算法的共同特点是都要用到复数乘法,占用的时间比较多。相对于 上述两种正交变换,沃尔什变换更为便利、有效。沃尔什变换的主要优点在于存储空间小和运算速度快。 对于图像处理来说,这一点很重要,尤其在对大量图像数据进行实时处理时,沃尔什变换更能体现出它第 6 章 图像处理中的正交变换 · 283· 的优势。 沃尔什函数是在 1923 年由美国数学家沃尔什(Walsh)提出来的,沃尔什原始论文中沃尔什函数的 递推公式是按照函数的序数在正交区间内过零点平均数来定义的。不久后,这种规定函数的方法也被波 兰数学家卡兹马兹(Kaczmarz)采用了,所以通常将这种规定称作沃尔什-卡兹马兹定义。 美国数学家佩利(Paley)在 1931 年对沃尔什函数给出新的定义:沃尔什函数可以用有限个拉德梅 克(Rademacher)函数来定义。这种定序方法是用二进制来定序的,又称作自然序数。 沃尔什函数的第三种定序法来源于法国数学家哈达玛(Hadamard)在 19 世纪提出的哈达玛矩阵。 近几十年来,半导体和计算机的迅速发展,为沃尔什函数的实用解决了手段问题,使得沃尔什函数 得到了进一步的发展。 6.3.1 沃尔什函数 沃尔什函数是完备的正交函数集,其值只取+1 或者-1。从排列顺序来定义一共有 3 种:沃尔什排 列、佩利排列和哈达玛排列。下面分别讨论上述 3 种排列方法定义的沃尔什函数。 1.按沃尔什排列的沃尔什函数 按沃尔什排列的沃尔什函数用 walw(i,t)来表示,函数的波形如图 6-6 所示。 通常我们把正交区间内波形变号次数的 1/2 称作列率(Sequency)。按沃尔什排列的沃尔什函数就是 按列率的增长来排列的。如果令 i 为波形在正交区间的变号次数,那么函数 walw(i,t)的列率通常由式 6-47 来表示: Á   0 0 Á Á Á i s i i + = =   为奇数 为偶数 (6-47) 按照沃尔什排列的沃尔什函数可以由拉德梅克(Rademacher)函数构成,表达式如下: ()1 () w 0 ( , )= 1, Á p g i k wal i t R k t − = +  ∏ (6-48) 式中 R(k+1,t)是拉德梅克函数,g(i)是 i 的格雷码,g(i)k 是此格雷码的第 k 位数字,p 为正整数。 拉德梅克(Rademacher)函数是一个不完整的正交函数集,它可以用式 6-49 来表示: ( , ) (sin 2 nR n t sgn t= (6-49) 其中 sgn 函数的定义为: 1 0 1 0 0 x sgn x x > = − <  =无定义 (6-50) 由 sgn 函数的周期性可以知道, (,)R n t 也是周期性函数。一般情况下, (,)R n t 的周期性可以由式 6-51 表示: Á 1 2(,)(,)ÁR n t R n t −= + (6-51) Visual C++数字图像获取、处理及实践应用 · 284· ÁÁÁÁÁÁÁÁÁ ÁÁÁÁÁÁÁÁÁ ÁÁÁÁÁÁÁÁ ÁÁÁÁÁÁÁ ÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁ ÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁ ÁÁÁÁÁÁ ÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁ ÁÁÁÁÁÁÁ ÁÁÁÁÁÁÁ ÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁ Á Á Á Á Á ÂÁ Á Á Á ÃÄÅÁÆÇÈÁÉÁ ÃÄÅÁÆ ÈÁÉÁ ÃÄÅÁÆ ÈÁÆÁ ÃÄÅÁÆ ÈÁÉÁ ÃÄÅÁÆ ÈÁÉÁ ÃÄÅÁÆÇÁÉÁ ÃÄÅÁÆÈÁÉÁ ÃÄÅÁÆÈÁÉÁ Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á 图 6-6 按沃尔什序排列的沃尔什函数 即 (,)R n t 的周期为 Á 1 2Á− 。拉德梅克函数的波形如图 6-7 所示。 ÁÁÂÃÂÄÅ ÁÁÆÃÂÄÅ ÁÁÇÃÂÃÅ ÁÁÈÃÂÄÅ ÂÄ ÂÄ ÂÄ ÂÄ ÇÅ ÈÅ ÉÇÅ ÇÅ ÈÅ ÉÇÅ ÇÅ ÈÅ ÉÇÅ ÇÅ ÈÅ ÉÇÅ 图 6-7 拉德梅克函数 第 6 章 图像处理中的正交变换 · 285· 当 p=3 时的 8 个 w (,)wal i t 经过取样后可写成如下矩阵形式: Á Á Á Á Á Á Á Á (0, ) 1 1 1 1 1 1 1 1 (1, ) 1 1 1 1 1 1 1 1 (2, ) 1 1 1 1 1 1 1 1 (3, ) 1 1 1 1 1 1 1 1 (4,) 1 1 11 1 1 11 1 1 1 1 1 1 1 1(5, ) 1 1 1 1 1 1 1 1(6, ) 1 1 1 1 1 1 1 1(7, ) Wal t Wal t Wal t Wal t Wal t Wal t Wal t Wal t       − − − −      − − − −   − − − −  =   − − − −   − − − −    − − − −    − − − −  ÂÂ×                 按沃尔什排列的沃尔什函数也可用三角函数定义。在正交区间[ ]0,1 内,i=0,1,2,…,= ( )2 1p − ,p 为正整数的情况下,由式 6-52 来定义: 1 w 0 ( , )= cos 2 p k k k wal i t sgn i t − =   ∏ (6-52) 式中 ki 是 i 二进制码的第 k 位数字, { }0,1ki ∈ 。 2.按佩利排列的沃尔什序列 用 Á(,)wal i t 来表示按佩利排列的沃尔什函数,其波形如图 6-8 所示。 当 p=3 时的 8 个 p (,)wal i t 经过取样后可写成如下矩阵形式: Á Á Á Á Á Á Á Á (0, ) 1 1 1 1 1 1 1 1 (1, ) 1 1 1 1 1 1 1 1 (2, ) 1 1 1 1 1 1 1 1 (3, ) 1 1 1 1 1 1 1 1 (4,) 1 11 11 11 1 1 1 1 1 1 1 1 1(5, ) 1 1 1 1 1 1 1 1(6, ) 1 1 1 1 1 1 1 1(7, ) Wal t Wal t Wal t Wal t Wal t Wal t Wal t Wal t       − − − −   − − − −    − − − −  = − − − −    − − − −  − − − −    − − − −   ÂÂ×                   按佩利排列的沃尔什函数也可以由拉德梅克函数产生。其定义由式 6-53 表示: ()1 p 0 ( , )= 1, Á p i k wal i t R k t − = +  ∏ (6-53) 式中 ( 1, )R k t+ 是拉德梅克函数, ki 是将函数序号写成自然二进码的第 k 位数字, { }0,1ki ∈ 。 Visual C++数字图像获取、处理及实践应用 · 286· ÁÁÁÁÁÁÁÁÁ ÁÁÁÁÁÁÁÁÁ ÁÁÁÁÁÁÁÁ ÁÁÁÁÁÁÁ ÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁ ÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁ ÁÁÁÁÁÁ ÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁ ÁÁÁÁÁÁÁ ÁÁÁÁÁÁÁ ÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁ Á Á Á Á Á Á Á Á Á ÃÄÅÁÂÃÄÁÅÁ ÃÄÅÁÂÆÄÁÅÁ ÃÄÅÁÂÇÄÁÅÁ ÃÄÅÁÂÈÄÁÅÁ ÃÄÅÁÂÉÄÁÅÁ ÃÄÅÁ ÄÁÅÁ ÃÄ Á ÄÁÅÁ ÃÄÅÁ ÄÁÅÁ Á Á  Á Á Á  Á Á Á  Á Á Á  Á Á Á  Á Á Á  Á Á Á  Á Á Á  Á 图 6-8 按佩利排列的沃尔什函数 3.按哈达玛排列的沃尔什函数 按哈达玛排列的沃尔什函数是从 2n 阶哈达玛矩阵得来的。2n 阶哈达玛矩阵每一行对应一个离散的 沃尔什函数。 2n 阶哈达玛矩阵形式如下: [ ](0) 0H = (6-54) 1 1(1) 1 1H  =  −  (6-55) ( 1) ( 1)()( 1) ( 1) H m H mH m H m H m − − =  − − −  (6-56) 按照式 6-56 可以产生任意 2n 阶的哈达玛矩阵。 哈达玛排列的沃尔什函数用 H (,)wal i t 表示,其波形如图 6-9 所示。 第 6 章 图像处理中的正交变换 · 287· WalÁ(4,t) WalÁ(3,t) WalÁ(2,t) t t t t t t t t WalÁ(7,t) WalÁ(6,t) WalÁ(5,t) WalÁ(1,t) WalÁ(0,t) 1 0 -1 1 0 -1 1 0 -1 1 0 -1 1 0 -1 1 0 -1 1 0 -1 1 0 -1 1 0 -1 图 6-9 按哈达玛排列的沃尔什函数 当 p=3 时的 8 个 H (,)wal i t 经过取样以后可写成如下矩阵形式: H H H H H H H H (0, ) 1 1 1 1 1 1 1 1 (1, ) 1 1 1 1 1 1 1 1 (2, ) 1 1 1 1 1 1 1 1 (3, ) 1 1 1 1 1 1 1 1 (4,) 11 1 1 1 1 1 1 1 1 1 1 1 1 1 1(5, ) 1 1 1 1 1 1 1 1(6, ) 1 1 1 1 1 1 1 1(7, ) Wal t Wal t Wal t Wal t Wal t Wal t Wal t Wal t       − − − −      − − − −    − − − −  =   − − − −   − − − −    − − − −  − − − −    8 8×                 Visual C++数字图像获取、处理及实践应用 · 288· 按哈达玛排列的沃尔什矩阵也可以由拉德梅克函数产生,解析式如式 6-57 所示: ()1 ( ) H 0 ( , )= 1, Á p i k wal i t R k t − = +  ∏ (6-57) 式中 ( 1, )R k t+ 是拉德梅克函数, ( )ki 是将 i 的自然二进码写反后的第 k 位数字, { }( ) 0,1ki ∈ 。 6.3.2 离散沃尔什变换 离散沃尔什变换可以由下式表示: 1 0 1()()(,) N t W i f t wal i tN − = = ∑  (6-58) 反变换为: 1 0 ()()(,) N i f t W i wal i t − = = ∑  (6-59) 离散沃尔什变换和反变换写成矩阵形式为: [] (0) (0) (1) (1)1 () ( 1) ( 1) W f W fwal NN W N f N            =       − −      (6-60) [] (0) (0) (1) (1)() ( 1) ( 1) f W f Wwal N f N W N            =       − −      (6-61) 另外,沃尔什变换还可以用指数形式表示。沃尔什函数可以写成以下的形式: Á ÁÁ  () ( , ) ( 1) Á Á à t i i wal i t − − − + = ⊕∑ = − (6-62) 式中 t= ( )1 2 2 1 0p p k B t t t t t t− −   ,i= ( )1 2 2 1 0p p ki i i i i i− −   ,N= 2 p 。因此,可以得到: Á ÂÃÁÁ Ä1 0 1( ) ( ) ( 1) Á ÂÃÃÁÄÄÄÅN t W i f tN − ⊕∑ − − + = − = = −∑  (6-63) Á ÂÃÁÁ Ä1 0 ( ) ( )( 1) Á ÂÃÃÁÄÄÄÄN n f t W i − ⊕∑ − − + = − = = −∑ (6-64) 第 6 章 图像处理中的正交变换 · 289· 由沃尔什函数的定义可以知道,按哈达玛排列和按沃尔什排列的沃尔什函数只是排列的顺序不同, 其本质没有什么不同。但是哈达玛矩阵具有简单的递推关系,这就使得沃尔什-哈达玛变换有很多方便 之处。因此,在平时计算中,沃尔什-哈达玛变换运用较多。 只要在离散沃尔什变换的式子中用哈达玛排列的沃尔什函数代替沃尔什排列的沃尔什函数,就得到 了离散沃尔什-哈达玛变换。沃尔什-哈达玛变换和逆变换的矩阵形式如下: [] (0) (0) (1) (1)1 () ( 1) ( 1) W f W fHNN W N f N            =       − −      (6-65) [] (0) (0) (1) (1)() ( 1) ( 1) f W f WHN f N W N            =       − −      (6-66) 在图像处理中广泛应用的是二维变换。二维沃尔什-哈达玛变换和逆变换的指数形式如下: ÁÁ  1 1 0 0 1 1( , ) ( , ) ( 1) ÁÁÂà ÂÃÄÄÅÅ ÄÅ N N x u y v xy y xx y W u v f x yNN −− = = − − + = = ∑ ∑ = −⋅∑ ∑ (6-67) 式中 (,)f x y 代表图像的像素, (,)ÁÂW u v 代表变换系数。 ÁÁÂÂx u y v、、、 是 x、u、y、v 二进制码的第 r 位 数字,y、v=0,1,2…, 1 ÁN − , 2 ÁpyN = ,x、u=0,1,2,…, 1xN − , 2 Áp xN = 。 二维沃尔什—哈达玛变换可以用一维沃尔什-哈达玛变换计算,步骤如下: (1)以 N= xN 对 [ ](,)f x y 中每一列做变换,得到 [ ](,)xW u y 。 (2)以 N= yN 对 [ ](,)xW u y 中每一行做变换,得到 (,)xyW u v   。 另外一种计算方法是将二维的沃尔什-哈达玛变换当作一维来计算。这种方法是将数据矩阵的列依 次顺序排列,这样就形成有 x yNN 个元素的列阵列。然后按照一维沃尔什-哈达玛变换的方法进行计算。 6.3.3 快速沃尔什变换 利用离散沃尔什变换的快速算法,完成一次变换只需计算 2logNN 次加减法,运算速度可以大大 提高。由于沃尔什-哈达玛变化有清晰的分解过程,而且快速沃尔什变换可以由沃尔什-哈达玛变换进 行变换得到,所以本节着重讨论沃尔什-哈达玛变换的快速算法。 以 8 阶沃尔什-哈达玛变换为例,讨论其分解过程以及快速算法。 Visual C++数字图像获取、处理及实践应用 · 290· [ ] [ ] [ ]8 2 4 4 4 4 4 4 4 4 4 4 4 2 2 2 2 4 4 2 2 4 4 2 2 2 2 2 2 2 2 4 4 2 2 2 4 4 2 2 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 HHH HH HH HII HII HH HHII HHII HH HII H IIII H IIII H II G = ⊗  =  −     =    −       −   =    −  −          −     =      −    −       = [ ][ ][ ]0 1 2GG (6-68) 由上面的分解有: [ ] [ ][ ][ ][ ]1 H 0 1 28()()W n G G G f t= (6-69) 则: [ ] [ ][ ]1 2()()f t G f t= (6-70) [ ] [ ][ ]2 1 1()()f t G f t= (6-71) [ ] [ ][ ]3 0 2()()f t G f t= (6-72) 则: [ ] [ ]1 H 38()()W n f t= (6-73) 下面是具体计算 [ ] [ ] [ ]ÁÂÃ()()()f t f t f t、、 的公式,其流程图如图 6-10 所示。 图 6-10 快速沃尔什-哈达玛变换信号流图 第 6 章 图像处理中的正交变换 · 291· [ ] [ ][ ]1 2 1 1 1 1 1 1 1 1 ()() (0) (0)1 0 0 0 1 0 0 0 (1) (1)0 1 0 0 0 1 0 0 (2) (2)0 0 1 0 0 0 1 0 (3) (3)0 0 0 1 0 0 0 1 (4) (4)1 0 0 0 1 0 0 0 (5) (5)0 1 0 0 0 1 0 0 (6) (60 0 1 0 0 0 1 1 (7) 0 0 0 1 0 0 0 1 f t G f t f f f f f f f f f f f f f f f =                        =   −   −       −    −      (0) (4) (1) (5) (2) (6) (3) (7) (0) (4) (1) (5) ) (2) (6) (7) (3) (7) f f f f f f f f f f f f f f f f f +       +       +    +   =   −   −       −    −       (6-74) [ ] [ ][ ]2 1 1 2 1 2 1 2 1 2 1 2 1 2 2 2 ()() (0) (0)1 0 1 0 0 0 0 0 (1) (1)0 1 0 1 0 0 0 0 (2) (2)1 0 1 0 0 0 0 0 (3) (3)0 1 0 1 0 0 0 0 (4) (4)0 0 0 0 1 0 1 0 (5) 0 0 0 0 0 1 0 1 (6) 0 0 0 0 1 0 1 0 (7) 0 0 0 0 0 1 0 1 f t G f t f f f f f f f f f f f f f f =                −    −   =               −    −      1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 (0) (2) (1) (3) (0) (2) (1) (3) (4) (6) (5) (5) (7) (6) (4) (6) (7) (5) (7) f f f f f f f f f f f f f f f f f f +       +       −    −   =   +   +       −    −       (6-75) [ ] [ ][ ]3 0 2 3 2 3 2 3 2 3 2 3 2 3 3 3 ()() (0) (0)1 1 0 0 0 0 0 0 (1) (1)1 1 0 0 0 0 0 0 (2) (2)0 0 1 1 0 0 0 0 (3) (3)0 0 1 1 0 0 0 0 (4) (4)0 0 0 0 1 1 0 0 (5) 0 0 0 0 1 1 0 0 (6) 0 0 0 0 0 0 1 1 (7) 0 0 0 0 0 0 1 1 f t G f t f f f f f f f f f f f f f f = −                   −   =       −            −      2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 (0) (1) (0) (1) (2) (3) (2) (3) (4) (5) (5) (4) (5) (6) (6) (7) (7) (6) (7) f f f f f f f f f f f f f f f f f f +       −       +    −   =   +   −       +    −       (6-76) 对于一般情况,N= 2 p ,p=0,1,2,…,则矩阵可以分成 p 个矩阵 pG   之乘积,即: [ ][ ]1 0 1 12 0 Á p p p p HGGGG − − =      = =     ∏  (6-77) 所以,任意阶沃尔什-哈达玛变换的公式以及蝶形图不难由上述方法引申。 Visual C++数字图像获取、处理及实践应用 · 292· Á 6.3.4 沃尔什-哈达玛变换的 Visual C++实现 接下来建立沃尔什-哈达玛变换的函数库。函数库包括实现一维沃尔什-哈达玛变换和反变换快速 算法及实现二维变换的函数。最后将在菜单中加入对二维图像实现沃尔什-哈达玛变换的功能。 函数 WALSH 用来实现一维快速沃尔什-哈达玛变换,代码如下。 /************************************************************************* * * 函数名称: * WALSH() * * 参数: * double * dpf - 指向时域值的指针 * double * dpF - 指向频域值的指针 * r - 2 的幂数 * * 返回值: * 无 * * 说明: * 该函数用来实现一维快速沃尔什-哈达玛变换 * *********************************************************************** */ VOID WINAPI WALSH(double *dpf, double *dpF, int r) { // 沃尔什-哈达玛变换点数 LONG lNum; // 快速沃尔什变换点数 lNum = 1 << r; // 循环变量 int i,j,k; // 中间变量 int nTemp,m; double *X1,*X2,*X; // 分配运算所需的数组 X1 = new double[lNum]; X2 = new double[lNum]; 第 6 章 图像处理中的正交变换 · 293· // 将时域点写入数组 X1 memcpy(X1, dpf, sizeof(double) * lNum); for(k = 0; k < r; k++) { for(j = 0; j < 1<GetDimensions(); Visual C++数字图像获取、处理及实践应用 · 296· lWidth = SizeDim.cx; lHeight = SizeDim.cy; //得到实际的图像存储大小 CSize SizeRealDim; SizeRealDim = pDib->GetDibSaveDim(); // 图像每行的字节数 LONG lLineBytes; // 计算图像每行的字节数 lLineBytes = SizeRealDim.cx; //图像数据的指针 LPBYTE lpDIBBits = pDib->m_lpImage; // 保证离散余弦变换的宽度和高度为 2 的整数次方 while(lW * 2 <= lWidth) { lW = lW * 2; wp++; } while(lH * 2 <= lHeight) { lH = lH * 2; hp++; } // 分配内存 double *dpf = new double[lW * lH]; double *dpF = new double[lW * lH]; // 时域赋值 for(i = 0; i < lH; i++) { // 列 for(j = 0; j < lW; j++) { // 指向 DIBi 行 j 列像素的指针 lpSrc = lpDIBBits + lLineBytes * (lHeight - 1 - i) + j; // 将像素值赋值给时域数组 dpf[j + i * lW] = *(lpSrc); 第 6 章 图像处理中的正交变换 · 297· } } for(i = 0; i < lH; i++) // 对 y 方向进行沃尔什-哈达玛变换 WALSH(dpf + lW * i, dpF + lW * i, wp); // 保存计算结果 for(i = 0; i < lH; i++) { for(j = 0; j < lW; j++) { dpf[j * lH + i] = dpF[j + lW * i]; } } for(j = 0; j < lW; j++) // 对 x 方向进行沃尔什-哈达玛变换 WALSH(dpf + j * lH, dpF + j * lH, hp); // 行 for(i = 0; i < lH; i++) { // 列 for(j = 0; j < lW; j++) { // 计算频谱 dTemp = fabs(dpF[j * lH + i] * 1000); if (dTemp > 255) { // 超过 255 直接设置为 255 dTemp = 255; } // 指向 DIBi 行 j 列像素的指针 lpSrc = lpDIBBits + lLineBytes * (lHeight - 1 - i) + j; // 更新源图像 * (lpSrc) = (BYTE)(dTemp); } } //释放内存 Visual C++数字图像获取、处理及实践应用 · 298· delete dpf; delete dpF; // 返回 return TRUE; } 最后在 ImageProcessingView.cpp 中对沃尔什-哈达玛变换添加命令处理函数,代码如下。 void CImageProcessingView::OnFreqWalsh() { // 图像离散沃尔什-哈达玛变换 // 更改光标形状 BeginWaitCursor(); // 获取文档 CImageProcessingDoc* pDoc = GetDocument(); // 获得图像 CDib 类的指针 CDib * pDib = pDoc->m_pDibInit; LPBITMAPINFOHEADER lpBMIH=pDib->m_lpBMIH; // 判断是否是 8-bpp 位图(这里为了方便,只处理 8-bpp 位图的沃尔什-哈达玛变换) if (lpBMIH->biBitCount != 8) { // 提示用户 MessageBox("目前只支持 256 色位图的离散沃尔什变换!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } DIBWalsh(pDib); // 设置脏标记 pDoc->SetModifiedFlag(TRUE); // 更新视图 pDoc->UpdateAllViews(NULL); // 恢复光标 EndWaitCursor(); 第 6 章 图像处理中的正交变换 · 299· } 用菜单中的功能对图像进行离散余弦变换,图 6-11 所示是进行变换前后的效果。 图 6-11 lena 图像的沃尔什-哈达玛变换 可以看出经过沃尔什-哈达玛变换,变换域的能量集中在低频分量附近,这一点性质和离散余弦变 换很相似。 6.4 基于特征向量的变换 本节介绍两种与上述讨论的各种变换都不同的变换—霍特林(Hotelling)变换和 SVD 变换。它们 的共同特点是都通过特征分析(Eigenanalysis)得出基函数,是基于图像统计特性的变换。 6.4.1 特征分析 对于一个 NN× 的矩阵 A,有 N 个标量 0,1, 1k k Nλ = −, ,使得: 0kAIλ− = (6-78) 其中, k λ 称作矩阵的特征值。此外,N 个满足 k k kAV Vλ= (6-79) 的向量 kV 叫做 A 的特征向量。特征向量为 N×1 维,每个 kV 对应一个特征值 k λ 。这些特征向量构成一 个正交向量组。 6.4.2 主向量分析(PCA) “主分量法”是由霍特林(Hotelling)提出的,这是一个可以去掉随机变量中各元素间相关性的线性 变换。此后,卡胡南(Karhunen)和列夫(Loeve)提出了一种针对连续信号的类似的变换。由这种方法 又派生出了一种离散图像变换的方法。 Visual C++数字图像获取、处理及实践应用 · 300· 假设 X 是一个 N×1 维的随机向量,X 的每个元素 ix 都是一个随机变量。 [ ]1 2 3 T NX x x x x=  X 的均值可以通过 L 个样本向量来近似估计: {} 1 1 L X l l m E X XL = = ≈ ∑ (6-80) 而它的协方差阵可以由式 6-81 来近似估计: ()(){ } 1 1 LT TT x X X l l X X l C E X m X m X X m mL = = − − = −∑ (6-81) 协方差矩阵是 N×N 的实对称阵。对角线元素是各个随机变量的方差,非对角元素是它们的协方差。 如果上述矢量 ÁX 和 ÁX 不相关,那么它们的协方差为零,即 0ijC = 。 因为矩阵 xC 是一个实对称矩阵,所以总可以找到它的一组 N 个正交特征值。用矩阵 A 来定义一个 线性变换,它可以由任意向量 X 通过式 6-82 得到一个新的向量 Y: { }xY A X m= − (6-82) 其中,A 的行向量就是 xC 的特征向量,并且 A 的第一行对应为 xC 最大特征值的特征向量,A 的最 后一行对应为 xC 的最小特征值的特征向量。 变换后的向量 Y 是具有零均值的随机向量,它的协方差矩阵与 X 的协方差矩阵的关系为: T YXC AC A= (6-83) 由于 A 的行是 xC 的特征向量,所以 YC 是对角阵且其对角元素为 xC 的特征值,即: 1 0 0 Y N C λ λ    =          (6-84) 从式 6-84 可以看出, K λ 也是 YC 的特征值。又因为 YC 的非对角线元素都是零,所以 Y 的各个元 素之间都是不相关的。于是,线性变换 A 去掉变量间的相关性。此外, K λ 是第 K 个变换后的变量 KY 的 方差。 6.4.3 霍特林(Hotelling)变换 公式 6-82 定义了一个一维离散变换,它也被称作霍特林(Hotelling)变换或卡胡南-列夫(K-L) 变换。 第 6 章 图像处理中的正交变换 · 301· 霍特林变换的一个重要性质是与从 Y 重建 X 有关的。因为 A 的各行都是正交归一化向量,所以 1 TAA− = ,从式 6-85 可以得到任意一个 Y 对应的矢量 X。 T xX A Y m= + (6-85) 我们可以通过略去对应于较小特征值的一个或者多个特征向量来给 Y 降维。令 B 为 M×N 的矩阵 MN<(),它是通过丢弃 A 底下的 NM− 行得到的。用 B 重建的向量 ˆX 为: ˆ T xX B Y m= + (6-86) 可以证明 X 和 ˆX 之间的均方误差为: 1 1 ! NKN ms j j j j j j K e λ λ λ = = = + = − =∑ ∑ ∑ (6-87) 从上式可以看出,如果 K=N,则误差为零。通过选择对应最大特征值的各特征向量可以使得 X 和 ˆX 之间的均方误差最小。通常,特征值的幅度差别很大。所以忽略其中一些较小的值不会引起很大的误 差。Hotelling 变换的降维能力使其对图像压缩非常有用。 图 6-12 所示为一个利用霍特林变换实现图像旋转的例子。如果将图 6-12 所示的矩形看作一个二维 分布,则其上的每一个点 r 都可以用一个二维矢量表示为 [ ], Tr a b= ,其中 a 和 b 分别对应 r 的 1x 轴坐 标和 1y 轴坐标。由于 x 和 y 是随机分布的,所以可以将 r 看做一个二维分布的随机变量,然后利用矩阵 上各点坐标值来计算物体坐标值的矢量均值和协方差矩阵。 xÁ yÁ yàxàeÁÂeà图 6-12 霍特林变换示意 由式 6-82 可知,霍特林变换实际上是将点 r 映射到新的坐标系 2 2x y、 。 xr m− 先将原坐标系的 原点位移到 xm ,也就是矩形上每一点坐标的均值,而乘以 A 矩阵,则是按照 A 进行坐标旋转。A 是矩 形坐标值协方差矩阵 rC 的特征矢量,旋转是将一个位移后的矩形沿特征矢量 1 2e e、 方向进行对齐。可 以说,通过霍特林变换将数据解除了相关。 ÁVisual C++数字图像获取、处理及实践应用 · 302· 6.4.4 SVD 变换 一个 N×M 的图像 A 可看作为 NM 维空间中的一个向量,也可将此图像表示在 r 维子空间,r 为矩阵 [A]的秩。设图像是实值的,且 M=N,矩阵 TAA 和 TAA 是非负和对称的,有着同样的特征值{ }mnλ 。 又因为 M=N,所以最多有 r=M 个非零特征值。 任意一个 N×M 的矩阵都可以写成下式: TA UV= Λ (6-88) 其中, Λ 为 N×M 的矩阵,沿其对角线包含 A 的奇异值。它的形式如下: 0 0 0 Σ Λ =    其中 ( )1 2, ,, rdiag σ σ σΣ =  ,并且有 1 2 0n σ σ σ≥ ≥ ≥ > , k σ 就是 A 的奇异值。 U 和 V 分别是 N×r 和 M×r 矩阵,它们的列向量分别是 TAA 和 TA A 的正交特征向量,由于 U 和 V 都是正交的,所以 TVAUΛ = (6-89) 公式 6-88 和 6-89 分别是一个酉变换对的正变换和反变换。这个变换叫做奇异值分解(SVD)变换。 不同于前面讨论的变换,U 和 V 的核矩阵取决于正被变换的图像 A。通常需要计算正被变换的图像 的 TAA 和 TA A 的特征向量。另外由于 Λ 是对角阵,所以它至多有 M 个非零元素。这样,我们至少可 以获得 N 倍的无损压缩。通常,奇异值中总有一些小到可以被忽略,不会带来什么误差。这样,可以通 过忽略一些小的奇异值来实现有损压缩,获得更高的压缩比。 实际上,整幅的图像虽然可以被压缩成 Λ 的对角,但是每一幅图像的 AT A 和是惟一的,也就是说 他们必须被传输到接收方,才能够重构图像。 下面给出一个用 SVD 变换来压缩 5×5 图像矩阵的例子。 T 0 1210 61418146 1 3431 1436483614 A A2 4542 1848654818 1 3431 1436483614 0 1210 61418146 A                = =               ⋅ 求得其特征值和特征向量为: 147.1 0.186 0.637 0.241 0.695 0.695 1.872 0.475 0.058 0.520 0.133 0.128 U=0.058 0.691 0.422 0.587 0 0 0 0.475 0.058 0.520 0.133 0.128 0 0.186 0.637 0.241 0.695 0.695 λ − −       −       = −    −      − −    第 6 章 图像处理中的正交变换 · 303· 所以根据公式 6-89 有正变换为: 12.585 0 0 0 0 0 1.143 0 0 0 0 0 0.557 0 0 0 0 0 0 0 0 0 0 0 0 TVAU    −   Λ = =       (6-90) 根据公式 6-88 有反变换为: 0 1 2 1 0 1 3 4 3 1 2 4 5 4 2 1 3 4 3 1 0 1 2 1 0 TAUV        = Λ =       (6-91) 6.4.5 霍特林变换的 Visual C++实现 下面用 Visual C++来实现用霍特林变换对图像中的物体进行旋转。为简化起见,处理的是二值图像, 图像中的黑色像素被认为是物体的一部分,白色像素被认为是背景。最后为了便于显示变换的结果,把 变换后坐标系的原点平移到图像中心来。也就是如果图像的宽度为 lWidth,长度为 lHeight,那么变换后 的坐标轴原点位于(lWidth/2,lHeight/2)。 在功能的实现过程中要用到 3 个函数:DIBHOTELLING、BSTQ 和 THREECROSS。DIBHOTELLING 是主函数,用来实现对物体坐标值进行霍特林变换。在进行霍特林变换的过程中,要计算协方差矩阵的 特征向量矩阵 A,利用矩阵特征值和特征向量在正交变换下的不变性,首先由函数 THREECROSS 利用 Householder 变换将对称协方差矩阵变成对称三对角矩阵,接着用 BSTQ 求三对角阵的特征值向量矩阵 A。 通过函数 THREECROSS 和 BSTQ,可以实现求一个实对称矩阵的特征值和特征向量的的功能。 下面是函数 DIBOHOTELLING 的代码。 /************************************************************************* * * 函数名称: * DIBOHOTELLING() * * 参数: * CDib *pDib - 指向 CDib 类的指针 * * 返回值: * BOOL - 成功返回 TRUE,否则返回 FALSE * * 说明: * 该函数用霍特林变换来对图像进行旋转 * *********************************************************************** Visual C++数字图像获取、处理及实践应用 · 304· */ BOOL WINAPI DIBHOTELLING(CDib *pDib) { // 指向源图像的指针 unsigned char* lpSrc; // 循环变量 LONG i; LONG j; //图像的宽度和高度 LONG lWidth; LONG lHeight; // 图像每行的字节数 LONG lLineBytes; // 经过变换后图像最大可能范围 LONG lMaxRange; // 物体坐标的均值 LONG AverEx; LONG AverEy; // 物体总的像素数 LONG ToaCount; // 坐标值的协方差矩阵 double Matr4C[2][2]; // 存放协方差矩阵的特征向量 double QMatrix[2][2]; // 三对角阵的主对角和次对角线元素 double MainCross[2]; double HypoCross[2]; // 中间变量 double dTemp; LONG lTempI; LONG lTempJ; 第 6 章 图像处理中的正交变换 · 305· //得到图像的宽度和高度 CSize SizeDim; SizeDim = pDib->GetDimensions(); lWidth = SizeDim.cx; lHeight = SizeDim.cy; //得到实际的 Dib 图像存储大小 CSize SizeRealDim; SizeRealDim = pDib->GetDibSaveDim(); // 计算图像每行的字节数 lLineBytes = SizeRealDim.cx; //图像数据的指针 LPBYTE lpDIBBits = pDib->m_lpImage; // 估计图像经过旋转后可能最大的宽度和高度 if(lWidth>lHeight) lMaxRange = lWidth; else lMaxRange =lHeight; // 赋初值 AverEx=0.0; AverEy=0.0; ToaCount = 0; Matr4C[0][0] = Matr4C[0][1] = Matr4C[1][0] = Matr4C[1][1] = 0.0; // 分配内存 double *F = new double[lWidth*lHeight]; // 行 for(i = 0; i < lHeight; i++) { // 列 for(j = 0; j < lWidth; j++) { // 给旋转后坐标轴的每个点赋零值(灰度值 255 对应显示白色) F[i*lWidth + j] = 255; // 指向位图 i 行 j 列像素的指针 lpSrc = lpDIBBits + lLineBytes*i + j; // 值小于 255(非背景色白色)的像素认为物体的一部分 Visual C++数字图像获取、处理及实践应用 · 306· // 并将其坐标值 x 和 y 看作二维随机矢量 if((*lpSrc) < 255) { // 属于物体像素的 Y 坐标和 X 坐标累计值 AverEx=AverEx+i; AverEy=AverEy+j; // 物体总的像素数加一 ToaCount++; // 随机矢量协方差矩阵的累计值 Matr4C[0][0] = Matr4C[0][0] + i*i; Matr4C[0][1] = Matr4C[0][1] + i*j; Matr4C[1][0] = Matr4C[1][0] + j*i; Matr4C[1][1] = Matr4C[1][1] + j*j; } } } // 计算随机矢量的均值 AverEx = AverEx/ToaCount; AverEy = AverEy/ToaCount; // 计算随机矢量的协方差矩阵 Matr4C[0][0] = Matr4C[0][0]/ToaCount - AverEx*AverEx; Matr4C[0][1] = Matr4C[0][1]/ToaCount - AverEx*AverEy; Matr4C[1][0] = Matr4C[1][0]/ToaCount - AverEx*AverEy; Matr4C[1][1] = Matr4C[1][1]/ToaCount - AverEy*AverEy; // 规定迭代的计算精度 double Eps = 0.000001; // 将协方差矩阵化作三对角对称阵 THREECROSS(*Matr4C, 2, *QMatrix, MainCross, HypoCross); // 求协方差矩阵的特征值和特征矢向量 BSTQ(2, MainCross,HypoCross, *Matr4C, Eps, 50); // 将特征列向量转化称特征列向量 dTemp = Matr4C[0][1]; Matr4C[0][1] = Matr4C[1][0]; Matr4C[1][0] = dTemp; 第 6 章 图像处理中的正交变换 · 307· // 对特征列向量进行归一化 for(i=0;i<=1;i++) { dTemp = pow(Matr4C[i][0],2) + pow(Matr4C[i][1],2); dTemp = sqrt(dTemp); Matr4C[i][0] = Matr4C[i][0]/dTemp; Matr4C[i][1] = Matr4C[i][1]/dTemp; } // 查找经霍特林变换后的坐标点在原坐标系中的坐标 for(i = -lMaxRange+1; i < lMaxRange; i++) { for(j = -lMaxRange+1; j < lMaxRange; j++) { // 将新坐标值映射到旧的坐标系 int Cx = (int)(i*Matr4C[0][0]-j*Matr4C[0][1])+AverEx; int Cy = (int)(-i*Matr4C[1][0]+j*Matr4C[1][1])+AverEy; // 映射值是否属于源图像 if( Cx>=0 && Cx=0 && Cy=0 && lTempI=0 && lTempJ= 1; i--) { h = 0.0; if (i > 1) for (k = 0; k <= i-1; k++) { u = i*Rank + k; h = h + QMatrix[u]*QMatrix[u]; } // 如果一行全部为零 if (h + 1.0 == 1.0) { HypoCross[i] = 0.0; if (i == 1) HypoCross[i] = QMatrix[i*Rank+i-1]; MainCross[i] = 0.0; } // 否则进行 householder 变换,求正交矩阵的值 else { // 求次对角元素的值 HypoCross[i] = sqrt(h); // 判断 i 行 i-1 列元素是不是大于零 u = i*Rank + i - 1; if (QMatrix[u] > 0.0) HypoCross[i] = -HypoCross[i]; h = h - QMatrix[u]*HypoCross[i]; QMatrix[u] = QMatrix[u] - HypoCross[i]; Visual C++数字图像获取、处理及实践应用 · 310· f = 0.0; // householder 变换 for (j = 0; j <= i-1; j++) { QMatrix[j*Rank+i] = QMatrix[i*Rank+j] / h; g = 0.0; for (k = 0; k <= j; k++) g = g + QMatrix[j*Rank+k]*QMatrix[i*Rank+k]; if (j+1 <= i-1) for (k = j+1; k <= i-1; k++) g = g + QMatrix[k*Rank+j]*QMatrix[i*Rank+k]; HypoCross[j] = g / h; f = f + g*QMatrix[j*Rank+i]; } h2 = f / (h + h); // 求正交矩阵的值 for (j = 0; j <= i-1; j++) { f = QMatrix[i*Rank + j]; g = HypoCross[j] - h2*f; HypoCross[j] = g; for (k = 0; k <= j; k++) { u = j*Rank + k; QMatrix[u] = QMatrix[u] - f*HypoCross[k] - g*QMatrix[i*Rank + k]; } } MainCross[i] = h; } } // 赋零值 for (i = 0; i <= Rank-2; i++) HypoCross[i] = HypoCross[i + 1]; HypoCross[Rank - 1] = 0.0; MainCross[0] = 0.0; 第 6 章 图像处理中的正交变换 · 311· for (i = 0; i <= Rank-1; i++) { // 主对角元素的计算 if ((MainCross[i] != 0.0) && (i-1 >= 0)) for (j = 0; j <= i-1; j++) { g = 0.0; for (k = 0; k <= i-1; k++) g = g + QMatrix[i*Rank + k]*QMatrix[k*Rank + j]; for (k = 0; k <= i-1; k++) { u = k*Rank + j; QMatrix[u] = QMatrix[u] - g*QMatrix[k*Rank + i]; } } // 将主对角线的元素进行存储,以便返回 u = i*Rank + i; MainCross[i] = QMatrix[u]; QMatrix[u] = 1.0; // 将三对角外所有的元素赋零值 if (i-1 >= 0) for (j = 0; j <= i-1; j++) { QMatrix[i*Rank + j] = 0.0; QMatrix[j*Rank+i] = 0.0; } } // 返回值 return(TRUE); } 函数 BSTQ 用变形 QR 方法计算实对称三角矩阵的全部特征值以及相应的特征向量,代码如下。 /************************************************************************* * * 函数名称: * BSTQ() * * 参数: * int Rank - 矩阵 A 的阶数 * double *MainCross - 对称三角阵中的主对角元素,返回时存放 A 的特征值 * double *HypoCross - 对称三角阵中的次对角元素 * double *Matrix - 返回对称矩阵 A 的特征向量 Visual C++数字图像获取、处理及实践应用 · 312· * double Eps - 控制精度 * int MaxT - 最大迭代次数 * * 返回值: * BOOL - 成功返回 TRUE,否则返回 FALSE * * 说明: * 该函数用变形 QR 方法计算实对称三角矩阵的全部特征值以及相应的特征向量 * * *********************************************************************** */ BOOL WINAPI BSTQ(int Rank, double *MainCross, double *HypoCross, double *Matrix, double Eps, int MaxT) { // 变量的定义 int i, j, k, m, it, u, v; double d, f, h, g, p, r, e, s; // 赋零值 HypoCross[Rank - 1] = 0.0; d = 0.0; f = 0.0; for(j = 0; j <= Rank-1; j++) { // 迭代精度的控制 it = 0; h = Eps * (fabs(MainCross[j]) + fabs(HypoCross[j])); if(h > d) d = h; m = j; while((m <= Rank-1) && (fabs(HypoCross[m]) > d)) m = m + 1; if(m != j) { // 进行迭代,求得矩阵 A 的特征值和特征向量 do { // 超过迭代次数,返回迭代失败 if(it == MaxT) return(FALSE); 第 6 章 图像处理中的正交变换 · 313· it = it + 1; g = MainCross[j]; p = (MainCross[j + 1] - g) / (2.0 * HypoCross[j]); r = sqrt(p*p + 1.0); // 如果 p 大于 0 if (p >= 0.0) MainCross[j] = HypoCross[j]/(p + r); else MainCross[j] = HypoCross[j]/(p - r); h = g - MainCross[j]; // 计算主对角线的元素 for (i = j + 1; i <= Rank - 1; i++) MainCross[i] = MainCross[i] - h; // 赋值 f = f + h; p = MainCross[m]; e = 1.0; s = 0.0; for(i = m - 1; i >= j; i--) { g = e * HypoCross[i]; h = e * p; // 主对角线元素的绝对值是否大于次对角线元素的 if(fabs(p) >= fabs(HypoCross[i])) { e = HypoCross[i] / p; r = sqrt(e*e + 1.0); HypoCross[i + 1] = s*p*r; s = e / r; e = 1.0 / r; } else { e = p / HypoCross[i]; r = sqrt(e*e + 1.0); HypoCross[i+1] = s * HypoCross[i] * r; s = 1.0 / r; e = e / r; } p = e*MainCross[i] - s*g; Visual C++数字图像获取、处理及实践应用 · 314· MainCross[i + 1] = h + s*(e*g + s*MainCross[i]); // 重新存储特征向量 for(k = 0; k <= Rank - 1; k++) { u = k*Rank + i + 1; v = u - 1; h = Matrix[u]; Matrix[u] = s*Matrix[v] + e*h; Matrix[v] = e*Matrix[v] - s*h; } } // 将主对角线和次对角线元素重新赋值 HypoCross[j] = s * p; MainCross[j] = e * p; } while (fabs(HypoCross[j]) > d); } MainCross[j] = MainCross[j] + f; } // 返回 A 的特征值 for (i = 0; i <= Rank-1; i++) { k = i; p = MainCross[i]; // 将 A 特征值赋给 p if(i+1 <= Rank-1) { j = i + 1; while((j <= Rank-1) && (MainCross[j] <= p)) { k = j; p = MainCross[j]; j = j+1; } } // 存储 A 的特征值和特征向量 if (k != i) { MainCross[k] = MainCross[i]; 第 6 章 图像处理中的正交变换 · 315· MainCross[i] = p; for(j = 0; j <= Rank-1; j++) { u = j*Rank + i; v = j*Rank + k; p = Matrix[u]; Matrix[u] = Matrix[v]; Matrix[v] = p; } } } // 返回值 return(TRUE); } 最后在 ImageProcessingView.cpp 中对霍特林变换添加命令处理函数,代码如下。 void CImageProcessingView::OnFreqHotelling() { // 图像霍特林变换 // 更改光标形状 BeginWaitCursor(); // 获取文档 CImageProcessingDoc* pDoc = GetDocument(); // 获得图像 CDib 类的指针 CDib * pDib = pDoc->m_pDibInit; // 获得图像的头文件信息 LPBITMAPINFOHEADER lpBMIH=pDib->m_lpBMIH; // 判断是否是 8-bpp 位图(这里为了方便,只处理 8-bpp 位图的离散霍特林变换) if (lpBMIH->biBitCount != 8) { // 提示用户 MessageBox("目前只支持 256 色位图的离散霍特林变换!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } // 图像的霍特林变换 Visual C++数字图像获取、处理及实践应用 · 316· DIBHOTELLING(pDib); // 设置脏标记 pDoc->SetModifiedFlag(TRUE); // 更新视图 pDoc->UpdateAllViews(NULL); // 恢复光标 EndWaitCursor(); } 霍特林变换实现坐标旋转的结果如图 6-13 所示。 xÁ yÁ yàxàeÁÂeà图 6-13 霍特林变换实现坐标旋转的结果 6.5 小波变换 傅立叶变换从倡导到至今已经快两个世纪了(1807 年由 J.Fourier 提出),它是分析信号频率分量的 有小波变换的出现使得傅立叶分析中遇到的上述问题迎刃而解。小波分析方法的提出,可以追溯到 1910 年 Harr 提出的小波规则正交基及 1938 年 Littlewood-Paley 对傅立叶级数建立的 L-P 理论,即按二进制 频率成分分组傅立叶变换的相位变化在本质上不影响函数的形状和大小。其后,Caldern 于 1975 年用其 早期发现的再生公式给出抛物线空间 H′ 上的原子分解,这个公式后来成了许多函数分解的出发点,它 的离散形式已接近小波展开,只是还无法得到组成一正交系的结论。1981 年 Stromberg 对 Harr 系进行了 改造,证明了小波函数的存在性。值得注意的是,1984 年法国地球物理学家 Morlet 在分析地震的局部性 质时,发现传统的变换难以达到要求,因此,他引入小波概念于信号分析中,对信号进行分解。随后, 理 论 物 理 学 家 Grossman 尝 试 对 一 个 确 定 函 数 ()xΨ 进 行 了 伸 缩 、 平 移 成 为 ((1 2 : , , 0x ba a b R aa ψ −   ∈ ≠     形式,他对 Morlet 的这种信号依此方法进行展开的可行性进行了 研究,这无疑为小波分析的形成开创了先河。 真正的小波热开始于 1986 年,当时 Meyer 创造性地构造了具有一定衰减性的光滑函数 ? ,其二进第 6 章 图像处理中的正交变换 · 317· 制伸缩与平移: ()(){ }2 , 2 2 : , j j j k t t k j k Zψ ψ− −= − ∈ (6-92) 构成 ( )2LR 的规范正交基。 继 Mayer 提出小波变换后,Lemarie 和 Battle 又分别独立给出了具有指数衰减的小波函数。1987 年, Mallat 巧妙地将计算机视觉领域内的多尺度分析的思想引入到小波分析中。小波函数的改造及信号按小 波变换的分解与重构,成功地统一了前人提出的具体函数的构造,研究了小波变换的离散化情况,提出 了著名的 Mallat 算法,并将其应用于图像的分解与重构。它不仅为纯粹数学的研究提供了有力的工具, 而且为图像处理理论的发展树立了里程碑。 6.5.1 连续小波变换 若记基本小波函数为 h(x),伸缩和平移因子分别为 a 和 b,则小波是一个满足条件 ( )d 0R h x x =∫ 的 函数 h 通过平移和伸缩而产生的一个函数簇 ,a bh : 1 2 , ( ) , , 0a b x bh x a h a b R aa − − = ∈ ≠   (6-93) 通常称 h 为基本小波。令 2 ()LR 为可测的、平方可积函数 f(x)的矢量空间,R 为实数集,对于任意 的 2()()f x L R∈ 的连续小波变换给出如下定义: 1 2 ,,( ) ( ) ( )d ( ) d +∞ +∞− −∞ −∞ − = == ⋅   ∫ ∫a b a b x bWx hxfxxa fxh xa (6-94) 写成内积形式,即有: ,,( ) ( ), ( )a b a bW x f x h x= (6-95) 它对应于 2()()f x L R∈ 在函数簇 , ()a bh x 上的分解,这一分解必须满足下列可容性条件: 2| ( ) |d| |h HW ω ωω +∞ −∞ = < ∞∫ (6-96) 或: ( )d 0h x x +∞ −∞ =∫ 其中 ()H ω 是 h(x)的傅立叶变换,这样的函数 h(x)称为解析小波。由式 6-96 可知,函数 h(x)可以描述为 一带通滤波器的脉冲响应,因此小波变换式 6-94 和 6-95 可以描述为函数 2()()f x L R= 通过一带通滤Visual C++数字图像获取、处理及实践应用 · 318· 波函数的滤波输出,且具有频率放大作用(放大系数为 a)。可以证明:如果 h(x)满足式 6-96,则由 , ( )a bW x 可以恢复出 f(x),即此小波变换的逆变换存在,且有如下定义: ,, 1( ) ( ) ( )d da b a b h f x W x h x a bW +∞ +∞ −∞ −∞ = ∫ ∫ (6-97) 下面给出连续小波变换的两个重要性质。 Parseval 公式: 设 2 ()f g L R∈、 , (,)fW a b 及 (,)gW a b 分别表示 f、g 关于同一基本小波 h 的小波变换,则有 等式: f2 1 W ( , ) ( , )d d ,g hRR a b W a b a b W f ga =∫ ∫ (6-98) 式中 2| ( ) | | |h HW d ω ωω +∞ −∞ = ∫ 。 能量公式: 设 2 ()f L R∈ , (,)fW a b 是 f 的小波变换,则有: 2 1 2 2 d d| ( ) | d | ( , ) |h fRRR a bh x x W W a b a −=∫ ∫ ∫ (6-99) 由式 6-97、6-98、6-99 可以看出小波变换是一种信息保持型的可逆变换,原来信号的信息完全保留 在小波变换的系数中,变换只是使得原信号的能量重新分配。另外,可根据具体问题中 f 的特征来选取 h,使得小波变换能更方便地表示 f 的特征。 下面,我们来研究连续小波在相空间的局部化格式,先假设 h 是标准的双窗函数,记为: 2 0 0 | ( ) | dHω ω ω ω ∧∞= ∫ 注意到: 1 2, | | e ( )j b a bH a H aω ω ∧ ∧−= ,因此,h ba, 的正频窗口中心为 ( )Á, ab ω + (当 0a > 时)。此外, , | |a bh a h∆ = ⋅∆ , , 1 | |a bh ha ∧ ∧ ∆ = ∆ 。 由上可知,尽管由 h ba, 确定的窗的面积大小固定,但形状各异。对于大的中心频率 Á a ω + ,窗变窄, 即尺寸参数 a 较小,分辨率在时域或空间域较低,而在频域较高;对于小的中心频率 Á a ω + ,窗变宽,即 尺度参数 a 增大,分辨率在时域或空间域增加,在频域减小。亦即,在高频端,小波变换在时间上较快, 而在低频端,小波变换在频率上较快。这相当于参数 a 的变化不仅改变连续小波的频谱结构,而且也改 变其窗口的大小与形状,如图 6-14 所示。为了比较分析,图 6-15 给出了窗口傅立叶变换的基函数和时 间-频率图。不同于小波变换的是,此时的分析窗的形状和大小与频率无关且保持不变,所以分析的分 辨率在整个时间-频率平面上不变。 第 6 章 图像处理中的正交变换 · 319· 1 0 a +ω 2 0 a +ω w b a1 1+ b a2 2+ t 1 0 a +ω 2 0 a +ω w b a2 2+ b a1 1+ t 图 6-14 小波变换的时间-频率窗 图 6-15 傅立叶变换的时间-频率窗 6.5.2 离散小波变换 由式 6-94 可通过对其伸缩标度因子 a 和平移标度因子 b 的取样而离散化。如果对 a 和 b 按如下规 律取样: 0 ,ma a= 0 0 mb nb a= 其中 0 1a > , 0b R∈ , 2m n z∈、 ,则由式 6-93 有: / 2 , 0 0 0()()m m m nh x a h a x nb− −= − (6-100) 这样离散小波变换可定义为: 2 ,,,() ()d (), (),(,)m n m n m nDW fxh xx fxh x mn z +∞ −∞ = = ∈∫ (6-101) 因此,离散小波变换式 6-101 也是一种时频分析,它从集中在某个区间上的基本函数开始,以规定 步长向左或向右移动基本波形,并用标度因子 a 0 来扩张或压缩以构造其函数系。一系列小波由此而生, 这就是“小波”一词的由来。这里 m 和 n 分别称为频率范围指数和时间步长变化指数。由于 ,m nh 正比于 0 ma− ,故高频时(对应于小的 m 值) ,m nh 高度集中,反之亦然;步长的变化则与 n 成正比。 为了从离散小波变换式 6-101 重构函数信号 f(x),算子 2 2 2 , :()()m nDW L R l z→ 必须是一有界可逆算 子,即对于某个 A>0, B < ∞ : 2 2 2 , , || || | ( ), ( ) | || ||m n m n z A f f x h x B f ∈ < <∑ 对 一 切 2()()f x L R∈ 成 立 。 其 中 2 2()l z 表 示 二 元 平 方 可 和 序 列 , ()m nh x 矢 量 空 间 , 范 数Visual C++数字图像获取、处理及实践应用 · 320· 2 2|| || | ( ) | df f x x +∞ −∞ = ∫ 。 6.5.3 二进小波变换 当取 0 2a = , 0 1b = 及 m j= 时,将其代入式 6-100,可得到在 L2(R)上,该小波正交基为如下形 式的函数族: 22 , ( ) 2 (2 ) | ( , ) j j j n x x n j n zψ ψ − − = − ∈    (6-102) 代入式 6-101,则得到非常重要的离散正交二进小波变换: 2 22 ()(2 )d (), () Á j j jW f x h x n x f x xψ +∞− − −∞ = ⋅ − =∫ (6-103) 令 2 2()()ÁÁx xψ ψ ∧ = − ,函数 f(x)可以由它的二进小波变换重建: ,,()()j nj nf x W xψ +∞ ∧ −∞ = ⋅∑ 6.5.4 小波变换的多分辨率分析 空间 2 ()LR 中的一列闭子空间 { }j j z V ∈ 称为 2 ()LR 的一个多分辨率分析或逼近,它满足如下条 件: (1)单调性:对于任意 j z∈ , 1j jVV −⊂ 。 (2)逼近性: { }0j j z V ∈ = , 2 ()j j z VLR ∈ = 。 (3)伸缩性: 1( ) (2 )j ju x V u x V −∈ ⇔ ∈ 。 (4)Riesz 基:存在 0Vφ ∈ ,使得{ }( ) |k zx kφ ∈− 构成 0V 的标准正交基。 (5) 1 2 3j j j jV w w w+ + += ⊕ ⊕ ⊕ 。 条件(3)表明,空间列{ }jV 中任一空间 iV 的基可由其中另一空间 jV 的基经过简单的伸缩变换得 到 。 因 此 , 若 { }( ) |k zx kφ ∈− 是 0V 的标准正交基,则对于任意 j z∈ , 函 数 基 2 , ( ) 2 (2 ) | j j j n n zx x nφ φ − − ∈  = −    构成 jV 的标准正交基。函数 ()xφ 称为尺度函数,具有低通滤波的第 6 章 图像处理中的正交变换 · 321· 作用,且满足: 1,() () () 2 ()(2 )nx h n x h n x nφ φ φ +∞ +∞ − −∞ −∞ = ⋅ = −∑ ∑ (6-104) 其中: 1,() (), () 2 ()(2 )dnh n x x x x n xφ φ φ φ+∞ − −∞ = = −∫ 式 6-104 称为双尺度方程。 在 iV 的正交补空间 jw 中,函数基 2 , ( ) 2 (2 ) | j j n n zx x nψ ψ − ∈  = −    构成 jw 的标准正交基。函数 ()xψ 称为小波函数,具有高通滤波的作用,并满足: () 2 (1) (1 )(2 ) 2 ()(2 )nx h n x n g n x nψ φ φ +∞ +∞ −∞ −∞ = − ⋅ − ⋅ − = −∑ ∑ 其中 ( ) ( 1) (1 )ng n h n= − − 。在信号处理中,g(n)和 h(n)称为正交镜象滤波器。 6.5.5 Mallat 算法 小波变换为信号分析提供了一种新工具,但有应用意义的是离散二进小波与正交镜象滤波器(QMF) 的巧妙结合,它是小波变换的核心。 在 Burt 和 Adelson 图像分解和重建的塔形算法启发下,基于多分辨率分析框架,Mallat 建立了离散 正交小波的一种快速算法— Mallat 算法,通过与 QMF 的卷积可以分解或重建给定的信号。 1. 一维信号的 Mallat 算法 设 Vj 是分辨率为 2 j− 的信号空间,Pj 是信号 ( )f x 在 Vj 的投影,Wj 为 Vj 的正交补空间,Dj 为 ( )f x 在 Wj 上的投影。φ 和ψ 分别是相应的尺度函数和小波函数。 其中: ()() ()() 2 , 2 , 2 2 2 2 j j j n j j j n x x n x x n φ φ ψ ψ − − − − = − = − Pj 可以表示为: ()()()(),,, 2 2j j j j n j nP f x f u u xφ ψ +∞ −∞ = ∑ 离散信号 ( ) ( ) ( ),,d j j nP f x f u uφ= 称为 ( )f x 在分辨率 2 j− 的离散逼近。 Dj 可表示为: Visual C++数字图像获取、处理及实践应用 · 322· ()()()()xuufxfD nj j n nj j j ,, 22, ψψ∑+∞ −∞= −= 离 散 信 号 ( ) ( ) ( ),,d j j nD f x f u uψ= 称 为 ( )f x 在 分 辨 率 2 j− 的离散细节信号。由于 (),2 j j n xφ 和 (),2 j j n xψ 的正交性, d jP f 和 d jD f 可直接由 1 d jP f− 导出: ()()()12d d j j k P f n h k n P f k +∞ − =−∞ = −∑ (6-105) ()()()12d d j j k D f n g k n P f k +∞ − =−∞ = −∑ (6-106) 其中: ( ) ( ) ( )1,0 ,h n u u nφ φ= − ()()()()()1,0 , 1 1ng n u u n h nψ φ= − = − − 假设 h(n)与 g(n)分别对应的离散滤波器为 H 和 G, H 和 G 分别为 H 和 G 的镜像滤波器,其冲激 响应分别为 ()()h n h n= − 与 ()()g n g n= − 。 从式 6-105 和 6-106 可以看到, 1 d jP f− 经过离散线性滤波 H 和 G ,再由抽样得到 d jP f 和 d jD f 。 由此可知,数据的小波变换可以由数据通过高、低通滤波器 H 和 G 来实现,如图 6-16 所示。上述的分 解过程可以不断重复直至分辨率为 2J。 (a) 分解过程 (b) 重构过程 ↓ 2 2↓ G~ H~ 2↑ H~ × 2 P f Á  −à P f Á  fDÁ  P f Á  −à 2↑ G~ 图 6-16 一维小波分解与恢复 因此,一维 Mallat 算法可总结为图 6-17 所示。 J 层小波快速分解算法 J层小波快速重构算法 () j while j J = > 0 () j J while j = < 0 { P f P f HÁ  Á Â= ∗−à ~ { P f P f H D f GÁ  Á  Á  − = ∗ + ∗à } D f P f G j j Á  Á Â= ∗ = + −à 1 ~ } j j= −1 图 6-17 Mallat 一维快速小波算法 第 6 章 图像处理中的正交变换 · 323· Á 2. 二维信号的 Mallat 算法 小波变换的概念可以从一维推广到二维,用于图像的小波分解与重建。一个简单而有用的特例是二 维可分离模型,此时二维尺度函数 ( ) ( )2 2,x y L Rφ ∈ 可表示为两个一维尺度函数的乘积: ( ) ( ) ( ),x y x yφ φ φ= 令 ( )xψ 、 ( )yψ 分别为与 ( )xφ 、 ( )yφ 相对应的一维小波,则在分辨率 k 层,二维的二进小波 可表示为以下 3 个可分离的正交基函数: ( ) ( ) ( ) ()()() ()()() 1 2 3 , , , x y x y x y x y x y x y ψ φ ψ ψ ψ φ ψ ψ ψ = = = 相应的二维 Mallat 小波分解算法如图 6-18 所示,其中 jP 对应于 1jP − 的低频部分,为 1jP − 的逼近图 像; 1 jD 对应于在水平方向上的细节图像; 2 jD 对应于垂直方向上的细节图像; 3 jD 对应于 45 Á、135 Á对 角方向上的细节图像。 H~ G~ H~ 21 ↓ 12 ↓ G~ 12 ↓ 21 ↓ H~ 21 ↓ G~ 21 ↓ Pj−1 Pj D j 1 D j 2 D j 3 行 列 2 取 1 列 行 2 取 1 图 6-18 二维小波变换的 Mallat 算法 6.5.6 小波变换的 Visual C++实现 下面用 Visual C++对小波变换进行编程实现。对小波变换,采用了常用的 Daubechies 系列小波基, 一共了 10 个系列,读者可以根据实际的要求,从中选取合适的小波基。如果需要别的小波基,也可以参 考本程序编写小波变换程序。另外,为了能更好地看清小波变换的过程,在演示程序中,给出的是每次 只进行一层的小波变换(或反变换),同时,也给出了变换任意层数的小波变换函数。这些在程序的注 释中都进行了说明。 首先给出一维小波变换的代码。其中 DWT_1D()是表示进行 nDWTSteps 层小波变换(分解或者反 变换)的函数,而 DWTStep_1D 则表示进行一层小波变换(或反变换)的函数。变换和反变换用参数 nInvVisual C++数字图像获取、处理及实践应用 · 324· 来表示,代码如下。 /************************************************************************* * * \函数名称: * DWT_1D() * * \输入参数: * double * pDbSrc - 指向源数据的指针 * int nMaxLevel - 最大可分解的层数 * int nDWTSteps - 需要分界的层数 * int nInv - 是否为 DWT,1 表示为 IDWT,0 表示 DWT * int nStep - 当前的计算层数 * int nSupp - 小波基紧支集的长度 * * \返回值: * BOOL - 成功则返回 TRUE,否则返回 FALSE * * \说明: * 该函数用对存放在 pDBSrc 中的数据进行一维 DWT 或者 IDWT。其中,nInv 为表示进行 * DWT 或者 IDWT 的标志。nStep 为当前已经分界的层数。计算后数据仍存放在 pDbSrc 中 * ************************************************************************* */ BOOL DWT_1D(double* pDbSrc, int nMaxLevel, int nDWTSteps, int nInv, int nStep, int nSupp ) { // 计算最小可分界的层数 int MinLevel = nMaxLevel-nDWTSteps; // 判断是否为 DWT if (!nInv) { // DWT int n = nMaxLevel; while (n>MinLevel) // 调用 DWTStep_1D 进行第 n 层的 DWT if (!DWTStep_1D(pDbSrc, n--, nInv, nStep, nSupp)) return FALSE; } // nInv 为 1 则进行 IDWT else { // IDWT int n = MinLevel; 第 6 章 图像处理中的正交变换 · 325· while (n=0); // 计算当前层数的长度 Visual C++数字图像获取、处理及实践应用 · 326· int CurN = 1<10 || CurN<2*nSupp) return FALSE; // 分配临时内存用于存放结果 double *ptemp = new double[CurN]; if (!ptemp) return FALSE; double s1, s2; int Index1, Index2; // 判断是进行 DWT 还是 IDWT if (!nInv) { // DWT Index1=0; Index2=2*nSupp-1; // 进行卷积,其中 s1 为低频部分,s2 为高频部分的结果 for (int i=0; iMinWLevel) if (!DWTStep_2D(pDbSrc, n--, m--, nMaxWLevel, nMaxHLevel, nInv, nStep, nSupp)) return FALSE; } // 否则进行 IDWT 第 6 章 图像处理中的正交变换 · 329· else { // IDWT int n = MinWLevel, m = MinHLevel; // 调用 DWTStep_2D 进行 IDWT,进行恢复的层数为 nDWTSteps while (nnMaxWLevel || nDWTSteps>nMaxHLevel || nStep<=0) return FALSE; // 获得 X,Y 方向上的最大像素数(2 次幂对齐) int W = 1<>nDWTSteps, minH = H>>nDWTSteps; int i, j, index; // 分配临时内存存放结果 double* pDbTemp = new double[W*H]; if (!pDbTemp) return FALSE; // 判断是进行 DWT 还是 IDWT,然后将数据存放到临时内存中,需要注意的是,需要进行采 样 if (!nInv) // DWT for (index=0; index=255) return (BYTE)255; else return (BYTE)(f+0.5); } /************************************************************************* * * \函数名称: * FloatToChar() * * \输入参数: * double f - 输入双精度变量 * * \返回值: * Char - 返回字符变量 * * \说明: * 该函数将输入的双精度变量转换为 Char 型的变量 * ************************************************************************* */ char FloatToChar(double f) { if (f>=0) if (f>=127.0) return (char)127; else return (char)(f+0.5); Visual C++数字图像获取、处理及实践应用 · 334· else if (f<=-128) return (char)-128; else return -(char)(-f+0.5); } 在小波变换子菜单的单击处理事件中添加如下代码。 void CImageProcessingView::OnTransDwt() { CImageProcessingDoc * pDoc = (CImageProcessingDoc *)this->GetDocument(); CDib * pDib = pDoc->m_pDibInit; m_nSupp = 1; BeginWaitCursor(); int rsl = DIBDWTStep(pDib,0); EndWaitCursor(); if (!rsl) return; pDoc->SetModifiedFlag(TRUE); pDoc->UpdateAllViews(FALSE); } 在小波反变换的子菜单的单击事件中添加如下代码。 void CImageProcessingView::OnTransIdwt() { // TODO: Add your command handler code here CImageProcessingDoc * pDoc = (CImageProcessingDoc *)this->GetDocument(); CDib * pDib = pDoc->m_pDibInit; BeginWaitCursor(); int rsl = DIBDWTStep(pDib,1); EndWaitCursor(); if (!rsl) return; pDoc->UpdateAllViews(FALSE); } 在实现小波变换之前,我们先在 View 类中定义两个类内成员变量。一个是 m_nSupp,它表示选择 的小波基的紧支集的长度。在给出的程序中,实际上是根据 Daubechies 给出的小波基进行设计的,该小 波基的系数如下所示。在给出的示例中,是根据第一个小波基进行计算的,即设 m_nSupp 为 0。 // Daubechies 紧致正交小波基 // 不同支撑区间长度下的滤波器系数如下 const double hCoef[10][20] = { { .707106781187, .707106781187}, 第 6 章 图像处理中的正交变换 · 335· { .482962913145, .836516303738, .224143868042, -.129409522551 }, { .332670552950, .806891509311, .459877502118, -.135011020010, -.085441273882, .035226291882 }, { .230377813309, .714846570553, .630880767930, -.027983769417, -.187034811719, .030841381836, .032883011667, -.010597401785 }, { .160102397974, .603829269797, .724308528438, .138428145901, -.242294887066, -.032244869585, .077571493840, -.006241490213, -.012580751999, .003335725285 }, { .111540743350, .494623890398, .751133908021, .315250351709, -.226264693965, -.129766867567, .097501605587, .027522865530, -.031582039318, .000553842201, .004777257511, -.001077301085 }, { .077852054085, .396539319482, .729132090846, .469782287405, -.143906003929, -.224036184994, .071309219267, .080612609151, -.038029936935, -.016574541631, .012550998556, .000429577973, -.001801640704, .000353713800 }, { .054415842243, .312871590914, .675630736297, .585354683654, -.015829105256, -.284015542962, .000472484574, .128747426620, -.017369301002, -.044088253931, .013981027917, .008746094047, -.004870352993, -.000391740373, .000675449406, -.000117476784 }, { .038077947364, .243834674613, .604823123690, .657288078051, .133197385825, -.293273783279, -.096840783223, .148540749338, .030725681479, -.067632829061, .000250947115, .022361662124, -.004723204758, -.004281503682, .001847646883, .000230385764, -.000251963189, .000039347320 }, { .026670057901, .188176800078, .527201188932, .688459039454, .281172343661, -.249846424327, -.195946274377, .127369340336, .093057364604, -.071394147166, -.029457536822, .033212674059, .003606553567, -.010733175483, .001395351747, .001992405295, -.000685856695, -.000116466855, .000093588670, -.000013264203 } }; 另一个变量为 double* m_pDbImage,此变量用来存储小波变换的中间结果。由于我们采用的是一层 一层变换(或反变换)的步骤,因此,需要一个类内的变量来存储中间结果。在 View 类的构造函数中 需要给 m_pDbImage 赋初值,并在析构函数中释放已分配的内存。 在本例中,为了能更好地显示每一步,构造了一个函数 DIBDWTStep。这个函数在第一次进入的时 候,需要给 m_pDbImage 分配内存,并进行赋值。其实现代码如下。 BOOL CImageProcessingView::DIBDWTStep(CDib* pDib, int nInv) { // 循环变量 int i, j; Visual C++数字图像获取、处理及实践应用 · 336· // 获取图像的长度和宽度 int nWidth = pDib->m_lpBMIH->biWidth; int nHeight = pDib->m_lpBMIH->biHeight; // 获取变换的最大层数 int nMaxWLevel = Log2(nWidth); int nMaxHLevel = Log2(nHeight); int nMaxLevel; if (nWidth == 1<GetDibSaveDim(); // 临时变量 double *pDbTemp; BYTE *pBits; // 如果小波变换的存储内存还没有分配,则分配此内存 if(!m_pDbImage){ m_pDbImage = new double[nWidth*nHeight]; if (!m_pDbImage) return FALSE; // 将图像数据放入 m_pDbImage 中 for (j=0; jm_lpImage + (nHeight-1-j)*sizeImageSave.cx; for (i=0; i>m_nDWTCurDepth, lfh = nHeight>>m_nDWTCurDepth; for (j=0; jm_lpImage + (nHeight-1-j)*sizeImageSave.cx; for (i=0; i #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif ///////////////////////////////////////////////////////////////////////////// // CDlgHuffman dialog 第 7 章 图像压缩编码 · 345· CDlgHuffman::CDlgHuffman(CWnd* pParent /*=NULL*/) : CDialog(CDlgHuffman::IDD, pParent) { //{{AFX_DATA_INIT(CDlgHuffman) m_dEntropy = 0.0; m_dCodLength = 0.0; m_dRatio = 0.0; //}}AFX_DATA_INIT } void CDlgHuffman::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); //{{AFX_DATA_MAP(CDlgHuffman) DDX_Control(pDX, IDC_LST_Table, m_lstTable); DDX_Text(pDX, IDC_EDIT1, m_dEntropy); DDX_Text(pDX, IDC_EDIT2, m_dCodLength); DDX_Text(pDX, IDC_EDIT3, m_dRatio); //}}AFX_DATA_MAP } BEGIN_MESSAGE_MAP(CDlgHuffman, CDialog) //{{AFX_MSG_MAP(CDlgHuffman) //}}AFX_MSG_MAP END_MESSAGE_MAP() ///////////////////////////////////////////////////////////////////////////// // CDlgHuffman message handlers BOOL CDlgHuffman::OnInitDialog() { // 调用默认得 OnInitDialog()函数 CDialog::OnInitDialog(); // 循环变量 LONG i; LONG j; LONG k; // 中间变量 double dT; Visual C++数字图像获取、处理及实践应用 · 346· // 字符串变量 CString str2View; // 控件 ListCtrl 的 ITEM LV_ITEM lvItem; // 保存控件 ListCtrl 中添加的 ITEM 编号 int nItem2View; // 保存计算中间结果的数组 double * dTemp; // 数组用来存放灰度值和其位置之间的映射 int * n4Turn; // 初始化变量 m_dEntropy = 0.0; m_dCodLength = 0.0; // 分配内存 m_strCode = new CString[nColorNum]; n4Turn = new int[nColorNum]; dTemp = new double[nColorNum]; // 令 dTemp 赋值 // 开始的灰度值按灰度值大小排列相同 for (i = 0; i < nColorNum; i ++) { dTemp[i] = dProba[i]; n4Turn[i] = i; } // 用冒泡法对进行灰度值出现的概率排序 // 同时改变灰度值位置的映射关系 for (j = 0; j < nColorNum - 1; j ++) { for (i = 0; i < nColorNum - j - 1; i ++) { if (dTemp[i] > dTemp[i + 1]) { dT = dTemp[i]; dTemp[i] = dTemp[i + 1]; dTemp[i + 1] = dT; 第 7 章 图像压缩编码 · 347· // 将 i 和 i+1 灰度的位置值互换 for (k = 0; k < nColorNum; k ++) { if (n4Turn[k] == i) n4Turn[k] = i + 1; else if (n4Turn[k] == i + 1) n4Turn[k] = i; } } } } /******************************************************* 计算哈夫曼编码表 *******************************************************/ // 从概率大于 0 处开始编码 for (i = 0; i < nColorNum - 1; i ++) { if (dTemp[i] > 0) break; } for (; i < nColorNum - 1; i ++) { // 更新 m_strCode for (k = 0; k < nColorNum; k ++) { // 灰度值是否 i if (n4Turn[k] == i) { // 灰度值较小的码字加 1 m_strCode[k] = "1" + m_strCode[k]; } else if (n4Turn[k] == i + 1) { // 灰度值较小的码字加 0 m_strCode[k] = "0" + m_strCode[k]; } } // 概率最小的两个概率相加,保存在 dTemp[i + 1]中 dTemp[i + 1] += dTemp[i]; Visual C++数字图像获取、处理及实践应用 · 348· // 改变映射关系 for (k = 0; k < nColorNum; k ++) { // 将位置为 i 的灰度值 i 改为灰度值 i+1 if (n4Turn[k] == i) n4Turn[k] = i + 1; } // 重新排序 for (j = i + 1; j < nColorNum - 1; j ++) { if (dTemp[j] > dTemp[j + 1]) { // 互换 dT = dTemp[j]; dTemp[j] = dTemp[j + 1]; dTemp[j + 1] = dT; // // 将 i 和 i+1 灰度的位置值互换 for (k = 0; k < nColorNum; k ++) { if (n4Turn[k] == j) n4Turn[k] = j + 1; else if (n4Turn[k] == j + 1) n4Turn[k] = j; } } else // 退出循环 break; } } // 计算图像熵 for (i = 0; i < nColorNum; i ++) { if (dProba[i] > 0) { // 计算图像熵 m_dEntropy -= dProba[i] * log(dProba[i]) / log(2.0); } } 第 7 章 图像压缩编码 · 349· // 计算平均码字长度 for (i = 0; i < nColorNum; i ++) { // 累加 m_dCodLength += dProba[i] * m_strCode[i].GetLength(); } // 计算编码效率 m_dRatio = m_dEntropy / m_dCodLength; // 保存变动 UpdateData(FALSE); /************************************************* 输出计算结果 *************************************************/ // 设置 CListCtrl 控件样式 m_lstTable.ModifyStyle(LVS_TYPEMASK, LVS_REPORT); // 给 List 控件添加 Header m_lstTable.InsertColumn(0, "灰度值", LVCFMT_LEFT, 60, 0); m_lstTable.InsertColumn(1, "概率值", LVCFMT_LEFT, 78, 0); m_lstTable.InsertColumn(2, "哈夫曼编码", LVCFMT_LEFT, 110, 1); m_lstTable.InsertColumn(3, "码字长度", LVCFMT_LEFT, 78, 2); // 设置样式为文本 lvItem.mask = LVIF_TEXT; // 添加显示 for (i = 0; i < nColorNum; i ++) { // 第一列显示 lvItem.iItem = m_lstTable.GetItemCount(); str2View.Format("%u",i); lvItem.iSubItem = 0; lvItem.pszText= (LPTSTR)(LPCTSTR)str2View; nItem2View = m_lstTable.InsertItem(&lvItem); // 其他列显示 lvItem.iItem = nItem2View; // 添加灰度值的概率值 Visual C++数字图像获取、处理及实践应用 · 350· lvItem.iSubItem = 1; str2View.Format("%f",dProba[i]); lvItem.pszText = (LPTSTR)(LPCTSTR)str2View; m_lstTable.SetItem(&lvItem); // 添加哈夫曼编码 lvItem.iSubItem = 2; lvItem.pszText = (LPTSTR)(LPCTSTR)m_strCode[i]; m_lstTable.SetItem(&lvItem); // 添加码字长度 lvItem.iSubItem = 3; str2View.Format("%u",m_strCode[i].GetLength()); lvItem.pszText = (LPTSTR)(LPCTSTR)str2View; m_lstTable.SetItem(&lvItem); } // 内存释放 delete n4Turn; delete dTemp; // 返回 TRUE return TRUE; } 最后在 ImageProcessingView.cpp 文件中对霍夫曼编码子菜单添加命令处理函数如下。 void CImageProcessingView::OnCodingHuffman() { // 哈夫曼编码表 // 获取文档 CImageProcessingDoc * pDoc = GetDocument(); // 指向源图像像素的指针 unsigned char * lpSrc; // 图像的高度和宽度 LONG lHeight; LONG lWidth; // 图像每行的字节数 LONG lLineBytes; // 图像像素总数 LONG lCountSum; 第 7 章 图像压缩编码 · 351· // 循环变量 LONG i; LONG j; // 数组指针用来保存各个灰度值出现概率 double * dProba; // 当前图像颜色数目 int nColorNum; // 获得图像 CDib 类的指针 CDib * pDib = pDoc->m_pDibInit; //图像数据的指针 LPBYTE lpDIBBits = pDib->m_lpImage; // 头文件信息 LPBITMAPINFOHEADER lpBMIH=pDib->m_lpBMIH; // 判断是否是 8-bpp 位图(只处理 8-bpp 位图的霍夫曼编码) if (lpBMIH->biBitCount != 8) { // 提示用户 MessageBox("目前只支持 256 色位图的霍夫曼编码!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } // 更改光标形状 BeginWaitCursor(); /******************************************************************** 开始计算各个灰度级出现的概率 如果需要对指定的序列进行哈夫曼编码, 只要将这一步改成给各个灰度级概率赋值即可 ********************************************************************** */ // 由头文件信息得到图像的比特数,从而得到颜色信息 nColorNum = (int)pow(2,lpBMIH->biBitCount); Visual C++数字图像获取、处理及实践应用 · 352· // 分配内存 dProba = new double[nColorNum]; //得到图像的宽度和高度 CSize SizeDim; SizeDim = pDib->GetDimensions(); lWidth = SizeDim.cx; lHeight = SizeDim.cy; // 计算图像像素总数 lCountSum = lHeight * lWidth; //得到实际的 Dib 图像存储大小 CSize SizeRealDim; SizeRealDim = pDib->GetDibSaveDim(); // 计算图像每行的字节数 lLineBytes = SizeRealDim.cx; // 赋零值 for (i = 0; i < nColorNum; i ++) { dProba[i] = 0.0; } // 计算各个灰度值的计数 for (i = 0; i < lHeight; i ++) { for (j = 0; j < lWidth; j ++) { // 指向图像指针 lpSrc = lpDIBBits + lLineBytes * i + j; // 计数加 1 dProba[*(lpSrc)] = dProba[*(lpSrc)] + 1; } } // 计算各个灰度值出现的概率 for (i = 0; i < nColorNum; i ++) { dProba[i] = dProba[i] / (FLOAT)lCountSum; } 第 7 章 图像压缩编码 · 353· /*************************************************** 构建霍夫曼编码的码表 并用对话框显示霍夫曼码表 ****************************************************/ // 创建对话框 CDlgHuffman dlgCoding; // 初始化变量值 dlgCoding.dProba = dProba; dlgCoding.nColorNum = nColorNum; // 显示对话框 dlgCoding.DoModal(); // 恢复光标 EndWaitCursor(); } 图 7-4 所示是用子菜单实现的功能对 lena 图像生成霍夫曼码表的例子。 图 7-4 lena 图像的霍夫曼编码表 7.4 香农-费诺(Shannon-Fano)编码 香农-费诺编码也是一种常用的统计编码。这种码有时候也可以得到最佳的编码性能。 7.4.1 香农-费诺编码的理论及算法 香农-费诺码的编码条件要符合非续长的条件,在码字中 1 和 0 是独立的,而且差不多是等概率的。 这样的准则一方面保证无需用区间来区分码字,同时又保证每传 1 位码就有 1bit 的信息。 Visual C++数字图像获取、处理及实践应用 · 354· 香农-费诺码的码表可以通过下面几个步骤来实现。 第一步,设信源有非递增的概率分布: 1 2 3 1 2 3 M M u u u uX P PPP  =       其中 1 2 3 MP PPP≥ ≥ ≥ ≥ ,将 X 分成两个集合,有: 1 2 3 1 1 2 3 1 2 3 2 1 2 3 k k k k k M k k k M u u u uX PPPP u u u uX P PPP + + + + + +  =      =         分类的准则是尽量保证: 1 1 k M i i i i k P P = = + ≈∑ ∑ (7-8) 第二步,给两个子集中的消息分别赋值 1 和 0 或者 0 和 1。 第三步,重复上述步骤,将两个子集 1 2X X和 以式 7-8 为准则细分为两个子集,再进行第二步中的赋 值。这样的步骤重复下去,直到每个子集内只包含一个消息为止。对每个消息所赋过的值依次排列出来就可 以构成香农-费诺码表。 对于香农-费诺码来说,如果满足下式: ( ) 2 ÁÁ ÂP b −= (7-9) 且有: Á 2 1Á Á  à − = =∑ (7-10) 就会使得编码效率达到 100%。 对于不满足式 7-9 和 7-10 的信源,效率不能达到 100%,但也相对较高,香农-费诺码仍是一种相当 好的编码。 7.4.2 香农-费诺码的 Visual C++实现 下面用 Visual C++来实现对一幅图像灰度值的出现概率进行统计,并得到香农-费诺码表的功能。 先添加一个名为“香农-费诺编码”的对话框,用来显示香农-费诺编码表。下面是对话框头文件。 class CDlgShannon : public CDialog { // Construction public: // 灰度级别数目 int m_nColorNum; 第 7 章 图像压缩编码 · 355· // 各个灰度值出现频率 double * m_dProba; // 香农-弗诺编码表 CString * m_strCode; CDlgShannon(CWnd* pParent = NULL); // standard constructor // Dialog Data //{{AFX_DATA(CDlgShannon) enum { IDD = IDD_CODING_SHANFINO }; CListCtrl m_lstTable; double m_dEntropy; double m_dAvgCodeLen; double m_dEfficiency; //}}AFX_DATA // Overrides // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(CDlgShannon) protected: virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support //}}AFX_VIRTUAL // Implementation protected: // Generated message map functions //{{AFX_MSG(CDlgShannon) virtual BOOL OnInitDialog(); //}}AFX_MSG DECLARE_MESSAGE_MAP() }; 接下来是对话框的源文件 DlgCodingShanFino.cpp。 #include "stdafx.h" #include "imageprocessing.h" #include "DlgCoding.h" #include #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif Visual C++数字图像获取、处理及实践应用 · 356· ///////////////////////////////////////////////////////////////////////////// // CDlgShannon dialog CDlgShannon::CDlgShannon(CWnd* pParent /*=NULL*/) : CDialog(CDlgShannon::IDD, pParent) { //{{AFX_DATA_INIT(CDlgShannon) m_dEntropy = 0.0; m_dAvgCodeLen = 0.0; m_dEfficiency = 0.0; //}}AFX_DATA_INIT } void CDlgShannon::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); //{{AFX_DATA_MAP(CDlgShannon) DDX_Control(pDX, IDC_LST_Table, m_lstTable); DDX_Text(pDX, IDC_EDIT1, m_dEntropy); DDX_Text(pDX, IDC_EDIT2, m_dAvgCodeLen); DDX_Text(pDX, IDC_EDIT3, m_dEfficiency); //}}AFX_DATA_MAP } BEGIN_MESSAGE_MAP(CDlgShannon, CDialog) //{{AFX_MSG_MAP(CDlgShannon) //}}AFX_MSG_MAP END_MESSAGE_MAP() ///////////////////////////////////////////////////////////////////////////// // CDlgShannon message handlers BOOL CDlgShannon::OnInitDialog() { // 调用默认的 OnInitDialog()函数 CDialog::OnInitDialog(); // 初始化变量 m_dEntropy = 0.0; m_dAvgCodeLen = 0.0; // 字符串变量 CString str; 第 7 章 图像压缩编码 · 357· str = "1"; // 循环变量 LONG i; LONG j; // 控件 ListCtrl 的 ITEM LV_ITEM lvItem; // 保存控件 ListCtrl 中添加的 ITEM 编号 int nItem2View; // 中间变量 double dT; LONG lTemp; // 保存计算中间结果的数组 double * dTemp; dTemp = new double[m_nColorNum]; // 数组用来存放灰度值和其位置之间的映射 LONG * l4Turn; l4Turn = new LONG[m_nColorNum]; // 当前编码区间的概率和 double dAccum; dAccum = 1.0; // 已经编码灰度值概率的统计和 double dSum; dSum = 0; // 已编码灰度值 LONG lCount = 0; // 起始位置 LONG lBegin; // 指示编码是否已经完成一段 BOOL * bFinished; bFinished = new BOOL[m_nColorNum]; // 分配内存 m_strCode = new CString[m_nColorNum]; Visual C++数字图像获取、处理及实践应用 · 358· for (i = 0; i < m_nColorNum; i ++) { // 初始化为 FALSE bFinished[i] = FALSE; // 将概率赋值 dTemp 数组 dTemp[i] = m_dProba[i]; // 按灰度值大小顺序排列 l4Turn[i] = i; } // 用冒泡法对进行灰度值出现的概率排序 // 同时改变灰度值位置的映射关系 for (j = 0; j < m_nColorNum - 1; j ++) for (i = 0; i < m_nColorNum - j - 1; i ++) if (dTemp[i] > dTemp[i + 1]) { // 互换 dT = dTemp[i]; dTemp[i] = dTemp[i + 1]; dTemp[i + 1] = dT; // 将 i 和 i+1 灰度的位置值互换 lTemp = l4Turn[i]; l4Turn[i] = l4Turn[i+1]; l4Turn[i+1] = lTemp; } /******************************************************* 计算香农-费诺编码表 *******************************************************/ // 从概率大于 0 处开始编码 for (lBegin = 0; lBegin < m_nColorNum - 1; lBegin ++) if (dTemp[lBegin] > 0) break; // 开始编码 while(lCount < m_nColorNum) 第 7 章 图像压缩编码 · 359· { // 从概率大于零的灰度值开始编码 lCount = lBegin; // 对区间进行分割,对每个区间的灰度值编码 for (i = lBegin; i < m_nColorNum; i ++) { // 判断是否编码完成 if (bFinished[i] == FALSE) { // 增加当前编码区间的概率综合 dSum += dTemp[i]; // 判断是否超出总和的一半 if (dSum > dAccum/2.0) { // 超出,追加的字符改为 0 str = "0"; } // 追加字符 m_strCode[l4Turn[i]] = m_strCode[l4Turn[i]] + str; // 判断是否编码完一段 if (dSum == dAccum) { // 完成一部分编码,重新计算 dAccum // 初始化 dSum 为 0 dSum = 0; // 判断是不是对所有灰度值已经编码一遍 if (i == m_nColorNum - 1) j = lBegin; else j = i + 1; // 保存 j 值 lTemp = j; str = m_strCode[l4Turn[j]]; // 计算下一编码区间的概率总和 dAccum = 0; for (; j < m_nColorNum; j++) Visual C++数字图像获取、处理及实践应用 · 360· { // 判断是否是同一段编码 if ((m_strCode[l4Turn[j]].Right(1) != str.Right(1)) || (m_strCode[l4Turn[j]].GetLength() != str.GetLength())) break; // 当前区间的概率总和增加 dAccum += dTemp[j]; } // 码字增加值为 1 str = "1"; // 判断该段编码已经完成 if (lTemp + 1 == j) bFinished[lTemp] = TRUE; } } else { // 开始下一轮编码 lCount ++; // 重新赋 2dSum 为 0 dSum = 0; // 判断是不是对所有灰度值已经编码一遍 if (i == m_nColorNum - 1) j = lBegin; else j = i + 1; // 保存 j 值 lTemp = j; str = m_strCode[l4Turn[j]]; // 计算下一编码区间的概率总和 dAccum = 0; for (; j < m_nColorNum; j++) { // 判断是否是同一段编码 if ((m_strCode[l4Turn[j]].Right(1) != str.Right(1)) || (m_strCode[l4Turn[j]].GetLength() != str.GetLength())) { 第 7 章 图像压缩编码 · 361· // 退出循环 break; } // 累加 dAccum += dTemp[j]; } str = "1"; // 判断该段编码已经完成 if (lTemp + 1 == j) bFinished[lTemp] = TRUE; } } } // 计算图像熵 for (i = 0; i < m_nColorNum; i ++) { // 判断概率是否大于 0 if (m_dProba[i] > 0) { // 计算图像熵 m_dEntropy -= m_dProba[i] * log(m_dProba[i]) / log(2.0); } } // 计算平均码字长度 for (i = 0; i < m_nColorNum; i ++) { // 累加 m_dAvgCodeLen += m_dProba[i] * m_strCode[i].GetLength(); } // 计算编码效率 m_dEfficiency = m_dEntropy / m_dAvgCodeLen; // 保存变动 UpdateData(FALSE); /************************************************* 输出编码结果 *************************************************/ Visual C++数字图像获取、处理及实践应用 · 362· // 设置 List 控件样式 m_lstTable.ModifyStyle(LVS_TYPEMASK, LVS_REPORT); // 给 List 控件添加 Header m_lstTable.InsertColumn(0, "灰度值", LVCFMT_LEFT, 60, 0); m_lstTable.InsertColumn(1, "灰度值概率", LVCFMT_LEFT, 78, 0); m_lstTable.InsertColumn(2, "香农-弗诺编码", LVCFMT_LEFT, 110, 1); m_lstTable.InsertColumn(3, "码字长度", LVCFMT_LEFT, 78, 2); // 设置样式为文本 lvItem.mask = LVIF_TEXT; // 添加显示 for (i = 0; i < m_nColorNum; i ++) { // 第一列显示 lvItem.iItem = m_lstTable.GetItemCount(); str.Format("%u",i); lvItem.iSubItem = 0; lvItem.pszText= (LPTSTR)(LPCTSTR)str; nItem2View = m_lstTable.InsertItem(&lvItem); // 其他列显示 lvItem.iItem = nItem2View; // 添加灰度值出现的频率 lvItem.iSubItem = 1; str.Format("%f",m_dProba[i]); lvItem.pszText = (LPTSTR)(LPCTSTR)str; m_lstTable.SetItem(&lvItem); // 添加香农-弗诺编码 lvItem.iSubItem = 2; lvItem.pszText = (LPTSTR)(LPCTSTR)m_strCode[i]; m_lstTable.SetItem(&lvItem); // 添加码字长度 lvItem.iSubItem = 3; str.Format("%u",m_strCode[i].GetLength()); lvItem.pszText = (LPTSTR)(LPCTSTR)str; m_lstTable.SetItem(&lvItem); } 第 7 章 图像压缩编码 · 363· // 内存释放 delete l4Turn; delete dTemp; delete bFinished; return TRUE; } 最后在 ImageProcessingView.cpp 文件中对香农-费诺编码子菜单添加命令处理函数,代码如下。 void CImageProcessingView::OnCodingShanfino() { // 香农-费诺编码表 // 获取文档 CImageProcessingDoc * pDoc = GetDocument(); // 指向源图像像素的指针 unsigned char * lpSrc; // 图像的高度 LONG lHeight; LONG lWidth; // 图像每行的字节数 LONG lLineBytes; // 获取当前 DIB 颜色数目 int nColorNum; // 图像像素总数 LONG lCountSum; // 循环变量 LONG i; LONG j; // 保存各个灰度值出现概率的数组指针 double * dProba; // 获得图像 CDib 类的指针 CDib * pDib = pDoc->m_pDibInit; //图像数据的指针 LPBYTE lpDIBBits = pDib->m_lpImage; Visual C++数字图像获取、处理及实践应用 · 364· // 头文件信息 LPBITMAPINFOHEADER lpBMIH=pDib->m_lpBMIH; // 判断是否是 8-bpp 位图(这里为了方便,只处理 8-bpp 位图的香农-费诺编码) if (lpBMIH->biBitCount != 8) { // 提示用户 MessageBox("目前只支持 256 色位图的香农-费诺编码!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } // 更改光标形状 BeginWaitCursor(); /****************************************************************************** // 开始计算各个灰度级出现的概率 // // 如果需要对指定的序列进行香农-弗诺编码, //只要将这一步改成给各个灰度级概率赋值即可 ***************************************************************************** */ // 灰度值总数的计算 nColorNum = (int)pow(2,lpBMIH->biBitCount); // 分配内存 dProba = new double[nColorNum]; //得到图像的宽度和高度 CSize SizeDim; SizeDim = pDib->GetDimensions(); lWidth = SizeDim.cx; lHeight = SizeDim.cy; // 计算图像像素总数 lCountSum = lHeight * lWidth; //得到实际的 Dib 图像存储大小 CSize SizeRealDim; SizeRealDim = pDib->GetDibSaveDim(); // 计算图像每行的字节数 第 7 章 图像压缩编码 · 365· lLineBytes = SizeRealDim.cx; // 计算图像像素总数 lCountSum = lHeight * lWidth; // 重置计数为 0 for (i = 0; i < nColorNum; i ++) { dProba[i] = 0.0; } // 计算各个灰度值的计数(对于非 256 色位图,此处给数组 dProba 赋值方法将不同) for (i = 0; i < lHeight; i ++) { for (j = 0; j < lWidth; j ++) { // 指向图像指针 lpSrc = lpDIBBits + lLineBytes * i + j; // 计数加 1 dProba[*(lpSrc)] = dProba[*(lpSrc)]+ 1; } } // 计算各个灰度值出现的概率 for (i = 0; i < nColorNum; i ++) { dProba[i] /= (double)lCountSum; } /*************************************************** 构建香农-费诺编码的码表 并用对话框显示香农-费诺码表 ****************************************************/ // 创建对话框 CDlgShannon dlgPara; // 初始化变量值 dlgPara.m_dProba = dProba; dlgPara.m_nColorNum = nColorNum; // 显示对话框 Visual C++数字图像获取、处理及实践应用 · 366· dlgPara.DoModal(); //释放内存 delete dProba; // 恢复光标 EndWaitCursor(); } 图 7-5 所示为用子菜单实现的功能对 lena 图像生成香农-费诺码表的例子。 图 7-5 lena 图像的香农-费诺编码 7.5 算术编码 前面介绍的几种编码方式的码字必定是整数比特长,但是在某些情况下就会出现问题。例如,若一个 符号的出现概率为 1 3 ,则在理论上编码该符号的最少位数是 1.6bit 1 3( ( ) log3)HA = ,可是霍夫曼只能将码 字设定为 1bit 或者 2bit,并且每种选择都会得到比理论上可能的长度更长的比特表示。又如,在压缩二值图 像时,用 0 和 1 表示黑和白,它们都占用一个单独的比特,如果用前面几种方法进行编码,一点都不能压缩。 在 20 世纪的 60 年代,由 Elias 首先提出把这种依附 Shannon 编码的概念推广到对符号序列直接编码的 概念上,提出了算术(Arithmetic Coding)编码的概念。1979 年,由 Rissanen 和 Langdon G G 一起将其系统 化。1981 年,他们将 AC 推广到二值图像编码上,大大提高了其压缩效率。例如,对二元平稳马尔可夫信源, 效率可以高达 95%。 算术编码没有对各输入符号的信息量为整数的限制。它的基本思想是将信源输出序列的概率和实数段 [0,1)中的一个浮点小数 p 联系起来,序列的概率刚好等于 p 所在区间的长度,序列越长,p 的区间长度越短。 该浮点数 p 的二进制展开式即为原符号串的压缩编码结果。因为小数随位数的增加,它的精度也随之提高, 从信息的角度来说,它所含有的信息量也随之增加。 和哈夫曼编码相比,算术编码对源字母进行编码不如哈夫曼编码好。但对源字母序列编码,算术编码 比哈夫曼编码好,不需要在 N 改变时重新计算和分配比特数。可以用分数比特编码,获得较高的压缩率。 算术编码算法同符号概率统计是相互独立的,不像哈夫曼编码那样,符号概率统计的改变需要重新建立哈夫第 7 章 图像压缩编码 · 367· 曼树,并改变码表,算术编码更易于实现自适应。 7.5.1 算术编码的理论及算法 1.编码过程 算术编码的方法是将被编码的消息或者符号串表示成 0 和 1 之间的一个间隔,即将其编码成[0,1)之间 的浮点小数。符号序列越长,编码表示它的间隔也就越小,表示这一间隔所需的位数也就越多。由于信源的 符号序列需要根据某种编码模式生成概率的大小来减少间隔,出现概率大的符号 F 要比出现概率小的符号减 少范围小,因此,只要增加较少的比特位就可以对新增加的信息进行编码。 在编码任何消息之前,符号串的完整范围被设定为[0,1)。当一个符号被处理时,这一范围就根据分配 给这一符号的范围变窄。算术编码的过程,实际上就是根据信号源发生概率对区间[0,1)进行分割的过程。 现以二值信源的算术编码为例来说明编码过程,多值信源的编码方法可以类推。 设信源有两种符号 0 和 1,它们出现的概率分别是 1/4 和 3/4。区间[0,1)被分割成两个概率范围[0,1/4)、 [1/4,1),其中 0 是符号 0 的 rangelow,1/4 是符号 0 的 rangehigh,同时 1/4 还是符号 1 的 rangelow,1 是符号 1 的 rangehigh。这时区间的范围(range)为 1,范围的下限(low)为 0,上限(high)为 1。 对于消息 S 的符号序列{ }ka 的算术编码过程可以概括成以下几步。 (1)初始化时,low=0,high=1,被分割的范围 range=high-low=1。 (2)下一个范围的 high 和 low 可以根据输入的不同符号 ka ( ka 的取值是 1 或者 0),由下式计算: low low range rangelow high low range rangehigh = + ×  = + × (7-11) 其中等号右侧的 low 为上一个被编码字符 1ka − 所在区间的下限,high 为上限。rangehigh 为当前编码字 符 ka 概率范围的下限,rangelow 为下限。通过上式的计算,左侧的 low 得到当前编码字符区间的下限,high 为上限。 (3)重复过程(2)直到所有的符号都被编码为止。 编码结果就是消息落在的区间范围。从上面的编码过程可知,对于不同的消息,编码过程是惟一的, 最后的区间范围也是惟一的。所以选取区间中任何一点,都可以表示这个区间,也就是被编码的消息 S,且 不会有冲突。 下面来讨论如何选取这个点,令: ()2 1logL p s  =     (7-12) 式中符号 Y   代表取大于或者等于 Y 的整数, ( )p s 代表被编码消息的出现概率,可以由每个字符的 概率 ( )kp a 相乘得到: ( ) ( ) ( ) ( ) ( )1 2 1n np s p a p a p a p a−= × × × × (7-13) 最后把得到编码结果的区间下限,也就是 rangelow 写成二进制的小数,取其前 L 位,如果后面尚有位Visual C++数字图像获取、处理及实践应用 · 368· 数,就进位到第 L 位,这样得到一个数 C,用 C 就可以作为消息 S 的算术编码码字。 如果消息为 0010,具体的计算如下: 从[0,1]开始,一个符号一个符号地迭代分解区间。 第一字符 0,取[0,1]区间的前 1/4,即区间[0,1/4]。 第二字符 0,取[0,1/4]区间的前 1/4,即区间[0,1/16]。 第三字符 1,取[0,1/16]区间的后 3/4,即区间[1/64,4/64]。 第四字符 0,取[1/64,4/64]区间的前 1/4,即区间[4/256,7/256],其二进制表示为[0.000000100,0.00000111)。 消息 0010 的出现概率 ( )p s 为: 3( ) 0.25 0.75 0.01171875p s = × = 码字的长度为: ()2 1log 7L p s  = =    由于 rangelow=0.00000100,码字长度为 7,且没有余数,因此数 C 为 0.0000010。最后输出的二进制 码字为 0000010。 上述的编码过程若用数轴上的点来表示,将更加清楚,如图 7-6 所示。 0 0 0.01 1 0 0 0.0001 0.01 1 0 0.000001 0.0001 0 0.00000100 0.00000101 0.00010000 图 7-6 算术编码过程 综上所述,算术编码就是从全局出发,采用递推形式的一种连续编码。不同的输入符号一定落入不同 的区间,不会互相重叠,因此编码结果是惟一的。不同信息组合映射到不同的实数区间,信息中所用符号出 现的概率愈大,对应的区间也愈大,区间愈大,就愈有机会选择较短的码字来表示该信息。当编码的符号数 目足够多时,编码效率趋向于这些信息的熵值。 2.解码过程 解码是编码的逆过程,理解了编码过程,解码过程的操作就容易多了。由于编码的结果 0.0000010 是消 息 0010 的惟一编码,因此在解码的时候,只需要判断当前的解码值落在哪一个符号的概率范围,就能正确 解出符号,进行输出。 例如,二进制 0.0000010 落在区间[0,1/4)之中,所以第一个符号就是 0。解出第一个符号以后,由 0 的 rangehigh 和 rangelow,以及当前区间的范围(range)1,首先减去符号 0 的 rangelow(0.0),得到二进制 0.0000010,然后用 0.0000010 除以符号 0 的概率 1/4 得到二进制 0.0001。0.001 落在区间[0,1/4)之中,所以第 二个符号也为 0。按照上述的方法,就可以得到编码时的全部符号。 解码过程可以综合如下: 第 7 章 图像压缩编码 · 369· 0.00000100 : 0.00000101 0.0000010 00 : 0.00010.01 0.0001 01: 0.010.01 0.01 0.010 : 00.11 = − = − = − = 解码公式可以概括为: number rangelow numberrange − = (7-14) 7.5.2 算术编码的 Visual C++实现 下面用 Visual C++实现将有限长的二进制序列进行算术编码的功能。程序中字符 0 的出现概率为 1/4, 字符 1 的出现概率为 3/4。区间[0,1)被分割成两个概率范围[0,1/4),[1/4,1),其中 0 是符号 0 的 rangelow, 1/4 是符号 0 的 rangehigh,同时 1/4 还是符号 1 的 rangelow,1 是符号 1 的 rangehigh。 首先添加一个名为“算术编码”的对话框,用来进行算术编码和译码。考虑到浮点数运算的精度问题, 输入的二进制序列长度应限制在 15 以内。下面是对话框的头文件。 class CDlgArith : public CDialog { // Construction public: CDlgArith(CWnd* pParent = NULL); // standard constructor // Dialog Data //{{AFX_DATA(CDlgArith) enum { IDD = IDD_CODING_ARITH }; CEdit m_ConArithSer; CButton m_coding; CButton m_decoding; CString m_ArithSerial; CString m_ArithOutput; double m_ArithEffi; CString m_ArithDecode; //}}AFX_DATA // Overrides // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(CDlgArith) protected: virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support //}}AFX_VIRTUAL Visual C++数字图像获取、处理及实践应用 · 370· // Implementation protected: // Generated message map functions //{{AFX_MSG(CDlgArith) virtual void OnOK(); afx_msg void OnDecoding(); afx_msg void OnCoding(); //}}AFX_MSG DECLARE_MESSAGE_MAP() }; 接下来是对话框的源文件 DlgArithCoding.cpp。 ///////////////////////////////////////////////////////////////////////////// // CDlgArith dialog #include "stdafx.h" #include "ImageProcessing.h" #include "DlgCoding.h" #include #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif #define fPro4Zero 0.25; #define fPro4One 0.75; CDlgArith::CDlgArith(CWnd* pParent /*=NULL*/) : CDialog(CDlgArith::IDD, pParent) { //{{AFX_DATA_INIT(CDlgArith) m_ArithSerial = _T(""); m_ArithOutput = _T(""); m_ArithDecode = _T(""); //}}AFX_DATA_INIT } void CDlgArith::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); //{{AFX_DATA_MAP(CDlgArith) DDX_Control(pDX, IDC_EDIT1, m_ConArithSer); 第 7 章 图像压缩编码 · 371· DDX_Control(pDX, IDCODING, m_coding); DDX_Control(pDX, IDDECODING, m_decoding); DDX_Text(pDX, IDC_EDIT1, m_ArithSerial); DDV_MaxChars(pDX, m_ArithSerial, 15); DDX_Text(pDX, IDC_EDIT2, m_ArithOutput); DDX_Text(pDX, IDC_EDIT4, m_ArithDecode); //}}AFX_DATA_MAP } BEGIN_MESSAGE_MAP(CDlgArith, CDialog) //{{AFX_MSG_MAP(CDlgArith) ON_BN_CLICKED(IDDECODING, OnDecoding) ON_BN_CLICKED(IDCODING, OnCoding) //}}AFX_MSG_MAP END_MESSAGE_MAP() ///////////////////////////////////////////////////////////////////////////// // CDlgArith message handlers void CDlgArith::OnOK() { // CDialog::OnOK(); } /////////////////////////////////////////////////////////////// // DlgCodingIArith dialog /*********************************************************** 实现对已经编码的码字进行解码的功能 ********************************************************* */ void CDlgArith::OnDecoding() { // 二值序列的长度 int nOutLength; // 算术编码的长度 int nInLength; // 编码区间的上限和下限 double dHigh=1.0; double dLow=0.0; // 编码区间的长度 double dRange=1.0; Visual C++数字图像获取、处理及实践应用 · 372· // 判断二值序列是否全零 int nNo1=0; // 循环变量 int i; // 二进制表示为十进制 double dTenCode=0; // 中间变量 double d2Pow; double dTemp; // 接收数据 UpdateData(TRUE); // 解码显示清空 m_ArithDecode = _T(""); // 显示数据 UpdateData(FALSE); // 算术编码的长度 nInLength =m_ArithOutput.GetLength(); // 将二进制序列转化为十进制,并判断是否为零 for (i=0; idTenCode) { // 输出 0 m_ArithDecode=m_ArithDecode+’0’; // 编码区间上下限的计算 dLow=dLow; dHigh=dLow+dRange*fPro4Zero; // 区间范围 dRange=dHigh-dLow; } else { // 输出 1 m_ArithDecode=m_ArithDecode+’1’; // 编码区间上下限的计算 dLow=dLow+dRange*fPro4Zero; dHigh=dHigh; // 区间范围 dRange=dHigh-dLow; } } } else { for(i=0;i= d2Pow) { m_ArithOutput = m_ArithOutput + "1"; dTemp = dTemp - d2Pow; } else m_ArithOutput = m_ArithOutput + "0"; } // 转化后是否有余数 if(dTemp > 0) { // 二进制小数进行进位 for(i = nOutLength-1; i >= 0; i--) { // 进位,1 转化为 0 if(m_ArithOutput.Mid(i,1) == ’1’) { m_ArithOutput.Delete(i,1); m_ArithOutput.Insert(i,"0"); } // 进位完成,最后的 0 位转化为 1 else { m_ArithOutput.Delete(i,1); m_ArithOutput.Insert(i,"1"); break; } } } 第 7 章 图像压缩编码 · 377· // 编码完成,数据更新 UpdateData(FALSE); // 允许进行解码 m_decoding.EnableWindow(TRUE); // 解码前禁止编码 m_coding.EnableWindow(FALSE); // 解码前禁止输入新的二进制序列 m_ConArithSer.EnableWindow(FALSE); } 最后在 ImageProcessingView.cpp 文件中对算术编码子菜单添加命令处理函数,代码如下。 void CImageProcessingView::OnCodingArith() { CDlgArith dlgCoding; // 显示对话框 dlgCoding.DoModal(); } 图 7-7 所示为用子菜单实现的功能对输入二值序列实现算术编码的例子。 图 7-7 二进制序列的算术编码 7.6 游程编码(Run Length Coding) 游程编码适用于有较多灰度相同对象的图像,例如海洋、湖泊的卫星图像,医学图像中的细胞,染色 体,材料的显微图像等。计算机中的 PCX 和 BMP 格式的图像都采用游程编码进行压缩。RLE 的原理相当 简单,计算效率高。 7.6.1 基本原理 游程编码的原理很简单:将一行中颜色值相同的相邻像素用一个计数值和该颜色值来代替。 Visual C++数字图像获取、处理及实践应用 · 378· 例如在图 7-8 中所示的图像元素可以编码为(5,3)(3,1)(4,4)(4,2)(3,2)(2,1)(3,1)(2,2)。 图 7-8 游程编码示意 下面讨论一下 RLE 编码的效率。 设图像的灰度级为 M,一行的长度为 N,则对每一行来说,游程数最少为 1,最大为 N。若将数对表示 为(gk,lk)的序列,用普通二进制码存放(gk,lk)序列,并设一行中的游程数为 m,则描述一行像素需要的码字 长度为: 2 2m (log M +log N) bit (7-15) 而直接存储原图像一行所需的位数为 ÁNlog M bit。 显然,只有当 m<8) return FALSE; // 指向源图像的指针 BYTE * lpSrc; //图像的宽度和高度 LONG lWidth; LONG lHeight; 第 7 章 图像压缩编码 · 383· // 图像每行的字节数 LONG lLineBytes; // 循环变量 LONG i; LONG j; // 中间变量 BYTE bTemp; BYTE bA; // 二进制第 i 位对应的十进制值 BYTE bCount; // 指向编码后图像数据的指针 BYTE * lpDst; //得到图像的宽度和高度 CSize SizeDim; SizeDim = pDib->GetDimensions(); lWidth = SizeDim.cx; lHeight = SizeDim.cy; //得到实际的 Dib 图像存储大小 CSize SizeRealDim; SizeRealDim = pDib->GetDibSaveDim(); // 计算图像每行的字节数 lLineBytes = SizeRealDim.cx; //图像数据的指针 LPBYTE lpDIBBits = pDib->m_lpImage; bCount = 1<<(bBitNum - 1); for (i = 0; im_pDibInit; 第 7 章 图像压缩编码 · 385· // 头文件信息 LPBITMAPINFOHEADER lpBMIH=pDib->m_lpBMIH; // 判断是否是 8-bpp 位图(这里为了方便,只处理 8-bpp 位图的位平面分解) if (lpBMIH->biBitCount != 8) { // 提示用户 MessageBox("目前只支持 256 色位图的位平面分解!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } DIBBITPLANE(pDib,bBitNum); // 设置脏标记 pDoc->SetModifiedFlag(TRUE); // 更新视图 pDoc->UpdateAllViews(NULL); } 7.8 预测编码 预测编码(Predictive Coding)是一种经典的数据压缩编码方法。预测编码建立在信号数据的相关性上, 根据某一模型利用以往样本值对新样本进行预测,减少数据在空间和时间上的相关性,以达到压缩数据的目 的。 从模拟量到数字量的转换过程为脉冲编码调制过程 PCM(Pulse Code Modulation),也称 PCM 编码。 对图像而言,直接以 PCM 编码,存储量很大。预测编码可以利用相邻像素之间的相关性,用前面已出现的 像素值估计当前像素值,对实际值与估计值的差值进行编码。常用的一种线性预测编码方法是差分脉冲编码 调制编码 DPCM(Differential Pulse Code Modulation),它属于有损编码,本节主要介绍它的原理及应用。 7.8.1 DPCM 的基本原理 由图像的统计特性可知,相邻像素之间有着较强的相关性,也就是其采样值比较的接近。因此,当前 编码像素的值可以由已经编码的几个像素来估计,即预测。 由信息论的知识可知,对具有 K 种取值的符号序列{ }if (i=1,2,3,…),第 N 个符号的信息熵满足下式: ( ) ( ) ( ) Á1 1 2 1log | |K NNNNNNHf Hff Hfff fH− − − ∞≥ ≥ ≥ >  (7-18) 从式 7-18 可知,如果在知道第 N 个符号前面的符号 ()Mf M N< 的情况下,对符号 N 进行预测,则 Mf 知道得越多,N 就越容易进行预测。这也意味着信源的不确定性减小,其信息熵就小。预测的准确程度Visual C++数字图像获取、处理及实践应用 · 386· 取决于信源的概率分布和相关性。一般来说,预测值和实际值不可能完全相同,但是可以尽量的接近实际值。 这就是预测编码的理论依据。 DPCM 系统由编码器和解码器组成,它们各有一个相同的预测器。系统的基本结构如图 7-12 和图 7-13 所示。 编码器 压缩图像 量化器 输入图像 预测器 图 7-12 DPCM 编码器结构 压缩图像 解压图像 预测器 解码器 图 7-13 DPCM 解码器结构 DPCM 预测编码器输出的是当前实际值与信号预测值之间的差值。当输入图像的像素序列 ( )1,2,3,nf n =  逐个进入编码器时,预测器根据若干个过去的输入产生对当前输入像素的预测(估计) 值。预测器的输出舍入成最接近的整数 fn 并被用来计算预测误差 ne : ˆn n ne f f= − (7-19) ne 经过量化器量化后输出 ˆne ,其中量化器的量化误差为 Nq 。经过量化后 ˆne 是真正用来进行编码传输 的信息。 在解码端,解码器根据收到的码字解出 ˆne ,然后利用 ˆne 和解码端的预测器产生的预测值 fn ,进行对编 码端输入图像的重建工作。解码端整个过程可用下式来表示: ˆn n nf e f= + (7-20) 从 DPCM 的编解码过程中可以看出,损失的信息来自量化器环节。如果从图 7-11 中去掉量化器环节, 则有 ne = ˆne , Nq =0。也就是不带量化器的 DPCM 可以完全不失真的编/解码原始信息 nf 。 7.8.2 预测编码的类型 如果某一时刻之前的已知样值和由它们产生的预测值之间呈现某种函数关系,则该函数一般分为线性 和非线性两种。预测编码的编/解码器也根据预测函数的形式分为线性预测器和非线性预测器两种。 若预测值 Nf 和之前用于估计的样本值 1 2 3 1,,, Nf f f f − 之间呈现为: 第 7 章 图像压缩编码 · 387· 1 1 ˆ N N i i i f a f − = = ∑ (7-21) 其中的 1 2 3 1,,, Na a a a − 为常量,则称之为线性预测。 1 2 3 1,,, Na a a a − 被称为预测系数。 在图像数据的压缩中,通常采用下面几种线性的预测方案。 (1)前值预测, 1NNf af −= 。 (2)一维预测,也就是用 Nf 的同一扫描行中的前几个样值对 Nf 进行预测。 (3)二维预测,不但用同一扫描行中样值对 Nf 进行预测,还要用位于 Nf 前一或前几行中的样值对 Nf 进行预测。 如果实际值 Nf 与用来预测的样本值 1 2 3 1,,, Nf f f f − 之间不是如式 7-21 所示的线性组合关系,而是 非线性关系,则称这样的编解码器为非线性的。 7.8.3 预测编码的 Visual C++实现 下面来实现对图像文件进行预测编解码的功能。图 7-14 所示中为 D 表示当前编码的像素值,A、B、C 均为已经编码的相邻像素。 Á C A D B 图 7-14 DPCM 编码像素 对像素的预测方法如下: 如果 D 位于第一行第一列,则认为 D 就是编码值,也就是残差为 0; 如果 D 位于第一行其他列,用像素 A 的值来作为对 D 的预测值; 如果 D 位于其余行第一列,用像素 B 的值来作为对 D 的预测值; 如果 D 位于其他位置,则用(B-C)/2+A 来作为 D 的预测值。 编码值也就是残差均为 D-预测值。由于没有量化这一过程,进行的预测编码是无损编码,经过编码和 解码后的图像没有信息损失。 预测编码后的文件后缀为我们自己定义的一种文件格式“IMG”,它由 3 个部分组成。 1.头文件信息 头文件的格式为: typedef struct{ BYTE bBpp; WORD wLeft; WORD wTop; WORD wRight; Visual C++数字图像获取、处理及实践应用 · 388· WORD wBottom; WORD wXResolution; WORD wYResolution; } IMGHEADER; 数据结构中各个关键字的含义如下。 byCoding:数据压缩方式。目前固定为 1,表示采用 RLC 方法。 bBpp:每个像素所需的位数(指各个色彩平面)。 wLeft:图像相对于屏幕左上角的X坐标(以像素为单位)。 wTop:图像相对于屏幕左上角的Y坐标(以像素为单位)。 wRight:图像相对于屏幕右下角的X坐标(以像素为单位)。 wBottom:图像相对于屏幕右下角的Y坐标(以像素为单位)。 wXResolution:图像的水平分辨率(每英寸有多少个像素)。 wYResolution:图像的垂直分辨率(每英寸有多少个像素)。 2.像素信息 IMG 格式图像的像素值。 3.调色板信息 储存了像素对应的 RGB 颜色值。 函数 WRITE2IMG 实现了将一幅图像进行预测编码,并写成“IMG”格式文件的功能,代码如下。 /************************************************************************* * * 函数名称: * WRITE2IMG() * * 参数: * CDib * pDib - 指向 CDib 对象的指针 * CFile& file - 要保存的文件 * * 返回值: * BOOL - 成功返回 True,否则返回 False * * 说明: * 该函数将指定的图像保存为 IMG 文件 * *************************************************************************/ BOOL WINAPI WRITE2IMG(CDib * pDib, CFile& file) { // 指向源图像的指针 unsigned char* lpSrc; //图像的宽度和高度 LONG lWidth; 第 7 章 图像压缩编码 · 389· LONG lHeight; // 图像每行的字节数 LONG lLineBytes; // 循环变量 LONG i; LONG j; // 参与预测的像素和当前编码的像素 BYTE bCharA; BYTE bCharB; BYTE bCharC; BYTE bCharD; // 预测值 int nTemp; // 预测后的残差 int nDpcm; // 指向编码后图像数据的指针 BYTE * lpDst; //得到图像的宽度和高度 CSize SizeDim; SizeDim = pDib->GetDimensions(); lWidth = SizeDim.cx; lHeight = SizeDim.cy; //得到实际的 Dib 图像存储大小 CSize SizeRealDim; SizeRealDim = pDib->GetDibSaveDim(); // 计算图像每行的字节数 lLineBytes = SizeRealDim.cx; //图像数据的指针 LPBYTE lpDIBBits = pDib->m_lpImage; /********************************************************************** *写入 IMG 文件头信息 *********************************************************************** */ Visual C++数字图像获取、处理及实践应用 · 390· IMGHEADER Header4IMG; // 给文件头赋值 // 像素位数(256 色为 8 位) Header4IMG.bBpp = 8; // 图像相对于屏幕的左上角 X 坐标(以像素为单位) Header4IMG.wLeft = 0; // 图像相对于屏幕的左上角 Y 坐标(以像素为单位) Header4IMG.wTop = 0; // 图像相对于屏幕的右下角 X 坐标(以像素为单位) Header4IMG.wRight = lWidth - 1; // 图像相对于屏幕的右下角 Y 坐标(以像素为单位) Header4IMG.wBottom = lHeight - 1; // 图像的水平分辨率 Header4IMG.wXResolution = lWidth; // 图像的垂直分辨率 Header4IMG.wYResolution = lHeight; // 写入文件头 file.Write((LPSTR)&Header4IMG, sizeof(IMGHEADER)); // 编码第 0 行 i = 0; for ( j = 0; j < lWidth; j++) { // 指向图像 0 行 j 列像素的指针 lpSrc = (BYTE *)lpDIBBits + lLineBytes * (lHeight - 1 - i) +j ; // 给 bCharD 赋值 bCharD = *lpSrc; // 如果是第 0 行 0 列,直接将像素值写入 if(j == 0) nDpcm = (int)bCharD; // 利用 Dpcm =D - A 计算残差 else { 第 7 章 图像压缩编码 · 391· bCharA = *(lpSrc - 1); nDpcm = (int)bCharD - (int)bCharA; } // 将残差写入文件 file.Write(&nDpcm , sizeof(int)); } // 编码第 1 行到 lHeight-1 行 for ( i=1;i 255) nTemp = 255; else nTemp = nTemp; // 得到残差 nDpcm = (int)bCharD - nTemp; } // 将残差写入文件 Visual C++数字图像获取、处理及实践应用 · 392· file.Write(&nDpcm , sizeof(int)); } } // 开辟一片缓冲区以保存调色板 lpDst = new BYTE[769]; // 调色板起始字节 * lpDst = 0x0C; // 得到图像的调色板 LPRGBQUAD lpbmc = (LPRGBQUAD)pDib->m_lpvColorTable; // 读取当前图像的调色板 for (i = 0; i < 256; i ++) { // 读取 DIB 调色板红色分量 lpDst[i * 3 + 1] = lpbmc[i].rgbRed; // 读取 DIB 调色板绿色分量 lpDst[i * 3 + 2] = lpbmc[i].rgbGreen; // 读取 DIB 调色板蓝色分量 lpDst[i * 3 + 3] = lpbmc[i].rgbBlue; } // 写入调色板信息 file.Write((LPSTR)lpDst, 769); // 返回值 return TRUE; } 函数 LOADIMG 用来读取“IMG”文件,代码如下。 /************************************************************************* * * 函数名称: * LOADIMG() * * 参数: * CDib * pDib - 指向 CDib 类的指针 * CFile& file - 要读取的文件 * * 返回值: 第 7 章 图像压缩编码 · 393· * BOOL - 成功返回 TRUE * * 说明: * 该函数将读取指定的 IMG 文件 * *************************************************************************/ BOOL WINAPI LOADIMG(CDib * pDib, CFile& file) { // 图像指针 LPSTR pDIB; // 循环变量 LONG i; LONG j; // 重复像素计数 int iCount; // 图像高度 LONG lHeight; // 图像宽度 LONG lWidth; // 图像每行的字节数 LONG lLineBytes; // 中间变量 BYTE bChar; int nTemp; // 指向源图像像素的指针 int * lpSrc; // 指向编码后图像数据的指针 BYTE * lpDst; // 临时指针 int * lpTemp; // 用来参与预测的 3 个像素和当前编码像素 BYTE bCharA; Visual C++数字图像获取、处理及实践应用 · 394· BYTE bCharB; BYTE bCharC; // IMG 头文件 IMGHEADER Header4IMG; /********************************************************************** *读出 IMG 文件头信息 *********************************************************************** */ // 尝试读取文件头 if (file.Read((LPSTR)&Header4IMG, sizeof(IMGHEADER)) != sizeof(IMGHEADER)) { // 大小不对,返回 NULL。 return NULL; } // 获取图像高度 lHeight = Header4IMG.wBottom - Header4IMG.wTop + 1; // 获取图像宽度 lWidth = Header4IMG.wRight - Header4IMG.wLeft + 1; // 计算图像每行的字节数 lLineBytes = FOURBYTES(lWidth * 8); // 获得显示图像的头文件 LPBITMAPINFOHEADER lpBI=pDib->m_lpBMIH; // 给图像头文件成员赋值 lpBI->biSize = 40; lpBI->biWidth = lWidth; lpBI->biHeight = lHeight; lpBI->biPlanes = 1; lpBI->biBitCount = 8; lpBI->biCompression = BI_RGB; lpBI->biSizeImage = lHeight * lLineBytes; lpBI->biXPelsPerMeter = Header4IMG.wXResolution; lpBI->biYPelsPerMeter = Header4IMG.wYResolution; lpBI->biClrUsed = 0; lpBI->biClrImportant = 0; 第 7 章 图像压缩编码 · 395· // 分配内存以读取编码后的像素 lpSrc = new int[(file.GetLength() - sizeof(IMGHEADER)-769) ]; lpTemp = lpSrc; // 读取编码后的像素 if (file.ReadHuge(lpSrc, file.GetLength() - sizeof(IMGHEADER)-769) != file.GetLength() - sizeof(IMGHEADER)-769 ) { return FALSE; } // 重新分配内存,以适应新的图像大小 delete pDib->m_lpImage; pDib->m_lpImage = new unsigned char[lHeight * lLineBytes]; // CDib 类中像素位置 lpDst = pDib->m_lpImage; // 解码第 0 行 i = 0; for(j = 0; j < lWidth; j++) { if(j==0) { // 如果是 0 行 0 列,编码值就是真实值 lpDst[j + lLineBytes * (lHeight - 1 - i)] = (BYTE)(*lpTemp); lpTemp ++; } else { // 利用 D=A+残差 得到原来的像素 lpDst[j+ lLineBytes * (lHeight - 1 - i)] = (BYTE)(*lpTemp) + lpDst[j + lLineBytes * (lHeight - 1 - i) - 1]; lpTemp++; } } // 解码第 1 行到第 lHeight-1 行 for (i = 1; i < lHeight; i++) { for (j = 0; j < lWidth; j++) { Visual C++数字图像获取、处理及实践应用 · 396· // 得到像素 B 的值 bCharB = lpDst[j + lLineBytes * (lHeight - i)]; // 解码第一列 if(j==0) { // 利用 D=B+残差 得到原来的像素值 lpDst[j+ lLineBytes * (lHeight - 1 - i)] = (BYTE)((*lpTemp) + bCharB); lpTemp++; } // 解码剩下的列 else { // 利用 D=(B-C)/2 + A + 残差 得到原来的像素值 bCharA=lpDst[j - 1 + lLineBytes * (lHeight - 1 - i)]; bCharC=lpDst[j - 1 + lLineBytes * (lHeight - i)]; // 解码时的预测 nTemp=(int)((bCharB - bCharC) / 2 +bCharA); // 预测值小于 0,直接赋 0 if(nTemp<0) nTemp = 0; // 预测值大于 255,直接赋值 255 else if(nTemp>255) nTemp = 255; else nTemp = nTemp; // 预测值+残差 lpDst[j + lLineBytes * (lHeight - 1 - i)] = (BYTE)(*lpTemp + (BYTE)nTemp); lpTemp++; } } } // 释放内存 delete lpSrc; lpDst = NULL; // 读调色板标志位 file.Read(&bChar, 1); 第 7 章 图像压缩编码 · 397· if (bChar != 0x0C) { // 返回 NULL return FALSE; } // 分配内存以读取编码后的像素 lpDst = new BYTE[768]; // 图像中调色板的位置 LPRGBQUAD lpbmc = (LPRGBQUAD)pDib->m_lpvColorTable; // 读取调色板 if (file.Read(lpDst, 768) != 768) { return FALSE; } // 给调色板赋值 for (i = 0; i < 256; i++) { lpbmc[i].rgbRed = lpDst[i * 3 + 2]; lpbmc[i].rgbGreen = lpDst[i * 3 + 1]; lpbmc[i].rgbBlue = lpDst[i * 3]; lpbmc[i].rgbReserved = 0; } // 返回值 return TRUE; } 对菜单中的保存 IMG 文件子菜单添加命令处理函数,代码如下。 void CImageProcessingView::OnCodingWriteimg() { // 对当前图像进行 DPCM 编码(存为 IMG 格式文件) // 获取文档 CImageProcessingDoc * pDoc = GetDocument(); // 获得图像 CDib 类的指针 CDib * pDib = pDoc->m_pDibInit; //图像数据的指针 LPBYTE lpDIBBits = pDib->m_lpImage; Visual C++数字图像获取、处理及实践应用 · 398· // 头文件信息 LPBITMAPINFOHEADER lpBMIH=pDib->m_lpBMIH; // 判断是否是 8-bpp 位图(处理 8-bpp 位图的 DPCM 编码) if (lpBMIH->biBitCount != 8) { // 提示用户 MessageBox("目前只支持 256 色位图的 DPCM 编码!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } // 更改光标形状 BeginWaitCursor(); // 文件保存路径 CString strFilePath; // 获取原始文件名 strFilePath = pDoc->GetPathName(); // 更改后缀为 IMG if (strFilePath.Right(4).CompareNoCase(".BMP") == 0) { strFilePath = strFilePath.Left(strFilePath.GetLength()-3) + "IMG"; } else { strFilePath += ".IMG"; } // 创建 SaveAs 对话框 CFileDialog dlg(FALSE, "IMG", strFilePath, OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT, "IMG 图像文件 (*.IMG) | *.IMG|所有文件 (*.*) | *.*||", NULL); // 提示用户选择保存的路径 if (dlg.DoModal() != IDOK) { // 恢复光标 EndWaitCursor(); 第 7 章 图像压缩编码 · 399· return; } // 获取用户指定的文件路径 strFilePath = dlg.GetPathName(); // CFile 和 CFileException 对象 CFile file; CFileException fe; // 尝试创建指定的 IMG 文件 if (!file.Open(strFilePath, CFile::modeCreate | CFile::modeReadWrite | CFile::shareExclusive, &fe)) { MessageBox("打开指定 IMG 文件时失败!", "系统提示" , MB_ICONINFORMATION | MB_OK); return; } // 调用 WRITE2IMG()函数将当前的 DIB 保存为 IMG 文件 if (::WRITE2IMG(pDib, file)) { MessageBox("成功保存为 IMG 文件!", "系统提示" , MB_ICONINFORMATION | MB_OK); } else { MessageBox("保存为 IMG 文件失败!", "系统提示" , MB_ICONINFORMATION | MB_OK); } // 恢复光标 EndWaitCursor(); } 最后对菜单中的载入 IMG 文件子菜单添加命令处理函数,代码如下。 void CImageProcessingView::OnCodingLoadimg() { // 读入 IMG 文件 // 获取文档 CImageProcessingDoc * pDoc = GetDocument(); Visual C++数字图像获取、处理及实践应用 · 400· // 获得图像 CDib 类的指针 CDib * pDib = pDoc->m_pDibInit; // 文件路径 CString strFilePath; // 创建 Open 对话框 CFileDialog dlg(TRUE, "PCX", NULL, OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT, "IMG 图像文件 (*.PCX) | *.IMG|所有文件 (*.*) | *.*||", NULL); // 提示用户选择保存的路径 if (dlg.DoModal() != IDOK) { // 返回 return; } // 获取用户指定的文件路径 strFilePath = dlg.GetPathName(); // CFile 和 CFileException 对象 CFile file; CFileException fe; // 尝试打开指定的 PCX 文件 if (!file.Open(strFilePath, CFile::modeRead | CFile::shareDenyWrite, &fe)) { // 提示用户 MessageBox("打开指定 PCX 文件时失败!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } // 更改光标形状 BeginWaitCursor(); // 调用 LOADIMG()函数读取指定的 IMG 文件 BOOL Succ = LOADIMG(pDib, file); if (Succ == TRUE) { 第 7 章 图像压缩编码 · 401· // 提示用户 MessageBox("成功读取 IMG 文件!", "系统提示" , MB_ICONINFORMATION | MB_OK); } else { // 提示用户 MessageBox("读取 IMG 文件失败!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 更新视图 pDoc->UpdateAllViews(NULL); // 恢复光标 EndWaitCursor(); } 图 7-15 所示为原始 lena 图和经过编解码的图像的比较,两幅图像完全相同,可见编码是无损的。 编码前 lena 图像 DPCM编解码后 lena 图像 图 7-15 DPCM 编码前后 lena 图像对比 7.9 JPEG 2000 编码 JPEG 是联合图像专家组(Joint Photographic Experts Group)的简称,它是一个由国际标准组织(ISO, International Standardization Organization)和国际电话电报咨询委员会(CCITT,Consultation Committee of the International Telephone and Telegraph)所建立的,从事静态图像压缩标准制定的委员会。它于 20 世纪 80 年 代末 90 年代初制定出了第一套国标静态图像压缩标准 ISO 10918-1,也就是我们所说的 JPEG。由于 JPEG 算法复杂度低,压缩性能较好,使得它在短短的几年内就获得极大的成功,目前网站上 80%的图像都是采用 JPEG 的压缩标准。 JPEG 静止图像压缩标准虽然在中高速率上压缩效果较好,但在低比特速率的情况下,重构图像存在严 重的方块效应,不能很好的适应网络传输图像的需要。尽管 JPEG 标准有 44 种操作模式,但是大部分模式 是针对不同的应用提出的,不具有通用性,这给交换、传输压缩图像带来很大的麻烦。因此,更高压缩率以 及更多新功能的新一代静态图像压缩技术 JPEG 2000 就诞生了。与 JPEG 不同,JPEG 2000 基于小波变换,Visual C++数字图像获取、处理及实践应用 · 402· 采用当前最新的嵌入式编码技术,在获得优于目前 JPEG 标准压缩效果的同时,生成的压缩码流具有强大的 功能,可应用于 Internet、移动通信、传真、医疗、数字图书馆、数字摄象、遥感以及电子商务等方面的图 像压缩。国际标准化组织的 WG1 小组已于 2000 年 8 月制定了最终的国际标准化草案(The Final Draft International Standard,简称 FDIS)。 在这一节中,将介绍 JPEG 2000 的特点、编解码过程以及它的码流格式。编程实现 JPEG 2000 的读写 较为复杂,由于 JPEG 2000 标准的公开性,可以在因特网上找到相应的源程序,限于篇幅这里就不给出源程 序了。 7.9.1 JPEG 2000 概述 JPEG 2000 标准具有新的特征,这些特征对于一些新产品(如数码相机)和应用(如互联网)是非常重 要的。其目标是在一个统一的集成系统中,在不同的应用环境下(如客户/服务器、实时传输、图像库驱动、 有限缓冲和带宽资源等),对不同特征(如自然图像、计算机图形、医疗图像、遥感图像以及复合文本等)、 不同类型(如二值、灰度、彩色或多分量图像)的静止图像进行压缩,在低比特率的情况下,获得比目前标 准更好的率失真性能和主观重构图像质量。它的最主要的特征如下。 1.良好的低比特率压缩性能 这是 JPEG 2000 最主要的特征。从前的 JPEG标准,对于细节分量多的灰度图像,当压缩码率低于 0.25bpp 时,视觉失真大。JPEG 2000 格式的图片压缩比可在 JPEG 的基础上再提高 10%~30%,而且压缩后的图像 显得更加细腻平滑。尤其在低比特压缩码率下,具有良好的率失真性能,以适应窄带网络、移动通信等带宽 有限的应用需要。 2.无损压缩和有损压缩 JPEG 2000 提供无损和有损两种压缩方式。在图像质量要求很高的医学图像、图像库等方面的应用时, 进行无损压缩是必须的。同时 JPEG 2000 提供的是嵌入式码流,允许从有损到无损的渐进解压。 3.累进式传输 现采用 JPEG 压缩的图像下载时是按“块”传输的,因此只能一行一行地显示,而采用 JPEG 2000 格式的 图像支持累进传输(Progressive Transmission)。累进式图像传输允许图像按照所需的分辨率或像素精度进行 编码和重构。用户根据需要,对图像传输进行控制,在获得所需的图像分辨率或质量要求后,在不必接收解 码整个图像的压缩码流情况下,便可终止解码。 4.码流的随机访问和处理 这一特征允许用户在图像中随机地定义感兴趣的区域,使得这一区域的图像质量高于其他图像区域; 码流的随机处理允许用户进行旋转、移动、滤波和特征提取等操作。 5.良好的抗差误性 在码流中提供抗差误性对于通信是必要的。例如在无线传输等误码很高的通信信道中传输图像时,JPEG 2000 系统采取一定的编码措施和码流格式来减小因解码失败造成的图像失真。 6.开放的框架结构 为了在不同的图像类型和应用领域优化编码系统,提供一个开放的框架结构是必须的。在这种开放的 结构中编码器只实现核心的工具算法和码流解析,如果需要解码器可以要求数据源发送未知的工具算法。 第 7 章 图像压缩编码 · 403· Á 7.9.2 JPEG 2000 图像编解码系统 JPEG 2000 标准的核心算法是 EBCOT(Embedded Block Coding with Optimized Truncation),它不仅能 实现对图像的有效压缩,同时产生的码流具有分辨率可伸缩性、信噪比可伸缩性、随机访问和处理等非常好 的特性。而这些特性正是 JPEG 2000 标准所要实现的,所以联合图像专家组才以该算法作为 JPEG 2000 的核 心算法并对 EBCOT 算法做了改进,进一步降低了算法的复杂度。 JPEG 2000 国际标准最终草本 FCD15444 Part1 从 4 个方面描述了 JPEG 2000 图像编码系统:编码器、 解码器、码流格式以及可选的文件格式 JP2。图像数据的编解码过程在编码器和解码器部分分别进行了阐述。 码流格式规定了统一的压缩码流格式,而 JP2 是 JPEG 2000 图像编码系统提供的一种可选的文件格式,用于 存储和交换 JPEG 2000 图像压缩数据。JPEG 2000 图像编码系统总体框图如图 7-16 所示。 小波变换 熵解码解量化小波反变 换 熵编码量化 压缩图像数据 压缩图像数据 存储或传输 源图像数据 重建图像数据 (aǎ (bǎ 图 7-16 JPEG 2000 编码器和解码器结构框图 下面介绍 JPEG 2000 大致编码过程。 1.图像分解和表示 图像可以是多分量的,如通常的彩色图像,可由 YUV3 个分量表示。在空域上,首先将图像样本的各 个分量映射到图 7-17 所示的参考坐标系上。 ÁÂÃÄÁ 图 7-17 图像的表示 其中,“图像区域”的左上角坐标是(XOsiz,YOsiz),右下角坐标是(Xsiz-1,Ysiz-1)。图像在参考坐标系 上的起始点可调,这样在显示图像时,可根据需要,改变图像的起始坐标,调整图像的显示位置。 2.将图像及其分量分块 JPEG 2000 的处理对象不是整幅图像,而是把原图像划分成若干个互相不重叠的矩形数据单元,称为 tile,再对每一个 tile 进行独立的编解码操作。 将图像划分成 tiles,对每个 tile 进行独立的压缩编码,可以减少处理数据所需的内存空间。在并行机制 下,采用多线程编程,可对多个 tiles 同时进行编/解码操作,减小系统运行时间。此外,将图像划分成小的 tiles,在解码时,可以仅对所感兴趣的图像区域进行解码操作,而不需解码整个图像。 tiles 的划分如图 7-18 所示。其中,(XTOSiz,YTOSiz)是 tile 的起始坐标,不一定与参考坐标系原点 或者图像起始点相重合。除了边界上的 tiles 外,其他 tiles 大小均相同。其中 tile 大小是指图像区域与 tile 坐 标系的重合区域。tile 中的每个图像分量称为 tile-component。tile 的起点坐标(XTOSiz,YTOSiz)与大小(XTsiz, YTsiz)作为编码参数存储在压缩码流的信息头中。 Visual C++数字图像获取、处理及实践应用 · 404· 图 7-18 tiles 划分示意图 在对图像划分 tiles 以后,还有一项重要的工作,就是对无符号的图像分量进行 DC 电平位移,其目的 是在解码时,能够从有符号的数值中正确恢复重构图像的无符号样本值。 3.对 tile 实施小波变换 JPEG 2000 与 JPEG 的本质区别在于它采用小波变换进行图像压缩。在图像中,存在着图像边缘这样的 突变点,尽管它们的能量很小,但在很大程度上影响图像的视觉效果。小波变换的一个最大特点就是能够同 时在时域和频域上反映信号的局部特性。图像经过小波变换后,被分解成不同频段的子带。根据人类视觉特 性,对不同频段的小波系数进行粗细不同的量化处理,可达到较好的压缩效果。此外,涌现出许多优秀的基 于小波变换的图像压缩算法,不仅大大提高了压缩性能,而且还增强了压缩码流的功能。 小波变换的原理以及算法可以参看本书图像正交变换的小波变换一节,这里就不再累述了。 4.对分解后的小波系数进行量化并组成矩形的编码块 小波变换后,编码系统对每个 tile-component 中的小波系数进行量化。利用人类视觉系统对图像的分辨 率存在局限性的特点,在不影响图像主观质量的前提下,通过适当的量化减小变换系数的精度,可达到图像 压缩的目的。量化的关键是根据变换后的图像系数特征、重构图像质量要求等因素设计合理的量化步长。 JPEG 2000 图像编解码系统支持无损和有损两种压缩,对于无损压缩,不进行量化,即量化步长为 1; 对于有损压缩,按式 7-22 进行标量量化。量化后的系数用符号和模值表示为: (,)( , ) ( ( , )) b b b b a u vq u v sign a u v  = ⋅ ∆  (7-22) 式 7-22 中 b∆ 是子带 b 的量化步长, )),(( vuasign b 表示小波系数 ),( vuab 的符号, ),( vuab 为该系 数的模值。 在 JPEG 2000 图像编码系统中,每个子带的量化步长与该子带系数模的动态取值范围 bR 有关,用指数 bε 和尾数 bµ 表示为: 112 1 2 ÁÁR b b ε µ−  ∆ = +   (7-23) JPEG 2000 图像编码系统有两种方式存储量化步长。 • 显式量化,把所有子带的量化步长参数 bε 和 bµ 都存储在压缩码流的量化信息标志段中。 第 7 章 图像压缩编码 · 405· • 隐式量化,仅存储最低子带 LL 的 0ε 和 0µ ,其他子带的 bε 和 bµ 通过式 7-24 计算得到: ( ) ( )0 0 0,,b b bnsd nsdε µ ε µ= + − (7-24) 其中,( )00 ,µε 是子带 LL 的量化参数, bnsd 是从原始图像进行小波分解得到子带 b 时所分解的级 数。 5.码块的位平面熵编码 量化后,每个子带被划分成更小的数据单元—码块。按照位平面从高到低的顺序,在每个位平面上, 根据小波系数的上下文,分别在 3 个编码通道上对每个码块的小波系数位进行算术熵编码,得到码块的嵌入 式压缩位流。 图 7-19 所示是将子带划分成码块的示意图。码块的划分起始于坐标原点,大小是 2 的整数次幂。 (ax0,ay0)和(ax1,ay1)分别表示子带 a 的左上角和右下角坐标。对子带区域内所覆盖的码块进行嵌入式码 块编码。 (ax0,ay0) (ax1,ay1) 图 7-19 子带划分成码块示意图 与传统的依次对每个系数进行算术熵编码不同,JPEG 2000 编码系统把码块中的量化系数组织成若干个 位平面。从最高有效位平面(MSB)开始,依次对每个位平面上的所有小波系数位进行算术编码。如果压缩 位流被截取,则码块可能丢失部分或者所有系数的低有效位,这等价于采用较大的量化步长对子带系数进行 量化,压缩码流被截取后,依然能够进行正常的解码,只不过图像的重构质量将会有所下降,码块的嵌入式 压缩位流具有质量可分级性。位平面编码的具体思想可以参照 7.9 节。 在码块的每个位平面上,从码块的左上角系数位开始,从左到右,从上到下,以一列 4 个小波系数位 为单元进行扫描。如图 7-20 所示。先扫描第一列索引号为 0,1,2,3 的系数位,然后扫描第二列索引号为 4,5,6,7 的系数位。在扫描完索引号为 60,61,62,63 的系数位后,开始扫描标号 64,65 的系数位。 采用这种扫描方式可以利用相邻小波系数之间的相关性对小波系数归类,在适当的情况下,使用游程编码提 高压缩效率。 按照图 7-20 所示的扫描顺序,在每个位平面上将小波系数位分别在 3 个编码通道上进行算术熵编码。 位平面上的每个系数位必须在且仅在其中的一个编码通道上进行编码操作。这 3 个编码通道分别是有效性通 道(Significance Pass)、幅度细化通道(Refinement Pass)和清除通道(Cleanup Pass)。在这 3 个编码通道上 分别进行 4 种算术编码操作:有效性编码(Significance Coding)、符号编码(Sign Coding)、幅度细化编码 (Refinement Pass)和清除编码(Cleanup Coding)。在清除通道中,根据适当的条件,进行游程编码,来减 少进行算术编码的二进制符号个数。算术编码的具体思想以及算法可以参照 7.6 节。 Visual C++数字图像获取、处理及实践应用 · 406· 图 7-20 位平面系数扫描方式 每个码块的第一个位平面上,只进行清除通道编码。从第二个位平面开始,依次进行有效通道编码, 幅度细化编码和清除通道编码。 小波系数在哪一个编码通道上进行编码以及算术熵编码所需的上下文,是由其本身和邻域小波系数的 当前状态共同来决定的。在开始进行嵌入式码块编码时,码块中所有小波系数的初始状态为无效状态,用数 字 0 表示。从码块的最高有效位平面开始扫描,当在某一位平面上,小波系数的二进制位第一次由 0 变为 1 时,该小波系数状态便从无效状态转为由数字 1 表示的有效状态。 小波系数的上下文邻域如图 7-21 所示。其中 X 表示待编码的小波系数,其他字母表示 X 的 8 个邻域小 波系数的状态。由于 8 个邻域系数状态具有 256 种组合,因此,待编码的小波系数具有 256 种上下文。 图 7-21 用来形成上下文的 8 领域系数 经过大量的试验,针对编码通道的不同统计特性,JPEG 2000 对 256 种上下文进行了选择与合并,简化 了编码通道所使用的上下文种类,降低了算术编码的复杂度,缩短了编码所需的时间。 将位平面进一步细分为 3 个编码通道进行编码,即把小波系数进行分类,不仅可以利用小波系数之间 的统计特性和相关性,建立合适的上下文,提高编码效率,而且还可以为率失真优化提供更细致的率失真截 断点候选集合,使得率失真优化更加准确。 6.在码流中添加相应的标识(Maker) 标识是构成信息头的基本元素,用来存储编码参数和图像的基本信息,在 7.9.3 一节中将对 JPEG 2000 的标识做具体的介绍。 7. 可选的文件格式 我们可以用可选的文件格式来描述图像和它的各个成分的意义。 相对于编码过程,JPEG 2000 系统的解码过程比较简单。在解码端,首先根据压缩码流中存储的编码参 数以及用户所规定的解码参数,截取适当的压缩码流数据段。截取可根据规定的比特率或者所需的分辨率来 进行。接下来根据码流中存储的量化步长信息,进行反量化,得到小波系数,进行反向小波变换,并根据编 码参数,决定是否需要进行反向分量变换。最后,对无符号的图像分量进行反向 DC 电平位移,得到重构的 图像数据输出。 第 7 章 图像压缩编码 · 407· 7.9.3 JPEG 2000 图像压缩码流格式 JPEG 2000 标准规定了图像的压缩码流格式。图像压缩的数据和编码的参数信息按照规定的格式存放。 在解码器端,对压缩码流进行解析,读取相关的信息进行解码,重构图像。 JPEG 2000 压缩码流中有两种信息头:main header 和 tile-part header。构成信息头的基本元素是标志 (Marker)和标志段(Marker Segment),用来存储编码参数和图像的基本信息。信息头由若干个标志和标志 段组成。标志固定长为 2 字节,第一个字节是 0xFF,第二个字节表示该标志的功能。因为标志是以特殊的 码字 0xFF 开始,因此对压缩码流具有定界的作用。标志段由一个标志和相关的标志参数构成,其结构如图 7-22 所示。 图 7-22 标志段结构示意图 字母 MAR 表示的两个字节是标志,位于标志段的开始。紧接着用字母 Lmar 表示的两个字节存放不包 括 MAR 标志本身的标志段长度,按字节计算。接下来的 Bmar、Cmar、Dmar、Emar1 等表示该标志的相关 参数。标志参数的长度是可变的,并且根据具体规定,同一个标志段中可出现若干个相同功能的标志参数。 JPEG 2000 规定了 6 种标志以及相应的标志段。 (1)限界标志(Delimiting)及其标志段:用于定位头信息和压缩数据的位置。 (2)固定信息标志(Fixed Information)及其标志段:提供图像的一些基本信息,如图像尺寸、分量个 数等。 (3)功能(Functional)标志及其标志段:描述压缩码流的编码参数信息,如压缩码流分层的层数、小 波分解级数、量化步长等。 (4)比特流内置(In Bit Stream)标志及其标志段:与压缩码流的容错性能有关。 (5)指针(Pointer)标志及其标志段:存放指向某一段特定压缩位流的位置信息。 (6)信息(Informational)标志及其标志段:用于存放附加信息,如开发商信息等,不是必须的,是可 选项目。 标志、标志段以及压缩码流必须遵循以下规则。 (1)标志段和头信息的长度必须是整数字节。而且头信息必须从整数字节开始,也就是说位于头信息 之间的位流数据必须用位填充方法填补为整数字节。 (2)在一个 tile-part 头中的所有标志和标志段仅对该 tile 有效。 (3)没有具体规定的 tiles 由 main header 中的所有标志和标志段进行规定。 (4)定界和固定信息标志段在码流中有确定的位置。 (5)标志段应正确描述压缩码流所表示的图像。如果压缩码流被改变,则应及时更新相应的标志段信息。 (6)标志段中参数的排列顺序是从压缩码流头开始,按顺序存放。 (7)0xFF30 至 0xFF3F 之间的标志预留,为以后扩展准备。 JPEG 2000 的压缩码流包含且只包含一个 main header 和至少 1 个 tile-part。压缩码流的结构如图 7-23 所 示。main header 位于 JPEG 2000 压缩码流的开头,紧接其后的是若干个 tile-part。tile-part 中存放的是一个 tile 的编码信息以及相应的压缩数据。一个 tile-part 可以存放 tile 的全部或部分数据。tile-part 由两部分组成:tile-part header 和 tile-part 位流数据。tile-part header 位于每个 tile-part 的起始处。main header 和 tile-part header 的结构 分别如图 7-24 和 7-25 所示,实箭头所指的标志和标志段必须出现在相应的头中,虚箭头所指则不是必须的。Visual C++数字图像获取、处理及实践应用 · 408· 码流起始标志 main 头文件 main header 信息 tile-part 起始标志 tile-part tile-part header 信息 头文件 tile-part 数据开始 标志 tile-part 数据 码流结 束标志 SOC main SOT tile-part1 marker SOD tile-part 1 SOT tile-part2 marker SOD tile-part 2 EOC 图 7-23 JPEG 2000 压缩码流结构示意图 压缩码流起始标志 图像信息标志 编码类型标志 图像分量编码类型标志 量化标志 图像分量的量化标志 感兴趣区域(ROI)标志 包排列顺序 包的头信息 tile-part 长度信息 包长度信息 附加信息 SOC SIZ COD QCD COC QCC RGN POD PPM TLM PLM CME 压缩码流起始标志 图像信息标志 编码类型标志 图像分量编码类型标志 缺省的量化标志 图像分量的量化标志 感兴趣区域(ROI) 标志 包排列顺序 包的头信息 tile-part 长度信息 包的长度信息 附加信息 SOC SIZ COD QCD COC QCC RGN POD PPM TLM PLM CME 图 7-24 main header 结构示意图 图 7-25 tile-part header 结构示意图 在 JPEG 2000 图像解码过程中,从算术熵解码到小波反变换得到重构图像输出的功能模块比较简单, 分别是编码过程中相应模块的逆过程。 JPEG 2000 图像解码系统允许根据规定的解码速率,对压缩码流解码,得到相应质量的重构图像。若解 码速率大于编码速率,则根据实际的编码速率对整个图像压缩码流进行解码;若解码速率小于编码速率,则 仅读取部分压缩码流进行重构。 对于有要求较好的图像质量、较低的比特率或者是一些特殊特性的要求(渐进传输和感兴趣区域编码 等)时,JPEG 2000 将是最好的选择,它所具有的这些特性是以前的静态编码标准所不能达到的。但是在一 些低复杂度的应用中,JPEG 2000 不可能代替 JPEG,因为 JPEG 2000 的算法复杂度不能满足这些领域的要 求。 第第 88 章章 图图像像配配准准 图像配准是对取自不同时间、不同传感器或者不同视角的同一场景的两幅图像或者多幅图像匹配的 过程,它被广泛地应用在遥感图像、医学影像、三维重构、机器人视觉等诸多领域中。 在过去的几十年里,研究者们为各种不同种类的数据和问题开发了很多的技术。总的说来,这些技 术可以按照图像的不同分为以下 3 类: • 第一类: 由于获取图像的不同,造成图像之间的错位。为了配准这些图像,可以采用空域 的方法来去除这些差别。 • 第二类: 由于获取图像的不同而造成的,不过这主要是由于光照或者天气条件不同而造成 的差别。这样的图像通常在亮度上有所区别,也有的会在空域中产生差别,例如透视变形。 • 第三类: 图像的差别是由于目标的运动、生长或者其他景物的变化而引起的。 第二类和第三类图像差别并不是直接通过配准消除的,但这给图像配准带来了巨大的困难。 对于图像配准技术,可以把它看作是以下 4 点选择的组合: • 特征空间 • 搜索空间 • 搜索策略 • 相似度测量 特征空间指的是从图像中提取出来的用来匹配的信息;搜索空间则是指能用来校准图像的图像变换 集;搜索策略决定如何在这个空间中选择下一个变换,如何测试并搜索出最优的变换;相似度测量则决 定了每一个配准测试中的相关特性。 在图像配准中,特征空间、相似度测量、搜索空间以及搜索策略等的选择都会影响到最后配准的结 果。所有的图像配准方法都可以认为是这些选择的组合。这个框架对于我们理解现在存在的各种各样的 配准方法的特点以及其之间的关系是相当有用的。而且,在后面的叙述中我们也将按照这样的结构来讲 述图像配准的技术。 然而,这些内容想在一章中说全是不可能的,因此在本章中,我们将对其中的一些关键内容进行说 明,有些内容如果读者有兴趣的话,可以参考相关的文献。 本章后面的内容是这样安排的,首先简单介绍一些图像配准中所用到的理论基础,在第二节中则简 单的对配准方法进行介绍,最后给出一个利用 Visual C++实现的图像配准示例。 8.1 图像配准理论基础 在这一节中,我们对图像配准中用到的一些图像变换、相似性测度、插值算法以及最小二乘法进行 一些简单的介绍,如果读者对这一部分内容有兴趣的话,可以参看相关的文献。 8.1.1 图像变换 将一幅图像与另一幅图像对准,常需对一幅图像进行一系列的变换,这些变换可分为刚体变换、仿 射变换、投影变换和非线性变换。下面对此进行一一介绍。 1. 刚体变换 如果第一幅图像中的两点间的距离经变换到第二幅图像中后仍保持不变,则这种变换称为刚体变Visual C++数字图像获取、处理及实践应用 · 410· 换。刚体变换可分解为平移、旋转和反转(镜像)。在二维空间中,点 ( ,)x y 经刚体变换到点 ( ’, ’)x y 的 变换公式为: ’ cos sin ’ sin cos x y tx x ty y j j j j 轾轾 轾 轾± 犏犏 犏 犏= + 犏犏 犏 犏 犏臌 臌 臌 臌m (8-1) 其中 j 为旋转角, 轾犏犏犏臌 x y t t 为平移向量。 2. 仿射变换 经过变换后第一幅图像上的直线映射到第二幅图像仍为直线,并且保持平衡关系,这样的变换称为 仿射变换。仿射变换可以分解为线性(矩阵)变换和平移变换。在 2D 空间中,变换公式为: 11 12 21 22 ’ ’ x y ta ax x ta ay y 轾轾轾 轾 犏犏犏 犏= + 犏犏犏 犏 犏臌 臌臌 臌 (8-2) 其中 11 12 21 22 轾犏犏臌 a a a a 为实矩阵。 3. 投影变换 经过变换后第一幅图像上的直线映射到第二幅图像上仍为直线,但平行关系基本不保持,这样的变 换称为投影变换。投影变换可用高维空间上的线性(矩阵)变换来表示。变换公式为: 11 12 13 21 22 23 ’ ’ 1 xa a ax ya a ay 轾犏轾轾 犏犏犏 = 犏犏犏臌 臌 犏犏臌 (8-3) 4. 非线性变换 非线性变换可把直线变换为曲线。在 2D 空间中,可以用以下公式表示: ( ’, ’) ( , )x y F x y= 其中,F 表示把第一幅图像映射到第二幅图像上的任意一种函数形式。典型的非线性变换如多项式变换, 在 2D 空间中,多项式函数可写成如下形式: 2 2 00 10 01 20 11 02 2 2 00 10 01 20 11 02 ’ ’ x a axayax axyay y b bxbybx bxyby = + + + + + + = + + + + + +   (8-4) 非线性变换比较适用于那些具有全局性形变的图像配准问题,以及整体近似刚体但局部有形变的配 准情况。 8.1.2 相似性测度 相似性测度主要有互相关相似性测度、序贯相似性测度、相位相关测度以及其一些变形等。这些相 似性测度的更详细的介绍将在后面的章节中给出,在这里,由于篇幅的限制,只能进行简要的说明。 Á第 8 章 图像配准 · 411· Á 1. 互相关相似性测度 最基本的相似性测度就是互相关相似性测度了。假设有模板 T 和搜索图 S, jiS , 表示模板覆盖下的 那块搜索图,其中 i、j 表示位置。则其互相关相似性测度为: , 1 1 , 2 1 1 [ ( , ) ( , )] (,) [ ( , )] MM i j m n MM i j m n S m n T m n R i j S m n = = = = × = ∑∑ ∑∑ (8-5) 或者归一化为: , 1 1 , 2 2 1 1 1 1 [ ( , ) ( , )] (,) [ ( , )] [ ( , )] MM i j m n MMMM i j m n m n S m n T m n R i j S m n T m n = = = = = = × = ∑∑ ∑∑ ∑∑ (8-6) 2. 基于傅立叶分析的测度 基于傅立叶分析的测度主要有两种,一个是将空域中的互相关在频域中进行计算,另一个是相位相 关法。 由傅立叶分析中的相关定理可知,两个函数在定义域中的卷积等于它们在频域中的乘积,而相关则 是卷积的一种特定形式。因此,可以利用快速傅立叶变换来求解相似度。 对 T 和 S 分别求 DFT,可得相关的离散傅立叶变换 ),( vuφ 为: *(,)(,)(,)u v X u v Y u vφ = (8-7) 其中 X 和 Y 分别是 T 和 S 的傅立叶变换。其中*为共扼运算的符号。 对 ),( vuφ 求傅立叶反变换,就可以得出空间域中的相关函数 ),( kjφ 为: 1 1 * 0 0 (,) [(,)* (,)] MM uj vk MM u v j k X u v Y u vφ ω ω − − = = = ∑ ∑ (8-8) 相位相关法可以用力计算两幅图像的平移。图像的傅立叶变换由实部和虚部组成,即: (,)(,)(,)F u v R u v iI u v= + (8-9) 其中 i 为虚部算子。这也可以表示为: (,)( , ) | ( , ) | ei u vF u v F u v φ= (8-10) 则: 1(,) tan [(,)/ (,)]u v I u v R u vφ −= (8-11) 相位相关则是基于傅立叶变换的这个性质来建立的。两幅图像的平移可以认为是其傅立叶变换的角 度差别 ÁÂ( ( , ) ( , ))ei u v u vφ φ− 。这样计算两幅图像的互功率谱就可以得到两幅图像的角度差别: Visual C++数字图像获取、处理及实践应用 · 412· * 1 2 * 1 2 (,)(,) e| ( , ) ( , ) | ÁÂud vdF u v F u v F u v F u v += (8-12) 其中 Ád 和 Ád 分别表示 X 和 Y 上的平移量。 3. 序贯相似性检测 序贯相似性检测算法的算法实际上基于互相关法,是为了快速计算而设计的算法。其基本思路就是 设计一个阈值,在计算相似性的时候,累计误差大于阈值则停止计算,这样可以减少在误匹配点上的计 算量。具体的算法可以参见后面的章节。 在序贯相似性检测算法的基础上,也有不少的变形,例如基于排序的序贯相似性检测算法,分层的 序贯相似性检测算法等。 8.1.3 插值 在图像配准中,还需要涉及到许多的插值算法。由于粗配准后所得出的像素坐标位置可能不在整数 像素上,因此需要用灰度插值的方法对像素值进行估计。灰度插值的方法建立在信号抽样理论基础上。 现实世界中的图像可以看作是一个连续函数 ),( yxfc ,数字图像是对这个函数的采样,恢复连续函数可 以用一个理想低通滤波器对离散化的图像进行滤波,在空域上就表现为与 Sinc 函数的卷积。但是 Sinc 函数计算比较复杂,一般用比较容易计算的函数来逼近,常用的方法包括最近邻法、双线性法,但它们 的精度对于细节比较丰富的图像来说还不够。在本文中,采用 3 次插值的方法用当前像素周围的 4×4 个像素进行插值,用如下所示的核函数对图像进行卷积操作: ( 2) 1 ( 1)Cubic( ) (( 5) 8.0) 4.0 (1 2) X XX Xx XXXX − + ≤=  − + − + < ≤ (8-13) 图 8-1 是 Cubic 函数和 Sinc 函数的图像。从图中可以看出 Cubic 函数是 Sinc 函数的 3 次良好逼近。 图 8-1 Cubic 函数对 Sinc 函数的逼近 8.1.4 最小二乘法 最小二乘法(LSM)是一种有效的统计算法,它可以用多个配准点找出最优参数解。设对应匹配点 的坐标分别为 ),( ii YX 和 ),( ii YX′′ 。由仿射模型可得如下方程组: 第 8 章 图像配准 · 413· AW X BW Y ′=  ′= (8-14) 配准需要解决的问题就是求出线性最小二乘解 A 和 B,其中 A 和 B 为仿射变换参数,以 6 参数的 仿射变换为例 ( ) ( )1 2 3 1 2 3, A a a a B b b b= = 1 2 1 2 ... ... 1 1 ... 1 M M XXX WYYY    =     , ( ) ( )1 2 1 2... , ...MMXXXXYYYY′′′′′′= = 。可以 证明,只要 W 的秩大于 6,则存在 A 和 B 的最小二乘解; -1 -1 () () TT TT A X W WW B Y W WW ′= ′= (8-15) 8.2 图像配准中常用的技术 介绍了上面的一些数学理论基础后,下面简单介绍一下在图像配准中的技术。 图像配准涉及到的技术比较多,由于篇幅限制,特别是现在的图像配准技术也在不断的前进,不可 能都涉及到。总的说来,配准的方法主要有互相关法、傅立叶变换法、点映射法和弹性模型法。前两种 方法已经在前面的理论基础中进行了简单的介绍,下面,首先介绍一下点映射,然后介绍基于弹性模型 的匹配。介绍了这两个基本的方法后(确切的说,这是理论基础),再回过头叙述一下配准中需要注意的 问题,包括特征空间的选择,相似性的选择以及搜索空间和策略的选择。 8.2.1 点映射 点映射或者是基准点映射技术是在不知道两幅图像的映射方式时最常采用的配准方法。例如,如果 图像是在同一个场景中从不同的角度按照平滑的景深变化拍摄到的,则这两幅图像会由于透视变形而有 所区别。我们不能确定正确的透视变换,因为通常并不知道在场景中真实的景深。但我们能在两幅图像 中找到基准点,并利用一个普通的变换来进行配准。然而,如果场景并不是由光滑的表面组成,而是有 很大的景深变化,这样两幅图像之间就会产生变形以及遮挡等问题,物体将出现在图像的不同位置,而 且一些变形也会是局部的。当更多的变形是局部的,这就给全局的点映射方法图像配准带来了很大的困 难。在这个情况下,利用局部的变换,例如局部点映射方法将会比较合适。 点映射方法通常由 3 个步骤组成。 第一步:计算图像中的特征。 第二步:在参考图像上找到特征点,也就是经常说的控制点,并在待配准图像中找到相对应的特征 点。 第三步:利用这些匹配的特征点计算出空间映射参数,这个空间通常是 2D 的多项式函数。通常, 还需要在一幅图像中进行重采样,对另一幅图像应用空间映射和插值。 点映射方法经常不能稳定地得到配准,因此,点映射方法还经常利用各个阶段之间地反馈来找到最 优地变换。 前面提到的控制点在图像配准中起到了重要的作用。在点匹配后,点映射的方法就只剩下插值或者Visual C++数字图像获取、处理及实践应用 · 414· 逼近了。这样,点匹配的精度就确定了最后配准的精度。 很多特征都可以用来作为控制点,我们可以将它们分为内在的和外部的特征点。内在的控制点指的 是图像中不依赖于图像数据本身的一些点,它们通常是为了配准的目的放入场景中的标记点,并且很容 易进行识别。例如在医学图像中,就经常往患者的皮肤或者其他不会产生变形和移位的位置上放置一些 特征点,以便进行配准。外在的控制点指的是那些从数据中得到点。这些点可以是手工的得到的、也可 以是自动获取的。手工的控制点也就是利用人的交互来得到的点,例如一些可鉴别的基准点或者解剖结 构。这些点一般都是选取为刚性、稳定并且在数据中很容易点击得到等。当然,这需要由在其领域的专 门知识。还有许多应用自动定位控制点。用来自动定位的控制点典型的有角点、直线的交点、曲线中的 局部最大曲率点、具有局部最大曲率的窗口的中心、闭合区域的重心等等。这些特征通常都是在配准的 两幅图像中惟一的,并且对于局部的变形表现更鲁棒一些。 得到控制点后,就可以对这些控制点进行匹配了。另一幅图像中匹配的控制点可以用手工点击得到, 也可以利用互相关等方法自动获取。 对于具有反馈的点映射方法,在这里就不介绍了,有兴趣的读者可以参考有关的文献。 8.2.2 基于弹性模型的匹配 在很多图像配准中,没有直接应用插值来计算控制点之间的映射,而是采用了基于弹性模型的方法 来进行。这些方法对图像中的变形利用了一个弹性的变形来模型化。从另一个角度来说,配准变换就是 一个弹性的材料经过最小的弯曲和拉伸变换的结果。弯曲和拉伸的量由弹性材料的能量状态来表示。然 而,插值的方法也和此类似,因为能量的最小化需要满足弹性模型的限制,而这可以用样条来解决。 通常,这些方法是逼近图像之间的匹配,尽管它们有时也利用特征,但他们没有包括点匹配这个步 骤。图像或者物体是模型化为一个弹性的整体,并且两幅图像之间点或者特征点的相似性是用整体的“拉 伸”的外部的力来表示的。这些方法通常利用硬度或者是光滑度的约束上给了用户很多的灵活性。最后 确定的最小能量的状态将决定定义配准的变形变换。但其问题在于最小能量状态的求取上通常包括迭代 的计算过程。 8.2.3 特征空间的选择 配准的过程中,第一步就是特征空间的选择。有许多的特征可供选择,这可能就是图像本身的亮度, 同时也有许多其他类似的选择,这包括边沿、曲线、表面。显著的特征也可以用来配准,例如角点、直 线交点,高曲率的点。也可以是统计特征,例如不变矩、重心等。高层的结果和语义描述也可以用来配 准。 显著的特征通常是指在图像中可以容易辨别处理的有意义特征的一些特殊的像素。统计特征指的是 一个区域(这个区域通常是通过一个分割的预处理得到)的测度,它表示了这个区域的估计。特征空间 是图像配准,同时是几乎所有的高层图像处理或者计算机视觉的基础。 对于图像配准,特征空间的选择将影响: • 传感器和场景中的数据什么特征的敏感的(通常,特征是选择那些减少传感器噪声和其他变形, 例如亮度的变换等); • 图像的什么特征将会匹配(例如在匹配结构比纹理特征更有利); • 计算代价,通过减少计算相似性,或者从另一个角度来说,增加预计算的必要性的代价。 8.2.4 相似性测度的选择 图像配准的第二步就是设计或者选择相似性测度。这个阶段和匹配特征的选择有很大关系。对于内 部结构,也就是说图像的不变特征,通常是通过特征空间和通过相似性测度提取得到的。典型的相似性第 8 章 图像配准 · 415· 测度有:经过或者不经过预先滤波的互相关(例如,匹配滤波或者统计相关)、差别的绝对值(这是很高 效的方法)、傅立叶不变特征(例如相位相关)。利用曲线和表面作为特征空间的方法则需要在最近邻域 差别的平方和。结构或者语义的方法则需要高度依赖于其特征的测度。例如,“自由”图形之间的熵的最 小变化就用来最为一个结构模式识别的相似性测度。 相似性测度的选择是一个图像配准中最重要的步骤之一,它将决定如何确定配准变换。而且,其匹 配的程度最后应转化为匹配或者不匹配。 8.2.5 搜索空间和策略的选择 由于很多的匹配特征和相似度测量方法都需要大量的计算量,因此图像配准中最后一步设计就是一 个选择搜索策略的问题。记住,搜索空间通常是将要找到配准的最优变换的变换集。我们可在特征空间 上利用相似性测度计算每一个变换。然而,在很多情况下,例如利用相关作为相似性测度的方法。减少 测量计算的数量是很重要的。误匹配位置越多,计算量越大,这个问题就越严重。在许多情况中,搜索 空间就是所有变换的空间。通常,可以将这个集合从其影响的大小和搜索空间的复杂度分为全局或者局 部的变换。在有些情况下,可以利用一些可得到的信息去掉不可能匹配的搜索子空间,从而达到减少计 算量的目的。 通常的搜索策略包括分层或多分辨率技术、判决序列、松弛、广义 Hough 变换、线性规划、树和图 像匹配、动态规划以及启发式的搜索等等。 8.3 Visual C++编程实现图像配准 上面讲述了图像配准的基本理论,下面利用 Visual C++来实现图像的配准。我们将给出一个半自动 的基于特征的图像配准程序。在程序中,需要手工选取特征点,程序将自动寻找到相匹配的特征点,然 后自动计算仿射变换参数,并将两幅图像进行拼接,合成一幅图像。 由于图像配准中包括基准图像和待配准图像,因此有必要添加一个对话框来完成图像配准的设置和 相关的图像配准操作。 向工程中添加名为 IDD_DLG_REG 的对话框,并在对话框中添加“打开配准图像”、“选取特征点” 和“图像配准”等控件。为此对话框创建新的类 CDlgReg,其具体实现代码如下所示。 (1)对话框头文件(DlgReg.h) #if !defined(AFX_DlgReg_H__096C39D7_72FA_4ABD_90AC_688669D1692C__INCLUDED_) #define AFX_DlgReg_H__096C39D7_72FA_4ABD_90AC_688669D1692C__INCLUDED_ #include "ImageProcessingDoc.h" #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 // DlgReg.h : header file // ///////////////////////////////////////////////////////////////////////////// // CDlgReg dialog Visual C++数字图像获取、处理及实践应用 · 416· class CDlgReg : public CDialog { // Construction public: CImageProcessingDoc* m_pDoc; CDlgReg(CWnd* pParent = NULL, CImageProcessingDoc* pDoc = NULL); // standard constructor // Dialog Data //{{AFX_DATA(CDlgReg) enum { IDD = IDD_DLG_REG }; // NOTE: the ClassWizard will add data members here //}}AFX_DATA // Overrides // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(CDlgReg) protected: virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support //}}AFX_VIRTUAL // Implementation protected: // 基准图像数据指针,用于图像拼接 LPBYTE m_lpBaseImg; // 待配准图像数据指针,用于图像拼接 LPBYTE m_lpSampImg; // 计算图像位置的标志位。FALSE 表示还没有计算图像位置 BOOL m_bCalImgLoc; // 选取特征点标志位。FALSE 表示还没有选取 BOOL m_bChoseFeature; // 设置图像等控件的位置大小 void CalImageLocation(); // 基准图像 CDib* m_pDibInit; // 待配准图像 第 8 章 图像配准 · 417· CDib* m_pDibSamp; // 配准后的图像 CDib* m_pDibResult; // 基准图像显示区域 CRect m_rectInitImage; // 待配准图像显示区域 CRect m_rectResltImage; // 待配准特征点位置 CPoint m_pPointSampl[3]; // 配准的特征点位置 CPoint m_pPointBase[3]; // 寻找配准点 CPoint FindMatchPoint(CDib* pDibBase, CDib* pDibSamp, CPoint pointSamp); // 计算相似度 double CalCorrelation(unsigned char* pBase, unsigned char* pSamp, int nBlockLen); // 画出特征点 void DrawFeature(CDC* pDC); // 获得仿射变换系数 void GetAffinePara(CPoint* pPointBase, CPoint* pPointSampl, double* pDbBs2SpAffPara); // 计算两个矩阵相乘 void CalMatProduct(double* pDbSrc1, double *pDbSrc2, double *pDbDest, int nX, int nY, int nZ); // 计算矩阵的逆 BOOL CalInvMatrix(double* pDbSrc, int nLen); // 获得待配准图像仿射变换后的区域 CRect GetAftAffDim(double* pDbAffPara); // 计算仿射变换后的待配准图像 LPBYTE SetSampImgAftAff(double* pDbAffPara, CRect rectNewImg); // 计算 3 次插值 unsigned char CalSpline(unsigned char *pUnchCorr, double x, double y); Visual C++数字图像获取、处理及实践应用 · 418· // 计算仿射变换后的基准图像 LPBYTE SetBaseImgAftAff(CRect rectNewImg); // Generated message map functions //{{AFX_MSG(CDlgReg) afx_msg void OnPaint(); afx_msg void OnRegOpen(); afx_msg void OnRegReg(); afx_msg void OnRegChoseFeature(); afx_msg void OnLButtonUp(UINT nFlags, CPoint point); afx_msg void OnMouseMove(UINT nFlags, CPoint point); //}}AFX_MSG DECLARE_MESSAGE_MAP() private: int m_nChsFeatureNum; // 选择特征点的数目 }; //{{AFX_INSERT_LOCATION}} // Microsoft Visual C++ will insert additional declarations immediately before the previous line. #endif // !defined(AFX_DlgReg_H__096C39D7_72FA_4ABD_90AC_688669D1692C__INCLUDED_) (2)对话框文件(DlgReg.cpp) // DlgReg.cpp : implementation file // #include "stdafx.h" #include "ImageProcessing.h" #include "ImageProcessingDoc.h" #include "GlobalApi.h" #include "DlgReg.h" #include "DlgAftReg.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif ///////////////////////////////////////////////////////////////////////////// // CDlgReg dialog CDlgReg::CDlgReg(CWnd* pParent /*=NULL*/, CImageProcessingDoc* pDoc) 第 8 章 图像配准 · 419· : CDialog(CDlgReg::IDD, pParent) { //{{AFX_DATA_INIT(CDlgReg) // NOTE: the ClassWizard will add member initialization here //}}AFX_DATA_INIT // 获取文档类指针 m_pDoc = pDoc; // 设置计算图像位置标志位 FALSE m_bCalImgLoc = FALSE; // 设置基准图像为原始打开的图像 m_pDibInit = pDoc->m_pDibInit; // 设置待配准 m_pDibSamp = new CDib; // 设置选取特征点的数目初始值 m_nChsFeatureNum = 0; // 设置选取特征点的标志位为 FALSE m_bChoseFeature = FALSE; } void CDlgReg::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); //{{AFX_DATA_MAP(CDlgReg) // NOTE: the ClassWizard will add DDX and DDV calls here //}}AFX_DATA_MAP } BEGIN_MESSAGE_MAP(CDlgReg, CDialog) //{{AFX_MSG_MAP(CDlgReg) ON_WM_PAINT() ON_BN_CLICKED(IDC_REG_OPEN, OnRegOpen) ON_BN_CLICKED(IDC_REG_REG, OnRegReg) ON_BN_CLICKED(IDC_REG_CHOSE_FEATURE, OnRegChoseFeature) ON_WM_LBUTTONUP() ON_WM_MOUSEMOVE() //}}AFX_MSG_MAP END_MESSAGE_MAP() ///////////////////////////////////////////////////////////////////////////// Visual C++数字图像获取、处理及实践应用 · 420· // CDlgReg message handlers void CDlgReg::OnPaint() { CPaintDC dc(this); // device context for painting // 如果还没有计算图像的位置,则进行计算 if(!m_bCalImgLoc){ CalImageLocation(); } // 显示的区域 CSize sizeDisplay; // 显示的位置 CPoint pointDisplay; // 显示基准图像 if(!m_pDibInit->IsEmpty()){ sizeDisplay.cx=m_pDibInit->m_lpBMIH->biWidth; sizeDisplay.cy=m_pDibInit->m_lpBMIH->biHeight; pointDisplay.x = m_rectInitImage.left; pointDisplay.y = m_rectInitImage.top; m_pDibInit->Draw(&dc,pointDisplay,sizeDisplay); } // 显示待配准图像 if(!m_pDibSamp->IsEmpty()){ sizeDisplay.cx=m_pDibSamp->m_lpBMIH->biWidth; sizeDisplay.cy=m_pDibSamp->m_lpBMIH->biHeight; pointDisplay.x = m_rectResltImage.left; pointDisplay.y = m_rectResltImage.top; m_pDibSamp->Draw(&dc,pointDisplay,sizeDisplay); } // 显示特征点与配准的特征点 DrawFeature(&dc); } /************************************************************************* * * \函数名称: * CalImageLocation() * 第 8 章 图像配准 · 421· * \输入参数: * 无 * * \返回值: * 无 * * \说明: * 该函数设置对话框中的控件位置和大小,并设置显示图像的位置。默认的图像大小为 352 * 288,如果图像小于此大小,则控件大小设置为 352×288,并将图像放置在控件中间 * ************************************************************************* */ void CDlgReg::CalImageLocation() { // 获得控件 IDC_REG_INIT_IMAGE 的句柄,并获得控件的初始位置信息 CWnd* pWnd=GetDlgItem(IDC_REG_INIT_IMAGE); WINDOWPLACEMENT *winPlacement; winPlacement=new WINDOWPLACEMENT; pWnd->GetWindowPlacement(winPlacement); // 图像宽度 int nImageWidth; nImageWidth = m_pDibInit->m_lpBMIH->biWidth; // 图像高度 int nImageHeight; nImageHeight = m_pDibInit->m_lpBMIH->biHeight; // 调整控件 IDC_REG_INIT_IMAGE 的大小位置,并同时设置显示基准图像的位置 if(nImageHeight > 352){ winPlacement->rcNormalPosition.bottom = winPlacement->rcNormalPosition.top + nImageHeight; m_rectInitImage.bottom = winPlacement->rcNormalPosition.bottom; m_rectInitImage.top = winPlacement->rcNormalPosition.top; } else{ winPlacement->rcNormalPosition.bottom = winPlacement->rcNormalPosition.top + 352; m_rectInitImage.bottom = winPlacement->rcNormalPosition.top + 176 + nImageHeight/2; m_rectInitImage.top = winPlacement->rcNormalPosition.top + 176 - nImageHeight/2; } if(nImageWidth > 288){ winPlacement->rcNormalPosition.right = winPlacement->rcNormalPosition.left + nImageWidth; Visual C++数字图像获取、处理及实践应用 · 422· m_rectInitImage.right = winPlacement->rcNormalPosition.right; m_rectInitImage.left = winPlacement->rcNormalPosition.left; } else{ winPlacement->rcNormalPosition.right = winPlacement->rcNormalPosition.left + 288; m_rectInitImage.right = winPlacement->rcNormalPosition.left + 144 + nImageWidth/2; m_rectInitImage.left = winPlacement->rcNormalPosition.left + 144 - nImageWidth/2; } // 设置 IDC_REG_INIT_IMAGE 控件的大小位置 pWnd->SetWindowPlacement(winPlacement); // 获得显示基准图像控件的右边位置,以便确认显示待配准图像控件的位置 int nIniImgRight; nIniImgRight = winPlacement->rcNormalPosition.right; int nIniImgLeft; nIniImgLeft = winPlacement->rcNormalPosition.left; // 获得 IDC_REG_INIT_IMAGE 控件的下边位置,以便调整其他控件的位置 int nIniImgBottom; nIniImgBottom = winPlacement->rcNormalPosition.bottom; // 获得控件 IDC_REG_RESLT_IMAGE 的句柄,并获得初始位置信息 pWnd=GetDlgItem(IDC_REG_RESLT_IMAGE); pWnd->GetWindowPlacement(winPlacement); // 如果还未打开待配准图像,则设置待配准图像大小和基准图像大小相等 if(!m_pDibSamp->IsEmpty()){ nImageWidth = m_pDibSamp->m_lpBMIH->biWidth; nImageHeight = m_pDibSamp->m_lpBMIH->biHeight; } // 调整控件 IDC_REG_RESLT_IMAGE 的大小位置,并同时设置显示待配准图像的位置 // 先调整控件的左边位置,和 IDC_REG_INIT_IMAGE 控件相隔 15 个像素 winPlacement->rcNormalPosition.left = nIniImgRight + 15; if(nImageHeight > 352){ winPlacement->rcNormalPosition.bottom = winPlacement->rcNormalPosition.top + nImageHeight; m_rectResltImage.bottom = winPlacement->rcNormalPosition.bottom; m_rectResltImage.top = winPlacement->rcNormalPosition.top; } 第 8 章 图像配准 · 423· else{ winPlacement->rcNormalPosition.bottom = winPlacement->rcNormalPosition.top + 352; m_rectResltImage.bottom = winPlacement->rcNormalPosition.top + 176 + nImageHeight/2; m_rectResltImage.top = winPlacement->rcNormalPosition.top + 176 - nImageHeight/2; } if(nImageWidth > 288){ winPlacement->rcNormalPosition.right = winPlacement->rcNormalPosition.left + nImageWidth; m_rectResltImage.right = winPlacement->rcNormalPosition.right; m_rectResltImage.left = winPlacement->rcNormalPosition.left; } else{ winPlacement->rcNormalPosition.right = winPlacement->rcNormalPosition.left + 288; m_rectResltImage.right = winPlacement->rcNormalPosition.left + 144 + nImageWidth/2; m_rectResltImage.left = winPlacement->rcNormalPosition.left + 144 - nImageWidth/2; } // 设置 IDC_REG_RESLT_IMAGE 控件的大小位置 pWnd->SetWindowPlacement(winPlacement); if(nIniImgBottom < winPlacement->rcNormalPosition.bottom) nIniImgBottom = winPlacement->rcNormalPosition.bottom; nIniImgBottom = winPlacement->rcNormalPosition.bottom; nIniImgRight = winPlacement->rcNormalPosition.right; // 设置控件 IDOK 的位置大小 pWnd=GetDlgItem(IDOK); pWnd->GetWindowPlacement(winPlacement); winPlacement->rcNormalPosition.top = nIniImgBottom +15; winPlacement->rcNormalPosition.bottom = nIniImgBottom + 60; pWnd->SetWindowPlacement(winPlacement); // 设置控件 IDCANCEL 的位置大小 pWnd=GetDlgItem(IDCANCEL); pWnd->GetWindowPlacement(winPlacement); winPlacement->rcNormalPosition.top = nIniImgBottom +15; winPlacement->rcNormalPosition.bottom = nIniImgBottom + 60; pWnd->SetWindowPlacement(winPlacement); Visual C++数字图像获取、处理及实践应用 · 424· // 设置控件 IDC_REG_OPEN 的位置大小 pWnd=GetDlgItem(IDC_REG_OPEN); pWnd->GetWindowPlacement(winPlacement); winPlacement->rcNormalPosition.top = nIniImgBottom +15; winPlacement->rcNormalPosition.bottom = nIniImgBottom + 60; pWnd->SetWindowPlacement(winPlacement); // 设置控件 IDC_REG_REG 的位置大小 pWnd=GetDlgItem(IDC_REG_REG); pWnd->GetWindowPlacement(winPlacement); winPlacement->rcNormalPosition.top = nIniImgBottom +15; winPlacement->rcNormalPosition.bottom = nIniImgBottom + 60; pWnd->SetWindowPlacement(winPlacement); // 设置控件 IDC_REG_CHOSE_FEATUR 的位置大小 pWnd=GetDlgItem(IDC_REG_CHOSE_FEATURE); pWnd->GetWindowPlacement(winPlacement); winPlacement->rcNormalPosition.top = nIniImgBottom +15; winPlacement->rcNormalPosition.bottom = nIniImgBottom + 60; pWnd->SetWindowPlacement(winPlacement); // 调整此对话框的大小 //pWnd = GetDlgItem(IDD_DLG_REG); this->GetWindowPlacement(winPlacement); //winPlacement->rcNormalPosition.top = nIniImgBottom +15; winPlacement->rcNormalPosition.bottom = nIniImgBottom + 300; winPlacement->rcNormalPosition.left = nIniImgLeft - 20; winPlacement->rcNormalPosition.right = nIniImgRight + 20; this->SetWindowPlacement(winPlacement); // 释放已分配内存 delete winPlacement; // 设置计算图像控件位置标志位为 TRUE m_bCalImgLoc = TRUE; } /************************************************************************* * * \函数名称: * DrawFeature() * * \输入参数: * 无 第 8 章 图像配准 · 425· * * \返回值: * 无 * * \说明: * 该函数根据类的成员变量确定特征点的数目和位置,并在图像中进行显示 * ************************************************************************* */ void CDlgReg::DrawFeature(CDC* pDC) { // 循环变量 int i; // 临时变量 CPoint pointTemp; // 半径 int nRadius; nRadius = 5; // 设置画图类型 pDC->SelectStockObject(HOLLOW_BRUSH); // 声明画笔 CPen penWhite(PS_SOLID,1,RGB(255,255,255)); CPen *pOldPen; // 将画笔选入,并保存以前的画笔 pOldPen = pDC->SelectObject(&penWhite); for(i=0; iEllipse(rectSamp); // 再显示配准特征点 Visual C++数字图像获取、处理及实践应用 · 426· // 确定此点的显示位置 pointTemp.x = m_pPointBase[i].x + m_rectInitImage.left; pointTemp.y = m_pPointBase[i].y + m_rectInitImage.top ; // 画出此特征点,其中园的半径为 nRadius CRect rectBase(pointTemp.x-nRadius , pointTemp.y-nRadius , pointTemp.x+nRadius , pointTemp.y+nRadius); pDC->Ellipse(rectBase); } // 回复以前的画笔 pDC->SelectObject(pOldPen); penWhite.DeleteObject(); } /************************************************************************* * * \函数名称: * OnRegOpen() * * \输入参数: * 无 * * \返回值: * 无 * * \说明: * 该函数打开待配准图像,并将图像存放在 m_pDibSamp 中 * ************************************************************************* */ void CDlgReg::OnRegOpen() { // TODO: Add your control notification handler code here // 文件操作对话框 CFileDialog dlg(TRUE,"bmp","*.bmp"); // 打开文件 if(dlg.DoModal() == IDOK) { // 文件指针 CFile file; // 打开图像的路径名 第 8 章 图像配准 · 427· CString strPathName; // 获取图像文件路径名 strPathName = dlg.GetPathName(); // 打开文件 if( !file.Open(strPathName, CFile::modeRead | CFile::shareDenyWrite)) { // 返回 return ; } // 读入模板图像 if(!m_pDibSamp->Read(&file)){ // 恢复光标形状 EndWaitCursor(); // 清空已分配内存 m_pDibSamp->Empty(); // 返回 return; } } // 判断是否是 8-bpp 位图(这里为了方便,只处理 8-bpp 位图的模板配准,其他的可以类推) if(m_pDibSamp->m_nColorTableEntries != 256) { // 提示用户 MessageBox("目前只支持 256 色位图!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 清空已分配内存 m_pDibSamp->Empty(); // 返回 return; } // 如果打开新的待配准文件,将图像位置设置标志位设为 FALSE,以便再次调整位置 m_bCalImgLoc = FALSE; // 更新显示 this->Invalidate(); Visual C++数字图像获取、处理及实践应用 · 428· } /************************************************************************* * * \函数名称: * OnRegChoseFeatureOnRegReg() * * \输入参数: * 无 * * \返回值: * 无 * * \说明: * 该函数设置选取特征点标志位,然后调用函数在待配准图像中选取特征点,并 * 配准这些特征点。特征点的数目至少应该选取 3 个 * ************************************************************************* */ void CDlgReg::OnRegChoseFeature() { // 如果待配准图像尚未打开,则不能进行特征点选取工作 if((m_pDibSamp->IsEmpty())){ AfxMessageBox("尚未打开待配准图像文件,请打开待配准图像"); return; } // 设置选取特征点标志位 m_bChoseFeature = TRUE; AfxMessageBox("请在待配准图像中选取特征点"); } /************************************************************************* * * \函数名称: * OnLButtonUp() * * \输入参数: * 无 * * \返回值: * 无 * * \说明: 第 8 章 图像配准 · 429· * 该函数根据鼠标所标定的位置设置特征点。然后调用特征配准函数配准此特征点。 * 特征点的数目至少应该选取 3 个 * ************************************************************************* */ void CDlgReg::OnLButtonUp(UINT nFlags, CPoint point) { // 循环变量 int i,j; // 如果特征选取标志位为 TRUE,则进行特征点的选取和配准,否则退出 if(!m_bChoseFeature){ return; } // 待配准图像的特征选取区域,在这里选择特征点的选择区域要比图像的区 // 域小一圈 CRect rectChoose; rectChoose.bottom = m_rectResltImage.bottom - 5; rectChoose.top = m_rectResltImage.top + 5; rectChoose.left = m_rectResltImage.left + 5; rectChoose.right = m_rectResltImage.right - 5; // 特征点的区域 CRect rectFeature; // 标志位,表示此点是否是已经选择的特征点 BOOL bFlag = FALSE; // 判断此点是否合法,并判断此点是否已经选择,如果是,则去掉此点 if(rectChoose.PtInRect(point)) { // 如果所选择的特征点是以前的特征点,则去掉此点 for( i = 0; im_lpImage; // 待配准图像数据指针 unsigned char* pSamp; pSamp = (unsigned char *)pDibSamp->m_lpImage; // 特征点位置的数据配准块 unsigned char* pUnchSampBlock; pUnchSampBlock = new unsigned char[nBlockLen*nBlockLen]; // 临时分配内存,用于存放配准数据块 unsigned char* pUnchBaseBlock; pUnchBaseBlock = new unsigned char[nBlockLen*nBlockLen]; 第 8 章 图像配准 · 433· // 相似度 double dbCor; // 最大相似度 double dbMaxCor = 0; // 基准图像的存储大小 CSize sizeBaseImg; sizeBaseImg = pDibBase->GetDibSaveDim(); // 待配准图像的存储大小 CSize sizeSampImg; sizeSampImg = pDibSamp->GetDibSaveDim(); // 从待配准图像中提取以特征点为中心的 nBlockLen*nBlockLen0 的数据块 for(i=-nBlockHalfLen; i<=nBlockHalfLen; i++){ for(j=-nBlockHalfLen; j<=nBlockHalfLen; j++){ // 计算此点在图像中的位置 nX = pointSamp.x + i; nY = sizeSampImg.cy - pointSamp.y + j +1; // 提取图像数据 pUnchSampBlock[(j+nBlockHalfLen)*nBlockLen + (i+nBlockHalfLen)] = pSamp[nY*sizeSampImg.cx + nX]; } } // 基准图像的高度和宽度 int nBaseImgHeight, nBaseImgWidth; nBaseImgHeight = pDibBase->m_lpBMIH->biHeight; nBaseImgWidth = pDibBase->m_lpBMIH->biWidth; // 在基准图像中寻找配准特征点,采取的搜索方法为全局搜索 for(m = nBlockHalfLen; m< nBaseImgHeight-nBlockHalfLen; m++){ for(n=nBlockHalfLen; n dbMaxCor){ dbMaxCor = dbCor; pointBase.x = n; pointBase.y = m; } } } return pointBase; } /************************************************************************* * * \函数名称: * CalCorrelation() * * \输入参数: * unsigned char* pBase - 基准图像数据指针 * unsigned char* pSamp - 待配准图像数据指针 * int nBlockLen - 配准数据块的尺度大小 * * \返回值: * double - 返回两个数据块配准的相似度 * * \说明: * 该函数对给定的两个大小为 nBlockLen*nBlockLen 的数据块,计算两者之间的 * 的配准相似度。其中,去掉均值以消除亮度变换的影响 * ************************************************************************* */ double CDlgReg::CalCorrelation(unsigned char* pBase, unsigned char* pSamp, int nBlockLen) { // 临时变量 double dbSelfBase=0,dbSelfSamp=0; 第 8 章 图像配准 · 435· // 相似度 double dbCor=0; // 块均值 double dbMeanBase=0,dbMeanSamp=0; // 计算两个块的平均值 for(int i=0;iGetDibSaveDim(); // 拷贝调色板 memcpy(m_pDibResult->m_lpvColorTable, m_pDibInit->m_lpvColorTable, m_pDibResult->m_nColorTableEntries*sizeof(RGBQUAD)); // 应用调色板 m_pDibResult->MakePalette(); // 分配内存给合并后的图像 LPBYTE lpImgResult; lpImgResult = (LPBYTE)new unsigned char[sizeSaveResult.cx * sizeSaveResult.cy]; // 对图像进行赋值 for( int i=0; im_lpImage = lpImgResult; // 显示合并后的图像 CDlgAftReg* pDlg; pDlg = new CDlgAftReg(NULL, m_pDibResult); pDlg->DoModal(); Visual C++数字图像获取、处理及实践应用 · 438· // 删除对象 delete pDlg; // 释放已分配内存 delete[]lpBaseImg; delete[]lpSampImg; delete[]pDbBs2SpAffPara; delete[]pDbSp2BsAffPara; } /************************************************************************* * * \函数名称: * GetAffinePara() * * \输入参数: * double *pDbBs2SpAffPara - 用于存放基准图像到待配准图像的仿射变换系数 * double *pDbSp2BsAffPara - 用于存放待配准图像到基准图像的仿射变换系数 * * \返回值: * 无 * * \说明: * 该函数根据得到的三对配准的特征点,计算仿射变换系数。得到的仿射变换系数 * 存放在两个输入参数所指向的内存中 * ************************************************************************* */ void CDlgReg::GetAffinePara(CPoint* pPointBase, CPoint* pPointSampl, double* pDbAffPara) { // pDbBMatrix 中存放的是基准图像中特征点的坐标, // 大小为 2*m_nChsFeatureNum,前 m_nChsFeatureNum 为 X 坐标 double *pDbBMatrix; pDbBMatrix = new double[2*m_nChsFeatureNum]; // pDbSMatrix 中存放的是待配准图像中特征点的扩展坐标 // 大小为 3*m_nChsFeatureNum,其中前 m_nChsFeatureNum 为 X 坐标 // 中间 m_nChsFeatureNum 个为 Y 坐标,后面 m_nChsFeatureNum 为 1 double *pDbSMatrix; pDbSMatrix = new double[3*m_nChsFeatureNum]; // pDbSMatrixT 中存放的 pDbSMatrix 的转置矩阵, // 大小为 m_nChsFeatureNum*3 double *pDbSMatrixT; 第 8 章 图像配准 · 439· pDbSMatrixT = new double[m_nChsFeatureNum*3]; // pDbInvMatrix 为临时变量,存放的是 pDbSMatrix*pDbSMatrixT 的逆 // 大小为 3*3 double *pDbInvMatrix; pDbInvMatrix = new double[3*3]; // 临时内存 double *pDbTemp; pDbTemp = new double[2*3]; // 循环变量 int count; // 给矩阵赋值 for(count = 0; countd) { d = p; is[k] = i; js[k] = j; } } if(d+1.0==1.0) { delete is; delete js; return FALSE; } if(is[k] != k) for(j=0;j=0; k--) { if(js[k] != k) for(j=0; jm_lpBMIH->biWidth; nBaseImgHeight= m_pDibInit->m_lpBMIH->biHeight; // 待配准图像的宽度和高度 int nSamplImgWidth, nSamplImgHeight; nSamplImgWidth = m_pDibSamp->m_lpBMIH->biWidth; nSamplImgHeight= m_pDibSamp->m_lpBMIH->biHeight; // 基准图像的原始区域 CRect rectBase(0,0,nBaseImgWidth,nBaseImgHeight); // 临时变量 CPoint pointTemp; double tx,ty; // 图像的端点 pointTemp.x = 0; pointTemp.y = 0; Visual C++数字图像获取、处理及实践应用 · 444· // 计算点 pointTemp 经过仿射变换后的坐标 tx = pDbAffPara[0*3 +0]*pointTemp.x + pDbAffPara[0*3 + 1]*pointTemp.y + pDbAffPara[0*3 + 2]; ty = pDbAffPara[1*3 + 0]*pointTemp.x + pDbAffPara[1*3 + 1]*pointTemp.y + pDbAffPara[1*3 + 2]; // 判断 pointTemp 经过仿射变换后是否超出原来的大小 if(txrectBase.right) rectBase.right = (int)tx+1; if(tyrectBase.bottom) rectBase.bottom = (int)ty+1; // 计算端点(0, nSamplImgHeight)变换后的坐标 pointTemp.x = 0; pointTemp.y = nSamplImgHeight; tx = pDbAffPara[0*3 + 0]*pointTemp.x + pDbAffPara[0*3 + 1]*pointTemp.y + pDbAffPara[0*3 + 2]; ty = pDbAffPara[1*3 +0]*pointTemp.x + pDbAffPara[1*3 + 1]*pointTemp.y + pDbAffPara[1*3 + 2]; // 判断是否越界 if(txrectBase.right) rectBase.right = (int)tx+1; if(tyrectBase.bottom) rectBase.bottom = (int)ty+1; // 计算端点(nSamplImgWidth, nSamplImgHeight)变换后的坐标 pointTemp.x = nSamplImgWidth; pointTemp.y = nSamplImgHeight; tx = pDbAffPara[0*3 + 0]*pointTemp.x + pDbAffPara[0*3 + 1]*pointTemp.y + pDbAffPara[0*3 + 2]; ty = pDbAffPara[1*3 + 0]*pointTemp.x + pDbAffPara[1*3 + 1]*pointTemp.y + pDbAffPara[1*3 + 2]; // 判断是否越界 if(txrectBase.right) rectBase.right = (int)tx+1; if(tyrectBase.bottom) rectBase.bottom = (int)ty+1; // 计算端点(nSamplImgWidth, 0)变换后的坐标 pointTemp.x = nSamplImgWidth; pointTemp.y = 0; tx = pDbAffPara[0*3 + 0]*pointTemp.x + pDbAffPara[0*3 + 1]*pointTemp.y + pDbAffPara[0*3 + 2]; ty = pDbAffPara[1*3 + 0]*pointTemp.x + pDbAffPara[1*3 + 1]*pointTemp.y + pDbAffPara[1*3 + 2]; // 判断是否越界 if(txrectBase.right) rectBase.right = (int)tx+1; if(tyrectBase.bottom) rectBase.bottom = (int)ty+1; // 返回待配准图像变换后的区域大小 return(rectBase); } /************************************************************************* * * \函数名称: * SetSampImgAftAff() * * \输入参数: * double *pDbAffPara - 仿射变换系数矩阵 * CRect rectNewImg - 变换后图像的大小尺寸 * * \返回值: * LPBYTE - 返回变换后的图像 * * \说明: * 该函数根据仿射变换系数,计算待配准图像仿射变换后的图像。并返回此图像指针 * 此图像的大小为 rectNewImg Visual C++数字图像获取、处理及实践应用 · 446· * ************************************************************************* */ LPBYTE CDlgReg::SetSampImgAftAff(double* pDbAffPara, CRect rectNewImg) { // pUnchSect 是 4*4 大小的矩阵数组 unsigned char *pUnchSect; pUnchSect = new unsigned char[4*4]; // 新的图像宽度和高度 int nNewImgWidth, nNewImgHeight; nNewImgWidth = rectNewImg.right - rectNewImg.left; nNewImgHeight = rectNewImg.bottom- rectNewImg.top; // 待配准图像的宽度和高度 int nSamplImgWidth, nSamplImgHeight; nSamplImgWidth = m_pDibSamp->m_lpBMIH->biWidth; nSamplImgHeight= m_pDibSamp->m_lpBMIH->biHeight; // 待配准图像的存储宽度 int nSampSaveWidth; nSampSaveWidth = m_pDibSamp->GetDibSaveDim().cx; // pUnchAftAffSamp 是一个大小为 rectNewImg 大小的图像, // 其中 rectNewImg 表示变换后的图像大小 unsigned char *pUnchAftAffSamp; pUnchAftAffSamp = new unsigned char[nNewImgWidth * nNewImgHeight]; double tx,ty; // 计算在变换后的图像的数据 for(int i=0;i=nSamplImgHeight||n<0||n>=nSamplImgWidth) pUnchSect[(m-(int)ty+1)*4 + (n-(int)tx+1)] = 0; 第 8 章 图像配准 · 447· else pUnchSect[(m-(int)ty+1)*4 + (n-(int)tx+1)] = m_pDibSamp->m_lpImage[(nSamplImgHeight-m-1)* nSampSaveWidth + n]; } // 确定变换的坐标 ty = ty - (int)ty + 1; tx = tx - (int)tx + 1; // 确定变换后此坐标的数值 pUnchAftAffSamp[i*nNewImgWidth + j] = CalSpline(pUnchSect,tx,ty); } // 释放内存 delete[]pUnchSect; // 返回指针 return (LPBYTE)pUnchAftAffSamp; } /************************************************************************* * * \函数名称: * CalSpline() * * \输入参数: * unsigned char *pUnchCorr - 插值的点 * double dX - X 坐标 * double dY - Y 坐标 * * \返回值: * unsigned char - 插值后的值 * * \说明: * 该函数根据邻近位置的数值进行插值。 * 此图像的大小为 rectNewImg * ************************************************************************* */ unsigned char CDlgReg::CalSpline(unsigned char *pUnchCorr, double x, double y) { double ret=0, Cx, Cy; Visual C++数字图像获取、处理及实践应用 · 448· double Temp; for(int i=0;i<4;i++) for(int j=0;j<4;j++) { Temp = pUnchCorr[i*4 + j]; if(fabs(y-i)<1) Cy = 1-2*fabs(y-i)*fabs(y-i)+fabs(y-i)*fabs(y-i)*fabs(y-i); if(fabs(y-i)>=1) Cy = 4-8*fabs(y-i)+5*fabs(y-i)*fabs(y-i)-fabs(y-i)*fabs(y-i)*fabs(y-i); if(fabs(x-j)<1) Cx = 1-2*fabs(x-j)*fabs(x-j)+fabs(x-j)*fabs(x-j)*fabs(x-j); if(fabs(x-j)>=1) Cx = 4-8*fabs(x-j)+5*fabs(x-j)*fabs(x-j)-fabs(x-j)*fabs(x-j)*fabs(x-j); ret += Temp*Cy*Cx; } if(ret<0) ret=0; if(ret>255) ret=255; return (unsigned char)ret; } /************************************************************************* * * \函数名称: * SetBaseImgAftAff() * * \输入参数: * double *pDbAffPara - 仿射变换系数矩阵 * * \返回值: * 无 * * \说明: * 该函数根据仿射变换系数,计算基准图像仿射变换后的图像,并返回存放此 * 数据的指针 * ************************************************************************* */ LPBYTE CDlgReg::SetBaseImgAftAff(CRect rectNewImg) { 第 8 章 图像配准 · 449· // 新图像的大小 int nNewImgWidth, nNewImgHeight; nNewImgWidth = rectNewImg.right - rectNewImg.left; nNewImgHeight = rectNewImg.bottom - rectNewImg.top; // 变换后图像 unsigned char *pUnchAftAffBase; pUnchAftAffBase = new unsigned char[nNewImgWidth*nNewImgHeight]; // 基准图像的高度和宽度 int nBaseWidth, nBaseHeight; nBaseWidth = m_pDibInit->m_lpBMIH->biWidth; nBaseHeight = m_pDibInit->m_lpBMIH->biHeight; // 基准图像的存储宽度 int nBaseSaveWidth; nBaseSaveWidth = m_pDibInit->GetDibSaveDim().cx; for(int i=0;i=-rectNewImg.top+nBaseHeight||j<-rectNewImg.left||j>= -rectNewImg.left+nBaseWidth) pUnchAftAffBase[i*nNewImgWidth + j] = 0; else pUnchAftAffBase[i*nNewImgWidth + j] = m_pDibInit->m_lpImage[(nBaseHeight - (i+rectNewImg.top) - 1)*nBaseSaveWidth + (j+rectNewImg.left)]; } // 返回 return (LPBYTE)pUnchAftAffBase; } 在基准图像与待配准图像合成后,弹出了一个显示合成后图像的对话框。下面介绍此对话框。在项 目中添加对话框 IDD_DLG_AFTREG,对话框的设置请参看附件中的工程文件。为此对话框创建新的类 CDlgAftReg。其具体实现代码如下。 (1)对话框头文件(DlgAftReg.h) #if !defined(AFX_AFTREG_H__986DCC9D_F114_4D88_A06B_FC955F33D7BB__INCLUDED_) #define AFX_AFTREG_H__986DCC9D_F114_4D88_A06B_FC955F33D7BB__INCLUDED_ #include "Cdib.h" Visual C++数字图像获取、处理及实践应用 · 450· #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 // AftReg.h : header file // ///////////////////////////////////////////////////////////////////////////// // CDlgAftReg dialog class CDlgAftReg : public CDialog { // Construction public: // 计算控件位置标志位 BOOL m_bCalImgLoc; // 显示图像的区域 CRect m_rectImage; // 计算控件位置 void CalImgLocation(); // 显示的图像 CDib* m_pDib; // 析构函数 CDlgAftReg(CWnd* pParent = NULL, CDib* pDibShow = NULL); // standard constructor // Dialog Data //{{AFX_DATA(CDlgAftReg) enum { IDD = IDD_DLG_AFTREG }; // NOTE: the ClassWizard will add data members here //}}AFX_DATA // Overrides // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(CDlgAftReg) protected: virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support //}}AFX_VIRTUAL // Implementation protected: 第 8 章 图像配准 · 451· // Generated message map functions //{{AFX_MSG(CDlgAftReg) afx_msg void OnPaint(); // NOTE: the ClassWizard will add member functions here //}}AFX_MSG DECLARE_MESSAGE_MAP() }; //{{AFX_INSERT_LOCATION}} // Microsoft Visual C++ will insert additional declarations immediately before the previous line. #endif // !defined(AFX_AFTREG_H__986DCC9D_F114_4D88_A06B_FC955F33D7BB__INCLUDED_) (2)对话框文件(DlgAftReg.cpp) // DlgAftReg.cpp : implementation file // #include "stdafx.h" #include "ImageProcessing.h" #include "DlgAftReg.h" #include "CDib.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif ///////////////////////////////////////////////////////////////////////////// // CDlgAftReg dialog CDlgAftReg::CDlgAftReg(CWnd* pParent /*=NULL*/, CDib* pDibShow) : CDialog(CDlgAftReg::IDD, pParent) { //{{AFX_DATA_INIT(CDlgAftReg) // NOTE: the ClassWizard will add member initialization here //}}AFX_DATA_INIT // 设置显示图像的指针 m_pDib = pDibShow; // 设置计算图像控件位置的标志位为 FALSE Visual C++数字图像获取、处理及实践应用 · 452· m_bCalImgLoc = FALSE; } void CDlgAftReg::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); //{{AFX_DATA_MAP(CDlgAftReg) // NOTE: the ClassWizard will add DDX and DDV calls here //}}AFX_DATA_MAP } BEGIN_MESSAGE_MAP(CDlgAftReg, CDialog) //{{AFX_MSG_MAP(CDlgAftReg) ON_WM_PAINT() // NOTE: the ClassWizard will add message map macros here //}}AFX_MSG_MAP END_MESSAGE_MAP() ///////////////////////////////////////////////////////////////////////////// // CDlgAftReg message handlers void CDlgAftReg::OnPaint() { // 设备上下文 CPaintDC dc(this); // 如果还没有计算对话框控件位置,则计算对话框控件位置 if(!m_bCalImgLoc) CalImgLocation(); // 显示大小 CSize sizeDisplay; // 显示位置 CPoint pointDisplay; // 如果有图像数据,则显示图像 if(m_pDib != NULL){ if(!m_pDib->IsEmpty()){ sizeDisplay.cx=m_pDib->m_lpBMIH->biWidth; sizeDisplay.cy=m_pDib->m_lpBMIH->biHeight; pointDisplay.x = m_rectImage.left; pointDisplay.y = m_rectImage.top; 第 8 章 图像配准 · 453· m_pDib->Draw(&dc,pointDisplay,sizeDisplay); } } } /************************************************************************* * * \函数名称: * CalImageLocation() * * \输入参数: * 无 * * \返回值: * 无 * * \说明: * 该函数设置对话框中的控件位置和大小,并设置显示图像的位置。默认的图像大小为 352 * ×288,如果图像小于此大小,则控件大小设置为 352×288,并将图像放置在控件中间 * ************************************************************************* */ void CDlgAftReg::CalImgLocation() { // 获得控件 IDC_DlgAftReg_IMAGE 的句柄,并获得控件的初始位置信息 CWnd* pWnd=GetDlgItem(IDC_AFTREG_IMAGE); WINDOWPLACEMENT *winPlacement; winPlacement=new WINDOWPLACEMENT; pWnd->GetWindowPlacement(winPlacement); // 图像宽度 int nImageWidth = 0; // 图像高度 int nImageHeight = 0; // 判断图像指针是否不为空 if(m_pDib != NULL){ nImageWidth = m_pDib->m_lpBMIH->biWidth; nImageHeight = m_pDib->m_lpBMIH->biHeight; } // 调整控件 IDC_REG_INIT_IMAGE 的大小位置,并同时设置显示基准图像的位置 Visual C++数字图像获取、处理及实践应用 · 454· if(nImageHeight > 352){ winPlacement->rcNormalPosition.bottom = winPlacement->rcNormalPosition.top + nImageHeight; m_rectImage.bottom = winPlacement->rcNormalPosition.bottom; m_rectImage.top = winPlacement->rcNormalPosition.top; } else{ winPlacement->rcNormalPosition.bottom = winPlacement->rcNormalPosition.top + 352; m_rectImage.bottom = winPlacement->rcNormalPosition.top + 176 + nImageHeight/2; m_rectImage.top = winPlacement->rcNormalPosition.top + 176 - nImageHeight/2; } if(nImageWidth > 288){ winPlacement->rcNormalPosition.right = winPlacement->rcNormalPosition.left + nImageWidth; m_rectImage.right = winPlacement->rcNormalPosition.right; m_rectImage.left = winPlacement->rcNormalPosition.left; } else{ winPlacement->rcNormalPosition.right = winPlacement->rcNormalPosition.left + 288; m_rectImage.right = winPlacement->rcNormalPosition.left + 144 + nImageWidth/2; m_rectImage.left = winPlacement->rcNormalPosition.left + 144 - nImageWidth/2; } // 设置 IDC_DlgAftReg_IMAGE 控件的大小位置 pWnd->SetWindowPlacement(winPlacement); // 获得 IDC_DlgAftReg_IMAGE 控件的下边位置,以便调整其他控件的位置 int nIniImgBottom, nIniImgRight,nIniImgLeft; nIniImgBottom = winPlacement->rcNormalPosition.bottom; nIniImgLeft = winPlacement->rcNormalPosition.left; nIniImgRight = winPlacement->rcNormalPosition.right; // 设置控件 IDOK 的位置大小 pWnd=GetDlgItem(IDOK); pWnd->GetWindowPlacement(winPlacement); winPlacement->rcNormalPosition.top = nIniImgBottom +15; winPlacement->rcNormalPosition.bottom = nIniImgBottom + 60; pWnd->SetWindowPlacement(winPlacement); // 设置控件 IDCANCEL 的位置大小 pWnd=GetDlgItem(IDCANCEL); pWnd->GetWindowPlacement(winPlacement); winPlacement->rcNormalPosition.top = nIniImgBottom +15; winPlacement->rcNormalPosition.bottom = nIniImgBottom + 60; 第 8 章 图像配准 · 455· pWnd->SetWindowPlacement(winPlacement); // 设置对话框的大小 this->GetWindowPlacement(winPlacement); winPlacement->rcNormalPosition.bottom = nIniImgBottom + 60+ 70; winPlacement->rcNormalPosition.left = nIniImgLeft - 20; winPlacement->rcNormalPosition.right = nIniImgRight + 20; this->SetWindowPlacement(winPlacement); // 设置计算控件位置标志位为 TRUE m_bCalImgLoc = TRUE; // 释放已分配内存 delete winPlacement; } 设置完图像配准对话框后,向“图像配准”菜单中添加菜单项“图像配准”,如图 8-2 所示。 图 8-2 图像配准菜单项 在 CimageProcessingView 类中添加该菜单项的点击事件处理程序,其实现代码如下所示。 /************************************************************************* * * \函数名称: * OnRegReg() * * \输入参数: * 无 * * \返回值: * 无 * * \说明: * 该函数实现图像的配准 * ************************************************************************* */ void CImageProcessingView::OnRegReg() { // 获得文档类句柄 CImageProcessingDoc* pDoc; pDoc = GetDocument(); Visual C++数字图像获取、处理及实践应用 · 456· // 判断是否是 8-bpp 位图(这里为了方便,只处理 8-bpp 位图的水平镜象,其他的可以类推) if(pDoc->m_pDibInit->m_nColorTableEntries != 256) { // 提示用户 MessageBox("目前只支持 256 色位图的图像配准!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } // 打开图像配准对话框 CDlgReg* pDlg=new CDlgReg(NULL,pDoc); pDlg->DoModal(); delete pDlg; } 运行上述代码,按照如图 8-3 所示的对话框设置,运行结果如图 8-4 所示。 图 8-3 图像配准对话框 图 8-4 图像配准运行结果 第第 99 章章 目目标标检检测测与与运运动动检检测测 运动信息是生活中一个重要的信息来源。随着计算机技术的不断提高,运动信息的研究和应用也受 到了广泛的重视。其实,即使在简单的动物身上都存在着复杂的观察、跟踪和利用运动的功能。例如, 青蛙可以有效地探测小飞虫。这样的动物可以有选择地对视场中运动着的小的、深色的物体进行跟踪。 家蝇可以跟踪运动物体并发现目标跟背景之间的相对运动,甚至在物体与背景在纹理上相同的情况下, 它也能够很好地捕捉到目标的信息。因此如果没有相对运动,是很难区分背景和目标的。 在高等动物包括灵长目动物中,运动的分析从早期视觉处理阶段开始就渗透到视觉系统中。某些生 物,如鸽子和兔子在视网膜的水平上完成基本运动分析。其他动物,包括猫和灵长目动物,视觉皮层中 从眼睛接受输入的第一个神经里已经涉及到运动的分析。些神经对沿某一方向运动的刺激反应很灵敏, 但对沿相反方向的运动却反应很小,或根本不反应。运动视觉在生理系统中占主要地位是不令人奇怪的, 因为运动揭示了关于环境的有价值的信息,可以更好地反映自然界的瞬间情况。 随着计算机处理能力的提高,利用计算机来分析、模拟人体运动也受到越来越多的注意。人体运动 分析遇到的第一个问题就是运动检测。基于视频或者图像的人体运动目标检测或识别,目前已经在多种 多样的场合得到应用,如在监控系统中对某个目标进行运动检测,进而分析运动目标的一些特征等。对 目标跟踪将会节省大量的人力物力。更为重要的是,有些场所由于客观原因,人类可能不方便或者根本 不可能亲自到现场进行查看,这时,只有通过其他方式,如用计算机进行实时监视来完成需要的工作。 本章对根据运动检测目标以及跟踪目标等内容进行介绍。出于方便,下面讲述的运动检测和目标跟 踪都是针对视频或者图像序列的。在视频图像处理中,运动的检测和估计主要有两种比较常用的方法: 一种是直接的帧间变化检测,另一种是基于块匹配或光流方法的运动矢量场计算。运动检测方法快捷简 单,适合于运动快且形变较大的运动目标,但对有全局运动的场景不能直接使用。因为这种方法速度较 快,适合对实时性要求较高的应用。而运动矢量方法比较精确、鲁棒,给出的信息更丰富,并可处理有 全局运动的情况,缺点是计算需要更多时间,对过于复杂和快速的运动效果不好。 本章第一节介绍从静止背景中检测运动目标的方法,从运动背景下运动目标的检测则在第二节中进 行介绍,第三节将给出目标跟踪的方法,并在第四节中给出运动目标检测的实例。 9.1 静止背景下的运动目标检测 对于静止背景下的运动检测,完全可以采用帧间变化检测(Change Detection)检测出运动目标。在 本节中,将对帧间变化检测方法进行简单的介绍。 对于静止背景下的运动目标检测,其基本流程如图 9-1 所示。 视频序列 预处理 背景恢复 第 n 帧图像 运动目标提取 图 9-1 静止背景下运动目标检测流程框图 从图 9-1 所示中可以看到,基于静止背景的运动目标检测主要分为 3 个部分:预处理、背景恢复和 运动目标提取。下面将分别介绍这几个部分的内容。 • 预处理:目的是在分割前去除噪声的影响。去除噪声的方法有很多种,例如前面介绍的各种平滑Visual C++数字图像获取、处理及实践应用 · 458· 操作。 • 背景恢复:视频序列帧间具有很强的相关性,仅仅利用单帧的信息进行处理容易产生错误,更好 的方法是联合多帧信息进行分析。基于这一思想,可以根据各个坐标处像素值在整个序列中的统计 信息对运动场景的背景进行恢复。 • 运动目标提取:利用恢复的背景以及当前帧的信息,分割出所有运动目标的近似区域。 由于预处理部分已经在前面的章节中进行了详细的叙述,这里就后面的两个部分,即背景恢复和运 动目标提取进行详细的介绍。 9.1.1 背景恢复 由于视频序列记录了视频目标在一段时间内的运动和变化信息,因此理想的视频分割方法是在较长 的时间范围内对数据进行分析并充分利用帧间的相关信息。基于这样一种思路,我们可以对各个像素点 沿时间轴的变化规律进行分析,并根据统计规律在整个序列中挑选合适的点对背景进行恢复。 我们定义图像序列为 I(x,y,i),其中 x、y 代表空间坐标,i 代表帧数(i=1…N),N 为序列总帧数。 序列的亮度分量为 IL(x,y,i)、则视频帧差(Change Detection Mask,CDM)反应了相邻帧之间的灰度变化: , if CDM( , , ) , = ( , , +1)- ( , , )0, if <   ≥= ÁÁ d d Txyi dIxyi Ixyid T (9-1) 其中,阈值 T 被用来去除噪声。对固定的坐标位置(x,y),CDM(x,y,i)可以表示为帧数 i 的函数,它 记录了在位置(x,y)处像素点沿时间轴的变化曲线。可以根据 CDM(x,y,i)是否大于零将这条曲线分段,并 将其中被检测到的静止部分用集合{Sj(x,y) , 1 ≤ j ≤ M}表示,如图 9-2 所示,其中,Sj 的起点和终点分别 是 STj 和 ENj。下一步,在每一个位置(x,y)对应的{Sj}集合中,挑选最长的静止分段并记录该分段中点 的对应帧号为 M(x,y)。最后,第 M(x,y)帧处的点被用来填充视频背景中的相应位置,该逻辑可以用下面 的公式描述: (,)( (,) (,))/2 ( , )=( , , (, ) ) M x y ST x y EN x y B x y I x y M x y = + (9-2) 其中,ST(x,y)和 EN(x,y) 是对应于最长静止分段的起点和终点,B(x,y)为重建的视频背景。这一方法 的基本假设是在视频序列中,运动员不会始终站在某一位置不动,而必定会移开,使背景显露出来。 1S MSjS Frame Number i ,CDM ( )x y i jST jEN 图 9-2 亮度帧差图沿时间轴的变化函数 9.1.2 基于静止背景的运动目标提取 在恢复场景背景之后,可以在每一帧和背景之间用减法运算得到亮度分量的背景帧差图 IDL: d, if ( , , ) = , = ( , , ) ( , )0, if < LLL d TIDxyi d IxyiBxyd T   ≥ − (9-3)第 9 章 目标检测与运动检测 · 459· 其中,BL 是背景的亮度分量。该方法可以很好的对运动目标进行分割。在实际情况中,背景并不是 完全静止的,由于光照或者其他干扰而引入了一些局部的噪声,这些干扰对运动目标的正确检测和定位 带来很大的困难,因此,可以利用色度等其他信息或形态学的方法对这些噪声进行消除。对于这些后处 理的方法,在这里就不详细介绍了,读者可以参考相关的文献。 9.2 运动背景下的运动目标检测 运动背景下的目标检测和静止背景的目标检测方法有所不同。静止背景的目标检测可以恢复出背 景,但是对于含有运动背景的视频,是不可能恢复出静止的背景的。运动背景一般会包含全局运动,所 以一般来讲,运动背景下的目标检测大致有两种方法:第一是利用光流方法检测运动目标,第二是根据 当前帧及其前面数帧的信息预测背景,从而检测到运动目标。 9.2.1 光流方法 光流的方法一般是根据当前帧及前后帧的信息,计算某个像素的运动矢量。 令 ),( yxFk 表 示 第 k 帧 ),( yx 点 对 应 的 像 素 , ( d( ), d( ))k lF x x y y+ + + 表 示 第 lk + 帧 中 ( d( ), d( ))x x y y+ + 点所对应的像素。如果 ),( yx 点像素对应运动目标的某一点,并且在第 lk + 帧 运动到 ))(),(( ydyxdx ++ ,那么如果捕捉的视频没有噪声,应该存在等式: (,) ( d(), d())k k lF x y F x x y y+= + + (9-4) 还有一种等价形式: (,) ( d(), d())k k lF x y F x x y y−= − − (9-5) 这两个等式在本质上是相同的,区别在于:式 9-4 是第 k 帧的像素运动到第 lk + 中,而式 9-5 是 第 k 帧的像素在第 lk − 帧的位置。一般把式 9-4 叫做前向预测,式 9-5 叫做后向预测。 上述两式中的 d( )x 与 d( )y 就是 ),( yx 像素的运动矢量,但是这个信息在视频中不能直接获取。 一方面是因为在成像的过程中会出现噪声,使得上述等式不会成立;另一方面,d( )x 与 d( )y 的计算是 后验方式的:只有视频信息,在目标提取之前,没有运动信息,而且因为三维物体成像到二维图像时, 本身就会丢失一部分信息。三维信息丢失会带来两个问题:第一是遮挡问题,第二是孔径问题。这里只 介绍遮挡问题,关于孔径问题及更多的专业知识,请读者参考相关的技术文献。 遮挡问题是这样产生的:对一段视频,一般的处理方法是需要把某些像素点进行匹配 (Correspondence),如式 9-4 指出的那样,需要找到 ),( yx 像素在下一帧中相对应的位置。然而实际的 拍摄由于从三维信息到二维信息丢掉了一部分信息,因此某些匹配是不可能完成的。 图 9-3(a)所示中的阴影部分表示物体运动过程中目标遮挡了背景,而当前帧被遮住的背景部分和 下一帧遮挡住的背景部分是不同的,所以阴影部分的背景在下一帧中是看不见的,当然当前帧的相应区 域在下一帧也就找不到对应的区域。图 9-3(b)所示中的阴影区域表示在物体运动后显露出来的背景区 域,显然,下一帧中,阴影区域对应的背景在当前帧是不可能找到对应区域的。 Visual C++数字图像获取、处理及实践应用 · 460· (a) (b) 图 9-3 遮挡问题图解 为了解决这些问题,实际中利用光流方法提取运动信息,一般会利用其他方面的信息,如颜色、形 状等。实际的匹配(Correspondence)方法,或者说利用光流计算运动矢量一般有以下几个方法:根据 参数建立光流方程,求解光流方程;二阶微分方法;块运动方法;梯度估计方法。当然还有很多其他方 法,因为涉及到过于专业的内容,因此这里就不再介绍了,有兴趣的读者可以参考相关的文献。 9.2.2 全局运动预测 运动背景遇到的最大问题就是背景是变化的,因此不能直接利用帧差的方法进行计算。利用帧差的 方法进行计算,并不是要求背景总是固定的,而是要求已知的背景是一段视频中每一帧的背景。所以严 格来讲,只要能够想办法知道一段视频中每一帧的背景,那么帧差的办法仍然是可以使用的。 全局运动是需要解决的最大问题,为了处理全局运动给背景带来的变化,这里介绍一种基于预测的 方法。设第 i 帧中坐标(x,y)点的像素为 p(i,x,y),如果能够根据 0,1,2⋯ i-1 目标检测的结果计算出背景的大 致运动情况,那么就可以利用已知的信息,对第 i 帧的背景进行预测。如果预测出来的背景和实际的背 景相差不大,那么用这种方法将会取得不错的效果。在某些视频中,尤其是全局运动方向和运动速度相 差不多时,利用这种方法进行目标检测,不但会减少计算时间,而且检测结果也很好。图 9-4 所示为全 局运动检测的流程。 ÁÂÃÄÅÁ ÆÇÈÉ Â ÁÂÁ ÂÁ     Âà  Á   Æ Âà Á ÂÁ !É"#Á $%& Á 图 9-4 全局运动检测框图 第 9 章 目标检测与运动检测 · 461· 9.2.3 基于块的运动检测方法 这一节介绍基于块的运动检测方法。前两个小节讲述了如何处理运动背景的全局运动。但是如何检 测全局运动是一个重要问题。基于块(Block_Based)的方法则是把每一帧分成很多小块,然后利用这些 小块进行跟踪及处理。基于块的处理的假设条件是,对于图像或者视频中的目标可以分成很多小的块。 换句话说,如果把一帧分成许多小块,那么这些小块就会完全的属于目标或者背景。当然这种假设是有 漏洞的,不过在某些应用中,基于块的方法已经能够检测出足够精确的目标。 假设第 i 帧中含有块 A,如图 9-5(a)所示。经过运动后,在下一帧这个块出现在 9-5(b) 所示图中的相应位置,设块 A 的大小为 NN × ,那么理论上: 1(,) ( d(), d())i iF x y F x x y y+= + + Ayx ∈),( (a) (b) 图 9-5 基于块的处理示意图 根据这一点,可以对 A 块进行全图像搜索,找到 A 的运动矢量。但是这种搜索方法会遇到几个问 题: (1)如前所示,A 会可能不完全属于目标或者背景。 (2)实际捕捉到的视频会有噪声,所以即使是内容相同,对应像素的像素值也可能不相同。 (3)有些运动的物体可能移出了镜头,所以找不到对应的块,即使能够找到,也是错误的。 (4)从三维到二维的信息转变会发生遮挡等难以解决的问题,如物体发生形变、光照发生变化等。 实际的基于块的运动矢量计算方法一般考虑了物体形变等多方面因素,而不止是只考虑平移的情况, 一般采用的方法是利用可变形(Deformable Block)块进行匹配,计算运动矢量。在这方面,已经有人利用 6-参数,或者 8-参数的放射(Affine)变换模型来试图解决这个问题。下面就简单的介绍放射变换。 如果运动只有平移,那么可以逐像素的进行计算。令 ),( ii yx 表示第 i 帧的一个像素点, ),( 11 ++ ii yx 是其对应的像素点,那么,这两点应该满足: 1 1 d( ) d( ) i i i i x x x y y y + + = + = + (9-6) 如果运动同时包含有旋转和形变,那么可能需要更为复杂的公式,称之为 6-参数放射变换(Affine Transformation)。具体的公式形式如下: 1 1 d( ) d( ) i i i i i i x ax by x y cy dy y + + = + + = + + (9-7) 其中 a、b、c、d 是待定的参数。这个变换可以处理平移、旋转以及二维的形变。除了放射变换,还有其Visual C++数字图像获取、处理及实践应用 · 462· 他的两个变换,分别称作透视变换和双线性变换。 ),( 21 ii xx 表示第 i 帧的一个像素点, ),( 1 2 1 1 ++ ii xx 是其 对应的像素点,透视变换的变换公式为: 1 1 1 2 2 3 1 7 1 8 2 1 4 1 5 2 6 2 7 1 8 2 1 1 i i i i i i i i i i a x a x ax a x a x a x a x ax a x a x + + + += + + + += + + (9-8) 双线性变换(Bilinear)公式为: 1 1 1 1 2 2 3 1 2 4 1 2 5 1 6 2 7 1 2 8 i i i i i i i i i i x a x a x a x x a x a x a x a x x a + + = + + + = + + + (9-9) 图 9-6 展示了上述几种变换能够处理的运动。 (a)放射变换 (b)放射变换 (d)透视变换 (c)双线性变换 图 9-6 几种变换能够处理的运动 第 9 章 目标检测与运动检测 · 463· 9.3 Visual C++编程实现 前面已经介绍了有关目标检测和运动检测的原理。下面给出一个在静止背景下恢复背景的示例程 序。得到背景之后,通过背景和当前帧图像相减,就可以得到当前的运动目标了。 下面给出用函数 GetBackground 实现静止背景的检测的代码。 /************************************************************************* * * \函数名称: * GetBackground() * * \输入参数: * CString strFilePath - 第一帧图像的文件名 * int nTotalFrameNum - 进行检测的图像帧数 * int nImageWidth - 图像宽度 * int nImageHeight - 图像高度 * unsigned char * pUnchBackGround - 指向返回背景数据的指针 * * \返回值: * BOOL - 成功则返回 TRUE,否则返回 FALSE * * \说明: * 该函数根据指定文件名的图像序列求取静止背景 * ************************************************************************* */ BOOL GetBackground(CString strFilePath, int nTotalFrameNum, int nImageWidth, int nImageHeight, unsigned char* pUnchBackGround) { // pUnchTemp1 和 pUnchTemp2 用来计算相邻两帧之间的帧差 // 每次只要读入一帧即可,即:假设刚刚比较 k-1 和 k 帧,那么现在比较 k 和 // k+1 帧,那么 k 帧是不需要重新读入的 unsigned char* pUnchTemp1; unsigned char* pUnchTemp2; pUnchTemp1 = new unsigned char[nImageWidth * nImageHeight * sizeof(unsigned char)]; pUnchTemp2 = new unsigned char[nImageWidth * nImageHeight * sizeof(unsigned char)]; // 临时存放图像数据的 CDib 指针 CDib* pDibTemp; pDibTemp = new CDib; // 读出第一帧数据并放入 pDibTemp Visual C++数字图像获取、处理及实践应用 · 464· pDibTemp->Empty(); if(!LoadDibSeq(strFilePath,1,nTotalFrameNum,pDibTemp)){ return FALSE; } // 然后将数据取出,存放在 pUnchTemp1 中 memcpy(pUnchTemp2,pDibTemp->m_lpImage,nImageWidth*nImageHeight*sizeof (unsigned char)); // pChResultAfterMor 是用来记录帧间变化的内存区域 unsigned char * pUnchTrackBox = new unsigned char[(nTotalFrameNum)* nImageWidth*nImageHeight*sizeof(unsigned char)]; unsigned int index = 0; // 帧间差的区域,二进制 unsigned char *pUnchTemp3=new unsigned char [nImageWidth*nImageHeight*sizeof(unsigned char)]; // 腐蚀之后的区域,二进制 unsigned char * pUnchResultAfterMor = new unsigned char[nImageWidth*nImageHeight*sizeof(unsigned char)]; // 对每一帧进行比较 for (int i = 2; iEmpty(); if(!LoadDibSeq(strFilePath , i , nTotalFrameNum , pDibTemp)){ return FALSE; } // 然后将数据取出,存放在 pUnchTemp2 中 memcpy(pUnchTemp2,pDibTemp->m_lpImage,nImageWidth*nImageHeight); // 对图像帧差进行二值化处理,并将二值化后的图像存放在 pUnchTemp3 中 BinaFrameDiff(pUnchTemp1,pUnchTemp2 ,nImageWidth,nImageHeight,pUnchTemp3,10); // 对二值化后的图像进行腐蚀处理,在这里对腐蚀窗口的大小设置为 2,阈值为 7 ErodeFrameDiff(pUnchTemp3,nImageWidth,nImageHeight,2,7,pUnchResultAfterMor); // 将此二值化后的程序放入 pUnchTrackBox 的相应位置 memcpy(pUnchTrackBox+index,pUnchResultAfterMor,sizeof(unsigned char) *nImageWidth*nImageHeight); 第 9 章 目标检测与运动检测 · 465· // 计算图像数据在 pUnchTrackBox 中的偏移量 index = index + nImageWidth*nImageHeight*sizeof(unsigned char); // 每做完两帧之间的比较,就使帧号下移一个,pUnchTemp1 // 中是存 k 帧内容,pUnchTemp2 帧是存 k+1 // 帧内容,所以,每次只要把 pUnchTemp2 中的内容给 pTemp1,而 pTemp2 重新 //读入既可以了 unsigned char* pUnchTag = NULL; pUnchTag = pUnchTemp1; pUnchTemp1 = pUnchTemp2; pUnchTemp2 = pUnchTag; } // 释放已分配内存 delete []pUnchTemp1; pUnchTemp1 = NULL; delete []pUnchTemp2; pUnchTemp2 = NULL; delete []pUnchTemp3; pUnchTemp3 = NULL; delete []pUnchResultAfterMor; pUnchResultAfterMor=NULL; // 每一帧的大小 int nFrameSize = nImageWidth * nImageHeight * sizeof(unsigned char); // 记录最大长度 int * pnTrackSegLen = new int [nImageWidth*nImageHeight]; // 记录最大长度中的中间帧标号 int * pnTrackSegFrame = new int [nImageWidth*nImageHeight]; // 对每一个像素点跟踪最大为 0 的长度,并将最大长度中的中间帧标号记录下来 for (int y = 0; y= nTotalFrameNum - 1,则说明,已经遍历到最后一帧了 if (t >= nTotalFrameNum - 1) break; // 此时应为此长度的开始 segStart = t; while ((t < nTotalFrameNum - 1) && (pUnchTrackBox[t*nFrameSize+offset] == 0)) t++; // 此长度的结束帧标号 segEnd = t - 1; // 获得此连续为 0 的帧的长度 segLen = segEnd +1 -segStart; // 判断是否为最大长度,是则进行替换 if (segLen > largeLen) { largeLen = segLen; frameNum = (segEnd + segStart)/2; } } pnTrackSegLen[offset] = largeLen; pnTrackSegFrame[offset] = frameNum; 第 9 章 目标检测与运动检测 · 467· } } delete []pUnchTrackBox; pUnchTrackBox=NULL; // 因为对每个像素而言,背景可能出现在不同帧里,此时需要把所有帧调入内存 unsigned char* pBuffer = new unsigned char[nTotalFrameNum*(nImageWidth*nImageHeight)]; for (int k=1; kEmpty(); LoadDibSeq(strFilePath , k , nTotalFrameNum , pDibTemp); // 然后将数据取出,存放在 pBuffer 的相应位置中 memcpy(pBuffer+k*nFrameSize,pDibTemp->m_lpImage,nImageWidth*nImageHeight); } // 遍历整个图像,设置背景数据 for (y=0; ynTotalFrameNum) { AfxMessageBox("Invalidate file frame number"); return FALSE; } // 获得当前帧的图像文件名 CString strTempFileName; strTempFileName=GetFileName(strFilePath,nCurFrameNum); CFile fileOpen=NULL; 第 9 章 目标检测与运动检测 · 469· // 打开文件并读取 fileOpen.Open(strTempFileName,CFile::modeRead); if(pDib->Read(&fileOpen)==FALSE){ AfxMessageBox("can not open the file "+strTempFileName); return FALSE; } return TRUE; } /************************************************************************* * * \函数名称: * GetFileName() * * \输入参数: * CString strFilePathName - 图像的文件名 * int nCurFrameNum - 当前帧的图像文件名 * * \返回值: * CString - 返回给定帧数的图像文件名 * * \说明: * 该函数根据指定文件路径名和当前图像序列的帧数获取图像文件名 * 该函数中需要注意的是,只能读取 0-999 帧图像,图像为 bmp 格式,且按照 * 帧数数字进行存储,例如第一帧图像文件名为×××001.bmp,第 33 帧图像 * 的文件名为×××033.bmp。如果不是 bmp 文件,则返回"NULL" * ************************************************************************* */ CString GetFileName(CString strFilePathName, int nCurFrameNum) { //文件的路径名 CString strTempFileName; int nNumPos=strFilePathName.Find("."); if(nNumPos==-1){ AfxMessageBox("Please choose a bmp file"); return "NULL"; } //表示去掉了扩展名和数字标号的路径名,在这里,限定帧数为 0~999,所以采用三位来表示 CString strFileNameNoExtNoNum=strFilePathName.Left(nNumPos-3); Visual C++数字图像获取、处理及实践应用 · 470· //表示标号的字符串 CString strTempNum; if(nCurFrameNum<10){ strTempNum.Format("00%d",nCurFrameNum); } else { if(nCurFrameNum<100 &&nCurFrameNum>=10){ strTempNum.Format("0%d",nCurFrameNum); } else{ strTempNum.Format("%d",nCurFrameNum); } } // 得到图像文件名 strTempFileName=strFileNameNoExtNoNum+strTempNum+".bmp"; // 返回 return strTempFileName; } /************************************************************************* * * \函数名称: * BinaFrameDiff() * * \输入参数: * unsigned char* pUnchImg1 - 图像的文件名 * unsigned char* pUnchImg2 - 当前帧的图像文件名 * int nWidth * int nHeight * unsigned char* pUnchResult * int nThreshold * * \返回值: * CString - 返回给定帧数的图像文件名 * * \说明: * 该函数比较 pUnchImg1 和 pUnchImg2 两个区域中的内容,如果两个区域内 *容的差值的绝对值比 Threshold 大,则将 pUnchResult 相应的元素设置为逻辑值 1, *用灰度 255 表示,否则为 0,并用灰度 0 表示 * ************************************************************************* 第 9 章 目标检测与运动检测 · 471· */ void BinaFrameDiff(unsigned char *pUnchImg1, unsigned char *pUnchImg2, int nWidth, int nHeight, unsigned char * pUnchResult, int nThreshold) { int nTemp=0; for (int i=0;i nThreshold ? 255:0; } return ; } /************************************************************************* * * \函数名称: * ErodeFrameDiff() * * \输入参数: * unsigned char* pUnchImg1 - 图像数据指针 * int nWidth - 图像宽度 * int nHeight - 图像高度 * int nErodeHalfWin - 腐蚀窗口大小的一半 * unsigned char* pUnchResult - 结果数据制止 * int nThreshold - 阈值 * * \返回值: * 无 * * \说明: * 该函数进行腐蚀操作,形态学操作对 pUnchImg 中的每一点,计算这一点对应的 *窗口内的一些参数,然后根据参数结果给这个点设置相应的值. 功能上相当于广义滤波 * ************************************************************************* */ void ErodeFrameDiff(unsigned char *pUnchImg, int nWidth, int nHeight, int nErodeHalfWin, int nErodeThreshold, unsigned char *pUnchResult) { // 搜索整个图像,对图像进行腐蚀处理 for (int i=nErodeHalfWin;iGetPathName(); // 序列的总帧数 int nTotalFrameNum = 20; // 图像的宽度 int nImageWidth; nImageWidth = pDoc->m_pDibInit->m_lpBMIH->biWidth; // 图像的高度 int nImageHeight; nImageHeight = pDoc->m_pDibInit->m_lpBMIH->biHeight; // 图像的静止背景 unsigned char* pUnchBackGround; pUnchBackGround = new unsigned char[nImageWidth*nImageHeight]; // 更改光标形状 BeginWaitCursor(); // 调用 GetBackground 函数提取静止背景 bFlag = GetBackground(strPathName, nTotalFrameNum, nImageWidth,nImageHeight, pUnchBackGround); if(bFlag == FALSE){ return; } Visual C++数字图像获取、处理及实践应用 · 474· // 将背景设置为当前显示图像 LPBYTE lpTemp; lpTemp = pDoc->m_pDibInit->m_lpImage; // 将数据拷贝到图像中 memcpy(lpTemp, (LPBYTE)pUnchBackGround, nImageWidth*nImageHeight); // 恢复光标形状 EndWaitCursor(); // 释放已分配内存 delete[]pUnchBackGround; pUnchBackGround = NULL; // 设置脏标记 pDoc->SetModifiedFlag(TRUE); // 更新视图 pDoc->UpdateAllViews(NULL); } 运行上述代码,得到如图 9-8 所示的结果,其中(a)和(b)为图像序列中的两帧图像,(c)为获得的背 景。 (a) (b) (c) 图 9-8 获得背景运行结果 第第 1100 章章 图图像像形形状状特特征征分分析析 视觉系统对景物认识的初级阶段则是其形状。在经过图像的边缘提取和图像分割后,得到的是若干区域 和边界。通常把感兴趣的部分叫目标,其余部分叫背景。对人的视觉系统而言,物体的形状是一个赖以分辨 和识别的重要特征。下一步工作就是能让计算机识别这些目标,并用计算机所能提供的数字与符号把它表示 出来就可以进行形状识别和理解。要实现这一点,必须描述目标即提供它的有用性质或特征和相互关系。任 何一个物体的形状特征均可由其几何属性如长短、面积、距离、凸凹等,统计属性如投影和拓扑属性如连通、 欧拉数进行描述。 用计算机图像处理和分析系统对目标提取形状特征的过程就称为形状和结构分析。景物的形状信息概念 有 3 种方式,一是图像经过分割处理后的区域,二是图像经过边缘抽取后的边界,三是区域的骨架。在图像 上,区域或骨架提供了形状信息。对于区域内部或边界来说,由于我们只关心它们的形状特征,其灰度信息 往往可以忽略,只要能将它与其他目标或背景区分开来即可,所以往往把区域内部或边界的像素赋予“1” 值,而背景和其他不感兴趣的目标像素赋予“0”值,形成二值图像。二值图像给出了清晰的形状概念,它 在形状和结构分析中占有很重要的地位,本章讨论的许多算法部是基于二值化图像的。 区域所具有的形状特征可以通过对区域内部或外形的各种变换来提取,也可以用图像层次型数据结构来 表达。骨架是形状特征描述的重要方法,对于某些特殊的图像区域,如文字,它提供了极为重要的形状概念。 所以,对区域骨架的抽取及其形状特征的识别是很重要的工作。 10.1 图像的矩 10.1.1 矩的定义 给定二维连续函数 (,)f x y ,下式定义了其 pq 阶矩: ( , )d d , 0,1,2,pq p qM x y f x y x y pq +∞ +∞ −∞ −∞ = =∫ ∫  (10-1) 矩之所以能被用来表征一幅二维图像是基于帕普利斯(Papoulis)惟一性定理:如果 (,)f x y 是分段连 续的,只在 xy 平面的有限部分中有非零值,则所有各阶矩皆存在,并且矩序列{ }pqM 能惟一地被 (,)f x y 所确定,反之{ }pqM 也惟一地确定 (,)f x y 。 对于一幅二值化图像 ( , ), , 0,1,2f i j i j = 来说,上述的条件可被满足。所以可以定义其 pq 阶矩为: (,)pq p qM f i j i j= ∑∑ (10-2) 从矩出发可定义其数字特征: (1)重心 10 00 01 00(,)(/,/)i j M M M M= (10-3)Visual C++数字图像获取、处理及实践应用 · 476· (2)重心矩 ( , )( ) ( )pq p qm f i j i i j j= − −∑∑ (10-4) 10.1.2 Visual C++编程求图像的矩 首先为这一章加入相应的菜单,如图 10-1 所示。 图 10-1 图像特征提取菜单 函数 DIBMOMENT 用来计算图像的二阶矩 11M ,改变参数 ip、jq 就可计算图像任意阶的矩,代码如 下。 /************************************************************************* * * 函数名称: * DIBMOMENT() * * 参数: * CDib * pDib - 指向 CDib 类的指针 * * 返回值: * BOOL - 成功返回 True,否则返回 False * * 说明: * 该函数计算图像的力矩 * *************************************************************************/ BOOL DIBMOMENT(CDib *pDib) { // 指向源图像的指针 BYTE * lpSrc; //图像的宽度和高度 LONG lWidth; LONG lHeight; 第 10 章 图像形状特征分析 · 477· // 图像每行的字节数 LONG lLineBytes; //得到图像的宽度和高度 CSize SizeDim; SizeDim = pDib->GetDimensions(); lWidth = SizeDim.cx; lHeight = SizeDim.cy; //得到实际的 Dib 图像存储大小 CSize SizeRealDim; SizeRealDim = pDib->GetDibSaveDim(); // 计算图像每行的字节数 lLineBytes = SizeRealDim.cx; //图像数据的指针 LPBYTE lpDIBBits = pDib->m_lpImage; // 图像的矩 long nImageMoment; // 循环变量 int i,j; int ip,jq; // 临时变量 double temp; nImageMoment = 0; // 计算一阶矩 ip = jq = 1; // 力矩的计算 for (j = 0; j < lHeight; j++) { for(i = 0; i < lWidth; i++) { lpSrc = (unsigned char *)lpDIBBits + lLineBytes * j + i; temp = pow((double)i,ip)*pow(double(j),jq); Visual C++数字图像获取、处理及实践应用 · 478· temp = temp * (*lpSrc); nImageMoment = nImageMoment + (int)temp; } } // 将结果进行输出 CString CView; CView.Format("图像的力矩为:%d",nImageMoment); MessageBox(NULL,CView, "计算结果" , MB_ICONINFORMATION | MB_OK); return true; } 函数 DIBBARYCENTERMOMENT 用来求图像重心矩,代码如下。 /************************************************************************* * * 函数名称: * DIBBARYCENTERMOMENT() * * 参数: * CDib * pDib - 指向 CDib 类的指针 * * 返回值: * BOOL - 成功返回 True,否则返回 False * * 说明: * 该函数计算图像的重心矩 * *************************************************************************/ BOOL DIBBARYCENTERMOMENT(CDib *pDib) { // 指向源图像的指针 BYTE * lpSrc; //图像的宽度和高度 LONG lWidth; LONG lHeight; // 图像每行的字节数 LONG lLineBytes; //得到图像的宽度和高度 CSize SizeDim; 第 10 章 图像形状特征分析 · 479· SizeDim = pDib->GetDimensions(); lWidth = SizeDim.cx; lHeight = SizeDim.cy; //得到实际的 Dib 图像存储大小 CSize SizeRealDim; SizeRealDim = pDib->GetDibSaveDim(); // 计算图像每行的字节数 lLineBytes = SizeRealDim.cx; //图像数据的指针 LPBYTE lpDIBBits = pDib->m_lpImage; // 图像像素值 int nPixelValue; // 图像重心矩 long nBarycenterMoment; // 0 次矩 m00,x 方向的一次矩 m01 和 y 方向的一次矩 m10 long m00, m10, m01; // 重心 x,y 坐标值 int nBarycenterX,nBarycenterY; // 循环变量 int i,j; // 临时变量 double temp; // 赋初值为零 m00 = 0; m01 = 0; m10 = 0; nBarycenterMoment = 0; // 求 0 次矩 m00,x 方向的一次矩 m01 和 y 方向的一次矩 m10 for (j = 0; j < lHeight; j++) { for(i = 0; i < lWidth; i++) { lpSrc = (unsigned char *)lpDIBBits + lLineBytes * j + i; Visual C++数字图像获取、处理及实践应用 · 480· nPixelValue = *lpSrc; m00 = m00 + nPixelValue; temp = i * nPixelValue; m01 = m01 + temp; temp = j * nPixelValue; m10 = m10 + temp; } } // 重心 x,y 坐标值 nBarycenterX = (int)(m01 / m00 + 0.5); nBarycenterY = (int)(m10 / m00 + 0.5); // 计算重心矩 for (j = 0; j < lHeight; j++) { for(i = 0; i < lWidth; i++) { lpSrc = (unsigned char *)lpDIBBits + lLineBytes * j + i; nPixelValue = *lpSrc; temp = (i - nBarycenterX) * (j - nBarycenterY); temp = temp * nPixelValue; nBarycenterMoment = nBarycenterMoment + (int)temp; } } // 将结果进行输出 CString CView; CView.Format("图像的重心矩为(%d,%d),重心矩为%d" ,nBarycenterX,nBarycenterY,nBarycenterMoment); MessageBox(NULL,CView, "计算结果" , MB_ICONINFORMATION | MB_OK); return true; } 第 10 章 图像形状特征分析 · 481· 10.2 图像的空穴检出 10.2.1 定义及算法 空穴是由白色背景区域包围起来的一个黑区域。通常空穴检出的对象是只有黑色和白色两种颜色的二 值图像。算法的思想如下。 (1)将所有的白色像素(背景)赋 0,所有黑色像素(空穴所在)赋-1,空穴数置 0。 (2)寻找一个空穴的开始像素(值为-1),并将其值改为当前空穴数,存储,空穴数增加 1。 (3)所有像素的正向搜索。找到值为-1 的像素(表示没有被搜索过),正向搜索其周围有没有值为当前 空穴数的像素。如果有,将当前像素值赋以空穴数的值。 (4)所有像素的反向搜索。找到值为-1 的像素(表示没有被搜索过),反向搜索其周围有没有值为当前 空穴数的像素。如果有,将当前像素值赋以空穴数的值。 (5)如果正向和反相都没有像素,表示当前空穴所有像素已被遍历,转步骤 2。 (6)如果步骤 2 中没有寻找到开始像素,表示所有的空穴已被遍历。 10.2.2 空穴检出的 Visual C++实现 下面利用空穴检出的算法来实现对二值图像进行小面积区域的消除这样一个功能。编程的基本思想是先 进行空穴检出,然后检查每个空穴的面积,如果小于阈值,就将当前空穴所有的像素赋以背景值,进行消去。 函数 DIBHOLENUMBER 用来实现对二值图像的小面积区域消除功能。程序中小区域面积的阈值为 50 个像素点,代码如下。 /************************************************************************* * * 函数名称: * DIBHOLENUMBER() * * 参数: * CDib * pDib - 指向 CDib 类的指针 * * 返回值: * BOOL - 成功返回 True,否则返回 False * * 说明: * 该函数将消去图像中面积小于阈值的小区域 * *************************************************************************/ BOOL DIBHOLENUMBER(CDib *pDib) { // 指向源图像的指针 BYTE * lpSrc; Visual C++数字图像获取、处理及实践应用 · 482· //图像的宽度和高度 LONG lWidth; LONG lHeight; // 图像每行的字节数 LONG lLineBytes; //得到图像的宽度和高度 CSize SizeDim; SizeDim = pDib->GetDimensions(); lWidth = SizeDim.cx; lHeight = SizeDim.cy; //得到实际的 Dib 图像存储大小 CSize SizeRealDim; SizeRealDim = pDib->GetDibSaveDim(); // 计算图像每行的字节数 lLineBytes = SizeRealDim.cx; //图像数据的指针 LPBYTE lpDIBBits = pDib->m_lpImage; // 循环变量 int i, j, s, n; // 空穴的数目以及面积阈值 int nHoleNum, nMinArea; int nBlackPix, temp; // 正向和反响传播标志 int nDir1,nDir2; // 用来存储的一位数组 int *pnBinary; pnBinary =new int[lHeight*lLineBytes]; // 小区域的阈值面积为 20 个像素点 nMinArea = 50; // 将图像二值化 for (j = 0; j < lHeight; j++) 第 10 章 图像形状特征分析 · 483· { for(i = 0; i < lWidth; i++) { // 指向源图像倒数第 j 行,第 i 个像素的指针 lpSrc = (unsigned char *)lpDIBBits + lLineBytes * j + i; // 白色像素为背景,存成 0 if(*lpSrc > 200) { pnBinary[lLineBytes * j + i] = 0; } // 黑像素存成-1 else { pnBinary[lLineBytes * j + i] = -1; } } } // 空穴数赋初值 nHoleNum = 1; do { s=0; // 寻找每个空穴的初始像素值 for (j = 1; j < lHeight - 1; j++) { for(i = 1; i < lWidth - 1; i++) { // 找到初始像素 if(pnBinary[lLineBytes * j + i] == -1) { s = 1; // 将像素值改成当前的空穴数值 pnBinary[lLineBytes * j + i] = nHoleNum; // 跳出循环 j = lHeight; i = lLineBytes; } } } Visual C++数字图像获取、处理及实践应用 · 484· //没有初始像素,跳出循环 if(s == 0) break; else { do { // 正向和反响传播系数赋初值 0 nDir1 = 0; nDir2 = 0; // 正向扫描 for (j = 1; j < lHeight-1; j++) { for(i = 1; i < lWidth-1; i++) { nBlackPix = pnBinary[lLineBytes * j + i]; // 如果像素已经被扫描,或者是背景色,进行下一个循环 if(nBlackPix != -1) continue; // 如果上侧或者左侧的像素值已经被扫描,且属于当前的空穴,当前 的像素值 // 改成空穴的数值 nBlackPix=pnBinary[lLineBytes * (j-1) + i]; if(nBlackPix == nHoleNum) { pnBinary[lLineBytes * j + i] = nHoleNum; nDir1 = 1; continue; } nBlackPix =pnBinary[lLineBytes * j + i - 1]; if(nBlackPix == nHoleNum) { pnBinary[lLineBytes * j + i] = nHoleNum; nDir1 = 1; } } 第 10 章 图像形状特征分析 · 485· } // 正向像素全部被扫描,跳出循环 if(nDir1 == 0) break; // 反向扫描 for (j = lHeight-2; j >= 1 ; j--) { for(i = lWidth-2; i >= 1 ; i--) { nBlackPix = pnBinary[lLineBytes * j + i]; // 如果像素已经被扫描,或者是背景色,进行下一个循环 if(nBlackPix != -1) continue; // 如果下侧或者右侧的像素值已经被扫描,且属于当前的空穴,当前 的像素值 // 改成空穴的数值 nBlackPix=pnBinary[lLineBytes * (j+1) + i]; if(nBlackPix == nHoleNum) { pnBinary[lLineBytes * j + i] = nHoleNum; nDir2 = 1; continue; } nBlackPix =pnBinary[lLineBytes * j + i + 1]; if(nBlackPix == nHoleNum) { pnBinary[lLineBytes * j + i] = nHoleNum; nDir2 = 1; } } } if(nDir2 == 0) break; } while(1); Visual C++数字图像获取、处理及实践应用 · 486· } // 空穴数增加 nHoleNum++; } while(1); nHoleNum -- ; // 寻找面积小于阈值的空穴区域 for(n = 1; n <= nHoleNum; n++) { s = 0; for (j = 0; j < lHeight - 1; j++) { for(i = 0; i < lWidth - 1; i++) { nBlackPix =pnBinary[lLineBytes * j + i]; if(nBlackPix == n) s++; // 如果区域面积已经大于阈值,跳出循环 if(s > nMinArea) break; } if(s > nMinArea) break; } // 小于阈值的区域,赋以与背景一样的颜色,进行消去 if(s <= nMinArea) { for (j = 0; j < lHeight - 1; j++) { for(i = 0; i < lWidth - 1; i++) { nBlackPix =pnBinary[lLineBytes * j + i + 1]; if(nBlackPix == n) { pnBinary[lLineBytes * j + i + 1] = 0; } 第 10 章 图像形状特征分析 · 487· } } } } // 存储像素值,输出 for(j = 0; j < lHeight; j++) { // 列 for(i = 0; i < lWidth; i++) { // 二值图像 temp = pnBinary[j * lLineBytes + i] ; // 指向位图 i 行 j 列像素的指针 lpSrc = (unsigned char*)lpDIBBits + lLineBytes * j + i; // 更新源图像 if(temp != 0) * (lpSrc) = 0; else * (lpSrc) = 255; } } delete pnBinary; return true; } 在 ImageProcessingView.cpp 中对阈值区域消去子菜单添加命令处理函数,代码如下。 void CImageProcessingView::OnAnalysisHolenum() { // 消去二值图像中小于阈值面积的区域 // 更改光标形状 BeginWaitCursor(); // 获取文档 CImageProcessingDoc* pDoc = GetDocument(); // 获得图像 CDib 类的指针 Visual C++数字图像获取、处理及实践应用 · 488· CDib * pDib = pDoc->m_pDibInit; // 获得图像的头文件信息 LPBITMAPINFOHEADER lpBMIH=pDib->m_lpBMIH; // 判断是否是 8-bpp 位图 if (lpBMIH->biBitCount != 8) { // 提示用户 MessageBox("目前只支持 256 色位图的图像!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } ::DIBHOLENUMBER(pDib); // 设置脏标记 pDoc->SetModifiedFlag(TRUE); // 更新视图 pDoc->UpdateAllViews(NULL); // 恢复光标 EndWaitCursor(); } 图 10-2 所示为利用菜单实现的功能进行阈值面积区域消去的例子。 二值图像 阈值面积消去 图 10-2 阈值面积区域消去 第 10 章 图像形状特征分析 · 489· 10.3 图像的骨架检出 10.3.1 图像的市街区距离 二值图像的市街区距离的定义如下: 1 2 1 2Ad i i j j= − + − (10-5) 式中的 1 1(,)i j 是图像中黑色像素坐标值, 2 2(,)i j 是距离黑色像素最近的背景像素点(白色)的坐标。 图 10-3 所示是对图像中的物体检出其市街区距离,并用不同的灰度级来表示不同的市街区距离大小的 一个例子,它可以使我们对图像的市街区距离有一个直观的认识。 原图 市街区距离变换 图 10-3 二值图像的市街区距离变换 从图中可以看出根据物体各个像素到背景的最近距离的不同,形成了不同的市街区距离。颜色最深的 部分离背景区最远,颜色浅的次之。如果将相对于周围像素颜色最深的部分看作一个物体的骨架,只要将这 些颜色最深的部分提出,就可以将物体退化为一个骨架的形式,并可以由骨架来表示图像的特征。这正是要 编程实现的功能。 10.3.2 图像骨架提取的 Visual C++实现 函数 DIBFREAMEWORK 实现了对二值图像中的物体利用市街区距离提取其骨架的功能。 算法的基本思想是先对图像利用市街区距离进行变换,然后用每个像素的市街区和其周围像素的市街 区距离进行比较,如果周围像素的市街区距离比当前像素都要小的话,则当前像素留下作为物体的骨架,否 则消去为背景。 /************************************************************************* * * 函数名称: * DIBFREAMEWORK() * * 参数: * CDib * pDib - 指向 CDib 类的指针 * Visual C++数字图像获取、处理及实践应用 · 490· * 返回值: * BOOL - 成功返回 True,否则返回 False * * 说明: * 该函数利用市街区距离,生成图像的骨架 * *************************************************************************/ BOOL DIBFREAMEWORK(CDib *pDib) { // 指向源图像的指针 BYTE * lpSrc; //图像的宽度和高度 LONG lWidth; LONG lHeight; // 图像每行的字节数 LONG lLineBytes; //得到图像的宽度和高度 CSize SizeDim; SizeDim = pDib->GetDimensions(); lWidth = SizeDim.cx; lHeight = SizeDim.cy; //得到实际的 Dib 图像存储大小 CSize SizeRealDim; SizeRealDim = pDib->GetDibSaveDim(); // 计算图像每行的字节数 lLineBytes = SizeRealDim.cx; //图像数据的指针 LPBYTE lpDIBBits = pDib->m_lpImage; // 循环和临时变量 int i,j; int temp, s; // 存储像素值的数组 int *pnBinary, *pnStore; int nImageValue; 第 10 章 图像形状特征分析 · 491· pnBinary = new int[lHeight*lLineBytes]; pnStore = new int[lHeight*lLineBytes]; // 将图像二值化 for (j = 0; j < lHeight; j++) { for(i = 0; i < lWidth; i++) { // 指向源图像倒数第 j 行,第 i 个像素的指针 lpSrc = (unsigned char *)lpDIBBits + lLineBytes * j + i; // 白色像素为背景,存成 0 if(*lpSrc > 200) { pnBinary[lLineBytes * j + i] = 0; pnStore [lLineBytes * j + i] = 0; } // 黑像素存成 1 else { pnBinary[lLineBytes * j + i] = 1; pnStore [lLineBytes * j + i] = 1; } } } s = 1; while(s == 1) { s = 0; // 进行距离的累加 for (j = 1; j < lHeight - 1; j++) { for(i = 1; i < lWidth - 1; i++) { nImageValue = pnBinary[lLineBytes * j + i]; // 如果是背景,进行下一个循环 if(nImageValue == 0) continue; Visual C++数字图像获取、处理及实践应用 · 492· // 如果当前像素值和四周的值一样,像素值增加 1 if(nImageValue==pnBinary[lLineBytes * (j-1) + i] && nImageValue==pnBinary[lLineBytes * (j+1) + i]) if(nImageValue==pnBinary[lLineBytes * j + i-1] && nImageValue==pnBinary[lLineBytes * j + i+1]) { pnStore[lLineBytes * j + i]++; s=1; } } } // 在进行下一轮循环前将当前的结果储存 for (j = 0; j < lHeight; j++) for(i = 1; i < lWidth; i++) pnBinary[lLineBytes * j + i] = pnStore[lLineBytes * j + i]; } for (j = 0; j < lHeight; j++) for(i = 0; i < lWidth; i++) pnStore[lLineBytes * j + i] = 0; // 如果当前的像素值比周围的都要高,做为骨架 for (j = 1; j < lHeight - 1; j++) for(i = 1; i < lWidth - 1; i++) { nImageValue = pnBinary[lLineBytes * j + i] + 1; // 在四连通域进行比较 if(nImageValue!=pnBinary[lLineBytes * (j-1) + i] && nImageValue!=pnBinary[lLineBytes * (j+1) + i]) if(nImageValue!=pnBinary[lLineBytes * j + i-1] && nImageValue!=pnBinary[lLineBytes * j + i+1]) pnStore[lLineBytes * j + i] = pnBinary[lLineBytes * j + i]; } // 存储像素值,输出 for(j = 0; j < lHeight; j++) { // 列 for(i = 0; i < lWidth; i++) { // 骨架的像素值 第 10 章 图像形状特征分析 · 493· temp = pnStore[j * lLineBytes + i] ; // 指向位图 i 行 j 列像素的指针 lpSrc = (unsigned char*)lpDIBBits + lLineBytes * j + i; // 更新源图像,并将像素值进行变换,以便输出 if(temp != 0) * (lpSrc) = temp; else * (lpSrc) = 255; } } delete pnStore; delete pnBinary; return true; } 在 CImageProcessingView.cpp 中对图像骨架提取子菜单添加命令处理函数,代码如下。 void CImageProcessingView::OnStreetFramework() { // 街区距离骨架提取 // 更改光标形状 BeginWaitCursor(); // 获取文档 CImageProcessingDoc* pDoc = GetDocument(); // 获得图像 CDib 类的指针 CDib * pDib = pDoc->m_pDibInit; // 获得图像的头文件信息 LPBITMAPINFOHEADER lpBMIH=pDib->m_lpBMIH; // 判断是否是 8-bpp 位图 if (lpBMIH->biBitCount != 8) { // 提示用户 MessageBox("目前只支持 256 色位图!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; Visual C++数字图像获取、处理及实践应用 · 494· } ::DIBFREAMEWORK(pDib); // 设置脏标记 pDoc->SetModifiedFlag(TRUE); // 更新视图 pDoc->UpdateAllViews(NULL); // 恢复光标 EndWaitCursor(); } 利用菜单中的功能对图像中的物体实现骨架提取,如图 10-4 所示。 二值图象中的物体 骨架提取 图 10-4 物体的骨架提取 现在来考虑一个问题,如何将经骨架提取后的物体恢复成原来的模样?根据图像市街区距离的定义和 骨架提取的过程可以知道,如果一个点属于物体骨架的一部分,相应的街区距离为 a,那么在以该点为中心, 横轴和竖轴长度为(2a+1)的菱形范围内所有像素都应该属于这个物体。只要将每个骨架点周围的菱形距 离都赋成黑色像素,提取前物体的几何形状就可以得到恢复。 函数 DIBCHESSBOARDDISRESTORE 实现了将骨架恢复成原来物体的功能,代码如下。 /************************************************************************* * * 函数名称: * DIBCHESSBOARDDISRESTORE() * * 参数: * CDib * pDib - 指向 CDib 类的指针 * * 返回值: * BOOL - 成功返回 True,否则返回 False。 * 第 10 章 图像形状特征分析 · 495· * 说明: * 该函数利用骨架对原图像进行恢复。 * *************************************************************************/ BOOL DIBCHESSBOARDDISRESTORE(CDib *pDib) { // 指向源图像的指针 BYTE * lpSrc; //图像的宽度和高度 LONG lWidth; LONG lHeight; // 图像每行的字节数 LONG lLineBytes; //得到图像的宽度和高度 CSize SizeDim; SizeDim = pDib->GetDimensions(); lWidth = SizeDim.cx; lHeight = SizeDim.cy; //得到实际的 Dib 图像存储大小 CSize SizeRealDim; SizeRealDim = pDib->GetDibSaveDim(); // 计算图像每行的字节数 lLineBytes = SizeRealDim.cx; //图像数据的指针 LPBYTE lpDIBBits = pDib->m_lpImage; // 循环变量 int i, j, n, m; // 临时变量 int temp, s; // 用来存放像素值的数组 int *pnBinary, *pnStore; int nImageValue; pnBinary = new int[lHeight*lLineBytes]; Visual C++数字图像获取、处理及实践应用 · 496· pnStore = new int[lHeight*lLineBytes]; // 数组赋值 for (j = 0; j < lHeight; j++) { for(i = 0; i < lWidth; i++) { // 指向源图像倒数第 j 行,第 i 个像素的指针 lpSrc = (unsigned char *)lpDIBBits + lLineBytes * j + i; pnBinary[lLineBytes * j + i] = 0; if((*lpSrc) != 255) pnStore [lLineBytes * j + i] = (*lpSrc); else pnStore [lLineBytes * j + i] = 0; } } // 进行图像的恢复 for (j = 1; j < lHeight - 1; j++) { for(i = 1; i < lWidth - 1; i++) { nImageValue = pnStore[lLineBytes * j + i]; if(nImageValue == 0) continue; if(nImageValue == 1) { pnBinary[lLineBytes * j + i] = 1; continue; } // 如果像素值大于等于 2,将以(2×nImageValue)+1 的菱形范围的像素值赋 1 s = nImageValue; // 菱形的主轴 for(m = -s; m <= s; m++) { pnBinary[lLineBytes * j + i + m] = 1; } 第 10 章 图像形状特征分析 · 497· // 菱形的上半部分 for(n = -s; n < 0; n++) for(m = -s - n; m <=s + n; m++) pnBinary[lLineBytes * (j+n) + i+m] = 1; // 菱形的下半部分 for(n = 1; n <= s; n++) for(m = -s + n; m <= s - n; m++) pnBinary[lLineBytes * (j+n) + i+m] = 1; } } // 存储像素值,输出 for(j = 0; j < lHeight; j++) { // 列 for(i = 0; i < lWidth; i++) { // 骨架的像素值 temp = pnBinary[j * lLineBytes + i] ; // 指向位图 i 行 j 列像素的指针 lpSrc = (unsigned char*)lpDIBBits + lLineBytes * j + i; // 更新源图像 if(temp != 0) // 黑色像素输出 * (lpSrc) = 0; else // 白色像素输出 * (lpSrc) = 255; } } delete pnStore; delete pnBinary; return true; } 在 ImageProcessingView.cpp 中对骨架反变换子菜单添加命令处理函数,代码如下。 void CImageProcessingView::OnFrameRestore() { // 街区距离骨架复原 Visual C++数字图像获取、处理及实践应用 · 498· // 更改光标形状 BeginWaitCursor(); // 获取文档 CImageProcessingDoc* pDoc = GetDocument(); // 获得图像 CDib 类的指针 CDib * pDib = pDoc->m_pDibInit; // 获得图像的头文件信息 LPBITMAPINFOHEADER lpBMIH=pDib->m_lpBMIH; // 判断是否是 8-bpp 位图 if (lpBMIH->biBitCount != 8) { // 提示用户 MessageBox("目前只支持 256 色位图!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } ::DIBCHESSBOARDDISRESTORE(pDib); // 设置脏标记 pDoc->SetModifiedFlag(TRUE); // 更新视图 pDoc->UpdateAllViews(NULL); // 恢复光标 EndWaitCursor(); } 利用骨架反变换的功能对提取的骨架进行恢复,效果如图 10-5 所示。 原图 骨架提取 复原图 图 10-5 骨架复原 第 10 章 图像形状特征分析 · 499· 10.4 轮廓提取 10.4.1 基本概念 二值图像轮廓提取的算法很简单,只要将内部点掏空就可以得到图形的轮廓。如果原图中有某个像素 点是黑色像素,且它的 8 邻域都是黑色像素时,则该点是内部点,应将该点删除。 一个二值图像中物体的轮廓如图 10-6 所示。 原图 轮廓提取 图 10-6 二值图像的轮廓 10.4.2 Visual C++实现 函数 DIBOUTLINE 实现了对二值图像中的物体提取轮廓的功能,代码如下。 /************************************************************************* * * 函数名称: * DIBOUTLINE() * * 参数: * CDib * pDib - 指向 CDib 类的指针 * * 返回值: * BOOL - 成功返回 True,否则返回 False * * 说明: * 该函数对二值图像进行轮廓检出 * ************************************************************************* */ BOOL DIBOUTLINE(CDib *pDib) { Visual C++数字图像获取、处理及实践应用 · 500· // 指向源图像的指针 BYTE * lpSrc; //图像的宽度和高度 LONG lWidth; LONG lHeight; // 图像每行的字节数 LONG lLineBytes; //得到图像的宽度和高度 CSize SizeDim; SizeDim = pDib->GetDimensions(); lWidth = SizeDim.cx; lHeight = SizeDim.cy; //得到实际的 Dib 图像存储大小 CSize SizeRealDim; SizeRealDim = pDib->GetDibSaveDim(); // 计算图像每行的字节数 lLineBytes = SizeRealDim.cx; //图像数据的指针 LPBYTE lpDIBBits = pDib->m_lpImage; // 循环变量 int i, j; int nPixelValue; int n1,n2,n3,n4,n5,n6,n7,n8; // 用来存放像素值的数组 int *pnBinary; pnBinary = new int[lHeight*lLineBytes]; // 将图像二值化 for (j = 0; j < lHeight; j++) { for(i = 0; i < lWidth; i++) { 第 10 章 图像形状特征分析 · 501· // 指向源图像倒数第 j 行,第 i 个像素的指针 lpSrc = (unsigned char *)lpDIBBits + lLineBytes * j + i; // 白色像素为背景,存成 0 if(*lpSrc > 200) { pnBinary[lLineBytes * j + i] = 0; } // 黑像素存成 1 else { pnBinary[lLineBytes * j + i] = 1; } } } for (j = 1; j < lHeight - 1; j++) { for(i = 1; i < lWidth - 1; i++) { nPixelValue = pnBinary[lLineBytes * j + i]; lpSrc = (unsigned char *)lpDIBBits + lLineBytes * j + i; // 如果当前像素是白色,进入下一个循环 if(nPixelValue == 0) { // 将相应的像素值改成零 *lpSrc = 255; continue; } // 如果当前像素是黑色 else { // 检查周边的 8 连通域 n1 = pnBinary[lLineBytes * (j+1) + i + 1]; n2 = pnBinary[lLineBytes * (j+1) + i - 1]; n3 = pnBinary[lLineBytes * (j+1) + i]; n4 = pnBinary[lLineBytes * (j-1)+ i + 1]; n5 = pnBinary[lLineBytes * (j-1) + i - 1]; n6 = pnBinary[lLineBytes * (j-1) + i]; n7 = pnBinary[lLineBytes * j + i + 1]; n8 = pnBinary[lLineBytes * j + i - 1]; Visual C++数字图像获取、处理及实践应用 · 502· //如果相邻的八个点都是黑点 if(n1&&n2&&n3&&n4&&n5&&n6&&n7&&n8 == 1) { *lpSrc = (unsigned char)255; } } } } delete pnBinary; // 返回 return TRUE; } 在 ImageProcessingView.cpp 中对图像轮廓提取子菜单添加命令处理函数,代码如下。 void CImageProcessingView::OnOutline() { // 二值图像边界提取 // 更改光标形状 BeginWaitCursor(); // 获取文档 CImageProcessingDoc* pDoc = GetDocument(); // 获得图像 CDib 类的指针 CDib * pDib = pDoc->m_pDibInit; // 获得图像的头文件信息 LPBITMAPINFOHEADER lpBMIH=pDib->m_lpBMIH; // 判断是否是 8-bpp 位图 if (lpBMIH->biBitCount != 8) { // 提示用户 MessageBox("目前只支持 256 色位图!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } ::DIBOUTLINE(pDib); 第 10 章 图像形状特征分析 · 503· // 设置脏标记 pDoc->SetModifiedFlag(TRUE); // 更新视图 pDoc->UpdateAllViews(NULL); // 恢复光标 EndWaitCursor(); } 第第 1111 章章 图图像像分分割割 图像分割是一种重要的图像技术,在理论研究和实际应用中都得到了人们的广泛重视。图像分割的 方法和种类有很多,有些分割运算可直接应用于任何图像,而另一些分割运算只能适用于分割特殊类别 的图像。有些算法需要先对图像进行粗分割,因为它们需要从图像中提取出来的信息。例如,可以用对 图像的灰度级设置门限的方法分割。值得指出的是,没有惟一的标准的分割方法。许多不同种类的图像 或景物都可作为待分割的图像数据,不同类型的图像,已经有相对应的分割方法对其分割,同时,某些 分割方法也只是适用于某些特殊类型的图像分割。因为这一原因,本章介绍的算法不可能是标准的算法, 也不可能是对任何一幅图像都能成功分割的算法。分割结果的好坏需要根据具体的场合及要求衡量。图 像分割是从图像处理到图像分析的关键步骤,可以说,图像分割结果的好坏直接影响对图像的理解。 自然景物一般由多个目标组成,这些目标反映在图像中就是众多的区域,每个区域可以理解成具有 某种意义的最小单元。图像分割的任务是把图像分成互不交叠的有意义的区域,以便进一步的处理、分 析、应用。分开的区域一般是图像中我们感兴趣的目标。分割的精确程度影响甚至决定其他部分分析的 精确程度。图像分割是图像分析和理解的第一步,在如下的科学研究和工程技术领域有着广泛的应用。 (1)工业图像处理:矿藏分析、探伤分析、无接触式检测、自动化检测和识别、产品的精度和纯度 分析等。 (2)军事图像处理:军事目标检测和定位、地形配准、无人驾驶飞机、军事导航系统、地形侦察等。 (3)生物医学图像处理:计算机断层图像 CT(Computer Tomograph)、X 光透视、核磁共振图像(MRI)、 B 超体内病变检测、各种细胞自动计数、病毒细胞的自动检测和识别、生物图片分析等。 (4)图像传输:数字电视、高清晰度电视(HDTV)、多媒体信息处理、多媒体编码、传输、可视电 话、会议电视等。 (5)文本图像分析处理和识别:文字识别、版面分析和理解等。 (6)身份鉴定:指纹识别、虹膜识别等。 (7)机器人视觉:水下机器人、自动化生产线、无人驾驶汽车。 在数字计算时代来临以前,图像处理算法主要是模拟图像处理,自然,图像分割的算法也主要是通 过模拟的手段完成的。但是随着计算机性能价格比的不断提高以及有关数字处理方法的发展,数字图像 分割技术无论在科学研究上还是工业生产中得到了越来越多的应用。数字图像处理比模拟图像处理更为 方便、实用。更为重要的是,数字图像处理的某些算法用模拟方法是不可能实现的。不过,数字图像需 要处理的数据量大、数据处理相关性高,这就对图像分割的算法提出了很大的考验。在实时图像编码、 视频传输等领域,算法的速度也是一个重要考虑因素。 11.1 图像分割研究 11.1.1 图像分割定义 图像分割研究领域和其他领域的发展过程类似,发展至今,人们对图像分割提出了不同的解释和理 解。在不同的阶段,研究者们根据研究的水平和实际的要求提出了很多图像分割的定义,目前广为人接 受的是通过集合定义的图像分割。 令集合 R 代表整个图像区域,对 R 的图像分割可以看作是将 R 分成 N 个满足以下条件的非空子集第 11 章 图像分割 · 505· NRRR,,, 21  : (1) N R = Rii=1  (2)对 Ni ,2,1= , )( iRP =TRUE (3)对 ji,∀ ji ≠ ,有对 φ=ji RR  (4)对 ji,∀ ji ≠ )( ji RRP  =FALSE (5)对 Ni ,2,1= , iR 是连通的区域 P(Ri)=TRUE 指出在分割结果中,每个区域的像素有着相同的特性。P(Ri∪Rj)=FALSE 表示在分割 结果中,不同的子区域具有不同的特性,它们没有公共的特性。 N R = Rii=1  表示分割的所有子区域的并 集就是原来的图像,这一点非常重要,因为这一点是保证图像中每个像素都被处理的充分条件。 这些条件对分割也有一定的指导作用,但是,对上面的定义需要补充的是,实际的图像处理和分析 都是面向某种特定的应用,所以,条件中的各种关系也是需要和实际要求结合而设定的。迄今为之,还 没有找到一种通用的办法,可以把人类的要求完全转换成图像分割中的各种条件关系,所有的条件表达 式都是近似的。 11.1.2 图像分割的方法 分割问题的困难在于图像数据的模糊和噪声的干扰。前面已经提到,到目前为止,还没有一种或者 几种完善的分割方法,可以按照人们的意愿准确地分割任何一种图像。实际图像中景物情况各异,具体 问题具体分析,需要根据实际情况选择适合的方法。分割结果的好坏或者正确与否,目前还没有一个统 一的评价判断准则,分割的好坏必须从分割的效果和实际的应用场景来判断。不过在人类研究图像的历 史中,还是积累了很多经典的图像分割方法。虽然这些分割方法不适合于所有类型的图像分割,但是这 些方法却是图像分割方法进一步发展的基础。事实上,现代一些分割算法恰恰是从经典的分割方法衍生 出来的。 早期的图像研究中,图像分割的方法主要可以分成两大类。一类是边界方法,这种方法的假设是图 像分割结果的某个子区域在原来图像中一定会有边缘存在;一类是区域方法,这种方法的假设是图像分 割结果的某个子区域一定会有相同的性质,而不同区域的像素则没有共同的性质。这两种方法都有缺点 和优点,有的学者也试图把两者结合起来进行图像分割,至于如何结合满足人们的需要,已经超出了本 书考虑的范畴。本节将会着重介绍图像分割的经典算法。图 11-1 所示的都是图像分割的经典方法,随着 计算机处理能力的提高,很多方法不断涌现,如基于彩色分量分割、纹理图像分割。所使用的数学工具 和分析手段也是不断地扩展,从时域信号到频域信号处理,近来小波变换也应用在图像分割当中。 从图 11-1 所示可以看出,图像分割主要包括 4 种技术:并行边界分割技术、串行边界分割技术、并 行区域分割技术和串行区域分割技术。本章将会逐一介绍。 Visual C++数字图像获取、处理及实践应用 · 506· 图像分割 不连续 性检测 边界分割  孤立点  孤立线  组成边界  边界跟踪 区域分割  阈值分割  区域分裂 与合并  自适应 灰度 相似性 并 行 边 界 分割技术 串 行 边 界 分割技术 并 行 区 域 分割技术 串 行 区 域 分割技术 图 11-1 图像分割方法框架 11.2 并行边界分割 图像的边缘对人的视觉有重要意义,在图像编码、传输中,边缘检测都是很有意义的。某些实验结 果显示,生物的视觉系统似乎是利用边缘检测,如人类识别物体很大程度依赖边缘。当人看一个亮度渐 变的区域时,无论如何努力,都不能把这个区域分为轮廓分明的两部分。然而,如果此区域有亮度突变, 人们就立刻能感受到边缘的存在。图像分割的一种重要途径是通过边缘检测,即检测灰度级或者结构具 有突变的地方,表明一个区域的终结,也是另一区域开始的地方。这种不连续性称为边缘。不同的图像 灰度不同,边界处一般有明显的边缘,利用此特征可以分割图像。边缘特征不仅用于图像分割,也是纹 理分析等其他图像分析的重要信息源和形状特征基础。 边缘提取和分割是图像分析的经典研究课题之一,目前的理论和方法仍存在许多不足之处,仍在不 断改进和发展。需要说明的是:边缘与物体间的边界并不等同,边缘指的是图像中像素的值有突变的地 方,而物体间的边界指的是现实场景中的存在于物体之间的边界。有可能有边缘的地方并非边界,也有 可能边界的地方并无边缘,因为现实中的物体是三维的,而图像只具有二维信息,从三维到二维的投影 成象不可避免的会丢失一部分信息;另外成象过程中的光照和噪声也是不可避免的重要因素。正是因为 这些原因,基于边缘的图像分割仍然是当前图像研究中的世界级难题,目前研究者们正在试图在边缘提 取中加入高层的语义信息。 灰度突变有多种形式。常见的灰度突变有如图 11-2 所示的 4 种形式。需要指出的是,图像中的灰 度突变可能会有 1 种或者多种突变形式。 第 11 章 图像分割 · 507· (a)阶跃型 (c)阶跃型 (d)冲击型 (b)屋顶型 图 11-2 灰度突变的种类 11.2.1 边界检测的数学基础 梯度对应的是一阶导数的信息,梯度算子是一阶导数算子。在边缘灰度值过渡比较明显,而且在图 像模糊程度和噪声较小的情况下,梯度算子工作的情况是令人满意的。对一个连续的图像 ),( yxf ,在 位置 ),( yx 的梯度可以表示成一个矢量,假设用 xG 和 yG 来表示 ),( yxf 沿着 x 方向和 y 方向的梯度, 那么梯度矢量可以表示为: (,) (,)[](,) ∂   ∂ ∇ = = ∂   ∂  x y f x y G xf x y f x yG y (11-1) 梯度方向是图像灰度值变化最快的方向。 令 gθ 表示梯度方向: ( )xyg ff /tan 1−=θ (11-2) 在 gθ 方向的变化率的速度(就是梯度的幅度)为: 22 ),(),(),(),(       ∂ ∂+     ∂ ∂=∇= y yxf x yxfyxfyxg (11-3) g 为梯度算子,幅度计算是以 2 为模的(对应欧式距离)。当然幅度计算也可以采用其他等价的范 数,常用的两种计算方式为: f(x,y) f(x,y)g(x, y) x y ∂ ∂= +∂ ∂ (11-4)Visual C++数字图像获取、处理及实践应用 · 508· 或者采用无穷大范数: )),(,),(max(),( y yxf x yxfyxg ∂ ∂ ∂ ∂= (11-5) 事实上,现在计算机处理的图像大部分是数字图像,在数字图像处理领域中,根据实际的情况,上 述的微分经常用差分代替,其定义的形式为: ),1(),(),( yxfyxfyxf x −−= (11-6) )1,(),(),( −−= yxfyxfyxf y (11-7) 实际的图像处理中,二阶导数信息也是经常利用的。二阶导数信息在数字图像处理领域一般是基于 公式: 2 2 2 2 2 y f x ff ∂ ∂+ ∂ ∂=∇ (11-8) 二阶导数信息是一阶导数变化的标志,在有些情况中,如灰度变化均匀的图像,只利用一阶导数可 能找不到边界,此时二阶导数就能提供很有用的信息。一般来讲,图像处理的过程中,对图像可以采用 差分方法计算三阶或者更高阶导数,但是因为噪声的影响,三阶以上的导数信息往往失去了应用价值, 所以实际中的图像分割,往往只用到二阶导数。二阶导数还可以说明灰度突变的类型。若 ),(2 yxf∇ 在 像素点 (,)x y 处发生零交叉(就是 ),(2 yxf∇ 曲线和 x 坐标轴有交点),则 (,)x y 为阶跃型边缘点。若 在边缘点的二阶导数达到局部极小值,则 (,)x y 为屋顶型边缘点。由于利用了二阶微分信息,所以对噪 声更加敏感。解决的方法是先对图像进行平滑滤波,消除部分噪声,再进行边缘检测。不过,利用二阶 导数信息的算法是基于过零检测的,因此得到的边缘点数比较少,有利于后续的处理及识别等工作。 11.2.2 数字图像的边界检测 目前图像处理大多数都是针对数字图像的,为了计算方便,常用小区域模板进行卷积来近似计算梯 度。构造边缘检测算子的数学基础是一阶和二阶导数变化,二维图像 x 和 y 方向的导数变化用梯度表示。 对 xG 和 yG 各用一个模板,然后把两个模板组合起来构成一个梯度算子。虽然微分(差分)公式的形 式相同,但是图像处理自从采用模板的方式计算以来,已经提出了许多种不同的模板算子,而且在这些 模板和算子的研究过程中,研究者们也想出了很多实用的算法,并且每种方法具有不同的优缺点。下面 介绍数字图像处理领域常用的几种算子。 1. Roberts 算子     − 01 10     −10 01 从上面模板的形式可以看出,Roberts 计算时利用的像素数一共有 4 个,Roberts 算子边缘定位准, 但是对噪声敏感。适用于边缘明显而且噪声较少的图像分割。第 11 章 图像分割 · 509· 2. Prewitt 算子           − − − 101 101 101           −−− 111 000 111 Prewitt 算子对噪声有抑制作用,抑制噪声的原理是通过像素平均。但是像素平均相当于对图像的低 通滤波,所以 Prewitt 算子对边缘的定位不如 Roberts 算子。 3. Sobel 算子           − − − 101 202 101           −−− 121 000 121 Sobel 算子和 Prewitt 算子都是加权平均,但是 Sobel 算子认为,邻域的像素对当前像素产生的影响 不是等价的,所以距离不同的像素具有不同的权值,对算子结果产生的影响也不同。一般来说,距离越 大,产生的影响越小。 4. Isotropic Sobel 算子           − − − 101 202 101           −−− 121 000 121 Isotropic Sobel 算子也是加权平均算子,权值反比于邻点与中心点的距离,当沿不同方向检测边缘 时梯度幅度一致,就是通常所说的各向同性。 上面介绍的这些算子都是利用一阶导数信息,在实际的图像处理中,二阶导数也是经常利用的。下 面介绍的 Laplacian 算子就利用了二阶导数信息。Laplacian 算子是二阶微分算子,可以证明,它具有各 向同性,即与坐标轴方向无关,坐标轴旋转后梯度结果不变。 如果邻域系统是 4 邻域,Laplacian 算子的模板为:           −=∇ 010 141 010 2 如果邻域系统是 8 邻域,Laplacian 算子的模板为:           −=∇ 111 181 111 2 前面提过,Laplacian 算子对噪声比较敏感,所以图像一般先经过平滑处理,因为平滑处理也是用模 板进行的,所以,通常的分割算法都是把 Laplacian 算子和平滑算子结合起来生成一个新的模板。 11.2.3 并行边界分割的 Visual C++实现 有了以上的理论基础,就可以建立图像分割的函数库了。函数库包含并行边界、并行区域、串行边Visual C++数字图像获取、处理及实践应用 · 510· 界和串行区域 4 类分割函数。最后将在菜单中加入对二维图像实现图像分割的功能。11.2.2 节介绍了几 种并行边界的分割方法,下面是这些分割方法的具体实现函数。 1. Roberts 算子的实现函数如下: /************************************************************************* * * \函数名称: * RobertsOperator() * * \输入参数: * CDib * pDib - 指向 CDib 类的指针,含有原始图像信息 * double * pdGrad - 指向梯度数据的指针,含有图像的梯度信息 * * \返回值: * 无 * * \说明: * Roberts 算子 * * 并行边界分割 * ************************************************************************* */ void RobertsOperator(CDib * pDib, double * pdGrad) { // 遍历图像的纵坐标 int y; // 遍历图像的横坐标 int x; // 图像的长宽大小 CSize sizeImage = pDib->GetDimensions(); int nWidth = sizeImage.cx ; int nHeight = sizeImage.cy ; // 图像在计算机在存储中的实际大小 CSize sizeImageSave = pDib->GetDibSaveDim(); // 图像在内存中每一行像素占用的实际空间 int nSaveWidth = sizeImageSave.cx; // 图像数据的指针 LPBYTE pImageData = pDib->m_lpImage; 第 11 章 图像分割 · 511· // 初始化 for(y=0; yGetDocument(); CDib * pDib = pDoc->m_pDibInit; 第 11 章 图像分割 · 513· LPBITMAPINFOHEADER lpBMIH=pDib->m_lpBMIH; // 判断是否是 8-bpp 位图 if (lpBMIH->biBitCount != 8) { // 提示用户 MessageBox("目前只支持 256 色位图的图像分割!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } // 图像的长宽大小 CSize sizeImage = pDib->GetDimensions() ; int nWidth = sizeImage.cx ; int nHeight = sizeImage.cy ; // 指向梯度数据的指针 double * pdGrad; // 按照图像的大小开辟内存空间,存储梯度计算的结果 pdGrad=new double[nHeight*nWidth]; // 图像数据的指针 LPBYTE pImageData = pDib->m_lpImage; // 图像在计算机在存储中的实际大小 CSize sizeImageSave = pDib->GetDibSaveDim(); // 图像在内存中每一行像素占用的实际空间 int nSaveWidth = sizeImageSave.cx; // 应用 Roberts 算子求梯度 RobertsOperator(pDib, pdGrad); // 根据梯度计算结果进行显示 for(y=0; y50) *( pImageData+y*nSaveWidth+x )=0; else *( pImageData+y*nSaveWidth+x )=255; Visual C++数字图像获取、处理及实践应用 · 514· } // 释放梯度结果使用的内存空间 delete pdGrad; pdGrad=NULL; // 恢复光标形状 EndWaitCursor(); // 刷新屏幕 Invalidate(); } 用在菜单中实现的功能对图像采用 Roberts 算子进行图像分割,图 11-4 所示是分割结果。 图 11-4 Roberts 算子分割 正如前面分析的,Roberts 算子对边缘定位比较准,所以分割结果的边界宽度不象后面的 Prewitt 分 割结果那样宽。在图像噪声较少的情况下,分割的结果还是相当不错的。 2. Sobel 算子的实现函数如下: /************************************************************************* * * \函数名称: * SobelOperator() * * \输入参数: * CDib * pDib - 指向 CDib 类的指针,含有原始图像信息 * double * pdGrad - 指向梯度数据的指针,含有图像的梯度信息 * * \返回值: * 无 * * \说明: 第 11 章 图像分割 · 515· * Sobe 算子 * 并行边界分割 * ************************************************************************* */ void SobelOperator(CDib * pDib, double * pdGrad) { // 遍历图像的纵坐标 int y; // 遍历图像的横坐标 int x; // 图像的长宽大小 CSize sizeImage = pDib->GetDimensions() ; int nWidth = sizeImage.cx ; int nHeight = sizeImage.cy ; // 图像在计算机在存储中的实际大小 CSize sizeImageSave = pDib->GetDibSaveDim(); // 图像在内存中每一行像素占用的实际空间 int nSaveWidth = sizeImageSave.cx; // 图像数据的指针 LPBYTE lpImage = pDib->m_lpImage; // 初始化 for(y=0; yGetDocument(); CDib * pDib = pDoc->m_pDibInit; LPBITMAPINFOHEADER lpBMIH=pDib->m_lpBMIH; // 判断是否是 8-bpp 位图 if (lpBMIH->biBitCount != 8) { // 提示用户 MessageBox("目前只支持 256 色位图的图像分割!", "系统提示" , MB_ICONINFORMATION | MB_OK); Visual C++数字图像获取、处理及实践应用 · 518· // 返回 return; } // 图像的长宽大小 CSize sizeImage = pDib->GetDimensions() ; int nWidth = sizeImage.cx ; int nHeight = sizeImage.cy ; // 指向梯度数据的指针 double * pdGrad; // 按照图像的大小开辟内存空间,存储梯度计算的结果 pdGrad=new double[nHeight*nWidth]; // 图像数据的指针 LPBYTE lpImage = pDib->m_lpImage; // 图像在计算机在存储中的实际大小 CSize sizeImageSave = pDib->GetDibSaveDim(); // 图像在内存中每一行像素占用的实际空间 int nSaveWidth = sizeImageSave.cx; // 应用 Sobel 算子求梯度 SobelOperator(pDib, pdGrad); // 根据计算结果设置需要显示的值 for(y=0; y50) *( lpImage+y*nSaveWidth+x )=0; else *( lpImage+y*nSaveWidth+x )=255; } // 释放内存空间 delete []pdGrad; pdGrad=NULL; // 恢复光标形状 EndWaitCursor(); 第 11 章 图像分割 · 519· // 刷新屏幕 Invalidate(); } 用在菜单中实现的功能对图像采用 Sobel 算子进行图像分割,图 11-5 所示是分割结果。 图 11-5 Sobel 算子分割 Soble 算子对噪声有抑制作用,因此不会出现很多孤立的边缘像素点。不过从图 11-5 中可以看到, Sobel 算子对边缘的定位不是很准确,图像的边界宽度往往不止一个像素,不适合于对边缘定位的准确 性要求很高的应用。 3. Prewitt 算子的实现函数如下: /************************************************************************* * * \函数名称: * PrewittOperator() * * \输入参数: * CDib * pDib - 指向 CDib 类的指针,含有原始图像信息 * double * pdGrad - 指向梯度数据的指针,含有图像的梯度信息 * * \返回值: * 无 * * \说明: * Prewitt 算子 * 并行边界分割 * ************************************************************************* */ void PrewittOperator(CDib * pDib, double * pdGrad) { // 遍历图像的纵坐标 Visual C++数字图像获取、处理及实践应用 · 520· int y; // 遍历图像的横坐标 int x; // 图像的长宽大小 CSize sizeImage = pDib->GetDimensions(); int nWidth = sizeImage.cx ; int nHeight = sizeImage.cy ; // 图像在计算机在存储中的实际大小 CSize sizeImageSave = pDib->GetDibSaveDim(); // 图像在内存中每一行像素占用的实际空间 int nSaveWidth = sizeImageSave.cx; // 图像数据的指针 LPBYTE lpImage = pDib->m_lpImage; // 初始化 for(y=0; yGetDocument(); CDib * pDib = pDoc->m_pDibInit; LPBITMAPINFOHEADER lpBMIH=pDib->m_lpBMIH; // 判断是否是 8-bpp 位图 if (lpBMIH->biBitCount != 8) { // 提示用户 MessageBox("目前只支持 256 色位图的图像分割!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } // 图像的长宽大小 第 11 章 图像分割 · 523· CSize sizeImage = pDib->GetDimensions(); int nWidth = sizeImage.cx ; int nHeight = sizeImage.cy ; // 指向梯度数据的指针 double * pdGrad; // 按照图像的大小开辟内存空间,存储梯度计算的结果 pdGrad=new double[nHeight*nWidth]; //图像数据的指针 LPBYTE lpImage = pDib->m_lpImage; // 图像在计算机在存储中的实际大小 CSize sizeImageSave = pDib->GetDibSaveDim(); // 图像在内存中每一行像素占用的实际空间 int nSaveWidth = sizeImageSave.cx; // 应用 Prewitt 算子求梯度 PrewittOperator(pDib, pdGrad); // 根据结果设置需要显示的值 for(y=0; y50) *( lpImage+y*nSaveWidth+x )=0; else *( lpImage+y*nSaveWidth+x )=255; } // 释放内存空间 delete []pdGrad; pdGrad=NULL; // 恢复光标形状 EndWaitCursor(); // 刷新屏幕 Invalidate(); } Visual C++数字图像获取、处理及实践应用 · 524· 用在菜单中实现的功能对图像采用 Prewitt 算子进行图像分割,图 11-6 所示是分割结果。 图 11-6 Prewitt 算子分割 4. Laplacian 算子的实现函数如下: /************************************************************************* * * \函数名称: * LaplacianOperator() * * \输入参数: * CDib * pDib - 指向 CDib 类的指针,含有原始图像信息 * double * pdGrad - 指向梯度数据的指针,含有图像的梯度信息 * * \返回值: * 无 * * \说明: * LaplacianOperator 算子是二阶算子,不象 Roberts 算子那样需要两个模板计算 * 梯度,LaplacianOperator 算子只要一个算子就可以计算梯度。但是因为利用了 * 二阶信息,对噪声比较敏感 * * 并行边界分割 * ************************************************************************* */ void LaplacianOperator(CDib * pDib, double * pdGrad) { // 遍历图像的纵坐标 int y; // 遍历图像的横坐标 第 11 章 图像分割 · 525· int x; // 图像的长宽大小 CSize sizeImage = pDib->GetDimensions() ; int nWidth = sizeImage.cx ; int nHeight = sizeImage.cy ; // 图像在计算机在存储中的实际大小 CSize sizeImageSave = pDib->GetDibSaveDim(); // 图像在内存中每一行像素占用的实际空间 int nSaveWidth = sizeImageSave.cx; // 图像数据的指针 LPBYTE lpImage = pDib->m_lpImage; // 初始化 for(y=0; yGetDocument(); CDib * pDib = pDoc->m_pDibInit; LPBITMAPINFOHEADER lpBMIH=pDib->m_lpBMIH; // 判断是否是 8-bpp 位图 if (lpBMIH->biBitCount != 8) { // 提示用户 MessageBox("目前只支持 256 色位图的图像分割!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } // 图像的长宽大小 CSize sizeImage = pDib->GetDimensions() ; int nWidth = sizeImage.cx ; int nHeight = sizeImage.cy ; // 指向梯度数据的指针 double * pdGrad; // 按照图像的大小开辟内存空间,存储梯度计算的结果 pdGrad=new double[nHeight*nWidth]; // 图像数据的指针 LPBYTE lpImage = pDib->m_lpImage; // 图像在计算机在存储中的实际大小 CSize sizeImageSave = pDib->GetDibSaveDim(); // 图像在内存中每一行像素占用的实际空间 int nSaveWidth = sizeImageSave.cx; // 应用 Laplacian 算子求梯度 LaplacianOperator(pDib, pdGrad); Visual C++数字图像获取、处理及实践应用 · 528· for(y=0; y50) *( lpImage+y*nSaveWidth+x )=0; else *( lpImage+y*nSaveWidth+x )=255; } // 释放内存空间 delete []pdGrad; pdGrad=NULL; // 恢复光标形状 EndWaitCursor(); // 刷新屏幕 Invalidate(); } 用在菜单中实现的功能对图像采用 Laplacian 算子进行图像分割,图 11-7 所示是分割结果。 图 11-7 Laplacian 算子分割 因为 Laplacian 算子对噪声比较敏感,所以分割结果中在一些像素上出现了散碎的边缘像素点。不 过 Laplacian 算子对边缘的定位还是比较准的。 11.3 串行边界分割 11.2 节提到的并行边缘图像分割方法,对图像的每一点上所作的处理不依赖于其他的点处理结 果,因此这些方法“并行地”(即同时在所有点上)运用于图像。“并行”算法可以在并行计算机上实第 11 章 图像分割 · 529· 现并行计算。本节论述的分割方法,在处理图像点时不但利用了本身像素的信息,而且利用前面已经 处理过像素的结果。对某个像素的处理,以及是否把它分类成为边界点,和先前对其他点处理得到的 信息有关。 串行边界分割技术是指采用串行的方法对目标边界检测来实现分割的方法。串行边界技术通常是通 过顺序的搜索边缘点来工作的,一般有 3 个关键的步骤,这些步骤每一个确定或者解决的好坏,直接影 响到分割结果的好坏。 (1)起始边缘点的确定。 (2)搜索准则,将根据这个准则确定下一个边缘点。 (3)终止条件,设定搜索过程结束的条件。 串行边界分割技术是一类重要的图像分割技术。它可以和其他方法结合进行图像分割。 对普通计算机而言,串行分割法比并行分割法有潜在的优点。并行分割方法必须在每个图像点完成 相同的计算,因为接受或放弃这个像素点的惟一依据就是它本身的计算结果。但是这种计算带来的问题 是:如果希望分割是可靠的,这些计算可能会相当的复杂,因为不得不在处理该像素的时候,考虑邻域 甚至更多其他像素。采用串行分割则可采用简单的、方便的计算。 11.3.1 边界跟踪 边界跟踪是由梯度图中一个边缘点出发,搜索并连接边缘点进而逐步检测所有边界的方法。11.2 节 提到的并行边缘图像分割方法,边缘像素不一定能够组成闭合的曲线,因为边界上有可能会遇到缺口。 缺口可能太大而不能用一条直线或者曲线连接,也有可能不是同一条边界上的缺口。边界跟踪的方法则 可以在一定程度上解决这些问题,对于某些图像,这种方法的分割结果更好。图 11-8 所示中的 C 点是当 前像素,P 是上一个边界点。 P C P C P C P C P C P C P C P C 图 11-8 边界跟踪示意图 Visual C++数字图像获取、处理及实践应用 · 530· 先对原图像进行梯度计算,梯度计算可以采用 11.2 节中的方法。按照串行边界分割的 3 步可以按照 如下方法对图像进行分割。 (1)起始点:对梯度图搜索,找到梯度最大点,作为边界跟踪的开始点。 (2)生长准则:在 C 的 8 邻域像素中,梯度最大的点被当作边界,同时这个点还会作为下一个搜索 的起始点。 (3)终止条件:按照(2)的准则一直搜索,直到梯度绝对值小于一个阈值时,搜索停止。 有时为了保证边界的光滑性,每次只是在标有阴影的候选像素中选择。这样得到的边界点不但能够 保证连通性,而且还能保持光滑性。根据 P 点和 C 点的位置,共有图 11-8 所示的 8 种情形,也就是 P 点分别在 C 的 8 邻域不同位置的所有情况。 11.3.2 边界跟踪的 Visual C++实现 这一节建立实现边界跟踪的函数库,程序中需要指定边界跟踪的跟踪条件,本函数是通过寻找当前 像素 8 邻域的最大梯度点来确定边界跟踪的下一个像素的。最后在菜单中添加对二维图像实现边界跟踪 的功能。 实现边界跟踪的函数如下。 /************************************************************************* * * \函数名称: * EdgeTrack() * * \输入参数: * CDib * pDib - 指向 CDib 类的指针,含有原始图像信息 * unsigned char * pUnEdgeTrack - 指向边界跟踪结果的指针 * * \返回值: * 无 * * \说明: * pUnEdgeTrack 指针指向的数据区存储了边界跟踪的结果,其中 1(逻辑)表示 * 对应像素为边界点,0 表示为非边界点 * * 串行边界分割 * ************************************************************************* */ void EdgeTrack(CDib * pDib, unsigned char * pUnEdgeTrack) { // 对图像 8 邻域进行遍历时使用的数组 static int nDx[8]={-1,-1,-1, 0, 0, 1, 1, 1}; static int nDy[8]={-1, 0, 1,-1, 1,-1, 0, 1}; // 遍历图像的纵坐标 int y; 第 11 章 图像分割 · 531· // 遍历图像的横坐标 int x; // 图像的长宽大小 CSize sizeImage = pDib->GetDimensions() ; int nWidth = sizeImage.cx ; int nHeight = sizeImage.cy ; // 指向梯度数据的指针 double * pdGrad; // 按照图像的大小开辟内存空间,存储梯度计算的结果 pdGrad=new double[nHeight*nWidth]; // 调用 Roberts 算子求梯度 RobertsOperator(pDib, pdGrad); // 定义当前像素梯度值 double dCurrGrad = 0; // 定义最大梯度值 double dMaxGrad; // 设置初值 dMaxGrad = 0; // 最大梯度值对应的像素点坐标 int nPx; int nPy; nPx = 0; nPy = 0; // 求梯度最大值所在的像素点坐标 for(y=0; y10) { // 设置当前点为边界点 pUnEdgeTrack[nPy*nWidth + nPx] = 255 ; dMaxGrad = 0 ; for(i=0; i<8; i++) { nDetX=nDx[i]; nDetY=nDy[i]; y = nPy + nDetY; x = nPx + nDetX; // 判断是否在图像内部 if(x>=0 && x=0 && y dMaxGrad) && ( pUnEdgeTrack[y*nWidth + x] == 0) ) { dMaxGrad = pdGrad[y*nWidth + x] ; 第 11 章 图像分割 · 533· yy = y; xx = x; } } } // 下一个边界点的梯度,横纵坐标 dCurrGrad = dMaxGrad ; nPy = yy; nPx = xx; } // 释放内存 delete pdGrad; pdGrad = NULL; } 在 ImageProcessingView.cpp 中对边界跟踪子菜单添加命令处理函数,代码如下。 void CImageProcessingView::OnEdgeTrack() { // TODO: Add your command handler code here // 更改光标形状 BeginWaitCursor(); // 获得 Doc 类的指针 CImageProcessingDoc * pDoc = (CImageProcessingDoc *)this->GetDocument(); // 获得 CDib 类的指针 CDib * pDib = pDoc->m_pDibInit; LPBITMAPINFOHEADER lpBMIH=pDib->m_lpBMIH; // 判断是否是 8-bpp 位图 if (lpBMIH->biBitCount != 8) { // 提示用户 MessageBox("目前只支持 256 色位图的图像分割!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } // 图像在计算机在存储中的实际大小 Visual C++数字图像获取、处理及实践应用 · 534· CSize sizeImageSave = pDib->GetDibSaveDim(); // 图像在内存中每一行像素占用的实际空间 int nSaveWidth = sizeImageSave.cx; // 遍历图像的纵坐标 int y; // 遍历图像的横坐标 int x; // 图像的长宽大小 CSize sizeImage = pDib->GetDimensions() ; int nWidth = sizeImage.cx ; int nHeight = sizeImage.cy ; // 指向图像数据的指针 LPBYTE lpImage ; lpImage = pDib->m_lpImage ; // 边界跟踪后的结果区域 unsigned char * pUnEdgeTrack ; pUnEdgeTrack = new unsigned char[nWidth * nHeight] ; // 调用跟踪函数进行边界跟踪 EdgeTrack(pDib, pUnEdgeTrack); for(y=0; yGetDimensions() ; int nWidth = sizeImage.cx ; int nHeight = sizeImage.cy ; // 图像在计算机在存储中的实际大小 CSize sizeImageSave = pDib->GetDibSaveDim(); // 图像在内存中每一行像素占用的实际空间 int nSaveWidth = sizeImageSave.cx; // 图像数据的指针 LPBYTE pImageData = pDib->m_lpImage; for(y=0; yGetDocument(); CDib * pDib = pDoc->m_pDibInit; LPBITMAPINFOHEADER lpBMIH=pDib->m_lpBMIH; // 判断是否是 8-bpp 位图 if (lpBMIH->biBitCount != 8) { // 提示用户 MessageBox("目前只支持 256 色位图的图像分割!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } // 调用函数实现固定阈值分割 RegionSegFixThreshold(pDib,200); // 恢复光标形状 EndWaitCursor(); // 刷新屏幕 Invalidate(); } 上面代码中的语句 RegionSegFixThreshold(pDib,200)表示在分割中使用的固定阈值为 200,分割的 结果如图 11-14 所示。 图 11-14 固定阈值分割 第 11 章 图像分割 · 541· 最后再次指出,阈值的指定需要根据实际的图像考虑,而不是所有的图像都可以采用统一的阈值。 2. 自适应阈值分割的实现函数如下: /************************************************************************* * * \函数名称: * RegionSegAdaptive() * * \输入参数: * CDib * pDib - 指向 CDib 类的指针,含有原始图像信息 * * \返回值: * 无 * * \说明: * 1(逻辑)表示对应像素为前景区域,0 表示背景 * 阈值分割的关键问题在于阈值的选取。阈值的选取一般应该视实际的应用而 * 灵活设定。本函数中,阈值不是固定的,而是根据图像像素的实际性质而设定的。 * 这个函数把图像分成 4 个子图像,然后计算每个子图像的均值,根据均值设置阈值 * 阈值只是应用在对应的子图像 * * 自适应并行区域分割 * ************************************************************************* */ void RegionSegAdaptive(CDib * pDib) { // 遍历图像的纵坐标 int y; // 遍历图像的横坐标 int x; // 图像的长宽大小 CSize sizeImage = pDib->GetDimensions() ; int nWidth = sizeImage.cx ; int nHeight = sizeImage.cy ; // 图像在计算机存储中的实际大小 CSize sizeImageSave = pDib->GetDibSaveDim(); // 图像在内存中每一行像素占用的实际空间 int nSaveWidth = sizeImageSave.cx; Visual C++数字图像获取、处理及实践应用 · 542· // 图像数据的指针 LPBYTE lpImage = pDib->m_lpImage; // 局部阈值 int nThd[2][2] ; // 子图像的平均值 int nLocAvg ; // 对左上图像逐点扫描: nLocAvg = 0 ; // y 方向 for(y=0; yGetDocument(); CDib * pDib = pDoc->m_pDibInit; LPBITMAPINFOHEADER lpBMIH=pDib->m_lpBMIH; Visual C++数字图像获取、处理及实践应用 · 546· // 判断是否是 8-bpp 位图 if (lpBMIH->biBitCount != 8) { // 提示用户 MessageBox("目前只支持 256 色位图的图像分割!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } // 自适应区域分割 RegionSegAdaptive(pDib); // 恢复光标形状 EndWaitCursor(); // 刷新屏幕 Invalidate(); } 自适应阈值分割的结果如图 11-15 所示。 图 11-15 自适应阈值分割 11.5 串行区域分割 串行区域分割是指采用串行处理策略对目标区域直接检测来实现分割的方法。串行区域分割的特点 是整个处理过程可以分解为顺序的多个步骤依次进行。串行区域分割一般可以分成两种方法:一是区域 生长,二是分裂合并。 11.5.1 区域生长 区域生长是指从某个像素出发,按照一定的准则,逐步加入邻近像素,当满足一定的条件时,区域 生长终止。图 11-16 所示给出了区域生长的一个示例,其中邻域系统采用 4 邻域,图 11-16 (a)中,带第 11 章 图像分割 · 547· 有阴影的像素为初始的种子点,假设生长准则是种子点和所考虑像素灰度值差的绝对值,并且小于或等 于某个阈值 T,就将该像素包括到该种子像素所在的区域。图 11-16(b)中给出了 T=1 时的区域生长 结果,图像被分成了 4 个区域。图 11-16(c)给出了 T=3,种子点为图 11-16(a)中像素值为 2 和 11 的两个像素,结果是整个图像被分成了 2 个区域。图 11-16(d)给出了 T=5,种子点为图 11-16(a)中 像素值为 2 的像素,生长的结果是最后全图像变成了一个区域。从这个例子可以看出:关系区域生长结 果的好坏有以下 3 个条件: (1)初始点(种子点)的选取; (2)生长准则; (3)终止条件。 Á 2 Á 5 Á 3 Á 3 Á 2 Á 5 Á 4 Á 4 Á 8 Á 8 Á 9 Á 8 Á 10 Á 11 Á 10 Á 10 Á 2 Á 4 Á 8 Á 10 Á 10 Á 10 Á 2 Á 2 Á 2 Á 4 Á 4 Á 4 Á 8 Á 8 Á 8 Á 10 (a) (b) Á 2 Á 11 Á 11 Á 11 Á 2 Á 2 Á 2 Á 11 Á 2 Á 2 Á 2 Á 2 Á 11 Á 11 Á 11 Á 11 Á 2 Á 2 Á 2 Á 2 Á 2 Á 2 Á 2 Á 2 Á 2 Á 2 Á 2 Á 2 Á 2 Á 2 Á 2 Á 2 (c) (d) 图 11-16 区域生长示例 初始点的选择,可以是人工加入的交互信息,也就是告诉计算机初始点。当不清楚初始点应该在什 么位置时,可以让计算机自己选取种子点进行区域生长。具体步骤如下。 (1)对图像顺序扫描,找到第一个还没有归属的像素,设该像素为 ),( 00 yx 。 (2) 以 ),( 00 yx 为中心,考虑 ),( 00 yx 的 4 邻域像素 ),( yx ,如果 ),( yx 满足生长准则,则将 ),( yx 与 ),( 00 yx 和并,同时将 ),( yx 压入堆栈。 (3)从堆栈中取出一个像素,把它当作 ),( 00 yx ,回到步骤(2)。 (4)当堆栈为空时,回到步骤(1)。 (5)重复(1)~(4),直到图像中的每个点都有归属时,生长结束。 Visual C++数字图像获取、处理及实践应用 · 548· 11.5.2 分裂合并 区域生长是从某个或者某些像素点出发,最后得到整个区域,进而实现目标提取。分裂合并差不多 是区域生长的逆过程:从整个图像出发,不断分裂得到各个子区域,然后再把前景区域合并,实现目标 提取。分裂合并的假设是对于一幅图像,前景区域由一些相互连通的像素组成的,因此,如果把一副图 像分裂到像素级,那么就可以判定该像素是否为前景像素,当所有像素点或者子区域完成判断以后,把 前景区域或者像素合并就可得到前景目标。 11.5.3 区域生长的 Visual C++实现 这一节我们建立实现区域生长的函数库。正如前面讲述的那样,区域生长有需要根据实际情况解决 3 个问题,在本书中,初始的种子点选择图像的中心点,生长准则是像素值的绝对值小于设定的阈值,终止 条件是一直遍历到没有符合生长条件的像素为止。最后在菜单中添加对二维图像实现区域生长的功能。 区域生长的实现代码如下。 /************************************************************************* * * \函数名称: * RegionGrow() * * \输入参数: * CDib * pDib - 指向 CDib 类的指针,含有原始图像信息 * unsigned char * pUnRegion - 指向区域生长结果的指针 * * \返回值: * 无 * * \说明: * pUnRegion 指针指向的数据区存储了区域生长的结果,其中 1(逻辑)表示 * 对应像素为生长区域,0 表示为非生长区域 * 区域生长一般包含三个比较重要的问题: * 1. 种子点的选取 * 2. 生长准则 * 3. 终止条件 * 可以认为,这三个问题需要具体分析,而且每个问题解决的好坏直接关系到 * 区域生长的结果。 * 本函数的种子点选取为图像的中心,生长准则是相邻像素的像素值小于 * nThreshold, 终止条件是一直进行到再没有满足生长准则需要的像素时为止 * * 串行区域分割 * ************************************************************************* */ void RegionGrow(CDib * pDib, unsigned char * pUnRegion, int nThreshold) 第 11 章 图像分割 · 549· { // 遍历图像的 4 邻域时使用的数组 static int nDx[]={-1,0,1,0}; static int nDy[]={0,1,0,-1}; // 图像的长宽大小 CSize sizeImage = pDib->GetDimensions() ; int nWidth = sizeImage.cx ; int nHeight = sizeImage.cy ; // 图像在计算机在存储中的实际大小 CSize sizeImageSave = pDib->GetDibSaveDim(); // 图像在内存中每一行像素占用的实际空间 int nSaveWidth = sizeImageSave.cx; // 初始化 memset(pUnRegion,0,sizeof(unsigned char)*nWidth*nHeight); // 种子点 int nSeedX, nSeedY; // 设置种子点为图像的中心 nSeedX = nWidth /2 ; nSeedY = nHeight/2 ; // 定义堆栈,存储坐标 int * pnGrowQueX ; int * pnGrowQueY ; // 分配空间 pnGrowQueX = new int [nWidth*nHeight]; pnGrowQueY = new int [nWidth*nHeight]; // 图像数据的指针 unsigned char * pUnchInput =(unsigned char * )pDib->m_lpImage; // 定义堆栈的起点和终点 // 当 nStart=nEnd, 表示堆栈中只有一个点 int nStart ; int nEnd ; // 初始化 nStart = 0 ; Visual C++数字图像获取、处理及实践应用 · 550· nEnd = 0 ; // 把种子点的坐标压入栈 pnGrowQueX[nEnd] = nSeedX; pnGrowQueY[nEnd] = nSeedY; // 当前正在处理的像素 int nCurrX ; int nCurrY ; // 循环控制变量 int k ; // 图像的横纵坐标,用来对当前像素的 4 邻域进行遍历 int xx; int yy; while (nStart<=nEnd) { // 当前种子点的坐标 nCurrX = pnGrowQueX[nStart]; nCurrY = pnGrowQueY[nStart]; // 对当前点的 4 邻域进行遍历 for (k=0; k<4; k++) { // 4 邻域像素的坐标 xx = nCurrX+nDx[k]; yy = nCurrY+nDy[k]; // 判断像素(xx,yy) 是否在图像内部 // 判断像素(xx,yy) 是否已经处理过 // pUnRegion[yy*nWidth+xx]==0 表示还没有处理 // 生长条件:判断像素(xx,yy)和当前像素(nCurrX,nCurrY) 像素值差的绝对值 if ( (xx < nWidth) && (xx>=0) && (yy=0) && (pUnRegion[yy*nWidth+xx]==0) && abs(pUnchInput[yy*nSaveWidth+xx] – pUnchInput[nCurrY*nSaveWidth+nCurrX])GetDocument(); CDib * pDib = pDoc->m_pDibInit; LPBITMAPINFOHEADER lpBMIH=pDib->m_lpBMIH; // 判断是否是 8-bpp 位图 if (lpBMIH->biBitCount != 8) { // 提示用户 MessageBox("目前只支持 256 色位图的图像分割!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } // 遍历图像的纵坐标 Visual C++数字图像获取、处理及实践应用 · 552· int y; // 遍历图像的横坐标 int x; // 图像在计算机存储中的实际大小 CSize sizeImageSave = pDib->GetDibSaveDim(); // 图像在内存中每一行像素占用的实际空间 int nSaveWidth = sizeImageSave.cx; // 图像的长宽大小 CSize sizeImage = pDib->GetDimensions() ; int nWidth = sizeImage.cx ; int nHeight = sizeImage.cy ; // 指向图像数据的指针 LPBYTE lpImage ; lpImage = pDib->m_lpImage ; unsigned char * pUnchRgRst = new unsigned char[nWidth * nHeight]; // 初始化 memset(pUnchRgRst, 0 , sizeof(unsigned char)*nWidth*nHeight ); // 调用函数进行区域生长 RegionGrow(pDib, pUnchRgRst); for(y=0; y= 0 && (i+x) < nWidth ) { // 利用高斯系数对图像进行数字滤波 dDotMul += (double)pUnchImg[y*nWidth + (i+x)] * pdKernel[nHalfLen+i]; dWeightSum += pdKernel[nHalfLen+i]; } } pdTmp[y*nWidth + x] = dDotMul/dWeightSum ; } } // y 方向进行滤波 for(x=0; x= 0 && (i+y) < nHeight ) { dDotMul += (double)pdTmp[(y+i)*nWidth + x] * pdKernel[nHalfLen+i]; dWeightSum += pdKernel[nHalfLen+i]; } } pUnchSmthdImg[y*nWidth + x] = (unsigned char)(int)dDotMul/dWeightSum ; } } // 释放内存 delete []pdKernel; pdKernel = NULL ; delete []pdTmp; pdTmp = NULL; } 2. 产生一个一维高斯分布函数,这个函数用在高斯平滑函数中生成高斯滤波系数 /************************************************************************* * * \函数名称: * MakeGauss() * * \输入参数: * double sigma - 高斯函数的标准差 * double **pdKernel - 指向高斯数据数组的指针 * int *pnWindowSize - 数据的长度 * * \返回值: * 无 * * \说明: * 这个函数可以生成一个一维的高斯函数的数字数据,理论上高斯数据的长度应 * 该是无限长的,但是为了计算的简单和速度,实际的高斯数据只能是有限长的 * pnWindowSize 就是数据长度 * ************************************************************************* */ void MakeGauss(double sigma, double **pdKernel, int *pnWindowSize) { 第 11 章 图像分割 · 557· // 循环控制变量 int i ; // 数组的中心点 int nCenter; // 数组的某一点到中心点的距离 double dDis ; // 中间变量 double dValue; double dSum ; dSum = 0 ; // 数组长度,根据概率论的知识,选取[-3*sigma, 3*sigma]以内的数据。 // 这些数据会覆盖绝大部分的滤波系数 *pnWindowSize = 1 + 2 * ceil(3 * sigma); // 中心 nCenter = (*pnWindowSize) / 2; // 分配内存 *pdKernel = new double[*pnWindowSize] ; // 生成高斯数据 for(i=0; i< (*pnWindowSize); i++) { dDis = (double)(i - nCenter); dValue = exp(-(1/2)*dDis*dDis/(sigma*sigma)) / (sqrt(2*PI) * sigma ); (*pdKernel)[i] = dValue ; dSum += dValue; } // 归一化 for(i=0; i<(*pnWindowSize) ; i++) { (*pdKernel)[i] /= dSum; } } 3. 计算方向导数,利用原图像计算图像像素的方向导数 /************************************************************************* * * \函数名称: Visual C++数字图像获取、处理及实践应用 · 558· * DirGrad() * * \输入参数: * unsigned char *pUnchSmthdImg - 经过高斯滤波后的图像 * int nWidht - 图像宽度 * int nHeight - 图像高度 * int *pnGradX - x 方向的方向导数 * int *pnGradY - y 方向的方向导数 * \返回值: * 无 * * \说明: * 这个函数计算方向导数,采用的微分算子是(-1 0 1) 和 (-1 0 1)’(转置) * 计算的时候对边界像素采用了特殊处理 * ************************************************************************* */ void DirGrad(unsigned char *pUnchSmthdImg, int nWidth, int nHeight, int *pnGradX , int *pnGradY) { // 循环控制变量 int y ; int x ; // 计算 x 方向的方向导数,在边界出进行了处理,防止要访问的像素出界 for(y=0; y abs(gx)) { // 计算插值的比例 weight = fabs(gx)/fabs(gy); g2 = pnMag[nPos-nWidth] ; g4 = pnMag[nPos+nWidth] ; // 如果 x,y 两个方向的方向导数的符号相同 // C 是当前像素,与 g1-g4 的位置关系为: // g1 g2 // C // g4 g3 Visual C++数字图像获取、处理及实践应用 · 562· if (gx*gy > 0) { g1 = pnMag[nPos-nWidth-1] ; g3 = pnMag[nPos+nWidth+1] ; } // 如果 x,y 两个方向的方向导数的符号相反 // C 是当前像素,与 g1-g4 的位置关系为: // g2 g1 // C // g3 g4 else { g1 = pnMag[nPos-nWidth+1] ; g3 = pnMag[nPos+nWidth-1] ; } } // 如果方向导数 x 分量比 y 分量大,说明导数的方向更加“趋向”于 x 分量 // 这个判断语句包含了 x 分量和 y 分量相等的情况 else { // 计算插值的比例 weight = fabs(gy)/fabs(gx); g2 = pnMag[nPos+1] ; g4 = pnMag[nPos-1] ; // 如果 x,y 两个方向的方向导数的符号相同 // C 是当前像素,与 g1-g4 的位置关系为: // g3 // g4 C g2 // g1 if (gx*gy > 0) { g1 = pnMag[nPos+nWidth+1] ; g3 = pnMag[nPos-nWidth-1] ; } // 如果 x,y 两个方向的方向导数的符号相反 // C 是当前像素,与 g1-g4 的位置关系为: // g1 // g4 C g2 // g3 else 第 11 章 图像分割 · 563· { g1 = pnMag[nPos-nWidth+1] ; g3 = pnMag[nPos+nWidth-1] ; } } // 下面利用 g1-g4 对梯度进行插值 { dTmp1 = weight*g1 + (1-weight)*g2 ; dTmp2 = weight*g3 + (1-weight)*g4 ; // 当前像素的梯度是局部的最大值 // 该点可能是个边界点 if(dTmp>=dTmp1 && dTmp>=dTmp2) { pUnchRst[nPos] = 128 ; } else { // 不可能是边界点 pUnchRst[nPos] = 0 ; } } } //else } // for } } 6. 根据梯度计算及经过非最大值抑制后的结果设定阈值,这个函数也是 Canny 算子的重要部分, 阈值的设定直接涉及到哪些像素点可能为边界点 /************************************************************************* * * \函数名称: * EstimateThreshold() * * \输入参数: * int *pnMag - 梯度幅度图 * int nWidth - 图像数据宽度 * int nHeight - 图像数据高度 * int *pnThdHigh - 高阈值 * int *pnThdLow - 低阈值 * double dRatioLow - 低阈值和高阈值之间的比例 * double dRatioHigh - 高阈值占图像像素总数的比例 Visual C++数字图像获取、处理及实践应用 · 564· * unsigned char *pUnchEdge - 经过 non-maximum 处理后的数据 * * \返回值: * 无 * * \说明: * 经过 non-maximum 处理后的数据 pUnchEdge,统计 pnMag 的直方图,确定阈值。 * 本函数中只是统计 pUnchEdge 中可能为边界点的那些像素。然后利用直方图, * 根据 dRatioHigh 设置高阈值,存储到 pnThdHigh。利用 dRationLow 和高阈值, * 设置低阈值,存储到*pnThdLow。dRatioHigh 是一种比例:表明梯度小于 * *pnThdHigh 的像素数目占像素总数目的比例。dRationLow 表明*pnThdHigh * 和*pnThdLow 的比例,这个比例在 canny 算法的原文里,作者给出了一个区间 * ************************************************************************* */ void EstimateThreshold(int *pnMag, int nWidth, int nHeight, int *pnThdHigh,int *pnThdLow, unsigned char * pUnchEdge, double dRatioHigh, double dRationLow) { // 循环控制变量 int y; int x; int k; // 该数组的大小和梯度值的范围有关,如果采用本程序的算法, // 那么梯度的范围不会超过 pow(2,10) int nHist[1024] ; // 可能的边界数目 int nEdgeNb ; // 最大梯度值 int nMaxMag ; int nHighCount ; nMaxMag = 0 ; // 初始化 for(k=0; k<1024; k++) { nHist[k] = 0; } // 统计直方图,然后利用直方图计算阈值 第 11 章 图像分割 · 565· for(y=0; y= nThdHigh)) { // 设置该点为边界点 pUnchEdge[nPos] = 255; TraceEdge(y, x, nThdLow, pUnchEdge, pnMag, nWidth); } } } // 那些还没有被设置为边界点的像素已经不可能成为边界点 for(y=0; y=nLowThd) { // 把该点设置成为边界点 pUnchEdge[yy*nWidth+xx] = 255 ; 第 11 章 图像分割 · 569· // 以该点为中心进行跟踪 TraceEdge(yy, xx, nLowThd, pUnchEdge, pnMag, nWidth); } } } 9. Canny 算子实现 /************************************************************************* * * \函数名称: * Canny() * * \输入参数: * unsigned char *pUnchImage - 图像数据 * int nWidth - 图像数据宽度 * int nHeight - 图像数据高度 * double sigma - 高斯滤波的标准方差 * double dRatioLow - 低阈值和高阈值之间的比例 * double dRatioHigh - 高阈值占图像像素总数的比例 * unsigned char *pUnchEdge - canny 算子计算后的分割图 * * \返回值: * 无 * * \说明: * canny 分割算子,计算的结果保存在 pUnchEdge 中,逻辑 1(255)表示该点为 * 边界点,逻辑 0(0)表示该点为非边界点。该函数的参数 sigma,dRatioLow * dRatioHigh,是需要指定的。这些参数会影响分割后边界点数目的多少 ************************************************************************* */ void Canny(unsigned char *pUnchImage, int nWidth, int nHeight, double sigma, double dRatioLow, double dRatioHigh, unsigned char *pUnchEdge) { // 经过高斯滤波后的图像数据 unsigned char * pUnchSmooth ; // 指向 x 方向导数的指针 int * pnGradX ; // 指向 y 方向导数的指针 int * pnGradY ; // 梯度的幅度 Visual C++数字图像获取、处理及实践应用 · 570· int * pnGradMag ; pUnchSmooth = new unsigned char[nWidth*nHeight] ; pnGradX = new int [nWidth*nHeight] ; pnGradY = new int [nWidth*nHeight] ; pnGradMag = new int [nWidth*nHeight] ; // 对原图像进行滤波 GaussianSmooth(pUnchImage, nWidth, nHeight, sigma, pUnchSmooth) ; // 计算方向导数 DirGrad(pUnchSmooth, nWidth, nHeight, pnGradX, pnGradY) ; // 计算梯度的幅度 GradMagnitude(pnGradX, pnGradY, nWidth, nHeight, pnGradMag) ; // 应用 non-maximum 抑制 NonmaxSuppress(pnGradMag, pnGradX, pnGradY, nWidth, nHeight, pUnchEdge) ; // 应用 Hysteresis,找到所有的边界 Hysteresis(pnGradMag, nWidth, nHeight, dRatioLow, dRatioHigh, pUnchEdge); // 释放内存 delete pnGradX ; pnGradX = NULL ; delete pnGradY ; pnGradY = NULL ; delete pnGradMag ; pnGradMag = NULL ; delete pUnchSmooth ; pUnchSmooth = NULL ; } 最后在 ImageProcessingView.cpp 中对 Canny 算子子菜单添加命令处理函数,代码如下。 void CImageProcessingView::OnEdgeCanny() { // TODO: Add your command handler code here // 更改光标形状 BeginWaitCursor(); // 获得文档类指针 CImageProcessingDoc * pDoc = (CImageProcessingDoc *)this->GetDocument(); // 获得 Cdib 类的指针 第 11 章 图像分割 · 571· CDib * pDib = pDoc->m_pDibInit; LPBITMAPINFOHEADER lpBMIH=pDib->m_lpBMIH; // 判断是否是 8-bpp 位图 if (lpBMIH->biBitCount != 8) { // 提示用户 MessageBox("目前只支持 256 色位图的图像分割!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } // 循环控制变量 int y; int x; // 获得图像的大小 CSize sizeImage = pDib->GetDimensions(); int nWidth = sizeImage.cx ; int nHeight= sizeImage.cy ; int nSaveWidth = pDib->GetDibSaveDim().cx; // 开辟内存,存储图像数据 unsigned char * pUnchImage = new unsigned char[nWidth*nHeight]; for(y=0; ym_lpImage[y*nSaveWidth+x]; } } // canny 算子计算后的结果 unsigned char * pUnchEdge = new unsigned char[nWidth*nHeight]; // 调用 canny 函数进行边界提取 Canny(pUnchImage, nWidth, nHeight, 0.4, 0.4, 0.79, pUnchEdge) ; for(y=0; ym_lpImage[y*nWidth+x]=(unsigned char)(255-pUnchEdge[y*nWidth+x]); } } // 释放内存 delete []pUnchImage; pUnchImage = NULL ; delete []pUnchEdge ; pUnchEdge = NULL ; // 恢复光标形状 EndWaitCursor(); // 刷新屏幕 Invalidate(); } 在上一段的程序中,代码 Canny(pUnchImage, nWidth, nHeight, 0.4, 0.4, 0.79, pUnchEdge) 中的数字 0.4,0.4,0.79 是需要初始设定的,具体的意义请参见源程序。需要说明的是,利用 Canny 对图像进行 分割时,初始参数的设定会影响分割的结果。一般来说,函数 Canny(unsigned char *pUnchImage, int nWidth, int nHeight, double sigma, double dRatioLow, double dRatioHigh, unsigned char *pUnchEdge)的参数 dRatioHigh 设置的值越高,检测出来的边缘点数目会越少。利用 Canny 算子分割图像的结果如图 11-18 所示。 图 11-18 Canny 算子分割结果 第第 1122 章章 图图像像的的模模式式识识别别 前面已经介绍了图像的变换、增强、复原等技术,它们都是对输入图像的某种有效的改善,其输出 仍然是一幅完整的图像。 随着数字图像处理技术的发展和实际应用的需求,出现了另一类问题,就是不要求其结果输出是一 幅完整图像的本身,而是将经过上述处理后的图像,再经过分割和描述提取有效的特征,进而加以判决 分类。例如要从遥感图像中分割出各种农作物、森林资源、矿产资源等,并进一步判断其产量或蕴藏量、 由气象云图结合其他气象观察数据进行自动天气预报、用人工地震波形图寻找有油的岩层结构、根据医 学 x 光图像断层分析各种病变、邮政系统中的信函自动分拣等等。因此,可以认为把图像进行区别分类 就是图像的模式识别。模式识别方法和应用十分广泛,也相当复杂,正在发展之中。模式识别的研究对 象基本上可概括为两大类:一类是有直觉形象的如图像、相片、图案、文字等,一类是没有直觉形象而 只有数据或信息波形如语声、心电脉冲、地震波等。但是,对模式识别来说,无论是数据、信号还是平 面图形或立体景物都是除掉他们的物理内容而找出它们的共性,把具有同一共性的归为一类,而具有另 一种共性者归为另一类。模式识别研究的目的是研制能够自动处理某些信息的机器系统,以便代替人完 成分类和辨识的任务。 一个图像识别系统可分为 3 个主要部分,如图 12-1 所示。第一部分是图像信息的获取,它相当于对 被研究对象的调查和了解,从中得到数据和材料,对图像识别来说就是把图片、底片、文字、图形等用 光电扫描设备变换为电信号以备后续处理。第二部分是信息的加工与处理。它的作用在于把调查了解到 的数据材料进行加工、整理、分析、归纳以去伪存真,去粗取精,抽出能反映事物本质的特征。当然, 抽取什么待征,保留多少特征与采用何种判决有很大关系。第三部分是判决或分类。这相当于人们从感 性认识上升到理性认识而做出结论的过程。第三部分与特征拍取的方式密切相关。它的复杂程度也依赖 于特征的抽取方式。例如,类似度、相关性、最小距离等。 待识别图像 图像信息获取 信息处理特征 提取 判决 结果 图 12-1 图像的模式识别简单框图 模式识别已初步形成 3 大类:统计模式识别、结构模式识别、模糊集识别。在这里,只介绍统计识 别的内容,并介绍一些常用的算法。 本章的内容安排是这样的,第一节讲述模板匹配的概念以及快速算法;第二节将介绍统计模式识别 的概念,并重点的给出线性分类器的概念和相关算法;第三节中,将给出模糊模式识别和结构模式识别 的概念。如果读者想更深入的了解模式识别方面的内容,请参阅相关的文献资料。 12.1 图像匹配 在介绍模式识别之前,先讲述一下图像匹配的概念。主要包括模板匹配及其他快速算法。因为,模 板匹配的概念以及相应的快速算法在模式识别中经常用到。 Visual C++数字图像获取、处理及实践应用 · 574· Á 12.1.1 模板匹配 在机器识别事物的过程中,常需把不同传感器或同一传感器在不同时间,不同成象条件下对同一景 物获取的两幅或多幅图像在空间上对准,或根据已知模式到另一幅图中寻找相应的模式,这就叫做匹配。 在遥感图像处理中需把不同波段传感器对同一景物拍的多光谱图像按象点对应一一套准,然后根据象点 的性质进行地物分类。如果利用在不同时间对同一地面拍摄得两幅照片,经套准后找出其中那些特征有 了变化的象点,就可以用来分析图中哪些部分发生了变化。其他如对序列图像匹配求光流场,以它描述 三维动态景物,计算刚体的空间结构和运动参量等,凡此种种,匹配技术都起着至关重要的作用,自然 地受到人们的很大重视,并已提出众多的匹配方法。 如图 12-2 所示,图中有若干个目标,现在需要寻找一下有无三角形的图像。若在被搜索图中有待寻 找的目标,且模板有一样的尺寸和方向,它的基本原则就是通过相关函数的计算找到它以及在被搜索图 中的位置。 图 12-2 模板(b)与被搜索图(a) 设模板 T 叠放在搜索图 S 上平移,模板覆盖下的那块搜索图叫做子图 jiS , ,i、j 为这块子图的左上 角象点在 S 图中的坐标,称为参考点,不难从图 12-3 中看出,i 和 j 的取值范围为 1,1 +−<< MNji N-M-1 N-M-1 M-1 M-1 M M 图 12-3 模板(b)与被搜索图(a) 第 12 章 图像的模式识别 · 575· 现在可以比较 T 和 jiS , 的内容。若两者一致,则 T 和 S 之差为零。所以可以用下列两种测度之一来 衡量 T 和 jiS , 的相似程度: , 2 1 1 (,) [ (,) (,)] MM i j m n D i j S m n T m n = = = −∑∑ (12-1) 或者: , 1 1 (,) | (,) (,)| MM i j m n D i j S m n T m n = = = −∑∑ (12-2) 如果展开前一个式子,则有: , 2 , 2 1 1 1 1 1 1 (,) [ (,)] [ (,) (,)] [(,)] MMMMMM i j i j m n m n m n Dij Smn SmnTmn Tmn = = = = = = = − × +∑∑ ∑∑ ∑∑ (12-3) 右边第三项表示模板的总能量,是一个常数,与(i, j)无关,第一项是模板覆盖下那块图像子图的 能力,它随(i, j)位置而缓慢改变,第二项是子图像和模板的互相关,随(i, j)而改变。T 和 jiS , 匹配 时这项取值最大,因此可用下列相关函数作相似性测度: , 1 1 , 2 1 1 [ ( , ) ( , )] (,) [ ( , )] MM i j m n MM i j m n S m n T m n R i j S m n = = = = × = ∑∑ ∑∑ (12-4) 或者归一化为: , 1 1 , 2 2 1 1 1 1 [ ( , ) ( , )] (,) [ ( , )] [ ( , )] MM i j m n MMMM i j m n m n S m n T m n R i j S m n T m n = = = = = = × = ∑∑ ∑∑ ∑∑ (12-5) 根据施瓦兹不等式可以知道,式 12-5 中 1),(0 << jiR ,并且仅在比值 ),( ),(, nmT nmS ji 为常数时取极 大值(等于 1)。式 12-5 可写成更简洁的内积形式,令 ),(1 jiS 表示子图,t 表示模板,则有 1 1 1 (,)(,) (,)(,) T TT t S i jR i j t t S i j S i j = (12-6) 当矢量 t 和 ),(1 jiS 之间的夹角为零时,即 ),(1 jiS =kt 时,这里 k 为标量常数,有 R(i,j)=1,否 则 R(i,j)<1。用相关法求匹配的计算量很大,因为模板要在(N— M 十 1)个参考位置上做相关计算,其 中除一点以外都是在非匹配点上做无用功。因此,人们希望有一种快速计算方法,人们提出一类叫序贯 相似性检测的算法,简称 SSDA,其要点如下: Visual C++数字图像获取、处理及实践应用 · 576· (1)定义绝对误差值。 TnmTjiSnmSnmji kk ji kk ji kk ˆ),(),(ˆ),(),,,(,, +−−=ε 其中: ∑∑ = = = M m M n ji nmSMjiS 1 1 , 2 ),(1),(ˆ ∑∑ = = = M m M n nmTMjiT 1 1 2 ),(1),(ˆ (2)取一不变阈值 Tk。 (3)在子图 ),(, nmS ji 中随机选取象点。计算它同 T 中对应点的误差值,然后把这个差值和其他 点对的差值累加起来,当累加 r 次误差超过 Tk,则停止累加,并记下次数 r,定义 SSDA 的检测曲面为 Á1 (,) {|min[(,, , ) ]}k k kr m I i j r i j m n Tε ≤ ≤ = ≥ (12-7) (4)把 I(i,j)值大的(i,j)点作为匹配点,因为这点上需要很多次累加才使总误差超过 Tk,见图 12-4, 图中给出了在 A、B、C3 个参考点上得到的误差累计增长曲线。A、B、C 反映模板 T 不在匹配点上,这 时总误差增长很快,超出阈值,曲线 C 中总误差增长很慢,很可能是一套准的候选点。 I(i,j) Ti=Tk 图 12-4 TÁ为常数时的累计误差增长曲线 对 SSDA 算法还可以进一步改进计算效率,办法是: (1)对于(N-M+1)个参考点的选用顺序可以不逐点推进,即模板不一定对每点都平移到,例如可 用粗-细结合的均匀搜索,即先每隔 m 点搜一下匹配好坏,然后在有极大匹配值周围的局部范围内对各 参考点位置求匹配。这策略能否不丢失真正匹配点,将取决于表面 I(i,j)的平滑性和单峰性。 (2)在某参考点(i,j)处,对模板覆盖下的 M2 个点对的计算顺序可用与 i、j 无关的随机方式计算误 差。也可采用适应图像内容的方式,按模板中突出特征选取伪随机序列,决定计算误差的先后顺序,以 便及早抛弃那些非匹配点。 (3)模板在(i,j)点得到的累计误差映射为上述曲面数值的方法,是否最佳还可以探索。 (4)不选用固定阈值 Tk,而改用单调增长的阈值序列,使非匹配点用更少的计算就达到阈值而被丢 弃,真匹配点则需更多次误差累计才达到阈值,如图 12-5 所示。由于除一点以外,绝大多数的情况下都第 12 章 图像的模式识别 · 577· 是对非匹配点计算的,显然,越早丢弃非匹配点将越节省时间。 n n ε∑ 以前的常数阈值 n 不需要计算 的区域 增加计算的 区域 图 12-5 用单调增加阈值序列的情形 SSDA 方法比用 FFT 相关法快 50 倍,是较受人重视的一种算法。对于二值图,SSDA 还可简化, 这时模板与对应子图中的成对象点的差值为: ),(),( ),(),( , ,,, nmTnmS STTSnmTnmS ji jijiji ⊕= −=− 式中 ⊕ 表示异或处理(模 2 加),由此得到: , 1 1 , 1 1 (,)(,)(,) (,)(,) MM i j m n MM i j m n D i j S m n T m n S m n T m n = = = = = − = ⊕ ∑∑ ∑∑ (12-8) 这常被称为二进制的 Hamming 距离,D 越小,则子图同模板越相似。 12.1.2 其他快速匹配方法 下面以地形和地图匹配的技术为例,讨论加快匹配的其他算法,文中提到的实时图是指飞行器实时 测量地面所得到的数据图。而飞行器预存在机内的已知地面的数据图叫做基准图,两图的匹配计算可以 对地形进行分析,并可以指示出飞行器当前的位置。 由前面讨论可知,任何一种匹配算法的总计算量由所采用的相关算法的计算量与搜索位置数之积, 即: 总计算量=(相关算法的计算量)×(搜索位置数) 来决定的。因此,为了减少总的计算量,除上面介绍的方法以外再介绍几种方法。 1. 幅度排序相关算法 这种算法由两个步骤组成,第一步把实时图中的各个灰度值按幅度大小排成列的形式,然后再对它 进行二进制(或三进制)编码,最后,根据二进位排序的诸列,把实时图变换成二进制阵列的一个有序Visual C++数字图像获取、处理及实践应用 · 578· 的集合{Cn,n=1,2,⋯, N}。这一过程称之为幅度排序的预处理。第二步,序贯地将这些二进制阵列 与基准图进行由粗到细的相关,直到确定出匹配点为止。这里,为了说明这种算法的原理,举一个简单 的 3×3 实时图的例子。 第一步:预处理。 首先把 3x 3 实时图中各个灰度值按大小次序排成一列,并算出各个灰度值在原图中的位置(j, k), 如图 12-6 所示。 然后把排序后的灰度幅度值分成数目相等的两组,且幅度大的一组赋值为 1,而幅度小的一组赋值 为 0,若幅度数为奇数,则中间的那个幅度就规定为×,如图 12-6(b)所示。进一步,把每一组分成两半, 并同样地赋予 1 值和 0 值。这个过程一直进行到各组划分为一个单元为止,并由此形成二进制排序。 图 12-6 3×3 实时图的预处理 于是,根据二进制排序的次序①、②、③和各个二进制值及其位置,便可构成 Cl、C2、C3 等二进 制阵列,如图 12-7 所示。同理,对于一般情况可得{Cn,n=1,2,⋯, N},此处 N 为二进制排序的分 层数。 图 12-7 本例的二进制阵列 第二步 由粗到细的相关过程。 首先,用 C1 阵列与基准图阵列作相关运算,得 ÁÁ 1 , , ,, ( , ) 1 ( , ) 0 (,) j u k v j u k v j k j k C j k C j k u v X Xϕ + + + + = = = −∑ ∑ (12-9) 这意味着,当 C1 阵列放在基准图的某一搜索位置(u, v)上时,与 C1 中的 1 值所对应的基准图的像元 值之和减去与 C1 中的 0 值所对应的基准图的像元值之和(而与 C1 中×号所对应的基准图像元,则被忽 略之)。所以 ),(1 vuϕ 实际上是一比特量化实时图与基准图的积相关函数,它反映了实时图中最粗糙的第 12 章 图像的模式识别 · 579· 图像结构的信息(即一种高低表示)与基准图的相关。 ),(1 vuϕ 称为基本的相关面。 在基准图全区域的搜索过程中,若设定一个门限值 T1,并舍弃那些 11 ),( Tvu <ϕ 的试验点,则就 可以大大减少下一轮搜索时的试验位置数。 然而,在 11 ),( Tvu >ϕ 的试验位置上,再进行细的相关运算,这可以用下式来计算: ÁÁ 2 1 , , ,, ( , ) 1 ( , ) 0 1(,)(,){}2 j u k v j u k v j k j k C j k C j k u v u v X Xϕ ϕ + + + + = = = + −∑ ∑ (12-10) 同理,为了减少有争议的匹配点的数目,设门限值为 T2,并在 22 ),( Tvu >ϕ 的试验位置上,以 C2 为基础进行更细的相关运算。 }{2 1),(),( 0),( , , 1),( , ,223 ÁÁ ∑∑ = ++ = ++ −+= kjC kj vkuj kjC kj vkuj XXvuvu ϕϕ 再设门限 T3 等,依次类推,可得到第 n 个相关面为: 1 , ,2 ,, ( , ) 1 ( , ) 0 1(,)(,){}2n n j u k v j u k v j k j k C j k C j k u v u v X Xϕ ϕ − + + + + = = = + −∑ ∑ (12-11) 当设置门限值为 Tn 时,若以 nn Tvu >),(**ϕ 的位置只有一个,就宣布该位置 ),(** vu 为匹配位置。 显然,各个门限值有如下的关系: Tn>Tn-1>⋯> Tn >T1 因此,逐次细化相关的试验位置将越来越少,直到找出匹配位置时为止。利用此方法减少了总的计 算量,亦即提高相关的处理速度。 这种二进制位的幅度排序算法(即 BARC 算法),所需要的加法总计算量为: 1 1 2 2 1 2( 1)( 1)q k M N M N N N= − + − + (12-12) 其中 k 是一个在 1 和 2 之间的常数,而 M1、M2 和 N1、N2 分别为基准图和实时图的尺寸。 2. FFT 的相关算法 由傅立叶分析中的相关定理可知,两个函数在定义域中的卷积等于它们在频域中的乘积,而相关则 是卷积的一种特定形式。因之,存在着另一种计算相关函数的方法。但是这样做在时间上并没有多少可 取之处,但由于快速傅立叶变换技术比直接法计算速度提高了一个数量级,为此用 FFT 进行领域相关计 算也是一种可行的方法。 首先把基准图和实时图进行二维离散博里叶变换(DFT),对于基准图,有: 1 1 2 0 0 1(,)(,) MM uj vk MM j k X u v x j kM ω ω − − − − = = = ∑ ∑ (12-13) 其中 u 和 v 分别表示 j 和 k 方向上的频率变量,且 )2exp( MjM πω ≡ Visual C++数字图像获取、处理及实践应用 · 580· 并假定基准图的尺寸是 M×M 维的。实时图的离散傅立叶变换 Y(u, v)是用同样的方法计算得到的。然后, 由相关定理可以写出相关的离散傅立叶变换 ),( vuφ 为: ),(),(),(* vuYvuXvu =φ 为此,对 ),( vuφ 求傅立叶反变换,就可以得出空间域中的相关函数 ),( kjφ 为 1 1 * 0 0 (,) [(,)* (,)] MM uj vk M M u v j k X u v Y u vφ ω ω − − = = = ∑ ∑ (12-14) 其中*为共扼运算的符号。 由此可见,相关函数可以通过 DFT 的方法计算出来。而计算 DFT 最有效的方法就是采用 FFT 算法, 在一般的教科书上都可以找到。所以这里略去对它的讨论。 最后根据以上的关系式,可以画出 FFT 的相关算法流程图,如图 12-8 所示。显然,这种相关算法 可以容易地推广到 FFT 的归一化相关算法。 Ф(j, k) Y(u, v) X(u, v) 扩充的 FFT 扩充的 FFT 复共轭相乘 FFT Á1 x(j, k) y(j, k) 图 12-8 FFT 相关算法 如果测试图像的像元素和试验位置数越大,那么,相对地说,这种算法在时间上的节省就更大。但 是,要注意,由于博里叶变换是个周期性函数,因此匹配点会周期地出现,所以在运算时,必须采取其 他适当的措施才行。 3. 分层搜索的序贯判决算法 这种分层搜索算法是直接基于人们先粗后细寻找事物的惯例而形成的,例如,在世界地图上找北京 的位置时,可以先找出中国这个广阔的地域,称其为粗相关,然后在这个地域内,再仔细确定北京的位 置,这叫做细相关。很明显,利用这种找法,就可以很快地找到北京的位置。所以由这种思想形成的分 层搜索算法具有相当高的处理速度。如 BARC 算法一样,它也是由两个步骤组成的。 第一步:预先处理。 首先,对被匹配的图像进行分层预处理,方法是将图 2×2 维的邻区逐个网络地进行平均处理,从 而得到一个分辨率较低和维数较小的图像。然后,将此图像再用同样的方法处理后,得到一个分辨率更 低和维数更小的图像。依次进行下去,如果一共进行了 X 次分层处理的话,那么就可以得到 X 个处理后 的图像。于是,加上原图像后,便可构成一组分辨率由高到低,而维数由大到小的图像序列。这种技巧 称之为分层预处理。 如果将上述分层技术应用于基准图和实时图,就可以获得两个这样的图像序列基准图,另一个是实 时图的,它们分别表示为: 第 12 章 图像的模式识别 · 581· :()2 2 :()2 2 k k k k k k MMX NNY × × (12-15) 其中 k=0,1,2,⋯, L,且已假设基准图是 M×M 维的,而实时图是 N×N 维的。 因为原图的分辨率最高.且维数最大,所以 k=0 的图像 Xo 和 Yo 具有最高的分辨率,而 k=L 的图 像 XL 和 YL,则具有最低的分辨率。 如前所述,若采用先粗后细的相关方法,则可以很快地找到匹配点。所以,第一次相关搜索是从分 辨率最低和维数最小的一对图像 Xk、Yk (K=L)开始的。这时,由于 Yk 的象元数比较少,加上损失了一部 分高频传息,所以,在粗相关的过程中,正确截获概率 k CP 将是不大的,所以,为了提高 k CP 应设法改善 XL 和 YL 的信噪比,例如,将较高分辨率的 Xk-1(或 Yk-1 )通过低通滤波器以后.再以二倍于它的空间采 样间隔进行采样,而得到的 Xk (或 Yk )比用直接分层法得到的具有更高的信噪比。因此,相对地说, 提高了 k CP 。显然,其他各层,亦作同样的处理。这种技术称之为分层搜索顶处理。 第二步:先粗后细的相关过程。 如前所述,第一次相关是从 Xk 和 Yk 开始的,为了找到可能的粗匹配位置,应将 Xk 在 Yk 的所有搜 索位置上进行相关,并确定出粗匹配位置 ),( kk vu 。因为这时 Xk 和 Yk 的维数最小,所以搜索过程是很 快的,但是这时 k CP 值较小,故可能产生若干个可能的粗匹配位置。第二次相关是在较高分辨率的图像 Xk-1 和 Yk-1 之间进行的。这时,因为已经知道了可能的粗匹配位置,所以 Yk-1 只需要在 Xk-1 的一个或若 干个粗匹配位置附近进行相关搜索就可以了,从而找出一个或少数几个可能性更大的匹配位置 ),( 11 −− kk vu 。在上述相关过程中,为了不致于丢失匹配点,应在粗匹配位置 ),( kk vu 附近增加几个补 充的试验位置。显然,第三次相关与第二次相关是类似的。如此进行下去,一直到最高分辨率的实时图 Y0。在基准图 X0 上找到匹配位置时为止。由此可见,整个搜索过程是从最低分辨率到最高分辨率一层 一层地进行下去的,为了进一步提高处理速度,相关运算常常采用 SSDA 算法。这种技术称之为分层搜 索的序贯判决算法。 (1)搜索位置数 由上述讨论可知,除了在分辨率最低和尺寸最小的图上作全区域搜索之外,其他各层的搜索都是在 少数几个可能匹配的位置上进行的。因此,由式 12-15 可知,当最低分辨率的两图进行相关时,总的搜 索位置数为: 2 2 2 ()( 1)2 2 2LLL MNMN−− + ≈ (12-16) 而当最高分辨率(L=0)的两图相关时,搜索位置数则为: 22 )()1( NMNM −≈+− 因此,如果不考虑其他各层的搜索位置数(很少)的话,那么分层搜索算法的搜索位置为一般算法 的 L22 1 ,从而大大地提高了处理速度。 Visual C++数字图像获取、处理及实践应用 · 582· 实验表明,当用分层搜索算法时,搜索位置数只有 2)1lg( −− NMK 个,其中 K=1~2。 (2)分层搜索序贯判决算法的门限序列 这里,假定第 K 次搜索级(即第 K 分层)的低分辨率的图像是由第(K-1)搜索级较高分辨率的图像 通过低通滤波器以后,再以二倍于它的间隔采样后得到的。因此,如果低通滤波器的频率特性比较理想 的话,那么由采样定理可知,这相当于把低通滤波器的噪声通带减小到 1/2,换言之,从第 K 级搜索到 第 K-1 级时,图像噪声增加了 2 1 倍。 如果采用 SSDA 算法,及采用均值一偏差门限序列,则其门限序列公式为: ( 2) ( ) Ák L n n k LT n g r −= + (12-17) 其中 k nT 表示第 K 搜索级的判决门取序列,k=0,1,2,⋯, L。 Lr 是在最低分辨率的 L 级搜索级匹配位 置上求得的噪声绝对值的均值,而 kg 是由搜索级 K 的匹配概率决定的。 应该指出,对于同样的分层搜索技术,若采用不同的滤波预处理和不同的相关算法,则就可以形成 不同的分层搜索的匹配算法,例如采用对函数的积相关算法,则可以形成分层搜索的对函数匹配算法等。 12.2 统计模式识别 统计模式识别的过程如图 12-9 所示,这是计算机识别的基本过程。预处理的目的是去除干扰、噪 声及差异、将原始信号变成适合于计算机进行特征抽取的形式,然后对经过预处理的信号进行特征抽取, 最后进行判决分类,得到识别的结果。为了进行分类,必须有图像样本。对样本进行特征选择和学习是 识别处理中所必要的分析工作。 图像 信息 数字化 预处理 特征提取 分类 学习 特征选择 识别 分析 图像 样本 图 12-9 统计模式识别框图 12.2.1 决策理论方法 正如图 12-9 的框图所示,统计模式识别方法最终归结为分类问题。假如已抽取出 N 个特征,而图 像可分为 M 类。那么就可以对特征进行分类,从而决定未知图像属于 m 类中的哪一类。一般把识别模 式看成是 N 维空间中的向量 X,即: T NxxxX ],,[ 21 = 模式类别为 1ω 、 2ω 、… mω ,识别就是要判断 X 是否属于 iω 以及 xi 属于其中哪一类。在这个过 程中主要解决两个问题:一是如何抽取特征,要求特征数 N 尽可能小而且对分类判断有效,二是假设已第 12 章 图像的模式识别 · 583· 有了代表模式的向量,如何决定它属于哪一类,这就需要判别函数。例如,模式有 1ω 、 2ω 、… mω , 共 m 个类别,则应有 D1(X)、D2(X)、⋯、 Dm(X)共 m 个判别函数。如果 X 属于第 i 类,则有: );,...,3,2,1( )()( ijmjXDXD ji ≠=> 在两类的分界线上,则有 )()( XDXD ji = 这时 X 既属于第 i 类,也属于第 j 类,因此这种判别失效。 为了进行识别就必须重新考虑其他特征,再进行识别。问题的关键是找到合适的判别函数。 1. 线性判别函数 线性判别函数是应用较广的一种判别函数,所谓线性判别函数是指判别函数是图像所有特征量的线 性组合,即: 0 1 () N i ik k i k DXXω ω = = +∑ (12-18) 其中, )(XDi 表示第 i 个判别函数, ikω 为系数或权值, 0iω 为常数,或称为阈值。在两类的判 决处有: ( ) ( ) 0i jDXDX− = (12-19) 该方程在二维空间是直线,在三维空间是平面,在 N 维空间则是超平面。 )()( XDXD ji − 可以 写成以下的形式: ∑ = −+−=− N k jikjkikji XXDXD 1 00 )()()()( ωωωω 其 判 决 过 程 为 : 如 果 )()( XDXD ji > , 或 0 )()( >− XDXD ji , 则 iX ω~ ; 如 果 )()( XDXD ji < ,或 0 )()( <− XDXD ji ,则 jX ω~ 。 用线性判别函数进行分类是线性分类器。任何 m 类问题都可以分解为(m-1)个 2 类识别问题。方法 是先把模式空间分为 1 类和其他类,如此进行下去即可。因此,两类线性分类器是最简单和最基本的。 分离两类的判决界由 0 )()( =− XDXD ji 表示。对于任何特定的输入模式必须判定 D1 大还是 D2 大。若考虑某个函数 )()( XDXDD ji −= ,对于 1 类模式 D 为正,对于二类模式 D 为负。于是, 只要处理与 D 相应的一组权输入模式并判断输出符号即可进行分类。执行这种运算法的分类器的原理框 图如图 12-10 所示。 在线性分类器中要找到合适的系数,以便使分类尽可能不出差错,惟一的办法就是试验法。例如, 先设所有的系数为 1,送进每一个模式,如果分类有错就调整系数,这个过程就叫做线性分类器的训练 或学习。例如,把 N 个特征 X 和 1 放在一起叫做 Y,N+1 个系数为ω ,即: 1 2 1 2 1 [ , , , ,1] [,,,,] T N T NN YXXX ω ω ω ω ω + = =   (12-20)Visual C++数字图像获取、处理及实践应用 · 584· Á ∑ Á 图 12-10 两类线性分类器 考虑分别属于两个不同模式类,m=2,此时有两个训练集 T1 和 T2。两个训练集合是线性可分的, 这意味着存在一个加权向量ω ,若是: 1 2 0 0 T T YYT YYT ω ω > ∈ < ∈ (12-21) 如果分类器的输出不能满足式 12-21 的条件,可以通过“误差校正”的训练步骤对系数加以调整。 例如,如果第一类模式 ωTY 不大于零,则说明系数不够大,可用加大系数的方法进行误差修正。具体 修正方法如下: 对于任一 1TY ∈ ,若 0≤ωTY ,则使 Yαωω +=1 对于任一 2TY ∈ ,若 0>ωTY ,则使 Yαωω −=1 通常使用的误差修正方法有固定增量规则,绝对修正规则及部分修正规则。固定增量规则是选择“为 一个因定的非负数”。绝对修正规则是取“为一最小整数”,它可使 ωTY 的值刚好大于零,即: T T Y Y ω α ω= 大于 的最小整数 部分修正规则可取为下式所决定的值: 0 2 T T Y YY ω α γ γ= < ≤ (12-22) 2. 最小距离分类器 线性分类器中重要的一类是用输入模式与特征空间作为模板的点之间的距离作为分类的准则。假定 有 m 类,给出 m 个参考向量 R1、R2、⋯、 Rm,Ri 与模式类 iω 相联系。对于 Ri 的最小距离分类就是把输 入的新模式 X 分为 iω 类,其分类准则就是 X 与参考模型原型 R1、R2、⋯、 Rm 之间的距离,跟哪一个最 近就属于哪一类。X 和 R 之间的距离可表示为: ()()T i i iXRXRXR− = − − (12-23)第 12 章 图像的模式识别 · 585· 其中 T iRX)( − 是 )( iRX − )的转置,由式 12-23 可得: )( )()( 2 i T i T ii TT i T i T ii TT i T i i RRXRRXXX RRXRRXXX RXRX RX −+−= +−−= −−= − 由此可设定最小距离判别函数 )(XDi 为: ( ) ( 1,2,3, , )TTT i i i i iD X X R R X R R i m= + − =  (12-24) 由上边的判别函数,在分类中,如果 iX ω∈ ,则 min),( =iRXd 。由式 12-24 可见, )(XDi 是 一个线性函数,因此最小距离分类器也是一个线性分类器。在最小距离分类中,在决策边界上的点与相 邻两类都是等距离的,这种方法就难于解决,此时,必须寻找新的特征,重新分类。 这种分类还可以用决策区域来表示。例如有二类问题 1ω , 2ω ,其模板分别为 R1,R2,当距离 ),(),( 21 RXdRXd < ,或者: 2/1 1 2 2 2/1 1 2 1 ])([])([ ∑∑ == −<− n i i n i i RXRX 则 1ω∈X ,并可用决策区域来表示,如图 12-11 所示。 将模板 R1、R2 作连线,再作平分线,平分线左边为 R1 区域,平分线右边为 R2 区域,R1R2 为决策区 域,中间为决策面。在这种分类中,两类情况界面为线,决策区为两平面。对于 3 类情况,界面为超平 面,决策区为半空间。 图 12-11 二类问题决策区域 3. 最近邻域分类法 最近邻域分类法是图像识别中应用较多的一种方法。在最小距离分类法中,是取一个最标准的向量 作为代表。将这类问题稍微扩张一下,一类不能只取一个代表,把最小距离的概念从一个点和一个点之Visual C++数字图像获取、处理及实践应用 · 586· 间的距离扩充到一个点和一组点之间的距离。这就是最近邻城分类法的基本思路。 设 R1,R2⋯、 Rm 分别是与类 1ω 、 2ω 、⋯、 mω 相对应的参考向量的 m 个集合,在 Ri 中的向量为 k iR ,即 i k i RR ∈ , ilk ,2,1,0= ,也就是: },,,{ 21 l iiii RRRR = 输入特征向量 X 与 Ri 之间的距离用下式表示: k ii RXRXd −= min),( , ilk ,2,1,0= 这就是说,X 和 Ri 之间的距离是 X 和 Ri 中每一个向量的距离中的最小者。如果 X 和 k iR 之间的距 离由式 12-22 决定,则其判决函数为: ( ) min{ ( ) ( ) } ( 1,2, , ; 1,2,3, , )T k k T k T k i i i i i iDX XRRXRR k li m= + − = =  (12-25) 设: k i Tk i Tk i k i Tk i RRXRRXXD)()()( −+= 则 ),,3,2,1;,,2,1( (X)}min{)( milkDXD i k ii  === 其中 (X)k iD 是特征的线性组合,决策边界将是分段线性的。例如,如图 12-12 所示,有一个两类判别 问题, 1ω 类的代表为 1 1R 、 2 1R , 2ω 类的代表为 1 2R 、 2 2R 、 3 2R 。如果有一个模式送入识别系统,首 先要计算它与每个点的距离,然后找最短距离。这种方法的概念简单,分段线性边界可以代表很复杂的 曲线,也可能本来是非线性边界,现在可用分段线性来近似代替。 图 12-12 二类最近邻域分类 4. 非线性判别函数 线性判别函数很简单,但也有缺点。它对于较复杂的分类往往不能胜任。在较复杂的分类问题中就 要提高判别函数的次数,因此根据问题的复杂性,可将判别函数从线性推广到非线性。非线性判别函数 可写成下式的形式: 第 12 章 图像的模式识别 · 587· 0 1 1 2 2 12 1 2 12 1 3 1 1 2 2 2 11 1 22 2 2 0 1 1 1 1 ()NN NN NNN NNNN kk k k k ki k i k k k i D X x x x x x x x x x x x x x x x x ω ω ω ω ω ω ω ω ω ω ω ω ω ω = = = = = + + + + + + + + + + + + = + +∑ ∑ ∑∑    (12-26) 上式是一个二次型判别函数,通常二次型判别函数的决策边界是一个超二次曲面。 12.2.2 统计分类法 以上谈到的分类方法是在没有噪声干扰的情况下进行的,此时测得的特征确能代表模式。如果在抽 取特征时有噪声,那么可能抽取的特征代表不了模式,这时就要用统计分类法。用统计方法对图像进行 特征抽取、学习和分类是研究图像识别的主要方法之一,而统计方法的最基本内容之一是贝叶斯分析, 其中包括贝叶斯决策方法、分类器、估计理论、贝叶斯学习、贝叶斯距离等。 1. 贝叶斯公式 在古典概率中贝叶斯定理已为大家所熟悉 1 ()(/)(/) ()(/) i i i n j j j PBPABPBA PBPAB = = ∑ (12-27) 式中 B1、B2、⋯、 Bn 是 n 个互不相容的事件,P(Bi )是事件 Bi 的先验概率,P(A/Bi))是 A 在 Bi 已发生条 件下的条件概率。贝叶斯定理说明在给定了随机事件 B1、B2、⋯、Bn 的各先验概率 P(Bi)及条件概率 P(A/Bi) 时,可算出事件 A 出现时,事件 Bi 出现的后验概率 P(Bi/A)。假定事件 A 代表肝炎病发生,而 B1、B2 分 别代表引起肝炎病发生的事件,如 B1 代表抽血时的交叉感染,B2 代表吃某种不卫生食品所引起的感染, 而 P(A/Bi)表示在 Bi 发生时,肝炎病发生的概率,则肝炎病发生时由某种原因 Bi 导致的后验概率就可以 用贝叶斯定理来计算。 贝叶斯公式常用于分类问题和参数估值问题中。假如设 X 表示事件的状态或特征的随机变量,它可 以表示图像的灰度或形状等,设 iω 表示事件类别的离散随机变量。对事物(如是图像的亮度或形状)行 分类就可以用如下的公式: ()(/)(/) ()(/) i i i j j j PPXPX PPX ω ωω ω ω = ∑ (12-28) 式中 )( iP ω 称为先验概率,它表示事件属于 iω 的预先粗略了解, )/( iXP ω 表示事件属于 iω 类而具 有 X 状态的条件概率, )/(XP iω 叫做 X 条件下 iω 的后验概率,它表示对事件 X 的状态作观察后判断 属于 iω 类的可能性。由式 12-28 可知,只要类别的先验概率及 X 的条件概率为已知,就可以得到类别的 后验概率。再加上最小误差概率或最小风险法则,就可以进行统计判决分类。 在参数估计问题中,贝叶斯公式中两个变量常常为连续随机变量,如果写作变量 X 及参数 Q,则有Visual C++数字图像获取、处理及实践应用 · 588· 如下的公式: ()(/)(/) ( ) ( / )d PQPXQPQX PQPXQQ = ∫ (12-29) 通过式 12-29,由参数的先验分布 P(Q)及预先设定的条件分布 P(X/Q),即可求得参数的后验分布 P(Q/X)。贝叶斯公式是参数估计的有力工具。 2. 贝叶斯分类法 假设有两类,每类有两种统计参数代表,即: 1 1 1 2 2 2 : ( ), ( / ) : ( ), ( / ) PPX PPX ω ω ω ω ω ω (12-30) 其中 )( 1ωP 、 )( 2ωP 是先验概率, )/( 1ωXP、 )/( 2ωXP 是条件概率密度函数。在噪声不确定的 影响下,每个模式已不能用一个向量来表示,因此只能得到某一类模式的概率分布。 如果用贝叶斯规则,结果是: 如果 )/()()/()( 2211 ωωωω XPPXPP > ,则有 1ω∈X ; 如果 )/()()/()( 2211 ωωωω XPPXPP < ,则有 2ω∈X 。 显 然 )/()( ii XPP ωω 在这里起到了判别函数的作用。在应用中,为方便起见,常采用 )/()( ii XPP ωω 的对数形式,即: )/()(lg)/()(lg 2211 ωωωω XPPXPP < > 也就是: 2 1 2 2 1 1 1 2 2 1 )(lg )(lg )/(lg )/(lg )(lg )(lg )/(lg )/(lg ωω ω ω ω ωω ω ω ω ∈< ∈> XP P XP XP XP P XP XP 在两类问题中,分界面为: 0)/()(lg)/()(lg 2211 =− ωωωω XPPXPP 或者: 0)()/( )()/(lg 22 11 =ωω ωω PXP PXP 假如一个模式遵循正态分布,它的均值为 64,协方差矩阵是 Ki,设 m=2,可得到其决策分界面如 下,因为 )/( 1ωXP 是正态分布,所以: 第 12 章 图像的模式识别 · 589· / 2 1/ 2 11( / ) (2        2 NT i i i i iPXKXMKXMω − − −= − − − (12-31) 当 i=1,2 时,按贝叶斯准则,如果: 1 2 2 1 lg ( / ) lg ( ) lg ( / ) lg ( ) PXP PXP ω ω ω ω> (12-32) 则 1X ω∈ 。 由式 12-31 和 12-32 式,可得到: )( )(lg)()(2 1)()(2 1 || ||lg2 1 1 2 2 1 221 1 11 2 1 ω ω P PMXKMXMXKMXK K TT >−−+−−− −− (12-33) 这时,两类间的决策边界是二次的。 如果两个协方差矩阵相同,即 K1= K2= K3,则: 1 1 2 21 1 2121 1 )( )(lg)()(2 1)( ωω ω ∈>−++− −− XP PMMKMMMMKXTT,则有 2 1 2 21 1 2121 1 )( )(lg)()(2 1)( ωω ω ∈<−++− −− XP PMMKMMMMKXTT,则有 (12-34) 在这种情况下,决策边界成为线性的,所以求两类分类问题时,如果每类都是正态分布,但有不同 的协方差矩阵,分界是二次函数。如果 N 很大,求 K-1 相当麻烦。 除了上述方法之外,也可以用最小风险来求其类别。 考虑 Nxxx ,,, 11  是随机变量,对于每一类模式 iω ,i=1,2,⋯, m,其 )/( ωXP 及 iω 出现的 概率 )/( iXP ω 都是已知的。以 )/( iXP ω 及 )( iP ω 为基础,一个分类器的成功条件是要在误识概率 最小的条件下来完成分类任务。我们可定义一个决策函数 )(xd ,其中 idxd =)( 表示假设 iX ω∈ 被 接受。如果输入模式实际是来自此 iω ,而作出的决策是 jd ,则可用 ),( ji dL ω 表示分类器引起的损失。 条件风险为: ( , ) ( , ) ( / )di i j ir d L d P X Xω ω ω= ∫ (12-35) 对于给定的先验概率集 )}(,),(),({ 21 iPPPP ωωω = ,平均风险为: 1 (,)()(,) m i i i R P d P r dω ω = = ∑ (12-36) 把式 12-36 代入式 12-35,并且令: Visual C++数字图像获取、处理及实践应用 · 590· 1 (,)()(/) (,)() m i i i i x L d P P X r P d PX ω ω ω == ∑ (12-37) 则有 : ( , ) ( ) ( , )dxR P d P X r P d X= ∫ 其中 ),( dPrx 定义为对于给定的特征向量 X,决策为 d 的后验条件平均风险。 这里的问题在于选择适当的决策 id ,i=1,2,⋯, m,以使平均风险 ),( dPR 取极小,或者使条 件平均风险 ),( dPrx 的极大值取极小。这种使平均风险取极小的决策规则称为贝叶斯规则。如果 *d 是 在使平均损失极小的意义上的最优决策,则: ),(),(* dPrdPr xx ≤ 即: * 1 1 (,)()(/)(,)()(/) m m i i i i i i i i L d P P X L d P P Xω ω ω ω ω ω = = ≤∑ ∑ (12-38) 对于(0,1)损失函数为:    ≠ == ji jidL i 1 0),(ω 平均风险实际上也就是误识的概率。在这种情形下贝叶斯规则是: ()(/)()(/)i i j jPPXPPXω ω ω ω≥ (12-39) 对所有的 j=1,2,⋯, m。 3. 贝叶斯分类器 多类贝叶斯分类器如图 12-13 所示。其中 )/( iXP ω 与 )( iP ω 的乘积就是第 i 类判别函数 )(XDi 。 如果 )()(XDXD ji > ,对于一切 ji ≠ 的情况下,则分类器就把给定的一个特性量归于 iω 类。 xÁ xÂà )/( ÁωXP )/( ÁωXP )/( ÁXP ω 极大值捡出 xÁ ÁX ω= 图 12-13 多类贝叶斯分类器 第 12 章 图像的模式识别 · 591· 二类贝叶斯分类器,如图 12-14 所示,在这类范畴的问题中,有时不制定两个判别函数 )(1 XD 和 )(2 XD,而是定义一个判别函数。若 0)( >XD,则决策 1ω ,否则决策 2ω 。 D(X) XÁÁ +1 XÁ ÂÂÂÂÂÂÂÂÂÂÂÂÂÂÂÂÂÂÂÂÂÂÃÁ ÂÂÂXÁ Á Á ω ω 特征向量 决策 阈值单元 判决分类器 图 12-14 二类贝叶斯分类器 12.2.3 特征的提取和选择 在模式识别中,确定判据是重要的,但是问题的另一方面,即如何抽取特征也是相当重要的。如果 特征找不对,分类就不可能准确。特征选取的方法是很多的。从一个模式中提取什么特征,将因不同的 模式而异,并且与识别的目的、方法等有直接关系。有关的方法,如图像特征的各处检测方法,曲线拟 合,Hough 变换等。 如果把所有的特征不分主次全都罗列出来,N 会很大,这也会给正确判断带来麻烦,如图 12-15 所 示。有两类模式,用两个特征 1x 、 2x 来表达,在 1x 上的投影为 ab、cd,在 2x 上的投影为 ef、gh。那 么,由图可见,ac 这一段肯定属于 1ω ,bd 段肯定属于 2ω 的,但是 cb 段就难以分出属于哪一类。一种 设想是把坐标轴作一个旋转,变成 1y 、 2y ,此时不再去测量 1x 、 2x 、而而去测量 1y 、 2y ,如图 12-16 所示。由图可见,这时检测 1y 当然也分不清,可是检测 2y 就可以分得很清,这说明,当作一变换后, 2y 是一个很好的特征。 a b c d e f g h x1 x2 w2 w1 a b c d e f g h xÁ x wÁ w 图 12-15 两类模式持征抽取之一 图 12-16 两类模式特征抽取之二 对于在图 12-15 和图 12-16 中所说明的特征抽取的例子中,用坐标旋转的方法得到了既少又好的特Visual C++数字图像获取、处理及实践应用 · 592· 征。空间坐标的旋转就是特征空间的线性变换。究竟怎样变换才能找到较好的特征呢?普通的方法是把每 一类的协方差矩阵变成对角形矩阵,在变换后的矩阵中取其特征向量及其相对应的特征值,然后,把持 征向量按其特征值的大小排列起来。特征值大的那个特征向量就是最好的特征。另外,在变换后的空间 中,如果有 M 个彼此关联的特征,可采用前几个最大特征值对应的特征向量作为特征,这样既可保证均 方误差最小,又可大大减少特征的数目。 另外一个途径是寻找一种变换,使同一类向量靠得更近一些,以便把它聚合到一起去,在这种思想 的指导下,可以找到每一类点与点之间的距离,使它最小化。这样作是应用特征值最小的那些特征向量。 假定有两类模式,测量两种特征都是正态分布,均值是 1M 和 2M ,这两个分布离得越远越容易识 别。所谓离得远不一定是均值相差较远。在这种情况下,不能用点与点之间的距离,也不是点与一组点 间的距离,而是两个分布间的距离,这是一个统计距离。如果在统计意义上两类离得远就容易识别。如 果有 M 个特征,就要计算它们的统计距离,哪个特征上统计距离最远,哪个特征就最好。一般计算统计 距离的方法有许多。例如贝叶斯误差概率、疑义度或仙农嫡、贝叶斯距离、广义柯尔莫哥洛夫距离等等。 12.3 线性分类器 在上一节中,简单介绍了统计模式识别的概念和一般方法。在这一节中,将着重介绍线性分类器。 由于线性判别函数易于分析,所以关于这方面的研究特别多。 对于线性分类器的概念,在上一节中已经对线性判别函数的基本概念和设计线性分类器的基本考虑 进行了简单的介绍。这里,将在给出两类情况下,介绍基于几个常用的准则函数的线性分类器设计方法。 这几个准则函数是:Fisher 准则、感知准则、最小错分样本数准则、最小平方误差(MSE)准则和最小 错误线性判别函数准则。在这几种准则函数中,我们将详细阐述线性分类器中的 Fisher 判别准则,对于 其他的几个准则,读者可以参考相关文献。 12.3.1 Fisher 判别准则 应用统计方法解决模式识别问题时,一再碰到的问题之一是维数问题。在低维空间里解析上或计算 上行得通的方法,在高维空间里往往行不通。因此,降低维数有时就称为处理实际问题的关键。 我们考虑 d 维空间的样板投影到一条直线上,形成一维空间。然而,即使样本在 d 维空间里面形成 若干紧凑的互相分得开的集群,若把它们投影到一条任意的指向上,也可能使几类样本混在一起而变得 无法识别。但在一般情况下,总可以找到某个方向,使在这个方向的直线上,样本的投影能分开得最好。 问题是如何根据实际情况找到这条最好得、最易于分类的投影线。这就是 Fisher 法所要解决的基本问题。 在定义 Fisher 准则函数之前,先定义几个必要的基本参量。 1.在 d 维 X 空间 各类样本的均值向量: 1 , 1,2i i m x iN = =∑ (12-40) 样本类内离散度矩阵和总类内离散度矩阵: 1 2 ( )( ) , 1,2T i i i w S x m x m i SSS = − − = = + ∑ (12-41) 样本类间离散度矩阵: 第 12 章 图像的模式识别 · 593· 1 2 1 2( )( )T bS m m m m= − − (12-42) 2.在一维 Y 空间中 各类样本均值: 1 i i i m y yN ω= ∈∑ (12-43) 样本类内离散度和总类内离散度: 各类离散度 22 ()i iS y m= −∑  总离散度 2 2 1 2wSSS= +   iy ω∈ (12-44) 现在定义 Fisher 准则函数。希望是投影后: (1)同类样本在 W 上的投影聚集性强 (2)不同在样本在 W 上的投影离散性强 因此,Fisher 准则定义为: 2 1 2 2 2 1 2 ()()F m mJW SS −= +     (12-45) 转化成与 W 有关的函数: [ ] WSWWmmmmWmmWmWmWmm mWXNW XWNyNm b TTTTTT i T i T T ii i =−−=−=−=−∴ = == ∑ ∑ ∑ ))(()()()~~( 1 11~ 2121 2 21 2 21 2 21 其中 Sb 为样本类间离散度矩阵,而: [] [] WSWWmxmxW WmxmxWmXW mWXWmyS i TT ii T T ii T i T i TT ii =−−= −−=−= −=−= ∑ ∑∑ ∑ ∑ ))(( ))(()( )()~(~ 2 222 (12-46) 其中 Si 为样本类内离散度矩阵。 21 SSSW += 样本总类内离散矩阵,则: () T b F T W WSWJWWSW = (12-47) 下面确定最佳的 W。即使 JF(W)达最大的 W 时的条件极值。 Visual C++数字图像获取、处理及实践应用 · 594· 采用拉格朗日乘子方法,定义 Lagrange 函数为: L(W, λ )=WTSbW- λ (WTSWW-C) (12-48) 令偏导数为零: 00),(** =−⇒= WSWSW WL Wb λ∂ λ∂ 向量求导: W=(W1, W2 )T             = 2 1 W L W L W L ∂ ∂ ∂ ∂ ∂ ∂ ** 1 * * b W W b SWSW SSWW λ λ− = = (12-49) 由于 常数*)())(( 21 * 2121 * mmWmmmmWS T b −=−−= ,忽略比例因子有: * 1 1 2()−≈ −WW S m m (12-50) 以上做的工作全部是将 d 为空间的样本映射成一维样本集,这个一维空间的方向 W*是相对于 Fisher 准则是最好的。 在将 d 维分类问题转化为一维分类问题后,只要确定一个阈值 y0,将投影点 yn 与 y0 相比较,便可 作出决策。在此,简单介绍几种分类问题的基本原则。 (1)当维数 d 和样本数 N 都很大时,可采用贝叶斯决策准则,从而获得一种在一维空间的“最优” 分类器。 (2)如果上述条件不满足,也可利用先验知识选定分界阈值点 y0,如 (1) 1 2 0 2 m my +=   (12-51) (2) 1 1 2 2 0 1 2 N m N my mNN += =+    (12-52) (3) 1 2 1 2 0 1 2 ln( ( ) / ( ) 2 2 m m P Py NN ω ω+= + + −   (12-53) 其中 )( 1ωP 和 )( 2ωP 分别为 1ω 类和 2ω 类样本的先验概率。这样,对于任意给定的未知样本 x, 只要计算它的投影点 y: xwy T*= 再根据决策规则: 第 12 章 图像的模式识别 · 595· 0 1 0 2 y y x y y x ω ω > ∈ < ∈ (12-54) 就可以判断 x 属于什么类别。 12.4 Visual C++编程实现 在介绍了有关图像识别方面的理论后,利用 Visual C++来进行实现。由于进行统计的模式识别涉及 到许多需要统计的东西,因此,在这里就模板匹配进行具体的实现,并给出实现的代码。 由于模板匹配涉及到两幅图像,因此涉及了一个对话框来实现模板的匹配。向工程中添加名为 IDD_DLG_RECOG_MATCH 的对话框,其具体的设置请参见附件中的工程文件。然后给此对话框创建新 的类 CdlgRecMatch,在这个类中,将实现模板的匹配,并最后显示出匹配的模板的位置。其具体实现代 码如下所示。 1.话框头文件(DlgRecMatch.h) #if !defined(AFX_DLGRECMATCH_H__7947D7E1_3494_11D0_9E74_000021CDD41E__INCLUDED _) #define AFX_DLGRECMATCH_H__7947D7E1_3494_11D0_9E74_000021CDD41E__INCLUDED_ //#include "GlobalApi.h" #include "CDib.h" #include "ImageProcessingDoc.h" #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 // DlgRecMatch.h : header file // ///////////////////////////////////////////////////////////////////////////// // CDlgRecMatch dialog class CDlgRecMatch : public CDialog { // Construction public: CDib* m_pDibInit; // 初始图像 CDib* m_pDibModel; // 模板图像 CDib* m_pDibResult; // 匹配后的图像 CRect m_rectInitImage; // 初始图像显示区域 CRect m_rectModelImage; // 模板图像显示区域 CRect m_rectResltImage; // 匹配后图像显示区域 Visual C++数字图像获取、处理及实践应用 · 596· BOOL m_bCalImgLoc; // 计算图像位置的标志位。FALSE 表示还没有计算图像 位置 CImageProcessingDoc* m_pDoc; // 文档类指针 BOOL TemplateMatch(CDib* pDibSrc, CDib* pDibTemplate); // 模板匹配 void CalImageLocation(); // 设置图像等控件的位置大小 CDlgRecMatch(CWnd* pParent = NULL,CImageProcessingDoc* pDoc=NULL); // standard constructor // Dialog Data //{{AFX_DATA(CDlgRecMatch) enum { IDD = IDD_DLG_RECOG_MATCH }; // NOTE: the ClassWizard will add data members here //}}AFX_DATA // Overrides // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(CDlgRecMatch) protected: virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support //}}AFX_VIRTUAL // Implementation protected: // Generated message map functions //{{AFX_MSG(CDlgRecMatch) afx_msg void OnRecogOpenModel(); afx_msg void OnRecogMatch(); afx_msg void OnPaint(); virtual void OnOK(); virtual void OnCancel(); //}}AFX_MSG DECLARE_MESSAGE_MAP() }; //{{AFX_INSERT_LOCATION}} // Microsoft Visual C++ will insert additional declarations immediately before the previous line. #endif // !defined(AFX_DLGRECMATCH_H__7947D7E1_3494_11D0_9E74_000021CDD41E__INCLUDED_) 第 12 章 图像的模式识别 · 597· 2.对话框文件(DlgRecMatch.cpp) // DlgRecMatch.cpp : implementation file // #include "stdafx.h" #include "ImageProcessing.h" #include "DlgRecMatch.h" #include "CDib.h" #include "GlobalApi.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif ///////////////////////////////////////////////////////////////////////////// // CDlgRecMatch dialog CDlgRecMatch::CDlgRecMatch(CWnd* pParent /*=NULL*/,CImageProcessingDoc* pDoc) : CDialog(CDlgRecMatch::IDD, pParent) { //{{AFX_DATA_INIT(CDlgRecMatch) // NOTE: the ClassWizard will add member initialization here //}}AFX_DATA_INIT // 设置计算图像控件位置标志位为 FALSE m_bCalImgLoc = FALSE; // 设置初始图像 m_pDibInit = pDoc->m_pDibInit; // 获得文档指针 m_pDoc = pDoc; // 分配模板图像内存 m_pDibModel = new CDib; // 分配结果图像 m_pDibResult = new CDib; } void CDlgRecMatch::DoDataExchange(CDataExchange* pDX) { Visual C++数字图像获取、处理及实践应用 · 598· CDialog::DoDataExchange(pDX); //{{AFX_DATA_MAP(CDlgRecMatch) // NOTE: the ClassWizard will add DDX and DDV calls here //}}AFX_DATA_MAP } BEGIN_MESSAGE_MAP(CDlgRecMatch, CDialog) //{{AFX_MSG_MAP(CDlgRecMatch) ON_BN_CLICKED(IDC_RECOG_OPEN_MODEL, OnRecogOpenModel) ON_BN_CLICKED(IDC_RECOG_MATCH, OnRecogMatch) ON_WM_PAINT() //}}AFX_MSG_MAP END_MESSAGE_MAP() ///////////////////////////////////////////////////////////////////////////// // CDlgRecMatch message handlers void CDlgRecMatch::OnRecogOpenModel() { CFileDialog dlg(TRUE,"bmp","*.bmp"); if(dlg.DoModal() == IDOK) { CFile file; CString strPathName; strPathName = dlg.GetPathName(); // 打开文件 if( !file.Open(strPathName, CFile::modeRead | CFile::shareDenyWrite)) { // 返回 return ; } // 读入模板图像 if(!m_pDibModel->Read(&file)){ // 恢复光标形状 EndWaitCursor(); // 清空已分配内存 m_pDibModel->Empty(); 第 12 章 图像的模式识别 · 599· // 返回 return; } } // 判断是否是 8-bpp 位图(这里为了方便,只处理 8-bpp 位图的模板配准,其他的可以类推) if(m_pDibModel->m_nColorTableEntries != 256) { // 提示用户 MessageBox("目前只支持 256 色位图!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 清空已分配内存 m_pDibModel->Empty(); // 返回 return; } // 初始图像的长宽大小 CSize sizeImage = m_pDibInit->GetDimensions(); int nImageWidth = sizeImage.cx ; int nImageHeight = sizeImage.cy ; // 模板图像的长宽大小 CSize sizeModelImage = m_pDibModel->GetDimensions() ; int nModelWidth = sizeImage.cx ; int nModelHeight = sizeImage.cy ; // 判断模板尺寸和初始图像的大小,如果模板大于初始图像,则推出 if(nModelHeight > nImageHeight || nModelWidth > nImageWidth ) { // 提示用户 MessageBox("模板尺寸大于源图像尺寸!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 释放已分配内存 m_pDibModel->Empty(); // 返回 return; } Visual C++数字图像获取、处理及实践应用 · 600· // 如果打开新的待配准文件,将图像位置设置标志位设为 FALSE,以便再次调整位置 m_bCalImgLoc = FALSE; // 更新显示 this->Invalidate(); } /************************************************************************* * * \函数名称: * CalImageLocation() * * \输入参数: * 无 * * \返回值: * 无 * * \说明: * 该函数设置对话框中的控件位置和大小,并设置显示图像的位置。默认的图像大小为 * 352×288,如果图像小于 * 此大小,则控件大小设置为 256*256,并将图像放置在控件中间 * ************************************************************************* */ void CDlgRecMatch::CalImageLocation() { // ------------------------------------------------------ // 获得控件 IDC_RECOG_INIIMAGE 的句柄,并获得控件的初始位置信息 CWnd* pWnd=GetDlgItem(IDC_RECOG_INIIMAGE); WINDOWPLACEMENT *winPlacement; winPlacement=new WINDOWPLACEMENT; pWnd->GetWindowPlacement(winPlacement); // 图像宽度 int nImageWidth; nImageWidth = m_pDibInit->m_lpBMIH->biWidth; // 图像高度 int nImageHeight; nImageHeight = m_pDibInit->m_lpBMIH->biHeight; // ----------------------------------------------------------------- 第 12 章 图像的模式识别 · 601· // 调整控件 IDC_RECOG_INIIMAGE 的大小位置,并同时设置显示基准图像的位置 if(nImageHeight > 256){ winPlacement->rcNormalPosition.bottom = winPlacement->rcNormalPosition.top + nImageHeight; m_rectInitImage.bottom = winPlacement->rcNormalPosition.bottom; m_rectInitImage.top = winPlacement->rcNormalPosition.top; } else{ winPlacement->rcNormalPosition.bottom = winPlacement->rcNormalPosition.top + 256; m_rectInitImage.bottom = winPlacement->rcNormalPosition.top + 128 + nImageHeight/2; m_rectInitImage.top = winPlacement->rcNormalPosition.top + 128 - nImageHeight/2; } if(nImageWidth > 256){ winPlacement->rcNormalPosition.right = winPlacement->rcNormalPosition.left + nImageWidth; m_rectInitImage.right = winPlacement->rcNormalPosition.right; m_rectInitImage.left = winPlacement->rcNormalPosition.left; } else{ winPlacement->rcNormalPosition.right = winPlacement->rcNormalPosition.left + 256; m_rectInitImage.right = winPlacement->rcNormalPosition.left + 128 + nImageWidth/2; m_rectInitImage.left = winPlacement->rcNormalPosition.left + 128 - nImageWidth/2; } // 设置 IDC_RECOG_INIIMAGE 控件的大小位置 pWnd->SetWindowPlacement(winPlacement); // 获得显示模板图像控件的右边位置,以便确认显示模板图像控件的位置 int nIniImgRight; nIniImgRight = winPlacement->rcNormalPosition.right; int nIniImgLeft; nIniImgLeft = winPlacement->rcNormalPosition.left; // 获得 IDC_REG_INIT_IMAGE 控件的下边位置,以便调整其他控件的位置 int nIniImgBottom; nIniImgBottom = winPlacement->rcNormalPosition.bottom; // 获得 IDC_REG_INIT_IMAGE 控件的下边位置,以便调整其他控件的位置 int nIniImgtop = winPlacement->rcNormalPosition.top; // ------------------------------------------------------ // 获得控件 IDC_RECOG_MODIMAGE 的句柄,并获得初始位置信息 pWnd=GetDlgItem(IDC_RECOG_MODIMAGE); Visual C++数字图像获取、处理及实践应用 · 602· pWnd->GetWindowPlacement(winPlacement); // 如果还未打开模板图像,则设置结果图像大小和初始图像大小相等 if(!m_pDibModel->IsEmpty()){ nImageWidth = m_pDibModel->m_lpBMIH->biWidth; nImageHeight = m_pDibModel->m_lpBMIH->biHeight; } // 调整控件 IDC_RECOG_MODIMAGE 的大小位置,并同时设置显示结果图像的位置 // 先调整控件的左边位置,和 IDC_REG_INIT_IMAGE 控件相隔 15 个像素 winPlacement->rcNormalPosition.left = nIniImgRight + 15; if(nImageHeight > 256){ winPlacement->rcNormalPosition.top = nIniImgtop; winPlacement->rcNormalPosition.bottom = winPlacement->rcNormalPosition.top + nImageHeight; m_rectModelImage.bottom = winPlacement->rcNormalPosition.bottom; m_rectModelImage.top = nIniImgtop; } else{ winPlacement->rcNormalPosition.top = nIniImgtop; winPlacement->rcNormalPosition.bottom = winPlacement->rcNormalPosition.top + 256; m_rectModelImage.bottom = winPlacement->rcNormalPosition.top + 128 + nImageHeight/2; m_rectModelImage.top = winPlacement->rcNormalPosition.top + 128 - nImageHeight/2; } if(nImageWidth > 256){ winPlacement->rcNormalPosition.right = winPlacement->rcNormalPosition.left + nImageWidth; m_rectModelImage.right = winPlacement->rcNormalPosition.right; m_rectModelImage.left = winPlacement->rcNormalPosition.left; } else{ winPlacement->rcNormalPosition.right = winPlacement->rcNormalPosition.left + 256; m_rectModelImage.right = winPlacement->rcNormalPosition.left + 128 + nImageWidth/2; m_rectModelImage.left = winPlacement->rcNormalPosition.left + 128 - nImageWidth/2; } // 设置 IDC_RECOG_MODIMAGE 控件的大小位置 pWnd->SetWindowPlacement(winPlacement); // 获得 IDC_RECOG_MODIMAGE 控件的右边位置,以便调整其他控件的位置 第 12 章 图像的模式识别 · 603· nIniImgRight = winPlacement->rcNormalPosition.right; // ------------------------------------------------------ // 获得控件 IDC_RECOG_RESLTIMAGE 的句柄,并获得初始位置信息 pWnd=GetDlgItem(IDC_RECOG_RESLTIMAGE); pWnd->GetWindowPlacement(winPlacement); // 如果还未生成结果图像,则设置结果图像大小和初始图像大小相等 if(!m_pDibResult->IsEmpty()){ nImageWidth = m_pDibResult->m_lpBMIH->biWidth; nImageHeight = m_pDibResult->m_lpBMIH->biHeight; } // 调整控件 IDC_RECOG_RESLTIMAGE 的大小位置,并同时设置显示结果图像的位置 // 先调整控件的左边位置,和 IDC_RECOG_MODIMAGE 控件相隔 15 个像素 winPlacement->rcNormalPosition.left = nIniImgRight + 15; if(nImageHeight > 256){ winPlacement->rcNormalPosition.top = nIniImgtop; winPlacement->rcNormalPosition.bottom = winPlacement->rcNormalPosition.top + nImageHeight; m_rectResltImage.bottom = winPlacement->rcNormalPosition.bottom; m_rectResltImage.top = winPlacement->rcNormalPosition.top; } else{ winPlacement->rcNormalPosition.top = nIniImgtop; winPlacement->rcNormalPosition.bottom = winPlacement->rcNormalPosition.top + 256; m_rectResltImage.bottom = winPlacement->rcNormalPosition.top + 128 + nImageHeight/2; m_rectResltImage.top = winPlacement->rcNormalPosition.top + 128 - nImageHeight/2; } if(nImageWidth > 256){ winPlacement->rcNormalPosition.right = winPlacement->rcNormalPosition.left + nImageWidth; m_rectResltImage.right = winPlacement->rcNormalPosition.right; m_rectResltImage.left = winPlacement->rcNormalPosition.left; } else{ winPlacement->rcNormalPosition.right = winPlacement->rcNormalPosition.left + 256; m_rectResltImage.right = winPlacement->rcNormalPosition.left + 128 + nImageWidth/2; m_rectResltImage.left = winPlacement->rcNormalPosition.left + 128 - nImageWidth/2; } Visual C++数字图像获取、处理及实践应用 · 604· // 设置 IDC_REG_RESLT_IMAGE 控件的大小位置 pWnd->SetWindowPlacement(winPlacement); // ------------------------------------------------------ if(nIniImgBottom > winPlacement->rcNormalPosition.bottom) nIniImgBottom = winPlacement->rcNormalPosition.bottom; nIniImgRight = winPlacement->rcNormalPosition.right; // 设置控件 IDOK 的位置大小 pWnd=GetDlgItem(IDOK); pWnd->GetWindowPlacement(winPlacement); winPlacement->rcNormalPosition.top = nIniImgBottom +15; winPlacement->rcNormalPosition.bottom = nIniImgBottom + 60; pWnd->SetWindowPlacement(winPlacement); // 设置控件 IDCANCEL 的位置大小 pWnd=GetDlgItem(IDCANCEL); pWnd->GetWindowPlacement(winPlacement); winPlacement->rcNormalPosition.top = nIniImgBottom +15; winPlacement->rcNormalPosition.bottom = nIniImgBottom + 60; pWnd->SetWindowPlacement(winPlacement); // 设置控件 IDC_RECOG_OPEN_MODEL 的位置大小 pWnd=GetDlgItem(IDC_RECOG_OPEN_MODEL); pWnd->GetWindowPlacement(winPlacement); winPlacement->rcNormalPosition.top = nIniImgBottom +15; winPlacement->rcNormalPosition.bottom = nIniImgBottom + 60; pWnd->SetWindowPlacement(winPlacement); // 设置控件 IDC_RECOG_MATCH 的位置大小 pWnd=GetDlgItem(IDC_RECOG_MATCH); pWnd->GetWindowPlacement(winPlacement); winPlacement->rcNormalPosition.top = nIniImgBottom +15; winPlacement->rcNormalPosition.bottom = nIniImgBottom + 60; pWnd->SetWindowPlacement(winPlacement); // 调整此对话框的大小 this->GetWindowPlacement(winPlacement); //winPlacement->rcNormalPosition.top = nIniImgtop -50; winPlacement->rcNormalPosition.bottom = nIniImgBottom + 272; winPlacement->rcNormalPosition.left = nIniImgLeft - 20; winPlacement->rcNormalPosition.right = nIniImgRight + 20; 第 12 章 图像的模式识别 · 605· this->SetWindowPlacement(winPlacement); // 释放已分配内存 delete winPlacement; // 设置计算图像控件位置标志位为 TRUE m_bCalImgLoc = TRUE; } void CDlgRecMatch::OnPaint() { CPaintDC dc(this); // device context for painting // 如果还没有计算图像的位置,则进行计算 if(!m_bCalImgLoc){ CalImageLocation(); } // 显示大小 CSize sizeDisplay; // 显示位置 CPoint pointDisplay; // 显示初始图像 if(!m_pDibInit->IsEmpty()){ sizeDisplay.cx=m_pDibInit->m_lpBMIH->biWidth; sizeDisplay.cy=m_pDibInit->m_lpBMIH->biHeight; pointDisplay.x = m_rectInitImage.left; pointDisplay.y = m_rectInitImage.top; m_pDibInit->Draw(&dc,pointDisplay,sizeDisplay); } // 显示模板图像 if(!m_pDibModel->IsEmpty()){ sizeDisplay.cx=m_pDibModel->m_lpBMIH->biWidth; sizeDisplay.cy=m_pDibModel->m_lpBMIH->biHeight; pointDisplay.x = m_rectModelImage.left; pointDisplay.y = m_rectModelImage.top; m_pDibModel->Draw(&dc,pointDisplay,sizeDisplay); } // 显示结果图像 if(!m_pDibResult->IsEmpty()){ Visual C++数字图像获取、处理及实践应用 · 606· sizeDisplay.cx=m_pDibResult->m_lpBMIH->biWidth; sizeDisplay.cy=m_pDibResult->m_lpBMIH->biHeight; pointDisplay.x = m_rectResltImage.left; pointDisplay.y = m_rectResltImage.top; m_pDibResult->Draw(&dc,pointDisplay,sizeDisplay); } } /************************************************************************* * * \函数名称: * TemplateMatch() * * \输入参数: * CDib* pDibSrc - 指向 CDib 类的指针,含有待匹配图像信息 * CDib* pDibTemplate - 指向 CDib 类的指针,含有模板图像信息 * * \返回值: * BOOL - 成功则返回 TRUE,否则返回 FALSE * * \说明: * 该函数将对图像进行模板匹配操作。需要注意的是,此程序只处理 256 灰度级的 * 图像 * ************************************************************************* */ BOOL CDlgRecMatch::TemplateMatch(CDib* pDibSrc, CDib* pDibTemplate) { // 指向源图像的指针 LPBYTE lpSrc,lpTemplateSrc; // 指向缓存图像的指针 LPBYTE lpDst; // 循环变量 long i; long j; long m; long n; // 中间结果 double dSigmaST; double dSigmaS; 第 12 章 图像的模式识别 · 607· double dSigmaT; // 相似性测度 double R; // 最大相似性测度 double dbMaxR; // 最大相似性出现位置 int nMaxWidth; int nMaxHeight; // 像素值 unsigned char unchPixel; unsigned char unchTemplatePixel; // 获得图像数据存储的高度和宽度 CSize sizeSaveImage; sizeSaveImage = pDibSrc->GetDibSaveDim(); // 获得模板图像数据存储的高度和宽度 CSize sizeSaveTemplate; sizeSaveTemplate = pDibTemplate->GetDibSaveDim(); // 暂时分配内存,以保存新图像 CDib* pDibNew; pDibNew = new CDib; // 如果分配内存失败,则退出 if(!CopyDIB(pDibSrc,pDibNew)){ // 释放已分配内存 pDibNew->Empty(); // 返回 return FALSE; } // 初始化新分配的内存,设定初始值为 255 lpDst = (LPBYTE)pDibNew->m_lpImage; memset(lpDst, (BYTE)255, pDibNew->GetSizeImage()); // 图像的高度 Visual C++数字图像获取、处理及实践应用 · 608· int nImageHeight ; nImageHeight = pDibSrc->m_lpBMIH->biHeight; // 图像的宽度 int nImageWidth; nImageWidth = pDibSrc->m_lpBMIH->biWidth; // 模板图像的高度 int nTemplateHeight; nTemplateHeight = pDibTemplate->m_lpBMIH->biHeight; // 模板图像的宽度 int nTemplateWidth; nTemplateWidth = pDibTemplate->m_lpBMIH->biWidth; // 计算 dSigmaT dSigmaT = 0; for (n = 0;n < nTemplateHeight ;n++) { for(m = 0;m < nTemplateWidth ;m++) { // 指向模板图像倒数第 j 行,第 i 个像素的指针 lpTemplateSrc = (LPBYTE)pDibTemplate->m_lpImage + sizeSaveTemplate.cx * n + m; unchTemplatePixel = (unsigned char)*lpTemplateSrc; dSigmaT += (double)unchTemplatePixel*unchTemplatePixel; } } // 找到图像中最大相似性的出现位置 dbMaxR = 0.0; for (j = 0;j < nImageHeight - nTemplateHeight +1 ;j++) { for(i = 0;i < nImageWidth - nTemplateWidth + 1;i++) { dSigmaST = 0; dSigmaS = 0; for (n = 0;n < nTemplateHeight ;n++) { for(m = 0;m < nTemplateWidth ;m++) { // 指向源图像倒数第 j+n 行,第 i+m 个像素的指针 lpSrc = (LPBYTE)pDibSrc->m_lpImage + sizeSaveImage.cx * (j+n) 第 12 章 图像的模式识别 · 609· + (i+m); // 指向模板图像倒数第 n 行,第 m 个像素的指针 lpTemplateSrc = (LPBYTE)pDibTemplate->m_lpImage + sizeSaveTemplate.cx * n + m; unchPixel = (unsigned char)*lpSrc; unchTemplatePixel = (unsigned char)*lpTemplateSrc; dSigmaS += (double)unchPixel*unchPixel; dSigmaST += (double)unchPixel*unchTemplatePixel; } } // 计算相似性 R = dSigmaST / ( sqrt(dSigmaS)*sqrt(dSigmaT)); // 与最大相似性比较 if (R > dbMaxR) { dbMaxR = R; nMaxWidth = i; nMaxHeight = j; } } } // 将最大相似性出现区域部分复制到目标图像 for (n = 0;n < nTemplateHeight ;n++) { for(m = 0;m < nTemplateWidth ;m++) { lpTemplateSrc = (LPBYTE)pDibTemplate->m_lpImage + sizeSaveTemplate.cx * n + m; lpDst = (LPBYTE)pDibNew->m_lpImage + sizeSaveImage.cx * (n+nMaxHeight) + (m+nMaxWidth); *lpDst = *lpTemplateSrc; } } // 复制图像 memcpy(pDibSrc->m_lpImage, pDibNew->m_lpImage, nImageWidth * nImageHeight); // 释放内存 pDibNew->Empty(); Visual C++数字图像获取、处理及实践应用 · 610· // 返回 return TRUE; } /************************************************************************* * * \函数名称: * OnRecogMatch() * * \输入参数: * 无 * * \返回值: * 无 * * \说明: * 根据图像模板,在待匹配的图像中找到匹配的位置 * ************************************************************************* */ void CDlgRecMatch::OnRecogMatch() { // 更改光标形状 BeginWaitCursor(); // 分配结果图像内存 if(!m_pDibInit->IsEmpty()){ // 如果分配内存失败,则退出 if(!CopyDIB(m_pDibInit,m_pDibResult)){ // 释放已分配内存 m_pDibResult->Empty(); // 返回 return ; } } // 设置计算图像控件位置标志位为 FALSE 以重新设置图像控件位置 m_bCalImgLoc = FALSE; // 调用 TemplateMatch()函数进行模板匹配 if (TemplateMatch(m_pDibResult, m_pDibModel)) { 第 12 章 图像的模式识别 · 611· // 更新显示 Invalidate(); } else { // 提示用户 MessageBox("分配内存失败!", "系统提示" , MB_ICONINFORMATION | MB_OK); } // 恢复光标 EndWaitCursor(); // 更新视图 Invalidate(); } void CDlgRecMatch::OnOK() { // TODO: Add extra validation here // 释放已分配内存 if(!m_pDibModel->IsEmpty()){ m_pDibModel->Empty(); m_pDibModel = NULL; } if(!m_pDibResult->IsEmpty()){ m_pDibResult->Empty(); m_pDibResult = NULL; } CDialog::OnOK(); } void CDlgRecMatch::OnCancel() { // TODO: Add extra cleanup here // 释放已分配内存 if(!m_pDibModel->IsEmpty()){ m_pDibModel->Empty(); m_pDibModel = NULL; } if(!m_pDibResult->IsEmpty()){ m_pDibResult->Empty(); m_pDibResult = NULL; Visual C++数字图像获取、处理及实践应用 · 612· } CDialog::OnCancel(); } 设置好对话框后,添加“图像识别”菜单,并在此菜单下添加“模板匹配”菜单项,如图 12-17 所 示。 图 12-17 图像识别菜单 向类 CImageProcessingView 中添加此菜单的点击事件处理程序,具体代码如下。 /************************************************************************* * * \函数名称: * OnRecogMatch() * * \输入参数: * 无 * * \返回值: * 无 * * \说明: * 根据图像模板,在待匹配的图像中找到匹配的位置 * ************************************************************************* */ void CImageProcessingView::OnRecogMatch() { // 获得文档类句柄 CImageProcessingDoc* pDoc; pDoc = GetDocument(); // 判断是否是 8-bpp 位图(这里为了方便,只处理 8-bpp 位图的水平镜象,其他的可以类推) if(pDoc->m_pDibInit->m_nColorTableEntries != 256) { // 提示用户 MessageBox("目前只支持 256 色位图的图像配准!", "系统提示" , MB_ICONINFORMATION | MB_OK); // 返回 return; } 第 12 章 图像的模式识别 · 613· // 打开图像识别对话框 CDlgRecMatch* pDlg = new CDlgRecMatch(NULL, pDoc); pDlg->DoModal(); delete pDlg; } 运行上述代码,选择如图 12-18 的对话框参数设置,其结果如图 12-19 所示,其中(a)为原图像,(b) 为模板图像,(c)为运行结果。 图 12-18 图像识别对话框 (a) (b) (c) 图 12-19 模板匹配运行结果Á
还剩621页未读

继续阅读

下载pdf到电脑,查找使用更方便

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

需要 20 金币 [ 分享pdf获得金币 ] 3 人已下载

下载pdf

pdf贡献者

liuyc81

贡献于2011-07-18

下载需要 20 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf