首页 > 分享 > 基于深度学习的人脸情绪识别检测系统(VGG、CNN、ResNet)

基于深度学习的人脸情绪识别检测系统(VGG、CNN、ResNet)

第一部分:情绪识别模型训练部分,算法三种(CNN、VGG、ResNet)

1、基本表情介绍

人类的面部表情至少有21种,除了常见的高兴、吃惊、悲伤、愤怒、厌恶和恐惧6种,还有惊喜(高兴+吃惊)、悲愤(悲伤+愤怒)等15种可被区分的复合表情。详细介绍可以阅读下面博主的系列文章:
https://link.zhihu.com/target=https%3A//mp.weixin.qq.com/s%3F__biz%3DMzA3NDIyMjM1NA%3D%3D%26mid%3D2649039958%26idx%3D1%26sn%3Dbdbbb3690b8b23e107c56e14b72bf589%26chksm%3D87129a2bb065133d971793c1ca14b053737b593a5702c8375c5da5d328f0d422922ea7531b9b%26token%3D1054133372%26lang%3Dzh_CN%23rd
在这里插入图片描述
在这里插入图片描述

2、数据采集和训练

人脸情绪识别的训练数据有Kaggle和自行采集本地数据2种方式。
在这里插入图片描述
Fer2013人脸表情数据集由35886张人脸表情图片组成,其中,测试图(Training)28708张,公共验证图(PublicTest)和私有验证图(PrivateTest)各3589张,每张图片是由大小固定为48×48的灰度图像组成,共有7种表情,分别对应于数字标签0-6,具体表情对应的标签和中英文如下:
0 anger 生气;
1 disgust 厌恶;
2 fear 恐惧;
3 happy 开心;
4 sad 伤心;
5 surprised 惊讶;
6 normal 中性。
(数据集下载地址)
在这里插入图片描述

FER2013数据集

FER2013数据集由28709张训练图,3589张公开测试图和3589张私有测试图组成。每一张图都是像素为48*48的灰度图。FER2013数据库中一共有7中表情:愤怒,厌恶,恐惧,开心,难过,惊讶和中性。分别对应于数字标签0-6,具体表情对应的标签和中英文如下:0 anger 生气; 1 disgust 厌恶; 2 fear 恐惧; 3 happy 开心; 4 sad 伤心;5 surprised 惊讶; 6 normal 中性。 1

该数据库是2013年Kaggle比赛的数据,由于这个数据库大多是从网络爬虫下载的,存在一定的误差性。这个数据库的人为准确率是65% 士 5%。Kaggle的官网,数据、科学、机器学习比赛等。

3、数据清洗

在下载原文件中,emotion和pixels(像素)人脸像素数据是整合在一起的。为了方便操作,决定利用pandas库进行数据分离,即将所有emotion数据读出后,写入新创建的文件emotion.csv中去;将所有的像素数据读出后,写入新创建的文件pixels.csv文件中去。
pandas教程

关于.CSV文件,大小为28710行X2305列;

总计28710行中,其中第一行为描述信息即“emotion”和“pixels”两个单词,其余每行内含有一个样本信息,即共有28709个样本;

在2305列中,其中第一列为该样本对应的emotion,取值范围为0-6。其余2304列为包含着每个样本大小为48X48人脸图片的像素值(2304=48X48),每个像素值取值范围在0到255之间;

像素值列表

实现代码如下:data_separation.py 1

# 将emotion和pixels像素数据分离 import pandas as pd # 注意train.csv是在你电脑本地的相对或绝对路劲地址 path = 'dataset/train.csv' # 读取数据 df = pd.read_csv(path) # 提取emotion数据 df_y = df[['emotion']] # 提取pixels数据 df_x = df[['pixels']] # 将emotion写入emotion.csv df_y.to_csv('dataset/emotion.csv', index=False, header=False) # 将pixels数据写入pixels.csv df_x.to_csv('dataset/pixels.csv', index=False, header=False)

1234567891011121314151617

