简介这是一份围绕人脸识别全流程搭建的学习实践包面向具备一定 Python 与深度学习基础、希望把课堂理论落到可运行项目的开发者与学生。内容以 YoloV5 完成人脸检测结合 ArcFace 的角度间隔损失提取高区分度特征并接入活体检测模块辨识照片、视频等伪造攻击覆盖训练、测试与数据生成等环节适合课程设计、毕业项目与算法入门复现。压缩包共 54 个文件约 3.4MB以 26 个 Python 源码为主辅以 9 个 pyc、7 个 YAML 配置、2 个 pth 权重及少量图片和说明文档目录按检测、识别、活体检测与模型权重分区便于按模块阅读和调试。已有 179 人学习。读者可据此理解从人脸检测、特征比对到活体判定的系统链路参考完整项目架构、参数配置与模型加载方式并借助现成脚本完成数据准备和效果验证较快建立可扩展的人脸识别工程雏形。1. 从一张工位照片到门禁结果这套人脸识别流水线到底做了什么先说个反直觉的结论人脸识别项目里最容易让整个流程失效的往往不是模型精度不够而是对齐那一步差了几像素——检测框稍微歪一点ArcFace 提取出的特征就会从 0.7 的相似度掉到 0.2接下来无论怎么调阈值都救不回来。这个压缩包做的事情就是把人脸识别拆成检测、对齐、活体、特征比对四段每段一个独立模块、一份独立权重跑通之后你能清楚看到误差是哪一段带进来的而不是面对一个一大坨代码构成的整体干瞪眼。YOLOv5 负责人脸框和五点关键点ArcFace 负责把对齐后的人脸压成 512 维向量做余弦比对活体检测在识别之前先做一次真伪分类挡住手机翻拍和打印照片。三件套均为开源方案权重可直接下载不需要自己从头训练。适合两类人一类是想把「人脸识别」从调包 Demo 变成能讲清链路的学习者另一类是手里有具体场景考勤、门禁、签到、相册聚类需要一个能改、能换库、能接业务接口的起点工程。下面按目录结构、环境搭建、核心代码、常见问题排查、二次开发五段拆。2. 工程结构拆解detector、recognizer、anti_spoof 三段式目录怎么分工2.1 一张人脸进来经过四道关整条链路的顺序是固定的检测 → 关键点 → 活体 → 对齐 → 特征 → 比对。这里有两个容易被写反的地方需要记住。第一活体判定要放在特征提取之前。原因很实际活体模型输入是 80×80 的小图一次前向大约几毫秒ArcFace 输入 112×112R50 主干在 CPU 上要几十毫秒。如果先识别再判活体一张翻拍照片也会完整走完特征提取和库检索多花的算力白扔。把活体做成短路开关判定是假脸就直接返回 unknown后面的活体判定和库检索全部跳过。第二对齐必须夹在检测和识别之间不能省。ArcFace 这一类基于 margin 损失训练出来的模型训练时所有样本人脸都被五点相似变换摆到固定的 112×112 画布上——左眼在 (38.29, 51.70)、右眼在 (73.53, 51.50) 这种硬编码坐标。推理时如果你的输入没有摆到同一个位置模型看到的就是一个训练分布外的样本特征自然对不上。我见过太多人把检测框直接 resize 成 112×112 就送进去跑出来相似度全在 0.1 到 0.2 之间然后怀疑权重有问题。检测框本身只保证「框住脸」不保证「脸在框里是正的、大小一致的」。侧脸、抬头、低头的时候框会跟着变形直接裁剪会把人脸拉伸。五点关键点就是用来做相似变换的锚cv2.estimateAffinePartial2D只解旋转、缩放、平移四个自由度不引入剪切五官比例不会被拉歪。2.2 目录结构与模块职责拿到包先别急着跑app.py把目录过一遍后面改哪块心里有数。典型结构是这样face_lab/ ├── configs/ │ └── config.yaml # 阈值、路径、设备全在这儿改参数不用动代码 ├── weights/ │ ├── yolov5n-face.pt # 检测 五点小模型够用 │ ├── arcface_r50.onnx # 特征提取onnx 比 pt 好部署 │ └── minifasnet.onnx # 静默活体3 分类 ├── core/ │ ├── detector.py # letterbox 前向 NMS 坐标还原 │ ├── recognizer.py # 对齐 归一化 特征 L2 │ ├── anti_spoof.py # 多尺度裁剪 分类 融合 │ └── pipeline.py # 把上面三个串起来唯一对外入口 ├── utils/ │ ├── face_align.py # ARCFACE_DST 模板 norm_crop │ ├── ops.py # xywh2xyxy / nms / letterbox │ ├── gallery.py # 人脸库读写与检索 │ └── draw.py # 画框、画点、贴名字 ├── data/ │ └── gallery/ # 一人一个子目录放建库用的原图 ├── enroll.py # 建库脚本图片 - embeddings.npy ├── app.py # 摄像头/视频/图片三种输入的演示入口 └── requirements.txt这套划分的核心思路是每个模块只对外暴露一个方法输入输出都是 numpy 和 dict不互相 import。detector不知道有 ArcFace 这回事recognizer也不知道活体存在所有编排逻辑集中在pipeline.py。好处是你想换检测器比如换 RetinaFace 拿更稳的五点只要新写一个类输出{bbox: [...], kps: (5,2), score: float}这三样pipeline 一行不用改。2.3 模块之间的接口约定把接口固化成抽象基类比写注释管用——换模型的时候 IDE 会直接告诉你少实现了什么from abc import ABC, abstractmethod import numpy as np class BaseDetector(ABC): abstractmethod def detect(self, img: np.ndarray) - list[dict]: 输入 BGR 原图返回 [{bbox: [x1,y1,x2,y2], kps: np.ndarray(5,2), score: float}] class BaseRecognizer(ABC): abstractmethod def embed(self, aligned_bgr: np.ndarray) - np.ndarray: 输入已对齐的 112x112 BGR 图返回 L2 归一化后的 512 维向量 class BaseAntiSpoof(ABC): abstractmethod def predict(self, img: np.ndarray, bbox: list) - float: 输入原图 人脸框返回真人概率 0~1三个约定里最容易忽略的是「BGR 还是 RGB」。OpenCV 读进来是 BGRYOLOv5 训练时用的也是 BGR 转 RGB 后的数据ArcFace 官方实现同样吃 RGB。两个模型都要 RGB但转换时机不同——检测那一步在 letterbox 之后转识别那一步在对齐之后转。这个约定如果不写死后面接新模型必踩坑。2.4 为什么是这三件套环节这个项目用的常见替代选它的理由检测 关键点YOLOv5-faceMTCNN、RetinaFace一次前向同时出框和五点速度比 MTCNN 快一个量级n/s 版本在 1080p 上能跑实时特征提取ArcFace (R50)FaceNet、CosFacemargin 系列在同样数据量下类间区分度更好512 维够用且检索快活体MiniFASNet 静默式眨眼/摇头动作式不需要用户配合单帧即可判定适合门禁这种「走过去就要出结果」的场景动作式活体让用户眨眼、张嘴实现简单但体验差且需要连续帧状态机静默式只要一张图工程上更好嵌。代价是对强光、逆光比较敏感这个后面排查章节会讲。3. 环境与权重就位把一张图跑出姓名标签的最小闭环3.1 环境准备先确认你装的是 GPU 版依赖不多但有一个必踩的坑直接pip install torch装到的是 CPU 轮子推理一帧四百毫秒你会以为模型有问题。先建环境再按显卡的 CUDA 版本装对应的轮子conda create -n facelab python3.9 -y conda activate facelab # 先看显卡驱动支持的 CUDA 版本再决定装哪个轮子 nvidia-smi # 按上面的版本号去 PyTorch 官网拿对应命令别照抄别人的 pip install torch torchvision # 其余依赖 pip install opencv-python numpy onnxruntime-gpu pyyaml tqdm # 装完立刻自检这行输出 False 后面全是白折腾 python -c import torch; print(torch.cuda.is_available(), torch.cuda.get_device_name(0))onnxruntime-gpu和onnxruntime不能共存两个都装会互相覆盖报Provider not found之类的错。如果你只打算 CPU 跑装onnxruntime就行但记得把 config 里的device改成cpu否则初始化 provider 时直接异常。跑之前再确认三件事cv2.__version__别太老老版本没有estimateAffinePartial2D的某些重载、weights/下三个文件都在且非 0 字节、data/gallery/下至少有一个人的子目录。这三条不满足app.py起来的第一个报错基本就跑不掉。3.2 配置文件所有可调参数集中在一处参数散落在代码里是二次开发的头号敌人。配置统一放configs/config.yamldetector: weights: weights/yolov5n-face.pt imgsz: 640 # letterbox 目标边长必须和训练时一致 conf_thres: 0.5 # 低于这个值的人脸框直接丢误检多就往上调 iou_thres: 0.45 # NMS 阈值人脸密集场景可以提到 0.5 max_det: 10 # 单帧最多保留几张脸门禁场景 1~3 就够 recognizer: weights: weights/arcface_r50.onnx input_size: 112 # ArcFace 的标准输入别改 sim_thres: 0.35 # 余弦相似度阈值最需要你自己标定的一个数 anti_spoof: weights: weights/minifasnet.onnx scales: [2.7, 4.0] # 裁剪倍数两个尺度结果取平均更稳 real_thres: 0.6 # 真人概率低于这个值判定为假脸 runtime: device: cuda half: false # 半精度onnx 路径下先别开容易出 NaNsim_thres是整套系统里最需要自己调的参数后面的排查章节会单说。scales这两个数不是随便定的——2.7 倍裁剪能带进一点背景和发际线4.0 倍裁剪把整张脸连同脖子一起框进去两个尺度互补单尺度在侧脸上容易翻车。3.3 建库把 gallery 目录变成可检索的向量表识别不是「拿两张图比一下」而是「拿一张图去库里找最近的」。所以先要有库。约定data/gallery/下一人一个子目录目录名就是返回的姓名data/gallery/ ├── 张三/ │ ├── 01.jpg │ └── 02.jpg └── 李四/ └── 01.jpg建库脚本做三件事逐张检测取最大人脸、对齐提特征、L2 归一化后堆成矩阵存盘。每张图还要过一道质量门——检测置信度低于 0.7、关键点超出图像边界、人脸宽度小于 80 像素的直接跳过并打日志。这条过滤非常关键用一张侧脸建库后面这个人永远认不准你还会怀疑是模型的问题。# enroll.py 核心片段 embs, metas [], [] for name_dir in sorted(GALLERY_DIR.iterdir()): for img_path in name_dir.glob(*.jpg): img cv2.imread(str(img_path)) faces detector.detect(img) if not faces: continue face max(faces, keylambda f: (f[bbox][2]-f[bbox][0]) * (f[bbox][3]-f[bbox][1])) w face[bbox][2] - face[bbox][0] if face[score] 0.7 or w 80: # 质量门置信度 分辨率 print(f[skip] {img_path.name} score{face[score]:.2f} w{w:.0f}) continue aligned norm_crop(img, face[kps], 112) embs.append(recognizer.embed(aligned)) # 已 L2 归一化 metas.append({name: name_dir.name, img: img_path.name}) np.save(data/embeddings.npy, np.stack(embs).astype(np.float32)) json.dump(metas, open(data/meta.json, w), ensure_asciiFalse, indent2)存下来的embeddings.npy是[N, 512]的矩阵meta.json是同长度的列表一一对应。同一个人放多张图是可以的——检索时取最相似的那条返回姓名相当于多模板匹配比只存一张稳。但别放太多张同一个角度的重复模板对召回没有增益只会拖慢检索。3.4 三种输入源跑通第一帧app.py用--source参数区分图片、视频、摄像头# 单张图片先确认整条链路是通的 python app.py --source data/test/demo.jpg --save out/demo_result.jpg # 摄像头0 是默认设备号 python app.py --source 0 --show # 视频文件 python app.py --source data/test/entry.mp4 --save out/entry_result.mp4第一次跑一定先用单图。图能出正确姓名说明权重、对齐、库、阈值全部没问题再去开摄像头。反过来先开摄像头一堆实时性问题掉帧、缓冲堆积、窗口卡死会盖住真正的问题你根本分不清是模型错了还是取流错了。输出目录会生成带框带点的结果图画面上同时打印姓名和相似度分数。建议调试期把相似度也画上哪怕最后上线要去掉——它是你判断「差一点认对」还是「完全认错」的唯一依据比你盯着框猜靠谱得多。4. 核心代码逐段读YOLOv5 五点检测、ArcFace 对齐比对、活体分类4.1 检测输出解析与 NMS 后处理YOLOv5-face 的输出是[N, 15]前 4 列是cx, cy, w, h第 5 列是目标置信度 obj第 6 列是类别分数 cls后 10 列是五个关键点的 x、y 坐标。这个排列顺序各家魔改版略有差异拿到权重先用一张图打印前几行确认列的含义再写后处理别信文档。import torch import numpy as np from torchvision.ops import nms from utils.ops import letterbox, xywh2xyxy torch.no_grad() def detect_faces(model, img, conf_thres0.5, iou_thres0.45, max_det10, devicecuda): canvas, ratio, pad_x, pad_y letterbox(img, 640) # BGR - RGB, HWC - CHW, 归一化到 0~1 x torch.from_numpy(canvas[:, :, ::-1].copy()).permute(2, 0, 1)[None].float().to(device) / 255.0 out model(x)[0] # [N, 15] out out[out[:, 4] conf_thres] # 先按 obj 粗筛减少 NMS 的输入量 if out.numel() 0: return [] boxes xywh2xyxy(out[:, :4]) scores out[:, 4] * out[:, 5] # 单类场景下等价于 conf多类要分开算 keep nms(boxes, scores, iou_thres)[:max_det] # keep 是索引张量 results [] for i in keep.tolist(): # 关键点必须从 640 的 letterbox 坐标系映射回原图否则点会整体偏移 kps out[i, 6:16].reshape(5, 2).clone() kps[:, 0] (kps[:, 0] - pad_x) / ratio kps[:, 1] (kps[:, 1] - pad_y) / ratio b boxes[i].tolist() b [(b[0]-pad_x)/ratio, (b[1]-pad_y)/ratio, (b[2]-pad_x)/ratio, (b[3]-pad_y)/ratio] results.append({bbox: b, score: float(scores[i]), kps: kps.cpu().numpy()}) return results几个参数的实际影响conf_thres设 0.5 是个折中调到 0.3 会把背景里的纹理误检成人脸调到 0.8 侧脸和戴口罩的脸就漏了。iou_thres在多人场景下要注意——人脸密集时两个框的重叠度可能超过 0.45调低会误删相邻的人脸通常 0.45 到 0.5 之间比较合适。max_det只是截断不参与筛选逻辑门禁场景设 3 就够设 10 只是以防万一。坐标还原这一步是纯数学但漏掉(x - pad) / ratio里的减法是最常见的 bug框会整体朝右下偏偏差量正好等于 padding 的像素数。小图看不出来1080p 上偏一百多像素你会以为是模型输出错了。4.2 五点对齐ArcFace 的隐形入口对齐函数短但决定了识别的上限。核心是ARCFACE_DST这个模板和estimateAffinePartial2D这个求解器import cv2 import numpy as np # ArcFace 标准五点模板左眼、右眼、鼻尖、左嘴角、右嘴角 ARCFACE_DST 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 norm_crop(img, kps, image_size112): if image_size % 112 0: # 支持 112/224 等整数倍尺寸 ratio image_size / 112.0 dst ARCFACE_DST * ratio else: dst ARCFACE_DST # partial 只有旋转缩放平移没有剪切五官比例不会被拉伸 M, _ cv2.estimateAffinePartial2D(kps.astype(np.float32), dst, methodcv2.LMEDS) return cv2.warpAffine(img, M, (image_size, image_size), borderValue0.0)estimateAffinePartial2D和estimateAffine2D的区别值得强调一次。后者是完整的六自由度仿射能表达剪切和不等比缩放五点稍有噪声就会解出一个拉伸矩阵人脸被压扁或者拉长ArcFace 拿到的是变形样本。前者只解四个自由度多余的形变被约束掉鲁棒性明显更好。这个改动只要一行但对相似度的提升是肉眼可见的。borderValue0.0也不能省。对齐后人脸可能超出原图边界不指定填充值的话 OpenCV 会保留未初始化的内存出现花屏条纹特征直接崩。另外传给这个函数的kps顺序必须是左眼、右眼、鼻尖、左嘴角、右嘴角——和ARCFACE_DST一一对应。YOLOv5-face 的默认训练标签一般就是按这个顺序打的但不同人魔改的数据集不一定拿到新权重务必先可视化一遍点别凭假设。4.3 特征提取与库检索特征提取用 onnxruntime 推理比 pt 加载快、显存占用小也方便后面换 TensorRT。注意输入归一化到[-1, 1]不是常见的[0, 1]——这个也是 ArcFace 系列模型的固定约定搞错的话特征向量还在但分布整体偏了相似度会系统性偏低。import onnxruntime as ort import numpy as np class ArcFace: def __init__(self, onnx_path): self.sess ort.InferenceSession( onnx_path, providers[CUDAExecutionProvider, CPUExecutionProvider]) self.input_name self.sess.get_inputs()[0].name def embed(self, aligned_bgr): # BGR - RGB归一化到 [-1, 1]HWC - NCHW x (aligned_bgr[:, :, ::-1].astype(np.float32) - 127.5) / 127.5 x x.transpose(2, 0, 1)[None] emb self.sess.run(None, {self.input_name: x})[0][0] return emb / (np.linalg.norm(emb) 1e-12) # 归一化后才能用点积当余弦归一化之后检索就是一次矩阵乘法不需要任何近似索引class FaceGallery: def __init__(self, emb_pathdata/embeddings.npy, meta_pathdata/meta.json): self.embs np.load(emb_path) # [N, 512]建库时已归一化 self.meta json.load(open(meta_path, encodingutf-8)) def search(self, emb, thres0.35): sims self.embs emb # 归一化向量点积 余弦相似度 idx int(np.argmax(sims)) best float(sims[idx]) return (self.meta[idx][name], best) if best thres else (unknown, best)一千人以内的库这种暴力检索在 CPU 上也就零点几毫秒完全没必要上 faiss。人多了再考虑embeddings.npy换成 faiss 的IndexFlatIP就行——内积索引和归一化点积是一回事接口几乎透明。4.4 活体判定与多尺度融合静默活体模型输入是 80×80输入来自人脸框按倍数放大后裁剪出来的局部图像。两个尺度分别推理再取平均能压掉单尺度的抖动def _crop_scale(img, bbox, scale): x1, y1, x2, y2 [int(v) for v in bbox] cx, cy (x1 x2) // 2, (y1 y2) // 2 w, h int((x2 - x1) * scale), int((y2 - y1) * scale) nx1, ny1 max(cx - w // 2, 0), max(cy - h // 2, 0) nx2, ny2 min(cx w // 2, img.shape[1]), min(cy h // 2, img.shape[0]) patch img[ny1:ny2, nx1:nx2] return cv2.resize(patch, (80, 80), interpolationcv2.INTER_LINEAR) class AntiSpoof: def __init__(self, onnx_path, scales(2.7, 4.0)): self.sess ort.InferenceSession(onnx_path, providers[CPUExecutionProvider]) self.scales scales def predict(self, img, bbox): probs [] for s in self.scales: patch _crop_scale(img, bbox, s) x patch[:, :, ::-1].astype(np.float32).transpose(2, 0, 1)[None] / 255.0 logit self.sess.run(None, {input: x})[0][0] e np.exp(logit - logit.max()) probs.append((e / e.sum())[1]) # 索引 1 是真人必须和你训练时的 label 顺序对齐 return float(np.mean(probs))最后那行注释是重点。类别索引 1 到底是真人还是假脸取决于训练时的类别映射不同版本仓库有的是[fake, real]有的是[real, fake, fake_print]三分类。搞反了的表现很典型照片能过、真人过不去。判断方法很简单——拿一张确认是本人的正脸照打印出三类概率看一眼真人概率最高的那个索引就是对的。活体不做硬性拒绝也可以做成降权if not alive: 返回 unknown是最简单的策略严格但可能误伤逆光下的真人。宽松一点的做法是活体分数低于 0.6 但仍然识别成功的话返回name (待复核)交给业务侧决定要不要人工确认。哪种更合适取决于你的场景能承受多少误拒。4.5 串起来pipeline 的主循环编排逻辑集中在pipeline.py二十行不到def run_one(img, det, rec, spoof, gallery): out [] for f in det.detect(img): alive spoof.predict(img, f[bbox]) if alive 0.6: # 短路假脸不浪费后续算力 out.append({**f, name: unknown, sim: 0.0, alive: alive}) continue aligned norm_crop(img, f[kps], 112) emb rec.embed(aligned) name, sim gallery.search(emb, thres0.35) out.append({**f, name: name, sim: sim, alive: alive}) return out视频流场景下det的模型加载放循环外别在循环里torch.load。另外torch.no_grad()一定要包住推理忘了的话显存会一路涨到 OOM这个坑不报错只是越跑越慢然后崩掉。5. 排查清单跑不通、认错人、活体误判的常见坑5.1 环境与性能类现象能跑通但处理一帧要三百到五百毫秒nvidia-smi里看不到任何进程。原因pip install torch装的是 CPU 版轮子torch.cuda.is_available()返回 False代码没做断言默默降级到 CPU 跑。解决在detector.py初始化时加一句assert device cpu or torch.cuda.is_available()让它在启动阶段就炸而不是在推理阶段慢慢磨。然后按显卡支持的 CUDA 版本重装。装完nvidia-smi里应该能看到 python 进程占着几百兆显存。5.2 检测与坐标类现象框整体向右下偏移一段距离图越大偏得越多小图上几乎看不出来。原因letterbox 的pad_x / pad_y和缩放比ratio只用了前半段缩放忘了后半段还原或者只还原了框没还原关键点。解决把(v - pad) / ratio这个还原写成一个小函数框和关键点、x 和 y 全部走同一个函数杜绝漏改。调试时直接在 640 的 canvas 上画一次再在原图上画一次两次位置一致就说明还原对了。现象识别相似度全部在 0.1 到 0.2同一个人也认不出来但检测框画得挺准。原因五点顺序和ARCFACE_DST模板不一致或者用estimateAffine2D全仿射导致人脸被拉伸。解决拿一张正脸图把五个点按顺序画上编号肉眼核对左眼是不是第 0 个、左嘴角是不是第 3 个。顺序对了再检查求解器换成estimateAffinePartial2D。改完这两个同一个人的相似度通常能到 0.6 以上。现象整体能用但相似度普遍偏低边缘样本经常掉到阈值以下。原因通道顺序搞反。OpenCV 读进来是 BGRYOLOv5 和 ArcFace 训练时都吃 RGB转换时机没对准。解决在detector和recognizer里各自显式写[:, :, ::-1]并且保持 BGR 输入绝不把 RGB 图存到磁盘之后又cv2.imread读一遍。这种 bug 不会报错只会让你的阈值怎么调都不对。5.3 识别与活体类现象真人被判定为照片或者照片被判定为真人。原因活体模型的类别索引和训练时的 label 映射对不上。三分类模型里经常出现0real, 1fake, 2fake_print和0fake, 1real两种排列。解决写一个三行的调试脚本把一张确认真人的正脸图送进去打印完整概率向量看哪个索引最大。确认之后把索引写进 config 的real_index字段别硬编码在代码里。现象陌生人走过去全被认成库里某一个人相似度还挺高0.5 以上。原因sim_thres照搬了别人的值。余弦相似度的分布在不同的模型、不同的库规模下差异很大R50 和 mobilefacenet 的合理阈值能差 0.2。解决自己标定。准备两组数据——同一个人的不同照片正样本对、不同人的照片负样本对各跑一遍算出相似度分布取一个让负样本对误识率可接受的点。工程上一般从 0.3 到 0.4 起调库越大阈值要越高因为检索空间大了撞上一个高分的概率也跟着涨。6. 二次开发改造换库、调阈值、接摄像头与业务接口这套工程的改造点集中在三个地方按投入产出比排序。改造方向改哪个文件工作量收益换自己的人脸库data/gallery/ 重跑enroll.py半小时立刻能用在自己场景换检测/识别模型core/下新增一个类实现对应基类半天精度或速度提升阈值自动化标定新增tools/eval_thres.py半天告别拍脑袋调参接业务接口新增server.pyFastAPI一天从 Demo 变成服务接口化最快的做法是把run_one包一层 HTTP。注意别在请求处理函数里建 sessiononnxruntime 的InferenceSession初始化要几百毫秒每个请求建一次直接把 QPS 打到个位数。用全局单例 线程锁或者启动时挂到app.state上。from fastapi import FastAPI, UploadFile import numpy as np, cv2 app FastAPI() pipe None # 启动时初始化全局复用 app.on_event(startup) def warmup(): global pipe pipe build_pipeline(configs/config.yaml) dummy np.zeros((640, 640, 3), dtypenp.uint8) pipe.run_one(dummy) # 先跑一遍预热避开首个请求的延迟尖峰 app.post(/recognize) async def recognize(file: UploadFile): buf np.frombuffer(await file.read(), np.uint8) img cv2.imdecode(buf, cv2.IMREAD_COLOR) faces pipe.run_one(img) return {count: len(faces), faces: [{name: f[name], sim: round(f[sim], 3), alive: round(f[alive], 3)} for f in faces]}预热这一步是我觉得最容易被跳过、又最值得加的一段。不预热的话第一个请求会慢到两三秒前端超时重试看起来像是服务挂了。之前在一个考勤场景上就是因为漏了预热上线第一天早上第一个人打卡永远失败——用户站在门口刷了三次第二次之后才成功。从那以后我每次起服务都强制走一遍预热哪怕只是一个全零的假图。另一个值得做的改造是把sim_thres和real_thres从配置里的固定值改成按场景分档门禁严格一点0.4 / 0.7相册聚类宽松一点0.28 / 0.5两个人的相似度本来就该分场景定。这套拆分做完你会发现自己不再纠结「到底多少才算对」而是开始问「这个场景能容忍多少误识」这才是工程上正确的问法。希望这些能帮你少走点弯路。本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
YOLO安全帽反光衣工作服数据集训练验收与难例回流 简介:面向施工工地、工厂等安全监管场景的YOLO目标检测数据集,聚焦人员安全帽、反光衣与工作服穿戴状态的自动识别需求,适合计算机视觉学习者、算法工程师及安全信息化项目开发者用于模型训练、验证与场景落地。压缩包共2000个文件࿰… · 2026/11/2 15:36:09
拒绝钓鱼源码:二手交易平台前端复刻与后台脚手架的正规技术学习路径 简介:前端页面复刻与后台管理系统脚手架是常见的工程训练方式:前者通过组件拆分、路由、列表虚拟滚动与图片懒加载还原真实交互;后者围绕 RBAC 权限模型、JWT 鉴权刷新、数据表 CRUD 与分页查询构建可复用底座。支付链路则强调下单、回调验签… · 2026/11/2 15:35:08
ResNet152叶片图像分类:迁移学习训练与ONNX部署避坑 简介:面向植物病害识别与计算机视觉迁移学习实践者的ResNet152项目资料包,适合具备一定深度学习基础、希望复现高精度图像分类流程的高校学生与算法开发者。包内标注文件总数2000,主要类型为5412张植物叶片jpg图像、6个py训练与推理脚本、2个… · 2026/11/2 15:35:08
软件设计师考试:CPU结构与存储体系核心考点解析 1. 软件设计师考试核心考点解析:CPU与存储体系刚接触软件设计师考试那会儿,我被计算机组成原理这部分折磨得不轻——那些枯燥的CPU结构图、抽象的流水线概念,还有永远分不清的主存辅存关系。直到自己真正动手在Logisim上搭建单总线CPU模型&am… · 2026/11/3 5:35:05
kubectl-ssh插件:简化K8s节点SSH访问的利器 1. 项目概述kubectl-ssh 是一个 Kubernetes 命令行工具的插件,它允许管理员直接通过 kubectl 命令 SSH 登录到集群节点。这个工具解决了传统节点访问方式需要跳板机、记录多组凭证的痛点,将节点访问流程集成到 Kubernetes 原生工作流中。在典型的运维场景… · 2026/11/3 5:35:05
Qoder:面向嵌入式开发的CLI优先AI编程协作者 1. Qoder 是什么:一个被误读但极具潜力的 AI 编程协作者Qoder 这个名字最近在开发者社区里频繁出现,但很多人第一次看到时会下意识联想到 Codex、Copilot 或者 Claude 的 CLI 工具——毕竟搜索热词里混着“codex安装”“copilot使用教程”“claude cli 用… · 2026/11/3 5:35:05
逆向工程工业化流水线:从二进制到可推演知识图谱 1. 这不是“黑客工具”,而是一套逆向工程的工业化流水线“Reverse Engineer Anything”——光看这个名字,很多人第一反应是:又一个炫技型玩具?写几行Python调用IDA Pro的胶水脚本?或者封装个Ghidra Web UI凑热闹&#… · 2026/11/3 5:35:05
PMBOK第七版核心:绩效域、原则与价值交付三维模型 简介:本资源是一份专为PMP备考者设计的PMBOK第七版核心知识体系思维导图PDF,面向项目管理初学者、转岗从业者及冲刺认证的考生,旨在系统梳理新版指南的逻辑框架与关键考点,解决知识碎片化、记忆负担重、过程组与知识领域交叉难理解… · 2026/11/3 5:35:05
Unity对象克隆与销毁机制详解 1. Unity中的对象克隆基础在Unity游戏开发中,Instantiate方法是创建对象副本的核心机制。这个函数的工作方式类似于编辑器中的复制命令,但提供了更多运行时控制选项。当你需要动态生成游戏对象时——无论是子弹、敌人还是特效粒子——Instantiate都是你的… · 2026/11/3 5:34:05
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 4:42:45
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 5:17:18
STM32F411RE与MAX14431实现SPI隔离:工业现场抗共模干扰实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
ESP32+大模型AI硬件开发:从Demo到产品的8个工程问题 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
基于MAX14431BAEE+与PIC18F85J10的SPI隔离方案设计与实践 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 4:42:45
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 5:17:18