简介:CodeFormer图像修复的C#实现源码,面向需要在.NET环境中完成人脸老照片修复、人像卡通化等任务的开发者,也适合研究ONNX模型跨语言落地的C#程序员。整个压缩包共65个文件,大小约352.79MB,核心内容包括可编译的C#工程、预训练ONNX模型、运行时所需dll及可直接运行的exe程序,文件类型覆盖源码、配置、调试符号与中间缓存,拿到后即可在Visual Studio中打开还原运行。目前已有395人学习下载。源码基于WinForms构建,集成了OpenCvSharp与Microsoft.ML.OnnxRuntime,完整展示了从模型加载、图像预处理、推理到结果后处理的调用链路,并附带人像卡通化示例,便于对照学习CodeFormer的核心流程。无论是想将AI图像修复能力嵌入桌面应用,还是用于相关课题设计,这套资源都能提供可直接参考的C#实现范式。
1. 从 ONNX 到 WinForms:CodeFormer 图像修复的 C# 落地路线
拿到这份 C# CodeFormer 图像修复源码,意味着你不需要 Python 环境、不需要 PyTorch,只要一个 codeformer.onnx 和 OpenCvSharp 就能在 Windows 桌面程序里完成人脸修复。我最初以为只是调一下 OnnxRuntime 的 API,结果发现预处理、色域转换、模型输出的后处理才是真正耗时的部分。这份源码适合两类人:一是要在 .NET 工具链里集成图像增强能力的桌面/上位机开发者,二是想把 ONNX 模型移植到 C# 而找不到参考的算法工程师。下面的内容会顺着模型推理的原理、工程实现、参数调优和批量落地逐一展开。
2. CodeFormer 原理与 ONNX 模型选型:为什么 C# 里不能直接跑 PyTorch
2.1 CodeFormer 的退化修复机制
CodeFormer 全称是 CodeFormer: Robust Face Restoration via Codebook Lookup Transformer。它把人脸修复当成一个 codebook 检索问题:先用自编码器把输入人脸映射到 latent code,再通过 Transformer 在离散码本里查找最接近的干净人脸先验,最后融合还原。与 GFPGAN 这类单纯用 GAN 直接生成不同,CodeFormer 的权重(weight)可以在 0.1~1.0 之间调节,控制先验强度和保真度。权重越小,越忠实于原图;权重越大,修复痕迹越重,但平滑效果越明显。这个参数在 C# 端是通过 ONNX 的第二根输入 tensor 传入的,而不是像某些 Python 脚本那样靠改模型配置文件,所以在 WinForms 界面里做一个 TrackBar 就能实时调整修复强度。
CodeFormer 是专门针对人脸设计的,摄像头拍摄的模糊、低分辨率、压缩伪影、老照片划痕都在它的处理范围内。对于非人脸物体,比如文字、风景,修复效果会大打折扣,甚至会引入不存在的细节。这份源码对应的模型固定输入是 512x512,意味着不管原图多大,都会先缩放再推理。因此使用前最好先用人脸检测器把人脸区域裁剪出来,再把裁剪块交给模型,否则整张图里的小脸会被缩放到 512 后丢失大量细节。
2.2 为什么 ONNX 是 C# 部署的合理选择
原版 CodeFormer 是 PyTorch 模型,网络结构里有 ViT 和 VQGAN,直接跑需要 Python 环境和 torch,对 WinForms 工具型软件是灾难。导出的 codeformer.onnx 把整个 forward 流程固定下来,输入为 1x3x512x512 的 RGB 张量,输出同样为 1x3x512x512 的 RGB 张量。C# 侧只需借助 Microsoft.ML.OnnxRuntime 这个 NuGet 包,配合 OpenCvSharp 做图像解码和缩放,就能实现完整的图像修复流程。
常见做法是用 OnnxRuntime 的 CPU 版跑,一张 512 的人脸大约 2~4 秒,具体取决于机器主频和是否多线程。如果要上 GPU,得换 GPU 版 OnnxRuntime,并在 SessionOptions 里注册 CUDA EP。但源码默认没有接 GPU 逻辑,我建议先跑通 CPU 版,再考虑性能。对比之下,PyTorch 原版虽然测试方便,但要给客户部署一台 Windows 电脑就得装 Python、torch、一堆 CUDA 依赖,显然不适合。
| 对比项 | PyTorch 原版 | ONNX Runtime |
|---|---|---|
| 运行环境 | Python + torch + CUDA | .NET 桌面程序,免 Python |
| 模型格式 | .pt/.pth | .onnx |
| 预处理 | torchvision.transforms | OpenCvSharp 手动实现 |
| 部署粒度 | 适合服务/脚本 | 适合 WinForms/WPF 工具 |
| 内存占用 | 常驻 2GB+ | 常见峰值 500MB~1GB |
2.3 模型导出的输入输出约束
CodeFormer 导出时通常有两个输入:第一个是 input,第二个是 weight。在 codeformer.onnx 中,weight 是标量张量 shape 为 [1],类型为 float。很多首次接触的人只传了 image,结果报错缺少第二个输入。如果用 onnxruntime 提供的模型查看工具,会看到输入信息形如input: [1,3,512,512] float32, w: [1] float32。所以 OnnxHelper 里必须用 NamedOnnxValue 同时塞两个值,缺一不可。
输出张量的 shape 是[1,3,512,512],值域 0~1。这个细节非常关键:如果你直接把 float 数组当成 0~255 的像素值去显示,图像会几乎是黑的。正确做法是先 clamp 到 0~1,再乘 255 转成 byte。另外,输入归一化必须使用 CodeFormer 训练时的统计量,即 mean = (0.485, 0.456, 0.406),std = (0.229, 0.224, 0.225),不能随意改成 OpenCV 常用的 0~1 缩放。否则模型推理出来的色调会偏移,肤色发灰。
3. OnnxHelper 与 Form1 的推理流水线:从 Bitmap 到修复结果的关键实现
3.1 源码的文件结构
从项目给出的文件列表里可以看到,核心不是 Form1 本身,而是OnnxHelper.cs。Form1.cs只负责打开图片、调用 helper、把结果贴到 PictureBox。codeformer.onnx被放在模型目录,onnxruntime.dll和OpenCvSharpExtern.dll分别负责推理引擎和 OpenCV 原生层。.vs、obj、x64、x86这些是编译产物目录,真正的业务代码集中在几个 .cs 文件里。
我一般会先看 OnnxHelper.cs,因为它是整个项目的技术关键,决定模型能不能跑起来。下面的代码是对源码逻辑简化后的核心结构,去掉了控件耦合,方便你抽取到自己的类库中。
3.2 初始化与 Session 创建
using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; using OpenCvSharp; using System; using System.Collections.Generic; using System.Drawing; using System.IO; using System.Linq; public class OnnxHelper : IDisposable { private InferenceSession _session; private readonly int _size = 512; private readonly float[] _mean = { 0.485f, 0.456f, 0.406f }; private readonly float[] _std = { 0.229f, 0.224f, 0.225f }; public OnnxHelper(string modelPath) { var opts = new SessionOptions(); opts.LogSeverityLevel = OrtLoggingLevel.ORT_LOGGING_LEVEL_WARNING; _session = new InferenceSession(modelPath, opts); } public void Dispose() => _session.Dispose(); }这里通过InferenceSession加载 ONNX 模型。SessionOptions可以指定执行提供程序,默认 CPU 段。LogSeverityLevel 建议设成 Warning,否则每次推理都会刷一堆 C++ 日志,干扰调试。Dispose方法用于释放自然运行时资源,在 WinForms 关闭窗口时调用,防止句柄泄漏。
3.3 图像预处理与推理调用
public unsafe Bitmap Restore(Bitmap src, float weight = 0.8f) { using var srcMat = OpenCvSharp.Extensions.BitmapConverter.ToMat(src); using var rgbMat = new Mat(); // 关键:OpenCvSharp 解码后是 BGR,模型需要 RGB Cv2.CvtColor(srcMat, rgbMat, ColorConversionCodes.BGR2RGB); using var resized = new Mat(); Cv2.Resize(rgbMat, resized, new Size(_size, _size)); // 手动 CHW 布局,避免依赖 torchvision var input = new DenseTensor<float>(new[] { 1, 3, _size, _size }); var ptr = resized.DataPointer; int channels = 3; for (int c = 0; c < channels; c++) { for (int h = 0; h < _size; h++) { for (int w = 0; w < _size; w++) { float val = ptr[(h * _size * channels) + (w * channels) + c] / 255f; input[0, c, h, w] = (val - _mean[c]) / _std[c]; } } } var weightTensor = new DenseTensor<float>(new[] { 1 }, new[] { weight }); var feeds = new List<NamedOnnxValue> { NamedOnnxValue.CreateFromTensor("input", input), NamedOnnxValue.CreateFromTensor("w", weightTensor) }; using var results = _session.Run(feeds); var output = results.First().AsTensor<float>(); return ConvertTensorToBitmap(output); }这段代码有三个容易出错的点。第一是通道顺序:OpenCvSharp 默认是 BGR,必须先用CvtColor转成 RGB,否则模型把红蓝通道对调,修复后肤色会发青。第二是归一化参数,用的是 ImageNet 的 mean/std,和原版 PyTorch 一致。第三是 weight 张量必须传,且形状是[1],否则 onnxruntime 会报输入个数不匹配。NamedOnnxValue.CreateFromTensor("input", ...)里的键名要和导出 ONNX 时的输入名一致,不一定是input,可以通过工具确认。
3.4 从输出张量拼回 Bitmap
private Bitmap ConvertTensorToBitmap(Tensor<float> output) { var data = output.ToArray(); var outMat = new Mat(_size, _size, MatType.CV_8UC3); for (int c = 0; c < 3; c++) { for (int h = 0; h < _size; h++) { for (int w = 0; w < _size; w++) { float val = data[c * _size * _size + h * _size + w]; val = Math.Max(0f, Math.Min(1f, val)); outMat.At<Vec3b>(h, w)[c] = (byte)(val * 255); } } } // 转回 BGR 再显示,否则 Bitmap 颜色错乱 using var outRgb = new Mat(); Cv2.CvtColor(outMat, outRgb, ColorConversionCodes.RGB2BGR); return OpenCvSharp.Extensions.BitmapConverter.ToBitmap(outRgb); }这里把线性的输出数组按 CHW 顺序还原成 Mat,然后转回 BGR 交给 Bitmap。Math.Max和Math.Min防止少量越界值导致图像出现白点或黑点。性能上,这种坐标循环在 512x512 下大约几毫秒,是可以接受的;如果后续要做大图批量修复,建议改用Buffer.BlockCopy或指针拷贝,避免逐像素访问At<T>的开销。
4. 参数调优与常见坑:face_enhance、upscale 权重和 OpenCvSharp 的色域陷阱
4.1 weight 参数与修复强度的关系
codeformer.onnx 内部已经封装了上采样逻辑,不需要在外部额外缩放 weight。weight这个参数很关键,下表是我在自己电脑上的测试结果:
| weight 取值 | 效果特征 | 适用场景 |
|---|---|---|
| 0.1~0.3 | 保真度高,只去除细小噪点 | 低噪图片、证件照 |
| 0.5~0.7 | 修复与保真平衡 | 大部分人像 |
| 0.8~1.0 | 强修复,可能改变五官特征 | 严重模糊、老照片 |
使用方式是在 Form1 中通过 TrackBar 或 NumericUpDown 绑定Restore方法的 weight 参数。推荐默认 0.8,因为在 512 输入下,0.5 以下对明显划痕几乎没效果。如果你想做“先修复再人工微调”的工作流,可以把 weight 暴露成用户可调参数,这样无需重新训模型就能控制结果。
4.2 OpenCvSharp 的 BGR/RGB 陷阱
先看错误示例:
// 错误:没有做通道转换,直接缩放 Cv2.Resize(srcMat, resized, new Size(_size, _size));这样喂给模型,人脸会变成蓝紫色。原因在于 CodeFormer 训练时用 RGB 顺序入模,而 OpenCvSharp 解码 JPEG 后是 BGR。正确做法是Cv2.CvtColor转换一次。输出侧同样要转回 BGR,否则当你用BitmapConverter.ToBitmap显示时,颜色会错乱。我见过有人在这上面耗了一下午,最后发现只是少了两次 CvtColor。
4.3 内存与线程调度
WinForms 里如果直接在 UI 线程跑_session.Run,推理期间窗口会卡死。常见做法是挂到Task.Run里,同时用SemaphoreSlim控制并发,因为 ONNX Runtime 的 CPU EP 在单会话并发时会有锁竞争,反而变慢。我一般会在Restore方法外部加锁,并让 Form1 的按钮事件变成 async:
private async void btnRestore_Click(object sender, EventArgs e) { btnRestore.Enabled = false; try { var input = (Bitmap)picInput.Image.Clone(); var result = await Task.Run(() => _helper.Restore(input, trackBarWeight.Value / 100f)); picOutput.Image = result; } finally { btnRestore.Enabled = true; } }这里把推理丢到线程池,避免阻塞 UI。注意Bitmap的跨线程访问问题,Clone后不要再碰原图。如果你需要连续处理多张图,建议用队列加后台线程处理,否则界面会频繁刷新导致卡顿。
4.4 模型文件缺失的排查
如果运行时报DllNotFoundException: onnxruntime.dll,优先检查输出的可执行目录下有没有原生依赖。项目里x64和x86文件夹是给不同 CPU 架构用的,需把对应目录加入 DLL 搜索路径。常见做法是在Program.cs里根据Environment.Is64BitProcess设置 PATH:
[STAThread] static void Main() { string nativePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Environment.Is64BitProcess ? "x64" : "x86"); Environment.SetEnvironmentVariable("PATH", nativePath + ";" + Environment.GetEnvironmentVariable("PATH")); Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); }这样就能同时兼容 32 位和 64 位系统。如果还报错,用 Dependencies 工具查onnxruntime.dll是否依赖 VC++ 运行库。另外,OpenCvSharpExtern.dll必须和OpenCvSharp.dll放在同一目录,且位数一致。
5. 进阶:批量修复与内存复用,让 CodeFormer 在 C# 里跑得更稳
5.1 文件夹批量处理循环
实际使用中,往往不是单张修复,而是整个文件夹的人脸压缩。批量处理时要注意创建InferenceSession的开销很大,不能每张图重新加载模型。可以把OnnxHelper做成单例,循环调用。下面是一个简单的批量示例:
string[] files = Directory.GetFiles(@"D:\input", "*.jpg"); using var helper = new OnnxHelper(@"D:\model\codeformer.onnx"); Directory.CreateDirectory(@"D:\output"); foreach (string file in files) { using var bmp = new Bitmap(file); using var result = helper.Restore(bmp, 0.8f); result.Save(Path.Combine(@"D:\output", Path.GetFileName(file))); }这段代码在 100 张图上实测会占用大量内存,原因在于每次Restore都会分配多个 DenseTensor 和 Mat。一个直接优化是复用缓冲区:把_input、_weightTensor、_resized、_outMat作为类成员并在循环外创建,只更新数值。注意,DenseTensor的底层内存是连续的,只要形状不变,就可以重复写值,避免频繁触发 GC,任务管理器里的内存占用能降低 40% 左右。
5.2 验证修复效果的客观指标
不要只靠眼睛看。我会用Cv2.Laplacian算子计算输出图像的方差来评估边缘锐化程度,但 CodeFormer 这类模型重在视觉自然度,PSNR 反而可能低于输入。更靠谱的验证是肉眼对比五官轮廓、牙齿、眼睛细节,并检查是否有伪影。如果你需要定量,可以用 OpenCvSharp 的Cv2.MatchTemplate检测修复前后人脸关键点漂移,误差超过 10 像素说明权重过大。具体操作是先把输入和输出都缩放到同一边长,再分别提取人脸关键点,这里推荐使用 Dlib 或 MediaPipe 的 C# 绑定。
5.3 与 C# 上位机图像链路的搭配
在 C# 上位机或桌面工具中,CodeFormer 很少单独出现。常见链路是:先用 OpenCvSharp 的 CascadeClassifier 框出人脸,裁剪后送入 CodeFormer,再贴回原图。这里有个提醒:如果裁剪区域小于 512 会拉长变形,建议缩放时保持长宽比,并填充到 512 后再做归一化。源码中直接Resize默认拉伸,人脸比例会失真,修复后看起来会“胖一圈”。另外,如果原始人脸很清晰,不建议调用模型,可以先计算拉普拉斯方差,低于阈值再去修复,这样能节省大量 CPU 资源。
本文还有配套的精品资源,点击获取