Yolov5+Python实战:人脸识别、表情识别与异常行为检测

发布时间:2026/9/24 22:33:22
Yolov5+Python实战:人脸识别、表情识别与异常行为检测 简介基于Yolov5Python构建的人脸识别、细粒度表情识别及异常行为检测源码是一套集多任务于一体的完整项目。项目针对毕业设计、期末大作业等学习场景设计代码包含详细注释结构模块化清晰即使刚接触目标检测与深度学习的新手也能根据YAML配置和Python脚本快速理解整个识别流程。资源包共107个文件涵盖37个Python算法脚本、18个YOLO模型配置、21个TXT标注与说明另有预训练模型、UI界面、Dockerfile和测试图片等配套资源压缩包总大小约24.81MB部署时无需额外拼凑文件。系统功能完善、界面美观操作与二次开发门槛低经过严格调试下载后简单配置即可运行。目前已有269人学习整体完成度较高可直接作为课程设计、期末大作业或毕业设计的高分参考对需要一人完成视觉识别全流程开发的开发者十分实用。1. 为什么人脸识别、表情识别、异常行为检测可以共享一套 Yolov5 底座门禁要人脸识别访客要表情分析摄像头还要盯跌倒和逗留三个需求同时压过来时大部分团队的惯性做法是拆成三个独立模块人脸识别上 Facenet 或 ArcFace表情识别单独训一个 CNN行为检测再上一套 OpenPose。结果往往是接口五花八门推理链路各跑各的视频流要过三遍模型GPU 占用翻倍维护成本直线上升。基于 Yolov5Python 实现的人脸识别、人脸细粒度表情识别、异常行为检测这套方案核心思路正好相反三个人脸相关的视觉任务共用同一个 Yolov5 检测底座检测框同时喂给特征比对模块和表情分类模块行为检测则在检测结果之上用规则状态机完成。真正决定精度上限的往往不是算法选得多先进而是人脸检测框画得准不准、稳不稳。这套方案适合有 Python 基础、想用一套框架同时解决检测、识别、分类三类任务的工程师和学生落地成本远低于想象。2. 先做对数据三类任务的数据集设计与 YOLO 格式标注2.1 三个任务各要什么数据别再指望一个公开数据集通吃先说结论人脸检测、表情识别、异常行为检测这三个任务的数据来源完全不同不能拿一个数据集硬套。人脸检测的目标是输出“人脸在哪”训练数据需要的是带框标注的图片。开源数据集中 WIDER Face 是最常用的包含 3 万多张图片、约 40 万张人脸但它的难点样本偏多直接训练容易让模型在小脸上过度敏感实际项目里我会混合 WIDER Face 和自己场景里拍的监控截图比例控制在 2:1 左右效果比单用任何一个都好。细粒度表情识别的数据则完全不同。它需要的是“一张脸对应一个表情标签”的分类数据集而不是框。RAF-DB 是细粒度表情领域绕不开的选择包含 7 类基本表情加一个“轻蔑”共 8 类约 3 万张图FER2013 虽然更出名但图像质量差、人脸对齐不稳定训出来的模型在真实摄像头场景很容易翻车。如果项目对“细粒度”有要求比如要区分“微笑”和“大笑”那 RAF-DB 也不够需要自己采集并标注程度级别。异常行为检测最特殊——它没有公认的统一数据集。跌倒检测可以参考 UR Fall Detection 或 Le2i 数据集打架可以用 Hockey Fight 数据集但这些数据集场景单一、摄像头角度固定迁移到你的现场往往水土不服。我的一般做法是在自己项目的摄像头机位录 20 到 30 分钟正常状态和异常状态的视频抽帧标注比任何公开数据集都管用。数据集的准备工作量里行为检测占一半以上这一点要有心理准备。2.2 把 VOC / 其他格式转成 YOLO 训练格式转换脚本与四个边界坑Yolov5 训练需要的是 YOLO 格式的 txt 标注文件每一行代表一个目标格式是“类别序号 中心点x 中心点y 宽度 高度”坐标全部归一化到 0 到 1。网上很多数据集给的是 VOC 格式的 XML直接用会报“no labels found”或者训练时 loss 直接发散。转换脚本并不复杂但边界坑特别多。import os import glob import xml.etree.ElementTree as ET from pathlib import Path # 类别顺序必须和后续 data.yaml 里的 names 完全一致 CLASSES [face, helmet, person] def voc_to_yolo(xml_path, out_dir): tree ET.parse(xml_path) root tree.getroot() img_w int(root.find(size).find(width).text) img_h int(root.find(size).find(height).text) lines [] for obj in root.iter(object): name obj.find(name).text # 跳过不在类别列表里的目标 if name not in CLASSES: continue # 跳过 VOC 标注里被标记为难例的目标 difficult obj.find(difficult) if difficult is not None and int(difficult.text) 1: continue idx CLASSES.index(name) box obj.find(bndbox) x1 float(box.find(xmin).text) y1 float(box.find(ymin).text) x2 float(box.find(xmax).text) y2 float(box.find(ymax).text) # 边界裁剪防止越界坐标导致训练报错 x1 max(0, min(x1, img_w)) x2 max(0, min(x2, img_w)) y1 max(0, min(y1, img_h)) y2 max(0, min(y2, img_h)) # 宽度或高度为 0 的标注是脏数据直接丢 if x2 - x1 0 or y2 - y1 0: continue # 转归一化的中心点 宽高 cx (x1 x2) / 2 / img_w cy (y1 y2) / 2 / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h lines.append(f{idx} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) if lines: txt_path os.path.join(out_dir, Path(xml_path).stem .txt) with open(txt_path, w, encodingutf-8) as f: f.write(\n.join(lines)) if __name__ __main__: in_dir Annotations/ # 原始 XML 目录 out_dir labels/ # 输出 txt 目录 os.makedirs(out_dir, exist_okTrue) for xml_file in glob.glob(os.path.join(in_dir, *.xml)): voc_to_yolo(xml_file, out_dir)这段脚本的逻辑是解析 XML 里的 size 节点拿到图片宽高遍历每个 object 节点把类别名映射成类别序号再把左上角右下角坐标转成中心点加宽高的归一化形式。四个最容易踩的坑分别是类别顺序和 yaml 不一致模型训出来所有框都偏差一个类别坐标系越界比如标注框有一小半在图片外面不裁剪直接归一化会产生大于 1 的坐标difficult 标记为 1 的目标被当成普通目标一起训练宽高为 0 的异常框在计算 loss 时直接导致 NaN。脚本里已经处理了这四类情况直接用即可。Yolov5 训练自己的数据集时转换完后一定要随机抽几张图把生成的 txt 和原图画在一起可视化检查这一步能省掉后面所有排查标签的时间。2.3 标注规范与样本量建议多少张能训出可用模型三个任务的样本量需求差别很大经验值如下表任务最低样本量建议构成备注人脸检测5000 张图 / 10 万框正脸 60%、侧脸 25%、小脸 15%小脸比例低于 10% 时监控场景漏检严重表情识别每类 3000 张以上8 类相对均衡类别不平衡时用 WeightedRandomSampler异常行为每类 20 段视频 / 5 万帧跌倒、逗留、入侵各单独录公开数据集只用来预训练必须加入现场数据表情识别的细粒度要求越高每类的样本量就越要往 5000 张靠。真实项目里最常犯的错是把公开数据集的 8 类比例直接拿来训结果“中性”占了一大半“厌恶”和“轻蔑”几乎学不到特征推理时这两类永远不输出。碰到这种情况先统计训练集分布用类别权重或者过采样补齐。另外人脸检测的标注框太贴脸也会影响后续表情识别——框贴着皮肤边缘裁出来的脸缺了一部分表情特征就断了标注时四边各留 5% 的余量比较合适。3. 用 Yolov5 训练自己的数据集环境、命令与 5 个必调参数3.1 环境安装Python 3.8 PyTorch 的常见坑Yolov5 的环境配置在网上被吐槽最多的是版本兼容Python 版本、PyTorch 版本、CUDA 版本三者只要有一个不对编译阶段就会报错。我长期用的是 Python 3.8配 PyTorch 1.12 或 2.0 都能稳定跑Python 3.10 以上也能装但必须用新版 PyTorch否则 torchvision 编译时会报“cannot be resolved against python helper roots”一类的错误。安装命令如下conda create -n yolo5 python3.8 -y conda activate yolo5 # 先装 CPU 版跑通流程 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu # 有 NVIDIA 显卡再装 CUDA 版 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple第一行创建独立环境避免把系统 Python 搞乱。第二行激活环境。第三行装 CPU 版 PyTorch 是验证代码逻辑最快的路径网络模型小跑通再切 GPU 版能过滤掉一半的环境问题。requirements.txt 里需要装的是 opencv、numpy、matplotlib、seaborn 这些依赖国内网络环境下用清华源速度会快很多。PyTorch 版本和 CUDA 版本不匹配属于“玄学”问题最靠谱的做法是去 PyTorch 官网的 get-started 页面复制对应你 CUDA 版本的完整命令不要自己拼装。环境装完先跑一句python -c import torch; print(torch.__version__, torch.cuda.is_available())确认 GPU 可用再进入下一步。Yolov5 基础笔记里提到的最常见启动报错是AssertionError: CUDA unavailable九成是 PyTorch 装了 CPU 版。3.2 数据配置与训练命令从 yolov5s 起步环境就绪后第一步是写好数据配置 yaml。Yolov5 通过这个文件定位训练集、验证集的图片路径以及类别数量和类别名。文件内容如下train: ./datasets/face/images/train val: ./datasets/face/images/val nc: 3 names: [face, helmet, person]train 和 val 路径指向存放图片的目录即可Yolov5 会自动到同级目录下找 labels 文件夹里的 txt 标注所以图片目录结构一般是datasets/ face/ images/ train/ val/ labels/ train/ val/注意这里不能把 labels 写在 yaml 里配置路径Yolov5 硬编码了 labels 目录与 images 目录的相对关系改路径反而会报“found no labels”。训练命令我一般这样写python train.py \ --data face.yaml \ --weights yolov5s.pt \ --img 640 \ --batch-size 16 \ --epochs 100 \ --cache第一个参数 --data 指定数据配置--weights 用预训练权重不要用--weights 从零训练迁移学习能大幅缩短收敛时间。--img 是输入分辨率人脸检测不建议低于 640人脸属于中小目标分辨率太低会在下采样阶段丢失特征。--batch-size 按显存调节24G 显存跑 yolov5s 可以到 328G 显存就老实开到 8 到 16。--epochs 以验证集 mAP 不再上升为准一般 100 轮内收敛。--cache 把图片加载进内存能省掉每轮训练都读磁盘的时间但内存小于 16G 时反而会拖慢系统谨慎使用。训练过程中观察两个指标一个是val/box_loss和val/cls_loss是否持续下降另一个是mAP0.5是否逼近 0.9 以上。如果 mAP 在 0.5 上下徘徊先别调模型结构回去检查标注和类别分布。3.3 超参数怎么调lr、mosaic、anchor 只动这三个Yolov5 的超参数全在data/hyps/hyp.scratch-low.yaml里对新手来说里面几十个参数不需要挨个理解。基于训练经验人脸相关任务真正值得动的只有三个学习率 lr0、mosaic 增强、anchor 计算。lr0: 0.01 lrf: 0.2 mosaic: 1.0lr0 是初始学习率人脸检测任务样本量大但单个目标小0.01 经常会造成前期 loss 震荡我一般改成 0.001收敛更稳。lrf 是学习率衰减的最终比例0.2 意味着最后学习率降为初始的 20%如果验证 loss 在后期反复横跳改成 0.1 让学习率降得更彻底。mosaic 增强是把 4 张图拼成一张训练对小目标检测帮助很大但也会带来一个副作用人脸被拼接缝切断导致模型学到“半张脸”的错误特征。检测框明显偏大或者漏检时把 mosaic 从 1.0 降到 0.5或者配合--cache一起用症状会缓解。anchor 是 Yolov5 为人脸任务调整最频繁的项。默认的 anchor 是针对 COCO 的物体尺寸设计的人脸相对更小、更扁。在训练命令里加上--multi-scale的效果有限更直接的办法是用 Yolov5 自带的自动 anchor 计算首次在自定义数据集上训练时Yolov5 会自动调用 k-means 重新计算 anchor。但如果数据集里小脸占比特别高自动计算结果可能依然偏向中等尺度这时可以手动在模型 yaml 里按小脸的实际像素调整。# yolov5s.yaml 中 anchors 部分人脸场景改小前 3 组 anchors: - [6, 10, 8, 16, 10, 22] - [18, 38, 28, 50, 40, 76] - [90, 140, 160, 180, 260, 320]第一组 anchor 对应 80x80 特征图上的小目标第二组对应 40x40第三组对应 20x20。人脸平均像素低于 24 时把第一组的宽高往 10 以下调。注意训练前先用一张测试图跑python detect.py --source xxx.jpg --weights yolov5s.pt看检测框尺度再决定是否手动改 anchor。经验法则同一尺度目标占全部目标比例超过 30% 时手动调整 anchor 比自动计算更有效。3.4 人脸识别用检测、表情识别用分类同一个 Yolov5 的两种打开方式人脸识别和表情识别在 Yolov5 里有两种完全不同的用法新手最容易混在一起。人脸识别的本质是“先检测后比对”——Yolov5 只负责把人脸框出来识别是谁这件事要交给特征提取模型比如 FaceNet 或 ArcFace。这里如果用 Yolov5 的分类头去直接分类人脸身份类别会随着注册人数增加而不断膨胀每次加人都要重训模型实际项目里完全不可行。表情识别则相反它的输出是有限的类别比如 8 类表情所以可以直接用 Yolov5 自带的分类模式训练。命令是python classify/train.py \ --model yolov5s-cls.pt \ --data datasets/expression/ \ --img 112 \ --epochs 50 \ --batch-size 64datasets/expression/目录下按类别分子文件夹train/angry/、train/happy/这样组织即可。--model参数用yolov5s-cls.pt这是 Yolov5 官方针对 ImageNet 预训练的分类版本。输入尺寸--img 112是参考 ArcFace 的输入设计人脸区域本身信息密度高不需要太大的分辨率112 既能保留细粒度表情特征又能把训练和推理速度提升一倍。如果追求更高的细粒度表情识别精度比如区分“真笑”和“假笑”可以在 backbone 上换 EfficientNetV2 或 small ViT。ViT 在表情这种细粒度任务上确实能涨点但前提是每类样本量不低于 5000 张数据不够时效率反而不如 EfficientNetV2后者在中等数据规模下性价比更高。4. Python 推理链路从检测框到人脸识别与细粒度表情识别4.1 加载模型与检测循环别在循环里重复 load训练完成后推理链路的第一个原则是模型只加载一次放在循环外。很多新手把torch.hub.load写进 while True 循环里每帧加载一次权重FPS 直接掉到 1 以下还以为是模型太慢。正确的姿势如下import cv2 import torch # 加载本地训练好的权重sourcelocal 不会去连外网 model torch.hub.load(., custom, pathruns/train/exp/weights/best.pt, sourcelocal) model.conf 0.45 # 置信度阈值 model.iou 0.45 # NMS 的 IoU 阈值 model.classes [0] # 只保留 face 这一类 cap cv2.VideoCapture(0) while True: ret, frame cap.read() if not ret: break results model(frame, size640) det results.xyxy[0] # [x1, y1, x2, y2, conf, cls] 的 Tensor for *box, conf, cls in det.tolist(): x1, y1, x2, y2 [int(b) for b in box] face_crop frame[y1:y2, x1:x2] # 后续的人脸识别和表情识别都基于这个 face_crop pass cv2.imshow(yolov5 face, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()model.conf 0.45是置信度阈值低于该值的检测框被过滤人脸误检多的场景可以调到 0.6。model.iou 0.45控制 NMS 的合并程度值越小越容易合并重叠框。model.classes [0]限定只检测第 0 类如果不加限制模型会把 person 和 helmet 全输出后处理逻辑会变复杂。results.xyxy[0]返回的是 Torch Tensor每个检测框有 x1、y1、x2、y2、置信度、类别序号 6 个值。很多人用的results.pandas().xyxy[0]虽然读取方便但每一帧都要做 DataFrame 转换在实时流里会多出 5 到 8 毫秒的耗时上线时建议用纯 Tensor 版本。4.2 人脸识别Yolov5 只负责找脸比对交给特征向量拿到人脸框后人脸识别模块要做的是把裁剪出来的人脸转成特征向量再和注册库里的人脸特征比对。这里我用face_recognition库做特征提取它封装的底层是 FaceNet对中小项目完全够用部署也简单。如果需要更高的识别精度或者要求特征维度可控可以换成 ArcFace。import cv2 import numpy as np import face_recognition # 全局注册库姓名 - 128 维特征向量 face_db {} def register_face(name, image_path): 注册单张人脸用 Yolov5 检测结果裁剪后入库 img face_recognition.load_image_file(image_path) locs face_recognition.face_locations(img) if len(locs) ! 1: return False, f检测到 {len(locs)} 张脸注册失败 enc face_recognition.face_encodings(img, locs)[0] face_db[name] enc.tolist() return True, f注册成功 def identify(face_bgr, threshold0.45): 传入 Yolov5 裁剪出来的人脸 BGR 图返回姓名和距离 face_rgb cv2.cvtColor(face_bgr, cv2.COLOR_BGR2RGB) encs face_recognition.face_encodings(face_rgb) # 裁剪区域里没检测到人脸返回 unknown if not encs: return unknown, 0.0 query np.array(encs[0]) best_name, best_dist unknown, 1.0 for name, vec in face_db.items(): # 欧氏距离越小越相似 dist np.linalg.norm(query - np.array(vec)) if dist best_dist: best_dist dist best_name name # 超过阈值的都算陌生人 return (best_name if best_dist threshold else unknown), best_distthreshold0.45是欧氏距离的常用起始值这个值不是固定的它受摄像头分辨率、光线、人脸角度影响很大。现场注册 5 到 10 个熟悉的人分别拍正面、侧面再测一遍距离取“本人最大距离”和“他人最小距离”的中间值作为阈值比直接抄网上的 0.45 靠谱。face_recognition.face_encodings内部自带人脸检测加特征提取但这里传入的已经是 Yolov5 裁剪好的区域它只负责提取特征不需要再做一次检测。注册时直接把 Yolov5 的检测结果喂进去能保证注册和识别时用同一套检测定位逻辑避免两套检测框不一致导致特征提取差异。另外注册多张不同角度的照片、取特征均值入库识别成功率会明显提升这是成本最低的优化手段。4.3 细粒度表情识别裁剪、对齐、置信度过滤表情识别的输入是同一个 face_crop但处理链路完全不同。人体检测框到人脸框的转换以及人脸框的四边留白都会影响细粒度表情的精度。代码实现如下import torch import torch.nn as nn from torchvision import models, transforms EXPRESSIONS [angry, disgust, fear, happy, sad, surprise, neutral, contempt] class ExpressionNet(nn.Module): def __init__(self, num_classes8): super().__init__() self.backbone models.efficientnet_b0(weightsIMAGENET1K_V1) in_features self.backbone.classifier[1].in_features self.backbone.classifier[1] nn.Linear(in_features, num_classes) def forward(self, x): return self.backbone(x) # 加载训练好的权重 expr_model ExpressionNet(num_classes8) expr_model.load_state_dict(torch.load(expression_best.pt, map_locationcpu)) expr_model.eval() # 推理预处理和训练时的预处理保持完全一致 expr_transform transforms.Compose([ transforms.ToTensor(), transforms.Resize((112, 112)), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) def predict_expression(face_bgr): 输入 Yolov5 裁剪的人脸 BGR 图输出表情类别和置信度 face_rgb cv2.cvtColor(face_bgr, cv2.COLOR_BGR2RGB) x expr_transform(face_rgb).unsqueeze(0) with torch.no_grad(): probs torch.softmax(expr_model(x), dim1)[0] score, idx torch.max(probs, dim0) if score.item() 0.6: return unknown, score.item() return EXPRESSIONS[idx.item()], score.item()关键参数是Resize((112, 112))和置信度阈值0.6。输入尺寸为什么是 112 而不是 224——细粒度表情识别关注的是眼睛、嘴角这些局部区域112 分辨率下局部特征依然保留但推理速度提升约 3 倍。score 0.6时输出unknown很关键实际部署中经常出现光线异常、口罩遮挡导致的表情误判加一个低置信度过滤能减少一多半错报。注意训练时的预处理和推理时不一致是最隐蔽的坑训练用了 Resize 到 224推理用 112或者训练时做了额外的随机裁剪而推理时没做都会导致精度下降 5 到 10 个点。训练和推理的 transform 必须完全一致一个像素都不能差。细粒度表情的提升还可以靠 alignment。Yolov5 的人脸框是矩形但人脸在画面中是有旋转角度的裁出来的脸歪着表情特征自然对不准。常见做法是用 OpenCV 的仿射变换按两只眼睛的位置把脸摆正再裁剪这一步在实时推理中耗时约 2 毫秒但对细粒度表情的准确率提升显著。5. 避坑与排查训练和部署中最常见的 5 个翻车现场5.1 训练 loss 不降、框画错位先查标签再查模型现象训练了 20 轮box_loss一直在 0.1 左右不动验证集上画出来的框每个都偏到目标的左上角。原因几乎总是标注文件的问题而不是模型结构的问题。比如类别序号从 1 开始而不是 0或者归一化坐标算错模型学到的永远是“目标实际位置和标注差一个偏移量”。解决先用可视化脚本把标签画到图上检查不要直接改模型。在 Yolov5 的跑通训练之前抽 20 张训练图用 OpenCV 的 rectangle 函数按 txt 还原坐标画框肉眼确认框和物体贴合再花时间调参。也可以运行python train.py --weights yolov5s.pt前先跑python val.py --data face.yaml --weights yolov5s.pt但最直接的还是可视化标签。这个问题占我遇到过的训练问题里四成以上。5.2 小脸全漏imgsz 与 anchor 的关系现象测试图上 100 像素以上的大脸都能框住但 30 像素的小脸一个不输出。原因有两个输入尺寸--img 480导致小脸下采样后只剩几个像素或者训练数据里小脸样本本身太少。解决输入尺寸从 480 提到 640 甚至 768代价是 FPS 下降约 20%。如果提高到 768 依然漏说明数据问题远大于参数问题去训练集里统计所有标注框的宽高分布小脸占比低于 15% 时用复制粘贴增强把大脸缩小后贴到场景里补样本。这里注意不要用整图缩放的方式冒充小脸因为监控场景里的小脸往往伴随模糊和遮挡单纯缩小图引入的是清晰小脸分布还是不一致。调参数之前先想清楚是“模型没 capacity”还是“数据没有这个分布”。5.3 表情识别遇到侧脸就崩margin 与数据增强现象正面脸表情识别准确率超过 90%但摄像头前的用户稍微侧身表情立刻变成unknown或者乱跳。原因训练时用 RAF-DB里面大多是正脸侧脸样本不够或者 Yolov5 的人脸框裁得太紧侧脸时嘴巴和眼睛被切掉一部分。解决推理时在检测框基础上向外扩 20% 再做裁剪代码里就是x1 max(0, x1 - (x2-x1)*0.1)这样四边各扩 10%。训练阶段给表情数据加左右翻转、随机亮度扰动和 ±15 度的随机旋转侧脸泛化能力明显提升。RAF-DB 本身没有左右翻转增强大部分开源表情模型在侧脸场景失效都是这个原因。另外细粒度表情标注时如果发现“恐惧”和“惊讶”经常混淆把这两类的样本各挑出 200 张人工复核一遍通常是标签错而不是模型错。5.4 实时视频只有 5 帧跳帧 跟踪 half 精度现象单个模型推理一张图要 60 毫秒理论上有 16 FPS但加上人脸识别、表情识别、画框显示整体掉到 5 FPS视频明显卡顿。原因每帧都跑 Yolov5而人脸识别模块里face_recognition.face_encodings内部又做了一次人脸检测等于一帧跑两遍检测器。解决分三层第一跳帧检测——检测模块每 3 帧跑一次中间 2 帧用上一帧的位置人脸移动不太快时完全可行第二人体跟踪任务用 IoU tracker 或 ByteTrack 维护轨迹检测结果里直接带上 track_id后续的人脸识别结果按 track_id 做缓存比如同一轨迹每 2 秒才重新做一次特征比对第三Yolov5 推理加halfTrue参数模型用 FP16 精度GPU 上速度直接翻倍。face_recognition如果项目工期紧可以直接换成 OpenCV 的EigenFaceRecognizer或LBPHFaceRecognizer速度极快但精度有限适合初期验证流程最终上线建议还是 ArcFace 或 FaceNet。FPS 问题的本质是重复计算太多先找出哪一步做了重复工作再考虑优化模型本身。5.5 人脸比对认错人阈值与归一化现象注册了 10 个人测试时 A 的脸经常被识别成 B但单看特征距离又不大。原因直接用face_recognition.face_encodings返回的原始特征计算欧氏距离没有做归一化或者阈值直接用 0.45没有按现场数据校准。解决比对前先对特征做 L2 归一化query query / np.linalg.norm(query)这样处理后再计算距离不同人脸的特征尺度差异会大幅缩小。阈值校准的方法是抓 20 个注册人的正面照片两两计算归一化后的距离统计“同一个人不同照片”的距离分布和“不同人”的距离分布找到两个分布不重叠的位置作为阈值。如果两个分布重叠严重说明特征提取的能力不足换 ArcFace 比调阈值有效。注册库也要注意每人用 3 到 5 张不同光照、不同角度的照片注册取特征均值入库比单张照片的鲁棒性好得多。这些血泪经验总结起来就是一句话先确保特征质量再调阈值。6. 用规则引擎做异常行为检测阈值参数与回放验证6.1 模型只做“人在哪”行为判断交给状态机异常行为检测的落地方式跟很多人想的不一样。真正上线时如果直接用模型分类“跌倒”“逗留”“打架”你会发现三个问题异常样本极少模型根本学不到每个现场的角度、距离都不同模型迁移一次废一次行为有强时序性单帧分类永远是割裂的。我的一般做法是Yolov5 只负责检测 person输出每一帧的人体框行为判断全部放到 Python 侧的状态机里根据框的宽高比、位置变化、持续时间做规则判定。这方案有三个优势——人体检测的公开权重成熟度高不用为行为单独积累海量数据规则可解释业务方提出“跌倒后要 3 秒内报警”直接改参数就能满足现场迁移只需要重新录几段视频验证阈值不用重新训练。6.2 三个典型行为的判定阈值与代码class BehaviorState: 每个人的状态机按 track_id 单独维护实例 def __init__(self, max_still30, fps25): self.last_center None self.low_frame 0 # 连续低宽高比帧数 self.still_frame 0 # 连续静止帧数 self.max_still int(max_still * fps / 30) # 逗留判定帧数 self.state normal def update(self, bbox): x1, y1, x2, y2 bbox w, h x2 - x1, y2 - y1 ratio h / max(w, 1e-6) # 宽高比站立时 1.5 center ((x1 x2) / 2, (y1 y2) / 2) # 位移量中心点相对上一帧的移动距离 disp abs(center[0] - self.last_center[0]) abs(center[1] - self.last_center[1]) \ if self.last_center else float(inf) self.last_center center # 判定跌倒宽高比显著变小 位移极小倒地后基本不动 is_down ratio 0.6 and disp 20 self.low_frame self.low_frame 1 if is_down else 0 # 判定逗留位移极小且持续时间超阈值 is_still disp 5 self.still_frame self.still_frame 1 if is_still else 0 # 状态切换 if self.low_frame 20: # 持续 20 帧倒地处为跌倒 self.state fallen elif self.still_frame self.max_still: self.state lingering else: self.state normal return self.state三个阈值参数决定行为判定的灵敏度ratio 0.6是宽高比阈值正常人站立时宽高比在 1.5 到 2.5 之间跌倒后人体横向展开宽高比降到 0.6 以下但弯腰捡东西也会短暂出现低宽高比所以还要加位移条件disp 20是中心点位移阈值单位是像素跌倒后人体基本静止弯腰捡东西则伴随明显位移low_frame 20是持续时间阈值按 25 FPS 算约 0.8 秒防止单帧误判。三个阈值必须配合使用只靠宽高比会把低头看手机的人判成跌倒。摄像头角度很关键俯视场景下宽高比失效需要单独训练一个头顶检测模型或者把阈值改成基于人体框面积变化率。6.3 回放验证用标注好的视频卡阈值规则判定最大的优势是验证成本低。准备 5 段已标注异常事件的视频标出每段视频里“异常开始帧”和“异常结束帧”然后用不同阈值组合跑一遍回放计算帧级 precision 和 recall选择 F1 最高的那组阈值。这个流程我每到一个新现场都要做一次30 分钟能完成。Python 脚本的核心逻辑是读入视频逐帧调用 Yolov5 检测 person把检测框喂给状态机记录状态输出的帧号和标注的 ground truth 对比。如果 recall 低说明阈值太严调大low_frame或放宽ratio如果 precision 低说明误报多调低low_frame或收紧disp阈值。阈值调参比模型调参快得多这也是规则方案在生产环境更受运维欢迎的原因。最后记住一个习惯——把所有调好的阈值写进配置文件不要硬编码在代码里现场光线、摄像头角度一变第一件事就是重新回放验证阈值而不是改模型。这套“检测模型加规则状态机”的组合我用了两年多误报率比早期全模型方案少了一半以上希望帮到你。本文还有配套的精品资源点击获取