基于YOLOv5的实时人脸识别与异常行为检测系统解析
2026/9/16 15:38:36 网站建设 项目流程

简介:基于Yolov5与Python实现的视觉分析源码,整合人脸识别、细粒度表情识别与异常行为检测三项功能,面向计算机视觉方向的毕业设计、课程设计及项目实战,也适合希望快速上手YOLO框架的初学者。代码含详尽注释,逻辑完整,并配有GUI界面,便于理解与二次开发。资源包共107个文件,压缩包24.81MB。以Python脚本为主(37个py),辅以YOLO配置文件(18个yaml)、训练权重(pt)、界面文件(ui)、说明文档及测试图片等,覆盖数据配置、模型推理、结果显示等环节。已有268人学习下载,适合需要可直接运行的高分项目范例。资源内含Dockerfile,可支持容器化部署;同时提供多张测试图与动态演示gif,方便快速验证效果。整体模块划分明确,既可作为Yolov5实际应用的入门学习素材,也能直接作为毕业设计或期末大作业的完整方案,具备较高的实用与参考价值。

1. 一套Yolov5+Python的识别源码,实际是把三条检测管线合成一条

人脸识别门禁、课堂专注度分析、安防摄像机这三种场景看起来毫不相关,但它们底层共享同一个前提:先知道“人”在哪。所谓“基于Yolov5+Python实现的人脸识别、细粒度表情识别、异常行为检测”,本质上是把Yolov5这个目标检测器当成公共底座,在其上并联了三条不同深度的分析管线——人脸比对走特征向量路线,表情识别走分类网络路线,异常行为走跟踪与时序判断路线。难点不在Yolov5本身,而在于三套下游算法如何共用同一个检测输出、如何在CPU或边缘GPU上轮流工作而不互相拖慢。这篇文章按“检测层→识别层→行为层→部署层”的顺序,把每一段的可运行代码、参数取法和常见坑位一起讲清楚,适合正在做毕设、准备小型安防原型、或想快速把手头Yolov5模型扩展成多任务系统的工程师。

2. 用Yolov5训练自己的数据集:人脸与人体检测的模型选型和标注

2.1 检测层选Yolov5而不是MTCNN或OpenCV的理由

人脸识别领域有专门的小模型,MTCNN就是经典选择,召回率不低,速度也快。但这套源码的特别之处在于:同一个检测器还要负责人体检测,供异常行为分析使用。如果人脸用MTCNN、人体用另一个检测器,系统里就要维护两套预处理、两套NMS参数、两个推理引擎,边缘设备上内存和显存都会被压得很紧。Yolov5一个模型可以同时输出人脸框和人体框,只要分类列表里同时包含“face”和“person”两个类别。

训练好的Yolov5对遮挡、暗光、侧脸的鲁棒性也好于传统人脸检测器。OpenCV的Haar或DNN人脸检测在校正用户正对镜头时够用,但摄像头装在走廊顶棚或者教室侧面时,俯视和侧脸场景会让Haar召回率掉到60%以下。Yolov5基于anchor的目标检测方式对“部分可见”的目标天然不敏感,配合mosaic增强后能撑住大部分真实监控角度。

2.2 标注与人脸/人体数据集的搭配策略

检测层不建议从头训练。Yolov5官方仓库提供了在COCO上训练好的权重,其中已经包含person类,人体检测直接可以用。人脸检测推荐在WIDER Face上微调,或者直接使用社区训练好的face模型。自建数据时,标注格式是YOLO txt格式,每一行内容如下:

0 0.4531 0.3828 0.1211 0.1641 1 0.7314 0.5210 0.0986 0.2134

第一列是类别id,人脸设为0、人体设为1,后面四列分别是归一化后的中心点x、中心点y、宽度w、高度h。标注工具用LabelImg或X-AnyLabeling都可以,导出时选择YOLO格式。建议在标注时注意一个细节:人脸框不要紧贴下巴和颅顶,留出约5%的边距,因为后续做人脸对齐时需要用到眼睛和嘴巴的位置,框太紧会把一部分五官切掉。

2.3 训练命令与需要在yolov5超参数里调整的三个值

假设数据目录结构为datasets/face_person/,内部包含images/trainimages/vallabels/trainlabels/val四个子目录,训练命令如下:

python train.py \ --img 640 \ --batch 16 \ --epochs 100 \ --data ../datasets/face_person/face_person.yaml \ --weights yolov5s.pt \ --hyp data/hyps/hyp.scratch-low.yaml \ --cache ram

