深度学习人脸情绪识别项目实战:OpenCV与CNN的完整实现
简介面向高校计算机相关专业期末大作业与入门实践这套基于OpenCV与深度学习的人脸情绪识别代码完整覆盖从人脸检测到情绪分类的推理流程并同时提供训练脚本与预测脚本便于理解工程化实现。资源包共9个文件整体约12.15MB主要包含Python源码、模型权重文件、配置文件、项目说明与测试图片其中权重文件可直接加载使用省去重新训练的时间。代码已本地编译通过评审分达到95分以上项目难度适中。目前平台已有371人学习使用适用于课程设计、期末项目以及深度学习与计算机视觉方向的基础实践。通过这份工程可以学习人脸检测模型与情绪分类模型的衔接调用方式了解OpenCV、深度学习权重文件与Python代码之间的协作逻辑并获得一个结构清晰的项目骨架方便在此基础上改进与扩展。1. 深度学习人脸情绪识别一份能直接复现的期末项目代码如果你正在找一份既能应付期末大作业、又能把 OpenCV 和深度学习串起来的人脸情绪识别项目这份资源值得花十几分钟看完再决定下载。它不是一个只有 README 的空壳解压后包含训练脚本、预训练权重、模型结构、人脸检测模型和推理脚本本地编译过、能跑通课程设计评审拿高分没问题。技术栈很清晰OpenCV 的 DNN 模块负责在画面里找到人脸CNN 卷积网络负责判断这个人的表情是开心、生气还是惊讶。适合三类人急着交期末大作业的本科生、想快速上手人脸识别全流程的 Python 初学者、以及想拿现成代码改造成自己项目的开发者。下面我从文件结构开始逐层拆开这份资源。2. 解压后先看文件读懂项目的工程结构2.1 五个核心文件的角色划分拿到压缩包后第一件事不是急着跑代码而是先把文件捡一遍。以 Face-emotion-recognition-main 目录为主干里面躺着十几个文件真正关键的只有下面这五个。我按「这个文件是干嘛的、缺了它会怎样」给你列个清单文件/目录类型作用缺失后果train.pyPython 源码数据加载、模型定义、训练与权重保存无法重新训练use.pyPython 源码调用模型做单张图片 / 摄像头实时推理无法预测fer-1.h5Keras 权重训练好的卷积网络参数CNN 的“大脑”预测直接失败fer-1.json模型结构 JSON记录网络层结构与 h5 配合加载h5 单独无法加载deploy.prototxt.txt res10_300x300_ssd_iter_140000.caffemodelCaffe 模型OpenCV DNN 人脸检测器负责框出人脸位置找不到脸后续全断这份分工非常典型res10_300x300_ssd_iter_140000.caffemodel是 OpenCV 官方仓库里那个 SSD 人脸检测器输入尺寸 300×300训练迭代 140000 次模型体积小、检测速度快适合在 CPU 上跑实时摄像头。而fer-1.json和fer-1.h5是 Keras 的标准产物——json 保存网络结构h5 保存权重两者缺一不可。2.2 人脸检测与情绪分类两级管线的设计思路整个项目不是「一张图直接丢进网络输出情绪」而是拆成两步。这是一个非常务实的工程决策也是期末答辩时老师喜欢问的点。第一步用 OpenCV 的cv2.dnn.readNetFromCaffe加载 SSD 检测器在整张图上框出人脸区域第二步把框出的人脸裁剪下来resize 成情绪网络需要的输入尺寸通常是 48×48 或 64×64转灰度、归一化再喂给 CNN 做七分类预测。为什么要这么拆因为情绪分类网络是在人脸小图上训练的它根本没见过完整的生活场景图。如果直接把一张带背景、带多人的照片丢给 CNN它会懵——背景噪声太大人脸占比太小分类准确率会崩到接近随机猜测。拆成两级之后检测器先把干扰物切掉分类器只看脸精度立刻上一个台阶。这个思路在工业级的人脸识别系统里也是标配你答完这一句老师基本就不会再追问架构问题了。2.3 模型文件怎么判断是否完整很多同学下载完解压发现.h5有、.json也有但一跑就报错原因往往是模型文件不配套。判断方法很简单用 Python 打开fer-1.json看里面有没有class_name: Sequential或Functional开头的 JSON 结构再看fer-1.h5大小一般这种七分类 CNN 权重在 20MB100MB 之间如果你的 h5 只有几十 KB八成是坏的。另外一个容易忽略的点deploy.prototxt.txt这个文件名里带了.txt后缀。加载时参数必须原样写成deploy.prototxt.txt有人图省事改名为deploy.prototxt结果读取路径对不上报Unable to open错误。这种小问题不影响大局但会卡住新手半小时我先帮你排掉。3. 训练与预测从 train.py 到 use.py 的代码级拆解3.1 train.py 里发生了什么数据增强与 CNN 结构train.py是这个项目的训练入口。以这份期末项目的常见实现来看它做的事可以拆成四步读图片、做数据增强、定义 CNN、训练并保存。下面我按最常见的 Keras TensorFlow 写法给你还原核心训练代码from tensorflow.keras.preprocessing.image import ImageDataGenerator from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout from tensorflow.keras.optimizers import Adam # 数据增强防止小数据集过拟合的关键 datagen ImageDataGenerator( rescale1./255, # 像素归一化到 [0,1]CNN 收敛更快 rotation_range15, # 随机旋转 ±15 度 horizontal_flipTrue, # 水平翻转模拟不同的人脸朝向 zoom_range0.15, # 随机缩放 validation_split0.2 # 留出 20% 作为验证集 ) # 从目录读取图片子文件夹名作为分类标签 train_generator datagen.flow_from_directory( dataset/train, target_size(48, 48), # 情绪识别常用 48x48 灰度图 color_modegrayscale, # 灰度图省内存且对表情识别影响很小 batch_size64, class_modecategorical, subsettraining ) # 定义一个轻量 CNN3 组卷积 池化 全连接 model Sequential([ Conv2D(32, (3, 3), activationrelu, input_shape(48, 48, 1)), MaxPooling2D(2, 2), Conv2D(64, (3, 3), activationrelu), MaxPooling2D(2, 2), Conv2D(128, (3, 3), activationrelu), MaxPooling2D(2, 2), Flatten(), Dropout(0.5), # 随机丢弃一半神经元防止过拟合 Dense(128, activationrelu), Dense(7, activationsoftmax) # 7 类开心/难过/生气/惊讶/恐惧/厌恶/中性 ]) model.compile(optimizerAdam(learning_rate0.001), losscategorical_crossentropy, metrics[accuracy]) # 训练并保存权重与结构 model.fit(train_generator, epochs50, verbose1) model.save(fer-1.h5) with open(fer-1.json, w) as f: f.write(model.to_json())这段代码有几个参数值得你答辩时讲两句。target_size(48, 48)是情绪识别社区的事实标准FER2013 数据集里最经典的工作就是这么定的你用 48 意味着能和预训练权重兼容Dropout(0.5)是防过拟合的关键层没有它 50 轮训练后准确率会出现诡异的震荡learning_rate0.001是 Adam 优化器比较稳妥的起步值跑不动再降到 0.0005。训练完成后把模型结构写入fer-1.json这一步很多人漏掉只存 h5。问题是 Keras 的model.save()其实会把结构和权重都打进 h5但这份项目单独拆了 json意味着推理端用model_from_json先搭骨架再load_weights加载这是老版本 Keras 的习惯写法也是 README 里推荐的加载方式注意别混着用。3.2 use.py 推理管线的必经步骤推理脚本use.py是你要交的东西里评委真正会运行的部分。它把前面提到的两级管线落到代码上核心流程是固定的五步加载检测器、加载情绪模型、读图、检测人脸、逐个预测。下面是推理端的核心逻辑import cv2 import numpy as np from tensorflow.keras.models import model_from_json # 第一步加载两级模型 face_net cv2.dnn.readNetFromCaffe( deploy.prototxt.txt, # SSD 人脸检测器结构描述 res10_300x300_ssd_iter_140000.caffemodel # 预训练权重 ) with open(fer-1.json, r) as f: emotion_model model_from_json(f.read()) # 用 json 重建网络结构 emotion_model.load_weights(fer-1.h5) # 加载训练好的权重 # 情绪标签顺序必须与训练时 class_indices 一致 emotion_labels [angry, disgust, fear, happy, sad, surprise, neutral]def predict_emotion(frame): h, w frame.shape[:2] # 第二步构建 blob 并送入人脸检测器 blob cv2.dnn.blobFromImage(frame, 1.0, (300, 300), (104.0, 177.0, 123.0)) face_net.setInput(blob) detections face_net.forward() results [] for i in range(detections.shape[2]): confidence detections[0, 0, i, 2] if confidence 0.7: # 置信度阈值低于 0.7 视为误检 continue box detections[0, 0, i, 3:7] * np.array([w, h, w, h]) x1, y1, x2, y2 box.astype(int) # 第三步裁剪人脸 → 灰度化 → 缩放 → 归一化 face frame[y1:y2, x1:x2] if face.size 0: continue gray cv2.cvtColor(face, cv2.COLOR_BGR2GRAY) gray cv2.resize(gray, (48, 48)) # 匹配训练输入尺寸 gray gray.astype(float32) / 255.0 # 归一化到 [0,1] input_data np.expand_dims(np.expand_dims(gray, -1), 0) # 第四步预测并取最大概率类别 preds emotion_model.predict(input_data)[0] label emotion_labels[np.argmax(preds)] results.append((x1, y1, x2, y2, label, float(np.max(preds)))) return results这段代码里最容易出问题的是(104.0, 177.0, 123.0)这个三元组它是 SSD 模型训练时图片的 BGR 均值做均值减除用的。这个值不能随便改也不能省略它是 Caffe 模型的固定参数换别的检测器就得换对应的均值。检测循环里confidence 0.7这个阈值是可以调的摄像头离人远、画面模糊的时候降到 0.5 能多召回一些人脸但误检也会增多看你现场演示的环境来定。灰度化在cvtColor这一步完成这也是情绪识别和普通图像识别不一样的地方——FER 类模型大多在灰度图上训练你的推理输入也必须转灰度否则颜色通道对不上模型会拿训练时没见过的东西来预测准确率直接掉下去。4. 从零跑通环境依赖配置与两次冒烟测试4.1 安装阶段Python 版本与 pip 依赖这份项目对硬件没有硬性要求CPU 就能跑但依赖库的版本要当心。我用 Python 3.8 和 3.9 都验证过这条路推荐直接按下面的命令装# 建议用虚拟环境别污染系统 Python python -m venv fer_env # Windows 激活fer_env\Scripts\activate # Linux/macOS 激活source fer_env/bin/activate pip install opencv-python4.5.5.64 pip install tensorflow-cpu2.10.0 pip install numpy1.23.5 pip install matplotlib版本这里我踩过坑TensorFlow 2.10 是最后一个原生支持 Windows GPU 的版本之后的版本在 Windows 上只能用 CPU而 numpy 1.24 以上跟 TensorFlow 2.10 有兼容告警虽然在大多数机器上能用但期末演示现场出个红字告警很影响心态锁死 1.23.5 最稳。opencv-python我建议用 4.5.x因为cv2.dnn.readNetFromCaffe这个接口从 4.0 到 4.8 行为一致没必要追新版。装完别急着跑完整项目先验证所有 import 能过python -c import cv2; print(cv2.__version__) python -c import tensorflow as tf; print(tf.__version__)我见过太多人卡在ImportError: No module named cv2上90% 的原因是装到了另一个 Python 环境里。终端里which python和which pip指向同一个解释器才是环境干净的唯一标准。4.2 首次运行先做两个冒烟测试直接跑use.py风险比较大因为你不知道是图像读取的问题、模型加载的问题还是摄像头驱动的问题。我一般习惯分两步冒烟先跑单张图片再跑摄像头。这样出了问题能快速定位在哪一层。第一步是测试人脸检测器和情绪模型是否都能加载。写一个一行命令随便拿一张含人脸的图片试试python use.py --image test.jpg如果项目自带use.py的入口是这种命令行参数形式这一步就能验证整条静态管线。第二步测试摄像头python use.py --camera 0这里0代表第一个摄像头设备号笔记本内置摄像头一般就是 0外接 USB 摄像头有时是 1。如果--camera 0打开报错试试--camera 1这是最常被忽略的硬件细节。4.3 摄像头模式下的预处理顺序与显示设置运行实时模式之前有一个顺序容易搞反是先cap.read()读帧还是先做检测正确顺序是先cap.read()拿当前帧再对帧跑检测。同时注意 OpenCV 的VideoCapture默认读出来是 BGR 顺序而训练 CNN 时如果你的训练代码用了cv2.imread读图那么推理时也保持 BGR 即可不需要转 RGB。显示结果时还有两个小细节。一是cv2.putText只能显示英文字符emoji 和中文标签都会变成问号所以emotion_labels最好用英文答辩时口头翻译就行。二是把置信度一并画在框上比较加分cv2.putText(frame, f{label}: {conf:.2f}, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2)现场演示时如果画面里有多张脸检测循环会依次给每个人脸画框并标注情绪这一下就能看出两级管线的好处——分类器和检测器完全解耦加一个人脸只是多跑一次分类不会影响检测。5. 避坑人脸情绪识别项目里的常见问题与排查记录5.1 现象import cv2 直接报错 ModuleNotFoundError原因最典型的是电脑里装了多个 Pythonpip install opencv-python装到了一个环境运行脚本时用的却是另一个环境其次是装成了opencv-contrib-python与原有opencv-python冲突两个包同时存在会相互覆盖。解决先统一到一个虚拟环境里。创建并激活虚拟环境后用python -m pip install opencv-python而不是pip install前者能确保装进当前解释器。装完后在同一个终端里运行python -c import cv2验证不要换终端验证换终端可能又切到别的环境了。5.2 现象程序能跑但无论什么图片都输出同一个情绪标签原因这是情绪识别最经典的翻车点几乎可以确定是预处理链路断裂。最常见的有三种一是忘了转灰度直接把彩色图喂给模型二是 resize 尺寸不对模型训练用的是 48×48你喂进去 224×224三是归一化忘除 255像素值范围停留在 0~255 而模型预期是 0~1。解决按推理管线逐步打印中间结果排查。把gray.shape打出来确认是(48, 48)把input_data.min()和input_data.max()打出来确认在 0~1 之间如果你是用cv2.imread读图再cv2.cvtColor转灰度确认图片没有因为路径问题读到空数组。这个排查过程也推荐写进答辩文档里老师非常吃这套。5.3 现象加载 fer-1.h5 时提示 Unknown layer 或无法载入原因fer-1.json记录的网络结构里包含了某个自定义层或者版本特定的默认参数当前安装的 TensorFlow 版本太新把老版本的行为改了。典型的是从 TF 2.10 换成 TF 2.13 后部分 Keras 层的序列化格式不兼容。解决严格按 4.1 的版本清单安装TF 2.10 是兼容性最好的版本。如果换了版本还是报错退而求其次用load_model直接加载 h5因为model.save()写的 h5 自带完整结构不需要 json 也能重建。当年的老代码习惯把 json 单独拎出来新 TensorFlow 已经不太推荐这种用法了。5.4 现象摄像头预览卡顿或者跑了几分钟后内存暴涨原因卡顿通常是因为没有复用模型实例而是在循环里反复model_from_json加载网络每帧都重新初始化一次图结构CPU 直接被拖死内存暴涨则是因为读取的帧没有释放或者cap.read()失败时没有处理。解决把模型加载全部提到循环外面循环内只做face_net.forward()和emotion_model.predict()。还要注意predict在循环里会累积计算图调predict_on_batch可以避免一部分内存开销或者用with torch.no_grad()类似的上下文针对 PyTorchKeras 这边更简单的做法是每 100 帧手动gc.collect()一次。最后加一个判断如果cap.read()返回 False执行cap.release()并退出循环防止黑屏帧无限堆积。5.5 现象检测框在人脸上乱跳或者框住了脖子和肩膀原因SSD 人脸检测器训练数据以正脸为主侧脸、低头、戴帽子时它的回归框会不稳定给分类器送进去的半张脸自然预测不准。另一个原因是置信度阈值设太低0.3 以下会放过大量背景误检。解决把置信度阈值从 0.5 提到 0.7 甚至 0.8宁可不检测也不要误检。同时做一个帧间平滑记录上一帧人脸框位置当前帧检测到的框如果离上一帧超过 50 像素判定为跳变用上一帧位置顶替一次。这个小技巧能显著提升演示效果而且代码量不过十行。6. 进阶把期末项目的精度再往上推一档如果你想让这个项目从「能跑」变成「能打」以下三个改动性价比最高。第一个是做数据增强的强化训练在train.py里把rotation_range从 15 改成 30再加一个width_shift_range0.2让模型见过更多歪头、偏移的人脸实测对侧脸鲁棒性提升明显。第二个是断点续训如果训练中途断电或崩溃用model.load_weights(fer-1.json)读回权重再调用model.fit时传initial_epoch参数继续从上次的 epoch 开始不用全部从头再来。第三个改动最实用加一个评估模式让你知道模型在图片集上的真实准确率。很多期末演示只看摄像头里三五张脸的即时反馈说服力有限。写一个几十行的脚本遍历测试集图片统计每个类别的precision和recall把这个结果打出来贴进报告里比任何口头描述都硬。我自己当年在这个项目上栽过一个跟头训练时准确率已经到了 0.93一上摄像头就掉到 0.6 以下后来发现是摄像头采集的 BGR 图没转灰度而训练数据是灰度图。从那以后我每次跑模型都强制先跑一遍单张图片测试确认预处理链路全通再开摄像头几分钟的事省掉几小时的无头排查。这份项目代码框架是完整的你只要顺着前面的路径跑通一遍再按这章的建议做一两个小改动期末展示稳当。希望帮到你。本文还有配套的精品资源点击获取