人脸特征提取

一.实验内容

实现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展成一个向量,再把向量拼成一个矩阵。

image-20191231112210347

若有n张图像,可得如下矩阵

image-20191231113122378

下面是我们用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)

到此,我们就完成了人脸图像的特征提取,中间结果是“平均脸”与“特征脸”,可视化效果如下:

lyzgxh_eigen_mu lyzgxh_eigen_feature

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特征,左边为特征值,后面为类别:

image-20191231144817974

由于经过Fisher提取后,只有一个特征向量,所以相当于只有一个特征脸。
到此,我们就完成了人脸图像的特征提取,中间结果是“平均脸”与“特征脸”,可视化效果如下:

lyztest_fisher_mu lyztest_fisher_feature

3.LBP特征提取

LBP特征是通过领域像素与中心像素在数值上的关系,来计算中心像素的LBP值,本实验我们使用圆形领域形式计算LBP值,如下图:

image-20191231145718548

由上图可以知道,每个像素点的LBP值是一个八位的二进制数,通过遍历整个图像上的像素点,得到每个像素点的LBP值,这个矩阵便是实验中所说的“LBP特征图像”,可视化形式如下图:

lyzwh_lbp_example

有了LBP特征图像,我们用统计直方图的方法便可得到LBP的特征向量,具体计算过程,我们将在接下来的代码讲解中会详细介绍。

image-20191231151026996

上面过程的主体部分代码部分如下,也就是先用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维向量拼在一起,组成新的特征向量。
具体的情况如下图

image-20191231154911626
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
  • 3
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值