├── FERDSProcess.py ├── README.md ├── camera.py ├── haarcascade_frontalface_default.xml ├── model.py ├── predict.py ├── preprocess.py └── train.py /FERDSProcess.py: -------------------------------------------------------------------------------- 1 | # encoding:utf-8 2 | import pandas as pd 3 | import numpy as np 4 | from PIL import Image 5 | import os 6 | 7 | emotions = { 8 | '0': 'anger', # 生气 9 | '1': 'disgust', # 厌恶 10 | '2': 'fear', # 恐惧 11 | '3': 'happy', # 开心 12 | '4': 'sad', # 伤心 13 | '5': 'surprised', # 惊讶 14 | '6': 'normal' # 中性 15 | } 16 | 17 | 18 | def saveImageFromFer2013(file): 19 | # 读取CSV文件 20 | faces_data = pd.read_csv(file) 21 | image_count = 0 22 | 23 | # 遍历CSV文件内容,将图像数据按类别保存 24 | for index, row in faces_data.iterrows(): 25 | try: 26 | emotion_data = row[0] 27 | image_data = row[1] 28 | usage_data = row[2] 29 | 30 | # 将图像数据转换成48*48矩阵 31 | data_array = np.fromstring(image_data, sep=' ', dtype=np.float32) 32 | image = data_array.reshape(48, 48) 33 | 34 | # 创建保存路径 35 | dir_name = usage_data 36 | emotion_name = emotions.get(str(emotion_data), "unknown") 37 | image_path = os.path.join(dir_name, emotion_name) 38 | os.makedirs(image_path, exist_ok=True) 39 | 40 | # 保存图像 41 | image_name = os.path.join(image_path, f"{index}.jpg") 42 | # 将图像数据(数组)保存为灰度图像 43 | Image.fromarray(image).convert('L').save(image_name) 44 | image_count += 1 45 | except Exception as e: 46 | print(f"Error processing row {index}: {e}") 47 | 48 | print(f'总共有 {image_count} 张图片') 49 | 50 | saveImageFromFer2013('fer2013.csv') -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FER 2 | 基于CNN的面部表情识别 3 | ## 1. 数据集选择 4 | FER2013包含七种情感,分别为愤怒、厌恶、恐惧、快乐、悲伤、惊讶、中性,共35887张图像。数据最初通过网络收集,包含不同年龄、种族和性别的面部图像,增加了数据的多样性,但同时也带来了噪声和标注错误。由于图像分辨率较低,表情的细微差异在视觉上不太明显。此外,不同个体的表情差异较大,加之数据集中各情感类别的分布不均匀,这为模型的训练带来了挑战。 5 | 数据集的下载地址为: 6 | ## 2. 模型选择 7 | CNN具有局部特征不变性、权值共享等特点。在处理图像任务上表现优异,能够有效地提取出图像的多层次特征,具有高效的空间不变性和较强的泛化能力,是目前图像处理任务的主流模型。 8 | 9 | 算法:用于面部表情识别的卷积神经网络(CNN) 10 | 输入:大小为 48x48 的灰度图像 11 | 输出:7 类情感类别中的一种 12 | 13 | 步骤: 14 | 15 | Ⅰ初始化一个顺序模型。 16 | 17 | Ⅱ添加卷积层: 18 | 19 |  2x卷积层:64 个过滤器,3x3 卷积核,BatchNormalization,激活函数 ELU。 20 |  2x2 最大池化层。 21 |  2x卷积层:128 个过滤器,3x3 卷积核,BatchNormalization,激活函数 ELU。 22 |  2x2 最大池化层。 23 |  2x卷积层:256 个过滤器,3x3 卷积核,BatchNormalization,激活函数 ELU。 24 |  2x2 最大池化层。 25 | Ⅲ添加全连接层: 26 | 27 |  扁平化层。 28 |  全连接层:128 个单元,激活函数 ELU,BatchNormalization。 29 |  输出层:7 个单元,激活函数 softmax。 30 | 31 | Ⅳ编译模型:使用 Adam 优化器(学习率 $\alpha$=0.001),损失函数为 categorical_crossentropy,评估指标为准确率accuracy。 32 | 33 | Ⅴ训练模型:训练 100 个周期,在测试集上进行评估。 34 | 35 | 输出:返回训练好的模型及其性能指标。 36 | ```python 37 | # model.py 38 | from keras.src.models import Sequential 39 | from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout, BatchNormalization 40 | from tensorflow.keras.optimizers import Adam 41 | 42 | def create_model(): 43 | model = Sequential([ 44 | # 第一组卷积层: 2个卷积层 (64 filters, 3x3), BatchNormalization, ELU 45 | Conv2D(64, (3, 3), padding='same', activation='elu', input_shape=(48, 48, 1)), 46 | BatchNormalization(), 47 | Conv2D(64, (3, 3), padding='same', activation='elu'), 48 | BatchNormalization(), 49 | MaxPooling2D((2, 2)), 50 | 51 | # 第二组卷积层: 2个卷积层 (128 filters, 3x3), BatchNormalization, ELU 52 | Conv2D(128, (3, 3), padding='same', activation='elu'), 53 | BatchNormalization(), 54 | Conv2D(128, (3, 3), padding='same', activation='elu'), 55 | BatchNormalization(), 56 | MaxPooling2D((2, 2)), 57 | 58 | # 第三组卷积层: 2个卷积层 (256 filters, 3x3), BatchNormalization, ELU 59 | Conv2D(256, (3, 3), padding='same', activation='elu'), 60 | BatchNormalization(), 61 | Conv2D(256, (3, 3), padding='same', activation='elu'), 62 | BatchNormalization(), 63 | MaxPooling2D((2, 2)), 64 | 65 | # 在模型的卷积层和全连接层之间添加扁平化层将多维特征转换为一维向量,以便全连接层能够接收这些特征进行进一步处理。 66 | Flatten(), 67 | 68 | # 全连接层 69 | Dense(128, activation='elu'), 70 | BatchNormalization(), 71 | 72 | # 输出层 73 | Dense(7, activation='softmax') 74 | ]) 75 | 76 | # 编译模型 77 | model.compile(optimizer=Adam(learning_rate=0.001), loss='categorical_crossentropy', metrics=['accuracy']) 78 | return model 79 | 80 | ``` 81 | ## 3. 训练和优化 82 | ### 3.1 数据集预处理 83 | 加载和预处理数据,进行图像归一化和标签编码,并划分训练集与测试集 84 | ```python 85 | # preprocess.py 86 | import pandas as pd 87 | import numpy as np 88 | from keras.src.utils import to_categorical 89 | 90 | def load_and_preprocess_data(file_path): 91 | data = pd.read_csv(file_path) 92 | 93 | # 图像和标签处理 94 | # np.fromstring() 将该字符串快速转为数值数组。.reshape(-1, 48, 48, 1) 将每张图像重塑为 48x48 的矩阵,并增加一个通道维度 1,以便于神经网络处理(即为灰度图) 95 | X = np.array([np.fromstring(image, sep=' ') for image in data['pixels']]).reshape(-1, 48, 48, 1) / 255.0 # 将像素值归一化 96 | # 使用 to_categorical 将情感标签转换为 one-hot 编码 97 | y = to_categorical(data['emotion'].values) 98 | 99 | # 根据 "Usage" 列划分训练集和测试集 100 | train_mask = data['Usage'] == 'Training' 101 | test_mask = data['Usage'] == 'PublicTest' 102 | 103 | X_train, y_train = X[train_mask], y[train_mask] 104 | X_test, y_test = X[test_mask], y[test_mask] 105 | 106 | return X_train, X_test, y_train, y_test 107 | 108 | ``` 109 | ### 3.2 损失函数选择 110 | 交叉熵损失函数categorical_crossentropy,计算的是预测的类别概率与真实类别之间的交叉熵,即模型输出概率分布与目标类别分布之间的距离。适用于多分类问题。公式为: 111 | 112 | $$Loss = -\sum_{i=1}^{N}y_{i}\log(p_{i})$$ 113 | 114 | 其中: 115 | $y_{i}$是真实标签(one-hot编码) 116 | $p_{i}$是模型预测的该类的概率 117 | ### 3.3 早停设置 118 | 设置早停可以帮助防止模型在训练集上过拟合,从而提高泛化能力。特别是在深度学习模型的训练中,早停能有效节省时间。 119 | 配置 EarlyStopping 回调函数,在验证损失 (val_loss) 停止降低后提前停止训练,并恢复最佳权重,以防止过拟合。 120 | ```python 121 | # train.py 122 | 123 | # 在验证损失 (val_loss) 停止降低后提前停止训练,10个epoch内无提升后恢复最佳权重 124 | early_stopping = EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True) 125 | ``` 126 | ### 3.4 批量归一化 127 | BatchNormalization批量归一化是一种深度学习中的正则化技术,主要用于加速神经网络的训练过程并提升模型的稳定性和准确性。 128 | 在训练过程中,BatchNormalization 会对每个 batch 内的输入进行标准化处理,使其均值为 0,方差为 1 129 | Ⅰ对输入的 $x$ 求均值 $\mu$ 和方差 $\delta^{2}$ 130 | Ⅱ标准化输入: 131 | 132 | $$\hat{x} = \frac{x-\mu}{\sqrt{\delta^{2}+\epsilon}}$$ 133 | 134 | 其中, $\epsilon$ 是一个很小的常数,用于防止除零错误 135 | 136 | Ⅲ应用缩放和平移参数 137 | 138 | $$y=\gamma\hat{x}+\beta$$ 139 | 140 | 其中, $\gamma$ 和 $\beta$ 是可学习的参数,用于恢复网络的表达能力 141 | ## 4. 实时面部表情识别 142 | ### 4.1 人脸检测 143 | 检测视频帧中的人脸,可以使用OpenCV提供的Haar特征级联分类器,需要haarcascade_frontalface_default.xml文件。 144 | 下载链接: 145 | ### 4.2 面部表情识别 146 | 使用已经训练好的模型进行面部表情识别,具体代码如下 147 | ```python 148 | # camera.py 149 | import cv2 150 | import numpy as np 151 | import tensorflow as tf 152 | 153 | # 加载训练好的模型 154 | model = tf.keras.models.load_model('saved_model/emotion_model.keras') 155 | print("模型加载成功") 156 | 157 | # 定义情感类别 158 | emotion_labels = ['anger', 'disgust', 'fear', 'happy', 'sad', 'surprised', 'neutral'] 159 | 160 | # 加载人脸检测器 161 | face_cascade = cv2.CascadeClassifier('haarcascade_frontalface_default.xml') 162 | if face_cascade.empty(): 163 | raise IOError("无法加载人脸检测器 XML 文件。请确保路径正确。") 164 | 165 | # 初始化摄像头 166 | cap = cv2.VideoCapture(0) # 0 是默认摄像头 167 | 168 | if not cap.isOpened(): 169 | raise IOError("无法打开摄像头。请检查摄像头是否连接正确。") 170 | 171 | while True: 172 | ret, frame = cap.read() 173 | if not ret: 174 | print("无法读取摄像头帧。") 175 | break 176 | 177 | # 将帧转换为灰度图像 178 | gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) 179 | 180 | # 检测人脸 181 | faces = face_cascade.detectMultiScale(gray, scaleFactor=1.3, minNeighbors=5) 182 | 183 | for (x, y, w, h) in faces: 184 | # 绘制人脸矩形框 185 | cv2.rectangle(frame, (x, y), (x+w, y+h), (255, 0, 0), 2) 186 | 187 | # 提取人脸区域 188 | face_roi = gray[y:y+h, x:x+w] 189 | try: 190 | # 调整人脸区域大小为 48x48 191 | face_roi = cv2.resize(face_roi, (48, 48)) 192 | except: 193 | continue 194 | 195 | # 预处理图像 196 | face_roi = face_roi.astype('float32') / 255.0 197 | face_roi = np.expand_dims(face_roi, axis=0) 198 | face_roi = np.expand_dims(face_roi, axis=-1) # 添加通道维度 199 | 200 | # 进行预测 201 | predictions = model.predict(face_roi) 202 | max_index = int(np.argmax(predictions)) 203 | predicted_emotion = emotion_labels[max_index] 204 | confidence = predictions[0][max_index] 205 | 206 | # 显示预测结果 207 | label = f"{predicted_emotion} ({confidence*100:.2f}%)" 208 | cv2.putText(frame, label, (x, y-10), 209 | cv2.FONT_HERSHEY_SIMPLEX, 0.9, (36,255,12), 2) 210 | 211 | # 显示结果帧 212 | cv2.imshow('Real-Time Facial Emotion Recognition', frame) 213 | 214 | # 按 'q' 键退出 215 | if cv2.waitKey(1) & 0xFF == ord('q'): 216 | break 217 | 218 | # 释放资源 219 | cap.release() 220 | cv2.destroyAllWindows() 221 | 222 | ``` 223 | ## 5. 演示视频 224 | 225 | -------------------------------------------------------------------------------- /camera.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import numpy as np 3 | import tensorflow as tf 4 | 5 | # 加载训练好的模型 6 | model = tf.keras.models.load_model('saved_model/emotion_model.keras') 7 | print("模型加载成功") 8 | 9 | # 定义情感类别 10 | emotion_labels = ['anger', 'disgust', 'fear', 'happy', 'sad', 'surprised', 'neutral'] 11 | 12 | # 加载人脸检测器 13 | face_cascade = cv2.CascadeClassifier('haarcascade_frontalface_default.xml') 14 | if face_cascade.empty(): 15 | raise IOError("无法加载人脸检测器 XML 文件。请确保路径正确。") 16 | 17 | # 初始化摄像头 18 | cap = cv2.VideoCapture(0) # 0 是默认摄像头 19 | 20 | if not cap.isOpened(): 21 | raise IOError("无法打开摄像头。请检查摄像头是否连接正确。") 22 | 23 | while True: 24 | ret, frame = cap.read() 25 | if not ret: 26 | print("无法读取摄像头帧。") 27 | break 28 | 29 | # 将帧转换为灰度图像 30 | gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) 31 | 32 | # 检测人脸 33 | faces = face_cascade.detectMultiScale(gray, scaleFactor=1.3, minNeighbors=5) 34 | 35 | for (x, y, w, h) in faces: 36 | # 绘制人脸矩形框 37 | cv2.rectangle(frame, (x, y), (x+w, y+h), (255, 0, 0), 2) 38 | 39 | # 提取人脸区域 40 | face_roi = gray[y:y+h, x:x+w] 41 | try: 42 | # 调整人脸区域大小为 48x48 43 | face_roi = cv2.resize(face_roi, (48, 48)) 44 | except: 45 | continue 46 | 47 | # 预处理图像 48 | face_roi = face_roi.astype('float32') / 255.0 49 | face_roi = np.expand_dims(face_roi, axis=0) 50 | face_roi = np.expand_dims(face_roi, axis=-1) # 添加通道维度 51 | 52 | # 进行预测 53 | predictions = model.predict(face_roi) 54 | max_index = int(np.argmax(predictions)) 55 | predicted_emotion = emotion_labels[max_index] 56 | confidence = predictions[0][max_index] 57 | 58 | # 显示预测结果 59 | label = f"{predicted_emotion} ({confidence*100:.2f}%)" 60 | cv2.putText(frame, label, (x, y-10), 61 | cv2.FONT_HERSHEY_SIMPLEX, 0.9, (36,255,12), 2) 62 | 63 | # 显示结果帧 64 | cv2.imshow('Real-Time Facial Emotion Recognition', frame) 65 | 66 | # 按 'q' 键退出 67 | if cv2.waitKey(1) & 0xFF == ord('q'): 68 | break 69 | 70 | # 释放资源 71 | cap.release() 72 | cv2.destroyAllWindows() 73 | -------------------------------------------------------------------------------- /model.py: -------------------------------------------------------------------------------- 1 | # model.py 2 | from keras.src.models import Sequential 3 | from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout, BatchNormalization 4 | from tensorflow.keras.optimizers import Adam 5 | 6 | def create_model(): 7 | model = Sequential([ 8 | # 第一组卷积层: 2个卷积层 (64 filters, 3x3), BatchNormalization, ELU 9 | Conv2D(64, (3, 3), padding='same', activation='elu', input_shape=(48, 48, 1)), 10 | BatchNormalization(), 11 | Conv2D(64, (3, 3), padding='same', activation='elu'), 12 | BatchNormalization(), 13 | MaxPooling2D((2, 2)), 14 | 15 | # 第二组卷积层: 2个卷积层 (128 filters, 3x3), BatchNormalization, ELU 16 | Conv2D(128, (3, 3), padding='same', activation='elu'), 17 | BatchNormalization(), 18 | Conv2D(128, (3, 3), padding='same', activation='elu'), 19 | BatchNormalization(), 20 | MaxPooling2D((2, 2)), 21 | 22 | # 第三组卷积层: 2个卷积层 (256 filters, 3x3), BatchNormalization, ELU 23 | Conv2D(256, (3, 3), padding='same', activation='elu'), 24 | BatchNormalization(), 25 | Conv2D(256, (3, 3), padding='same', activation='elu'), 26 | BatchNormalization(), 27 | MaxPooling2D((2, 2)), 28 | 29 | # 全连接层 30 | Flatten(), 31 | Dense(128, activation='elu'), 32 | BatchNormalization(), 33 | 34 | # 输出层 35 | Dense(7, activation='softmax') 36 | ]) 37 | 38 | # 编译模型 39 | model.compile(optimizer=Adam(learning_rate=0.001), loss='categorical_crossentropy', metrics=['accuracy']) 40 | return model 41 | -------------------------------------------------------------------------------- /predict.py: -------------------------------------------------------------------------------- 1 | # predict.py 2 | import tensorflow as tf 3 | from preprocess import load_and_preprocess_data 4 | 5 | model = tf.keras.models.load_model('saved_model/emotion_model.keras') 6 | 7 | print("模型加载成功") 8 | 9 | # 编译指标信息 10 | model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy']) 11 | 12 | # 加载和预处理数据 13 | X_train, X_test, y_train, y_test = load_and_preprocess_data('fer2013.csv') 14 | 15 | # 在测试集上评估模型 16 | loss, accuracy = model.evaluate(X_test, y_test) 17 | print(f"测试集上的准确率: {accuracy:.2%}") 18 | -------------------------------------------------------------------------------- /preprocess.py: -------------------------------------------------------------------------------- 1 | # preprocess.py 2 | import pandas as pd 3 | import numpy as np 4 | from keras.src.utils import to_categorical 5 | 6 | def load_and_preprocess_data(file_path): 7 | data = pd.read_csv(file_path) 8 | 9 | # 图像和标签处理 10 | # np.fromstring() 将该字符串快速转为数值数组。.reshape(-1, 48, 48, 1) 将每张图像重塑为 48x48 的矩阵,并增加一个通道维度 1,以便于神经网络处理(即为灰度图) 11 | X = np.array([np.fromstring(image, sep=' ') for image in data['pixels']]).reshape(-1, 48, 48, 1) / 255.0 # 将像素值归一化 12 | # 使用 to_categorical 将情感标签转换为 one-hot 编码 13 | y = to_categorical(data['emotion'].values) 14 | 15 | # 根据 "Usage" 列划分训练集和测试集 16 | train_mask = data['Usage'] == 'Training' 17 | test_mask = data['Usage'] == 'PublicTest' 18 | 19 | X_train, y_train = X[train_mask], y[train_mask] 20 | X_test, y_test = X[test_mask], y[test_mask] 21 | 22 | return X_train, X_test, y_train, y_test 23 | -------------------------------------------------------------------------------- /train.py: -------------------------------------------------------------------------------- 1 | # train.py 2 | from keras.src.callbacks import EarlyStopping 3 | from preprocess import load_and_preprocess_data 4 | from model import create_model 5 | import os 6 | 7 | # 加载和预处理数据 8 | X_train, X_test, y_train, y_test = load_and_preprocess_data('fer2013.csv') 9 | 10 | # 创建模型 11 | model = create_model() 12 | 13 | # 设置早停 14 | # 配置 EarlyStopping 回调函数,在验证损失 (val_loss) 停止降低后提前停止训练,允许10个epoch内无提升,并恢复最佳权重 15 | early_stopping = EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True) 16 | 17 | # 训练模型 18 | # 使用 fit 方法进行模型训练,通过指定 epochs 和 batch_size,模型会在训练集上进行迭代学习 19 | history = model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=100, batch_size=64, callbacks=[early_stopping]) 20 | 21 | # 在测试集上评估模型 22 | loss, accuracy = model.evaluate(X_test, y_test) 23 | print(f"测试集上的准确率: {accuracy:.2%}") 24 | 25 | # 保存模型 26 | os.makedirs('saved_model', exist_ok=True) 27 | model.save('saved_model/emotion_model.keras') 28 | print("模型已保存至 'saved_model/emotion_model.keras'") 29 | --------------------------------------------------------------------------------