ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PP-MattingV2 人像抠图:从 PaddleSeg 导出 ONNX 并用 ONNXRuntime 推理

PP-MattingV2 人像抠图:从 PaddleSeg 导出 ONNX 并用 ONNXRuntime 推理 简介这份资源面向深度学习部署与计算机视觉方向的开发者提供在ONNXRuntime上运行PaddleSeg实时人像抠图模型PP-MattingV2的完整工程解决跨框架推理与发丝级抠图落地问题适合具备一定C或Python基础、希望掌握模型转换与高性能部署的中高级学习者。压缩包共7个文件约2.85MB包含1个Python脚本、1个C源文件、1份README说明文档及4张示例图片分别对应Python快速验证、C高性能推理、使用说明与效果参考覆盖从接口调用到结果比对的主要环节。目前已有500人学习。读者可据此理解ONNX格式转换、ONNXRuntime多语言API调用及PP-MattingV2的推理流程并借助C与Python双版本对照掌握在Windows、Linux等平台部署人像抠图模型的关键思路为视频编辑、虚拟背景等场景提供可复用的实践参考。1. 从 PaddleSeg 到 ONNXRuntimePP-MattingV2 人像抠图为什么要换推理后端训练框架和部署框架分离是图像分割类模型落地时绕不开的一步。PaddleSeg 里跑通的 PP-MattingV2权重是动态图格式依赖 PaddlePaddle 运行时而实际业务里抠图往往要嵌进直播推流、视频会议、证件照生成这类 C 服务或者塞进 Python 的批处理流水线。这时候把模型导出成 ONNX再用 ONNXRuntime 加载就成了最省事的路径一次导出C 和 Python 两端共用同一份模型文件推理结果还能对齐。PP-MattingV2 本身是 PaddleSeg 里针对高分辨率人像抠图做的模型特点是用了引导滤波式的细化模块边缘过渡比早期 Matting 模型干净头发丝、半透明区域的处理也更稳。它输出的不是简单的 0/1 掩码而是带 alpha 通道的软掩码这对下游合成很关键。但很多人卡在第一步Paddle 动态图转 ONNX 时输入尺寸、动态轴、算子版本没设对导出的模型要么加载报错要么输出和原模型对不上。这篇面向的是已经拿到 PP-MattingV2 权重、想用 ONNXRuntime 做推理的 IT 从业者。不管你是要在 C 里集成还是先用 Python 验证效果核心链路是一样的导出 ONNX、检查输入输出、写推理代码、对齐预处理和后处理。下面按这个顺序拆开讲参数和坑都会落到具体命令上。2. PP-MattingV2 导出 ONNX 与 ONNXRuntime 环境准备2.1 导出前的模型结构与输入输出约定PP-MattingV2 的推理图通常接收一个 NCHW 的 float32 张量尺寸一般是 1x3xHxWH 和 W 需要是 32 的倍数常见做法是先用 512x512 或 1024x1024 验证。输出是一个 1x1xHxW 的 alpha 图值域在 0 到 1 之间。导出前要确认两件事一是模型是否带了多尺度或引导输入如果有额外输入ONNX 里也要对应上二是归一化参数PaddleSeg 默认用 mean[0.485, 0.456, 0.406]、std[0.229, 0.224, 0.225]这个必须和推理端一致否则 alpha 图会整体偏亮或偏暗。导出命令一般走 PaddleSeg 的export.py关键参数是--output_op和--input_shape。如果只想拿 alpha 输出--output_op设成argmax或none要看版本PP-MattingV2 通常保留原始输出更稳妥。动态轴方面如果业务里图片尺寸不固定可以把 H、W 设为动态但 ONNXRuntime 对动态 shape 的支持在 CPU 上没问题在 TensorRT 上会麻烦一些建议先固定尺寸跑通再改。# 在 PaddleSeg 目录下导出 PP-MattingV2 为 ONNX python export.py \ --config configs/ppmattingv2/ppmattingv2-stdc1-human_512.yml \ --model_path output/ppmattingv2/best_model/model.pdparams \ --save_dir inference_model \ --input_shape 1 3 512 512 \ --output_op none \ --export_onnx True这段命令里--config指向训练时用的配置文件--model_path是权重路径--input_shape固定成 1x3x512x512--export_onnx True触发 ONNX 导出。导出后会在inference_model下生成.onnx文件。如果报算子不支持通常是 Paddle 版本和 ONNX opset 不匹配把 opset 调到 11 或 12 再试。2.2 ONNXRuntime 的安装与版本选择ONNXRuntime 分 CPU 和 GPU 两个包Python 端安装很直接但要注意和 CUDA、cuDNN 的对应关系。如果只是验证模型先装 CPU 版最省事要上生产再换 GPU 版。C 端则要下载对应的预编译库或者自己编译Windows 下常见的是拿onnxruntime-win-x64-*.zipLinux 下用.tgz。# Python 端安装 CPU 版 ONNXRuntime pip install onnxruntime # 如果需要 GPU 推理换成 pip install onnxruntime-gpu安装完可以用一段最小代码检查模型能不能加载同时打印输入输出信息。这一步能提前暴露 shape 不匹配、opset 过旧的问题。import onnxruntime as ort # 加载导出的 PP-MattingV2 ONNX 模型 sess ort.InferenceSession(inference_model/model.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)providers参数决定用哪个后端CPU 版只能填CPUExecutionProviderGPU 版可以填CUDAExecutionProvider。打印出来的 shape 如果是[batch, 3, height, width]这种带字符串的说明导出时用了动态轴推理时传任意尺寸都行但首次运行会慢一些。2.3 用 onnxruntime 动态库做 C 集成的目录结构C 端集成时常见做法是把 ONNXRuntime 的头文件、动态库和 PP-MattingV2 的 ONNX 模型放在一起目录大致如下目录/文件用途include/onnxruntime_cxx_api.hC API 头文件lib/onnxruntime.dll/.so动态库运行时必须能找到models/ppmattingv2.onnx导出的抠图模型src/main.cpp推理入口负责预处理和后处理Windows 下如果报microsoft visual c 14.0 is required装一下 Visual C Redistributable 就能解决这是 ONNXRuntime 动态库的运行时依赖。Linux 下则要把libonnxruntime.so的路径加进LD_LIBRARY_PATH否则运行时会提示找不到库。3. Python 端 PP-MattingV2 推理预处理、会话与 alpha 图后处理3.1 图像预处理与归一化参数对齐Python 端推理的第一步是把输入图转成模型要的张量。PP-MattingV2 期望的是 RGB、float32、NCHW并且按 ImageNet 的 mean/std 归一化。很多人直接用cv2.imread读进来是 BGR忘了转 RGB结果 alpha 图整体错位。下面这段是标准做法import cv2 import numpy as np def preprocess(image_path, input_size(512, 512)): # 读取图像并转成 RGB img cv2.imread(image_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 缩放到模型输入尺寸 img cv2.resize(img, input_size, interpolationcv2.INTER_LINEAR) # 归一化到 [0,1] 再按 mean/std 标准化 img img.astype(np.float32) / 255.0 mean np.array([0.485, 0.456, 0.406], dtypenp.float32) std np.array([0.229, 0.224, 0.225], dtypenp.float32) img (img - mean) / std # HWC 转 NCHW并增加 batch 维度 img img.transpose(2, 0, 1)[np.newaxis, ...] return np.ascontiguousarray(img)input_size要和导出时的--input_shape一致如果导出用了动态轴这里可以传原图尺寸但建议还是缩放到固定尺寸速度更稳。np.ascontiguousarray是为了保证内存连续ONNXRuntime 对非连续数组会多一次拷贝。3.2 创建 InferenceSession 并跑通一次推理会话创建时有两个参数值得调intra_op_num_threads控制单算子内部线程数graph_optimization_level控制图优化级别。CPU 推理时把线程数设成物理核数优化级别用ORT_ENABLE_ALL能明显降延迟。import onnxruntime as ort import numpy as np # 创建会话指定线程数和优化级别 options ort.SessionOptions() options.intra_op_num_threads 4 options.graph_optimization_level ort.GraphOptimizationLevel.ORT_ENABLE_ALL sess ort.InferenceSession(inference_model/model.onnx, sess_optionsoptions, providers[CPUExecutionProvider]) # 准备输入 input_tensor preprocess(test.jpg) input_name sess.get_inputs()[0].name # 推理拿到 alpha 输出 alpha sess.run(None, {input_name: input_tensor})[0] print(alpha shape:, alpha.shape, min/max:, alpha.min(), alpha.max())sess.run的第一个参数是输出名列表传None表示拿所有输出。PP-MattingV2 如果只有一个输出alpha就是 1x1xHxW 的数组。打印 min/max 能快速判断是否正常如果全是 0 或全是 1多半是归一化或输入通道搞错了。3.3 alpha 图还原到原图尺寸并合成模型输出的 alpha 是缩放后的尺寸要还原回原图大小再和原图做合成。合成时用alpha / 255作为权重背景可以换成纯色或另一张图。def postprocess(alpha, original_path, background_color(0, 255, 0)): # 去掉 batch 和 channel 维度得到 HxW alpha alpha[0, 0] # 裁剪到 [0,1] 并转成 0-255 alpha np.clip(alpha, 0, 1) alpha (alpha * 255).astype(np.uint8) # 读原图并缩放到 alpha 尺寸 original cv2.imread(original_path) original cv2.resize(original, (alpha.shape[1], alpha.shape[0])) # 生成纯色背景 background np.zeros_like(original) background[:] background_color # 按 alpha 合成 alpha_3c cv2.merge([alpha, alpha, alpha]) / 255.0 result original * alpha_3c background * (1 - alpha_3c) return result.astype(np.uint8)alpha_3c是把单通道 alpha 复制成三通道方便和 BGR 图逐像素乘。background_color按 BGR 顺序给绿色就是(0, 255, 0)。如果合成后人像边缘有白边通常是预处理时 resize 的插值方式导致的把INTER_LINEAR换成INTER_AREA再试。4. C 端 ONNXRuntime 加载 PP-MattingV2会话、张量与内存管理4.1 C 环境配置与动态库链接C 端用 ONNXRuntime第一步是把头文件和库链上。Windows 下在 Visual Studio 里加 include 目录和 lib 目录再把onnxruntime.dll放到 exe 同目录Linux 下在 CMakeLists 里用find_package或直接指定路径。下面是一个最小 CMake 片段cmake_minimum_required(VERSION 3.10) project(ppmatting_demo) set(ONNXRUNTIME_DIR /path/to/onnxruntime) include_directories(${ONNXRUNTIME_DIR}/include) link_directories(${ONNXRUNTIME_DIR}/lib) add_executable(ppmatting_demo src/main.cpp) target_link_libraries(ppmatting_demo onnxruntime)ONNXRUNTIME_DIR换成实际解压路径。Linux 下链接的是libonnxruntime.soWindows 下是onnxruntime.lib。如果编译时报microsoft visual c 14.0 is required那是缺 VC 运行库装 Redistributable 即可和 ONNXRuntime 本身无关。4.2 创建会话与构造输入张量C 端创建会话比 Python 啰嗦一些但流程固定初始化环境、设置会话选项、加载模型、拿输入输出名。输入张量用Ort::Value::CreateTensor构造内存要自己管理。#include onnxruntime_cxx_api.h #include opencv2/opencv.hpp int main() { // 初始化环境和会话选项 Ort::Env env(ORT_LOGGING_LEVEL_WARNING, ppmatting); Ort::SessionOptions session_options; session_options.SetIntraOpNumThreads(4); session_options.SetGraphOptimizationLevel( GraphOptimizationLevel::ORT_ENABLE_ALL); // 加载 PP-MattingV2 ONNX 模型 Ort::Session session(env, models/ppmattingv2.onnx, session_options); // 获取输入输出名称 Ort::AllocatorWithDefaultOptions allocator; auto input_name session.GetInputNameAllocated(0, allocator); auto output_name session.GetOutputNameAllocated(0, allocator); // 准备输入数据这里假设已经做好归一化 std::vectorint64_t input_shape {1, 3, 512, 512}; std::vectorfloat input_data(1 * 3 * 512 * 512, 0.0f); // 用输入数据构造张量 auto memory_info Ort::MemoryInfo::CreateCpu( OrtArenaAllocator, OrtMemTypeDefault); Ort::Value input_tensor Ort::Value::CreateTensorfloat( memory_info, input_data.data(), input_data.size(), input_shape.data(), input_shape.size()); // 执行推理 const char* input_names[] {input_name.get()}; const char* output_names[] {output_name.get()}; auto outputs session.Run(Ort::RunOptions{nullptr}, input_names, input_tensor, 1, output_names, 1); // 拿到 alpha 输出 float* alpha_data outputs[0].GetTensorMutableDatafloat(); auto alpha_shape outputs[0].GetTensorTypeAndShapeInfo().GetShape(); // 后续做后处理... return 0; }GetInputNameAllocated返回的是智能指针不用手动释放。CreateTensor里的input_data必须在session.Run期间保持有效否则会读到野指针。outputs是std::vectorOrt::ValueGetTensorMutableData拿到的是模型输出缓冲区的指针不要在外面 free。4.3 预处理和后处理在 C 里的对应实现C 端没有 numpy预处理要自己写循环。核心是把 BGR 转 RGB、归一化、HWC 转 CHW。下面这段是常见写法cv::Mat img cv::imread(test.jpg); cv::cvtColor(img, img, cv::COLOR_BGR2RGB); cv::resize(img, img, cv::Size(512, 512)); std::vectorfloat input_data(1 * 3 * 512 * 512); float mean[3] {0.485f, 0.456f, 0.406f}; float std[3] {0.229f, 0.224f, 0.225f}; for (int c 0; c 3; c) { for (int h 0; h 512; h) { for (int w 0; w 512; w) { float val img.atcv::Vec3b(h, w)[c] / 255.0f; input_data[c * 512 * 512 h * 512 w] (val - mean[c]) / std[c]; } } }三层循环虽然直观但性能一般生产里可以用指针遍历或者 OpenCV 的split加convertTo。后处理拿到 alpha 后同样要裁剪到 [0,1]、转 0-255、再和原图合成。C 里合成可以用cv::Mat的逐像素操作也可以用cv::addWeighted但 alpha 是三通道时要注意通道顺序。注意C 端最容易出的问题是输入张量的内存生命周期。input_data如果是局部变量session.Run必须在同一个作用域内调用否则会崩。5. 推理结果对齐与性能调优让 PP-MattingV2 在 ONNXRuntime 上跑得又准又快5.1 用 Python 和 C 交叉验证 alpha 输出导出 ONNX 后第一件事是确认 ONNXRuntime 的输出和 Paddle 原模型一致。做法是用同一张图分别跑 Paddle 推理和 ONNXRuntime 推理比较 alpha 图的差异。如果平均绝对误差在 1e-3 以内基本可以认为对齐了如果差很多优先查归一化参数和输入通道顺序。# 假设 paddle_alpha 和 onnx_alpha 都是 HxW 的 numpy 数组 diff np.abs(paddle_alpha - onnx_alpha) print(max diff:, diff.max(), mean diff:, diff.mean())max diff偶尔大一点没关系可能是边缘插值差异mean diff如果超过 0.01就要回头检查预处理。常见原因是 Paddle 端用了cv2.INTER_LINEAR而 ONNX 端用了别的插值或者 mean/std 写反了。5.2 动态 shape 与固定 shape 的取舍导出时如果用了动态轴ONNXRuntime 每次遇到新尺寸都会重新做一次 shape 推断首次推理会慢。固定 shape 则可以在会话创建时完成所有优化延迟更稳。实际业务里如果输入尺寸变化不大建议固定成最常用的尺寸比如 512x512 或 1024x1024然后在预处理里做缩放。模式优点缺点适用场景固定 shape延迟低且稳定换尺寸要重新导出直播、视频会议动态 shape灵活首次推理慢图片批处理如果一定要动态可以在SessionOptions里开EnableMemPattern和EnableCpuMemArena减少内存分配开销。GPU 端则要注意 TensorRT 对动态 shape 的支持有限通常还是固定尺寸更省心。5.3 线程数与执行提供者的调优参数CPU 推理时intra_op_num_threads设成物理核数通常最好超线程有时候反而拖慢。inter_op_num_threads控制多个算子并行PP-MattingV2 这种单链路的模型设成 1 就行。GPU 端则要确认CUDAExecutionProvider的device_id和cudnn_conv_algo_search后者设成EXHAUSTIVE会花时间找最快卷积算法适合固定 shape 的长期服务。options ort.SessionOptions() options.intra_op_num_threads 8 options.inter_op_num_threads 1 options.graph_optimization_level ort.GraphOptimizationLevel.ORT_ENABLE_ALL # GPU 端额外配置 providers [(CUDAExecutionProvider, { device_id: 0, cudnn_conv_algo_search: EXHAUSTIVE })] sess ort.InferenceSession(model.onnx, sess_optionsoptions, providersproviders)cudnn_conv_algo_search设成EXHAUSTIVE后第一次推理会明显变慢但之后每次都快。如果服务是短生命周期的用HEURISTIC更合适。调完这些参数可以用time.perf_counter()包住sess.run测几十次取平均对比调优前后的延迟。5.4 边缘发丝区域的常见问题与修正PP-MattingV2 的强项是发丝和半透明区域但 ONNXRuntime 推理后如果发现边缘发灰或有锯齿通常是两个原因一是输入分辨率太低512 的模型处理 1080p 图会丢细节可以改成 1024 导出二是后处理时 alpha 被过早二值化软掩码变成了硬边。修正方法是保留 alpha 的浮点值只在最后合成时做一次缩放中间不要astype(np.uint8)。# 错误做法过早转 uint8 会丢精度 alpha_uint8 (alpha * 255).astype(np.uint8) # 正确做法保持 float32 直到合成 alpha_float np.clip(alpha, 0, 1) result original * alpha_float[..., None] background * (1 - alpha_float[..., None])alpha_float[..., None]是给 alpha 加一个通道维度方便广播。这样合成出来的边缘过渡更自然发丝区域不会出现硬切。如果业务对速度要求高可以在合成前把 alpha 缩到原图尺寸但缩放时用INTER_LINEAR而不是INTER_NEAREST后者会产生锯齿。本文还有配套的精品资源点击获取
返回列表