此时,在dataset的文件夹下,就会生成两个新文件emotion.csv以及pixels.csv。在执行代码前,注意修改train.csv为你电脑上文件所在的相对或绝对路径。

4、将像素值数据还原为图片

给定的数据集是.csv格式的,考虑到图片分类问题的常规做法,决定先将其全部可视化,还原为图片文件后再进行图像数据处理。

在python环境下,将csv中的像素数据还原为图片并保存下来,有很多图片数据处理库都能实现类似的功能,如Pillow,opencv等。这里我采用的是用opencv来实现这一功能。

将数据分离后,人脸像素数据全部存储在pixels.csv文件中,其中每行数据就是一张人脸。按行读取数据,利用opencv将每行的2304个数据恢复为一张48X48的人脸图片,并保存为jpg格式。在保存这些图片时,将第一行数据恢复出的人脸命名为0.jpg,第二行的人脸命名为1.jpg…,以方便与label[0]、label[1]…一一对应。所以建立image-emotion映射关系表还是相当有必要的。

4.实现代码如下

import cv2 import numpy as np # 指定存放图片的路径path = 'face_images'# 读取像素数据data = np.loadtxt('dataset/pixels.csv') # 按行取数据for i in range(data.shape[0]): face_array = data[i, :].reshape((48, 48)) # reshape cv2.imwrite(path + '//' + '{}.jpg'.format(i), face_array) # 写图片 123456

涉及到大量数据的读取和大批图片的写入,因此占用的内存资源较多,且执行时间较长(视机器性能而定,一般要几分钟到十几分钟不等)。代码执行完毕,我们来到指定的图片存储路径,就能发现里面全部是写好的人脸图片。
人脸图像数据
这里总共写入有28709张人脸图片数据

5、创建映射表

创建image图片名和对应emotion表情数据集的映射关系表。

我们需要划分一下训练集和验证集。在项目中,共有28709张图片,取前24000张图片作为训练集,其他图片作为验证集。在工程文件夹face_images下新建文件夹train_set和verify_set,将0.jpg到23999.jpg放进文件夹train_set,将其他图片放进文件夹verify_set。在继承torch.utils.data.Dataset类定制自己的数据集时,由于在数据加载过程中需要同时加载出一个样本的数据及其对应的emotion,因此最好能建立一个image的图片名和对应emotion表情数据的关系映射表,其中记录着image的图片名和其emotion表情数据的映射关系。实现源码:image_emotion_mapping.py

def image_emotion_mapping(path): # 读取emotion文件 df_emotion = pd.read_csv('dataset/emotion.csv', header = None) # 查看该文件夹下所有文件 files_dir = os.listdir(path) # 用于存放图片名 path_list = [] # 用于存放图片对应的emotion emotion_list = [] # 遍历该文件夹下的所有文件 for file_dir in files_dir: # 如果某文件是图片,则将其文件名以及对应的emotion取出,分别放入path_list和emotion_list这两个列表中 if os.path.splitext(file_dir)[1] == ".jpg": path_list.append(file_dir) index = int(os.path.splitext(file_dir)[0]) emotion_list.append(df_emotion.iat[index, 0]) # 将两个列表写进image_emotion.csv文件 path_s = pd.Series(path_list) emotion_s = pd.Series(emotion_list) df = pd.DataFrame() df['path'] = path_s df['emotion'] = emotion_s df.to_csv(path+'image_emotion.csv', index=False, header=False) def main(): # 指定文件夹路径 train_set_path = 'face_images/train_set' verify_set_path = 'face_images/verify_set' image_emotion_mapping(train_set_path) image_emotion_mapping(verify_set_path)

1234567891011121314151617181920212223242526272829303132

代码执行完毕后,会在train_set和verify_set文件夹下各生成一个名为image-emotion.csv的关系映射表。
映射表序号排列
映射表

6、加载数据集

现在我们有了图片,但怎么才能把图片读取出来送给模型呢?一般在平常的时候,我们第一个想到的是将所有需要的数据聚成一堆一堆然后通过构建list去读取我们的数据:

