Python计算机视觉——第一章 基本的图像操作和处理
目录
本章讲解操作和处理图像的基础知识,将通过大量示例介绍处理图像所需的Python工具包,并介绍用于读取图像、图像转换和缩放、计算导数、画图和保存结果等的基本工具。
1.1 PIL:Python图像处理类库
PIL(Python Imaging Library Python,图像处理类库)提供了通用的图像处理功能,以及大量有用的基本图像操作,比如图像缩放、裁剪、旋转、颜色转换等
PIL 中最重要的模块为Image。要读取一幅图像,可以使用:
from PIL import Image
pil_im = Image.open('empire.jpg')
上述代码的返回值pil_im是一个PIL图像对象。
要读取一幅图像,并将其转换成灰度图像,只需要加上convert('L')
pil_im=Image.open('empire.jpg').convert('L')
1.1.1 转换图像格式
通过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() 方法用于保存图像到具有指定文件名的文件
PIL 是个足够智能的类库,可以根据文件扩展名来判定图像的格式。PIL 函数会进行简单的检查,如果文件不是JPEG 格式,会自动将其转换成JPEG 格式;如果转换失败,它会在控制台输出一条报告失败的消息。
1.1.2 创建缩略图
使用PIL 可以很方便地创建图像的缩略图。thumbnail() 方法接受一个元组参数(该参数指定生成缩略图的大小),然后将图像转换成符合元组参数指定大小的缩略图。
pil_im.thumbnail((128,128))
1.1.3 复制和粘贴图像区域
使用crop() 方法可以从一幅图像中裁剪指定区域:
box = (100,100,400,400)
region = pil_im.crop(box)
该区域使用四元组来指定。四元组的坐标依次是(左,上,右,下)。PIL 中指定坐标系的左上角坐标为(0,0)
1.1.4 调整尺寸和旋转
要调整一幅图像的尺寸,我们可以调用resize() 方法。该方法的参数是一个元组,用来指定新图像的大小:
out = pil_im.resize((128,128))
要旋转一幅图像,可以使用逆时针方式表示旋转角度,然后调用rotate() 方法:
out = pil_im.rotate(45)
1.2 Matplotlib
Matplotlib 可以绘制出高质量的图表,就像本书中的许多插图一样。
1.2.1 绘制图像、点和线
下面是用几个点和一条线绘制图像的例子:
from PIL import Image
from pylab import *
# 读取图像到数组中
im = array(Image.open(r'C:\Users\23675\Pictures\ren.jpg'))
# 绘制图像
imshow(im)
# 一些点
x = [100,100,400,400]
y = [200,500,200,500]
# 使用红色星状标记绘制点
plot(x,y,'r*')
# 绘制连接前两个点的线
plot(x[:2],y[:2])
# 添加标题,显示绘制的图像
title('Plotting: "ren.jpg"')
show()

上面的代码首先绘制出原始图像,然后在x 和y 列表中给定点的x 坐标和y 坐标上绘制出红色星状标记点,最后在两个列表表示的前两个点之间绘制一条线段(默认为蓝色)。
show() 命令首先打开图形用户界面(GUI),然后新建一个图像窗口。该图形用户界面会循环阻断脚本,然后暂停,直到最后一个图像窗口关闭。在每个脚本里,你只能调用一次show() 命令,而且通常是在脚本的结尾调用。
注意,在PyLab 库中,我们约定图像的左上角为坐标原点。
加上下列命令可以使坐标轴不显示:
axis('off')

控制图像的颜色和样式:
1.2.2 图像轮廓和直方图
图像的直方图用来表征该图像像素值的分布情况。
因为绘制轮廓需要对每个坐标[x, y] 的像素值施加同一个阈值,所以首先需要将图像灰度化:
from PIL import Image
from pylab import *
# 读取图像到数组中
im = array(Image.open(r'C:\Users\23675\Pictures\ren.jpg').convert('L'))
# 新建一个图像
figure()
# 不使用颜色信息
gray()
# 在原点的左上角显示轮廓图像
contour(im, origin='image')
axis('equal')
axis('off')
figure()
hist(im.flatten(),128)
show()

