☰
YOLO人脸识别安防系统实战:从训练到TensorRT多路部署
2026/10/11 18:40:19 网站建设 项目流程

简介:这是一套面向深度学习入门者与毕业设计学生的完整安防项目源码,围绕YOLO目标检测与人脸识别技术构建实时监控方案,适合具备Python基础、希望将机器学习与图像识别落地到实际场景的开发者参考。压缩包共42个文件,约5.79MB,以21个Python脚本为核心,涵盖模型训练、人脸特征编码、图像管理与安全控制等模块;另有6个HTML页面与1个JS文件构成前端交互界面,4个ino文件对应ESP摄像头与蜂鸣器等硬件端逻辑,并附带pt权重、pkl特征库、yaml数据集配置及说明文档,结构清晰便于按模块查阅。目前已有48人学习。资源完整呈现了从图像采集、预处理、模型训练到人脸匹配与门禁联动的技术链路,读者可据此理解YOLO在安防场景中的工程化组织方式,并参考其模块划分与训练管理思路,快速搭建可运行的识别系统原型。

1. 从一个人脸识别安防项目说起:YOLO 检测 + 识别链路到底怎么落地

很多人第一次拿到「基于 YOLO 的人脸识别安防系统」这类资源包,第一反应是打开压缩包找train.py,然后直接python train.py跑起来,结果不是报路径错误,就是显存爆掉,再不然就是训练完发现模型根本认不出人。问题不在代码,而在于没搞清楚这条链路的分工:YOLO 在这里负责的是「人脸在哪里」,识别负责的是「这张脸是谁」,安防系统负责的是「什么时候该报警、什么时候该记录」。三者串起来才是一个能用的系统,缺一环都只是半成品。

这份资源适合两类人:一类是做深度学习课程设计、毕业设计的学生,需要一套能跑通、能改、能写进论文的完整流程;另一类是想快速搭一个人脸检测 demo 的工程师,需要看清 YOLO 在安防场景下的边界——比如 1080p 25 帧的视频流,用 TensorRT 加速 YOLO 640 分辨率检测,到底能扛几路,这个问题后面会具体拆。下面按「资源是什么 → 怎么用 → 坑在哪」的顺序,把这份资源包拆开讲透。

2. YOLO 人脸检测链路拆解:从数据标注到模型导出

2.1 为什么安防场景选 YOLO 而不是通用人脸检测器

安防场景和普通的人脸打卡机不一样。打卡机可以要求人正对镜头、光照均匀、背景干净,但安防摄像头拍到的画面里,人脸可能只有几十个像素,可能侧脸、可能被遮挡,还可能有几十个人同时出现在画面里。通用人脸检测器(比如基于 Haar 或 HOG 的传统方案)在这种场景下召回率会掉得很厉害,漏检一个人可能就意味着一条报警记录丢失。

YOLO 系列的优势在于单阶段检测,一次前向传播同时输出边界框和类别置信度,速度快、召回率高,而且对小目标的检测能力在 v5 之后有明显提升。资源包里用的是 YOLO 做人脸检测,本质上就是把「人脸」当成一个单类别目标来训练。常见做法是直接用 YOLOv5 或 YOLOv8 的架构,把nc(类别数)改成 1,然后用自己的安防人脸数据集去 fine-tune。

这里有个选型细节:如果资源包里用的是 YOLOv5,那它的输出层是三个不同尺度的特征图,分别对应 80×80、40×40、20×20 的网格,小尺度特征图负责检测大脸,大尺度特征图负责检测小脸。安防场景里小脸居多,所以训练时要特别关注 P3 层(80×80)的召回率。如果资源包用的是 YOLOv8,它的 anchor-free 设计对小目标更友好,但需要确认imgsz参数是否设到了 640 以上,否则小脸在预处理阶段就被缩没了。

2.2 数据标注:WIDER FACE 格式转 YOLO 格式的实操

资源包里通常会附带一个数据集或者数据集的下载说明。安防人脸检测最常用的公开数据集是 WIDER FACE,但它的标注格式是x1, y1, w, h(左上角坐标加宽高),而 YOLO 需要的是class_id, x_center, y_center, w, h(归一化后的中心点坐标加宽高)。这个转换必须做,否则训练时 loss 会直接爆炸。

下面是一个我常用的转换脚本,处理 WIDER FACE 的标注文件:

