多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

基于CNN的人脸表情识别项目实战:从检测到七类情绪分类

基于CNN的人脸表情识别项目实战:从检测到七类情绪分类 简介这份资源是一套基于深度卷积神经网络的人脸面部表情识别系统完整项目源码面向人工智能、深度学习方向的初学者与进阶开发者适合用于课程设计、毕业设计或表情识别算法练手。项目支持从本地图片导入或调用摄像头拍摄对静态图片与视频进行表情分析并可在不同模型间切换对比效果。压缩包共47个文件约12.18MB包含5个Python脚本负责训练与推理、1个hdf5模型权重文件、1个ui界面文件及qrc资源文件另有png、jpg、gif等图片素材和mp4演示视频以及docx问题记录、ppt项目展示和log训练日志覆盖从数据到界面的完整链路。系统采用fer2013人脸数据集流程涵盖图像获取、预处理、特征提取与分类判别并附带训练脚本与测试图片便于读者复现训练、调试模型并理解表情识别工程结构。目前已有937人学习下载适合希望快速上手CNN表情识别实战的读者参考。1. 表情识别项目源码拆包从一张人脸到七类情绪的完整链路前阵子帮一个做在线教育的朋友看他们的课堂专注度分析模块他们想统计学生上课时的情绪分布第一反应是找外包报价结果对方开口就是六位数。我让他先别急把这份基于深度学习卷积神经网络实现的人脸面部表情识别系统项目源代码.zip 拿过去跑了一遍从环境配到推理出结果一个下午就跑通了。这份资源解决的就是「给定一张含人脸的图片或一段摄像头画面输出这张脸属于哪一类表情」的问题典型输出是七类生气、厌恶、恐惧、开心、悲伤、惊讶、中性。它适合两类人一是想拿一个能跑通的深度学习项目练手、把 CNN 从论文概念落到代码里的新手二是需要在业务里快速验证表情识别可行性、不想从零造轮子的工程师。整套代码围绕人脸检测加表情分类两段式流程组织人脸检测负责把人脸框出来CNN 负责对裁剪后的人脸做分类这个分工是后面所有调参和排错的基础。2. 两段式架构拆解人脸检测与 CNN 分类为什么必须分开2.1 为什么不做端到端而是检测加分类很多人第一反应是搞一个端到端网络输入整张图直接吐表情标签。这条路在论文里能跑在工程里翻车概率极高。原因很直接一张 640×480 的图里人脸可能只占 80×80 像素背景占了绝大部分面积端到端网络会把大量算力浪费在背景上而且背景里的纹理、光照变化会直接干扰分类头。两段式的好处是人脸检测先把无关区域裁掉送进分类网络的都是对齐后的人脸区域输入分布稳定模型收敛快出问题也容易定位——是没检测到脸还是检测到了但分类错了一眼能分清。常见做法是检测用 OpenCV 自带的 Haar 级联或 DNN 模块分类用自己搭的 CNN。这份源码走的就是这条路检测部分不依赖重型框架部署时少一层依赖。代价是 Haar 在侧脸、遮挡、暗光下漏检率偏高这是后面避坑章节要重点说的。2.2 分类网络的典型结构长什么样表情分类的 CNN 不需要 ResNet 那种深度七类分类任务在 48×48 灰度图上做四到六个卷积块足够。典型结构是卷积 → 激活 → 池化重复几次最后接全连接加 Softmax。下面是我按这份源码思路整理出的网络定义可以直接对照你手里的代码看结构是否一致。import torch import torch.nn as nn class EmotionCNN(nn.Module): def __init__(self, num_classes7): super(EmotionCNN, self).__init__() # 第一个卷积块提取边缘、纹理等低级特征 self.block1 nn.Sequential( nn.Conv2d(1, 32, kernel_size3, padding1), # 灰度图输入通道为1 nn.BatchNorm2d(32), # 加速收敛稳定训练 nn.ReLU(inplaceTrue), nn.MaxPool2d(2) # 48x48 - 24x24 ) # 第二个卷积块组合出眼睛、嘴角等局部结构 self.block2 nn.Sequential( nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2) # 24x24 - 12x12 ) # 第三个卷积块形成更抽象的表情相关特征 self.block3 nn.Sequential( nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2) # 12x12 - 6x6 ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(128 * 6 * 6, 256), nn.ReLU(inplaceTrue), nn.Dropout(0.5), # 防止全连接层过拟合 nn.Linear(256, num_classes) ) def forward(self, x): x self.block1(x) x self.block2(x) x self.block3(x) return self.classifier(x)逻辑上三个卷积块逐级把空间尺寸从 48 压到 6通道数从 1 升到 128最后展平送全连接。参数上几个关键点输入通道写 1 是因为表情数据集普遍用灰度图如果你手里的数据是 RGB这里要改成 3Dropout 设 0.5 是分类任务里比较稳的值数据量小可以调到 0.3 到 0.6 之间试num_classes 固定 7如果你的数据集类别数不同改这里的同时要确认标签映射顺序和训练时一致否则预测结果会整体错位。2.3 数据预处理的三个固定动作送进网络之前人脸区域要过三步灰度化、尺寸归一化到 48×48、像素值归一化到 0 到 1 或减均值除标准差。这三步顺序不能乱先裁剪再缩放否则缩放会把背景一起带进来。下面这段是推理时的预处理训练时同理。import cv2 import numpy as np def preprocess_face(face_img): # 统一转灰度表情识别对颜色不敏感灰度还能减少计算量 gray cv2.cvtColor(face_img, cv2.COLOR_BGR2GRAY) # 缩放到网络固定输入尺寸插值方式用面积插值缩小图像更平滑 resized cv2.resize(gray, (48, 48), interpolationcv2.INTER_AREA) # 归一化到 [0,1]与训练时的预处理保持一致 normalized resized.astype(np.float32) / 255.0 # 增加通道维度变成 (1, 48, 48) return np.expand_dims(normalized, axis0)参数说明INTER_AREA 适合缩小图像比默认的双线性插值更少产生摩尔纹除以 255 是最简单的归一化如果你训练时用的是减 0.5 除 0.5推理这里必须同步改训练和推理预处理不一致是新手最常见的精度暴跌原因没有之一。3. 从零跑通环境配置、训练与推理的完整命令3.1 环境依赖与版本选择这份源码基于 Python 和 PyTorch配套 OpenCV 做人脸检测。环境配置是深度学习项目第一道坎版本对不上报错能让你怀疑人生。我一般会先建独立虚拟环境避免和系统里的包打架。# 创建虚拟环境Python 版本建议 3.8 到 3.10 python -m venv emotion_env # 激活环境Windows 用 emotion_env\Scripts\activate source emotion_env/bin/activate # 安装核心依赖版本按你显卡驱动和 CUDA 情况调整 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install opencv-python numpy matplotlib参数说明torch 的 index-url 里 cu118 代表 CUDA 11.8如果你没有 NVIDIA 显卡或不想折腾 CUDA把整行换成pip install torch torchvision装 CPU 版即可训练会慢但能跑通。opencv-python 装基础版就够不需要 contrib 版除非源码里用到了 face 模块的深度学习检测器。3.2 训练脚本怎么读、怎么改训练脚本的核心是数据加载、损失函数、优化器三块。拿到源码先别急着跑打开训练文件确认这几处数据集的根目录路径、类别文件夹命名、batch size、学习率。下面是我按常见组织方式写的训练骨架对照你手里的代码逐项核对。import torch from torch.utils.data import DataLoader from torchvision import datasets, transforms # 训练集预处理加随机水平翻转做数据增强缓解过拟合 train_tf transforms.Compose([ transforms.Grayscale(num_output_channels1), transforms.Resize((48, 48)), transforms.RandomHorizontalFlip(p0.5), transforms.ToTensor(), transforms.Normalize(mean[0.5], std[0.5]) ]) # 数据集目录结构要求root/类别名/图片文件 train_set datasets.ImageFolder(root./data/train, transformtrain_tf) train_loader DataLoader(train_set, batch_size64, shuffleTrue, num_workers2) device torch.device(cuda if torch.cuda.is_available() else cpu) model EmotionCNN(num_classes7).to(device) criterion torch.nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3) for epoch in range(30): model.train() running_loss 0.0 for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() outputs model(imgs) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() print(fepoch {epoch1}, loss {running_loss/len(train_loader):.4f})逻辑说明ImageFolder 要求数据按类别分文件夹存放这是最省事的组织方式源码如果用的是自定义 Dataset逻辑一样只是读取方式不同。参数上batch size 设 64 是 48×48 小图比较合适的值显存不够就降到 32学习率 1e-3 配 Adam 是常规起点训练 loss 震荡就降到 1e-4epoch 设 30 是参考值实际看验证集准确率什么时候不再涨就停。RandomHorizontalFlip 对表情识别要谨慎左右翻转会改变某些表情的语义方向如果验证集准确率反而下降把这个增强去掉。3.3 推理与摄像头实时识别训练完保存权重后推理脚本负责把检测和分类串起来。下面这段是摄像头实时版本的骨架把视频流换成单张图片读取就是图片推理。import cv2 import torch # 加载人脸检测器Haar 级联文件随 OpenCV 安装包提供 face_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml) model EmotionCNN(num_classes7) model.load_state_dict(torch.load(emotion_cnn.pth, map_locationcpu)) model.eval() emotion_labels [angry, disgust, fear, happy, sad, surprise, neutral] cap cv2.VideoCapture(0) while True: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 检测人脸scaleFactor 和 minNeighbors 是两个关键调参点 faces face_cascade.detectMultiScale(gray, scaleFactor1.1, minNeighbors5) for (x, y, w, h) in faces: roi frame[y:yh, x:xw] tensor torch.from_numpy(preprocess_face(roi)).unsqueeze(0).float() with torch.no_grad(): pred model(tensor).argmax(dim1).item() cv2.rectangle(frame, (x, y), (xw, yh), (0, 255, 0), 2) cv2.putText(frame, emotion_labels[pred], (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 255, 0), 2) cv2.imshow(emotion, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()参数说明detectMultiScale 的 scaleFactor 控制每次缩放比例1.1 是精度和速度的平衡点调到 1.05 检测更细但更慢minNeighbors 控制误检调大减少误检但可能漏掉侧脸5 是常用默认值。emotion_labels 的顺序必须和训练时 ImageFolder 按文件夹名排序的结果一致这个顺序错了预测标签会整体错位是血泪经验里排前三的坑。4. 避坑与排查表情识别项目最容易翻车的五个地方4.1 现象训练准确率很高推理结果全是同一类原因训练和推理的预处理不一致最常见的是训练用了 Normalize 减 0.5 除 0.5推理只做了除以 255。输入分布偏移导致模型输出退化成常数。解决把训练脚本里的 transforms 完整抄到推理预处理里逐行比对灰度化、尺寸、归一化三步确保数值范围完全一致。可以在推理前打印一张图的像素均值和方差和训练时对比。4.2 现象摄像头画面里人脸框闪烁表情标签乱跳原因Haar 检测器逐帧独立检测帧间没有平滑加上光照变化导致检测框大小抖动裁剪出的人脸区域每帧都不一样。解决对检测框做简单平滑比如用上一帧的框和当前帧做加权平均或者降低检测频率每三帧检测一次中间帧沿用上次的框。表情标签也可以加一个多数投票窗口连续五帧里取出现次数最多的标签再显示。4.3 现象侧脸和戴眼镜的人脸检测不到原因Haar 级联用的是正面人脸特征训练对侧脸、遮挡、反光镜片的鲁棒性差这是算法本身的局限不是代码写错了。解决换用 OpenCV DNN 模块加载深度学习人脸检测模型或者用 mediapipe 的人脸检测对侧脸和遮挡的容忍度明显更高。代价是依赖变重部署包体积增大需要权衡。4.4 现象训练 loss 不下降一直卡在 1.9 左右原因七类分类的随机猜测 loss 约等于 ln(7)≈1.95卡在这个值说明模型没学到任何东西。常见诱因是学习率过大导致梯度爆炸或者标签和输入没对齐。解决先把学习率降到 1e-4 试再检查 DataLoader 吐出来的一个 batch把图片和标签打印出来人工核对确认图片内容和标签对应。如果标签是乱序的检查 ImageFolder 的目录结构和类别映射。4.5 现象换自己的数据集后准确率断崖式下跌原因公开表情数据集多是实验室环境、正面、光照均匀你自己的数据可能是监控角度、暗光、戴口罩域差异巨大。解决先在自己的数据上做少量微调冻结卷积层只训练全连接层学习率设小一点比如 1e-4。同时补充针对性数据增强比如随机调整亮度对比度、加高斯噪声让模型见过更多样的输入。5. 进阶技巧用混淆矩阵定位模型到底弱在哪一类跑通只是起点真正让这个项目有价值的是知道模型在哪些表情上容易错。我习惯在验证集上跑一遍混淆矩阵比只看一个总体准确率有用得多。七类表情里恐惧和惊讶、悲伤和中性这两组最容易混混淆矩阵能直接告诉你混在哪。import torch import numpy as np from sklearn.metrics import confusion_matrix, classification_report model.eval() all_preds, all_labels [], [] with torch.no_grad(): for imgs, labels in val_loader: outputs model(imgs.to(device)) preds outputs.argmax(dim1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) # 打印每一类的精确率、召回率、F1 print(classification_report(all_labels, all_preds, target_namesemotion_labels, digits3)) # 混淆矩阵行是真实标签列是预测标签 cm confusion_matrix(all_labels, all_preds) print(cm)拿到混淆矩阵后重点看两处对角线上的值越大越好非对角线里数值大的格子就是主要混淆对。如果恐惧被大量预测成惊讶说明这两类的特征在 48×48 灰度图里区分度不够可以考虑把输入尺寸提到 64×64或者引入注意力机制让网络关注眉毛和眼睛区域。如果某一类召回率特别低优先补这一类别的训练样本数据层面的收益通常比改网络结构来得快。还有一个实用技巧是给预测加置信度阈值。Softmax 输出的最大概率低于某个值比如 0.6 时不输出具体表情而是标记为「不确定」。这在业务里比强行输出一个错误标签更负责任尤其是课堂专注度分析这种场景误判比不判更麻烦。从那以后我每次拿到一个新的分类项目都强制先跑一遍混淆矩阵再谈优化不看这个就调参基本是盲人摸象。希望这份拆解能帮你少走几个弯路把这份源码真正用起来。本文还有配套的精品资源点击获取
返回列表