简介:面向计算机相关专业毕业设计的高分面部表情识别系统完整项目包,涵盖Python源码、毕业论文、数据集与答辩PPT,配套齐全,可直接用于课设与毕设展示。基于深度学习框架实现CNN、VGG、ResNet等多种主流模型及对比实验脚本,并提供数据预处理、表情映射、实时视频识别演示等辅助工具,覆盖从数据划分到模型部署的完整流程。压缩包共36个文件,以py源码、ipynb交互式分析笔记、docx论文文档、zip数据集、pptx答辩材料为主,附带模型权重pkl和演示视频,整体大小446MB,目录结构清晰便于按需查阅。目前已有276人学习使用。整套资料评审分98分并经导师指导认可,既适合毕业设计参考,也可用于课程设计、期末大作业及深度学习实战练习,帮助学习者快速搭建完整的人脸表情识别系统。
1. 这个面部表情识别项目,先看数据还是先跑模型?
拿到一套基于深度学习的面部表情识别系统源码,我的习惯是先别急着敲命令。这个项目打包了 CNN、VGG、ResNet 三种模型,用 Fer2013 数据集训练,还配了论文、答辩 PPT 和演示视频,典型的高分毕业设计结构。第一次跑通它,你就能看到一条完整的“人脸检测 -> 人脸裁剪 -> 表情分类”链路。它特别适合两类人:正在做计算机视觉方向毕设的学生,以及想在深度学习项目里练手的人。数据、模型、Web 模板都是现成的,改起来比从零搭要省力得多。
2. 架构与数据链路:从原始 CSV 到训练集的预处理管线
很多同学拿到这套源码第一反应是直接跑 model_CNN.py,结果大概率会在加载数据时报错。原因是这个项目的输入不是一堆图,而是一个 CSV 文件。所以理解它的数据链路,比理解模型更重要。
2.1 目录结构与文件角色
先看一眼根目录下的文件,我把它们按职责分成了四类:
| 类别 | 文件 | 作用 |
|---|---|---|
| 模型 | model_CNN.py / model_VGG.py / model_ResNet.py | 定义网络结构并直接作为训练入口 |
| 测试对比 | model_CNN_test.py / model_VGG_test.py / model_ResNet_test.py / model_All_Compare.py | 加载模型做准确率评估和横向对比 |
| 数据处理 | data_process.py / data_separation.py / data_view.py | 负责解析 CSV、划分数据集、可视化样本 |
| 业务层 | haarcascade_frontalface_default.xml / templates / image_emotion_mapping.py | 人脸检测、Web 页面、情绪标签映射 |
另外还有dataset/存放数据,face_images/放测试用的人脸图,手册.docx是作者写的运行说明,论文和 PPT 放在根目录。这套结构的好处是:训练、测试、部署三块互相独立,你替换数据集或换模型时,不用动整个工程。
templates目录值得多说一句。文件列表里没有 app.py 这类 Flask 入口,但存在 HTML 模板,常见做法是作者用简易 Web 应用包装了识别接口。我一般会补一个 main.py 把模型封装成可调用的函数,再挂到 Flask 路由上,这样就能从浏览器上传图片看结果。
2.2 数据管线:从 CSV 像素串到 PyTorch Dataset
Fer2013 是 Kaggle 上经典的表情数据集,有 35887 张 48x48 灰度图,标签对应 7 类表情:anger、disgust、fear、happy、sad、surprise、neutral。原始文件fer2013.csv的每一行大约长这样:
emotion,pixels,Usage 0,70 80 82 72 58 58 ...,Trainingpixels是 2304 个 0-255 的灰度值,以空格分隔。Usage字段把样本分成 Training、PublicTest、PrivateTest 三部分。这个项目里的data_process.py干的就是解析这部分字符串并重构出图像矩阵的事。
import numpy as np import pandas as pd def load_fer2013(csv_path): df = pd.read_csv(csv_path) pixels = df['pixels'].tolist() emotions = df['emotion'].tolist() images = [] for px in pixels: arr = np.array(px.split(), dtype=np.float32) arr = arr.reshape(48, 48) / 255.0 # 归一化到 [0,1] images.append(arr) return np.expand_dims(images, axis=-1), np.array(emotions)这段代码把每一行的像素字符串按空格拆开,转成 float32 数组,再缩放到 48x48 的单通道图。np.expand_dims(..., axis=-1)是为了给 PyTorch 的卷积层补上通道维,得到(N, 48, 48, 1)的形状。
这里有两个常见坑:一是 CSV 里的像素值可能有缺失或多余空格,用split()不指定分隔符可以自动跳过连续空格;二是归一化要在 reshape 之前还是之后都行,但一定要用 float32,否则后续torch.tensor会隐式转换导致精度丢失。
data_separation.py负责把数据按Usage列拆成 train / valid / test 三个集合,而不是随机划分。这样你能直接拿来和论文里的准确率做对比,因为验证集是官方固定的。data_view.py则把每个类别的样本分布画成条形图,也能随机展示 9 张图,用来确认预处理后的图像没有扭曲或标签错位。
3. 核心模型拆解:CNN、VGG 与 ResNet 的代码级实现
这个项目最大的价值是同时提供了三种主流的深度模型实现。新手可以从最小的 CNN 学起,做实验对比的人可以直接复用 VGG 和 ResNet。三者的训练脚本是同一套模板,区别只在网络定义。
3.1 基线 CNN:一个 3 层卷积的轻量分类器
model_CNN.py开头是标准的 PyTorch 结构定义。因为 Fer2013 图像只有 48x48 灰度图,所以第一层的输入通道数是 1。这套网络设计得不算深,但足够在表情识别上作为基线。
import torch.nn as nn class EmotionCNN(nn.Module): def __init__(self, num_classes=7, dropout=0.5): super().__init__() self.features = nn.Sequential( nn.Conv2d(1, 32, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2), nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2), nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2), ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(128 * 6 * 6, 256), nn.ReLU(inplace=True), nn.Dropout(p=dropout), nn.Linear(256, num_classes), )参数说明:kernel_size=3配合padding=1保证卷积不改变特征图尺寸,stride=2的池化把 48x48 依次降到 24x24、12x12、6x6,所以最后的展平维度是128 * 6 * 6。如果你自己改输入尺寸,这里必须同步调。nn.Dropout(p=0.5)是在全连接前随机丢弃一半神经元,防止小数据集过拟合。
逻辑上它就是标准的卷积提取特征 -> 全连接分类。这个模型在 Fer2013 上能跑到大约 60%-63% 的准确率,不要嫌低,Fer2013 本身标签噪声很大,人脸对齐也不严格,60% 已经是不错的基线。
3.2 VGG 与 ResNet:迁移学习的两种写法
model_VGG.py和model_ResNet.py没有从零实现 VGG 或 ResNet,而是直接调用了torchvision.models里预训练好的模型。因为表情识别数据集远小于 ImageNet,常见做法是复用 ImageNet 上预训练的参数,只替换最后一层全连接。
import torchvision.models as models import torch.nn as nn def build_resnet18(num_classes=7): model = models.resnet18(pretrained=True) in_features = model.fc.in_features # 默认是 512 model.conv1 = nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3, bias=False) model.fc = nn.Linear(in_features, num_classes) return model这里有个关键改动:预训练 ResNet 默认接受三通道 RGB 输入,而 Fer2013 是单通道灰度图。直接喂会报形状错误,所以需要把conv1的输入通道从 3 改成 1。需要注意的是,这种改动会让第一层卷积的预训练权重失效,因为通道数对不上。我一般会保留前三通道的平均值初始化为单通道权重,或者干脆接受第一层随机初始化,只让后面的层用预训练权重。
VGG 的改法类似,VGG16 的classifier[6]是最后一层 Linear,把它替换成num_classes即可。由于 VGG 参数量大,在 CPU 上训练会非常慢,作者给出的代码里通常会在 ImageFolder 或 Dataset 加载时把图片convert('L')转成灰度,再在预处理里归一化。
3.3 训练、测试与模型对比
三个模型的训练脚本共用一套训练循环,核心就是交叉熵损失加 Adam 优化器。别看后面论文写了一大堆,训练代码其实很标准:
import torch import torch.nn as nn from torch.utils.data import DataLoader def train_one_epoch(model, dataloader, optimizer, device): model.train() criterion = nn.CrossEntropyLoss() total_loss, correct, total = 0, 0, 0 for images, labels in dataloader: images, labels = images.to(device), labels.to(device) optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() total_loss += loss.item() preds = outputs.argmax(dim=1) correct += (preds == labels).sum().item() total += labels.size(0) return total_loss / len(dataloader), correct / total这段代码没什么玄学,值得注意的有两点:一是outputs.argmax(dim=1)取每个样本得分最高的类别作为预测,dim=1表示在类别维度上操作;二是每个 epoch 结束后应该保存 state_dict,否则训练中断就得重来。model_All_Compare.py会加载三个模型在 PrivateTest 集上跑一遍,把准确率画成柱状图,这也是答辩 PPT 里最常用的素材。
如果你自己改模型,记得把model_CNN_test.py里的加载路径从.pkl改成你实际保存的文件名。这个项目里模型权重存在model_resnet.pkl,但 PyTorch 官方更推荐.pth,避免和 pickle 序列化混淆。
4. 运行与避坑:让这套源码在自己的机器上稳定跑起来
我见过太多人在这一步翻车:明明代码没问题,就是跑不起来。这项目涉及 CSV 解析、模型权重、人脸检测三个环节,任何一个出错都会让你觉得是玄学。下面按实际操作顺序写。
4.1 环境配置与训练启动
建议用 Python 3.8 或 3.10,装好以下依赖:
pip install torch torchvision opencv-python numpy pandas matplotlib flask如果你想用 GPU 训练,需要根据你的 CUDA 版本去 PyTorch 官网装对应版本,否则torch.device('cuda')会直接报错。训练入口很简单:
python data_separation.py # 先把 CSV 拆分成 train.csv / val.csv / test.csv python model_CNN.py # 开始训练 CNNdata_separation.py会生成新的 CSV 或样本文件,后面的 Dataset 读取的就是这些结果,不是原始fer2013.csv。这也是很多人跳过预处理直接跑模型报“找不到图片”的原因。如果项目里已经给出了拆好的数据,可以跳过这一步,但最好还是自己跑一遍,确认数据路径正确。
model_CNN.py在训练结束后会把模型保存到model目录。如果看到FileNotFoundError: 'model',那就先手动mkdir model。这是最基础但最常见的坑。
4.2 图片与视频识别流程
训练好模型后,最终要落到“给一张人脸照片,返回表情”。这里用的是 OpenCV 的 Haar 级联检测器,把检测到的人脸区域缩放成 48x48 输入模型。
import cv2 def predict_emotion(image_path, model, emotion_dict): img = cv2.imread(image_path) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) face_cascade = cv2.CascadeClassifier('haarcascade_frontalface_default.xml') faces = face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(48, 48)) for (x, y, w, h) in faces: face = gray[y:y+h, x:x+w] face = cv2.resize(face, (48, 48)) face = face.reshape(1, 1, 48, 48).astype('float32') / 255.0 # model 输出 tensor,取 argmax return emotionsscaleFactor=1.1表示每次搜索窗口扩大 10%,minNeighbors=5控制误检率——值越大人脸框越少,但也越准。这里把 BGR 转成灰度是必须的,因为模型在灰度图上训练。如果你想直接跑视频,视频流里的每一帧都执行同样的检测,demo 文件里那个example_dsh.mp4就是自带测试素材。
4.3 常见问题排查(避坑)
下面这五条是我实际踩过或看别人踩过的坑,每个都按现象、“原因”、“解决”的顺序写,建议收藏。
现象一:训练时
loss不降,准确率一直在 20% 以下。原因:标签和 Channels 顺序错了,比如灰度图被当成三通道读了,或者数据没有归一化。解决:打印一个 batch 的images.shape和labels.unique(),确认是(B, 1, 48, 48)、标签范围 0-6。现象二:加载验证集时报
IndexError: index out of range。原因:你的data_separation.py划分后训练集标签类别数少于 7,比如某个类别在划分后没样本。解决:检查pd.value_counts(df['emotion']),确保每个类别都有样本,或用分层采样。现象三:
torch.load报ModuleNotFoundError: model_CNN。原因:你直接加载.pkl时,pickle 需要用同一类结构。解决:在加载脚本前先把model_CNN.py里的类 import 进来,或用torch.load(weights_path, map_location='cpu')并先构建好 model 再load_state_dict。现象四:CPU 训练 VGG 慢到怀疑人生。原因:VGG16 参数量巨大,在 CPU 上跑 30 个 epoch 要几个小时。解决:先用 CNN 或 ResNet18 调试通流程,最后再跑 VGG 出对比数据;训练时降低图片分辨率或改用
batch_size=32。现象五:OpenCV 检测不到侧面人脸或戴口罩人脸。原因:Haar 级联的本质是弱分类器堆叠,对姿态和遮挡非常敏感。解决:把
minNeighbors调低到 2-3 提高召回率,或者在识别前用dlib的 68 点人脸检测替代,但这需要额外装dlib并处理模型文件。
5. 二次开发思路:从毕设 Demo 到可用的表情识别服务
这套项目跑通只是第一步,如果你想把它写进简历,或者让导师觉得工作量超纲,下面这几个方向都很容易加。
5.1 换更现代的数据集
Fer2013 虽然经典,但类别不平衡且噪声大。你可以把data_process.py替换成 RAF-DB 或 AffectNet 的数据接口。核心改动只有 Dataset 的__getitem__——原来是从 CSV 读像素串,现在是从图片文件夹读文件并返回标签。模型代码完全不用动,这正好验证了项目分层是否合理。
5.2 加入摄像头实时识别
把 4.2 的预测函数包一层,用 OpenCV 打开摄像头:
cap = cv2.VideoCapture(0) while cap.isOpened(): ret, frame = cap.read() emotion = predict_emotion_frame(frame, model, face_cascade) cv2.putText(frame, emotion, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 1, (0,255,0), 2) cv2.imshow('emotion', frame)在视频流里,你需要把emotion_dict映射成可读字符串,例如{0:'angry', 1:'disgust', ...}。如果想提高流畅度,每 5 帧做一次人脸检测,中间几帧直接复用上一次的脸部区域,省掉多余计算。
5.3 我的个人习惯
事实上,我拿到这套代码后第一件做的事,不是去增加新模型,而是删掉Dropout跑 10 个 batch 看模型能不能过拟合到 100%。如果连几个样本都记不住,说明网络结构或数据加载有问题,后面调参全是白费。还有,每次训练前我都会在同一个文件里写好torch.manual_seed(0),否则两个模型的对比结果会因为随机初始化不公平,答辩时经不起问。
从那以后,我每次打开一个开源的深度学习项目,都会先做一次“最小过拟合测试”,再决定要不要扩大模型规模。久病成医,这样能筛掉一大半环境问题。希望这个习惯也能帮到你。
本文还有配套的精品资源,点击获取