1.2.3 交互式标注
PyLab 库中的ginput() 函数就可以实现交互式标注。
from PIL import Image
from pylab import *
im = array(Image.open(r'C:\Users\23675\Pictures\ren.jpg'))
imshow(im)
print ('Please click 3 points')
x = ginput(3)
print ('you clicked:',x)
show()
上面的脚本首先绘制一幅图像,然后等待用户在绘图窗口的图像区域点击三次。程序将这些点击的坐标[x, y] 自动保存在x列表里。
1.3 NumPy
是非常有名的Python 科学计算工具包,其中包含了大量有用的思想,比如数组对象(用来表示向量、矩阵、图像等)以及线性代数函数。
1.3.1 图像数组表示
NumPy 中的数组对象是多维的,可以用来表示向量、矩阵和图像。
一个数组对象很像一个列表(或者是列表的列表),但是数组中所有的元素必须具有相同的数据类型。
from PIL import Image
from numpy import *
im = array(Image.open(r'C:\Users\23675\Pictures\ren.jpg'))
print (im.shape, im.dtype)
im = array(Image.open(r'C:\Users\23675\Pictures\ren.jpg').convert('L'),'f')
print (im.shape, im.dtype)
每行的第一个元组表示图像数组的大小(行、列、颜色通道),紧接着的字符串表示数组元素的数据类型。
图像通常被编码成无符号八位整数(uint8)
位于坐标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]) # 倒数第二行
1.3.2 灰度变换
将图像读入NumPy 数组对象后,我们可以对它们执行任意数学操作。一个简单的例子就是图像的灰度变换。
from PIL import Image
from numpy import *
from pylab import *
im=array(Image.open(r'C:\Users\23675\Pictures\ren.jpg').convert('L'))
print(int(im.min()),int(im.max()))
im2=255-im #对图像进行反向处理
print(int(im2.min()),int(im2.max())) #查看最大/最小元素
im3=(100.0/255)*im+100 #将图像像素值变换到100...200区间
print(int(im3.min()),int(im3.max()))
im4=255.0*(im/255.0)**2 #对像素值求平方后得到的图像
print(int(im4.min()),int(im4.max()))
figure()
gray()
subplot(141)
imshow(im)
axis('off')
title(r'$f(x)=x$')
subplot(142)
imshow(im2)
axis('off')
title(r'$f(x)=255-x$')
subplot(143)
imshow(im3)
axis('off')
title(r'$f(x)=\frac{100}{255}x+100$')
subplot(144)
imshow(im4)
axis('off')
title(r'$f(x)=255(\frac{x}{255})^2$')
show()

第一个例子将灰度图像进行反相处理;第二个例子将图像的像素值变换;第三个例子对图像使用二次函数变换。图像中像素的最小值和最大值

1.3.3 图像缩放
NumPy 的数组对象是我们处理图像和数据的主要工具。想要对图像进行缩放处理没有现成简单的方法。我们可以使用之前PIL 对图像对象转换的操作,写一个简单的用于图像缩放的函数。
def imresize(im,sz):
""" 使用PIL 对象重新定义图像数组的大小"""
pil_im = Image.fromarray(uint8(im))
return array(pil_im.resize(sz))
1.3.4 直方图均衡化
直方图均衡化是指将一幅图像的灰度直方图变平,使变换后的图像中每个灰度值的分布概率都相同。
直方图均衡化通常是对图像灰度值进行归一化的一个非常好的方法,并且可以增强图像的对比度。
直方图均衡化的变换函数是图像中像素值的累积分布函数(cumulative distribution function,简写为cdf,将像素值的范围映射到目标范围的归一化操作)。
下面的函数是直方图均衡化的具体实现:
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
该函数有两个输入参数,一个是灰度图像,一个是直方图中使用小区间的数目。
from PIL import Image
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)
im = array(Image.open(r'C:\Users\23675\Pictures\ren.jpg').convert('L'))
im2, cdf = imtools.histeq(im)
figure()
subplot(2, 2, 1)
axis('off')
gray()
title(u'原始图像', fontproperties=font)
imshow(im)
subplot(2, 2, 2)
axis('off')
title(u'直方图均衡化后的图像', fontproperties=font)
imshow(im2)
subplot(2, 2, 3)
axis('off')
title(u'原始直方图', fontproperties=font)
#hist(im.flatten(), 128, cumulative=True, normed=True)
hist(im.flatten(), 128, density=True)
subplot(2, 2, 4)
axis('off')
title(u'均衡化后的直方图', fontproperties=font)
#hist(im2.flatten(), 128, cumulative=True, normed=True)
hist(im2.flatten(), 128, density=True)
show()

