简介:这是一份基于YOLOv5与ArcFace的人脸识别项目资源包,面向具备一定深度学习基础的算法工程师、计算机视觉学习者及本科高年级学生,解决从人脸检测到身份识别的完整工程落地问题。压缩包共51个文件,以26个Python脚本为核心,涵盖模型训练、测试、识别及数据预处理等环节,另含7个YAML模型配置、2个PTH预训练权重、9个PYC缓存文件以及4张样例图片、Shell脚本、配置与说明文档,整体仅3.41MB,轻量但模块划分清晰,适合快速理解源码结构。项目将YOLOv5的实时目标检测能力和ArcFace的加性角度间隔损失机制有机结合:前者准确框出人脸区域,后者通过类间角度约束增强特征判别力,从而提升复杂场景下的识别准确率。资源附带的预训练权重、推理脚本及说明文档,可帮助读者掌握数据标注、模型优化与部署的关键细节,并直接运行体验完整识别流程。该资源已有1511人学习,适合用于课程设计、毕业设计或企业级安防、考勤等场景的前期算法验证。
1. 人脸识别项目实战:为什么用YOLOv5做检测、ArcFace做识别
我在做某个考勤系统改造时,拿到了这套基于YOLOv5做检测、ArcFace做特征提取的人脸识别项目。花一个下午跑通后,我觉得它最大的价值不在某一个算法有多新,而是把“人脸在哪”和“这个人是谁”两件事彻底拆开了:YOLOv5负责从画面里框出人脸,ArcFace负责把人脸区域变成一串向量,再靠余弦相似度判断身份。全流程在CPU上处理一张图耗时可压到300毫秒左右,接门禁、签到、图片检索都够用。适合想把深度学习模型真正落到业务里的开发者,不管是当代码模板还是当算法基座,这个项目的工程划分都值得抄一抄。
2. 整体架构与工程结构:检测、对齐、识别三个模块的边界
人脸识别的工程链路看起来简单,但第一次自己搭的时候很容易把逻辑全塞在一个脚本里。这个项目的做法是拆成三个独立模块,每个模块只对外暴露一个函数,模块之间的数据流用 numpy 数组和字典传递,不互相 import 模型对象。这个设计在你后替换模型、加缓存、部署成服务时会省很多事。
2.1 检测输出到识别输入的数据流
一条完整的人脸识别链路可以切成四段:
- YOLOv5 检测模型输入整张图片,输出人脸框坐标和五个关键点(两只眼睛、鼻尖、两个嘴角)。
- 根据人脸框把原图 ROI 裁出来,再用关键点做仿射变换,将人脸摆正并缩放到 112x112。
- ArcFace 模型接收 112x112 的标准化图像,输出 512 维的特征向量。
- 将当前特征向量与底库向量做点积,得到余弦相似度,超过阈值判定为同一个人。
这个流程里最容易搞混的是第二步和第三步的边界:YOLOv5 的输出是像素坐标,ArcFace 的输入是经过标准化的人脸图像,中间这层对齐放大如果省略,识别准确率会崩得厉害。所以项目里把对齐单独抽成align模块,而不是塞进识别模块里。
2.2 目录结构说明
整个项目采用按职责分层的目录结构,拿到手先看这几个目录:
face_recognition/ ├── config.py # 全局配置:阈值、路径、模型参数 ├── requirements.txt # 依赖清单 ├── main.py # 入口:支持注册/验证/检索三种模式 ├── detector/ │ └── yolo_face_detector.py # YOLOv5检测封装,输出bbox+5关键点 ├── align/ │ └── face_align.py # 仿射变换对齐,输出112x112人脸图 ├── recognition/ │ ├── arcface_feature.py # ArcFace特征提取 │ ├── compare.py # 相似度计算与阈值判断 │ └── database.py # 底库注册与检索(numpy实现) ├── models/ │ ├── yolov5_face.pt # 人脸检测权重 │ └── arcface_r50.pth # ArcFace权重 ├── test_minimal.py # 最小链路验证脚本 └── utils/ └── image_loader.py # 图片读取统一入口detector 和 recognition 完全不互相导入,中间通过align输出的标准人脸图串联。这种解耦方式的好处是,你后面如果想换检测模型,比如把 YOLOv5 换成 SCRFD,只需要保证detect()函数返回同样的字典结构即可,下游代码一行都不用改。
2.3 模型文件与配置的存放约定
模型的路径和关键超参数都集中在config.py,不散落在各个模块里。这个项目里我比较认可的一点是阈值默认给的是 0.6,而不是常见的 0.5,因为 ArcFace 特征在 L2 归一化后,同一个人的余弦相似度通常在 0.6 到 0.85 之间,不同人一般低于 0.3,用 0.6 作为分界线可以留出明显缓冲。
# config.py MODEL_CONFIG = { "detector_weights": "models/yolov5_face.pt", # 人脸检测权重 "arcface_weights": "models/arcface_r50.pth", # 特征提取权重 "arcface_input_size": 112, # ArcFace 标准输入尺寸 "detect_conf": 0.5, # 检测置信度阈值 "detect_iou": 0.45, # NMS 的 IoU 阈值 "verify_threshold": 0.6, # 识别阈值 "embedding_dim": 512 # 特征向量维度 }所有的路径都用相对models/目录的写法,这样项目整体拷贝到别的机器上不需要改配置。如果你下载到的权重文件名不同,改这里的字符串就行,不用动代码。
3. 环境搭建与模型准备:从裸机到跑通第一张图
这个项目的环境坑主要在 PyTorch 和 CUDA 版本匹配上。YOLOv5 对 PyTorch 版本不算挑剔,但 ArcFace 的推理代码如果涉及到旧版load_state_dict,在 PyTorch 2.x 下偶尔会报missing keys警告。先用 CPU 版本把链路跑通,再考虑 GPU 加速,是最省心的路径。
3.1 依赖安装与版本约束
先创建虚拟环境,然后安装依赖。requirements.txt里的核心项大概是这样的:
# requirements.txt 核心依赖 torch>=1.10.0 torchvision>=0.11.0 numpy>=1.21.0 opencv-python>=4.5.0 scikit-image>=0.18.0 tqdm>=4.62.0 pyyaml>=5.4.0安装时注意一点:如果你是在 CPU 环境跑,不要直接pip install torch,那样会拖下来好几个 GB 的 CUDA 依赖。我一般会根据平台选择对应的 CPU 版本安装命令,这样安装体积小,启动也快。GPU 机器则直接安装对应 CUDA 版本的 PyTorch,然后用torch.cuda.is_available()验证一下是否真的能用。
3.2 模型权重文件放对位置
这个项目本身不带模型权重,需要你自己下载后放进models/目录。检测权重建议使用带五关键点输出的 YOLOv5 人脸变体,这样省掉一个单独的关键点模型。ArcFace 权重则选择输出 512 维特征的预训练模型即可。
放好后先验证权重能不能正常加载:
# test_load_weights.py import torch # 检测模型加载验证 detector_weights = "models/yolov5_face.pt" model = torch.hub.load("./yolov5", "custom", path=detector_weights, source="local", force_reload=False) print("detector loaded:", model is not None) # ArcFace权重加载验证 arcface_weights = "models/arcface_r50.pth" state_dict = torch.load(arcface_weights, map_location="cpu") print("arcface keys:", len(state_dict.keys()))第一次加载 YOLOv5 时,torch.hub.load需要本地有一个yolov5仓库目录。如果加载报FileNotFoundError,多半是仓库没准备好,先把官方 YOLOv5 仓库克隆到项目根目录,项目通常默认这个路径。
3.3 用一张照片验证最小链路
环境就绪后,用一张单人正面照片做端到端验证。test_minimal.py的逻辑是:读取图片 → 检测 → 第一张图注册到底库 → 第二张图验证比对 → 打印相似度。
# test_minimal.py import cv2 from detector.yolo_face_detector import FaceDetector from align.face_align import FaceAligner from recognition.arcface_feature import ArcFaceExtractor from recognition.compare import compare_embeddings detector = FaceDetector() aligner = FaceAligner() extractor = ArcFaceExtractor() img1 = cv2.imread("data/person1_1.jpg") img2 = cv2.imread("data/person1_2.jpg") box1, landmarks1 = detector.detect(img1)[0] aligned1 = aligner.align(img1, landmarks1) emb1 = extractor.extract(aligned1) box2, landmarks2 = detector.detect(img2)[0] aligned2 = aligner.align(img2, landmarks2) emb2 = extractor.extract(aligned2) score = compare_embeddings(emb1, emb2) print(f"similarity: {score:.4f}") print("verified:", score >= 0.6)如果两张图是同一个人的不同照片,相似度一般能到 0.65 以上。如果低于 0.5,优先怀疑对齐出了问题,而不是模型权重问题。
4. 核心代码逐段拆解:注册、验证、1:N检索的完整链路
这个项目的核心代码集中在四个文件里:检测封装、对齐模块、特征提取、比对计算。下面按数据流顺序逐段走读。
4.1 带关键点输出的 YOLOv5 检测封装
检测模块把 YOLOv5 模型封装成统一的detect接口,返回人脸框和关键点:
# detector/yolo_face_detector.py import cv2 import numpy as np import torch class FaceDetector: def __init__(self, weights_path="models/yolov5_face.pt", conf_thres=0.5, iou_thres=0.45): self.model = torch.hub.load( "./yolov5", "custom", path=weights_path, source="local", force_reload=False ) self.model.conf = conf_thres # 控制误检与漏检的平衡 self.model.iou = iou_thres # NMS重叠度,多脸场景调低 self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") self.model.to(self.device) self.model.eval() def detect(self, img_bgr): results = self.model(img_bgr) # 结果中包含 [x1, y1, x2, y2, conf, cls] 及关键点信息 pred = results.xyxy[0].cpu().numpy() boxes, landmarks = [], [] for det in pred: if det[4] < self.model.conf: continue x1, y1, x2, y2 = int(det[0]), int(det[1]), int(det[2]), int(det[3]) boxes.append([x1, y1, x2, y2]) # 关键点以 [x,y] 形式保存在 det[5:] 的每两个维度 kpts = det[5:].reshape(-1, 2) landmarks.append(kpts) return boxes, landmarksconf_thres是最影响体验的参数:设 0.25 时人脸框很多但夹杂误检,设 0.7 以上时远距离人脸容易漏检。在考勤场景我一般推荐 0.5,配合后续识别阈值过滤,整体更稳。iou_thres控制在多人重叠场景下的框合并力度,默认 0.45 够用。
4.2 人脸对齐:112x112 标准化输入怎么来的
ArcFace 对输入图像有严格的对齐要求。模型训练时用的对齐模板是固定的五个点坐标,因此推理时也必须把检测到的关键点变换到这个标准模板上。
# align/face_align.py import cv2 import numpy as np class FaceAligner: def __init__(self, output_size=112): self.output_size = output_size # ArcFace 训练时使用的标准五点坐标 self.ref_pts = np.array([ [38.2946, 51.6963], # 左眼 [73.5318, 51.6963], # 右眼 [56.0252, 71.7366], # 鼻尖 [41.5493, 92.3655], # 左嘴角 [70.7299, 92.3655], # 右嘴角 ], dtype=np.float32) def align(self, img_bgr, landmarks): # 输入 landmarks 来自检测模型,顺序与五点模板一致 M, _ = cv2.estimateAffinePartial2D(landmarks, self.ref_pts) aligned = cv2.warpAffine(img_bgr, M, (self.output_size, self.output_size)) return alignedestimateAffinePartial2D求解的是相似变换矩阵,只包含旋转、平移和等比缩放,不会产生畸变。这里有个容易被忽视的点:landmark 顺序必须和模板顺序完全一致——眼睛、鼻子、嘴角的顺序错了,对齐后的脸会上下颠倒。
4.3 ArcFace 特征提取:从像素到 512 维向量
对齐得到 112x112 图像后,送入 ArcFace 提取特征:
# recognition/arcface_feature.py import cv2 import numpy as np import torch class ArcFaceExtractor: def __init__(self, weights_path="models/arcface_r50.pth", device=None): self.device = device or torch.device("cuda" if torch.cuda.is_available() else "cpu") self.model = self.load_model(weights_path) self.model.to(self.device) self.model.eval() # 预训练模型的标准化参数 self.mean = np.array([0.5, 0.5, 0.5], dtype=np.float32) self.std = np.array([0.5, 0.5, 0.5], dtype=np.float32) def load_model(self, weights_path): # 初始化网络结构并加载权重(不同权重对应不同网络定义) from recognition.arcface_backbone import get_model model = get_model("r50", dropout=0, fp16=False) state_dict = torch.load(weights_path, map_location="cpu") model.load_state_dict(state_dict) return model def extract(self, aligned_img): # BGR 转 RGB,缩放到 [0,1],再按均值方差归一化 img = cv2.cvtColor(aligned_img, cv2.COLOR_BGR2RGB) img = img.astype(np.float32) / 255.0 img = (img - self.mean) / self.std tensor = torch.from_numpy(img).permute(2, 0, 1).unsqueeze(0).to(self.device) with torch.no_grad(): embedding = self.model(tensor).cpu().numpy() # L2归一化:让特征向量模长为1,后续点积即余弦相似度 embedding = embedding / np.linalg.norm(embedding, axis=1, keepdims=True) return embedding[0].astype(np.float32)L2 归一化这步是比对逻辑的前提。代码里get_model("r50")需要和权重文件对应的 backbone 一致,如果权重是 MobileFaceNet,这里就要用"mobilefacenet",否则维度对不上或者加载报错。很多项目跑到这里翻车,都是 backbone 类型没对齐。
4.4 相似度计算与阈值选择:为什么点积就能比相似度
特征向量经过 L2 归一化后,两个向量的点积在数学上就等于余弦相似度,所以不需要专门调cosine_similarity函数:
# recognition/compare.py import numpy as np def compare_embeddings(emb1, emb2): # 两个向量模长都为1,点积即为余弦相似度 return float(np.dot(emb1, emb2)) def verify(emb1, emb2, threshold=0.6): score = compare_embeddings(emb1, emb2) return score >= threshold, score阈值选择要看具体场景。人脸考勤机在室内固定点位,光线稳定,0.6 合理;手机解锁这种手持设备角度变化大,一般要放宽到 0.55;做 1:N 检索时,为了压低误检,我通常把阈值提到 0.65。这属于业务调参,代码里把阈值暴露成配置项就行。
4.5 注册人脸底库与 1:N 检索入口
底库模块支持先注册若干条人脸特征,再对当前人脸做 1:N 检索。实现上用简单的 numpy 矩阵乘法完成暴力检索,人少时性能足够:
# recognition/database.py import numpy as np class FaceDatabase: def __init__(self, threshold=0.6): self.embeddings = [] # 特征向量列表 self.names = [] # 对应的姓名/ID self.threshold = threshold def register(self, name, embedding): self.names.append(name) self.embeddings.append(embedding) def search(self, embedding): # 所有特征堆成矩阵,批量点积 if not self.embeddings: return None, -1 mat = np.stack(self.embeddings) # [N, 512] scores = mat @ embedding.reshape(-1, 1) # [N, 1] max_idx = int(np.argmax(scores)) max_score = float(scores[max_idx]) if max_score >= self.threshold: return self.names[max_idx], max_score return None, max_score这个底库设计可以支撑几千人的规模。注册时需要注意:一个人最好注册 2 到 3 张不同角度的照片,因为单张正面照的泛化能力有限,换个光线角度时相似度会掉。
5. 避坑记录:人脸识别项目最常翻车的五个位置
这个项目我在跑通和改造过程中踩了不少坑,有些是环境问题,有些是算法使用方式的问题,下面是几条比较有代表性的。
5.1 检测框出一堆,识别却全部失败
现象:摄像头画面里明明只有一个人,YOLOv5 却框出六七个框,底库比对全部返回不认识。
原因:检测模型用了 COCO 数据集上预训练的原始 YOLOv5,classes没有限定,模型把背景里的水杯、海报上的人脸印刷体全部当成目标。或者用的是不带人脸类别的权重,class id 对应的是 person,而不是 face。
解决:人脸检测必须使用专门的人脸权重。如果是带关键点的 YOLOv5 人脸变体,直接默认只输出人脸类别;如果加载后检测类别是 person,就要在推理时限定类别索引:
self.model.classes = [0] # 根据训练数据的人脸类别索引设置5.2 侧脸、低头时识别率断崖式下降
现象:正脸识别 99% 准确,人侧过脸或低头看手机时直接比对失败。
原因:ArcFace 特征本身对姿态有一定容忍度,但前提是对齐模块能拿到准确的关键点。YOLOv5 的检测框在侧脸时依然能输出,但关键点精度明显下降,仿射变换强行把侧脸“掰正”反而制造了畸变。
解决:在比对前增加一个质量判断,当检测框内人脸面积或关键点置信度低于阈值时,直接丢弃该帧。同时底库注册时多录几个角度。不要指望模型对齐逆天改命,侧脸超过 60 度该拒绝就拒绝。
5.3 每帧都重新加载模型,显存涨到卡死
现象:跑实时视频时显存占用肉眼可见地往上涨,几秒后 CUDA out of memory。
原因:很多人会在视频循环里写FaceDetector()和ArcFaceExtractor()的实例化代码,而构造函数里torch.load和to(device)每次都会重新分配显存,旧的模型又没被释放。
解决:全局只实例化一次模型,循环里只用detect()和extract()。如果要在多线程里用,把模型加载放到进程启动阶段,并加一个单例装饰器:
@singleton class FaceEngine: def __init__(self): self.detector = FaceDetector() self.extractor = ArcFaceExtractor()5.4 CPU 推理速度上不去,摄像头采集卡顿
现象:笔记本 CPU 上实时视频只有 4 到 5 帧,画面肉眼可见地卡。
原因:很多人为了让检测更准,把 YOLOv5 的输入分辨率调到了 1280,同时把检测和特征提取都放在主线程里同步执行,整个循环被拖慢。
解决:检测分辨率保持 640 就够。把特征提取放到独立线程,用队列传递对齐后的人脸图;或者帧率优先时每 3 帧才做一次检测,中间帧用上一次的检测结果。实际体验会比每帧强算流畅得多。
5.5 阈值怎么调,误检率都压不下去
现象:设置 0.6 时同一个人偶尔被拒,调到 0.55 时陌生人又频繁通过,怎么都找不到平衡点。
原因:阈值只是最后一道防线,真正影响误检的是底库质量。如果底库里某个人注册的照片模糊、带墨镜,它的特征本身就和常态人脸差别大,阈值怎么调都救不回来。
解决:注册时对输入图像做清晰度检查,分辨率低于某个阈值直接拒绝注册。底库里每个身份的照片尽量保证光照均匀、五官无遮挡。在这个基础上再调阈值,会发现 0.6 附近有很干净的分界线。
6. 二次开发思路:从人脸验证工具到一个完整的人脸服务
把这个项目跑通后,我做的第一件事是把 1:1 验证的代码扩成独立服务,第二步是给系统加活体检测。这里记录一下改造思路,供你参考。
6.1 先加 HTTP 接口,对外提供能力
我用 Flask 包了一层简单服务,接口只做两件事:注册和验证。请求带图片,返回相似度与是否通过。这样前端摄像头采集和后端识别解耦,也方便把识别服务部署到单独的机器上。
6.2 活体检测防照片攻击
这是从 demo 到落地最重要的一步。在原有链路的检测和对齐之间加入一个活体判别环节,常见做法是眨眼的时序检测或 rPPG 心率信号分析。项目里我选择了基于关键点时序的眨眼检测,因为关键点已经有了,只需要连续几帧对比眼部的纵横比变化,实现成本最低。
6.3 性能优化:检测与识别分开部署
当并发量上来之后,检测和识别放同一个进程的瓶颈明显。我把检测服务部署在靠近摄像头的边缘节点,识别服务单独拉出来,通过内部接口传对齐后的 112x112 图像。这样识别服务的输入数据量极小,可以横向扩展。特征底库也顺势从 numpy 数组换成了支持增量检索的向量索引库,维护起来省心不少。
这套改造走完之后,我最大的教训是:不要等系统上线了才去考虑阈值和质量问题,这些东西在设计底库结构时就要定下来。从那以后,我每次接手人脸识别项目都强制先跑一遍最小链路、再检查底库注册质量,最后才调参和部署。希望帮到你。
本文还有配套的精品资源,点击获取