YOLOv5 7.0与PyQt5表情识别实战:从目标检测到桌面应用
2026/9/12 22:00:49 网站建设 项目流程

简介:面向毕业设计、课程设计与期末大作业等场景,提供一套基于YOLOv5 7.0版本的人脸表情识别完整项目,配套PyQt5图形界面,可直接运行。项目包含全部Python源码与说明文档,关键代码附注释,便于新手理解目标检测、表情分类、模型配置与界面交互的衔接方式;适合计算机、电子信息等专业学生作为高分项目参考。压缩包共2000个文件,以1930个txt文本(日志、说明与数据标注等)、39个Python脚本(模型训练/检测/界面逻辑)、20个YAML配置、6个Shell脚本以及4个Markdown说明为主,另含JSON文件,包体约227.47MB,结构清晰且便于按模块部署调试。已有222人学习下载,项目经过严格调试,可快速搭建本地环境,体验完整的人脸表情识别流程并在此基础上扩展功能。

1. YOLOv5 7.0 + PyQt5 表情识别:这个高分项目到底在做什么

拿人脸表情识别做毕业设计或小项目的人很多,但大部分都停在“笔记本里跑个demo、按个空格看摄像头窗口”的级别。这个标题里的关键词其实是两个时间节点:YOLOv5 7.0 是官方仓库发布的最终大版本,PyQt5 负责把模型从命令行搬到桌面应用。前者解决“人脸在哪”,后者解决“人怎么用”,中间还有一条不少初学者会忽略的链路:YOLOv5 只负责检测人脸框,框中人的情绪分类必须靠第二级网络完成。从这个思路往下走,你得到的不是一个“调包演示”,而是一套能自然衔接训练、推理、界面交互的完整工程骨架,这也正是项目说明里“高分”两个字的分量所在。


2. YOLOv5 7.0 网络结构变化与表情识别的模型分工

2.1 为什么 7.0 值得单拿出来讲两分钟

YOLOv5 从 6.0 到 7.0 的跨度比很多教程里描述的“版本号 +1”要大得多。7.0 在主干网络的 PANet 结构里引入了基于注意力机制的特征融合模块 C2PSA,把原来 PANet 里标准 C3 模块替换成了带自注意力的变体;同时在数据增强、检测头格式、OBB 旋转框支持等方面做了改动。一个最直接的影响是:如果你手里有 old.pt(3.0~6.x 版本训练的权重),拿到 7.0 代码里加载,往往会提示结构不匹配,甚至在某些自定义类别上推理出现类别错乱。这不是权重坏了,是模型结构定义变了。

这几处结构改动对表情识别项目意味着什么?你的Backbone能提取得更细、人脸小目标召回更稳,但计算量也略有上升。实际跑出来的效果是,在离摄像头 2~3 米的中距离下,7.0 的人脸框比 6.2 更少漏检,这对后续靠框内像素做表情判断非常关键。如果框都没框准,眉毛和嘴巴区域切不齐,第二级的分类准确率会直接崩掉。

2.2 检测与识别必须分离:YOLOv5 不会“一眼看出你是笑了还是皱眉”

标题叫“人脸表情识别”,但 YOLOv5 7.0 从原理上是一个目标检测器,它的输出是边界框坐标、置信度和类别 ID。让它直接输出“happy”或“angry”不是做不到,而是你需要准备足够多的表情位置标注数据去训练,成本远高于单独做一个表情分类器。

工程上更好的分工是:

环节承担模型输出内容数据要求
人脸定位YOLOv5 7.0每个人脸的 x, y, w, h + confidence一张图里有人就行,类别 1 个
表情判断小型 CNN 或预训练分类器angry / disgust / fear / happy / sad / surprise / neutral 共 7 类概率裁剪对齐的人脸图片

这种设计的好处是两套模型可以独立训练、独立更换。人脸检测用通用权重做迁移(比如先用 COCO 预训练,再用 WIDERFACE 或自己拍的人脸数据微调),表情分类器单独在自己的数据集上训练。换一套更轻量的分类网络也不需要动 YOLO 的代码,两个模块的耦合只有一个“先检测再裁剪”的数据流关系。

