尧图精选

OpenVINO+ONNX人脸关键点检测部署:从模型导出到int8量化

🕒 发布时间:2026/9/13 1:41:46 📁 来源:尧图网络
简介一套基于OpenVINO和ONNX的人脸关键点检测算法部署项目源码面向计算机视觉与模型部署开发者重点解决68点与39点landmark模型从训练框架转换、优化到英特尔硬件高效推理的完整工程问题适用于人机交互、智能监控等场景。资源包共188个文件压缩包32.59MB主要包含Python脚本、ONNX模型、模型权重、测试图片与说明文档便于对照学习和快速复现。目前已有275人学习属于中等热度的优质项目。项目清晰拆解了模型准备、ONNX转换、OpenVINO优化、部署测试四个环节源码中附带mobilefacenet模型、推理脚本及演示动画还能看到不同角度人脸的检测效果既能帮助初学者理清部署流程也可作为二次开发的基础框架省去重复搭建环境与调试的周期。1. 为什么人脸关键点检测部署绕不开OpenVINOONNX这条链路把PyTorch训练好的人脸关键点检测模型交给OpenVINO通过ONNX这个中间格式落地是很多团队在Intel CPU上做算法部署时的首选。原因很直接训练框架的依赖太重服务端不可能为每个模型装一套Python环境而ONNX先把网络固定成计算图OpenVINO再把计算图映射成CPU上高效的指令级代码两边解耦。人脸关键点检测里68点是常规配置39点则常用于眼睛、眉毛、嘴部细粒度分析场景。一个模型两个输出头部署时能同时返回两套landmark坐标省去重复推理。支持两种点数意味着业务方只需一套推理管线切换起来代价很低。这篇文章把模型导出、OpenVINO加载、后处理坐标还原、量化int8和最后可视化验证的路径完整走一遍。适合正在做算法落地、被pytorch模型直接上生产环境搞崩过的人也适合刚接触ONNX部署、想找一份能复现的参考脚本的新手。2. 部署前的模型准备从pt转onnx到验证68/39点输出2.1 先搞清楚ONNX模型该长什么样ONNX模型本质上是protobuf格式的计算图里面包含算子的连接结构和每个张量的形状。导出前如果不定清楚输入输出名和shape后面到OpenVINO里排查会非常被动。人脸关键点模型常用112x112或128x128输入输出为68点和39点两个分支。下面是一份可复用的接口约定表项目源码里大概率也是按这个思路组织的名称形状说明input[1,3,112,112]NCHW布局BGR或RGB需要和训练保持一致landmark_68[1,68,2]68点坐标每个点一对x,ylandmark_39[1,39,2]39点坐标按业务顺序排列这里最容易忽略的是坐标值语义。有些模型输出的是归一化坐标即每个点的x和y都在0到1之间有些模型则直接回归裁剪后图片上的像素坐标。这个语义不统一后处理写错是部署阶段最常见的问题所以导出前先在训练代码里确认好。2.2 pt转onnx最小导出脚本假设训练好的模型文件叫landmark.pth模型类里有两个输出头。下面这段代码可以直接复用用torch.onnx.export导出。import torch import torch.onnx from model import LandmarkModel model LandmarkModel(num_branches2) state torch.load(landmark.pth, map_locationcpu) model.load_state_dict(state[state_dict]) model.eval() dummy_input torch.randn(1, 3, 112, 112) torch.onnx.export( model, dummy_input, landmark.onnx, input_names[input], output_names[landmark_68, landmark_39], opset_version12, dynamic_axes{ input: {0: batch}, landmark_68: {0: batch}, landmark_39: {0: batch}, }, ) print(export done)这段代码的关键点在于opset_version12。ONNX算子集版本越高能表达的算子越丰富但OpenVINO的兼容层不一定跟得那么快。12是一个兼容性比较好的折中。dynamic_axes这里只保留了batch维度可变让推理时一次可以传多张人脸宽高保持112固定OpenVINO对静态尺寸的优化会更激进。2.3 用ONNX Runtime检查图结构导出成功不代表模型就没问题。我先用ONNX Runtime跑一遍确认输入输出名、shape和类型都和预期一致再去走OpenVINO这样能少踩一半的坑。import onnxruntime as ort sess ort.InferenceSession(landmark.onnx, providers[CPUExecutionProvider]) for inp in sess.get_inputs(): print(input:, inp.name, inp.shape, inp.type) for out in sess.get_outputs(): print(output:, out.name, out.shape, out.type)这里显式指定了CPUExecutionProvider避免装过CUDA版onnxruntime时默认跑到GPU上去。打印结果对比2.1的表格只要输出名和shape一致就可以放心往下走。如果输出名对不上优先回导出脚本里检查output_names的拼写不要急着改OpenVINO代码。2.4 直接读ONNX还是转IROpenVINO两种方式都支持。开发调试阶段直接read_model读ONNX最方便改一行就能跑生产部署时我会用ovc把ONNX转成IR格式也就是.xml加.bin文件减少模型加载时间也能提前发现问题。ovc landmark.onnx --output_model landmark_openvino core.read_model(landmark_openvino.xml)ovc是OpenVINO 2023.3之后的命令行入口旧版本用mo --input_model landmark.onnx。如果界面提示找不到ovc检查OpenVINO的Python包是否装完整python -c import openvino; print(openvino.__version__)能打印版本就说明环境正常。转出来的IR在路径上会附带一个.bin权重文件部署时要一起带上。提示不管用ovc还是直接读ONNX都不要在导出时混入训练用算子比如Dropout。Dropout在推理模式下被忽略还好一旦误用会导致每次预测结果不确定这种现象在关键点坐标上会表现为小幅度抖动。3. 用OpenVINO在Linux上跑ONNX68点39点landmark输出与后处理3.1 core.read_model加载ONNX并编译OpenVINO的Python入口非常简洁。先创建Core对象读模型再编译到CPU设备。编译对象可以理解为已经做完了图优化和算子调度的可执行体。from openvino import Core core Core() model core.read_model(landmark.onnx) compiled core.compile_model(model, CPU) print(inputs:, [p.any_name for p in model.inputs]) print(outputs:, [p.any_name for p in model.outputs])compile_model的第二个参数指定设备为CPU不要贪方便用AUTO。在只有CPU的机器上显式指明设备能让日志更干净。p.any_name是OpenVINO 2023年之后的写法老版本用p.get_any_name()如果你的OpenVINO是旧版接口名需要回退。推理时直接调用编译对象传入一个带batch维度的numpy数组即可import numpy as np input_tensor np.random.randn(1, 3, 112, 112).astype(np.float32) output compiled([input_tensor]) landmark_68 output[compiled.output(landmark_68)] landmark_39 output[compiled.output(landmark_39)]compiled.output(landmark_68)返回一个输出张量的句柄取出来的numpy数组shape是[1,68,2]。注意输入必须用np.float32OpenVINO不会帮你在内部做类型转换传成float64会直接报类型错误。3.2 人脸框裁剪与前处理关键点检测通常是跑在检测框内部所以需要先把人脸区域从原图里切出来。前处理函数要同时记录裁剪框坐标否则后处理无法把坐标还原回原图。import cv2 import numpy as np def preprocess(image, box, size(112, 112), mean0.5, std0.5): x1, y1, x2, y2 [int(v) for v in box] face image[y1:y2, x1:x2] face cv2.resize(face, size) face cv2.cvtColor(face, cv2.COLOR_BGR2RGB) face face.astype(np.float32) / 255.0 face (face - mean) / std face face.transpose(2, 0, 1) return face[np.newaxis, ...]这里假设原图是OpenCV读取的BGR格式模型训练时用的是RGB所以先做颜色空间转换。mean和std取0.5是常见设置能覆盖大多数归一化方案如果训练代码里用的是ImageNet均值方差记得改成对应值。transpose(2, 0, 1)把HWC改成CHW最后再加一个batch维度变成[1,3,112,112]。3.3 把归一化坐标映射回原图后处理是整个部署链路里最容易被写错的地方。假设模型输出的是归一化坐标每个点的x和y在0到1之间代表相对裁剪框宽高的比例。那么还原到原图坐标需要做一次线性变换。def map_points(points, box, size(112, 112)): x1, y1, x2, y2 box face_w, face_h x2 - x1, y2 - y1 pts points.reshape(-1, 2).copy() pts[:, 0] pts[:, 0] * face_w x1 pts[:, 1] pts[:, 1] * face_h y1 return pts.reshape(points.shape)size参数在这个函数里暂时没用到原因是归一化坐标不依赖实际resize尺寸。如果模型输出的是裁剪图上的像素坐标例如0到112之间的数值那映射公式就要写成x / size[0] * face_w x1。两种写法的差别可以用下面的表格概括模型输出格式还原公式适用情况归一化坐标0~1x * face_w x1训练时把坐标除以框宽高像素坐标0~112x / 112 * face_w x1训练时直接回归裁剪图坐标用reshape(-1, 2)再映射可以同时兼容68点和39点的输出函数逻辑不用改两份。3.4 多输出切换的技巧业务方可能这次只需要68点下次只需要39点。OpenVINO支持直接按名字取输出但整个推理图仍会把两个分支都算完。如果想省掉不必要分支的计算可以在转换阶段裁剪输出。ovc landmark.onnx --output landmark_39 --output_model landmark_39_openvino这样生成的IR就只有一个输出OpenVINO的图优化器会自动把68点分支相关的节点一并裁掉。开发阶段需要频繁切分支时我倾向于直接维护两个ONNX一个保留两个输出一个单独导出39点。不要在后处理里通过if逻辑去屏蔽某个输出那只能省内存省不了CPU计算。4. 性能调优与常见坑.onnx量化int8和OpenVINO拉满CPU4.1 先测准单帧延迟优化之前先测量。关键点模型很小输入112x112单帧延迟通常只有几毫秒直接跑一遍计时容易被CPU频率抖动干扰。常见做法是先预热再计时。import time def bench(compiled, input_tensor, n200): for _ in range(50): compiled([input_tensor]) t0 time.perf_counter() for _ in range(n): compiled([input_tensor]) dt (time.perf_counter() - t0) / n * 1000 print(favg {dt:.2f} ms)预热50次是为了让CPU缓存命中率和变频策略稳定下来。Windows和Linux的结果会有差异Linux上还要注意是否有其他进程抢占CPU。如果你在做性能对比优先在相同机器、相同频率环境下测否则数据没有说服力。4.2 compile_model的关键参数OpenVINO的CPU插件有几个配置直接影响延迟和吞吐。部署在线服务时我一般会显式限制线程数避免框架自动调度导致延迟抖动。配置键推荐值作用CPU_THREADS_NUM4限制线程数稳定延迟CPU_THROUGHPUT_STREAMS1低延迟用1高吞吐用2或4CPU_BIND_THREADSNUMA避免线程切换抖动代码里这样配置config { CPU_THREADS_NUM: 4, CPU_THROUGHPUT_STREAMS: 1, } compiled core.compile_model(model, CPU, config)CPU_THROUGHPUT_STREAMS默认可能是0即由OpenVINO自动决定。自动模式偏向于把CPU核用满适合批量离线处理在线API接口我改成1保证单帧延迟更平稳。CPU_BIND_THREADS设为NUMA在双路服务器上效果明显单颗CPU时可以不设。4.3 .onnx量化int8的常见做法int8量化是OpenVINO在Intel CPU上做性能压榨的主要手段。支持VNNI指令集的CPU上量化后模型推理速度能提升50%以上但关键点坐标回归任务比较敏感输出坐标的误差会有所放大。需要先量化再用测试集评估。OpenVINO当前提供的量化接口在NNCF包里安装后可以用以下方式量化import nncf from openvino import Core core Core() model core.read_model(landmark.onnx) model.reshape([1, 3, 112, 112]) calibration_dataset ... # 生成器每次输出一个输入张量 quantized_model nncf.quantize(model, calibration_dataset, subset_size200) core.save_model(quantized_model, landmark_int8.xml)subset_size是校准样本数量200在关键点任务上已经是比较稳妥的量级。校准集应该尽量覆盖光线和姿态差异不要全用同一类正脸图片。model.reshape固定batch为1是因为量化过程要求静态shape。保存成xml后加载方式和普通IR一样业务代码不需要改动。提示int8量化不是百分百稳定。如果量化后关键点偏差超过验收阈值可以尝试nncf.quantize(model, calibration_dataset, model_typenncf.ModelType.TRANSFORMER)以外的默认设置或者退回只量化权重不量化激活具体接口在NNCF文档里能查到。4.4 常见报错与对应解法部署过程中遇到的报错大多是模型和运行时之间的协议不匹配。列几个我在OpenVINO项目里见过的高频问题No such operatorONNX的opset版本高于OpenVINO支持范围。解决方法是把导出脚本里的opset_version调到11或12重新导出。Input shape is not static模型还带着动态batch维度OpenVINO编译阶段无法确定shape。用model.reshape({input: [1, 3, 112, 112]})固定。Layout mismatch输入布局和模型不匹配。先确认导出时用的是NCHW如果模型来自TensorFlow则可能是NHWC需要在转换时用--layout参数指定。Can not read modelONNX文件损坏或算子不兼容。先用ONNX Runtime跑一遍能跑通再检查OpenVINO版本是否需要升级。还有一个隐蔽问题推理输出全部是NaN。这个不是OpenVINO的问题而是前处理或模型本身有问题。优先检查归一化参数是否和训练一致以及裁剪人脸后是否出现宽高为0的异常框。5. 一个落地技巧可视化68/39点并计算NME回归指标5.1 把关键点画回图片上部署完成后第一件事不是接REST接口而是把关键点画回原图上肉眼检查一遍。这一步能发现坐标映射方向、颜色通道、框坐标错位等一堆问题。import cv2 import numpy as np def draw_landmarks(image, points, color(0, 255, 0), radius2): for (x, y) in points.astype(np.int32): cv2.circle(image, (x, y), radius, color, -1) return imagepoints传入的是经过map_points映射回原图后的坐标数组。先画68点再画39点用不同颜色更容易看出两套点是否对齐到正确位置。如果画出来的点整体偏移基本可以确定是裁剪框坐标和原图坐标系没对齐如果只有边缘点偏那就是归一化公式用错了。5.2 用NME做回归测试关键点模型不能用分类准确率来评估使用Normalized Mean Error也就是NME。它把平均点距离除以两眼中心距离消除人脸尺寸带来的差异是landmark任务通用的离线验证指标。def nme(pred, gt, left_eye_idx, right_eye_idx): d np.linalg.norm(pred - gt, axis1) inter_ocular np.linalg.norm(gt[left_eye_idx] - gt[right_eye_idx]) return np.mean(d) / inter_ocularleft_eye_idx和right_eye_idx是标注数据里左右眼中心点的索引68点和39点的索引定义不同需要从训练配置里抄过来。把OpenVINO的推理结果和PyTorch原始模型的标准输出各算一遍NME如果差值小于0.01说明部署链路没有引入明显精度损失如果偏差大优先怀疑预处理里的mean/std和颜色通道。5.3 把三者封装成一个纯函数前处理、推理、后处理这三个环节各自独立测试通过后我会把它们封装成单一入口函数方便后续对接HTTP服务或视频流处理。def estimate(image, box): tensor preprocess(image, box) out compiled([tensor]) pts68 map_points(out[compiled.output(landmark_68)], box) pts39 map_points(out[compiled.output(landmark_39)], box) return pts68, pts39封装之后上层业务完全不用关心ONNX还是OpenVINO。后续如果换成IR模型或者切到int8版本只需要改这个函数内部的加载路径。把NME和单帧延迟一起记录到日志作为后续量化int8或换IR时的增量对照值下次优化时就能直接拿来判断是变好了还是变差了。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →