本章主要介绍图像分类和图像内容分类算法,先介绍一些简单有效的方法和性能最好的分类器,并运用它们解决两类和多类分类问题,最后展示手势识别和目标识别的应用实例。
8.1 K领近分类法(KNN)
在分类方法中,最简单且用得最多的一种方法之一就是KNN(K-Nearest Neighbor ,K 邻近分类法),这种算法把要分类的对象(例如一个特征向量)与训练集中已知类标 记的所有对象进行对比,并由k近邻对指派到哪个类进行投票。
实现最基本的KNN形式非常简单。给定训练样本集和对应的标记列表,下面的代码 可以用来完成这一工作。这些训练样本和标记可以在一个数组里成行摆放或者干脆摆 放列表里,训练样本可能是数字、字符串等任何你喜欢的形状。将定义的类对象添加 到名为knn.py的文件里:
import numpy as np
class KnnClassifier(object):
def __init__(self,labels,samples):
"""使用训练数据初始化分类器"""
self.labels = labels
self.samples = samples
def classify(self,point,k=3):
"""在训练数据上采用k近邻分类,并返回标记"""
#计算所有训练数据点的距离
dist = np.array([L2dist(point,s) for s in self.samples])
#对它们进行排序
ndx = dist.argsort()
#用字典存储k近邻
votes = {}
for i in range(k):
label = self.labels[ndx[i]]
votes.setdefault(label,0)
votes[label] += 1
return max(votes)
def L2dist(p1,p2):
return np.sqrt(sum((p1-p2)**2))
8.1.1 一个简单的二维示例
我们首先建立一些简单的二维示例数据集来说明并可视化分类器的工作原理,下面的 脚本将创建两个不同的二维点集,每个点集有两类,用Pickle模块来保存创建的数据:
import pickle
import knn
import imtools
import numpy as np
import pylab as plt
# 用Pickle 载入二维数据点
with open('points_normal.pkl', 'r') as f:
class_1 = pickle.load(f)
class_2 = pickle.load(f)
labels = pickle.load(f)
model = knn.KnnClassifier(labels,np.vstack((class_1,class_2)))
# 用Pickle 模块载入测试数据
with open('points_normal_test.pkl', 'r') as f:
class_1 = pickle.load(f)
class_2 = pickle.load(f)
labels = pickle.load(f)
# 在测试数据集的第一个数据点上进行测试
print(model.classify(class_1[0]))
# 定义绘图函数
def classify(x,y,model=model):
return np.array([model.classify([xx,yy]) for (xx,yy) in zip(x,y)])
# 绘制分类边界
imtools.plot_2D_boundary([-6,6,-6,6],[class_1,class_2],classify,[1,-1])
plt.show()
这里我们创建了一个简短的辅助函数以获取x和y二维坐标数组和分类器,并返回 一个预测的类标记数组。现在我们把函数作为参数传递给实际的绘图函数。把下面 的函数添加到文件imtools中:
def plot_2D_boundary(plot_range,points,decisionfcn,labels,values=[0]):
"""Plot_range 为(xmin,xmax,ymin,ymax), points 是类数据点列表,
decisionfcn 是评估函数,labels 是函数 decidionfcn 关于每个类返回的标记列表"""
clist = ['b','r','g','k','m','y'] # 不同的类用不同的颜色标识
#在一个网格上进行评估,并画出决策函数的边界
x = arange(plot_range[0],plot_range[1],.1)
y = arange(plot_range[2],plot_range[3],.1)
xx,yy = meshgrid(x,y)
xxx,yyy = xx.flatten(),yy.flatten() # 网格中的 x,y 坐标点列表
zz = array(decisionfcn(xxx,yyy))
zz = zz.reshape(xx.shape)
# 以 values 画出边界
contour(xx,yy,zz,values)
# 对于每类,用*画出分类正确的点,用o画出分类不正确的点
for i in range(len(points)):
d = decisionfcn(points[i][:, 0], points[i][:, 1])
correct_ndx = labels[i] == d
incorrect_ndx = labels[i] != d
plot(points[i][correct_ndx, 0], points[i][correct_ndx, 1], '*', color=clist[i])
plot(points[i][incorrect_ndx, 0], points[i][incorrect_ndx, 1], 'o', color=clist[i])
axis('equal')
运行结果如下
8.1.2 用稠密SIFT作为图像特征
要对图像进行分类,我们需要一个特征向量来表示 一幅图像。
import sift
import numpy as np
from PIL import Image
import os
def process_image_dsift(imagename,resultname,size=20,steps=10,force_orientation=False,resize=None):
""" 用密集采样的SIFT描述子处理一幅图像,并将结果保存在一个文件中。可选的输入:
特征的大小size,位置之间的步长steps,是否强迫计算描述子的方位force_orientation
(False 表示所有的方位都是朝上的),用于调整图像大小的元组"""
im = Image.open(imagename).convert('L')
if resize != None:
im = im.resize(resize)
m, n = im.size
if imagename[-3:] != 'pgm':
# 创建一个 pgm 文件
im.save('tmp.pgm')
imagename = 'tmp.pgm'
# 创建帧,并保存到临时文件
scale = size / 3.0
x, y = np.meshgrid(range(steps, m, steps), range(steps, n, steps))
xx, yy = x.flatten(), y.flatten()
frame = np.array([xx, yy, scale * np.ones(xx.shape[0]), np.zeros(xx.shape[0])])
np.savetxt('tmp.frame', frame.T, fmt='%03.3f')
if force_orientation:
cmmd = str("sift " + imagename + " --output=" + resultname +
" --read-frames=tmp.frame --orientations")
else:
cmmd = str("sift " + imagename + " --output=" + resultname +
" --read-frames=tmp.frame")
os.system(cmmd)
print('processed', imagename, 'to', resultname)
为了使用命令行处理,我们用savetxt()函数 将帧数组存储在一个文本文件中,该函数的最后一个参数可以在提取描述子之前对 图像的大小进行调整。最后,如果force_orientation为真,则提取出来的描述子会基于 局部主梯度方向进行归一化;否则,则所有的描述子的方向只是简单地朝上。利用类似下面的代码可以计算稠密SIFT描述子,并可视化它们的位置:
import dsift,sift2
import numpy as np
import pylab as plt
from PIL import Image
dsift.process_image_dsift('eg.jpg','eg.sift',90,40,True)
l,d = sift2.read_features_from_file('eg.sift')
im = np.array(Image.open('eg.jpg'))
sift2.plot_features(im,l,True)
plt.show()
运行结果如图所示
8.1.3 图像分类:手势识别
我们会用稠密SIFT描述子来表示这些手势图像,并建立一个简单 的手势识别系统。将下载后的所有图像放在一个名为uniform的文件夹 里,每一类均分两组,并分别放入名为train和test的两个文件夹中。
import dsift
# 将图像尺寸调为(50,50),然后进行处理
for filename in imlist:
featfile = filename[:-3]+'dsift'
dsift.process_image_dsift(filename,featfile,10,5,resize=(50,50))
上面代码会对每一幅图像创建一个特征文件,文件名后缀为.dsift。注意,这里将图 像分辨率调成了常见的固定大小。这是非常重要的,否则这些图像会有不同数量的描述子,从而每幅图像的特征向量长度也不一样,这将导致在后面比较它们时出错。
定义一个辅助函数,用于从文件中读取稠密SIFT描述子,如下:
import os, sift
def read_gesture_features_labels(path):
# 对所有以.dsift 为后缀的文件创建一个列表
featlist = [os.path.join(path,f) for f in os.listdir(path) if f.endswith('.dsift')]
# 读取特征
features = []
for featfile in featlist:
l,d = sift.read_features_from_file(featfile)
features.append(d.flatten())
features = array(features)
# 创建标记
labels = [featfile.split('/')[-1][0] for featfile in featlist]
return features,array(labels)
然后,我们可以用下面的脚本读取训练集、测试集的特征和标记信息:
features,labels = read_gesture_features_labels('train/')
test_features,test_labels = read_gesture_features_labels('test/')
classnames = unique(labels)
现在我们可以在该数据上使用前面的K近邻代码:
# 测试KNN
k = 1
knn_classifier = knn.KnnClassifier(labels,features)
res = array([knn_classifier.classify(test_features[i],k) for i in
range(len(test_labels))])
# 准确率
acc = sum(1.0*(res==test_labels)) / len(test_labels)
print ('Accuracy:', acc)
首先,用训练数据及其标记作为输入,创建分类器对象;然后,我们在整个测试集 上遍历并用classify()方法对每幅图像进行分类。下面的函数会打印出标记及相应的混淆矩阵:
def print_confusion(res,labels,classnames):
n = len(classnames)
#混淆矩阵
class_ind = dict([(classnames[i],i) for i in range(n)])
confuse = zeros((n,n))
for i in range(len(test_labels)):
confuse[class_ind[res[i]],class_ind[test_labels[i]]] += 1
print 'Confusion matrix for'
print classnames
print confuse
print_confusion(res,test_labels,classnames)
打印输出如下
8.2 贝叶斯分类器
贝叶斯分类器是一种基于贝叶斯条件概率定理的概率分类器,它假设特征是彼此独立不相的 (这就是它“朴素”的部分)。贝叶斯分类器可以非常有效地被训练出来,原因在于每一个特征模型都是独立选取的。该分类器是通过将各个特征的条件概率相乘得到一个类的总概率,然后选取概率最 高的那个类构造出来的。
首先让我们看一个使用高斯概率分布模型的贝叶斯分类器基本实现,也就是用从 训练数据集计算得到的特征均值和方差来对每个特征单独建模。
class BayesClassifier(object):
def __init__(self):
""" 使用训练数据初始化分类器 """
self.labels = [] # 类标签
self.mean = [] # 类均值
self.var = [] # 类方差
self.n = 0 # 类别数
def train(self,data,labels=None):
""" 在数据 data(n×dim 的数组列表)上训练,标记labels是可选的,默认为0…n-1 """
if labels==None:
labels = range(len(data))
self.labels = labels
self.n = len(labels)
for c in data:
self.mean.append(mean(c,axis=0))
self.var.append(var(c,axis=0))
def classify(self,points):
""" 通过计算得出的每一类的概率对数据点进行分类,并返回最可能的标记"""
# 计算每一类的概率
est_prob = array([gauss(m,v,points) for m,v in zip(self.mean,self.var)])
# 获取具有最高概率的索引,该索引会给出类标签
ndx = est_prob.argmax(axis=0)
est_labels = array([self.labels[n] for n in ndx])
return est_labels, est_prob
该模型每一类都有两个变量,即类均值和协方差。train()方法获取特征数组列表 (每个类对应一个特征数组),并计算每个特征数组的均值和协方差。classify()方 法计算数据点构成的数组的类概率,并选概率最高的那个类,最终返回预测的类标 记及概率值,同时需要一个高斯辅助函数:
def gauss(m,v,x):
""" 用独立均值m和方差v评估d维高斯分布 """
if len(x.shape)==1:
n,d = 1,x.shape[0]
else:
n,d = x.shape
# 协方差矩阵,减去均值
S = diag(1/v)
x = x-m
# 概率的乘积
y = exp(-0.5*diag(dot(x,dot(S,x.T))))
# 归一化并返回
return y * (2*pi)**(-d/2.0) / ( sqrt(prod(v)) + 1e-6)
将该贝叶斯分类器用于上一节的二维数据,下面的脚本将载入上一节中的二维数据, 并训练出一个分类器:
import pickle
import bayes
import imtools
# 用Pickle 模块载入二维样本点
with open('points_normal.pkl', 'r') as f:
class_1 = pickle.load(f)
class_2 = pickle.load(f)
labels = pickle.load(f)
# 训练贝叶斯分类器
bc = bayes.BayesClassifier()
bc.train([class_1,class_2],[1,-1])
# 用Pickle 模块载入测试数据
with open('points_normal_test.pkl', 'r') as f:
class_1 = pickle.load(f)
class_2 = pickle.load(f)
labels = pickle.load(f)
#在某些数据点上进行测试
print bc.classify(class_1[:10])[0]
#绘制这些二维数据点及决策边界
def classify(x,y,bc=bc):
points = vstack((x,y))
return bc.classify(points.T)[0]
imtools.plot_2D_boundary([-6,6,-6,6],[class_1,class_2],classify,[1,-1])
show()
用PCA降维
import pca
V,S,m = pca.pca(features)
# 保持最重要的成分
V = V[:50]
features = array([dot(V,f-m) for f in features])
test_features = array([dot(V,f-m) for f in test_features])
这里的features和 test_features 与 K 邻近中的例子中加载的数组是一样的。在本 例中,我们在训练数据上用PCA降维,并保持在这50维具有最大的方差。这可以 通过均值m(是在训练数据上计算得到的)并与基向量V相乘做到。对测试数据进 行同样的转换。
训练并测试贝叶斯分类器如下:
# 测试贝叶斯分类器
bc = bayes.BayesClassifier()
blist = [features[where(labels==c)[0]] for c in classnames]
bc.train(blist,classnames)
res = bc.classify(test_features)[0]
由于BayesClassifier 需要获取数组列表(每一类对应一个数组),在把数据传递给 train() 函数之前,我们需要对数据进行转换。因为我们目前还不需要概率,所以只 需返回预测的类标记。输出如下:
虽然分类效果不如K邻近分类器,但是贝叶斯分类器不需要保存任何训练数据, 而且只需保存每个类的模型参数。这一结果会随着PCA维度选取的不同而发生巨大 的变化。
8.3 支持向量机
最简单的SVM通过在高维空间中寻找一 个最优线性分类面,尽可能地将两类数据分开。对于一特征向量x的决策函数为:
其中w是常规的超平面,b是偏移量常数。通过在训练集上求解那些带有标记 的特征向量
的最优化问题,使超平面在两类间具有最大分开间隔,从 而找到上面决策函数中的参数w和b。该决策函数的常规解是训练集上某些特征向 量的线性组合:
所以决策函数可以写为:
这里的i是从训练集中选出的部分样本,这里选择的样本称为支持向量,因为它们 可以帮助定义分类的边界。
8.3.1 使用LibSVM
LibSVM是最好的、使用最广泛的SVM实现工具包。LibSVM为Python提供了 一个良好的接口(也为其他编程语言提供了接口)。我们看看LibSVM在二维样本数据点上是怎样工作的。下面的脚本会载入在前面 kNN范例分类中用到的数据点,并用径向基函数训练一个SVM分类器:
import pickle
from svmutil import *
import imtools
# 用Pickle 载入二维样本点
with open('points_normal.pkl', 'r') as f:
class_1 = pickle.load(f)
class_2 = pickle.load(f)
labels = pickle.load(f)
# 转换成列表,便于使用libSVM
class_1 = map(list,class_1)
class_2 = map(list,class_2)
labels = list(labels)
samples = class_1+class_2 # 连接两个列表
# 创建SVM
prob = svm_problem(labels,samples)
param = svm_parameter('-t 2')
# 在数据上训练SVM
m = svm_train(prob,param)
#在训练数据上分类效果如何?
res = svm_predict(labels,samples,m)
现在,载入其他数据集,并对该分类器进行测试:
#用Pickle模块载入测试数据
with open('points_normal_test.pkl', 'r') as f:
class_1 = pickle.load(f)
class_2 = pickle.load(f)
labels = pickle.load(f)
#转换成列表,便于使用LibSVM
class_1 = map(list,class_1)
class_2 = map(list,class_2)
#定义绘图函数
def predict(x,y,model=m):
return array(svm_predict([0]*len(x),zip(x,y),model)[0])
# 绘制分类边界
imtools.plot_2D_boundary([-6,6,-6,6],[array(class_1),array(class_2)],predict,[-1,1])
show()
运行效果如下