2.2.1 表情分类器怎么选,一个 5 年经验的人会怎么定

我不会一上来堆一个大网络。常见做法是拿 ResNet18 或者更轻的 MobileNetV3 做骨干,输入统一缩放到 48×48 或 64×64,全连接层输出 7 类。为什么是 48×48?因为公开表情数据集(如 FER2013、RAF-DB)的原始分辨率大多很低,强行放大到 112×112 只会引入插值噪声。分类精度在验证集上做到 85%~90% 并不难,难点是“摄像头实时流的准确率”和“静态图片测试的准确率”之间通常有 5~10 个点的落差,因为视频帧里有运动模糊和姿态偏转。处理办法不是换更大的模型,而是在 YOLOv5 的人脸框输出后,做一步简单的关键点对齐,或者至少按框的宽度高比例做 0.2 的向外扩充,避免裁剪框把额头和下巴边缘切掉太多。

2.3 YOLOv5 7.0 的检测头改动对推理代码的影响

7.0 版本里检测头的输出格式增加了更多元信息,PyTorch 层面直接 forward 出来的结果是 List[Tensor],每个 tensor 的 shape 是[batch, num_anchors, 5+num_classes],其中前五项是中心点 x、中心点 y、宽 w、高 h 和对象置信度。这两个坐标是中心点形式,转成矩形框时要用xywh2xyxy这类工具函数处理。

如果你以前用的是 YOLOv5 5.0 或者 6.0 的推理脚本,直接在 7.0 上跑会遇到的典型错误是:

# 7.0 里 Tensor 最后一维不再是 [x, y, w, h, conf, class...] 的旧顺序 # 如果你按 6.x 的习惯写 boxes[:, 0] 拿 xmin,大概率得到的是中心点 x

这一行代码说的是坐标格式差异在 7.0 和 6.x 之间没有那么恐怖,但新手最容易犯的错误是把检测框直接当做人脸分类网络的输入尺寸。yolo 框是矩形带长宽比的,直接 resize 到正方形送入分类器会让脸部比例失真,影响表情判定的形状特征。我会在裁完人脸后做一次保持比例的 letterbox 填充,而不是粗暴拉伸,细节后面代码里给。


3. 环境搭建与命令行推理:先跑通 YOLOv5 再写界面

3.1 用 conda 固定 Python 与 PyTorch 版本是省时间的关键

YOLOv5 官方推荐的 Python 版本是 3.8 到 3.11,PyTorch 建议从 1.8 到 2.x 都可以。但这不是“随便一个版本都能装成功”的意思。2025 年这时候装环境最稳的组合是 Python 3.10 + PyTorch 2.1 或 2.2 + CUDA 12.1。PyQt5 这边注意一个点:PyQt5 从 5.15.9 之后就不再更新新特性,但一直同步 Qt 5.15 系列的 patch,Python 3.12 下有些版本会缺编译好的 wheel,所以把 Python 锁在 3.10,全链路都顺。

# 创建虚拟环境,指定 Python 版本,避免和高版本系统 Python 打架 conda create -n emo-yolo python=3.10 -y conda activate emo-yolo # 先装 PyTorch,用官方 index 地址,CPU/GPU 版本在此区分 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121 # 再装 YOLOv5 的依赖 cd yolov5-7.0 pip install -r requirements.txt # 最后单独装界面库,不要放 requirements 里一起装 pip install pyqt5==5.15.11 pyqt5-tools

逻辑说明:torchtorchvision的版本必须配套,一个能用 GPU 一个不能用会让你误判为 CUDA 装好了。pyqt5-tools提供 designer.exe,你后面的 .ui 界面文件要靠它拖拽生成,纯代码写界面不是说不行,而是调试按钮布局时效率差很多。

如果 conda 从默认 channel 下载缓慢或者解析慢,换成清华的镜像源即可。这里不给镜像地址,搜索引擎一找就有。

3.2 用 CLI 跑通最小推理命令,确认模型和权重正常

环境装完不要立刻写 PyQt5 界面,先跑最原始的命令行推理,排除“模型加载失败”和“摄像头调用失败”两类问题。7.0 仓库里detect.py的参数比之前版本更多,但日常只需要关注这两个:

# 用摄像头实时检测,显示检测框,并保存带框结果到 runs/detect/ python detect.py --weights yolov5n.pt --source 0 --conf-thres 0.4 --imgsz 640 # 用单张图片检测,快速验证权重文件完整性和模型结构匹配 python detect.py --weights yolov5n.pt --source test.jpg --conf-thres 0.25 --save-txt

参数说明:--weights指定权重文件路径;7.0 的--conf-thres默认是 0.25,摄像头场景建议调到 0.4 以上,否则人脸的误检框会频繁闪烁;--imgsz控制输入分辨率,640 是速度和精度的均衡点,如果机器是 GTX 1660 级别的显卡,640 是上限,再往上帧率会跌到 10 以下。--save-txt会把检测结果导出成 txt 文件,里面一行一个人脸框坐标,这能帮你验证的代码读坐标时格式对不对,而不用依赖可视化效果。

命令行能出框,说明权重、环境、摄像头三项全部正常,下面再进界面开发就只有一个变量——PyQt5 本身。


4. PyQt5 界面接入 YOLOv5 的线程模型与信号槽设计

4.1 界面卡死的根因是推理阻塞了 UI 线程,不是代码写得慢

PyQt5 的页面刷新依赖 Qt 的事件循环。如果你的推理代码写在窗口类的某个按钮槽函数里,当模型前向传播耗时 100~200ms,这个期间界面重绘事件全部排队。表现出来就是拖动窗口时白屏、按钮点下去没反应,偶尔还会被系统判定为“程序未响应”。

正确的做法是把模型推理放进一个纯函数,通过线程把结果以信号形式发回主线程更新界面。PyQt5 里 QThread 的真正用法有两种:一个是继承 QThread 重写 run,另一个是把业务对象 moveToThread。表情识别这种场景我会选继承 QThread,因为推理任务是一次性长循环,不存在对象生命周期复杂的场景。

from PyQt5.QtCore import QThread, pyqtSignal import cv2 import torch class DetectThread(QThread): # 信号类型:原图帧、人脸框列表、表情标签列表、单帧耗时 frame_ready = pyqtSignal(object, list, list, float) def __init__(self, model, device=None): super().__init__() self.model = model self.device = device if device else ('cuda' if torch.cuda.is_available() else 'cpu') self.running = True self.cap = cv2.VideoCapture(0) # 人脸框中心点坐标需要转xyxy,供绘制用 self.transform = lambda xywh: [ int((xywh[0] - xywh[2] / 2)), int((xywh[1] - xywh[3] / 2)), int((xywh[0] + xywh[2] / 2)), int((xywh[1] + xywh[3] / 2)), ] def run(self): while self.running: ret, frame = self.cap.read() if not ret: self.frame_ready.emit(frame, [], [], 0.0) continue start_ts = cv2.getTickCount() # yolov5的推理入口,7.0里直接调用forward会拿到未后处理的结果 # 用自带的non_max_suppression处理检测头输出 results = self.model(frame, size=640) boxes, labels, confidence = [], [], [] for det in results.xyxy[0].cpu().numpy(): x1, y1, x2, y2, conf, cls = det.astype(float) boxes.append([int(x1), int(y1), int(x2), int(y2)]) labels.append("face") elapsed_ms = (cv2.getTickCount() - start_ts) / cv2.getTickFrequency() * 1000 self.frame_ready.emit(frame, boxes, labels, elapsed_ms) def stop(self): self.running = False self.cap.release() self.wait()

代码逻辑说明:frame_ready信号携带四种类型的数据,其中第一个object类型承载的是原始 numpy 帧,因为 Qt 的信号不能直接序列化 numpy 数组,要用object包装。在run()里做推理循环,每处理完一帧把结果通过emit发出去,主线程的槽函数里收到后更新 QLabel 的画面。7.0 模型的 forward 和旧版本在后处理上有差异,这里直接使用results.xyxy[0],已经是 NMS 之后的检测框列表,不需要再手写 NMS。

4.2 主窗口槽函数如何把结果画到界面上