假如我们编写了上述的图像加载数据集代码,在训练中我们就可以依靠get_training_data()这个函数来得到batch_size个数据,从而进行训练,乍看下去没什么问题,但是一旦我们的数据量超过1000:
将所有的图像数据直接加载到numpy数据中会占用大量的内存
由于需要对数据进行导入,每次训练的时候在数据读取阶段会占用大量的时间
只使用了单线程去读取,读取效率比较低下
拓展性很差,如果需要对数据进行一些预处理,只能采取一些不是特别优雅的做法
如果用opencv将所有图片读取出来,最简单粗暴的方法就是直接以numpy中array的数据格式直接送给模型。如果这样做的话,会一次性把所有图片全部读入内存,占用大量的内存空间,且只能使用单线程,效率不高,也不方便后续操作。
既然问题这么多,到底说回来,我们应该如何正确地加载数据集呢?
其实在pytorch中,有一个类(torch.utils.data.Dataset)是专门用来加载数据的,我们可以通过继承这个类来定制自己的数据集和加载方法。
Dataset类是Pytorch中图像数据集中最为重要的一个类,也是Pytorch中所有数据集加载类中应该继承的父类。其中父类中的两个私有成员函数必须被重载,否则将会触发错误提示:

def getitem(self, index): def len(self): 12

其中__len__应该返回数据集的大小,而__getitem__应该编写支持数据集索引的函数,例如通过dataset[i]可以得到数据集中的第i+1个数据。

class Dataset(object): """An abstract class representing a Dataset. All other datasets should subclass it. All subclasses should override ``__len__``, that provides the size of the dataset, and ``__getitem__``, supporting integer indexing in range from 0 to len(self) exclusive. """ #这个函数就是根据索引,迭代的读取路径和标签。因此我们需要有一个路径和标签的 ‘容器’供我们读 def __getitem__(self, index):raise NotImplementedError #返回数据的长度 def __len__(self):raise NotImplementedError def __add__(self, other):return ConcatDataset([self, other]) 1234567891011121314

我们通过继承Dataset类来创建我们自己的数据加载类,命名为FaceDataset,完整代码如下请看代码:Dataset.py

模型训练

这里采用的是基于 CNN 的优化模型,这个模型是源于github一个做表情识别的开源项目,可惜即使借用了这个项目的模型结构,但却没能达到源项目中的精度(acc在74%)。下图为该开源项目中公布的两个模型结构,这里我采用的是 Model B ,且只采用了其中的卷积-全连接部分。CNN训练模型代码开源
这里:算法参考的论文原稿我已经下载文原版在参考文献文件夹下有。需要的(V我:ZQZ2551931023)
论文PDF
原文网址
对比如下
在这里插入图片描述
CNN模型

CNN模型和算法原理实现

下图我们可以看出,在 Model B 的卷积部分,输入图片 shape 为 48X48X1,经过一个3X3X64卷积核的卷积操作,再进行一次 2X 2的池化,得到一个 24X24X64 的 feature map 1(以上卷积和池化操作的步长均为1,每次卷积前的padding为1,下同)。将 feature map 1经过一个 3X3X128 卷积核的卷积操作,再进行一次2X2的池化,得到一个 12X12X128 的 feature map 2。将feature map 2经过一个 3X3X256 卷积核的卷积操作,再进行一次 2X2 的池化,得到一个 6X6X256 的feature map 3。卷积完毕,数据即将进入全连接层。进入全连接层之前,要进行数据扁平化,将feature map 3拉一个成长度为 6X6X256=9216 的一维 tensor。随后数据经过 dropout 后被送进一层含有4096个神经元的隐层,再次经过 dropout 后被送进一层含有 1024 个神经元的隐层,之后经过一层含 256 个神经元的隐层,最终经过含有7个神经元的输出层。一般再输出层后都会加上 softmax 层,取概率最高的类别为分类结果。
CNN模型
通过数据的前向传播和误差的反向传播来训练模型了。在此之前,还需要指定优化器(即学习率更新的方式)、损失函数以及训练轮数、学习率等超参数。在本项目中,采用的优化器是SGD,即随机梯度下降,其中参数weight_decay为正则项系数;损失函数采用的是交叉熵;可以考虑使用学习率衰减。

