
简介这份资源面向需要在移动端或嵌入式设备上落地人像分割功能的C开发者提供基于PP-HumanSeg lite轻量模型的NCNN部署方案。包内共7个文件约16.39MB包含onnx与ncnn两类模型文件param、bin以及cpp、h源码覆盖从模型加载到推理输出的完整链路。代码以HumanSeg类封装分割逻辑主入口负责调用与结果处理模型目录则存放转换后的网络权重方便直接替换或二次训练。已有1457人学习下载说明该方案在实时人像分割场景中具备一定参考价值。读者可据此快速搭建可运行的C推理工程理解ONNX到NCNN的模型转换与部署流程掌握移动端前向计算、输入预处理与掩码后处理等关键环节并在此基础上接入虚拟背景、视频会议或直播抠像等应用。1. PP-HumanSeg lite 上 NCNN为什么这套组合值得你花一个下午跑通如果你手上有一台没有独显的工控机、树莓派或者老款笔记本却要跑实时人像分割大概率会经历一轮模型选型焦虑MediaPipe 自带的 Selfie Segmentation 精度一般换背景边缘毛刺明显换成大模型CPU 上单帧几百毫秒视频直接卡成 PPT。PP-HumanSeg lite 就是为这个场景准备的——它是 PaddleSeg 系列里专门做人像分割的轻量模型输入 192×192 或 398×398参数量小、边缘干净官方在 Paddle 侧给了完整的训练和导出链路。但真正落地到 C 工程里很多人卡在「Paddle 推理库太重、依赖难装」这一步。NCNN 就是绕开这个问题的常见做法腾讯开源的纯 C 推理框架无第三方依赖编译出来一个静态库模型转成.param.bin两个文件塞进你的 C 工程里就能跑。这套「PP-HumanSeg lite NCNN C」的组合适合做视频会议虚拟背景、直播抠像、证件照换底、边缘设备人像预处理这类需求。下面我按自己实际部署的顺序把模型导出、转换、C 推理、后处理、踩坑一条线讲清楚你照着能复现。2. 从 Paddle 权重到 NCNN 模型导出与转换的完整链路2.1 为什么不能直接拿 Paddle 的 inference model 喂给 NCNNPaddleSeg 训练完导出的是model.pdparams或者inference.pdmodelinference.pdiparams这是 Paddle 自己的序列化格式NCNN 读不了。中间必须经过 ONNX 这一层。整条链路是Paddle 动态图权重 → 静态图 inference model → ONNX → NCNN。每一步都有坑尤其是动态输入尺寸和算子支持。先确认你的环境。PaddleSeg 对 PaddlePaddle 版本敏感我一般锁在 2.5.x 配 PaddleSeg 2.8太新的版本导出 ONNX 时容易遇到paddle2onnx算子映射缺失。安装命令如下# 建议在 conda 独立环境里做避免污染主环境 conda create -n humanseg python3.9 -y conda activate humanseg # PaddlePaddle CPU 版即可导出阶段不需要 GPU pip install paddlepaddle2.5.2 -i https://mirror.baidu.com/pypi/simple pip install paddleseg2.8.0 pip install paddle2onnx1.0.6 pip install onnx1.14.0 onnxruntime1.16.0参数说明paddlepaddle选 CPU 版是因为导出只做图变换不跑前向paddle2onnx版本必须和 Paddle 匹配1.0.6 对应 2.5.x 比较稳onnx锁 1.14 是因为更高版本和paddle2onnx的 IR 版本偶尔冲突。2.2 导出 PP-HumanSeg lite 的 inference modelPP-HumanSeg lite 的配置文件在 PaddleSeg 仓库的configs/pp_humanseg/下lite 版本对应pp_humanseg_lite_192x192.yml或398x398。假设你已经下载了官方提供的预训练权重PaddleSeg 的 model zoo 里有文件名类似pp_humanseg_lite_192x192_78930导出命令# 进入 PaddleSeg 根目录 python tools/export.py \ --config configs/pp_humanseg/pp_humanseg_lite_192x192.yml \ --model_path pretrained/pp_humanseg_lite_192x192/model.pdparams \ --save_dir output/humanseg_lite执行完output/humanseg_lite/下会有model.pdmodel和model.pdiparams。这里有个关键点export.py默认会做一次前向如果配置文件里的val_dataset路径不存在会报错可以在 yml 里把val_dataset注释掉或者加--input_shape 1 3 192 192显式指定输入形状。我一般显式指定避免动态 shape 导出后 ONNX 里带一堆-1NCNN 转换时对动态维度支持不好。2.3 转 ONNX 时把动态轴固定住paddle2onnx \ --model_dir output/humanseg_lite \ --model_filename model.pdmodel \ --params_filename model.pdiparams \ --save_file humanseg_lite.onnx \ --opset_version 11 \ --input_shape_dict {x: [1, 3, 192, 192]}参数说明--opset_version 11是 NCNN 的onnx2ncnn支持最完整的版本别贪高--input_shape_dict把输入固定成1×3×192×192batch 固定为 1因为部署时基本是单帧推理。如果你的场景需要 398×398把最后两个数改掉但注意 NCNN 转换后模型体积和推理时间都会涨。转完用 onnxruntime 验一下输出形状import onnxruntime as ort import numpy as np sess ort.InferenceSession(humanseg_lite.onnx) # 输入名从 sess.get_inputs()[0].name 拿通常是 x inp np.random.randn(1, 3, 192, 192).astype(np.float32) out sess.run(None, {x: inp}) print(out[0].shape) # 期望 (1, 2, 192, 192) 或 (1, 1, 192, 192)逻辑说明PP-HumanSeg lite 输出是 2 通道背景/人像的 logits经过 argmax 得到 mask。如果输出是 1 通道说明导出时把 softmax 融进去了后处理要相应调整。这一步不做验证后面 NCNN 出来结果不对你根本不知道是哪一层出的问题。2.4 onnx2ncnn 转换与模型精简NCNN 官方提供onnx2ncnn工具需要先编译 NCNN 源码。编译流程后面讲这里假设你已经有了onnx2ncnn可执行文件onnx2ncnn humanseg_lite.onnx humanseg_lite.param humanseg_lite.bin # 用 ncnnoptimize 做算子融合和 fp16 压缩 ncnnoptimize humanseg_lite.param humanseg_lite.bin \ humanseg_lite_opt.param humanseg_lite_opt.bin 65536参数说明ncnnoptimize最后一个参数65536表示 fp16 存储65536是 flag不是精度值模型体积能砍掉近一半CPU 推理速度也有提升。但注意如果你的目标平台是某些老 ARM 芯片fp16 可能不被支持那就传0保持 fp32。转换后打开.param文件看一眼第一行应该是7767517开头的魔数第二行是层数和 blob 数。如果层数明显偏少说明转换时丢了算子得回去查onnx2ncnn的报错日志。3. NCNN C 工程搭建从编译到第一次推理出图3.1 编译 NCNN静态库 Vulkan 按需选NCNN 的编译本身不复杂但选项决定你后面部署的难易。我一般这么配git clone https://github.com/Tencent/ncnn.git cd ncnn mkdir build cd build cmake -DCMAKE_BUILD_TYPERelease \ -DNCNN_VULKANOFF \ -DNCNN_BUILD_TOOLSON \ -DNCNN_BUILD_EXAMPLESOFF \ -DNCNN_OPENMPON \ -DCMAKE_INSTALL_PREFIX../install .. make -j$(nproc) make install参数说明NCNN_VULKANOFF是因为人像分割在 CPU 上 192×192 输入已经能到 30fps 以上开 Vulkan 反而增加部署复杂度需要 Vulkan SDK 和驱动NCNN_BUILD_TOOLSON是为了拿到onnx2ncnn和ncnnoptimizeNCNN_OPENMPON开启多线程CPU 推理必开。编译完install/下有include/、lib/、bin/C 工程直接链libncnn.a即可。3.2 C 推理代码加载、预处理、前向、后处理下面是一段能直接跑的最小推理代码输入一张 BGR 的cv::Mat输出人像 mask#include opencv2/opencv.hpp #include net.h // 输入 192x192输出 2 通道 logits static const int TARGET_W 192; static const int TARGET_H 192; cv::Mat humanseg_infer(ncnn::Net net, const cv::Mat bgr) { // 1. 预处理resize 到 192x192保持简单缩放 cv::Mat resized; cv::resize(bgr, resized, cv::Size(TARGET_W, TARGET_H)); // 2. 转 ncnn::Mat注意 NCNN 是 RGB 顺序且归一化到 [0,1] ncnn::Mat in ncnn::Mat::from_pixels( resized.data, ncnn::Mat::PIXEL_BGR2RGB, TARGET_W, TARGET_H); // PP-HumanSeg 训练时用的 mean/std必须和训练配置一致 const float mean_vals[3] {0.5f * 255.f, 0.5f * 255.f, 0.5f * 255.f}; const float norm_vals[3] {1.f / 255.f, 1.f / 255.f, 1.f / 255.f}; in.substract_mean_normalize(mean_vals, norm_vals); // 3. 前向 ncnn::Extractor ex net.create_extractor(); ex.set_num_threads(4); ex.input(x, in); // x 是 onnx 里的输入名转换后保持一致 ncnn::Mat out; ex.extract(save_infer_model/scale_0.tmp_1, out); // 输出 blob 名以 param 文件为准 // 4. 后处理2 通道取 argmax得到 0/1 mask cv::Mat mask(TARGET_H, TARGET_W, CV_8UC1); for (int y 0; y TARGET_H; y) { const float* p0 out.channel(0).row(y); const float* p1 out.channel(1).row(y); uchar* pm mask.ptruchar(y); for (int x 0; x TARGET_W; x) { pm[x] (p1[x] p0[x]) ? 255 : 0; } } // 5. 放大回原图尺寸 cv::Mat full_mask; cv::resize(mask, full_mask, bgr.size(), 0, 0, cv::INTER_LINEAR); return full_mask; }逻辑说明预处理里的mean_vals和norm_vals是最容易翻车的地方。PP-HumanSeg 训练配置里Normalize的 mean 是[0.5, 0.5, 0.5]、std 是[0.5, 0.5, 0.5]换算到 0-255 尺度就是上面写的。如果你直接抄别的模型的[104, 117, 123]输出会是一团糊。输出 blob 名不要猜打开.param文件最后几行看通常是save_infer_model/scale_0.tmp_1这种不同导出方式名字会变。3.3 输入输出 blob 名怎么确认很多人卡在ex.input(x, in)报找不到 blob。确认方法打开humanseg_lite_opt.param第一行是魔数第二行是层数 blob数从第三行开始每行是一个层定义格式是层类型 层名 输入blob数 输出blob数 ...。输入层通常是Input x 0 1 x所以输入名是x。输出 blob 找最后一层的输出名或者用 Netron 打开 ONNX 看输出节点名转换后一般会保留。如果实在找不到可以在 C 里遍历net.blobs()打印所有 blob 名但 NCNN 的 API 不直接暴露稳妥办法还是看 param 文件。3.4 后处理优化边缘羽化和阈值argmax 出来的 mask 是硬边直接贴到原图上会有锯齿。实际项目里我一般做两件事一是对 logits 做 softmax 得到概率再用alpha sigmoid做透明度混合二是对 mask 做一次 3×3 的高斯模糊边缘过渡自然很多。如果追求速度至少把cv::resize的插值从INTER_NEAREST换成INTER_LINEAR成本几乎为零但观感提升明显。4. 避坑与排查部署 PP-HumanSeg lite 时最容易翻车的 5 个点4.1 现象推理结果全黑或全白mask 没有任何人像原因预处理归一化参数和训练不一致或者输入通道顺序错了。PP-HumanSeg 训练用的是 RGB mean 0.5/std 0.5如果你用 BGR 直接喂进去或者 mean 用了 ImageNet 的[0.485, 0.456, 0.406]输出 logits 会整体偏移argmax 后要么全背景要么全人像。解决在 Python 侧用同一张图跑一遍 Paddle 推理把中间 tensor 打出来和 C 侧对比。最直接的办法是把 C 预处理后的ncnn::Mat存成图片和 Python 预处理后的图做像素级 diff差超过 1 就说明归一化有问题。4.2 现象onnx2ncnn报Unsupported operator Resize或转换后层数缺失原因paddle2onnx导出的 ONNX 里 Resize 算子的coordinate_transformation_mode或mode属性 NCNN 不支持。PP-HumanSeg lite 的上采样用的是双线性插值某些 opset 下会导出成 NCNN 不认的变体。解决把--opset_version降到 11并且在 PaddleSeg 的 yml 里确认上采样方式是bilinear而不是nearest。如果还是不行用onnx-simplifier先过一遍pip install onnxsim onnxsim humanseg_lite.onnx humanseg_lite_sim.onnx把冗余算子消掉再转。4.3 现象C 程序编译通过运行时报find_blob_index_by_name x failed原因输入 blob 名不对。ONNX 转换到 NCNN 后输入名可能被改写比如加了前缀或者变成input.1。解决打开.param文件找到Input那一行第一个字段后面的名字就是输入 blob 名。如果 param 里写的是Input input 0 1 x那输入名是x如果写的是Input input 0 1 input.1那就要用input.1。别凭记忆写一定看文件。4.4 现象推理速度远低于预期192×192 输入单帧超过 100ms原因NCNN 编译时没开 OpenMP或者set_num_threads设成了 1或者模型没做ncnnoptimize融合。解决确认编译选项-DNCNN_OPENMPONC 里ex.set_num_threads(4)按 CPU 核心数设一般 4 或 8。另外检查是否误用了 fp32 模型ncnnoptimize后的 fp16 模型在支持 NEON 的 ARM 上速度差异明显。如果还慢用ncnn::get_cpu_count()看实际可用核心数容器环境里可能被限制了。4.5 现象视频流处理时内存持续增长跑几小时 OOM原因每帧都create_extractor()但没有复用或者ncnn::Mat在循环里反复分配大块内存没释放。解决ncnn::Extractor可以复用但注意它不是线程安全的多线程要每个线程一个 extractor。更关键的是ncnn::Mat的分配如果每帧输入尺寸固定可以预分配一个ncnn::Mat反复用from_pixels填充。另外 OpenCV 的cv::Mat在 resize 时也会分配建议用cv::Mat::create预分配输出 buffer。5. 进阶技巧让 PP-HumanSeg lite 在 NCNN 上再快 30% 的两个手段第一个手段是输入尺寸的动态选择。192×192 是精度和速度的平衡点但如果你做的是视频会议背景虚化人物占画面比例大其实可以降到 160×160 甚至 128×128速度线性下降边缘质量在虚化场景下肉眼几乎看不出差别。改法很简单导出 ONNX 时把--input_shape_dict改成{x: [1, 3, 160, 160]}C 里TARGET_W/TARGET_H同步改后处理 resize 回原图。我实测在 4 核 ARM 上192 到 160 能省 25% 左右的时间。第二个手段是输出层裁剪。PP-HumanSeg lite 最后输出 2 通道但如果你只需要人像的 alpha 通道可以在ncnnoptimize之后手动改.param把最后一层Softmax或ArgMax去掉直接取第 1 通道的 logits 做 sigmoid。这样省掉一次全图 softmax192×192 下大概省 3-5ms。改 param 有风险改完一定用同一张图对比输出确认数值一致再上线。验证方法上我习惯做一个「黄金样本」回归固定一张带人像的测试图Python 侧 Paddle 推理存下 mask 作为基准C 侧每次改完代码或模型都跑一遍算 mask 的 IoU。IoU 低于 0.98 就说明改动引入了偏差得回查。这个习惯帮我拦下过好几次「以为只是改了预处理、结果把归一化改错」的事故。最后说个血泪教训NCNN 的.param文件是纯文本很多人图省事直接手改改完不验证就打包发版。我有一次把输出 blob 名改错了一个字符程序不报错只是输出全零排查了一下午才发现。后来我定了个规矩任何对 param/bin 的改动必须过一遍黄金样本回归IoU 达标才算完。这套流程跑顺之后PP-HumanSeg lite NCNN 在边缘设备上做人像分割稳定性和速度都够用值得你投入。希望帮到你。本文还有配套的精品资源点击获取