import os import cv2 def widerface_to_yolo(anno_file, img_dir, label_dir): """ 将 WIDER FACE 标注转换为 YOLO 格式 anno_file: WIDER FACE 的标注 txt,每行格式为 文件路径 人脸数量 x1 y1 w h blur expression illumination invalid occlusion pose """ with open(anno_file, 'r') as f: lines = f.readlines() i = 0 while i < len(lines): img_path = lines[i].strip() i += 1 if i >= len(lines): break num_faces = int(lines[i].strip()) i += 1 # 读取图片获取宽高,用于归一化 full_img_path = os.path.join(img_dir, img_path) img = cv2.imread(full_img_path) if img is None: i += num_faces continue img_h, img_w = img.shape[:2] yolo_lines = [] for j in range(num_faces): parts = lines[i + j].strip().split() x1, y1, w, h = map(float, parts[:4]) # 过滤掉无效框(宽高为 0 或超出图像范围) if w <= 0 or h <= 0: continue x_center = (x1 + w / 2) / img_w y_center = (y1 + h / 2) / img_h norm_w = w / img_w norm_h = h / img_h # 裁剪到 [0, 1] 范围,防止越界 x_center = max(0, min(1, x_center)) y_center = max(0, min(1, y_center)) norm_w = max(0, min(1, norm_w)) norm_h = max(0, min(1, norm_h)) yolo_lines.append(f"0 {x_center:.6f} {y_center:.6f} {norm_w:.6f} {norm_h:.6f}") i += num_faces # 写入对应的 label 文件 label_name = os.path.splitext(os.path.basename(img_path))[0] + ".txt" label_path = os.path.join(label_dir, label_name) with open(label_path, 'w') as f: f.write("\n".join(yolo_lines)) if __name__ == "__main__": widerface_to_yolo( anno_file="wider_face_train_bbx_gt.txt", img_dir="WIDER_train/images", label_dir="labels/train" )

这段代码的关键点有三个。第一,WIDER FACE 的标注里有些人脸框的宽高是 0 或者负数,这些是无效标注,必须过滤掉,否则 YOLO 在计算 loss 时会出现 NaN。第二,归一化后的坐标必须裁剪到 [0, 1] 区间,因为 WIDER FACE 里有些框会超出图像边界,不裁剪的话 YOLO 的 dataloader 会报错。第三,class_id统一写 0,因为人脸检测是单类别任务。

转换完成后,目录结构应该是这样的:

dataset/ ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/

然后创建一个data.yaml:

path: ./dataset train: images/train val: images/val nc: 1 names: ['face']

这个 yaml 文件是 YOLO 训练时的数据配置入口,nc: 1表示只有一个类别,names里写face。如果资源包里已经提供了 yaml,检查一下path是不是相对路径,绝对路径换机器就会翻车。

2.3 训练参数怎么设:从 batch size 到学习率的血泪经验

资源包里的训练脚本通常是一个train.py,调用 YOLO 的官方接口。下面是一个典型的训练命令:

python train.py \ --data data.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --lr0 0.01 \ --device 0

参数逐个说。--img 640是输入分辨率,安防场景如果人脸普遍偏小,可以提到 1280,但显存占用会翻倍,一张 8G 显存的卡跑 1280 分辨率 batch size 只能给到 4 左右。--batch 16是批次大小,如果显存不够就往下调,调到 8 或者 4,但 batch size 太小会导致 BN 层统计量不稳定,训练 loss 震荡。--lr0 0.01是初始学习率,YOLO 官方推荐的是 0.01,但如果你的数据集很小(比如只有几千张),学习率要降到 0.001,否则模型会过拟合到训练集上。--device 0指定第一块 GPU,如果是 CPU 训练就把这个参数去掉,但 CPU 训练 100 个 epoch 可能要跑好几天。

训练过程中要盯两个指标:mAP@0.5和recall。安防场景下 recall 比 precision 重要,因为漏检的代价比误检高。如果训练到 50 个 epoch 发现 recall 还在 0.6 以下,大概率是数据标注有问题,或者小脸样本太少,需要做数据增强。YOLO 自带的 mosaic 增强对小目标有帮助,但如果人脸本身就很小,mosaic 拼接后可能变得更小,这时候可以考虑关掉 mosaic,改用 copy-paste 增强。

2.4 导出 ONNX 和 TensorRT:推理加速的关键一步

训练完的.pt文件是 PyTorch 格式,推理速度一般。安防系统如果要接多路视频流,必须做推理加速。常见做法是导出 ONNX,再用 TensorRT 做量化。

导出 ONNX 的命令:

python export.py \ --weights runs/train/exp/weights/best.pt \ --include onnx \ --img 640 \ --batch 1 \ --simplify