信号发出后,主线程要做的只有三件事:把帧转成 QPixmap、把方框和表情文字画上、刷新标签。转换过程中注意一次深拷贝,因为摄像头每一帧都是复用缓冲区,如果不拷贝,下一帧数据会把上一帧覆盖。

def on_frame_ready(self, frame, boxes, labels, elapsed): if frame is None: return # 深拷贝,避免下一帧覆盖导致画面闪烁重影 display = frame.copy() for (x1, y1, x2, y2), label in zip(boxes, labels): cv2.rectangle(display, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(display, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) # BGR 和 RGB 通道顺序转换,否则 Qt 里颜色偏蓝 rgb_image = cv2.cvtColor(display, cv2.COLOR_BGR2RGB) h, w, ch = rgb_image.shape bytes_per_line = ch * w qimage = QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format_RGB888) self.video_label.setPixmap(QPixmap.fromImage(qimage).scaled( self.video_label.size(), Qt.KeepAspectRatio)) self.fps_label.setText(f"{1000 / elapsed:.1f} FPS" if elapsed > 0 else "0 FPS")

这里最难调的其实是通道顺序。YOLOv5 读进模型的是 BGR,OpenCV 存储的也是 BGR,但 QImage 的 Format_RGB888 默认是 RGB。你如果不做转换,红颜色的人脸框会显示成蓝色,表情文字也会变色。这个 bug 藏得很深,新手排查半天容易以为是检测框坐标绘制错了。

4.2.1 摄像头实时推理的显示刷新频率怎么控制

run()里的 while 循环是无限拉的,摄像头一秒钟会产生 30 帧,但模型推理速度通常在 15~20 FPS 之间,这种情况下没有必要追求“每帧都显示”。两种常见处理策略:一是循环里加time.sleep(0.02)限制到 50FPS 上限;另一种更明智:界面更新频率不需要等于摄像头帧率,frame_ready信号发出了就显示当前帧,没有发出就显示上一帧,QTimer 只负责刷新标签,不负责触发推理。

我一般会在检测线程里额外加一个简单的跳帧计数器,隔一帧推理一次。因为表情识别的分类器对相邻帧的判定结果差异很小,你不需要每一帧都算,跳帧之后 CPU 占用会从 70% 降到 40% 左右,对没有独立显卡的机器提升非常明显。

# 在 DetectThread 初始化里加一个跳帧计数器 self.skip_counter = 0 self.skip_n = 1 # 每 2 帧推理一次

在 run 循环开头判断self.skip_counter % self.skip_n == 0才走推理,否则直接把 frame 原样发出。这是一个实战向的取舍,多数教程不会告诉你默认按帧推理在低配电脑上就是灾难。

4.3 集成表情分类器:在有人脸框检测结果之后插入二级推理

到这一步,YOLOv5 的人脸框已经画到界面上了,剩下就是把每个框裁下来送给表情分类网络。

def infer_emotion(self, frame, boxes): emotions = [] for x1, y1, x2, y2 in boxes: # 扩边 10%,避免裁剪太贴脸;框若有缺失,用 clamp 限制边界 w, h = x2 - x1, y2 - y1 expand_x, expand_y = int(w * 0.1), int(h * 0.1) x1e, y1e = max(0, x1 - expand_x), max(0, y1 - expand_y) x2e, y2e = min(frame.shape[1], x2 + expand_x), min(frame.shape[0], y2 + expand_y) face_crop = frame[y1e:y2e, x1e:x2e] # 保持宽高比缩放到 48x48,剩余区域填 gray resized_face = self._letterbox(face_crop, (48, 48)) tensor = torch.from_numpy(resized_face).permute(2, 0, 1).unsqueeze(0).float() / 255.0 with torch.no_grad(): pred = self.emotion_model(tensor.to(self.device)) emotions.append(self.emotion_classes[pred.argmax(dim=1).item()]) return emotions

需要注意的细节是:表情分类模型和 YOLO 检测模型是两个独立的模型,它们共享同一个 device 没有问题。但两个模型同时加载在显存里,如果你的卡显存是 4GB,最好让小分类器跑 CPU。不然显存爆掉会出 OOM,且 OOM 出现后 PyQt5 界面会直接卡死,异常不好捕获。


