目录
第 1 章 基本的图像操作和处理
1.1 PIL:Python图像处理类库
1.1.1 转换图像格式
1.1.2 创建缩略图
1.2 Matplotlib
1.2.1 绘制图像、点和线
1.2.2 图像轮廓和直方图
1.2.3 交互式标注
1.3 NumPy
1.3.1 图像数组表示
1.3.2 灰度变换
1.3.3 图像缩放
1.3.4 直方图均衡化
1.3.5 图像平均
1.3.6 图像的主成分分析(PCA)
1.3.7 使用pickle模块
1.4 SciPy
1.4.1 图像模糊
1.4.2 图像导数
1.4.3 形态学:对象计数
1.4.4 一些有用的SciPy模块
1.5 高级示例:图像去噪
本章讲解操作和处理图像的基础知识,将通过大量示例介绍处理图像所需的 Python 工具包,并介绍用于读取图像、图像转换和缩放、计算导数、画图和保存结果等的基本工具。这些工具的使用将贯穿本书的剩余章节。
PIL(Python Imaging Library Python,图像处理类库)提供了通用的图像处理功能,以及大量有用的基本图像操作,比如图像缩放、裁剪、旋转、颜色转换等。PIL 是免费的,可以从 http://www.pythonware.com/products/pil/ 下载。
利用 PIL 中的函数,我们可以从大多数图像格式的文件中读取数据,然后写入最常见的图像格式文件中。PIL 中最重要的模块为 Image
。要读取一幅图像,可以使用:
from PIL import Image
pil_im = Image.open('empire.jpg')
上述代码的返回值 pil_im
是一个 PIL 图像对象。
图像的颜色转换可以使用 convert()
方法来实现。要读取一幅图像,并将其转换成灰度图像,只需要加上 convert('L')
,如下所示:
pil_im = Image.open('empire.jpg').convert('L')
在 PIL 文档中有一些例子,参见 http://www.pythonware.com/library/pil/handbook/index.htm。这些例子的输出结果如图 1-1 所示。
图 1-1:用 PIL 处理图像的例子
通过 save()
方法,PIL 可以将图像保存成多种格式的文件。下面的例子从文件名列表(filelist
)中读取所有的图像文件,并转换成 JPEG 格式:
from PIL import Image
import os
for infile in filelist:
outfile = os.path.splitext(infile)[0] + ".jpg"
if infile != outfile:
try:
Image.open(infile).save(outfile)
except IOError:
print("cannot convert", infile)
PIL 的 open()
函数用于创建 PIL 图像对象,save()
方法用于保存图像到具有指定文件名的文件。除了后缀变为“.jpg”,上述代码的新文件名和原文件名相同。PIL 是个足够智能的类库,可以根据文件扩展名来判定图像的格式。PIL 函数会进行简单的检查,如果文件不是 JPEG 格式,会自动将其转换成 JPEG 格式;如果转换失败,它会在控制台输出一条报告失败的消息。
本书会处理大量图像列表。下面将创建一个包含文件夹中所有图像文件的文件名列表。首先新建一个文件,命名为 imtools.py,来存储一些经常使用的图像操作,然后将下面的函数添加进去:
import os
def get_imlist(path):
""" 返回目录中所有JPG 图像的文件名列表"""
return [os.path.join(path,f) for f in os.listdir(path) if f.endswith('.jpg')]
现在,回到 PIL。
使用 PIL 可以很方便地创建图像的缩略图。thumbnail()
方法接受一个元组参数(该参数指定生成缩略图的大小),然后将图像转换成符合元组参数指定大小的缩略图。例如,创建最长边为 128 像素的缩略图,可以使用下列命令:
pil_im.thumbnail((128,128))
1.1.3 复制和粘贴图像区域
使用 crop()
方法可以从一幅图像中裁剪指定区域:
box = (100,100,400,400)
region = pil_im.crop(box)
该区域使用四元组来指定。四元组的坐标依次是(左,上,右,下)。PIL 中指定坐标系的左上角坐标为(0,0)。我们可以旋转上面代码中获取的区域,然后使用 paste()
方法将该区域放回去,具体实现如下:
region = region.transpose(Image.ROTATE_180)
pil_im.paste(region,box)
1.1.4 调整尺寸和旋转
要调整一幅图像的尺寸,我们可以调用 resize()
方法。该方法的参数是一个元组,用来指定新图像的大小:
out = pil_im.resize((128,128))
要旋转一幅图像,可以使用逆时针方式表示旋转角度,然后调用 rotate()
方法:
out = pil_im.rotate(45)
上述例子的输出结果如图 所示。最左端是原始图像,然后是灰度图像、粘贴有旋转后裁剪图像的原始图像,最后是缩略图。
我们处理数学运算、绘制图表,或者在图像上绘制点、直线和曲线时,Matplotlib
是个很好的类库,具有比 PIL 更强大的绘图功能。Matplotlib
可以绘制出高质量的图表,就像本书中的许多插图一样。Matplotlib
中的 PyLab
接口包含很多方便用户创建图像的函数。Matplotlib
是开源工具,可以从 http://matplotlib.sourceforge.net/ 免费下载。该链接中包含非常详尽的使用说明和教程。下面的例子展示了本书中需要使用的大部分函数。
尽管 Matplotlib
可以绘制出较好的条形图、饼状图、散点图等,但是对于大多数计算机视觉应用来说,仅仅需要用到几个绘图命令。最重要的是,我们想用点和线来表示一些事物,比如兴趣点、对应点以及检测出的物体。下面是用几个点和一条线绘制图像的例子:
from PIL import Image
from pylab import *
# 读取图像到数组中
im = array(Image.open('empire.jpg'))
# 绘制图像
imshow(im)
# 一些点
x = [100,100,400,400]
y = [200,500,200,500]
# 使用红色星状标记绘制点
plot(x,y,'r*')
# 绘制连接前两个点的线
plot(x[:2],y[:2])
# 添加标题,显示绘制的图像
title('Plotting: "empire.jpg"')
show()
上面的代码首先绘制出原始图像,然后在 x 和 y 列表中给定点的 x 坐标和 y 坐标上绘制出红色星状标记点,最后在两个列表表示的前两个点之间绘制一条线段(默认为蓝色)。该例子的绘制结果如图 1-2 所示。show()
命令首先打开图形用户界面(GUI),然后新建一个图像窗口。该图形用户界面会循环阻断脚本,然后暂停,直到最后一个图像窗口关闭。在每个脚本里,你只能调用一次 show()
命令,而且通常是在脚本的结尾调用。注意,在 PyLab
库中,我们约定图像的左上角为坐标原点。
图像的坐标轴是一个很有用的调试工具;但是,如果你想绘制出较美观的图像,加上下列命令可以使坐标轴不显示:
axis('off')
在绘图时,有很多选项可以控制图像的颜色和样式。最有用的一些短命令如表 1-1、表 1-2 和表 1-3 所示。使用方法见下面的例子:
plot(x,y) # 默认为蓝色实线
plot(x,y,'r*') # 红色星状标记
plot(x,y,'go-') # 带有圆圈标记的绿线
plot(x,y,'ks:') # 带有正方形标记的黑色虚线
表1-1:用PyLab
库绘图的基本颜色格式命令
颜色 |
|
---|---|
|
蓝色 |
|
绿色 |
|
红色 |
|
青色 |
|
品红 |
|
黄色 |
|
黑色 |
|
白色 |
表1-2:用PyLab
库绘图的基本线型格式命令
线型 |
|
---|---|
|
实线 |
|
虚线 |
|
点线 |
表1-3:用PyLab
库绘图的基本绘制标记格式命令
标记 |
|
---|---|
|
点 |
|
圆圈 |
|
正方形 |
|
星形 |
|
加号 |
|
叉号 |
下面来看两个特别的绘图示例:图像的轮廓和直方图。绘制图像的轮廓(或者其他二维函数的等轮廓线)在工作中非常有用。因为绘制轮廓需要对每个坐标 [x, y] 的像素值施加同一个阈值,所以首先需要将图像灰度化:
from PIL import Image
from pylab import *
# 读取图像到数组中
im = array(Image.open('empire.jpg').convert('L'))
# 新建一个图像
figure()
# 不使用颜色信息
gray()
# 在原点的左上角显示轮廓图像
contour(im, origin='image')
axis('equal')
axis('off')
像之前的例子一样,这里用 PIL 的 convert()
方法将图像转换成灰度图像。
图像的直方图用来表征该图像像素值的分布情况。用一定数目的小区间(bin)来指定表征像素值的范围,每个小区间会得到落入该小区间表示范围的像素数目。该(灰度)图像的直方图可以使用 hist()
函数绘制:
figure()
hist(im.flatten(),128)
show()
hist()
函数的第二个参数指定小区间的数目。需要注意的是,因为 hist()
只接受一维数组作为输入,所以我们在绘制图像直方图之前,必须先对图像进行压平处理。flatten()
方法将任意数组按照行优先准则转换成一维数组。
1.2.3 交互式标注
有时用户需要和某些应用交互,例如在一幅图像中标记一些点,或者标注一些训练数据。PyLab
库中的 ginput()
函数就可以实现交互式标注。下面是一个简短的例子:
from PIL import Image
from pylab import *
im = array(Image.open('empire.jpg'))
imshow(im)
print('Please click 3 points')
x = ginput(3)
print('you clicked:',x)
show()
上面的脚本首先绘制一幅图像,然后等待用户在绘图窗口的图像区域点击三次。程序将这些点击的坐标 [x, y] 自动保存在 x 列表里。
1.3 NumPy
NumPy
(http://www.scipy.org/NumPy/)是非常有名的 Python 科学计算工具包,其中包含了大量有用的思想,比如数组对象(用来表示向量、矩阵、图像等)以及线性代数函数。NumPy
中的数组对象几乎贯穿用于本书的所有例子中 1 数组对象可以帮助你实现数组中重要的操作,比如矩阵乘积、转置、解方程系统、向量乘积和归一化,这为图像变形、对变化进行建模、图像分类、图像聚类等提供了基础。
PyLab
实际上包含 NumPy
的一些内容,如数组类型。这也是我们能够在 1.2 节使用数组类型的原因。
NumPy
可以从 http://www.scipy.org/Download 免费下载,在线说明文档(http://docs.scipy.org/doc/numpy/)包含了你可能遇到的大多数问题的答案。关于 NumPy
的更多内容,请参考开源书籍 [24]。
在先前的例子中,当载入图像时,我们通过调用 array()
方法将图像转换成 NumPy
的数组对象,但当时并没有进行详细介绍。NumPy
中的数组对象是多维的,可以用来表示向量、矩阵和图像。一个数组对象很像一个列表(或者是列表的列表),但是数组中所有的元素必须具有相同的数据类型。除非创建数组对象时指定数据类型,否则数据类型会按照数据的类型自动确定。
对于图像数据,下面的例子阐述了这一点:
im = array(Image.open('empire.jpg'))
print(im.shape, im.dtype)
im = array(Image.open('empire.jpg').convert('L'),'f')
print(im.shape, im.dtype)
控制台输出结果如下所示:
(800, 569, 3) uint8
(800, 569) float32
每行的第一个元组表示图像数组的大小(行、列、颜色通道),紧接着的字符串表示数组元素的数据类型。因为图像通常被编码成无符号八位整数(uint8),所以在第一种情况下,载入图像并将其转换到数组中,数组的数据类型为“uint8”。在第二种情况下,对图像进行灰度化处理,并且在创建数组时使用额外的参数“f”;该参数将数据类型转换为浮点型。关于更多数据类型选项,可以参考图书 [24]。注意,由于灰度图像没有颜色信息,所以在形状元组中,它只有两个数值。
数组中的元素可以使用下标访问。位于坐标 i、j,以及颜色通道 k 的像素值可以像下面这样访问:
value = im[i,j,k]
多个数组元素可以使用数组切片方式访问。切片方式返回的是以指定间隔下标访问该数组的元素值。下面是有关灰度图像的一些例子:
im[i,:] = im[j,:] # 将第 j 行的数值赋值给第 i 行
im[:,i] = 100 # 将第 i 列的所有数值设为100
im[:100,:50].sum() # 计算前100 行、前 50 列所有数值的和
im[50:100,50:100] # 50~100 行,50~100 列(不包括第 100 行和第 100 列)
im[i].mean() # 第 i 行所有数值的平均值
im[:,-1] # 最后一列
im[-2,:] (or im[-2]) # 倒数第二行
注意,示例仅仅使用一个下标访问数组。如果仅使用一个下标,则该下标为行下标。注意,在最后几个例子中,负数切片表示从最后一个元素逆向计数。我们将会频繁地使用切片技术访问像素值,这也是一个很重要的思想。
我们有很多操作和方法来处理数组对象。本书将在使用到的地方逐一介绍。你可以查阅在线文档或者开源图书 [24] 获取更多信息。
将图像读入 NumPy
数组对象后,我们可以对它们执行任意数学操作。一个简单的例子就是图像的灰度变换。考虑任意函数 f,它将 0...255 区间(或者 0...1 区间)映射到自身(意思是说,输出区间的范围和输入区间的范围相同)。下面是关于灰度变换的一些例子:
from PIL import Image
from numpy import *
im = array(Image.open('empire.jpg').convert('L'))
im2 = 255 - im # 对图像进行反相处理
im3 = (100.0/255) * im + 100 # 将图像像素值变换到100...200 区间
im4 = 255.0 * (im/255.0)**2 # 对图像像素值求平方后得到的图像
第一个例子将灰度图像进行反相处理;第二个例子将图像的像素值变换到 100...200 区间;第三个例子对图像使用二次函数变换,使较暗的像素值变得更小。图 1-4 为所使用的变换函数图像。图 1-5 是输出的图像结果。你可以使用下面的命令查看图像中的最小和最大像素值:
print int(im.min()), int(im.max())
f(x)=255-x 对图像进行反相处理(b);f(x)=(100/255)x+100 对图像进行变换(c);f(x)=255(x/255)2 对图像做二次变换(d)
如果试着对上面例子查看最小值和最大值,可以得到下面的输出结果:
0 254
1 255
100 199
0 253
array()
变换的相反操作可以使用 PIL 的 fromarray()
函数完成:
pil_im = Image.fromarray(im)
如果你通过一些操作将“uint8”数据类型转换为其他数据类型,比如之前例子中的 im3 或者 im4,那么在创建 PIL 图像之前,需要将数据类型转换回来:
pil_im = Image.fromarray(uint8(im))
如果你并不十分确定输入数据的类型,安全起见,应该先转换回来。注意,NumPy
总是将数组数据类型转换成能够表示数据的“最低”数据类型。对浮点数做乘积或除法操作会使整数类型的数组变成浮点类型。
NumPy
的数组对象是我们处理图像和数据的主要工具。想要对图像进行缩放处理没有现成简单的方法。我们可以使用之前 PIL 对图像对象转换的操作,写一个简单的用于图像缩放的函数。把下面的函数添加到 imtool.py 文件里:
def imresize(im,sz):
""" 使用PIL 对象重新定义图像数组的大小"""
pil_im = Image.fromarray(uint8(im))
return array(pil_im.resize(sz))
我们将会在接下来的内容中使用这个函数。
图像灰度变换中一个非常有用的例子就是直方图均衡化。直方图均衡化是指将一幅图像的灰度直方图变平,使变换后的图像中每个灰度值的分布概率都相同。在对图像做进一步处理之前,直方图均衡化通常是对图像灰度值进行归一化的一个非常好的方法,并且可以增强图像的对比度。
在这种情况下,直方图均衡化的变换函数是图像中像素值的累积分布函数(cumulative distribution function,简写为 cdf,将像素值的范围映射到目标范围的归一化操作)。
下面的函数是直方图均衡化的具体实现。将这个函数添加到 imtool.py 里:
def histeq(im,nbr_bins=256):
""" 对一幅灰度图像进行直方图均衡化"""
# 计算图像的直方图
imhist,bins = histogram(im.flatten(),nbr_bins,normed=True)
cdf = imhist.cumsum() # cumulative distribution function
cdf = 255 * cdf / cdf[-1] # 归一化
# 使用累积分布函数的线性插值,计算新的像素值
im2 = interp(im.flatten(),bins[:-1],cdf)
return im2.reshape(im.shape), cdf
该函数有两个输入参数,一个是灰度图像,一个是直方图中使用小区间的数目。函数返回直方图均衡化后的图像,以及用来做像素值映射的累积分布函数。注意,函数中使用到累积分布函数的最后一个元素(下标为 -1),目的是将其归一化到 0...1 范围。你可以像下面这样使用该函数:
from PIL import Image
from numpy import *
im = array(Image.open('AquaTermi_lowcontrast.jpg').convert('L'))
im2,cdf = imtools.histeq(im)
下图为上面直方图均衡化例子的结果。上面一行显示的分别是直方图均衡化之前和之后的灰度直方图,以及累积概率分布函数映射图像。可以看到,直方图均衡化后图像的对比度增强了,原先图像灰色区域的细节变得清晰。
1.3.5 图像平均
图像平均操作是减少图像噪声的一种简单方式,通常用于艺术特效。我们可以简单地从图像列表中计算出一幅平均图像。假设所有的图像具有相同的大小,我们可以将这些图像简单地相加,然后除以图像的数目,来计算平均图像。下面的函数可以用于计算平均图像,将其添加到 imtool.py 文件里:
# 图像平均操作是减少图像噪声的一种简单方式,通常用于艺术特效。
# 可以简单地从图像列表中计算出一幅平均图像。
# 假设所有的图像具有相同的大小,可以将这些图像简单地相加,然后除以图像的数目,来计算平均图像。
from PIL import Image
from PCV.tools.imtools import get_imlist
from pylab import *
from PCV.tools import imtools
# 添加中文字体支持
from matplotlib.font_manager import FontProperties
font = FontProperties(fname=r"c:\windows\fonts\SimSun.ttc", size=14)
filelist = get_imlist('.\\data\\avg')
print(filelist)
avg = imtools.compute_average(filelist)
for impath in filelist:
im1 = array(Image.open(impath))
subplot(2,2,filelist.index(impath)+1)
imshow(im1)
imNum = str(filelist.index(impath)+1)
title(u'待平均图像'+imNum, FontProperties=font)
axis('off')
subplot(2,2,4)
imshow(avg)
title(u'平均后的图像',FontProperties=font)
axis('off')
show()
# 分析:
# 该函数包括一些基本的异常处理技巧,可以自动跳过不能打开的图像。
# 我们还可以使用 mean() 函数计算平均图像。
# mean() 函数需要将所有的图像堆积到一个数组中;
# 也就是说,如果有很多图像,该处理方式需要占用很多内存。
该函数包括一些基本的异常处理技巧,可以自动跳过不能打开的图像。我们还可以使用 mean()
函数计算平均图像。mean()
函数需要将所有的图像堆积到一个数组中;也就是说,如果有很多图像,该处理方式需要占用很多内存。我们将会在下一节中使用该函数。
PCA(Principal Component Analysis,主成分分析)是一个非常有用的降维技巧。
它可以在使用尽可能少维数的前提下,尽量多地保持训练数据的信息,在此意义上是一个最佳技巧。
即使是一幅 100×100 像素的小灰度图像,也有 10 000 维,可以看成 10 000 维空间中的一个点。
一兆像素的图像具有百万维。由于图像具有很高的维数,在许多计算机视觉应用中,我们经常使用降维操作。
PCA 产生的投影矩阵可以被视为将原始坐标变换到现有的坐标系,坐标系中的各个坐标按照重要性递减排列。
为了对图像数据进行 PCA 变换,图像需要转换成一维向量表示。我们可以使用 NumPy
类库中的 flatten()
方法进行变换。
将变平的图像堆积起来,我们可以得到一个矩阵,矩阵的一行表示一幅图像。
在计算主方向之前,所有的行图像按照平均图像进行了中心化。我们通常使用 SVD(Singular Value Decomposition,奇异值分解)方法来计算主成分;但当矩阵的维数很大时,SVD 的计算非常慢,所以此时通常不使用 SVD 分解。下面就是 PCA 操作的代码:
from PIL import Image
from numpy import *
from pylab import *
from PCV.tools.imtools import get_imlist
import os
def pca(X):
'''
主成分分析:
输入:矩阵X,其中该矩阵中存储训练数据,每一行为一条训练数据
返回:投影矩阵(按照维度的重要性排序)、方差和均值
'''
#获取维度
num_date, dim = X.shape
#数据中心化
mean_X = X.mean(axis=0)
X = X-mean_X
if dim > num_date:
#(样本数相对少,图像大的时候
#PCA-使用紧凑
#协方差矩阵
M = dot(X,X.T)
#特征值和特征向量
e,EV = linalg.eigh(M)
#这是紧致技巧
tmp = dot(X.T,EV).T
#由于最后的特征向量是我们所需要的,所以需要将其逆转
V = tmp[::-1]
#由于特征值是按照递增顺序排列的,所以需要将其逆转
S = sqrt(e)[::-1]
for i in range(V.shape[1]):
V[:,i] /= S
else:
#(样本相对多,图像小的时候)
#PCA-使用SVD方法
U,S,V = linalg.svd(X)
#仅仅返回前num_data维的数据才合理
V = V[:num_date]
#返回投影矩阵、方差、均值
return V, S, mean_X
def my_get_imlist(path):
return [os.path.join(path,f) for f in os.listdir(path) if f.endswith('.png')]
imlist = my_get_imlist('.\\data\\data_for_pca')
#打开一幅图像
im = array(Image.open(imlist[0]))
imshow(im)
#获得图像大小
m, n = im.shape[:2]
#获取图像样本数
imnbr = len(imlist)
#创建矩阵,保存所有压平后的图像数据
immatrix = array([array(Image.open(im).convert('L')).flatten() for im in imlist],'f')
#执行PCA操作
V, S, immean = pca(immatrix)
#显示一些图像(均值图像和前7个模式)
figure()
gray()
subplot(2,4,1)
imshow(immean.reshape(m,n))
for i in range(7):
subplot(2,4,i+2)
imshow(V[i].reshape(m,n))
show()
注意,这些图像在拉成一维表示后,必须用reshape()函数将它重新转换回来。
本实验用的是彩色图像,通道数为3。彩色图像拉成一维再恢复成3维在本实验行不通,因此先将图像转成灰度图再拉成一维。
pickle
模块如果想要保存一些结果或者数据以方便后续使用,Python 中的 pickle
模块非常有用。pickle
模块可以接受几乎所有的 Python 对象,并且将其转换成字符串表示,该过程叫做封装(pickling)。从字符串表示中重构该对象,称为拆封(unpickling)。这些字符串表示可以方便地存储和传输。
import pickle
datalist = [[1,1,'yes'],
[1,1,'yes'],
[1,0,'no'],
[0,1,'no'],
[0,1,'no']]
#使用dump()将数据序列化到文件
fw = open('.\\data\\dataFile.txt','wb')
pickle.dump(datalist,fw,-1)
pickle.dump(datalist,fw,0)
fw.close()
#使用load()将数据从文件中序列化读出
fr = open('.\data\dataFile.txt','rb')
data1 = pickle.load(fr)
print(data1)
data2 = pickle.load(fr)
print(data2)
fr.close()
SciPy
(http://scipy.org/) 是建立在 NumPy
基础上,用于数值运算的开源工具包。SciPy
提供很多高效的操作,可以实现数值积分、优化、统计、信号处理,以及对我们来说最重要的图像处理功能。接下来,本节会介绍 SciPy
中大量有用的模块。SciPy
是个开源工具包,可以从 http://scipy.org/Download 下载。
图像的高斯模糊是非常经典的图像卷积例子。本质上,图像模糊就是将(灰度)图像 I 和一个高斯核进行卷积操作:
其中 * 表示卷积操作; Gσ 是标准差为 σ 的二维高斯核,定义为 :
高斯模糊通常是其他图像处理操作的一部分,比如图像插值操作、兴趣点计算以及很多其他应用
SciPy 有用来做滤波操作的 scipy.ndimage.filters 模块。该模块使用快速一维分离的方式来计算卷积。
from PIL import Image
from pylab import *
from numpy import *
from scipy.ndimage import filters
# 添加中文字体支持
from matplotlib.font_manager import FontProperties
font = FontProperties(fname=r"c:\windows\fonts\SimSun.ttc", size=14)
im = array(Image.open('.\\data\\flower.jpg').convert('L'))
figure()
gray()
axis('off')
subplot(1,4,1)
axis('off')
title(u'原图',FontProperties=font)
imshow(im)
for bi,blur in enumerate([2,5,10]):
im2 = zeros(im.shape)
im2 = filters.gaussian_filter(im,blur)
im2 = np.uint8(im2)
imNum = str(blur)
subplot(1,4,2+bi)
axis('off')
title(u'标准差为'+imNum,FontProperties=font)
imshow(im2)
show()
#如果是彩色图,则分别三个通道进行模糊
im3 = array(Image.open('.\\data\\flower.jpg'))
figure()
gray()
axis('off')
subplot(1,4,1)
axis('off')
title(u'原图',FontProperties=font)
imshow(im3)
for bi,blur in enumerate([2,5,10]):
im4 = zeros(im3.shape)
for i in range(3):
im4[:,:,i] = filters.gaussian_filter(im3[:,:,i],blur)
im4 = np.uint8(im4)
imNum = str(blur)
subplot(1,4,2+bi)
axis('off')
title(u'标准差为'+imNum,FontProperties=font)
imshow(im4)
show()
图像显示随着 σ 的增加,一幅图像被模糊的程度。σ 越大,处理后的图像细节丢失越多。
整本书中可以看到,在很多应用中图像强度的变化情况是非常重要的信息。
强度的变化可以用灰度图像 (对于彩色图像,通常对每个颜色通道分别计算导数)的 和 方向导数 和 进行描述。
图像的梯度向量为。
梯度有两个重要的属性,一是梯度的大小:,它描述了图像强度变化的强弱。
一是梯度的角度:,它描述了图像中在每个点(像素)上强度变化最大的方向。
NumPy
中的 arctan2()
函数返回弧度表示的有符号角度,角度的变化区间为 -π...π。
我们可以用离散近似的方式来计算图像的导数。图像导数大多数可以通过卷积简单地实现:
和
对于 和 ,通常选择 Prewitt 滤波器:
和
或者 Sobel 滤波器:
和
这些导数滤波器可以使用 scipy.ndimage.filters
模块的标准卷积操作来简单地实现。
from PIL import Image
from pylab import *
from scipy.ndimage import filters
import numpy
# 添加中文字体支持
from matplotlib.font_manager import FontProperties
font = FontProperties(fname=r"c:\windows\fonts\SimSun.ttc", size=14)
im = array(Image.open('.\\data\plant.png').convert('L'))
gray()
subplot(1,4,1)
axis('off')
title(u'原图',FontProperties=font)
imshow(im)
#sobel derivative filters
imx = zeros(im.shape)
filters.sobel(im,1,imx)
subplot(1,4,2)
axis('off')
title(u'x方向差分',FontProperties=font)
imshow(imx)
imy = zeros(im.shape)
filters.sobel(im,0,imy)
subplot(1,4,3)
axis('off')
title(u'y方向差分',FontProperties=font)
imshow(imy)
#mag = numpy.sqrt(imx**2+imy**2)
mag = 255-numpy.sqrt(imx**2+imy**2)
subplot(1,4,4)
title(u'梯度幅度',FontProperties=font)
axis('off')
imshow(mag)
show()
上述计算图像导数的方法有一些缺陷:在该方法中,滤波器的尺度需要随着图像分 辨率的变化而变化。为了在图像噪声方面更稳健,以及在任意尺度上计算导数,我们可以使用高斯导数滤波器:
和
其中,Gσx和 Gσy 表示 Gσ 在 x 和 y 方向上的导数,Gσ 为标准差为 σ 的高斯函数。
我们之前用于模糊的 filters.gaussian_filter()
函数可以接受额外的参数,用来计算高斯导数。可以简单地按照下面的方式来处理:
该函数的第三个参数指定对每个方向计算哪种类型的导数,第二个参数为使用的标准差。你可以查看相应文档了解详情。图 1-11 显示了不同尺度下的导数图像和梯度大小。可以和相同尺度模糊的图像做比较。
形态学(或数学形态学)是度量和分析基本形状的图像处理方法的基本框架与集合。形态学通常用于处理二值图像,但是也能够用于灰度图像。二值图像是指图像的每个像素只能取两个值,通常是 0 和 1。二值图像通常是,在计算物体的数目,或者度量其大小时,对一幅图像进行阈值化后的结果。你可以从 http://en.wikipedia.org/wiki/Mathematical_morphology 大体了解形态学及其处理图像的方式。
scipy.ndimage
中的 morphology
模块可以实现形态学操作。你可以使用 scipy.ndimage
中的 measurements
模块来实现二值图像的计数和度量功能。
from PIL import Image
from numpy import *
from scipy.ndimage import measurements, morphology
from pylab import *
""" This is the morphology counting objects example in Section 1.4. """
# 添加中文字体支持
from matplotlib.font_manager import FontProperties
font = FontProperties(fname=r"c:\windows\fonts\SimSun.ttc", size=14)
# load image and threshold to make sure it is binary
figure()
gray()
im = array(Image.open('../data/houses.png').convert('L'))
subplot(221)
imshow(im)
axis('off')
title(u'原图', fontproperties=font)
im = (im < 128)#二值化
labels, nbr_objects = measurements.label(im)
print("Number of objects:", nbr_objects)
subplot(222)
imshow(labels)
axis('off')
title(u'标记后的图', fontproperties=font)
# morphology - opening to separate objects better
im_open = morphology.binary_opening(im, ones((9, 5)), iterations=2)
subplot(223)
imshow(im_open)
axis('off')
title(u'开运算后的图像', fontproperties=font)
labels_open, nbr_objects_open = measurements.label(im_open)
print("Number of objects:", nbr_objects_open)
subplot(224)
imshow(labels_open)
axis('off')
title(u'开运算后进行标记后的图像', fontproperties=font)
show()
上面的脚本首先载入该图像,通过阈值化方式来确保该图像是二值图像。通过和 1 相乘,脚本将布尔数组转换成二进制表示。然后,我们使用 label()函数寻找单个的物体,并且按照它们属于哪个对象将整数标签给像素赋值。下图是 labels 数组的图像。图像的灰度值表示对象的标签。可以看到,在一些对象之间有一些小的连接。进行二进制开(binary open)操作,我们可以将其移除。
binary_opening()
函数的第二个参数指定一个数组结构元素。该数组表示以一个像素为中心时,使用哪些相邻像素。在这种情况下,我们在 y 方向上使用 9 个像素(上面 4 个像素、像素本身、下面 4 个像素),在 x 方向上使用 5 个像素。你可以指定任意数组为结构元素,数组中的非零元素决定使用哪些相邻像素。 参数iterations
决定执行该操作的次数。你可以尝试使用不同的迭代次数 iterations值,看一下对象的数目如何变化。你可以在下图中查看经过开操作后的图像,以及相应的标签图像。正如你想象的一样,binary_closing() 函数实现相反的操作。 我们将该函数和在 morphology 和 measurements模块中的其他函数的用法留作练习。你可以从 scipy.ndimage 模块文档 http://docs.scipy.org/doc/scipy/reference/ndimage.html 中了解关于这些函数的更多知识。
形态学示例。使用二值开操作将对象分开,然后计算物体的数目:(a)为原始二值图像;(b)为对应原始图像的标签图像,其中灰度值表示物体的标签;(c)为使用开操作后的二值图像;(d)为开操作后图像的标签图像
1.4.4 一些有用的SciPy
模块
SciPy
中包含一些用于输入和输出的实用模块。下面介绍其中两个模块:io
和 misc
。
1.读写.mat文件
如果你有一些数据,或者在网上下载到一些有趣的数据集,这些数据以 Matlab 的 .mat 文件格式存储,那么可以使用 scipy.io
模块进行读取。
data = scipy.io.loadmat('test.mat')
上面代码中,data
对象包含一个字典,字典中的键对应于保存在原始 .mat 文件中的变量名。由于这些变量是数组格式的,因此可以很方便地保存到 .mat 文件中。你仅需创建一个字典(其中要包含你想要保存的所有变量),然后使用 savemat()
函数:
import scipy.io as sio
x = [1,2,3,4]
#创建字典
data = {}
#将变量x保存到字典中
data['x'] = x
sio.savemat('.\\data\\test.mat',data)
data = sio.loadmat('.\\data\\test.mat')
print(data)
为上面的脚本保存的是数组 x,所以当读入到 Matlab 中时,变量的名字仍为 x。关于 scipy.io
模块的更多内容,请参见在线文档 http://docs.scipy.org/doc/scipy/reference/io.html。
2.以图像形式保存数组
因为我们需要对图像进行操作,并且需要使用数组对象来做运算,所以将数组直接保存为图像文件 4 非常有用。本书中的很多图像都是这样的创建的。
imsave()
函数可以从 scipy.misc
模块中载入。要将数组 im
保存到文件中,可以使用下面的命令:
from scipy.misc import imsave
imsave('test.jpg',im)
scipy.misc
模块同样包含了著名的 Lena 测试图像:
lena = scipy.misc.lena()
该脚本返回一个 512×512 的灰度图像数组。
所有 Pylab
图均可保存为多种图像格式,方法是点击图像窗口中的“保存”按钮。
我们通过一个非常实用的例子——图像的去噪——来结束本章。图像去噪是在去除图像噪声的同时,尽可能地保留图像细节和结构的处理技术。我们这里使用 ROF(Rudin-Osher-Fatemi)去噪模型。该模型最早出现在文献 [28] 中。图像去噪对于很多应用来说都非常重要;这些应用范围很广,小到让你的假期照片看起来更漂亮,大到提高卫星图像的质量。ROF 模型具有很好的性质:使处理后的图像更平滑,同时保持图像边缘和结构信息。
from pylab import *
from numpy import *
from numpy import random
from scipy.ndimage import filters
#from scipy.misc import imsave
from PCV.tools import rof
""" This is the de-noising example using ROF in Section 1.5. """
# 添加中文字体支持
from matplotlib.font_manager import FontProperties
font = FontProperties(fname=r"c:\windows\fonts\SimSun.ttc", size=14)
# create synthetic image with noise
im = zeros((500,500))
im[100:400,100:400] = 128
im[200:300,200:300] = 255
im = im + 30*random.standard_normal((500,500))
U,T = rof.denoise(im,im)
G = filters.gaussian_filter(im,10)
# save the result
#imsave('synth_original.pdf',im)
#imsave('synth_rof.pdf',U)
#imsave('synth_gaussian.pdf',G)
# plot
figure()
gray()
subplot(1,3,1)
imshow(im)
#axis('equal')
axis('off')
title(u'原噪声图像', fontproperties=font)
subplot(1,3,2)
imshow(G)
#axis('equal')
axis('off')
title(u'高斯模糊后的图像', fontproperties=font)
subplot(1,3,3)
imshow(U)
#axis('equal')
axis('off')
title(u'ROF降噪后的图像', fontproperties=font)
show()
其中第一幅图示原噪声图像,中间一幅图示用标准差为10进行高斯模糊后的结果,最右边一幅图是用ROF降噪后的图像。上面原噪声图像是模拟出来的图像,现在我们在真实的图像上进行测试:
from PIL import Image
from pylab import *
from numpy import *
from numpy import random
from scipy.ndimage import filters
#from scipy.misc import imsave
from PCV.tools import rof
""" This is the de-noising example using ROF in Section 1.5. """
# 添加中文字体支持
from matplotlib.font_manager import FontProperties
font = FontProperties(fname=r"c:\windows\fonts\SimSun.ttc", size=14)
im = array(Image.open('.\\data\\plant.png').convert('L'))
U,T = rof.denoise(im,im)
G = filters.gaussian_filter(im,10)
# save the result
#imsave('synth_original.pdf',im)
#imsave('synth_rof.pdf',U)
#imsave('synth_gaussian.pdf',G)
# plot
figure()
gray()
subplot(1,3,1)
imshow(im)
#axis('equal')
axis('off')
title(u'原噪声图像', fontproperties=font)
subplot(1,3,2)
imshow(G)
#axis('equal')
axis('off')
title(u'高斯模糊后的图像', fontproperties=font)
subplot(1,3,3)
imshow(U)
#axis('equal')
axis('off')
title(u'ROF降噪后的图像', fontproperties=font)
show()
正如你所看到的,ROF 算法去噪后的图像很好地保留了图像的边缘信息。