简介本资源面向计算机视觉入门与进阶开发者提供一套基于TensorFlow与OpenCV的人脸识别与表情识别完整项目方案可用于安全监控、人机交互等场景的学习与二次开发。项目围绕人脸检测、特征提取与七类基本表情分类展开涉及MTCNN、FaceNet、VGG16、ResNet等模型的构建、微调与评估并借助OpenCV完成人脸定位、灰度转换、归一化及实时视频流处理。压缩包共23个文件约9.29MB包含8个Python脚本、8个编译文件、1个hdf5模型权重、1个xml配置、1个pdf说明及演示视频与动图等覆盖源码、权重、配置与文档目录结构清晰。已有500人学习下载。通过该资源读者可掌握从数据预处理、模型训练到实际预测的完整流程理解深度学习在计算机视觉中的落地方式并具备开发相关识别系统的能力。1. 从一张合照里同时认出“谁”和“什么心情”这套源码能跑通什么公司团建拍完合照老板丢过来一句“把每个人截出来顺便看看谁笑得最开心”——如果你第一反应是打开 PS 手动抠图那这套基于深度学习的人脸识别加表情识别设计就是给你准备的。它用 TensorFlow 搭卷积神经网络做表情分类用 OpenCV 负责摄像头采集、人脸检测和画框两条链路串起来先定位人脸再判断这张脸属于谁、当前是什么情绪。整套代码是典型的深度学习实战项目结构数据、训练、推理、可视化四块分得比较清楚适合想拿一个完整案例入门 CNN 的从业者也适合要交课程设计、做课堂状态检测这类小系统的同学。它不追求工业级精度但胜在链路完整、依赖常见、能在一台带 GPU 的普通电脑上跑起来改改数据就能换成自己的场景。2. 环境配置与数据准备把 TensorFlow、OpenCV 和 GPU 版本对齐2.1 为什么这套组合对版本这么敏感人脸识别和表情识别放在一个工程里最容易被忽略的坑不是模型结构而是 TensorFlow 和 OpenCV 的版本咬合。表情识别走的是 TensorFlow 的 CNN 训练与推理人脸检测常见做法是用 OpenCV 自带的 Haar 级联或 DNN 模块两者对 NumPy 的版本要求经常打架TensorFlow 2.x 早期版本锁 NumPy 1.19 以下而新版 OpenCV 又要求更高的 NumPy。血泪经验是先把 TensorFlow 装稳再让 pip 自己去解 OpenCV 的依赖不要反过来。另一个敏感点是 GPU。深度学习环境配置 GPU 版时CUDA、cuDNN 和 TensorFlow 三者版本必须严格对应差一个小版本就可能出现“能 import 但一训练就崩”的玄学现象。如果只是跑推理和演示CPU 版完全够用训练阶段再上 GPU 更划算。2.2 从零搭一个可复现的环境下面这套流程在 Ubuntu 20.04 和 Windows 上都能走通区别只在 CUDA 驱动安装方式。建议用 conda 建独立环境避免污染系统 Python。# 创建独立环境Python 版本跟 TensorFlow 走 conda create -n face_expr python3.8 -y conda activate face_expr # 先装 TensorFlowGPU 版会自动带 CUDA 依赖Linux 下 pip install tensorflow2.10.0 # 再装 OpenCV 和常用科学计算库 pip install opencv-python4.8.0.74 pip install numpy pandas matplotlib scikit-learn pillow逻辑说明先建环境是为了隔离依赖Python 3.8 是 TensorFlow 2.10 官方支持较稳的版本。TensorFlow 放最前面装是因为它会锁定一批底层依赖的版本区间后面装 OpenCV 时 pip 会在这些约束下求解。参数上tensorflow2.10.0是最后一个在 Windows 原生支持 GPU 的版本如果你在 Windows 且要用显卡选它省事Linux 下可以上更新的版本。装完做一次自检确认 GPU 是否真的被识别import tensorflow as tf import cv2 import numpy as np print(TF:, tf.__version__) print(OpenCV:, cv2.__version__) print(NumPy:, np.__version__) print(GPU 可用:, tf.config.list_physical_devices(GPU))如果GPU 可用返回空列表说明要么没装 GPU 版要么 CUDA 没配好。此时先别急着折腾驱动用 CPU 把流程跑通确认代码逻辑没问题再回头解决 GPU。2.3 数据集的目录结构与标签映射表情识别常用 FER2013 这类公开数据集7 类情绪生气、厌恶、恐惧、开心、难过、惊讶、中性。人脸识别部分则通常用 LFW 或自己采集的人脸。工程里最稳妥的做法是按类别分文件夹让数据加载器直接读目录名当标签。dataset/ ├── train/ │ ├── angry/ │ ├── disgust/ │ ├── fear/ │ ├── happy/ │ ├── sad/ │ ├── surprise/ │ └── neutral/ └── val/ └── 同样的 7 个类别文件夹目录名就是标签顺序由class_names sorted(os.listdir(train_dir))决定训练和推理必须用同一份顺序否则会出现“模型说开心、你显示难过”的翻车。常见做法是把这份顺序存成labels.json推理时读回来别靠记忆。提示FER2013 原始数据是 CSV 格式需要先转成图片再按上面结构摆放。转换时注意像素值是 0-255 的灰度图别当成彩色三通道读。3. 表情识别 CNN 的搭建、训练与调参3.1 网络结构怎么定别一上来就堆 ResNet表情识别的输入通常是人脸区域的灰度图48×48 或 64×64 就够。这个尺度下一个三四层的卷积网络就能拿到不错的基线堆太深反而过拟合。常见做法是“卷积 池化”重复三次接全局池化或展平最后全连接输出 7 类。import tensorflow as tf from tensorflow.keras import layers, models def build_emotion_cnn(num_classes7, input_shape(48, 48, 1)): model models.Sequential([ # 第一层卷积提取边缘、纹理等低级特征 layers.Conv2D(32, (3, 3), activationrelu, input_shapeinput_shape), layers.BatchNormalization(), layers.MaxPooling2D((2, 2)), layers.Dropout(0.25), # 第二层组合出眼睛、嘴角等局部结构 layers.Conv2D(64, (3, 3), activationrelu), layers.BatchNormalization(), layers.MaxPooling2D((2, 2)), layers.Dropout(0.25), # 第三层更高层语义 layers.Conv2D(128, (3, 3), activationrelu), layers.BatchNormalization(), layers.MaxPooling2D((2, 2)), layers.Dropout(0.25), layers.Flatten(), layers.Dense(128, activationrelu), layers.Dropout(0.5), layers.Dense(num_classes, activationsoftmax) ]) return model model build_emotion_cnn() model.summary()逻辑说明每个卷积块后接 BatchNormalization 是为了稳定训练、加快收敛Dropout 用来抑制过拟合。参数上卷积核统一 3×3 是性价比最高的选择通道数 32→64→128 逐层翻倍符合特征从低级到高级的规律最后 Dropout 0.5 比前面的 0.25 更狠因为全连接层参数量大、最容易记住训练集。input_shape里的 1 表示灰度单通道如果你用彩色图就改成 3。3.2 数据增强与训练循环表情数据集普遍偏小且类别不平衡“开心”样本往往远多于“厌恶”。不处理的话模型会倾向于预测多数类。常见做法是加数据增强并在编译时用类别权重。from tensorflow.keras.preprocessing.image import ImageDataGenerator train_datagen ImageDataGenerator( rescale1./255, # 归一化到 0-1 rotation_range15, # 随机旋转 ±15 度 width_shift_range0.1, # 水平平移 height_shift_range0.1, # 垂直平移 horizontal_flipTrue, # 水平翻转 zoom_range0.1 # 随机缩放 ) val_datagen ImageDataGenerator(rescale1./255) train_gen train_datagen.flow_from_directory( dataset/train, target_size(48, 48), color_modegrayscale, class_modecategorical, batch_size64 ) val_gen val_datagen.flow_from_directory( dataset/val, target_size(48, 48), color_modegrayscale, class_modecategorical, batch_size64 ) model.compile( optimizertf.keras.optimizers.Adam(learning_rate1e-3), losscategorical_crossentropy, metrics[accuracy] ) history model.fit( train_gen, validation_dataval_gen, epochs50, callbacks[ tf.keras.callbacks.EarlyStopping(patience8, restore_best_weightsTrue), tf.keras.callbacks.ReduceLROnPlateau(factor0.5, patience4) ] )逻辑说明rescale1./255把像素压到 0-1是 CNN 训练的常规操作不做的话梯度容易爆炸。旋转和平移模拟真实拍摄中的姿态变化水平翻转对表情识别要谨慎——左右脸对称的表情翻转后语义不变但“左眼眯”这类细节会变所以翻转是否开启要看数据。EarlyStopping的 patience8 表示验证集 8 轮不提升就停restore_best_weightsTrue是后悔药保证留下的是最优权重而不是最后一轮的。ReduceLROnPlateau在指标停滞时把学习率减半帮助跳出平台期。3.3 训练过程看什么指标训练时别只盯 accuracy。表情识别类别不平衡accuracy 高不代表模型真的好。重点看验证集的混淆矩阵尤其是“恐惧”和“惊讶”这两类它们在人脸上都表现为张嘴瞪眼最容易混。如果这两类互相误判严重说明特征区分度不够可以考虑加大数据增强力度或者引入注意力机制。import numpy as np from sklearn.metrics import confusion_matrix, classification_report val_gen.reset() preds model.predict(val_gen, verbose1) y_pred np.argmax(preds, axis1) y_true val_gen.classes print(classification_report(y_true, y_pred, target_nameslist(val_gen.class_indices.keys()))) print(confusion_matrix(y_true, y_pred))classification_report会给出每类的精确率、召回率和 F1比单一 accuracy 信息量大得多。如果某一类召回率特别低优先补这类样本而不是盲目加层。4. 人脸检测与识别串联OpenCV 定位、TensorFlow 判情绪4.1 用 OpenCV 先框出人脸表情识别的前提是拿到人脸区域。OpenCV 自带 Haar 级联检测器开箱即用适合快速验证对精度要求高时换成 DNN 人脸检测器。import cv2 # 加载 Haar 级联人脸检测器OpenCV 自带 face_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) def detect_faces(frame): gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces face_cascade.detectMultiScale( gray, scaleFactor1.1, # 每次缩放比例越小越慢但越全 minNeighbors5, # 邻域阈值越大越严格、误检越少 minSize(48, 48) # 最小人脸尺寸跟模型输入对齐 ) return faces, gray逻辑说明scaleFactor1.1是精度和速度的折中调到 1.05 更全但更慢。minNeighbors控制误检人脸密集场景调大漏检多就调小。minSize设成 48×48 是为了和表情模型的输入对齐太小的脸放大后模糊识别没意义。4.2 把检测到的人脸喂给表情模型检测框拿到后裁剪、转灰度、缩放到 48×48、归一化再送进模型。这一步的预处理必须和训练时完全一致否则精度断崖式下跌。def predict_emotion(model, face_img, class_names): # 统一预处理灰度 - 缩放 - 归一化 - 加 batch 维度 gray cv2.cvtColor(face_img, cv2.COLOR_BGR2GRAY) resized cv2.resize(gray, (48, 48)) normalized resized.astype(float32) / 255.0 input_tensor normalized.reshape(1, 48, 48, 1) preds model.predict(input_tensor, verbose0) idx int(np.argmax(preds)) return class_names[idx], float(preds[0][idx])逻辑说明reshape(1, 48, 48, 1)里的第一个 1 是 batch 维度模型永远按批处理单张图也要凑成一批。class_names必须和训练时的顺序一致这就是前面强调存labels.json的原因。返回置信度是为了在画框时过滤低置信结果避免满屏乱标。4.3 摄像头实时推理的完整循环把检测、识别、画框串起来就是一个能实时跑的表情识别 demo。cap cv2.VideoCapture(0) class_names [angry, disgust, fear, happy, sad, surprise, neutral] while True: ret, frame cap.read() if not ret: break faces, gray detect_faces(frame) for (x, y, w, h) in faces: face_roi frame[y:yh, x:xw] label, conf predict_emotion(model, face_roi, class_names) if conf 0.5: # 置信度过滤低于阈值不显示 cv2.rectangle(frame, (x, y), (xw, yh), (0, 255, 0), 2) cv2.putText(frame, f{label} {conf:.2f}, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imshow(Face Emotion, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()逻辑说明cv2.waitKey(1)里的 1 是毫秒值越小循环越快实时性越好但 CPU 占用越高。置信度阈值 0.5 是经验值调高会漏掉一些真实表情调低会引入噪声。人脸识别部分如果要认“是谁”常见做法是在表情分支之外再挂一个 ArcFace 或 FaceNet 特征提取分支用余弦相似度比对底库这里不展开但工程结构上两条分支可以共用同一张人脸 ROI。注意实时循环里每帧都调model.predict会比较吃资源常见优化是隔几帧识别一次中间帧沿用上一次结果。5. 避坑与排查那些让模型“看起来能跑但结果不对”的细节5.1 训练精度很高摄像头前却全错现象验证集 accuracy 到 0.9接上摄像头后七类乱跳几乎没有对的。原因通常是预处理不一致——训练时用了rescale1./255和灰度推理时忘了转灰度或忘了归一化输入分布完全变了。解决把训练和推理的预处理写成同一个函数两边都调它别各写各的。5.2 人脸框一直在抖表情标签跟着闪现象静止坐着检测框大小和位置每帧都在变标签也跟着跳。原因是 Haar 检测对光照和角度敏感逐帧独立检测没有时序平滑。解决对检测框做简单滑动平均或者用minNeighbors调大减少误检表情标签可以加一个多数投票窗口连续几帧结果一致才更新显示。5.3 GPU 显存够但训练报 OOM现象显卡显存明明还有余量训练却报 out of memory。原因多半是 batch size 设太大或者数据加载时把整个数据集读进了内存。解决先把 batch size 降到 32 或 16 试用flow_from_directory这种生成器方式加载不要一次性np.array整个数据集。TensorFlow 默认会占满显存可以开显存按需增长gpus tf.config.list_physical_devices(GPU) for gpu in gpus: tf.config.experimental.set_memory_growth(gpu, True)5.4 类别不平衡导致模型只会猜“开心”现象混淆矩阵里大部分样本都被预测成样本量最大的那一类。原因是损失函数对每类一视同仁多数类主导了梯度。解决用class_weight给少数类加权或者对多数类做欠采样。model.fit里传class_weightclass_weight_dict即可权重按类别样本数的倒数来算。5.5 保存的模型换台机器加载失败现象本机训练保存的.h5或 SavedModel换台机器load_model报错。原因通常是自定义层或自定义损失函数没注册或者 TensorFlow 版本不一致。解决优先用model.save(model.keras)这种新格式如果用了自定义组件加载时通过custom_objects传进去跨机器部署时把 TensorFlow 版本写进 requirements。6. 从 demo 到可用模型导出、量化与一个提升精度的小技巧跑通 demo 只是起点真要拿去用得考虑部署形态。TensorFlow 训练出来的模型有几种导出方式选哪种取决于你的目标设备。导出格式适用场景命令/方法注意点SavedModel服务器端 TF Servingmodel.save(saved_model/)目录形式跨平台好.keras本地继续训练/加载model.save(model.keras)新格式推荐TFLite手机、树莓派、边缘设备tf.lite.TFLiteConverter需量化精度略降ONNX跨框架部署tf2onnx转换算子支持要验证边缘设备上跑TFLite 量化是最实用的一步。把 float32 权重压成 int8模型体积能缩到四分之一左右推理速度明显提升代价是精度掉一两个点。对表情识别这种 7 分类任务通常可以接受。converter tf.lite.TFLiteConverter.from_keras_model(model) converter.optimizations [tf.lite.Optimize.DEFAULT] # 开启默认量化 tflite_model converter.convert() with open(emotion_model.tflite, wb) as f: f.write(tflite_model)逻辑说明Optimize.DEFAULT会做动态范围量化权重存成 int8、计算时再转回浮点不需要代表性数据集最省事。如果要全整数量化还得提供一批校准数据精度更稳但流程更复杂。导出后务必用同一批验证图对比量化前后的预测一致性别直接上线。再分享一个提升精度的小技巧表情识别对人脸对齐很敏感。检测到人脸后先用眼睛位置做一次仿射变换把脸摆正再送进模型通常能涨几个点。OpenCV 有现成的眼睛检测器或者用face_alignment这类库。我一般会在预处理里加这一步虽然多花几毫秒但换来的是模型在侧脸和低头场景下不再乱猜。从那以后我每次做完训练都会强制走一遍“训练预处理函数 推理预处理函数”的对比检查把同一张图分别过两条路径确认输出张量完全一致才敢接摄像头。这个习惯帮我省掉了无数次“模型没问题但结果就是不对”的排查时间。希望帮到你。本文还有配套的精品资源点击获取