✨✨✨
感谢优秀的你打开了小白的文章
“希望在看文章的你今天又进步了一点点,生活更加美好!”🌈🌈🌈
目录
1.前言
随着社会与科学技术的进步,人与人的交流也越来越多。人类一直主要是通过语言来进行交流,语言是传递语言的主要载体。但有研究表明,在人与人的交流中,语言所传达的信息不足10%,而人的面部表情所传达的信息超过50%。通过对方的面部表情所传达的信息,轻微的感情变化就可以反映一个人的心理状态的变化,因此可以分析得出对方的内心世界与情感态度,结合语言、动作就可以准确判定一个人的心理活动。随着人工智能技术的浪潮爆发之后,人脸识别,语音识别,虹膜识别等迅速发展,同时,人脸表情识别在学术界也产生越来越重要的影响,对人脸表情识别的准确度以及广泛性要求也越来越高。人类的很多基本感情都通常表现在人脸表情中,但是人脸表情的识别精度问题以及能否将其进一步深入挖掘,影响着人类对情感计算的探索。
人脸表情识别技术可以根据所建立模型提取的人脸特征来得到情感状态,人脸表情可以说是表达人类情感的最有效的方式之一。使用计算机对人脸表情数据进行量化处理,促进了人机交互的进步与发展。随着科学技术的进步,手机、电脑等设备的使用数目不断提高,人脸识别技术相关的技术框架以及开源代码、数据资源不断大量涌现。高性能的技术框架和应用设备不管是在软件还是硬件都支持更加精准的识别。如今,人脸表情识别技术已经是跨越生物学,教育学,医学的综合性课题。在传统的表情识别的方法中,广泛应用的如局部二值模式,主元分析法等,都是用人工的方法进行对算法的设计,来进行特定的特征分类,其效率大打折扣。随着对数据提取能力的要求逐渐提高,神经网络的出现提高了对人脸表情识别的认知。人工神经网络局限于编程员的编程能力,在本质上是各种分类器的组合物,并不能适应各种复杂的环境。卷积神经网络有良好的性质与优点可以大大增加了识别效率能够达到对图像的识别。本文即是卷积神经网络与分类器共同实现的结果。
2.1 卷积神经网络基本原理
卷积神经网络[7]属于前馈神经网络中的一种。通过从图片进入网络到输出之间的映射关系的学习,避免过程中复杂的数学公式,再用设计好的模型训练数据即可使网络模型获得学习能力。
2.1.1 局部感受野
简单来说,每次神经元只对一个图像的一小块局部区域进行处理,就是卷积神经网络的局部感受野特性[6]。每一层只对局部的像素进行特征处理能够大大减少一个网络结构中的参数数量。如果使用全连接方式处理一个大小为128×128的图片,并且每层包含300个神经元,那么所需的参数数量将达到128×128×300个,大约491万个参数,面对这样的一个量级的数据量,会大大降低训练效率。局部感受野模型图如图2-1。
图2-1 感受野模型图
现在引入感受野的特性,感受野区域的大小是5×5,隐藏层中有300个神经元,则所需的参数数量为5×5×300,即7500个。这个数量相比于完全连接网络所需的128×128×300个参数要小得多,能够达到削减参数的目的。
2.1.2 权值共享
卷积神经网络的权值共享特性[23]是指在对图片数据集进行卷积操作过程中,对于图像的不同位置,其可以同时使用同一个参数。如图2-2所示的权值共享的结构图,存在w1、w2、w3三个权重进行全连接,在共享与不共享权值对比,两种方式相差9个参数量。因此,在局部感受野下使用权值共享便可以大大降低参数量。对于上述2.1.1中所述情况,共享权值与不共享权值分别使用7500个参数和25个参数,可以大大的提高了训练速度。
图2-2 权值共享示意图
2.2 卷积神经网络的特性
卷积神经网络与传统的基于全连接原理手工神经网络不同,它可以通过局部感受野或者共享权值等方式来减少参数,以此改善反向传播算法[13]的运行速度。卷积神经网络的前几层通过获取图像的纹理以及边缘等特征,在经过后几层的主要是通过组合的方法获取更多图像的信息。卷积神经网络由不同作用的层级构成,经过一级一级的操作得到了系统需要的图像特征,每一个层级都包含许多神经单元,即神经元。卷积神经网络的模型图如2-3所示。
图2-3卷积神经网络结构图
首先,将原始的图像与多个滤波器进行卷积操作,产生特征图放在第一层。再对生成的特征映射图经过加,乘,加权操作之后,再经过激活函数的处理放到下一层第二层中。接下来对第二层的提取的特征送往下一层,重复操作,将结果放入第四层,在经过全连接的处理获得高层特征。在上面的结构图中,第一层和第三层是卷积层,二层与四层是采样层。卷积分析的目的在于从大量的数据中抽取出有用的信息,并将这些信息应用到每一个模型中。这样,我们就可以根据感受野的变化,准确地找出各个模型的关键点。采样层主要是为了特征的映射,将特征映射关系存入特征映射图上,同时神经元共享权值,这也是卷积神经网络的一个较大的优点,从而保证了特征的提取唯一不变性,保证了网络结构模型的稳定性。
卷积神经网络具有很好的性质,它可以保证对图像平移、放大与缩小和旋转的不变性。卷积神经网络的处理步骤大致相同,如图2-4所示的卷积神经网络模型图,将图像输入到模型中,首先经过卷积操作获取特征,激活函数增加非线性操作,在进行图像特征压缩,直到送往模型最后分类完成。
卷积层 |
激活函数 |
池化层 |
全连接层 |
输出层 |
输入层 |
图2-4卷积神经网络模型图
2.2.1 卷积层
卷积层是卷积神经网络的功能实现的基础。所谓的卷积操作[9]是指卷积核与图像中选择的区域矩阵相乘再相加。确定滑动窗口的方式自左向右,自上而下的运动,以及步长的大小设置,从而得到特征图谱。步长的选取也会直接影响到特征提取的精确度,若步长设计的过大会导致特征提取过程中漏掉很多有用的信息,导致后期识别不准的问题。如2-5所示不同步长的移动过程。
图2-5 不同步长移动过程
如图2-6所示的卷积神经网络中卷积操作的计算图,假设步长为1,卷积核大小为2×2,当前所在区域为图中圆圈所选择的区域,包含x1 、x2 、x4 、x6 四个参数,图中b1 、b2 、b3 、b4 为偏置量,若不考虑则偏置量为0。由于步长为1,那么下次移动的区域应该是x2 、x3 、x5 、x6 ,依次进行卷积操作,经过四次卷积操作后所得到一个2×2的特征图,包含p1 、p2 、p3 、p4 四个参数,则四个参数所对应的值如公式2-1,2-2,2-3,2-4所示。
p1=w1×x1+w2×x2+w3×x4+w4×x5+b1 (2-1)
p2=w1×x2+w2×x3+w3×x5+w4×x6+b2 (2-2)
p3=w1×x4+w2×x5+w3×x7+w4×x8+b3 (2-3)
p4=w1×x5+w2×x6+w3×x8+w4×x9+b4 (2-4)
图2-6 卷积操作示意图
卷积核代表一个神经网络的局部感受野的大小,实验表明,多个卷积核相对较小的卷积层和卷积核较大的卷积层作用效果相似。最佳卷积核的大小在5×5左右,那么我们就可以选择两个3×3的卷积核进行连续卷积的方法代替5×5所描述的效果。用这种非线性的方法,使得复杂的图像简单化,通过大幅度减少参数,卷积操作可以实现权重共享以及局部感知野的有效利用,从而提高效率。单层卷积学到的知识往往是局部的,倘若卷积层的层数在一定程度中越高,那么学到的特征就越全面。
2.2.2 池化层
池化层也叫子采样层[5],其主要目的是为了降维,从而减少特征参数的数量,同时保留关键信息。来自卷积层的神经元数量庞大,若在卷积层之后直接加入一个分类器,那么就会因为特征映射图的维度过高,导致过拟合现象。利用池化层可以有效地降低维度,故一般都要在卷积层之后加入池化层,有效地防止了过拟合问题。具体来说,就是将来自卷积层的特征映射图的区域进行重新划分,设置一个池化窗口,一般都是尽量选择不重复的区域来进行最大限度的降维。
在池化窗口所选择的区域中,通常采取两种方式进行降维,最大池化法和均值池化法。如图2-7所示的池化操作示意图,黄色区域像素最大值是6,蓝色区域像素最大值是8,红色区域像素最大值为3,绿色区域像素最大值为4,故最大池化法取得的结果是(6,8,3,4)。采用均值池化法求黄色区域为(1+1+4+6)/4=6,那么求得平均池化法的结果为(3,5,2,2)。
图2-7 池化操作示意图
2.2.3 全连接层
全连接层的主要功能是将卷积层和池化层的特征映射图进行分类,故又称分类层。全连接的工作过程像是一个黑盒子[8],中间过程用户不得而知。一般情况下,会在神经网络模型的末端加上合适层数的全连接层来改善参数量。全连接层通过前后层的所有节点进行连接,这也会造成参数量剧增,主要是计算量增大,训练速度也会降低。如2-8所示的全连接示意图,第一层有7个神经元,第二层有5个,经过全连接后会产生(7+1)×5=40个新的参数。即公式2-5所示,其中F 为全连接层的输出,x 为上一层的特征输出,w 为权重,b 为偏置量,f() 为激活函数。
F=fx×w+b (2-5)
图2-8 全连接操作示意图
2.3 激活函数
为了帮助神经元获得更加复杂的学习能力,激活函数[6]决定了一个神经元向下一个神经元所传输的内容。因此激活函数扮演着非常重要的角色,主要是对卷积操作或者池化操作后加一个非线性的操作,使神经网络获得对更加复杂数据的处理能力。若没有激活函数的非线性操作来叠加层数,处理数据能力也不会那么优秀。
激活函数被划分为两类,饱和和非饱和的激活函数[10]。饱和函数是指输入值趋近于正负无穷,输出值趋近于一个常数,非饱和激活函数恰恰相反,输出值不会趋近于一个常数,饱和需满足左极限饱和与右极限饱和,否则为不饱和。饱和函数包含Sigmoid函数等,非饱和函数包含ReLU、ELU、PReLU、RReLU等。
2.3.1 ReLU函数
ReLU函数[7]也是系统的一个关键函数,一般在进行网络结构设计时,会使用其来增加模型非线性,函数表达式如公式2-8所示。
ReLUx=max0,x (2-8)
可以看出,函数是非常简单,只需要判断是否大于0即可,故计算速度非常快。一个很重要的性质是,与Sigmoid等函数相比,ReLU函数的计算速度更加的快,并且有很好的收敛情况,在解决梯度下降消失问题上,有较好的处理结果,也是常用的算法,如下图2-10所示。
2.3.2 Softmax函数
Softmax函数[17]主要用于将一组实数转化为概率分布,从而将数据分类0。在k 种类别的分类操作过程中,Softmax函数可以将类别变为一个实向量,其维度为k ,每个分量都介于0和1之间,并且总和等于1。使预测结果更加准确。通常情况下,Softmax函数的公式如2-9所示,其中n 是输出层的节点数,x 是第i 个节点的原始输出。
Softmaxxi=exijnexj,i=1,2…,n (2-9)
在本系统中,采用Softmax算法进行非线性分类,将神经网络中传送过来的表情数据送到Softmax分类器中进行分类,输出每种表情的概率,保存最大值。首先,系统定义训练集x1,y1,x2,y3,…,(xn,yn) , xi 为输如的原始特征,一共分为其中表情,故yi∈{1,2,3,4,5,6,7} 。那么每个表情的概率为p ,假设Softmax函数的图像的参数为αi ,那么结合上述公式2-9,可以推导出概率的公式如公式2-10所示。
3.数据集介绍
使用fer2013数据集,包含35887张48×48像素的灰度人脸图像,按照比例关系,将其中的28709张图片放进训练集中,将其中的3589张图片放进测试集中,还剩下最后的3589张图片放进验证集中。
Emotion | Number |
Angry | 4953 |
Disgust | 547 |
Fear | 5121 |
Happy Sad Surprise Neutral | 8989 6077 4002 6198 |
4.表情识别
4.1 灰度处理
灰度处理[26]是一种图像处理方法,是把彩色图像中的颜色信息去掉,只保留亮度信息,从而得到灰度图像。其目的时为了统一灰度值,为后续的特征提取或者分类打好基础,同时能够消除因为图像质量对图像识别的影响,增加算法的鲁棒性。灰度处理一般都是通过计算每个像素点的红、绿、蓝三种颜色的R 、G 、B 分量来进行计算,一般的计算公式如4-1所示,权重已由多次实验取得。
Grey= 0.299 × R + 0.587 × G + 0.114 × B (4-1)
在OpenCV库中,也可以很方便的使用库函数来进行处理,只需要将所需要的图片gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)即可。
4.2面部特征点检测
本次系统使用Dlib进行特征点检测,使用它标注了68个特征点,包括眉毛、眼睛、鼻子、嘴唇和脸部轮廓的检测点,如图4-2人脸特征示意图所示。
Dlib预先设置好了一个训练好的人脸特征点检测器,检测器使用了一种集成回归树的算法,通过建立一个级联的残次回归树来使人脸逐步回归到一个真实的形状。可以通过使用训练好的模型shape_predictor_68_face_landmarks.dat文件导入来进行快速特征提取,将提取的特征点用蓝色标记,锁定人的面部位置用红色矩形标记
5.流程
6.系统结构图
本系统基于卷积神经网络设计,构建相应的卷积神经网络结构图。第一层输入的图片经过卷积操作,池化操作获得不同参数值得图像,可以看到图像参数的变化。大趋势就是图像逐渐缩小,但是获取得图像特征逐步增多,在每一个卷积操作后添加最大池化法来删除参数量,最后在2048和1024以及7个神经单元的全连接层相互计算扩大数据量
卷积神经网络参数表:
Layer | Output Shape | Param |
conv2d_1 | (48, 48, 32) | 64 |
conv2d_2 | (48, 48, 32) | 25632 |
max_pooling2d_1 | (24, 24, 32) | |
conv2d_3 max_pooling2d_2 conv2d_4 max_pooling2d_3 dense_1 dense_2 dense_3 | (24, 24, 32) (12, 12, 32) (12, 12, 64) (6, 6, 64) | 9248 51264 4720640 2098176 7175 |
混淆矩阵:
验证集:
7.关键代码
在所设计的系统中,主要分为三部分,第一部分是数据处理部分,第二部分是模型的训练部分,第三部分是人脸表情识别部分。在下面的流程图中也可以看出,数据经过数据预处理与划分数据集之后会将处理完成的数据集应用于模型的训练。在第二部分中将设计好的卷积神经网络模型,构建各个功能层之后,使用第一部分所处理好的数据获得学习能力,训练模型。这样便会得到一个训练好的模型,那么系统将会进行第三部分,使用训练好的模型,已保存在一个文件中,将训练的参数导入人脸表情识别部分。这样可以使人脸表情识别获得更好的拟合数据的能力,同时更好的进行分类与预测。在第三部分人脸表情识别中,系统根据自己的学习能力,调用电脑摄像头获取实时图片数据,这样能够实现实时交互。系统将获取的每一帧图片根据68个特征点,锁定面部,获得面部实时表情。将获取的表情特征与训练数据集中学习到的特征做对比,从而预测并分类每一帧获得的表情,同时能够做到预测此时所提取的特征应该是何种表情,做到预测,并获得分类结果。
7.1数据处理
import csv
import os
from PIL import Image
import numpy as np
data_path = os.getcwd() + "/data/"
csv_file = data_path + 'fer2013.csv'
train_csv = data_path + 'train.csv'
val_csv = data_path + 'val.csv'
test_csv = data_path + 'test.csv'
train_set = os.path.join(data_path, 'train')
val_set = os.path.join(data_path, 'val')
test_set = os.path.join(data_path, 'test')
with open(csv_file) as f:
csv_r = csv.reader(f)
header = next(csv_r)
print(header)
rows = [row for row in csv_r]
trn = [row[:-1] for row in rows if row[-1] == 'Training']
csv.writer(open(train_csv, 'w+'), lineterminator='\n').writerows([header[:-1]] + trn)
print(len(trn))
val = [row[:-1] for row in rows if row[-1] == 'PublicTest']
csv.writer(open(val_csv, 'w+'), lineterminator='\n').writerows([header[:-1]] + val)
print(len(val))
tst = [row[:-1] for row in rows if row[-1] == 'PrivateTest']
csv.writer(open(test_csv, 'w+'), lineterminator='\n').writerows([header[:-1]] + tst)
print(len(tst))
for save_path, csv_file in [(train_set, train_csv), (val_set, val_csv), (test_set, test_csv)]:
if not os.path.exists(save_path):
os.makedirs(save_path)
num = 1
with open(csv_file) as f:
csv_r = csv.reader(f)
header = next(csv_r)
for i, (label, pixel) in enumerate(csv_r):
# 0 - 6 文件夹分别label为:
# angry ,disgust ,fear ,happy ,sad ,surprise ,neutral
pixel = np.asarray([float(p) for p in pixel.split()]).reshape(48, 48)
sub_folder = os.path.join(save_path, label)
if not os.path.exists(sub_folder):
os.makedirs(sub_folder)
im = Image.fromarray(pixel).convert('L')
image_name = os.path.join(sub_folder, '{:05d}.jpg'.format(i))
print(image_name)
im.save(image_name)
7.2 训练数据集
from keras.layers import Dense, Dropout, Activation, Flatten, Conv2D, MaxPooling2D
from keras.models import Sequential
from keras.preprocessing.image import ImageDataGenerator
from keras.optimizers import SGD
batch_siz = 128
num_classes = 7
nb_epoch = 100
img_size = 48
data_path = './data'
model_path = './model'
class Model:
def __init__(self):
self.model = None
def build_model(self):
self.model = Sequential()
self.model.add(Conv2D(32, (1, 1), strides=1, padding='same', input_shape=(img_size, img_size, 1)))
self.model.add(Activation('relu'))
self.model.add(Conv2D(32, (5, 5), padding='same'))
self.model.add(Activation('relu'))
self.model.add(MaxPooling2D(pool_size=(2, 2)))
self.model.add(Conv2D(32, (3, 3), padding='same'))
self.model.add(Activation('relu'))
self.model.add(MaxPooling2D(pool_size=(2, 2)))
self.model.add(Conv2D(64, (5, 5), padding='same'))
self.model.add(Activation('relu'))
self.model.add(MaxPooling2D(pool_size=(2, 2)))
self.model.add(Flatten())
self.model.add(Dense(2048))
self.model.add(Activation('relu'))
self.model.add(Dropout(0.5))
self.model.add(Dense(1024))
self.model.add(Activation('relu'))
self.model.add(Dropout(0.5))
self.model.add(Dense(num_classes))
self.model.add(Activation('softmax'))
self.model.summary()
def train_model(self):
sgd = SGD(lr=0.01, decay=1e-6, momentum=0.9, nesterov=True)
self.model.compile(loss='categorical_crossentropy', optimizer=sgd, metrics=['accuracy'])
# 自动扩充训练样本
train_datagen = ImageDataGenerator(
rescale=1. / 255,
shear_range=0.2,
zoom_range=0.2,
horizontal_flip=True)
# 归一化验证集
val_datagen = ImageDataGenerator(
rescale=1. / 255)
eval_datagen = ImageDataGenerator(
rescale=1. / 255)
# 以文件分类名划分label
train_generator = train_datagen.flow_from_directory(
data_path + '/train',
target_size=(img_size, img_size),
color_mode='grayscale',
batch_size=batch_siz,
class_mode='categorical')
val_generator = val_datagen.flow_from_directory(
data_path + '/val',
target_size=(img_size, img_size),
color_mode='grayscale',
batch_size=batch_siz,
class_mode='categorical')
eval_generator = eval_datagen.flow_from_directory(
data_path + '/test',
target_size=(img_size, img_size),
color_mode='grayscale',
batch_size=batch_siz,
class_mode='categorical')
# early_stopping = EarlyStopping(monitor='loss', patience=3)
history_fit = self.model.fit_generator(
train_generator,
steps_per_epoch=800 / (batch_siz / 32), # 28709
nb_epoch=nb_epoch,
validation_data=val_generator,
validation_steps=2000,
# callbacks=[early_stopping]
)
# history_eval=self.model.evaluate_generator(
# eval_generator,
# steps=2000)
history_predict = self.model.predict_generator(
eval_generator,
steps=2000)
with open(model_path + '/model_fit_log', 'w') as f:
f.write(str(history_fit.history))
with open(model_path + '/model_predict_log', 'w') as f:
f.write(str(history_predict))
def save_model(self):
model_json = self.model.to_json()
with open(model_path + "/model_json.json", "w") as json_file:
json_file.write(model_json)
self.model.save_weights(model_path + '/model_weight.h5')
self.model.save(model_path + '/model.h5')
if __name__ == '__main__':
model = Model()
model.build_model()
print('model built')
model.train_model()
print('model trained')
model.save_model()
print('model saved')
7.3表情识别部分
import cv2
import numpy as np
from keras.models import model_from_json
model_path = './model_train/'
img_size = 48
emotion_labels = ['angry', 'disgust', 'fear', 'happy', 'sad', 'surprise', 'neutral']
num_class = len(emotion_labels)
# 从json中加载模型
json_file = open(model_path + 'model_json.json')
loaded_model_json = json_file.read()
json_file.close()
model = model_from_json(loaded_model_json)
# 加载模型权重
model.load_weights(model_path + 'model_weight.h5')
# 加载emotion
emotion_images = {}
for emoji in emotion_labels:
emotion_images[emoji] = cv2.imread("./emoji/" + emoji + ".png", -1)
def face2emoji(face, emotion_index, position):
x, y, w, h = position
emotion_image = cv2.resize(emotion_images[emotion_index], (w, h))
overlay_img = emotion_image[:, :, :3]/255.0
overlay_bg = emotion_image[:, :, 3:]/255.0
background = (1.0 - overlay_bg)
face_part = (face[y:y + h, x:x + w]/255.0) * background
overlay_part = overlay_img * overlay_bg
face[y:y + h, x:x + w] = cv2.addWeighted(face_part, 255.0, overlay_part, 255.0, 0.0)
return face
# 创建VideoCapture对象
capture = cv2.VideoCapture(0)
# 使用opencv的人脸分类器
cascade = cv2.CascadeClassifier(model_path + 'haarcascade_frontalface_alt.xml')
while True:
ret, frame = capture.read()
# 灰度化处理
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# 呈现用emoji替代后的画面
emoji_show = frame.copy()
# 识别人脸位置
faceLands = cascade.detectMultiScale(gray, scaleFactor=1.1,
minNeighbors=1, minSize=(120, 120))
if len(faceLands) > 0:
for faceLand in faceLands:
x, y, w, h = faceLand
images = []
result = np.array([0.0] * num_class)
# 裁剪出脸部图像
image = cv2.resize(gray[y:y + h, x:x + w], (img_size, img_size))
image = image / 255.0
image = image.reshape(1, img_size, img_size, 1)
# 调用模型预测情绪
predict_lists = model.predict_proba(image, batch_size=32, verbose=1)
# print(predict_lists)
result += np.array([predict for predict_list in predict_lists
for predict in predict_list])
# print(result)
emotion = emotion_labels[int(np.argmax(result))]
print("Emotion:", emotion)
emoji = face2emoji(emoji_show, emotion, (x, y, w, h))
cv2.imshow("Emotion", emoji)
# 框出脸部并且写上标签
cv2.rectangle(frame, (x - 20, y - 20), (x + w + 20, y + h + 20),
(0, 255, 255), thickness=10)
cv2.putText(frame, '%s' % emotion, (x, y - 50),
cv2.FONT_HERSHEY_DUPLEX, 2, (255, 255, 255), 2, 30)
cv2.imshow('Face', frame)
if cv2.waitKey(60) == ord('q'):
break
# 释放摄像头并销毁所有窗口
capture.release()
cv2.destroyAllWindows()
7.4运行结果
8. 源码获取方式
公众号‘二六编程’ 回复’毕设表情识别‘
或者在下载中心下载