1.3.5 图像平均
图像平均操作是减少图像噪声的一种简单方式。可以简单地从图像列表中计算出一幅平均图像。假设所有的图像具有相同的大小,我们可以将这些图像简单地相加,然后除以图像的数目,来计算平均图像。
用于计算平均图像:
def compute_average(imlist):
""" 计算图像列表的平均图像"""
# 打开第一幅图像,将其存储在浮点型数组中
averageim = array(Image.open(imlist[0]), 'f')
for imname in imlist[1:]:
try:
averageim += array(Image.open(imname))
except:
print imname + '...skipped'
averageim /= len(imlist)
# 返回uint8 类型的平均图像
return array(averageim, 'uint8')
1.3.6 图像的主成分分析(PCA)
PCA(Principal Component Analysis,主成分分析)是一个非常有用的降维技巧。它可以在使用尽可能少维数的前提下,尽量多地保持训练数据的信息,在此意义上是一个最佳技巧。
PCA 产生的投影矩阵可以被视为将原始坐标变换到现有的坐标系,坐标系中的各个坐标按照重要性递减排列。
为了对图像数据进行PCA 变换,图像需要转换成一维向量表示。我们可以使用NumPy 类库中的flatten() 方法进行变换。
PCA 操作的代码:
from PIL import Image
from numpy import *
def pca(X):
""" 主成分分析:
输入:矩阵X ,其中该矩阵中存储训练数据,每一行为一条训练数据
返回:投影矩阵(按照维度的重要性排序)、方差和均值"""
# 获取维数
num_data,dim = X.shape
# 数据中心化
mean_X = X.mean(axis=0)
X = X - mean_X
if dim>num_data:
# 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)
V = V[:num_data] # 仅仅返回前nun_data 维的数据才合理
# 返回投影矩阵、方差和均值
return V,S,mean_X
该函数首先通过减去每一维的均值将数据中心化,然后计算协方差矩阵对应最大特征值的特征向量,此时可以使用简明的技巧或者SVD 分解。
range() 函数,该函数的输入参数为一个整数n,函数返回整数0...(n-1) 的一个列表。
如果数据个数小于向量的维数,我们不用SVD分解,而是计算维数更小的协方差矩阵\(XX^T\)的特征向量。通过仅计算对应前k(k 是降维后的维数)最大特征值的特征向量,可以使上面的PCA 操作更快。
1.3.7 使用pickle模块
pickle 模块可以接受几乎所有的Python 对象,并且将其转换成字符串表示,该过程叫做封装(pickling)。从字符串表示中重构该对象,称为拆封(unpickling)。这些字符串表示可以方便地存储和传输。
1.4 SciPy
SciPy是建立在NumPy 基础上, 用于数值运算的开源工具包。SciPy 提供很多高效的操作,可以实现数值积分、优化、统计、信号处理,以及图像处理功能。
1.4.1 图像模糊
图像的高斯模糊是非常经典的图像卷积例子。本质上,图像模糊就是将(灰度)图像\(I\)和一个高斯核进行卷积操作
\(I_σ=I*G_σ\)
其中* 表示卷积操作;\(G_σ\)是标准差为σ 的二维高斯核, 定义为:
\(G_{\sigma}=\frac{1}{2\pi\sigma}e^{-(x^{2}+y^{2})/2\sigma^{2}}\)
from PIL import Image
from numpy import *
from pylab 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(r'c:users\23675\pictures\s.jpg').convert('L'))
figure()
gray()
axis('off')
subplot(141)
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()

