Python深度学习实战:人脸关键点(15点)检测pytorch实现

Posted 爱你是长久之计~

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python深度学习实战:人脸关键点(15点)检测pytorch实现相关的知识,希望对你有一定的参考价值。

引言

人脸关键点检测即对人类面部若干个点位置进行检测,可以通过这些点的变化来实现许多功能,该技术可以应用到很多领域,例如捕捉人脸的关键点,然后驱动动画人物做相同的面部表情;识别人脸的面部表情,让机器能够察言观色等等。

如何检测人脸关键点

本文是实现15点的检测,至于N点的原理都是一样的,使用的算法模型是深度神经网络,使用CV也是可以的。

如何检测

这个问题抽象出来,就是一个使用神经网络来进行预测的功能,只不过输出是15个点的坐标,训练数据包含15个面部的特征点和面部的图像(大小为96x96),15个特征点分别是:left_eye_center, right_eye_center, left_eye_inner_corner, left_eye_outer_corner, right_eye_inner_corner, right_eye_outer_corner, left_eyebrow_inner_end, left_eyebrow_outer_end, right_eyebrow_inner_end, right_eyebrow_outer_end, nose_tip, mouth_left_corner, mouth_right_corner, mouth_center_top_lip, mouth_center_bottom_lip
因此神经网络需要学习一个从人脸图像到15个关键点坐标间的映射。

使用的网络结构

在本文中,我们使用深度神经网络来实现该功能,基本卷积块使用Google的Inception网络,也就是使用GoogLeNet网络,该结构的网络是基于卷积神经网络来改进的,是一个含有并行连接的网络。
众所周知,卷积有滤波、提取特征的作用,但到底采用多大的卷积来提取特征是最好的呢?这个问题没有确切的答案,那就集百家之长:使用多个形状不一的卷积来提取特征并进行拼接,从而学习到更为丰富的特征;特别是里面加上了1x1的卷积结构,能够实现跨通道的信息交互和整合(其本质就是在多个channel上的线性求和),同时能在feature map通道数上的降维(读者可以验证计算一下,能够极大减少卷积核的参数),也能够增加非线性映射次数使得网络能够更深。
下面是Inception块的示意图:

整个GoogLeNet的结构如下所示:

接下来是代码实现部分,后续作者会补充神经网络的相关原理知识,若对此感兴趣的读者也可继续关注支持~

代码实现

import torch as tc
from torch import nn
from torch.nn import functional as F
from torch.utils.data import DataLoader
from torch.utils.data import TensorDataset
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
from sklearn.utils import shuffle

# 对图片像素的处理
def proFunc1(data,testFlag:bool=False) -> tuple:
    data['Image'] = data['Image'].apply(lambda im: np.fromstring(im, sep=' '))
    # 处理na
    data = data.dropna()  
    # 神经网络对数据范围较为敏感 /255 将所有像素都弄到[0,1]之间
    X = np.vstack(data['Image'].values) / 255
    X = X.astype(np.float32)
    # 特别注意 这里要变成 n channle w h 要跟卷积第一层相匹配
    X = X.reshape(-1, 1,96, 96) 
    # 等会神经网络的输入层就是 96 96 黑白图片 通道只有一个
    # 只有训练集才有y 测试集返回一个None出去
    if not testFlag:  
        y = data[data.columns[:-1]].values
        # 规范化
        y = (y - 48) / 48  
        X, y = shuffle(X, y, random_state=42)  
        y = y.astype(np.float32)
    else:
        y = None

    return X,y

