资讯详情

资讯详情

YOLOv11两段式人脸表情识别系统:从数据准备到部署全解析

简介面向深度学习开发者与计算机视觉研究者的YOLOv11人脸检测与表情识别完整工程包覆盖自定义YOLO模型改进、人脸检测、面部关键点定位与表情分类全流程适用于智能交互、安全监控、用户行为分析等实时场景。压缩包共1022个文件约56.71MB包含396个Markdown说明文档、190个Python源码、97个YAML模型配置文件、59个PyTorch权重文件以及Docker部署镜像文件、演示视频、示例图片和项目文档可支撑从模型训练、推理到环境部署的完整链路。已有61人学习浏览适合作为算法二次开发、实验复现与工程落地的参考。包内包含自定义YOLO网络结构SPP空间金字塔池化、多尺度特征融合、注意力机制与表情识别模块可对照Python脚本和notebook理解改进细节附带的推理代码、关键点检测脚本及多平台Docker配置能帮助快速搭建运行环境并验证模型效果。1. 不是又一个人脸框YOLOv11 与表情识别这套组合的落地价值我之前在普通笔记本上跑过一个现成的人脸检测加表情识别 Demo图像上框得很准一开摄像头就原形毕露——帧率掉到个位数表情还清一色识别成“生气”最后发现是模型把所有偏暗的人脸都判成了负面情绪。真正的问题是很多人做表情识别只拿一个分类模型硬怼整张图而这份资源走的是两段式路线——先用 YOLOv11 把脸稳定框出来再把裁切后的人脸交给一个自定义 YOLO 变体做表情分类。它解决的不只是“能不能识别”而是“在摄像头场景下还能不能保持稳定”。适合正在做毕设、课程设计或者刚入门 CV 想做出一个完整系统的人。想复现的话关键不在跑通 Demo而在数据格式、模型头设计和部署三个环节。2. 数据准备公开数据集打底、自定义数据调优的完整处理流程2.1 公开数据集与自定义数据怎么选表情识别和人脸检测是两类不同的数据任务。人脸检测需要的是“整图 人脸框”表情识别需要的是“人脸图 表情类别”。这套系统里YOLOv11 负责检测所以它需要的是前者自定义 YOLO 负责表情分类需要的是后者。常见做法是先用公开人脸检测数据集预训练 YOLOv11 的检测权重再拿自定义数据做场景微调。表情分类模型则直接用小规模的人脸表情数据集训练。数据来源规模标注精度场景覆盖适用环节公开通用人脸数据大框位准类别粗多样但不覆盖你的实际场景YOLOv11 预训练公开表情数据中类别齐全人脸基本居中、表情偏夸张表情分类模型初训自定义采集数据小由你控制贴合摄像头真实场景二次微调、域适配实际训练时不要一上来就完全依赖自定义数据量不够容易过拟合。我一般会在公开数据上把权重训到收敛再把自定义数据混进去微调比例控制在 3:1 到 5:1 之间。2.2 把数据组织成 YOLO 能吃的格式YOLOv11 和自定义 YOLO 模型的人脸检测部分都要求 YOLO 格式的标签每张图片对应一个同名 txt每行是“类别 id 中心点x 中心点y 框宽 框高”坐标都是相对图片宽高的归一化值。表情分类部分不需要框直接用裁切后的人脸图和类别索引。import os import random from pathlib import Path IMG_EXTENSIONS {.jpg, .jpeg, .png} dataset_root Path(dataset) target dataset_root / labels # 标签目录 image_paths [] for ext in IMG_EXTENSIONS: image_paths.extend(dataset_root.glob(fimages/*{ext})) random.shuffle(image_paths) train_ratio 0.8 split_idx int(len(image_paths) * train_ratio) def write_split(file_list, split_name): with open(dataset_root / f{split_name}.txt, w) as f: for img_path in file_list: label_path target / (img_path.stem .txt) if label_path.exists() and label_path.stat().st_size 0: f.write(str(img_path.resolve()) \n) write_split(image_paths[:split_idx], train.txt) write_split(image_paths[split_idx:], val.txt)这段代码做的事情很基础但非常关键把图片路径按 8:2 拆成训练集和验证集并且只保留那些确实有标签文件的图片。第一版我直接全量写入结果十几个空标签图片混进训练集模型把无脸图也学进去了推理时频繁误检。参数上注意train_ratio 0.8是常见默认值但如果自定义数据量特别小建议调到 0.85 甚至 0.9给训练多留样本。空标签过滤这里必须做因为标注软件偶尔会漏存文件漏一张就是一颗雷。2.3 标注清洗训练前必做的数据体检很多人拿到标注数据就开训结果 loss 怎么也降不下去。最常见的三个问题标签类别 id 和模型类别配置对不上、标注框面积过小、某些表情类别样本量只有个位数。清洗这一步就是训练的后悔药。from collections import Counter import os label_dir dataset/labels category_counter Counter() tiny_box_count 0 empty_label_count 0 for label_file in os.listdir(label_dir): path os.path.join(label_dir, label_file) with open(path) as f: lines f.readlines() if len(lines) 0: empty_label_count 1 continue for line in lines: parts line.strip().split() cls_id int(parts[0]) w, h float(parts[3]), float(parts[4]) category_counter[cls_id] 1 if w * h 0.001: # 归一化面积小于千分之一 tiny_box_count 1 print(类别分布:, dict(category_counter)) print(空标签文件数:, empty_label_count) print(过小框数量:, tiny_box_count)这段脚本在训练前花两分钟跑一遍能避免大多数低级翻车。类别分布是重点如果“惊讶”只有十几张要么去补数据要么在训练配置里给这个类别加权。过小框我通常直接过滤掉因为人脸检测场景下人脸占比一般不会低于千分之一出现这种框基本都是标注错的。3. 模型训练YOLOv11 选择、自定义模型头与超参调节3.1 YOLOv11 模型大小怎么选YOLOv11 提供不同规模版本核心区别是主干宽度和深度。n 系列最快但特征提取能力弱s 是多数人的平衡点m 和 l 适合显卡显存充足、对精度要求高的场景。做表情识别系统人脸检测只是前置环节不需要过度追求检测精度所以优先保证推理速度。模型规模显存占用推理速度适用算力本项目建议n小快无独显也能跑不推荐小脸漏检明显s中等较快入门独显即可默认选择m较大中等6GB 以上显存数据量大时可换l大慢8GB 以上显存追求极限精度才用我在这套系统里推荐用 s因为摄像头场景下的人脸往往有一定的像素面积s 的检测能力完全够用省下来的算力要给后面的表情分类模型留出来。表情分类模型本身很小两者加在一起才可能在普通设备上保持可用的帧率。3.2 自定义 YOLO 模型头的设计方式标题里“自定义 YOLO 模型”这个说法我拆过几种实现最常见的不是从零写一个网络而是复用 YOLOv11 的 backbone 和 neck 做特征提取把检测头换成分类头。这样可以拿到 YOLOv11 预训练权重做迁移学习又不用重新设计特征层。# custom_expression_model.yaml backbone: name: yolov11_s_backbone pretrained: true neck: name: yolov11_s_pan out_channels: [256, 512, 1024] head: name: classification_head in_channels: 1024 # neck 输出的最大特征层 pooling: global_avg_pool # 全局平均池化 fc: [1024, 512] num_classes: 7 # 表情类别数 dropout: 0.2这个配置文件的意思是把 YOLOv11 前面的特征提取部分原样保留但在最后不再输出预测框而是把三个尺度的特征合并后做全局平均池化再接两层全连接输出 7 类表情概率。num_classes必须和你实际数据集的类别数一致差一位都会在训练时报错或者指标混乱。这里的关键点是neck.out_channels要与 backbone 实际输出对齐。我第一次改的时候想当然写成了 512维度对不上训练直接崩属于非常典型的自定义头翻车现场。建议先读取预训练权重打印结构确认每一层的通道数再填进去。3.3 训练超参数设置与启动命令训练脚本和参数直接影响最终效果。我用的是一套经过多次验证的参数组合可以直接作为起点。from ultralytics import YOLO model YOLO(yolov11s.pt) # 用官方预训练权重初始化 model.train( datadataset.yaml, # 数据集配置文件 epochs100, # 训练轮次 batch16, # 批大小根据显存调整 imgsz640, # 输入分辨率检测模型常用 lr00.01, # 初始学习率 lrf0.01, # 最终学习率系数 optimizerSGD, # 优化器 weight_decay0.0005, # 权重衰减 warmup_epochs3, # 预热轮次 cos_lrTrue, # 余弦退火学习率 workers4, # 数据加载线程数 device0 # 使用第一块 GPU )逐参数说明imgsz640是检测模型的标准输入不要随意调大显存占用按平方上升batch16是一个比较保险的值显存不足时先降 batch 而不是降分辨率lr00.01是微调时的常用值如果是从头训练可以适当提高到 0.02但风险也在增加warmup_epochs3让学习率从 0 渐入能有效防止前期 loss 飞掉cos_lrTrue让学习率缓慢下降比固定学习率更容易收敛到好的局部最优点。表情分类模型是单独训练的。把自定义模型定义好之后训练命令和上面类似只是输入是人脸裁切图分辨率通常降到 112 或 128因为表情识别不需要太高的分辨率分辨率过大反而容易过拟合。3.4 训练过程的监控与止损训练不是把命令丢进终端就完事了。我每轮都会看一眼 loss 曲线和验证集 mAP。loss 在最初几个 epoch 下降很快是正常的但如果 30 轮之后还在剧烈波动先看学习率是不是太大。mAP 上不去但 loss 在降大概率是过拟合这时增加数据增强或减小模型规模比继续训练更有效。一个比较玄学的经验是验证集指标高但实际摄像头效果差先检查训练集和测试集的图像分布。公开数据集大多是摄影师拍的正脸、光照均匀摄像头场景下是斜脸、背光、动态模糊模型自然水土不服。解决方式是混入 10% 左右的摄像头实拍数据重新微调效果立竿见影。4. 推理与部署从图片检测到摄像头实时识别的完整链路4.1 两段式推理管线的实现顺序系统运行时的流程是YOLOv11 首先对整帧图片做人脸检测得到边界框然后按边界框裁切人脸区域缩放到固定尺寸最后把缩放后的人脸图送进表情分类模型得到表情类别。import cv2 import numpy as np def run_inference(frame, detect_model, expression_model, conf_thres0.5): results detect_model(frame, confconf_thres, verboseFalse)[0] boxes results.boxes.xyxy.cpu().numpy().astype(int) output [] for x1, y1, x2, y2 in boxes: face_crop frame[y1:y2, x1:x2] if face_crop.size 0: continue face_resized cv2.resize(face_crop, (112, 112)) face_input face_resized[:, :, ::-1].transpose(2, 0, 1) # BGR转RGB face_input np.expand_dims(face_input, axis0).astype(np.float32) / 255.0 pred expression_model(face_input) expr_id pred.argmax(axis1)[0] output.append((x1, y1, x2, y2, int(expr_id))) return output这段代码是整套系统的核心粘合层。注意conf_thres0.5是检测置信度阈值摄像头场景建议降到 0.35 到 0.4因为动态模糊会让置信度整体偏低。face_crop必须判空检测框越界时会切出空的数组直接送进分类模型会报错。112x112是表情分类模型的输入尺寸和训练时保持一致不一致会导致精度断崖式下降。4.2 摄像头实时推理的帧率优化实时视频流和单张图片推理有一个本质区别连续帧之间存在大量重复信息。如果每一帧都做完整的两段式推理多数电脑扛不住。cap cv2.VideoCapture(0) frame_skip 2 # 每隔2帧处理一次 frame_count 0 while True: ret, frame cap.read() if not ret: break frame_count 1 if frame_count % frame_skip ! 0: continue # 可选缩小输入尺寸加快检测 frame_small cv2.resize(frame, (640, 480)) detections run_inference(frame_small, detect_model, expression_model) # 在原始帧上绘制检测结果 for x1, y1, x2, y2, expr_id in detections: cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, str(expr_id), (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imshow(face_expression, frame) if cv2.waitKey(1) 0xFF ord(q): break参数上frame_skip2代表每三帧处理一帧手感基本无感但推理负载降一半。如果还是卡优先把输入帧从 1080p 缩到 720p损失一点小脸检测精度换回流畅度。注意detect_model内部也做了 resize这种双重缩放会让标注框位置偏大所以我在实际项目里更推荐只对检测模型输入做缩放不在循环里额外缩小帧。4.3 置信度阈值与 NMS 参数的现场调优检测结果被 NMS 过滤后能留下来的框取决于两个参数conf_thres控制候选框的置信度底线iou_thres控制重叠框的合并力度。这两个参数是现场调参的常客。参数值效果适用场景conf_thres0.5干净但容易漏检图片质量好、人脸清晰conf_thres0.35均衡摄像头、有动态模糊conf_thres0.25召回高但误检增加远距离、小脸场景iou_thres0.45默认值单人脸和多人脸均可iou_thres0.6重叠框合并更激进侧脸、遮挡场景实际调参时不要只盯检测框数量。我一般会在同一帧上连续滑动 conf 值观察框的位置是否稳定。如果框在脸上轻微抖动说明 iou 阈值偏低如果同一个人的脸被框了两次说明 iou 阈值偏高。这个调节过程没有捷径只能多看几帧。5. 常见问题与避坑训练和部署中最容易翻车的几个点5.1 表情类别和索引错位模型训练却没报错现象训练时 loss 正常下降验证集准确率也不错但跑摄像头推理时所有表情都多偏一位或者“开心”和“难过”互换。原因数据集标签的类别 id 和模型配置文件里的类别顺序不对应。标注软件生成的 txt 里写的是 0 到 6但 model.yaml 里的类别列表可能是从 1 到 7。解决写一个脚本读取第一张标签文件和模型配置逐行打印类别 id 和名称的对应关系。# 检查标签id与类别名映射 cls_names [anger, disgust, fear, happy, neutral, sad, surprise] label_file dataset/labels/00001.txt with open(label_file) as f: line f.readline().strip() cls_id int(line.split()[0]) print(f标签文件里的id: {cls_id}) print(f模型配置里的对应类别: {cls_names[cls_id]}) print(f实际表情: 根据图片人工确认)先确认一张图再确认代码路径里 cls_names 的顺序和训练配置一致。从那以后我再也没犯过这个错但这个坑几乎每隔一段时间就会出现在评论区里。5.2 摄像头推理卡顿显示帧率却没有明显降低现象摄像头画面看起来一顿一顿的但代码里打印 FPS 有 20 多。原因打印的是检测线程的处理频率不是画面显示频率。摄像头采集和检测是串行关系检测偶尔慢一帧但 FPS 统计只算处理成功了的时间或者队列里积压了旧帧。解决用时间戳统计“从读到帧到画完框”的完整耗时并且在循环里手动清空队列积压帧。具体做法是把cv2.VideoCapture的缓冲区调小或者每次read()前循环读取并丢弃若干旧帧。5.3 验证集 mAP 很高一到现场就乱框现象公开数据上训练的模型换到教室、办公室等真实场景框的位置飘表情也不准。原因域差异。公开数据集大多是摆拍正脸、光照充足、背景干净现场有逆光、侧脸、低头、遮挡。解决在数据集中混入 20% 左右的现场实拍数据重新微调。我把实拍数据单独放一个文件夹用 0.4 的学习率系数微调损失函数权重不变效果提升明显。如果没有实拍数据先做随机亮度扰动和水平翻转也能缓解。5.4 训练 loss 突然变 NaN之前的权重全白费现象训练到某个 epochloss 变成 nan之后所有指标归零之前的训练成果等于作废。原因最常见是学习率过高或数据里出现了异常的标签值。比如坐标大于 1 的归一化框、类别 id 超出配置范围让损失函数算出了无穷大。解决先检查标签文件里有没有坐标 1 或 0 的行把异常行过滤掉。然后降低初始学习率到 0.005并开启warmup_epochs。如果还是 nan检查输入图片有没有全黑或全白的损坏文件这类图片会让特征的方差为 0。5.5 自定义模型接 YOLOv11 特征时维度对不上现象定义好自定义分类模型后一前向传播就报矩阵维度错误。原因neck 输出的特征层通道数和分类头in_channels不一致。YOLOv11 不同版本输出通道不一样s 和 n 的通道数差异很大填错其中一个就对不上。解决先加载官方预训练权重打印模型每一层输出的张量形状再把打印结果填进自定义配置的in_channels。我通常会在脚本里加一段代码直接遍历注册到模型上的所有模块并输出 shape一步到位不做任何猜测。6. 进阶用法从跑通到真正可用的三条细节技巧6.1 用验证集做置信度校准表情分类模型输出的概率分布往往偏保守实际使用时很容易所有类别概率都在 0.3 到 0.5 之间argmax 不准。常见做法是在验证集上统计每个类别的平均概率然后算出每个类别的校准偏移。import numpy as np calibration {} for class_id in range(num_classes): scores validation_scores[validation_labels class_id] calibration[class_id] 1.0 / (scores.mean() 1e-6) calibrated raw_probabilities * np.array([calibration[i] for i in range(num_classes)]) pred_class calibrated.argmax(axis1)这段代码把概率偏低的类别放大偏高的类别压低相当于给分类头做了个通道注意力。参数上注意calibration的系数不要大于 3否则会过度放大噪声类别反而把准的类别带偏。6.2 把模型导出成加速引擎格式部署阶段PyTorch 动态图的推理开销比较大。我一般在训练收敛后把 YOLOv11 检测模型和自定义表情模型都导出成静态图格式再转成半精度权重跑。静态图的好处是输入尺寸固定后网络结构被提前展开推理路径短了很多。导出后记得重新用验证集跑一遍精度半精度在小数点后几位有截断通常精度损失在 0.5% 以内超过这个范围就要检查有没有数值溢出。6.3 用眼睛状态做表情的辅助判断很多情况下表情识别误判来自嘴巴和眼睛的状态冲突。一个实用的技巧是在检测到人脸后额外切出眼睛区域用一个二分类模型判断“睁眼/闭眼”。如果嘴巴是笑的但眼睛闭着多半是打哈欠或假笑如果眼睛睁大且眉毛抬高大概率是惊讶。这个小分支模型很小单独跑一遍只增加几毫秒但能把惊讶和开心、难过和困倦之间的混淆明显拉开。这套系统我从训练到部署完整跑过几轮才稳定下来。最直接的教训是换任何一批新数据都要先跑一遍标签检查和类别分布脚本再开始训练。每一轮自定义模型头修改后也要先打印输出维度再训练。这些都是十分钟内能做完的事但能省掉后面数小时的重训成本。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →