简介:本资源面向C#开发者与图像处理方向的工程人员,提供在.NET环境中通过OnnxRuntime部署RMBG-2.0模型、实现高精度人像抠图与背景去除的完整工程包。RMBG-2.0基于生成对抗网络的多尺度融合思路,能较好保留发丝、衣物纹理等细节边缘,可应用于视频通话、虚拟背景、互动娱乐等实时场景。压缩包共234个文件,约313.76MB,包含47个dll动态库、9个cs源码、9个nupkg包、若干xml与json配置、so与dylib跨平台库、png与jpg示例图及sln解决方案等,覆盖模型推理、依赖管理与跨平台运行所需组件。资源已有149人学习,适合希望快速跑通推理流程、理解OnnxRuntime集成方式并对照示例代码排查环境配置问题的开发者参考。
1. 从一张 PNG 到可上线的抠图服务:RMBG-2.0 在 C# 里到底怎么跑
电商详情页要批量换白底、证件照要抠发丝、短视频封面要一键去背,这些需求背后都指向同一个动作:把主体从背景里干净地分离出来。RMBG-2.0 是 BRIA 开源的第二代人像与通用主体分割模型,相比第一代在边缘过渡、半透明区域和复杂发丝上的表现提升明显,官方权重以 PyTorch 形式发布。但真正落到生产环境,很多团队的后端是 .NET 技术栈,不可能为了一个抠图模型单独维护一套 Python 服务。把 RMBG-2.0 导出成 ONNX,再用 C# 的 OnnxRuntime 加载推理,就能在纯 .NET 进程里完成高精度背景去除,省掉跨语言调用的网络开销和部署复杂度。这篇笔记拆的就是这条链路:模型怎么导出、C# 侧张量怎么构造、前后处理有哪些参数、显存和耗时怎么控。适合已经会写 C#、想把人像抠图能力直接嵌进现有服务的工程师,也适合正在做上位机或桌面工具、需要离线抠图的人。
2. 模型导出与 OnnxRuntime 环境搭建:把 PyTorch 权重变成 C# 能吃的 ONNX
2.1 为什么选 ONNX 而不是直接调 Python
RMBG-2.0 官方仓库给的是 PyTorch 推理脚本,输入输出都是 tensor,逻辑不复杂。但生产环境里,Python 服务意味着额外的运行时、依赖冲突、进程间通信,以及一套独立的部署流水线。ONNX 的价值在于把模型结构和权重固化成一个自包含文件,C# 侧只需要一个 OnnxRuntime 的 NuGet 包就能加载,不依赖 Python 解释器,也不依赖 CUDA 之外的任何框架。常见做法是:在开发机或 CI 上用 Python 完成一次导出,把生成的 .onnx 文件作为静态资源随应用发布,运行时只做推理。
选 OnnxRuntime 而不是其他 .NET 推理库,理由也直接:它对 CPU 和 GPU 的 EP(Execution Provider)支持最全,DirectML、CUDA、TensorRT 都能挂,API 稳定,社区案例多。对于抠图这种单模型、单输入单输出的场景,OnnxRuntime 的InferenceSession足够用,不需要引入更重的封装。
2.2 导出 RMBG-2.0 到 ONNX 的完整脚本
导出这一步必须在 Python 环境里做,建议用独立的虚拟环境,避免污染主环境。RMBG-2.0 的模型结构基于 BiRefNet 系,输入是固定尺寸的 RGB 图像,输出是单通道的显著性图。导出时要把动态轴设好,否则 C# 侧换分辨率会直接报错。
# export_rmbg2_onnx.py # 在 Python 虚拟环境中执行,依赖 torch、transformers、onnx import torch from transformers import AutoModelForImageSegmentation # 加载 RMBG-2.0 权重,trust_remote_code 是因为模型用了自定义结构 model = AutoModelForImageSegmentation.from_pretrained( "briaai/RMBG-2.0", trust_remote_code=True ) model.eval() # 构造一个示例输入,尺寸用 1024x1024,和官方推理默认一致 dummy_input = torch.randn(1, 3, 1024, 1024) # 导出 ONNX,opset 用 17,兼容性最好 torch.onnx.export( model, dummy_input, "rmbg2.onnx", input_names=["input"], output_names=["output"], dynamic_axes={ "input": {0: "batch", 2: "height", 3: "width"}, "output": {0: "batch", 2: "height", 3: "width"} }, opset_version=17, do_constant_folding=True ) print("export done")这段脚本的关键点有三个。第一,trust_remote_code=True不能省,RMBG-2.0 的模型定义不在 transformers 主干里,需要从仓库拉取自定义代码。第二,dynamic_axes把 batch、height、width 都设成动态,这样 C# 侧可以传不同分辨率的图,但要注意:动态轴只是让 ONNX 接受变尺寸,模型内部如果有固定位置编码,换尺寸后精度可能下降,所以实际推理时还是建议把输入缩放到 1024 附近。第三,opset_version=17是经过验证的稳定值,低于 15 可能不支持某些算子,高于 18 部分 OnnxRuntime 版本还没跟上。
导出完成后,用onnxruntime的 Python 包做一次快速校验,确认输入输出名字和形状:
import onnxruntime as ort sess = ort.InferenceSession("rmbg2.onnx", providers=["CPUExecutionProvider"]) print([i.name for i in sess.get_inputs()]) # ['input'] print([o.name for o in sess.get_outputs()]) # ['output'] print(sess.get_inputs()[0].shape) # ['batch', 3, 'height', 'width']如果输出形状里 height/width 是具体数字而不是字符串,说明动态轴没生效,回到导出脚本检查dynamic_axes的键名是否和input_names一致。
2.3 C# 侧 NuGet 依赖与 Session 初始化
C# 项目里需要装两个包:Microsoft.ML.OnnxRuntime是核心推理库,Microsoft.ML.OnnxRuntime.Managed提供托管封装。如果要用 GPU,再按显卡类型加Microsoft.ML.OnnxRuntime.Gpu或Microsoft.ML.OnnxRuntime.DirectML。版本上建议锁定一个稳定版,不要用 preview,避免 API 变动。
# 在项目目录下执行 dotnet add package Microsoft.ML.OnnxRuntime --version 1.17.1 dotnet add package Microsoft.ML.OnnxRuntime.Managed --version 1.17.1 # 如果用 NVIDIA 显卡 dotnet add package Microsoft.ML.OnnxRuntime.Gpu --version 1.17.1Session 的初始化只做一次,放在单例或 DI 容器里,不要每次推理都 new。SessionOptions里可以配线程数、图优化级别、是否启用内存复用。
using Microsoft.ML.OnnxRuntime; public sealed class RmbgSession : IDisposable { private readonly InferenceSession _session; public RmbgSession(string modelPath, bool useGpu = false) { var options = new SessionOptions(); // 图优化开到最高,首次加载稍慢,后续推理更快 options.GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_ALL; // 线程数按 CPU 核数设,一般 4 到 8 之间 options.IntraOpNumThreads = 4; options.InterOpNumThreads = 1; if (useGpu) { // CUDA EP,需要机器上有对应版本的 CUDA 和 cuDNN options.AppendExecutionProvider_CUDA(0); } _session = new InferenceSession(modelPath, options); } public void Dispose() => _session.Dispose(); }GraphOptimizationLevel.ORT_ENABLE_ALL会把算子融合、常量折叠都做掉,对抠图这种计算密集但结构固定的模型收益明显。IntraOpNumThreads控制单个算子内部并行度,设太大反而会因为线程调度开销变慢,4 到 8 是常见甜点区。GPU 模式下AppendExecutionProvider_CUDA(0)里的 0 是设备序号,多卡机器要按实际选。
提示:如果导出时用了动态轴,但 C# 侧始终传固定 1024x1024,可以把动态轴去掉重新导出,固定形状的模型推理速度会略快,内存分配也更可控。
3. 张量构造与前后处理:C# 里把 Bitmap 喂给 ONNX 的每一步
3.1 输入张量的形状、归一化与内存布局
RMBG-2.0 的输入是[1, 3, H, W]的 float32 张量,通道顺序是 RGB,数值范围归一化到 0 到 1 之间,再按 ImageNet 的均值和标准差做标准化。这一步在 Python 里通常用 torchvision 的 transform 一行搞定,在 C# 里要手动展开。最容易翻车的地方是内存布局:ONNX 要的是 NCHW,而 Bitmap 的像素是行优先的 BGRA,需要逐通道拆开并按平面排列。
using System.Drawing; using System.Drawing.Imaging; using Microsoft.ML.OnnxRuntime.Tensors; public static DenseTensor<float> Preprocess(Bitmap bitmap, int targetSize = 1024) { // 先缩放到目标尺寸,保持长宽比会引入 padding,这里直接拉伸 using var resized = new Bitmap(bitmap, new Size(targetSize, targetSize)); var tensor = new DenseTensor<float>(new[] { 1, 3, targetSize, targetSize }); // ImageNet 标准化参数 float[] mean = { 0.485f, 0.456f, 0.406f }; float[] std = { 0.229f, 0.224f, 0.225f }; var data = resized.LockBits( new Rectangle(0, 0, targetSize, targetSize), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb); try { unsafe { byte* ptr = (byte*)data.Scan0; int stride = data.Stride; for (int y = 0; y < targetSize; y++) { for (int x = 0; x < targetSize; x++) { // Format24bppRgb 的内存顺序是 BGR byte b = ptr[y * stride + x * 3 + 0]; byte g = ptr[y * stride + x * 3 + 1]; byte r = ptr[y * stride + x * 3 + 2]; tensor[0, 0, y, x] = (r / 255f - mean[0]) / std[0]; tensor[0, 1, y, x] = (g / 255f - mean[1]) / std[1]; tensor[0, 2, y, x] = (b / 255f - mean[2]) / std[2]; } } } } finally { resized.UnlockBits(data); } return tensor; }这段代码里有几个必须注意的点。第一,PixelFormat.Format24bppRgb的实际内存顺序是 BGR 而不是 RGB,命名有历史误导性,取通道时 b 在偏移 0、r 在偏移 2,写反了会导致颜色整体偏蓝或偏红,抠图边缘会出现奇怪的色边。第二,data.Stride不一定等于width * 3,Windows 下 Bitmap 每行会按 4 字节对齐,所以必须用 stride 而不是直接乘。第三,标准化用的 mean/std 是 ImageNet 的通用值,RMBG-2.0 官方推理也是这套,不要自己改,改了显著性图会整体偏移。
如果不想用 unsafe,可以用Marshal.Copy把整块内存拷到 byte 数组再处理,代价是多一次内存分配,1024x1024 大概多 3MB,对吞吐要求不高的场景可以接受。
3.2 推理调用与输出显著性图的解析
推理本身只有一行_session.Run(inputs),但输入输出的绑定方式会影响性能。推荐用NamedOnnxValue或者直接构造OrtValue,前者写法简单,后者在批量推理时更省内存。
public float[] RunInference(DenseTensor<float> input) { var inputs = new List<NamedOnnxValue> { NamedOnnxValue.CreateFromTensor("input", input) }; using var results = _session.Run(inputs); var output = results.First().AsTensor<float>(); // 输出形状是 [1, 1, H, W],展平成一维 int h = output.Dimensions[2]; int w = output.Dimensions[3]; var mask = new float[h * w]; for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) mask[y * w + x] = output[0, 0, y, x]; return mask; }输出是单通道的 logits,不是 0 到 1 的概率,需要做一次 sigmoid 或者直接按阈值二值化。RMBG-2.0 的输出经过训练后,正负样本的分界大致在 0 附近,所以常见做法是先 sigmoid 再取 0.5 阈值,或者直接对 logits 取 0 阈值。两种方式在干净人像上差别不大,但在半透明区域(比如婚纱、玻璃杯)上,sigmoid 后的软掩码能保留更多过渡信息。
// sigmoid 把 logits 映射到 0 到 1 float Sigmoid(float x) => 1f / (1f + MathF.Exp(-x)); // 生成软掩码,后续可以做羽化 var alpha = new float[mask.Length]; for (int i = 0; i < mask.Length; i++) alpha[i] = Sigmoid(mask[i]);3.3 把掩码贴回原图:尺寸还原与边缘处理
推理用的 1024x1024 和原图尺寸通常不一致,掩码要缩回原图大小再和原图做 alpha 合成。直接缩放掩码会带来边缘锯齿,常见做法是先把掩码放大到原图尺寸,再做一次小半径的高斯模糊,让边缘过渡自然。
public static Bitmap ApplyMask(Bitmap original, float[] mask, int maskSize) { int w = original.Width; int h = original.Height; var result = new Bitmap(w, h, PixelFormat.Format32bppArgb); // 把 mask 缩放到原图尺寸,这里用双线性插值 for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { // 映射回 mask 坐标 float mx = (float)x / w * maskSize; float my = (float)y / h * maskSize; int x0 = (int)mx, y0 = (int)my; int x1 = Math.Min(x0 + 1, maskSize - 1); int y1 = Math.Min(y0 + 1, maskSize - 1); float fx = mx - x0, fy = my - y0; float v = mask[y0 * maskSize + x0] * (1 - fx) * (1 - fy) + mask[y0 * maskSize + x1] * fx * (1 - fy) + mask[y1 * maskSize + x0] * (1 - fx) * fy + mask[y1 * maskSize + x1] * fx * fy; byte a = (byte)Math.Clamp(v * 255, 0, 255); var pixel = original.GetPixel(x, y); result.SetPixel(x, y, Color.FromArgb(a, pixel.R, pixel.G, pixel.B)); } } return result; }GetPixel/SetPixel在循环里性能很差,1024x1024 的图要几秒,生产环境必须换成LockBits直接操作内存,或者用System.Drawing.Common之外的图像库。这里为了把逻辑讲清楚用了慢写法,实际项目里我会把这段替换成指针操作,速度能提升两个数量级。
注意:
System.Drawing.Common在 .NET 6 之后只支持 Windows,跨平台项目要换成SkiaSharp或ImageSharp,张量构造和掩码合成的逻辑不变,只是像素访问 API 不同。
4. 避坑与排查:C# 部署 RMBG-2.0 最容易翻车的五个点
4.1 现象:推理结果全黑或全白,掩码没有任何区分度
原因通常是输入归一化写错了。RMBG-2.0 对输入分布敏感,如果忘了减均值除标准差,或者把 RGB 顺序搞反,模型输出的 logits 会整体偏向一个方向,sigmoid 后要么全接近 1 要么全接近 0。另一个可能是导出时do_constant_folding把某些依赖输入的常量也折叠了,导致输出恒定。
解决:先用一张纯色图跑一遍,打印输出的最小值和最大值。正常情况应该在 -10 到 10 之间分布。如果全是同一个值,回到导出脚本,把do_constant_folding设为 False 重新导出,再检查 C# 侧的 mean/std 是否和 Python 一致。
4.2 现象:换分辨率后报错,提示输入形状不匹配
导出时如果没设dynamic_axes,ONNX 会把 1024x1024 写死,C# 侧传其他尺寸直接抛异常。反过来,如果设了动态轴但模型内部有固定尺寸的位置编码,换尺寸后不报错但精度崩掉,边缘会出现规律性的错位。
解决:确认导出脚本里dynamic_axes的键名和input_names完全一致,大小写敏感。如果只是想在固定几个尺寸间切换,更稳的做法是导出多个固定形状的 ONNX,运行时按尺寸选,避免动态轴带来的不确定性。
4.3 现象:GPU 模式下首次推理特别慢,之后正常
这是 CUDA EP 的典型行为。OnnxRuntime 在第一次Run时才会真正把模型加载到显存、编译 kernel,后续调用才走缓存。如果每次请求都新建InferenceSession,就会反复触发这个冷启动,耗时从几十毫秒涨到几秒。
解决:InferenceSession必须单例,在应用启动时初始化一次。如果显存紧张,可以配options.EnableMemoryPattern = false关掉内存模式复用,代价是每次推理稍慢,但显存占用更平稳。
4.4 现象:边缘出现彩色噪点或半透明区域被硬切
彩色噪点一般是通道顺序错了,BGR 当 RGB 用,模型学到的颜色先验对不上。半透明区域被硬切则是阈值取太死,sigmoid 后直接二值化,婚纱、烟雾、玻璃这些区域会丢失过渡。
解决:通道顺序用一张纯红图验证,输出掩码应该只保留红色区域。半透明场景改用软掩码,把 sigmoid 后的 alpha 直接作为透明度,不要二值化,再配合 1 到 2 像素的高斯模糊做边缘羽化。
4.5 现象:批量处理时内存持续上涨,最终 OOM
DenseTensor和NamedOnnxValue如果没正确释放,或者每次推理都新建大数组,GC 跟不上分配速度,内存会一路涨。GPU 模式下还有显存碎片问题。
解决:输入张量用ArrayPool<float>租借,推理完归还。NamedOnnxValue放在using里,IDisposableTensor也要显式释放。批量推理时控制并发数,一般设成 GPU 显存能容纳的 batch 数,不要无限制并行。
5. 进阶:把单张推理做成可复用的抠图管线
单张跑通只是起点,真正上线要考虑的是吞吐、显存和画质之间的平衡。我一般会把整条链路封装成一个RmbgPipeline类,内部维护一个InferenceSession单例、一个输入张量缓冲池、一个可配置的后处理参数集。输入尺寸不固定为 1024,而是按原图长边缩放到 1024 的倍数附近,短边做 padding,推理完再把 padding 裁掉,这样既满足模型对尺寸的偏好,又不破坏原图比例。
批量推理时,把多张图拼成一个 batch 喂进去,GPU 利用率会明显高于逐张跑。但 batch 不是越大越好,RMBG-2.0 在 1024x1024 下单张显存占用大约 1.5GB 到 2GB(取决于 EP 和精度),batch 设 4 就要 6GB 以上,普通消费级显卡扛不住。我的经验是先在目标机器上跑一个显存探测:从 batch=1 开始,每次加 1,直到显存占用超过 80% 就停,取上一个值作为上限。
画质方面,软掩码的阈值和羽化半径是两个可调参数。阈值调低,保留更多半透明区域,适合婚纱、玻璃制品;阈值调高,边缘更干净,适合硬边商品。羽化半径一般 1 到 3 像素,太大主体边缘会发虚。这两个参数不要写死,暴露成配置项,让业务侧按品类调。
验证抠图质量不能只看肉眼,我习惯用两种方式交叉检查。一是把结果贴到纯色背景上,看边缘有没有残留的背景色;二是把 alpha 通道单独导出成灰度图,检查发丝区域是否有连续的过渡,而不是断断续续的斑点。如果发丝断裂严重,说明输入分辨率不够或者模型没吃满,可以尝试把输入尺寸提到 1280 再跑一次对比。
从那以后我每次接入新的分割模型,都会先用一张带发丝的人像和一张半透明物体各跑一遍,确认边缘和过渡都正常,再进批量流程。这套习惯帮我省掉了不少返工。希望帮到你。
本文还有配套的精品资源,点击获取