# 工具类
class UtilClass:

    def __init__(self,model,procFun,trainFile:str='data/training.csv',testFile:str='data/test.csv') -> None:
        self.trainFile = trainFile
        self.testFile = testFile
        self.trainData = None
        self.testData = None
        self.trainTarget = None
        self.model = model
        self.procFun = procFun

    @staticmethod
    def procData(data, procFunc ,testFlag:bool=False) -> tuple:
        return procFunc(data,testFlag)

    def loadResource(self):
        rawTrain = pd.read_csv(self.trainFile)
        rawTest = pd.read_csv(self.testFile)
        self.trainData , self.trainTarget = self.procData(rawTrain,self.procFun)
        self.testData , _ = self.procData(rawTest,self.procFun,testFlag=True)

    def getTrain(self):
        return tc.from_numpy(self.trainData), tc.from_numpy(self.trainTarget)

    def getTest(self):
        return tc.from_numpy(self.testData)

    @staticmethod
    def plotData(img, keyPoints, axis):
        axis.imshow(np.squeeze(img), cmap='gray') 
        # 恢复到原始像素数据 
        keyPoints = keyPoints * 48 + 48 
        # 把keypoint弄到图上面
        axis.scatter(keyPoints[0::2], keyPoints[1::2], marker='o', c='c', s=40)

# 自定义的卷积神经网络
class MyCNN(tc.nn.Module):
    def __init__(self,imgShape = (96,96,1),keyPoint:int = 15):
        super(MyCNN, self).__init__()
        self.conv1 = tc.nn.Conv2d(in_channels=1, out_channels =10, kernel_size=3)
        self.pooling = tc.nn.MaxPool2d(kernel_size=2)
        self.conv2 = tc.nn.Conv2d(10, 5, kernel_size=3)
        # 这里的2420是通过下面的计算得出的 如果改变神经网络结构了 
        # 需要计算最后的Liner的in_feature数量 输出是固定的keyPoint*2
        self.fc = tc.nn.Linear(2420, keyPoint*2)

    def forward(self, x):
        # print("start----------------------")
        batch_size = x.size(0)
        # x = x.view((-1,1,96,96))
        # print('after view shape:',x.shape)
        x = F.relu(self.pooling(self.conv1(x)))
        # print('conv1 size',x.shape)
        x = F.relu(self.pooling(self.conv2(x)))
        # print('conv2 size',x.shape)
        # print('end--------------------------')
        # 改形状
        x = x.view(batch_size, -1)
        # print(x.shape)
        x = self.fc(x)
        # print(x.shape)
        return x

# GoogleNet基本的卷积块
class MyInception(nn.Module):

    def __init__(self,in_channels, c1, c2, c3, c4,) -> None:
        super().__init__()
        
        self.p1_1 = nn.Conv2d(in_channels, c1, kernel_size=1)
        
        self.p2_1 = nn.Conv2d(in_channels, c2[0], kernel_size=1)
        self.p2_2 = nn.Conv2d(c2[0], c2[1], kernel_size=3, padding=1)
        
        self.p3_1 = nn.Conv2d(in_channels, c3[0], kernel_size=1)
        self.p3_2 = nn.Conv2d(c3[0], c3[1], kernel_size=5, padding=2)
        
        self.p4_1 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
        self.p4_2 = nn.Conv2d(in_channels, c4, kernel_size=1)

    def forward(self, x):
        p1 = F.relu(self.p1_1(x))
        p2 = F.relu(self.p2_2(F.relu(self.p2_1(x))))
        p3 = F.relu(self.p3_2(F.relu(self.p3_1(x))))
        p4 = F.relu(self.p4_2(self.p4_1(x)))
        # 在通道维度上连结输出
        return tc.cat((p1, p2, p3, p4), dim=1)

# GoogLeNet的设计 此处参数结果google大量实验得出
b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
                   nn.ReLU(),
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

