ARTICLE DETAIL

资讯详情

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

C# OnnxRuntime 部署 DDColor 图像上色模型实战

C# OnnxRuntime 部署 DDColor 图像上色模型实战 简介这份资源面向具备一定C#与深度学习基础的开发者提供在.NET环境下用OnnxRuntime推理引擎部署DDColor老照片上色模型的完整工程示例可用于桌面端图像修复与上色应用的二次开发。压缩包共300个文件、约847.06MB包含sln解决方案、csproj工程文件与cs源码以及大量dll、nupkg、targets、props等依赖与构建配置另有onnx模型、xml文档、jpg/jpeg/png示例图片和txt说明覆盖从模型加载、会话创建到推理输出的完整链路。目前已有51人学习下载。读者可据此掌握将训练模型转为ONNX格式、在C#中调用OnnxRuntime执行推理、处理输入输出并展示上色结果的方法同时参考其目录组织与依赖管理方式理解模型加载优化、运行效率提升与色彩预测准确性调优的实践思路对老照片修复类应用的落地具有较高参考价值。1. C# OnnxRuntime 部署 DDColor从模型文件到可交互上色工具手里有一个训练好的 DDColor 模型想把它塞进 C# 桌面程序里让用户点一下按钮就能给黑白老照片上色——这件事听起来简单实际动手时会发现卡点不在模型本身而在“怎么让 C# 和 ONNX 模型说上话”。DDColor 是一个基于 Transformer 架构的图像上色模型输入一张灰度图或彩色退化图输出对应的彩色版本效果在人物、风景、建筑等场景上都比较稳。OnnxRuntime 则是微软推出的跨平台推理引擎C# 通过它加载.onnx模型文件不需要 Python 环境也不需要把 PyTorch 一起打包进去。这个组合适合做本地化的图像处理工具、老照片修复软件、或者嵌入到已有的 C# 上位机里做增值功能。整条链路的核心就三件事模型导出成 ONNX、C# 里把张量喂进去、把输出张量还原成图片。下面按实际落地顺序拆开讲。2. 模型导出与 OnnxRuntime 环境搭建先把路铺平2.1 DDColor 转 ONNX 的关键参数与导出脚本DDColor 官方仓库提供的是 PyTorch 权重要部署到 C# 必须先转成 ONNX 格式。导出时最容易翻车的地方是输入输出节点的命名和动态轴设置——如果轴写死成固定 batchC# 端换一张不同尺寸的图就会直接报维度不匹配。常见做法是只固定高度和宽度把 batch 轴留成动态。import torch from ddcolor.model import DDColor # 假设模型定义在 ddcolor.model 下 # 加载权重注意 map_location 避免 GPU 环境依赖 model DDColor() state torch.load(ddcolor_paper_tiny.pth, map_locationcpu) model.load_state_dict(state) model.eval() # 构造示例输入1 张 3 通道 512x512 的图 dummy torch.randn(1, 3, 512, 512) torch.onnx.export( model, dummy, ddcolor.onnx, input_names[input], output_names[output], opset_version17, # 建议 17对 Transformer 算子支持更完整 dynamic_axes{ input: {0: batch, 2: height, 3: width}, output: {0: batch, 2: height, 3: width}, }, do_constant_foldingTrue, )这段脚本的逻辑是先把模型切到推理模式关掉 dropout 和 batch norm 的训练行为然后用一个假输入走一遍前向ONNX 导出器会记录计算图。opset_version选 17 是因为 DDColor 里的注意力机制涉及一些较新的算子opset 太低会导出失败或运行时找不到实现。dynamic_axes里把 batch、height、width 都标成动态C# 端才能自由传不同分辨率的图。导出完成后建议用onnxruntime的 Python 版先跑一遍验证确认输出和 PyTorch 原版差异在可接受范围内再进 C#。2.2 C# 项目里引入 OnnxRuntime 的两种方式与选择C# 用 OnnxRuntime 有两条路一是 NuGet 包Microsoft.ML.OnnxRuntime二是手动引用动态库。绝大多数场景直接上 NuGet 就行它会把 CPU 版的onnxruntime.dll一起带进来省去手动配 PATH 的麻烦。如果要用 GPU 加速换成Microsoft.ML.OnnxRuntime.Gpu但要注意 CUDA 版本和驱动匹配否则运行时会静默回退到 CPU性能差一大截。# 在项目目录下执行安装 CPU 版 dotnet add package Microsoft.ML.OnnxRuntime --version 1.17.0 # 如果需要 GPU 版 dotnet add package Microsoft.ML.OnnxRuntime.Gpu --version 1.17.0版本号建议锁在 1.17 左右这个区间对 opset 17 的支持比较稳定。安装完之后项目文件里会自动多出引用编译时onnxruntime.dll会被复制到输出目录。如果遇到DllNotFoundException先检查输出目录下有没有这个文件再看平台目标是不是 x64——OnnxRuntime 不支持 AnyCPU必须明确指定 x64 或 arm64。2.3 用 C# 加载模型并跑通第一次推理环境就绪后第一件事是确认模型能加载、能推理先不管图片预处理。下面这段代码构造一个全零张量走一遍Run看输出形状对不对。using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; // 加载模型SessionOptions 里可以开线程数、日志级别 var options new SessionOptions(); options.IntraOpNumThreads 4; // 单算子内部并行线程数 options.LogSeverityLevel OrtLoggingLevel.ORT_LOGGING_LEVEL_WARNING; using var session new InferenceSession(ddcolor.onnx, options); // 构造输入1x3x512x512 的全零张量 var inputTensor new DenseTensorfloat(new[] { 1, 3, 512, 512 }); var inputs new ListNamedOnnxValue { NamedOnnxValue.CreateFromTensor(input, inputTensor) }; using var results session.Run(inputs); var output results.First().AsTensorfloat(); Console.WriteLine($输出维度: {string.Join(, , output.Dimensions)});IntraOpNumThreads控制单个算子内部的并行度设成 CPU 核心数的一半到全部之间比较合适设太大反而会因为线程切换开销导致延迟上升。Run返回的是一个IDisposable集合用using包住避免内存泄漏。输出张量的维度应该是[1, 3, 512, 512]如果看到[1, 2, ...]或者别的通道数说明导出时输出节点接错了需要回 Python 端检查。3. 图像预处理与后处理把 Bitmap 变成张量再变回来3.1 灰度图转张量的归一化参数怎么定DDColor 的输入不是随便一张 RGB 图它期望的是经过特定归一化的张量。常见做法是把像素值缩到[0, 1]再按 ImageNet 的均值和标准差做标准化。但 DDColor 原版训练时用的归一化方式可能不同最稳妥的办法是翻一下官方推理脚本里的transforms部分照搬参数。public static DenseTensorfloat BitmapToTensor(Bitmap bmp, int targetSize 512) { // 缩放到目标尺寸保持长宽比可选这里直接拉伸 var resized new Bitmap(bmp, new Size(targetSize, targetSize)); var tensor new DenseTensorfloat(new[] { 1, 3, targetSize, targetSize }); // ImageNet 均值与标准差 float[] mean { 0.485f, 0.456f, 0.406f }; float[] std { 0.229f, 0.224f, 0.225f }; for (int y 0; y targetSize; y) { for (int x 0; x targetSize; x) { var pixel resized.GetPixel(x, y); // 灰度图三个通道相同彩色图分别取 R/G/B tensor[0, 0, y, x] (pixel.R / 255f - mean[0]) / std[0]; tensor[0, 1, y, x] (pixel.G / 255f - mean[1]) / std[1]; tensor[0, 2, y, x] (pixel.B / 255f - mean[2]) / std[2]; } } return tensor; }逐像素循环在 C# 里性能一般512x512 大概几十毫秒可以接受。如果追求更快用LockBits拿裸内存指针再配合Parallel.For能压到几毫秒。归一化参数如果和训练时不一致输出会偏色——比如整体发绿或发灰这时候优先怀疑 mean/std 写错了。3.2 输出张量还原成 Bitmap 的裁剪与钳位模型输出同样是[1, 3, H, W]的浮点张量值域大致在[-2, 2]之间需要反归一化再乘 255最后钳到[0, 255]。public static Bitmap TensorToBitmap(Tensorfloat output, int width, int height) { float[] mean { 0.485f, 0.456f, 0.406f }; float[] std { 0.229f, 0.224f, 0.225f }; var bmp new Bitmap(width, height, PixelFormat.Format24bppRgb); for (int y 0; y height; y) { for (int x 0; x width; x) { int r (int)Math.Clamp((output[0, 0, y, x] * std[0] mean[0]) * 255f, 0, 255); int g (int)Math.Clamp((output[0, 1, y, x] * std[1] mean[1]) * 255f, 0, 255); int b (int)Math.Clamp((output[0, 2, y, x] * std[2] mean[2]) * 255f, 0, 255); bmp.SetPixel(x, y, Color.FromArgb(r, g, b)); } } return bmp; }Math.Clamp不能省浮点误差会让个别像素超出[0, 255]直接转int再塞进Color会抛异常或者产生溢出颜色。如果输出图整体偏暗检查是不是忘了乘 255如果偏亮发白检查反归一化时 mean 和 std 是不是用反了。3.3 用 LockBits 把逐像素操作提速一个数量级GetPixel和SetPixel在循环里是出了名的慢512x512 的图来回两趟能跑出几百毫秒。换成LockBits直接操作字节数组同样的逻辑能压到 10ms 以内。public static Bitmap TensorToBitmapFast(Tensorfloat output, int width, int height) { var bmp new Bitmap(width, height, PixelFormat.Format24bppRgb); var rect new Rectangle(0, 0, width, height); var data bmp.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb); int stride data.Stride; byte[] buffer new byte[stride * height]; for (int y 0; y height; y) { for (int x 0; x width; x) { int idx y * stride x * 3; buffer[idx 2] (byte)Math.Clamp((output[0, 0, y, x] * 0.229f 0.485f) * 255f, 0, 255); // R buffer[idx 1] (byte)Math.Clamp((output[0, 1, y, x] * 0.224f 0.456f) * 255f, 0, 255); // G buffer[idx 0] (byte)Math.Clamp((output[0, 2, y, x] * 0.225f 0.406f) * 255f, 0, 255); // B } } Marshal.Copy(buffer, 0, data.Scan0, buffer.Length); bmp.UnlockBits(data); return bmp; }注意Format24bppRgb在内存里的字节顺序是 BGR不是 RGB写 buffer 的时候要对应好。stride可能大于width * 3因为每行会做对齐填充所以不能用y * width * 3直接算偏移。这段代码配合Parallel.For改外层循环还能再快一截。4. 避坑与排查部署 DDColor 时最容易翻车的五个地方4.1 推理结果全黑或全灰现象模型能跑通输出张量形状也对但还原出来的图是一片黑或者均匀灰色。原因通常是输入张量没有做归一化或者归一化参数和训练时不一致。DDColor 对输入分布比较敏感如果直接把[0, 255]的像素值塞进去模型内部激活会饱和输出就退化成常数。解决方法是回官方推理脚本确认transforms.Normalize的具体数值逐一对齐。4.2 换一张图就报维度不匹配现象用 512x512 的图跑得好好的换一张 640x480 就抛OnnxRuntimeException提示维度不兼容。原因是导出 ONNX 时没有把 height 和 width 标成动态轴模型只认 512x512。解决方法是重新导出在dynamic_axes里把空间维度都设成动态或者在 C# 端强制 resize 到固定尺寸但后者会损失画质。4.3 GPU 版跑起来比 CPU 还慢现象装了Microsoft.ML.OnnxRuntime.Gpu但推理耗时反而比 CPU 版高。原因通常是 CUDA 或 cuDNN 版本不匹配OnnxRuntime 静默回退到了 CPU而 GPU 版的初始化开销又比纯 CPU 版大。排查方法是打开详细日志看 session 创建时有没有Fallback to CPU字样。解决方法是按官方文档对齐 CUDA 版本或者干脆用 CPU 版加多线程。4.4 内存持续增长直到崩溃现象连续处理几十张图后进程内存占用越来越高最终OutOfMemoryException。原因是InferenceSession.Run返回的IDisposable结果没有释放或者DenseTensor被意外持有。解决方法是确保每次Run的结果都用using包住Bitmap对象用完立刻Dispose不要放在静态字段里缓存。4.5 输出图有网格状伪影现象上色结果整体正常但放大看有规律的网格纹路。原因是输入图片被拉伸到 512x512 时用了最近邻插值或者输出还原时尺寸对不上。解决方法是 resize 时用双三次插值并且记录原始尺寸输出后再缩回去。如果模型本身支持动态尺寸直接传原图尺寸避免两次缩放。5. 进阶技巧用动态尺寸输入和批处理把吞吐拉满前面跑通的是固定 512x512、单张推理的版本实际用起来会碰到两个瓶颈一是不同尺寸的图都要缩到 512 再缩回去画质有损二是单张推理时 GPU 利用率很低。这两个问题可以一起解决。先说动态尺寸。导出 ONNX 时如果把 height 和 width 标成动态C# 端就可以传任意尺寸。但要注意DDColor 的 Transformer 结构对尺寸有隐式约束——空间维度最好是 32 的倍数否则 patch embedding 阶段会因为除不尽而报错。我一般会把输入图的长边缩到 512 到 1024 之间短边按比例算然后向上取整到 32 的倍数。这样既保留了更多细节又不会触发维度错误。public static (int w, int h) CalcDynamicSize(int origW, int origH, int maxSide 1024) { float scale Math.Min(1f, (float)maxSide / Math.Max(origW, origH)); int w (int)(origW * scale); int h (int)(origH * scale); // 向上取整到 32 的倍数 w (w 31) / 32 * 32; h (h 31) / 32 * 32; return (w, h); }再说批处理。OnnxRuntime 支持一次传多个样本把 batch 轴设成大于 1 就行。对于批量处理老照片的场景一次传 4 张或 8 张GPU 利用率能从 20% 拉到 70% 以上。但要注意显存限制512x512 的图batch 设 8 大概占 2GB 左右显存设太大反而会 OOM。CPU 版则不建议开 batch因为并行度已经靠线程数吃满了加 batch 只会增加内存压力。// 构造 batch4 的输入张量 var batchTensor new DenseTensorfloat(new[] { 4, 3, 512, 512 }); for (int b 0; b 4; b) { var single BitmapToTensor(bitmaps[b]); for (int c 0; c 3; c) for (int y 0; y 512; y) for (int x 0; x 512; x) batchTensor[b, c, y, x] single[0, c, y, x]; }最后说一个验证技巧拿同一张图分别用 PyTorch 原版和 C# OnnxRuntime 版跑一遍把两个输出张量逐元素求差的绝对值看最大差异。如果最大差异在 1e-3 量级以内说明整条链路没有引入额外误差如果差异很大优先查预处理和后处理的归一化参数是否一致。这个对比我每次部署新模型都会做一遍比肉眼看图靠谱得多。踩过最深的坑是导出时忘了设do_constant_foldingTrue结果模型里留了一堆训练专用的算子C# 端加载直接报Not implemented。后来养成习惯导出后先用 Python 的onnxruntime跑一遍确认没问题再进 C#。希望帮到你。本文还有配套的精品资源点击获取
返回列表