基于MTCNN和XCEPTION的人脸检测与表情识别优化实践
简介这份人工智能项目实践资源围绕“人脸检测表情识别”展开面向希望用更高精度算法完成人脸检测并对七类基本表情进行识别的开发者、学生或工程人员。项目在经典 face_classification 基础上做了针对性改进由原 OpenCV Haar 级联检测升级为 MTCNN 检测增加 detect_face.py 人脸检测程序并修改视频表情识别演示代码同时保留训练脚本、Grad-CAM 可视化与多种输入方式结构完整便于二次开发。压缩包共 73 个文件其中35个 hdf5 为训练好的性别/表情模型22个 Python 文件覆盖训练、检测、演示等流程另有 XML 模型配置、数据集目录、报告 PDF 和示例图/动画整体大小 76.05MB。已有 274 人学习下载。通过这套资源读者能直接运行现成模型完成实时/图片人脸表情识别也可对比 MTCNN 与传统 Haar 检测差异深入了解模型训练、数据准备、可视化等完整项目链路适合作为课程设计、毕业设计或项目预研的参考。1. 一份人脸检测与表情识别项目的升级实录把 OpenCV 换掉之后能稳多少这份人工智能项目实践里最有价值的不是表情识别分类器本身而是把检测器从 OpenCV 默认人脸检测换成 MTCNN 之后的整体可用性提升。拿到解压包对比旧仓库和新版几乎只改了 detect_face.py 以及 demo 脚本的调用方式但正脸摆拍以外的场景漏检率明显下降。所谓“人脸检测加表情识别”落到代码上就是两段链路先用 MTCNN 找到人脸框再用预训练的 XCEPTION 识别 FER2013 的七类表情同时用另一个分类器判断性别。要是做 AI 大作业、毕业设计演示或者项目实践需要能跑的完整样例都会从这份资源里受益。我直接把仓库里几个关键脚本过了一遍下面按选型原因、代码走读、复现步骤、避坑顺序来写。2. 选型把 OpenCV 换掉之后为什么要用 MTCNN 和 XCEPTION2.1 Haar 级联的上限和 MTCNN 的补位原仓库是较早时期的开源项目OpenCV 提供的 haarcascade_frontalface_default.xml 是 Viola-Jones 时期的产物。正脸、平视、光线均匀时速度极快但实际摄像头场景里低头写作业、三人合影的侧面、逆光窗户都会直接漏检。漏检之后整个链路失败表情识别再准也没有用——上游缺框下游连输入都没有。所以仓库里除了保留 haarcascade 文件还多了一个 detect_face.py。我的理解是它对 MTCNN 做了封装。MTCNN 全称 Multi-task Cascaded Convolutional Networks三个小网络级联第一层 P-Net 粗扫全图产生候选框第二层 R-Net 精修并过滤第三层 O-Net 输出最后的边界框和五个关键点左眼、右眼、鼻尖、左嘴角、右嘴角。三个网络一次前向既出框又出关键点这意味着可以做后续的人脸对齐对表情识别也很关键。从个人使用习惯来说我不会在每一路视频流里同时跑 Haar 和 MTCNN两者互补的场景不多。Haar 快但弱MTCNN 稳但稍慢。除非在嵌入式设备上做实时否则无脑 MTCNN 的收益通常高于损失。而这一份资源正是沿着这个方向改的检测准确度的提升主要来自这里。2.2 检测器对比一次很直观的换道对比项OpenCV Haar 检测MTCNN 检测detect_face.py典型漏检侧脸、低头、遮挡、暗光大部分场景改善明显输出信息矩形框矩形框 关键点 置信度计算成本低CPU 可实时比 Haar 高不少视频流需注意帧率对齐支持无可基于关键点做校正集成难度cv2 自带需额外安装 mtcnn 包换检测器不是“调参”而是换了整个召回模型。MTCNN 的 min_face_size 参数控制了最小人脸像素调大之后漏检变多但速度更快调小以后侧脸和远脸更容易被抓到但误检也会变多。detect_face.py 里常见的做法是保留默认阈值或者把三级阈值里的后两个调低 0.05 左右。我的经验是会议视频场景里把置信度要求放低一点给表情识别留出余量。2.3 XCEPTION 表情分类器和 FER2013 的七个标签仓库里两个预训练模型分别是 fer2013_mini_XCEPTION 和 fer2013_big_XCEPTION都是基于 FER2013 数据集训练的 XCEPTION 结构。XCEPTION 的核心是 depthwise separable convolution把通道卷积和空间卷积拆开参数量更小、训练更快在表情识别任务里是典型且好用的主干。FER2013 的标签一共有七类angry、disgust、fear、happy、sad、surprise、neutral。实际使用中不少项目会把 disgust 合并掉因为样本量太小但这份资源保留了完整 7 类。输出层用 softmax直接给出每个类别的概率。我一般看结果时不只取 argmax而是把前两名概率一起打印出来尤其在 happy 和 surprise 这类容易混淆的样本上前两名连看才能判断模型是不是在犹豫。2.4 模型文件里两个量级怎么选trained_models/emotion_models 下的文件适用场景fer2013_mini_XCEPTION.119-0.65.hdf5体积更小CPU 推理更快适合视频流 demofer2013_big_XCEPTION.54-0.66.hdf5参数更多理论上限更高适合单张图片精细识别选择原因不玄学在这台机器上先跑 mini如果准确率不够再换 big。文件名里的 119 和 54 是 epoch0.65 和 0.66 是验证精度所以两个模型都接近收敛状态。需要知道的是用 hdf5 权重时加载前建议把 keras.backend.clear_session() 放在前面否则旧图残留会导致显存占用缓慢上涨具体坑放到第五章展开。3. 代码走读detect_face.py 到 demo 的推理链路3.1 detect_face.py 里的人脸检测封装先看 src 里 detect_face.py 的核心逻辑。from mtcnn import MTCNN import cv2 class FaceDetector: def __init__(self, min_face_size20, thresholds(0.6, 0.7, 0.7)): self.min_face_size min_face_size self.thresholds thresholds self.detector MTCNN(min_face_sizemin_face_size, thresholdsthresholds) def detect(self, frame_bgr): # MTCNN 接收 RGBOpenCV 出来的是 BGR先做一次颜色空间转换 frame_rgb cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB) results self.detector.detect_faces(frame_rgb) boxes [] for res in results: x, y, w, h res[box] conf res[confidence] boxes.append((x, y, x w, y h, conf)) return boxes这里有两个细节。第一颜色空间转换不是可选项MTCNN 内部做特征提取时对通道顺序敏感直接传 BGR 框会歪。第二结果里的 box 是 (x, y, w, h)转成 (x1, y1, x2, y2) 之后才方便后面直接用数组切片裁剪人脸。detect_face.py 的重点就是这段封装以及对外提供一个跟 OpenCV 习惯接近的接口。demo 调用时很少直接操作这个类而是 import detect_face 后用统一的函数入口。原仓库风格的调用链大概是这样from detect_face import FaceDetector detector FaceDetector(min_face_size20) boxes detector.detect(frame_bgr)min_face_size20 的意思是小于约 20 像素的人脸直接忽略。视频流里人脸离镜头远可以降到 10~15代价是误检可能变多。这个参数是第一个需要按场景调的东西我一般会在远程会议和教室场景分别测一轮再固定。3.2 image_emotion_gender_demo.py静态图识别链路把图片里每张脸的表情和性别一起输出的 demo逻辑可以拆成四段。我把关键几步写在下面注意这里是完整链路而不是片段。# 关键步骤逻辑顺序按检测 - 裁剪 - 预处理 - 两个分类器 img cv2.imread(input_path) faces detector.detect(img) for (x1, y1, x2, y2, conf) in faces: # 给检测框一个 margin避免额头/下巴被切掉 margin int(0.2 * (x2 - x1)) y1 max(0, y1 - margin) y2 min(img.shape[0], y2 margin) x1 max(0, x1 - margin) x2 min(img.shape[1], x2 margin) face_roi img[y1:y2, x1:x2] # 表情模型用的是 64x64 灰度图 gray cv2.cvtColor(face_roi, cv2.COLOR_BGR2GRAY) gray cv2.resize(gray, (64, 64), interpolationcv2.INTER_AREA) emotion_input gray.reshape(1, 64, 64, 1).astype(float32) / 255.0 emotion_probs emotion_model.predict(emotion_input, verbose0)[0] # 性别模型常用 48x48 三通道彩色图注意归一化方式 color cv2.resize(face_roi, (48, 48), interpolationcv2.INTER_AREA) gender_input np.expand_dims(color / 255.0, axis0) gender_probs gender_model.predict(gender_input, verbose0)[0]这段才是这份资源里最容易踩坑的地方。表情模型的输入是 64x64 单通道灰度性别模型不见得相同如果两个模型都用同一个预处理会出现其中一个准确率崩坏的现象。FER2013 本身是灰度图所以表情分类器接受灰度合情合理性别分类器一般训练在彩色图上仓库里按彩色传即可。归一化要统一到 [0, 1]OpenCV 读出的 uint8 像素需要除以 255.0漏了这一步概率输出会全部集中在某一个类别上。margin 参数来自工程经验。MTCNN 框得再准也会把额头切掉一点表情识别对五官区域敏感框太紧会直接掉点。0.2 这个系数是这类 demo 里常见的做法我自己的测试表明在 0.15~0.3 之间浮动影响不大但超过 0.5 会把背景大片代入反而干扰分类。3.3 video_emotion_gender_demo.py视频流与帧率取舍视频 demo 与静态图 demo 的区别不在于识别而在于循环和绘制。OpenCV 从摄像头或视频文件逐帧读取每帧先送 detect_face再对每个框做表情和性别分类最后用 cv2.rectangle 和 cv2.putText 把结果画回画面。cap cv2.VideoCapture(video_path_or_camera_index) while True: ok, frame cap.read() if not ok: break faces detector.detect(frame) for (x1, y1, x2, y2, conf) in faces: emotion, gender predict_face(frame[y1:y2, x1:x2]) cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, f{gender} {emotion}, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imshow(emotion demo, frame) if cv2.waitKey(1) 0xFF ord(q): break如果直接把每一帧的检测结果画上去MTCNN 在连续帧里会出现同一张脸被重复画框的抖动。原因是相邻帧的框坐标不完全一致MTCNN 对每一帧独立推理没有帧间关联。要做平滑的话可以维护一个上一帧的框列表用 IoU 判断是不是同一张脸再对坐标做指数滑动平均。仓库 demo 没做这一步所以演示时会看到框轻微颤抖这不影响识别结果但如果拿去录屏交作业建议自己加一个轻量跟踪IoU 大于 0.5 就算同一目标然后取当前帧和上一帧坐标的平均值即可。还有一点在 CPU 上跑 MTCNN 每帧大概几十毫秒到百毫秒级视频 demo 的帧率主要卡在这里而不是表情分类。想提速就把每两帧检测一次中间帧沿用上一帧的框——这个策略在大多数视频演示里效果都很能打。4. 复现流程环境、目录与三条运行命令4.1 环境清单里容易翻车的两个位置REQUIREMENTS.txt 是我拿到项目后会第一眼看的文件它决定你能不能在一个小时内跑起来。这份仓库的依赖大概是这样一组tensorflow、keras、opencv-python、mtcnn、numpy、matplotlib。这几个包的历史版本冲突是最常见的卡壳点新版 TensorFlow 2.x 里 Keras 被内置之后直接 import keras 可能出现版本不匹配这时候把 keras 固定为 2.x 相对稳妥。pip install tensorflow2.4.1 keras2.4.3 opencv-python mtcnn numpy matplotlib这是我在本机验证过相对稳妥的一组。TensorFlow 2.4.1 和 Keras 2.4.3 配对比较顺再新的版本不是跑不了是加载 hdf5 权重时可能出现旧权重标记不兼容的复杂报错。你真要用新版 TensorFlow也可以但“开箱即用”就要打折扣需要自己处理迁移。4.2 目录结构和模型放哪不能错解压后你会看到 src 下面有 models、datasets、trained_models 标准布局detect_face.py 放在 src 根目录模型权重放在 trained_models 下的 emotion_models 和 gender_models 里。demo 脚本默认以相对路径去找权重模型文件一旦移动位置加载就会失败。emotion_recognition-master/ ├── src/ │ ├── detect_face.py │ ├── train_emotion_classifier.py │ ├── train_gender_classifier.py │ ├── image_emotion_gender_demo.py │ ├── video_emotion_gender_demo.py │ ├── video_emotion_color_demo.py │ ├── video_dectect_emotion.py │ ├── image_gradcam_demo.py │ ├── video_gradcam_demo.py │ └── models/ │ └── trained_models/ │ ├── emotion_models/ │ │ ├── fer2013_mini_XCEPTION.119-0.65.hdf5 │ │ └── fer2013_big_XCEPTION.54-0.66.hdf5 │ ├── gender_models/ │ └── detection_models/我实践中的习惯是不要把 src 搬到项目根层之外而是在外面写一个小壳脚本去调用 src这样保持相对路径稳定以后换机器也少一次改动。4.3 图像 demo、视频 demo、摄像头 demo 各一条命令最稳的验证路径是先跑静态图。项目自带 images 下面有 test_image.jpg、12_angry_men.jpg、solvay_conference.jpg 这些测试图直接拿来看输出比一上来就连摄像头要好排查很多。# 静态图验证检测和分类是否正常 python src/image_emotion_gender_demo.py --image_path images/test_image.jpg # 视频文件验证连续帧推理 python src/video_emotion_gender_demo.py --video_path videos/demo.mp4 # 摄像头video_path 传 0 代表默认摄像头 python src/video_emotion_gender_demo.py --video_path 0三条命令对应三种输入源。我建议第一次运行只跑第一条因为它只处理一帧即使出错堆栈也简短视频和摄像头一旦出错经常会被后面的帧信息淹没。参数名我按常见写法给出实际仓库脚本里可能写成 --file 或 --input运行前先 python xxx.py --help 看一眼即可。有 Docker 环境的话直接构建镜像也是一种方式仓库里 Dockerfile 设计的是从源码目录构建镜像进入容器后再跑同一套命令。4.4 重新训练分类器时要改的三个地方如果不想只用预训练模型src 里还有 train_emotion_classifier.py 和 train_gender_classifier.py。常见训练入口大概就是读 fer2013.csv 或者自备 CSV然后用数据增强把图片喂给网络训练完成后把 hdf5 存回 trained_models。这里我不展开整个训练代码只提示三个必改项数据集路径。默认路径大概率是 datasets/fer2013/fer2013.csv你要改成自己数据所在位置。类别数。FER2013 如果是 7 类模型输出层就是 7你要是自己只用了 5 类输出层和权重都要对齐。batch size。显存不够就调小到 32CPU 训练建议 16调大只会让等待变长不会提升准确率。训练一次表情分类器到可用的程度在普通消费级 GPU 上也要数小时起步用 CPU 的话建议直接劝退优先用预训练权重改 demo。5. 避坑实录模型加载、颜色通道和帧率跑不动的几个片段5.1 hdf5 权重文件加载报错不是有效的 HDF5现象运行 image_emotion_gender_demo.py 时报 unable to open file或者提示该文件不是有效的 HDF5。原因有两种一是模型文件没有放到脚本期望的相对路径里二是下载时 zip 解压不完整导致 hdf5 文件头损坏。第二种在网盘平台下载时特别常见文件大小看着对但解压出来二进制不完整。解决先核对 trained_models 下文件大小与源仓库标注的字节数做比对再用 python 打开一次确认完整性import h5py f h5py.File(fer2013_big_XCEPTION.54-0.66.hdf5, r) print(f.keys())能打印出 model_weights 之类的 key证明文件完整。打不开就重新解压整个 zip不要单独把 hdf5 拖出来。5.2 颜色通道顺序错乱整张脸识别成“surprise”现象图片能检测表情结果总是错的甚至每一张脸都集中在惊讶这一个类别上。原因OpenCV 读图默认 BGR而 MTCNN 和训练时的数据分布都基于 RGB。三通道输入被直接送进脚本后颜色特征整体偏移模型看到的不是训练时的肤色而是被反转后的伪彩色。解决detect_face.py 里已经做了 cvtColor多数 demo 脚本也做了。问题往往出在自己扩展的新代码里。把所有读图片的地方都检查一遍凡是有 cv2.imread 或 VideoCapture.read后面必须跟一次 COLOR_BGR2RGB表情模型如果是灰度输入就先用 BGR2GRAY不要直接取某一个通道。5.3 视频帧率只有个位数瓶颈大概率在检测器现象用摄像头跑 video_emotion_gender_demo.py 卡到无法接受画面像幻灯片。原因MTCNN 三级网络对 CPU 不友好。静态图看不出问题连续帧场景里检测器变成最大瓶颈。默认设置下每一帧都做完整检测再加上表情、性别两个分类器速度自然起不来。解决先做两步微调。第一步每隔一帧才检测一次中间帧复用上一帧人脸位置第二步把 MTCNN 的 min_face_size 从 20 调到 24 或 30减少候选框数量。如果还不行换用 mini XCEPTION 模型big 模型只在离线的单图识别场景使用。这是用检测频率换实时性的常规操作不影响作业演示的完整度。5.4 同一张脸出现多个框以及框体抖动现象画面上一个人被标了 2~3 个框或者同一个人的框在相邻帧里明显跳变。原因MTCNN 对分辨率较高的脸可能输出重叠候选并且帧与帧之间没有时间一致性人一动框坐标自然跳。解决检测结果按置信度排序再用非极大值抑制把重叠框中置信度低的删掉。写一个 iou_nms 函数IoU 大于 0.3 就保留分数更高的那个。至于抖动在帧间做指数平滑即可公式不复杂box_smooth 0.7 * box_smooth 0.3 * box_current0.7 的平滑系数意味着新框只占 30% 权重快速运动场景可以再降到 0.5。注意这是对同一身份的人脸做平滑跨身份时必须清零否则换人后旧框还会拖尾。5.5 维度不匹配64x64 灰度还是 48x48 彩色现象predict 时报维度错误比如模型需要 (48, 48, 3)你喂进去的是 (64, 64, 1)。原因表情模型和性别模型的输入尺寸、通道数都不同。自己新写代码时很容易套用同一个预处理函数导致形状冲突。解决加载 hdf5 后先打印 model.input_shape再决定走灰度还是彩色分支。也可以写一个通用预处理函数根据 input_shape 自动 resize 和转通道这样后面换模型不用改主逻辑。我在实践中已经把这个函数固化成标准工具每次新项目直接复用省掉不少调试时间。6. 不止于识别用 GradCAM 看模型注意力再定两三个上线参数6.1 GradCAM 可视化的校验价值仓库里同时给了 image_gradcam_demo.py 和 video_gradcam_demo.py这是我觉得很加分的一部分。GradCAM 把分类层的梯度回传到最后一个卷积层生成一张热力图直接看到模型是凭什么特征判出表情的。跑完之后大概是这样的效果一张图片带着彩色热力图叠加输出红色区域是模型判断时主要依据的位置。看热力图能一眼看出模型是不是只盯着背景、脖子或衣服而不是人脸——这种“假达标”在作业演示里很常见因为整体准确率看着并不低。日常我会把夸张表情和侧脸照各跑一张如果热力图中心明显不在脸部区域就说明检测 margin 太大或者预处理有问题。6.2 我固定下来的三组上线参数在这些 demo 基础上真要往外部署我会固定三个值参数推荐值用途MTCNN min_face_size20~24保留远距离小脸同时过滤噪点检测框 margin0.2 倍框宽保护五官区域不被裁切表情分类置信度0.7低于此值标注为“不确定”置信度 0.7 这个值不是拍脑袋。FER2013 本身标签噪声不小模型概率低于 0.7 时前两名往往只差零点零几这时候硬取 argmax 容易放大错误。把结果写成 happy(0.52) / surprise(0.43) 反而比只写一个类别更真实答辩或演示时老师问起来也能自圆其说。6.3 一点个人习惯从那以后我每次拿到这类识别项目第一件事不再是把所有 demo 都跑通而是把检测、预处理、分类三条链路分开走先用一张图验证检测框位置合理再裁剪人脸验证分类准确最后才接入视频流。这个过程看着多花十几分钟实际上把后面排错时间砍掉一大半。表情识别这类模型的黑匣子属性很强不看到中间结果你永远不知道错误是从检测漏框开始还是从颜色预处理开始。保持这个习惯之后再看到那些“识别不准”的项目我也不会先怀疑模型而是先问一句检测框对吗预处理对吗输入分布对吗。希望这个思路也能帮到你。本文还有配套的精品资源点击获取