简介:一套基于Yolov5和Python实现的人脸识别、细粒度表情识别与异常行为检测源码,面向毕业设计、期末大作业和课程设计场景,也非常适合希望快速落地视觉项目的初学者。压缩包共107个文件,包含37个Python脚本、18个YAML配置、21个TXT说明,另有模型权重、测试图片、动态演示GIF、界面文件以及Dockerfile等,整体约24.81MB。项目代码注释详细,部署简单,由开发者个人手打完成,曾获导师高度认可并被评为98分高分作品,目前已有269人学习下载。系统内置完整图形界面,功能覆盖人脸检测、表情分类与异常行为预警,内部示例图片和演示文件能辅助理解运行流程,也能直接用于展示答辩。整体按模块组织,py脚本负责核心逻辑,yaml配置模型参数,pt权重提供推理能力,md文档和txt说明指导快速使用,方便初学者对照学习,也便于后续二次开发扩展。
1. 基于YOLOv5+Python的三合一检测方案:人脸识别、表情识别、异常行为为什么能共用一套代码
基于YOLOv5+Python实现的人脸识别、人脸细粒度表情识别、异常行为检测源码,本质上不是三个独立项目,而是一套「检测+分类+规则」三层串联的工程方案:第一层用YOLOv5把人脸、人体从画面里框出来,第二层对框出来的人脸分别做身份识别和表情分类,第三层则盯着人体框的位置和大小变化,根据规则判断有没有摔倒、入侵、徘徊这类异常行为。很多第一次接触这套代码的同学会误以为表情识别也要用YOLO去输出,实际不是,表情这层用的是分类网络,YOLO只负责把该分类的区域喂进去。这套方案比较适合校园监控、门店客群分析、园区安全这类场景,你在本地有一块普通N卡就能跑通全流程,部署到树莓派5或Jetson上也只是换一套推理后端的事。
2. 用YOLOv5搭检测底座:人脸检测与行人检测的分工
2.1 为什么方案里不放一个检测模型,而是人脸和行人分开跑
这是这套源码设计里最值得先想清楚的一点。人脸检测和行人检测虽然都能用YOLOv5实现,但两者的数据分布完全不同:人脸框宽高比接近1:1,目标普遍偏小,密集出现在画面中上部;行人框宽高比在1:2到1:3,目标尺寸跨度极大,而且经常互相遮挡。把两类目标混在一个模型里训练,虽然标签文件里能同时画两类框,但训练时的我们先验框(Anchor)是针对COCO数据集设计的,人脸和行人的尺寸分布差异会让Anchor匹配阶段出现大量低质量正样本,最终两边精度都被拉低。
常见做法是保留两个独立检测模型:一个用人脸数据集训练,负责给人脸识别和表情识别供框;一个用行人数据集训练,负责给异常行为检测提供人体位置和尺度。两个模型用同一套YOLOv5训练流程,推理阶段共享同一个预处理和后处理代码,只是在输出分支上各自过滤各自类别的置信度。这样做还有一个工程上的好处:人脸识别服务如果要对齐算法做升级,替换的只是人脸检测权重,不影响人体检测和异常行为判定。
2.2 最小可跑通的检测环境:Python、Conda与YOLOv5的配置顺序
我一般建议在Anaconda里单独建一个环境来跑这套代码,不要直接装进base环境。YOLOv5官方对Python和PyTorch的版本有组合约束,如果用PyCharm打开项目,解释器必须指向这个新建的conda环境,否则会碰到cannot be resolved against python helper roots这类让新手半天摸不着头脑的报错,本质是IDE里的解释器路径和conda环境没对上。
conda create -n yolo python=3.8 -y conda activate yolo git clone https://github.com/ultralytics/yolov5 cd yolov5 pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple这里把Python版本固定为3.8是为了兼容PyTorch 1.8到1.13这一整个区间的预编译包,很多老权重文件是用这些版本训练的,版本跨度拉太大容易出现权重加载警告。克隆官方仓库而不是自己从零搭文件结构,是因为YOLOv5的训练入口、数据加载器、增强策略都已经在train.py和dataset.py里写好了,我们只需要替换数据和配置,不需要动内部实现。国内源只影响下载速度,不影响版本行为。
2.3 训练自己的数据集:从标注到YOLOv5可用的完整流程
这套代码里我见过最多的需求是「检测人但不想用COCO那种通用标签」,比如只保留行人、不检测车辆。这种需求下重新标注比过滤标签更可控。我习惯用LabelImg做标注,输出格式直接选YOLO格式,它会自动生成每个图片对应的txt文件,每行是类别id x_center y_center width height,坐标全部归一化到0到1之间。标注完的目录结构要严格按YOLOv5的约定来摆:
datasets/ ├── face/ │ ├── images/ │ │ ├── train/ │ │ └── val/ │ └── labels/ │ ├── train/ │ └── val/ └── face.yamlface.yaml里面填三样东西:train和val的路径、类别数量、类别名。路径建议写绝对路径,省得不同机器上运行时相对路径解析出错。我第一次训练时就是漏了路径里的前缀,结果模型训练完了验证集mAP一直是0,查了半天才发现val图片根本没加载进来,这种情况属于典型的黑匣子假象,指标很难看的时候先怀疑数据有没有读对,再怀疑模型有没有问题。
python train.py \ --data face.yaml \ --weights yolov5s.pt \ --epochs 100 \ --batch-size 16 \ --img-size 640weights参数填yolov5s.pt表示拿官方在COCO上预训练过的权重做迁移学习初始权重,这样哪怕你的人脸数据只有几千张,也能训练出一个能用的模型。--img-size 640是人脸检测的基准分辨率,如果画面里的人脸普遍小,建议改成960甚至1280,代价是显存占用和推理耗时一起上升。batch-size在16G显存的卡上跑yolov5s大概是16到24,如果你的卡只有8G,就把batch-size降到8,同时把--workers降到4。
2.4 从yolov5n到yolov5s怎么选:先看部署到哪里
模型尺寸的选择不应该在训练之后做,而应该在做项目方案时就想好。同一个仓库里提供n、s、m、l、x五个尺寸,差别主要在网络宽度和深度上。如果这套源码最终要跑在树莓派5或者Jetson Nano这种边缘设备上,我从一开始就会选yolov5n或者yolov5s,因为n模型量化后可以做到50ms一帧,而l模型在板子上基本跑不动。如果只在服务器上做离线分析和事后检索,那可以用m甚至l来换精度。
python train.py --data face.yaml --weights yolov5n.pt --epochs 100 --batch-size 32 --img-size 640n模型的参数量大约是s的四分之一,训练速度明显快,但小目标召回率会牺牲一些。这里我给你的建议是:方案初期用s模型把整个链路跑通,等确认这个技术方向可行、评估上线后再针对性去换n加量化。先把流程通起来比一味追求精度重要,否则很容易在调参里消耗掉大量时间。
3. 人脸识别与细粒度表情:检测框之外的第二层网络
3.1 细粒度表情为什么不能拿通用分类网络硬做
很多人拿到这套源码想直接复用一份带表情标签的分类模型,比如用EfficientNetV2直接分七类表情,结果训练集准确率很高,一到实际摄像头画面就乱猜。原因是细粒度表情任务里,生气和厌恶、惊讶和恐惧之间的类间差异极小,而遮挡、侧脸、光照带来的类内差异极大,普通分类网络学到的往往是背景和肤色这些无关特征,而不是肌肉纹理这种真正的判别特征。
所以细粒度表情识别网络在实际工程里会有两个专门处理:一是输入人脸必须经过对齐,把两只眼睛拉平到同一水平线并缩放到固定尺寸,这样能消除头部姿态带来的几何歧义;二是在分类层之前加注意力机制,让网络重点看嘴部、眼角、眉毛附近的局部区域,而不是把整张脸等同对待。VIT-or-EfficientNetV2这个纠结里,我会告诉你选EfficientNetV2更务实,原因是VIT在超大模型和超大数据量上优势才明显,人脸表情数据集普遍只有几万张,VIT更容易过拟合,而且Transformer结构在边缘设备上部署需要额外的算子支持,EfficientNetV2在ONNX和TensorRT里的兼容性好很多。
3.2 识别用的特征向量模型和表情分类模型要拆开
这一层的另一个关键点:人脸识别和表情识别虽然是同一个输入区域,但它是两个任务,不能共用一个模型。
人脸识别要做的事是把人脸映射成一个高维特征向量,同一个人不同照片的向量距离近,不同人的向量距离远,所以它本质是度量学习,业界成熟的方案是ArcFace或者FaceNet。人脸识别网络输出的特征维度一般是512或者128,我们拿这个向量去和库里已经注册好的向量做余弦相似度,超过某个阈值就判定为同一人,这个阈值才是最终身份判断的依据,并不需要网络直接输出一个类别。
表情识别则纯粹是一个分类任务,输出维度是表情类别数,比如7类,取softmax之后的最高分作为结果。两个模型要求的输入尺寸都不一样:人脸识别一般要112x112,表情分类用224x224更稳。所以代码里会为每个检测到的人脸裁剪两次,一次供识别用,一次供表情用,这种重复计算在多人同框时会比较明显,需要靠缓存检测结果、隔帧做人脸识别来压耗时。
3.3 在推理脚本里把检测、识别、表情串成一条流水线
import cv2 import torch import numpy as np class FacePipeline: def __init__(self, det_weights, rec_weights, emo_weights): self.det = torch.hub.load('ultralytics/yolov5', 'custom', path=det_weights, force_reload=True) self.rec_model = load_arcface(rec_weights) # 输出512维特征 self.emo_model = load_effnetv2(emo_weights) # 输出7类表情概率 self.id_db = {} # 注册库 {name: feature_vector} def process(self, frame): results = self.det(frame) # 只保留人脸类别 boxes = results.xyxy[0].cpu().numpy() for box in boxes: x1, y1, x2, y2, conf, cls = box face_img = frame[int(y1):int(y2), int(x1):int(x2)] face_align = align_face(face_img) # 按两眼坐标对齐 feature = self.rec_model(face_align) identity = self.match_id(feature) # 余弦相似度匹配 emotion = self.emo_model(face_align.resize(224, 224)) draw_frame = draw_info(frame, identity, emotion) return draw_framedet部分的torch.hub.load是YOLOv5官方推荐的推理加载方式,每次写代码都不用自己拼NMS和anchors,省掉很多重复工作。align_face这一步非常关键,实际项目里如果跳过了它,识别准确率会掉五个点到十个点,原因是人的头部在画面里往往有旋转和俯仰,不做对齐就输入给ArcFace会引入很大的姿态噪声。match_id内部是存储向量与当前向量的余弦距离,阈值我会设在0.35到0.45之间,数值越小判定越严格。表情模型输入要resize到和训练时一致,不要用其他尺寸硬喂,否则输出概率分布是乱套的。
4. 异常行为检测:把坐标序列变成可告警的事件
4.1 三种最常见异常事件的判定逻辑
异常行为检测在这套代码里不需要额外训练一个分类模型,它是基于YOLOv5人体检测框做规则判定。事件类型和判定逻辑通常是这样的:
| 事件 | 依据 | 典型误报 |
|---|---|---|
| 摔倒 | 人体框宽高比由站立时的1:2.5变成接近1:1,且中心点高度迅速下降 | 蹲下系鞋带、弯腰捡东西 |
| 区域入侵 | 人体框中心点进入预设多边形区域 | 边界画错、误把门口拉入区域 |
| 徘徊停留 | 同一个人的轨迹中心点长时间停留在一个小范围内 | 两个不同的人被跟踪器错配 |
规则判定做起来简单,但鲁棒性完全取决于前置的两个环节:检测框稳定性和跟踪ID的一致性。如果每一帧检测框抖动得很厉害,宽高比的变化可能比摔倒本身还夸张;如果跟踪ID频繁切换,一个人的停留会被算成多个人在路过。所以异常行为检测模块里一定要有一套帧间匹配算法,常见做法是维护当前所有人体框的IoU匹配矩阵,用匈牙利算法做相邻帧之间的人体关联,这样每个人的轨迹才是连续的。
4.2 用帧间坐标差实现摔倒判定
摔倒判定里最直观的信号就是人体框中心点纵坐标在一段时间内的快速下降,然后框的宽高比发生倒转。我用一个滑动窗口来保存最近30帧的人体框信息,只有当窗口内的平均下降速度超过阈值、且当前宽高比小于0.9时才触发告警,这种做法能过滤掉瞬时抖动。
class FallDetector: def __init__(self, window_size=30): self.history = [] self.threshold_speed = 0.08 # 每帧中心点y归一化坐标变化率 self.threshold_ratio = 0.9 # 宽高比低于该值判定躺倒 def update(self, bbox): self.history.append(bbox) if len(self.history) > self.window_size: self.history.pop(0) if len(self.history) < self.window_size: return False first_box = self.history[0] w1, h1 = first_box[2] - first_box[0], first_box[3] - first_box[1] center_y1 = (first_box[1] + first_box[3]) / 2 center_y2 = (bbox[1] + bbox[3]) / 2 speed = (center_y2 - center_y1) / (self.window_size - 1) # 归一化近似 ratio = (bbox[2] - bbox[0]) / max(1e-5, (bbox[3] - bbox[1])) if speed > self.threshold_speed and ratio < self.threshold_ratio: return True return False这里bbox坐标是归一化后的值,所以speed的单位是「每帧下降的画面比例」,这个阈值和摄像头安装高度直接相关。摄像头装在3米高俯视时,摔倒的中心点下降量比装在1.5米平视时要小,需要把阈值调低到0.05左右。实际校验中我发现用画面尺寸乘以系数折算成像素值会更直观,比如1080p画面里一帧下降超过15像素就算快速倒地。另外max(1e-5, ...)是防止人体框高度变成0导致除零异常,这种边界问题在真实视频流里时常出现,空帧或者半截人体进画面时检测框偶尔会出现异常尺寸。
4.3 全流程整合:把三路任务合并进同一个主循环
这个源码设计的核心是把人脸识别、表情识别和异常行为检测放在同一个帧处理循环里,用固定频率调度。一般我一帧里面做一次YOLOv5检测,然后对人脸分支每帧跑表情识别,但身份识别每5帧才跑一次,并把结果缓存下来,这能省掉不少重复的embedding计算;行人分支则每帧都送入跟踪器,因为摔倒判定的输入频率要求比较高。
cap = cv2.VideoCapture(0) skip = 0 fall = FallDetector() while True: ret, frame = cap.read() if not ret: break boxes = human_detector(frame) track_id = tracker.update(boxes) keep = True if not skip % 5 == 0: keep = False for tid, bbox in track_id.items(): if not keep: continue face_features = face_pipeline(frame, bbox) # 对每个人脸做识别 emotion = emotion_pipeline(frame, bbox, tid) for tid, bbox in track_id.items(): fall_state = fall.update(bbox) skip += 1skip这个变量本质是给身份识别部分做一个时间降采样。你不要小看这个操作,树莓派5上跑完整流程时,这个5帧一次的跳过能让整体FPS从9提升到14左右,效果很明显。tracker.update返回的tid是每个人的唯一编号,异常行为判定要按这个号分组,否则单人场景监视会变成多目标监视,干扰判定逻辑。
需要特别提示的是,异常行为检测的结果要经过连续帧确认才能触发告警,比如摔倒事件必须连续3帧都判为摔倒,才进入告警流程。因为单帧误判太常见了,摄像头自身的小幅震动或者人体大幅转身都可能造成瞬时假阳,时序确认是成本最低的降噪手段。
5. 训练与部署避坑:环境、显存、小目标与边缘设备
5.1 环境配置的深坑:PyCharm、Anaconda和YOLOv5环境不对齐
现象:在PyCharm里运行train.py,报错cannot be resolved against python helper roots,或者导入cv2时提示NoneType没有shape属性,但命令行里明明能正常工作。
原因:PyCharm里右下角选择的Python解释器不是当前conda环境的Python,而是系统自带Python。YOLOv5在多个依赖(特别是opencv-python)的版本上很挑,系统Python里很可能装的是不同大版本的cv2,接口不兼容。
解决:在PyCharm的Settings -> Project -> Python Interpreter里选择Conda Environment -> Existing Environment,路径指到conda env list里yolo环境对应的python.exe。完成后在终端执行conda list opencv确认版本和IDE里一致,再跑一次import cv2; print(cv2.__version__)验证。环境问题里一半以上是解释器路径漂移造成的,先查这个再考虑重装包。
5.2 训练时显存溢出:改batch-size和改分辨率哪个优先
现象:torch.cuda.OutOfMemoryError: CUDA out of memory,训练进程直接崩掉,重启后看日志才发现在第几个epoch爆的。
原因:训练时显存占用主要由三部分组成——特征图占用、梯度占用和优化器状态占用,其中batch-size和--img-size是影响最大的两个参数。很多人一遇到OOM就急着把batch-size减半,减完发现速度慢了一半,其实可以先检查分辨率。
解决:如果原始设置是640分辨率,先看能不能把输入分辨率降到512,精度损失很小但显存占用能降30%左右;如果512还是溢出,再把batch-size从16改到8。autoanchor在每次训练开始时都会按新输入尺寸重新计算Anchor,所以不用担心换分辨率导致Anchor失效,YOLOv5会自己处理这个流程。另外把--workers调低也可以释放一些内存,但那是内存不是显存,不要混淆。
5.3 小目标人脸检不准:数据增强和二阶段切图
现象:训练完在监控画面上测试,远处的人脸漏检率特别高,近处的人脸又没有任何问题,mAP在远距离段持续走低。
原因:YOLOv5在训练时默认会把图片缩放到640分辨率,而远距离人脸在原始画面里可能只有20x20像素,缩放后变成10x10以内,特征图上的信息几乎丢失了,小目标的锚点匹配数量也远少于中大型目标,所以漏检是必然结果。
解决:有两个常用方案,一是在hyp.yaml里把mosaic和mixup的概率适当降低,因为这两种增强会把多个目标拼接混合,对小目标来说容易产生截断和变形;二是做两阶段检测,先在低分辨率全图上检测出人群密集区域,再把对应区域裁切放大后送入检测器做第二次识别。第二种方案在1000万像素以上的摄像头下效果提升非常明显,代价是推理耗时翻倍。如果部署目标是树莓派5这种设备,我更推荐训练时直接把--img-size设为960,让模型在缩小时多保留一点特征,再配合n模型尺寸来平衡速度。
5.4 树莓派5上部署:模型转换和算力取舍
现象:把训练好的yolov5s.pt放到树莓派5上跑,用torch.hub.load加载,测试时只有2FPS到3FPS,完全不可用。这个帧率跑人脸识别绰绰有余,但放在异常行为检测这类连续分析里就会丢帧严重。
原因:.pt文件在板载CPU上跑的是PyTorch的eager模式,每一层算子都有调度开销,树的推理效率远低于编译优化过的ONNX Runtime或者NCNN这样的专用推理框架。
解决:先把yolov5s.pt导出成ONNX,再用onnx2ncnn转成NCNN格式,配合NCNN的Int8量化,树莓派5上可以跑到60ms到80ms一帧。导出时命令行里要指定--img-size和--dynamic参数,如果不在导出阶段固定输入尺寸,NCNN转换时会出现大量不支持的动态维度算子。另一个常用加速点是关闭人脸识别分支的定期推理,比如每5帧才做一次embedding计算,这在前面已经提过。树莓派5的CPU算力对YOLOv5n来说已经是底线了,所以这种设备上的模型设计从第一天起就应该是n加量化,不要等到部署时才发愁。
6. 最后一步:指标验证与上线前自检
6.1 用mAP和混淆矩阵验证检测与分类模型
训练完成的模型不要只看训练日志里的loss曲线,loss是训练过程的体现,代表拟合状态,不代表任务效果。模型验证优先看验证集上的mAP、Precision和Recall,YOLOv5的训练过程会自动在验证集上做评测,训练完打印的最后一行指标就是模型的最好结果。但这里有一个容易出错的地方:验证集图片必须和训练集来自同一个真实分布。如果你把自己抓拍的摄像头画面全放进了训练集,验证集也来自同一批摄像头,那mAP会虚高,因为同一场景的光照和背景已经被模型记住了。
表情分类模型则要额外看混淆矩阵,特别是生气、厌恶、惊讶这三类之间的混淆程度。混淆矩阵的每行是真实类别,每列是预测类别,如果你发现生气大量被预测成厌恶,并且集中出现在某个光照条件下,那大概率是训练数据在这个类别上存在严重的不平衡,你需要去补数据,而不是调模型结构或超参数。
6.2 上线前的三个自检用例
我在交付这类项目前会固定跑三个自检,你照着做就行。第一个是「单人近景」,距离摄像头2米内,人脸清晰无遮挡,应该能正确识别身份、表情输出稳定、无异常告警;第二个是「多人中景」,三到四人在画面中行走,应该能保持各自ID稳定且不互相抢ID,异常行为不误报;第三个是「远距离小目标」,距离摄像头8米以上,人脸检测允许漏检,但人体检测不能漏,不然异常行为告警就会失效。
这三个自检跑完还需要在真实光照下做一轮连续30分钟的稳定性测试,记录有没有内存持续上升、帧率逐步下降这种隐藏问题。这类问题常见于跟踪模块使用循环数组存储历史轨迹,只增不删,运行长时间后每个目标的历史列表无限增长,内存就被拖垮了。解决办法是做列表长度限制或者定期清理长期未出现的目标ID。
6.3 一套我常用的参数记忆习惯
最终你的模型能不能用于生产,其实取决于整体参数协同,不是某一个参数单独起作用。我自己记忆的方法是:
- 检测部分:
img-size提及小目标下限,conf-thres影响误检率,iou-thres影响密集人群下的漏检率 - 识别部分:cosine距离阈值影响误识率,越低越安全,但阈值过低会导致大量无法识别
- 表情部分:softmax概率阈值和温度系数影响模型对模糊表情的拒判能力,不要使用argmax裸输出
这几组参数共同决定了人脸识别、表情识别和异常行为检测三个子任务在真实环境下的表现,所以你调试时要一组一组地记录每次改动对应的准确率和误报率,不要凭感觉同时调好多组。我自己的习惯是每改一次参数,就在一个文本文件里记录当时的参数组合和对应的测试结果,下次再调参时先看之前的记录,避免从零开始。这个习惯帮我避开了很多调参的麻烦,希望帮到你。
本文还有配套的精品资源,点击获取