由实验可以看出,随着δ值增加,图像逐渐变模糊,图像细节丢失越多。
1.4.2 图像导数
整本书中可以看到,在很多应用中图像强度的变化情况是非常重要的信息。强度的变化可以用灰度图像\(I\)(对于彩色图像,通常对每个颜色通道分别计算导数)的x和y方向导数\(I_x\)和\(I_y\)进行描述。
图像的梯度向量为\(\nabla I=|[I_{x},I_{y}]|^{r}\)
梯度的大小:描述了图像强度变化的强弱,\(\begin{vmatrix}\nabla I\end{vmatrix}=\sqrt{I_x^2+I_y^2}\)
梯度的角度:描述了图像中在每个点(像素)上强度变化最大的方向,\(\alpha=arctan2(I_y,I_x)\)
可以用离散近似的方式来计算图像的导数。图像导数大多数可以通过卷积简单地实现:
\(I_x=I*D_x\)和\(I_y=I*D_y\)
对于\(D_x\)和\(D_y\),通常选择Prewitt 滤波器:
\(D_x=\begin{bmatrix}-1&0&1\\[2ex]-1&0&1\\[2ex]-1&0&1\end{bmatrix}\)和\(D_{y}=\begin{bmatrix}-1&-1&-1\\0&0&0\\1&1&1\end{bmatrix}\)
或者Sobel 滤波器:
\(D_x=\begin{bmatrix}-1&0&1\\[0.3em]-2&0&2\\[0.3em]-1&0&1\end{bmatrix}\)和\(D_y=\begin{bmatrix}-1&-2&-1\\0&0&0\\1&2&1\end{bmatrix}\)
这些导数滤波器可以使用scipy.ndimage.filters 模块的标准卷积操作来简单地实现
使用Sobel 滤波器来计算x 和y 的方向导数,以及梯度大小:
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(r'c:users\23675\pictures\s.jpg').convert('L'))
gray()
subplot(141)
axis('off')
title(u'(a)原图',fontproperties=font)
imshow(im)
# sobel derivative filters
imx=zeros(im.shape)
filters.sobel(im,1,imx)
subplot(142)
axis('off')
title(u'(b)x方向差分',fontproperties=font)
imshow(imx)
imy=zeros(im.shape)
filters.sobel(im,0,imy)
subplot(143)
axis('off')
title(u'(c)y方向差分',fontproperties=font)
imshow(imy)
mag=255-numpy.sqrt(imx**2+imy**2)
subplot(144)
title(u'(d)梯度幅值',fontproperties=font)
axis('off')
imshow(mag)
show()

在导数图像中,正导数显示为亮的像素,负倒数显示为暗的像素,灰色区域表示导数的值接近于零。
1.4.3 形态学:对象计数
形态学(或数学形态学)是度量和分析基本形状的图像处理方法的基本框架与集合。形态学通常用于处理二值图像,但是也能够用于灰度图像。二值图像是指图像的每个像素只能取两个值,通常是0 和1。二值图像通常是,在计算物体的数目,或者度量其大小时,对一幅图像进行阈值化后的结果。
scipy.ndimage 中的morphology 模块可以实现形态学操作。你可以使用scipy.ndimage 中的measurements 模块来实现二值图像的计数和度量功能。
计算该图像中的对象个数可以通过下面的脚本实现:首先载入该图像,通过阈值化方式来确保该图像是二值图像。通过和1相乘,脚本将布尔数组转换成二进制表示。然后,我们使用label() 函数寻找单个的物体,并且按照它们属于哪个对象将整数标签给像素赋值。
from scipy.ndimage import measurements,morphology
# 载入图像,然后使用阈值化操作,以保证处理的图像为二值图像
im = array(Image.open('houses.png').convert('L'))
im = 1*(im<128)
labels, nbr_objects = measurements.label(im)
print "Number of objects:", nbr_objects
在一些对象之间有一些小的连接。进行二进制开(binary open)操作,我们可以将其移除:binary_opening()函数的第二个参数指定一个数组元素。该数组表示以一个像素为中心时,使用哪些相邻像素。参数iterations决定执行该操作的次数。
# 形态学开操作更好地分离各个对象
im_open = morphology.binary_opening(im,ones((9,5)),iterations=2)
labels_open, nbr_objects_open = measurements.label(im_open)
print "Number of objects:", nbr_objects_open
程序代码:
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(r'c:users\23675\pictures\s.jpg').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()

![]()
更多推荐






所有评论(0)