
简介本资源是一套基于YOLOv11的人脸检测与表情识别系统完整源码与配套文档面向计算机视觉学习者、深度学习入门者及需要快速搭建表情识别演示的开发者。系统采用YOLOv11完成人脸定位并用自定义训练的YOLO模型识别愤怒、厌恶、高兴、中性、悲伤、惊讶六种基本表情支持图像、视频文件与实时摄像头三种输入方式界面基于PyQt5开发并支持暗色主题同时集成综合数据集模型与FER2013增强模型等多套权重。压缩包共1022个文件约58.53MB包含190个Python源码、97个YAML配置、59个pt权重、395个Markdown说明文档以及少量C、Rust、Dockerfile等部署相关文件覆盖训练、推理与容器化运行环节。目前已有166人学习下载。读者可获得端到端可运行工程、多模型推理脚本、界面代码与详细文档便于二次开发与课程实践。1. 从一张 1080p 摄像头画面说起YOLOv11 人脸检测与表情识别到底能落地到什么程度很多人第一次接触「基于 YOLOv11 的人脸检测与表情识别系统」这个标题脑子里浮现的是实验室 demo一张正脸、均匀光照、七类表情分类准确率 95%。但真正把摄像头接上、把模型塞进产线或门店终端之后你会发现翻车点根本不在分类头而在人脸框抖动、侧脸漏检、光照突变和推理帧率掉到 8 FPS。这个方案要解决的核心问题其实很具体用一套 YOLOv11 权重同时完成人脸定位和表情分类支持图像、视频文件、实时摄像头三种输入并且允许你用自己的数据重新训练。它适合两类人——一类是想把表情识别接进考勤、课堂专注度、零售客流分析这类真实场景的工程师另一类是手里已经有一批标注好的人脸数据、想找一个能跑通训练到部署全链路的基线方案的人。读完你应该能判断这套东西值不值得投入以及投入之后第一个坑会出现在哪。2. YOLOv11 做表情识别为什么不用「检测分类」两段式而要走多任务头2.1 两段式 pipeline 的隐性成本最常见的做法是 RetinaFace 或 YOLO 人脸检测 一个独立的 CNN 表情分类网络比如 ResNet18 或 MobileNetV3。这个方案上手快但落地时会暴露三个问题。第一是延迟叠加检测 15ms、分类 8ms看起来还行但两段之间要做人脸对齐、裁剪、归一化CPU 上这一套下来经常比推理本身还慢。第二是框抖动传导检测框每帧有 2~3 像素的抖动裁剪出来的表情图就跟着变分类结果会在「中性」和「高兴」之间反复横跳视频里看起来像抽风。第三是训练数据割裂检测数据集和表情数据集往往来自不同分布检测器在侧脸、遮挡场景下框歪了分类器再准也没用。YOLOv11 的多任务头思路是把这两步合成一个前向。Ultralytics 的 YOLOv11 本身是检测头但它的 head 结构是解耦的分类分支和回归分支分开你可以在检测头旁边挂一个表情分类分支共享 backbone 和 neck 的特征。这样一次前向同时输出人脸框和表情 logits框和表情来自同一组特征抖动传导问题从根上缓解。2.2 多任务头的结构改法与损失权重具体改法是在ultralytics/nn/modules/head.py里新增一个MultiTaskHead或者更省事的做法是继承Detect类在forward里额外接一个全连接分支。下面是一个最小可用的改法基于 Ultralytics 8.x 的模块组织方式import torch import torch.nn as nn from ultralytics.nn.modules.head import Detect class DetectWithEmotion(Detect): def __init__(self, nc1, emotion_classes7, ch()): super().__init__(ncnc, chch) # 表情分支全局池化 两层全连接 c_last ch[-1] if isinstance(ch, (list, tuple)) else ch self.emotion_pool nn.AdaptiveAvgPool2d(1) self.emotion_fc nn.Sequential( nn.Linear(c_last, 256), nn.ReLU(inplaceTrue), nn.Dropout(0.3), nn.Linear(256, emotion_classes) ) def forward(self, x): # x 是 neck 输出的多尺度特征列表 det_out super().forward(x) # 原始检测输出 feat x[-1] if isinstance(x, (list, tuple)) else x emo self.emotion_pool(feat).flatten(1) emo_logits self.emotion_fc(emo) return det_out, emo_logits逻辑说明Detect的forward返回的是训练时的 tuple 或推理时的 tensor这里不改动它只在旁边加一条表情分支。emotion_pool把最后一层特征图压成向量emotion_fc输出 7 类 logits。参数上emotion_classes按你的数据集定常见 FER2013 是 7 类愤怒、厌恶、恐惧、高兴、悲伤、惊讶、中性如果只做「高兴/中性/负面」三分类改成 3 即可。Dropout(0.3)是防止表情分支过拟合人脸数据量通常比检测数据少一个量级。损失函数要改训练脚本检测损失用 YOLO 自带的v8DetectionLoss表情损失用交叉熵总损失是加权和cls_loss nn.CrossEntropyLoss(label_smoothing0.1) total_loss det_loss 0.5 * emo_loss权重 0.5 是经验值。表情分支太强会干扰检测收敛太弱则表情学不动。我一般会先冻结 backbone 训 5 个 epoch 只训表情头再解冻联合训 50 epoch。2.3 自定义训练的数据组织与最小命令数据目录按 Ultralytics 的检测格式组织表情标签单独放一个 CSV 或 JSON用图片路径做 keydataset/ images/train/ images/val/ labels/train/ labels/val/ # YOLO 格式的人脸框 emotions.csv # filename,emotion_id训练命令在改完 head 之后需要自定义 trainer 或在model.train()里传emotion_csv参数。最小可跑的命令yolo detect train modelyolo11n.yaml dataface_emotion.yaml epochs80 imgsz640 batch16 device0yolo11n.yaml里把 head 换成DetectWithEmotionface_emotion.yaml里nc: 1只检测人脸一类。imgsz640是平衡速度和精度的常用值如果只做近景人脸imgsz416能再快 30%。batch16在 8G 显存上跑 yolo11n 比较稳显存不够就降到 8。提示表情分支的输入是全局池化后的特征不是人脸区域裁剪所以训练时人脸框要尽量紧否则背景信息会污染表情特征。3. 图像、视频、摄像头三路输入的工程实现与参数调优3.1 图像推理批量处理和结果保存图像输入是最简单的场景但批量处理时容易踩显存和 IO 的坑。下面是一个批量推理并保存结果的脚本from ultralytics import YOLO import cv2, os, glob model YOLO(runs/detect/train/weights/best.pt) img_paths glob.glob(inputs/*.jpg) for p in img_paths: results model(p, imgsz640, conf0.4, iou0.5, verboseFalse) r results[0] frame cv2.imread(p) for box, emo_id, emo_conf in zip(r.boxes.xyxy, r.emotion_ids, r.emotion_confs): x1, y1, x2, y2 map(int, box) label f{EMO_NAMES[emo_id]} {emo_conf:.2f} cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, label, (x1, y1-8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,255,0), 2) cv2.imwrite(foutputs/{os.path.basename(p)}, frame)逻辑说明conf0.4是人脸检测置信度阈值比默认 0.25 高因为表情识别对人脸框质量敏感低置信框往往对应侧脸或遮挡表情结果不可信。iou0.5是 NMS 阈值人群密集场景可以降到 0.4 减少重叠框。r.emotion_ids和r.emotion_confs需要你在推理后处理里从多任务头输出解析出来Ultralytics 原生 Results 对象不包含这两个字段要在自定义 predictor 里加。3.2 视频文件帧率对齐和跳帧策略视频文件推理的核心矛盾是「处理速度 vs 时间连续性」。如果每帧都跑1080p 视频在单卡 3090 上大概 40 FPS但很多视频是 30 FPS处理完比原视频还慢。常见做法是跳帧 结果插值cap cv2.VideoCapture(input.mp4) fps cap.get(cv2.CAP_PROP_FPS) step max(1, int(round(proc_fps / target_fps))) # 跳帧步长 idx 0 while True: ret, frame cap.read() if not ret: break if idx % step 0: results model(frame, imgsz640, conf0.4, verboseFalse) last_boxes parse(results[0]) draw(frame, last_boxes) # 非推理帧复用上一帧结果 out.write(frame) idx 1target_fps设成 15 就够表情识别用了表情变化本身是秒级的不需要 30 FPS。step计算出来如果是 2就是每两帧推理一次中间帧复用上一帧的框和表情标签。这个策略在课堂专注度分析里很实用因为学生表情不会在 1/30 秒内突变。3.3 实时摄像头延迟控制和线程模型摄像头场景最容易被忽视的是「采集-推理-显示」的流水线阻塞。如果在一个线程里顺序做采集 5ms、推理 25ms、显示 5ms总延迟 35ms看起来还行但一旦推理波动到 50ms画面就会卡顿。稳妥做法是双线程采集线程只负责cap.read()并放入队列推理线程从队列取最新帧丢弃过期帧。import threading, queue q queue.Queue(maxsize2) def capture(): cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) while running: ret, frame cap.read() if not ret: continue if q.full(): try: q.get_nowait() # 丢弃旧帧 except queue.Empty: pass q.put(frame) def infer(): while running: frame q.get() results model(frame, imgsz640, conf0.4, verboseFalse) draw_and_show(frame, results[0])maxsize2是关键队列太长会导致延迟累积太短会丢帧。摄像头分辨率设 1280x720 而不是 1920x1080因为 1080p 下 yolo11n 在 CPU 上只有 10 FPS 左右720p 能到 25 FPS。如果必须 1080p建议用 yolo11s 或加 TensorRT。注意实时场景下conf阈值不要低于 0.35否则背景纹理容易被误检成人脸表情分支会输出随机结果看起来像「玄学抖动」。4. 避坑与排查表情识别落地时最容易翻车的 5 个点4.1 现象视频里同一个人表情在「高兴」和「中性」之间高频跳变原因检测框每帧抖动 2~3 像素表情分支的全局池化特征对框位置敏感导致 logits 波动。解决在推理后处理里加一个滑动窗口投票最近 5 帧的表情 logits 做平均再取 argmax。代码上维护一个deque(maxlen5)每帧 append 当前 logits输出平均后的类别。这个改动能把跳变率降低 70% 以上。4.2 现象侧脸和低头场景漏检严重表情完全识别不到原因训练数据里正脸占比过高YOLOv11 的检测头对极端姿态泛化不足。解决在数据增强里开启degrees15、shear5、perspective0.0005并额外收集 10%~15% 的侧脸样本。如果漏检集中在低头场景可以把mosaic1.0关掉最后 10 个 epoch让模型在真实分布上微调。4.3 现象训练 loss 正常下降但验证集表情准确率卡在 40% 不动原因表情标签噪声大。FER2013 这类公开数据集本身标注一致性只有 65% 左右人工标的表情经常在「恐惧」和「惊讶」之间混淆。解决先把 7 类合并成 4 类高兴、中性、负面、惊讶准确率通常能跳到 70% 以上。如果业务只关心「高兴/不高兴」直接做二分类准确率能到 85%。另一个检查点是表情 CSV 的 filename 和实际图片是否一一对应路径错位会导致标签全乱。4.4 现象摄像头推理跑几分钟后帧率从 25 FPS 掉到 8 FPS原因OpenCV 的VideoCapture缓冲区累积。默认缓冲区会缓存多帧如果推理速度跟不上采集速度缓冲区越来越长cap.read()返回的是旧帧同时内存占用上升。解决设置cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)并在采集线程里用grab()丢弃过期帧。另外检查是否有cv2.imshow在主线程阻塞显示操作要放在独立线程或降低显示频率。4.5 现象自定义训练后模型在测试集上检测框正常但表情输出全是同一类原因表情分支的梯度被检测损失淹没。多任务训练时检测损失的量级通常比交叉熵大一个数量级表情分支学不到东西。解决把表情损失权重从 0.5 提到 2.0或者用 GradNorm 这类动态权重方法。更简单的做法是分阶段训练前 10 epoch 只训检测头冻结表情分支后 40 epoch 联合训练表情分支的学习率设成检测分支的 3 倍。5. 进阶技巧用 TensorRT 加速和置信度校准把实时表情识别推到可用5.1 TensorRT 导出与 INT8 量化的边界YOLOv11 导出 TensorRT 的命令很直接yolo export modelbest.pt formatengine halfTrue device0 workspace4halfTrue是 FP16在 30 系以上显卡上通常能提速 1.5~2 倍精度损失小于 0.5%。INT8 量化需要校准集yolo export formatengine int8True dataface_emotion.yaml但表情分支对量化更敏感我实测 INT8 下表情准确率会掉 3~5 个百分点检测框几乎无损。如果业务对表情精度要求高建议只对 backbone 做 INT8表情头保持 FP16。导出后推理接口不变model YOLO(best.engine)即可。5.2 置信度校准让表情输出从「能看」到「可信」原始 softmax 输出的置信度往往偏高一个明显是「中性」的脸可能输出「高兴 0.7」。校准方法是温度缩放Temperature Scaling在验证集上拟合一个温度参数 T推理时用softmax(logits / T)。T 通常大于 1能把过度自信的分布压平。实现上只需要在推理后处理里加一行emo_probs torch.softmax(emo_logits / T, dim-1)T 的拟合用验证集的 NLL 损失做网格搜索范围 0.5~3.0步长 0.1。校准后可以设一个「拒识阈值」如果最大概率低于 0.5输出「不确定」而不是硬分类。这个技巧在课堂专注度分析里特别有用因为低头、遮挡场景下模型本来就不该给确定答案。5.3 一个我踩过的坑别在摄像头线程里做可视化早期我把cv2.imshow和推理放在同一个线程结果帧率一高就卡死查了半天以为是模型问题。后来把显示拆到独立线程主线程只做推理和队列管理帧率立刻稳定。另一个习惯是每次改完 head 结构后先用model.info()打印参数量和层结构确认表情分支真的挂上去了而不是被 Ultralytics 的模型解析器静默忽略。这个方案值不值得做取决于你的场景能不能接受「表情识别本质是概率输出」这件事——如果能接受YOLOv11 多任务头是目前工程复杂度最低、部署路径最短的选择之一。希望帮到你。本文还有配套的精品资源点击获取