简介这是一套面向计算机视觉学习者与深度学习开发者的端到端人脸检测与表情识别系统源码基于YOLOv11实现人脸定位并采用自定义训练的YOLO模型完成表情分类可识别愤怒、厌恶、高兴、中性、悲伤、惊讶六种基本表情。系统支持静态图像、视频文件与实时摄像头三类输入配套基于PyQt5的暗色主题图形界面并集成综合数据集模型、FER2013增强模型等多个训练权重处理结果可实时显示与保存。资源包共1022个文件约58.53MB以190个Python源码、395个Markdown文档、97个YAML配置、59个pt权重及若干图像与视频素材为主另含Docker部署文件与推理示例便于复现与二次开发。目前已有166人学习下载适合希望掌握YOLOv11实战、表情识别建模与GUI集成完整流程的读者参考。1. 从一张模糊的侧脸说起这套 YOLOv11 表情识别源码到底能跑出什么上周帮朋友调一个考场情绪监测的小工具摄像头画面里考生侧脸占屏不到 1/6光照还是顶光传统 Haar 级联直接漏检。换成这套基于 YOLOv11 的人脸检测与表情识别系统源码后同一段视频里侧脸召回明显上来了表情分类也稳住了。它解决的不是能不能识别人脸这种入门问题而是把检测和表情分类串成一条可落地的推理链路图像、视频文件、实时摄像头三种输入都支持模型是自定义训练的 YOLO 权重不是拿现成 COCO 人脸权重凑数。适合谁做课堂专注度分析、驾驶员疲劳监测、互动装置情绪反馈的开发者以及想拿一个完整 YOLOv11 实战项目练手的人。源码包里带详细文档环境配置、训练流程、推理脚本都有说明0 基础也能照着走一遍但前提是你愿意先把 Python 和 PyTorch 环境理顺。2. YOLOv11 做人脸表情识别的选型逻辑为什么不是分类网络单干2.1 检测与分类合并到一阶段的取舍很多人第一反应是人脸检测用 MTCNN表情分类用 ResNet两段式不香吗。香但工程上多一段就多一次坐标对齐误差和一次推理延迟。这套源码走的是 YOLOv11 单阶段路线把人脸框回归和表情类别放在同一个检测头里输出。YOLOv11 相比 v8 在 C3k2 模块和 SPPF 上的改动让浅层特征保留更完整小目标人脸——也就是画面里占几十个像素的那种——召回率有实际提升。自定义训练时数据集标注格式是 YOLO 标准的 txtclass_id x_center y_center width height表情类别直接作为 class_id比如 0 是 angry、1 是 happy、2 是 neutral 这样排下去。这样一份标注同时服务检测和分类省掉两套数据管线的维护成本。代价也要说清楚单阶段模型在极端小脸小于 20×20 像素和严重遮挡下分类置信度会掉得比两段式快。如果你的场景里人脸普遍很大、很正两段式反而更省心如果是视频流、要实时、脸的大小忽大忽小这套一阶段方案更合适。2.2 自定义训练与预训练权重的边界源码里给的权重是自定义训练产物不是官方 COCO 权重。这意味着两件事第一它的类别数和你数据集的表情类别数必须对齐改类别就得重训或至少微调第二它的泛化边界由你的训练数据决定拿它去识别训练集里没出现过的极端表情比如夸张的惊恐大概率翻车。常见做法是先用官方 YOLOv11 检测权重做人脸预标注再人工修正表情标签最后用自定义数据微调。这样比从零训练收敛快得多也不容易把检测能力训废。2.3 环境配置把 ultralytics 和推理依赖装对环境是第一个拦路虎。ultralytics 版本和 PyTorch 版本不匹配会出现ImportError: cannot import name xxx from ultralytics这类玄学报错。我一般会先锁一个组合再装# 建议 Python 3.9~3.11太新或太旧都可能踩依赖坑 conda create -n face_emo python3.10 -y conda activate face_emo # 先装 PyTorch按你的 CUDA 版本去官网选对应命令这里以 CUDA 11.8 为例 pip install torch2.1.0 torchvision0.16.0 --index-url https://download.pytorch.org/whl/cu118 # 再装 ultralytics不要盲目装最新跟源码文档对齐 pip install ultralytics8.3.0 # 推理侧常用依赖 pip install opencv-python numpy pandas matplotlib pyyaml tqdm逻辑说明先建独立环境避免污染全局PyTorch 必须早于 ultralytics 装否则 ultralytics 可能拉一个不匹配的 torch 版本进来。参数上torch2.1.0和ultralytics8.3.0是实测比较稳的组合你如果 CUDA 是 12.x把 index-url 换成 cu121 对应版本即可。装完跑一句python -c from ultralytics import YOLO; print(ok)能打印 ok 再往下走。3. 从标注到推理把自定义 YOLOv11 表情模型跑通3.1 数据集目录结构与 data.yaml 写法YOLOv11 训练对目录结构有固定要求摆错了不会报错但会静默训出废模型。标准结构如下datasets/face_emo/ ├── images/ │ ├── train/ # 训练图片 │ └── val/ # 验证图片 ├── labels/ │ ├── train/ # 对应 txt 标注 │ └── val/ └── data.yamldata.yaml是训练入口的配置核心# data.yaml path: ./datasets/face_emo # 数据集根目录 train: images/train # 相对 path 的训练图路径 val: images/val # 相对 path 的验证图路径 # 表情类别顺序必须和标注里的 class_id 一致 names: 0: angry 1: disgust 2: fear 3: happy 4: sad 5: surprise 6: neutral逻辑说明path写相对或绝对都行但train/val是相对path的别写成绝对路径否则找不到。names的键就是标注 txt 第一列的数字顺序错一位训出来的模型就会把开心识别成生气这种坑排查起来很费时间。类别数不用单独写ultralytics 会从 names 长度推断。3.2 训练命令与关键超参训练脚本一行就够但参数值得掰开说yolo detect train \ modelyolo11n.pt \ # 从官方预训练权重起步收敛快 datadatasets/face_emo/data.yaml \ epochs100 \ # 表情数据量小的话 100 轮够大了加到 200 imgsz640 \ # 输入尺寸小脸多可以提到 960 batch16 \ # 显存不够就降到 8 或 4 lr00.01 \ # 初始学习率微调时可降到 0.001 patience20 \ # 20 轮没提升就早停省时间 projectruns/face_emo \ # 输出目录 nameexp1逻辑说明modelyolo11n.pt是 nano 版速度快适合实时精度不够换yolo11s.pt或yolo11m.pt但推理延迟会上去。imgsz是双刃剑提到 960 小脸召回好但显存和耗时都涨。lr0从预训练权重微调时别用 0.01容易把已有特征打乱0.001 更稳。patience是早停表情数据集通常几千张100 轮里后 30 轮基本在震荡早停能省不少电费。3.3 图像、视频、摄像头三种输入的推理脚本源码支持三种输入核心都是model.predict或model.track差别在 source 参数from ultralytics import YOLO import cv2 # 加载自定义训练权重 model YOLO(runs/face_emo/exp1/weights/best.pt) # 1) 单张图像推理 results model.predict(sourcetest.jpg, conf0.4, saveTrue) # conf 是置信度阈值表情识别建议 0.4~0.5太低会出很多误检 # 2) 视频文件推理 results model.predict(sourcetest.mp4, conf0.4, saveTrue, streamTrue) for r in results: pass # streamTrue 时逐帧返回适合长视频不爆内存 # 3) 实时摄像头推理 cap cv2.VideoCapture(0) while cap.isOpened(): ret, frame cap.read() if not ret: break results model.predict(sourceframe, conf0.4, verboseFalse) annotated results[0].plot() # 把框和表情标签画回画面 cv2.imshow(face_emo, annotated) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()逻辑说明conf是置信度阈值表情分类比纯人脸检测更容易误报阈值别低于 0.4。streamTrue用于视频文件逐帧 yield 结果避免一次性把整段视频读进内存。摄像头循环里verboseFalse关掉每帧日志否则控制台刷屏拖慢速度。results[0].plot()返回带标注的 numpy 图像直接 imshow 即可。如果要做跟踪而不是逐帧独立检测把predict换成track加persistTrue保持轨迹。3.4 推理结果保存与导出格式saveTrue默认把结果存到runs/detect/predict/下图像存 jpg视频存 mp4。要拿结构化数据每个框的坐标、类别、置信度就遍历 resultsfor r in results: boxes r.boxes for box in boxes: xyxy box.xyxy[0].tolist() # 左上右下坐标 cls_id int(box.cls[0]) # 类别 id conf float(box.conf[0]) # 置信度 label model.names[cls_id] # 类别名 print(xyxy, label, conf)逻辑说明box.xyxy是绝对像素坐标box.cls是类别索引用model.names映射回表情名。这套结构可以直接写进 CSV 或数据库做后续统计分析。注意saveTrue和手动遍历可以同时用互不冲突。4. 避坑与排查表情识别项目里最容易翻车的五件事4.1 现象训练 loss 一直不降mAP 卡在 0.1 以下原因九成是标注格式或类别顺序错了。YOLO 的 txt 要求坐标归一化到 0~1很多人直接写像素坐标模型学不到东西。另一个常见原因是data.yaml里 names 顺序和标注 class_id 对不上。解决写个脚本抽查几张标注把归一化坐标反算回像素画框肉眼确认框对了再训。names 顺序和标注生成时的映射表逐行核对一遍。4.2 现象推理时所有脸都被识别成同一个表情原因类别极度不平衡。比如 neutral 占了 80% 的样本模型学会全猜 neutral就能拿到高准确率但实际没用。解决训练时加类别权重或用过采样把少数类补上来。ultralytics 本身不直接暴露 class_weight常见做法是在数据集层面做平衡或者用fraction参数控制采样比例配合自定义 sampler。4.3 现象摄像头实时推理卡成幻灯片原因每帧都走完整预处理推理后处理且没关日志。或者 imgsz 设太大nano 模型也扛不住。解决verboseFalse关日志imgsz降到 416 或 320 试用halfTrue开 FP16 推理需 GPU 支持再不行换更小的输入或跳帧处理。4.4 现象换一台机器跑报 CUDA out of memory原因batch 或 imgsz 是按原机器显存调的换小显存机器直接爆。解决推理时 batch 设 1训练时 batch 降到 4 或 2配合ampTrue混合精度。实在不够就上 CPU 推理速度慢但能跑。4.5 现象视频推理结果保存后没有声音原因OpenCV 的 VideoWriter 默认不写音频轨这是库的限制不是 bug。解决要保留音频就用 ffmpeg 把原视频音频和推理后视频合并或者推理时只输出标注数据后期用原视频叠加。别在这上面死磕 OpenCV。5. 进阶技巧把表情识别接到业务流里的两个实用手法第一个手法是滑动窗口投票。实时摄像头逐帧独立分类会有抖动同一张脸上一帧 happy 下一帧 neutral体验很差。我一般会维护一个长度 10 的队列每帧的类别往里塞输出队列里出现次数最多的类别。这样表情切换有平滑不会闪。代码上就是在 3.3 的摄像头循环里加一个collections.deque(maxlen10)每帧 append 当前 label然后Counter(queue).most_common(1)取众数。代价是表情切换有约 10 帧延迟30fps 下大概 0.3 秒可接受。第二个手法是置信度过滤加二次确认。表情分类的误报往往集中在低置信度区间conf0.4能滤掉一批但边界样本还是会漏。我的习惯是对0.4~0.6这个区间的结果不直接输出而是累积几帧后再判定相当于给模型一个再想想的机会。这套逻辑在课堂专注度统计里特别有用避免因为一个哈欠就把学生标成疲劳。验证模型有没有训好别只看 mAP。我一般会做两件事一是拿训练集里没出现过的真实场景视频跑一遍肉眼看误检和漏检分布二是画混淆矩阵看哪两个表情最容易混——通常 fear 和 surprise、disgust 和 angry 是重灾区如果这两对混淆严重说明数据里这两类的区分度不够得补样本而不是调参。从那以后我每次拿到一个新的表情识别权重都强制走一遍真实视频肉眼验证 混淆矩阵这两步不再信训练日志里的漂亮数字。希望帮到你。本文还有配套的精品资源点击获取