简介:一套面向深度学习与计算机视觉开发者的完整人脸识别项目,基于YOLOv5与ArcFace构建,覆盖从人脸检测到身份识别的全流程,可应用于监控、门禁、考勤等实际场景。压缩包共51个文件,以Python源码(26个py)、模型权重(2个pth)、YAML配置(7个yaml)为核心,同时包含测试图片、shell启动脚本及说明文档,整体仅3.41MB,目录结构清晰,适合快速导入与二次开发。项目已吸引1510人学习下载,尤其适合具有Python基础、正在研究检测与识别任务的中高级开发者作为参考和起点。资源不仅提供训练与推理代码、数据预处理逻辑、模型评估与部署要点,还内置活体检测模块,可帮助读者深入理解YOLOv5的网格检测机制与ArcFace的角度余量损失,并在实际项目中直接复用其数据增强、模型优化和推理脚本,从而缩短从论文到落地的距离。
1. 人脸识别项目先别急着上模型:先搞清检测与识别是两件事
做门禁、考勤或者安防项目时,最容易让人脸识别项目翻车的点,不是模型选得不够新,而是把"人脸检测"和"人脸识别"混成了一件事。YOLOv5负责在画面里把人脸框出来,ArcFace负责把框出来的脸变成一串可以比对的向量,两段各干各的,缺一环整个链路就断掉。很多第一次搭这套方案的开发者会直接拿检测模型的输出去做余弦相似度比对,结果发现同一个人的分数还没不同人的分数高,整套系统形同虚设。这篇文章沿着"检测出脸→对齐人脸→提取特征→比对身份"这条主线展开,把每一段用什么模型、怎么训练/加载、参数怎么定、哪些地方最容易出玄学问题讲透。适合已经跑通YOLOv5基础检测、想往识别方向迈一步的开发者,也适合正在评估"检测+识别"两段式方案值不值得投入的从业者。读完你能照着命令把整条pipeline搭起来,并且知道出问题时该去看哪一段。
2. 整体架构与选型:为什么是YOLOv5加ArcFace而不是端到端方案
人脸识别项目在工程上极少用一个模型同时完成"定位+识别",主流做法是检测与识别分开的两段式pipeline。这一章先把架构讲清楚,再回答一个几乎所有评估过这个方向的人都会问的问题:为什么选YOLOv5做检测、ArcFace做识别,而不是用一个号称端到端的模型省事。
2.1 两段式pipeline的三层结构
整套系统从上到下分三层:视频/图像输入层、检测对齐层、特征比对层。输入层负责从摄像头拉流、解码、缩放,输出给检测模块;检测模块用YOLOv5找脸框,再用关键点做仿射变换把脸对齐;特征比对层把对齐后的脸送入ArcFace模型产出512维向量,和注册库里的向量做相似度计算,最后根据阈值给出身份结论。
为什么强调分层?因为每一层替换成本不同。检测模型想换更轻量的或者更准的,只动中间一段;ArcFace想从resnet50换mobilefacenet,也只动特征提取那一段。各层之间通过"脸框坐标+对齐后的人脸图"解耦,接口稳定,调试时可以逐段验证。
# pipeline骨架:展示三层结构如何衔接 import cv2 import numpy as np def pipeline(frame, detector, aligner, encoder, threshold=0.6): # 第一层:检测 boxes, confs = detector.detect(frame) # 返回人脸框和置信度 results = [] for box, conf in zip(boxes, confs): if conf < 0.5: continue # 第二层:对齐(关键点+仿射变换) face_aligned = aligner.align(frame, box) # 输出112x112对齐图 # 第三层:特征提取与比对 embedding = encoder.embedding(face_aligned) # 512维向量 results.append((box, embedding)) return results这段代码是整个项目的主干骨架。detect、align、embedding三个环节各自独立,任何一环出问题都可以单独替换、单独测试。实际项目中我不建议把这三步写进一个大函数里,而是拆成模块,方便后续在摄像头流里逐帧调试。
2.2 检测选型:YOLOv5的准确率与工程生态优势
YOLOv5在检测这个环节的优势不是某个指标碾压别人,而是工程生态太成熟。训练脚本、数据格式、导出onnx/tensorrt的流程全都现成,遇到问题搜一下就能找到同类案例,这在实际落地时比模型涨0.5个点重要得多。人脸检测本质上是单类别目标检测,YOLOv5训练时把类别数设为1即可,widerface这类公开人脸数据集可以直接用来训练。
选用YOLOv5s还是YOLOv5m要看部署环境。CPU推理选s,GPU或者边缘盒子选m,追求极限速度可以用n。很多开发者一上来就选最大的x模型,结果帧率上不去,回头怪模型不行——这其实是选型问题。人脸检测对框的精度要求没有通用检测那么苛刻,因为后续对齐和识别对框的小幅偏移有一定容忍度,但框不能丢、不能把背景当成人脸。
2.3 识别选型:ArcFace的margin设计为什么能拉开同类与异类的距离
ArcFace的核心创新在损失函数——在softmax之前给特征向量和权重向量之间的夹角加上一个固定的margin,让网络在训练时被迫学到类间更分散、类内更紧凑的特征分布。这个设计直接解决了人脸识别最容易遇到的"不同人长得像"的问题,在公开测试集上比传统softmax损失提升明显。
实际项目里ArcFace的常见实现是配合insightface仓库的训练流程,模型结构以resnet50和mobilefacenet为主。resnet50精度高但参数量大,mobilefacenet速度飞快适合边缘部署。需要理解一个关键点:ArcFace输出的512维向量必须做L2归一化后再计算余弦相似度,否则分数分布会乱掉,阈值也没法统一。这一条经常被忽略,很多人直接拿原始embedding算内积,导致同一个人在不同光线下的分数忽高忽低。
3. 检测侧落地:用YOLOv5把每一张脸先找出来
检测是整个人脸识别项目的地基,地基没打牢,后面所有环节都在处理残缺或错误的数据。这一章从数据准备讲到训练命令,再讲到推理时如何把检测框转成对齐所需的坐标,每一步都给出可直接照抄的命令。
3.1 检测模型训练:数据、标注与训练命令
训练YOLOv5检测脸,数据标注只需要一个类别,命名为face即可。标注工具常见做法是用labelImg或者labelme导出yolo格式的txt,每行是"类别 中心x 中心y 宽 高",坐标值归一化到0-1之间。如果是从widerface数据集转过来,注意它的标注格式是"x1 y1 w h"而且用的是绝对坐标,写脚本转一次就行。
# 训练YOLOv5人脸检测模型的标准命令 python train.py \ --data face.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --project runs/face_train这里几个参数值得细说。img设为640是速度和精度的平衡点,人脸检测不需要像文本检测那么高的分辨率,但太低会漏掉小脸。batch大小按显存调整,12G显存跑s模型16没问题。epochs 100对人脸单类别足够,再多容易过拟合到训练集的姿态分布上,导致侧脸大量漏检。
face.yaml文件里要写清楚数据集路径和类别数,我一般这样配置:
path: ./datasets/face_dataset train: images/train val: images/val nc: 1 names: ['face']训练完成后,验证集上的mAP50一般能到95以上,但这只是第一步。真正衡量检测好坏要看在摄像头画面里的小脸、侧脸、逆光脸的召回率,这些指标mAP反映不出来。我会额外跑一段视频,把每一帧的检测结果画出来逐个看,确认没有系统性漏检再往下一步走。
3.2 推理服务化:把检测结果转成对齐坐标
检测模型输出的是xywh格式的框,而后续对齐需要的是人脸关键点或者至少是稳定的框坐标。YOLOv5本身不输出关键点,所以常见做法是检测后接一个关键点模型,或者用一个轻量级的关键点检测网络(如某个5点landmark模型)在框内再跑一次。5个关键点分别对应左眼、右眼、鼻尖、左嘴角、右嘴角,对齐时利用这5点做仿射变换。
# 用onnx加载YOLOv5检测模型并解析输出 import onnxruntime as ort import cv2 import numpy as np session = ort.InferenceSession("face_det.onnx", providers=["CUDAExecutionProvider"]) input_name = session.get_inputs()[0].name # 输入为640x640归一化后的图像 blob = cv2.dnn.blobFromImage(img_resized, 1/255.0, (640, 640), swapRB=True) outputs = session.run(None, {input_name: blob}) # 输出形状: [1, 25200, 6],最后维度为 cx,cy,w,h,obj_conf,cls_conf preds = outputs[0][0] boxes = preds[preds[:, 4] > 0.5][:, :4]这段代码的关键在于对YOLOv5输出的理解。25200是640x640输入下三个尺度特征图的锚框总数,每个锚框预测6个值。筛选时先看目标置信度obj_conf,再决定是否保留。导出onnx时记得把nms放在模型外做,否则在python里循环做nms会很慢。实际项目里我会把检测和后续的信息在一个线程里串行处理,保证每帧延迟可控。
检测框稳定之后,还有一个人眼可见但容易被忽视的问题:框会随着人脸细微移动而抖动。框抖直接导致对齐后的图不稳定,进而让ArcFace提取的特征向量在边界处跳变。解决方案不是调模型,而是在框坐标上做时间平滑,常见做法是EMA(指数移动平均)。
4. 识别侧落地:用ArcFace把脸变成可比较的向量
检测把脸框出来,接下来就到了ArcFace的主场。这一章讲清楚ArcFace模型怎么加载、输入输出是什么、预处理有什么讲究、比对逻辑怎么写,以及最终阈值怎么定才靠谱。
4.1 特征提取:onnxruntime加载ArcFace模型
ArcFace模型训练好之后导出为onnx是工程落地最稳的方式。常见做法是用insightface仓库里的模型结构,训练完成后用torch.onnx.export导出。输入是112x112的RGB对齐人脸图,输出是512维的embedding向量。加载和推理的代码非常简洁:
import onnxruntime as ort import cv2 import numpy as np session = ort.InferenceSession("arcface.onnx", providers=["CUDAExecutionProvider"]) input_name = session.get_inputs()[0].name def get_embedding(face_img): # face_img 已对齐且resize到112x112 img = cv2.cvtColor(face_img, cv2.COLOR_BGR2RGB) img = img.astype(np.float32) / 255.0 # 归一化到[-1,1]是ArcFace的标准做法 img = (img - 0.5) / 0.5 blob = np.transpose(img, (2, 0, 1))[None, ...] emb = session.run(None, {input_name: blob})[0][0] # L2归一化,让所有向量长度一致 emb = emb / np.linalg.norm(emb) return emb这段代码里有三个细节直接影响识别效果。一是颜色通道必须用RGB,用BGR直接喂进去等于让模型在"颠倒"的颜色空间里工作,特征会乱掉。二是归一化到[-1,1]还是[0,1],要看你用的模型训练时是怎么预处理的,insightface的标准流程是归一化到[-1,1]。三是L2归一化这步不能省,否则向量模长不一样,余弦相似度计算的实际上是带权重的内积,分数分布会很乱。
4.2 比对逻辑:从向量到相似度再到阈值
有了embedding,比对逻辑就简单了。注册时把每个人的人脸图过一遍ArcFace,得到512维向量存进数据库;识别时把当前帧的人脸向量和库里所有向量做余弦相似度,取最大值和阈值比大小。
def cosine_similarity(emb1, emb2): return float(np.dot(emb1, emb2)) def identify(emb, gallery, threshold=0.6): """gallery: List[(person_id, embedding)]""" best_id = None best_score = -1.0 for pid, gemb in gallery: score = cosine_similarity(emb, gemb) if score > best_score: best_score = score best_id = pid if best_score < threshold: return None, best_score # 未识别 return best_id, best_score阈值0.6不是拍脑袋定的,而是要根据实际场景里的误识率和拒识率标定。门禁场景要求误识率低,阈值往上调;考勤场景要求少漏签,阈值往下调。正确做法是找一个包含20人以上、每人多张不同光线/角度照片的验证集,算不同阈值下的误识和拒识曲线,再取平衡点。这一步偷懒的话,后面上线被投诉"认错人"或者"不认人"就是必然结果。
另外一个容易被忽略的点:一个ID最好注册多张人脸。光照、表情、妆容的变化会让同一个人在不同帧的embedding存在偏移,单张注册在这种偏移下很容易跌破阈值。我一般每个ID注册3到5张,取这些embedding的均值作为该ID的代表向量,能显著提升稳定性。算均值之前所有向量都已经L2归一化了,所以直接逐元素平均后再次归一化即可。
5. 避坑专题:检测与识别联调中的五个常见坑
检测和识别单独跑都没问题,一联调就出玄学问题,这是人脸识别项目最磨人的阶段。这一章把我在实际联调中遇到过的高频问题按"现象→原因→解决"写出来,每一条都可以直接对号入座。
5.1 检测框抖动导致识别分数忽高忽低
现象是同一个站在摄像头前不动,识别分数在0.55到0.68之间来回跳,偶尔还会跌破阈值导致识别失败。原因在于检测框每一帧都有几个像素的抖动,对齐后的112x112图也跟着抖,ArcFace提取的特征对细微位移很敏感。解决方法是做框的EMA时间平滑,同时对对齐图上做轻微的高斯模糊抑制高频噪声。注意平滑系数不能太大,否则人快速移动时候框会拖尾,反而引入更严重的错位。
5.2 没做对齐就提取特征,识别率直接掉一截
现象是训练时验证集准确率99%,一到摄像头画面就掉到70%。原因是对齐步骤被跳过了——直接拿检测框裁剪后resize到112x112就送进ArcFace,人脸角度一变,特征就和注册时对不上。解决方法是必须用5点关键点做仿射变换:检测框给出大致位置,关键点给出精确的眼睛/鼻子/嘴角坐标,变换到标准位置后再裁剪。这个步骤是整个人脸识别pipeline里性价比最高的一个环节,实现了它,识别率能提升10个点以上。
5.3 阈值拍脑袋设0.6,误识率失控
现象是系统频繁把A识别成B,或者反过来谁也不认。原因在于阈值没有经过数据标定,0.6在某些场景下太松(比如员工食堂人流密集、光线杂),在另一些场景下太紧(比如仓库这类光线暗的场景)。解决方法是准备一个包含真实摄像头画面的验证集,画出一条"误识率-拒识率"曲线再选阈值。每换一个摄像头位置,光照变了,这条曲线都可能要重新看一遍。
5.4 注册库越用越大,检索越来越慢
现象是刚开始50人实时比对毫无压力,加到500人时每帧延迟从3ms涨到200ms。原因是对比逻辑还是最原始的线性扫描,每来一帧都要和库里所有向量算一遍余弦相似度。500人还好,5000人就是灾难。解决方法是把向量索引换成faiss的IndexFlatIP,它用批量矩阵乘法实现暴力检索,速度比Python循环快一两个数量级。后续人再多,可以换成IVF或者HNSW这类近似索引。
5.5 逆光、侧脸场景下检测漏检
现象是背对窗户站的人脸框直接消失,侧脸角度超过60度也框不出来。原因有两个:训练数据里正脸占比太高,模型对极端姿态和强光比的鲁棒性不够;以及预处理时没有做数据增强,特别是随机亮度扰动和随机旋转。解决方法是训练时把hsv增强的hue参数调低、value增强调高,让模型见过更多"暗脸";再在数据集中混入20%左右的侧脸和低头脸数据,而不是全部用正脸。
6. 进阶:用faiss建索引把秒级比对压到毫秒级
注册库超过几百人之后,线性扫描的比对耗时开始影响实时性。这一章给出一个可复用的faiss接入方案,覆盖建索引、查询、动态增删三类高频操作,并顺手解决一个容易踩的坑:向量类型必须统一为float32。
import faiss import numpy as np class FaceGallery: def __init__(self, dim=512): self.index = faiss.IndexFlatIP(dim) # 内积索引需要向量已归一化 self.ids = [] # 维护外部id到索引位置的映射 def add(self, pid, embedding): vec = embedding.astype(np.float32)[None, :] self.index.add(vec) self.ids.append(pid) def query(self, embedding, topk=1): vec = embedding.astype(np.float32)[None, :] scores, idx = self.index.search(vec, topk) if topk == 1: return self.ids[idx[0][0]], float(scores[0][0]) return [(self.ids[i], float(s)) for i, s in zip(idx[0], scores[0])]使用IndexFlatIP的前提是所有放入索引的向量都已经L2归一化,这样内积就等于余弦相似度。查询时返回的是索引内部位置,必须通过ids列表映射回真实的人员编号。注意动态删除需要用到faiss的remove_ids接口,并且要配合IDMap,直接重建索引反而是更简单的方案。
实际部署时,我习惯把注册库的索引整体加载到内存里,每来一个新注册用户就add一次,每识别一帧就search一次。在普通服务器上,IndexFlatIP做一万个向量的暴力检索耗时在10ms量级,完全满足实时视频流的要求。如果想进一步压缩耗时,可以换IVF索引,但要注意IVF是近似检索,有概率漏掉真正的最近邻。对人脸识别这种对误识率敏感的场景,建议先暴力索引,确认误差范围后再考虑近似索引。
最后说一个我在这个项目上栽过的跟头:把ArcFace输出的向量直接用float16存进数据库,检索时又转回float32,结果精度变化导致分数全部偏了零点零几,阈值正好卡在偏差点上,上线当天误报频发。后来统一改成全程float32才稳定下来。这类问题在联调阶段很难发现,往往要到压力测试或者换一个光照条件才能暴露。所以我的习惯是:向量类型、归一化方式、预处理顺序这些细节,从第一天就写进接口文档里,谁都不许临时改。希望这篇笔记能帮你把这条pipeline一次搭稳,少走几个我走过的弯路。
本文还有配套的精品资源,点击获取