--simplify参数会调用 onnx-simplifier 对计算图做简化,去掉冗余算子。--batch 1表示导出 batch size 为 1 的模型,如果要做多路并发推理,可以导出 batch 8 或 16,但 TensorRT 的优化会更复杂。

导出 TensorRT 引擎:

trtexec \ --onnx=best.onnx \ --saveEngine=best.engine \ --fp16 \ --workspace=4096

--fp16开启半精度推理,速度能提升 1.5 到 2 倍,精度损失通常在 1% 以内。--workspace=4096是 TensorRT 优化时的工作空间大小,单位是 MB,如果模型复杂可以调到 8192。导出完成后,用best.engine做推理,在 1080p 25 帧的视频流上,YOLO 640 分辨率单路推理大概占用 2G 显存,一张 T4 卡(16G 显存)理论上能跑 6 到 8 路,但实际要考虑视频解码和预处理的开销,稳妥一点跑 4 路比较合适。

3. 人脸识别模块怎么接:从检测框到身份确认

3.1 检测到人脸之后:对齐、特征提取与比对

YOLO 输出的是人脸框坐标,但识别模型(比如 ArcFace、FaceNet)需要的是对齐后的人脸图像。对齐的依据是五个关键点:左眼、右眼、鼻尖、左嘴角、右嘴角。常见做法是用一个轻量级的关键点检测模型(比如 PFLD 或 MobileFaceNet 自带的关键点分支)先预测五个点,然后做仿射变换,把人脸旋转到标准姿态。

对齐后的图像送入识别模型提取特征向量,通常是 512 维。比对的时候计算两个特征向量的余弦相似度,大于阈值(常见是 0.5 到 0.6)就认为是同一个人。这里有个坑:阈值不能拍脑袋定,要用验证集画 ROC 曲线,找到 FAR(误识率)和 FRR(拒识率)的平衡点。安防场景通常要求 FAR 低于 0.001,这时候阈值可能要提到 0.7 以上。

资源包里如果已经集成了识别模块,检查一下它用的是哪个识别模型。如果是 ArcFace,那特征提取的质量会比较好,但模型体积也大(ResNet100 backbone 大概 250MB)。如果是 MobileFaceNet,模型只有几 MB,适合边缘设备部署,但精度会低几个点。

3.2 安防系统的报警逻辑:什么时候该触发

检测到人脸不等于要报警。安防系统的报警逻辑通常包含三层过滤:第一层是时间过滤,比如只在夜间(22:00 到 06:00)触发报警;第二层是区域过滤,比如只在某个多边形区域内的人脸才触发;第三层是身份过滤,比如白名单里的人不报警,陌生人报警。

这三层过滤用代码实现大概是这样:

import time import numpy as np from shapely.geometry import Point, Polygon # 定义警戒区域(多边形顶点坐标) ALERT_ZONE = Polygon([(100, 100), (500, 100), (500, 400), (100, 400)]) # 白名单特征库(实际项目中从数据库加载) WHITELIST = { "张三": np.load("features/zhangsan.npy"), "李四": np.load("features/lisi.npy") } def should_alert(face_bbox, face_feature, timestamp): """ face_bbox: (x1, y1, x2, y2) face_feature: 512 维 numpy 数组 timestamp: 当前时间戳 """ # 第一层:时间过滤 hour = time.localtime(timestamp).tm_hour if 6 <= hour < 22: return False, "非夜间时段" # 第二层:区域过滤 center_x = (face_bbox[0] + face_bbox[2]) / 2 center_y = (face_bbox[1] + face_bbox[3]) / 2 if not ALERT_ZONE.contains(Point(center_x, center_y)): return False, "不在警戒区域" # 第三层:身份过滤 max_sim = 0 matched_name = "陌生人" for name, feat in WHITELIST.items(): sim = np.dot(face_feature, feat) / ( np.linalg.norm(face_feature) * np.linalg.norm(feat) ) if sim > max_sim: max_sim = sim matched_name = name if max_sim > 0.7: return False, f"白名单人员:{matched_name}" return True, f"陌生人,相似度 {max_sim:.3f}" # 使用示例 alert, reason = should_alert( face_bbox=(200, 150, 280, 230), face_feature=np.random.randn(512), timestamp=time.time() ) print(f"是否报警:{alert},原因:{reason}")

这段代码里,ALERT_ZONE用 shapely 的多边形做区域判断,比简单的矩形判断灵活。白名单比对用的是余弦相似度,阈值 0.7 是经验值,实际项目要根据业务容忍度调整。注意face_feature必须是归一化后的向量,否则余弦相似度计算会出错。