b2 = nn.Sequential(nn.Conv2d(64, 64, kernel_size=1),
                   nn.ReLU(),
                   nn.Conv2d(64, 192, kernel_size=3, padding=1),
                   nn.ReLU(),
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

b3 = nn.Sequential(MyInception(192, 64, (96, 128), (16, 32), 32),
                   MyInception(256, 128, (128, 192), (32, 96), 64),
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

b4 = nn.Sequential(MyInception(480, 192, (96, 208), (16, 48), 64),
                   MyInception(512, 160, (112, 224), (24, 64), 64),
                   MyInception(512, 128, (128, 256), (24, 64), 64),
                   MyInception(512, 112, (144, 288), (32, 64), 64),
                   MyInception(528, 256, (160, 320), (32, 128), 128),
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

b5 = nn.Sequential(MyInception(832, 256, (160, 320), (32, 128), 128),
                   MyInception(832, 384, (192, 384), (48, 128), 128),
                   nn.AdaptiveAvgPool2d((1,1)),
                   nn.Flatten())


uClass = UtilClass(model=None,procFun=proFunc1)
uClass.loadResource()
xTrain ,yTrain = uClass.getTrain()
xTest = uClass.getTest()

dataset = TensorDataset(xTrain, yTrain)
trainLoader = DataLoader(dataset, 64, shuffle=True, num_workers=4)

# 训练net并进行测试 由于显示篇幅问题 只能打印出极为有限的若干测试图片效果
def testCode(net):
    optimizer = tc.optim.Adam(params=net.parameters())
    criterion = tc.nn.MSELoss()
        
    for epoch in range(30):
        trainLoss = 0.0
        # 这里是用的是mini_batch 也就是说 每次只使用mini_batch个数据大小来计算
        # 总共有total个 因此总共训练 total/mini_batch 次
        # 由于不能每组数据只使用一次 所以在下面还要使用一个for循环来对整体训练多次
        for batchIndex, data in enumerate(trainLoader, 0):
            input_, y = data
            yPred = net(input_)
            loss = criterion(yPred, y)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
            trainLoss += loss.item()
            # 只在每5个epoch的最后一轮打印信息
            if batchIndex % 30 ==29 and not epoch % 5 :
                print("[,] loss:".format(epoch + 1, batchIndex + 1, trainLoss / 300))
                trainLoss = 0.0
    # 测试
    print("-----------test begin-------------")
    # print(xTest.shape)
    yPost = net(xTest)
    # print(yPost.shape)
    import matplotlib.pyplot as plt
    %matplotlib inline
        
    fig = plt.figure(figsize=(20,20))
    fig.subplots_adjust(left=0, right=1, bottom=0, top=1, hspace=0.05, wspace=0.05)
    for i in range(9,18):
        ax = fig.add_subplot(3, 3, i - 9 + 1, xticks=[], yticks=[])
        uClass.plotData(xTest[i], y[i], ax)
    print("-----------test end-------------")
    

if __name__ == "__main__":
    # 训练MyCNN网络 并可视化在9个测试数据的效果图
    myNet = MyCNN()
    testCode(myNet)
    inception = nn.Sequential(b1, b2, b3, b4, b5, nn.Linear(1024, 30))
	testCode(inception)

本文使用的数据可在此找到两个data文件,本文有你帮助的话,就给个点赞关注支持一下吧!

PyTorch深度学习实战 | 基于ResNet的人脸关键点检测

人脸关键点检测指的是用于标定人脸五官和轮廓位置的一系列特征点的检测,是对于人脸形状的稀疏表示。关键点的精确定位可以为后续应用提供十分丰富的信息。因此,人脸关键点检测是人脸分析领域的基础技术之一。许多应用场景(如人脸识别、人脸三维重塑、表情分析等)均将人脸关键点检测作为其前序步骤来实现。本文将通过深度学习的方法来搭建一个人脸关键点检测模型。
1995年,Cootes提出 ASM(active shape model) 模型用于人脸关键点检测,掀起了一波持续多年的研究浪潮。这一阶段的检测算法常常被称为传统方法。2012年,AlexNet 在 ILSVRC 中力压榜眼夺冠,将深度学习带进人们的视野。随后 Sun 等在 2013 年提出了 DCNN 模型,首次将深度方法应用于人脸关键点检测。自此,深度卷积神经网络成为人脸关键点检测的主流工具。本期主要使用 Keras 框架来搭建深度模型。

目前,开源的人脸关键点数据集有很多。例如AFLW、300W、MTFL/MAFL 等,关键点个数也从 5 个到上千个不等。本章中采用的是 CVPR 2018 论文Look at Boundary: A Boundary-Aware Face Alignment Algorithm中提出的 WFLW(wider facial landmarks in-the-wild) 数据集。这一数据集包含了 10000 张人脸信息,其中 7500 张用于训练,剩余 2500 张用于测试。每张人脸图片被标注以 98 个关键点,人脸关键点分布如图1所示。

■ 图1 人脸关键点分布

由于关键点检测在人脸分析任务中的基础性地位,工业界往往拥有标注了更多关键点的数据集。但是由于其商业价值,这些信息一般不会被公开,因此目前开源的数据集还是以 5 点和 68 点为主。在本项目中使用的 98 点数据集不仅能够更加精确地训练模型,同时还可以更加全面地对模型表现进行评估。

然而另一方面,数据集中的图片并不能直接作为模型输入。对于模型来说,输入图片应该是等尺寸且仅包含一张人脸的。但是数据集中的图片常常会包含多个人脸,这就需要首先对数据集进行预处理,使之符合模型的输入要求。

1、人脸裁剪与缩放

数据集中已经提供了每张人脸所处的矩形框,可以据此确定人脸在图像中的位置,人脸矩形框示意如图2所示。但是直接按照框选部分进行裁剪会导致两个问题:一是矩形框的尺寸不同,裁剪后的图片还是无法作为模型输入;二是矩形框只能保证将关键点包含在内,耳朵、头发等其他人脸特征则排除在外,不利于训练泛化能力强的模型。

■ 图2 人脸矩形框示意

为了解决上述的第一个问题,将矩形框放大为方形框,因为方形图片容易进行等比例缩放而不会导致图像变形。对于第二个问题,则单纯地将方形框的边长延长为原来的1.5倍,以包含更多的脸部信息。相关代码如代码清单1所示。

代码清单1

代码清单1以及其余的全部代码中涉及的 image 对象均为 PIL.Image 类型。PIL(python imaging library) 是一个第三方模块,但是由于其强大的功能与广泛的用户基础,几乎已经被认为是 Python 官方图像处理库了。PIL 不仅为用户提供了 jpg、png、gif 等多种图片类型的支持,还内置了十分强大的图片处理工具集。上面提到的 Image 类型是 PIL 最重要的核心类,除了具备裁剪 (crop) 功能外,还拥有创建缩略图(thumbnail)、通道分离 (split) 与合并 (merge)、缩放 (resize)、转置 (transpose) 等功能。下面给出一个图片缩放的例子,如代码清单2所示。

代码清单2

代码清单2将人脸图片和关键点坐标一并缩放至 128×128px。在 Image.resize()函数的调用中,第一个参数表示缩放的目标尺寸,第二个参数表示缩放所使用的过滤器类型。在默认情况下,过滤器会选用 Image.NEAREST ,其特点是压缩速度快但压缩效果较差。因此,PIL官方文档中建议是如果对于图片处理速度的要求不是那么苛刻,推荐使用 Image.ANTIALIAS 以获得更好的缩放效果。在本项目中,由于 _resize() 函数对每张人脸图片只会调用一次,因此时间复杂度并不是问题。况且图像经过缩放后还要被深度模型学习,缩放效果很可能是决定模型学习效果的关键因素,所以这里选择了 Image.ANTIALIAS 过滤器进行缩放。图2经过裁剪和缩放处理后的效果图如图3所示。

■ 图3 经过裁剪和缩放处理后的效果示意

2、数据归一化处理

经过裁剪和缩放处理所得到的数据集已经可以用于模型训练了,但是训练效果并不理想。对于正常图片,模型可以以较高的准确率定位人脸关键点。但是在某些过度曝光或者经过了滤镜处理的图片面前,模型就显得力不从心了。为了提高模型的准确率,这里进一步对数据集进行归一化处理。所谓归一化,就是排除某些变量的影响。例如,希望将所有人脸图片的平均亮度统一,从而排除图片亮度对模型的影响,如代码清单3所示。

代码清单3

mageStat和 ImageEnhance 分别是 PIL 中的两个工具类。顾名思义 ImageStat 可以对图片中每个通道进行统计分析,代码清单3中就对图片的三个通道分别求得了平均值;ImageEnhance 用于图像增强,常见用法包括调整图片的亮度、对比度以及锐度等。

提示/

颜色通道是一种用于保存图像基本颜色信息的数据结构。最常见的 RGB 模式图片由红、绿、蓝三种基本颜色组成。也就是说,RGB 图片中的每个像素都是用这三种颜色的亮度值来表示的。在一些印刷品的设计图中会经常遇到另一种称为 CYMK 的颜色模式,这种模式下的图片包含四个颜色通道,分别表示青、黄、红、黑。PIL 可以自动识别图片文件的颜色模式,因此多数情况下用户并不需要关心图像的颜色模式。但是在对图片应用统计分析或增强处理时,底层操作往往是针对不同通道分别完成的。为了避免因为颜色模式导致的图像失真,用户可以通过 PIL.Image.mode 属性查看被处理图像的颜色模式。

类似地,希望消除人脸朝向所带来的影响。这是因为训练集中朝向左边的人脸明显多于朝向右边的人脸,导致模型对于朝向右侧的人脸识别率较低。具体做法是随机地将人脸图片进行左右翻转,从而在概率上保证朝向不同方向的人脸图片具有近似平均的分布,如代码清单4所示。

代码清单4

图片的翻转比较容易完成,只需要调用 PIL.Image 类的转置方法即可,但是关键点的翻转则需要一些额外的操作。举例来说,左眼 96 号关键点在翻转后会成为新图片的右眼 97 号关键点(见图1),因此其在 pts 数组中的位置也需要从 96 变为 97 。为了实现这样的功能,定义全排列向量 perm 来记录关键点的对应关系。为了方便程序调用, perm 被保存在文件中。但是如果每次调用 _fliplr()函数时都从文件中读取,显然会拖慢函数的执行;而将 perm 作为全局变量加载,又会污染全局变量空间,破坏函数的封装性。这里的解决方案是将 perm 作为函数对象 _fliplr() 的一个属性,从外部加载并始终保存在内存中,如代码清单5所示。

代码清单5

提示/

熟悉 C/C++ 的读者可能会联想到 static 修饰的静态局部变量。很遗憾的是, Python 作为动态语言是没有这种特性的。代码清单5就是为了实现类似效果所做出的一种尝试。

3、整体代码

前面定义了对于单张图片的全部处理函数,接下来就只需要遍历数据集并调用即可,如代码清单6所示。由于训练集和测试集在 WFLW 中是分开进行存储的,但是二者的处理流程几乎相同,因此可以将其公共部分抽取出来作为 preprocess()函数进行定义。训练集和测试集共享同一个图片库,其区别仅仅在于人脸关键点的坐标以及人脸矩形框的位置,这些信息被存储在一个描述文件中。preprocess()函数接收这个描述文件流作为参数,依次处理文件中描述的人脸图片,最后将其保存到 dataset 目录下的对应位置。

代码清单6

在preprocess()函数中,将 50 个数据组成一批 (batch) 进行存储,这样做的目的是方便模型训练过程中的数据读取。在机器学习中,模型训练往往是以批为单位的,这样不仅可以提高模型训练的效率,还能充分利用 GPU 的并行能力加快训练速度。处理后的目录结构如代码清单7所示。

代码清单7

以上是关于Python深度学习实战:人脸关键点(15点)检测pytorch实现的主要内容,如果未能解决你的问题,请参考以下文章

人脸检测实战:使用opencv加载深度学习模型实现人脸检测

OpenCV-Python实战(15)——面部特征点检测详解(仅需5行代码学会3种面部特征点检测方法)

基于 Tensorflow 2.x 从零训练 15 点人脸关键点检测模型

基于 Tensorflow 2.x 从零训练 15 点人脸关键点检测模型

基于 Tensorflow 2.x 从零训练 15 点人脸关键点检测模型

OpenCV-Python实战(14)——人脸检测详解(仅需6行代码学会4种人脸检测方法)