一.实验内容
实现PCA,Fisher,LBP三种特征提取方法,显示特征图像;并观察特征数据形式。本实验主要是需要学生理解三种特征提取方法的基本原理,会使用OpenCV相关接口函数完成人脸特征提取。
二.实验步骤
本实验可以使用给定的数据集完成特征提取与显示任务,数据集存放在以“data+名字首字母缩写后缀”为名字的文件夹中,在项目中,我们已经编写好了数据采集,图像读取与可视化部分,这里我们只需要重点学习PCA,Fisher与LBP特征提取的方法实现与结果输出方式,在文件Feature.py中,eigenface,fisherface,lbpface实现了三种方法的特征提取,main函数实现了特征提取后对数据的展示。
1.PCA特征提取
由于opencv-python中带有PCA接口,本实验PCA特征是使用opencv-python提供的接口的完成,这次过程中,我们输出“平均脸”与“特征脸”图像,以及最后降维得到的人脸数据。opencv-python提供的PCA接口包括:cv2.PCACompute和cv2.PCAProject(但这里为了统一接口,没有使用这个接口,而是使用的统一的自己编写的project接口)。cv2.PCACompute负责计算平均特征,特征值和特征向量;project负责投影,即将向量根据cv2.PCACompute计算出来的从原来特征特征空间投影到新的特征空间。
def eigenface(X,y):
'''
eigenface人脸特征提取方法
:param X: 人脸数据
:param y: 人脸标签
:return: 最后返回的特征向量都是(原特征数,新特征数)
'''
X=asRowMatrix(X)
# X size:(501,34225),第一维为图片张数,第二维是图片像素
# 调用opencv的pca接口
mu, eigenvectors = cv2.PCACompute(X, None)
eigenvectors=eigenvectors.T
# 保留的主元个数,形成一个数组
# mu 平均脸的大小是:(1,34225)
# 在不指定主成分个数的情况下,主成分分析的主成分个数指定为x.shape[0],
# 这里也就是501,shape为(501,34225)
#将原图像投影到新的向量空间中
projection=project(eigenvectors,X,mu)
return mu,eigenvectors,projection
对于上面函数提取出来的特征值和特征向量,随后,使用plt.imshow()可以展示出矩阵形式的图片(平均脸),使用自己编写的subplot接口绘制出特征向量(特征脸图像)。
'''
特征脸部分
'''
mu_e,eigenvectors_e,projection_e=eigenface(X,y)
#显示出特征脸图像
E = []
#在特征矩阵中进行提取,特征矩阵的每一行都是一个特征向量
#一共只提取最多16个特征向量,len(X)=802
print(eigenvectors_e[:,:16])
for i in range(min(len(X), 16)):
e = eigenvectors_e[:,i].reshape(X[0].shape)
E.append(normalize(e,0,255))
#绘图
fig=plt.figure()
plt.imshow(mu_e.reshape(X[0].shape))
#保存文件
subplot(title="Eigen", images=E, rows=4, cols=4, sptitle="Eigenface", colormap=cm.jet)
#不保存文件,绘制图片
#subplot(title="Eigen", images=E, rows=4, cols=4, sptitle="Eigenface", colormap=cm.jet, filename="python_eigenfaces.pdf")
其中PCACompute的实现时此部分的重点,接下来详细介绍一下它的实现,此函数形式如下:
def PCACompute(data, mean, eigenvectors=None, maxComponents=None): # real signature unknown; restored from __doc__
"""
PCACompute(data, mean[, eigenvectors[, maxComponents]]) -> mean, eigenvectors
. wrap PCA::operator()
PCACompute(data, mean, retainedVariance[, eigenvectors]) -> mean, eigenvectors
. wrap PCA::operator()
"""
pass
首先人脸数据是存储在文件夹中的,每个文件夹中都存储着一个人的所有提取出来的人脸图像。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0I8Y8yL2-1591925775065)(/home/tina/.config/Typora/typora-user-images/image-20191231112041520.png)]
第一,data是图像数据,这里的图像是所有图片组成的矩阵,首先将图片用asRowMatrix展成一个向量,再把向量拼成一个矩阵。
若有n张图像,可得如下矩阵
下面是我们用asRowMatrix完成对图片矩阵的转化过程,这个接口时从底层实现的,未使用相关接口,代码如下:
def asRowMatrix(X):
'''
将所有图像编程一个矩阵,变成pca能处理的形式
:param X: 图像组成的list
:return: 处理后的矩阵
'''
#入输入的X中没有图像,则返回一个空的array
if len(X) == 0:
return np.array([])
#否则初始化一个矩阵,这里列数初始化为X[0].size是为了后面的vstack叠加
mat = np.empty((0, X[0].size), dtype=X[0].dtype)
for row in X:
#将每一行的数据拼起来,变成多行
mat = np.vstack((mat, np.asarray(row).reshape(1,-1)))
return mat
第二,mean如果不指定平均值,则在用平均值归一化的时候计算出平均值,如果指定,就用指定值。
第三,eigenvectors这里不进行讨论,对本实验不重要。
第四,maxComponents是最大的主成分个数。
本部分的另一个重点就是project函数,因为后面也需要降维,所以也需要使用这个函数,project的主要实现是通过numpy来实现向量乘法来完成投影操作
def project(W, X, mu=None):
'''
投影操作,向量(矩阵),从原来的特征空间,投影到新的投影空间
:param W:新的特征空间基向量组成的矩阵
:param X:需要投影的向量(矩阵
:param mu:平均值
:return:投影后的向量(矩阵)
'''
#如果不需要归一化,则直接使用投影
if mu is None:
#使用向量乘法进行投影
return np.dot(X, W)
return np.dot(X - mu, W)
到此,我们就完成了人脸图像的特征提取,中间结果是“平均脸”与“特征脸”,可视化效果如下:
2.Fisher特征提取
Fisher特征提取的原理在实验原理部分已经详细讲述过,Fisher的特征提取流程与PCA类似,,其中线性判别分析就是通过一个平面将特征空间分成两部分,对于本实验只有两个分类,最终得到一个特征向量即可。下面讲解如何实现Fisher的特征提取,由于opencv-python中只提供了PCA的接口,没有提供LDA的接口,所以本次试验中的LDA部分是我们自己实现的。
由于LDA中需要用到标签类别信息,所以在对X进行处理的同时也要对y进行处理,并计算类别数。这一部分的关键函数主要是cv2.PCAProject和lda,后者主要用来对PCA降维后的特征再次进行一次降维计算,得到用来进行降维的特征值和特征向量,然后通过将PCA计算出的特征向量与LDA计算出的特征向量进行结合,然后进行project。
def fisherface(X,y):
'''
fisherface人脸特征提取方法,
返回类型都是ndarray形式
:param X: 人脸数据
:param y: 人脸标签
:return:最后返回的特征向量都是(原特征数,新特征数)
'''
# 将y转化成ndarray形式
y = np.array(y)
# 对X进行处理
X = asRowMatrix(X)
(n, _) = np.shape(X)
# 获取类别数目
c = len(np.unique(y))
# 先进行PCA计算
mu_pca, eigenvectors_pca = cv2.PCACompute(X, None, maxComponents=n - c)
# (800, 34225) egenvectors_pca
# 再进行LDA计算
eigenvalues_lda, eigenvectors_lda = lda(cv2.PCAProject(X, mu_pca,eigenvectors_pca), y, 0)
# 计算出两个特征向量的乘积
eigenvectors = np.dot(eigenvectors_pca.T, eigenvectors_lda)
# 显示出特征脸图像
# 将原图像投影到新的向量空间中
projection = project(eigenvectors,X, mu_pca)
return mu_pca,eigenvectors,projection
对于上面函数提取出来的特征值和特征向量,随后,使用plt.imshow()可以展示出矩阵形式的图片(平均脸),使用自己编写的subplot接口绘制出特征向量(特征脸图像)。
'''
fisher部分
'''
mu_f, eigenvectors_f, projection_f = fisherface(X, y)
F = []
#在特征矩阵中进行提取,特征矩阵的每一行都是一个特征向量
#一共只提取最多16个特征向量,
# eigenvectors_f.shape[1]这里是特征矩阵的第二维(因为只有两类,所以有两格特征向量,
# 代表特征向量个数,这里一共使用1个
print(eigenvectors_f[:,:16])
for i in range(min(eigenvectors_f.shape[1], 16)):
f = eigenvectors_f[:,i].reshape(185,185)
F.append(normalize(f,0,255))
#绘图
fig=plt.figure()
plt.imshow(mu_f.reshape(185,185))
#保存文件
subplot(title="Fisher", images=F, rows=4, cols=4, sptitle="Fisherface", colormap=cm.jet)
#不保存文件,绘制图片
# subplot(title="Fisher", images=F, rows=4, cols=4, sptitle="Fisherface", colormap=cm.jet, filename="python_fisher.pdf")
其中lda是本部分的重点,下面时它的具体实现
def lda(X, y, num_components=0):
'''
lda操作,思想是每次都在分开效果最好的方向
进行投影,但是他的特点是最多只能选择(类别-1)
个特征值,因为这个方法的目的是将每一类都分开,
又因为lda是线性方法用来解决线性可分的问题
则将一类和其他类分开只需要一条直线
:param X: 需要处理的矩阵
:param y: 矩阵中向量对应点标签
:param num_components: 保留的成分
:return: 特征值,特征向量,平均值
'''
#将y转化成ndarray的形式
y = np.asarray(y)
[n, d] = X.shape
c = np.unique(y)
if (num_components <= 0) or (num_components > (len(c) - 1)):
num_components = (len(c) - 1)
#求出平均值
meanTotal = X.mean(axis=0)
#类内距离初始化
Sw = np.zeros((d, d), dtype=np.float32)
#类间距离初始化
Sb = np.zeros((d, d), dtype=np.float32)
#对于每一类进行计算
for i in c:
#求出数据中属于该类的数据
Xi = X[np.where(y == i)[0], :]
#求出该类所有数据的平均值
meanClass = Xi.mean(axis=0)
#计算类内间距
Sw = Sw + np.dot((Xi - meanClass).T, (Xi - meanClass))
#计算类间距离
Sb = Sb + n * np.dot((meanClass - meanTotal).T, (meanClass - meanTotal))
#将类内距离和类间距离相结合,求出广义瑞丽商
eigenvalues, eigenvectors = np.linalg.eig(np.linalg.inv(Sw) * Sb)
#根据特征值进行排序
idx = np.argsort(-eigenvalues.real)
eigenvalues, eigenvectors = eigenvalues[idx], eigenvectors[:, idx]
#提取特征值最大的几个
eigenvalues = np.array(eigenvalues[0:num_components].real, dtype=np.float32, copy=True)
eigenvectors = np.array(eigenvectors[0:, 0:num_components].real, dtype=np.float32, copy=True)
return eigenvalues, eigenvectors
对于当前的二分类问题,lda只会计算出来一个特征向量,也就是说,后面经过pca计算出来的特征向量组成的降维矩阵与lda计算出来的特征向量组成的降维矩阵相乘得到一个新的降维矩阵,这样再进行投影后,就可以把一张图片转化成一个数字,下面时举例的降维后的Fisher特征,左边为特征值,后面为类别:
由于经过Fisher提取后,只有一个特征向量,所以相当于只有一个特征脸。
到此,我们就完成了人脸图像的特征提取,中间结果是“平均脸”与“特征脸”,可视化效果如下:
3.LBP特征提取
LBP特征是通过领域像素与中心像素在数值上的关系,来计算中心像素的LBP值,本实验我们使用圆形领域形式计算LBP值,如下图:
由上图可以知道,每个像素点的LBP值是一个八位的二进制数,通过遍历整个图像上的像素点,得到每个像素点的LBP值,这个矩阵便是实验中所说的“LBP特征图像”,可视化形式如下图:
有了LBP特征图像,我们用统计直方图的方法便可得到LBP的特征向量,具体计算过程,我们将在接下来的代码讲解中会详细介绍。
上面过程的主体部分代码部分如下,也就是先用LBP特征提取特征,然后得到直方图后进行统计。
def lbpface(X,y):
'''
lbp人脸识别方法
:param X: 人脸数据
:param y: 人脸标签
:return:原图像投影后的内容
'''
#将每张图片进行处理,这里要进行展平
projection=np.array([calLbpHistogram(LBP(x)).ravel() for x in X])
return projection
然后具体来看看每一步的具体实现
首先是LBP特征的提取,由于这里使用的时numpy,所以可以进行批量处理数据,首先是计算出所有需要提取提取特征的像素点的具体位置,然后初始化一个与原来图像大小相同的全零的矩阵。但是,由于边上的像素点周围像素点不全所以无法计算,为了使得所有的像素点都可以计算,所以在周围加入了一圈padding再进行计算。最后对于要计算的八个位置的每个位置进行计算。
def LBP(I, radius=2, count=8):
'''
得到图像的LBP特征
:param I:二维图像
:param radius: lbp操作的半径
:param count: 每个点考虑的点的数量
:return: 处理后的图像
'''
#根据半径和数目计算所有点的位置
dh = np.round([radius * math.sin(i * 2 * math.pi / count) for i in range(count)])
dw = np.round([radius * math.cos(i * 2 * math.pi / count) for i in range(count)])
#获得图像的长和宽
height, width = I.shape
#先创建一个初始值为0的矩阵,便于后面相加使用,使用numpy是便于操作
lbp = np.zeros(I.shape, dtype=np.int)
#填充数据,对二维图像周围加一个padding
I1 = np.pad(I, radius, 'edge')
for k in range(count):
#获取每一个点的坐标
h, w = int(radius + dh[k]),int(radius + dw[k])
#这里使用了bool索引
#1.(I > I1[h:h + height, w:w + width])得到一个0,1组成的矩阵,
# 0代表不大于,1代表大于0
#2.然后进行移位
lbp += ((I > I1[h:h + height, w:w + width]) << k)
return lbp
随后是计算直方图然后统计获得特征的过程,这里的实现方式是,先对进行lbp处理后的图像进行分块,然后对于每一块,统计0-255一共256个像素的数量,然后使用g_mapping将其从256维映射到59维的向量,最后将每一块的59维向量拼在一起,组成新的特征向量。
具体的情况如下图
g_mapping = [
0, 1, 2, 3, 4, 58, 5, 6, 7, 58, 58, 58, 8, 58, 9, 10,
11, 58, 58, 58, 58, 58, 58, 58, 12, 58, 58, 58, 13, 58, 14, 15,
16, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58,
17, 58, 58, 58, 58, 58, 58, 58, 18, 58, 58, 58, 19, 58, 20, 21,
22, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58,
58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58,
23, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58,
24, 58, 58, 58, 58, 58, 58, 58, 25, 58, 58, 58, 26, 58, 27, 28,
29, 30, 58, 31, 58, 58, 58, 32, 58, 58, 58, 58, 58, 58, 58, 33,
58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 34,
58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58,
58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 35,
36, 37, 58, 38, 58, 58, 58, 39, 58, 58, 58, 58, 58, 58, 58, 40,
58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 41,
42, 43, 58, 44, 58, 58, 58, 45, 58, 58, 58, 58, 58, 58, 58, 46,
47, 48, 58, 49, 58, 58, 58, 50, 51, 52, 58, 53, 54, 55, 56, 57]
def calLbpHistogram(lbp, hCount=5, wCount=5, maxLbpValue=255):
'''
分块计算lbp直方图,这里要进行降维处理,
即将255维进行降维,根据g_mapping
:param lbp: lbp处理后的图像
:param hCount: 分块时候的纵行
:param wCount: 分块时候的横行
:param maxLbpValue: 最大的像素值
:return:
'''
height, width = lbp.shape
res = np.zeros((hCount * wCount, max(g_mapping) + 1), dtype=np.float)
#分块进行计算
for h in range(hCount):
for w in range(wCount):
#提取出每一块
blk = lbp[int(height * h / hCount):int(height * (h + 1) / hCount), int(width * w / wCount):int(width * (w + 1) / wCount)]
#计算每一块的像素统计
hist1 = np.bincount(blk.ravel(), minlength=maxLbpValue)
#这里是将hist作为res中数据的引用,改变hist也会改变res
hist = res[h * wCount + w, :]
#根据映射,将255维空间投影到59维
for v, k in zip(hist1, g_mapping):
hist[k] += v
#求归一化后的值
hist /= hist.sum()
return res