1. 环境准备与项目搭建
嘿,朋友们,今天咱们来点硬核又好玩的东西——用最新的YOLOv11和经典的PyQt5,从零开始手搓一个能实时识别你喜怒哀乐的表情识别系统。我花了差不多一周时间,把整个流程从数据准备、模型训练到界面部署都踩了一遍坑,现在把最干的干货分享给你。你不用有太深的AI背景,跟着我的步骤走,保证你能在自己的电脑上跑起来一个炫酷的实时应用。
首先,你得把“厨房”收拾好,也就是搭建开发环境。我强烈建议你使用Anaconda来管理Python环境,这能避免各种依赖包版本冲突的“玄学”问题。打开你的终端(Windows用Anaconda Prompt,Mac/Linux用终端),咱们一步步来。
创建一个新的Python环境,我习惯用Python 3.9,比较稳定:
conda create -n yolo11_emotion python=3.9 -y
conda activate yolo11_emotion
接下来安装核心的PyTorch。这里有个小坑,如果你有NVIDIA显卡并且想用GPU加速训练和推理,一定要去PyTorch官网根据你的CUDA版本选择对应的安装命令。比如我的CUDA是11.8,我就这么装:
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
如果你没有显卡或者不想折腾CUDA,直接用CPU版本也行,就是后面训练和实时推理会慢一些:
pip install torch torchvision torchaudio
然后安装本次项目的两大主角:Ultralytics的YOLOv11库和PyQt5。YOLOv11是Ultralytics刚推出不久的新版本,在速度和精度上又有提升,我们用它的接口会非常方便。PyQt5则是用来打造我们那个酷炫桌面界面的。
pip install ultralytics opencv-python PyQt5 pillow numpy
这一行命令把计算机视觉的OpenCV、图像处理的Pillow、科学计算的NumPy都一并装好了。装完之后,你可以先写个几行代码测试一下核心库是否正常。
import torch
print(torch.__version__)
print(torch.cuda.is_available()) # 输出True说明GPU可用
from ultralytics import YOLO
print(YOLO) # 确认能导入
如果都没报错,恭喜你,基础环境搞定!接下来我们去把项目的“原材料”——代码和数据准备一下。我强烈建议你直接使用我整理好的开源项目,里面已经把UI界面、训练脚本和基础模型都打包好了,省去你从头造轮子的时间。你可以用Git克隆下来,如果Git速度慢,我也提供了国内的镜像地址。
# 从GitHub克隆
git clone https://github.com/AND-Q/Facial-Expression-Recognition.git
# 或者从Gitee克隆(国内访问更快)
git clone https://gitee.com/memoryQZY/Facial-Expression-Recognition.git
进入项目目录,你会看到一个结构清晰的文件树。我简单说一下几个核心文件:UI.py是主界面入口;train.py是模型训练脚本;yolo_face_detection.py负责人脸检测逻辑;image_emotion_recognition.py处理静态图片的表情识别。咱们的实战就从这里开始。
1.1 数据集获取与预处理
模型要想识别的准,就得吃“好粮食”,也就是高质量的数据集。对于表情识别,有几个公开的经典数据集我们可以用。最常用的是FER2013,它包含了大约3.5万张灰度人脸图片,标注为愤怒、厌恶、恐惧、高兴、悲伤、惊讶和中性这7类。不过恐惧的样本比较少,有时候我们会把它合并或忽略,做成6分类。另一个更强大的是AffectNet,它有一百多万张图片,标注也更丰富,但数据量太大,下载和预处理需要时间。
我这里以FER2013为例,教你如何准备数据。你可以在Kaggle上搜索并下载fer2013.csv文件。下载后,我们需要写个小脚本把它转换成YOLO训练所需的目录格式。YOLO希望的数据格式是,一个images文件夹放所有图片,一个labels文件夹放对应的标注文件(不过对于纯分类任务,我们更常用的是文件夹名即类别名的方式)。
我写了一个预处理脚本,你可以直接拿来用。它的作用是把CSV文件里的像素字符串读出来,还原成48x48的灰度图,然后按照表情类别,分别保存到train和test文件夹下的对应子文件夹里(比如angry, happy)。这样,数据集的架子就搭好了。记得要把数据集路径在后续的训练配置文件中指对,这是很多新手会栽跟头的地方,明明代码没错,但就是读不到数据,八成是路径问题。
2. 模型训练:从零调教一个“读脸”专家
环境搭好了,数据也备齐了,接下来就是最核心也最有成就感的环节——训练我们自己的表情识别模型。很多人觉得训练深度学习模型很神秘,其实用YOLOv11的API,过程已经简化得像搭积木一样直观了。咱们不搞那些复杂的理论,直接上手干。
首先,我们需要创建一个数据集配置文件,告诉YOLO我们的数据长什么样、放在哪。在项目根目录下创建一个叫fer2013.yaml的文件,内容大致如下:
# 数据集路径(请根据你的实际路径修改)
path: /path/to/your/fer2013_dataset
# 训练集和验证集路径,相对于上面的path
train: images/train
val: images/test
# 表情类别数
nc: 7
# 类别名称,顺序很重要,要和文件夹名对应
names: ['angry', 'disgust', 'fear', 'happy', 'sad', 'surprise', 'neutral']
这个yaml文件是训练的总指挥,路径一定要写对。接下来,打开train.py文件,我们可以看到训练的核心代码。我强烈建议你不要直接运行,而是先理解里面几个关键参数,这能帮你省下大量瞎试的时间。
2.1 训练参数深度解读
model.train()函数里有一大堆参数,我挑几个最重要的给你掰扯清楚:
data: 这里就填我们刚创建的fer2013.yaml的路径。epochs: 训练轮数。不是越多越好!对于FER2013这样规模的数据,100-200个epoch通常足够了。设太多会导致过拟合,模型只认得训练集里的图片,遇到新图就懵了。我实测下来,150轮是个不错的起点。batch: 批大小。如果你的显卡显存大(比如16G以上),可以设大点,比如128或256,训练速度快。显存小(比如8G),就设小点,比如32或64。它会影响模型更新的稳定性和速度。imgsz: 输入图片的尺寸。YOLOv11的预训练权重通常是基于224x224或640x640训练的。对于表情识别这种人脸细节任务,224x224足够,而且推理速度更快。这是平衡精度和速度的关键。optimizer: 优化器。代码里用的是AdamW,这是目前最主流的优化器,比普通的Adam更稳定,通常不需要你额外调整。lr0: 初始学习率。这是最重要的超参数之一!太大了训练会发散(损失值NaN),太小了收敛慢。对于这种任务,0.001是个安全的起点。如果你发现训练了很久损失都不怎么降,可以尝试稍微调大点,比如0.005。cos_lr: 启用余弦退火学习率调度。这个一定要开! 它能让学习率像余弦曲线一样从初始值平滑下降到接近0,有助于模型在训练后期“微调”到一个更优的位置,避免震荡。augment和mixup: 数据增强开关。务必保持开启! 数据增强是防止过拟合、提升模型泛化能力的“神器”。它会随机给图片做旋转、裁剪、变色等操作,让模型看到更多样的数据。mixup是一种高级增强,会把两张图片混合在一起,让模型学习更鲁棒的特征。
理解了这些,你就可以启动训练了。在终端激活你的环境,运行:
python train.py
你会看到终端开始疯狂刷日志,显示每一轮(epoch)的训练损失和验证损失。重点关注val/loss和metrics/accuracy这两个值。理想情况下,训练损失和验证损失都应该稳步下降,验证准确率稳步上升。如果出现训练损失降但验证损失升,那就是过拟合了,需要加大数据增强力度,或者用dropout、weight_decay等正则化手段(代码里已经设置了)。
训练完成后,会在runs/classify/train目录下生成一堆文件,其中最重要的就是weights/best.pt,这是验证集上表现最好的模型权重,就是我们后续要用的“终极模型”。还会有一个results.png的图表,直观展示训练过程,一定要学会看这个图来评估训练效果。
3. 人脸检测与表情识别核心代码拆解
模型训练好了,就像一个武功高手内功已成。接下来要给他配上“招式”,也就是写代码让他能实际看到人脸并做出判断。这部分代码主要集中在yolo_face_detection.py和image_emotion_recognition.py里。我带你看看里面的关键门道。
首先是人脸检测。我们直接使用Ultralytics官方提供的一个针对人脸优化过的YOLOv11模型yolov11n-face.pt。这个模型在通用人脸检测数据集上微调过,比直接用通用的YOLOv11检测人更准。加载模型只需要一行:
from ultralytics import YOLO
face_model = YOLO('yolov11n-face.pt')
检测的代码也很简洁。但这里有个我踩过的大坑:直接检测到的人脸框,往往卡得太紧,只框住了五官,丢失了额头、下巴和部分脸颊。这些区域对判断表情(比如惊讶时的抬头纹、悲伤时的嘴角下垂)其实很重要。所以我在代码里做了一个边界框扩展的操作,把框往外扩了20%:
# 原始检测框
x1, y1, x2, y2 = box.xyxy[0].cpu().numpy().astype(int)
# 计算扩展量
expand_x = int((x2 - x1) * 0.2)
expand_y = int((y2 - y1) * 0.2)
# 扩展,并确保不超出图片边界
x1_expanded = max(0, x1 - expand_x)
y1_expanded = max(0, y1 - expand_y)
x2_expanded = min(frame_width, x2 + expand_x)
y2_expanded = min(frame_height, y2 + expand_y)
这个简单的操作,在我实测中让表情识别的准确率提升了大概3-5个百分点,非常实用。
检测到人脸并扩展区域后,就要把这个区域裁剪出来,送给我们的表情识别模型(也就是你刚训练好的best.pt)去分类。这里有个关键预处理步骤:FER2013数据集是灰度图,所以我们的模型是在灰度图上训练的。因此,在推理时,我们也必须把人脸区域转换成灰度图,保持一致。
# 裁剪出扩展后的人脸区域
face_roi = frame[y1_expanded:y2_expanded, x1_expanded:x2_expanded]
# 转换为灰度图
face_roi_gray = cv2.cvtColor(face_roi, cv2.COLOR_BGR2GRAY)
# 但YOLO模型需要3通道输入,所以再转回3通道(三个通道值相同)
face_roi_gray_3ch = cv2.cvtColor(face_roi_gray, cv2.COLOR_GRAY2BGR)
为什么多此一举转回3通道?因为YOLO的分类模型骨干网络(比如CNN)通常默认输入是3通道(RGB)。我们用灰度图训练时,实际上是把同一张灰度图复制了三份当成RGB三个通道。推理时也必须这么做。
最后,调用表情模型进行预测,并绘制结果:
emotion_model = YOLO('path/to/your/best.pt') # 加载你的训练模型
emotion_results = emotion_model(face_roi_gray_3ch)
probs = emotion_results[0].probs.data.tolist()
predicted_class_id = probs.index(max(probs))
confidence = max(probs)
emotion_label = ['愤怒', '厌恶', '恐惧', '高兴', '悲伤', '惊讶', '中性'][predicted_class_id]
# 把标签和置信度画到图上
cv2.putText(frame, f"{emotion_label}: {confidence:.2f}", (x1, y1-10), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 255, 0), 2)
这样,一个完整的“检测-裁剪-识别-绘制”流水线就完成了。你可以先用image_emotion_recognition.py脚本测试单张图片,感受一下效果。
3.1 多线程与实时视频流处理
处理单张图片不难,难的是处理摄像头实时视频流还要保持界面不卡顿。这就是UI.py里VideoThread这个类的精髓所在。PyQt的界面主线程如果被耗时的视频处理任务阻塞,整个窗口就会“未响应”。所以必须把视频处理丢到另一个工作线程(QThread)里去跑。
VideoThread线程的核心在run方法里,它在一个循环中不断从摄像头抓取帧,然后调用我们上面写好的检测和识别函数,最后把画好框和标签的图片通过信号(change_pixmap_signal)发回给主界面更新显示。主界面只需要连接这个信号,收到新图片就刷新一下显示区域,完全不会卡。
我在这里也优化了一下:不是每一帧都进行人脸检测。因为人脸位置不会每帧都剧烈变化,我们可以每处理5帧或10帧做一次检测,中间帧直接用上一帧的人脸位置进行表情识别,这能大幅提升实时帧率。这个技巧在VideoThread的循环逻辑里可以实现,你可以尝试修改,在流畅度和准确性之间找到平衡。
4. PyQt5图形界面设计与功能集成
模型和核心逻辑都搞定后,我们需要一个漂亮的“外壳”把它包装起来,让用户(或者你自己演示的时候)能方便地使用。这就是PyQt5大显身手的时候了。UI.py文件里定义了一个FaceDetectionApp类,它继承自QMainWindow,是整个应用的主窗口。
设计界面有两种方式:纯代码手写控件,或者用Qt Designer拖拽设计.ui文件再转成Python代码。对于功能不太复杂的界面,我更喜欢手写,因为更灵活可控。这个项目的界面布局清晰:上方是菜单栏和工具栏,中间是视频显示区域,下方和右侧是控制面板。
控制面板里有几个关键控件你需要关注:
- 输入源选择:一组单选按钮(QRadioButton),让用户选择是使用摄像头、打开图片文件还是视频文件。切换时,需要重置视频线程的模式和路径。
- 模型选择下拉框(QComboBox):这里可以加载
models文件夹下你准备好的多个模型文件(比如best_fer.pt,best_affectnet.pt),让用户根据场景选择。轻量模型速度快,大模型精度高。 - 置信度阈值滑块(QSlider):连接一个数值显示。这个值决定了模型多“自信”才把结果展示出来。调高(比如0.8)会减少误判,但可能漏掉一些模糊表情;调低(比如0.3)会更敏感,但可能把中性脸误判为其他表情。我一般设置在0.5到0.7之间。
- 开始/停止按钮:控制视频处理线程的启动和终止。
- 保存按钮:点击后可以将当前帧或整个处理后的视频保存下来。保存图片用
cv2.imwrite,保存视频则需要用cv2.VideoWriter创建一个写入器,把每一帧写进去。
界面逻辑的核心是信号与槽机制。比如,当用户点击“开始检测”按钮,这个按钮的clicked信号就会触发一个自定义的槽函数,在这个函数里我们启动VideoThread线程。线程处理完一帧后,发出change_pixmap_signal信号,主窗口里连接这个信号的槽函数就会收到处理好的图片数据,并更新显示。所有耗时的操作都在线程里,界面始终保持流畅响应。
为了让界面更美观,我还加入了暗色主题的支持。PyQt5可以通过设置QSS(Qt样式表,类似CSS)来改变控件的外观。我在代码里定义了一套暗色风格的QSS字符串,应用之后,按钮、背景、文字颜色都会变成深色系,看起来更酷,也适合长时间使用。
5. 性能调优与实战部署技巧
系统能跑起来只是第一步,跑得又快又准才是终极目标。这里分享几个我实战中总结出来的性能调优和部署技巧。
第一,模型轻量化。我们训练出的best.pt模型可能比较大(几十兆到上百兆)。对于实时应用,我们可以使用YOLOv11提供的模型导出功能,将其转换为更高效的格式。最常用的是转成ONNX格式,它可以在多种推理引擎上运行,有时还能获得加速。使用命令很简单:
from ultralytics import YOLO
model = YOLO('best.pt')
model.export(format='onnx')
导出的best.onnx文件可以用ONNX Runtime库来加载和推理,在某些CPU上比直接跑PyTorch模型更快。你甚至可以用TensorRT进一步优化,在NVIDIA显卡上获得极致推理速度,不过那需要更多环境配置。
第二,推理分辨率调整。在emotion_model推理时,我们传入的是裁剪后的人脸区域。这个区域大小不固定。YOLO模型内部会将其缩放到训练时的尺寸(比如224x224)。但你可以尝试在加载模型时指定一个更小的尺寸,比如imgsz=160,这能显著提升推理速度,但可能会轻微损失精度。这是一个典型的“速度-精度”权衡,需要你在自己的场景中测试决定。
第三,利用硬件加速。确保你的PyTorch是GPU版本,并且代码确实跑在GPU上。在加载模型时,可以显式指定设备:
face_model = YOLO('yolov11n-face.pt').to('cuda')
emotion_model = YOLO('best.pt').to('cuda')
在视频处理循环中,也要确保每一帧数据从CPU内存传到GPU显存。虽然Ultralytics的接口通常会自动处理,但了解底层原理有助于排查问题。
第四,处理多张人脸。我们之前的逻辑是按顺序处理一张张人脸。但在实际场景中,摄像头前可能有多个人。我们的代码已经能处理了,因为face_model检测返回的就是所有框的列表。在循环里对每个框都执行一遍裁剪和表情识别即可。但要注意,人太多时计算量会成倍增加,可能导致帧率下降。这时可以考虑对距离镜头最近(框最大)的几个人脸进行识别,或者适当降低检测的频率。
最后,打包成可执行文件。如果你想把这个系统分享给没有Python环境的朋友用,可以使用PyInstaller打包。在项目根目录下创建一个spec文件或者直接使用命令行:
pip install pyinstaller
pyinstaller -w -F UI.py --add-data "models;models" --hidden-import PyQt5.sip
-w表示生成窗口程序(不显示控制台),-F表示打包成单个exe文件,--add-data把存放模型的models文件夹也打包进去。打包过程可能会遇到一些动态库链接的问题,需要根据报错信息慢慢调整,这是部署的最后一公里,虽然麻烦但值得。
382



被折叠的 条评论
为什么被折叠?



