
简介基于深度学习TensorFlow与PyQt5实现的人脸表情识别毕业设计项目面向计算机相关专业正在完成大作业、毕业设计的本科生以及需要项目实战练习的学习者。项目包含完整的人脸检测与表情分类流程利用级联分类器定位人脸调用训练好的权重模型完成开心、中性、惊讶、难过、愤怒等表情识别并通过PyQt5构建GUI界面支持摄像头实时识别与静态图片测试。资源包共22个文件以Python源码界面逻辑、主流程、摄像头线程、h5权重文件、png表情图标、XML人脸检测模型、UI界面描述文件为主附使用说明与依赖环境清单整体体积约13.36MB。所有源码均经本地编译调试并获导师认可评审分98分难度适中可直接运行目前已有59人学习下载解压后按说明部署环境即可使用也便于在现有代码基础上二次开发。1. 一套能直接答辩的TensorFlow人脸表情识别工程长什么样临近交毕设文档的时候最怕的不是模型效果差而是演示环节界面卡死、摄像头黑屏。这套基于TensorFlow和PyQt5的人脸表情识别源码恰好把最容易翻车的三块都补齐了weight.h5模型权重、haarcascade_frontalface_default.xml人脸检测器、Camera_Thread_class.py摄像头线程。工程里还带mainwindow2.ui、表情png图标和测试图片不用自己拼界面启动入口是mainfile.py。它的核心链路很清晰OpenCV定位人脸TensorFlow模型输出五类表情PyQt5把结果实时画到窗口上。适合正在做大作业或毕业设计的学生也适合想从终端推理跨到GUI工程的学习者。难度偏向中等但把“识别”和“界面”连起来这一步就是这个项目的含金量所在。2. weight.h5与TensorFlow模型从权重快照到五类表情推理管线表情识别可以拆成“人在哪里-脸长什么样-表情是什么”三段前两段不需要深度网络OpenCV的Haar特征就可以完成。真正决定精度的是第三段weight.h5里保存的TensorFlow模型权重。拆这个项目的第一步不是打开GUI而是先搞清楚模型结构和输入约定。2.1 五类标签与模型输出层设计从emoji_pics目录里的png文件名能直接看出训练时的标签集合angry、happy、neutral、sad、surprise恰好是5类。所以模型最后全连接层应该是5个神经元激活函数用softmax输出是“属于每一类的概率”。预测时取argmax拿到索引再通过标签表映射成文字才不会在UI里张冠李戴。索引标签对应图标文件0angryemoji_pics/angry.png1happyemoji_pics/happy.png2neutralemoji_pics/neutral.png3sademoji_pics/sad.png4surpriseemoji_pics/surprise.png标签顺序要与训练时保持一致这是最容易踩的坑。如果项目自带使用说明.txt第一步先看它是否标注了类别顺序没有标注的话就按上面的索引顺序对照代码里的labels数组常常会发现有人把fear也混进来导致softmax输出维度对不上。2.2 人脸检测与预处理Haar级联的参数边界weight.h5本身不知道人脸在哪需要先靠haarcascade_frontalface_default.xml框出人脸。实际使用OpenCV的CascadeClassifier加载然后对灰度图调用detectMultiScale。import cv2 face_cascade cv2.CascadeClassifier(haarcascade_frontalface_default.xml) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) faces face_cascade.detectMultiScale( gray, scaleFactor1.1, minNeighbors5, minSize(48, 48) )这里的参数说明一下scaleFactor是每次缩放搜索窗口的比例1.1表示每次缩小10%数值越小检测越慢、漏检越少minNeighbors是候选框需要满足的邻近框数量调大到6~7能过滤掉一部分误检但也可能把侧脸丢掉minSize限制最小人脸尺寸如果窗口比输入到模型的人脸还小resize后信息量不够所以直接设成48x48最稳妥。2.2.1 人脸框的扩展和裁剪Haar框出来的人脸往往贴着皮肤眉毛和嘴巴边缘容易被切掉。常见做法是把框向外扩展20%再裁剪。要注意不能扩出图像边界clip一下就好x, y, w, h faces[0] margin int(0.2 * w) x0 max(0, x - margin) y0 max(0, y - margin) x1 min(img.shape[1], x w margin) y1 min(img.shape[0], y h margin) face_roi gray[y0:y1, x0:x1]2.2.2 灰度图resize与归一化常见卷积输入是48x48单通道所以把裁剪后的人脸resize到这个尺寸再除以255归一化。有些模型训练时用的归一化范围是[-1, 1]也就是face_roi / 127.5 - 1如果按[0,1]预测效果明显偏差就改成后一种再试。2.3 加载weight.h5并跑通单张图片拿到weight.h5要先确认它是完整模型还是纯权重。直接尝试load_model如果报错或打不出模型结构就说明它只是权重快照需要在mainfile.py里找到构建模型的部分先建同结构模型再load_weights。from tensorflow.keras.models import load_model import numpy as np import cv2 # 完整模型直接加载 model load_model(weight.h5) # 只是权重先重建网络结构再加载权重 # model build_model() # model.load_weights(weight.h5) img cv2.imread(imgs/coffee.jpg) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) faces face_cascade.detectMultiScale(gray, 1.1, 5, minSize(48, 48)) _, x, y, w, h faces[0] face_roi gray[y:yh, x:xw] face_roi cv2.resize(face_roi, (48, 48)) face_roi face_roi.astype(float32) / 255.0 face_roi face_roi.reshape(1, 48, 48, 1) pred model.predict(face_roi, verbose0)[0] class_idx int(np.argmax(pred)) labels [angry, happy, neutral, sad, surprise] print(labels[class_idx], pred[class_idx])注意reshape时维度顺序是(batch, height, width, channels)PyQt5里的图像是RGBOpenCV是BGR但灰度图不涉及通道顺序问题。模型输入要是3通道彩色图就把gray[y:yh, x:xw]换成img[y:yh, x:xw]reshape最后一位改成3。预测概率confidence如果普遍低于0.4基本可以判断是预处理规范和训练时不一致优先检查归一化范围。2.3.1 只有权重时重建模型结构如果weight.h5用load_model报错只看得到权重字典就需要按原来的训练代码重建结构。以48x48灰度输入为例表情识别常用的结构是若干卷积池化层后接全连接from tensorflow.keras import Sequential from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout def build_model(): model Sequential([ Conv2D(32, (3, 3), activationrelu, paddingsame, input_shape(48, 48, 1)), MaxPooling2D((2, 2)), Conv2D(64, (3, 3), activationrelu, paddingsame), MaxPooling2D((2, 2)), Flatten(), Dense(128, activationrelu), Dropout(0.5), Dense(5, activationsoftmax) ]) return model第一层input_shape必须与训练时一致Dense(5)对应表情类别数Dropout只在训练时生效不影响加载权重。如果load_weights时提示shape mismatch多半是输入尺寸或最后一层类别数对不上。项目里如果有model summary或使用说明里写了input shape以那份为准。3. PyQt5界面与Camera_Thread_class.py实时显示不卡顿的线程设计GUI程序直接在主线程里用OpenCV循环读摄像头是大忌read()会阻塞事件循环拖动窗口就白屏。这个项目里单独放了Camera_Thread_class.py目的就是把取帧放到QThread里。界面本身在mainwindow2.py里实现线程只负责源源不断把帧发出来。有的初级版本在主线程里用QTimer每30ms调一次cap.read()摄像头响应快但一旦在回调里做模型推理界面事件循环被阻塞窗口拖动、按钮点击都会卡。QThread方案把取帧和推理全部移出主线程主线程只接收结果这才是PyQt5 GUI稳定工作的前提。3.1 mainwindow2.ui与mainwindow2.py分工mainwindow2.ui是用Qt Designer画好的界面布局包括摄像头显示区、按钮、表情文本或图标。运行时可以动态加载ui文件也可以用pyuic5转换成mainwindow2.py。两种方式没有对错但动态加载利于改布局转成py文件则方便直接引用控件变量。项目里两个文件都存在说明原始工程可能用Designer改完再转py。窗口里的背景图、按钮样式和emoji_pics图标都在designer里指定代码里只需要操作对象名。3.2 摄像头线程核心实现看命名Camera_Thread_class.py本质上就是下面这个结构的封装import cv2 from PyQt5.QtCore import QThread, pyqtSignal class CameraThread(QThread): frame_ready pyqtSignal(object) def __init__(self, camera_index0): super().__init__() self.camera_index camera_index self.running True def run(self): cap cv2.VideoCapture(self.camera_index) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) while self.running: ret, frame cap.read() if not ret: continue self.frame_ready.emit(frame) cap.release() def stop(self): self.running False self.wait()frame_ready是一个pyqtSignalemit出的frame是numpy数组。PyQt的跨线程信号槽会自己切到主线程所以后面连接的槽函数里可以直接操作QLabel不会出现“Cannot create children for a parent that is in a different thread”这种报错。3.2.1 低速识别场景下的帧丢弃如果在线程里同时做人脸检测和TensorFlow推理一帧可能要100ms以上640x480帧源源不断塞给主线程会让队列堆积。常见做法是在线程里把推理做完只往主线程发“画面结果”两样东西不要在帧信号里再做识别。也可以加一个标志位处理完一帧再去read下一帧while self.running: ret, frame cap.read() if not ret: continue frame self.process_frame(frame) # 返回带识别结果的画帧 self.frame_ready.emit(frame)这样就不会丢帧收到的每一帧都是已经画好框和文字的。3.2.2 线程退出策略在closeEvent里调用stop否则程序退出后摄像头可能仍被占用。stop()里先置running为False再wait()等待run退出。注意不要在线程run里调self.wait()会造成死锁。3.3 从按钮点击到表情回显的数据流mainwindow2.py里启动按钮的槽函数一般只做三件事建线程、连信号、start。帧信号到达后先处理灰度、人脸检测、模型推理再把表情结果用QLabel显示出来。下面是一段可直接替换的update_frame槽实现from PyQt5.QtGui import QImage, QPixmap def update_frame(self, frame): gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) result_frame, label self.run_face_expression(gray, frame) self.result_label.setText(label) self.emoji_label.setPixmap(QPixmap(femoji_pics/{label}.png)) rgb cv2.cvtColor(result_frame, cv2.COLOR_BGR2RGB) h, w, ch rgb.shape image QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888) self.camera_label.setPixmap(QPixmap.fromImage(image))这里run_face_expression内部是第2章写的检测、预处理、predict结果帧里已经带框和文字主线程只负责显示和图标切换。注意QImage构造时传入的bytes数据要和rgb声明周期同时建议把rgb存到self.last_rgb防止PyQt显示时引用失效。控件对象名常见命名类型作用camera_labelQLabel显示摄像头实时画面result_labelQLabel显示当前表情文字emoji_labelQLabel显示对应的表情png图标start_buttonQPushButton启动/停止摄像头线程quit_buttonQPushButton退出程序这套结构和mainwindow2.ui对得上实际操作时只改控件对象名就行。mainfile.py是入口文件负责创建QApplication、加载窗口并显示mainwindow2.ui是designer源文件不直接执行。4. 环境搭建与版本匹配TensorFlow、PyQt5、OpenCV的兼容区间这套工程能不能一次跑起来多半看环境。有人用Python 3.12装TensorFlow又pin一个TensorFlow 2.16后PyQt5的DLL炸了。下面是我在自己机器上复现时用的组合优先级是“TensorFlow可用 OpenCV可用 PyQt5可用”而不是全装最新版。4.1 requirements.txt之外的版本对照requirements.txt里列的是项目作者本机的依赖直接照装可能踩到坑。更稳的做法是按表格里的组合手动指定。依赖包推荐版本备注python3.9与tensorflow和pyqt5兼容面最宽tensorflow2.10.0Windows下原生GPU的最后一个稳定版本PyQt55.15.95.15.x系列稳定pyqt5-qt55.15.19注意与PyQt5版本配套opencv-python4.8.0.74提供cascade和VideoCapturenumpy1.24.3兼容tensorflow 2.10h5py3.8.0读取weight.h5的底层库特别注意TensorFlow 2.10是Windows原生GPU的最后一个稳定版本2.11开始Windows GPU支持转移到WSL所以老项目用2.10最省事。PyQt5和pyqt5-qt5版本要配套出现过PyQt5 5.15.10配pyqt5-qt5 5.15.2后加载失败的案例。4.2 用Anaconda创建隔离环境并配置PyQt5先建环境再装包避免污染base环境。conda create -n face python3.9 -y conda activate face pip install -r requirements.txt pip install PyQt55.15.9 pyqt5-qt55.15.19 pip install tensorflow2.10.0 pip install opencv-python4.8.0.74 numpy1.24.3 h5py3.8.0如果requirements.txt里已有PyQt5直接install会把最新版拉进来可能破坏上面约束。建议先装requirements再用上面命令覆盖关键包。另外requirements.txt里如果出现pyqt5-qt55.15.19 registryhttps://pypi.tuna...这样的PEP 508写法pip解析时可能卡住把它换成普通pyqt5-qt55.15.19就好。在PyCharm里把Project Interpreter切到conda face环境然后直接运行mainfile.py。4.3 模型加载失败和摄像头打不开的定位手段运行时报错分三类先说现象再给判断方式。第一类ValueError: Unable to synchronously read attribute from HDF5 file。说明h5文件不是完整的Keras模型文件。用h5py看顶层键确认它只是权重字典。import h5py with h5py.File(weight.h5, r) as f: print(list(f.keys()))有model_config或model_weights说明是完整模型只看到层名开头的data组就必须走load_weights分支。第二类DLL load failed: The specified procedure could not be found。通常是opencv-python或PyQt5的C扩展和Python版本不匹配。在conda环境里重装对应版本即可别混用pip和conda。第三类摄像头黑屏但程序不崩。检查cap cv2.VideoCapture(0)是否返回False返回False说明编号不对。换1、2或直接传摄像头设备路径再试。另外有的电脑自带相机被隐私设置禁用了需要到系统设置里打开“桌面应用可访问相机”。这套工程里mainfile.py读写文件用的是相对路径所以工作目录必须放在项目根目录。在PyCharm Run Configuration里把Working directory指向解压目录否则找不到weight.h5和xml。5. 让答辩演示更稳的验证路径从静态图到实时叠加先不要直接上摄像头。把模型、UI、线程都分开验证出问题好定位。5.1 先用测试图把模型链路钉死项目里有imgs/coffee.jpg和img.png可以先写一个小脚本只跑第2章的检测和推理确认能输出“happy”这类结果。只要静态图能出标签说明weight.h5和预处理没问题后面调试GUI时不会怀疑模型。这一步如果发现置信度都在0.3以下优先怀疑resize尺寸和归一化范围。5.2 把识别结果直接画到每一帧画面上很多模板只在UI上放文字演示时观感很干。把结果框和标签画在原图上在摄像头线程里做掉UI只负责显示效果立刻不一样。def process_frame(self, frame): gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces face_cascade.detectMultiScale(gray, 1.1, 5, minSize(48, 48)) label unknown for (x, y, w, h) in faces: face_roi cv2.resize(gray[y:yh, x:xw], (48, 48)) face_roi face_roi.astype(float32) / 255.0 pred model.predict(face_roi.reshape(1, 48, 48, 1), verbose0)[0] label labels[int(np.argmax(pred))] cv2.rectangle(frame, (x, y), (xw, yh), (0, 255, 0), 2) cv2.putText(frame, label, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) self.expression_signal.emit(label) # 让主线程切换emoji图标 return frame这里在同一个循环里做了检测和推理process_frame返回后一帧才送出去相当于强制“拿完一帧再做识别再显示”不会越积越多。多人脸时for循环会画出多个框UI里emoji只显示最后一个框的label演示时站远一点只看单人更稳。如果觉得模型预测太慢可以只对固定区域做推理先让Haar找脸没有脸直接跳过不做predict。这套工程的静态图路径验证好后摄像头路径只是把输入从imread换成cap.read再用信号槽和emoji_pics的setPixmap联动就是完整的答辩演示了。本文还有配套的精品资源点击获取