在过去的很长一段时间里,只有少数能够接触到昂贵设备的专业人员才有机会使用计算机操作数字图像(即数字图像处理)。这种专业人员与设备的组合通常只会出现在一些研究性实验室,因此数字图像处理技术起源于学术领域。如今,台式计算机的处理能力已日益增强,而且几乎每个人都拥有一些获取数字图像的设备,例如手机摄像头、数码相机、扫描仪。这些设备产生了大量的数字图像,使得数字图像处理变得和文字处理一样普及。
如今,IT界的专业人员不能局限于简单地熟悉数字图像处理过程,而是要能够有理论深度地操作图像及相关的数字媒体。在医学和媒体以及其他所有领域的工作中,这已经成为工作流程中愈来愈重要的一环。同样,软件工程师和计算机科学家在开发程序、数据库以及相关系统时也越来越多地面临需要正确处理数字图像的情况。对图像处理实际经验的缺乏,加之对基础理论理解的模糊,以及对问题难度的低估,常常使我们的解决方案效率低下,甚至出现严重错误致使我们对图像处理失去信心。
尽管术语“图像处理”和“图像编辑”经常可以交换使用,我们还是要引入下面更加精确的定义。数字图像编辑是指用已有的软件(如Photoshop、Corel Paint)来操作数字图像;而数字图像处理是指数字图像处理程序的概念、设计、开发以及增强。
现代编程环境以及它们所提供的广泛的API(应用程序接口)使非专用人员可以轻松获得几乎所有方面的计算处理功能,例如网络、数据库、图形、声音或者成像。开发一款能够深入图像、处理其内部独立元素的程序是很吸引人的。你会发现在正确的理论知识指导下,一幅图像最终会变为一个简单的数值矩阵;利用合适的工具,你可以对它做任何想要的操作。
相对于数字图像处理,计算机图形学致力于从诸如三维模型之类的几何描述中合成数字图像[31,37,103]。尽管图形学专家感兴趣的工作时虚拟现实场景,特别是电脑游戏中的快速渲染,该领域仍使用了大量源自图像处理的方法,比如图像变换(变形)、从图像数据完成三维模型重建以及基于图像的非真实渲染等等专业技术[77,104]。相似的,图像处理也利用了许多源自于计算几何和计算机图形学的思想,例如医学图像处理中的体模型。这两个领域可能在进行电影或视频的后期数字制作、创建特效的时候最为接近[105]。本书将全面介绍图像和图像序列(即视频)的有效处理方法。
数学图像是本书的核心主题,与多年前不同,数学图像这个术语在当令已被普遍使用,因此我们没有必要再去深入地解释它。然而,本书并非针对所有数字图像类型,而是将注意力集中在那些由排列在规则矩形网络上的图像元素(通常被称为像素)构成的图像上。
人们每天都会接触到各种各样的数字光栅图像,例如人物和风景的彩色照片、打印文档的灰度扫描、建筑平面图、传真的文档、屏幕截图、诸如X射线和超声波之类的医学图像等(图2.1)。不论这些图像源自哪里,它们最终会以图像元素构成的矩形有序阵列的形式呈现。
把真实场景变为数字图像的过程是复杂多样的,而且在大多数情况下你所处理的图像已经是数字格式的,因此在这里我们只略述这个过程中的核心步骤。因为大多数图像获取方法在本质上都是基于典型光学照相机的变种,所以我们将从详细考察光学照相机的成像过程开始。
从13世纪起,针孔照相机作为最简单的照相机模型被广泛使用,那时它被称为“奥布斯古拉照相机”。尽管现在除了一些摄影爱好者以外已经没有人再用它了,但是针孔照相机模型对于理解简单照相机的核心光学组件仍然是十分有用的。
针孔照相机由一个密闭的盒子组成,在前侧面板上有一个小孔,光线透过这个孔在后侧面板上形成场景的缩小反转的像(图2.2)。
针孔照相机的几何性质十分简单,光轴穿过针孔垂直交于图像平面。我们假设一个可见目标(图2.2中的仙人掌)位于与针孔水平距离为Z、与光轴垂直距离为Y的位置,它的投影的高度y取决于两个参数:照相机盒子的固定深度f和坐标系原点与目标之间的距离Z。通过比较,
随着像的比例而变化,且和盒子的深度即f成正比,这和日常照相机焦距变化相似。对于一幅固定尺寸的图像,小的f(即短焦距)会产生较小的像和较大的视角,就像使用了广角镜头一样;而增加焦距f会造成较大的像和较小的视角,正如使用了远距镜头的效果。公式(2.1)中的负号代表投影图像在水平方向和垂直方向经过了反转并且旋转了180度。公式(2.1)描述了今天普遍认可的投影变换关系,这个理论模型的两个重要性质是3D空间中的直线总是投影为2D直线,而3D空间中的圆投影为椭圆。
实际上,玻璃透镜或者光学透镜系统被用来代替针孔,它们具有多方面的优越性但同时也复杂得多。用图2.3中所示“薄透镜”代替针孔,它们具有多方面的优越性但同时也复杂得多。
投影在照相机像平面上的像,本质上是一种二维的、时间相关的、连续分布的光能量。
为了把这种像转换成计算机上的数字图像,需要经过以下三个主要步骤。
1.连接的光线分布必须被空间采样。
2.步骤1的采样结果必须在时间域被采样,以创建一幅图像。
3.最后,采样的值必须被量化到有限范围的整数,这样它们才能在计算机中被表示。
像的空间采样(即从连续信号到其离散形式的转换)依赖于图像获取设备(例如数码相机和摄像机)传感器单元的几何特征。各个传感器单元被排列在传感器平面(图2.4)上有序的行中,彼此之间几乎总是保持着合适的夹角。另外,可以在某些特殊的产品中发现一些具有六边形单元和圆形传感器结构的成像传感器。
时域采样是通过在均匀的时间间隔上测量每个独立传感器单元上的光入射量来完成的。数码相机中的CCD通过触发充电过程,然后测量指定时间内在CCD接受光照所积累的电荷数量来完成时域采样。
为了让计算机上存储和处理图像中的数值,它们通常被转换为整数值(例如256=28或者4096=212)。在一些专业应用中(例如医学图像处理)偶尔会出现需要用到浮点型的情况。转换是通过使用一个模数转换器进行的,这个转换器通常被直接集成在传感器单元中(这样转换就发生在图像拍摄的过程中)或者通过专门接口硬件执行。
把图像看作离散函数
这三步的操作结果是以二维有序整数矩阵(图2.5)的形式对图像的描述。形式化表述为:一幅数字图像 I 是一个将整数坐标N * N映射到某个范围的图像值P的二维函数,使得
现在我们已经做好了把图像转移到计算机中的准备,因此图像可以被保存、压缩或者处理成我们所选择的文件格式。这时,图像源自何处对我们来说已经不重要了,因为现在它就是一个简单的二维数值数据矩阵。在继续讲解以前,我们需要了解一些更重要的定义。
后面我们将假设图像是矩形的,尽管这是一个相对安全的假设,但仍然存在例外。一幅图像的尺寸可以直接由图像矩阵 I 的宽度M(列数)和高度N(行数)确定。
一幅图像的分辨率指定了它在真实世界中的空间尺寸,以每个计量单位的图像元素数给出,例如印刷产品中使用的点每英寸(dpi)或者线每英寸(lpi),又如卫星图像中的像素每千米。在大多数情况下,图像分辨率在水平方向和垂直方向上是相同的,因为图像元素是方的。
除了一些处理几何运算的算法以外,一般没有必要了解一幅图像的空间分辨率。然而,在涉及几何元素的情况下精确的分辨率信息就变得非常重要了,例如要在图像上绘制圆或者测量图像中的某段距离。正因为此,大部分的图像格式和专业用途设计的软件系统都包含精确的分辨率信息。
为了确定图像上哪个位置对应哪个像素,我们需要引入一个坐标系。与数学上惯用的坐标系相反,图像处理中的坐标系在垂直方向经过了反转;也就是说y坐标自顶向下逐渐增大,原点位于左上角(图2.6)。尽管这种坐标系不论在理论上还是实际上都没有什么优越性,事实上它还使得几何变换描述起来更加困难,但这种坐标系几乎无一例外地在图像处理软件系统中被使用。此系统起源于电视电子学,图像中的各行随着电子束按照从屏幕顶部到底部的顺序被显示。我们让行和列的编号从0开始,因为Java中数组的下标是从0开始的。
一个图像元素中所含的信息取决于用来表示它的数据类型。像素值实际上总是长度为k的二进制字,因此一个像素可以表示2k种不同的值。k值由图像的位深度(或深度)决定,通常是处理器的字长。
一个单独像素的位数取决于图像的种类,例如二值图、灰度图、或者RGB彩色图。表2.1总结了常用图像类型。
一幅灰度图像的图像数据由代表图像强度、亮度或密度的单个通道构成。在大多数情况下,只有正的值才有意义,因为数字代表了光能量的强度,而这个强度不可能是负的,因此通常使用[0…2k-1]中的整数值。例如,一幅典型的灰度图像每个像素使用k=8位(1字节),即强度值在范围[0…255]中,其中0代表最小亮度(黑色),255代表最大亮度(白色)。
在专业的摄影、印刷以及医学和天文学中,一个像素8位并不够用,在这些领域中经常会遇到12、14甚至16位的图像深度。注意位深度是指用于表示单个颜色的位数,不是表示整个像素所需的位数。例如,一幅8位深度的RGB编码彩色图像每个通道需要8位,总共需要24位,而具有12位深度的同样的图像总共需要36位。
直到几年前,从事图像处理的还只是那些能够接触昂贵的商业图像处理工具或者需要自己开发软件包的一小群人。通常这些自制环境都是从一些小的软件组件开始的,例如,从磁盘上载入图像或者将图像保存至磁盘的程序。开发这些小软件并不总是很容易,因为它需要处理一些蹩脚的甚至私有的文件格式。最常用的解决方法是:简单定义一种新的图像文件格式,针对特殊领域、特殊应用、甚至一个单独的工程,这种格式经常进行了优化。这种解决方法直接导致多种不同的文件格式的产生,大多数文件格式在今天已经不再被使用甚至已经被遗忘[71,74]。然而,在20世纪80年代和90年代初期,编写这些不同格式间转换的软件却是一项重要的工作,占用了大量的人力。早期,即使在计算机屏幕上显示图像也是很难的,因为操作系统、API和显示硬件只能提供一些边缘的支持,而用普通的硬件将图像或视频捕获到计算机中也是近乎不可能的。因此,为了在计算机上做一些初级的图像操作或者进行一些高级的图像处理,人民往往需要花费几个星期甚至几个月来做前期的准备工作。
幸运的是,如今的情况以及大为好转,只有一些通用的图像文件格式仍然存在(参考第2.3节),这些图像格式可以被许多现有的工具和软件库方便地处理。大部分为C/C++、Java和其他流行的编程语言设计的标准API,已经至少可提供一些对图像和其他媒体数据操作的基本支持。尽管在这个层次下还有许多开发工作要做,但它已经使我们的工作变得更加简单,由此我们可以把注意力集中于数字图像处理中那些更有趣的方面。
传统上,数字图像处理软件的目标可以是图像的操作或处理,其面向的用户也具有不同需求:既有从业者和设计人员,也有软件编程人员。图像操作软件包(例如Adobe Photoshop , Corel Paint等)通常提供了方便的用户接口、大量易于使用的函数以及图像进行相关交互式工作的工具,有时甚至可以通过编写脚本或者添加自编程组件 来扩展其标准功能。
相对于上面的一类工具来说,数字图像处理软件主要是针对软件开发者、科学家以及从事图像相关工作的工程师而设计的。这些软件开发的重点不在于软件本身的交互性和易用性,而是提供广泛的、成熟的软件库来简化新的图像处理算法、原型和应用的实现过程。在众多此类工具中,比较流行的有Khoros/VisiQuest、IDL、Matlab和ImageMagick。这类系统不仅支持C/C++,还提供专门的脚本语言或可视化的编程辅助工具。
实际上,图像操作和图像处理紧密相关。例如,尽管Photoshop的目标是帮助非程序设计人员进行图像操作,但软件本身却实现了许多传统的图像处理算法。同样,许多Web应用程序(例如那些基于ImageMagick的程序)也是使用看服务器端的图像处理算法。因此,图像处理是任何图像软件的基础,并不是一个完全不同的类别。
ImageJ是本书所采用的软件,它是以上讨论的两类工具的结合。它提供了一套现成的工具,用于图像的查看和交互操作,同时也可以通过应用某种“真正的”编程语言编写新的软件组件来得到扩展。ImageJ完全采用Java语言编写,因此它与平台无关,可以不加修改地运行在Windows、MasOS或者Linux操作系统上。Java的动态执行模型允许将新的模块(“插件”)作为独立的Java代码段来编写,这些代码甚至不需要重新启动ImageJ就可以被编码、加载和在运行的系统中执行,这使得ImageJ成为开发和测试新的图像处理技术和算法的理想平台。由于Java语言作为许多工程课程的首选语言已经非常流行,所以通常学生不需要花费大量的时间去学习另一种编程语言就能够非常轻松地使用ImageJ。同时,ImageJ可免费获取,不论学生、教师还是从业人员都可以合法地在任何计算机上安装使用它,而不必购买许可。因此,ImageJ是一个进行数字图像处理教学和自我训练的理想平台,同时它也在全球范围内的许多实验室里被用于正式的研究和应用程序开发,特别是在生物学和医学的图像处理中。
ImageJ是由美国国家卫生研究院(NIH)的Wayne Rasband[79]开发的,起初是作为它的前身(只能在Apple Macintosh平台下使用的)NIH-Image的替代品。ImageJ的当前版本、更新、文档、全部源代码、测试图像以及不断增加的第三方插件集都可以在ImageJ的网站下载。软件的安装很简单,具体的操作指南可以从网站、Werner Bailer的编程教程[4]以及本书的附录C中获得。
尽管ImageJ是一款优秀的工具,但由于其起源和历史,使得它并不是十全十美的。从软件工程的角度来看,它的架构设计并不直观,我们期望它具有更强的灵活性(例如,若干任务可以通过多种不同的方式来完成)。出于结构化考虑,附录C按照任务的不同领域进行了分类,并精选了一些关键功能;一些很少使用的特殊功能被省略了,当然它们可以在ImageJ的文档和(在线)源代码中找到。
作为一个纯粹的Java应用程序,ImageJ应该能运行在任务安装了Java运行时环境(JRE)的计算机上。事实上ImageJ包含了自己的Java运行时,因此无需在计算机上再单独安装Java。大多数情况下,ImageJ被当作一个独立的应用程序来使用,但是它也可以作为Java小应用程序(applet)在Web浏览器中运行。有时它还会在服务器端基于Java的Web应用程序中被使用(细节参考[4])。总的来说,imageJ的主要特征包括如下:
ImageJ启动后首先打开的是主窗口(图3.1),其中包括下列菜单项。
插件是采用简单的标准化接口来扩展ImageJ功能的小型Java模块(图3.2)。你可以通过ImageJ主菜单中的Plugin菜单(图3.1)被创建、编辑、编译、调用以及组织。插件可以通过分组来提高其模块化,而插件命令可以任意地放置在主菜单结构中。另外,ImageJ许多内置的功能实际上也是通过插件来实现的。
从技术上来讲,插件是实现了由ImageJ定义的特殊接口规范的Java类,包括
两种不同类型的插件:
int setup(String arg,ImagePlus im)
当插件启动时,ImageJ首先调用这个方法来验证该插件的功能是否与目标图像匹配。setup()以32位整型值的形式返回一个二进制标志位向量来描述插件的属性。
void run(ImageProcessor ip)
这个方法执行插件的实际功能。它接受一个单独的参数ip,这个参数是ImageProcessor类的对象,即包括待处理的图像和相关的信息。run()方法没有返回值(void),但可能会修改传入的图像或者创建新的图像。
让我们用一个实际的例子来快速阐明这种机制。我们第一个插件的任务是对一幅8位灰度图像取反,使其由正片变为负片。后面我们将会看到,对图像的灰度值取反是一种典型的点运算(点运算的内容将在第5章详细讨论)。在ImageJ中,8位灰度图像的像素值取值范围是0(黑色)~255(白色),此外我们假设图像的宽度和高度分别是M和N。取反操作十分简单:将每个像素的灰度值I(u,v)替换为它的相反值,
其中(u,v)是图像坐标,u=0…M-1,v=0…N-1 。
把我们的第一个插件命名为“My_Inverter”,它既是Java类的名字,也是包含它的源文件的名字(程序3.1)。名字中的下划线“_”使这个类识别为一个插件,并且在启动时自动把它插入菜单列表。文件My_Inverter.java中的Java源代码包含一下import声明,紧接着实现了PlugInFilter接口(因为它将要应用于一幅已存图像)的类My_Inverter的定义。
当一个PlugInFilter类型的插件被执行时,ImageJ首先调用它的setup()方法来获得关于插件本身的信息。在这个例子中,setup()只返回值DOES_8G(由PlugInFilter接口指定的静态整型常量),表明这个插件可以处理8位灰度图像(程序3.1 第8行)。setup()方法的参数arg和im在这个例子中没有被使用(参考联系3.4)。
package com.myplugin;
import ij.ImagePlus;
import ij.plugin.filter.PlugInFilter;
import ij.process.ImageProcessor;
public class My_Inverter implements PlugInFilter{
@Override
public void run(ImageProcessor ip) {
int w = ip.getWidth();
int h = ip.getHeight();
//在图像所有坐标中循环
for (int u = 0; u < w; u++) {
for (int v = 0; v < h; v++) {
int p = ip.getPixel(u, v);
ip.putPixel(u, v, 255-p);
}
}
}
@Override
public int setup(String arg0, ImagePlus arg1) {
return DOES_8G;
}
}
如上所述,一个PlugInFilter插件的run()方法接受一个ImageProcessor类型的对象(ip),其中包含待处理的图像及其所有相关信息。首先,我们用ImageProcessor类的方法getWidth()和getHeight()来获得ip所引用图像的尺寸,接着我们用两个嵌套的for循环(循环变量u、v分别代表水平和垂直坐标)遍历图像的所有像素。为了读取和写入像素值,我们用到了ImageProcessor类的另外两个方法:
int getPixel(int u,int v)
返回(u,v)处的像素值,如果(u,v)超过图像边界则返回0。
void putPixel(int u,int v,int a)
设置(u,v)处的像素值为一个新值a,如果(u,v)超过图像边界则不做任何操作。
关于这些方法和其他方法的细节可以在附录C中的ImageJ参考文献中找到。
若确定没有图像边界以外的坐标被访问(像程序3.1中My_Inverter那样),同时能够保证插入的像素值不超过图像处理算法的范围,那么我们就可以用较快的方法get()和set()来分别代替getPixel()和putPixel()方法(见附录C)。处理图像最有效的方法是彻底避免使用读/写方法,而直接访问相应像素数组的元素,参见附录C.7.6。
我们的插件的源代码应该被保存在文件My_Inverter.java中,这个文件位于/plugins/或者一个一级子目录中,新的插件文件可以通过ImageJ的Plugins——>New菜单创建。ImageJ还提供了一个用于编写插件的内置Java编辑器,可以通过Plugins——>Edit…菜单来使用它,可惜的是它对较正式的编程没有多大帮助。一个比较好的选择是使用现代的编辑器或者专业的Java编程环境,例如Eclipse、NetBeans或者JBuilder,这些软件均可免费获取。
为了编译插件(到Java字节码),ImageJ附带了自己的Java编译器作为运行时环境的一部分。编译和执行新的插件,可以简单地使用菜单:
编译错误将会显示在独立的日志窗口中。一旦插件被编译,相应的.class文件就会被自动加载,该插件则被应用于当前操作图像。如果当前没有打开的图像或者当前没有打开的图像或者当前图像不能被这个插件处理,则显示一条错误消息。
启动时,ImageJ会自动地加载所有在/plugins/目录(或一级子目录)下找到的正确命名的插件,并把它们装入Plugins菜单。这些插件无需重新编译就可以立即执行。对插件的引用也可以通过命令
手工置于ImageJ菜单树的任何其他位置。一系列插件的调用和其他ImageJ命令可以通过Plugins——>Macros——>Record记录为宏程序。
程序3.1中,我们的第一个插件并没有创建新的图像,而是“破坏性的”修改了目标图像。情况并不总是这样,插件也可以另外创建图像或者只是进行统计计算,而不对原始图像进行任何修改。我们的插件并不包含任何显示修改后图像的命令,这也许有些奇怪。实际上当传递给插件的图像认为被修改时,ImageJ会自动地完成该图像的显示。另外,ImageJ在将图像传递给PluginFilter类型的run()方法以前会自动保存一个副本(“快照”),这个功能使得用插件处理过图像之后还可以重新恢复原始图像(Edit——>Undo菜单),而不必在插件代码中加入任务显式的预防措施。
直方图是一种频率分布图,它描述了不同强度值在图像中出现的频率。这个概念可以通过图4.2所示的灰度图像来解释,一幅 图像I的灰度值范围为:
它的直方图h中正好包含K个条目(对于一幅典型的8位灰度图像,K=28=256),每一个单独的条目被定义为:
因此,h(0)表示灰度值为0的像素点数目,h(1)表示灰度值为1的像素点数目,依次类推。最后,h(255)表示具有最大灰度值255=K-1的白色像素点数目。灰度直方图运算的结果是一个长度为K的一维向量h。图4.3给出了一个具有K=16种可能灰度值的图像的例子。
由于直方图并未反映出其每个条目对应于图像中的位置,因此直方图不包含图像中像素点的空间排列信息。这是因为直方图的主要功能是以紧凑的形式反映图像的统计信息(例如强度值的分布)。是否可以仅利用直方图来重建一幅图像呢?也就是说,直方图是否可以以某种方式逆转?在非特殊情况下,答案是否定的。例如,用同样数目的具有特定灰度值的像素点可以建立多种多样的图像,尽管这些图像看上去差别很大,但它们却具有完全相同的直方图(图4.4)。
直方图能够描述图像获取过程中产生的问题,例如那些涉及对比度以及动态范围的问题,以及由图像处理过程造成的瑕疵。通过检查直方图分布的范围和均匀程度,可以确定一幅图像是否有效地利用了它的强度范围(图4.5)。
直方图使典型的曝光 问题变得显而易见。例如,一端较大强度取值范围未使用而另一端充满峰值的直方图(图4.6)就代表图像曝光不合适。
对比度包含两方面的含义,一方面是指一幅给定图像中强度值的有效利用范围,另一方面是指图像中最大和最小像素强度值的差距。一幅全对比度图像有效地利用了全部的可用强度值范围:
利用这个定义,一幅图像的对比度可用轻易地从直方图中得到。图4.7说明了对比度的变化时怎样影响直方图的。
一幅图像的动态范围是指图像中不同像素灰度值的数目。理想情况下,即所有取值都被利用的情况下,动态范围是所有可能像素值K。如果一幅图像的可用对比度范围是:
那么动态范围的最大可能值将在这个可用对比度范围内所有强度值都被利用(即在图像中出现;图4.8)的情况下达到。