5. YOLOv5 推理与 PyQt5 联调的 3 个参数与常见的坑

5.1 三个必调参数:conf-thres、iou-thres、imgsz

这三个参数在detect.py里都有对应,换成界面推理后,它们变成设置项写到界面里,和信号槽体系配合。

参数推荐范围表情识别场景怎么设设错的典型症状
conf-thres0.25 ~ 0.50.35 左右,过高会漏检侧脸人脸框乱跳、时有时无
iou-thres0.25 ~ 0.7默认 0.45 即可同一人脸画两三个框
imgsz320 ~ 640机器太老用 480,高于 640 收益不明显显存不足或帧率掉一半

一个经验:摄像头场景下字体识别只是辅助,把 conf-thres 调高一点换取的稳定框优先级更高,因为表情分类器对“框的稳定性”依赖很强。框一旦抖动,裁剪出的人脸内容前后帧变化大,分类结果在开心和中性之间反复横跳,观感上就是“模型不准”。

5.2 四个常见的集成坑

坑一:ONNX 导出的模型在 PyQt5 里不能用 torch 的 forward。有些代码为了加速把 YOLOv5 转换成 ONNX,再通过 onnxruntime 推理。如果你的.pt是从 7.0 仓库导出的 ONNX,TensorRT 和 PyQt5 侧的加载方式完全不同,很容易出现“命令行能跑,界面程序却报段错误”。我一般不在桌面应用里用 ONNX 加速,因为 YOLOv5n 模型在 PyTorch 里已经足够快到实时,换 ONNX 省 10ms,但调试链条长一倍。

坑二:中文路径会导致权重加载失败。项目交付时如果文件夹放在D:\桌面文件\毕业设计\人脸表情识别(最终版)\,权重文件里的split路径处理在 Windows 上可能因为中文字符编码崩掉。解决方案很简单:项目根目录和所有模型文件路径全部用英文,界面标题可以写中文,但工作路径不要有中文。

坑三:PyQt5 的sip版本和系统 Python 冲突。装完 PyQt5 导入时如果报TypeError: 0x10020: cannot call...,多半是 PyQt5 和 PyQt5-sip 版本不匹配。直接升级pip install -U PyQt5-sip即可,这个坑在 Python 3.10 + PyQt5 5.15.11 的组合下很少出现,但一旦出现就要优先查 sip 而非重新装 PyQt5。

坑四:摄像头关闭后再打开失败。DetectingThread 里 stop 之后如果你再次启动检测线程,cv2.VideoCapture(0)会偶发获取失败,原因不是摄像头被占用,而是上一实例没有完全释放。在 stop 里加self.cap.release()之外,再等QThread.msleep(100)确保操作系统真正释放句柄。这个小等待的时间成本几乎为零,能省掉大量“界面第二次点开始就黑屏”的问题。

5.3 一个让毕业设计观感提升三个档次的交互技巧

前面的联调逻辑只是“能跑”,但真正做得分项目答辩时,建议在界面上加一个阈值滑块:QSlider直接绑定 conf-thres 和 iou-thres,拖动即生效,用户往左拉看到更多检出的框,往右拉框变少但更稳定。这比固定写死参数显得专业得多。

下面是滑块的信号连接核心的一行逻辑:

# 滑块值变化时只更新线程里的推理参数,不重启线程 detect_thread.set_param(conf_thres=float(slider.value() / 100))

set_param的实现就是在DetectThread里留一个成员变量,NMS 时读它。这个变量不需要加锁,因为 Python 的 GIL 保证了赋值原子性。需要注意的只有一点:检测线程里读到的值是界面线程最新写入的一次,无需用mutex,这是 Python 特性给的便利,放到 C++ 里就一定要加锁了。

最后值得单独说明的是:整个项目能拿高分,本质是 YOLOv5 7.0 的检测精度和 PyQt5 桌面交互的完成度都被验证过。你不需要换更重的模型,不需要加 TensorRT,也不需要做表情数据集的手工标注——用现成的公开数据集,再把上面这套线程模型和参数调稳,从“能跑”到“稳跑”的距离就补齐了。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询