尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

C# OnnxRuntime 部署 DDColor 黑白照片上色实战

C# OnnxRuntime 部署 DDColor 黑白照片上色实战 简介这份资源面向具备一定C#与深度学习基础的开发者提供在.NET环境下用OnnxRuntime推理引擎部署DDColor老照片上色模型的完整工程示例可用于桌面端图像修复与上色应用的二次开发。压缩包共300个文件、约847MB涵盖60个dll动态库、36个xml配置、10个cs源码、9个nupkg包及onnx模型、so/dylib跨平台库、jpg/jpeg示例图等覆盖依赖库、模型文件、演示代码与项目配置其中sln解决方案与Onnx Demo演示目录便于快速运行验证。已有51人学习下载。读者可据此掌握模型转ONNX、加载会话、设置输入、执行推理与处理输出的完整链路并参考色彩预测与性能优化思路将老照片上色能力落地到实际项目中。1. 从一张黑白老照片说起C# OnnxRuntime 部署 DDColor 到底在做什么手里有一批上世纪的黑白家庭照想批量上色又不想把照片传到别人的服务器上这个需求在 C# 上位机圈子里其实很常见。DDColor 是这两年图像上色方向里效果比较能打的一个模型它基于双解码器结构对人物肤色、天空、植被这些容易翻车的区域处理得比早期那些 GAN 方案稳。而 OnnxRuntime 是把这套权重从 Python 训练环境搬到 C# 桌面程序里的关键中间层——它不依赖 PyTorch 运行时一个动态库加一个模型文件就能推理。把这两者拼起来就是标题里说的「C# OnnxRuntime 部署 DDColor」。适合谁看写过 C# 上位机、用过 OpenCVSharp 或做过视觉检测现在想把 AI 上色塞进自己桌面工具里的工程师。整条链路的核心难点不在模型本身而在预处理对齐、张量维度、显存释放和色彩空间转换这四件事上下面一层层拆。2. DDColor 与 OnnxRuntime 的选型账为什么不用 Python 服务2.1 DDColor 相比 DeOldify、Style2Paints 的取舍做黑白上色绕不开三个候选DeOldify、Style2Paints 和 DDColor。DeOldify 基于 FastAI 那套效果偏「油画感」人物脸部经常出现不自然的红晕Style2Paints 更偏线稿上色对照片类输入不友好。DDColor 是 2023 年前后出来的方案双解码器分别负责语义理解和颜色预测在老照片这种低对比度、噪点多的输入上颜色溢出明显更少。从部署角度看DDColor 官方提供的是 PyTorch 权重要落到 C# 里必须走 ONNX 这条路。这里有个概念要分清ONNX 是一种模型交换格式只描述计算图OnnxRuntime 是微软维护的推理引擎负责把这个图跑起来。很多人把两者混为一谈实际部署时模型是.onnx文件引擎是Microsoft.ML.OnnxRuntime.dll加一堆原生依赖缺一不可。选 DDColor 而不是自己训一个理由很实际上色任务对色彩先验要求极高自己从零训需要几十万张彩色图做自监督成本不划算。DDColor 在 ImageNet 和 COCO 上预训练过迁移到老照片场景只需要少量微调甚至直接推理就能出可用结果。2.2 OnnxRuntime 在 C# 里的三种接入方式对比C# 调 OnnxRuntime 不是只有一条路实际项目里我见过三种做法各有适用边界接入方式依赖形态适用场景主要代价NuGet 包 Microsoft.ML.OnnxRuntime托管库 自动带原生 dll常规 x64 Windows 桌面包体积约 15MBGPU 版要单独换包手动加载 onnxruntime.dll纯原生动态库 P/Invoke需要控制加载路径、绿色部署要自己写 C API 封装工作量大通过 Python 子进程调用独立 exe 通信快速验证、模型频繁换进程通信开销大不适合实时绝大多数 C# 上位机场景选第一种。NuGet 上有 CPU 版和 GPU 版两个包GPU 版依赖 CUDA 和 cuDNN如果目标机器没有 N 卡或者驱动版本对不上直接退回 CPU 版更省心。DDColor 的模型不算大CPU 推理一张 512×512 的图大概 1 到 3 秒批量处理老照片完全够用没必要为了省那一秒去折腾 CUDA 环境。这里插一句血泪经验GPU 版 OnnxRuntime 对 CUDA 版本极其敏感12.x 和 11.x 的包不能混用装错了不会报错只会静默回退到 CPU你以为在用 GPU 其实一直在跑 CPU排查半天才发现。2.3 模型转换从 PyTorch 权重到可用的 ONNX 文件DDColor 官方仓库给的是.pth权重需要先导出成 ONNX。这一步通常在 Python 环境里做一次就行导出后 C# 端只认.onnx。导出时有两个参数必须盯死opset_version建议 17太低不支持某些算子太高部分 OnnxRuntime 版本不认input_names和output_names要记下来C# 里创建输入张量时名字必须完全一致大小写都不能错。导出命令大致长这样具体脚本名以你拿到的仓库为准python export_onnx.py \ --model ddcolor \ --checkpoint ddcolor_paper.pth \ --output ddcolor.onnx \ --opset 17 \ --input-size 512 512导出完别急着往 C# 里塞先用 Python 的 onnxruntime 跑一遍验证输出是否正常确认模型本身没问题再排查 C# 端。这个顺序能省掉大量「到底是模型错还是代码错」的扯皮。3. C# 端最小可跑通实现从加载模型到输出彩色图3.1 环境准备与 NuGet 包选择新建一个 .NET 6 或 .NET 8 的控制台或 WinForms 项目装两个包Microsoft.ML.OnnxRuntime和OpenCvSharp4加对应的OpenCvSharp4.runtime.win。OpenCvSharp 负责图像读写和色彩空间转换比 System.Drawing 在处理 BGR/RGB 顺序上更省事。dotnet add package Microsoft.ML.OnnxRuntime --version 1.17.0 dotnet add package OpenCvSharp4 --version 4.9.0 dotnet add package OpenCvSharp4.runtime.win --version 4.9.0版本号只是示例实际以你项目能还原到的稳定版为准。注意 OpenCvSharp 的 runtime 包必须和主包版本对齐否则运行时会报找不到OpenCvSharpExtern.dll。这个错误新手经常遇到现象是编译通过、一运行就崩原因就是原生库没被复制到输出目录。3.2 图像预处理把 Mat 变成模型要的张量DDColor 的输入是归一化后的 RGB 张量形状[1, 3, H, W]数值范围 0 到 1。C# 里 OpenCvSharp 读进来默认是 BGR必须转 RGB再按通道拆开填充到DenseTensorfloat。using OpenCvSharp; using Microsoft.ML.OnnxRuntime.Tensors; public static DenseTensorfloat Preprocess(string imagePath, int size 512) { using var src Cv2.ImRead(imagePath, ImreadModes.Color); using var resized new Mat(); Cv2.Resize(src, resized, new Size(size, size)); Cv2.CvtColor(resized, resized, ColorConversionCodes.BGR2RGB); var tensor new DenseTensorfloat(new[] { 1, 3, size, size }); for (int y 0; y size; y) { for (int x 0; x size; x) { Vec3b px resized.AtVec3b(y, x); tensor[0, 0, y, x] px.Item0 / 255f; // R tensor[0, 1, y, x] px.Item1 / 255f; // G tensor[0, 2, y, x] px.Item2 / 255f; // B } } return tensor; }逻辑说明先 resize 到模型固定输入尺寸DDColor 常见的是 512具体看你导出时的input-size。转 RGB 是因为 OpenCV 默认 BGR不转的话红蓝通道会互换出来的图脸色发青。归一化除以 255 是 DDColor 训练时的标准做法漏掉这步输出会全白或全黑。参数size必须和 ONNX 模型的输入维度严格一致不一致会在Run时抛维度异常。3.3 推理与后处理拿到输出张量还原成图片推理本身代码很短坑都在后处理。DDColor 输出通常是[1, 3, H, W]的浮点张量范围 0 到 1需要乘 255、转回 BGR、再 resize 回原图尺寸。using var session new InferenceSession(ddcolor.onnx); var inputTensor Preprocess(old.jpg); var inputs new ListNamedOnnxValue { NamedOnnxValue.CreateFromTensor(input, inputTensor) }; using var results session.Run(inputs); var output results.First().AsTensorfloat(); int h output.Dimensions[2]; int w output.Dimensions[3]; using var mat new Mat(h, w, MatType.CV_8UC3); for (int y 0; y h; y) { for (int x 0; x w; x) { byte r (byte)(Math.Clamp(output[0, 0, y, x], 0f, 1f) * 255); byte g (byte)(Math.Clamp(output[0, 1, y, x], 0f, 1f) * 255); byte b (byte)(Math.Clamp(output[0, 2, y, x], 0f, 1f) * 255); mat.Set(y, x, new Vec3b(b, g, r)); } } Cv2.ImWrite(color.jpg, mat);逻辑说明NamedOnnxValue.CreateFromTensor的第一个参数名必须和导出 ONNX 时的input_names一致写错会报「找不到输入节点」。Math.Clamp是后悔药模型偶尔会输出略大于 1 或小于 0 的值不夹紧直接转 byte 会溢出成乱码色块。输出通道顺序是 RGB写回 Mat 时要转成 BGR否则存出来的图颜色是反的。session.Run返回的results用完必须 Dispose否则连续处理几百张图后内存会涨到几个 G。3.4 批量处理时的会话复用与内存控制单张跑通只是第一步实际项目都是批量。这里有个关键点InferenceSession创建一次就够了不要每张图 new 一个。创建会话要加载模型、初始化算子开销在几百毫秒级别循环里反复创建会让整体速度慢十倍以上。using var session new InferenceSession(ddcolor.onnx); foreach (var file in Directory.GetFiles(input, *.jpg)) { using var inputTensor Preprocess(file); var inputs new ListNamedOnnxValue { NamedOnnxValue.CreateFromTensor(input, inputTensor) }; using var results session.Run(inputs); SaveResult(results, file.Replace(input, output)); // 每张处理完主动触发一次回收避免大图累积 GC.Collect(2, GCCollectionMode.Optimized); }参数说明GC.Collect那行不是必须但在处理 4K 以上大图时能明显压住内存峰值。GCCollectionMode.Optimized比默认模式温和不会造成明显卡顿。如果你的场景对延迟敏感可以改成每 10 张回收一次。4. 避坑与排查部署 DDColor 时最容易翻车的五件事4.1 现象推理结果全灰或全黑原因预处理归一化漏了或者输入张量数值范围是 0 到 255 而不是 0 到 1。DDColor 训练时用的是 0 到 1喂 0 到 255 进去模型内部激活直接饱和。解决在Preprocess里确认除以了 255f并且检查DenseTensor填充时没有把 byte 直接塞进 float 张量。可以在填充后打印几个值正常应该在 0 到 1 之间。4.2 现象报「Input name input is not a valid input name」原因ONNX 模型的实际输入节点名不是input导出时可能被命名成image、x或者带前缀的名字。解决用 Netron 打开.onnx文件看第一层节点的名字或者用 Python 跑session.get_inputs()[0].name打印出来把 C# 里的字符串改成一致。这个错误在换模型版本时特别常见同一个项目换个权重就崩多半是名字变了。4.3 现象处理几十张后程序内存爆掉原因InferenceSession.Run返回的IDisposableReadOnlyCollection没释放或者Mat对象没 using。OnnxRuntime 的原生内存不受 GC 直接管理托管对象回收了原生内存可能还挂着。解决所有results、Mat、DenseTensor都用 using 包起来。批量场景下每处理完一张显式调用一次GC.Collect。如果还压不住考虑把批量任务拆成多个进程每个进程处理固定数量后退出重启。4.4 现象GPU 版装了但速度没变化原因CUDA 版本和 OnnxRuntime GPU 包不匹配运行时静默回退 CPU。这个前面提过是典型的黑匣子问题不报错但也不加速。解决在创建SessionOptions时显式指定AppendExecutionProvider_CUDA如果 CUDA 环境有问题这行会抛异常而不是静默回退。另外用nvidia-smi确认推理时 GPU 占用有波动没波动就是没走上 GPU。4.5 现象输出图片尺寸和原图对不上原因预处理时 resize 到了 512后处理直接按 512 输出没有 resize 回原图尺寸。解决在Preprocess里记录原图的宽高后处理生成 Mat 后再Cv2.Resize回原始尺寸。注意 resize 用InterpolationFlags.Cubic比默认的线性插值边缘更干净。如果对细节要求高可以只对模型输出做上采样再和原图的亮度通道做融合这是进阶做法后面一章会提。5. 进阶技巧让上色结果更自然的两三个手段跑通最小链路之后真正决定成品质量的是后处理融合。DDColor 直接输出的图有时候颜色偏饱和尤其是天空和皮肤看起来「AI 味」很重。我一般会做一步亮度保持把原灰度图的 Y 通道提取出来和上色结果的 Y 通道做加权融合权重 0.7 比 0.3这样既保留颜色又不会丢掉原图的明暗层次。using var gray Cv2.ImRead(old.jpg, ImreadModes.Grayscale); using var color Cv2.ImRead(color.jpg); using var ycc new Mat(); Cv2.CvtColor(color, ycc, ColorConversionCodes.BGR2YCrCb); var channels Cv2.Split(ycc); channels[0] gray; // 用原图亮度替换 Cv2.Merge(channels, ycc); Cv2.CvtColor(ycc, color, ColorConversionCodes.YCrCb2BGR); Cv2.ImWrite(final.jpg, color);这段逻辑是把上色结果的亮度通道换成原灰度图的亮度色度和浓度保留模型预测的。效果是颜色还在但明暗关系完全忠于原图老照片那种年代感不会被抹掉。参数上唯一要注意的是Cv2.Split返回的 Mat 数组用完要逐个 Dispose否则又是内存泄漏。另一个技巧是分块推理。DDColor 输入固定 512如果原图是 2000 像素宽直接缩到 512 会丢细节。可以切成带重叠的 512 块分别推理再拼接重叠区域做线性羽化。代价是速度慢几倍适合对质量要求高的单张精修不适合批量。验证方法很简单准备三张典型图——一张人物特写、一张风景、一张室内弱光分别跑一遍看肤色、天空、暗部有没有明显色偏。如果这三类都稳基本可以投入使用了。我自己踩过的最大教训是别一上来就追求 GPU 加速和分块推理先把 CPU 单张跑稳把预处理和后处理的每个参数都对一遍再谈优化。很多所谓的「模型效果不行」最后查出来都是归一化或者通道顺序写错了。希望帮到你。本文还有配套的精品资源点击获取
返回列表