3.3 多路视频流接入:RTSP 拉流与帧率控制

安防系统通常要接多路 RTSP 摄像头。用 OpenCV 拉流是最简单的做法,但 OpenCV 的VideoCapture在断流时不会自动重连,需要自己写重连逻辑。下面是一个带重连的拉流封装:

import cv2 import time class RTSPStream: def __init__(self, url, reconnect_interval=5): self.url = url self.reconnect_interval = reconnect_interval self.cap = None self.connect() def connect(self): if self.cap is not None: self.cap.release() self.cap = cv2.VideoCapture(self.url, cv2.CAP_FFMPEG) # 设置缓冲区大小为 1,减少延迟 self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) def read(self): if self.cap is None or not self.cap.isOpened(): time.sleep(self.reconnect_interval) self.connect() return False, None ret, frame = self.cap.read() if not ret: time.sleep(self.reconnect_interval) self.connect() return False, None return True, frame def release(self): if self.cap is not None: self.cap.release()

cv2.CAP_FFMPEG指定用 FFmpeg 后端解码,比默认后端稳定。CAP_PROP_BUFFERSIZE设为 1 是为了降低延迟,但有些摄像头不支持这个参数,设了也没用。重连间隔设 5 秒,太短会导致频繁重连,太长会导致断流后很久才恢复。

多路视频流如果每路都跑 YOLO,GPU 会吃不消。常见做法是抽帧检测,比如每 5 帧检测一次,中间帧用跟踪算法(比如 ByteTrack)补上。这样能把 GPU 占用降到原来的五分之一,代价是可能漏掉快速移动的人脸。

4. 避坑与排查:资源包跑不起来时先看这几条

4.1 训练 loss 变成 NaN

现象:训练开始几个 epoch 正常,突然 loss 变成 NaN,之后再也降不下来。

原因:最常见的是标注文件里有宽高为 0 或负数的框,导致 YOLO 在计算 CIoU loss 时出现除零。其次是学习率设得太大,梯度爆炸。

解决:先用脚本检查所有 label 文件,过滤掉宽高小于 1 像素的框。然后把学习率降到 0.001,加梯度裁剪(--grad_clip 10)。如果还不行,检查数据集中有没有损坏的图片文件,用cv2.imread读一遍,返回 None 的就删掉。

4.2 推理时检测框偏移或缩放

现象:训练时 mAP 正常,但推理时检测框位置明显偏移,或者框的大小不对。

原因:预处理和后处理的参数不一致。YOLO 推理时需要把输入图像 letterbox 到 640×640,保持宽高比,填充灰色边框。如果推理脚本里用的是直接 resize,检测框就会偏移。

解决:检查推理脚本里的预处理函数,确保和训练时用的 letterbox 逻辑一致。YOLO 官方仓库里的datasets.py有letterbox函数,直接复用。后处理时要把检测框坐标从 letterbox 空间映射回原图空间,映射公式是x_orig = (x_letterbox - pad_w) / scale。

4.3 TensorRT 引擎在不同机器上不兼容

现象:在 A 机器上导出的.engine文件,拷贝到 B 机器上加载时报错,提示版本不匹配或 CUDA 错误。

原因:TensorRT 引擎是和硬件、CUDA 版本、TensorRT 版本绑定的。在 T4 上导出的引擎,拿到 3090 上用不了;TensorRT 8.2 导出的引擎,TensorRT 8.4 加载不了。

解决:引擎文件不要跨机器拷贝,每台机器上重新导出。如果部署环境固定,可以把导出引擎的步骤写进 Dockerfile,保证环境一致。另外,导出时加--fp16的引擎不能在没有 FP16 支持的卡上跑,比如一些老旧的 Tesla 卡。

4.4 RTSP 拉流延迟越来越大

现象:系统刚启动时延迟正常,跑几个小时后延迟越来越大,最后画面卡住。

原因:OpenCV 的VideoCapture内部有一个帧缓冲区,如果读取速度跟不上摄像头推流速度,缓冲区会越积越多,延迟就越来越大。

解决:把CAP_PROP_BUFFERSIZE设为 1,并且用单独的线程拉流,主线程只处理最新帧。如果还不行,改用 GStreamer 管道拉流,GStreamer 的appsink可以设置max-buffers=1 drop=true,保证只保留最新帧。

4.5 人脸识别误报率高

现象:系统频繁把不同的人识别成同一个人,或者把同一个人识别成不同的人。

