YOLOv5+ArcFace+活体检测:人脸识别工程实践拆解
简介面向希望打通人脸识别理论到工程落地环节的开发者与学习者这份学习包围绕YoloV5人脸检测、ArcFace特征提取和活体检测三条主线组织可用于课程设计、自学实验与项目原型验证。压缩包共54个文件、约3.4MB以26个Python源码和9个pyc文件构成主要代码骨架辅以7个yaml配置、2个pth权重、4张jpg样例图及txt、md、sh、cfg等说明与运行辅助文件便于按模块查阅和复现实验。内容覆盖人脸检测、特征比对、防伪判别等环节并给出从数据准备、模型调用到测试推理的实践线索适合在本地搭建小型人脸识别流程时参考。已有177人学习可帮助读者理解多算法协同的目录组织与关键脚本职责减少环境配置和模块衔接中的试错成本。1. 一张打印出来的工牌照把门禁刷开了去年帮一个做园区门禁的朋友做联调他们把「人脸识别」当成一个纯分类问题拿 YOLOv5 框出人脸ArcFace 算出 512 维特征库里 cosine 相似度超过 0.35 就开门。上线两周一个员工拿手机相册里的工牌照贴在摄像头前面连着刷开了三次日志里相似度全在 0.6 以上。问题不在 ArcFace特征比对本身没判错——它比对的就是同一张脸问题在于整条链路里缺了「这张脸是活的吗」这一环。后来我们在这条流水线里补上活体分支才把这类攻击挡在识别之前。这份「人脸识别_YoloV5_ArcFace_活体检测_学习实践」的资源包走的正是这套三段式思路YOLOv5 负责人脸检测和五点关键点ArcFace 负责把对齐后的人脸映射成可度量的特征向量活体检测负责在识别之前判断镜头前是真脸还是照片/屏幕回放。它适合两类人一类是刚接触人脸识别、想在本地把「检测→对齐→识别→活体」跑通一遍的开发者另一类是手里已经有一个能跑但不能用的 demo想知道工业场景里到底该在哪几个位置加约束的人。下面按模块拆代码能直接抄。2. 流水线拆解检测、对齐、识别、活体四段怎么串起来2.1 目录结构与各模块的职责边界学习实践类项目的目录通常不会太复杂但边界要清楚否则改一处崩三处。这个包解压后的结构大体是这样我把每个目录的职责和「改动频率」标出来方便你判断哪些能直接换、哪些动了要连带改。face_demo/ ├── configs/ │ ├── det.yaml # 检测输入尺寸、置信度、NMS 阈值 │ ├── rec.yaml # 识别输入尺寸、特征维度、比对阈值 │ └── anti.yaml # 活体输入尺寸、放行阈值、扩边系数 ├── models/ │ ├── yolov5_face.py # 检测网络1 类框 5 点关键点 │ ├── iresnet.py # 识别骨干IR-SE 结构 ArcFace head │ └── minifas.py # 活体网络轻量多尺度卷积 ├── weights/ │ ├── det_face.onnx # 检测权重导出 ONNX 后推理端不再依赖 torch │ ├── arcface_r50.onnx # 识别权重 │ └── anti_spoof.onnx # 活体权重 ├── core/ │ ├── detector.py # letterbox 前向 解码 NMS │ ├── align.py # 五点相似变换到 112x112 │ ├── recognizer.py # 归一化 特征提取 cosine │ └── anti_spoof.py # 扩边裁剪 活体打分 ├── pipeline.py # 串起四段的入口 └── tools/ ├── build_gallery.py # 建底库人脸 - 特征 - npy └── eval_pair.py # 阈值标定正负样本对打分configs和core是解耦的这是我认为这个项目结构上最值得学的一点所有阈值、尺寸都从 yaml 读core里的类只接受参数不接受硬编码。你后面要换检测模型只改detector.py一个文件加一个 yaml 段pipeline.py一行都不用动。反过来很多人写这类 demo 喜欢把conf0.5、0.38这种数直接写死在函数里标定阈值的时候要在五个文件里搜魔数那是真难受。weights目录统一放 ONNX意味着推理端不需要装 torch部署到没有 GPU 的工控机上只要onnxruntime一个依赖包体积从 2G 掉到几十兆。这一点在「学习实践」阶段体会不深等你要往边缘盒子上塞的时候就知道了。2.2 一张图从进到出的数据流与接口约定四段之间的数据契约最好在动手写之前定死我一般会先写一张「阶段 → 输入 → 输出 → 单位」的表后面所有函数都按这个签名来调试的时候哪一段出的问题一目了然。阶段输入输出坐标系/单位检测BGR 原图 HxWx3boxes Nx4 (x1,y1,x2,y2)、scores N、kps Nx5x2原图像素坐标对齐原图 5 点关键点112x112x3 对齐人脸RGB归一化到 [-1,1]识别对齐人脸512 维 float32 特征L2 归一化后单位球面活体原图 检测框1 个 real 概率0~1注意检测输出的关键点必须是原图坐标不能是网络输入尺寸下的坐标。这是第一个高频翻车点YOLOv5 训练时把图 letterbox 到 640关键点也是在 640 空间回归的推理时如果忘了按gain和pad还原回去五点会整体偏移并缩放后面estimateAffinePartial2D算出来的相似变换矩阵就是错的对齐出来的人脸是歪的ArcFace 特征直接废掉。我见过有人排查了两天以为是模型不行其实就是少了三行反算。串起来的伪代码大致长这样实际实现里每一段是一次 ONNX session 调用def run(image_bgr): # 1) 检测返回原图坐标系下的框与五点 boxes, scores, kps detector(image_bgr, conf0.5, iou0.45) results [] for box, kp in zip(boxes, kps): # 2) 活体先过算力小能把大部分攻击挡在识别之前 real_prob anti_spoof(image_bgr, box) if real_prob 0.85: results.append({box: box, pass: False, reason: spoof}) continue # 3) 对齐 识别 aligned align(image_bgr, kp) # 112x112 RGB float32 feat recognizer(aligned) # 512 维 # 4) 与底库比对 name, sim gallery.search(feat) results.append({box: box, name: name, sim: sim, pass: sim 0.35}) return results顺序上我把活体放在识别之前不是随手写的。活体网络输入 80x80、参数量通常只有零点几 M单张耗时在 CPU 上是毫秒级ArcFace R50 在 CPU 上一张要几十毫秒。攻击样本占的比例虽然不高但把便宜的判断放前面等于给后面那道贵的加了个前置过滤整体吞吐能提一截。有些实现喜欢先识别再活体逻辑上也能跑只是白烧算力。2.3 为什么是 YOLOv5 ArcFace 这套组合检测这块经典做法有 MTCNN、RetinaFace、DSFD 几条路线。MTCNN 三级级联在 CPU 上对多人脸图片很慢而且小脸召回一般RetinaFace 精度好但依赖可变形卷积往 ONNX 和某些推理框架上导的时候容易卡在算子不支持。YOLOv5 的优势不在精度天花板而在于工程链路极其成熟导出 ONNX、TensorRT、NCNN 都是一条命令的事社区里现成的人脸微调权重和五点回归头改法也最多。做学习实践选一条「能被部署出去」的路比选一条「榜单最高」的路更有价值。识别这块FaceNet 的 triplet loss 对采样策略太敏感batch 里难样本挖得不好就崩SphereFace 的训练稳定性需要一堆 trickArcFace 加了角度间隔m0.5把类间距离在角度空间上显式拉开收敛稳、复现性好scale64配合margin0.5基本是开箱即用的默认值。512 维特征在十万人级别的底库上用余弦相似度做暴力检索也扛得住不需要上 Faiss。活体这块业界主流是两类一类是基于纹理和频域的静默活体MiniFASNet 那一支一类是基于动作配合的眨眼、转头。学习项目基本都选前者因为它是单帧输入、纯前向、可以和识别放在同一个 pipeline 里不需要交互状态机。代价是它对屏幕摩尔纹、打印纸张纹理这些线索依赖比较强光照一变就容易掉点这个后面第 4、5 章会细说。3. 跑通推理从权重文件到一张人脸的特征向量3.1 依赖清单与权重放置环境上我建议直接用 Python 3.83.10 加 onnxruntime不要一上来就装 torch 全家桶。推理链路只需要四个包装完就能跑pip install onnxruntime opencv-python numpy pyyaml # GPU 机器换成 onnxruntime-gpu并确认 CUDA/cuDNN 版本对得上权重放置有两个约定要遵守否则脚本会找不到文件或者静默用错模型三个 onnx 文件统一放weights/文件名和configs/*.yaml里model_path字段严格一致如果用相对路径脚本必须在项目根目录下启动我一般会在入口处os.chdir(os.path.dirname(os.path.abspath(__file__)))兜一下省得换个工作目录就报FileNotFoundError。# configs/rec.yaml model_path: weights/arcface_r50.onnx input_size: [112, 112] embedding_dim: 512 threshold: 0.35 # 余弦相似度阈值先用 0.35后面用 eval_pair.py 标定threshold这个值不要照抄。0.35 是在底库几千人、注册照质量还不错的前提下比较稳的起点底库上万、注册照来源杂的时候可能要降到 0.28 并配合 Top-1 与 Top-2 的间隔做二次判定。判断方法在 3.4 说。3.2 YOLOv5 人脸检测与五点关键点解码检测部分是整条链路里最容易写错的地方重点是 letterbox 和反算。先看前处理import cv2 import numpy as np def letterbox(img, new_shape640, color(114, 114, 114)): 等比缩放 灰边填充返回图像和反算需要的 gain/pad h, w img.shape[:2] r min(new_shape / h, new_shape / w) new_unpad (int(round(w * r)), int(round(h * r))) dw (new_shape - new_unpad[0]) / 2 dh (new_shape - new_unpad[1]) / 2 if (w, h) ! new_unpad: img cv2.resize(img, new_unpad, interpolationcv2.INTER_LINEAR) top, bottom int(round(dh - 0.1)), int(round(dh 0.1)) left, right int(round(dw - 0.1)), int(round(dw 0.1)) img cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, valuecolor) return img, r, (left, top)r是缩放比(left, top)是左上角填充量这两个值就是后面反算的全部依据。-0.1/0.1的写法是为了让总填充量在奇偶情况下都对齐YOLOv5 官方实现里就是这个套路自己写的时候别图省事用int(dw)否则会有一像素的系统性偏移小脸上体现为框偏半格。后处理要做三件事把输出从(cx, cy, w, h)转成(x1, y1, x2, y2)把五点从 640 空间减pad再除gain还原到原图最后按类别做 NMS。人脸检测只有一类nc1输出通道是5 15 204 个框偏移 1 个 obj 5 点 × 2 坐标有些实现会把关键点也乘上 obj 置信度导出前要确认清楚不然五点会整体缩向框中心。def decode(pred, gain, pad, conf_thres0.5, iou_thres0.45): pred: (N, 20)已过 sigmoid返回原图坐标 boxes, scores, kps [], [], [] for row in pred: obj row[4] if obj conf_thres: continue cx, cy, w, h row[0:4] # 反 letterbox先减 pad再除 gain x1 (cx - w / 2 - pad[0]) / gain y1 (cy - h / 2 - pad[1]) / gain x2 (cx w / 2 - pad[0]) / gain y2 (cy h / 2 - pad[1]) / gain kp row[5:15].reshape(5, 2) kp[:, 0] (kp[:, 0] - pad[0]) / gain kp[:, 1] (kp[:, 1] - pad[1]) / gain boxes.append([x1, y1, x2, y2]) scores.append(float(obj)) kps.append(kp) keep nms(boxes, scores, iou_thres) # 标准 IoU NMS按得分排序 return (np.array(boxes)[keep], np.array(scores)[keep], np.array(kps)[keep])关键点顺序必须是固定的通常是「左眼、右眼、鼻尖、左嘴角、右嘴角」。这个顺序错了对齐矩阵就是错的而且错得很隐蔽——人脸还是会被裁出来、还是会缩放只是被镜像或者旋了肉眼看小图不一定发现。我建议在detector.py里加一个调试开关把五点画到原图上存一张debug_kps.jpg前几次跑务必肉眼确认一遍顺序。3.3 五点对齐与 ArcFace 特征提取对齐用的是相似变换把检测到的五点映射到 ArcFace 的标准五点模板上。这个模板是固定的112x112 输入对应下面这组坐标别自己改import cv2 import numpy as np # ArcFace 官方 112x112 五点模板左眼、右眼、鼻、左嘴角、右嘴角 ARC_SRC np.array([ [38.2946, 51.6963], [73.5318, 51.5014], [56.0252, 71.7366], [41.5493, 92.3655], [70.7299, 92.2041], ], dtypenp.float32) def align(img_bgr, kps, out_size112): M, _ cv2.estimateAffinePartial2D(kps.astype(np.float32), ARC_SRC, methodcv2.LMEDS) if M is None: return None # 五点退化共线/重合直接丢弃 face cv2.warpAffine(img_bgr, M, (out_size, out_size), borderValue0.0) face cv2.cvtColor(face, cv2.COLOR_BGR2RGB) face (face.astype(np.float32) - 127.5) / 128.0 return face # HWC, RGB, [-1,1]estimateAffinePartial2D只估计旋转、缩放、平移四个自由度不带错切这正好匹配「正面人脸」的假设。LMEDS比默认的最小二乘更抗单点异常五点里有一个点偏得离谱时不会拖垮整个矩阵。返回None的情况一定要判五点重合或者几乎共线的时候 OpenCV 会返回空不判就会在后面warpAffine那里抛一个莫名其妙的异常。归一化用(x - 127.5) / 128.0这是 ArcFace 训练时的标准预处理。这里必须和训练一致用/255或者 ImageNet 均值方差都会让特征分布整体偏移相似度普遍偏低表现就是「明明是同一个人分数只有 0.1 出头」。这种问题在日志里看不出来因为不报错只能靠对齐预处理这一条条对。推理调用很直白import onnxruntime as ort class Recognizer: def __init__(self, model_path): self.sess ort.InferenceSession( model_path, providers[CPUExecutionProvider]) def __call__(self, face_rgb): # NCHW blob face_rgb.transpose(2, 0, 1)[None].astype(np.float32) emb self.sess.run(None, {input.1: blob})[0][0] # L2 归一化后点积即余弦相似度 return emb / (np.linalg.norm(emb) 1e-10)输入节点名input.1是导出时的默认名换成别的模型要先用netron打开看一眼别硬套。特征做 L2 归一化之后两个向量的点积就等于余弦相似度底库可以全部存成归一化后的float32二维数组检索时一次矩阵乘法gallery feat出全部分数几千人的库在 CPU 上是微秒级比循环算快两个数量级。3.4 比对阈值怎么标定而不是拍脑袋阈值这件事我见过太多人直接抄一个 0.5 然后抱怨「同一个人过不去、不同的人能过」。正确做法是用tools/eval_pair.py跑一批正负样本对把分数分布画出来看。# 简易标定正样本对 同一人不同照片负样本对 不同人 pos np.array([cos(f1, f2) for f1, f2 in same_pairs]) neg np.array([cos(f1, f2) for f1, f2 in diff_pairs]) for t in np.arange(0.20, 0.61, 0.02): far (neg t).mean() # 误接受率 frr (pos t).mean() # 误拒绝率 print(fthr{t:.2f} FAR{far:.4f} FRR{frr:.4f})正样本建议至少 500 对覆盖不同光照、不同角度、有无眼镜负样本对数量要远大于正样本才能把 FAR 估准。选点的原则不是取 FAR 和 FRR 曲线的交点而是先定业务能接受的 FAR门禁场景一般要小于千分之一在这个约束下取 FRR 最小的那个阈值。同一批数据标出来的值通常落在 0.300.40 之间跟 0.5 差得很远这就是抄默认值的代价。底库侧还要做一件事注册照存进去之前先过一遍活体和质量门限模糊、侧脸超过 45 度、遮挡严重的直接拒绝重录。底库脏了后面所有阈值都是白标定。4. 活体检测模块静默活体分支的输入、损失与阈值联动4.1 活体分支在整体结构里的位置活体检测在这个项目里是一个独立的二阶段分支输入不是对齐后的 112x112 人脸而是检测框按比例扩边后的裁剪图。为什么要扩边屏幕回放和打印攻击的线索大量分布在人脸外围——手机屏幕的边框、纸张的边缘、摩尔纹在画面边缘更明显。如果只裁人脸内部这些线索全被切掉了模型只能靠皮肤纹理硬猜泛化会很差。扩边系数一般取 2.7 左右也就是以人脸框中心为中心把边长放大到原来的 2.7 倍再裁。这个数字不是随便定的它来源于常见静默活体模型训练时的裁剪策略训练和推理必须一致否则输入分布对不上。def crop_for_anti(img_bgr, box, scale2.7, out_size80): x1, y1, x2, y2 box cx, cy (x1 x2) / 2, (y1 y2) / 2 side max(x2 - x1, y2 - y1) * scale nx1, ny1 int(cx - side / 2), int(cy - side / 2) nx2, ny2 int(cx side / 2), int(cy side / 2) # 越界部分用 0 填充保持人脸在画面中的相对位置不变 pad max(0, -nx1, -ny1, nx2 - img_bgr.shape[1], ny2 - img_bgr.shape[0]) if pad 0: img_bgr cv2.copyMakeBorder(img_bgr, pad, pad, pad, pad, cv2.BORDER_CONSTANT, value(0, 0, 0)) nx1, ny1, nx2, ny2 nx1 pad, ny1 pad, nx2 pad, ny2 pad patch img_bgr[ny1:ny2, nx1:nx2] patch cv2.resize(patch, (out_size, out_size)) return cv2.cvtColor(patch, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0这里我没有用warpAffine因为活体分支不需要几何对齐反而要保留原始几何关系——屏幕的矩形边界一旦被仿射变换扭过边缘特征就变形了。这也是为什么活体分支放在对齐之前而不是复用人脸对齐的结果。放置位置上还有一个工程取舍如果你的人脸检测本身漏检率很低活体可以只对检测到的人脸做如果场景里有大量侧脸、遮挡检测本身就不可靠那活体误判会被人脸漏检掩盖此时更稳妥的做法是把活体放在整帧级别先做一次粗筛再走逐人脸精判。学习项目里前者就够用。4.2 训练侧的数据构造与标签定义活体模型的训练数据是这个模块真正的门槛网络结构反而次要。常见做法是把样本分成三类真人real、打印攻击print、屏幕回放replay用三分类交叉熵训练也有人做二分类只分真/假或者做深度图回归。三分类的好处是日志里能看出来「这次是被纸挡了还是被屏幕挡了」排查时有用代价是需要两类攻击样本都收集到。数据构造上最容易犯的错是增强过头。活体判断依赖的恰恰是那些「看起来像噪声」的线索屏幕的摩尔纹、纸张的纤维纹理、镜头的轻微失焦。如果你按常规分类任务那套上强色彩抖动、随机擦除、MixUp这些线索会被破坏掉模型转而学习人脸身份特征结果就是在训练集上 99%换一台摄像头就崩。我一般的增强策略是保留随机缩放、随机裁剪、JPEG 质量压缩q 从 90 降到 40模拟不同摄像头编码、轻微高斯模糊、亮度微调 ±20。慎用强对比度抖动、色相旋转、大比例随机擦除。禁用MixUp、CutMix因为把真脸和假脸混在一张图上标签本身就失去意义了。类别不均衡几乎是必然的真人样本好收集攻击样本要自己拿手机、打印纸去造。常见做法是按类别频率给损失加权或者用 focal loss 压低易分样本的权重。学习项目里样本量小我建议直接过采样攻击类到与真人 1:1比调 loss 参数直观。还有一点训练集和测试集必须按「摄像头型号 攻击载体」切分不能随机切。随机切的话同一次采集的照片会同时出现在训练和测试里指标虚高得离谱上线直接打回原形。这个坑我在别的项目上踩过一次血泪经验。4.3 推理侧打分与阈值的联动策略活体输出的是一个 3 类的 softmax取 real 那一类的概率作为分数。单阈值放行是最简单的做法但实际场景里更稳的是分档def decide(real_prob, sim, conf): if conf 0.6: # 检测置信度太低人脸可能太小/太糊 return retry if real_prob 0.90 and sim 0.38: return pass if real_prob 0.60 and sim 0.42: return verify # 转二次验证刷卡/密码 return reject分档的核心思路是「两个分数互相补偿但补偿有上限」活体分高可以适当放宽识别阈值但放宽幅度不超过 0.05识别分高不能反过来放宽活体因为照片攻击的识别分天然就高那是它的强项。这条非对称规则很重要我见过有人写成对称加权结果等于给攻击开了后门。retry这一档也值得留着。检测置信度低于 0.6 的时候与其硬判不如提示用户「请正对摄像头」用户体验比莫名其妙被拒好得多也减少了误拒投诉。阈值本身要跟着摄像头走。同一套权重在室内补光摄像头和室外逆光摄像头上的 real 分布能差 0.2 以上如果你的部署环境是固定的拿 200 张现场真人照 200 张现场攻击照重新标一次分档边界效果比换模型明显。5. 避坑与常见问题排查五个把结果搞崩的细节5.1 检测框比人脸大一圈对齐后的人脸总是偏现象可视化出来检测框明显比实际人脸大关键点也跟着往外飘对齐出来的 112x112 人脸偏上或者偏左。原因基本是 letterbox 反算没做或者pad的取整方式和前处理不一致——前处理用了int(round(dh - 0.1))后处理却用int(round(dh))差一两个像素人脸越小偏得越明显。解决前处理和反算共用同一个函数返回的gain和pad不要在两处各算一遍调试时把debug_kps.jpg存下来五点画在原图上一眼就能看出是整体偏移还是单点异常。5.2 同一个人注册和比对相似度只有 0.1 出头现象底库建好了同一个人换张照片去比cosine 只有 0.10.2但换成不同人反而能到 0.5分数完全没区分度。原因通常是预处理不一致按出现频率排BGR/RGB 弄反OpenCV 读进来是 BGR模型要 RGB、归一化方式不一致/128和/255混用、对齐模板五点顺序搞错导致人脸被镜像。解决写一个check_preprocess函数把对齐后的人脸存成 png 打开看一眼人脸是正的就说明对齐没问题再用同一张图分别走「注册路径」和「比对路径」打印两个特征向量的前 8 维不一致就说明两条路径的预处理有分叉。这类问题不报错、不打日志只能靠比对中间结果。5.3 活体阈值卡太死真人站在摄像头前反复被拒现象现场反馈「明明是我本人一直提示失败」但拿照片去试反而能过。原因一般是活体模型对当前光照/摄像头不适应或者训练数据里真人样本太单一比如全是在同一个房间采的模型把「这个房间的光照」当成了真人的特征。解决先分层看日志把real_prob按 0.1 的粒度画直方图如果真人样本大量堆在 0.60.8 这个区间说明是分布偏移而不是模型坏了用现场数据微调最后两层或者直接重新标定分档阈值同时检查摄像头是否有强逆光、红外补光这些会让纹理线索失真。5.4 多人场景帧率掉到个位数现象单人在画面里能跑 15 FPS站进去四个人直接掉到 3 FPS。原因是没有做任何调度每帧对全图跑一次检测然后对每个人脸都跑一次 ArcFace R50。解决分三层检测按帧跳过每 23 帧检一次中间帧用上一帧的框做 IOU 匹配跟踪识别结果按人脸 ID 缓存同一个人连续帧不重复提特征只在跟踪丢失或相似度掉出阈值时重算活体保持在每帧做因为它便宜且攻击是瞬时的。改完之后四人场景基本能回到 10 FPS 以上。5.5 跑一段时间内存一直涨最后进程被杀现象服务跑了几个小时 RSS 从 300M 涨到 3G重启就好。原因常见的是在请求处理函数里反复ort.InferenceSession(...)每个 session 都会分配自己的内存池不释放就累积另一个可能是把每帧的aligned人脸图塞进了某个全局列表做「最近记录」。解决三个 session 在模块加载时创建一次全局复用ONNX Runtime 的 session 本身是线程安全的任何缓存结构都要设上限比如只保留最近 200 条用collections.deque(maxlen200)而不是list.append。上线前跑一个 30 分钟的压测盯一眼 RSS 曲线是否平稳这个习惯能省掉很多半夜被告警叫醒的时刻。6. 二次开发换检测骨干、加跟踪、做批量回归拿到这套代码之后最常做的三件改造我按投入产出比排一下。第一件是换检测骨干。models/yolov5_face.py里如果用的是 yolov5s在边缘设备上可以换 yolov5n精度掉 12 个点但速度翻倍反过来如果要抓小脸监控场景换 yolov5m 或者把输入从 640 提到 960代价是显存和延迟。改的时候只需要动det.yaml里的weights和input_size后处理里nc1、通道数 20 这些常量如果抽成了配置项就不用改代码。我一般会在换骨干之后跑一遍固定测试集记录召回率和五点 NME归一化平均误差两个指标一起看只看召回容易被「框变大」骗过去。第二件是加人脸跟踪。这一步的价值不只是省算力更重要的是让识别结果在时间维度上稳定。做法很简单用 IoU 或者简单的卡尔曼做帧间匹配给每个人脸分配一个track_id维护一个{track_id: (feat, last_seen, hit_count)}的字典同一个 ID 连续 3 帧识别结果一致才输出单帧跳变直接丢弃。你会发现误识别率肉眼可见地下降因为偶发的错配被投票机制吃掉了。这里有个细节feat不要每帧覆盖用滑动平均或者保留最近 5 次的中位数照片攻击时的特征抖动会明显更小。改造点改动位置主要影响检测骨干替换configs/det.yaml 权重速度/小脸召回后处理一般无需改人脸跟踪与投票pipeline.py外层误识别率下降帧率提升活体分档阈值configs/anti.yaml现场通过率需按摄像头重新标定底库检索加速tools/build_gallery.py万级底库建议加 Faiss IVF批量化推理core/*.py的__call__batch 维拼接GPU 上吞吐提升明显第三件是批量回归脚本。这套链路里任何一个环节改了预处理、换了权重、动了阈值都必须在同一批固定样本上重跑一遍输出一张对比表底库 N 人、正样本对 M 组、攻击样本 K 张看 FAR/FRR/活体通过率三个数有没有退化。我吃过一次亏换了个「精度更高」的识别权重单项测试全绿上线后才发现新权重的归一化是/255跟老的对齐代码不匹配整库特征全部作废。从那以后我每次动core/里的任何一行预处理都强制先跑一遍那个回归脚本跑不过就不许合。这套流程看着笨但它把「凭感觉觉得没问题」变成了「有数可查」。这份资源包的结构和上面拆的这套基本对得上检测、识别、活体三个权重都在weights/下跑通pipeline.py之后建议先别急着改模型把tools/eval_pair.py的标定流程走一遍你会对自己手上这套权重的真实水平有个准确认知后面调什么都心里有底。希望帮到你。本文还有配套的精品资源点击获取