ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

C# OnnxRuntime实操:部署DDColor图像着色模型全流程

C# OnnxRuntime实操:部署DDColor图像着色模型全流程 简介在桌面应用与上位机开发中深度学习模型的集成常常依赖Python服务导致部署复杂、环境臃肿。其实借助ONNX这一开放的模型交换格式与微软自家的推理引擎OnnxRuntimeC#开发者完全可以脱离Python环境将PyTorch模型直接嵌入WinForms或WPF项目。本实践以DDColor图像着色模型为例详细讲解如何将预训练权重导出为ONNX文件并通过C#完成图像预处理、推理与后处理全链路。从Lab颜色空间转换、Tensor张量构造到OpenCvSharp的图像操作每一步都贴合实际工程场景。无论是老照片修复、黑白影像上色还是灰度监控图可视化这套C# OnnxRuntime方案都能提供稳定高效的推理能力为桌面应用赋予AI视觉功能提供了一条轻量化、易分发的落地路径。 做C#上位机这么多年其实一直有个执念想让桌面程序直接跑深度学习模型而不是动不动就拉个Python服务在后台蹲着。最近因为一个老照片修复的需求我拿到了一个标题叫“C# OnnxRuntime 部署 DDColor.rar”的压缩包里面是一个打包好的DDColor图像着色模型和相关的C#工程。花了两天时间把整个链路跑通之后我觉得这套方案非常值得分享出来——尤其是对C#开发者来说用OnnxRuntime把PyTorch的模型集成进WinForms/WPF项目里这条路完全走通了。这篇博客就把我拆包、推理、踩坑的全过程记录下来从模型转换到C#推理代码再到成图效果都给你说透。1. 项目核心思路DDColor与OnnxRuntime的搭配逻辑1.1 DDColor是什么能处理什么问题DDColor是ICCV 2023的一篇图像着色论文全称是Towards Photo-Realistic Image Colorization via Dual Decoders。和传统的着色模型不太一样它用的是双解码器结构一个解码器负责生成全局的颜色风格另一个解码器负责恢复图片细节和边缘信息最后融合成最终的A/B色度通道。换句话说它不只是简单地把灰度图染上一层颜色而是会先“理解”画面里的语义内容——比如知道这是一片天空那是一块草地——再按语义来给对应区域上色。所以它的成色效果比DeOldify这类老模型要自然得多很少出现那种颜色溢出或者整张图偏色的情况。这套模型对老照片修复、黑白影像上色、监控灰度图可视化都有很好的实用价值。更关键的是DDColor的模型权重和推理代码都是开源的这就给C#工程集成提供了前提。我们不需要去自己训练模型只要把预训练权重转成ONNX格式再用OnnxRuntime这个跨平台推理引擎去加载就能在纯C#环境下跑通整个着色流程。1.2 为什么选C# OnnxRuntime而不是继续待在Python很多C#开发者在做AI功能时第一反应是Python调模型多方便啊C#不适合干这个。这种想法放在两年前还有一定道理但现在真的变了。OnnxRuntime是微软自己出品的推理引擎C#是微软的亲儿子两者的配合度远比想象中高。你不需要去理解PyTorch的整个运行机制不需要管Python环境、pip依赖、conda虚拟环境这些破事只需要一个ONNX模型文件加一个NuGet包就能完成推理。我选择C# OnnxRuntime还有几个非常务实的理由。第一个是部署形态上位机软件用户不可能去装Python环境而C#可以直接把OnnxRuntime的native DLL和模型文件一起打包进发布目录用户拿到就能跑。第二个是线程控制C#里可以很方便地用async/await、Task、BlockingCollection这些来做图像队列调度和上位机已有的串口通信、PLC通讯逻辑能无缝整合。第三个是生态互补OpenCvSharp提供了完整的图像处理API配合OnnxRuntime后整个图像预处理、推理、后处理都能在C#一个进程内完成。1.3 整体架构设计我最终采用的架构很简单就是典型的“图像处理 推理引擎”两级结构。上层是WinForms界面负责选择图片、触发处理和显示结果中间层是一个ImageColorizer类封装了DDColor推理的完整逻辑底层依赖两个核心库——OpenCvSharp负责图像编解码、颜色空间转换和尺寸调整OnnxRuntime负责加载DDColor的ONNX模型并执行前向推理。整个处理流程可以拆成四个环节读取灰度图或彩色图、把图像转换到Lab颜色空间并取L亮度通道、将L通道输入模型推理得到A/B色度通道、最后把L/A/B三通道合并并转换回BGR彩色图。这套流程和DDColor官方Python代码的逻辑完全一致区别只是把skimage和torch的调用换成了OpenCvSharp和OnnxRuntime。值得强调的是这里的核心难点不在于推理本身而在于图像预处理和后处理时的颜色空间转换。DDColor模型输入是三通道的L通道灰度图输出是A/B两个色度通道我们必须保证C#端的Lab转换结果和官方Python端用skimage的rgb2lab结果足够接近否则成图效果会明显偏色。2. 模型准备从PyTorch权重到ONNX推理文件2.1 获取预训练权重DDColor的官方仓库在GitHub上直接搜DDColor就能找到项目来自piddnad。仓库里提供了完整的训练和推理代码。我们要用的是其中最核心的ddcolor_model_twins和ddcolor_model_convnext这两个预训练权重之一。我实际测试中选择了基于ConvNeXt的版本因为它的ONNX导出尺寸相对较小而且在C#环境下推理速度更可控。下载权重时要注意版本对应关系官方仓库里不同分支可能对应不同的模型结构。建议直接使用master分支上的权重文件文件名一般是pytorch_model.bin或者类似命名。下载完成后先用Python脚本跑一下官方提供的demo确认识别效果正常再做ONNX导出。这一步很重要——如果直接在PyTorch里跑出来的效果就不对那后面无论怎么部署都没有意义。另外如果读者在GitHub下载总是失败可以考虑先下载到本地再用公司内网传输或者用国内的开源镜像站。我当时是在一台有稳定网络的机器上把权重下载好再拷到开发机上操作的。2.2 导出ONNX的关键步骤导出ONNX是整个过程里最容易翻车的一步。DDColor官方仓库里其实已经提供了导出脚本路径大概是script/export_onnx.py这样的位置。这个脚本的思路很清晰加载预训练权重构造一个假输入然后调用torch.onnx.export把模型结构固化下来。如果你找不到导出脚本直接自己写也不难核心导出代码大概是这样的import torch from ddcolor import DDColor model DDColor(model_nameconvnext, input_size(256, 256)) checkpoint torch.load(pytorch_model.bin, map_locationcpu) model.load_state_dict(checkpoint[model_state_dict] if model_state_dict in checkpoint else checkpoint, strictFalse) model.eval() dummy_input torch.randn(2, 3, 256, 256) torch.onnx.export( model, dummy_input, ddcolor.onnx, input_names[input], output_names[output], dynamic_axes{ input: {0: batch, 2: height, 3: width}, output: {0: batch, 2: height, 3: width} }, opset_version11 ) print(export done)需要注意几个关键点。第一dynamic_axes务必加上这样导出的模型才能支持任意输入尺寸。如果不加之后在C#里就只能用256x256的固定分辨率那对大图做处理时非常尴尬。第二opset_version不要太高11左右的版本兼容性最好OnnxRuntime对低版本算子的支持很成熟。第三导出前一定要把模型切换到eval模式并调用torch.no_grad()上下文否则模型里的dropout和batchnorm会干扰导出结果。另外提醒一下PyTorch导出ONNX建议在当前模型官方指定的Python环境下操作。如果环境里缺少依赖可以新建一个conda环境按requirements.txt安装版本差一两个小版本通常不会有大问题。如果读者不想自己去转换也可以在网上搜索“ddcolor onnx”直接找现成的模型文件。但要注意来源可靠性建议优先找和官方仓库关联的转换版本避免被人动过手脚特别是涉及商业项目时更要注意模型文件的来源可信度。2.3 用Netron确认输入输出结构拿到ONNX模型后第一件事就是用Netron打开看一下结构。Netron是一个在线打开的神经网络可视化工具直接拖拽模型文件到网页上就能看。我们需要重点确认三个信息输入节点的名称、输入张量的Shape格式、输出节点的名称和Shape格式。按照官方导出脚本输入节点名一般是inputShape是(1, 3, 256, 256)这样的动态格式代表batch size为1、三通道、高度和宽度动态变化。输出节点名一般是outputShape是(1, 2, 256, 256)代表两通道的A/B色度信息。这里的三通道输入不是RGB三通道而是把L通道复制成三份——也就是说DDColor虽然要求输入三通道张量但三通道的内容是完全相同的灰度亮度值。这个结构认知非常关键直接影响后面C#代码里tensor的填充方式。我见过不少人在这一步栽跟头把灰度图按单通道填充进去结果推理直接报错还有人把三通道理解为RGB彩色图输入导致后处理时颜色完全错乱。3. C#工程实现从图像预处理到推理后处理3.1 创建项目与引入NuGet包创建C#工程这一步没有什么特别之处我用的是.NET 6 WinForms因为上位机项目还在用.NET Framework的太常见了但对新项目我建议直接用.NET 6以上版本这样NuGet依赖处理会省心很多。需要引入三个NuGet包Microsoft.ML.OnnxRuntimeCPU推理OpenCvSharp4图像处理OpenCvSharp4.runtime.winOpenCV原生库引入时要注意平台目标。OnnxRuntime和OpenCvSharp都分x86和x64版本开发机上一般用x64就好。如果不小心在x86模式下加载x64的native DLL运行时会出现BadImageFormatException异常排查起来很麻烦。我通常在项目属性里直接把平台目标设为x64这样最省心。还有一点Microsoft.ML.OnnxRuntime的版本和Microsoft.ML.OnnxRuntime.Gpu如果要用GPU之间最好不要混用否则容易出现native DLL重复加载的问题。纯CPU场景就只用CPU包需要CUDA加速时再换Gpu包。OnnxRuntime 1.16以后的版本对CUDA 11.8支持得很成熟但会让部署体积增加很多读者可以根据实际场景衡量。3.2 图像预处理灰度图转Lab、调整尺寸、归一化DDColor模型的预处理逻辑和常见的分类模型不一样它不接受BGR/RGB原始图像需要先把图像转到Lab颜色空间取出L亮度通道再进行归一化。所以在C#端第一步就是用OpenCvSharp把BGR图像转换成Labusing OpenCvSharp; Mat bgr Cv2.ImRead(inputPath, ImreadModes.Color); Mat lab new Mat(); Cv2.CvtColor(bgr, lab, ColorConversionCodes.BGR2Lab); Mat[] labChannels Cv2.Split(lab); Mat lChannel labChannels[0]; // L通道0-255 Mat aChannel labChannels[1]; // 官方图像A通道0-255 Mat bChannel labChannels[2]; // 官方图像B通道0-255这里有个容易混淆的点OpenCV的BGR2Lab转换出来的L通道范围是0-255但DDColor官方Python代码用的是skimage的rgb2labL通道范围是0-100。好在DDColor模型对输入做了归一化处理实际测试中直接把0-255的L通道值除以255缩放到0-1即可不必纠结L通道具体范围。关键是保持数值统一规律。接着需要把L通道调整到模型输入尺寸并归一化到0-1浮点范围。我建议实际处理时把输入尺寸设置为256x256通过Cv2.Resize实现。把L通道resize后需要用OpenCvSharp的MatToArray方法把像素值提取到float数组里然后每个元素除以255.0f。最后把这个长度为256*256的一维数组复制三份填进形状为(1, 3, 256, 256)的DenseTensor中Mat resizedL new Mat(); Cv2.Resize(lChannel, resizedL, new Size(256, 256)); float[] pixels new float[256 * 256]; for (int y 0; y 256; y) { for (int x 0; x 256; x) { pixels[y * 256 x] resizedL.Atbyte(y, x) / 255.0f; } } float[] inputData new float[3 * 256 * 256]; for (int c 0; c 3; c) { Array.Copy(pixels, 0, inputData, c * 256 * 256, 256 * 256); }这里不建议直接使用Mat.GetArray一次性提取因为OpenCV的Mat在内存中可能存在行对齐填充直接按一维数组读取容易错位。用At (y, x)虽然慢一点但能保证像素位置完全正确。对于只有256x256的尺寸来说这个性能开销完全可以接受。3.3 核心推理代码推理部分用OnnxRuntime写成这样using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; var session new InferenceSession(ddcolor.onnx); // 或指定完整路径 var inputMeta session.InputMetadata; string inputName inputMeta.Keys.First(); var inputShape inputMeta[inputName].Dimensions; Console.WriteLine($input: {inputName} {string.Join(,, inputShape)}); var inputTensor new DenseTensorfloat(inputData, new[] { 1, 3, 256, 256 }); var inputs new ListNamedOnnxValue { NamedOnnxValue.CreateFromTensor(inputName, inputTensor) }; using (var results session.Run(inputs)) { var output results.First().AsTensorfloat(); // output shape: [1, 2, 256, 256] float[] outputData output.ToArray(); }这段代码的核心逻辑已经基本完整。需要注意InferenceSession最好用using或者做成单例不要在每张图片推理时都new一个session因为模型加载和初始化本身有较大开销。在我的机器上加载DDColor模型需要大约几百毫秒这部分时间完全可以通过复用session来省掉。关于InferenceSession的线程安全性OnnxRuntime官方文档说明Run方法是线程安全的所以可以放心地在多个线程里复用同一个session实例。我在实测中同时让4个线程各自推理不同图片没有出现崩溃或数据错乱的情况。3.4 后处理ab通道回采、融合、转回RGB拿到模型的输出张量后需要做一系列后处理才能得到最终彩色图。模型输出的ab通道范围大约是-1到1需要先映射到0-255的整数范围才能和OpenCV的Lab图像数据兼容int size 256 * 256; byte[] abA new byte[size]; byte[] abB new byte[size]; for (int i 0; i size; i) { float aVal (outputData[i] 1.0f) * 128.0f; // 第一个通道 float bVal (outputData[size i] 1.0f) * 128.0f; // 第二个通道 abA[i] (byte)Math.Clamp(aVal, 0, 255); abB[i] (byte)Math.Clamp(bVal, 0, 255); }接着用这三个通道构造一个Lab空间的Mat然后通过OpenCV的Lab2BGR转换出最终彩色图。注意这里使用原来的L通道原分辨率配合上采样后的ab通道Mat lMat new Mat(256, 256, MatType.CV_8UC1); Mat aMat new Mat(256, 256, MatType.CV_8UC1); Mat bMat new Mat(256, 256, MatType.CV_8UC1); Marshal.Copy(lChannelPixels, 0, lMat.Data, lChannelPixels.Length); Marshal.Copy(abA, 0, aMat.Data, abA.Length); Marshal.Copy(abB, 0, bMat.Data, abB.Length); Mat[] labChannels { lMat, aMat, bMat }; Mat labOutput new Mat(); Cv2.Merge(labChannels, labOutput); Mat bgrOutput new Mat(); Cv2.CvtColor(labOutput, bgrOutput, ColorConversionCodes.Lab2BGR);然后用Resize把输出图调整回原始图像的宽高即可。如果要处理大图我建议直接让模型输出更大尺寸比如把输入设成原图等比例缩放后的尺寸而不是非得256x256后让后处理去放大。模型对输入尺寸有一定的泛化能力但分辨率大幅变化会明显影响着色质量。4. 性能优化与踩坑实录4.1 执行提供程序选型CPU、CUDA、DirectMLOnnxRuntime支持多种执行提供程序最常用的是CPU、CUDA、DirectML三种。我最初只在CPU上跑用256x256输入单张图推理耗时大约2到4秒具体看CPU性能。这个速度对单张老照片处理来说是能接受的但如果要批量处理一批照片就会显得很慢。如果你的机器有NVIDIA显卡可以换成Microsoft.ML.OnnxRuntime.Gpu包并加上CUDA执行提供程序。实测下来推理耗时可以从3秒降到0.3到0.5秒提升非常明显。但要注意CUDA版本和显卡驱动的依赖关系OnnxRuntime 1.16.3对应CUDA 11.8和cuDNN 8.7版本不对会在初始化时报错。C#代码如下var sessionOptions new SessionOptions(); sessionOptions.AppendExecutionProvider_CUDA(0); sessionOptions.AppendExecutionProvider_CPU(); var session new InferenceSession(ddcolor.onnx, sessionOptions);如果你不想操心CUDA那一堆环境变量可以用DirectML方案只需要引入Microsoft.ML.OnnxRuntime.DirectML包然后调用AppendExecutionProvider_DML()。DirectML的好处是适配几乎所有支持DirectX 12的显卡无需单独安装CUDA工具链但推理速度比CUDA略慢在10系显卡上大概0.5到1秒之间。这个方案对无法控制目标机器环境的上位机项目是很友好的。4.2 大图处理时的内存与耗时控制对于超过2000像素的大图直接以原图分辨率输入模型会导致内存暴涨和推理时间线性增加。我实测3000x2000的图如果直接以原图尺寸推理CPU模式可能耗时20秒以上而且内存峰值超过500MB这在一个常年运行的上位机程序里是不可接受的。常规做法是把长边限制在512或640像素。具体来说先计算缩放比例把长边缩放到640短边按比例缩放。推理输出后再把ab通道上采样回原图尺寸。这样做的好处是推理时间稳定在1到2秒内存占用小成色质量也没有肉眼可见的差异。DDColor的特征提取网络在256到512分辨率范围内效果差别不大大幅超过这个范围反而会因为细节信息过载而产生颜色噪点。内存控制上还要注意Mat对象的释放。OpenCvSharp的Mat实现了IDisposable处理大图时应尽量用using语句包裹并及时调用Dispose。我在批量处理100张图时如果不及时释放Mat内存占用会一路涨到接近1GBGC都救不回来。后来把所有中间Mat都改成using方式内存峰值稳定在200MB左右。4.3 常见异常与排查速查表这个环节必须单独列一个表因为我在这个项目里踩的坑比写代码的时间还多。最典型的是首次运行就报System.DllNotFoundException: Unable to load DLL onnxruntime原因是OnnxRuntime的native DLL没有被正确复制到输出目录或者缺少Visual C运行库。解决办法是在项目里检查是否引用了运行时包Microsoft.ML.OnnxRuntime.Managed同时确保输出目录有onnxruntime.dll并且安装VC Redistributable 2019以上版本。另外很常见的是System.BadImageFormatException这个通常是平台目标不匹配比如OnnxRuntime是x64的原生库但项目编译成了x86解决办法是右键项目属性把平台目标改成x64。还有一个容易被忽略的问题是推理时报告输入名称错误比如RuntimeError: Input input is missing。如果你的ONNX文件是用其他工具导出的输入节点名可能不是默认的input需要先用Netron确认实际名称再修改C#代码里对应字符串。最后是效果层面的大坑——推理完成后图像整体偏紫或偏绿。这个问题十有八九是ab通道的映射范围搞错了。模型输出接近-1到1要映射到0-255时必须先加1再乘128如果直接乘128或直接把负值强转byte颜色通道就会像被撕裂一样出现诡异的色调。我第一次部署时因为赶进度没有仔细看输出分布就在这一步翻了车成品图简直是灾难现场。后来用Netron和Python端输出对比才发现是范围映射的问题。下面是我把这次排障过程中遇到的主要问题整理成的一张速查表基本覆盖了C# OnnxRuntime部署DDColor时最常见的异常类型异常现象可能原因解决方法DllNotFoundException: onnxruntime缺少VC运行库或native DLL未复制安装VC 2019/2022运行库检查输出目录BadImageFormatException平台目标与native DLL不匹配项目平台目标设为x64推理失败提示输入名错误ONNX输入节点名不是默认值Netron查看实际名称同步修改代码图像输出偏紫/偏绿ab通道范围映射错误输出值先1再乘128再做Clamp输出图像有明显色块输入L通道值未归一化或范围不对检查L通道是否在0-255且除以255.0f批量处理时内存持续上涨Mat对象未及时释放用using包裹或finally中DisposeGPU推理报CUDA相关错误CUDA版本和OnnxRuntime不匹配换对应版本的Gpu包或改用DirectML5. 完整Demo与实测效果5.1 WinForms展示Demo为了让整个流程直观可验证我写了一个简单的WinForms窗体界面上就三个控件一个Open按钮选择图片、一个PictureBox显示原图和一个PictureBox显示着色结果。后台在Task.Run里执行推理避免阻塞UI线程。核心逻辑都封装在ImageColorizer类里Demo页面只负责调用。这样项目结构清晰后面如果集成到真正的上位机里只需要把ImageColorizer类整体搬过去就行。界面切换图片时我会先做一个低分辨率的预览图用输入尺寸推理并快速显示让用户看到大概效果然后异步放大到高分辨率进行一次精细推理完成后替换成高清图。这个交互设计在实际使用中很受欢迎因为用户在点击后不到1秒就能看到反馈不会干等。5.2 实测效果与耗时用一张上世纪的黑白人像照做测试模型输入256x256OpenCvSharp的Lab通道转换配合OnnxRuntime的CPU推理总耗时约3.2秒。着色结果里人物的肤色还原得比较自然背景里树叶的绿色没有出现大面积溢出说明DDColor的语义理解能力确实比传统方法强。我又拿了一张本身就是黑白的老建筑照片做测试砖墙颜色偏暖、天空偏蓝整体质感非常接近真实老照片修复的效果。在同样的CPU环境下把输入尺寸提高到512时耗时约7秒效果细节明显更丰富。如果打开DirectML执行提供程序在GTX 1060上512尺寸的推理可以压到1秒以内。这个性能表现对老照片修复、归档场景来说完全够用。5.3 扩展思路这个部署方案做完之后可以做的扩展方向还有好几个。一是接入摄像头实时流由于DDColor对单帧耗时要求较高可以先在低分辨率上做快速着色再结合流处理框架做实时预览。二是结合超分辨率模型先对老照片做超分再做着色两个ONNX模型串联在同一个C#进程里OnnxRuntime对这种多模型串联场景支持得很好。三是封装成REST API用ASP.NET Core承载让前端或其他系统通过HTTP调用着色服务这样就把着色能力完全平台化了。最后再说一个项目里的小细节。如果你在使用过程中发现每次初始化InferenceSession都要等很久可以在程序启动时提前加载模型并用一个静态字段保存session实例。这样后续每次调用都省去模型加载时间。另外如果你的目标是Windows系统可以考虑用ReadyToRun方式发布减少启动时JIT编译带来的额外延迟。6. 项目总结与经验心得这次用C# OnnxRuntime部署DDColor从拿到压缩包到跑通全流程总共花了两天时间其中大部分时间都耗在了颜色空间转换和ab通道范围映射这些细节上。走完这一遍之后我的直接感受是C#深度集成AI模型的门槛已经被OnnxRuntime降得足够低了真正卡人的不再是推理引擎本身而是你对模型输入输出语义的理解是否到位。有个小经验可以分享给后面做这个项目的读者在动手写C#代码之前先用Python把官方推理流程跑通然后把每一步的中间结果比如L通道值、模型输出shape、ab通道的数值范围打印出来后面在C#里逐一对应检查。这样能省去大量猜测时间因为颜色空间的坑如果不在最开始堵住到后期排查起来会非常痛苦。我也建议有条件的读者多试试DirectML的执行提供程序它让AMD和Intel核显也能参与推理加速在产线上部署比CUDA方案灵活得多。按我个人经验Deep Learning部署的重点其实是工程化稳定性和资源控制——在这一点上C#天生就有优势配合OnnxRuntime它是一门被低估的AI部署语言。本文还有配套的精品资源点击获取
返回列表