简介:基于TensorFlow实现的图片人脸情绪识别工具,面向深度学习入门者、课程设计及毕设学生。项目从模型加载到Web界面展示形成完整闭环,既可用于课堂练习,也可作为实战演练与立项演示的参考。压缩包共5个文件,其中2个Python脚本承担后端推理与启动逻辑,2个HTML页面提供前端交互界面,1个H5模型文件存放训练好的情绪识别权重,整体仅2.23MB,轻量易部署。目前已有675人学习下载,可见其实用性和受欢迎程度。下载即可直接运行,源码结构清晰,适合在此基础上扩展表情分类、实时摄像头识别等功能,也能帮助初学者快速理解TensorFlow模型的加载、推理与Web集成流程。
1. 图片人脸情绪识别,为什么值得拆一遍
好几天前有位读者把我写的另一个手写数字识别 demo 改成人脸情绪分类,没跑通就把代码发给我,报错信息是维度对不上。查下来根因很简单:模型加载没有问题,但进入 predict 之前,图像的大小、通道数、归一化顺序全部不一致。这类问题在课程设计和期末作业里非常常见,因为大家都喜欢直接下载别人打包好的“基于TensorFlow实现的图片人脸情绪识别工具”,却很少先弄清楚模型的输入口径。这个资源里包含 index.py、app.py、templates 下的 upload.html 与 index.html,以及一个情绪识别模型 emotion_detection_model.h5,整体思路是用 TensorFlow 训练一个卷积模型,再用 Flask 把推理过程包装成网页功能。适合做毕设演示,也适合第一次接触深度学习落地的同学拆解。CPU 就能跑,单张推理延迟在百毫秒级,不需要抢显卡。
2. 模型加载与图片预处理的正确顺序
2.1 先弄清楚 h5 模型期望的输入
拿到 emotion_detection_model.h5,第一件事不是急着运行,而是查看模型的输入形状。用 TensorFlow 的 Keras 接口加载模型后,直接访问 model.inputs 和 model.outputs,就能看到期望的 shape。如果一开始就跳过这一步,后面一连串维度错误全部源于此。
我一般会写一个独立探针脚本,先确认模型结构:
import tensorflow as tf model = tf.keras.models.load_model('emotion_detection_model.h5') print(model.inputs[0].shape) # 查看输入张量形状 print(model.outputs[0].shape) # 查看输出张量形状 print(model.summary()) # 打印层结构,确认是不是卷积网络这段脚本会打印出类似(None, 48, 48, 1)或(None, 64, 64, 1)的 shape。None 表示 batch 大小随意,后面三个数字分别是图像高度、宽度、通道数。大部分开源情绪识别模型来自 FER2013 数据集,常见输入是 48x48 灰度图,通道数为 1。输出 shape 如果是(None, 7),代表 7 类情绪;如果是 8,则说明是 8 类。这个数字决定了后面标签映射表的长度。
提示:不要看到 h5 文件就直接丢进 Flask。先把探针脚本跑一遍,很多项目里模型能加载,但输入口径不匹配,问题恰恰出在这一步。
2.2 图片读取、缩放、归一化的代码顺序
下面给出处理单张图片的标准函数。我用 OpenCV 读取,先转灰度图,再 resize 到模型期望的尺寸。resize 完成后必须把像素值除以 255 映射到 0-1 区间,否则模型推理结果会非常不可信。
import cv2 import numpy as np def preprocess_image(image_path, target_size=(48, 48)): img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) # 读取为灰度图 if img is None: raise ValueError('图片读取失败,请检查路径') img = cv2.resize(img, target_size) # 缩放到模型输入尺寸 img_array = img.astype('float32') / 255.0 # 归一化到[0,1] img_array = np.expand_dims(img_array, axis=-1) # 增加通道维: (48,48,1) img_array = np.expand_dims(img_array, axis=0) # 增加batch维: (1,48,48,1) return img_array这里有两个容易写错的地方。第一,cv2.resize 的 target_size 参数写法是 (width, height),如果模型输入是矩形,就必须注意 OpenCV 的宽度在前。第二,np.expand_dims 的顺序不能乱,先加通道维还是先加 batch 维取决于你的习惯,但最终 shape 必须和模型输入完全一致。更保守的替代方案是用 reshape,但 expand_dims 的语义更清晰,后续维护时不容易看错。
2.3 推理与情绪标签映射
模型推理用 predict 方法,返回的是一个二维数组,一行对应一张图片在各类别上的概率。常见做法是取出概率最大的索引,再映射为情绪字符串。
emotion_labels = ['angry', 'disgust', 'fear', 'happy', 'neutral', 'sad', 'surprise'] def predict_emotion(model, preprocessed_img): logits = model.predict(preprocessed_img, verbose=0) # 得到概率分布 idx = int(np.argmax(logits[0])) # 概率最大的类别索引 prob = float(np.max(logits[0])) # 对应的最高概率 return emotion_labels[idx], prob pred_label, confidence = predict_emotion(model, preprocess_image('test.jpg')) print(pred_label, confidence)注意,verbose=0 可以屏蔽预测时的进度条,在 Web 请求高频场景下能减少日志刷屏。predict 默认返回(N, C)数组,即使只传了一张图,也必须用[0]取出第一行结果。label 的顺序必须和训练时一致,如果模型是从别处下载的,不要想当然套用 FER2013 顺序,先跑几张已经知道情绪的照片验证一下,确认顺序无误后再固定映射表。
如果发现所有图片都被预测成同一个类,多半是归一化没做对。有些模型训练时像素值就是 0-255,有些则要求在加载时除以 255。分别用 0-255 和 0-1 各跑一遍,看哪个结果更符合直觉,就能反推出模型的训练口径。
3. Flask 入口与 Web 上传链路
3.1 路由设计与模型初始化时机
打开项目里的 app.py,可以看到典型的 Flask 服务结构。模型加载必须放在启动阶段,如果放在每个请求里,接口延迟会上升一个数量级。常见做法是在模块顶层加载模型,然后定义两个路由:根路径返回上传页面,/predict处理图片上传并返回结果。
from flask import Flask, request, render_template, jsonify import tensorflow as tf app = Flask(__name__) model = tf.keras.models.load_model('emotion_detection_model.h5') # 只加载一次 @app.route('/') def index(): return render_template('index.html') @app.route('/predict', methods=['POST']) def predict(): if 'image' not in request.files: return jsonify({'error': 'no image file'}), 400 file = request.files['image'] if file.filename == '': return jsonify({'error': 'empty filename'}), 400 img_path = 'temp_upload.png' file.save(img_path) preprocessed = preprocess_image(img_path) label, confidence = predict_emotion(model, preprocessed) return jsonify({'emotion': label, 'confidence': confidence})注意 predict 路由每次都把文件保存到磁盘,虽然方便调试验证,但生产环境里频繁写盘会增加延迟。我一般会直接读文件流到内存,再用 cv2.imdecode 解码,省去落盘步骤。这里保留 file.save 是为了贴近初学者直觉——先跑通,再优化。
3.2 上传页面与结果展示
项目里的 templates 目录下有 index.html 和 upload.html。upload.html 负责收集用户选择的图片,index.html 承担展示结果的任务。典型的表单提交方式如下:
<form action="/predict" method="post" enctype="multipart/form-data"> <input type="file" name="image" accept="image/*" required> <button type="submit">识别情绪</button> </form>enctype="multipart/form-data" 是文件上传必须设置的属性,漏掉之后 Flask 的 request.files 会一直为空。accept="image/*" 可以在浏览器端先过滤掉非图片文件,但后端仍然要再检查一遍,不能只依赖前端约束。
3.3 用 curl 独立验证 Web 接口
很多同学把浏览器当成唯一测试工具,接口报错了只能看到白屏或 500。换个思路,先用 curl 单独验证后端逻辑,能省去大量“前端改了还是没用”的无效调试。启动 Flask 服务后,在项目目录执行:
curl -X POST -F "image=@test_face.jpg" http://127.0.0.1:5000/predict如果正常返回类似{"emotion":"happy","confidence":0.97},说明后端链路已经通了。如果返回 500,直接看终端里的 Flask 日志,它会打印出错的具体代码行,定位要比猜测快得多。下面这张表可以帮你快速定位接口状态:
| HTTP 状态 | 含义 | 常见原因 |
|---|---|---|
| 400 | 请求参数错误 | 没传文件、文件名为空、文件不是图片 |
| 500 | 服务端异常 | 模型输入 shape 不对、图片解码失败 |
| 200 | 正常返回 | 情绪标签与置信度都拿到 |
3.4 并发与临时文件隔离
多人同时上传时,固定文件名 temp_upload.png 会被互相覆盖,这是此类项目里最常见的隐性 Bug。我一般会把文件名改成 uuid 加上时间戳再保存,推理完成后立即删除。
import uuid from pathlib import Path unique_name = f"{uuid.uuid4().hex}.png" save_path = Path('uploads') / unique_name save_path.parent.mkdir(exist_ok=True) file.save(str(save_path)) label, confidence = predict_emotion(model, preprocess_image(str(save_path))) save_path.unlink(missing_ok=True) # 推理完成后删除临时文件参数说明:uuid.uuid4().hex 生成 32 位随机十六进制字符串,能有效避免并发碰撞。save_path.parent.mkdir 确保 uploads 目录存在,不存在则自动创建。unlink 删除临时文件,missing_ok=True 让文件已经被删时也不会抛异常。如果调试阶段想看现场图片,把 unlink 那一行注释掉即可。
4. 参数边界与常见坑
4.1 维度不匹配的报错信息怎么看
运行 predict 时最常遇到Input 0 of layer sequential is incompatible with the layer。这个报错会直接告诉你期望的 shape 和实际传入的 shape。例如期望(1,48,48,1),实际得到(1,48,48,3),说明你的预处理函数没有把 RGB 转灰度,或者加通道维时 channel 位置写错。我的通用处理法则:先把模型输入 shape 打印出来写死在 preprocess_image 里,然后用断言验证处理结果。
assert img_array.shape == (1, 48, 48, 1), f"unexpected shape: {img_array.shape}"断言在开发和演示阶段非常有价值。一旦 shape 不对,程序会立刻停下,而不是等模型推理时才抛出一个更晦涩的错误。如果报错里期望 shape 是(None, 64, 64, 1),把 target_size 改成 (64,64)。确认 shape 之后,再排查归一化和通道数。
4.2 灰度图与彩色通道的选择
有些情绪识别模型直接接收彩色图,输入是(48,48,3)。如果硬转灰度会丢失颜色信息,情绪识别准确率也会下降。判断依据仍然是探针脚本的输出。如果模型 summary 第一层 Conv2D 输入是(48,48,1),就按灰度处理。如果是(48,48,3),就别用 IMREAD_GRAYSCALE,改用 cv2.imread 后转 RGB。OpenCV 默认读出来是 BGR,如果和训练时的 RGB 顺序错位,预测结果会变得不稳定。
| 模型输入 shape | 读取方式 | 注意点 |
|---|---|---|
| (48,48,1) | IMREAD_GRAYSCALE | 加通道维放在最后,shape 为 (1,48,48,1) |
| (48,48,3) | cv2.imread + cvtColor | OpenCV 默认 BGR,要转为 RGB |
| (64,64,1) | IMREAD_GRAYSCALE + resize(64,64) | resize 参数顺序是 宽在前、高在后 |
4.3 环境依赖与 TensorFlow 版本冲突
h5 模型基于 TensorFlow 保存,加载时最好使用同一个大版本。TensorFlow 2.16 加载一个 2.5 保存的模型大部分情况下兼容,但遇到自定义层或旧版 Optimizer 时会出现 Unknown layer 之类的报错。这时候先把报错里提到的类名找出来,到模型源码里找到对应自定义类,加载时用custom_objects传入。
本地演示建议用 Anaconda 建一个干净的 Python 3.9 环境,再安装tensorflow-cpu,不要和原有深度学习环境混在一起。这类 h5 资源体积不大,CPU 单张推理在 0.1 秒上下,没必要为了一个 demo 折腾 GPU 版 TensorFlow 和 CUDA。顺便说一句,现在 TensorFlow 与 PyTorch 之间的生态选择很热闹,但历史遗留的 h5 项目在课程设计里存量仍然很大,兼容问题的处理方式反而成了必会技能。
4.4 前端预览图与后端结果不一致
网页上传后识别出的情绪,和你在相册里看到的图像对不上,通常有两种原因。第一种是浏览器显示预览图时自动处理了 EXIF 方向,但后端读到的原图没有应用这个方向信息,人脸是翻转的。解决办法是用 Pillow 的ImageOps.exif_transpose做一次方向矫正再送入模型。第二种是用户上传了包含多张人脸的截图,而模型直接吃整图,没有人脸检测步骤。这个资源既然没有人脸框选逻辑,输入就必须控制在单张正脸。如果想扩展多人脸识别,需要先接 OpenCV 的 Haar Cascade 或者 MTCNN 做人脸裁剪,再逐个人脸推理。
另外,如果你在 Windows 上运行app.run(debug=True),模型会被加载两次,因为 debug 模式会启用 reloader。模型文件稍大时启动速度变慢。调试用 debug=True,做完演示切回debug=False。
5. 把 demo 变成自己的东西:微调与重训练
5.1 用少量自己的数据做增量训练
下载资源里的模型基于公开数据集训练。如果你想识别特殊表情,比如吐舌头、嘟嘴,可以在现有模型上做迁移学习:冻结前几层,只训练分类层。先加载原模型,替换顶层全连接层,接一个适配自己类别数的 softmax 层:
base_model = tf.keras.models.load_model('emotion_detection_model.h5') base_model.trainable = False x = base_model.layers[-2].output # 取倒数第二层特征 outputs = tf.keras.layers.Dense(7, activation='softmax', name='emotion_output')(x) new_model = tf.keras.Model(inputs=base_model.input, outputs=outputs) new_model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])参数说明:trainable 设为 False 会冻结底层卷积特征,只训练新增的全连接层。第一次训练时新层参数随机初始化,loss 偏大属于正常现象。如果只做四类情绪,把 Dense(7) 改成 Dense(4),训练数据的标签也要对应调整。
5.2 训练数据组织与数据增强
建议把图片按类别分目录,使用 ImageDataGenerator 读取并做增强。旋转范围控制在 10 度以内,翻转概率 0.5,亮度抖动幅度小一些,太强的增强会把真实表情特征扭曲掉。
train_datagen = tf.keras.preprocessing.image.ImageDataGenerator( rescale=1.0 / 255, rotation_range=10, width_shift_range=0.05, height_shift_range=0.05, horizontal_flip=True ) train_generator = train_datagen.flow_from_directory( 'train_data', target_size=(48, 48), color_mode='grayscale', batch_size=32, class_mode='categorical' )flow_from_directory 会按子目录的字母序生成类别标签,这个顺序和 app.py 里的 emotion_labels 不一致时,预测结果就会全部错位。训练完打印train_generator.class_indices,拿它去更新 emotion_labels 数组。这是最容易踩中的部署事故,很多人训练时准确率很高,一上线预测全错,查到最后就是 class_indices 顺序问题。
5.3 验证后替换模型
如果想快速判断新模型能不能部署,至少要看两个数:验证集准确率和 loss 曲线。如果训练准确率接近 1 而验证准确率还停在 0.6,说明过拟合了,减少训练轮数,或者回到 5.2 增大增强强度。验证完成后把新模型另存为 h5 文件,再替换项目里的 emotion_detection_model.h5。替换前备份原模型,替换后拿自己的实拍图和几张开源测试图各跑一遍,确认各情绪类别输出合理。这样的人脸情绪识别工具才算真正属于你,而不是一个只在终端闪两下的 demo。
本文还有配套的精品资源,点击获取