实现代码:model_CNN.py

class FaceCNN(nn.Module): # 初始化网络结构 def __init__(self): super(FaceCNN, self).__init__() # 第一次卷积、池化 self.conv1 = nn.Sequential( # 输入通道数in_channels,输出通道数(即卷积核的通道数)out_channels,卷积核大小kernel_size,步长stride,对称填0行列数padding # input:(bitch_size, 1, 48, 48), output:(bitch_size, 64, 48, 48), (48-3+2*1)/1+1 = 48 nn.Conv2d(in_channels=1, out_channels=64, kernel_size=3, stride=1, padding=1), # 卷积层 nn.BatchNorm2d(num_features=64), # 归一化 nn.RReLU(inplace=True), # 激活函数 # output(bitch_size, 64, 24, 24) nn.MaxPool2d(kernel_size=2, stride=2), # 最大值池化 ) # 第二次卷积、池化 self.conv2 = nn.Sequential( # input:(bitch_size, 64, 24, 24), output:(bitch_size, 128, 24, 24), (24-3+2*1)/1+1 = 24 nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, stride=1, padding=1), nn.BatchNorm2d(num_features=128), nn.RReLU(inplace=True), # output:(bitch_size, 128, 12 ,12) nn.MaxPool2d(kernel_size=2, stride=2), ) # 第三次卷积、池化 self.conv3 = nn.Sequential( # input:(bitch_size, 128, 12, 12), output:(bitch_size, 256, 12, 12), (12-3+2*1)/1+1 = 12 nn.Conv2d(in_channels=128, out_channels=256, kernel_size=3, stride=1, padding=1), nn.BatchNorm2d(num_features=256), nn.RReLU(inplace=True), # output:(bitch_size, 256, 6 ,6) nn.MaxPool2d(kernel_size=2, stride=2), ) # 参数初始化 self.conv1.apply(gaussian_weights_init) self.conv2.apply(gaussian_weights_init) self.conv3.apply(gaussian_weights_init) # 全连接层 self.fc = nn.Sequential( nn.Dropout(p=0.2), nn.Linear(in_features=256*6*6, out_features=4096), nn.RReLU(inplace=True), nn.Dropout(p=0.5), nn.Linear(in_features=4096, out_features=1024), nn.RReLU(inplace=True), nn.Linear(in_features=1024, out_features=256), nn.RReLU(inplace=True), nn.Linear(in_features=256, out_features=7), ) # 前向传播 def forward(self, x): x = self.conv1(x) x = self.conv2(x) x = self.conv3(x) # 数据扁平化 x = x.view(x.shape[0], -1) y = self.fc(x) return y def train(train_dataset, val_dataset, batch_size, epochs, learning_rate, wt_decay): # 载入数据并分割batch train_loader = data.DataLoader(train_dataset, batch_size) # 构建模型 model = FaceCNN() # 损失函数 loss_function = nn.CrossEntropyLoss() # 优化器 optimizer = optim.SGD(model.parameters(), lr=learning_rate, weight_decay=wt_decay) # 学习率衰减 # scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.8) # 逐轮训练 for epoch in range(epochs): # 记录损失值 loss_rate = 0 # scheduler.step() # 学习率衰减 model.train() # 模型训练 for images, emotion in train_loader: # 梯度清零 optimizer.zero_grad() # 前向传播 output = model.forward(images) # 误差计算 loss_rate = loss_function(output, emotion) # 误差的反向传播 loss_rate.backward() # 更新参数 optimizer.step() # 打印每轮的损失 print('After {} epochs , the loss_rate is : '.format(epoch+1), loss_rate.item()) if epoch % 5 == 0: model.eval() # 模型评估 acc_train = validate(model, train_dataset, batch_size) acc_val = validate(model, val_dataset, batch_size) print('After {} epochs , the acc_train is : '.format(epoch+1), acc_train) print('After {} epochs , the acc_val is : '.format(epoch+1), acc_val) return model

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104

代码测试结果如下:

在这里插入图片描述
训练模型保存在文件下为:model_resnet.pkl
训练过程

8、模型优化(VGG、ResNet)与算法改进

我们可以用理论部分提出的另外两种模型VGG模型和ResNet模型,对现有的项目进行优化,提升识别的准确度(三种算法已经实现),模型训练也已经完成。
此处省略----

9、本地数据采集[图像采集补充]

在这里插入图片描述

源码实现如下:

class FaceRecognition: def __init__(self, master): self.master = master self.master.title('CNN人脸识别') self.frame = Frame(self.master) self.frame.pack(pady=10) # 创建按钮 self.button1 = Button(self.frame, text='采集人脸数据', command=self.collect_face) self.button1.pack(side=LEFT, padx=10) self.button2 = Button(self.frame, text='训练模型', command=self.train_model) self.button2.pack(side=LEFT, padx=10) self.button3 = Button(self.frame, text='人脸识别', command=self.start_recognition) self.button3.pack(side=LEFT, padx=10) self.button4 = Button(self.frame, text='退出程序', command=self.master.quit) self.button4.pack(side=LEFT, padx=10) # 创建文本框用于显示结果 self.text = Text(self.master, width=30, height=10) self.text.pack(pady=10) # 加载模型 self.model = tf.keras.models.load_model(r'model/model.h5') # 获取分类器 self.haar = cv2.CascadeClassifier(r'haarcascade_frontalface_default.xml') # 获取人脸识别器 self.detector = dlib.get_frontal_face_detector() # 创建显示图片的标签 self.img_label = Label(self.master) self.img_label.pack() # 创建关闭人脸识别按钮 self.close_button = Button(self.master, text='关闭人脸识别', command=self.close_recognition) self.close_button.pack() # 是否进行人脸识别的标志 self.recognition_flag = False # 采集人脸数据 def collect_face(self): out_dir = './my_faces' if not os.path.exists(out_dir): os.makedirs(out_dir) # 改变亮度与对比度 def relight(img, alpha=1, bias=0): w = img.shape[1] h = img.shape[0] # image = [] for i in range(0, w): for j in range(0, h): for c in range(3): tmp = int(img[j, i, c] * alpha + bias) if tmp > 255: tmp = 255 elif tmp < 0: tmp = 0 img[j, i, c] = tmp return img # 打开摄像头 参数为输入流,可以为摄像头或视频文件 camera = cv2.VideoCapture(0) n = 1 while 1: if (n <= 5000): print('It`s processing %s image.' % n) # 读帧 success, img = camera.read() gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) faces = self.haar.detectMultiScale(gray_img, 1.3, 5) for f_x, f_y, f_w, f_h in faces: face = img[f_y:f_y + f_h, f_x:f_x + f_w] face = cv2.resize(face, (64, 64)) face = relight(face, random.uniform(0.5, 1.5), random.randint(-50, 50)) cv2.imshow('img', face) cv2.imwrite(out_dir + '/' + str(n) + '.jpg', face) n += 1 key = cv2.waitKey(30) & 0xff if key == 27: break else: break

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687

10、人脸情绪识别系统设与实现

测试效果

核心源码(Q:2551931023)

