现在的年轻人聊起天来都是一场场你来我往的表情包大战。稍有不慎,就会立马败下阵来。
你可能拥有数G个表情包存图,但总是苦于表情包太多太乱,每次挑选都是旷日持久。等好不容易终于选中一张满意的表情包,却发现对方早已切到下一回合。
要是有个功能可以把表情包一键分类就好了。这可能,会随着FER(面部表情识别技术)的发展成为现实。
表情识别vs人脸识别
面部表情识别技术源于1971年心理学家Ekman和Friesen的一项研究,他们提出人类主要有六种基本情感,每种情感以唯一的表情来反映当时的心理活动,这六种情感分别是愤怒(anger)、高兴(happiness)、悲伤 (sadness)、惊讶(surprise)、厌恶(disgust)和恐惧(fear)。
尽管人类的情感维度和表情复杂度远不是数字6可以量化的,但总体而言,这6种也差不多够描述了。
事实上,表情识别技术差不多可以算是在人脸识别技术的基础上发展而来的。因此,表情识别也需要依赖于人脸的特征点检测。
所谓的特征点,就是预先定义的一组脸部或五官轮廓的点。在人脸特征点检测中,通常我们比较关注的特征点一般位于眉毛、眼睛、嘴巴的位置,而鼻子在各种表情中的位置变化较不明显,因此很多研究中都忽略对鼻子位置的特征点进行检测。
图片来源:shutterlock
但两者还是有区别的,表情识别从技术上来说还是比人脸识别要更加复杂。人脸识别是一个静态识别问题,最经典的人脸识别案例就是输入两张人脸照片,然后让机器去判定两张脸是不是属于同一个人。而表情识别是给定一个人脸的连续动作帧,是一个时间段内表情变化的动态判定问题。
人脸识别实际上是个去表情的过程,不管作出什么表情,不管是哭还是笑,都要想办法去识别为同一个人。然而表情识别却是放大表情的过程,对于同一个人,通过观察表情变化来推断其情绪的起伏。
图片来源:shutterlock
相比于人脸识别,表情识别的动态不确定性成分更大,不仅需要做到精确,而且需要做到实时。如果愤怒被检测为悲伤,或者当前检测的表情已经是三秒前的表情状态了,那肯定都是没有办法满足实际应用需求的。
说到应用,人脸识别最常见的应用场景可能要数“身份验证”,而表情识别除了我们上面提到的能进行表情分类外,还可以广泛应用于多个领域。
比如用于电影制作,往后不再需要设计动画,只需要将真人的表情动作直接映射即可。用于产品投放前的反响测试,则可以通过分析被试者的表情来预测用户体验。用于公安的审问环节,可以通过观察受审者的细微表情变化来帮助判断其证词是否属实。当然,在未来,如果机器能够通过识别我们的表情来为我们提供个性化的服务,就能实现更好的人机交互。
图中右下角显示原始表情,根据表情来进行四川变脸。
当表情识别遇上深度学习
目前,深度学习已强势渗透进各个学科各个领域,大数据已成为这个时代最标志的特征之一。要用深度学习来做表情识别,第一步自然是选用一个数据丰富的表情库,目前比较常用的表情库主要有FER2013人脸数据集、日本ATR技术研究所建立的JAFFE日本女性表情数据库以及美国CMU机器人研究所和心理学系共同建立的CKACFEID人脸表情数据库。
JAFFE日本女性表情数据库
总体来说,基于深度学习的表情识别一般分为以下几个步骤:
1)图像获取:通过摄像头等来获得图像输入。
2)图像预处理:对图像中人脸识别子区域进行检测,从而分割出人脸并去掉背景和无关区域。然后,进一步对图像中的人脸进行标定。目前IntraFace是最常用的人脸标定方法,通过使用级联人脸关键特征点定位(SDM),可准确预测49个关键点。为了保证数据足够充分,可以采用旋转、翻转、缩放等图像增强操作,甚至可以利用现在大火的GAN来辅助生成更多的训练数据。
至此,准备工作可还没完事。由于图像中不同光照强度和头部姿态对表情识别的效果影响巨大,所以在开始正式工作前,还需要对光照和姿态做归一化处理。不必担心,站在巨人肩膀上的我们有很多现成的归一化方法,比如使用INFace工具箱对光照进行归一化,以及使用FF-GAN,TP-GAN,DR-GAN这些基于GAN的深度模型来矫正面部姿态从而生成正面人脸。
基于深度学习的面部表情识别系统
3)特征学习深度网络
传统表情识别技术和深度表情识别技术最大的区别就在于特征学习的方式不同。传统表情识别技术的特征提取方法主要有Gabor小波变换、局部二值模式(LBP)、局部线性嵌入(LLE)、梯度方向直方图(HOG)等。
近些年来,有越来越多的深度网络被用于FER,其中包括深度置信网络DBN、递归神经网络RNN以及卷积神经网络CNN等。
以CNN为例,面部表情识别的CNN框架如下图所示,与经典的卷积神经网络无甚差别,主要也是包含输入层、卷积层、全连接层和输出层。
面部表情识别CNN架构(改编自 埃因霍芬理工大学PARsE结构图)
其中,通过卷积操作来创建特征映射,将卷积核挨个与图像进行卷积,从而创建一组要素图,并在其后通过池化(pooling)操作来降维。
CNN表情识别网络中使用卷积和最大池化操作
通常,在致密层(又称为全连接层)的末端可以加上损失层,目的是修正正反向的传播误差,此后网络输出的直接就是每个输入样本的表情分类预测概率。当提供的数据越多,网络可以逐步进行微调,直至损失最小。看起来好像我们设置的网络节点越多,模型的表达能力就会越好,但这也同时会导致训练数据容易陷入过拟合状态。一般使用Dropout来解决过拟合问题,该方法不仅可以保证模型在训练期间的敏感性也可以保持框架的必要复杂度。
神经网络训练:前向传播(左)和后向传播(右)
输出层常用Sigmoid或者Softmax作为激活函数,通过激活函数将神经元的输入映射到输出端。实际上,激活函数并不是要激活什么,而是需要把激活的神经元特征保留并映射出来,其他的数据属于冗余就被剔掉了。在输出层我们就能直接得到每个表情所属的情绪类别及相应概率。
尽管这看起来并不难,但表情识别除面临光照变化、非正面头部姿态等带来的挑战之外,对低强度的表情识别也较为困难;并且,理想的表情数据库应该包含各个种族、各个年龄阶段的各种表情,除了数据获取的难度外,对大量复杂自然场景下的人脸进行精准标注也是一大难点。但随着数据的丰富和算法的改进,这些都将不会是什么大问题。
表情识别太高端?不,你也可以!
下面,我们将通过实例教你如何实现表情识别。
首先,请确保你的电脑上已经安装和配置好Keras、Flask和OpenCV。
然后,选择一个合适的表情库,在这个实例中,我们选择FER2013表情库
(链接: https://www.kaggle.com/c/challenges-in-representation-learning-facial-expression-recognition-challenge )。
这个数据集中大约包含36000张大小为48*48像素的灰度图像,并且都已自动调整过,基本每张人脸在图像中的位置和所占比例都差不多。除了心理学家Ekman和Friesen提出的6种情绪外,这里加入了一种新的情绪—中性(neutral)。每种情绪对应一个数字类别,0=Angry(愤怒), 1=Disgust(厌恶), 2=Fear(恐惧), 3=Happy(高兴), 4=Sad(悲伤), 5=Surprise(惊讶), 6=Neutral(中性)。
现在,正式工作开始。
原始数据由图像每个像素灰度值的数组数据组成,我们将数据转换为原始图像并将其拆分放进多个子文件中。其中,80%的数据放进训练集中,剩余20%的数据用于测试。
images/
train/
angry/
disgust/
fear/
happy/
neutral/
sad/
surprise/
validation/
angry/
disgust/
fear/
happy/
neutral/
sad/
surprise/
快速数据可视化
首先让我们看看我们的图像长什么样子:
# display some images for every different expression
import numpy as np
import seaborn as sns
from keras.preprocessing.image import load_img, img_to_array
import matplotlib.pyplot as plt
import os
# size of the image: 48*48 pixels
pic_size = 48
# input path for the images
base_path = "../input/images/images/"
plt.figure(0, figsize=(12,20))
cpt = 0
for expression in os.listdir(base_path "train/"):
for i in range(1,6):
cpt = cpt 1
plt.subplot(7,5,cpt)
img = load_img(base_path "train/" expression "/" os.listdir(base_path "train/" expression)[i], target_size=(pic_size, pic_size))
plt.imshow(img, cmap="gray")
plt.tight_layout()
plt.show()
部分训练样本
你能猜出这些图像对应的表情么?
这对人类来说可能非常简单,但对于机器来说还是相当具有挑战性的。毕竟上面这些图像分辨率不高,脸也不在同一位置,一些图上还有文字,甚至很多图都有手对面部进行了遮挡。
但与此同时,越复杂多样的图片训练出的模型泛化能力就会越好。
# count number of train images for each expression
for expression in os.listdir(base_path "train"):
print(str(len(os.listdir(base_path "train/" expression))) " " expression " images")
数一下每种表情的数量,我们得到:
4103 fear images
436 disgust images
4982 neutral images
7164 happy images
3993 angry images
3205 surprise images
4938 sad images
不难看出,除了“厌恶”的表情,其他每种表情的数量基本是平衡的。
设置数据生成器
Keras的ImageDataGenerator类可以从路径中提供批量数据:
from keras.preprocessing.image import ImageDataGenerator
# number of images to feed into the NN for every batch
batch_size = 128
datagen_train = ImageDataGenerator()
datagen_validation = ImageDataGenerator()
train_generator = datagen_train.flow_from_directory(base_path "train",
target_size=(pic_size,pic_size),
color_mode="grayscale",
batch_size=batch_size,
class_mode='categorical',
shuffle=True)
validation_generator = datagen_validation.flow_from_directory(base_path "validation",
target_size=(pic_size,pic_size),
color_mode="grayscale",
batch_size=batch_size,
class_mode='categorical',
shuffle=False)
Found 28821 images belonging to 7 classes.
Found 7066 images belonging to 7 classes.
训练集中共有28821张表情图片;验证集中共有7066张表情图片。值得一提的是,此处还可以在获取图像时执行数据增强(比如随机旋转和尺度缩放等)。上文代码中函数flow_from_directory()用于指定生成器以何种方式导入图像(路径,图像大小,颜色等)。
设置卷积神经网络(CNN)
先来定义我们的CNN网络架构:
from keras.layers import Dense, Input, Dropout, GlobalAveragePooling2D, Flatten, Conv2D, BatchNormalization, Activation, MaxPooling2D
from keras.models import Model, Sequential
from keras.optimizers import Adam
# number of possible label values
nb_classes = 7
# Initialising the CNN
model = Sequential()
# 1 - Convolution
model.add(Conv2D(64,(3,3), padding='same', input_shape=(48, 48,1)))
model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
# 2nd Convolution layer
model.add(Conv2D(128,(5,5), padding='same'))
model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
# 3rd Convolution layer
model.add(Conv2D(512,(3,3), padding='same'))
model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
# 4th Convolution layer
model.add(Conv2D(512,(3,3), padding='same'))
model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
# Flattening
model.add(Flatten())
# Fully connected layer 1st layer
model.add(Dense(256))
model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(Dropout(0.25))
# Fully connected layer 2nd layer
model.add(Dense(512))
model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(Dropout(0.25))
model.add(Dense(nb_classes, activation='softmax'))
opt = Adam(lr=0.0001)
model.compile(optimizer=opt, loss='categorical_crossentropy', metrics=['accuracy'])
我们的CNN中包含4个卷积层和2个全连接层,卷积层负责从图像中提取相关特征,全连接层用于对图像进行分类。我们使用ReLU函数在CNN中引入非线性,并使用BN(批量标准化)来提高网络性能。此外,通过Dropout来减少过拟合。我们选用交叉熵作为损失函数,激活函数选用常用于多分类任务的Softmax函数。
现在我们已经定义了CNN,让我们 正式 开始训练吧!
训练模型
# number of epochs to train the NN
epochs = 50
from keras.callbacks import ModelCheckpoint
checkpoint = ModelCheckpoint("model_weights.h5", monitor='val_acc', verbose=1, save_best_only=True, mode='max')
callbacks_list = [checkpoint]
history = model.fit_generator(generator=train_generator,
steps_per_epoch=train_generator.n//train_generator.batch_size,
epochs=epochs,
validation_data = validation_generator,
validation_steps = validation_generator.n//validation_generator.batch_size,
callbacks=callbacks_list)
Epoch 1/50
225/225 [==============================] - 36s 161ms/step - loss: 2.0174 - acc: 0.2333 - val_loss: 1.7391 - val_acc: 0.2966
Epoch 00001: val_acc improved from -inf to 0.29659, saving model to model_weights.h5
Epoch 2/50
225/225 [==============================] - 31s 138ms/step - loss: 1.8401 - acc: 0.2873 - val_loss: 1.7091 - val_acc: 0.3311
Epoch 00002: val_acc improved from 0.29659 to 0.33108, saving model to model_weights.h5
...
Epoch 50/50
225/225 [==============================] - 30s 132ms/step - loss: 0.6723 - acc: 0.7499 - val_loss: 1.1159 - val_acc: 0.6384
Epoch 00050: val_acc did not improve from 0.65221
从迭代输出中不难看出,我们的模型能够达到的最高验证准确度为65%,这可能并不算高,但对于多分类任务来说,已经相当不错了。
我们将CNN的结构保存到文件中:
# serialize model structure to JSON
model_json = model.to_json()
with open("model.json", "w") as json_file:
json_file.write(model_json)
分析结果
我们通过训练过程中保存的数据来绘制训练集和验证集的损失和准确度演变曲线:
# plot the evolution of Loss and Acuracy on the train and validation sets
import matplotlib.pyplot as plt
plt.figure(figsize=(20,10))
plt.subplot(1, 2, 1)
plt.suptitle('Optimizer : Adam', fontsize=10)
plt.ylabel('Loss', fontsize=16)
plt.plot(history.history['loss'], label='Training Loss')
plt.plot(history.history['val_loss'], label='Validation Loss')
plt.legend(loc='upper right')
plt.subplot(1, 2, 2)
plt.ylabel('Accuracy', fontsize=16)
plt.plot(history.history['acc'], label='Training Accuracy')
plt.plot(history.history['val_acc'], label='Validation Accuracy')
plt.legend(loc='lower right')
plt.show()
随着训练的迭代次数增加,损失和准确度的演变
除了损失和准确度演变曲线,我们还可以通过绘制混淆矩阵来帮助我们了解我们的模型是如何对图像进行分类的:
# show the confusion matrix of our predictions
# compute predictions
predictions = model.predict_generator(generator=validation_generator)
y_pred = [np.argmax(probas) for probas in predictions]
y_test = validation_generator.classes
class_names = validation_generator.class_indices.keys()
from sklearn.metrics import confusion_matrix
import itertools
def plot_confusion_matrix(cm, classes, title='Confusion matrix', cmap=plt.cm.Blues):
cm = cm.astype('float') / cm.sum(axis=1)[:, np.newaxis]
plt.figure(figsize=(10,10))
plt.imshow(cm, interpolation='nearest', cmap=cmap)
plt.title(title)
plt.colorbar()
tick_marks = np.arange(len(classes))
plt.xticks(tick_marks, classes, rotation=45)
plt.yticks(tick_marks, classes)
fmt = '.2f'
thresh = cm.max() / 2.
for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])):
plt.text(j, i, format(cm[i, j], fmt),
horizontalalignment="center",
color="white" if cm[i, j] > thresh else "black")
plt.ylabel('True label')
plt.xlabel('Predicted label')
plt.tight_layout()
# compute confusion matrix
cnf_matrix = confusion_matrix(y_test, y_pred)
np.set_printoptions(precision=2)
# plot normalized confusion matrix
plt.figure()
plot_confusion_matrix(cnf_matrix, classes=class_names, title='Normalized confusion matrix')
plt.show()
从混淆矩阵的结果来看,我们的模型在预测“高兴”和“惊讶”的表情时表现非常优秀,但是在预测“恐惧”的表情时则比较头疼,因为容易和“悲伤”的表情相混淆。
不管怎样,随着研究进一步深入以及更广泛的资源获取,模型性能肯定能得到优化和改善。下面,是时候在真实场景下检测我们的模型性能了。我们将使用Flask,以便通过网络摄像头的视频输入进行表情的实时检测。
实时预测
首先我们先创建一个类,它将为我们提供先前训练模型的预测:
from keras.models import model_from_json
import numpy as np
class FacialExpressionModel(object):
EMOTIONS_LIST = ["Angry", "Disgust",
"Fear", "Happy",
"Neutral", "Sad",
"Surprise"]
def __init__(self, model_json_file, model_weights_file):
# load model from JSON file
with open(model_json_file, "r") as json_file:
loaded_model_json = json_file.read()
self.loaded_model = model_from_json(loaded_model_json)
# load weights into the new model
self.loaded_model.load_weights(model_weights_file)
self.loaded_model._make_predict_function()
def predict_emotion(self, img):
self.preds = self.loaded_model.predict(img)
return FacialExpressionModel.EMOTIONS_LIST[np.argmax(self.preds)]
接下来我们将实现一个能够执行以下操作的类:
1) 从网络摄像头获取图像流
2) 使用OpenCV检测并框出人脸
3) 从我们的CNN网络获取预测结果并将预测标签添加到网络摄像头的图像流中
4) 返回处理后的图像流
import cv2
from model import FacialExpressionModel
import numpy as np
facec = cv2.CascadeClassifier('haarcascade_frontalface_default.xml')
model = FacialExpressionModel("model.json", "model_weights.h5")
font = cv2.FONT_HERSHEY_SIMPLEX
class VideoCamera(object):
def __init__(self):
self.video = cv2.VideoCapture(0)
def __del__(self):
self.video.release()
# returns camera frames along with bounding boxes and predictions
def get_frame(self):
_, fr = self.video.read()
gray_fr = cv2.cvtColor(fr, cv2.COLOR_BGR2GRAY)
faces = facec.detectMultiScale(gray_fr, 1.3, 5)
for (x, y, w, h) in faces:
fc = gray_fr[y:y h, x:x w]
roi = cv2.resize(fc, (48, 48))
pred = model.predict_emotion(roi[np.newaxis, :, :, np.newaxis])
cv2.putText(fr, pred, (x, y), font, 1, (255, 255, 0), 2)
cv2.rectangle(fr,(x,y),(x w,y h),(255,0,0),2)
_, jpeg = cv2.imencode('.jpg', fr)
return jpeg.tobytes()
你以为这就完了?
还没,我们将创建一个Flask应用程序,将我们的表情预测结果呈现到网页中。
from flask import Flask, render_template, Response
from camera import VideoCamera
app = Flask(__name__)
@app.route('/')
def index():
return render_template('index.html')
def gen(camera):
while True:
frame = camera.get_frame()
yield (b'--framern'
b'Content-Type: image/jpegrnrn' frame b'rnrn')
@app.route('/video_feed')
def video_feed():
return Response(gen(VideoCamera()),
mimetype='multipart/x-mixed-replace; boundary=frame')
if __name__ == '__main__':
app.run(host='0.0.0.0', debug=True)
激动人心的时刻到了,下面让我们一起看看最终的检测效果吧!
你可以在这里找到完整的代码: https://github.com/jonathanoheix/Real-Time-Face-Expression-Recognition
学会它,你就又习得一项实(shua)用(shuai)新技能。下次群聊发啥表情包,直接甩个表情识别网页程序过去,分分钟实力carry秒杀全场。你就是人群中最靓的仔!