ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

人脸检测与表情识别实战:从OpenCV DNN到PyTorch分类器的两段式管线

人脸检测与表情识别实战:从OpenCV DNN到PyTorch分类器的两段式管线 简介这是一份面向计算机视觉开发者的人脸检测与表情识别实战项目基于face_classification扩展改造将OpenCV传统检测替换为MTCNN有效提升复杂场景下的人脸定位精度适合希望在项目实践中融合检测与分类算法的学习者。整个项目包共73个文件压缩后约76MB其中包含35个HDF5预训练模型文件、22个Python脚本、少量图像样例、XML配置文件、Dockerfile以及一份PDF技术报告既能查看训练权重与推理演示也能对照脚本理解完整流程。开发者可借助detect_face.py与video/emotion/gender等示例快速实现摄像头或图片中的表情识别还能从fer2013等数据集的XCEPTION模型文件中了解训练与调优细节项目附带的demo结果图与gradcam可视化也便于效果对比。已有274人学习适合入门至中级水平的AI学习者按项目思路动手复现与二次开发。1. 人脸检测加表情识别为什么这个项目看着简单做起来却容易翻车人脸检测加表情识别是人工智能项目实践里最容易被低估的方向。很多人以为是「人脸检测 一个分类器」拼在一起结果真跑起来才发现检测框乱跳、表情识别对非人脸硬给结果、摄像头画面和训练集风格完全对不上。这个标题里的 .zip 项目包本质上是一个两段式的视觉管线——先用检测模型把人脸从画面里框出来再对一个固定尺寸的人脸区域做表情分类。它能解决的问题很具体给实时视频流里的人脸标一个情绪标签比如 neutral、happy、angry并画出带标签的框。它适合两类人——正在做人工智能大作业或专业毕业设计的学生以及想快速搭一个「检测 分类」基线再往里加业务逻辑的工程初学者。它的难点不在单个模型而在两个模型怎么衔接、数据怎么对齐以及推理性能怎么扛住视频流。2. 拆解两段式方案检测与识别为什么不能揉成一个模型2.1 两段式架构是工程默认先框人脸再认表情做表情识别业界默认的做法不是训练一个端到端大模型直接输出表情而是拆成两个阶段第一阶段是通用的人脸检测器第二阶段是一个专注表情分类的轻量网络。原因很朴素——人脸检测是高度通用的任务公开的检测模型在正脸、侧脸、遮挡场景下已经足够稳不需要为表情任务重新训练而表情识别的难点是类间差异小neutral 和 sad 的区别往往只有嘴角和眉毛的细微变化它需要的是把注意力集中在一个被裁切、归一化的人脸区域上而不是在整个画面里找脸。这个设计带来的直接好处是替换成本低。检测器可以随时换——从 OpenCV DNN 的轻量模型换成精度更高的 RetinaFace只要输出仍然是「框坐标 置信度」后面的表情分类器完全不用动。同理表情分类器也可以从三分类换成七分类检测部分不受影响。项目实践的初期这种「解耦」比任何花哨的模型结构都重要因为你可以分步调试检测框画得准不准、表情分类的混淆矩阵长什么样各自单独验证。2.2 检测器选型能跑视频流的才是好选择检测器候选方案里我实际用下来比较稳的路线有三条按投入从低到高排方案依赖精度速度CPU适合场景OpenCV DNN 加载 SSD 人脸模型opencv-python 权重文件中上正脸效果好20-30 FPS先跑通流程、验证数据MediaPipe Face Detectionmediapipe中上侧脸和小脸更好30 FPS 左右视频流实时体验YOLO 系列微调ultralytics高需要 GPU想要更高检测精度、兼顾其它目标我会优先建议第一条。理由很直接OpenCV DNN 的接口稳定不需要额外装推理框架且模型文件小作为项目基线很容易控制变量。MediaPipe 更快但它的输出是「相对于输入图像尺寸的归一化坐标」和 OpenCV 的画框坐标体系不同刚上手容易算错缩放。YOLO 微调是后期提精度的路但数据集标注成本和训练时间会拖慢项目节奏对「人脸检测加表情识别」这个实践项目来说前期没必要上。2.3 表情类别怎么定标签集合决定项目天花板表情识别的类别集合通常分两档三分类positive / neutral / negative和七分类angry / disgust / fear / happy / sad / surprise / neutral。如果你做的是人工智能课程项目建议先做三分类打底再扩到七分类。原因在数据——七分类里 disgust 和 fear 在公开数据集里样本量常年垫底类别不均衡会让模型把这两类 train 成「几乎永远不预测」的死类。数据这块常见的做法是两类来源结合一是公开的表情数据集如 FER2013 这类灰度人脸表情集图像尺寸统一为 48x48类别标签已经打好省去标注成本二是自己采集的人脸表情数据用检测器把脸裁出来按类别归档。两类混用时要注意灰度图和彩色图的分歧——FER 系列是灰度图你自采的是 BGR 彩色图训练时要么统一转灰度要么统一走彩色否则模型会在真实摄像头输入上表现暴跌。我会在第四章讲具体的数据加载姿势。提示先把类别串写到代码顶部做常量——EMOTIONS [angry, disgust, fear, happy, sad, surprise, neutral]。后续所有代码里的索引、预测结果的 argmax 都要以这个顺序为准这是最容易在项目后期引发「标签错位」的隐藏坑。3. 把检测器跑起来用 OpenCV DNN 完成人脸定位与裁切3.1 最小检测脚本读一帧图、画一个框这一节直接给一个能跑的检测脚本用的是 OpenCV DNN 加载公开的 SSD 人脸检测模型Caffe 格式。模型文件需要你自己准备两个文件一个.prototxt结构文件和一个.caffemodel权重文件。我通常把它们放在项目的models/目录下和代码分开方便打包时把模型单独剔除。import cv2 # 1. 加载检测模型 prototxt models/deploy.prototxt caffemodel models/res10_300x300_ssd_iter_140000.caffemodel net cv2.dnn.readNetFromCaffe(prototxt, caffemodel) # 2. 读取并预处理图像 image cv2.imread(test.jpg) # BGR 顺序读入 h, w image.shape[:2] blob cv2.dnn.blobFromImage( cv2.resize(image, (300, 300)), # 模型固定输入 300x300 1.0, # 缩放因子 (300, 300), # 目标尺寸 (104.0, 177.0, 123.0), # 模型自带的均值用于归一化 swapRBFalse ) net.setInput(blob) detections net.forward() # 输出 shape: [1, 1, N, 7] # 3. 解析检测结果画框 for i in range(detections.shape[2]): confidence detections[0, 0, i, 2] if confidence 0.5: # 置信度阈值可调节 continue box detections[0, 0, i, 3:7] * [w, h, w, h] x1, y1, x2, y2 box.astype(int) cv2.rectangle(image, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(image, f{confidence:.2f}, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) cv2.imwrite(output.jpg, image)这段代码的逻辑很直白先把输入图像缩放到 300x300 并做均值归一化送入网络后拿到的detections是一个四维张量其中每个检测结果用 7 个值描述——[batch_id, class_id, confidence, x1, y1, x2, y2]。注意最后四个坐标是归一化坐标范围 0 到 1所以要乘以原图的宽高w和h还原成像素坐标这是新手最容易算错的一步。参数方面有两个点值得提。第一blobFromImage的均值(104.0, 177.0, 123.0)是这个公开模型训练时用的不能随便去掉否则检测精度会明显下降如果你换一个模型均值要和那个模型匹配。第二置信度阈值 0.5 在正脸大图上够用但到了视频流里我会降到 0.3 左右因为侧脸和小脸的置信度天然偏低阈值太高会导致漏检这一点在第 5 章会细说。3.2 把检测框裁出来为表情分类准备干净的输入检测到人脸之后下一步不是直接送分类器而是先把人脸区域从原图里「裁」出来再缩放成表情模型需要的输入尺寸。这里有一个很关键的边界问题检测框是矩形但人脸是长宽比约 1:1 的椭圆区域直接用检测框裁剪会把背景也带进来。背景占比过高时表情分类器会学到「用背景区分表情」这种错误特征。import cv2 import numpy as np def crop_face(image, box, margin_ratio0.25, output_size(48, 48)): 裁切人脸区域并做边缘扩展 box: (x1, y1, x2, y2) 像素坐标 margin_ratio: 向四边外扩的比例缓解检测框过紧的问题 h, w image.shape[:2] x1, y1, x2, y2 box # 计算框的宽高并向外扩展 margin_ratio bw, bh x2 - x1, y2 - y1 mx, my int(bw * margin_ratio), int(bh * margin_ratio) x1 max(0, x1 - mx) y1 max(0, y1 - my) x2 min(w, x2 mx) y2 min(h, y2 my) face image[y1:y2, x1:x2] if face.size 0: return None # 统一缩放到分类模型的输入尺寸注意保持比例会导致形变但 48x48 训练时已接受这种形变 face_resized cv2.resize(face, output_size, interpolationcv2.INTER_AREA) return face_resized这段代码做了两件事外扩检测框并把裁出的区域缩放到固定尺寸。margin_ratio取 0.25 是我常用的经验值——把脸部边缘耳朵、部分额头和下巴包进来又不至于把大量背景带进分类器。缩放的插值方式选了INTER_AREA在缩小图像时它保留的信息更干净比INTER_LINEAR的锯齿少。这里有一个值得记的取舍直接拉伸成 48x48 会让人脸长宽比失真但公开的表情数据集在制作时本身就没有保留原始比例所以「形变」反而变成了训练和推理的一致性要求——只要你训练时是这么缩放的推理时也这么做模型就能接受。真正不能做的是训练时用正方形裁切、推理时用自由比例缩放这种不一致会让识别准确率断崖式下跌。4. 表情识别分类器从训练数据到可上车的推理权重4.1 数据加载与预处理训练时和推理时保持一致表情分类器的训练输入是「人脸图像 表情标签」。一个干净的数据组织方式是按类别分文件夹配合 PyTorch 的ImageFolder直接加载不需要自己写解析逻辑。目录结构大概是data/ train/ happy/ xxx.jpg ... sad/ xxx.jpg ... neutral/ xxx.jpg ... val/ happy/ ... sad/ ... neutral/ ...加载时要注意一个隐藏问题如果数据来源包含灰度图像比如 FER 数据集转存来的而测试图像是彩色摄像头帧两者在通道维上不一致。处理办法是写一个统一的预处理函数无论是训练、验证还是推理都走同一条路径。from torchvision import datasets, transforms from torch.utils.data import DataLoader # 训练和推理共用的预处理流程 transform_train transforms.Compose([ transforms.Resize((48, 48)), transforms.RandomHorizontalFlip(p0.5), # 只对训练集做增强 transforms.ToTensor(), transforms.Normalize(mean[0.5], std[0.5]) # 灰度图单通道 ]) transform_eval transforms.Compose([ transforms.Resize((48, 48)), transforms.ToTensor(), transforms.Normalize(mean[0.5], std[0.5]) ]) train_data datasets.ImageFolder(data/train, transformtransform_train) val_data datasets.ImageFolder(data/val, transformtransform_eval) train_loader DataLoader(train_data, batch_size64, shuffleTrue, num_workers2) val_loader DataLoader(val_data, batch_size64, shuffleFalse, num_workers2) print(类别映射:, train_data.class_to_idx)代码里的Normalize(mean[0.5], std[0.5])是灰度图的常用设定把像素从 [0, 1] 范围变成 [-1, 1] 范围。这里有一个极易埋坑的细节——如果你的图像是彩色的mean和std必须给三个通道的值比如(0.5, 0.5, 0.5)写成[0.5]会让你在训练时报通道数不匹配或者更隐蔽地让模型学到错误的通道统计信息。class_to_idx打印出来的映射必须和你在第 2 章定义的EMOTIONS顺序核对一遍目录名按字典序排列时ImageFolder的类别索引可能和你预想的不一样。4.2 小而能打的分类网络结构选型与训练入口表情识别不需要大网络。公开数据集里的图像分辨率本来就只有 48x48一个大网络反而会严重过拟合。我通常用三层卷积加两层全连接的小网络。结构选型的逻辑是第一层卷积负责低阶纹理特征嘴角、眉角的边缘第二层负责局部模式眼睛区域的形状第三层负责更高阶的组合特征整张脸的肌肉张力最后用全连接把特征映射到类别概率。import torch import torch.nn as nn import torch.optim as optim class EmotionNet(nn.Module): def __init__(self, num_classes7): super().__init__() self.features nn.Sequential( nn.Conv2d(1, 32, kernel_size3, padding1), # 单通道灰度输入 nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 24x24 nn.Conv2d(32, 64, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 12x12 nn.Conv2d(64, 128, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 6x6 ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(128 * 6 * 6, 256), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(256, num_classes) ) def forward(self, x): return self.classifier(self.features(x)) model EmotionNet(num_classeslen(train_data.classes)) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-3) for epoch in range(30): model.train() running_loss 0.0 for inputs, labels in train_loader: optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() print(fepoch {epoch1}, loss: {running_loss / len(train_loader):.4f})训练循环本身是标准模板值得讲的是三个参数。Dropout(0.5)在这样的小网络上不是锦上添花——公开表情数据集的同一个人在不同样本里可能出现多次模型很容易记住「某张脸属于某个人」而不是「某种表情长什么样」Dropout 是缓解这种身份泄露的主要手段。学习率 1e-3 对 Adam 来说是个稳的起点如果 loss 在 5 个 epoch 内下降不明显我会降到 3e-4 而不是直接调网络结构。batch_size64需要根据你的显存调整显存不足时报错会在 PyTorch 里直接抛出来把 64 改成 32 通常就能解决。训练完成后至少保存两个文件——权重和类别映射。类别映射就是第 2 章那个class_to_idx的逆映射推理时要把模型输出的索引还原成表情名字。很多项目在这里直接用 Python 的pickle存了一个class_names.json我建议你照做因为下次打开项目时你大概率已经不记得类别顺序了。4.3 验证阶段用混淆矩阵和单张图测试确认模型真的会了训练结束别急着接摄像头。先用验证集算一个准确率再用混淆矩阵看每个类别的具体表现。准确率 0.7 以上看起来不错但混淆矩阵往往会告诉你模型把所有「sad」都预测成了「neutral」因为两类样本在灰度图上确实太像了。这时候需要做的不是换网络而是回看数据——检查 sad 类的样本是否有很多低头、闭眼、光线暗的图这些图人眼都难分模型学不会不是结构问题是数据问题。import numpy as np from sklearn.metrics import confusion_matrix model.eval() all_preds, all_labels [], [] with torch.no_grad(): for inputs, labels in val_loader: outputs model(inputs) preds torch.argmax(outputs, dim1) all_preds.extend(preds.numpy()) all_labels.extend(labels.numpy()) cm confusion_matrix(all_labels, all_preds) print(混淆矩阵:\n, cm) print(类别顺序:, val_data.classes)这段验证代码的逻辑是关闭梯度计算torch.no_grad()跑完整个验证集用argmax拿到概率最大的类别索引最后用 sklearn 生成混淆矩阵。看混淆矩阵时对比val_data.classes的顺序逐行看——每一行是真实类别每一列是预测类别对角线是正确预测。如果某一列的数值明显偏高说明模型有「偏爱」某个类别的倾向。另外提醒一点如果你没装 sklearn可以直接用torch手动统计但 sklearn 的confusion_matrix输出格式更直观装一下成本很低。5. 串成实时管线与四个必踩的坑从摄像头读到画框标注5.1 帧处理主循环检测、裁切、识别、标注前面两章各自跑通了这一步把它们拼成一个循环。理论上就是把第 3 章的检测和第 4 章的推理串起来但真正容易出问题的是「帧率控制」和「人脸区域缩放」这两个环节——摄像机画面里人脸大小在变你需要保证每次送进分类器的图像内容尽量稳定而不是忽大忽小、忽亮忽暗。import cv2 import torch import numpy as np # 加载检测模型第 3 章 detect_net cv2.dnn.readNetFromCaffe(prototxt, caffemodel) # 加载表情分类模型第 4 章 emotion_model EmotionNet(num_classeslen(EMOTIONS)) emotion_model.load_state_dict(torch.load(emotion_model.pth, map_locationcpu)) emotion_model.eval() cap cv2.VideoCapture(0) frame_skip 2 # 每 3 帧跑一次检测降低 CPU 压力 frame_count 0 while True: ret, frame cap.read() if not ret: break frame_count 1 if frame_count % (frame_skip 1) ! 0: # 跳过的帧也画上一次的框和标签 if last_result: x1, y1, x2, y2, label last_result cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imshow(face_emotion, frame) if cv2.waitKey(1) 0xFF ord(q): break continue h, w frame.shape[:2] blob cv2.dnn.blobFromImage(cv2.resize(frame, (300, 300)), 1.0, (300, 300), (104.0, 177.0, 123.0), swapRBFalse) detect_net.setInput(blob) detections detect_net.forward() last_result None for i in range(detections.shape[2]): confidence detections[0, 0, i, 2] if confidence 0.3: # 视频流里阈值下调到 0.3 continue box detections[0, 0, i, 3:7] * [w, h, w, h] x1, y1, x2, y2 box.astype(int) face_crop crop_face(frame, (x1, y1, x2, y2)) if face_crop is None: continue # 灰度化 归一化和训练时完全一致 face_gray cv2.cvtColor(face_crop, cv2.COLOR_BGR2GRAY) face_tensor torch.FloatTensor(face_gray).unsqueeze(0).unsqueeze(0) / 255.0 face_tensor (face_tensor - 0.5) / 0.5 with torch.no_grad(): output emotion_model(face_tensor) pred_idx torch.argmax(output, dim1).item() label EMOTIONS[pred_idx] cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) last_result (x1, y1, x2, y2, label) break # 只处理置信度最高的一张脸多人场景放在第 6 章进阶 cv2.imshow(face_emotion, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这个主循环的功能说清楚每 3 帧做一次完整检测和识别中间跳过的 2 帧直接沿用上一次的框和标签画上去。frame_skip 2是 CPU 上跑实时管线的常用妥协——检测模型在 300x300 下开销不大但加上分类器的预处理和推理每帧都跑会掉到 10 FPS 以下而跳帧处理能稳定在 20 FPS 左右人眼几乎感觉不到延迟差异。逻辑上唯一要留意的是一旦检测不到人脸last_result保留旧值会导致画面里框的位置不动需要判断last_result存在才画。这种「跳帧 沿用旧结果」的模式在进阶阶段还可以平滑框的位置减少抖动后面会说。5.2 避坑一检测框在视频里抖动识别结果跟着跳现象静止坐着的时候检测框一会儿放大一会儿缩小表情标签在 happy 和 neutral 之间反复横跳。原因检测器对每一帧独立做预测人脸在画面中哪怕有 1 像素的位移框的尺寸就会有波动而表情分类器对输入图像的裁剪位置极其敏感——同一个人同一表情框往里缩一点和往外扩一点预测结果可能就变了。这不是分类器没训好是「输入分布抖动」造成的。解决给检测框做一阶指数平滑把当前帧的框和历史帧的框做加权平均。常见做法是维护一个平滑状态smoothed_box alpha * current_box (1 - alpha) * previous_boxalpha 取 0.4 左右比较稳。注意人脸位移大时比如转头平滑系数太大会导致框「追不上」人脸alpha 要根据实际视频帧率调帧率越低 alpha 越大。5.3 避坑二表情分类器对非人脸硬给结果现象手在镜头前晃过分类器给了一个「happy」标签画面角落有个模糊的人形也被贴上了表情。原因表情分类器用的是 Softmax 输出Softmax 的特性是「所有类别的概率加起来等于 1」它不会输出「都不是」这个选项哪怕输入是一块纯色噪声它也会硬分到某一个表情类别上。解决在分类器后面加一个置信度门控。具体做法是在torch.argmax之前先把最大概率值取出来只有当这个概率值大于阈值比如 0.7时才显示标签否则只画框不写表情。如果大量真实人脸都被门控拦下来说明分类器本身的不确定性太高优先回去看数据而不是调低阈值。这一节的完整实现放到第 6 章。5.4 避坑三训练图是灰度 48x48摄像头图是彩色大图通道处理不一致现象离线验证准确率 0.75一接摄像头几乎全预测成 neutral。原因训练时如果用了灰度图而推理时直接cv2.cvtColor(face_crop, cv2.COLOR_BGR2GRAY)按理说是一致的但很多项目在数据处理流程里做了额外操作比如训练时用了transforms.Grayscale(num_output_channels1)的增强变体或预处理里有归一化参数不同。灰度化后是否归一化到 [0, 1]、是否有均值方差任何一步不一致都会让模型面对一个「从未见过的分布」。解决记住一条铁律——推理代码里的预处理必须和验证代码里的transform_eval完全一致。最稳的做法是把推理时的预处理也写成transforms.Compose先Resize((48, 48))再ToTensor()再Normalize(0.5, 0.5)每一行都和训练验证对齐。不要手写face_gray / 255.0 - 0.5这种简化计算两个框架的数值精度可能会差出一个小数位在深层的归一化里被放大。5.5 避坑四OpenCV 的 BGR 通道顺序破坏检测精度现象同一张测试图片用cv2.imread读入后检测不到人脸但用 matplotlib 读入其实是 RGB就能检测到。原因OpenCV 的imread默认按 BGR 顺序读入图像而公开的人脸检测模型大多按 RGB 顺序训练。blobFromImage里的swapRBFalse表示不做通道交换这时你送入网络的就是 BGR 数据模型相当于在看一个「红蓝互换」的世界特征完全错位。解决在blobFromImage里把swapRB设为True或者在读图后手动cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)再送网络。这个参数单独记忆很难我的习惯是检测和识别模型的 preprocessing 函数各写各的绝不在主循环里随手改。另外如果你用 PyTorch 的ImageFolder训练表情分类器它内部读图用的是 PIL而 PIL 默认是 RGB 顺序——这意味着训练和推理的通道顺序已经天然一致不需要额外处理但心里要清楚这件事别在推理代码里多此一举地再换一次通道。6. 进阶加一个置信度门控让表情识别从「能跑」到「能看」置信度门控是表情识别从「能用」到「能看」的最关键一步。上一章避坑二已经提到Softmax 会把任何输入都硬分到一个类别里真实的摄像头场景中模糊、侧脸、遮挡、手部误检都会产生低置信度的预测这些结果如果直接展示给人的感觉就是「这个系统在乱猜」。门控的做法是只展示「模型自己有把握」的预测。def predict_with_gate(face_tensor, emotion_model, threshold0.7): with torch.no_grad(): output emotion_model(face_tensor.unsqueeze(0)).squeeze(0) probs torch.softmax(output, dim0) max_prob, pred_idx torch.max(probs, dim0) if max_prob.item() threshold: return EMOTIONS[pred_idx.item()], max_prob.item() return None, max_prob.item() # 低置信度时返回 None上层只画框这个函数返回两个值分类结果和置信度。在管线里拿到None就只画框不写标签拿到具体表情就在框上方写happy (0.83)这样的文本置信度可以让使用者直观看到「系统对自己判断的把握程度」。阈值 0.7 是从哪来的这个值通常来自验证集把验证集上所有样本的置信度分布打出来看预测正确的样本集中分布在哪个区间取正确样本分布的左侧边缘作为阈值。没有验证集时就用 0.5 起步太保守了就往下调调到误检率可以接受的平衡点。除了门控另一个值得做的验证方法是「小样本实地自测」拿手机对着自己拍 5 秒视频把每一帧都存下来人工数一下「真人脸出现」和「表情标签正确」的比例。你不需要算召回率精确率这种指标——只需要一个标准画面里有脸且在注意看时标签是否基本符合直觉镜头前没有人时画面里是否不出现任何表情标签。能做到这两点这个项目在课程展示或毕设答辩里就属于「能打」的水平。最后说一个我做这类项目养成的习惯也是这个方向真正能否落地的分水岭永远把「模型加载」和「模型推理」分开不要把readNetFromCaffe和load_state_dict写进帧循环里。模型文件读取是磁盘 IO一次只要几毫秒但如果在每一帧里都执行一次会让你的摄像头流畅度瞬间归零。先初始化再进循环这个顺序定下来项目到后期加任何功能都会顺很多。希望帮到你。本文还有配套的精品资源点击获取
返回列表