PyTorch到ONNX人脸识别系统:Flask部署与onnxruntime推理实战

发布时间:2026/10/3 3:49:51
PyTorch到ONNX人脸识别系统:Flask部署与onnxruntime推理实战 简介这是一套基于ONNX深度学习框架的人脸识别系统源码与模型资源面向计算机视觉学习者、课程设计或项目开发者解决人脸检测、识别、年龄性别判断与人脸关键点定位等多任务落地问题。压缩包共39个文件约667.61MB包含15个onnx模型文件buffalo_l、buffalo_m、buffalo_s等不同规格、6个Python脚本、3个pyc编译文件以及png、jpg示例图片、ttc字体、bin索引和md说明文档覆盖模型、代码与测试素材。资源提供图片路径识别、摄像头实时识别和Web接口识别三种调用方式并配有B站使用教程视频便于对照理解推理流程与接口封装。目前已有3333人学习下载适合希望快速跑通ONNX人脸识别全流程、研究多模型选型与工程化部署的读者参考。1. 从 PyTorch 到 ONNX一套能跑在 Flask 上的人脸识别系统到底长什么样很多团队做人脸识别卡住的不是模型精度而是部署那一步。训练时 PyTorch 跑得好好的一到生产环境要装 torch、要配 CUDA、要处理版本冲突服务器镜像动辄几个 G推理延迟还不稳定。这套基于 ONNX 的人脸识别系统解决的正是这个断层它把检测、识别、年龄性别属性判断三条链路统一导出成.onnx格式用 onnxruntime 做推理后端外面套一层 Flask 提供 HTTP 接口。换句话说你拿到的不是一份训练代码而是一份可以直接部署上线的推理服务。它适合两类人一类是手里已经有 PyTorch 权重、想把它变成轻量服务的算法工程师另一类是想快速搭一个人脸识别 Demo、又不想被深度学习框架环境折磨的后端开发者。整套系统的核心价值在于「解耦」——训练归训练部署归部署中间用 ONNX 这个中间表示隔开。下面我从模型结构、导出流程、Flask 集成到量化加速把这条链路拆开讲清楚包括我实际踩过的坑。2. ONNX 模型导出与 onnxruntime 推理三条链路怎么串起来2.1 为什么选 ONNX 而不是直接上 TorchScript先讲选型理由这决定了后面所有操作。TorchScript 是 PyTorch 自家的序列化方案优点是导出简单缺点是推理时仍然依赖 libtorch部署包体积压不下来跨语言调用也麻烦。ONNX 是开放格式onnxruntime 有 C、Python、C#、Java 多语言绑定一个.onnx文件丢到任何装了 onnxruntime 的环境都能跑不挑框架版本。人脸识别系统通常包含三个子模型人脸检测定位人脸框和关键点、人脸识别提取特征向量做比对、年龄性别识别属性分类。这三个模型如果都用 PyTorch 部署环境依赖会叠加统一转成 ONNX 后onnxruntime 一个运行时全包了。常见做法是检测用 RetinaFace 或 SCRFD 这类轻量结构识别用 ArcFace 或 MobileFaceNet属性判断用一个小型分类网络。具体用哪个版本取决于你手里的权重但导出流程是一样的。提示ONNX 只是中间表示它不负责训练。你必须在 PyTorch 里训练好、验证过精度再导出。导出后精度掉点是最常见的问题后面避坑章节会细讲。2.2 导出脚本把 PyTorch 权重转成 .onnx导出这一步的核心是torch.onnx.export但参数设错就会得到一个「能加载、结果全错」的模型。下面是我常用的导出模板以人脸识别模型为例import torch import torch.onnx # 加载训练好的模型eval 模式必须开否则 BN 和 Dropout 行为不一致 model MyFaceModel() model.load_state_dict(torch.load(face_rec.pth, map_locationcpu)) model.eval() # 构造一个符合实际输入尺寸的 dummy 输入 # 人脸识别模型常见输入是 112x112检测模型可能是 640x640 dummy_input torch.randn(1, 3, 112, 112) torch.onnx.export( model, dummy_input, face_rec.onnx, export_paramsTrue, # 把权重一起写进文件 opset_version11, # 算子集版本太低不支持某些算子太高部分运行时没跟上 do_constant_foldingTrue, # 常量折叠优化能减小模型体积 input_names[input], # 输入名后面推理时要对上 output_names[embedding],# 输出名同样要对上 dynamic_axes{ # 动态维度batch 可变时必须设 input: {0: batch_size}, embedding: {0: batch_size}, }, ) print(导出完成)逻辑说明model.eval()不能省训练模式下的 BatchNorm 会用到当前 batch 的统计量导出后推理结果会飘。opset_version我一般锁 11兼容性最好onnxruntime 各版本都支持。dynamic_axes是新手最容易漏的——不设的话模型 batch 维度被写死成 1服务端想批量推理就会报错。参数说明do_constant_folding建议开它会把能提前算的常量合并模型体积通常能小 5% 到 15%。input_names和output_names是你自己定的但推理代码里必须用同样的名字去取对不上就是黑匣子报错都找不到方向。2.3 onnxruntime 推理onnxruntime 和 onnx 的区别先分清很多人搜「onnxruntime 和 onnx 区别」这里一句话说清ONNX 是模型格式文件长什么样onnxruntime 是执行引擎怎么把这个文件跑起来。你导出的是.onnx文件跑它的是 onnxruntime。两者版本要匹配onnxruntime 版本太老可能不支持新 opset。推理代码长这样import onnxruntime as ort import numpy as np import cv2 # 创建推理会话CPU 用默认有 GPU 就换 CUDAExecutionProvider sess ort.InferenceSession( face_rec.onnx, providers[CPUExecutionProvider] # GPU 环境改成 CUDAExecutionProvider ) input_name sess.get_inputs()[0].name # 拿到输入名和导出时对应 output_name sess.get_outputs()[0].name # 预处理人脸图 resize 到 112x112归一化转 NCHW img cv2.imread(face.jpg) img cv2.resize(img, (112, 112)) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img img.astype(np.float32) / 255.0 img (img - 0.5) / 0.5 # 归一化具体均值方差按训练时来 img np.transpose(img, (2, 0, 1)) # HWC - CHW img np.expand_dims(img, axis0) # 加 batch 维 # 推理 embedding sess.run([output_name], {input_name: img})[0] print(embedding.shape) # 应该是 (1, 512) 之类逻辑说明providers决定用 CPU 还是 GPU装了 onnxruntime-gpu 才能填CUDAExecutionProvider普通版填了会报错。预处理必须和训练时完全一致均值方差、通道顺序RGB 还是 BGR错一个特征向量就全废了这是血泪经验。参数说明sess.get_inputs()[0].name是动态取的不要硬编码字符串模型换了名字就变。sess.run的第一个参数是输出名列表返回也是列表取[0]才是张量。2.4 Flask 封装把推理变成 HTTP 接口Flask 这层负责收图、调推理、返回 JSON。核心是把 onnxruntime 会话做成全局单例别每次请求都重新加载模型那样延迟会高到没法用。from flask import Flask, request, jsonify import onnxruntime as ort import numpy as np import cv2 app Flask(__name__) # 全局加载一次所有请求复用 sess ort.InferenceSession(face_rec.onnx, providers[CPUExecutionProvider]) input_name sess.get_inputs()[0].name output_name sess.get_outputs()[0].name def preprocess(image_bytes): arr np.frombuffer(image_bytes, np.uint8) img cv2.imdecode(arr, cv2.IMREAD_COLOR) img cv2.resize(img, (112, 112)) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0 img (img - 0.5) / 0.5 img np.transpose(img, (2, 0, 1)) return np.expand_dims(img, axis0) app.route(/recognize, methods[POST]) def recognize(): file request.files.get(image) if file is None: return jsonify({error: no image}), 400 tensor preprocess(file.read()) emb sess.run([output_name], {input_name: tensor})[0] return jsonify({embedding: emb.flatten().tolist()}) if __name__ __main__: app.run(host0.0.0.0, port5000)逻辑说明np.frombuffer加cv2.imdecode是从内存字节直接解码图片比先存盘再读快。全局sess是关键Flask 默认多线程onnxruntime 会话本身线程安全可以共享。参数说明host0.0.0.0才能被外部访问只写127.0.0.1就只能在本地调。生产环境别用app.run换 gunicorn 加多 worker但注意每个 worker 会各自加载一份模型内存要算够。3. 检测、识别、年龄性别三模型协同输入输出怎么对齐3.1 人脸检测模型的输出解析检测模型和识别模型不一样它的输出是框和关键点解析逻辑更绕。以常见的 SCRFD 或 RetinaFace 导出为例输出通常是多个尺度的特征图需要解码成实际坐标。这块如果自己写解码容易翻车常见做法是直接复用导出时配套的后处理或者用现成的 insightface 工具链。# 检测模型推理后输出一般是 (batch, num_anchors, 15) 之类 # 前 4 个是框偏移中间 2 个是置信度后面 10 个是 5 个关键点 def decode_detection(outputs, input_size640, conf_thres0.5): boxes, scores, kps [], [], [] for level_out in outputs: # 具体解码按你用的检测模型来这里只示意结构 # 关键把相对偏移还原成原图坐标再做 NMS pass return boxes, scores, kps逻辑说明检测输出必须做 NMS非极大值抑制去重否则同一张脸会出几十个框。置信度阈值conf_thres一般设 0.5漏检多就降到 0.3误检多就升到 0.7。参数说明输入尺寸input_size要和导出时一致检测模型常导 640x640改尺寸要重新导出。关键点用于人脸对齐对齐后再送识别模型精度能明显提升。3.2 识别模型的特征比对识别模型输出的是特征向量判断是不是同一个人靠余弦相似度。阈值怎么定是玄学不同模型差别很大。模型类型特征维度常用阈值说明MobileFaceNet1280.5~0.6轻量适合边缘ArcFace R505120.6~0.7精度高体积大自定义小模型256需实测用验证集刷def cosine_similarity(a, b): a a / np.linalg.norm(a) b b / np.linalg.norm(b) return float(np.dot(a, b)) sim cosine_similarity(emb1, emb2) is_same sim 0.6 # 阈值按你的模型实测调整逻辑说明先归一化再点积就是余弦相似度。阈值不能照搬别人的必须用自己的人脸验证集跑一遍 ROC取等错误率附近的点。参数说明np.linalg.norm是 L2 范数归一化后向量模长为 1点积范围就是 -1 到 1。3.3 年龄性别识别模型的接入年龄性别是分类任务输出通常是年龄区间和性别概率。它和识别模型共用同一张对齐后的人脸图所以流程上可以并行推理省时间。# 属性模型输出示意年龄 0-100 回归 或 分桶分类性别二分类 age_gender_sess ort.InferenceSession(age_gender.onnx, providers[CPUExecutionProvider]) ag_input age_gender_sess.get_inputs()[0].name ag_output age_gender_sess.get_outputs() result age_gender_sess.run(None, {ag_input: aligned_face}) # result[0] 可能是年龄result[1] 是性别 logits逻辑说明sess.run(None, ...)里第一个参数传 None 表示取所有输出省得一个个列名字。年龄如果是回归输出直接取数值如果是分桶要取 argmax 再映射回区间。参数说明属性模型输入尺寸可能和识别模型不同常见 96x96别搞混。对齐用的人脸关键点要和检测模型输出的一致。4. 避坑与排查导出、量化、部署里最容易翻车的五件事4.1 导出后精度掉点结果全错现象PyTorch 里推理正常导出 ONNX 后同一个输入结果差很多。原因模型没切 eval 模式或者预处理在导出前后不一致。解决导出前强制model.eval()并用同一张图分别跑 PyTorch 和 onnxruntime逐层对比输出定位是哪一层开始偏的。4.2 动态 batch 没设服务端批量推理报错现象单张图能跑一次传多张就报维度不匹配。原因导出时没设dynamic_axesbatch 维被写死成 1。解决重新导出给输入输出都加上{0: batch_size}导出后用工具确认维度是动态的。4.3 int8 量化后精度崩了现象.onnx量化 int8 后模型体积小了一半但识别准确率明显下降。原因量化校准集选得不好或者对量化敏感的层如第一层和最后一层也量化了。解决用有代表性的人脸图做校准集几百张起步对敏感层做混合精度保持 fp16 或 fp32。量化不是免费的午餐精度和速度要权衡。4.4 onnxruntime 版本和 opset 不匹配现象加载模型时报「Unsupported opset version」或某个算子找不到。原因导出用的 opset 版本高于当前 onnxruntime 支持的上限。解决查 onnxruntime 文档确认支持的 opset 范围要么降 opset 重新导出要么升级 onnxruntime。我一般锁 opset 11省心。4.5 Flask 多 worker 内存爆掉现象gunicorn 起 4 个 worker服务器内存直接打满。原因每个 worker 进程各自加载一份 onnxruntime 会话模型内存乘以 worker 数。解决减少 worker 数或者用单 worker 多线程模式onnxruntime 内部会并行再或者把推理拆成独立服务Flask 只做转发。5. 进阶技巧用 onnxruntime 的图优化和 IO 绑定把延迟压下来模型能跑通只是及格线真正上线还要抠延迟。这里讲两个我常用的手段都是 onnxruntime 自带的不用改模型。第一个是图优化级别。onnxruntime 默认做基础优化你可以手动拉满opts ort.SessionOptions() opts.graph_optimization_level ort.GraphOptimizationLevel.ORT_ENABLE_ALL opts.intra_op_num_threads 4 # 单次推理内部并行线程数 opts.inter_op_num_threads 2 # 多个算子间并行 sess ort.InferenceSession(face_rec.onnx, opts, providers[CPUExecutionProvider])ORT_ENABLE_ALL会启用算子融合、常量折叠等全部优化通常能再快 10% 到 20%。intra_op_num_threads设成物理核数别超过超了反而抢线程。inter_op_num_threads在单模型场景作用不大多模型并行时才有意义。第二个是 IO 绑定避免每次推理都拷贝输入输出内存。这个在 C 里效果明显Python 里收益有限但批量推理时值得开# 预分配输入输出 buffer复用内存 import numpy as np io_binding sess.io_binding() input_tensor np.zeros((1, 3, 112, 112), dtypenp.float32) io_binding.bind_cpu_input(input_name, input_tensor) io_binding.bind_output(output_name) # 每次推理前更新 input_tensor 内容然后 sess.run_with_iobinding(io_binding)逻辑说明bind_cpu_input把 numpy 数组直接绑到会话输入省去内部拷贝。适合高频调用场景比如视频流逐帧推理。参数说明绑定的数组 dtype 和 shape 必须和模型输入严格一致否则报错。GPU 场景用bind_input配合OrtValue写法不同别混用。还有一个验证技巧导出和量化之后一定要用一批真实数据跑端到端对比别只看单张图。我会准备 100 张带标注的人脸图分别跑 PyTorch 原模型和 ONNX 模型统计识别准确率和相似度分布偏差超过 1% 就回去查。从那以后我每次导出模型都强制走一遍这个对比流程再也没出现过上线才发现精度崩的情况。希望帮到你。本文还有配套的精品资源点击获取