face_person.yaml里至少要写明nc: 2(类别数量)和names: ['face', 'person']--hyp指定超参数文件,--cache ram表示把图片加载进内存以换取更快的训练速度。

训练时最值得调整的三个超参数在hyp文件里:

超参数默认值调整建议影响范围
lr00.01人脸+人体这类类别少、目标尺寸差异大的任务建议降到0.005学习率偏大会导致小目标不收敛
mosaic1.0如果人脸目标在画面中占比很小,保持1.0;目标接近正方形可降到0.8控制训练时是否做马赛克拼接增强
fl_gamma0.0正负样本极端不平衡时设为1.5focal loss的gamma参数,人脸样本占比低时有效

这三个值中,lr0的影响最直接。很多人直接拿官方的coco训练参数去跑自己的小数据集,结果是loss看起来在降,但val集的mAP@0.5死活上不去,换成更小的学习率后两个epoch就看到改善。如果使用--weights yolov5s.pt做迁移学习,Yolov5会自动冻结前三层,可以先不改动其他参数跑10个epoch,确认类别loss在下降后再做微调。

2.4 检测输出解析:把txt结果转成可下游使用的bbox

训练完成后,运行自带的detect.py导出检测结果:

python detect.py \ --weights runs/train/exp/weights/best.pt \ --source ../datasets/face_person/sample.mp4 \ --save-txt --save-conf --project runs/detect --name sample

每帧图片对应的txt文件会落在runs/detect/sample/labels/下,内容格式为:

0 0.4531 0.3828 0.1211 0.1641 0.92 1 0.7314 0.5210 0.0986 0.2134 0.87

对比之前的标注格式,多了最后一列置信度。注意这里的坐标仍然是归一化的,而且顺序是class x_center y_center width height conf。在Python中解析并转成像素坐标时有一个容易错的地方:直接用PIL或OpenCV读图的尺寸乘以归一化坐标会得到正确的值,但不能把x_center和y_center当成左上角;如果后面接人脸对齐或跟踪算法时,推荐转成[x1, y1, x2, y2]的整数坐标格式,避免浮点误差累积。

转坐标的通用代码段:

import cv2 import numpy as np def yolo_to_pixel(box_norm, img_w, img_h): cx, cy, w, h, conf = box_norm x1 = int((cx - w / 2) * img_w) y1 = int((cy - h / 2) * img_h) x2 = int((cx + w / 2) * img_w) y2 = int((cy + h / 2) * img_h) return x1, y1, x2, y2, conf

这里先把归一化中心点坐标减去宽高的一半得到左上角坐标,再乘以图像宽高。之所以强调顺序是先减后乘而不是先乘后减,是因为前者的误差取决于单次乘法,后者会引入两次独立的舍入误差;虽然差异只有一两像素,但对后续人脸对齐中的关键点坐标计算会产生连锁偏差。

3. 细粒度表情识别:人脸对齐、轻量分类器与AU辅助训练

3.1 细粒度与粗粒度表情识别的差别

先明确结论:FER2013式的七分类(生气、厌恶、恐惧、开心、悲伤、惊讶、中立)属于粗粒度表情识别,区分的是基本情绪类别。“细粒度”关注的是同类表情内部的强度差异和局部变化——同样是开心,嘴角上扬5度和上扬15度在七分类里都叫happy,但细粒度要求模型能区分这两种状态。

细粒度识别的关键不是加深网络,而是让网络注意到那些“类内差异小、类间差异近”的面部区域。做法上常见两个方向:一是回归FACS动作单元(AU)系数,比如AU12对应嘴角拉伸,AU6对应眼轮匝肌收缩,用这些系数的组合表达微表情强度;二是设计注意力模块,让模型自动关注眼睛、眉毛、嘴角这几个区域。后者更省事,适合直接接在Yolov5后面做分类。

3.2 从Yolov5检测框到表情分类的最小通路

实现的最小串联链路是:Yolov5检测人脸 → 裁剪人脸区域 → 仿射变换对齐到112×112 → 送入表情分类网络。对齐这一步经常被忽略,但实际用户体验差异最大的就是这个环节。同一个人的脸,倾斜5度后再缩放到112×112,像素分布完全不同,模型输出的情绪强度值波动会很大。

