简介:基于YOLOv5与ArcFace的人脸识别项目资源,面向具备深度学习基础的研究者与安防开发人员,适用于监控、门禁、智能安防等需要端到端检测与身份认证的场景。压缩包共51个文件,包含26个Python源码、9个预编译pyc模块、7个YAML配置、2个pth权重文件、4张人脸样例图片、Shell脚本、cfg及README说明,整体仅3.41MB,轻量且结构清晰,便于快速部署和学习。已有1511人学习下载。工程内置yoloV5_face检测、arc_face特征识别及silentFace防伪模块,目录分层组织模型、数据与工具脚本,pyc文件可确保即插即用。资源覆盖数据预处理、模型训练、测试与调优全流程,核心代码包括train.py训练脚本、recognition.py识别脚本、gen_data.py数据生成,以及resnet、metrics、focal_loss等实现。YOLOv5采用分阶段网格预测,ArcFace通过角度间距增强类间可分性,两者组合兼顾速度与精度,适合在有限算力下完成高鲁棒性人脸识别部署。同时附带的README和示例图片便于快速上手,作为课程设计或工程落地参考均具实用价值。
1. 从检测到识别:YOLOv5+ArcFace 这套组合为什么值得直接上手
做视觉的同行应该都有体会,人脸识别项目最磨人的不是模型选型,而是「检测」和「识别」两段各自为战,联调时经常翻车。这套基于 YOLOv5 做检测、ArcFace 做人脸特征提取的项目,恰好把这两段串成了一条完整可跑的流水线。它解决的不只是「框出人脸在哪」,而是「这个人是谁」的完整闭环,适合刚接触深度学习人脸识别的开发者、需要快速做原型验证的算法工程师,以及想在校招项目里拿出一套完整系统的同学。我拆过不少开源资源,这套是少有的、能从训练数据格式一路跑到特征比对的项目,值得照着复现一遍。
2. 为什么是 YOLOv5 + ArcFace:选型逻辑与这套组合的边界
2.1 YOLOv5 在检测环节的取舍:速度与精度的平衡点
人脸检测可选的方案不少,MTCNN、RetinaFace、YOLOv5 都有人在用。MTCNN 是传统三阶段级联,精度尚可但速度一般,而且对遮挡和小尺度的鲁棒性比较差。RetinaFace 精度高,但部署成本和训练成本都偏高。YOLOv5 的优势在于它本身是一个通用目标检测器,社区生态成熟,权重好找,转 ONNX、TensorRT 都有现成方案,而且推理速度在 GPU 上能做到实时。
在这个项目里,YOLOv5 扮演的角色是先把画面里的人脸位置框出来,然后把裁剪后的人脸区域交给后续的特征提取模块。也就是说,检测是前置环节,它的质量会直接影响识别效果。
2.2 ArcFace 在识别环节的优势:为什么它比 FaceNet 更适合做人脸比对
人脸识别的核心思路是把人脸图像映射成一个向量,然后通过向量之间的距离来判断是否同一人。早期方法直接用 softmax 分类,学到的特征判别力不够。FaceNet 提出了三元组损失,效果不错,但训练时三元组的选择非常影响收敛,属于「玄学调参」。
ArcFace 的做法是在 softmax 损失的基础上加了一个角度边界,让类内距更紧凑、类间距更拉得开。它在 LFW、MegaFace 等公开数据集上的精度都明显优于之前的方案,而且实现简单,只需要在损失函数里改几行代码。这个项目选用 ArcFace,等于直接站在了成熟方案的肩膀上,不用自己去踩三元组损失的坑。
2.3 这套组合的边界:什么场景能扛住,什么场景要另寻他路
这套 YOLOv5 + ArcFace 的组合适合的条件是:光照相对稳定、人脸角度不过分夸张、摄像头画面质量过得去。如果你的场景是人脸在画面里只占几十个像素、或者有大面积遮挡、或者需要应对大规模底库(比如上百万人的 1:N 检索),那这套方案就需要额外处理。
实际使用中,我一般会在前端做一次人脸质量过滤,把模糊、过小、脸不全的框直接丢掉,不送进 ArcFace,这样整体识别准确率能提升好几个百分点。这个细节在后面的代码里也会体现。
提示:这套组合适合做中小规模的人脸识别系统,底库在几万到几十万量级时效果和性能比较均衡。
3. 环境搭建与数据准备:把依赖和训练数据理顺再动手,否则后面全是坑
3.1 环境依赖与项目目录结构
这份资源里的代码依赖的核心库是 PyTorch、OpenCV、NumPy,YOLOv5 部分用的是官方仓库的精简版本。建议用 Anaconda 建虚拟环境,Python 版本锁定 3.8,PyTorch 用 1.8 以上即可,CUDA 版本根据你的显卡驱动来选,不一定追最新。下面是环境搭建的常见做法:
# 创建虚拟环境,锁定 Python 3.8 conda create -n face_recognition python=3.8 -y conda activate face_recognition # 安装 PyTorch(以 CUDA 11.3 为例,实际版本请根据驱动调整) pip install torch==1.10.0+cu113 torchvision==0.11.0+cu113 -f https://download.pytorch.org/whl/torch_stable.html # 安装项目依赖 pip install opencv-python numpy scikit-learn pillow tqdm这串命令里最关键的是 PyTorch 的安装方式,GPU 版和 CPU 版的性能差一个量级,人脸特征提取在 GPU 上只需几毫秒,在 CPU 上可能要到几十毫秒。有条件就上 GPU,后面推理时你会感谢这个决定。
项目目录一般会分为几个部分:检测模型权重目录、ArcFace 特征提取模型目录、训练数据目录、推理脚本和工具脚本。代码包里已经预留好了对应的文件夹结构,直接把下载的权重文件放进去即可。
3.2 训练数据格式:检测模型和识别模型各自需要什么标注格式
YOLOv5 的训练数据格式是每张图片配一个同名的 txt 标注文件,每一行是「类别 x_center y_center width height」,其中中心坐标和宽高都是相对于图片宽高的归一化数值。人脸检测通常只有一类,类别 ID 写 0 就行。
ArcFace 的训练数据格式不同,它是按目录组织的,每个身份一个文件夹,文件夹名就是该人的 ID,文件夹内是该身份的多张人脸图片。训练时用 DataLoader 按目录读取,每个文件夹相当于一个类别,类别数量就是底库的人数。
这份项目里附带的脚本可以完成两种格式的互相转换。如果你手头只有检测标注,需要切出人脸图去训练识别模型,常见做法是先用检测模型把人脸区域裁剪出来,再按身份归类到对应文件夹。这里给出一段核心转换脚本:
# data_preprocess/yolo_to_arcface.py import cv2 import os def crop_faces_from_yolo(image_path, txt_path, save_dir, identity_id): img = cv2.imread(image_path) h, w = img.shape[:2] with open(txt_path, 'r', encoding='utf-8') as f: for idx, line in enumerate(f): parts = line.strip().split() if len(parts) != 5: continue cls_id, x_center, y_center, box_w, box_h = parts box_w, box_h = float(box_w) * w, float(box_h) * h x_center, y_center = float(x_center) * w, float(y_center) * h x1, y1 = int(x_center - box_w / 2), int(y_center - box_h / 2) x2, y2 = int(x_center + box_w / 2), int(y_center + box_h / 2) # 边缘裁剪保护,防止越界 x1, y1 = max(0, x1), max(0, y1) x2, y2 = min(w, x2), min(h, y2) face = img[y1:y2, x1:x2] if face.size == 0: continue save_path = os.path.join(save_dir, str(identity_id)) os.makedirs(save_path, exist_ok=True) cv2.imwrite(os.path.join(save_path, f'{idx}.jpg'), face)这段脚本逻辑上没什么花头,就是把 YOLO 的归一化坐标还原成像素坐标,然后按 ID 存文件夹。需要注意的两个细节:一是边缘裁剪要做边界保护,否则坐标越界会直接报错;二是同一个身份的多张图要保证都传到同一个 identity_id,否则识别模型的类别会乱掉。
3.3 底库注册:识别模型不是拿来直接用的,先要建底库
很多人拿到项目后直接跑识别脚本,发现输出的相似度全是乱的,原因在于没搞懂底库(Gallery)的构建流程。ArcFace 模型输出的只是特征向量,你要先把你认识的每个人的照片都送进模型,把每个人的特征向量存成一个「底库文件」,识别时拍一张新脸,和底库里所有特征向量比对,取相似度最高的那个作为结果。
这段逻辑对应的是项目里的「注册」功能,代码里一般是循环读取底库目录下的图片,逐张提特征,存成 numpy 格式或者 json 格式。要特别注意:底库的图片质量直接影响识别上限,模糊的、角度偏的底库照片会让整个系统都跟着变差。
提示:底库照片每人大约 1-5 张即可,选正脸、光线均匀的图片。超过 5 张并不会带来精度提升,反而增加比对耗时。
4. 核心代码走读:从视频流到输出人脸 ID 的全链路实现
4.1 YOLOv5 检测封装:拿到人脸框的一手坐标
这份项目的推理主流程是:读入图像帧,YOLOv5 检测出人脸框,按置信度过滤后裁剪人脸,ArcFace 提取特征,最后做比对。第一步封装 YOLOv5 检测的常见做法是加载官方权重,设置置信度阈值和 NMS 阈值:
# infer/detector.py import torch import cv2 class FaceDetector: def __init__(self, weights_path, device='cuda', conf_thres=0.5, iou_thres=0.45): self.model = torch.hub.load('ultralytics/yolov5', 'custom', path=weights_path) self.model.conf = conf_thres # 置信度阈值:低于此值的框直接丢弃 self.model.iou = iou_thres # NMS 的 IoU 阈值:抑制重叠框 self.device = device if device == 'cuda': self.model.cuda() def get_faces(self, frame_bgr): results = self.model(frame_bgr) boxes = results.xyxy[0].cpu().numpy() faces = [] for box in boxes: x1, y1, x2, y2, conf, cls_id = box if int(cls_id) != 0: continue # 丢弃过小的人脸,减少误检 if (x2 - x1) < 20 or (y2 - y1) < 20: continue faces.append((int(x1), int(y1), int(x2), int(y2), float(conf))) return faces这段封装里需要解释的是model.conf和model.iou这两个参数。conf建议设置在 0.4-0.6 之间,设太低会出现大量误检框,设太高又容易漏检侧脸和小脸。iou是 NMS 参数,重叠框合并用的,0.45 是常见经验值。还有一个小细节:torch.hub.load首次运行会从网络拉取 YOLOv5 的模型结构代码,离线环境需要先手动把仓库 clone 到本地再指定source='local'。
4.2 ArcFace 特征提取:预处理那一步马虎不得
人脸识别模型对输入图片的尺寸、像素格式都很敏感。ArcFace 官方训练时用的是 112x112 的 RGB 输入,推理时要做同样的预处理:先把图片从 BGR 转成 RGB,再 resize 到 112x112,最后做归一化。很多人在这里直接用了 cv2.resize 就送进模型,结果识别率大跌。
# infer/feature_extractor.py import torch import cv2 import numpy as np from torchvision import transforms class ArcFaceExtractor: def __init__(self, backbone_path, device='cuda'): self.device = device self.model = self._load_backbone(backbone_path) self.model.to(device) self.model.eval() # 预处理流程:从 OpenCV 的 BGR 转为 RGB,再归一化 self.transform = transforms.Compose([ transforms.ToPILImage(), transforms.Resize((112, 112)), transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)), ]) def _load_backbone(self, path): # 项目里用的是 mobilefacenet 结构,加载权重 from backbone.mobilefacenet import MobileFaceNet model = MobileFaceNet(out_ch=512) state_dict = torch.load(path, map_location=self.device) model.load_state_dict(state_dict) return model def extract(self, face_bgr): face_rgb = cv2.cvtColor(face_bgr, cv2.COLOR_BGR2RGB) tensor = self.transform(face_rgb).unsqueeze(0).to(self.device) with torch.no_grad(): feature = self.model(tensor) # 特征向量 L2 归一化,方便后续用余弦相似度直接比对 feature = torch.nn.functional.normalize(feature, p=2, dim=1) return feature.cpu().numpy().flatten()这里的Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))是将像素值从 [0, 1] 映射到 [-1, 1],和训练时的预处理保持一致。normalize那行是关键,不做 L2 归一化,后面用余弦相似度比对时就等于白白加了无关紧要的尺度信息,结果会不稳定。MobileFaceNet 是我个人比较偏好的轻量骨干网络,参数量小,在嵌入式设备上能跑,精度也够用。
4.3 比对策略:从欧氏距离到余弦相似度,阈值怎么定
特征提取完成后,剩下的就是和底库特征做比对。这里常用的相似度指标是余弦相似度,计算方式是矩阵乘法。底库特征通常拼成一个矩阵,每行是一个人,查询特征是一个向量,两者做点积得到一组相似度分数,取 top-1 作为识别结果。
# infer/matcher.py import numpy as np class FaceMatcher: def __init__(self, gallery_path, threshold=0.5): # gallery: npy 文件,每行一个特征向量,同时存一个 names 列表 data = np.load(gallery_path, allow_pickle=True).item() self.gallery_feats = data['feats'] # shape: (N, 512) self.names = data['names'] # list, 长度 N self.threshold = threshold def match(self, query_feat): # 余弦相似度 = 点积(因为特征已做 L2 归一化) scores = np.dot(self.gallery_feats, query_feat) idx = int(np.argmax(scores)) score = float(scores[idx]) if score >= self.threshold: return self.names[idx], score else: return 'unknown', scorethreshold的取值直接决定系统的误识率和拒识率。阈值调高,陌生人不容易混进来,但认识的人也可能被拒;阈值调低,所有陌生人都可能被识别成某个人。我的习惯是先在验证集上把所有类内比对分数和类间比对分数画出来,取两者交叉处的值作为初始阈值,再按实际场景微调。
5. 避坑与常见问题排查:六个把新手卡到怀疑人生的点
5.1 现象:模型加载正常,推理时显存溢出
原因:YOLOv5 和 ArcFace 两个模型同时加载在 GPU 上,加上视频帧的数据缓冲,显存直接爆掉。常见的是 4G 显存的入门显卡最容易触发。解决:给检测和识别分开建两个推理进程,或者把检测模型切到 CPU 上跑,只把 ArcFace 放在 GPU 上。实际测试中,YOLOv5s 在 CPU 上检测一帧大约 100-200ms,如果视频流只需要每秒几帧,这个方案完全能接受。
5.2 现象:识别准确率很低,但单独看检测框都很准
原因:人脸裁剪出来后没有做对齐。ArcFace 在训练时用的是对齐后的人脸(眼睛、鼻子、嘴巴的位置经过仿射变换摆正),直接截框训练会引入大量角度变化,拉低特征质量。解决:项目里如果没带关键点检测分支,建议在裁剪后至少做一次水平翻转判断,或者用 OpenCV 的仿射变换把两只眼睛拉平。这个预处理步骤我之前也偷懒跳过,后来在测试集上对比过,对齐前后准确率差 8-12 个百分点,不是小数目。
5.3 现象:程序跑通了,但每帧推理耗时超过 300ms
原因:比对过程没有做批量处理。每帧都重新加载底库文件、循环算相似度,这种写法最慢。解决:底库在初始化时加载一次到内存,比对操作全部用 numpy 矩阵运算,不要写 for 循环。如果底库有几万条,还能用 faiss 建索引,把比对耗时的量级从 O(N) 降到近似 O(logN)。这套优化做完之后,单帧推理能压到 50ms 以内。
5.4 现象:识别时出现过拟合,训练集表现好、测试集一塌糊涂
原因:ArcFace 训练数据集合里同一个人的图片数量太少,或者训练集和底库图片来自同一场景,模型学到的是背景信息而不是人脸信息。解决:训练时每个 ID 的照片至少要 10 张以上,且要包含不同的角度、光照、表情;另外在 DataLoader 里做随机裁剪、色彩抖动等数据增强,强行让模型不依赖背景。这个就是典型的「看起来学得好,实则学到背景」的坑。
5.5 现象:程序运行一段时间后内存持续上涨,最后被杀
原因:OpenCV 的视频帧没有释放,或者 PyTorch 的 tensor 累积在列表里没清。常见的是把检测框、特征向量都存在循环内的变量里,没有及时清理。解决:在循环体的最后把帧变量置为 None,用del把大对象显式回收;如果还不行,用torch.cuda.empty_cache()在每处理完一批后清一次缓存。调内存问题花的时间往往比写功能还多,建议一开始就养成这种习惯。
5.6 现象:换了摄像头或换了场景,误检率突然升高
原因:YOLOv5 模型在训练时见过的人脸数据主要是自然场景,如果是俯视画面、鱼眼镜头、或者黑白红外相机,检测分布出现偏移。解决:在检测结果后加一个人脸质量校验,用传统方法检查人脸区域是否存在双眼和鼻子(比如用 OpenCV 的 Haar 级联做二次校验),挡掉一部分误检。再不行就得在目标场景下采集几百张图做微调训练。
6. 工程化进阶:把模型导出成 ONNX 做部署,以及识别结果的可解释性验证
当这套方案在本地跑通之后,下一步大概率是被要求「把模型部署到服务端或者边缘设备上」。PyTorch 模型直接推理在服务器上没问题,但到了嵌入式设备或者需要用 C++ 调用时,导出成 ONNX 是必经之路。ONNX 格式的好处是跨框架、跨语言,OpenCV DNN、TensorRT、ONNX Runtime 都能直接加载。导出流程如下:
# export/export_onnx.py import torch from backbone.mobilefacenet import MobileFaceNet model = MobileFaceNet(out_ch=512) model.load_state_dict(torch.load('weights/arcface_mobilefacenet.pth', map_location='cpu')) model.eval() dummy_input = torch.randn(1, 3, 112, 112) torch.onnx.export( model, dummy_input, 'weights/arcface_mobilefacenet.onnx', input_names=['input'], output_names=['embedding'], dynamic_axes={'input': {0: 'batch_size'}, 'embedding': {0: 'batch_size'}}, opset_version=11, )导出 ONNX 时最常踩的坑是动态轴尺寸的设置。dynamic_axes把 batch 维设成动态,这样部署时一次可以送入多张人脸图做批量推理。opset_version的选择也要注意,太低的版本不支持某些算子,太高的版本在老设备上可能不兼容,11 是我目前用下来兼容性最好的折中值。导出后用 ONNX Runtime 验证一遍输出和 PyTorch 是否一致,就是拿同一张图分别跑一次,比较特征向量之间的误差是否在 1e-5 级别以内。
部署时的性能权衡也值得提一句:ArcFace 的网络本身很小(MobileFaceNet 只有几百万参数),和 YOLOv5 相比简直轻量。如果是嵌入式设备,检测端才是瓶颈。我的做法是把 YOLOv5s 量化成 INT8,精度损失大约 2%-3%,但推理速度提升 2-3 倍;识别端保持 FP32,因为人脸特征的精度要求高,INT8 会让特征一致性明显变差。
最后说一个我自己的习惯:每次调完阈值、修改预处理步骤或者换底库,我都会拿固定的几组验证图片跑一遍全流程,记录输出的 ID 和相似度分数,放进一个表格里保存下来。这样下来,每次改动的影响都是可追踪的,不会出现「调了一下预处理,自己都没注意识别率降了」的情况。这套项目本身已经具备了这个闭环验证的基础——检测、特征提取、比对三段都各有独立的输入输出,改一段不影响其他段的调试。希望你在复现的时候也能用上这个方法,能帮你少走不少弯路,希望帮到你。
本文还有配套的精品资源,点击获取