资讯详情 C# 部署 APISR 动漫超分辨率:OnnxRuntime 实战指南
📅 2026/10/30 16:56:12
简介面向希望在.NET环境中部署深度学习模型的C#开发者一份完整的APISR动漫超分辨率推理方案。利用OnnxRuntime加载ONNX格式的APISR模型可对动漫图片进行高质量上采样解决C#工程中集成图像超分模型的落地难题。压缩包共275个文件主要包含C#源码与解决方案、ONNX模型文件、DLL依赖库、XML配置、TXT说明文档等另有样例图片与演示项目整体约374MB。已有57人学习下载。包内提供可直接运行的演示工程和依赖包涵盖从模型初始化、输入图像预处理到推理输出与结果保存的完整调用链路packages目录已将所需第三方依赖统一整理配合说明文件可快速理解各文件职责。目录结构区分源码、模型、依赖与文档便于按需取用适合有基础C#经验并希望快速上手OnnxRuntime部署的开发者参考也可作为在.NET中集成其他ONNX模型的通用范本。1. C# 跑 APISR 动漫超分辨率这可能是最适合落地的 OnnxRuntime 路线C# 工程师想把超分辨率模型接进自己的 WinForms / WPF / 上位机项目最尴尬的是模型选型。Real-ESRGAN 太重SwinIR 又大多只有 PyTorch 版本转 ONNX 之后算子兼容性也要折腾一轮。APISR 是 2024 年开源的一套动漫图像超分网络专攻脏线条、高噪点、色块边缘粗糙的卡通素材放大 2 倍和 4 倍的效果非常干净。配合 OnnxRuntime 部署C# 端只需要引入两个 NuGet 包就能把整套推理跑起来。这份资源包里正好是完整的实战工程模型导出脚本、ONNX 权重、C# 推理类和前后处理工具改改路径就能用。适合做动漫素材库增强、老片修复预处理、游戏立绘放大的开发者如果你只是想本地把一张模糊图变清晰它也能直接当工具用。2. APISR 原理与模型导出先搞清输入输出再谈部署2.1 APISR 的网络结构与推理行为APISR 的全称是 Anime Production Inspired Real-World Anime Super-Resolution从名字就能看出它不是通用超分模型。通用超分模型在摄影图上表现很好但对动漫原画里的硬边缘、大面积平色区域往往会出现锯齿或伪影。APISR 的训练数据来自真实动漫制作流程包括缩放、压缩、噪声叠加后的劣化图像所以它对线条的处理更接近“恢复原画”而不是单纯放大。推理行为上APISR 模型内部带有 GAN 结构输出带有生成器的锐化倾向。和 Real-ESRGAN 相比它在 2 倍超分时就能得到比较可用的结果4 倍超分时边缘振铃也更少。模型输入是 RGB 顺序的普通图像张量数值范围 0 到 1不需要 ImageNet 标准化。输出同样是 RGB 张量高和宽分别放大 scale 倍。部署时最容易搞错的就是这一点后面避坑章节会专门讲。资源包里常见两种配置2 倍模型和 4 倍模型。导出时直接决定你要哪种放大倍数因为模型内部已经把 scale 编译成固定算子运行时不能切换。换倍数必须重新导出 ONNX。模型配置输入张量输出张量典型用途APISR_x2[1, 3, H, W][1, 3, 2H, 2W]动画帧增强、实时预览APISR_x4[1, 3, H, W][1, 3, 4H, 4W]老番修复、立绘放大H 和 W 在导出时最好设置为动态维度这样 C# 端可以输入任意分辨率不用先缩放到固定尺寸。2.2 把 PyTorch 权重导出为 ONNXopset 与动态轴设置资源包里的模型导出脚本一般是 Python 写的。APISR 官方权重是 PyTorch 格式导出前先确认模型已经变成 eval 模式。常见做法是写一段脚本加载权重后用一个全 0 或全 1 的假张量走一遍前向确认网络能完整跑通再执行 torch.onnx.export。这里给一份最简导出脚本作为参考import torch from apisr import APISR model APISR.from_pretrained(APISR_GAN_x4) model.eval() dummy torch.randn(1, 3, 64, 64) torch.onnx.export( model, dummy, apisr_x4.onnx, opset_version17, input_names[input], output_names[output], dynamic_axes{ input: {0: batch, 2: height, 3: width}, output: {0: batch, 2: height, 3: width} }, do_constant_foldingTrue ) print(export done)导出时最值得注意的两个参数是opset_version和dynamic_axes。OnnxRuntime 对高版本算子支持很积极opset 17 在 1.17 系列的 OnnxRuntime 上已经跑得很成熟。dynamic_axes把高和宽声明为动态C# 端传给模型的张量就可以是任意尺寸如果这里不设置导出的模型会把 64x64 当成唯一合法尺寸C# 端输入一个 320x180 的图直接异常退出了。do_constant_folding对 GAN 这类大模型建议打开能把一部分固定计算提前折叠掉减少运行时开销。导出过程中如果报错多半是模型内部某个 reshape 或插值算子对动态轴支持不好。我的处理办法是先改成固定尺寸导出验证整条链路再改回动态轴重导。动态轴实在不行就在 C# 端增加统一缩放逻辑先缩到模型支持的尺寸附近再推理。2.3 用 Python 先跑一遍 ONNX确认数值没跑偏模型导出之后不要直接去写 C# 代码先在 Python 端用 onnxruntime 跑一遍和 PyTorch 输出对一下数值。这一步像是黑匣子验证能提前排除模型导出阶段引入的算子偏差。代码大概这样import onnxruntime as ort import numpy as np import torch model APISR.from_pretrained(APISR_GAN_x4) model.eval() x torch.randn(1, 3, 64, 64).numpy() pt_out model(torch.from_numpy(x)).detach().numpy() so ort.SessionOptions() so.graph_optimization_level ort.GraphOptimizationLevel.ORT_ENABLE_ALL sess ort.InferenceSession(apisr_x4.onnx, so, providers[CPUExecutionProvider]) onnx_out sess.run([output], {input: x})[0] diff np.max(np.abs(onnx_out - pt_out)) print(max diff:, diff)np.max(np.abs(onnx_out - pt_out))这个数值如果小于 1e-4说明导出的模型数值行为基本一致如果到 1e-2 这种量级说明某个算子的实现有差异。常见的原因是 APISR 里的上采样算子或归一化层在 ONNX 里被重排了在 C# 端表现就是输出图像偏亮或偏暗容易误判成后处理问题。另外需要注意 PyTorch 模型在model(torch.from_numpy(x))之前必须已经处于 eval 模式否则 dropout 或 batchnorm 的行为差异会直接污染对比结果。2.4 量化与半精度这一步不用着急做导出成 FP32 ONNX 之后文件体积比较大在 CPU 上推理也比较慢自然会想到转 FP16 或 INT8。FP16 导出能让体积减半GPU 推理时显存占用也更友好但 CPU 端跑 FP16 反而更慢因为大部分 CPU 没有高效 FP16 计算单元OnnxRuntime 内部要转换成 FP32 再算。INT8 量化对 APISR 这类 GAN 结构尤其敏感生成器的输出是图像量化误差会被放大成可见伪影连线和色块边缘很容易碎。所以我一般建议第一版部署先用 FP32 跑通功能确认图像质量没问题再去尝试精度优化。资源包里如果有 FP16 或量化后的版本也建议先用原始 FP32 版本做基准。3. C# 端推理实现OnnxRuntime 加载 APISR 模型的全流程3.1 工程配置与 NuGet 依赖清单部署 APISR 不需要在 C# 端安装任何深度学习框架OnnxRuntime 的 NuGet 包已经集成了推理引擎。图像解码和缩放我习惯用 OpenCvSharp因为后续超分完还要做边缘锐化或直方图调整用 Mat 一条链路下来最舒服。新建一个 .NET 6 或 .NET 8 的 WinForms 工程后引入下面这几个包就可以NuGet 包名版本建议作用Microsoft.ML.OnnxRuntime1.17.x模型加载与 CPU 推理Microsoft.ML.OnnxRuntime.GPU1.17.x可选在 CUDA 环境启用 GPU 推理OpenCvSharp4.Windows4.8.x图像读取、缩放、通道转换OpenCvSharp4.Extensions4.8.xMat 与 Bitmap 互转先装 CPU 包把流程跑通再考虑 GPU 包。GPU 包和 CUDA 版本绑定得很死装错了会在加载模型时直接报找不到 cudart64 相关 DLL具体问题在避坑章节展开。3.2 核心推理代码加载、预处理、Run、后处理这是资源包最核心的部分。C# 端完整流程分为四步加载模型、预处理图像、执行推理、把输出张量转回图像。下面这段代码可以直接套用using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; using OpenCvSharp; public class ApisrInference : IDisposable { private readonly InferenceSession _session; private readonly int _scale; public ApisrInference(string modelPath, int scale) { // 复用 Session避免每次推理都重新加载模型 var options new SessionOptions(); options.GraphOptimizationLevel GraphOptimizationLevel.ORT_ENABLE_ALL; _session new InferenceSession(modelPath, options); _scale scale; } public Mat Upscale(Mat srcBgr) { using Mat rgb new Mat(); Cv2.CvtColor(srcBgr, rgb, ColorConversionCodes.BGR2RGB); int h rgb.Height; int w rgb.Width; var input new DenseTensorfloat(new[] { 1, 3, h, w }); // 把 HWC 的 Mat 转成 CHW 的 float 张量同时归一化到 [0,1] var inputArray input.ToArray(); for (int y 0; y h; y) { for (int x 0; x w; x) { var p rgb.AtVec3b(y, x); inputArray[(0 * 3 0) * h * w y * w x] p[0] / 255f; inputArray[(0 * 3 1) * h * w y * w x] p[1] / 255f; inputArray[(0 * 3 2) * h * w y * w x] p[2] / 255f; } } var inputs new ListNamedOnnxValue { NamedOnnxValue.CreateFromTensor(input, input) }; using var results _session.Run(inputs); var output results.First().AsTensorfloat(); // 输出形状是 [1, 3, scale*H, scale*W] int outH h * _scale; int outW w * _scale; var outMat new Mat(outH, outW, MatType.CV_8UC3); for (int y 0; y outH; y) { for (int x 0; x outW; x) { var b (byte)Math.Clamp(output[(0, 0, y, x)] * 255f, 0, 255); var g (byte)Math.Clamp(output[(0, 1, y, x)] * 255f, 0, 255); var r (byte)Math.Clamp(output[(0, 2, y, x)] * 255f, 0, 255); outMat.Set(y, x, new Vec3b(b, g, r)); } } return outMat; } public void Dispose() _session.Dispose(); }这个实现逻辑不复杂但有两个细节值得说明。第一是_session必须复用InferenceSession的构造函数会加载模型权重、申请内存池每秒创建一次会让推理慢一个数量级。第二是using var results _session.Run(inputs);Run返回的对象里包装了输出 Tensor不释放的话连续推理几百张图后内存会持续上涨。预处理部分嵌套循环看起来笨但结构最直观实际性能敏感时可以用Mat.Split()分离通道再把三个通道的 float 数组按序拼成 CHW这样能省掉大量At调用速度更快资源包里两种写法都保留了。3.3 参数怎么调CPU/GPU 切换、线程与内存OnnxRuntime 的 CPU 推理默认会使用所有物理核心这在后台任务里会明显卡 UI 线程。控制并发最简单的方式是设置SessionOptions的线程数options.SetIntraOpNumThreads(Math.Max(2, Environment.ProcessorCount / 2)); options.SetSessionThreadPoolSize(2);对于 4 倍超分这种计算密集任务线程数设成物理核心数的一半是性价比最高的。全核跑时推理时间只减少 10% 左右但 CPU 占用率冲到 100%WinForms 界面拖动卡顿非常明显。如果目标机器有支持 CUDA 的 N 卡要改成 GPU 推理代码变化很小options.AppendExecutionProvider(CUDA); options.AppendExecutionProvider(CPU); // 备用兜底注意 GPU 包只有在 CUDA 与 cuDNN 版本匹配时才会生效有 GPU 却加载失败时不会报错而是静默用 CPU 跑。所以我在工程里都会加一段启动日志打印当前生效的 execution provider看到CUDAExecutionProvider出现在输出里才确认真正用上了 GPU。3.4 文件清单与运行入口压缩包解压后目录结构大概是这样的路径内容models/apisr_x4.onnx导出的 ONNX 权重scripts/export_onnx.pyPyTorch 转 ONNX 脚本scripts/verify_onnx.pyPython 端数值验证脚本src/C# 工程源码src/Program.cs命令行入口读图、推理、写图src/ApisrInference.cs推理封装类先用命令行入口测试最省事Program.cs接收输入图片路径和输出图片路径加载模型后调用Upscale。我习惯先跑命令行模式验证整条链路再集成到 WinForms 或 WPF 界面里这样排查问题时至少能区分是界面问题还是推理问题。4. 部署避坑五个常见问题与排查顺序4.1 排查工具把“跑不起来”拆成三层部署 APISR 遇到问题先不要急着改代码。我一般按三层排查模型层、数据层、环境层。模型层用 Python 脚本确认 ONNX 能正常推理数据层检查输入输出的形状和数值范围环境层确认 NuGet 包版本和本机 DLL。最快的诊断方式是加载 session 后把模型信息打出来看看输入名、输出名和维度是不是和预期一致foreach (var kvp in _session.InputMetadata) { Console.WriteLine($Input: {kvp.Key} {kvp.Value.Dimensions} {kvp.Value.ElementDataType}); }这一步能直接暴露模型是否带动态轴。如果打印出来是[1, 3, 64, 64]说明导出脚本里的 dynamic_axes 没生效后面无论 C# 写得多对都会出问题。4.2 色彩整体偏绿偏蓝通道顺序问题现象超分后的图对比原图红色变成了青色人脸肤色发绿看起来像胶片负片效果。原因模型训练时用的是 RGB 通道顺序OpenCvSharp 读进来的是 BGR直接喂给模型等于把 R 和 B 通道互换。APISR 对通道敏感度极高一旦通道错乱图像整体偏色会非常明显。解决推理前调用Cv2.CvtColor(srcBgr, rgb, ColorConversionCodes.BGR2RGB)后处理再转回去。我见过不少人在后处理输出时只取了 R 通道却不做整体转换结果图像依然偏色本质上还是通道序没掰正。4.3 输出图像边缘出现黑边尺寸没对齐现象超分结果四周出现不等的黑色或模糊条纹有的图明显有的图不明显。原因APISR 内部有多个 stride 为 2 的下采样卷积输入尺寸必须能被放大倍数整除。比如 4 倍模型输入宽高如果不是 4 的倍数边缘处会计算到不完整像素推理引擎只能补零黑边就是这么来的。解决推理前先对输入做对齐常见做法是把宽高修正到能被 scale 整除int alignedW w - (w % scale); int alignedH h - (h % scale); Cv2.Resize(srcBgr, srcAligned, new Size(alignedW, alignedH));注意对齐放在通道转换之后、归一化之前。如果不想改变构图也可以先按原尺寸推理再把边缘多出来的几个像素裁掉但这样会丢失画面内容不如提前重采样。4.4 换台机器就报“找不到 DLL”OpenCV 运行库问题现象开发机上跑得好好的打包发给同事启动就报System.DllNotFoundException: opencv_core480.dll。原因OpenCvSharp4.Windows 依赖 VC 运行库和本机 OpenCV 原生 DLL如果目标机器没有安装对应运行环境程序直接崩。这不是 OnnxRuntime 的问题是 OpenCV 的经典坑。解决把opencv_core480.dll、opencv_imgproc480.dll等几个 DLL 拷贝到输出目录同时在项目属性里打开“复制本地依赖项”。更省心的做法是图像处理层改用 ImageSharp纯托管实现不依赖本机 DLL对部署环境更友好代价是图像操作性能弱一些。4.5 多线程下内存暴涨Result 未释放 / Session 反复创建现象做批量超分任务处理到几百张时进程占内存 2GB 以上甚至 OutOfMemory。原因两个隐患叠加。Run返回的IDisposableReadOnlyCollectionNamedOnnxValue没有 using输出 Tensor 每次申请的内存等 GC 回收另一处是每次调用都 new InferenceSession底层内存池全部重建。解决Session 改成单例强约束自己只在程序启动时创建一次所有Run的返回值一律 using 包裹。这两个改掉后内存曲线会变得非常平稳。另外还有一个容易被忽略的细节如果推理模型时反复传递同一个DenseTensor对象OnnxRuntime 不会自动把上次计算结果清零输入张量里的旧数据可能污染本次推理结果。稳妥的做法是每次推理前重新创建DenseTensor。5. 性能优化与验证让 APISR 在低配机器上跑得动5.1 滑窗分块把大图拆成小图再拼回来4 倍超分一张 1920x1080 的图在纯 CPU 机器上可能要跑十几秒而且整张图直接推理会占用大量内存。常见的做法是滑窗分块把大图切成若干小块分别推理再拼回完整大图。切块尺寸要先对齐放大倍数int scale 4; int tileSize 320; tileSize - tileSize % scale;分块后每块独立推理拼接时相邻块之间留 8 到 16 像素重叠重叠区域做简单的加权融合能避免接缝处的亮度跳变。这个思路对 2000 像素以上的长图尤其有效内存占用从 2GB 级别降到 200MB 级别。5.2 验证方法超分效果到底对不对验证部署是否成功不要只看能不能跑出图。我的固定流程是准备一张带细线条的动漫截图同时跑三次原图双三次放大、APISR 输出、APISR 输出后降采样回原尺寸。如果降采样回原尺寸的图和原图差异很大说明颜色映射或尺寸计算有问题如果边缘出现黑白锯齿说明预处理或模型版本有问题。最后再放大到 200% 目测线条是否顺滑、噪点是否被锐化成颗粒。资源包里的 APISR 推理工程本身不复杂大头都在前处理对齐和 Session 复用上面。我最早部署这套模型时图省事没做尺寸对齐结果在客户现场出的图边缘一直带黑条排查了大半天才发现是宽度不能被 4 整除。从那以后每次换模型我都会强制在笔记本上走一遍三张图的验证流程原图、超分图、降采样回原尺寸图先在本地确认没问题再交付。这个习惯帮我挡掉了后面至少三次线上翻车希望也能帮到你。本文还有配套的精品资源点击获取