简介:这是一份面向C#开发者的DAViD动态注意力视频前景分割模型部署资源,依托ONNX Runtime在桌面应用中完成模型加载、图像预处理与前景提取,适合需要在监控、虚拟现实等场景集成深度学习视觉能力的工程人员。压缩包共275个文件,包含ONNX模型文件、C#源码及工程配置、dll与xml等运行库、txt说明文档,整体大小约763.71MB,结构覆盖从模型转换到推理调用的完整链路。已有41人学习下载,属于聚焦特定技术的实战型资源。借助资源内的源码与工程配置,可快速掌握在C#中解析模型输入输出、调整图像尺寸并输出分割掩码的方法,同时支持对静态图片和视频序列的动态前景分割,为后续行为分析、姿态估计等任务提供直接可用的基础能力。
1. 从一套 C# OnnxRuntime 资源开始:DAViD 软前景分割的落地价值
很多做 .NET 上位机、视觉检测软件的朋友,一提到图像分割就条件反射地认为必须起一个 Python 服务,C# 这边只负责发请求、收结果。我拿到这套 DAViD 软前景分割资源时,第一反应是终于可以把分割模型直接塞进 C# 进程里,用 OnnxRuntime 做本地推理,不用再折腾跨语言通信。DAViD 是视频抠像和软前景分割里效果比较稳的双解码器模型,输出的是每个像素 0 到 1 的连续 alpha 值,适合商品抠图、直播背景替换、视频合成这类需要柔和边缘的场景。这套资源解决的核心问题,就是让 C# 工程师不依赖 Python 运行时,也能把 DAViD 跑出可用的软前景 mask。适合手里有 C# 项目、想接分割能力又不想引入重型服务的人:上位机工程师、图像处理系统开发、视频工具开发者都能用上。
2. 软前景与 DAViD 双解码器:先看清模型再写代码
2.1 软前景和硬分割到底差在哪
常规的语义分割输出的是类别掩码,每个像素只有一个离散标签,比如 0 代表背景、1 代表前景。拿它去做抠图,边缘会出现明显的锯齿或者生硬的过渡带。软前景 mask 不同,它输出的是一张单通道灰度图,每个像素值代表该点属于前景的概率,介于 0 到 1 之间。发丝、半透明玻璃、模糊的运动物体,这类区域在软前景里的表现是中间值,边缘是渐变的。
做背景替换时,直接用这个软前景做 alpha 混合,就能得到比较自然的合成效果。DAViD 全称是 Dual Decoder Video Matting,它之所以适合做这件事,是因为它同时用两条解码分支:一条负责语义理解,另一条负责实例级别的精细边界估计。两条分支的特征融合之后,再输出 soft alpha matte。在人物抠像场景下,它对头发丝边缘的保持明显优于普通分割模型,这也是我当初选它的关键原因。
2.2 DAViD 的输入输出规格
DAViD 模型在视频抠图任务里通常输入一帧或一个短帧序列。单帧输入时,Tensor 形状是[1, 3, H, W],三通道是 RGB,像素值需要归一化到[0, 1]。输出则是[1, 1, H, W]的单通道 alpha matte,值域同样在[0, 1]。
如果资源包里导出的是带时序的版本,输入会变成[1, T, 3, H, W],T 是帧数。带时序的好处是能利用前后帧的光流信息,让 mask 在视频里更稳定,不容易闪。代价是 C# 侧要维护一个环形帧缓冲,每次推理前把最近的 T 帧塞进去,处理起来略麻烦。我这次在资源包里放的是单帧输入版本,理由很简单:C# 部署时业务方拿到的往往是不定帧率的视频源,单帧模式更容易接进现有工程,时序增强可以放到后处理阶段去平滑。
2.3 为什么选 OnnxRuntime 而不是 TensorFlow 或 OpenCV DNN
选 OnnxRuntime 是综合考虑了三件事:跨平台、硬件加速、模型覆盖度。ONNX 是模型格式,OnnxRuntime 是推理引擎,两者是一体两面的关系——前者解决模型从 PyTorch 生态导出的问题,后者解决运行时加载的问题。TensorFlow 在 .NET 侧支持不够友好,OpenCV DNN 对 Transformer 这类结构的算子支持明显落后。OnnxRuntime 的 Execution Provider 机制能让你在 CPU、DirectML、CUDA 之间切换,而不用改推理代码,这对 C# 桌面软件来说很实用。
资源包里通常包含以下内容:转换好的 ONNX 模型文件、C# 源码工程、测试图片、说明文档。你拿到手之后不需要再去搞环境,直接打开工程、确认 NuGet 包恢复、改一下模型路径就能跑。
3. 导出 ONNX 与参数检查:动手写 C# 之前的验证
3.1 把权重转成 ONNX 模型
如果你拿到的资源包里有现成的 ONNX 文件,这步可以跳过。但建议还是看一眼导出代码,方便以后换输入尺寸或者把单帧改成多帧版本,都要重新导出。PyTorch 转 ONNX 的代码通常长这样:
import torch from davit import DAViDModel # 假设模型定义在这个模块里 model = DAViDModel(arch="base", pretrained="models/david_base.pth") model.eval() # 注意:这里用单帧输入,batch=1 dummy_input = torch.randn(1, 3, 1024, 576) torch.onnx.export( model, dummy_input, "david_base_1024x576.onnx", input_names=["rgb"], output_names=["alpha"], dynamic_axes={ "rgb": {0: "batch"}, "alpha": {0: "batch"} }, opset_version=17 ) print("export done")这段代码里最关键的是dynamic_axes参数。我把batch维度设为动态,是因为 C# 侧有可能一次性喂多帧图进去批处理;高和宽没有设动态,是因为 Transformer 结构对尺寸变化比较敏感,推理时最好固定输入宽高。如果模型内部有 position embedding,宽高一旦改变,效果会明显变差。
opset_version也不建议随便填,DAViD 用到了GridSample、MultiheadAttention这类算子,版本太低会导致导出报错,太高可能超出 OnnxRuntime 的支持范围。我一般固定用 17,这个版本在 OnnxRuntime 1.15 之后支持得很稳定。
3.2 预处理参数说明
ONNX 模型不负责图像解码和归一化,这些活要 C# 侧自己做。DAViD 需要的预处理参数如下:
| 参数 | 值 | 说明 |
|---|---|---|
| 通道顺序 | RGB | 不是 BGR,和高清摄像头默认输出相反 |
| 像素范围 | [0, 1] | 需要把 uint8 的 0~255 除以 255 |
| 归一化 | 无 | 模型内部自带归一化,外部不需要减均值除方差 |
| 输入尺寸 | 1024 x 576 | 保持宽高比再缩放填充,不要直接拉伸 |
这里最容易翻车的是通道顺序。OpenCV 读出来的图是 BGR,模型要的是 RGB,如果没有转换,输出 mask 会在人脸的暗部和亮部上出现奇怪的伪影,但不会全黑全白,特别容易误判成模型问题。
3.3 用 Python 快速验证导出结果
转完 ONNX 先不要急着写 C#,用 Python 的 OnnxRuntime 跑一遍,确认模型文件没问题,能省掉后面一大半排查时间:
import onnxruntime as ort import numpy as np from PIL import Image sess = ort.InferenceSession("david_base_1024x576.onnx", providers=["CPUExecutionProvider"]) img = Image.open("test_person.jpg").convert("RGB") img = img.resize((1024, 576)) arr = np.asarray(img, dtype=np.float32) / 255.0 rgb = np.transpose(arr, (2, 0, 1))[None, ...] # 变成 (1, 3, H, W) alpha = sess.run(["alpha"], {"rgb": rgb})[0][0, 0] # (H, W) print(alpha.shape, alpha.min(), alpha.max())如果你的测试图片里有个明显人物,alpha.min()应该接近 0,alpha.max()接近 1,而且alpha矩阵里大部分像素分布在两端,只有边缘区是中间值。如果你拿到的输出整体都是 0.5 左右,说明预处理里大概率有值域或通道错误,先把这块解决好再往 C# 迁。
提示:ONNX 文件会不会调用私有算子,用
sess.get_providers()和sess.get_session_options()查一下即可;如果报错说找不到某个 Node,优先考虑升级 OnnxRuntime 版本。
4. 在 C# 中部署 DAViD:OnnxRuntime 推理主流程
4.1 工程准备与模型加载
C# 侧我用的是 .NET 6 + WPF 做界面,控制台测试也没问题。NuGet 包只需要两个:Microsoft.ML.OnnxRuntime和OpenCvSharp4。前者提供推理引擎,后者负责图像编解码和预处理。模型加载代码分三步走:
using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; var modelPath = @"models\david_base_1024x576.onnx"; var sessionOptions = new SessionOptions(); sessionOptions.LogSeverityLevel = OrtLoggingLevel.ORT_LOGGING_LEVEL_WARNING; using var session = new InferenceSession(modelPath, sessionOptions); Console.WriteLine($"Input: {string.Join(",", session.InputMetadata.Keys)}"); Console.WriteLine($"Output: {string.Join(",", session.OutputMetadata.Keys)}");这里为什么要设置LogSeverityLevel?默认日志级别是 INFO,一旦模型里有警告信息,控制台会被刷屏,而且 OnnxRuntime 内部用 C++ 打日志,输出格式和 .NET 不统一,调试时看着很乱。设成 WARNING 之后只在真正出错时报信息。
InputMetadata和OutputMetadata可以打印出模型期望的输入输出名,拿到的名字要和导出时保持一致——导出时用的是rgb和alpha,C# 侧字符串就得写这两个名字,不能猜。
4.2 图像预处理:从 Bitmap 到 Tensor
OpenCvSharp 读进来的图是 BGR 顺序,先转 RGB,再缩放、归一化、转 CHW,最后放进 DenseTensor:
using OpenCvSharp; var src = Cv2.ImRead("test_person.jpg", ImreadModes.Color); Cv2.Resize(src, src, new Size(1024, 576)); // BGR -> RGB Cv2.CvtColor(src, src, ColorConversionCodes.BGR2RGB); // 转 float 并归一化到 [0,1] src.ConvertTo(src, MatType.CV_32FC3, 1.0 / 255.0); // HWC -> CHW,同时把数据拷贝到连续内存 var tensorData = new float[1 * 3 * 576 * 1024]; var index = 0; for (var c = 0; c < 3; c++) { for (var h = 0; h < 576; h++) { for (var w = 0; w < 1024; w++) { var vec = src.At<Vec3f>(h, w); tensorData[index++] = c switch { 0 => vec.Item0, // R 1 => vec.Item1, // G 2 => vec.Item2, // B }; } } } var tensor = new DenseTensor<float>(tensorData, new[] { 1, 3, 576, 1024 });这里有两个性能细节值得说。第一,src.At<Vec3f>(h, w)单点访问在 576×1024 的图上要循环 177 万次,这个写法第一批跑出来会比较慢,但胜在逻辑直观、不容易错。如果你要追求帧率,可以用Mat.GetArray一次性把整行拷出来,或者直接用Marshal.Copy操作src.Data指针,能快一倍左右。第二,我用1.0 / 255.0而不是1f / 255f,是为了让ConvertTo输入双精度系数,避免在某些 OpenCV 版本下出现 float 换算误差。
4.3 推理与后处理拿到 alpha matte
输入输出都用NamedOnnxValue封装,Run 之后从结果里取出 alpha 张量,再转成 Bitmap:
using Microsoft.ML.OnnxRuntime.Tensors; var inputs = new List<NamedOnnxValue> { NamedOnnxValue.CreateFromTensor("rgb", tensor) }; using var results = session.Run(inputs); var alphaTensor = results.First(r => r.Name == "alpha").AsTensor<float>(); // alphaTensor 维度是 [1, 1, 576, 1024] var alphaMap = new float[576 * 1024]; alphaTensor.ToArray().CopyTo(alphaMap, 0); // 把 float 转成 8 位灰度图 var mask = new Mat(576, 1024, MatType.CV_8UC1); for (var h = 0; h < 576; h++) { for (var w = 0; w < 1024; w++) { var v = (byte)(alphaMap[h * 1024 + w] * 255f); mask.Set(h, w, v); } } Cv2.ImWrite("alpha_mask.png", mask);注意alphaTensor.ToArray()拿到的是一维float[],它默认是按行优先排列的,也就是下标h * width + w对应第 h 行第 w 列。如果模型输出维度带 batch 和 channel,ToArray()返回的数据顺序不会变,只是总长度里多了 batch 和通道维度,这里[1, 1, 576, 1024]的实际长度就是576 * 1024,取索引用h * width + w正好对上。
拿到 alpha mask 之后,别直接拿它做硬阈值二值化——那就把软前景的意义给丢了。做背景合成时,用 alpha 值作为前景透明度去混合两张图,效果才自然。
5. 高频踩坑与排查:C# 部署 DAViD 的五个翻车点
5.1 一运行就报 System.DllNotFoundException
现象:程序启动后立刻抛异常,提示找不到 onnxruntime.dll,或者 onnxruntime_providers_shared.dll。
原因:NuGet 包的 native DLL 放在runtimes/win-x64/native目录下,如果你的项目没有设置对应平台,或者输出目录少了这些文件,运行时就会找不到。
解决:在工程文件里显式指定平台:
<PropertyGroup> <PlatformTarget>x64</PlatformTarget> </PropertyGroup>然后在输出目录确认onnxruntime.dll存在。如果还是报错,把Microsoft.ML.OnnxRuntime的版本和项目目标框架对齐,比如 .NET 6 配 1.16.x,.NET Framework 4.7.2 配低一档的版本,高版本包对旧框架支持已经不太友好了。
5.2 输出 mask 全黑或全白
现象:模型能跑通,不报错,但 alpha 图要么全是 0,要么全是 255。
原因:最常见的是输入像素值没有归一化。你把 0~255 的 uint8 直接作为 float 喂进去,数值大了 DAViD 内部的注意力权重直接饱和,输出结果趋于两极。
解决:预处理里必须除以 255;另一个原因是通道反了,如果自己构造 Tensor 时把 BGR 当成 RGB 用了,模型会把颜色特征完全搞乱。建议先用一张纯红色图分别测试,看看模型输出值是否符合预期,用二分法快速定位是哪一步错了。
5.3 输出张量维度对不上
现象:alphaTensor.ToArray()拿到的数组长度和H*W不一致,或者results.First(r => r.Name == "alpha")抛 InvalidOperationException。
原因:模型实际输出维度是[1, 1, H, W],但你按[H, W]处理了数组;输入名不一致时,Run 会报错而不是返回空结果。
解决:打开模型元数据,先打印 InputMetadata 和 OutputMetadata 的实际名字再写硬编码字符串。我的习惯是封装一个诊断函数,启动时打印所有输入输出维度,正式跑之前先验证一次:
foreach (var kv in session.InputMetadata) Console.WriteLine($"Input {kv.Key}: {string.Join(",", kv.Value.Dimensions)}"); foreach (var kv in session.OutputMetadata) Console.WriteLine($"Output {kv.Key}: {string.Join(",", kv.Value.Dimensions)}");5.4 CPU 推理慢到没法用
现象:1024×576 的输入在 CPU 上跑一次要 2~3 秒,视频场景完全不可用。
原因:DAViD 是 Transformer 结构,参数量大,CPU 上的矩阵乘没有并行优化通道时确实慢。
解决:先换用Microsoft.ML.OnnxRuntime.Gpu包并追加 CUDA Execution Provider,在支持 NVIDIA 显卡的机器上能直接获得数量级提升;如果只能 CPU,把输入缩小到 640×384 会快很多,但需要重新用torch.onnx.export导出对应尺寸的模型,直接用Resize对 1024 模型降采样精度损失太大,不推荐。
5.5 连续推理时内存缓慢增长
现象:程序跑了几千帧之后,进程内存从 200MB 涨到了 800MB。
原因:每次 Run 都创建新的NamedOnnxValue列表,而且DenseTensor底层分配的 float 数组没有被及时回收,OnnxRuntime 的 native 侧缓冲区也占内存。
解决:把Run放在复用对象上,输入 Tensor 用新建但输出结果不要长期持有;另外,OnnxRuntime 的Run是线程安全的,你可以开两个 Session 轮流使用,避免频繁创建和销毁。用GC.AddMemoryPressure手动提示 native 内存变化,也能让 GC 更早介入。
6. 实战进阶:视频逐帧抠像与合成优化
当单帧推理跑通之后,自然要往视频方向走。逐帧处理的第一个问题是抖动——单帧模型在视频上输出的 mask 边缘会左右晃动,因为前后帧的遮挡关系在变化。最省事的平滑方案是用指数移动平均,对相邻两帧的 alpha 做时间维度加权:
const float blendFactor = 0.3f; var smoothed = prevAlpha * blendFactor + currentAlpha * (1 - blurFactor);这个权重不能太大,否则运动物体上会出现拖影;我一般控制在 0.2 到 0.4 之间,场景切换时手动把权重调成 0,避免残余画面串到新场景里。
合成新背景的完整流程是这样:把 alpha 转成 3 通道 matte,分别对前景和背景做线性混合。用 C# 代码表达就是:
var bg = Cv2.ImRead("new_background.jpg", ImreadModes.Color); Cv2.Resize(bg, bg, new Size(1024, 576)); using var fgMat = new Mat(576, 1024, MatType.CV_32FC3); using var bgMat = new Mat(576, 1024, MatType.CV_32FC3); using var matte = new Mat(576, 1024, MatType.CV_32FC3); // 把 alpha 转成三通道 Cv2.CvtColor(maskFloat, matte, ColorConversionCodes.GRAY2BGR); Cv2.Multiply(fgFloat, matte, fgMat); Cv2.Multiply(bgFloat, 1.0 - matte, bgMat); using var result = new Mat(); Cv2.Add(fgMat, bgMat, result); result.ConvertTo(result, MatType.CV_8UC3);这里maskFloat必须是CV_32FC1的软前景,不能是 255 灰度级的字节图,否则乘出来的前景亮度会被放大 255 倍。我的做法是从模型输出拿到 float 数组之后直接转成 Mat,生成 byte mask 只是用于可视化,真正做合成时用 float 版本。
最后说一个我自己反复踩的习惯:不管模型文件是谁给的,落地前一定先用 Python 验证一遍输出范围,然后把 C# 的 Tensor 构造代码和 Python 的预处理对齐,再跑第一帧对比。从那以后我每次做模型部署都会强制走一遍「导出验证 → C# 零修改运行 → 输出对比」这个流程,能省下大量排查时间。希望这篇笔记帮到你。
本文还有配套的精品资源,点击获取