import cv2 import numpy as np import torch from models.experimental import attempt_load from utils.general import non_max_suppression def align_face(img, bbox, landmarks=None): cx = (bbox[0] + bbox[2]) / 2 cy = (bbox[1] + bbox[3]) / 2 width = bbox[2] - bbox[0] height = bbox[3] - bbox[1] scale = 1.0 if landmarks is not None: left_eye = landmarks[0] right_eye = landmarks[1] angle = np.degrees(np.arctan2(right_eye[1] - left_eye[1], right_eye[0] - left_eye[0])) eyes_center = ((left_eye[0] + right_eye[0]) / 2, (left_eye[1] + right_eye[1]) / 2) M = cv2.getRotationMatrix2D(eyes_center, angle, scale) aligned = cv2.warpAffine(img, M, (img.shape[1], img.shape[0])) bbox_cx = (bbox[0] + bbox[2]) / 2 bbox_cy = (bbox[1] + bbox[3]) / 2 face_crop = aligned[int(bbox_cy - height/2):int(bbox_cy + height/2), int(bbox_cx - width/2):int(bbox_cx + width/2)] else: face_crop = img[int(cy - height/2):int(cy + height/2), int(cx - width/2):int(cx + width/2)] face_crop = cv2.resize(face_crop, (112, 112)) return face_crop

这段代码的关键点是:当有面部关键点时,先按双眼连线角度旋转图像,再从旋转后的图上截取人脸,而不是直接在原图上截取后再旋转。后者的坏处是旋转后四角出现黑色填充区域,这些区域被缩放到112×112后会被模型当成无效纹理信息。如果Yolov5没有输出关键点,就退回用bbox中心做旋转,实际效果会差一些但也能用。

3.3 模型结构:用注意力模块学习局部特征

细粒度表情识别网络不需要大。一个基础的ResNet18加上通道注意力模块就能达到可用水平,参数量约11M,在CPU上的单帧推理时间约18毫秒,放在检测管线后面不会造成明显瓶颈。下面是一个可替换的attention残差块定义:

class SELayer(nn.Module): def __init__(self, channel, reduction=16): super(SELayer, self).__init__() self.avg_pool = nn.AdaptiveAvgPool2d(1) self.fc = nn.Sequential( nn.Linear(channel, channel // reduction, bias=False), nn.ReLU(inplace=True), nn.Linear(channel // reduction, channel, bias=False), nn.Sigmoid() ) def forward(self, x): b, c, _, _ = x.size() y = self.avg_pool(x).view(b, c) y = self.fc(y).view(b, c, 1, 1) return x * y.expand_as(x) class BasicBlock(nn.Module): def __init__(self, inplanes, planes, stride=1): super(BasicBlock, self).__init__() self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=3, stride=stride, padding=1, bias=False) self.bn1 = nn.BatchNorm2d(planes) self.relu = nn.ReLU(inplace=True) self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, padding=1, bias=False) self.bn2 = nn.BatchNorm2d(planes) self.se = SELayer(planes) self.downsample = None if stride != 1 or inplanes != planes: self.downsample = nn.Sequential( nn.Conv2d(inplanes, planes, kernel_size=1, stride=stride, bias=False), nn.BatchNorm2d(planes) ) def forward(self, x): identity = x out = self.conv1(x) out = self.bn1(out) out = self.relu(out) out = self.conv2(out) out = self.bn2(out) out = self.se(out) if self.downsample is not None: identity = self.downsample(identity) out += identity out = self.relu(out) return out

中间层的SE模块用全局平均池化计算每个通道的权重,再与原特征图相乘。它的作用是让网络判断当前人脸时更依赖某些通道——例如检测到嘴角区域有强纹理响应时,提高对应通道的激活值。downsample分支解决的是stride=1的残差块里输入输出通道数不等时的维度匹配问题。输出层根据实际任务决定:如果是AU系数回归,替换成nn.Linear(512, 17)接Sigmoid;如果是细粒度强度分类,接Softmax。

3.4 训练技巧:中心损失与FACS动作单元辅助监督

只用交叉熵训练细粒度表情识别网络,得到的结果往往是:同一表情的不同强度被分到同一类,模型自信但区分度低。常见做法是再加一个中心损失,在训练时约束同一类样本的特征向量靠近类中心,同时配合交叉熵保持类间可分。总损失的计算方式:

criterion_cls = nn.CrossEntropyLoss() criterion_center = CenterLoss(num_classes=num_exp_classes, feat_dim=512, use_gpu=True) for images, labels in dataloader: features = model(images, return_features=True) output = model.classifier(features) loss_cls = criterion_cls(output, labels) loss_center = criterion_center(features, labels) loss = loss_cls + 0.01 * loss_center optimizer.zero_grad() loss.backward() for param in criterion_center.parameters(): param.grad.data *= (0.5 / (0.01 * 1.0)) optimizer.step()

CenterLoss的权重取0.01,后续反向传播时对中心参数的梯度额外乘以0.5,用于控制类中心更新的速度。如果训练数据里带AU标签,可以再加一个辅助分支做AU多标签分类,与表情分类共享backbone的底层特征,提升对嘴角和眼部局部形态的感知能力。

提示:细粒度表情模型需要的小技巧是数据增强中要多用随机擦除。在实际监控画面上,人脸经常被口罩、头发、手部遮挡,随机擦除能让模型学会在部分区域缺失时仍然准确推理。

4. 异常行为检测:跟踪轨迹、速度阈值与姿态判据

4.1 为什么行为检测必须做跟踪:单帧判定误报率太高

异常行为本质是时序概念。单帧图像里一个人躺在地上,可能是摔倒,也可能只是坐在懒人沙发上休息;一个人和另一个人靠近,可能是打架也可能只是握手。只依赖单帧目标检测做判断,误报率通常在每小时数次到数十次之间,无法商用。

合理的判断顺序是:先用Yolov5检测人体框,再用跟踪算法给每个目标分配独立id,维护每条轨迹的历史坐标序列,最后基于轨迹序列做行为和姿态判断。Yolov5本身不做跟踪,每帧输出的目标没有任何身份信息;DeepSort或ByteTrack的作用就是把相邻帧的检测框关联成轨迹。

4.2 DeepSort与Yolov5串联的必调参数:max_age与min_hits

常见做法是用DeepSort做跟踪。下面是与Yolov5串联的核心代码逻辑:

from deep_sort_realtime.deepsort_tracker import DeepSort tracker = DeepSort( max_age=30, max_iou_distance=0.7, n_init=3, nms_max_overlap=1.0, max_cosine_distance=0.4 ) for frame in video_stream: detections = model(frame) # Yolov5输出 [x1, y1, x2, y2, conf, cls] person_dets = [d for d in detections if d[5] == 1] tracks = tracker.update_tracks(person_dets, frame=frame) for track in tracks: if track.is_confirmed(): track_id = track.track_id ltrb = track.to_ltrb()

各参数的实际含义和调整方式:

参数默认值作用异常行为场景下的调整
max_age30目标丢失后轨迹保留的帧数目标被遮挡后重新出现,需要保留轨迹,可提高到50
max_iou_distance0.7检测框与预测框的IOU低于该值则关联失败人体快速移动时框之间IOU明显变小,可放宽到0.8
n_init3连续匹配成功帧数达到该值才确认轨迹希望更快输出id时可降到2,但会造成id切换
max_cosine_distance0.4表观特征的最大余弦距离相邻帧人体表观变化大时可提高到0.5

max_age是异常行为中最影响判断结果的参数。倒地场景中,人倒地后移动速度变慢,但检测框高度会骤降,如果跟踪器的max_age太短,倒地瞬间目标就丢了,后续无法判断“这个人在原地静止”。建议优先增大max_age到50,再看id切换率是否恶化。id切换的典型表现是同一个人的track_id在画面中反复变化,如果出现这种情况,优先降低max_iou_distance而不是加更多的形态特征。

4.3 倒地、奔跑、打架、禁区闯入的判定逻辑

有了轨迹之后,四种常见异常行为的判定逻辑分别如下。注意这些逻辑不依赖姿态估计,只依赖检测框坐标和尺寸变化,优点是CPU也能实时跑,缺点是倒地这种语义无法区分“蹲下系鞋带”和“跌倒”。更严格的做法是接Yolov5-pose或用MediaPipe提取关键点,判断头部与脚部关键点的连线角度。

def detect_fall(history_boxes, current_box, fps): if len(history_boxes) < 5: return False prev_h = np.mean([b[3]-b[1] for b in history_boxes[-5:]]) cur_h = current_box[3] - current_box[1] y_delta = current_box[1] - history_boxes[-1][1] frame_delta = 1.0 / fps velocity_y = y_delta / frame_delta center_x = (current_box[0]+current_box[2]) / 2 center_x_delta = center_x - (history_boxes[-1][0]+history_boxes[-1][2]) / 2 if (cur_h < prev_h * 0.6 and velocity_y > 250): return True if abs(center_x_delta) < 2 and velocity_y < -150 and cur_h < prev_h * 0.7: return True return False

倒地检测用的是两个条件:第一个条件抓“框高度骤降且伴随向下快速移动”的动作过程,第二个条件抓“倒地后静止在低位”的状态。实际工程中第二个条件更可靠,因为快速向下移动通常只持续2到3帧,处理稍慢就错过了。至于速度阈值250像素/秒,是把移动距离除以帧间隔时间得到的实际速度,具体值需要根据摄像头安装高度和画面像素密度标定,建议在正式使用前采集一段正常行走画面,统计正常速度的均值,再乘2作为异常阈值。

奔跑检测直接用中心点速度即可。打架行为的特征是两个人体的检测框在连续多帧内高度重叠,同时中心点在水平方向上高频率抖动;可以在代码中维护每个id的IOU矩阵,当某个时间窗口内两个目标IOU持续大于0.3且中心点距离波动超过阈值时触发告警。禁区闯入则是判断人体中心点是否在预设多边形区域内,用ray casting算法即可,不需要引入额外库。

5. 把三条管线合成一个服务,并压出可用帧率

5.1 线程或进程隔离:识别慢、检测快的调度问题

Yolov5检测每帧耗时约10-20毫秒(GPU上),人脸特征提取和表情分类各需要大约10-30毫秒。如果按“检测→识别→行为”串行执行一帧,总耗时可能达到60毫秒,帧率掉到15 fps以下。常见做法是拆成两个线程:主线程只跑Yolov5检测和跟踪,识别线程消费检测队列做表情与身份识别。人脸特征提取不是每帧都需要做,可以用一个简单的策略——目标id没有入库时从头提取一次特征,之后每30帧复检一次。

提示:检测输出与人脸特征提取使用同一份图像数据时,要处理好内存引用,不能用cv2的直接裁剪共享底层buffer,否则一侧修改会导致另一侧的人脸区域数据被破坏。

5.2 推理优化:半精度、TensorRT与帧采样

推理优化的优先顺序应该是:TensorRT/Float16 > 帧采样 > 降低分辨率。Yolov5本身自带半精度推理:

python detect.py --weights best.pt --source test.mp4 --half --img 640

--half参数将模型转为FP16推理,在T4或3090上通常可以取得1.5-2倍加速;但因为FP16的动态范围小,输入图像数据也会被转成半精度,某些光照剧变的视频帧会出现检测框抖动,因此要确认检测结果在目标场景下不退化再上线。TensorRT的加速更进一步,需要额外做模型转换和校准,适合要求稳定的项目。

如果摄像头数量多但GPU只有一张,帧采样反而是最经济的优化手段。人脸识别做每2帧一次、表情识别做每5帧一次、异常行为检测必须每帧都做——因为跌倒过程往往只有不到1秒,跳帧会导致动作过程被整段跳过。

5.3 用一张表验证识别效果:阈值、验证集划分、误报观察

部署完成后不要只看演示视频的观感,要把判定结果落成表格来观察。一张推荐的效果验证记录表至少包含这几列:

时间戳视频帧号检测id人脸置信度表情强度行为判定结果人工复核
14:03:21.5184370.930.78normal通过
14:03:22.0184970.910.35normal通过
14:05:10.3332130.870.62fall误报,起身太快

观察这张表的重点有两个。第一,表情强度值在短时间内剧烈跳变时,说明前处理或者模型阈值设置有问题,正常人的表情变化是平滑的;第二,行为判定结果要尽量配合人工复核回看视频,连续看半小时的误报比看半小时正确结果更能暴露参数问题。调试时把每次人工复核为误报的帧保存到独立目录,统计这些误报里还包含多少人脸检测框——这个方法能快速定位到底是检测层漏检还是行为判断逻辑本身的问题。

实际部署时还会遇到一个细节:同一张人脸在画面中从远到近移动,检测框从64×64变为256×256,人脸识别阈值固定不变时会出现在远处无法识别、近处正常识别的情况,表情强度也会随之漂移。针对这个问题,可以先固定识别阈值为0.6,再用检测框的尺寸对表情强度做一次归一化:adjusted = raw_intensity * (128 / face_width)。这一行补充逻辑通常能显著降低远距离误报。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询