图像内容分类
本章介绍图像分类和图像内容分类算法。首先,我们介绍一些简单而有效的方法和目前一些性能最好的分类器,并应用他们解决两类和多分类问题,最后展示两个用于手势识别和目标识别的应用实例。
1 K邻近分类器(KNN)
在分类算法中,最简单且用的最多的一种方法之一就是KNN(K-Nearset Neighbor,K邻近分类法),这种算法把要分类的对象(例如一个特征向量)与训练集中已知类标记的所有对象进行对比,并由k近邻对指派到哪个类进行投票。这种方法通常分类效果比较好,但是也有很多弊端:与K-means聚类算法一样,需要预先设定k值,k值的选择会影响分类的性能;此外,这种方法要求将整个训练集存储起来,如果训练集非常大,搜索起来就非常慢。对于大训练集,采取某些装箱形式通常会减少对比的次数从积极的一面来看,这种方法在采用何种距离度量方面是没有限制的;实际上,对于你所能想到的东西他都可以奏效,但这并不意味这对任何东西它的分类性能都很好。另外,这种算法的可并行性也很一般。
实现最基本的KNN形式非常简单。给定训练样本集和对应的标记列表,下面的代码可以用来完成这一工作。这些训练样本和标记可以在一个数组里成行摆放或者干脆摆放列表里,训练样本可能是数字、字符串等任何你喜欢的形状。将定义的类对象添加到名为knn.py的文件里:
from numpy import *
class KnnClassifier(object):
def __init__(self, labels, samples):
"""使用训练数据初始化分类器"""
self.labels = labels
self.samples = samples
def classify(self, point, k=3):
"""在训练数据上采用k近邻分类,并返回标记"""
# 计算所有训练数据点的距离
dist = array([sqrt(np.sum((s - point)**2)) 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 sqrt( sum((p1-p2)**2) )
定义一个类并用训练数据初始化非常简单;每次相对某些东西进行分类时,用KNN方法,我们就没有必要存储并将训练数据作为参数来传递。用一个字典来存储邻近标记,我们便可以用文本字符串或数字来表示标记。在这个例子中,我们用欧式距离 (L2) 进行度量,也可以使用其他度量方式,只需要将其作为函数添加到上面代码的最后。
1.1 一个简单的二维示例
我们首先建立一些简单的二维示例数据集来说明并可视化分类器的工作原理,下面的脚本将创建两个不同的二维点集,每个点集有两类,用Pickle模块来创建保存创建的数据:
# 创建二维样本数据
n = 200
# two normal distributions
# 两个正态分布数据集
class_1 = 0.2 * randn(n, 2)
class_2 = 1.6 * randn(n, 2) + array([5, 1])
labels = hstack((ones(n), -ones(n)))
# save with Pickle
# 用 Pickle 模块保存
# with open('points_normal.pkl', 'w') as f:
with open('points_normal.pkl', 'wb') as f:
pickle.dump(class_1, f)
pickle.dump(class_2, f)
pickle.dump(labels, f)
# normal distribution and ring around it
# 正态分布,并使数据成环绕状分布
class_1 = 0.6 * randn(n, 2)
r = 0.8 * randn(n, 1) + 5
angle = 2 * pi * randn(n, 1)
class_2 = hstack((r * cos(angle), r * sin(angle)))
labels = hstack((ones(n), -ones(n)))
# save with Pickle
# 用 Pickle 保存
# with open('points_ring.pkl', 'w') as f:
with open('points_ring.pkl', 'wb') as f:
pickle.dump(class_1, f)
pickle.dump(class_2, f)
pickle.dump(labels, f)
用不同的保存文件名运行该代码两次,例如第一次用points_normal.pkl和points_ring.pkl,则第二次改为points_normal_test.pkl和points_ring_test.pkl进行保存。得到4个二维数据集文件,每个分布都有两个文件,一个用来训练,一个用来做测试。
让我们看看怎么用KNN分类器来完成,用下面的代码来创建一个脚本:
# 用 Pickle 载入二维数据点
with open('points_ring.pkl', 'rb+') as f:
class_1 = pickle.load(f)
class_2 = pickle.load(f)
labels = pickle.load(f)
model = KnnClassifier(labels, vstack((class_1, class_2)))
# 用Pickle模块载入测试数据
with open('points_ring_test.pkl', 'rb+') 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 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])
show()
n=200,k=30:
每个示例中,不同颜色代表类标记,正确分类的点用星号表示,分类错误的点用圆点表示,曲线是分类器的决策边界。正如所看到的,kNN 决策边界适用于没有任何明确模型的类分布。
1.2 用稠密SIFT作为图像特征
我们来看如何对图像进行分类。要对图像进行分类,我们需要一个特征向量来表示一幅图像。在聚类一章我们用平均RGB像素值和PCA系数作为图像的特征向量;这里我们会介绍另外一种表示形式,即稠密SIFT特征向量。
在整幅图像上用一个规则的网格应用SIFT描述子可以得到稠密SIFT的表示形式,通过添加一些额外的参数来得到稠密SIFT特征。创建一个名为dsift.py的文件:
from PIL import Image
import os
from numpy import *
import sift
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 = meshgrid(range(steps,m,steps),range(steps,n,steps))
xx,yy = x.flatten(),y.flatten()
frame = array([xx,yy,scale*ones(xx.shape[0]),zeros(xx.shape[0])])
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
利用类似下面的代码可以计算稠密SIFT描述子,并可视化它们的位置:
# -*- coding: utf-8 -*-
from PCV.localdescriptors import sift, dsift
from pylab import *
from PIL import Image
dsift.process_image_dsift('empire.jpg','empire.sift',90,40,True)
l,d = sift.read_features_from_file('empire.sift')
im = array(Image.open('empire.jpg'))
sift.plot_features(im,l,True)
title('Dense SIFT')
show()
使用用于定位描述子的局部梯度方向(force_orientation设置为真),该代码可以在整个图像中计算出稠密SIFT特征。
1.3 图像分类:手势识别
用稠密 SIFT 描述子来表示这些手势图像,将图像放在一个名为 uniform 的文件夹 里,每一类均分两组,并分别放入名为 train 和 test 的两个文件夹中。
from PCV.localdescriptors import sift, dsift
from pylab import *
from PIL import Image
import os
from PIL import Image
# def get_imlist(path):
# """ Returns a list of filenames for
# all jpg images in a directory. """
#
# return [os.path.join(path, f) for f in os.listdir(path) if f.endswith('.ppm')]
#
# imlist = get_imlist('D:\\123\图像处理\Image Processing\Image Processing\Chpater8\\train')
# # print(imlist)
# # 将图像尺寸调为(50,50),然后进行处理
# for filename in imlist:
# featfile = filename[:-3]+'dsift'
# dsift.process_image_dsift(filename,featfile,10,5,resize=(50,50))
imlist = ['gesture/C-uniform02.ppm', 'gesture/B-uniform01.ppm',
'gesture/A-uniform01.ppm', 'gesture/Five-uniform01.ppm',
'gesture/Point-uniform01.ppm', 'gesture/V-uniform01.ppm']
figure()
for i, im in enumerate(imlist):
print(im)
dsift.process_image_dsift(im, im[:-3] + 'dsift', 10, 5, True, resize=(50,50))
l, d = sift.read_features_from_file(im[:-3] + 'dsift')
dirpath, filename = os.path.split(im)
im = array(Image.open(im))
# 显示手势含义title
titlename = filename[:-14]
subplot(2, 3, i + 1)
sift.plot_features(im, l, True)
title(titlename)
show()
定义一个辅助函数,用于从文件中读取稠密 SIFT 描述子,如下:
from PCV.localdescriptors import sift, dsift
from pylab import *
from PIL import Image
import os
from PIL import Image
import PCV.classifiers.knn as knn
def read_gesture_features_labels(path):
# create list of all files ending in .dsift
featlist = [os.path.join(path,f) for f in os.listdir(path) if f.endswith('.dsift')]
# read the features
features = []
for featfile in featlist:
l,d = sift.read_features_from_file(featfile)
features.append(d.flatten())
features = array(features)
# create labels
labels = [featfile.split('/')[-1][0] for featfile in featlist]
return features,array(labels)
def print_confusion(res,test_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)
def get_imlist(path):
""" Returns a list of filenames for
all jpg images in a directory. """
return [os.path.join(path, f) for f in os.listdir(path) if f.endswith('.ppm')]
# imlist = get_imlist('train/')
# # print(imlist)
# # 将图像尺寸调为(50,50),然后进行处理
# for filename in imlist:
# featfile = filename[:-3]+'dsift'
# dsift.process_image_dsift(filename,featfile,10,5,resize=(50,50))
# imlist = ['gesture/C-uniform02.ppm', 'gesture/B-uniform01.ppm',
# 'gesture/A-uniform01.ppm', 'gesture/Five-uniform01.ppm',
# 'gesture/Point-uniform01.ppm', 'gesture/V-uniform01.ppm']
#
# figure()
# for i, im in enumerate(imlist):
# print(im)
# dsift.process_image_dsift(im, im[:-3] + 'dsift', 10, 5, True, resize=(50,50))
# l, d = sift.read_features_from_file(im[:-3] + 'dsift')
# dirpath, filename = os.path.split(im)
# im = array(Image.open(im))
# # 显示手势含义title
# titlename = filename[:-14]
# subplot(2, 3, i + 1)
# sift.plot_features(im, l, True)
# title(titlename)
# show()
#
features,labels = read_gesture_features_labels('train/')
test_features,test_labels = read_gesture_features_labels('test/')
classnames = unique(labels)
# test 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))])
# accuracy
acc = sum(1.0*(res==test_labels)) / len(test_labels)
print ('Accuracy:', acc)
print_confusion(res,test_labels,classnames)
最后打印出准确度和混淆矩阵:
2 贝叶斯分类器
另一个简单却有效的分类器是贝叶斯分类器(或称朴素贝叶斯分类器)。贝叶斯分类器是一种基于贝叶斯条件概率定理的概率分类器,它假设特征是彼此独立不相关的(这就是他“朴素”的部分)。贝叶斯分类器可以非常有效地被训练出来,原因在于每一个特征模型都是独立选取的。尽管它们的假设非常简单,但是贝叶斯分类器已经在实际应用中获得显著成效,尤其是对垃圾邮件的过滤。贝叶斯分类器的另一个好处是,一旦学习了这个模型,就没有必要存储训练数据了,只需存储模型的参数。
该分类器是通过将各个特征的条件概率相乘得到一个类的总概率,然后选取概率最高的那个类构造出来的。
首先让我们看一个使用高斯概率分布模型的贝叶斯分类器基本实现,也就是用从训练数据集计算得到的特征均值和方差来对每个特征单独建模。把下面的BayesClassifier类添加到文件bayes.py中:
from pylab import *
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)
该函数用来计算单个高斯分布的乘积,返回给定一组模型参数m和v的概率。
将该贝叶斯分类器用于上一节的二维数据,下面的脚本将载入上一节的二维数据,并训练出一个分类器:
# 用Pickle模块载入二维样本点
with open('points_normal.pkl','rb+') as f:
class_1 = pickle.load(f)
class_2 = pickle.load(f)
labels = pickle.load(f)
# 训练贝叶斯分类器
bc = BayesClassifier()
bc.train([class_1, class_2],[1,-1])
# 用Pickle模块载入测试数据
with open('points_normal_test.pkl','rb+') 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()
[1 1 1 1 1 1 1 1 1 1]
2.1 用PCA降维
现在,我们尝试手势识别问题。由于稠密SIFT描述子的特征向量十分庞大(从前的例子可以看到,参数的选取超过了10000),在用数据拟合模型之前进行降维处理是一个很好的想法。主成分分析法,非常适合用于降维。下面的脚本就是用PCA进行降维:
features, labels = read_gesture_features_labels('train/')
test_features, test_labels = read_gesture_features_labels('test/')
classnames = unique(labels)
# print(features,labels,classnames)
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])
# 测试贝叶斯分类器
bc = BayesClassifier()
blist = [features[where(labels == c)[0]] for c in classnames]
# print(blist)
bc.train(blist, classnames)
res = bc.classify(test_features)[0]
acc = sum(1.0 * (res == test_labels)) / len(test_labels)
print('Accuracy:', acc)
print_confusion(res, test_labels, classnames)
3 支持向量机
SVM(Support Vector Machine,支持向量机)是一类强大的分类器,可以在很多分类问题中给出有水准很高的分类结果。最简单的SVM通过在高位空间中寻找一个最优线性分类面,尽可能地将两类数据分开。对于以特征向量x的决策函数为: f ( x ) = w ⋅ x − b f(x)=w·x-b f(x)=w⋅x−b其中w是常规的超平面,b是偏移量常数。该函数的阈值为0,它能够很好地将两类数据分开,使其一类为正数,另一类为负数。通过在训练集上求解哪些带有标记 y i ∈ { − 1 , 1 } y_i\in\{-1,1\} yi∈{−1,1}的特征向量xi的最优化问题,使超平面在两类间具有最大分开间隔,从而找到上面决策函数中的参数w和b。该决策函数的常规解是训练集上某些特征向量的线性组合: w = ∑ i α i y i x i w=\sum_i\alpha_iy_ix_i w=i∑αiyixi所以决策函数可以写为: f ( x ) = ∑ i α i y i x i ⋅ x − b f(x)=\sum_i\alpha_iy_ix_i ·x-b f(x)=i∑αiyixi⋅x−b这里的i是从训练集中选出的部分样本,这里选择的样本称为支持向量,因为它们可以帮助定义分类的边界。
SVM的一个优势是可以使用核函数(kernel function);核函数能够将特征向量映射到另一个不同维度的空间中,比如高纬度空间。通过核函数映射,依然可以保持对决策函数的控制,从而可以有效地解决非线性问题或者很难的分类问题。用核函数K(xi,x)替代上面决策函数中的内积xi·x。
scikit-learn中的SVM
我们先使用sklearn中的数据集构建两类点:
生成SVM训练模型:
画出决策边界:
调节超参数c观察结果:
画出决策边界的函数:
画出我们SVM模型的决策边界:
画出在超参数c变大时的决策边界:
尝试用SVM去解决回归问题:
支撑向量机在理论难度上还是很大的,重点在于理解整个过程的思想和解决问题的思路,现在只是很浅显的学习了SVM,之后还要仔细再去学习。