class EmotionClassifier: def __init__(self): self.face_cascade = cv2.CascadeClassifier( r".datasetshaarcascade_frontalface_default.xml") self.model = Model() self.model.load_state_dict( torch.load(r'.modelmodel_params.pkl', map_location='cpu')) self.model.eval() def get_emotion(self, inputs): inputs = self.preprocess(inputs) _, predicted = torch.max(self.model(inputs), 1) probability = F.softmax((self.model(inputs)), dim=1).detach().numpy().flatten() emotion = { 0: 'angry', 1: 'disgust', 2: 'fear', 3: 'happy', 4: 'sad', 5: 'surprised', 6: 'normal' } return emotion[predicted.item()], probability def preprocess(self, inputs): trans = transforms.Compose([ transforms.Grayscale(), transforms.ToTensor(), ]) inputs = trans(inputs) inputs = inputs.unsqueeze(0) return inputs class App: def __init__(self, video_source=0): self.window = Tk() self.window.title('基于CNN的情绪识别系统') self.video_source = video_source self.emo_cls = EmotionClassifier() self.canvas = Canvas(self.window, width=640, height=480) self.canvas.pack(side=LEFT) self.results_label = Label(self.window, text='情绪结果:', font=('Arial', 24), padx=20, pady=10) self.results_label.pack(side=TOP) self.bar_canvas = Canvas(self.window, width=320, height=240) self.bar_canvas.pack(side=TOP) self.quit_button = Button(self.window, text='退出系统', font=('Arial', 18), command=self.window.quit) self.quit_button.pack(side=BOTTOM, padx=20, pady=10) self.delay = 10 self.update() self.window.mainloop() def update(self): ret, frame = cap.read() if ret: frame = cv2.flip(frame, 1) img = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) rendered_img = ImageTk.PhotoImage(img) self.canvas.img = rendered_img self.canvas.create_image(0, 0, anchor=NW, image=rendered_img) gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = self.emo_cls.face_cascade.detectMultiScale(gray, 1.3, 5) for (x, y, w, h) in faces: face = cv2.resize(frame[y:(y + h), x:(x + w)], (42, 42)) emotion, probability = self.emo_cls.get_emotion(Image.fromarray(face)) results_str = '情绪检测结果:{}'.format(emotion) self.results_label.config(text=results_str) self.bar_canvas.delete('all') bar_height = self.bar_canvas.winfo_height() bar_width = self.bar_canvas.winfo_width() max_prob = np.max(probability) prob_heights = [int(round(x / max_prob * bar_height)) for x in probability] prob_colors = ['red', 'blue', 'green', 'yellow', 'purple', 'brown', 'orange'] for i in range(len(prob_heights)): start_x = 0 start_y = bar_height / len(prob_heights) * i end_x = prob_heights[i] / bar_height * bar_width end_y = bar_height / len(prob_heights) * (i + 1) # 拼接情绪标签、表情和百分比 emotion_text = ['愤怒', '厌恶', '恐惧', '高兴', '悲伤', '惊讶', '正常'][i] emoji_text = ['Anger ',' disgust ', 'fear ',' happy ', 'sad ',' surprise ', 'normal'][i] prob_text = '{:.2f}%'.format(probability[i] * 100) label_text = '{} {} {}'.format(emotion_text, emoji_text, prob_text) label_x = end_x label_y = (start_y + end_y) / 2 self.bar_canvas.create_rectangle(start_x, start_y, end_x, end_y, fill=prob_colors[i]) self.bar_canvas.create_text(label_x, label_y, text=label_text, font=('Arial', 12), anchor=W) self.window.after(self.delay, self.update)

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697

相关知识

100基于卷积神经网络之鸟鸣识别鸟的种类
基于卷积神经网络通过声音识别动物情绪的方法及系统
基于深度学习的宠物猫排泄物图像分类及其在宠物猫智能家居系统的应用研究
探索AIDog:智能狗狗识别与行为理解的开源项目
宠物识别
一种基于深度残差网络的宠物图像情绪识别方法与流程
PyTorch深度学习:猫狗情感识别
深度学习的艺术:从理论到实践
语音识别——基于深度学习的中文语音识别系统框架
鸟类声音识别技术综述:从传统方法到深度学习

网址: 基于深度学习的人脸情绪识别检测系统(VGG、CNN、ResNet) https://m.mcbbbk.com/newsview216428.html

所属分类:萌宠日常
上一篇: 使用 OpenCV 和 FER
下一篇: 可以采取什么措施来安抚焦虑的狗?