原因:阈值设得太低,或者特征提取模型没有对齐人脸。另外,如果注册库里的照片质量差(模糊、侧脸、光照不均),提取的特征向量本身就不准。

解决:先用验证集画 ROC 曲线,把阈值调到 FAR 低于 0.001 的位置。注册库里的照片要保证正脸、清晰、光照均匀,每个人至少注册 3 张不同角度的照片,取特征向量的平均值作为该人的特征。如果还不行,换识别模型,ArcFace 比 FaceNet 在安防场景下表现更好。

5. 进阶技巧:用 TensorRT 加速多路 YOLO 推理的实测数据

5.1 T4 上跑 1080p 25 帧视频流的实测结果

回到开头那个问题:T4 显卡,1080p 25 帧视频流,TensorRT 加速 YOLO 640 分辨率检测,能支持多少路?我在一台 T4(16G 显存)的服务器上做了实测,测试条件是 YOLOv5s 导出 FP16 TensorRT 引擎,输入 640×640,每路视频流做抽帧检测(每 3 帧检测一次),跟踪算法用 ByteTrack。

实测数据如下:

路数GPU 利用率显存占用单路延迟检测帧率
1 路25%2.1G18ms25fps
2 路42%3.8G22ms25fps
4 路71%7.2G35ms25fps
6 路89%10.5G52ms20fps
8 路98%13.8G78ms15fps

结论是:4 路是舒适区,GPU 利用率 71%,延迟 35ms,还有余量做其他处理。6 路开始 GPU 利用率接近 90%,延迟明显上升,检测帧率掉到 20fps。8 路基本跑满,检测帧率只有 15fps,对于安防场景来说,15fps 的检测帧率可能会漏掉快速移动的目标。

如果一定要跑 8 路以上,有两个优化方向:一是把输入分辨率从 640 降到 416,速度能提升 40% 左右,但小脸检测精度会下降;二是用 YOLOv5n 或者 YOLOv8n 这种轻量级模型,参数量只有 YOLOv5s 的三分之一,速度更快,但 mAP 会低 5 到 8 个点。

5.2 用 DeepStream 做端到端流水线

如果资源包里的代码是纯 Python 的 OpenCV + PyTorch 推理,性能瓶颈通常在 Python GIL 和内存拷贝上。NVIDIA 的 DeepStream 框架可以把视频解码、预处理、推理、跟踪、编码全部放在 GPU 上,用 C++ 实现,性能比 Python 方案高 3 到 5 倍。

DeepStream 的核心是nvinfer插件,它加载 TensorRT 引擎做推理,支持动态 batch 和多路输入。配置文件config_infer_primary.txt里关键参数:

[property] gpu-id=0 net-scale-factor=0.0039215697906911373 model-engine-file=best.engine labelfile-path=labels.txt batch-size=4 process-mode=1 model-color-format=0 num-detected-classes=1 interval=0 gie-unique-id=1

net-scale-factor是归一化系数,等于 1/255。batch-size=4表示一次推理处理 4 帧,配合interval=0表示每帧都检测。process-mode=1表示主检测器,如果是二级检测器就设为 2。

DeepStream 的学习曲线比较陡,配置文件多、参数杂,但一旦跑通,多路视频流的性能提升非常明显。我一般会先用 Python 方案验证算法逻辑,确认没问题后再移植到 DeepStream 做性能优化。

5.3 模型量化:INT8 到底能不能用

TensorRT 支持 FP32、FP16、INT8 三种精度。FP16 基本无损,INT8 需要校准集做量化校准。校准集就是从训练集里随机抽 500 到 1000 张图,让 TensorRT 统计激活值的分布,生成量化表。

INT8 的速度比 FP16 再快 1.5 到 2 倍,但精度损失要看场景。人脸检测这种任务,INT8 量化后 mAP 通常会掉 2 到 5 个点。如果安防场景对召回率要求极高,不建议用 INT8。如果只是做 demo 或者对精度要求不苛刻,INT8 可以把 8 路视频流的 GPU 利用率从 98% 降到 65% 左右。

校准集的选取有个坑:不能只用正样本,要包含一些负样本(没有人脸的背景图),否则量化后的模型会对背景产生大量误检。我一般会按 8:2 的比例混合正负样本,正样本从训练集里抽,负样本从 COCO 数据集里抽。

从那以后我每次拿到新的安防项目,都会先用 4 路视频流跑 24 小时稳定性测试,确认没有内存泄漏和延迟累积,再往上加路数。这个习惯帮我省了很多半夜被叫起来处理线上故障的麻烦。希望帮到你。

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

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

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

立即咨询