YOLOv5+ArcFace端到端人脸识别流水线 简介本资源是一个基于YOLOv5与ArcFace双模型协同的人脸识别完整项目实现面向深度学习初学者、计算机视觉开发者及AI安防应用实践者解决端到端人脸检测身份识别的一体化建模问题适用于门禁系统、考勤管理、监控分析等实际场景。压缩包共51个文件含26个核心Python脚本如main.py、train.py、predict_net.py、7个配置类YAML/Cfg文件定义网络结构与训练参数、4张示例人脸图像、2个预训练权重.pth模型以及README.md项目说明和shell部署脚本整体仅3.41MB轻量易部署。已有1512人学习下载资源结构清晰分层yoloV5_face模块负责高精度人脸定位arc_face模块实现特征提取与余弦相似度比对silentFace_model支持活体检测扩展配套utils、data、weights等目录形成开箱即用的工程闭环提供从数据准备、训练调优到推理测试的全流程可复现代码。1. 人脸检测特征提取一体化YOLOv5 负责“找脸”ArcFace 负责“认人”不接摄像头也能跑通的端到端识别流水线你有没有试过明明模型训练好了一跑 demo 就卡在cv2.VideoCapture(0)或者好不容易调通检测却发现人脸对齐后特征向量全为 NaN这个项目不是又一个“论文复现玩具”而是一套从图像输入、人脸定位、关键点校正、归一化裁剪到 ArcFace 特征提取与余弦相似度比对的完整闭环。它用 YOLOv5s 替代传统 MTCNN 做检测快 3 倍显存占用低 40%用轻量级仿射变换替代 OpenCVgetRotationMatrix2D实现毫秒级对齐所有模块都封装成FacePipeline类——传一张图进去直接返回[{bbox: [x1,y1,x2,y2], landmarks: [5x2], embedding: [512]}的结构化结果。适合刚学完 PyTorch 想落地人脸识别的新手也适合需要快速集成到边缘设备Jetson Nano / RK3588的嵌入式开发者。项目不依赖任何在线服务或闭源 SDK所有权重文件YOLOv5s-face.pt arcface_r100.pth均内置在weights/下解压即跑连requirements.txt都按 CUDA 11.3 / 11.7 / CPU 三档做了隔离。2. 环境搭建与最小可运行验证5 分钟内跑通单图识别绕过 pip install 的玄学依赖地狱2.1 精确匹配 CUDA 版本为什么 conda 创建环境比 pip install 更稳这个项目对 CUDA 版本极其敏感。YOLOv5 的torchvision依赖必须与torch严格对齐而 ArcFace 的insightface子模块又要求onnxruntime-gpu1.14——但 pip 安装时经常自动降级到 1.10导致ort_session.run()报InvalidArgument: Input node input has incompatible shape。我踩坑后固定方案是# 创建干净环境以 CUDA 11.7 为例 conda create -n face-y5a python3.9 conda activate face-y5a conda install pytorch1.13.1 torchvision0.14.1 pytorchaudio0.13.1 -c pytorch -c conda-forge pip install onnxruntime-gpu1.16.3 # 强制指定避免 pip 自动选错 pip install opencv-python4.8.1.78 numpy1.23.5 tqdm4.66.1提示如果你用的是 CUDA 11.3请把pytorch1.12.1和torchvision0.13.1CPU 用户直接pip install onnxruntime即可无需-gpu后缀。2.2 权重文件校验与路径规范weights/目录下必须存在的 3 个文件项目默认从weights/加载模型但原始下载包常漏掉arcface_r100.pth或yolov5s-face.pt。请手动确认该目录下有且仅有以下 3 个文件大小需吻合文件名大小约用途yolov5s-face.pt14.2 MBYOLOv5s 改进版输出带 5 个关键点左眼、右眼、鼻尖、左嘴角、右嘴角arcface_r100.pth178 MBResNet-100 主干的 ArcFace 模型输入 112×112 RGB 图输出 512 维特征向量face_template.jpg124 KB用于计算仿射变换的参考模板112×112五点坐标已预存若缺失arcface_r100.pth常见错误是RuntimeError: Error(s) in loading state_dict for ResNet:Missing key(s) in state_dict—— 这说明模型结构和权重不匹配不是代码写错了是文件根本没放对位置。2.3 单图推理脚本demo_single.py3 行代码验证全流程不要急着跑train.py或webcam.py。先用最简脚本确认 pipeline 是否打通# demo_single.py from face_pipeline import FacePipeline import cv2 pipeline FacePipeline( det_weightweights/yolov5s-face.pt, rec_weightweights/arcface_r100.pth, devicecuda # 或 cpu ) img cv2.imread(test_images/elon_musk.jpg) # 任意 JPG/PNG results pipeline.run(img) # 核心入口返回 list of dict print(f检测到 {len(results)} 张人脸) if results: print(f第一个人脸 embedding 形状: {results[0][embedding].shape}) # 应为 torch.Size([512])运行后若输出检测到 1 张人脸和torch.Size([512])说明检测对齐编码三步全部走通。此时再看results[0]的bbox坐标是否合理比如[120, 85, 280, 290]就能排除 90% 的预处理 bug。2.4 验证输出可视化用draw_results()快速定位对齐失败点光看数字不够直观。face_pipeline.py内置draw_results()函数能叠加 bbox、关键点、特征相似度标签# 接续上段代码 from utils.visualize import draw_results vis_img draw_results(img, results, conf_thres0.5) cv2.imwrite(output/vis_elon.jpg, vis_img)生成的图片中如果关键点红点明显偏离人脸五官比如鼻子点落在额头说明face_template.jpg的五点坐标与模型输出的关键点顺序不一致 —— 这是 ArcFace 训练时约定的顺序左眼中心、右眼中心、鼻尖、左嘴角、右嘴角必须严格对应。我们已在utils/align.py中硬编码了该顺序若你替换了其他检测模型务必检查landmarks输出是否为(5, 2)且顺序一致。3. 核心模块拆解YOLOv5-face 检测头改造 ArcFace 特征编码器封装3.1 YOLOv5-face 的 detection head如何让原生 YOLO 输出 5 个关键点标准 YOLOv5 只输出[x, y, w, h, conf]但人脸任务需要额外 5 个关键点坐标。本项目采用anchor-free 关键点回归分支方案在models/yolo.py的Detect类中新增self.m_kp层# models/yolo.py 第 127 行附近 class Detect(nn.Module): def __init__(self, nc80, anchors(), ch(), inplaceTrue): # ch: [256, 512, 1024] super().__init__() self.nc nc self.no nc 5 # 原始输出维度5 (xywhconf) nc self.no_kp 5 * 2 # 新增5 个点 × 2 坐标 10 维 self.nl len(anchors) # number of detection layers self.na len(anchors[0]) // 2 # number of anchors self.grid [torch.zeros(1)] * self.nl self.anchor_grid [torch.zeros(1)] * self.nl self.stride torch.tensor([8., 16., 32.]) # 原始检测分支不变 self.m nn.ModuleList(nn.Conv2d(x, self.no * self.na, 1) for x in ch) # 新增关键点回归分支只在最后一层 P5 上预测因高分辨率易误检 self.m_kp nn.Conv2d(ch[-1], self.no_kp * self.na, 1) # ch[-1] 1024 → 输出 10 维关键改动在forward()中原始pred self.m[i](x[i])输出(bs, na*(5nc), h, w)新增pred_kp self.m_kp(x[-1])输出(bs, na*10, h, w)仅作用于 P5 特征图最终将pred_kpreshape 为(bs, na, 10, h, w)再通过sigmoid归一化到[0,1]乘以stride[i]得到绝对坐标参数说明no_kp10是硬编码值不可修改ch[-1]必须等于 backbone 输出的通道数YOLOv5s 为 1024self.m_kp不参与 NMS只用于后续对齐因此不设conf分支。3.2 人脸对齐模块align_face()不用 dlib纯 PyTorch 实现的亚像素级仿射变换传统方案用cv2.estimateAffinePartial2D()但 OpenCV 版本差异会导致landmarks输入格式报错如(5,2)vs(2,5)。本项目改用torch.nn.functional.affine_gridgrid_sample全程 tensor 运算支持 GPU 加速# utils/align.py 第 43 行 def align_face(img_tensor, landmarks, template_ptsNone, size(112, 112)): img_tensor: (C, H, W) float32 tensor, range [0,1] landmarks: (5, 2) tensor, 像素坐标 template_pts: (5, 2) tensor, 模板五点默认 face_template.jpg 的标注 if template_pts is None: template_pts torch.tensor([ [30.2946, 51.6963], # 左眼 [65.5318, 51.5014], # 右眼 [48.0252, 71.7366], # 鼻尖 [33.5493, 92.3671], # 左嘴角 [62.7299, 92.2041] # 右嘴角 ], dtypetorch.float32) # 计算仿射变换矩阵最小二乘求解 A torch.cat([landmarks, torch.ones(5, 1)], dim1) # (5,3) sol, _ torch.lstsq(template_pts, A) # 解 Ax b得变换矩阵 M sol[:2].T # (2,3) - (2,3) # 构造 grid (H, W, 2) grid F.affine_grid(M.unsqueeze(0), (1, 3, size[1], size[0]), align_cornersFalse) aligned F.grid_sample(img_tensor.unsqueeze(0), grid, align_cornersFalse) return aligned.squeeze(0) # (C, H, W)注意template_pts是模板图上的五点坐标单位为像素landmarks是检测模型输出的原始坐标单位也是像素F.affine_grid要求M为(2,3)所以sol[:2].T是关键。若对齐后图像扭曲大概率是landmarks顺序错或template_pts坐标未归一化。3.3 ArcFace 编码器ArcFaceEncoder冻结 BN 层 半精度推理提速 40%ArcFace 模型在models/arcface.py中定义为ResNet100但直接加载arcface_r100.pth会触发BatchNorm的running_mean更新导致多图 batch 推理时特征漂移。我们在__init__中强制冻结# models/arcface.py 第 89 行 class ArcFaceEncoder(nn.Module): def __init__(self, weight_path): super().__init__() self.backbone ResNet100() self.backbone.load_state_dict(torch.load(weight_path, map_locationcpu)) # 冻结所有 BatchNorm 层关键否则 eval() 时 running_mean 仍会变 for m in self.backbone.modules(): if isinstance(m, nn.BatchNorm2d): m.eval() # 设为 eval 模式 m.weight.requires_grad False m.bias.requires_grad False def forward(self, x): # x: (N, 3, 112, 112), range [0,1] x x * 2.0 - 1.0 # 归一化到 [-1,1]ArcFace 训练时的输入规范 feat self.backbone(x) return F.normalize(feat, p2, dim1) # L2 归一化输出单位向量参数说明x * 2.0 - 1.0是必须步骤ArcFace 训练时用transforms.Normalize(mean[0.5,0.5,0.5], std[0.5,0.5,0.5])F.normalize确保输出向量模长为 1后续余弦相似度直接torch.mm(embed_a, embed_b.T)即可。4. 常见问题排查5 条血泪经验总结专治「能跑但不准」的黑匣子4.1 现象检测框准确但关键点全部偏移 20 像素以上原因YOLOv5-face 的关键点回归分支未正确加载权重self.m_kp层参数仍是随机初始化。检查models/yolo.py中Detect.__init__()是否漏掉self.m_kp ...或load_state_dict()时未包含该 key。解决在face_pipeline.py初始化时加断点print(m_kp weight sum:, model.model[-1].m_kp.weight.sum().item()) # 应为非零值若为0.0说明权重未加载需确认.pt文件是否为本项目定制版含m_kpkey而非通用 YOLOv5 权重。4.2 现象align_face()报RuntimeError: expected scalar type Float but found Half原因模型启用torch.cuda.amp.autocast()但align_face()中torch.lstsq不支持 half 精度。解决在调用前强制转 floatlandmarks landmarks.float() # 在 align_face() 开头加 template_pts template_pts.float()4.3 现象ArcFace 输出 embedding 全为nan原因输入图像未归一化到[-1,1]或x值超出范围如cv2.imread读取后未除 255。解决在ArcFaceEncoder.forward()前加校验assert x.min() 0.0 and x.max() 1.0, fInput range error: [{x.min():.3f}, {x.max():.3f}] x x * 2.0 - 1.04.4 现象余弦相似度恒为 0.999所有人脸都判为同一人原因F.normalize()未生效或embedding未 detach。GPU tensor 直接参与 numpy 计算会出错。解决确保特征向量已转 cpu detach numpyemb model(img).detach().cpu().numpy() # 必须 detach emb emb / np.linalg.norm(emb, axis1, keepdimsTrue) # 再次归一化防御性4.5 现象webcam.py卡顿严重FPS 3原因OpenCV 默认使用CAP_ANY后端在某些 Linux 系统上性能极差。解决强制指定后端cap cv2.VideoCapture(0, cv2.CAP_V4L2) # Linux # cap cv2.VideoCapture(0, cv2.CAP_DSHOW) # Windows cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)5. 二次开发实战从单图识别到动态注册库3 步实现「刷脸登录」原型5.1 构建人脸注册库用register_person()批量录入自动生成 ID 映射项目不预设数据库而是用内存字典self.register_db {}存储{person_id: {name: str, embeddings: List[np.ndarray]}}。核心是register_person()方法# face_pipeline.py 第 215 行 def register_person(self, person_name: str, img_paths: List[str], min_samples3, threshold0.3): person_name: 注册人姓名唯一标识 img_paths: 至少 3 张不同角度的人脸图路径 min_samples: 同一人至少采集 3 个 embedding 取均值 threshold: 过滤低质量检测IoU 0.3 则丢弃 embeddings [] for p in img_paths: img cv2.imread(p) if img is None: continue results self.detector.run(img) # 仅用检测对齐不走 ArcFace for r in results: if r[conf] threshold: continue # 对齐并编码 aligned self.aligner.align_face( torch.from_numpy(img).permute(2,0,1).float() / 255.0, torch.tensor(r[landmarks]) ) emb self.encoder(aligned.unsqueeze(0)).squeeze(0).detach().cpu().numpy() embeddings.append(emb) if len(embeddings) min_samples: raise ValueError(f{person_name} only has {len(embeddings)} valid samples ( {min_samples})) # 取均值作为该人的注册向量 avg_emb np.mean(np.stack(embeddings), axis0) avg_emb avg_emb / np.linalg.norm(avg_emb) # 再次归一化 self.register_db[person_name] { name: person_name, embedding: avg_emb, registered_at: time.time() } print(f✅ Registered {person_name} with {len(embeddings)} samples)使用示例pipeline.register_person( zhang_san, [reg/zs_1.jpg, reg/zs_2.jpg, reg/zs_3.jpg] )5.2 实时比对逻辑match_person()动态阈值 多帧投票防抖单纯一帧匹配易受光照、姿态影响。我们实现「连续 3 帧命中同一人且平均相似度 0.65」才触发登录# face_pipeline.py 第 268 行 def match_person(self, query_emb: np.ndarray, top_k1, threshold_base0.65): query_emb: 当前帧人脸 embedding (512,) top_k: 返回前 k 个最相似的人默认 1 threshold_base: 基础阈值动态调整策略见下文 scores [] names [] for name, data in self.register_db.items(): score np.dot(query_emb, data[embedding]) # 余弦相似度 scores.append(score) names.append(name) if not scores: return [] # 动态阈值取 top3 平均分的 0.8 倍避免固定阈值误判 top3_scores sorted(scores, reverseTrue)[:3] dynamic_th np.mean(top3_scores) * 0.8 if len(top3_scores) 2 else threshold_base # 筛选并排序 matches [(n, s) for n, s in zip(names, scores) if s dynamic_th] matches.sort(keylambda x: x[1], reverseTrue) return matches[:top_k] # 在 webcam.py 中维护历史帧 self.match_history deque(maxlen5) # 存最近 5 帧的 match_person 结果 def update_match_history(self, matches): self.match_history.append(matches) # 统计连续 3 帧是否同一人 if len(self.match_history) 3: recent_names [m[0][0] if m else None for m in self.match_history if m] if len(set(recent_names[-3:])) 1 and recent_names[-1] is not None: print(f Login confirmed: {recent_names[-1]})5.3 部署优化技巧ONNX 导出 TensorRT 加速Jetson 用户必看YOLOv5-face 和 ArcFace 均支持 ONNX 导出但需注意torch.onnx.export的dynamic_axes设置# 导出 YOLOv5-face dummy_input torch.randn(1, 3, 640, 640) torch.onnx.export( model, dummy_input, weights/yolov5s-face.onnx, input_names[input], output_names[pred, pred_kp], dynamic_axes{input: {0: batch, 2: height, 3: width}, pred: {0: batch}, pred_kp: {0: batch}} ) # 导出 ArcFace输入固定 112x112 dummy_input torch.randn(1, 3, 112, 112) torch.onnx.export( encoder, dummy_input, weights/arcface_r100.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch}, output: {0: batch}} )Jetson 用户可进一步用trtexec生成 TensorRT 引擎trtexec --onnxweights/yolov5s-face.onnx --saveEngineyolov5s-face.trt \ --fp16 --workspace2048 --optShapesinput:1x3x640x640实测 Jetson Orin 上TensorRT 版本比原生 PyTorch 快 2.3 倍功耗降低 35%。从那以后我每次交付人脸识别模块都强制走一遍「单图 → 注册 → 实时比对 → ONNX 导出」四步验证链哪怕客户只要求 demo。因为人脸项目里90% 的线上问题都源于对齐偏差或归一化遗漏而这些在单图阶段就能暴露。希望帮到你。本文还有配套的精品资源点击获取